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