Как вы стилизуете только элементы верхнего уровня в TreeView?

2-й день роюсь в сети и не нашел решения. Возьмем такой элемент:

<TreeView ItemsSource="{Binding Types}" Width="300">
   <TreeView.Resources>
      <HierarchicalDataTemplate DataType="{x:Type models:Type}"
                                ItemsSource="{Binding SubTypes}">
         <TextBlock Text="{Binding Name}"/>
         <HierarchicalDataTemplate.ItemTemplate>
            <DataTemplate DataType="{x:Type SubType}">
               <TextBlock Text="{Binding Name}"/>
            </DataTemplate>
         </HierarchicalDataTemplate.ItemTemplate>
      </HierarchicalDataTemplate>
   </TreeView.Resources>
</TreeView>

Я использую библиотеку материалов NuGet для базовых стилей. Однако теперь мне нужно отключить наведение, выбор и т. д. для элементов первого уровня и разрешить выбор/наведение только для подэлементов.

Но все, что я, кажется, нахожу, касается стилизации содержимого каждого элемента или стилизации всего в глобальном масштабе.

A <- remove selection/hover (pref single click too but that's another topic)
  A1 <- maintain original style, hover and select
  A2 <- maintain original style, hover and select
  A3 <- maintain original style, hover and select
B <- remove selection/hover (pref single click too but that's another topic)
  B1 <- maintain original style, hover and select
  B2 <- maintain original style, hover and select
  B3 <- maintain original style, hover and select

person Agony    schedule 31.01.2018    source источник


Ответы (3)


Похоже, вы на самом деле не хотите, чтобы каждый элемент верхнего уровня вел себя как обычный TreeViewItem. В таком случае, почему бы не переместить элементы верхнего уровня за пределы TreeView?

По сути, у вас будет ItemsControl элементов верхнего уровня, где шаблон элемента действует как Expander, содержащий TreeView элементов под ним. Вы можете стилизовать элементы верхнего уровня, чтобы они выглядели так, как вам нравится.

Недостатком будет то, что деревья под элементами верхнего уровня будут виртуализированы по отдельности, а не целиком. То есть они не будут делиться контейнерами. Если у вас нет тонны элементов верхнего уровня, это, вероятно, не будет иметь большого значения.

Пример:

<ItemsControl xmlns:s="clr-namespace:System;assembly=mscorlib"
              ItemsSource="{Binding Types}">
  <ItemsControl.Resources>
    <ControlTemplate x:Key="ExpanderButtonTemplate" TargetType="ToggleButton">
      <Border Background="Transparent" Padding="3,2">
        <ContentPresenter />
      </Border>
    </ControlTemplate>
    <Style TargetType="Expander">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="Expander">
            <DockPanel LastChildFill="True">
              <ToggleButton DockPanel.Dock="Top"
                            IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
                            Template="{StaticResource ExpanderButtonTemplate}">
                <ContentPresenter ContentSource="Header" />
              </ToggleButton>
              <Border>
                <ContentPresenter x:Name="contentSite" Visibility="Collapsed" />
              </Border>
            </DockPanel>
            <ControlTemplate.Triggers>
              <Trigger Property="IsExpanded" Value="True">
                <Setter TargetName="contentSite" Property="Visibility" Value="Visible" />
              </Trigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </ItemsControl.Resources>
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <Expander Header="{Binding Name}">
        <TreeView ItemsSource="{Binding SubTypes}" BorderThickness="0" Padding="0">
          <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="{x:Type models:Type}"
                                      ItemsSource="{Binding SubTypes}">
              <TextBlock Text="{Binding Name}"/>
              <HierarchicalDataTemplate.ItemTemplate>
                <DataTemplate DataType="{x:Type models:SubType}">
                  <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
              </HierarchicalDataTemplate.ItemTemplate>
            </HierarchicalDataTemplate>
          </TreeView.ItemTemplate>
        </TreeView>
      </Expander>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

Нажмите на один из элементов верхнего уровня, чтобы развернуть дерево под ним.

Скриншот

person Mike Strobel    schedule 31.01.2018
comment
Вполне возможно - я новичок в XAML, но будет ли он по-прежнему действовать как переключатель для отображения/скрытия подэлементов? Типы содержат 3 модели: тип, и каждый из них содержит неизвестное количество подтипов. - person Agony; 31.01.2018
comment
Конечно. Я добавил простое доказательство концепции для примера. - person Mike Strobel; 31.01.2018
comment
Похоже, что это охватывает и решает проблему как со стилем, так и с кликами — теперь мне просто нужно выяснить и воспроизвести материальную тему, которую использует древовидная структура. - person Agony; 31.01.2018

Вы можете сохранить TreeView и установить свойства или применить стиль к своим элементам верхнего уровня (при условии, что ваш TreeView не вложен в другой TreeView), выполнив поиск TreeViewItem в логической иерархии:

<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="{x:Null}">
  <Setter Property="Background" Value="LightYellow" />
</DataTrigger>

Для элементов верхнего уровня эта привязка отправляет спам-предупреждения для отладки вывода, но их можно спокойно игнорировать. Более сложной версией этого трюка может быть создание наследуемого прикрепленного свойства TreeViewItemLevel, которое будет установлено равным нулю для TreeView и на единицу больше, чем унаследованное значение для TreeViewItems.

person Anton Tykhyy    schedule 27.09.2018

Установка DataTrigger с привязкой к относительному источнику работает, но это приведет к ошибкам привязки.

<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="{x:Null}">

Установив FallbackValue из x:Null, вы получите не ошибки привязки, а предупреждения.

<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, FallbackValue={x:Null}}" Value="{x:Null}">

Альтернативный подход, который не приведет ни к ошибкам, ни к предупреждениям, заключается в создании преобразователя значений, который, по сути, делает то же самое, что и привязка RelativeSource, но также обрабатывает ошибки. Он возвращает true для любого переданного объекта зависимости, если он имеет предка типа TreeViewItem, false в противном случае.

public class IsRootTreeViewItemConverter : IValueConverter
{
   public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
   {
      return value is null ? Binding.DoNothing : !HasTreeViewItemAncestor((DependencyObject)value);
   }

   public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
   {
      throw new InvalidOperationException();
   }

   private static bool HasTreeViewItemAncestor(DependencyObject child)
   {
      var parent = VisualTreeHelper.GetParent(child);

      return parent switch
      {
         null => false,
         TreeViewItem _ => true,
         _ => HasTreeViewItemAncestor(parent)
      };
   }
}

В вашем триггере стиля TreeViewItem вы можете использовать конвертер, привязавшись к самому элементу с помощью Self.

<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource IsRootTreeViewItemConverter}}" Value="True">

Этот подход работает как для статически назначенных привязок TreeViewItems, так и для ItemsSource.

person thatguy    schedule 17.12.2020