【问题标题】:Selecting DataTemplate based on sub-object type根据子对象类型选择 DataTemplate
【发布时间】:2010-10-21 21:52:57
【问题描述】:

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

更具体地说:这将是一个用于游戏的 2D 地图查看器(尽管在当前状态下它还不是 2D)。我将 ItemsControl 数据绑定到 ObservableCollection,其中 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,其中包含一个棕褐色盒子(用于 Dirt)和一个银色盒子(用于 SteelPlate)。但我得到了不必要的开销。

我特别关心我的 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(从而查找上述 DataTemplate 中的 Terrain 对象之一)而不是渲染 collectionitem(并为 Square 对象查找 DataTemplate)?

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

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

【问题讨论】:

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

标签: wpf datatemplate itemscontrol


【解决方案1】:

我不确定您的模型是什么样子,但您始终可以使用 .绑定到对象属性。例如:

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

【讨论】:

  • 我添加了一个更新,我可能第一次没有理解你的问题。
  • 这看起来应该可行。删除了我的答案,因为它主要是解决 Silverlights 缺乏 WPF 机制的问题。
  • 再次更新为泥土/钢铁,我对你的模型仍然有点模糊。
  • 这可行,但有没有办法为地形重用现有的 DataTemplate,而不是用“if”灌木复制 DataTemplate 系统?
  • 查看他们使用对 MainWindow.Resources 的引用的链接。要仅在 Xaml 中执行此操作,我可能需要了解有关您的模型的更多信息。能否提供图表或示例代码?
【解决方案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 错误 - 不确定。我将不得不进一步调查。

【讨论】:

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

我正在添加另一个答案,因为这与我的另一个答案不同。

如果你想改变 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();
    }

}

【讨论】:

  • 其实我贴的代码是简化的;在我的真实项目中,这些数据模板包含用户控件,而不是纯色。我只是不想让代码变得比现在更复杂。不过,好主意。
  • 这种技术可能仍然可以交换用户控件,但它开始变得比你已经在做的更复杂。我认为你所拥有的一切都会很好。
猜你喜欢
  • 2021-07-03
  • 1970-01-01
  • 1970-01-01
  • 2016-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多