根据子对象类型选择DataTemplate

13

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

更具体地说:这是一个用于游戏的2D地图查看器(虽然它现在还不是2D)。我将ItemsControl数据绑定到ObservableCollection<Square>,其中Square有一个名为Terrain(类型为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。但是我获得它时存在一些不必要的开销。

我特别关注的是我针对 Square 的 DataTemplate:

<DataTemplate DataType="{x:Type TerrainDataModels:Square}">
    <ContentControl Content="{Binding Path=Terrain}"/>
</DataTemplate>
我想要表达的是“不需要渲染Square本身,而是直接渲染其Terrain属性”。这个方法接近于实现需求,但对于每个Square,会在可视树中增加额外的两个控件:ContentControl(如上XAML所示)和它的ContentPresenter。我并不需要ContentControl,我真正想要的是插入Terrain属性的DataTemplate作为控件树的一部分。
但是如何告诉ItemsControl渲染collectionitem.Terrain(查找以上DataTemplates中的一个Terrains对象的模板),而不是渲染collectionitem(查找Square对象的DataTemplate)?
我想使用terrains的DataTemplates,但不一定要用于Square - 这只是我找到的第一个有效方法。实际上,我真正想做的是完全不同的事情 - 我真正想做的是将ItemsControl的DisplayMemberPath设置为“Terrain”。这将直接呈现正确的对象(Dirt或SteelPlate对象),而无需添加额外的ContentControl或ContentPresenter。不幸的是,DisplayMemberPath始终呈现字符串,忽略了terrains的DataTemplates。因此,它有正确的想法,但对我来说是无用的。
这整个过程可能是过早进行优化,如果没有容易实现的方法,我可以接受目前的结果。但如果有我尚未了解的“WPF方式”来绑定属性而不是整个集合项,那将增加我对WPF的理解,这正是我追求的。

我添加了第二个答案。看看是否有帮助。 - bendewey
3个回答

10

我不太确定你的模型是什么样子的,但你可以使用“.”来绑定对象属性。例如:

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

这看起来应该是可以工作的。我删除了我的答案,因为它主要是在 Silverlight 缺乏 WPF 机制的情况下工作的。 - Mikko Rantanen
再次更新为 dirt/steel,你的模型我还有点模糊。 - bendewey
可以这样做,但是有没有一种方法可以重用现有的地形DataTemplates,而不是通过“if”语句复制DataTemplate系统? - Joe White
我添加了示例代码并修改了问题,试图更清楚地表达我的要求。 - Joe White
但是你如何在app.xaml > DataTemplate中展示一个“集合”呢? - Brampage
显示剩余4条评论

2

我认为消除视觉树开销(和冗余)的最佳方法是这样的:

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

我记得你可以通过直接为ItemsControl中的每个项目生成的ContentPresenterContent属性进行赋值,从而更进一步地操作:

<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 的一个 bug - 不确定。我需要进一步研究。


这与OP使用的示例有何不同? - bendewey
同意,这只是将DataTemplate内联移动,同时为集合中的每个项创建了额外的ContentControl。如果可以的话,我想避免控件树中的额外控制。 - Joe White
@Joe:如果您不想在可视树中使用ContentControl,则可以使用ContentPresenter。我会更新我的答案以反映这一点... - Kent Boogaart
是的,这确实可以减少一些开销。但是对于正方形的DataTemplate仍然需要一个ContentPresenter,而地形的DataTemplate也需要一个ContentPresenter。难道没有任何方法可以直接跳过正方形,直接显示地形吗?WPF没有内置此功能似乎有些奇怪。 - Joe White
@Joe:请查看我的编辑。如果我发现任何进一步的事情,我会回报的。 - Kent Boogaart
+1 我还没有尝试过,但它看起来不错。我认为 Setter Content= 可能需要改成 Setter Value=。 - bendewey

2

我再添加一个答案,因为这个与我的另一个答案有所不同。

如果你想要改变画布的背景,那么你可以使用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();
    }

}

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接