Множественное выделение в дереве используя M-V-VM
1 июня 2011 - 19:57
Стандартный WPF-ный TreeView не поддерживает возможность множественного выделения. Поэтому в том случае, когда это необходимо приходится либо пользоваться сторонними коммерческими контролами (которые, как правило, весьма невысокого качества), либо реализовывать свой вариант дерева, что является достаточно нетривиальной задачей. Но если вы в своём проекте используете шаблон проектирования Model - View - ViewModel (M-V-VM), то реализовать множественное выделение можно значительно более простым путём.
Итак, для начала, я создал простейшую модель для элемента дерева, реализующую интерфейс INotifyPropertyChanged и имеющую три свойства: заголовок (Header), флаг состояния выделен/не выделен (IsSelected) и коллекцию детей (Children):
Я использовал простую модель для примера, в рабочем приложении, как правило, модель более нагруженная.
Потом я реализовал ViewModel для дерева, она содержит:Для данного примера метод выделения реализован простейшим способом: если нажат Ctrl, то элемент добавляется в список выделенных, а если нет, то единственным выделенным становится данный элемент. В реальном приложении сюда можно добавить снятие выделения по Ctrl-у, поддержка нажатия Shift-а, выделение детей при выделении родителя, возможность выделения только какого-то одного типа элементов дерева (в том случае, если в вашем дереве находятся элементы различных типов) и многое другое, в зависимости от конкретных потребностей.
Потом я реализовал DataTemplate для элемента дерева:
Темплейт представляет из себя два Border-а, в которые заключён TextBlock. Border-ы используются для подсветки при выделении и наведении мышью, а TextBlock прибинден к Header-у модели.
После я реализовал вызов метода Select у ViewModel-и при изменении выделенного элемента дерева:
Конечно в том случае, если Вы используете какой-либо M-V-VM фреймворк, гораздо правильней было бы реализовать Select командой и вызывать её через behavior EventToCommand, но чтоб не загромождать пример я использовал такой вариант.
Итак, если сейчас запустить приложение, то можно увидеть, что кроме красивой рамочки из DataTemplate-а последний выделенный элемент ещё подсвечивается системным цветом подсветки. Это можно обойти реализовав свой стиль для TreeViewItem-а, в ControlTemplate-е которого не будет реализована подсветка по IsSelected. Но я пойду более простым путём и просто заменю цвет подсветки для данного элемента на белый:
Кроме визуальной проблемы с выделением TreeViewItem-а, которую я обошёл достаточно простым способом, есть более сложная проблема: событие SelectionItemChanged приходит при переходе фокуса к TreeViewItem-у, а не по клику, и, соответственно, если я выделю с Ctrl-ом несколько элементов, а потом без Ctrl-а кликаю на последний выделенный элемент, то ничего не происходит и все выделенные элементы так и останутся выделенными, тогда как ожидалось, что только кликнутый элемент будет выделен. Эту проблему можно решить различными способами. Например, в OnSelectionItemChanged переводить фокус ввода с элемента, этот способ достаточно прост, но при нём теряется возможность клавиатурной навигации по дереву. Можно использовать вместо события SelectionItemChanged событие TreeView.Select, которое с помощью AttachedProperty слать при клике на элемент, но в этом случае нужно учитывать, что это же событие будет слаться при получении элементом фокуса. На данный момент ни один из данных способов не устраивает меня в полной мере, поэтому я не буду выкладывать их реализацию. Так же теоретически возможен ряд других способов, но их я не пробовал на практике, поэтому описывать не буду. Если найдётся способ в полной мере удовлетворяющий моим требованиям, то я опишу его позже.
Скачать исходники:
Итак, для начала, я создал простейшую модель для элемента дерева, реализующую интерфейс INotifyPropertyChanged и имеющую три свойства: заголовок (Header), флаг состояния выделен/не выделен (IsSelected) и коллекцию детей (Children):
public class ItemModel: INotifyPropertyChanged { #region Implementation of INotifyPropertyChanged /// <summary> /// Occurs when a property value changes. /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Raise PropertyChanged event /// </summary> /// <param name="propertyName">Name of changed property</param> private void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion #region Properties #region Header /// <summary> /// ItemModel header /// </summary> private string header; /// <summary> /// Gets or sets ItemModel header /// </summary> public string Header { get { return header; } set { if (header != value) { header = value; RaisePropertyChanged("Header"); } } } #endregion #region Children /// <summary> /// ItemModel children collection /// </summary> private ObservableCollection<ItemModel> children; /// <summary> /// Gets ItemModel children collection /// </summary> public ObservableCollection<ItemModel> Children { get { if(children == null) children = new ObservableCollection<ItemModel>(); return children; } } #endregion #region IsSelected /// <summary> /// Value indicating whether a ItemModel is selecting /// </summary> private bool isSelected; /// <summary> /// Gets or sets a value indicating whether a ItemModel is selecting /// </summary> public bool IsSelected { get { return isSelected; } set { if (isSelected != value) { isSelected = value; RaisePropertyChanged("IsSelected"); } } } #endregion #endregion }
Потом я реализовал ViewModel для дерева, она содержит:
- Коллекцию элементов дерева:
#region Items /// <summary> /// Tree items collection /// </summary> private ObservableCollection<ItemModel> items; /// <summary> /// Gets tree items collection /// </summary> public ObservableCollection<ItemModel> Items { get { return items ?? (items = new ObservableCollection<ItemModel>()); } } #endregion
- Список выделенных элементов:
#region Fields /// <summary> /// Selected items collection /// </summary> private readonly List<ItemModel> selectedItems = new List<ItemModel>(); #endregion
- И метод для выделения элемента дерева:
/// <summary> /// Select item /// </summary> /// <param name="item">Item to select</param> public void Select(ItemModel item) { if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) { // If Ctrl is not pressed then clear selection foreach (ItemModel selectedItem in selectedItems) { selectedItem.IsSelected = false; } selectedItems.Clear(); } if (item != null) { if (!selectedItems.Contains(item)) { // Is item is not selected then select item item.IsSelected = true; selectedItems.Add(item); } } }
Потом я реализовал DataTemplate для элемента дерева:
<HierarchicalDataTemplate DataType="{x:Type Models:ItemModel}" ItemsSource="{Binding Children}"> <Border BorderThickness="1" CornerRadius="1" x:Name="outterBorder" Background="Transparent"> <Border BorderThickness="1" CornerRadius="2" x:Name="innerBorder"> <TextBlock Margin="5,3,10,3" VerticalAlignment="Center" Text="{Binding Header}" Foreground="Black"/> </Border> </Border> <HierarchicalDataTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="BorderBrush" TargetName="outterBorder" Value="#FFB8D6FB"/> <Setter Property="BorderBrush" TargetName="innerBorder"> <Setter.Value> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="#FFFCFDFE" Offset="0"/> <GradientStop Color="#FFF2F7FE" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="Background" TargetName="innerBorder"> <Setter.Value> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="#FFFAFBFD" Offset="0"/> <GradientStop Color="#FFEBF3FD" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> </Trigger> <DataTrigger Binding="{Binding IsSelected}" Value="True"> <Setter Property="BorderBrush" TargetName="outterBorder" Value="#FF7DA2CE"/> <Setter Property="BorderBrush" TargetName="innerBorder"> <Setter.Value> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="#FFEBF4FD" Offset="0"/> <GradientStop Color="#FFDBEAFD" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="Background" TargetName="innerBorder"> <Setter.Value> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientStop Color="#FFDCEBFC" Offset="0"/> <GradientStop Color="#FFC1DBFC" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> </DataTrigger> </HierarchicalDataTemplate.Triggers> </HierarchicalDataTemplate>
После я реализовал вызов метода Select у ViewModel-и при изменении выделенного элемента дерева:
private void OnSelectionItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { TreeViewModel viewModel = DataContext as TreeViewModel; if (viewModel != null && e.NewValue != null) { viewModel.Select(e.NewValue as ItemModel); } }
Итак, если сейчас запустить приложение, то можно увидеть, что кроме красивой рамочки из DataTemplate-а последний выделенный элемент ещё подсвечивается системным цветом подсветки. Это можно обойти реализовав свой стиль для TreeViewItem-а, в ControlTemplate-е которого не будет реализована подсветка по IsSelected. Но я пойду более простым путём и просто заменю цвет подсветки для данного элемента на белый:
<Style TargetType="TreeViewItem"> <Style.Resources> <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White"/> </Style.Resources> </Style>
Скачать исходники: