The developers of the ItemsControl class gave it a particular default behavior. They thought that any objects that extended the UIElement class would have their own UI container and so, should be displayed directly, rather than allowing them to be templated in the usual way.
There is a method named IsItemItsOwnContainer in the ItemsControl class, which is called by the WPF Framework, to determine if an item in the Items collection is its own item container or not. Let's first take a look at the source code of this method:
public bool IsItemItsOwnContainer(object item)
{
return IsItemItsOwnContainerOverride(item);
}
Note that internally, this method just calls the IsItemItsOwnContainerOverride method, returning its value unchanged. Let's take a look at the source code of that method now:
protected virtual bool IsItemItsOwnContainerOverride(object item) { return (item is UIElement); }
Here, we see two things: The first is the default implementation that was just mentioned, where true is returned for all items that extend the UIElement class, and false for all other types. The second is that this method is marked as virtual, so we are able to extend this class and override the method to return a different value.
Let's now look at the crucial part of the ItemsControl class source code (without the comments), where our overridden method would be used. This excerpt is from the GetContainerForItem method:
DependencyObject container; if (IsItemItsOwnContainerOverride(item)) container = item as DependencyObject; else container = GetContainerForItemOverride();
With the default implementation, we see that UIElement items are cast to the type of DependencyObject, and set as the container, while a new container is created for items of all other types. Before overriding this method, let's see what effect the default behavior has, using an example.
The aim of this example is to render a little hollow circle for each item in a collection. Think of a slide show, where these circles would represent the slides, or a page numbering or linking system. We, therefore, need a collection control containing some items and a DataTemplate, with which to define the circles. Let's see the collection control with the items on their own first:
<UserControl
x:Class="CompanyName.ApplicationName.Views.ForcedContainerItemsControlView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="175" Width="287"> <Grid> <Grid.Resources> <ItemsPanelTemplate x:Key="HorizontalPanelTemplate"> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> <Style TargetType="{x:Type Rectangle}"> <Setter Property="Width" Value="75" /> <Setter Property="Height" Value="75" /> <Setter Property="RadiusX" Value="15" /> <Setter Property="RadiusY" Value="15" /> </Style> </Grid.Resources> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <ListBox Name="ListBox" Height="105" Margin="20,20,20,0"
ItemsPanel="{StaticResource HorizontalPanelTemplate}"> <Rectangle Fill="Red" /> <Rectangle Fill="Orange" /> <Rectangle Fill="Green" /> </ListBox> </Grid> </UserControl>
We start with the resources, where we have declared an ItemsPanelTemplate, that is set to an instance of a StackPanel, with its Orientation property set to Horizontal. This will make the panel's items appear horizontally. We then added a basic Style, in which we set our common properties for the Rectangle class.
In the markup, we have a Grid panel with two rows. In the first row, we have a ListBox named ListBox, with three colored Rectangle objects declared within its Items collection. Its ItemsPanel property is set to the ItemsPanelTemplate instance that we declared in the control's Resources section. The second row is currently empty, but let's see the visual output so far:
So far, so good. We can see our three rounded rectangles in the ListBox control. Now, let's add a DataTemplate into the Resources section and an ItemsControl element into the second row of the Grid panel, declaring it directly underneath the ListBox XAML:
<DataTemplate x:Key="EllipseDataTemplate" DataType="{x:Type UIElement}"> <Ellipse Width="16" Height="16"
Stroke="Gray" StrokeThickness="2" Margin="4" /> </DataTemplate> ... <ItemsControl Grid.Row="1" ItemsSource="{Binding Items, ElementName=ListBox}"
ItemsPanel="{StaticResource HorizontalPanelTemplate}"
ItemTemplate="{StaticResource EllipseDataTemplate}"
HorizontalAlignment="Center" />
Note that this ItemsControl element has its ItemsSource property data bound to the Items property from the ListBox, using an ElementName binding. Like the ListBox control, it also arranges its items horizontally, using the ItemsPanelTemplate resource. It also applies the new DataTemplate element that we just added into the Resources section.
In this DataTemplate, we define a hollow gray Ellipse element to be rendered for each item in the collection, specifying its dimensions, spacing and stroke settings. Let's take a look at the visual output of our example now:
As you can see, we have some unexpected results. Instead of rendering the small gray ellipses that we defined in the DataTemplate, the items in the ItemsControl display the actual items from the ListBox. Even worse than that, as each UI element can only be displayed in one location at any given point in time, the original items no longer even appear in the ListBox.
You may see an ArgumentException being thrown regarding this issue:
Must disconnect the specified child from current parent Visual before attaching to new parent Visual.
But why haven't these objects been rendered as hollow circles in the second ListBox, according to our DataTemplate? Do you remember the IsItemItsOwnContainerOverride method that we investigated? Well, that is the reason.
The objects that are data-bound to the ItemsControl's ItemsSource property extend the UIElement class, and so the ItemsControl class uses them as their own containers, rather than creating a new container and applying the item template to them.
So, how do we change this default behavior? That's right, we need to extend the ItemsControl class and override the IsItemItsOwnContainerOverride method to always return false. In this way, a new container will always be created and the item template will always be applied. Let's see how this would look in a new class:
using System.Windows.Controls; namespace CompanyName.ApplicationName.Views.Controls { public class ForcedContainerItemsControl : ItemsControl { protected override bool IsItemItsOwnContainerOverride(object item) { return false; } } }
Here we have the very simple ForcedContainerItemsControl class, with its single overridden method, that always returns false. We need to do nothing else in this class, as we are happy to use the default behavior of the ItemsControl class for everything else.
All that remains is for us to use our new class in our example. We start by adding a XAML Namespace for our Controls CLR Namespace:
xmlns:Controls="clr-namespace:CompanyName.ApplicationName.Views.Controls"
Next, we replace the ItemsControl XAML with the following:
<Controls:ForcedContainerItemsControl Grid.Row="1"
ItemsSource="{Binding Items, ElementName=ListBox}"
ItemsPanel="{StaticResource HorizontalPanelTemplate}"
ItemTemplate="{StaticResource EllipseDataTemplate}"
HorizontalAlignment="Center" Height="32" />
Let's see the new visual output now:
Now, we see what we were originally expecting to see: a little hollow circle rendered for each item in the collection. The items in our custom ItemsControl have now all been generated a new container and had our template applied to them as expected.
But what if we need to make use of the selected item in this example? The ItemsControl class has no concept of a selected item, so in this case, we would need to use a ListBox control in the second row of the Grid panel.
However, note that the ListBox class has also overridden the IsItemItsOwnContainerOverride method, so that it does not suffer from this same problem.
In fact, it will only use an item as a container if it is actually the correct container for this class; a ListBoxItem. Let's see its overridden method:
protected override bool IsItemItsOwnContainerOverride(object item) { return (item is ListBoxItem); }
Therefore, if we need access to the SelectedItem property from the ListBox class, then we do not need to create our own extended class to override this method, and can instead use their standard implementation. To get the same visual output however, we would need some styles to hide the ListBox's border and selected item highlights. Let's see a basic example of this:
<Style x:Key="HiddenListBoxItems" TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <ContentPresenter /> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="HiddenListBox" TargetType="{x:Type ListBox}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBox}"> <ScrollViewer> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding
SnapsToDevicePixels}" /> </ScrollViewer> </ControlTemplate> </Setter.Value> </Setter> </Style>
We would also need to update our EllipseDataTemplate template to include a trigger to highlight the small Ellipse object when its related item is selected in the top ListBox control:
<DataTemplate x:Key="EllipseDataTemplate" DataType="{x:Type UIElement}"> <Ellipse Width="16" Height="16" Stroke="Gray" StrokeThickness="2"
Margin="8"> <Ellipse.Style> <Style TargetType="{x:Type Ellipse}"> <Setter Property="Fill" Value="Transparent" /> <Style.Triggers> <DataTrigger Binding="{Binding IsSelected,
RelativeSource={RelativeSource
AncestorType={x:Type ListBoxItem}}}" Value="True"> <Setter Property="Fill" Value="LightGray" /> </DataTrigger> </Style.Triggers> </Style> </Ellipse.Style> </Ellipse> </DataTemplate>
And finally, we'll need to replace our ForcedContainerItemsControl element with a standard ListBox and apply our styles to it and its containers:
<ListBox Grid.Row="1" ItemsSource="{Binding Items, ElementName=ListBox}"
ItemsPanel="{StaticResource HorizontalPanelTemplate}"
ItemTemplate="{StaticResource EllipseDataTemplate}"
SelectedItem="{Binding SelectedItem, ElementName=ListBox}"
Style="{StaticResource HiddenListBox}"
ItemContainerStyle="{StaticResource HiddenListBoxItems}"
HorizontalAlignment="Center" />
When we run the application now, we see that the small hollow Ellipse objects become filled when their related item is selected in the top ListBox:
So, we've seen how we can override these protected methods to change the default behavior of the built-in controls. Let's now take a look at how we can build these protected methods into our own custom classes, so that they can affect the natural flow of a piece of our control's functionality.