Selecting DataTemplate based on sub-object type
Asked Answered
U

3

13

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.

Upcast answered 26/4, 2009 at 14:6 Comment(1)
I added a second answer. Take a look and let me know if that helps.Elissa
E
10

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>
Elissa answered 26/4, 2009 at 14:15 Comment(9)
I added an update, I may not have understood your question right the first time.Elissa
This looks like something that should work. Removed my answer as it was just working around Silverlights lack of WPF mechanics mainly.Mckee
updated again to dirt/steel, I'm still alittle hazy on your model.Elissa
That could work, but is there a way to reuse the existing DataTemplates for the Terrains, rather than duplicating the DataTemplate system with an "if" bush?Upcast
See the link they use a reference to the MainWindow.Resources. To do it in Xaml only I might need to know more about your Model. Can you provide a graph or sample code?Elissa
Very good article on MVVM that could also shed some light. msdn.microsoft.com/en-us/magazine/dd419663.aspx (MSDN Mag) by Josh SmithElissa
I added sample code and revised the question, trying to be clearer about what I'm after.Upcast
That code seems off, do you mean, mapView.DataContext = new ObservableCollection<Square> { new Square(new Dirt()), new Square(new SteelPlate())};Elissa
But how would you show a "collection" in app.xaml > DataTemplate?Schizont
V
2

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.

Voidable answered 26/4, 2009 at 15:9 Comment(6)
How does this differ from the example that the OP was using?Elissa
Agreed, this just moves the DataTemplate inline, while still creating an extra ContentControl for each item in the collection. I want to avoid the extra controls in the control tree if I can.Upcast
@Joe: If you don't want the ContentControl in the visual tree, then use a ContentPresenter instead. Updating my answer to reflect this...Voidable
Yes, that does cut down on the overhead a bit. But there's still a ContentPresenter for the Square's DataTemplate, and then another one for the terrain's DataTemplate. Isn't there any way to short-circuit and just show the Terrain directly, skipping the Square? It seems odd for WPF not to have this ability built in.Upcast
@Joe: please see my edit. I will report back if I find anything further.Voidable
+1 I haven't tried it but it looks good, i think the Setter Content= might have to be Setter Value=.Elissa
E
2

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();
    }

}
Elissa answered 27/4, 2009 at 0:29 Comment(2)
Actually, the code I posted is simplified; in my real project those DataTemplates contain UserControls, not solid colors. I just didn't want to make the code any more complicated than it was already. Good thought, though.Upcast
It may still be possible with this technique to swap usercontrols, but it starting to get more complicated that what you're already doing. I think that what you have will work fine.Elissa

© 2022 - 2024 — McMap. All rights reserved.