Выбор DataTemplate на основе типа подобъекта - PullRequest
11 голосов
/ 26 апреля 2009

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

Точнее, это будет просмотрщик 2D-карт для игры (хотя в его текущем состоянии это еще не 2D). Я связываю ItemsControl с ObservableCollection , где у Square есть свойство с именем 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
}

И 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, содержащую солярий (для грязи) и серебряную коробку (для стали). Но я получаю это с ненужными накладными расходами.

Моя особая проблема связана с моим DataTemplate для Square:

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

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

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

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

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

Ответы [ 3 ]

10 голосов
/ 26 апреля 2009

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

<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 голосов
/ 27 апреля 2009

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

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

}
2 голосов
/ 26 апреля 2009

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

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

...