Выбор DataTemplate на основе типа под-объекта

Я хочу привязать элемент ItemsCollection, но вместо рендеринга элементов коллекции я хочу отобразить под-объекты, полученные через свойство в элементе коллекции.

Чтобы быть более конкретным: это будет просмотрщик 2D-карт для игры (хотя в его текущем состоянии это еще не 2D). Я привязываю элемент ItemsControl к ObservableCollection <Square> , где Square имеет свойство Terrain (типа Terrain). Ландшафт является базовым классом и имеет различные потомки.

Я хочу, чтобы ItemControl отображал свойство 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
}

И UserControl, называемый MapView, содержащий следующее:

<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 для Square:

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

То, что я действительно хочу сказать, - "нет, не мешайте рендерингу самого Квадрата, вместо этого возвращайте его свойство Terrain". Это приближается к этому, но это добавляет дополнительные два элемента управления к визуальному дереву для каждой площади: ContentControl, как явно закодировано в вышеупомянутом XAML, и его ContentPresenter. Я не особо хочу здесь ContentControl; Я действительно хочу закоротить и вставить свойство Terrain DataTemplate непосредственно в дерево управления.

Но как я могу показать ItemsControl для рендеринга collectionitem.Terrain(таким образом, глядя на один из вышеперечисленных DataTemplates для объекта Terrain), а не на рендеринг collectionitem (и ищет DataTemplate для объекта Square)?

Я хочу использовать DataTemplates для ландшафтов, но вовсе не обязательно для Square - это был только первый подход, который я нашел, который работал адекватно. Фактически, я действительно хочу сделать что-то совершенно другое - я действительно хочу, чтобы ItemControl DisplayMemberPath был "Terrain". Это делает правый объект (объект Dirt или SteelPlate) напрямую, без добавления дополнительного ContentControl или ContentPresenter. К сожалению, DisplayMemberPath всегда отображает строку, игнорируя DataTemplates для ландшафтов. Поэтому у него появилась правильная идея, но это бесполезно для меня.

Все это может быть преждевременной оптимизацией, и если нет простого способа получить то, что я хочу, я буду жить с тем, что у меня есть. Но если есть "способ WPF", который я еще не знаю о привязке к свойству, а не ко всему элементу коллекции, он добавит мне понимание WPF, и это действительно то, что мне нужно.

Ответ 1

Я не совсем уверен, как выглядит ваша модель, но вы всегда можете использовать a. для привязки к объекту. Например:

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

Ответ 2

Я считаю, что лучшее, что вы можете сделать для устранения наглядности визуального дерева (и избыточности), таково:

<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 - не уверен. Мне нужно будет изучить его дальше.

Ответ 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();
    }

}