问题 根据子对象类型选择DataTemplate


我想数据绑定ItemsCollection,但我想渲染通过集合项上的属性到达的子对象,而不是渲染集合项。

更具体地说:这将是游戏的2D地图查看器(尽管在当前状态下它还不是2D)。我将ItemsControl数据绑定到ObservableCollection <Square>,其中Square有一个名为Terrain的属性(Terrain类型)。地形是一个基类,有各种后代。

我想要的是ItemsControl从每个集合元素渲染Terrain属性,而不是集合元素本身。

我已经可以完成这项工作,但有一些不必要的开销。我想知道是否有一种很好的方法来消除不必要的开销。

我目前拥有以下课程(简化):

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
}

还有一个名为MapView的UserControl,包含以下内容:

<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}"/>

鉴于此代码,如果我这样做:

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

我得到的东西看起来与我的预期完全一样:StackPanel包含棕褐色盒子(用于Dirt)和银盒子(用于SteelPlate)。但我得到了不必要的开销。

我特别关注的是我的DataTemplate for Square:

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

我真正想说的是“不,不要费心渲染Square本身,而是渲染它的Terrain属性”。这接近于此,但是这为每个Square增加了两个可视树的控件:一个ContentControl,在上面的XAML中显式编码,以及它的ContentPresenter。我不是特别想在这里使用ContentControl;我真的想要短路并将Terrain属性的DataTemplate直接插入控制树。

但是,我如何告诉ItemsControl渲染collectionitem.Terrain(因此查找Terrain对象的上述DataTemplates之一)而不是渲染collectionitem(并为Square对象寻找DataTemplate)?

我想将DataTemplates用于地形,但对于Square来说根本不是必需的 - 这只是我发现的第一种充分工作的方法。事实上,我真正想要的是完全不同的东西 - 我真的想将ItemsControl的DisplayMemberPath设置为“Terrain”。这会直接呈现正确的对象(Dirt或SteelPlate对象),而无需添加额外的ContentControl或ContentPresenter。不幸的是,DisplayMemberPath总是呈现一个字符串,忽略了Terrains的DataTemplates。所以它有正确的想法,但它对我没用。

整个事情可能是过早优化,如果没有 简单 如何获得我想要的东西,我将与我所拥有的一起生活。但是如果有一个“WPF方式”我还不知道绑定到一个属性而不是整个集合项,它会增加我对WPF的理解,这正是我所追求的。


6497
2018-04-26 14:06


起源

我添加了第二个答案。看看,如果有帮助请告诉我。 - bendewey


答案:


我不完全确定你的模型是什么样的,但你可以随时使用。绑定到对象属性。例如:

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

更新

虽然,如果您正在寻找一种方法来绑定集合中的两个不同对象,您可能需要查看 ItemTemplateSelector 属性。

在您的场景中,它将是这样的(未经测试):

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

然后使用它你会有:

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>

10
2018-04-26 14:15



我添加了更新,我可能第一次没有理解你的问题。 - bendewey
这看起来应该有用。删除了我的答案,因为它只是解决Silverlight主要缺乏WPF机制。 - Mikko Rantanen
再次更新为灰尘/钢铁,我对你的模型仍然很朦胧。 - bendewey
这可能有用,但是有没有办法重用Terrains的现有DataTemplates,而不是用“if”bush复制DataTemplate系统? - Joe White
请参阅他们使用MainWindow.Resources引用的链接。要在Xaml中执行此操作,我可能需要了解有关您的模型的更多信息。你能提供图表或示例代码吗? - bendewey


我正在添加另一个答案,因为这是对问题的一种不同看法,然后是我的另一个答案。

如果您尝试更改Canvas的背景,那么您可以使用这样的DataTrigger:

<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>

你还需要使用这个转换器:

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

}

2
2018-04-27 00:29



实际上,我发布的代码是简化的;在我的真实项目中,那些DataTemplates包含UserControls,而不是纯色。我只是不想让代码变得比现在复杂得多。不过好好想一想。 - Joe White
使用这种技术可能仍然可以交换用户控件,但它开始变得比你已经在做的更复杂。我认为你所拥有的将会很好。 - bendewey


我相信你可以做的最好的消除视觉树开销(和冗余)是这样的:

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

我本可以发誓你可以通过直接分配到这个来更进一步 Content 的财产 ContentPresenter 为每个项目生成 ItemsControl

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

但是,那 ContentPresenter 似乎有父母 DataContext 就像它一样 DataContext 而不是 Square。这对我没有意义。它适用于 ListBox 或任何其他 ItemsControl 子类。也许这是一个WPF错误 - 不确定。我将不得不进一步研究它。


2
2018-04-26 15:09



这与OP使用的示例有何不同? - bendewey
同意,这只是内联DataTemplate,同时仍为集合中的每个项目创建额外的ContentControl。如果可以的话,我想避免控制树中的额外控件。 - Joe White
@Joe:如果您不想在可视化树中使用ContentControl,那么请改用ContentPresenter。更新我的答案以反映这一点...... - Kent Boogaart
是的,这确实减少了开销。但Square仍有一个ContentPresenter用于Square的DataTemplate,然后是另一个用于terrain的DataTemplate。有没有办法短路,只是直接显示地形,跳过广场? WPF没有内置此功能似乎很奇怪。 - Joe White
@Joe:请看我的编辑。如果我发现更进一步,我会报告。 - Kent Boogaart