/Julmar.Wpf.Helpers/Julmar.Wpf.Behaviors/Interactivity/MultiSelectTreeViewBehavior.cs

# · C# · 290 lines · 162 code · 30 blank · 98 comment · 41 complexity · 970030b73553b262c25f748824e4e656 MD5 · raw file

  1. using System.Collections.Generic;
  2. using System.Diagnostics;
  3. using System.Linq;
  4. using System.Windows;
  5. using System.Windows.Controls;
  6. using System.Windows.Input;
  7. using System.Collections;
  8. using System.Windows.Interactivity;
  9. using System.Windows.Media;
  10. namespace JulMar.Windows.Interactivity
  11. {
  12. /// <summary>
  13. /// Behavior to support multi-select in a traditional WPF TreeView control.
  14. /// </summary>
  15. /// <example>
  16. /// <![CDATA[
  17. /// <TreeView ...>
  18. /// <i:Interaction.Behaviors>
  19. /// <b:MultiSelectTreeViewBehavior SelectedItems="{Binding SelectedItems}" />
  20. /// </i:Interaction.Behaviors>
  21. /// </TreeView>
  22. /// ]]>
  23. /// </example>
  24. public class MultiSelectTreeViewBehavior : Behavior<TreeView>
  25. {
  26. private TreeViewItem _anchorItem;
  27. /// <summary>
  28. /// Selected Items collection
  29. /// </summary>
  30. public static readonly DependencyProperty SelectedItemsProperty =
  31. DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiSelectTreeViewBehavior), new PropertyMetadata(null));
  32. /// <summary>
  33. /// Selected Items collection (intended to be data bound)
  34. /// </summary>
  35. public IList SelectedItems
  36. {
  37. get { return (IList)GetValue(SelectedItemsProperty); }
  38. set { SetValue(SelectedItemsProperty, value); }
  39. }
  40. /// <summary>
  41. /// Selection attached property - can be used for styling TreeViewItem elements.
  42. /// </summary>
  43. public static readonly DependencyProperty IsSelectedProperty =
  44. DependencyProperty.RegisterAttached("IsSelected", typeof (bool), typeof (MultiSelectTreeViewBehavior),
  45. new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedChanged));
  46. /// <summary>
  47. /// Returns whether the TreeViewItem is selected
  48. /// </summary>
  49. /// <param name="obj"></param>
  50. /// <returns></returns>
  51. public static bool GetIsSelected(DependencyObject obj)
  52. {
  53. return (bool)obj.GetValue(IsSelectedProperty);
  54. }
  55. /// <summary>
  56. /// Changes the selection state of the TreeViewItem.
  57. /// </summary>
  58. /// <param name="obj"></param>
  59. /// <param name="value"></param>
  60. public static void SetIsSelected(DependencyObject obj, bool value)
  61. {
  62. obj.SetValue(IsSelectedProperty, value);
  63. }
  64. /// <summary>
  65. /// Called after the behavior is attached to an AssociatedObject.
  66. /// </summary>
  67. /// <remarks>
  68. /// Override this to hook up functionality to the AssociatedObject.
  69. /// </remarks>
  70. protected override void OnAttached()
  71. {
  72. AssociatedObject.AddHandler(TreeViewItem.UnselectedEvent, new RoutedEventHandler(OnTreeViewItemUnselected), true);
  73. AssociatedObject.AddHandler(TreeViewItem.SelectedEvent, new RoutedEventHandler(OnTreeViewItemSelected), true);
  74. AssociatedObject.AddHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnKeyDown), true);
  75. base.OnAttached();
  76. }
  77. /// <summary>
  78. /// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
  79. /// </summary>
  80. /// <remarks>
  81. /// Override this to unhook functionality from the AssociatedObject.
  82. /// </remarks>
  83. protected override void OnDetaching()
  84. {
  85. AssociatedObject.RemoveHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnKeyDown));
  86. AssociatedObject.RemoveHandler(TreeViewItem.UnselectedEvent, new RoutedEventHandler(OnTreeViewItemUnselected));
  87. AssociatedObject.RemoveHandler(TreeViewItem.SelectedEvent, new RoutedEventHandler(OnTreeViewItemSelected));
  88. base.OnDetaching();
  89. }
  90. /// <summary>
  91. /// This is called when the a tree item is unselected.
  92. /// </summary>
  93. /// <param name="sender"></param>
  94. /// <param name="e"></param>
  95. private void OnTreeViewItemUnselected(object sender, RoutedEventArgs e)
  96. {
  97. if ((Keyboard.Modifiers & (ModifierKeys.Control | ModifierKeys.Shift)) == ModifierKeys.None)
  98. {
  99. SetIsSelected((TreeViewItem)e.OriginalSource, false);
  100. }
  101. }
  102. /// <summary>
  103. /// This is called when the tree item is selected.
  104. /// </summary>
  105. /// <param name="sender"></param>
  106. /// <param name="e"></param>
  107. private void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
  108. {
  109. TreeViewItem item = (TreeViewItem) e.OriginalSource;
  110. // Look for a disconnected item. We can get this if the data source changes underneath us,
  111. // in which case we want to ignore this selection. This is actually a bug in WPF4, see:
  112. // http://connect.microsoft.com/VisualStudio/feedback/details/619658/wpf-virtualized-control-disconnecteditem-reference-when-datacontext-switch
  113. // Unfortunately, there's no way to reliably see this so we just look for the magic string here.
  114. // in WPF 4.5 they have a new static property which exposes this object off the BindingExpression.
  115. //
  116. // Could also check against this object, but not any safer than the string really.
  117. //var disconnectedItemSingleton = typeof(System.Windows.Data.BindingExpressionBase).GetField("DisconnectedItem", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
  118. if (item.DataContext != null && item.DataContext.ToString() == "{DisconnectedItem}")
  119. return;
  120. if ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control)) !=
  121. (ModifierKeys.Shift | ModifierKeys.Control))
  122. {
  123. switch ((Keyboard.Modifiers & ModifierKeys.Control))
  124. {
  125. case ModifierKeys.Control:
  126. ToggleSelect(item);
  127. break;
  128. default:
  129. if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
  130. AnchorMultiSelect(item);
  131. else
  132. SingleSelect(item);
  133. break;
  134. }
  135. }
  136. }
  137. /// <summary>
  138. /// This method locates the TreeView parent for a given TreeViewItem.
  139. /// </summary>
  140. /// <param name="item">Item</param>
  141. /// <returns>Parent</returns>
  142. static TreeView GetTree(TreeViewItem item)
  143. {
  144. FrameworkElement currentItem = item;
  145. while (!(VisualTreeHelper.GetParent(currentItem) is TreeView))
  146. currentItem = (FrameworkElement) VisualTreeHelper.GetParent(currentItem);
  147. return (TreeView) VisualTreeHelper.GetParent(currentItem);
  148. }
  149. /// <summary>
  150. /// This method is invoked when the attached selection is changed.
  151. /// </summary>
  152. /// <param name="sender"></param>
  153. /// <param name="args"></param>
  154. static void OnSelectedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
  155. {
  156. TreeViewItem item = (TreeViewItem)sender;
  157. TreeView tree = GetTree(item);
  158. Debug.Assert(tree != null);
  159. var msb = Interaction.GetBehaviors(tree).Single(b => b.GetType() == typeof (MultiSelectTreeViewBehavior)) as MultiSelectTreeViewBehavior;
  160. if (msb != null)
  161. {
  162. if (msb.SelectedItems != null)
  163. {
  164. var isSelected = GetIsSelected(item);
  165. if (isSelected)
  166. msb.SelectedItems.Add(item.DataContext ?? item);
  167. else
  168. msb.SelectedItems.Remove(item.DataContext ?? item);
  169. }
  170. }
  171. }
  172. /// <summary>
  173. /// This method is invoked when you press a key while the TreeView has focus.
  174. /// </summary>
  175. /// <param name="sender"></param>
  176. /// <param name="e"></param>
  177. private void OnKeyDown(object sender, KeyEventArgs e)
  178. {
  179. TreeView tree = (TreeView)sender;
  180. Debug.Assert(tree == AssociatedObject);
  181. // If you press CTRL+A, do a full select.
  182. if (e.Key == Key.A && e.KeyboardDevice.Modifiers == ModifierKeys.Control)
  183. {
  184. GetExpandedTreeViewItems(tree)
  185. .ToList()
  186. .ForEach(tvi => SetIsSelected(tvi, true));
  187. e.Handled = true;
  188. }
  189. }
  190. /// <summary>
  191. /// Returns the entire TreeView set of nodes. Unfortunately, in WPF the TreeView
  192. /// doesn't manage a selection state globally - it's singular, and compartmentalized into
  193. /// each ItemsControl expansion. This is a heavy-handed approach, but for reasonably sized
  194. /// tree views it should be ok.
  195. /// </summary>
  196. /// <returns></returns>
  197. private IEnumerable<TreeViewItem> GetExpandedTreeViewItems(ItemsControl container = null)
  198. {
  199. if (container == null)
  200. container = AssociatedObject;
  201. for (int i = 0; i < container.Items.Count; i++)
  202. {
  203. var item = container.ItemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
  204. if (item == null)
  205. continue;
  206. // Hand back this child
  207. yield return item;
  208. // Hand back all the children
  209. foreach (var subItem in GetExpandedTreeViewItems(item))
  210. yield return subItem;
  211. }
  212. }
  213. /// <summary>
  214. /// This is used to perform a multi-select operation using an anchor position.
  215. /// </summary>
  216. /// <param name="newItem"></param>
  217. private void AnchorMultiSelect(TreeViewItem newItem)
  218. {
  219. if (_anchorItem == null)
  220. {
  221. var selectedItems = GetExpandedTreeViewItems().Where(GetIsSelected).ToList();
  222. _anchorItem = (selectedItems.Count > 0
  223. ? selectedItems[selectedItems.Count - 1]
  224. : GetExpandedTreeViewItems().FirstOrDefault());
  225. if (_anchorItem == null)
  226. return;
  227. }
  228. var anchor = _anchorItem;
  229. var items = GetExpandedTreeViewItems();
  230. bool inSelectionRange = false;
  231. foreach (var item in items)
  232. {
  233. bool isEdge = item == anchor || item == newItem;
  234. if (isEdge)
  235. inSelectionRange = !inSelectionRange;
  236. SetIsSelected(item, (inSelectionRange || isEdge));
  237. }
  238. }
  239. /// <summary>
  240. /// This performs a single-select operation
  241. /// </summary>
  242. /// <param name="item"></param>
  243. private void SingleSelect(TreeViewItem item)
  244. {
  245. foreach (TreeViewItem selectedItem in GetExpandedTreeViewItems().Where(ti => ti != null))
  246. SetIsSelected(selectedItem, selectedItem == item);
  247. _anchorItem = item;
  248. }
  249. /// <summary>
  250. /// This toggles the selection
  251. /// </summary>
  252. /// <param name="item"></param>
  253. private void ToggleSelect(TreeViewItem item)
  254. {
  255. SetIsSelected(item, !GetIsSelected(item));
  256. if (_anchorItem == null)
  257. _anchorItem = item;
  258. }
  259. }
  260. }