SelectingItemsControl.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Collections.Specialized;
  7. using System.Linq;
  8. using Avalonia.Collections;
  9. using Avalonia.Controls.Generators;
  10. using Avalonia.Data;
  11. using Avalonia.Input;
  12. using Avalonia.Input.Platform;
  13. using Avalonia.Interactivity;
  14. using Avalonia.Styling;
  15. using Avalonia.VisualTree;
  16. namespace Avalonia.Controls.Primitives
  17. {
  18. /// <summary>
  19. /// An <see cref="ItemsControl"/> that maintains a selection.
  20. /// </summary>
  21. /// <remarks>
  22. /// <para>
  23. /// <see cref="SelectingItemsControl"/> provides a base class for <see cref="ItemsControl"/>s
  24. /// that maintain a selection (single or multiple). By default only its
  25. /// <see cref="SelectedIndex"/> and <see cref="SelectedItem"/> properties are visible; the
  26. /// current multiple selection <see cref="SelectedItems"/> together with the
  27. /// <see cref="SelectionMode"/> properties are protected, however a derived class can expose
  28. /// these if it wishes to support multiple selection.
  29. /// </para>
  30. /// <para>
  31. /// <see cref="SelectingItemsControl"/> maintains a selection respecting the current
  32. /// <see cref="SelectionMode"/> but it does not react to user input; this must be handled in a
  33. /// derived class. It does, however, respond to <see cref="IsSelectedChangedEvent"/> events
  34. /// from items and updates the selection accordingly.
  35. /// </para>
  36. /// </remarks>
  37. public class SelectingItemsControl : ItemsControl
  38. {
  39. /// <summary>
  40. /// Defines the <see cref="AutoScrollToSelectedItem"/> property.
  41. /// </summary>
  42. public static readonly StyledProperty<bool> AutoScrollToSelectedItemProperty =
  43. AvaloniaProperty.Register<SelectingItemsControl, bool>(
  44. nameof(AutoScrollToSelectedItem),
  45. defaultValue: true);
  46. /// <summary>
  47. /// Defines the <see cref="SelectedIndex"/> property.
  48. /// </summary>
  49. public static readonly DirectProperty<SelectingItemsControl, int> SelectedIndexProperty =
  50. AvaloniaProperty.RegisterDirect<SelectingItemsControl, int>(
  51. nameof(SelectedIndex),
  52. o => o.SelectedIndex,
  53. (o, v) => o.SelectedIndex = v,
  54. unsetValue: -1,
  55. defaultBindingMode: BindingMode.TwoWay);
  56. /// <summary>
  57. /// Defines the <see cref="SelectedItem"/> property.
  58. /// </summary>
  59. public static readonly DirectProperty<SelectingItemsControl, object> SelectedItemProperty =
  60. AvaloniaProperty.RegisterDirect<SelectingItemsControl, object>(
  61. nameof(SelectedItem),
  62. o => o.SelectedItem,
  63. (o, v) => o.SelectedItem = v,
  64. defaultBindingMode: BindingMode.TwoWay);
  65. /// <summary>
  66. /// Defines the <see cref="SelectedItems"/> property.
  67. /// </summary>
  68. protected static readonly DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
  69. AvaloniaProperty.RegisterDirect<SelectingItemsControl, IList>(
  70. nameof(SelectedItems),
  71. o => o.SelectedItems,
  72. (o, v) => o.SelectedItems = v);
  73. /// <summary>
  74. /// Defines the <see cref="SelectionMode"/> property.
  75. /// </summary>
  76. protected static readonly StyledProperty<SelectionMode> SelectionModeProperty =
  77. AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
  78. nameof(SelectionMode));
  79. /// <summary>
  80. /// Event that should be raised by items that implement <see cref="ISelectable"/> to
  81. /// notify the parent <see cref="SelectingItemsControl"/> that their selection state
  82. /// has changed.
  83. /// </summary>
  84. public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
  85. RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>(
  86. "IsSelectedChanged",
  87. RoutingStrategies.Bubble);
  88. /// <summary>
  89. /// Defines the <see cref="SelectionChanged"/> event.
  90. /// </summary>
  91. public static readonly RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
  92. RoutedEvent.Register<SelectingItemsControl, SelectionChangedEventArgs>(
  93. "SelectionChanged",
  94. RoutingStrategies.Bubble);
  95. private static readonly IList Empty = Array.Empty<object>();
  96. private int _selectedIndex = -1;
  97. private object _selectedItem;
  98. private IList _selectedItems;
  99. private bool _ignoreContainerSelectionChanged;
  100. private bool _syncingSelectedItems;
  101. private int _updateCount;
  102. private int _updateSelectedIndex;
  103. private IList _updateSelectedItems;
  104. /// <summary>
  105. /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
  106. /// </summary>
  107. static SelectingItemsControl()
  108. {
  109. IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ContainerSelectionChanged);
  110. }
  111. /// <summary>
  112. /// Occurs when the control's selection changes.
  113. /// </summary>
  114. public event EventHandler<SelectionChangedEventArgs> SelectionChanged
  115. {
  116. add { AddHandler(SelectionChangedEvent, value); }
  117. remove { RemoveHandler(SelectionChangedEvent, value); }
  118. }
  119. /// <summary>
  120. /// Gets or sets a value indicating whether to automatically scroll to newly selected items.
  121. /// </summary>
  122. public bool AutoScrollToSelectedItem
  123. {
  124. get { return GetValue(AutoScrollToSelectedItemProperty); }
  125. set { SetValue(AutoScrollToSelectedItemProperty, value); }
  126. }
  127. /// <summary>
  128. /// Gets or sets the index of the selected item.
  129. /// </summary>
  130. public int SelectedIndex
  131. {
  132. get
  133. {
  134. return _selectedIndex;
  135. }
  136. set
  137. {
  138. if (_updateCount == 0)
  139. {
  140. SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action<Action> notifyWrapper) =>
  141. {
  142. var old = backing;
  143. var effective = (val >= 0 && val < Items?.Cast<object>().Count()) ? val : -1;
  144. if (old != effective)
  145. {
  146. backing = effective;
  147. notifyWrapper(() =>
  148. RaisePropertyChanged(
  149. SelectedIndexProperty,
  150. old,
  151. effective,
  152. BindingPriority.LocalValue));
  153. SelectedItem = ElementAt(Items, effective);
  154. }
  155. }, value);
  156. }
  157. else
  158. {
  159. _updateSelectedIndex = value;
  160. _updateSelectedItems = null;
  161. }
  162. }
  163. }
  164. /// <summary>
  165. /// Gets or sets the selected item.
  166. /// </summary>
  167. public object SelectedItem
  168. {
  169. get
  170. {
  171. return _selectedItem;
  172. }
  173. set
  174. {
  175. if (_updateCount == 0)
  176. {
  177. SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action<Action> notifyWrapper) =>
  178. {
  179. var old = backing;
  180. var index = IndexOf(Items, val);
  181. var effective = index != -1 ? val : null;
  182. if (!object.Equals(effective, old))
  183. {
  184. backing = effective;
  185. notifyWrapper(() =>
  186. RaisePropertyChanged(
  187. SelectedItemProperty,
  188. old,
  189. effective,
  190. BindingPriority.LocalValue));
  191. SelectedIndex = index;
  192. if (effective != null)
  193. {
  194. if (SelectedItems.Count != 1 || SelectedItems[0] != effective)
  195. {
  196. _syncingSelectedItems = true;
  197. SelectedItems.Clear();
  198. SelectedItems.Add(effective);
  199. _syncingSelectedItems = false;
  200. }
  201. }
  202. else if (SelectedItems.Count > 0)
  203. {
  204. SelectedItems.Clear();
  205. }
  206. }
  207. }, value);
  208. }
  209. else
  210. {
  211. _updateSelectedItems = new AvaloniaList<object>(value);
  212. _updateSelectedIndex = int.MinValue;
  213. }
  214. }
  215. }
  216. /// <summary>
  217. /// Gets the selected items.
  218. /// </summary>
  219. protected IList SelectedItems
  220. {
  221. get
  222. {
  223. if (_selectedItems == null)
  224. {
  225. _selectedItems = new AvaloniaList<object>();
  226. SubscribeToSelectedItems();
  227. }
  228. return _selectedItems;
  229. }
  230. set
  231. {
  232. if (value?.IsFixedSize == true || value?.IsReadOnly == true)
  233. {
  234. throw new NotSupportedException(
  235. "Cannot use a fixed size or read-only collection as SelectedItems.");
  236. }
  237. UnsubscribeFromSelectedItems();
  238. _selectedItems = value ?? new AvaloniaList<object>();
  239. SubscribeToSelectedItems();
  240. }
  241. }
  242. /// <summary>
  243. /// Gets or sets the selection mode.
  244. /// </summary>
  245. protected SelectionMode SelectionMode
  246. {
  247. get { return GetValue(SelectionModeProperty); }
  248. set { SetValue(SelectionModeProperty, value); }
  249. }
  250. /// <summary>
  251. /// Gets a value indicating whether <see cref="SelectionMode.AlwaysSelected"/> is set.
  252. /// </summary>
  253. protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0;
  254. /// <inheritdoc/>
  255. public override void BeginInit()
  256. {
  257. base.BeginInit();
  258. ++_updateCount;
  259. _updateSelectedIndex = int.MinValue;
  260. }
  261. /// <inheritdoc/>
  262. public override void EndInit()
  263. {
  264. if (--_updateCount == 0)
  265. {
  266. UpdateFinished();
  267. }
  268. base.EndInit();
  269. }
  270. /// <summary>
  271. /// Scrolls the specified item into view.
  272. /// </summary>
  273. /// <param name="item">The item.</param>
  274. public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item);
  275. /// <summary>
  276. /// Tries to get the container that was the source of an event.
  277. /// </summary>
  278. /// <param name="eventSource">The control that raised the event.</param>
  279. /// <returns>The container or null if the event did not originate in a container.</returns>
  280. protected IControl GetContainerFromEventSource(IInteractive eventSource)
  281. {
  282. var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
  283. .OfType<IControl>()
  284. .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
  285. return item;
  286. }
  287. /// <inheritdoc/>
  288. protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
  289. {
  290. base.ItemsChanged(e);
  291. if (_updateCount == 0)
  292. {
  293. var newIndex = -1;
  294. if (SelectedIndex != -1)
  295. {
  296. newIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem);
  297. }
  298. if (AlwaysSelected && Items != null && Items.Cast<object>().Any())
  299. {
  300. newIndex = 0;
  301. }
  302. SelectedIndex = newIndex;
  303. }
  304. }
  305. /// <inheritdoc/>
  306. protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  307. {
  308. base.ItemsCollectionChanged(sender, e);
  309. switch (e.Action)
  310. {
  311. case NotifyCollectionChangedAction.Add:
  312. if (AlwaysSelected && SelectedIndex == -1)
  313. {
  314. SelectedIndex = 0;
  315. }
  316. break;
  317. case NotifyCollectionChangedAction.Remove:
  318. case NotifyCollectionChangedAction.Replace:
  319. var selectedIndex = SelectedIndex;
  320. if (selectedIndex >= e.OldStartingIndex &&
  321. selectedIndex < e.OldStartingIndex + e.OldItems.Count)
  322. {
  323. if (!AlwaysSelected)
  324. {
  325. selectedIndex = SelectedIndex = -1;
  326. }
  327. else
  328. {
  329. LostSelection();
  330. }
  331. }
  332. var items = Items?.Cast<object>();
  333. if (selectedIndex >= items.Count())
  334. {
  335. selectedIndex = SelectedIndex = items.Count() - 1;
  336. }
  337. break;
  338. case NotifyCollectionChangedAction.Move:
  339. case NotifyCollectionChangedAction.Reset:
  340. SelectedIndex = IndexOf(Items, SelectedItem);
  341. break;
  342. }
  343. }
  344. /// <inheritdoc/>
  345. protected override void OnContainersMaterialized(ItemContainerEventArgs e)
  346. {
  347. base.OnContainersMaterialized(e);
  348. var selectedIndex = SelectedIndex;
  349. var selectedContainer = e.Containers
  350. .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true);
  351. if (selectedContainer != null)
  352. {
  353. SelectedIndex = selectedContainer.Index;
  354. }
  355. else if (selectedIndex >= e.StartingIndex &&
  356. selectedIndex < e.StartingIndex + e.Containers.Count)
  357. {
  358. var container = e.Containers[selectedIndex - e.StartingIndex];
  359. if (container.ContainerControl != null)
  360. {
  361. MarkContainerSelected(container.ContainerControl, true);
  362. }
  363. }
  364. }
  365. /// <inheritdoc/>
  366. protected override void OnContainersDematerialized(ItemContainerEventArgs e)
  367. {
  368. base.OnContainersDematerialized(e);
  369. var panel = (InputElement)Presenter.Panel;
  370. if (panel != null)
  371. {
  372. foreach (var container in e.Containers)
  373. {
  374. if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
  375. {
  376. KeyboardNavigation.SetTabOnceActiveElement(panel, null);
  377. break;
  378. }
  379. }
  380. }
  381. }
  382. protected override void OnContainersRecycled(ItemContainerEventArgs e)
  383. {
  384. foreach (var i in e.Containers)
  385. {
  386. if (i.ContainerControl != null && i.Item != null)
  387. {
  388. var ms = MemberSelector;
  389. bool selected = ms == null ?
  390. SelectedItems.Contains(i.Item) :
  391. SelectedItems.OfType<object>().Any(v => Equals(ms.Select(v), i.Item));
  392. MarkContainerSelected(i.ContainerControl, selected);
  393. }
  394. }
  395. }
  396. /// <inheritdoc/>
  397. protected override void OnDataContextBeginUpdate()
  398. {
  399. base.OnDataContextBeginUpdate();
  400. ++_updateCount;
  401. }
  402. /// <inheritdoc/>
  403. protected override void OnDataContextEndUpdate()
  404. {
  405. base.OnDataContextEndUpdate();
  406. if (--_updateCount == 0)
  407. {
  408. UpdateFinished();
  409. }
  410. }
  411. protected override void OnKeyDown(KeyEventArgs e)
  412. {
  413. base.OnKeyDown(e);
  414. if (!e.Handled)
  415. {
  416. var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
  417. bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
  418. if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
  419. {
  420. SynchronizeItems(SelectedItems, Items?.Cast<object>());
  421. e.Handled = true;
  422. }
  423. }
  424. }
  425. /// <summary>
  426. /// Moves the selection in the specified direction relative to the current selection.
  427. /// </summary>
  428. /// <param name="direction">The direction to move.</param>
  429. /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
  430. /// <returns>True if the selection was moved; otherwise false.</returns>
  431. protected bool MoveSelection(NavigationDirection direction, bool wrap)
  432. {
  433. var from = SelectedIndex != -1 ? ItemContainerGenerator.ContainerFromIndex(SelectedIndex) : null;
  434. return MoveSelection(from, direction, wrap);
  435. }
  436. /// <summary>
  437. /// Moves the selection in the specified direction relative to the specified container.
  438. /// </summary>
  439. /// <param name="from">The container which serves as a starting point for the movement.</param>
  440. /// <param name="direction">The direction to move.</param>
  441. /// <param name="wrap">Whether to wrap when the selection reaches the first or last item.</param>
  442. /// <returns>True if the selection was moved; otherwise false.</returns>
  443. protected bool MoveSelection(IControl from, NavigationDirection direction, bool wrap)
  444. {
  445. if (Presenter?.Panel is INavigableContainer container &&
  446. GetNextControl(container, direction, from, wrap) is IControl next)
  447. {
  448. var index = ItemContainerGenerator.IndexFromContainer(next);
  449. if (index != -1)
  450. {
  451. SelectedIndex = index;
  452. return true;
  453. }
  454. }
  455. return false;
  456. }
  457. /// <summary>
  458. /// Updates the selection for an item based on user interaction.
  459. /// </summary>
  460. /// <param name="index">The index of the item.</param>
  461. /// <param name="select">Whether the item should be selected or unselected.</param>
  462. /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
  463. /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
  464. protected void UpdateSelection(
  465. int index,
  466. bool select = true,
  467. bool rangeModifier = false,
  468. bool toggleModifier = false)
  469. {
  470. if (index != -1)
  471. {
  472. if (select)
  473. {
  474. var mode = SelectionMode;
  475. var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
  476. var multi = (mode & SelectionMode.Multiple) != 0;
  477. var range = multi && SelectedIndex != -1 && rangeModifier;
  478. if (!toggle && !range)
  479. {
  480. SelectedIndex = index;
  481. }
  482. else if (multi && range)
  483. {
  484. SynchronizeItems(
  485. SelectedItems,
  486. GetRange(Items, SelectedIndex, index));
  487. }
  488. else
  489. {
  490. var item = ElementAt(Items, index);
  491. var i = SelectedItems.IndexOf(item);
  492. if (i != -1 && (!AlwaysSelected || SelectedItems.Count > 1))
  493. {
  494. SelectedItems.Remove(item);
  495. }
  496. else
  497. {
  498. if (multi)
  499. {
  500. SelectedItems.Add(item);
  501. }
  502. else
  503. {
  504. SelectedIndex = index;
  505. }
  506. }
  507. }
  508. if (Presenter?.Panel != null)
  509. {
  510. var container = ItemContainerGenerator.ContainerFromIndex(index);
  511. KeyboardNavigation.SetTabOnceActiveElement(
  512. (InputElement)Presenter.Panel,
  513. container);
  514. }
  515. }
  516. else
  517. {
  518. LostSelection();
  519. }
  520. }
  521. }
  522. /// <summary>
  523. /// Updates the selection for a container based on user interaction.
  524. /// </summary>
  525. /// <param name="container">The container.</param>
  526. /// <param name="select">Whether the container should be selected or unselected.</param>
  527. /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
  528. /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
  529. protected void UpdateSelection(
  530. IControl container,
  531. bool select = true,
  532. bool rangeModifier = false,
  533. bool toggleModifier = false)
  534. {
  535. var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
  536. if (index != -1)
  537. {
  538. UpdateSelection(index, select, rangeModifier, toggleModifier);
  539. }
  540. }
  541. /// <summary>
  542. /// Updates the selection based on an event that may have originated in a container that
  543. /// belongs to the control.
  544. /// </summary>
  545. /// <param name="eventSource">The control that raised the event.</param>
  546. /// <param name="select">Whether the container should be selected or unselected.</param>
  547. /// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
  548. /// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
  549. /// <returns>
  550. /// True if the event originated from a container that belongs to the control; otherwise
  551. /// false.
  552. /// </returns>
  553. protected bool UpdateSelectionFromEventSource(
  554. IInteractive eventSource,
  555. bool select = true,
  556. bool rangeModifier = false,
  557. bool toggleModifier = false)
  558. {
  559. var container = GetContainerFromEventSource(eventSource);
  560. if (container != null)
  561. {
  562. UpdateSelection(container, select, rangeModifier, toggleModifier);
  563. return true;
  564. }
  565. return false;
  566. }
  567. /// <summary>
  568. /// Makes a list of objects equal another.
  569. /// </summary>
  570. /// <param name="items">The items collection.</param>
  571. /// <param name="desired">The desired items.</param>
  572. internal static void SynchronizeItems(IList items, IEnumerable<object> desired)
  573. {
  574. var index = 0;
  575. foreach (object item in desired)
  576. {
  577. int itemIndex = items.IndexOf(item);
  578. if (itemIndex == -1)
  579. {
  580. items.Insert(index, item);
  581. }
  582. else if(itemIndex != index)
  583. {
  584. items.RemoveAt(itemIndex);
  585. items.Insert(index, item);
  586. }
  587. ++index;
  588. }
  589. while (index < items.Count)
  590. {
  591. items.RemoveAt(items.Count - 1);
  592. }
  593. }
  594. /// <summary>
  595. /// Gets a range of items from an IEnumerable.
  596. /// </summary>
  597. /// <param name="items">The items.</param>
  598. /// <param name="first">The index of the first item.</param>
  599. /// <param name="last">The index of the last item.</param>
  600. /// <returns>The items.</returns>
  601. private static IEnumerable<object> GetRange(IEnumerable items, int first, int last)
  602. {
  603. var list = (items as IList) ?? items.Cast<object>().ToList();
  604. int step = first > last ? -1 : 1;
  605. for (int i = first; i != last; i += step)
  606. {
  607. yield return list[i];
  608. }
  609. yield return list[last];
  610. }
  611. /// <summary>
  612. /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
  613. /// </summary>
  614. /// <param name="e">The event.</param>
  615. private void ContainerSelectionChanged(RoutedEventArgs e)
  616. {
  617. if (!_ignoreContainerSelectionChanged)
  618. {
  619. var control = e.Source as IControl;
  620. var selectable = e.Source as ISelectable;
  621. if (control != null &&
  622. selectable != null &&
  623. control.LogicalParent == this &&
  624. ItemContainerGenerator?.IndexFromContainer(control) != -1)
  625. {
  626. UpdateSelection(control, selectable.IsSelected);
  627. }
  628. }
  629. if (e.Source != this)
  630. {
  631. e.Handled = true;
  632. }
  633. }
  634. /// <summary>
  635. /// Called when the currently selected item is lost and the selection must be changed
  636. /// depending on the <see cref="SelectionMode"/> property.
  637. /// </summary>
  638. private void LostSelection()
  639. {
  640. var items = Items?.Cast<object>();
  641. if (items != null && AlwaysSelected)
  642. {
  643. var index = Math.Min(SelectedIndex, items.Count() - 1);
  644. if (index > -1)
  645. {
  646. SelectedItem = items.ElementAt(index);
  647. return;
  648. }
  649. }
  650. SelectedIndex = -1;
  651. }
  652. /// <summary>
  653. /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
  654. /// </summary>
  655. /// <param name="container">The container.</param>
  656. /// <param name="selected">Whether the control is selected</param>
  657. /// <returns>The previous selection state.</returns>
  658. private bool MarkContainerSelected(IControl container, bool selected)
  659. {
  660. try
  661. {
  662. var selectable = container as ISelectable;
  663. bool result;
  664. _ignoreContainerSelectionChanged = true;
  665. if (selectable != null)
  666. {
  667. result = selectable.IsSelected;
  668. selectable.IsSelected = selected;
  669. }
  670. else
  671. {
  672. result = container.Classes.Contains(":selected");
  673. ((IPseudoClasses)container.Classes).Set(":selected", selected);
  674. }
  675. return result;
  676. }
  677. finally
  678. {
  679. _ignoreContainerSelectionChanged = false;
  680. }
  681. }
  682. /// <summary>
  683. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
  684. /// </summary>
  685. /// <param name="index">The index of the item.</param>
  686. /// <param name="selected">Whether the item should be selected or deselected.</param>
  687. private void MarkItemSelected(int index, bool selected)
  688. {
  689. var container = ItemContainerGenerator?.ContainerFromIndex(index);
  690. if (container != null)
  691. {
  692. MarkContainerSelected(container, selected);
  693. }
  694. }
  695. /// <summary>
  696. /// Sets an item container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
  697. /// </summary>
  698. /// <param name="item">The item.</param>
  699. /// <param name="selected">Whether the item should be selected or deselected.</param>
  700. private void MarkItemSelected(object item, bool selected)
  701. {
  702. var index = IndexOf(Items, item);
  703. if (index != -1)
  704. {
  705. MarkItemSelected(index, selected);
  706. }
  707. }
  708. /// <summary>
  709. /// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
  710. /// </summary>
  711. /// <param name="sender">The event sender.</param>
  712. /// <param name="e">The event args.</param>
  713. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  714. {
  715. var generator = ItemContainerGenerator;
  716. IList added = null;
  717. IList removed = null;
  718. switch (e.Action)
  719. {
  720. case NotifyCollectionChangedAction.Add:
  721. SelectedItemsAdded(e.NewItems.Cast<object>().ToList());
  722. if (AutoScrollToSelectedItem)
  723. {
  724. ScrollIntoView(e.NewItems[0]);
  725. }
  726. added = e.NewItems;
  727. break;
  728. case NotifyCollectionChangedAction.Remove:
  729. if (SelectedItems.Count == 0)
  730. {
  731. if (!_syncingSelectedItems)
  732. {
  733. SelectedIndex = -1;
  734. }
  735. }
  736. foreach (var item in e.OldItems)
  737. {
  738. MarkItemSelected(item, false);
  739. }
  740. removed = e.OldItems;
  741. break;
  742. case NotifyCollectionChangedAction.Reset:
  743. if (generator != null)
  744. {
  745. removed = new List<object>();
  746. foreach (var item in generator.Containers)
  747. {
  748. if (item?.ContainerControl != null)
  749. {
  750. if (MarkContainerSelected(item.ContainerControl, false))
  751. {
  752. removed.Add(item.Item);
  753. }
  754. }
  755. }
  756. }
  757. if (SelectedItems.Count > 0)
  758. {
  759. _selectedItem = null;
  760. SelectedItemsAdded(SelectedItems);
  761. added = SelectedItems;
  762. }
  763. else if (!_syncingSelectedItems)
  764. {
  765. SelectedIndex = -1;
  766. }
  767. break;
  768. case NotifyCollectionChangedAction.Replace:
  769. foreach (var item in e.OldItems)
  770. {
  771. MarkItemSelected(item, false);
  772. }
  773. foreach (var item in e.NewItems)
  774. {
  775. MarkItemSelected(item, true);
  776. }
  777. if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
  778. {
  779. var oldItem = SelectedItem;
  780. var oldIndex = SelectedIndex;
  781. var item = SelectedItems[0];
  782. var index = IndexOf(Items, item);
  783. _selectedIndex = index;
  784. _selectedItem = item;
  785. RaisePropertyChanged(SelectedIndexProperty, oldIndex, index, BindingPriority.LocalValue);
  786. RaisePropertyChanged(SelectedItemProperty, oldItem, item, BindingPriority.LocalValue);
  787. }
  788. added = e.NewItems;
  789. removed = e.OldItems;
  790. break;
  791. }
  792. if (added?.Count > 0 || removed?.Count > 0)
  793. {
  794. var changed = new SelectionChangedEventArgs(
  795. SelectionChangedEvent,
  796. added ?? Empty,
  797. removed ?? Empty);
  798. RaiseEvent(changed);
  799. }
  800. }
  801. /// <summary>
  802. /// Called when items are added to the <see cref="SelectedItems"/> collection.
  803. /// </summary>
  804. /// <param name="items">The added items.</param>
  805. private void SelectedItemsAdded(IList items)
  806. {
  807. if (items.Count > 0)
  808. {
  809. foreach (var item in items)
  810. {
  811. MarkItemSelected(item, true);
  812. }
  813. if (SelectedItem == null && !_syncingSelectedItems)
  814. {
  815. var index = IndexOf(Items, items[0]);
  816. if (index != -1)
  817. {
  818. _selectedItem = items[0];
  819. _selectedIndex = index;
  820. RaisePropertyChanged(SelectedIndexProperty, -1, index, BindingPriority.LocalValue);
  821. RaisePropertyChanged(SelectedItemProperty, null, items[0], BindingPriority.LocalValue);
  822. }
  823. }
  824. }
  825. }
  826. /// <summary>
  827. /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
  828. /// </summary>
  829. private void SubscribeToSelectedItems()
  830. {
  831. var incc = _selectedItems as INotifyCollectionChanged;
  832. if (incc != null)
  833. {
  834. incc.CollectionChanged += SelectedItemsCollectionChanged;
  835. }
  836. SelectedItemsCollectionChanged(
  837. _selectedItems,
  838. new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  839. }
  840. /// <summary>
  841. /// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
  842. /// </summary>
  843. private void UnsubscribeFromSelectedItems()
  844. {
  845. var incc = _selectedItems as INotifyCollectionChanged;
  846. if (incc != null)
  847. {
  848. incc.CollectionChanged -= SelectedItemsCollectionChanged;
  849. }
  850. }
  851. private void UpdateFinished()
  852. {
  853. if (_updateSelectedIndex != int.MinValue)
  854. {
  855. SelectedIndex = _updateSelectedIndex;
  856. }
  857. else if (_updateSelectedItems != null)
  858. {
  859. SelectedItems = _updateSelectedItems;
  860. }
  861. }
  862. }
  863. }