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