From 594346e0a520d95b3e810ace6120bea91022df57 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Apr 2023 15:33:46 +0200 Subject: [PATCH 1/5] Make IsSelected an attached property. Make `IsSelected` an attached property on `SelectingItemsControl` and make it bind two-way. Follows the pattern defined in WPF. --- src/Avalonia.Controls/ListBoxItem.cs | 3 ++- src/Avalonia.Controls/MenuItem.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 23 +++++++++++++++++++ src/Avalonia.Controls/TabItem.cs | 2 +- src/Avalonia.Controls/TreeViewItem.cs | 2 +- 5 files changed, 28 insertions(+), 4 deletions(-) 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..310626cbb0 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. /// @@ -420,6 +428,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. /// 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/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 5dbfe49533..1bea605dc3 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. From e6098223118f0054311034cbb2a91e848aced9cb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 5 Apr 2023 16:42:22 +0200 Subject: [PATCH 2/5] Modernized some SelectingItemsControl tests. And moved some of the tests that were specific to user interaction to `ListBoxTests` as `SelectingItemsControl` doesn't define user interactions. --- .../ListBoxTests_Multiple.cs | 475 +++++++ .../SelectingItemsControlTests_Multiple.cs | 1242 +++++------------ 2 files changed, 806 insertions(+), 911 deletions(-) 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_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 1108780d9a..0ad8db43b2 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -10,28 +10,24 @@ 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 +36,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 +47,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 +60,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 +74,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 +91,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 +104,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,13 +128,9 @@ 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; @@ -177,20 +146,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; + (int)e.OldValue! == 0 && + (int)e.NewValue! == -1; target.SelectedItems.RemoveAt(0); @@ -200,23 +165,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 +186,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 +206,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 +265,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 +287,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 +309,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 +332,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 +347,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 +362,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 +378,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 +425,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 +466,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 +504,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 +525,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 +544,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 +564,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,366 +578,30 @@ 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() - { - 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(); - - AvaloniaLocator.CurrentMutable.Bind().ToConstant(new Mock().Object); - _helper.Click((Interactive)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 = 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); - } - - _helper.Click((Interactive)target.Presenter.Panel.Children[1]); - - VerifyAdded("Bar"); - - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - - VerifyAdded("Baz"); - - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - - VerifyAdded("Qux"); - - receivedArgs = null; - _helper.Click((Interactive)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 = 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); - _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(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz", "Qux" }, target.SelectedItems); - - _helper.Click((Interactive)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 = Template(), - 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((Interactive)target.Presenter.Panel.Children[1]); - _helper.Click((Interactive)target.Presenter.Panel.Children[2], modifiers: KeyModifiers.Control); - - Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - - _helper.Click((Interactive)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 = 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); - - 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 = 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); - - 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 = 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[0]); - _helper.Click((Interactive)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 = 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(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((Interactive)target.Presenter.Panel.Children[1]); - - VerifyAdded("Bar"); - - receivedArgs = null; - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Shift); - - VerifyAdded("Baz", "Qux"); - - receivedArgs = null; - _helper.Click((Interactive)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 = 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[0]); - - Assert.Equal(new[] { "Foo" }, target.SelectedItems); - - _helper.Click((Interactive)target.Presenter.Panel.Children[4], modifiers: KeyModifiers.Control); - - Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems); - - _helper.Click((Interactive)target.Presenter.Panel.Children[3], modifiers: KeyModifiers.Control); - - Assert.Equal(new[] { "Foo", "Bar", "Foo" }, target.SelectedItems); - - _helper.Click((Interactive)target.Presenter.Panel.Children[1], modifiers: KeyModifiers.Control); - - Assert.Equal(new[] { "Foo", "Bar", "Foo", "Bar" }, target.SelectedItems); - } - } [Fact] public void SelectAll_Sets_SelectedIndex_And_SelectedItem() { - var target = new TestSelector - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); target.SelectAll(); Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); + Assert.Equal("foo", target.SelectedItem); } [Fact] public void SelectAll_Raises_SelectionChanged_Event() { - var target = new TestSelector - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); - SelectionChangedEventArgs receivedArgs = null; + SelectionChangedEventArgs? receivedArgs = null; target.SelectionChanged += (_, args) => receivedArgs = args; @@ -1060,17 +615,10 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void UnselectAll_Clears_SelectedIndex_And_SelectedItem() { - var target = new TestSelector - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - SelectedIndex = 0, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz" }); + target.SelectedIndex = 0; target.UnselectAll(); Assert.Equal(-1, target.SelectedIndex); @@ -1080,248 +628,92 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void SelectAll_Handles_Duplicate_Items() { - var target = new TestSelector - { - Template = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, - SelectionMode = SelectionMode.Multiple, - }; + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar", "baz", "foo", "bar", "baz" }); - target.ApplyTemplate(); - 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.SelectedItems); } [Fact] public void Adding_Item_Before_SelectedItems_Should_Update_Selection() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "Baz" - }; - - var target = new ListBox - { - Template = Template(), - ItemsSource = items, - 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); target.SelectAll(); - items.Insert(0, "Qux"); - root.LayoutManager.ExecuteLayoutPass(); + items.Insert(0, "qux"); + Layout(target); Assert.Equal(1, target.SelectedIndex); - Assert.Equal("Foo", target.SelectedItem); - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); Assert.Equal(new[] { 1, 2, 3 }, SelectedContainers(target)); } [Fact] public void Removing_Item_Before_SelectedItem_Should_Update_Selection() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "Baz" - }; - - var target = new TestSelector - { - Template = Template(), - ItemsSource = items, - SelectionMode = SelectionMode.Multiple, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + using var app = Start(); + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = CreateTarget(itemsSource: items); target.SelectedIndex = 1; target.SelectRange(2); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); items.RemoveAt(0); Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { "bar", "baz" }, target.SelectedItems); Assert.Equal(new[] { 0, 1 }, SelectedContainers(target)); } [Fact] public void Removing_SelectedItem_With_Multiple_Selection_Active_Should_Update_Selection() { - var items = new ObservableCollection - { - "Foo", - "Bar", - "Baz" - }; - - var target = new ListBox - { - Template = Template(), - ItemsSource = items, - 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); target.SelectAll(); items.RemoveAt(0); Assert.Equal(0, target.SelectedIndex); - Assert.Equal("Bar", target.SelectedItem); - Assert.Equal(new[] { "Bar", "Baz" }, target.SelectedItems); + 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" - }; - - var target = new ListBox - { - Template = Template(), - ItemsSource = items, - 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); target.SelectAll(); - items[1] = "Qux"; - - Assert.Equal(new[] { "Foo", "Baz" }, target.SelectedItems); - } - - [Fact] - public void Left_Click_On_SelectedItem_Should_Clear_Existing_Selection() - { - 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]); - - 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 = Template(), - ItemsSource = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), - SelectionMode = SelectionMode.Multiple, - Width = 100, - Height = 100, - }; + items[1] = "qux"; - 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); - - 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 = 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(); - - 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); - - Assert.Equal(2, target.SelectedItems.Count); - - _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right); - - Assert.Equal(1, target.SelectedItems.Count); - } + Assert.Equal(new[] { "foo", "baz" }, target.SelectedItems); } [Fact] public void Adding_Selected_ItemContainers_Should_Update_Selection() { - var target = new TestSelector + using var app = Start(); + var items = new[] { - Items = - { - new ItemContainer(), - new ItemContainer(), - }, - SelectionMode = SelectionMode.Multiple, - Template = Template(), + new ItemContainer(), + new ItemContainer(), }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + var target = CreateTarget(items: items); + target.Items.Add(new ItemContainer { IsSelected = true }); target.Items.Add(new ItemContainer { IsSelected = true }); @@ -1330,70 +722,12 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { target.Items[2], target.Items[3] }, target.SelectedItems); } - [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, - }; - - var root = new TestRoot(target); - root.LayoutManager.ExecuteInitialLayoutPass(); - - 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.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 = 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(); - - 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); - } - } - [Fact] public void Adding_To_Selection_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); @@ -1402,15 +736,11 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Null_To_Selection_Should_Create_New_SelectionModel() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; - + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); var oldSelection = target.Selection; - target.Selection = null; + target.Selection = null!; Assert.NotNull(target.Selection); Assert.NotSame(oldSelection, target.Selection); @@ -1419,26 +749,20 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_SelectionModel_With_Different_Source_To_Selection_Should_Fail() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; - + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); 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 - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; - + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); var selection = new SelectionModel(); + target.Selection = selection; Assert.Same(target.ItemsSource, selection.Source); @@ -1447,16 +771,10 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Single_Selected_Item_To_Selection_Should_Set_SelectedIndex() { - var target = new TestSelector - { - ItemsSource = new[] { "foo", "bar" }, - Template = Template(), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - + using var app = Start(); + var target = CreateTarget(itemsSource: new[] { "foo", "bar" }); var selection = new SelectionModel { SingleSelect = false }; + selection.Select(1); target.Selection = selection; @@ -1468,16 +786,10 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Multiple_Selected_Items_To_Selection_Should_Set_SelectedIndex() { - 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" }); var selection = new SelectionModel { SingleSelect = false }; + selection.SelectRange(0, 2); target.Selection = selection; @@ -1489,13 +801,9 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Reassigning_Selection_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.Selection.Select(1); target.Selection = new SelectionModel(); @@ -1506,25 +814,20 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Selection_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); var selection = new SelectionModel { SingleSelect = false }; + selection.SelectRange(0, 1); target.Selection = selection; - var items = target.Items.Cast().ToList(); Assert.True(items[0].IsSelected); Assert.True(items[1].IsSelected); Assert.False(items[2].IsSelected); @@ -1533,17 +836,13 @@ namespace Avalonia.Controls.UnitTests.Primitives [Fact] public void Assigning_Selection_Should_Raise_SelectionChanged() { + using var app = Start(); var items = new[] { "foo", "bar", "baz" }; - - var target = new TestSelector - { - ItemsSource = items, - Template = Template(), - SelectedItem = "bar", - }; - + var target = CreateTarget(itemsSource: items); var raised = 0; + target.SelectedItem = "bar"; + target.SelectionChanged += (s, e) => { if (raised == 0) @@ -1560,9 +859,6 @@ namespace Avalonia.Controls.UnitTests.Primitives ++raised; }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - var selection = new SelectionModel { Source = items, SingleSelect = false }; selection.Select(0); selection.Select(2); @@ -1570,26 +866,150 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(2, raised); } + private static IEnumerable SelectedContainers(SelectingItemsControl target) { - return target.Presenter.Panel.Children + Assert.NotNull(target.ItemsPanel); + + return target.ItemsPanelRoot!.Children .Select(x => x.Classes.Contains(":selected") ? target.IndexFromContainer(x) : -1) .Where(x => x != -1); } - private static FuncControlTemplate Template() + private static TestSelector CreateTarget( + object? dataContext = null, + IList? items = null, + IList? itemsSource = null, + ControlTheme? itemContainerTheme = null, + IDataTemplate? itemTemplate = null, + bool performLayout = true) + { + var target = new TestSelector + { + DataContext = dataContext, + ItemContainerTheme = itemContainerTheme, + ItemTemplate = itemTemplate, + ItemsSource = itemsSource, + SelectionMode = SelectionMode.Multiple, + }; + + if (items is not null) + { + foreach (var item in items) + target.Items.Add(item); + } + + var root = CreateRoot(target); + + if (performLayout) + root.LayoutManager.ExecuteInitialLayoutPass(); + + return target; + } + + private static TestRoot CreateRoot(Control child) + { + return new TestRoot + { + Resources = + { + { typeof(TestSelector), CreateTestSelectorControlTheme() }, + { typeof(ScrollViewer), CreateScrollViewerTheme() }, + }, + Child = child, + }; + } + + private static ControlTheme CreateTestSelectorControlTheme() + { + return new ControlTheme(typeof(TestSelector)) + { + Setters = + { + new Setter(TreeView.TemplateProperty, CreateTestSelectorTemplate()), + }, + }; + } + + private static FuncControlTemplate CreateTestSelectorTemplate() + { + return new FuncControlTemplate((parent, scope) => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ScrollViewer + { + Name = "PART_ScrollViewer", + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [~ItemsPresenter.ItemsPanelProperty] = parent[~ItemsControl.ItemsPanelProperty], + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope) + }; + }); + } + + private static ControlTheme CreateScrollViewerTheme() + { + return new ControlTheme(typeof(ScrollViewer)) + { + Setters = + { + new Setter(TreeView.TemplateProperty, CreateScrollViewerTemplate()), + }, + }; + } + + private static FuncControlTemplate CreateScrollViewerTemplate() { - return new FuncControlTemplate((control, scope) => - new ItemsPresenter + return new FuncControlTemplate((parent, scope) => + new Panel { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }.RegisterInNameScope(scope)); + Children = + { + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty], + [~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty], + }.RegisterInNameScope(scope), + new ScrollBar + { + Name = "verticalScrollBar", + [~ScrollBar.MaximumProperty] = parent[~ScrollViewer.VerticalScrollBarMaximumProperty], + [~~ScrollBar.ValueProperty] = parent[~~ScrollViewer.VerticalScrollBarValueProperty], + } + } + }); + } + + private static void Layout(Control c) + { + (c.GetVisualRoot() as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + } + + public static IDisposable Start() + { + return UnitTestApplication.Start( + TestServices.MockThreadingInterface.With( + focusManager: new FocusManager(), + fontManagerImpl: new MockFontManagerImpl(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: new KeyboardNavigationHandler(), + inputManager: new InputManager(), + renderInterface: new MockPlatformRenderInterface(), + textShaperImpl: new MockTextShaperImpl())); } private class TestSelector : SelectingItemsControl { - public static readonly new AvaloniaProperty SelectedItemsProperty = + public static readonly new AvaloniaProperty SelectedItemsProperty = SelectingItemsControl.SelectedItemsProperty; public static readonly new DirectProperty SelectionProperty = SelectingItemsControl.SelectionProperty; @@ -1601,7 +1021,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public new IList SelectedItems { - get { return base.SelectedItems; } + get { return base.SelectedItems!; } set { base.SelectedItems = value; } } @@ -1639,7 +1059,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private class ItemContainer : Control, ISelectable { - public string Value { get; set; } + public string? Value { get; set; } public bool IsSelected { get; set; } } } From 191a835c6a6a4119feaed8d4678d24a3c2bee728 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 6 Apr 2023 12:04:20 +0200 Subject: [PATCH 3/5] Make container IsSelected bindings work again. Rather than using the `ISelectable` interface to communicate container selection from the `SelectingItemsControl` to the container, use the `SelectingItemsControl.IsSelected` attached property, setting it with `SetCurrentValue` so that bindings defined in a style or item container theme can override the selection. Required an extra virtual `ContainerForItemPreparedOverride` method on `ItemsControl`. --- src/Avalonia.Controls/ISelectable.cs | 13 +- src/Avalonia.Controls/ItemsControl.cs | 16 + .../Primitives/SelectingItemsControl.cs | 68 ++-- .../Primitives/SelectingItemsControlTests.cs | 7 +- .../SelectingItemsControlTests_AutoSelect.cs | 1 - .../SelectingItemsControlTests_Multiple.cs | 306 ++++++++++++++++-- 6 files changed, 346 insertions(+), 65 deletions(-) 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/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 310626cbb0..b924a3763d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,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( @@ -496,20 +495,32 @@ namespace Avalonia.Controls.Primitives } } - /// - protected internal override void PrepareContainerForItemOverride(Control element, object? item, int 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); + + if (containerIsSelected != Selection.IsSelected(index)) + { + if (containerIsSelected) + Selection.Select(index); + else + Selection.Deselect(index); + } } } @@ -531,8 +542,7 @@ namespace Avalonia.Controls.Primitives KeyboardNavigation.SetTabOnceActiveElement(panel, null); } - if (element is ISelectable) - MarkContainerSelected(element, false); + element.ClearValue(IsSelectedProperty); } /// @@ -1121,11 +1131,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) @@ -1135,31 +1148,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/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 0ad8db43b2..f34d090122 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -4,6 +4,7 @@ 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; @@ -134,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedItems.Add("foo"); bool raised = false; - target.PropertyChanged += (s, e) => + target.PropertyChanged += (s, e) => raised |= e.Property.Name == "SelectedIndex" || e.Property.Name == "SelectedItem"; @@ -152,9 +153,9 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedItems.Add("foo"); 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); @@ -708,14 +709,14 @@ namespace Avalonia.Controls.UnitTests.Primitives using var app = Start(); var items = new[] { - new ItemContainer(), - new ItemContainer(), + new TestContainer(), + new TestContainer(), }; var target = CreateTarget(items: items); - target.Items.Add(new ItemContainer { IsSelected = true }); - target.Items.Add(new ItemContainer { IsSelected = true }); + target.Items.Add(new TestContainer { IsSelected = true }); + target.Items.Add(new TestContainer { IsSelected = true }); Assert.Equal(2, target.SelectedIndex); Assert.Equal(target.Items[2], target.SelectedItem); @@ -867,12 +868,169 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(2, raised); } + [Fact] + public void Can_Bind_Initial_Selected_State_Via_ItemContainerTheme() + { + 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)) + { + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } + }; + + var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme); + + 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 Can_Bind_Initial_Selected_State_Via_Style() + { + 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()) + { + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } + }; + + var target = CreateTarget(itemsSource: items, styles: new[] { style }); + + 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 Selection_State_Is_Updated_Via_IsSelected_Binding() + { + 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)) + { + BasedOn = CreateTestContainerTheme(), + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } + }; + + // 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); + + items[1].IsSelected = true; + + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + Assert.Equal(0, target.SelectedIndex); + 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); + + items[0].IsSelected = false; + + 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 Selection_State_Is_Written_Back_To_Item_Via_IsSelected_Binding() + { + 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)) + { + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + } + }; + + var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme); + var container0 = Assert.IsAssignableFrom(target.ContainerFromIndex(0)); + var container1 = Assert.IsAssignableFrom(target.ContainerFromIndex(1)); + + SelectingItemsControl.SetIsSelected(container1, true); + + Assert.True(items[1].IsSelected); + + SelectingItemsControl.SetIsSelected(container0, false); + + Assert.False(items[0].IsSelected); + } + + [Fact] + public void Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding() + { + using var app = Start(); + var items = Enumerable.Range(0, 100).Select(x => new ItemViewModel($"Item {x}", false)).ToList(); + var itemTheme = new ControlTheme(typeof(ContentPresenter)) + { + Setters = + { + new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")), + new Setter(Control.HeightProperty, 100.0), + } + }; + + // 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()!; + + // Scroll item 1 out of view. + scroll.Offset = new(0, 1000); + Layout(target); + + Assert.Equal(10, panel.FirstRealizedIndex); + Assert.Equal(19, panel.LastRealizedIndex); + + // Select item 1 now it's unrealized. + items[1].IsSelected = true; + + // 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); + + // Scroll item 1 back into view. + scroll.Offset = new(0, 0); + Layout(target); + + // 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); + } + private static IEnumerable SelectedContainers(SelectingItemsControl target) { Assert.NotNull(target.ItemsPanel); return target.ItemsPanelRoot!.Children - .Select(x => x.Classes.Contains(":selected") ? target.IndexFromContainer(x) : -1) + .Select(x => SelectingItemsControl.GetIsSelected(x) ? target.IndexFromContainer(x) : -1) .Where(x => x != -1); } @@ -882,9 +1040,33 @@ namespace Avalonia.Controls.UnitTests.Primitives IList? itemsSource = null, ControlTheme? itemContainerTheme = null, IDataTemplate? itemTemplate = null, - bool performLayout = true) + IEnumerable