In ‘I’ is for Item Container, we learned that each item within an ItemsControl is represented by visuals hosted within a container element. The type of this “item container” is specific to the type of the ItemsControl. For example, the container for an item in a ListBox is the ListBoxItem element. Similarly, the container for an item in a ComboBox is the ComboBoxItem element. (A complete list of the native ItemsControl classes and their respective item containers can be found at the end of ‘I’ is for Item Container.)
In this episode, we examine the mechanism by which item containers come into existence.
Where did these containers come from?
In the previous examples in this series, the item containers have mysteriously (or perhaps magically) been created without our knowledge. All we typically do is set a binding on the ItemsSource property for the control. Consider the following ListBox example (again, from ‘I’ is for Item Container):
<ListBox ItemsSource="{Binding Source={StaticResource Characters}}"
ItemContainerStyle="{StaticResource CharacterContainerStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
We know that each item in the ListBox is contained within a ListBoxItem. What we don’t know is who created that ListBoxItem. It is logical to assume that the ItemsControl, itself, created the ListBoxItems, but as we’ll see moving forward, this is not the case.
Introduction to the ItemContainerGenerator class
It turns out that every ItemsControl has its own instance of an ItemContainerGenerator object. This generator is accessible via an appropriately named ItemContainerGenerator property.
As the class name implies, an ItemContainerGenerator provides methods by which item containers can be generated for items within an ItemsControl. Specifically, an ItemContainerGenerator knows how to create a container and link it to the item it will contain. It also knows how to remove a container that is no longer needed. Finally, the ItemContainerGenerator class contains several invaluable methods for mapping existing items within an ItemsControl to their containers and vice versa.
Who really holds the reigns of the generator?
An ItemContainerGenerator knows how to create a container and link it to an item, but who actually gets to decide when this container generation occurs? You might think that the generator, itself, should get to decide when to do its job, but actually, it intentionally remains “hands off” in these matters.
The ItemContainerGenerator does, however, monitor the collection view of its associated items collection (via a weak event listener for the INotifyCollectionChanged events). It uses the change notifications to know when to unlink an existing container from an item that is removed, but it never makes a decision on its own to generate a container for an added item. It just quietly maintains a record of all the items within the collection so that it is poised to create a container at a moment’s notice.
If it’s not the ItemContainerGenerator, then it must be the ItemsControl, right? Well, no, the ItemsControl does not directly generate the containers either, but it definitely plays some key roles in the process. Namely, it creates and owns the “generator” of containers (the ItemContainerGenerator instance) and it gets to decide the type of containers that will be created.
Okay, then who else could possibly be responsible for deciding when containers will be generated?
If you’ve been following this series for a while, it should be pretty obvious that neither the ItemsControl nor the ItemContainerGenerator can be the sole decision makers in the process because the ItemsControl class supports something called UI virtualization (wherein only visible UI elements are instantiated and added to the element tree).
In ‘P’ is for Panel, we learned that the items host (a.k.a., the items panel) of an ItemsControl is dynamic. As such, there is no way for the ItemsControl or its ItemContainerGenerator to generically know when containers will be visible. Clearly, the items panel must be directly involved in container generation. After all, the item containers are direct visual children of the items panel. Since the panel knows precisely where to place these children, it is very logical that it would be the one holding the proverbial reigns of the ItemContainerGenerator and telling it when to generate the containers.
How does the generator know what type of container to generate?
When it is time to generate a container, you might be wondering how the ItemContainerGenerator knows what type of object to instantiate. Well, really, it doesn’t know at all. Each ItemsControl gets to specify its own type of item container. The generator simply defers this container creation to its associated ItemsControl by calling its GetContainerForItem() method. This method is responsible for creating and returning a new container for an item.
What happens if the item is already the same type as the container?
We’ve previously looked at examples like the following in which a ListBoxItem is added directly to a ListBox:
<ListBox SelectedIndex="0" Width="100">
<ListBoxItem>Item 1</ListBoxItem>
<ListBoxItem>Item 2</ListBoxItem>
<ListBoxItem>Item 3</ListBoxItem>
<ListBoxItem>Item 4</ListBoxItem>
<ListBoxItem>Item 5</ListBoxItem>
</ListBox>
|
You may be wondering what the ItemContainerGenerator does in this case, since the item, itself, is already a container. Will it create a new ListBoxItem as the container for the specified ListBoxItem? The answer is no, it will not. Before the ItemContainerGenerator creates a new container, it first checks to see whether the item, itself, is already a container. It does this by calling the IsItemItsOwnContainer() method on the associated ItemsControl.
How can we specify our own container type for a custom ItemsControl?
Suppose we wish to create a custom ForceDirectedItemsControl class that will use a physics-aware items panel to host its children. To enable this scenario, we decide that each container will need to be an instance of a custom ForceDirectedItem class that will support the extra physics-based properties that control item layout in our custom panel (like friction, spring, repulsion, etc). How can we ensure that our custom ForceDirectedItemsControl class creates the ForceDirectedItem containers?
Well, it turns out that this is pretty easy… we just need to override two virtual methods in our ItemsControl class: IsItemItsOwnContainerOverride() and GetContainerForItemOverride().
The following is a typical implementation of a custom ItemsControl class with a custom item container:
public class ForceDirectedItemsControl : ItemsControl { protected override bool IsItemItsOwnContainerOverride(object item) { return (item is ForceDirectedItem); } protected override DependencyObject GetContainerForItemOverride() { return new ForceDirectedItem(); } }
Using the above class, when the ItemContainerGenerator is asked to generate a container for an item, it will first check to see if the item is already a ForceDirectedItem. If not, it will ask the ItemsControl to create a container by deferring to its GetContainerForItemOverride().
Caution: You should never assume there to be a constant tie between an item and its container. The GetContainerForItemOverride() method intentionally does not supply the item that will be hosted within the container. It is the responsibility of the ItemContainerGenerator to link an item to its container. When a virtualizing panel is hosting containers, it is common to link one item to a container and later link a completely different item to that same container. This provides a nice perf optimization.
How does UI virtualization work in an ItemsControl?
I previously promised that this episode would include a high level look at exactly how an ItemsControl supports this concept of UI virtualization. Feel free to skip ahead if you are not curious about the finer details of virtualization.
There are really only two noteworthy panel classes in the framework that take control of the ItemContainerGenerator and instruct it to generate containers: Panel and VirtualizingStackPanel.
Sidenote: There is actually a third native class, called ToolBarPanel, that uses the ItemContainerGenerator to generate items for a ToolBar. We won’t really spend any time looking at the ToolBar class in this series, since in many ways it breaks the traditional ItemsControl conventions. For more, see the note marked “**” at the end of ‘I’ is for Item Container.
The abstract Panel base class contains what we can consider the default container generation code for panels. This default logic is simply to generate containers for every item in the collection. So by default, there is no UI virtualization whatsoever.
The framework contains an abstract class called VirtualizingPanel which derives from Panel and then overrides the default container generation code to replace it with… well… nothing. So if you derive a panel from VirtualizingPanel, it will not generate any containers on its own. Instead, you will be responsible for implementing the code that leverages the ItemContainerGenerator to generate the containers.
There is only one virtualizing panel included in the early releases of the framework (.NET 3.0 and 3.5). It is called VirtualizingStackPanel. And if you paid attention to the chart at the end of ‘I’ is for Item Container, you know that VirtualizingStackPanel is the default items host for ListBox and ListView. So we get UI virtualization thrown in for free when we use these controls with their default items panel.
A VirtualizingStackPanel, like any other panel, is responsible for sizing and positioning its children (the item containers). As such, it knows exactly which children are visible within the viewport of the ItemsControl at any given time. It uses this knowledge to create, or “realize”, only the visible children (plus a few extra on either side of the viewport to enable keyboard navigation to work as expected). When a realized child is scrolled out of the viewport, the VirtualizingStackPanel queues that container to be removed, or “virtualized”, so that its resources can be reclaimed.
If you are writing a custom virtualizing panel, you will also need to implement the logic for realizing visible containers and virtualizing non-visible containers. Realization consists of generating the container (generator.GenerateNext()) and preparing it to host its item (generator.PrepareItemContainer()). Virtualization consists of removing the container (generator.Remove()).
We will go deeper into UI virtualization in a future article entitled ‘V’ is for Virtualization. In the meantime, if you are anxious to get started writing a virtualizing panel, you should check out Dan Crevier’s series on creating a virtualizing panel.
Tying It All Together
We’ve already covered many different aspects of an ItemsControl in this series and they all come into play in this process of item container generation. There is a very intricate dance involving an ItemsControl, its items host (‘P’ is for Panel), its Items collection (‘C’ is for Collection), and its ItemContainerGenerator (‘G’ is for Generator… this article). This dance results in the creation of item containers (‘I’ is for Item Container) that will host the items directly or host a visual representation of the items, as specified via an item template (‘D’ is for DataTemplate).
Finding Template Elements by Mapping Items to Containers
A very common question in the WPF Forum is, “How can I get a reference to a specific element in my item template at runtime?”
Sidenote: In most scenarios, you shouldn’t need to do this. Typically, the reason a person wants to do this is so they can programmatically change a property on some element in the template. If one carefully develops their view model (which will contain the data items represented within the ItemsControl), this type of property update can be handled via a binding to a property on a data item.
There are certainly a few scenarios where it is necessary to drill into an item’s visual subtree to get a specific element. For these scenarios, we can leverage a couple of nifty methods of the ItemContainerGenerator: ContainerFromIndex() and ContainerFromItem(). Once you have the container for the desired item, you can locate the element within its visual tree using a recursive routine like the GetDescendantByName() or GetDescendantByType() routines shown here:
public static Visual GetDescendantByName(Visual element, string name) { if (element == null) return null; if (element is FrameworkElement && (element as FrameworkElement).Name == name) return element; Visual result = null; if (element is FrameworkElement) (element as FrameworkElement).ApplyTemplate(); for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) { Visual visual = VisualTreeHelper.GetChild(element, i) as Visual; result = GetDescendantByName(visual, name); if (result != null) break; } return result; } public static Visual GetDescendantByType(Visual element, Type type) { if (element.GetType() == type) return element; Visual foundElement = null; if (element is FrameworkElement) (element as FrameworkElement).ApplyTemplate(); for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++) { Visual visual = VisualTreeHelper.GetChild(element, i) as Visual; foundElement = GetDescendantByType(visual, type); if (foundElement != null) break; } return foundElement; }
Finding the Container Associated with a Template Element
Similarly, there are times when you will want to handle an event on a specific template element and then locate the ancestor item container for the element. One option is to walk the ancestors looking for the element by type with the following GetAncestorByType() routine:
public static DependencyObject GetAncestorByType( DependencyObject element, Type type) { if (element == null) return null; if (element.GetType() == type) return element; return GetAncestorByType(VisualTreeHelper.GetParent(element), type); }
Another perfectly viable approach is to leverage a little fact that we learned in ‘I’ is for Item Container… namely, that the DataContext of the container is the very item that it contains. In most scenarios, this same DataContext will be inherited by all framework elements within the item template. So you can typically cast the original source of the event to a FrameworkElement and use the DataContext property to get the item that is represented by the template. You can then use the ContainerFromItem() method of the ItemContainerGenerator to get the container.
Most of these tricks work great for a simple ItemsControl, but then get a little tricky with a HeaderedItemsControl like a TreeView or MenuItem. For these cases, I strongly recommend leveraging the view model, commanding, and bindings to handle property updates within the view and to respond to user actions within the view model. We’ll explore these things further in future episodes.
Dealing with Asynchronous Container Generation
Alright, clearly the ItemContainerGenerator class provides some very handy, and even essential, methods for mapping items to containers and vice versa. There is still one very important thing to add to the whole equation… that is the timing of container generation.
Suppose you have a ListBox named CharacterListBox bound to an observable collection named Characters. You might be tempted to write code like the following:
private void AddScooby()
{
Character scooby = new Character("Scooby Doo");
Characters.Add(scooby);
ListBoxItem lbi = CharacterListBox.ItemContainerGenerator
.ContainerFromItem(scooby) as ListBoxItem;
lbi.IsSelected = true;
}
This code will actually result in an exception because the lbi member will be null. The reason is that containers are generated in a separate dispatcher operation. As a result, simply setting the ItemsSource property or modifying the bound collection does not cause containers to be created immediately.
The key point here is that we must always think of container generation as an asynchronous operation. So how can we add an item and then safely locate its container after it has been generated? For this very purpose, the ItemContainerGenerator class provides a Status property along with change notifications for the status. If we need to programmatically access containers after they are generated, we can subscribe to the StatusChanged event, as shown in the following code:
private void AddScooby() { _scooby = new Character("Scooby Doo"); Characters.Add(_scooby); CharacterListBox.ItemContainerGenerator.StatusChanged += OnStatusChanged; } private void OnStatusChanged(object sender, EventArgs e) { if (CharacterListBox.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { CharacterListBox.ItemContainerGenerator.StatusChanged -= OnStatusChanged; ListBoxItem lbi = CharacterListBox.ItemContainerGenerator .ContainerFromItem(_scooby) as ListBoxItem; if (lbi != null) { lbi.IsSelected = true; } } }
There are a couple of important things to notice about this code. First, the OnStatusChanged() method immediately checks the current status of the generator. This is important because the status could have changed to one of four different possible values: NotStarted, GeneratingContainers, ContainersGenerated, or Error. We should always specifically check for the status that we care about.
Second, as soon as the containers have been generated, the handler removes itself so that it will not be called when subsequent changes to the Items collection cause the generator’s status to change.
Bonus Tidbit: FindAncestor Bindings are Very Handy in Item Templates
This is really just an extra tidbit that I’m tagging onto this topic because of its usefulness. It is not really specific to container generation.
Recall that in ‘D’ is for DataTemplate, we learned how to provide a template of visuals to represent the items in an Items collection. When dealing with a Selector like ListBox, ListView, TreeView, etc, it is quite common to want to trigger a change in the visuals based on whether an item is selected. We now know that the concept of selection for these controls is based on the IsSelected property of their respective item containers. Any element in the item template can be bound to a property of the item container using a FindAncestor binding.
Below is a typical data trigger that might be found in a DataTemplate for an item within a ListBox:
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Value="True">
<Setter Property="Foreground" Value="#A1927E" TargetName="tb" />
</DataTrigger>
Up Next
In the next episode of this series, I plan to examine the “lookless” nature of ItemsControls. Please stay tuned!