Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Routed Template Selection in WPF

0.00/5 (No votes)
13 May 2007 1  
Examines a powerful technique for implementing DataTemplate selection logic.

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 DataTemplates 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 DataTemplates 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 DataTemplates 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 )
{
 // Get a reference to the Person object being templated.

 Person person = e.DataObject as Person;      

 // This is one way to create "alternate row colors" in an ItemsControl.

 ItemContainerGenerator generator = this.personList.ItemContainerGenerator;
 DependencyObject container = generator.ContainerFromItem( person );
 int visibleIndex = generator.IndexFromContainer( container );
 string templateKey = 
  visibleIndex % 2 == 0 ? 
  "PersonTemplateEven" : 
  "PersonTemplateOdd";

 // Specify the data template which should be used to render

 // the Person object.

 e.TemplateToUse = this.FindResource( templateKey ) as DataTemplate;

 // Mark the event as "handled" so that it stops bubbling up 

 // the element tree.

 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
{
 /// <summary>

 /// Represents the TemplateRequested bubbling routed event.

 /// </summary>

 public static readonly RoutedEvent TemplateRequestedEvent =
  EventManager.RegisterRoutedEvent(
   "TemplateRequested",
   RoutingStrategy.Bubble,
   typeof( TemplateRequestedEventHandler ),
   typeof( RoutedDataTemplateSelector ) );

 // This event declaration is only here so that the compiler allows

 // the TemplateRequested event to be assigned a handler in XAML.

 // Since DataTemplateSelector does not derive from UIElement it 

 // does not have the AddHandler/RemoveHandler methods typically

 // used within an explicit event declaration.

 [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." ); 
  }
 }

 /// <summary>

 /// Raises the TemplateRequested event up the 'container' element's logical

 /// tree so that the DataTemplate to return can be determined.

 /// </summary>

 /// <param name="item">The data object being templated.</param>

 /// <param name="container">The element which contains the data.</param>

 /// <returns>The DataTemplate to apply.</returns>

 public override DataTemplate SelectTemplate( 
    object item, DependencyObject container )
 {   
  // We need 'container' to be a UIElement because that class

  // exposes the RaiseEvent method.

  UIElement templatedElement = container as UIElement;
  if( templatedElement == null )
   throw new ArgumentException( 
    "RoutedDataTemplateSelector only works with UIElements." );

  // Bubble the TemplateRequested event up the logical tree, starting at the

  // templated element. This allows others to determine what template to use.

  TemplateRequestedEventArgs args = 
    new TemplateRequestedEventArgs( 
     TemplateRequestedEvent, templatedElement, item );

  templatedElement.RaiseEvent( args );

  // Return the DataTemplate selected by the outside world.

  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:

// This does not work!

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here