/src/MahApps.Metro/Controls/Helper/MultiSelectorHelper.cs

https://github.com/MahApps/MahApps.Metro · C# · 324 lines · 254 code · 29 blank · 41 comment · 30 complexity · b5348a9c495325de7d75422c49cf8aa2 MD5 · raw file

  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. using System;
  5. using System.Collections;
  6. using System.Collections.ObjectModel;
  7. using System.Collections.Specialized;
  8. using System.ComponentModel;
  9. using System.Windows;
  10. using System.Windows.Controls;
  11. using System.Windows.Controls.Primitives;
  12. namespace MahApps.Metro.Controls
  13. {
  14. /// <summary>
  15. /// Defines a helper class for SelectedItems binding on <see cref="ListBox"/>, <see cref="MultiSelector"/> or <see cref="MultiSelectionComboBox"/> controls.
  16. /// </summary>
  17. public static class MultiSelectorHelper
  18. {
  19. public static readonly DependencyProperty SelectedItemsProperty
  20. = DependencyProperty.RegisterAttached(
  21. "SelectedItems",
  22. typeof(IList),
  23. typeof(MultiSelectorHelper),
  24. new FrameworkPropertyMetadata(null, OnSelectedItemsChanged));
  25. /// <summary>
  26. /// Handles disposal and creation of old and new bindings
  27. /// </summary>
  28. private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  29. {
  30. if (d is not (ListBox or MultiSelector or MultiSelectionComboBox))
  31. {
  32. throw new ArgumentException("The property 'SelectedItems' may only be set on ListBox, MultiSelector or MultiSelectionComboBox elements.");
  33. }
  34. if (e.OldValue != e.NewValue)
  35. {
  36. GetSelectedItemBinding(d)?.UnBind();
  37. if (e.NewValue is IList newList)
  38. {
  39. var multiSelectorBinding = new MultiSelectorBinding((Selector)d, newList);
  40. SetSelectedItemBinding(d, multiSelectorBinding);
  41. multiSelectorBinding.Bind();
  42. }
  43. else
  44. {
  45. SetSelectedItemBinding(d, null);
  46. }
  47. }
  48. }
  49. /// <summary>
  50. /// Gets the selected items property binding
  51. /// </summary>
  52. [Category(AppName.MahApps)]
  53. [AttachedPropertyBrowsableForType(typeof(ListBox))]
  54. [AttachedPropertyBrowsableForType(typeof(MultiSelector))]
  55. [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))]
  56. public static IList? GetSelectedItems(DependencyObject element)
  57. {
  58. return (IList?)element.GetValue(SelectedItemsProperty);
  59. }
  60. /// <summary>
  61. /// Sets the selected items property binding
  62. /// </summary>
  63. [Category(AppName.MahApps)]
  64. [AttachedPropertyBrowsableForType(typeof(ListBox))]
  65. [AttachedPropertyBrowsableForType(typeof(MultiSelector))]
  66. [AttachedPropertyBrowsableForType(typeof(MultiSelectionComboBox))]
  67. public static void SetSelectedItems(DependencyObject element, IList? value)
  68. {
  69. element.SetValue(SelectedItemsProperty, value);
  70. }
  71. private static readonly DependencyProperty SelectedItemBindingProperty
  72. = DependencyProperty.RegisterAttached(
  73. "SelectedItemBinding",
  74. typeof(MultiSelectorBinding),
  75. typeof(MultiSelectorHelper));
  76. private static MultiSelectorBinding? GetSelectedItemBinding(DependencyObject element)
  77. {
  78. return (MultiSelectorBinding?)element.GetValue(SelectedItemBindingProperty);
  79. }
  80. private static void SetSelectedItemBinding(DependencyObject element, MultiSelectorBinding? value)
  81. {
  82. element.SetValue(SelectedItemBindingProperty, value);
  83. }
  84. /// <summary>
  85. /// Defines a binding between <see cref="Selector"/> and collection.
  86. /// </summary>
  87. private class MultiSelectorBinding
  88. {
  89. private readonly Selector selector;
  90. private readonly IList collection;
  91. /// <summary>
  92. /// Creates an instance of <see cref="MultiSelectorBinding"/>
  93. /// </summary>
  94. /// <param name="selector">The selector of this binding</param>
  95. /// <param name="collection">The bound collection</param>
  96. public MultiSelectorBinding(Selector selector, IList collection)
  97. {
  98. this.selector = selector;
  99. this.collection = collection;
  100. var selectedItems = GetSelectedItems(selector);
  101. if (selectedItems is not null)
  102. {
  103. selectedItems.Clear();
  104. foreach (var newItem in collection)
  105. {
  106. selectedItems.Add(newItem);
  107. }
  108. }
  109. }
  110. /// <summary>
  111. /// Registers the event handlers for selector and collection changes
  112. /// </summary>
  113. public void Bind()
  114. {
  115. // prevent multiple event registration
  116. this.UnBind();
  117. this.selector.SelectionChanged += this.OnSelectionChanged;
  118. if (this.collection is INotifyCollectionChanged notifyCollection)
  119. {
  120. notifyCollection.CollectionChanged += this.OnCollectionChanged;
  121. }
  122. }
  123. /// <summary>
  124. /// Clear the event handlers for selector and collection changes
  125. /// </summary>
  126. public void UnBind()
  127. {
  128. this.selector.SelectionChanged -= this.OnSelectionChanged;
  129. if (this.collection is INotifyCollectionChanged notifyCollection)
  130. {
  131. notifyCollection.CollectionChanged -= this.OnCollectionChanged;
  132. }
  133. }
  134. /// <summary>
  135. /// Updates the collection with changes made in the selector
  136. /// </summary>
  137. private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  138. {
  139. var notifyCollection = this.collection as INotifyCollectionChanged;
  140. if (notifyCollection is not null)
  141. {
  142. notifyCollection.CollectionChanged -= this.OnCollectionChanged;
  143. }
  144. try
  145. {
  146. var removedItems = e.RemovedItems;
  147. if (removedItems is not null)
  148. {
  149. foreach (var oldItem in removedItems)
  150. {
  151. this.collection.Remove(oldItem);
  152. }
  153. }
  154. var addedItems = e.AddedItems;
  155. if (addedItems is not null)
  156. {
  157. foreach (var newItem in addedItems)
  158. {
  159. this.collection.Add(newItem);
  160. }
  161. }
  162. }
  163. finally
  164. {
  165. if (notifyCollection is not null)
  166. {
  167. notifyCollection.CollectionChanged += this.OnCollectionChanged;
  168. }
  169. }
  170. }
  171. /// <summary>
  172. /// Updates the selector with changes made in the collection
  173. /// </summary>
  174. #if NET5_0_OR_GREATER
  175. private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
  176. #else
  177. private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  178. #endif
  179. {
  180. this.selector.SelectionChanged -= this.OnSelectionChanged;
  181. try
  182. {
  183. var selectedItems = GetSelectedItems(this.selector);
  184. if (selectedItems is not null)
  185. {
  186. switch (e.Action)
  187. {
  188. case NotifyCollectionChangedAction.Add:
  189. if (e.NewItems is not null)
  190. {
  191. foreach (var item in e.NewItems)
  192. {
  193. selectedItems.Add(item);
  194. }
  195. }
  196. break;
  197. case NotifyCollectionChangedAction.Remove:
  198. if (e.OldItems is not null)
  199. {
  200. foreach (var item in e.OldItems)
  201. {
  202. selectedItems.Remove(item);
  203. }
  204. }
  205. break;
  206. case NotifyCollectionChangedAction.Move:
  207. Move(selectedItems, e);
  208. break;
  209. case NotifyCollectionChangedAction.Replace:
  210. RemoveItems(selectedItems, e);
  211. AddItems(selectedItems, e);
  212. break;
  213. case NotifyCollectionChangedAction.Reset:
  214. selectedItems.Clear();
  215. break;
  216. }
  217. }
  218. }
  219. finally
  220. {
  221. this.selector.SelectionChanged += this.OnSelectionChanged;
  222. }
  223. }
  224. private static void RemoveItems(IList? selectedItems, NotifyCollectionChangedEventArgs e)
  225. {
  226. if (e.OldItems is not null && selectedItems is not null)
  227. {
  228. foreach (var item in e.OldItems)
  229. {
  230. selectedItems.Remove(item);
  231. }
  232. }
  233. }
  234. private static void AddItems(IList? selectedItems, NotifyCollectionChangedEventArgs e)
  235. {
  236. if (e.NewItems is not null && selectedItems is not null)
  237. {
  238. for (var i = 0; i < e.NewItems.Count; i++)
  239. {
  240. var insertionPoint = e.NewStartingIndex + i;
  241. if (insertionPoint > selectedItems.Count)
  242. {
  243. selectedItems.Add(e.NewItems[i]);
  244. }
  245. else
  246. {
  247. selectedItems.Insert(insertionPoint, e.NewItems[i]);
  248. }
  249. }
  250. }
  251. }
  252. /// <summary>
  253. /// Checks if the given collection is a ObservableCollection&lt;&gt;
  254. /// </summary>
  255. /// <param name="collection">The collection to test.</param>
  256. /// <returns>True if the collection is a ObservableCollection&lt;&gt;</returns>
  257. private static bool IsObservableCollection(IList? collection)
  258. {
  259. return collection is not null
  260. && collection.GetType().IsGenericType
  261. && collection.GetType().GetGenericTypeDefinition() == typeof(ObservableCollection<>);
  262. }
  263. private static void Move(IList? selectedItems, NotifyCollectionChangedEventArgs e)
  264. {
  265. if (selectedItems is not null && e.OldStartingIndex != e.NewStartingIndex)
  266. {
  267. if (selectedItems is ObservableCollection<object> observableCollection)
  268. {
  269. observableCollection.Move(e.OldStartingIndex, e.NewStartingIndex);
  270. }
  271. else if (IsObservableCollection(selectedItems))
  272. {
  273. var method = selectedItems.GetType().GetMethod("Move", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
  274. _ = method?.Invoke(selectedItems, new object[] { e.OldStartingIndex, e.NewStartingIndex });
  275. }
  276. else
  277. {
  278. RemoveItems(selectedItems, e);
  279. AddItems(selectedItems, e);
  280. }
  281. }
  282. }
  283. private static IList? GetSelectedItems(Selector selector)
  284. {
  285. return selector switch
  286. {
  287. ListBox listbox => listbox.SelectedItems,
  288. MultiSelector multiSelector => multiSelector.SelectedItems,
  289. MultiSelectionComboBox multiSelectionComboBox => multiSelectionComboBox.SelectedItems,
  290. _ => null
  291. };
  292. }
  293. }
  294. }
  295. }