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


Я хочу привязать коллекцию 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.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())
};

Я получаю то, что выглядит в точности так, как я ожидал: Стекпанель, содержащая коричневую коробку (для грязи) и серебряную коробку (для стальной пластины). Но я получаю его с ненужными накладными расходами.

Мое особое беспокойство связано с моей табличкой DataTemplate для Square:

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

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

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

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

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

3 11

3 ответа:

Я не совсем уверен, как выглядит ваша модель, но вы всегда можете использовать 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;
  }
}

Затем, чтобы использовать его, вы бы имеем:

Приложение.xaml

<Application ..>
  <Application.Resources>
    <DataTemplate x:Key="DirtTemplate">
      <!-- template here -->
    </DataTemplate>
    <DataTemplate x:Key="SteelTemplate">
      <!-- template here -->
    </DataTemplate>
  </Application.Resources>
</Application>

Окно.xaml

<Window  ..>
  <Window.Resources>
    <local:TerrainSelector x:Key="templateSelector" />
  </Window.Resources>
  <ItemsControl ItemSource="{Binding Path=Terrain}" ItemTemplateSelector="{StaticResource templateSelector}" />
</Window>

Я добавляю другой ответ, потому что это своего рода другой взгляд на проблему, чем мой другой ответ.

Если вы пытаетесь изменить фон холста, то вы можете использовать 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();
    }

}

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

<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 - не уверен. Я придется вникать в это и дальше.