From 25689b21ff95996b8f8a0446944a12237a44e80a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 6 Feb 2020 12:39:20 +0100 Subject: [PATCH] Use SelectionModel in SelectingItemsControl . --- samples/BindingDemo/MainWindow.xaml | 4 +- .../ViewModels/MainWindowViewModel.cs | 5 +- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- samples/VirtualizationDemo/MainWindow.xaml | 2 +- .../ViewModels/MainWindowViewModel.cs | 14 +- src/Avalonia.Controls/ComboBox.cs | 4 +- src/Avalonia.Controls/ListBox.cs | 20 +- .../Primitives/SelectingItemsControl.cs | 826 +++++------------- src/Avalonia.Controls/TreeView.cs | 3 +- src/Avalonia.Dialogs/ManagedFileChooser.xaml | 4 +- .../ManagedFileChooser.xaml.cs | 4 +- .../ManagedFileChooserViewModel.cs | 46 +- .../CarouselTests.cs | 6 +- .../Primitives/SelectingItemsControlTests.cs | 67 +- .../SelectingItemsControlTests_AutoSelect.cs | 4 +- .../SelectingItemsControlTests_Multiple.cs | 371 +++++--- .../Primitives/TabStripTests.cs | 9 +- .../TabControlTests.cs | 5 +- .../Xaml/BasicTests.cs | 50 +- 19 files changed, 576 insertions(+), 870 deletions(-) diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index b57a9a0a9e..26a62ebca6 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -74,11 +74,11 @@ - + - + diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs index 22d01e0765..a66038ff3e 100644 --- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Threading; using ReactiveUI; +using Avalonia.Controls; namespace BindingDemo.ViewModels { @@ -27,7 +28,7 @@ namespace BindingDemo.ViewModels Detail = "Item " + x + " details", })); - SelectedItems = new ObservableCollection(); + Selection = new SelectionModel(); ShuffleItems = ReactiveCommand.Create(() => { @@ -56,7 +57,7 @@ namespace BindingDemo.ViewModels } public ObservableCollection Items { get; } - public ObservableCollection SelectedItems { get; } + public SelectionModel Selection { get; } public ReactiveCommand ShuffleItems { get; } public string BooleanString diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index b1b3112e60..47b4ce7151 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index 12137cd03d..4bd657bf93 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -45,7 +45,7 @@ SelectedItems { get; } - = new AvaloniaList(); + public SelectionModel Selection { get; } = new SelectionModel(); public AvaloniaList Items { @@ -141,9 +140,9 @@ namespace VirtualizationDemo.ViewModels { var index = Items.Count; - if (SelectedItems.Count > 0) + if (Selection.SelectedIndices.Count > 0) { - index = Items.IndexOf(SelectedItems[0]); + index = Selection.SelectedIndex.GetAt(0); } Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); @@ -151,9 +150,9 @@ namespace VirtualizationDemo.ViewModels private void Remove() { - if (SelectedItems.Count > 0) + if (Selection.SelectedItems.Count > 0) { - Items.RemoveAll(SelectedItems); + Items.RemoveAll(Selection.SelectedItems.Cast().ToList()); } } @@ -167,8 +166,7 @@ namespace VirtualizationDemo.ViewModels private void SelectItem(int index) { - SelectedItems.Clear(); - SelectedItems.Add(Items[index]); + Selection.SelectedIndex = new IndexPath(index); } } } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index c2cf20b32d..0722802962 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -289,9 +289,9 @@ namespace Avalonia.Controls { var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - if (container == null && SelectedItems.Count > 0) + if (container == null && SelectedIndex != -1) { - ScrollIntoView(SelectedItems[0]); + ScrollIntoView(Selection.SelectedItem); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); } diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 4966e669ed..12c02b81c9 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -29,10 +29,10 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(Scroll), o => o.Scroll); /// - /// Defines the property. + /// Defines the property. /// - public static readonly new DirectProperty SelectedItemsProperty = - SelectingItemsControl.SelectedItemsProperty; + public static readonly new DirectProperty SelectionProperty = + SelectingItemsControl.SelectionProperty; /// /// Defines the property. @@ -66,11 +66,13 @@ namespace Avalonia.Controls private set { SetAndRaise(ScrollProperty, ref _scroll, value); } } - /// - public new IList SelectedItems + /// + /// Gets or sets a model holding the current selection. + /// + public new ISelectionModel Selection { - get => base.SelectedItems; - set => base.SelectedItems = value; + get => base.Selection; + set => base.Selection = value; } /// @@ -98,12 +100,12 @@ namespace Avalonia.Controls /// /// Selects all items in the . /// - public new void SelectAll() => base.SelectAll(); + public void SelectAll() => Selection.SelectAll(); /// /// Deselects all items in the . /// - public new void UnselectAll() => base.UnselectAll(); + public void UnselectAll() => Selection.ClearSelection(); /// protected override IItemContainerGenerator CreateItemContainerGenerator() diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 6bc4e71508..8c3487e893 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,15 +5,14 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.Diagnostics; using System.Linq; -using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; -using Avalonia.Logging; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -26,9 +25,9 @@ namespace Avalonia.Controls.Primitives /// provides a base class for s /// that maintain a selection (single or multiple). By default only its /// and properties are visible; the - /// current multiple selection together with the - /// properties are protected, however a derived class can expose - /// these if it wishes to support multiple selection. + /// current multiple together with the + /// properties are protected, however a derived class can expose these if it wishes to support + /// multiple selection. /// /// /// maintains a selection respecting the current @@ -69,13 +68,13 @@ namespace Avalonia.Controls.Primitives defaultBindingMode: BindingMode.TwoWay); /// - /// Defines the property. + /// Defines the property. /// - protected static readonly DirectProperty SelectedItemsProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectedItems), - o => o.SelectedItems, - (o, v) => o.SelectedItems = v); + public static readonly DirectProperty SelectionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Selection), + o => o.Selection, + (o, v) => o.Selection = v); /// /// Defines the property. @@ -104,16 +103,20 @@ namespace Avalonia.Controls.Primitives private static readonly IList Empty = Array.Empty(); - private readonly Selection _selection = new Selection(); + private ISelectionModel _selection; private int _selectedIndex = -1; private object _selectedItem; - private IList _selectedItems; private bool _ignoreContainerSelectionChanged; - private bool _syncingSelectedItems; private int _updateCount; private int _updateSelectedIndex; private object _updateSelectedItem; + public SelectingItemsControl() + { + // Setting Selection to null will cause default instance to be created. + Selection = null; + } + /// /// Initializes static members of the class. /// @@ -145,17 +148,15 @@ namespace Avalonia.Controls.Primitives /// public int SelectedIndex { - get - { - return _selectedIndex; - } - + get => Selection.SelectedIndex != default ? Selection.SelectedIndex.GetAt(0) : -1; set { if (_updateCount == 0) { - var effective = (value >= 0 && value < ItemCount) ? value : -1; - UpdateSelectedItem(effective); + if (value != SelectedIndex) + { + Selection.SelectedIndex = new IndexPath(value); + } } else { @@ -170,16 +171,12 @@ namespace Avalonia.Controls.Primitives /// public object SelectedItem { - get - { - return _selectedItem; - } - + get => Selection.SelectedItem; set { if (_updateCount == 0) { - UpdateSelectedItem(IndexOf(Items, value)); + SelectedIndex = IndexOf(Items, value); } else { @@ -190,32 +187,101 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets the selected items. + /// Gets or sets a model holding the current selection. /// - protected IList SelectedItems + protected ISelectionModel Selection { - get + get => _selection; + set { - if (_selectedItems == null) + value ??= new SelectionModel { - _selectedItems = new AvaloniaList(); - SubscribeToSelectedItems(); - } - - return _selectedItems; - } + SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple), + AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected), + RetainSelectionOnReset = true, + }; - set - { - if (value?.IsFixedSize == true || value?.IsReadOnly == true) + if (_selection != value) { - throw new NotSupportedException( - "Cannot use a fixed size or read-only collection as SelectedItems."); - } + if (value == null) + { + throw new ArgumentNullException(nameof(value), "Cannot set Selection to null."); + } + else if (value.Source != null && value.Source != Items) + { + throw new ArgumentException("Selection has invalid Source."); + } + + List oldSelection = null; + + if (_selection != null) + { + oldSelection = Selection.SelectedItems.ToList(); + _selection.PropertyChanged -= OnSelectionModelPropertyChanged; + _selection.SelectionChanged -= OnSelectionModelSelectionChanged; + MarkContainersUnselected(); + } + + _selection = value; + + if (oldSelection?.Count > 0) + { + RaiseEvent(new SelectionChangedEventArgs( + SelectionChangedEvent, + oldSelection, + Array.Empty())); + } + + if (_selection != null) + { + _selection.Source = Items; + _selection.PropertyChanged += OnSelectionModelPropertyChanged; + _selection.SelectionChanged += OnSelectionModelSelectionChanged; + + if (_selection.SingleSelect) + { + SelectionMode &= ~SelectionMode.Multiple; + } + else + { + SelectionMode |= SelectionMode.Multiple; + } + + if (_selection.AutoSelect) + { + SelectionMode |= SelectionMode.AlwaysSelected; + } + else + { + SelectionMode &= ~SelectionMode.AlwaysSelected; + } + + UpdateContainerSelection(); + + var selectedIndex = SelectedIndex; + var selectedItem = SelectedItem; + + if (_selectedIndex != selectedIndex) + { + RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, selectedIndex); + _selectedIndex = selectedIndex; + } - UnsubscribeFromSelectedItems(); - _selectedItems = value ?? new AvaloniaList(); - SubscribeToSelectedItems(); + if (_selectedItem != selectedItem) + { + RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem); + _selectedItem = selectedItem; + } + + if (selectedIndex != -1) + { + RaiseEvent(new SelectionChangedEventArgs( + SelectionChangedEvent, + Array.Empty(), + Selection.SelectedItems.ToList())); + } + } + } } } @@ -285,81 +351,18 @@ namespace Avalonia.Controls.Primitives /// protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { - base.ItemsChanged(e); - if (_updateCount == 0) { - var newIndex = -1; - - if (SelectedIndex != -1) - { - newIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); - } - - if (AlwaysSelected && Items != null && Items.Cast().Any()) - { - newIndex = 0; - } - - SelectedIndex = newIndex; + Selection.Source = e.NewValue; } + + base.ItemsChanged(e); } /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - if (_updateCount > 0) - { - base.ItemsCollectionChanged(sender, e); - return; - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); - break; - case NotifyCollectionChangedAction.Remove: - _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); - break; - } - base.ItemsCollectionChanged(sender, e); - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (AlwaysSelected && SelectedIndex == -1) - { - SelectedIndex = 0; - } - else - { - UpdateSelectedItem(_selection.First(), false); - } - - break; - - case NotifyCollectionChangedAction.Remove: - UpdateSelectedItem(_selection.First(), false); - ResetSelectedItems(); - break; - - case NotifyCollectionChangedAction.Replace: - UpdateSelectedItem(SelectedIndex, false); - ResetSelectedItems(); - break; - - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Reset: - SelectedIndex = IndexOf(Items, SelectedItem); - - if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) - { - SelectedIndex = 0; - } - break; - } } /// @@ -367,36 +370,18 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var resetSelectedItems = false; - foreach (var container in e.Containers) { if ((container.ContainerControl as ISelectable)?.IsSelected == true) { - if (SelectionMode.HasFlag(SelectionMode.Multiple)) - { - if (_selection.Add(container.Index)) - { - resetSelectedItems = true; - } - } - else - { - SelectedIndex = container.Index; - } - + Selection.Select(container.Index); MarkContainerSelected(container.ContainerControl, true); } - else if (_selection.Contains(container.Index)) + else if (Selection.IsSelected(container.Index) == true) { MarkContainerSelected(container.ContainerControl, true); } } - - if (resetSelectedItems) - { - ResetSelectedItems(); - } } /// @@ -425,7 +410,7 @@ namespace Avalonia.Controls.Primitives { if (i.ContainerControl != null && i.Item != null) { - bool selected = _selection.Contains(i.Index); + bool selected = Selection.IsSelected(i.Index) == true; MarkContainerSelected(i.ContainerControl, selected); } } @@ -447,6 +432,18 @@ namespace Avalonia.Controls.Primitives InternalEndInit(); } + protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + { + base.OnPropertyChanged(property, oldValue, newValue, priority); + + if (property == SelectionModeProperty) + { + var mode = newValue.GetValueOrDefault(); + Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple); + Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected); + } + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -461,7 +458,7 @@ namespace Avalonia.Controls.Primitives (((SelectionMode & SelectionMode.Multiple) != 0) || (SelectionMode & SelectionMode.Toggle) != 0)) { - SelectAll(); + Selection.SelectAll(); e.Handled = true; } } @@ -503,36 +500,6 @@ namespace Avalonia.Controls.Primitives return false; } - /// - /// Selects all items in the control. - /// - protected void SelectAll() - { - UpdateSelectedItems(() => - { - _selection.Clear(); - - for (var i = 0; i < ItemCount; ++i) - { - _selection.Add(i); - } - - UpdateSelectedItem(0, false); - - foreach (var container in ItemContainerGenerator.Containers) - { - MarkItemSelected(container.Index, true); - } - - ResetSelectedItems(); - }); - } - - /// - /// Deselects all items in the control. - /// - protected void UnselectAll() => UpdateSelectedItem(-1); - /// /// Updates the selection for an item based on user interaction. /// @@ -559,63 +526,35 @@ namespace Avalonia.Controls.Primitives if (rightButton) { - if (!_selection.Contains(index)) + if (Selection.IsSelected(index) == false) { - UpdateSelectedItem(index); + SelectedIndex = index; } } else if (range) { - UpdateSelectedItems(() => - { - var start = SelectedIndex != -1 ? SelectedIndex : 0; - var step = start < index ? 1 : -1; + using var operation = Selection.Update(); + var anchor = Selection.AnchorIndex; - _selection.Clear(); - - for (var i = start; i != index; i += step) - { - _selection.Add(i); - } - - _selection.Add(index); - - var first = Math.Min(start, index); - var last = Math.Max(start, index); - - foreach (var container in ItemContainerGenerator.Containers) - { - MarkItemSelected( - container.Index, - container.Index >= first && container.Index <= last); - } + if (anchor.GetSize() == 0) + { + anchor = new IndexPath(0); + } - ResetSelectedItems(); - }); + Selection.ClearSelection(); + Selection.AnchorIndex = anchor; + Selection.SelectRangeFromAnchor(index); } else if (multi && toggle) { - UpdateSelectedItems(() => + if (Selection.IsSelected(index) == true) { - if (!_selection.Contains(index)) - { - _selection.Add(index); - MarkItemSelected(index, true); - SelectedItems.Add(ElementAt(Items, index)); - } - else - { - _selection.Remove(index); - MarkItemSelected(index, false); - - if (index == _selectedIndex) - { - UpdateSelectedItem(_selection.First(), false); - } - - SelectedItems.Remove(ElementAt(Items, index)); - } - }); + Selection.Deselect(index); + } + else + { + Selection.Select(index); + } } else if (toggle) { @@ -623,7 +562,9 @@ namespace Avalonia.Controls.Primitives } else { - UpdateSelectedItem(index); + using var operation = Selection.Update(); + Selection.ClearSelection(); + Selection.Select(index); } if (Presenter?.Panel != null) @@ -696,25 +637,71 @@ namespace Avalonia.Controls.Primitives } /// - /// Gets a range of items from an IEnumerable. + /// Called when is raised. + /// + /// The sender. + /// The event args. + private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(SelectionModel.AnchorIndex)) + { + var index = Selection.AnchorIndex.GetSize() > 0 ? Selection.AnchorIndex.GetAt(0) : -1; + var item = index != -1 ? ElementAt(Items, index) : null; + + if (item != null) + { + ScrollIntoView(item); + } + } + } + + /// + /// Called when is raised. /// - /// The items. - /// The index of the first item. - /// The index of the last item. - /// The items. - private static List GetRange(IEnumerable items, int first, int last) + /// The sender. + /// The event args. + private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e) { - var list = (items as IList) ?? items.Cast().ToList(); - var step = first > last ? -1 : 1; - var result = new List(); + void Mark(int index, bool selected) + { + var container = ItemContainerGenerator.ContainerFromIndex(index); - for (int i = first; i != last; i += step) + if (container != null) + { + MarkContainerSelected(container, selected); + } + } + + foreach (var i in e.SelectedIndices) + { + Mark(i.GetAt(0), true); + } + + foreach (var i in e.DeselectedIndices) { - result.Add(list[i]); + Mark(i.GetAt(0), false); } - result.Add(list[last]); - return result; + var newSelectedIndex = SelectedIndex; + var newSelectedItem = SelectedItem; + + if (newSelectedIndex != _selectedIndex) + { + RaisePropertyChanged(SelectedIndexProperty, _selectedIndex, newSelectedIndex); + _selectedIndex = newSelectedIndex; + } + + if (newSelectedItem != _selectedItem) + { + RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem); + _selectedItem = newSelectedItem; + } + + var ev = new SelectionChangedEventArgs( + SelectionChangedEvent, + e.DeselectedItems.ToList(), + e.SelectedItems.ToList()); + RaiseEvent(ev); } /// @@ -794,301 +781,43 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Sets an item container's 'selected' class or . - /// - /// The index of the item. - /// Whether the item should be selected or deselected. - private void MarkItemSelected(int index, bool selected) - { - var container = ItemContainerGenerator?.ContainerFromIndex(index); - - if (container != null) - { - MarkContainerSelected(container, selected); - } - } - - /// - /// Sets an item container's 'selected' class or . - /// - /// The item. - /// Whether the item should be selected or deselected. - private int MarkItemSelected(object item, bool selected) - { - var index = IndexOf(Items, item); - - if (index != -1) - { - MarkItemSelected(index, selected); - } - - return index; - } - - private void ResetSelectedItems() - { - UpdateSelectedItems(() => - { - SelectedItems.Clear(); - - foreach (var i in _selection) - { - SelectedItems.Add(ElementAt(Items, i)); - } - }); - } - - /// - /// Called when the CollectionChanged event is raised. - /// - /// The event sender. - /// The event args. - private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_syncingSelectedItems) - { - return; - } - - void Add(IList newItems, IList addedItems = null) - { - foreach (var item in newItems) - { - var index = MarkItemSelected(item, true); - - if (index != -1 && _selection.Add(index) && addedItems != null) - { - addedItems.Add(item); - } - } - } - - void UpdateSelection() - { - if ((SelectedIndex != -1 && !_selection.Contains(SelectedIndex)) || - (SelectedIndex == -1 && _selection.HasItems)) - { - _selectedIndex = _selection.First(); - _selectedItem = ElementAt(Items, _selectedIndex); - RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue); - RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue); - } - } - - IList added = null; - IList removed = null; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - { - Add(e.NewItems); - UpdateSelection(); - added = e.NewItems; - } - - break; - - case NotifyCollectionChangedAction.Remove: - if (SelectedItems.Count == 0) - { - SelectedIndex = -1; - } - - foreach (var item in e.OldItems) - { - var index = MarkItemSelected(item, false); - _selection.Remove(index); - } - - removed = e.OldItems; - break; - - case NotifyCollectionChangedAction.Replace: - throw new NotSupportedException("Replacing items in a SelectedItems collection is not supported."); - - case NotifyCollectionChangedAction.Move: - throw new NotSupportedException("Moving items in a SelectedItems collection is not supported."); - - case NotifyCollectionChangedAction.Reset: - { - removed = new List(); - added = new List(); - - foreach (var index in _selection.ToList()) - { - var item = ElementAt(Items, index); - - if (!SelectedItems.Contains(item)) - { - MarkItemSelected(index, false); - removed.Add(item); - _selection.Remove(index); - } - } - - Add(SelectedItems, added); - UpdateSelection(); - } - - break; - } - - if (added?.Count > 0 || removed?.Count > 0) - { - var changed = new SelectionChangedEventArgs( - SelectionChangedEvent, - removed ?? Empty, - added ?? Empty); - RaiseEvent(changed); - } - } - - /// - /// Subscribes to the CollectionChanged event, if any. - /// - private void SubscribeToSelectedItems() + private void MarkContainersUnselected() { - var incc = _selectedItems as INotifyCollectionChanged; - - if (incc != null) + foreach (var container in ItemContainerGenerator.Containers) { - incc.CollectionChanged += SelectedItemsCollectionChanged; + MarkContainerSelected(container.ContainerControl, false); } - - SelectedItemsCollectionChanged( - _selectedItems, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - /// - /// Unsubscribes from the CollectionChanged event, if any. - /// - private void UnsubscribeFromSelectedItems() + private void UpdateContainerSelection() { - var incc = _selectedItems as INotifyCollectionChanged; - - if (incc != null) + foreach (var container in ItemContainerGenerator.Containers) { - incc.CollectionChanged -= SelectedItemsCollectionChanged; + MarkContainerSelected( + container.ContainerControl, + Selection.IsSelected(container.Index) != false); } } /// - /// Updates the selection due to a change to or - /// . + /// Sets an item container's 'selected' class or . /// - /// The new selected index. - /// Whether to clear existing selection. - private void UpdateSelectedItem(int index, bool clear = true) + /// The index of the item. + /// Whether the item should be selected or deselected. + private void MarkItemSelected(int index, bool selected) { - var oldIndex = _selectedIndex; - var oldItem = _selectedItem; - - if (index == -1 && AlwaysSelected) - { - index = Math.Min(SelectedIndex, ItemCount - 1); - } - - var item = ElementAt(Items, index); - var itemChanged = !Equals(item, oldItem); - var added = -1; - HashSet removed = null; - - _selectedIndex = index; - _selectedItem = item; - - if (oldIndex != index || itemChanged || _selection.HasMultiple) - { - if (clear) - { - removed = _selection.Clear(); - } - - if (index != -1) - { - if (_selection.Add(index)) - { - added = index; - } - - if (removed?.Contains(index) == true) - { - removed.Remove(index); - added = -1; - } - } - - if (removed != null) - { - foreach (var i in removed) - { - MarkItemSelected(i, false); - } - } - - MarkItemSelected(index, true); - - RaisePropertyChanged( - SelectedIndexProperty, - oldIndex, - index); - } - - if (itemChanged) - { - RaisePropertyChanged( - SelectedItemProperty, - oldItem, - item); - } - - if (removed != null && index != -1) - { - removed.Remove(index); - } - - if (added != -1 || removed?.Count > 0) - { - ResetSelectedItems(); - - var e = new SelectionChangedEventArgs( - SelectionChangedEvent, - removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty(), - added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty()); - RaiseEvent(e); - } - - if (AutoScrollToSelectedItem && _selectedIndex != -1) - { - ScrollIntoView(_selectedItem); - } - } + var container = ItemContainerGenerator?.ContainerFromIndex(index); - private void UpdateSelectedItems(Action action) - { - try - { - _syncingSelectedItems = true; - action(); - } - catch (Exception ex) - { - Logger.TryGet(LogEventLevel.Error)?.Log( - LogArea.Property, - this, - "Error thrown updating SelectedItems: {Error}", - ex); - } - finally + if (container != null) { - _syncingSelectedItems = false; + MarkContainerSelected(container, selected); } } private void UpdateFinished() { + Selection.Source = Items; + if (_updateSelectedItem != null) { SelectedItem = _updateSelectedItem; @@ -1133,104 +862,5 @@ namespace Avalonia.Controls.Primitives UpdateFinished(); } } - - private class Selection : IEnumerable - { - private readonly List _list = new List(); - private HashSet _set = new HashSet(); - - public bool HasItems => _set.Count > 0; - public bool HasMultiple => _set.Count > 1; - - public bool Add(int index) - { - if (index == -1) - { - throw new ArgumentException("Invalid index", "index"); - } - - if (_set.Add(index)) - { - _list.Add(index); - return true; - } - - return false; - } - - public bool Remove(int index) - { - if (_set.Remove(index)) - { - _list.RemoveAll(x => x == index); - return true; - } - - return false; - } - - public HashSet Clear() - { - var result = _set; - _list.Clear(); - _set = new HashSet(); - return result; - } - - public void ItemsInserted(int index, int count) - { - _set = new HashSet(); - - for (var i = 0; i < _list.Count; ++i) - { - var ix = _list[i]; - - if (ix >= index) - { - var newIndex = ix + count; - _list[i] = newIndex; - _set.Add(newIndex); - } - else - { - _set.Add(ix); - } - } - } - - public void ItemsRemoved(int index, int count) - { - var last = (index + count) - 1; - - _set = new HashSet(); - - for (var i = 0; i < _list.Count; ++i) - { - var ix = _list[i]; - - if (ix >= index && ix <= last) - { - _list.RemoveAt(i--); - } - else if (ix > last) - { - var newIndex = ix - count; - _list[i] = newIndex; - _set.Add(newIndex); - } - else - { - _set.Add(ix); - } - } - } - - public bool Contains(int index) => _set.Contains(index); - - public int First() => HasItems ? _list[0] : -1; - - public IEnumerator GetEnumerator() => _set.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 3a7ad97763..454830e806 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -41,7 +41,8 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty SelectedItemsProperty = - ListBox.SelectedItemsProperty.AddOwner( + AvaloniaProperty.RegisterDirect( + nameof(SelectedItems), o => o.SelectedItems, (o, v) => o.SelectedItems = v); diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml index af0c91e7bd..1ff458fcdb 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml @@ -58,7 +58,7 @@ @@ -113,7 +113,7 @@ Items="{Binding Items}" Margin="0 5" SelectionMode="{Binding SelectionMode}" - SelectedItems="{Binding SelectedItems}" + Selection="{Binding Selection}" ScrollViewer.HorizontalScrollBarVisibility="Disabled"> diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs index b967b40c0d..9978e1c8e2 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs @@ -61,7 +61,7 @@ namespace Avalonia.Dialogs return; } - var preselected = model.SelectedItems.FirstOrDefault(); + var preselected = (ManagedFileChooserItemViewModel)model.Selection.SelectedItem; if (preselected == null) { @@ -71,7 +71,7 @@ namespace Avalonia.Dialogs //Let everything to settle down and scroll to selected item await Task.Delay(100); - if (preselected != model.SelectedItems.FirstOrDefault()) + if (preselected != model.Selection.SelectedItem) { return; } diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs index 189e13802d..1f11e75a43 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -26,8 +26,7 @@ namespace Avalonia.Dialogs public AvaloniaList Filters { get; } = new AvaloniaList(); - public AvaloniaList SelectedItems { get; } = - new AvaloniaList(); + public SelectionModel Selection { get; } = new SelectionModel(); string _location; string _fileName; @@ -169,7 +168,7 @@ namespace Avalonia.Dialogs } Navigate(directory, (dialog as FileDialog)?.InitialFileName); - SelectedItems.CollectionChanged += OnSelectionChangedAsync; + Selection.SelectionChanged += OnSelectionChangedAsync; } public void EnterPressed() @@ -184,7 +183,7 @@ namespace Avalonia.Dialogs } } - private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) + private async void OnSelectionChangedAsync(object sender, SelectionModelSelectionChangedEventArgs e) { if (_scheduledSelectionValidation) { @@ -198,19 +197,24 @@ namespace Avalonia.Dialogs { if (_selectingDirectory) { - SelectedItems.Clear(); + Selection.ClearSelection(); } else { - var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder).ToList(); + var invalidItems = e.SelectedItems + .Cast() + .Where(i => i.ItemType == ManagedFileChooserItemType.Folder) + .ToList(); + foreach (var item in invalidItems) { - SelectedItems.Remove(item); + var index = Items.IndexOf(item); + Selection.Deselect(index); } if (!_selectingDirectory) { - var selectedItem = SelectedItems.FirstOrDefault(); + var selectedItem = (ManagedFileChooserItemViewModel)Selection.SelectedItem; if (selectedItem != null) { @@ -250,7 +254,7 @@ namespace Avalonia.Dialogs { Location = path; Items.Clear(); - SelectedItems.Clear(); + Selection.ClearSelection(); try { @@ -301,11 +305,23 @@ namespace Avalonia.Dialogs if (initialSelectionName != null) { - var sel = Items.FirstOrDefault(i => i.ItemType == ManagedFileChooserItemType.File && i.DisplayName == initialSelectionName); + var index = -1; + + for (var i = 0; i < Items.Count; ++i) + { + var item = Items[i]; + + if (item.ItemType == ManagedFileChooserItemType.File && + item.DisplayName == initialSelectionName) + { + index = i; + break; + } + } - if (sel != null) + if (index != -1) { - SelectedItems.Add(sel); + Selection.Select(index); } } @@ -361,7 +377,11 @@ namespace Avalonia.Dialogs } else { - CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + CompleteRequested?.Invoke( + Selection.SelectedItems + .Cast() + .Select(i => i.Path) + .ToArray()); } } diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index b16ac6bb8e..8de762a99b 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -275,7 +275,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Selected_Item_Changes_To_NextAvailable_Item_If_SelectedItem_Is_Removed_From_Middle() + public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle() { var items = new ObservableCollection { @@ -298,8 +298,8 @@ namespace Avalonia.Controls.UnitTests items.RemoveAt(1); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("FooBar", target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("Foo", target.SelectedItem); } private Control CreateTemplate(Carousel control, INameScope scope) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 8c16dd0f70..5f4af6600c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -15,7 +14,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; -using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -245,7 +243,7 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] - public void Setting_SelectedItems_Before_Initialize_Should_Retain() + public void Setting_Selection_Before_Initialize_Should_Retain_Selection() { var listBox = new ListBox { @@ -254,21 +252,18 @@ namespace Avalonia.Controls.UnitTests.Primitives }; var selected = new[] { "foo", "bar" }; - - foreach (var v in selected) - { - listBox.SelectedItems.Add(v); - } + + listBox.Selection.SelectRange(new IndexPath(0), new IndexPath(1)); listBox.BeginInit(); listBox.EndInit(); - Assert.Equal(selected, listBox.SelectedItems); + Assert.Equal(selected, listBox.Selection.SelectedItems); } [Fact] - public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value() + public void Setting_Selection_During_Initialize_Should_Take_Priority_Over_Previous_Value() { var listBox = new ListBox { @@ -278,18 +273,17 @@ namespace Avalonia.Controls.UnitTests.Primitives var selected = new[] { "foo", "bar" }; - foreach (var v in new[] { "bar", "baz" }) - { - listBox.SelectedItems.Add(v); - } + listBox.Selection.SelectRange(new IndexPath(1), new IndexPath(2)); listBox.BeginInit(); - listBox.SelectedItems = new AvaloniaList(selected); + var selection = new SelectionModel { Source = listBox.Items }; + selection.SelectRange(new IndexPath(0), new IndexPath(1)); + listBox.Selection = selection; listBox.EndInit(); - Assert.Equal(selected, listBox.SelectedItems); + Assert.Equal(selected, listBox.Selection.SelectedItems); } [Fact] @@ -554,33 +548,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); } - [Fact] - public void Moving_Selected_Item_Should_Update_Selection() - { - var items = new AvaloniaList - { - new Item(), - new Item(), - }; - - var target = new SelectingItemsControl - { - Items = items, - Template = Template(), - }; - - target.ApplyTemplate(); - target.SelectedIndex = 0; - - Assert.Equal(items[0], target.SelectedItem); - Assert.Equal(0, target.SelectedIndex); - - items.Move(0, 1); - - Assert.Equal(items[1], target.SelectedItem); - Assert.Equal(1, target.SelectedIndex); - } - [Fact] public void Resetting_Items_Collection_Should_Clear_Selection() { @@ -1101,8 +1068,8 @@ namespace Avalonia.Controls.UnitTests.Primitives items[1] = "Qux"; - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Qux", target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.Null(target.SelectedItem); } [Fact] @@ -1172,23 +1139,23 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() + public void Can_Set_Both_SelectedItem_And_Selection_During_Initialization() { // Issue #2969. var target = new ListBox(); - var selectedItems = new List(); + var selection = new SelectionModel(); target.BeginInit(); target.Template = Template(); target.Items = new[] { "Foo", "Bar", "Baz" }; - target.SelectedItems = selectedItems; + target.Selection = selection; target.SelectedItem = "Bar"; target.EndInit(); Assert.Equal("Bar", target.SelectedItem); Assert.Equal(1, target.SelectedIndex); - Assert.Same(selectedItems, target.SelectedItems); - Assert.Equal(new[] { "Bar" }, selectedItems); + Assert.Same(selection, target.Selection); + Assert.Equal(new[] { "Bar" }, selection.SelectedItems); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index a7010c521b..eb6b10fb44 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -78,8 +78,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedIndex = 2; items.RemoveAt(2); - Assert.Equal(2, target.SelectedIndex); - Assert.Equal("qux", target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("foo", target.SelectedItem); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 3a8c98983f..773582b912 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private MouseTestHelper _helper = new MouseTestHelper(); [Fact] - public void Setting_SelectedIndex_Should_Add_To_SelectedItems() + public void Setting_SelectedIndex_Should_Add_To_Selected_Items() { var target = new TestSelector { @@ -34,11 +34,11 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.SelectedIndex = 1; - Assert.Equal(new[] { "bar" }, target.SelectedItems.Cast().ToList()); + Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems.Cast().ToList()); } [Fact] - public void Adding_SelectedItems_Should_Set_SelectedIndex() + public void Adding_To_Selection_Should_Set_SelectedIndex() { var target = new TestSelector { @@ -47,13 +47,58 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems.Add("bar"); + target.Selection.Select(1); Assert.Equal(1, target.SelectedIndex); } [Fact] - public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() + public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar" }, + Template = Template(), + }; + + var oldSelection = target.Selection; + + target.Selection = null; + + Assert.NotNull(target.Selection); + Assert.NotSame(oldSelection, target.Selection); + } + + [Fact] + public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar" }, + Template = Template(), + }; + + var selection = new SelectionModel { Source = new[] { "baz" } }; + Assert.Throws(() => target.Selection = selection); + } + + [Fact] + public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_Source() + { + var target = new TestSelector + { + Items = new[] { "foo", "bar" }, + Template = Template(), + }; + + var selection = new SelectionModel(); + target.Selection = selection; + + Assert.Same(target.Items, selection.Source); + } + + [Fact] + public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex() { var target = new TestSelector { @@ -63,15 +108,18 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems = new AvaloniaList("bar"); + + var selection = new SelectionModel { Source = target.Items }; + selection.Select(1); + target.Selection = selection; Assert.Equal(1, target.SelectedIndex); - Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems); Assert.Equal(new[] { 1 }, SelectedContainers(target)); } [Fact] - public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex() + public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex() { // Note that we don't need SelectionMode = Multiple here. Multiple selections can always // be made in code. @@ -83,15 +131,18 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + + var selection = new SelectionModel { Source = target.Items }; + selection.SelectRange(new IndexPath(0), new IndexPath(2)); + target.Selection = selection; Assert.Equal(0, target.SelectedIndex); - Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); } [Fact] - public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set() + public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_Selected_Items_Are_Set() { // Issue #2565. var target = new TestSelector @@ -101,16 +152,16 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + target.SelectAll(); target.Presenter.ApplyTemplate(); Assert.Equal(0, target.SelectedIndex); - Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); } [Fact] - public void Reassigning_SelectedItems_Should_Clear_Selection() + public void Reassigning_Selection_Should_Clear_Selection() { var target = new TestSelector { @@ -119,15 +170,15 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems.Add("bar"); - target.SelectedItems = new AvaloniaList(); + target.Selection.Select(1); + target.Selection = new SelectionModel(); Assert.Equal(-1, target.SelectedIndex); Assert.Null(target.SelectedItem); } [Fact] - public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Changed() + public void Adding_First_Selected_Item_Should_Raise_SelectedIndex_SelectedItem_Changed() { var target = new TestSelector { @@ -148,14 +199,14 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems.Add("bar"); + target.Selection.Select(1); Assert.True(indexRaised); Assert.True(itemRaised); } [Fact] - public void Adding_Subsequent_SelectedItems_Should_Not_Raise_SelectedIndex_SelectedItem_Changed() + public void Adding_Subsequent_Selected_Items_Should_Not_Raise_SelectedIndex_SelectedItem_Changed() { var target = new TestSelector { @@ -164,14 +215,14 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems.Add("foo"); + target.Selection.Select(0); bool raised = false; - target.PropertyChanged += (s, e) => + target.PropertyChanged += (s, e) => raised |= e.Property.Name == "SelectedIndex" || e.Property.Name == "SelectedItem"; - target.SelectedItems.Add("bar"); + target.Selection.Select(1); Assert.False(raised); } @@ -186,21 +237,21 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems.Add("foo"); + target.Selection.Select(0); bool raised = false; - target.PropertyChanged += (s, e) => - raised |= e.Property.Name == "SelectedIndex" && - (int)e.OldValue == 0 && + target.PropertyChanged += (s, e) => + raised |= e.Property.Name == "SelectedIndex" && + (int)e.OldValue == 0 && (int)e.NewValue == -1; - target.SelectedItems.RemoveAt(0); + target.Selection.Deselect(0); Assert.True(raised); } [Fact] - public void Adding_SelectedItems_Should_Set_Item_IsSelected() + public void Adding_Selected_Items_Should_Set_Item_IsSelected() { var items = new[] { @@ -217,8 +268,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems.Add(items[0]); - target.SelectedItems.Add(items[1]); + target.Selection.Select(0); + target.Selection.Select(1); var foo = target.Presenter.Panel.Children[0]; @@ -228,7 +279,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Assigning_SelectedItems_Should_Set_Item_IsSelected() + public void Assigning_Selection_Should_Set_Item_IsSelected() { var items = new[] { @@ -245,7 +296,10 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems = new AvaloniaList { items[0], items[1] }; + + var selection = new SelectionModel { Source = items }; + selection.SelectRange(new IndexPath(0), new IndexPath(1)); + target.Selection = selection; Assert.True(items[0].IsSelected); Assert.True(items[1].IsSelected); @@ -253,7 +307,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Removing_SelectedItems_Should_Clear_Item_IsSelected() + public void Removing_Selected_Items_Should_Clear_Item_IsSelected() { var items = new[] { @@ -270,40 +324,14 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems.Add(items[0]); - target.SelectedItems.Add(items[1]); - target.SelectedItems.Remove(items[1]); + target.Selection.Select(0); + target.Selection.Select(1); + target.Selection.Deselect(1); Assert.True(items[0].IsSelected); Assert.False(items[1].IsSelected); } - [Fact] - public void Reassigning_SelectedItems_Should_Clear_Item_IsSelected() - { - var items = new[] - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }; - - var target = new TestSelector - { - Items = items, - Template = Template(), - }; - - target.ApplyTemplate(); - target.SelectedItems.Add(items[0]); - target.SelectedItems.Add(items[1]); - - target.SelectedItems = new AvaloniaList { items[0], items[1] }; - - Assert.False(items[0].IsSelected); - Assert.False(items[1].IsSelected); - } - [Fact] public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers() { @@ -316,8 +344,8 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems.Add("foo"); - target.SelectedItems.Add("bar"); + target.Selection.Select(0); + target.Selection.Select(1); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); @@ -348,7 +376,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedIndex = 1; target.SelectRange(3); - Assert.Equal(new[] { "bar", "baz", "qux" }, target.SelectedItems.Cast().ToList()); + Assert.Equal(new[] { "bar", "baz", "qux" }, target.Selection.SelectedItems.Cast().ToList()); } [Fact] @@ -373,7 +401,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedIndex = 3; target.SelectRange(1); - Assert.Equal(new[] { "qux", "baz", "bar" }, target.SelectedItems.Cast().ToList()); + Assert.Equal(new[] { "bar", "baz", "qux" }, target.Selection.SelectedItems.Cast().ToList()); } [Fact] @@ -399,7 +427,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectRange(5); target.SelectRange(4); - Assert.Equal(new[] { "baz", "qux", "qiz" }, target.SelectedItems.Cast().ToList()); + Assert.Equal(new[] { "baz", "qux", "qiz" }, target.Selection.SelectedItems.Cast().ToList()); } [Fact] @@ -462,7 +490,9 @@ namespace Avalonia.Controls.UnitTests.Primitives var selectedIndexes = new List(); target.GetObservable(TestSelector.SelectedIndexProperty).Subscribe(x => selectedIndexes.Add(x)); - target.SelectedItems = new AvaloniaList { "bar", "baz" }; + var selection = new SelectionModel { Source = target.Items }; + selection.SelectRange(new IndexPath(1), new IndexPath(2)); + target.Selection = selection; target.SelectedItem = "foo"; Assert.Equal(0, target.SelectedIndex); @@ -480,11 +510,11 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems.Add("foo"); - target.SelectedItems.Add("bar"); + target.Selection.Select(0); + target.Selection.Select(1); Assert.Equal(0, target.SelectedIndex); - Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems); + Assert.Equal(new[] { "foo", "bar" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); var raised = false; @@ -499,7 +529,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); Assert.Equal(1, target.SelectedIndex); - Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems); Assert.Equal(new[] { 1 }, SelectedContainers(target)); } @@ -508,21 +538,19 @@ namespace Avalonia.Controls.UnitTests.Primitives /// /// /// - Items is bound to DataContext first, followed by say SelectedIndex - /// - When the ListBox is removed from the visual tree, DataContext becomes null (as it's - /// inherited) - /// - This changes Items to null, which changes SelectedIndex to null as there are no - /// longer any items - /// - However, the news that DataContext is now null hasn't yet reached the SelectedItems - /// binding and so the unselection is sent back to the ViewModel + /// - When the ListBox is removed from the tree, DataContext becomes null (as it's inherited) + /// - This changes Items and Selection to null + /// - However, the news that DataContext is now null reaches the Selection after the change to + /// Items, so the SelectionModel.Source is set to null /// /// This is a similar problem to that tested by XamlBindingTest.Should_Not_Write_To_Old_DataContext. /// However, that tests a general property binding problem: here we are writing directly - /// to the SelectedItems collection - not via a binding - so it's something that the - /// binding system cannot solve. Instead we solve it by not clearing SelectedItems when - /// DataContext is in the process of changing. + /// to the SelectionModel - not via a binding - so it's something that the binding system cannot + /// solve. Instead we solve it by not clearing SelectedItems when DataContext is in the process of + /// changing. /// [Fact] - public void Should_Not_Write_To_Old_DataContext() + public void Should_Not_Reset_Selection_Source_When_DataContext_Changes() { var vm = new OldDataContextViewModel(); var target = new TestSelector(); @@ -533,33 +561,38 @@ namespace Avalonia.Controls.UnitTests.Primitives Mode = BindingMode.OneWay, }; - var selectedItemsBinding = new Binding + var selectionBinding = new Binding { - Path = "SelectedItems", + Path = "Selection", Mode = BindingMode.OneWay, }; // Bind Items and SelectedItems to the VM. target.Bind(TestSelector.ItemsProperty, itemsBinding); - target.Bind(TestSelector.SelectedItemsProperty, selectedItemsBinding); + target.Bind(TestSelector.SelectionProperty, selectionBinding); // Set DataContext and SelectedIndex target.DataContext = vm; target.SelectedIndex = 1; - // Make sure SelectedItems are written back to VM. - Assert.Equal(new[] { "bar" }, vm.SelectedItems); + // Make sure selection is written back to VM. + Assert.Same(vm.Selection, target.Selection); + Assert.Equal(new[] { "bar" }, vm.Selection.SelectedItems); + + vm.Selection.SelectionChanged += (s, e) => { }; - // Clear DataContext and ensure that SelectedItems is still set in the VM. + // Clear DataContext and ensure that selection is still set in the VM. target.DataContext = null; - Assert.Equal(new[] { "bar" }, vm.SelectedItems); + Assert.Same(vm.Items, vm.Selection.Source); + Assert.NotSame(vm.Selection, target.Selection); + Assert.Equal(new[] { "bar" }, vm.Selection.SelectedItems); - // Ensure target's SelectedItems is now clear. - Assert.Empty(target.SelectedItems); + // Ensure target's selection is now clear. + Assert.Empty(target.Selection.SelectedItems); } [Fact] - public void Unbound_SelectedItems_Should_Be_Cleared_When_DataContext_Cleared() + public void Unbound_Selected_Items_Should_Be_Cleared_When_DataContext_Cleared() { var data = new { @@ -577,14 +610,14 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Same(data.Items, target.Items); - target.SelectedItems.Add("bar"); + target.Selection.Select(1); target.DataContext = null; - Assert.Empty(target.SelectedItems); + Assert.Empty(target.Selection.SelectedItems); } [Fact] - public void Adding_To_SelectedItems_Should_Raise_SelectionChanged() + public void Adding_Selected_Items_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; @@ -604,13 +637,13 @@ namespace Avalonia.Controls.UnitTests.Primitives called = true; }; - target.SelectedItems.Add("bar"); + target.Selection.Select(1); Assert.True(called); } [Fact] - public void Removing_From_SelectedItems_Should_Raise_SelectionChanged() + public void Removing_Selected_Items_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; @@ -630,13 +663,13 @@ namespace Avalonia.Controls.UnitTests.Primitives called = true; }; - target.SelectedItems.Remove("bar"); + target.Selection.Deselect(1); Assert.True(called); } [Fact] - public void Assigning_SelectedItems_Should_Raise_SelectionChanged() + public void Assigning_Selection_Should_Raise_SelectionChanged() { var items = new[] { "foo", "bar", "baz" }; @@ -647,22 +680,35 @@ namespace Avalonia.Controls.UnitTests.Primitives SelectedItem = "bar", }; - var called = false; + var raised = 0; target.SelectionChanged += (s, e) => { - Assert.Equal(new[] { "foo", "baz" }, e.AddedItems.Cast()); - Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast()); - called = true; + if (raised == 0) + { + Assert.Empty(e.AddedItems.Cast()); + Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast()); + } + else + { + Assert.Equal(new[] { "foo", "baz" }, e.AddedItems.Cast()); + Assert.Empty(e.RemovedItems.Cast()); + } + + ++raised; }; target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems = new AvaloniaList("foo", "baz"); - Assert.True(called); + var selection = new SelectionModel { Source = items }; + selection.Select(0); + selection.Select(2); + target.Selection = selection; + + Assert.Equal(2, raised); } - + [Fact] public void Shift_Selecting_From_No_Selection_Selects_From_Start() { @@ -679,7 +725,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var panel = target.Presenter.Panel; - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); } @@ -752,13 +798,13 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.Selection.SelectedItems); _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); Assert.Equal(2, target.SelectedIndex); Assert.Equal("Baz", target.SelectedItem); - Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + Assert.Equal(new[] { "Baz", "Qux" }, target.Selection.SelectedItems); } [Fact] @@ -802,7 +848,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var panel = target.Presenter.Panel; - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar" }, target.Selection.SelectedItems); Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); } @@ -823,7 +869,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var panel = target.Presenter.Panel; - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); } @@ -844,7 +890,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var panel = target.Presenter.Panel; - Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); } @@ -891,6 +937,7 @@ namespace Avalonia.Controls.UnitTests.Primitives receivedArgs = null; _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: InputModifiers.Shift); + VerifyRemoved("Qux"); } @@ -908,19 +955,19 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - Assert.Equal(new[] { "Foo" }, target.SelectedItems); + Assert.Equal(new[] { "Foo" }, target.Selection.SelectedItems); _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: InputModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar" }, target.Selection.SelectedItems); _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: InputModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Foo", "Bar" }, target.Selection.SelectedItems); _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Control); - Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.Selection.SelectedItems); } [Fact] @@ -1000,7 +1047,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); target.SelectAll(); - Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.Selection.SelectedItems); } [Fact] @@ -1028,7 +1075,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedIndex); Assert.Equal("Foo", target.SelectedItem); - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); } @@ -1055,13 +1102,13 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedIndex = 1; target.SelectRange(2); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Bar", "Baz" }, target.Selection.SelectedItems); items.RemoveAt(0); Assert.Equal(0, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Bar", "Baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } @@ -1090,7 +1137,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(0, target.SelectedIndex); Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Bar", "Baz" }, target.Selection.SelectedItems); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } @@ -1117,7 +1164,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectAll(); items[1] = "Qux"; - Assert.Equal(new[] { "Foo", "Qux", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "Foo", "Baz" }, target.Selection.SelectedItems); } [Fact] @@ -1135,12 +1182,12 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); target.SelectAll(); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.Selection.SelectedItems.Count); _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - Assert.Equal(1, target.SelectedItems.Count); - Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(1, target.Selection.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.Selection.SelectedItems); Assert.Equal(new[] { 0 }, SelectedContainers(target)); } @@ -1159,11 +1206,11 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Presenter.ApplyTemplate(); target.SelectAll(); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.Selection.SelectedItems.Count); _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); - Assert.Equal(3, target.SelectedItems.Count); + Assert.Equal(3, target.Selection.SelectedItems.Count); } [Fact] @@ -1182,11 +1229,11 @@ namespace Avalonia.Controls.UnitTests.Primitives _helper.Click((Interactive)target.Presenter.Panel.Children[0]); _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: InputModifiers.Shift); - Assert.Equal(2, target.SelectedItems.Count); + Assert.Equal(2, target.Selection.SelectedItems.Count); _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); - Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); } [Fact] @@ -1212,7 +1259,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(2, target.SelectedIndex); Assert.Equal(items[2], target.SelectedItem); - Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems); + Assert.Equal(new[] { items[2], items[3] }, target.Selection.SelectedItems); } [Fact] @@ -1232,7 +1279,7 @@ namespace Avalonia.Controls.UnitTests.Primitives _helper.Click((Interactive)target.Presenter.Panel.Children[0]); _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Shift); - Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); } [Fact] @@ -1252,7 +1299,29 @@ namespace Avalonia.Controls.UnitTests.Primitives _helper.Click((Interactive)target.Presenter.Panel.Children[0]); _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Control); - Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(1, target.Selection.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Left_Click_With_Bound_SelectedIndex_Should_Not_Clear_Selection() + { + var target = new ListBox + { + DataContext = new SelectionViewModel(), + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + [!ListBox.SelectedIndexProperty] = new Binding(nameof(SelectionViewModel.SelectedIndex)), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Selection.Select(1); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0], modifiers: InputModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar" }, target.Selection.SelectedItems); } private IEnumerable SelectedContainers(SelectingItemsControl target) @@ -1275,13 +1344,15 @@ namespace Avalonia.Controls.UnitTests.Primitives private class TestSelector : SelectingItemsControl { - public static readonly new AvaloniaProperty SelectedItemsProperty = - SelectingItemsControl.SelectedItemsProperty; + public TestSelector() + { + SelectionMode = SelectionMode.Multiple; + } - public new IList SelectedItems + public new ISelectionModel Selection { - get { return base.SelectedItems; } - set { base.SelectedItems = value; } + get => base.Selection; + set => base.Selection = value; } public new SelectionMode SelectionMode @@ -1290,22 +1361,40 @@ namespace Avalonia.Controls.UnitTests.Primitives set { base.SelectionMode = value; } } - public new void SelectAll() => base.SelectAll(); - public new void UnselectAll() => base.UnselectAll(); + public void SelectAll() => Selection.SelectAll(); + public void UnselectAll() => Selection.ClearSelection(); public void SelectRange(int index) => UpdateSelection(index, true, true); public void Toggle(int index) => UpdateSelection(index, true, false, true); } + private class SelectionViewModel : NotifyingBase + { + private int _selectedIndex = -1; + + public int SelectedIndex + { + get => _selectedIndex; + set + { + if (_selectedIndex != value) + { + _selectedIndex = value; + RaisePropertyChanged(nameof(SelectedIndex)); + } + } + } + } + private class OldDataContextViewModel { public OldDataContextViewModel() { Items = new List { "foo", "bar" }; - SelectedItems = new List(); + Selection = new SelectionModel { Source = Items }; } public List Items { get; } - public List SelectedItems { get; } + public SelectionModel Selection { get; } } private class ItemContainer : Control, ISelectable diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs index b4570ec229..707723f809 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Removing_Selected_Should_Select_Next() + public void Removing_Selected_Should_Select_First() { var items = new ObservableCollection() { @@ -99,10 +99,9 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Same(items[1], target.SelectedItem); items.RemoveAt(1); - // Assert for former element [2] now [1] == "3rd" - Assert.Equal(1, target.SelectedIndex); - Assert.Same(items[1], target.SelectedItem); - Assert.Same("3rd", ((TabItem)target.SelectedItem).Name); + Assert.Equal(0, target.SelectedIndex); + Assert.Same(items[0], target.SelectedItem); + Assert.Same("first", ((TabItem)target.SelectedItem).Name); } private Control CreateTabStripTemplate(TabStrip parent, INameScope scope) diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index a9e86d71ee..44084eb444 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Removal_Should_Set_Next_Tab() + public void Removal_Should_Set_First_Tab() { var collection = new ObservableCollection() { @@ -126,8 +126,7 @@ namespace Avalonia.Controls.UnitTests target.SelectedItem = collection[1]; collection.RemoveAt(1); - // compare with former [2] now [1] == "3rd" - Assert.Same(collection[1], target.SelectedItem); + Assert.Same(collection[0], target.SelectedItem); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index eaf9f22406..d7004468c3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -836,31 +836,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal((object)NonControl.StringProperty, txt.Tag); } - [Fact] - public void Binding_To_List_AvaloniaProperty_Is_Operational() - { - using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) - { - var xaml = @" - - -"; - - var window = AvaloniaXamlLoader.Parse(xaml); - var listBox = (ListBox)window.Content; - - var vm = new SelectedItemsViewModel() - { - Items = new string[] { "foo", "bar", "baz" } - }; - - window.DataContext = vm; - - Assert.Equal(vm.Items, listBox.Items); - - Assert.Equal(vm.SelectedItems, listBox.SelectedItems); - } - } +//// [Fact] +//// public void Binding_To_List_AvaloniaProperty_Is_Operational() +//// { +//// using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) +//// { +//// var xaml = @" +//// +//// +////"; + +//// var window = AvaloniaXamlLoader.Parse(xaml); +//// var listBox = (ListBox)window.Content; + +//// var vm = new SelectedItemsViewModel() +//// { +//// Items = new string[] { "foo", "bar", "baz" } +//// }; + +//// window.DataContext = vm; + +//// Assert.Equal(vm.Items, listBox.Items); + +//// Assert.Equal(vm.SelectedItems, listBox.SelectedItems); +//// } +//// } [Fact] public void Element_Whitespace_Should_Be_Trimmed()