Выбор 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();
}
}