Click here to Skip to main content
11,708,923 members (71,812 online)
Click here to Skip to main content

Routed Template Selection in WPF

, 13 May 2007 CPOL 90K 627 49
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Josh Smith
Software Developer (Senior) Cynergy Systems
United States United States
Josh creates software, for iOS and Windows.

He works at Cynergy Systems as a Senior Experience Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

You may also be interested in...

Comments and Discussions

 
GeneralMy vote of 5 Pin
Tarun.K.S4-Nov-10 3:18
memberTarun.K.S4-Nov-10 3:18 
GeneralThanks Pin
AEgiomo28-Jun-09 23:37
memberAEgiomo28-Jun-09 23:37 
GeneralVS 2008 XAML editor does not load Window1.xaml Pin
Terry76030-Apr-08 5:21
memberTerry76030-Apr-08 5:21 
GeneralRe: VS 2008 XAML editor does not load Window1.xaml Pin
Josh Smith4-May-08 14:01
mvpJosh Smith4-May-08 14:01 
GeneralThanks again Pin
Kavan Shaban18-Apr-08 19:41
memberKavan Shaban18-Apr-08 19:41 
QuestionAnyway to apply this to a HierarchicalDataTemplate's ItemTemplateSelector? Pin
Scott Williams26-Mar-08 4:47
memberScott Williams26-Mar-08 4:47 
GeneralNevermind, I'm a doofus. Pin
Scott Williams26-Mar-08 10:48
memberScott Williams26-Mar-08 10:48 
GeneralRe: Nevermind, I'm a doofus. Pin
Josh Smith26-Mar-08 10:51
mvpJosh Smith26-Mar-08 10:51 
GeneralStyle Selection Pin
gmarappledude24-Oct-07 1:51
membergmarappledude24-Oct-07 1:51 
GeneralRe: Style Selection Pin
Josh Smith24-Oct-07 2:17
mvpJosh Smith24-Oct-07 2:17 
GeneralRe: Style Selection Pin
gmarappledude24-Oct-07 2:26
membergmarappledude24-Oct-07 2:26 
QuestionHow to cause templates to be re-selected after a state change Pin
Drew Noakes1-Aug-07 2:21
memberDrew Noakes1-Aug-07 2:21 
AnswerRe: How to cause templates to be re-selected after a state change Pin
Josh Smith1-Aug-07 2:58
mvpJosh Smith1-Aug-07 2:58 
GeneralRe: How to cause templates to be re-selected after a state change Pin
Drew Noakes1-Aug-07 5:06
memberDrew Noakes1-Aug-07 5:06 
GeneralRe: How to cause templates to be re-selected after a state change Pin
Josh Smith1-Aug-07 5:34
mvpJosh Smith1-Aug-07 5:34 
GeneralThank You Pin
jellodog11-Jul-07 7:19
memberjellodog11-Jul-07 7:19 
GeneralRe: Thank You Pin
Josh Smith11-Jul-07 7:32
mvpJosh Smith11-Jul-07 7:32 
GeneralEvent Triggers throws an exception if I set my own Routed event Pin
marlongrech24-May-07 22:09
membermarlongrech24-May-07 22:09 
GeneralRe: Event Triggers throws an exception if I set my own Routed event Pin
Josh Smith24-May-07 23:37
mvpJosh Smith24-May-07 23:37 
QuestionAnother way? Pin
mvtongeren23-May-07 0:03
membermvtongeren23-May-07 0:03 
AnswerRe: Another way? Pin
Josh Smith23-May-07 2:22
mvpJosh Smith23-May-07 2:22 
GeneralRe: Another way? Pin
mvtongeren24-May-07 1:29
membermvtongeren24-May-07 1:29 
GeneralHoly Cow Pin
Sacha Barber14-May-07 2:21
memberSacha Barber14-May-07 2:21 
GeneralRe: Holy Cow Pin
Josh Smith14-May-07 2:50
mvpJosh Smith14-May-07 2:50 
GeneralRe: Holy Cow Pin
Sacha Barber14-May-07 4:10
memberSacha Barber14-May-07 4:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150819.1 | Last Updated 13 May 2007
Article Copyright 2007 by Josh Smith
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid