Selecting DataTemplate based on sub-object type

Joe White picture Joe White · Apr 26, 2009 · Viewed 13.8k times · Source

I want to databind an ItemsCollection, but instead of rendering the collection items, I want to render sub-objects reached via a property on the collection item.

To be more specific: this will be a 2D map viewer for a game (though in its current state it isn't 2D yet). I databind an ItemsControl to an ObservableCollection<Square>, where Square has a property called Terrain (of type Terrain). Terrain is a base class and has various descendants.

What I want is for the ItemsControl to render the Terrain property from each collection element, not the collection element itself.

I can already make this work, but with some unnecessary overhead. I want to know if there's a good way to remove the unnecessary overhead.

What I currently have are the following classes (simplified):

public class Terrain {}
public class Dirt : Terrain {}
public class SteelPlate : Terrain {}
public class Square
{
    public Square(Terrain terrain)
    {
        Terrain = terrain;
    }
    public Terrain Terrain { get; private set; }
    // additional properties not relevant here
}

And a UserControl called MapView, containing the following:

<UserControl.Resources>
    <DataTemplate DataType="{x:Type TerrainDataModels:Square}">
        <ContentControl Content="{Binding Path=Terrain}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type TerrainDataModels:Dirt}">
        <Canvas Width="40" Height="40" Background="Tan"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type TerrainDataModels:SteelPlate}">
        <Canvas Width="40" Height="40" Background="Silver"/>
    </DataTemplate>
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding}"/>

Given this code, if I do:

mapView.DataContext = new ObservableCollection<Square> {
    new Square(new Dirt()),
    new Square(new SteelPlate())
};

I get something that looks exactly like what I expect: a StackPanel containing a tan box (for the Dirt) and a silver box (for the SteelPlate). But I get it with unnecessary overhead.

My specific concern is with my DataTemplate for Square:

<DataTemplate DataType="{x:Type TerrainDataModels:Square}">
    <ContentControl Content="{Binding Path=Terrain}"/>
</DataTemplate>

What I really want to say is "no, don't bother rendering the Square itself, render its Terrain property instead". This gets close to that, but this adds an extra two controls to the visual tree for every Square: a ContentControl, as coded explicitly in the above XAML, and its ContentPresenter. I don't particularly want a ContentControl here; I really want to short-circuit and insert the Terrain property's DataTemplate directly into the control tree.

But how do I tell the ItemsControl to render collectionitem.Terrain (thus looking up one of the above DataTemplates for the Terrain object) rather than rendering collectionitem (and looking for a DataTemplate for the Square object)?

I want to use DataTemplates for the terrains, but not at all necessarily for the Square -- that was just the first approach I found that worked adequately. In fact, what I really want to do is something completely different -- I really want to set the ItemsControl's DisplayMemberPath to "Terrain". That renders the right object (the Dirt or SteelPlate object) directly, without adding an extra ContentControl or ContentPresenter. Unfortunately, DisplayMemberPath always renders a string, ignoring the DataTemplates for the terrains. So it's got the right idea, but it's useless to me.

This whole thing may be premature optimization, and if there's no easy way to get what I want, I'll live with what I've got. But if there's a "WPF way" I don't yet know about to bind to a property instead of the whole collection item, it'll add to my understanding of WPF, which is really what I'm after.

Answer

bendewey picture bendewey · Apr 26, 2009

I'm not exactly sure what your model looks like, but you can always use a . to bind to an objects property. For example:

<DataTemplate DataType="TerrainModels:Square">
  <StackPanel>
    <TextBlock Content="{Binding Path=Feature.Name}"/>
    <TextBlock Content="{Binding Path=Feature.Type}"/>
  </StackPanel>
</DataTemplate>

Update

Although, if you are looking for a way to bind two different objects in a collection you might want to take a look at the ItemTemplateSelector property.

In your scenario it would be something like this (not tested):

public class TerrainSelector : DataTemplateSelector
{
  public override DataTemplate SelectTemplate(object item, DependencyObject container)
  {
    var square = item as Square;
    if (square == null) 
       return null;
    if (square.Terrain is Dirt)
    {
      return Application.Resources["DirtTemplate"] as DataTemplate;
    }
    if (square.Terrain is Steel)
    {
      return Application.Resources["SteelTemplate"] as DataTemplate;
    }
    return null;
  }
}

Then to use it you would have:

App.xaml

<Application ..>
  <Application.Resources>
    <DataTemplate x:Key="DirtTemplate">
      <!-- template here -->
    </DataTemplate>
    <DataTemplate x:Key="SteelTemplate">
      <!-- template here -->
    </DataTemplate>
  </Application.Resources>
</Application>

Window.xaml

<Window  ..>
  <Window.Resources>
    <local:TerrainSelector x:Key="templateSelector" />
  </Window.Resources>
  <ItemsControl ItemSource="{Binding Path=Terrain}" ItemTemplateSelector="{StaticResource templateSelector}" />
</Window>