Introduction
This article reviews a class which allows you to move template selection logic out of DataTemplateSelector
subclasses. Using this technique allows you to encapsulate knowledge of DataTemplate
resource keys into the places that actually contain those resources. It also makes it easier to implement template selection logic which requires more information about the state of the application than is typically available within a DataTemplateSelector
subclass. This technique can vastly reduce the number of template selector classes in a Windows Presentation Foundation (WPF) application, thus making it easier to extend and maintain.
Background
WPF controls often provide a means of programmatically selecting a DataTemplate
with which to render a data object. This functionality is exposed via properties whose names are suffixed with "TemplateSelector
". Some examples of this include the ContentTemplateSelector
property of ContentControl
, and ItemTemplateSelector
of ItemsControl
. Template selectors are classes which derive from DataTemplateSelector
and override the SelectTemplate
method.
The problem
Typically "template selector" classes end up containing hard-coded resource keys, for example:
public class MyTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(
object item, DependencyObject container )
{
FrameworkElement elem = container as FrameworkElement;
Foo foo = item as Foo;
if( foo.Name == "Cowabunga" )
return elem.FindResource( "SomeDataTemplate" );
else
return elem.FindResource( "SomeOtherDataTemplate" );
}
}
This is not always a desirable way to implement such logic. A DataTemplateSelector
cannot contain its own resources, so the DataTemplate
s it references are always defined in the Resources
collection of some other element. Referencing templates in a template selector duplicates knowledge of the resource keys. Duplicating information is generally a bad practice. If a DataTemplate
's resource key is changed, a new template is introduced, or an existing template is removed, then the template selector class must be updated accordingly. It would be better if template selectors were not dependent upon specific resource keys, so that they were not so tightly coupled to the elements which use them.
Template selectors are not always the ideal place to implement certain types of template selection logic. In some situations it is necessary to know the state of other elements in the user interface (UI) in order to determine which template should be used. The code which executes within a template selector's SelectTemplate
method has no direct visibility into other parts of the UI. It is sometimes necessary for template selection logic to know more than a template selector can know on its own.
The solution
My solution to this problem is to let the template selector delegate its job to another part of the application better equipped to determine which DataTemplate
to use. I created the RoutedDataTemplateSelector
class to do exactly that. The basic idea is that when a DataTemplate
needs to be selected, the RoutedDataTemplateSelector
bubbles an event up the element tree, starting at the element which requires the template. Whoever handles that event can determine the template to be used.
Using RoutedDataTemplateSelector
prevents a large number of DataTemplateSelector
subclasses from popping into existence, each with hard-coded resource keys. Instead you can embed the template selection logic into the Window
/Page
/UserControl
which contains both the DataTemplate
s to choose from and element being templated. The end result of using this approach is that changing an element's resources does not have a large ripple effect throughout your code base, and your template selection logic has more runtime context to work with.
Using the RoutedDataTemplateSelector
Suppose that we use an ItemsControl
to display a list of Person
objects, and we want the items in the list to display alternating background colors. We could achieve this by applying two DataTemplate
s to the items in the list, switching between the templates for each consecutive item. One template renders an item with one color and the other template renders an item with a different color. It might look like this:
We can easily implement this functionality by using the RoutedDataTemplateSelector
, as seen in the abridged example below.
<Window ... >
<Window.Resources>
<DataTemplate x:Key="PersonTemplateEven">
<Border ... >
<TextBlock Text="{Binding Path=Name}" Background="LightBlue" />
</Border>
</DataTemplate>
<DataTemplate x:Key="PersonTemplateOdd">
<Border ... >
<TextBlock Text="{Binding Path=Name}" Background="WhiteSmoke" />
</Border>
</DataTemplate>
<jas:RoutedDataTemplateSelector x:Key="PersonTemplateSelector" />
</Window.Resources>
<Grid>
<ItemsControl
x:Name="personList"
HorizontalContentAlignment="Stretch"
ItemsSource="{Binding}"
ItemTemplateSelector="{StaticResource PersonTemplateSelector}"
Margin="3"
jas:RoutedDataTemplateSelector.TemplateRequested="OnTemplateRequested"
/>
</Grid>
</Window>
The ItemsControl
markup seen above uses the "attached event" syntax to specify what method should be invoked when the RoutedDataTemplateSelector
's TemplateRequested
routed event is raised on it. The method which determines what DataTemplate
to apply to the Person
object is in the code-behind file of the Window
, as seen below:
void OnTemplateRequested( object sender, TemplateRequestedEventArgs e )
{
Person person = e.DataObject as Person;
ItemContainerGenerator generator = this.personList.ItemContainerGenerator;
DependencyObject container = generator.ContainerFromItem( person );
int visibleIndex = generator.IndexFromContainer( container );
string templateKey =
visibleIndex % 2 == 0 ?
"PersonTemplateEven" :
"PersonTemplateOdd";
e.TemplateToUse = this.FindResource( templateKey ) as DataTemplate;
e.Handled = true;
}
As this example demonstrates, the logic which selects a template to use is located in the Window
which contains the ItemsControl
being templated. This allows the template resource keys to only be known by the Window
which owns them, and makes it easy to figure out what color the templated item should be. If this logic was in a template selector then it would be brittle, and more difficult to determine at what index the item exists in the control.
How it works
RoutedDataTemplateSelector
is not a very complicated class. It is a DataTemplateSelector
subclass which exposes a bubbling routed event named TemplateRequested
. When the overridden SelectTemplate
method is invoked, it raises that event on the element to be templated and expects an ancestor in its logical tree to specify the DataTemplate
to return. That class is seen below:
public class RoutedDataTemplateSelector : DataTemplateSelector
{
public static readonly RoutedEvent TemplateRequestedEvent =
EventManager.RegisterRoutedEvent(
"TemplateRequested",
RoutingStrategy.Bubble,
typeof( TemplateRequestedEventHandler ),
typeof( RoutedDataTemplateSelector ) );
[EditorBrowsable( EditorBrowsableState.Never )]
public event TemplateRequestedEventHandler TemplateRequested
{
add
{
throw new InvalidOperationException(
"Do not directly hook the TemplateRequested event." );
}
remove
{
throw new InvalidOperationException(
"Do not directly unhook the TemplateRequested event." );
}
}
public override DataTemplate SelectTemplate(
object item, DependencyObject container )
{
UIElement templatedElement = container as UIElement;
if( templatedElement == null )
throw new ArgumentException(
"RoutedDataTemplateSelector only works with UIElements." );
TemplateRequestedEventArgs args =
new TemplateRequestedEventArgs(
TemplateRequestedEvent, templatedElement, item );
templatedElement.RaiseEvent( args );
return args.TemplateToUse;
}
}
The one oddity about this class is that it exposes a CLR wrapper event for the TemplateRequested
routed event, but using it will cause an exception to be thrown. That wrapper event declaration exists so that the compiler does not report an error when trying to assign a handler to TemplateRequested
in XAML. Since DataTemplateSelector
does not derive from UIElement
it does not have the AddHandler
and RemoveHandler
methods typically used to manage routed events. What that means in practical terms is that if you execute this code an exception will be thrown:
RoutedDataTemplateSelector selector = new RoutedDataTemplateSelector();
selector.TemplateRequested += this.OnTemplateRequested;
Instead you should use this approach:
someElement.AddHandler(
RoutedDataTemplateSelector.TemplateRequestedEvent,
new TemplateRequestedEventHandler( this.OnTemplateRequested ) );
History
- May 13, 2007 � Created article