diff --git a/src/Avalonia.Controls/ISelectable.cs b/src/Avalonia.Controls/ISelectable.cs index 144adaa2f5..d0da7c4932 100644 --- a/src/Avalonia.Controls/ISelectable.cs +++ b/src/Avalonia.Controls/ISelectable.cs @@ -1,16 +1,9 @@ -using Avalonia.Controls.Primitives; - namespace Avalonia.Controls { /// - /// Interface for objects that are selectable. + /// An interface that is implemented by objects that expose their selection state via a + /// boolean property. /// - /// - /// Controls such as use this interface to indicate the - /// selected control in a list. If changing the control's property - /// should update the selection in a or equivalent, then - /// the control should raise the . - /// public interface ISelectable { /// @@ -18,4 +11,4 @@ namespace Avalonia.Controls /// bool IsSelected { get; set; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1b2d0b1ca6..60b0f8b193 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -424,6 +424,21 @@ namespace Avalonia.Controls } } + /// + /// Called when a container has been fully prepared to display an item. + /// + /// The container control. + /// The item being displayed. + /// The index of the item being displayed. + /// + /// This method will be called when a container has been fully prepared and added to the + /// logical and visual trees, but may be called before a layout pass has completed. It is + /// called immediately before the event is raised. + /// + protected internal virtual void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + } + /// /// Called when the index for a container changes due to an insertion or removal in the /// items collection. @@ -654,6 +669,7 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { + ContainerForItemPreparedOverride(container, item, index); _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); ContainerPrepared?.Invoke(this, new(container, index)); } diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs index 66a46cab4a..0a6873cd59 100644 --- a/src/Avalonia.Controls/ListBoxItem.cs +++ b/src/Avalonia.Controls/ListBoxItem.cs @@ -1,6 +1,7 @@ using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; namespace Avalonia.Controls { @@ -14,7 +15,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - AvaloniaProperty.Register(nameof(IsSelected)); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Initializes static members of the class. diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 06d84e715d..a0dbf33a1d 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -57,7 +57,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - ListBoxItem.IsSelectedProperty.AddOwner(); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 9c060f2258..663a315732 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -104,6 +104,14 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register( nameof(SelectionMode)); + /// + /// Defines the IsSelected attached property. + /// + public static readonly StyledProperty IsSelectedProperty = + AvaloniaProperty.RegisterAttached( + "IsSelected", + defaultBindingMode: BindingMode.TwoWay); + /// /// Defines the property. /// @@ -111,9 +119,8 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); /// - /// Event that should be raised by items that implement to - /// notify the parent that their selection state - /// has changed. + /// Event that should be raised by containers when their selection state changes to notify + /// the parent that their selection state has changed. /// public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register( @@ -302,20 +309,9 @@ namespace Avalonia.Controls.Primitives { get { - if (_updateState?.Selection.HasValue == true) - { - return _updateState.Selection.Value; - } - else - { - if (_selection is null) - { - _selection = CreateDefaultSelectionModel(); - InitializeSelectionModel(_selection); - } - - return _selection; - } + return _updateState?.Selection.HasValue == true ? + _updateState.Selection.Value : + GetOrCreateSelectionModel(); } set { @@ -420,6 +416,21 @@ namespace Avalonia.Controls.Primitives /// The item. public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item)); + /// + /// Gets the value of the on the specified control. + /// + /// The control. + /// The value of the attached property. + public static bool GetIsSelected(Control control) => control.GetValue(IsSelectedProperty); + + /// + /// Gets the value of the on the specified control. + /// + /// The control. + /// The value of the property. + /// The value of the attached property. + public static void SetIsSelected(Control control, bool value) => control.SetValue(IsSelectedProperty, value); + /// /// Tries to get the container that was the source of an event. /// @@ -473,20 +484,36 @@ namespace Avalonia.Controls.Primitives } } - /// - protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index) + protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + // Ensure that the selection model is created at this point so that accessing it in + // ContainerForItemPreparedOverride doesn't cause it to be initialized (which can + // make containers become deselected when they're synced with the empty selection + // mode). + GetOrCreateSelectionModel(); + + base.PrepareContainerForItemOverride(container, item, index); + } + + protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { - base.PrepareContainerForItemOverride(element, item, index); + base.ContainerForItemPreparedOverride(container, item, index); - if ((element as ISelectable)?.IsSelected == true) + // Once the container has been full prepared and added to the tree, any bindings from + // styles or item container themes are guaranteed to be applied. + if (!container.IsSet(IsSelectedProperty)) { - Selection.Select(index); - MarkContainerSelected(element, true); + // The IsSelected property is not set on the container: update the container + // selection based on the current selection as understood by this control. + MarkContainerSelected(container, Selection.IsSelected(index)); } else { - var selected = Selection.IsSelected(index); - MarkContainerSelected(element, selected); + // The IsSelected property is set on the container: there is a style or item + // container theme which has bound the IsSelected property. Update our selection + // based on the selection state of the container. + var containerIsSelected = GetIsSelected(container); + UpdateSelection(index, containerIsSelected, toggleModifier: true); } } @@ -508,8 +535,7 @@ namespace Avalonia.Controls.Primitives KeyboardNavigation.SetTabOnceActiveElement(panel, null); } - if (element is ISelectable) - MarkContainerSelected(element, false); + element.ClearValue(IsSelectedProperty); } /// @@ -874,6 +900,17 @@ namespace Avalonia.Controls.Primitives return false; } + private ISelectionModel GetOrCreateSelectionModel() + { + if (_selection is null) + { + _selection = CreateDefaultSelectionModel(); + InitializeSelectionModel(_selection); + } + + return _selection; + } + private void OnItemsViewSourceChanged(object? sender, EventArgs e) { if (_selection is not null && _updateState is null) @@ -1098,11 +1135,14 @@ namespace Avalonia.Controls.Primitives { if (!_ignoreContainerSelectionChanged && e.Source is Control control && - e.Source is ISelectable selectable && control.Parent == this && - IndexFromContainer(control) != -1) + IndexFromContainer(control) is var index && + index >= 0) { - UpdateSelection(control, selectable.IsSelected); + if (GetIsSelected(control)) + Selection.Select(index); + else + Selection.Deselect(index); } if (e.Source != this) @@ -1112,31 +1152,18 @@ namespace Avalonia.Controls.Primitives } /// - /// Sets a container's 'selected' class or . + /// Sets the on the specified container. /// /// The container. /// Whether the control is selected /// The previous selection state. - private bool MarkContainerSelected(Control container, bool selected) + private void MarkContainerSelected(Control container, bool selected) { + _ignoreContainerSelectionChanged = true; + try { - bool result; - - _ignoreContainerSelectionChanged = true; - - if (container is ISelectable selectable) - { - result = selectable.IsSelected; - selectable.IsSelected = selected; - } - else - { - result = container.Classes.Contains(":selected"); - ((IPseudoClasses)container.Classes).Set(":selected", selected); - } - - return result; + container.SetCurrentValue(IsSelectedProperty, selected); } finally { diff --git a/src/Avalonia.Controls/TabItem.cs b/src/Avalonia.Controls/TabItem.cs index 76680c0420..46265fb5bc 100644 --- a/src/Avalonia.Controls/TabItem.cs +++ b/src/Avalonia.Controls/TabItem.cs @@ -22,7 +22,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - ListBoxItem.IsSelectedProperty.AddOwner(); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Initializes static members of the class. diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 2cf8d941ca..e3a9a05951 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Threading; using Avalonia.VisualTree; @@ -60,7 +61,8 @@ namespace Avalonia.Controls /// static TreeView() { - // HACK: Needed or SelectedItem property will not be found in Release build. + SelectingItemsControl.IsSelectedChangedEvent.AddClassHandler((x, e) => + x.ContainerSelectionChanged(e)); } /// @@ -430,9 +432,8 @@ namespace Avalonia.Controls private void MarkItemSelected(object item, bool selected) { - var container = TreeContainerFromItem(item)!; - - MarkContainerSelected(container, selected); + if (TreeContainerFromItem(item) is Control container) + MarkContainerSelected(container, selected); } private void SelectedItemsAdded(IList items) @@ -487,16 +488,24 @@ namespace Avalonia.Controls protected internal override Control CreateContainerForItemOverride() => new TreeViewItem(); protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is TreeViewItem; - protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index) + protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) { - base.PrepareContainerForItemOverride(container, item, index); + base.ContainerForItemPreparedOverride(container, item, index); - if (item == SelectedItem) + // Once the container has been full prepared and added to the tree, any bindings from + // styles or item container themes are guaranteed to be applied. + if (container.IsSet(SelectingItemsControl.IsSelectedProperty)) { - MarkContainerSelected(container, true); - if (AutoScrollToSelectedItem) - Dispatcher.UIThread.Post(container.BringIntoView); + // The IsSelected property is set on the container: there is a style or item + // container theme which has bound the IsSelected property. Update our selection + // based on the selection state of the container. + var containerIsSelected = SelectingItemsControl.GetIsSelected(container); + UpdateSelectionFromContainer(container, select: containerIsSelected, toggleModifier: true); } + + // The IsSelected property is not set on the container: update the container + // selection based on the current selection as understood by this control. + MarkContainerSelected(container, SelectedItems.Contains(item)); } /// @@ -663,7 +672,11 @@ namespace Avalonia.Controls var multi = mode.HasAllFlags(SelectionMode.Multiple); var range = multi && rangeModifier && selectedContainer != null; - if (rightButton) + if (!select) + { + SelectedItems.Remove(item); + } + else if (rightButton) { if (!SelectedItems.Contains(item)) { @@ -863,27 +876,44 @@ namespace Avalonia.Controls } /// - /// Sets a container's 'selected' class or . + /// Called when a container raises the + /// . /// - /// The container. - /// Whether the control is selected - private void MarkContainerSelected(Control? container, bool selected) + /// The event. + private void ContainerSelectionChanged(RoutedEventArgs e) { - if (container == null) + if (e.Source is TreeViewItem container && + container.TreeViewOwner == this && + TreeItemFromContainer(container) is object item) { - return; - } + var containerIsSelected = SelectingItemsControl.GetIsSelected(container); + var ourIsSelected = SelectedItems.Contains(item); - if (container is ISelectable selectable) - { - selectable.IsSelected = selected; + if (containerIsSelected != ourIsSelected) + { + if (containerIsSelected) + SelectedItems.Add(item); + else + SelectedItems.Remove(item); + } } - else + + if (e.Source != this) { - ((IPseudoClasses)container.Classes).Set(":selected", selected); + e.Handled = true; } } + /// + /// Sets a container's 'selected' class or . + /// + /// The container. + /// Whether the control is selected + private void MarkContainerSelected(Control container, bool selected) + { + container.SetCurrentValue(SelectingItemsControl.IsSelectedProperty, selected); + } + /// /// Makes a list of objects equal another (though doesn't preserve order). /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 5dbfe49533..806d7e320b 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty IsSelectedProperty = - ListBoxItem.IsSelectedProperty.AddOwner(); + SelectingItemsControl.IsSelectedProperty.AddOwner(); /// /// Defines the property. @@ -105,6 +105,11 @@ namespace Avalonia.Controls EnsureTreeView().PrepareContainerForItemOverride(container, item, index); } + protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index) + { + EnsureTreeView().ContainerForItemPreparedOverride(container, item, index); + } + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs index 7a724b070e..e77a3529b8 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs @@ -1,17 +1,22 @@ +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests { public class ListBoxTests_Multiple { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() { @@ -82,6 +87,468 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); } + [Fact] + public void Shift_Selecting_From_No_Selection_Selects_From_Start() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } + } + + + [Fact] + public void Ctrl_Selecting_Raises_SelectionChanged_Events() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + void VerifyAdded(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } + + void VerifyRemoved(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); + Assert.Empty(receivedArgs.AddedItems); + } + + _helper.Click(target.Presenter.Panel.Children[1]); + + VerifyAdded("Bar"); + + receivedArgs = null; + _helper.Click(target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + + VerifyAdded("Baz"); + + receivedArgs = null; + _helper.Click(target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + + VerifyAdded("Qux"); + + receivedArgs = null; + _helper.Click(target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + + VerifyRemoved("Bar"); + } + } + + [Fact] + public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[1]); + _helper.Click(target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + _helper.Click(target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + + _helper.Click(target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal("Baz", target.SelectedItem); + Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); + } + } + + [Fact] + public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[1]); + _helper.Click(target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + + _helper.Click(target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + } + + [Fact] + public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[3]); + _helper.Click(target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); + } + } + + [Fact] + public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[3]); + _helper.Click(target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); + } + } + + [Fact] + public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[0]); + _helper.Click(target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + + var panel = target.Presenter.Panel; + + Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); + } + } + + [Fact] + public void Shift_Selecting_Raises_SelectionChanged_Events() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + void VerifyAdded(params string[] selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(selection, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } + + void VerifyRemoved(string selection) + { + Assert.NotNull(receivedArgs); + Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); + Assert.Empty(receivedArgs.AddedItems); + } + + _helper.Click(target.Presenter.Panel.Children[1]); + + VerifyAdded("Bar"); + + receivedArgs = null; + _helper.Click(target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); + + VerifyAdded("Baz", "Qux"); + + receivedArgs = null; + _helper.Click(target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + + VerifyRemoved("Qux"); + } + } + + [Fact] + public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[0]); + + Assert.Equal(new[] { "Foo" }, target.SelectedItems); + + _helper.Click(target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + + _helper.Click(target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + + _helper.Click(target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + + Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); + } + } + + [Fact] + public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Click(target.Presenter.Panel.Children[0]); + + Assert.Equal(1, target.SelectedItems.Count); + Assert.Equal(new[] { "Foo", }, target.SelectedItems); + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + } + } + + [Fact] + public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SelectAll(); + + Assert.Equal(3, target.SelectedItems.Count); + + _helper.Click(target.Presenter.Panel.Children[0], MouseButton.Right); + + Assert.Equal(3, target.SelectedItems.Count); + } + } + + [Fact] + public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + _helper.Click(target.Presenter.Panel.Children[0]); + _helper.Click(target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _helper.Click(target.Presenter.Panel.Children[2], MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } + } + + [Fact] + public void Shift_Right_Click_Should_Not_Select_Multiple() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + + _helper.Click(target.Presenter.Panel.Children[0]); + _helper.Click(target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Multiple() + { + using (UnitTestApplication.Start()) + { + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + ItemsSource = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + Width = 100, + Height = 100, + }; + + var root = new TestRoot(target); + root.LayoutManager.ExecuteInitialLayoutPass(); + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + + _helper.Click(target.Presenter.Panel.Children[0]); + _helper.Click(target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + } private Control CreateListBoxTemplate(TemplatedControl parent, INameScope scope) { return new ScrollViewer @@ -119,5 +586,13 @@ namespace Avalonia.Controls.UnitTests // Now the ItemsPresenter should be reigstered, so apply its template. ((Control)target.Presenter).ApplyTemplate(); } + + private static IEnumerable SelectedContainers(SelectingItemsControl target) + { + return target.Presenter.Panel.Children + .Select(x => x.Classes.Contains(":selected") ? target.IndexFromContainer(x) : -1) + .Where(x => x != -1); + } + } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 9574598038..db6460e8aa 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -2170,7 +2170,12 @@ namespace Avalonia.Controls.UnitTests.Primitives private class Item : Control, ISelectable { public string Value { get; set; } - public bool IsSelected { get; set; } + + public bool IsSelected + { + get => SelectingItemsControl.GetIsSelected(this); + set => SelectingItemsControl.SetIsSelected(this, value); + } } private class MasterViewModel : NotifyingBase diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 71bd0933a1..3ef93c7cda 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -116,7 +116,6 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(0, target.SelectedIndex); Assert.Equal("bar", target.SelectedItem); - Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes); } private static FuncControlTemplate Template() diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 1108780d9a..c308a9cc92 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -4,34 +4,31 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; -using Avalonia.Input.Platform; -using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Styling; using Avalonia.UnitTests; -using Moq; +using Avalonia.VisualTree; using Xunit; +#nullable enable + namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests_Multiple { - private MouseTestHelper _helper = new MouseTestHelper(); - [Fact] public void Setting_SelectedIndex_Should_Add_To_SelectedItems() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - target.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(new[] { "bar" }, target.SelectedItems.Cast().ToList()); @@ -40,13 +37,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Adding_SelectedItems_Should_Set_SelectedIndex() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - target.ApplyTemplate(); target.SelectedItems.Add("bar"); Assert.Equal(1, target.SelectedIndex); @@ -55,14 +48,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - target.ApplyTemplate(); - ((Control)target.Presenter).ApplyTemplate(); target.SelectedItems = new AvaloniaList("bar"); Assert.Equal(1, target.SelectedIndex); @@ -73,14 +61,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); Assert.Equal(0, target.SelectedIndex); @@ -92,15 +75,14 @@ namespace Avalonia.Controls.UnitTests.Primitives public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set() { // Issue #2565. - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }, performLayout: false); - target.ApplyTemplate(); + Assert.Null(target.ItemsPanelRoot); target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); - target.Presenter.ApplyTemplate(); + + var root = Assert.IsType(target.GetVisualRoot()); + root.LayoutManager.ExecuteInitialLayoutPass(); Assert.Equal(0, target.SelectedIndex); Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); @@ -110,13 +92,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Reassigning_SelectedItems_Should_Clear_Selection() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - target.ApplyTemplate(); target.SelectedItems.Add("bar"); target.SelectedItems = new AvaloniaList(); @@ -127,25 +105,21 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Adding_First_SelectedItem_Should_Raise_SelectedIndex_SelectedItem_Changed() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); + var indexRaised = false; + var itemRaised = false; - bool indexRaised = false; - bool itemRaised = false; target.PropertyChanged += (s, e) => { indexRaised |= e.Property.Name == "SelectedIndex" && - (int)e.OldValue == -1 && - (int)e.NewValue == 1; + (int)e.OldValue! == -1 && + (int)e.NewValue! == 1; itemRaised |= e.Property.Name == "SelectedItem" && - (string)e.OldValue == null && - (string)e.NewValue == "bar"; + (string?)e.OldValue == null && + (string?)e.NewValue == "bar"; }; - target.ApplyTemplate(); target.SelectedItems.Add("bar"); Assert.True(indexRaised); @@ -155,17 +129,13 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Adding_Subsequent_SelectedItems_Should_Not_Raise_SelectedIndex_SelectedItem_Changed() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - target.ApplyTemplate(); target.SelectedItems.Add("foo"); bool raised = false; - target.PropertyChanged += (s, e) => + target.PropertyChanged += (s, e) => raised |= e.Property.Name == "SelectedIndex" || e.Property.Name == "SelectedItem"; @@ -177,20 +147,16 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Removing_Last_SelectedItem_Should_Raise_SelectedIndex_Changed() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - target.ApplyTemplate(); target.SelectedItems.Add("foo"); bool raised = false; - target.PropertyChanged += (s, e) => - raised |= e.Property.Name == "SelectedIndex" && - (int)e.OldValue == 0 && - (int)e.NewValue == -1; + target.PropertyChanged += (s, e) => + raised |= e.Property.Name == "SelectedIndex" && + (int)e.OldValue! == 0 && + (int)e.NewValue! == -1; target.SelectedItems.RemoveAt(0); @@ -200,23 +166,19 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Adding_SelectedItems_Should_Set_Item_IsSelected() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - Items = - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }, - Template = Template(), + new ListBoxItem(), + new ListBoxItem(), + new ListBoxItem(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = CreateTarget(items: items); + target.SelectedItems.Add(target.Items[0]); target.SelectedItems.Add(target.Items[1]); - var items = target.Items.Cast().ToList(); Assert.True(items[0].IsSelected); Assert.True(items[1].IsSelected); Assert.False(items[2].IsSelected); @@ -225,22 +187,18 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_SelectedItems_Should_Set_Item_IsSelected() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - Items = - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }, - Template = Template(), + new ListBoxItem(), + new ListBoxItem(), + new ListBoxItem(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectedItems = new AvaloniaList { target.Items[0], target.Items[1] }; + var target = CreateTarget(items: items); + + target.SelectedItems = new AvaloniaList { items[0], items[1] }; - var items = target.Items.Cast().ToList(); Assert.True(items[0].IsSelected); Assert.True(items[1].IsSelected); Assert.False(items[2].IsSelected); @@ -249,64 +207,51 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Removing_SelectedItems_Should_Clear_Item_IsSelected() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - Items = - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }, - Template = Template(), + new ListBoxItem(), + new ListBoxItem(), + new ListBoxItem(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectedItems.Add(target.Items[0]); - target.SelectedItems.Add(target.Items[1]); - target.SelectedItems.Remove(target.Items[1]); + var target = CreateTarget(items: items); + + target.SelectedItems.Add(items[0]); + target.SelectedItems.Add(items[1]); + target.SelectedItems.Remove(items[1]); - var items = target.Items.Cast().ToList(); Assert.True(items[0].IsSelected); Assert.False(items[1].IsSelected); } [Fact] - public void Reassigning_SelectedItems_Should_Clear_Item_IsSelected() + public void Reassigning_SelectedItems_Should_Not_Clear_Item_IsSelected() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - Items = - { - new ListBoxItem(), - new ListBoxItem(), - new ListBoxItem(), - }, - Template = Template(), + new ListBoxItem(), + new ListBoxItem(), + new ListBoxItem(), }; - target.ApplyTemplate(); + var target = CreateTarget(items: items); + target.SelectedItems.Add(target.Items[0]); target.SelectedItems.Add(target.Items[1]); + target.SelectedItems = new AvaloniaList { items[0], items[1] }; - target.SelectedItems = new AvaloniaList { target.Items[0], target.Items[1] }; - - var items = target.Items.Cast().ToList(); - Assert.False(items[0].IsSelected); - Assert.False(items[1].IsSelected); + Assert.True(items[0].IsSelected); + Assert.True(items[1].IsSelected); + Assert.False(items[2].IsSelected); } [Fact] public void Setting_SelectedIndex_Should_Unmark_Previously_Selected_Containers() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); target.SelectedItems.Add("foo"); target.SelectedItems.Add("bar"); @@ -321,21 +266,19 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Range_Select_Should_Select_Range() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - ItemsSource = new[] - { - "foo", - "bar", - "baz", - "qux", - "qiz", - "lol", - }, - Template = Template(), + "foo", + "bar", + "baz", + "qux", + "qiz", + "lol", }; - target.ApplyTemplate(); + var target = CreateTarget(items: items); + target.SelectedIndex = 1; target.SelectRange(3); @@ -345,22 +288,19 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Range_Select_Backwards_Should_Select_Range() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - ItemsSource = new[] - { - "foo", - "bar", - "baz", - "qux", - "qiz", - "lol", - }, - SelectionMode = SelectionMode.Multiple, - Template = Template(), + "foo", + "bar", + "baz", + "qux", + "qiz", + "lol", }; - target.ApplyTemplate(); + var target = CreateTarget(items: items); + target.SelectedIndex = 3; target.SelectRange(1); @@ -370,22 +310,19 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Second_Range_Select_Backwards_Should_Select_From_Original_Selection() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - ItemsSource = new[] - { - "foo", - "bar", - "baz", - "qux", - "qiz", - "lol", - }, - SelectionMode = SelectionMode.Multiple, - Template = Template(), + "foo", + "bar", + "baz", + "qux", + "qiz", + "lol", }; - target.ApplyTemplate(); + var target = CreateTarget(items: items); + target.SelectedIndex = 2; target.SelectRange(5); target.SelectRange(4); @@ -396,16 +333,8 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Setting_SelectedIndex_After_Range_Should_Unmark_Previously_Selected_Containers() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz", "qux" }, - Template = Template(), - SelectedIndex = 0, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz", "qux" }); target.SelectRange(2); @@ -419,16 +348,8 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Toggling_Selection_After_Range_Should_Work() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz", "foo", "bar", "baz" }, - Template = Template(), - SelectedIndex = 0, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); target.SelectRange(3); @@ -442,13 +363,8 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Suprious_SelectedIndex_Changes_Should_Not_Be_Triggered() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; - - target.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); var selectedIndexes = new List(); target.GetObservable(TestSelector.SelectedIndexProperty).Subscribe(x => selectedIndexes.Add(x)); @@ -463,14 +379,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Can_Set_SelectedIndex_To_Another_Selected_Item() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar", "baz" }, - Template = Template(), - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedItems.Add("foo"); target.SelectedItems.Add("bar"); @@ -515,8 +426,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Should_Not_Write_SelectedItems_To_Old_DataContext() { + using var app = Start(); var vm = new OldDataContextViewModel(); - var target = new TestSelector(); + var target = CreateTarget(); var itemsBinding = new Binding { @@ -555,8 +467,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Should_Not_Write_SelectionModel_To_Old_DataContext() { + using var app = Start(); var vm = new OldDataContextViewModel(); - var target = new TestSelector(); + var target = CreateTarget(); var itemsBinding = new Binding { @@ -592,17 +505,13 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Unbound_SelectedItems_Should_Be_Cleared_When_DataContext_Cleared() { + using var app = Start(); var data = new { Items = new[] { "foo", "bar", "baz" }, }; - var target = new TestSelector - { - DataContext = data, - Template = Template(), - }; - + var target = CreateTarget(dataContext: data); var itemsBinding = new Binding { Path = "Items" }; target.Bind(TestSelector.ItemsSourceProperty, itemsBinding); @@ -617,15 +526,8 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Adding_To_SelectedItems_Should_Raise_SelectionChanged() { - var items = new[] { "foo", "bar", "baz" }; - - var target = new TestSelector - { - DataContext = items, - Template = Template(), - ItemsSource = items, - }; - + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); var called = false; target.SelectionChanged += (s, e) => @@ -643,17 +545,11 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Removing_From_SelectedItems_Should_Raise_SelectionChanged() { - var items = new[] { "foo", "bar", "baz" }; - - var target = new TestSelector - { - ItemsSource = items, - Template = Template(), - SelectedItem = "bar", - }; - + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); var called = false; + target.SelectedItem = "bar"; target.SelectionChanged += (s, e) => { Assert.Equal(new[] { "bar" }, e.RemovedItems.Cast().ToList()); @@ -669,14 +565,10 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_SelectedItems_Should_Raise_SelectionChanged() { - var items = new[] { "foo", "bar", "baz" }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - var target = new TestSelector - { - ItemsSource = items, - Template = Template(), - SelectedItem = "bar", - }; + target.SelectedItem = "bar"; var called = false; @@ -687,909 +579,699 @@ namespace Avalonia.Controls.UnitTests.Primitives called = true; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); target.SelectedItems = new AvaloniaList("foo", "baz"); Assert.True(called); } - + [Fact] - public void Shift_Selecting_From_No_Selection_Selects_From_Start() + public void SelectAll_Sets_SelectedIndex_And_SelectedItem() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + target.SelectAll(); - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); - } + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("foo", target.SelectedItem); } [Fact] - public void Ctrl_Selecting_Raises_SelectionChanged_Events() + public void SelectAll_Raises_SelectionChanged_Event() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - - SelectionChangedEventArgs receivedArgs = null; - - target.SelectionChanged += (_, args) => receivedArgs = args; - - void VerifyAdded(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } - - void VerifyRemoved(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); - Assert.Empty(receivedArgs.AddedItems); - } + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + SelectionChangedEventArgs? receivedArgs = null; - VerifyAdded("Bar"); - - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + target.SelectionChanged += (_, args) => receivedArgs = args; - VerifyAdded("Baz"); + target.SelectAll(); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + Assert.NotNull(receivedArgs); + Assert.Equal(target.ItemsSource, receivedArgs.AddedItems); + Assert.Empty(receivedArgs.RemovedItems); + } - VerifyAdded("Qux"); + [Fact] + public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() + { + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + target.SelectedIndex = 0; + target.UnselectAll(); - VerifyRemoved("Bar"); - } + Assert.Equal(-1, target.SelectedIndex); + Assert.Equal(null, target.SelectedItem); } [Fact] - public void Ctrl_Selecting_SelectedItem_With_Multiple_Selection_Active_Sets_SelectedItem_To_Next_Selection() + public void SelectAll_Handles_Duplicate_Items() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + target.SelectAll(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + Assert.Equal(new[] { "foo", "bar", "baz", "foo", "bar", "baz" }, target.SelectedItems); + } - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); + [Fact] + public void Adding_Item_Before_SelectedItems_Should_Update_Selection() + { + using var app = Start(); + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = CreateTarget(itemsSource: items); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + target.SelectAll(); + items.Insert(0, "qux"); + Layout(target); - Assert.Equal(2, target.SelectedIndex); - Assert.Equal("Baz", target.SelectedItem); - Assert.Equal(new[] { "Baz", "Qux" }, target.SelectedItems); - } + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); } [Fact] - public void Ctrl_Selecting_Non_SelectedItem_With_Multiple_Selection_Active_Leaves_SelectedItem_The_Same() + public void Removing_Item_Before_SelectedItem_Should_Update_Selection() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + using var app = Start(); + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = CreateTarget(itemsSource: items); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + target.SelectedIndex = 1; + target.SelectRange(2); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); + items.RemoveAt(0); - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - } + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } [Fact] - public void Should_Ctrl_Select_Correct_Item_When_Duplicate_Items_Are_Present() + public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + using var app = Start(); + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = CreateTarget(itemsSource: items); - var panel = target.Presenter.Panel; + target.SelectAll(); + items.RemoveAt(0); - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - Assert.Equal(new[] { 3, 4 }, SelectedContainers(target)); - } + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } [Fact] - public void Should_Shift_Select_Correct_Item_When_Duplicates_Are_Present() + public void Replacing_Selected_Item_Should_Update_SelectedItems() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[3]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + using var app = Start(); + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = CreateTarget(itemsSource: items); - var panel = target.Presenter.Panel; + target.SelectAll(); + items[1] = "qux"; - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 3, 4, 5 }, SelectedContainers(target)); - } + Assert.Equal(new[] { "foo", "baz" }, target.SelectedItems); } [Fact] - public void Can_Shift_Select_All_Items_When_Duplicates_Are_Present() + public void Adding_Selected_ItemContainers_Should_Update_Selection() { - using (UnitTestApplication.Start()) + using var app = Start(); + var items = new[] { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + new TestContainer(), + new TestContainer(), + }; - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[5], modifiers: KeyModifiers.Shift); + var target = CreateTarget(items: items); - var panel = target.Presenter.Panel; + target.Items.Add(new TestContainer { IsSelected = true }); + target.Items.Add(new TestContainer { IsSelected = true }); - Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, SelectedContainers(target)); - } + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(target.Items[2], target.SelectedItem); + Assert.Equal(new[] { target.Items[2], target.Items[3] }, target.SelectedItems); } [Fact] - public void Shift_Selecting_Raises_SelectionChanged_Events() + public void Adding_To_Selection_Should_Set_SelectedIndex() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - SelectionChangedEventArgs receivedArgs = null; + target.SelectedItems.Add("bar"); - target.SelectionChanged += (_, args) => receivedArgs = args; + Assert.Equal(1, target.SelectedIndex); + } - void VerifyAdded(params string[] selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(selection, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } + [Fact] + public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel() + { + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); + var oldSelection = target.Selection; - void VerifyRemoved(string selection) - { - Assert.NotNull(receivedArgs); - Assert.Equal(new[] { selection }, receivedArgs.RemovedItems); - Assert.Empty(receivedArgs.AddedItems); - } + target.Selection = null!; - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); + Assert.NotNull(target.Selection); + Assert.NotSame(oldSelection, target.Selection); + } - VerifyAdded("Bar"); + [Fact] + public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail() + { + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); + var selection = new SelectionModel { Source = new[] { "baz" } }; - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); + Assert.Throws(() => target.Selection = selection); + } - VerifyAdded("Baz", "Qux"); + [Fact] + public void Assigning_SelectionModel_With_Null_Source_To_Selection_Should_Set_Source() + { + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); + var selection = new SelectionModel(); - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Shift); + target.Selection = selection; - VerifyRemoved("Qux"); - } + Assert.Same(target.ItemsSource, selection.Source); } [Fact] - public void Duplicate_Items_Are_Added_To_SelectedItems_In_Order() + public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); + var selection = new SelectionModel { SingleSelect = false }; - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + selection.Select(1); + target.Selection = selection; - Assert.Equal(new[] { "Foo" }, target.SelectedItems); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.Selection.SelectedItems); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + } - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); + [Fact] + public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex() + { + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); + var selection = new SelectionModel { SingleSelect = false }; - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); + selection.SelectRange(0, 2); + target.Selection = selection; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.Selection.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } - Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); + [Fact] + public void Reassigning_Selection_Should_Clear_Selection() + { + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); + target.Selection.Select(1); + target.Selection = new SelectionModel(); - Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); - } + Assert.Equal(-1, target.SelectedIndex); + Assert.Null(target.SelectedItem); } [Fact] - public void SelectAll_Sets_SelectedIndex_And_SelectedItem() + public void Assigning_Selection_Should_Set_Item_IsSelected() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, + new ListBoxItem(), + new ListBoxItem(), + new ListBoxItem(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = CreateTarget(items: items); + var selection = new SelectionModel { SingleSelect = false }; - target.SelectAll(); + selection.SelectRange(0, 1); + target.Selection = selection; - Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); + Assert.True(items[0].IsSelected); + Assert.True(items[1].IsSelected); + Assert.False(items[2].IsSelected); } [Fact] - public void SelectAll_Raises_SelectionChanged_Event() + public void Assigning_Selection_Should_Raise_SelectionChanged() { - var target = new TestSelector - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - SelectionChangedEventArgs receivedArgs = null; - - target.SelectionChanged += (_, args) => receivedArgs = args; - - target.SelectAll(); + using var app = Start(); + var items = new[] { "foo", "bar", "baz" }; + var target = CreateTarget(itemsSource: items); + var raised = 0; - Assert.NotNull(receivedArgs); - Assert.Equal(target.ItemsSource, receivedArgs.AddedItems); - Assert.Empty(receivedArgs.RemovedItems); - } + target.SelectedItem = "bar"; - [Fact] - public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() - { - var target = new TestSelector + target.SelectionChanged += (s, e) => { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - SelectedIndex = 0, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - target.UnselectAll(); - - Assert.Equal(-1, target.SelectedIndex); - Assert.Equal(null, target.SelectedItem); - } + 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()); + } - [Fact] - public void SelectAll_Handles_Duplicate_Items() - { - var target = new TestSelector - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, + ++raised; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.SelectAll(); + var selection = new SelectionModel { Source = items, SingleSelect = false }; + selection.Select(0); + selection.Select(2); + target.Selection = selection; - Assert.Equal(new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(2, raised); } [Fact] - public void Adding_Item_Before_SelectedItems_Should_Update_Selection() + public void Can_Bind_Initial_Selected_State_Via_ItemContainerTheme() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "Baz" - }; - - var target = new ListBox + using var app = Start(); + var items = new ItemViewModel[] { new("Item 0", true), new("Item 1", false), new("Item 2", true) }; + var itemTheme = new ControlTheme(typeof(ContentPresenter)) { - Template = Template(), - ItemsSource = items, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } }; - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme); - target.SelectAll(); - items.Insert(0, "Qux"); - root.LayoutManager.ExecuteLayoutPass(); - - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); + Assert.Equal(new[] { 0, 2 }, SelectedContainers(target)); + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(new[] { 0, 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[0], items[2] }, target.Selection.SelectedItems); } [Fact] - public void Removing_Item_Before_SelectedItem_Should_Update_Selection() + public void Can_Bind_Initial_Selected_State_Via_Style() { - var items = new ObservableCollection + using var app = Start(); + var items = new ItemViewModel[] { new("Item 0", true), new("Item 1", false), new("Item 2", true) }; + var style = new Style(x => x.OfType()) { - "Foo", - "Bar", - "Baz" - }; - - var target = new TestSelector - { - Template = Template(), - ItemsSource = items, - SelectionMode = SelectionMode.Multiple, + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - target.SelectedIndex = 1; - target.SelectRange(2); - - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); - - items.RemoveAt(0); + var target = CreateTarget(itemsSource: items, styles: new[] { style }); + Assert.Equal(new[] { 0, 2 }, SelectedContainers(target)); Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(new[] { 0, 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[0], items[2] }, target.Selection.SelectedItems); } [Fact] - public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection() + public void Selection_State_Is_Updated_Via_IsSelected_Binding() { - var items = new ObservableCollection + using var app = Start(); + var items = new ItemViewModel[] { new("Item 0", true), new("Item 1", false), new("Item 2", true) }; + var itemTheme = new ControlTheme(typeof(TestContainer)) { - "Foo", - "Bar", - "Baz" + BasedOn = CreateTestContainerTheme(), + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } }; - var target = new ListBox - { - Template = Template(), - ItemsSource = items, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + // For the container selection state to be communicated back to the SelectingItemsControl + // we need a container which raises the SelectingItemsControl.IsSelectedChangedEvent when + // the IsSelected property changes. + var target = CreateTarget( + itemsSource: items, + itemContainerTheme: itemTheme); - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - target.SelectAll(); - items.RemoveAt(0); + items[1].IsSelected = true; + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); - Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); - } - - [Fact] - public void Replacing_Selected_Item_Should_Update_SelectedItems() - { - var items = new ObservableCollection - { - "Foo", - "Bar", - "Baz" - }; + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(new[] { 0, 1, 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[0], items[1], items[2] }, target.Selection.SelectedItems); - var target = new ListBox - { - Template = Template(), - ItemsSource = items, - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + items[0].IsSelected = false; - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - target.SelectAll(); - items[1] = "Qux"; - - Assert.Equal(new[] { "Foo", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { 1, 2 }, SelectedContainers(target)); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(new[] { 1, 2 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[1], items[2] }, target.Selection.SelectedItems); } [Fact] - public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() + public void Selection_State_Is_Written_Back_To_Item_Via_IsSelected_Binding() { - using (UnitTestApplication.Start()) + using var app = Start(); + var items = new ItemViewModel[] { new("Item 0", true), new("Item 1", false), new("Item 2", true) }; + var itemTheme = new ControlTheme(typeof(ContentPresenter)) { - var target = new ListBox + Setters = { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } + }; - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme); + var container0 = Assert.IsAssignableFrom(target.ContainerFromIndex(0)); + var container1 = Assert.IsAssignableFrom(target.ContainerFromIndex(1)); - target.SelectAll(); + SelectingItemsControl.SetIsSelected(container1, true); - Assert.Equal(3, target.SelectedItems.Count); + Assert.True(items[1].IsSelected); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + SelectingItemsControl.SetIsSelected(container0, false); - Assert.Equal(1, target.SelectedItems.Count); - Assert.Equal(new[] { "Foo", }, target.SelectedItems); - Assert.Equal(new[] { 0 }, SelectedContainers(target)); - } + Assert.False(items[0].IsSelected); } [Fact] - public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + public void Selection_Is_Updated_On_Container_Realization_With_IsSelected_Binding() { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - target.SelectAll(); - - Assert.Equal(3, target.SelectedItems.Count); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + using var app = Start(); + var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList(); + items[0].IsSelected = true; + items[15].IsSelected = true; - Assert.Equal(3, target.SelectedItems.Count); - } - } - - [Fact] - public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() - { - using (UnitTestApplication.Start()) + var itemTheme = new ControlTheme(typeof(ContentPresenter)) { - var target = new ListBox + Setters = { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + new Setter(Control.HeightProperty, 100.0), + } + }; - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + // Create a SelectingItemsControl with a virtualizing stack panel. + var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme, virtualizing: true); + var panel = Assert.IsType(target.ItemsPanelRoot); + var scroll = panel.FindAncestorOfType()!; - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Shift); + // The SelectingItemsControl does not yet know anything about item 15's selection state. + Assert.Equal(new[] { 0 }, SelectedContainers(target)); + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(new[] { 0 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[0] }, target.Selection.SelectedItems); - Assert.Equal(2, target.SelectedItems.Count); + // Scroll item 15 into view. + scroll.Offset = new(0, 1000); + Layout(target); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); + Assert.Equal(10, panel.FirstRealizedIndex); + Assert.Equal(19, panel.LastRealizedIndex); - Assert.Equal(1, target.SelectedItems.Count); - } + // The final selection should be in place. + Assert.True(items[0].IsSelected); + Assert.True(items[15].IsSelected); + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(new[] { 0, 15 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[0], items[15] }, target.Selection.SelectedItems); + + // Although item 0 is selected, it's not realized. + Assert.Equal(new[] { 15 }, SelectedContainers(target)); } [Fact] - public void Adding_Selected_ItemContainers_Should_Update_Selection() + public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding() { - var target = new TestSelector + using var app = Start(); + var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList(); + var itemTheme = new ControlTheme(typeof(ContentPresenter)) { - Items = + Setters = { - new ItemContainer(), - new ItemContainer(), - }, - SelectionMode = SelectionMode.Multiple, - Template = Template(), + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + new Setter(Control.HeightProperty, 100.0), + } }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - target.Items.Add(new ItemContainer { IsSelected = true }); - target.Items.Add(new ItemContainer { IsSelected = true }); + // Create a SelectingItemsControl with a virtualizing stack panel. + var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme, virtualizing: true); + var panel = Assert.IsType(target.ItemsPanelRoot); + var scroll = panel.FindAncestorOfType()!; - Assert.Equal(2, target.SelectedIndex); - Assert.Equal(target.Items[2], target.SelectedItem); - Assert.Equal(new[] { target.Items[2], target.Items[3] }, target.SelectedItems); - } + // Scroll item 1 out of view. + scroll.Offset = new(0, 1000); + Layout(target); - [Fact] - public void Shift_Right_Click_Should_Not_Select_Multiple() - { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + Assert.Equal(10, panel.FirstRealizedIndex); + Assert.Equal(19, panel.LastRealizedIndex); - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + // Select item 1 now it's unrealized. + items[1].IsSelected = true; - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); + // The SelectingItemsControl does not yet know anything about the selection change. + Assert.Empty(SelectedContainers(target)); + Assert.Equal(-1, target.SelectedIndex); + Assert.Null(target.SelectedItem); + Assert.Empty(target.Selection.SelectedIndexes); + Assert.Empty(target.Selection.SelectedItems); - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Shift); + // Scroll item 1 back into view. + scroll.Offset = new(0, 0); + Layout(target); - Assert.Equal(1, target.SelectedItems.Count); - } + // The item and container should be marked as selected. + Assert.True(items[1].IsSelected); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + Assert.Equal(1, target.SelectedIndex); + Assert.Equal(items[1], target.SelectedItem); + Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes); + Assert.Equal(new[] { items[1] }, target.Selection.SelectedItems); } - [Fact] - public void Ctrl_Right_Click_Should_Not_Select_Multiple() + private static IEnumerable SelectedContainers(SelectingItemsControl target) { - using (UnitTestApplication.Start()) - { - var target = new ListBox - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); + Assert.NotNull(target.ItemsPanel); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - - _helper.Click((Interactive)target.Presenter.Panel.Children[0]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: KeyModifiers.Control); - - Assert.Equal(1, target.SelectedItems.Count); - } + return target.ItemsPanelRoot!.Children + .Select(x => SelectingItemsControl.GetIsSelected(x) ? target.IndexFromContainer(x) : -1) + .Where(x => x != -1); } - [Fact] - public void Adding_To_Selection_Should_Set_SelectedIndex() - { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), + private static TestSelector CreateTarget( + object? dataContext = null, + IList? items = null, + IList? itemsSource = null, + ControlTheme? itemContainerTheme = null, + IDataTemplate? itemTemplate = null, + IEnumerable