SelectingItemsControl.cs 32 KB

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