Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Selecting DataTemplate based on sub-object type

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.

like image 334
Joe White Avatar asked Apr 26 '09 14:04

Joe White


2 Answers

I'm adding another answer, because this is kind of a different take on the problem then my other answer.

If you are trying to change the background of the Canvas, then you can use a DataTrigger like this:

<DataTemplate DataType="{x:Type WpfApplication1:Square}">
    <DataTemplate.Resources>
        <WpfApplication1:TypeOfConverter x:Key="typeOfConverter" />
    </DataTemplate.Resources>
    <Canvas Name="background" Fill="Green" />
    <DataTemplate.Triggers>
        <DataTrigger Binding="{Binding Path="Terrain" Converter={StaticResource typeOfConverter}}" Value="{x:Type WpfApplication1:Dirt}">
            <Setter  TargetName="background"Property="Fill" Value="Tan" />
        </DataTrigger>
        <DataTrigger Binding="{Binding Path="Terrain" Converter={StaticResource typeOfConverter}}" Value="{x:Type WpfApplication1:SteelPlate}">
            <Setter TargetName="background" Property="Fill" Value="Silver" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>

You'd also need to use this Converter:

public class TypeOfConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value.GetType();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new System.NotImplementedException();
    }

}
like image 167
bendewey Avatar answered Oct 28 '22 09:10

bendewey


I believe the best you can do to eliminate visual tree overhead (and redundancy) is this:

<ItemsControl ItemsSource="{Binding Squares}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ContentPresenter Content="{Binding Terrain}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

I could have sworn you could take this a step further by directly assigning to the Content property of the ContentPresenter generated for each item in the ItemsControl:

<ItemsControl ItemsSource="{Binding Squares}">
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="ContentPresenter.Content" Content="{Binding Terrain}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

However, the ContentPresenter appears to have the parent DataContext as its DataContext rather than the Square. This makes no sense to me. It works fine with a ListBox or any other ItemsControl subclass. Perhaps this is a WPF bug - not sure. I will have to look into it further.

like image 23
Kent Boogaart Avatar answered Oct 28 '22 09:10

Kent Boogaart