Taking complete control

At times, we might want to display different objects of the same type in different ways, depending on the values of their properties. For example, with a collection of objects that represent vehicles, you might want to have different displays for different types of vehicle, as trucks have different specifications to motor boats. The DataTemplateSelector class enables us to do just that.

When extending the DataTemplateSelector class, we can override its single SelectTemplate method. In this method, we are provided with both the data object and the data bound object and can select different data templates to return, dependent on the data object's property values.

Let's see a very simple example, where we return one of two data templates based on the User's age. We'll first need to declare another DataTemplate for our User type:

<DataTemplate x:Key="InverseUserTemplate" 
  DataType="{x:Type DataModels:User}"> 
  <Border BorderBrush="White" BorderThickness="1" Background="Black"     
    TextElement.Foreground="White" CornerRadius="5" Padding="8,3,5,5"
    Margin="0,0,0,5"> 
    <StackPanel Orientation="Horizontal"> 
      <TextBlock Text="{Binding Name}" Margin="0,0,3,0" /> 
      <TextBlock Text="{Binding Age, StringFormat={}({0})}" /> 
    </StackPanel> 
  </Border> 
</DataTemplate> 

In this template, we have simply inverted the colors of the background and foreground from those in the first template. Let's now see our DataTemplateSelector class that will reference both this and the other DataTemplate element:

using System.Windows; 
using System.Windows.Controls; 
using CompanyName.ApplicationName.DataModels; 
 
namespace CompanyName.ApplicationName.Views.DataTemplateSelectors 
{ 
  public class UserAgeDataTemplateSelector : DataTemplateSelector 
  { 
    public override DataTemplate SelectTemplate(object item,  
      DependencyObject container) 
    { 
      FrameworkElement element = container as FrameworkElement; 
      if (element != null && item != null && item is User user) 
      { 
        if (user.Age < 35) return 
          (DataTemplate)element.FindResource("InverseUserTemplate"); 
        else return (DataTemplate)element.FindResource("UserTemplate"); 
      } 
      return null; 
    } 
  } 
} 

In this example, we first defensively cast the container input parameter to an object of type FrameworkElement, using the as keyword. We then perform the standard null checks for this new object and the other input parameter and use the is keyword to pattern match the correct type and automatically cast the item parameter to a User object, if it's of the right type. If it is, then we call the FindResource method on ourĀ FrameworkElement object, to return the appropriate data template, dependent upon the value of the Age property. Otherwise, we return null.

The FrameworkElement.FindResource method first searches the calling object for the data template and then its parent element, and so on, up the logical tree. If it doesn't find it in any parent element in the application window, it then looks through the App.xaml file. If it still does not find it there, it then searches in the themes and system resources.

The container input parameter is used to access the FindResource method. Note that it will typically be of type ContentPresenter if we're using a normal collection control, so we could have cast it to that type in order to access the data templates.

However, the default container could be overridden to use one of the parent classes that the ContentPresenter class is derived from. Therefore, to avoid the possibility of exceptions, it is safer to cast it to the FrameworkElement class that actually declares the FindResource method.

Let's see how we can use this class now. First, we need to add the XAML namespace prefix for our DataTemplateSelectors namespace:

xmlns:DataTemplateSelectors=
  "clr-namespace:CompanyName.ApplicationName.Views.DataTemplateSelectors"

Then we need to add an instance of our UserAgeDataTemplateSelector class to a Resources section:

<DataTemplateSelectors:UserAgeDataTemplateSelector 
  x:Key="UserAgeDataTemplateSelector" />

Finally, we set our resource selector to the ItemTemplateSelector property:

<ItemsControl ItemsSource="{Binding Users}" Padding="10"  
  ItemTemplateSelector="{StaticResource UserAgeDataTemplateSelector}" />

When running the application now, we'll see this new output:

Note that DataTemplateSelector classes are typically used with very different templates, such as those that make up the different editing or viewing modes of a custom control. Slight differences like those in our simple example can be far easier achieved using style triggers and we'll find out more about triggers and styles in the next chapter.