Browse Source

Merge pull request #10944 from AvaloniaUI/fixes/10249-itemscontrol-isselected-binding

Fix binding container IsSelected in styles/themes
pull/10892/head
Max Katz 3 years ago
committed by GitHub
parent
commit
01b2e91e46
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      src/Avalonia.Controls/ISelectable.cs
  2. 16
      src/Avalonia.Controls/ItemsControl.cs
  3. 3
      src/Avalonia.Controls/ListBoxItem.cs
  4. 2
      src/Avalonia.Controls/MenuItem.cs
  5. 123
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  6. 2
      src/Avalonia.Controls/TabItem.cs
  7. 76
      src/Avalonia.Controls/TreeView.cs
  8. 7
      src/Avalonia.Controls/TreeViewItem.cs
  9. 475
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs
  10. 7
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  11. 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs
  12. 1596
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  13. 249
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

13
src/Avalonia.Controls/ISelectable.cs

@ -1,16 +1,9 @@
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Interface for objects that are selectable. /// An interface that is implemented by objects that expose their selection state via a
/// boolean <see cref="IsSelected"/> property.
/// </summary> /// </summary>
/// <remarks>
/// Controls such as <see cref="SelectingItemsControl"/> use this interface to indicate the
/// selected control in a list. If changing the control's <see cref="IsSelected"/> property
/// should update the selection in a <see cref="SelectingItemsControl"/> or equivalent, then
/// the control should raise the <see cref="SelectingItemsControl.IsSelectedChangedEvent"/>.
/// </remarks>
public interface ISelectable public interface ISelectable
{ {
/// <summary> /// <summary>
@ -18,4 +11,4 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
bool IsSelected { get; set; } bool IsSelected { get; set; }
} }
} }

16
src/Avalonia.Controls/ItemsControl.cs

@ -424,6 +424,21 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Called when a container has been fully prepared to display an item.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item being displayed.</param>
/// <param name="index">The index of the item being displayed.</param>
/// <remarks>
/// 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 <see cref="ContainerPrepared"/> event is raised.
/// </remarks>
protected internal virtual void ContainerForItemPreparedOverride(Control container, object? item, int index)
{
}
/// <summary> /// <summary>
/// Called when the index for a container changes due to an insertion or removal in the /// Called when the index for a container changes due to an insertion or removal in the
/// items collection. /// items collection.
@ -654,6 +669,7 @@ namespace Avalonia.Controls
internal void ItemContainerPrepared(Control container, object? item, int index) internal void ItemContainerPrepared(Control container, object? item, int index)
{ {
ContainerForItemPreparedOverride(container, item, index);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
ContainerPrepared?.Invoke(this, new(container, index)); ContainerPrepared?.Invoke(this, new(container, index));
} }

3
src/Avalonia.Controls/ListBoxItem.cs

@ -1,6 +1,7 @@
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -14,7 +15,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsSelectedProperty = public static readonly StyledProperty<bool> IsSelectedProperty =
AvaloniaProperty.Register<ListBoxItem, bool>(nameof(IsSelected)); SelectingItemsControl.IsSelectedProperty.AddOwner<ListBoxItem>();
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ListBoxItem"/> class. /// Initializes static members of the <see cref="ListBoxItem"/> class.

2
src/Avalonia.Controls/MenuItem.cs

@ -57,7 +57,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsSelectedProperty = public static readonly StyledProperty<bool> IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner<MenuItem>(); SelectingItemsControl.IsSelectedProperty.AddOwner<MenuItem>();
/// <summary> /// <summary>
/// Defines the <see cref="IsSubMenuOpen"/> property. /// Defines the <see cref="IsSubMenuOpen"/> property.

123
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -104,6 +104,14 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>( AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
nameof(SelectionMode)); nameof(SelectionMode));
/// <summary>
/// Defines the IsSelected attached property.
/// </summary>
public static readonly StyledProperty<bool> IsSelectedProperty =
AvaloniaProperty.RegisterAttached<SelectingItemsControl, Control, bool>(
"IsSelected",
defaultBindingMode: BindingMode.TwoWay);
/// <summary> /// <summary>
/// Defines the <see cref="IsTextSearchEnabled"/> property. /// Defines the <see cref="IsTextSearchEnabled"/> property.
/// </summary> /// </summary>
@ -111,9 +119,8 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false); AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false);
/// <summary> /// <summary>
/// Event that should be raised by items that implement <see cref="ISelectable"/> to /// Event that should be raised by containers when their selection state changes to notify
/// notify the parent <see cref="SelectingItemsControl"/> that their selection state /// the parent <see cref="SelectingItemsControl"/> that their selection state has changed.
/// has changed.
/// </summary> /// </summary>
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent = public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>( RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>(
@ -302,20 +309,9 @@ namespace Avalonia.Controls.Primitives
{ {
get get
{ {
if (_updateState?.Selection.HasValue == true) return _updateState?.Selection.HasValue == true ?
{ _updateState.Selection.Value :
return _updateState.Selection.Value; GetOrCreateSelectionModel();
}
else
{
if (_selection is null)
{
_selection = CreateDefaultSelectionModel();
InitializeSelectionModel(_selection);
}
return _selection;
}
} }
set set
{ {
@ -420,6 +416,21 @@ namespace Avalonia.Controls.Primitives
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item)); public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item));
/// <summary>
/// Gets the value of the <see cref="IsSelectedProperty"/> on the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The value of the attached property.</returns>
public static bool GetIsSelected(Control control) => control.GetValue(IsSelectedProperty);
/// <summary>
/// Gets the value of the <see cref="IsSelectedProperty"/> on the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The value of the property.</param>
/// <returns>The value of the attached property.</returns>
public static void SetIsSelected(Control control, bool value) => control.SetValue(IsSelectedProperty, value);
/// <summary> /// <summary>
/// Tries to get the container that was the source of an event. /// Tries to get the container that was the source of an event.
/// </summary> /// </summary>
@ -473,20 +484,36 @@ namespace Avalonia.Controls.Primitives
} }
} }
/// <inheritdoc /> protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
protected internal override void PrepareContainerForItemOverride(Control element, 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); // The IsSelected property is not set on the container: update the container
MarkContainerSelected(element, true); // selection based on the current selection as understood by this control.
MarkContainerSelected(container, Selection.IsSelected(index));
} }
else else
{ {
var selected = Selection.IsSelected(index); // The IsSelected property is set on the container: there is a style or item
MarkContainerSelected(element, selected); // 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); KeyboardNavigation.SetTabOnceActiveElement(panel, null);
} }
if (element is ISelectable) element.ClearValue(IsSelectedProperty);
MarkContainerSelected(element, false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -874,6 +900,17 @@ namespace Avalonia.Controls.Primitives
return false; return false;
} }
private ISelectionModel GetOrCreateSelectionModel()
{
if (_selection is null)
{
_selection = CreateDefaultSelectionModel();
InitializeSelectionModel(_selection);
}
return _selection;
}
private void OnItemsViewSourceChanged(object? sender, EventArgs e) private void OnItemsViewSourceChanged(object? sender, EventArgs e)
{ {
if (_selection is not null && _updateState is null) if (_selection is not null && _updateState is null)
@ -1098,11 +1135,14 @@ namespace Avalonia.Controls.Primitives
{ {
if (!_ignoreContainerSelectionChanged && if (!_ignoreContainerSelectionChanged &&
e.Source is Control control && e.Source is Control control &&
e.Source is ISelectable selectable &&
control.Parent == this && 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) if (e.Source != this)
@ -1112,31 +1152,18 @@ namespace Avalonia.Controls.Primitives
} }
/// <summary> /// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>. /// Sets the <see cref="IsSelectedProperty"/> on the specified container.
/// </summary> /// </summary>
/// <param name="container">The container.</param> /// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param> /// <param name="selected">Whether the control is selected</param>
/// <returns>The previous selection state.</returns> /// <returns>The previous selection state.</returns>
private bool MarkContainerSelected(Control container, bool selected) private void MarkContainerSelected(Control container, bool selected)
{ {
_ignoreContainerSelectionChanged = true;
try try
{ {
bool result; container.SetCurrentValue(IsSelectedProperty, selected);
_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;
} }
finally finally
{ {

2
src/Avalonia.Controls/TabItem.cs

@ -22,7 +22,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsSelectedProperty = public static readonly StyledProperty<bool> IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner<TabItem>(); SelectingItemsControl.IsSelectedProperty.AddOwner<TabItem>();
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TabItem"/> class. /// Initializes static members of the <see cref="TabItem"/> class.

76
src/Avalonia.Controls/TreeView.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -60,7 +61,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static TreeView() static TreeView()
{ {
// HACK: Needed or SelectedItem property will not be found in Release build. SelectingItemsControl.IsSelectedChangedEvent.AddClassHandler<TreeView>((x, e) =>
x.ContainerSelectionChanged(e));
} }
/// <summary> /// <summary>
@ -430,9 +432,8 @@ namespace Avalonia.Controls
private void MarkItemSelected(object item, bool selected) private void MarkItemSelected(object item, bool selected)
{ {
var container = TreeContainerFromItem(item)!; if (TreeContainerFromItem(item) is Control container)
MarkContainerSelected(container, selected);
MarkContainerSelected(container, selected);
} }
private void SelectedItemsAdded(IList items) private void SelectedItemsAdded(IList items)
@ -487,16 +488,24 @@ namespace Avalonia.Controls
protected internal override Control CreateContainerForItemOverride() => new TreeViewItem(); protected internal override Control CreateContainerForItemOverride() => new TreeViewItem();
protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is 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); // The IsSelected property is set on the container: there is a style or item
if (AutoScrollToSelectedItem) // container theme which has bound the IsSelected property. Update our selection
Dispatcher.UIThread.Post(container.BringIntoView); // 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));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -663,7 +672,11 @@ namespace Avalonia.Controls
var multi = mode.HasAllFlags(SelectionMode.Multiple); var multi = mode.HasAllFlags(SelectionMode.Multiple);
var range = multi && rangeModifier && selectedContainer != null; var range = multi && rangeModifier && selectedContainer != null;
if (rightButton) if (!select)
{
SelectedItems.Remove(item);
}
else if (rightButton)
{ {
if (!SelectedItems.Contains(item)) if (!SelectedItems.Contains(item))
{ {
@ -863,27 +876,44 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>. /// Called when a container raises the
/// <see cref="SelectingItemsControl.IsSelectedChangedEvent"/>.
/// </summary> /// </summary>
/// <param name="container">The container.</param> /// <param name="e">The event.</param>
/// <param name="selected">Whether the control is selected</param> private void ContainerSelectionChanged(RoutedEventArgs e)
private void MarkContainerSelected(Control? container, bool selected)
{ {
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) if (containerIsSelected != ourIsSelected)
{ {
selectable.IsSelected = selected; if (containerIsSelected)
SelectedItems.Add(item);
else
SelectedItems.Remove(item);
}
} }
else
if (e.Source != this)
{ {
((IPseudoClasses)container.Classes).Set(":selected", selected); e.Handled = true;
} }
} }
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private void MarkContainerSelected(Control container, bool selected)
{
container.SetCurrentValue(SelectingItemsControl.IsSelectedProperty, selected);
}
/// <summary> /// <summary>
/// Makes a list of objects equal another (though doesn't preserve order). /// Makes a list of objects equal another (though doesn't preserve order).
/// </summary> /// </summary>

7
src/Avalonia.Controls/TreeViewItem.cs

@ -31,7 +31,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsSelected"/> property. /// Defines the <see cref="IsSelected"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsSelectedProperty = public static readonly StyledProperty<bool> IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner<TreeViewItem>(); SelectingItemsControl.IsSelectedProperty.AddOwner<TreeViewItem>();
/// <summary> /// <summary>
/// Defines the <see cref="Level"/> property. /// Defines the <see cref="Level"/> property.
@ -105,6 +105,11 @@ namespace Avalonia.Controls
EnsureTreeView().PrepareContainerForItemOverride(container, item, index); EnsureTreeView().PrepareContainerForItemOverride(container, item, index);
} }
protected internal override void ContainerForItemPreparedOverride(Control container, object? item, int index)
{
EnsureTreeView().ContainerForItemPreparedOverride(container, item, index);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{ {

475
tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

@ -1,17 +1,22 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
{ {
public class ListBoxTests_Multiple public class ListBoxTests_Multiple
{ {
private MouseTestHelper _helper = new MouseTestHelper();
[Fact] [Fact]
public void Focusing_Item_With_Shift_And_Arrow_Key_Should_Add_To_Selection() 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); 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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<string>((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<string>((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<string>((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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<string>((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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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<string>((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<PlatformHotkeyConfiguration>().ToConstant(new Mock<PlatformHotkeyConfiguration>().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) private Control CreateListBoxTemplate(TemplatedControl parent, INameScope scope)
{ {
return new ScrollViewer return new ScrollViewer
@ -119,5 +586,13 @@ namespace Avalonia.Controls.UnitTests
// Now the ItemsPresenter should be reigstered, so apply its template. // Now the ItemsPresenter should be reigstered, so apply its template.
((Control)target.Presenter).ApplyTemplate(); ((Control)target.Presenter).ApplyTemplate();
} }
private static IEnumerable<int> SelectedContainers(SelectingItemsControl target)
{
return target.Presenter.Panel.Children
.Select(x => x.Classes.Contains(":selected") ? target.IndexFromContainer(x) : -1)
.Where(x => x != -1);
}
} }
} }

7
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -2170,7 +2170,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class Item : Control, ISelectable private class Item : Control, ISelectable
{ {
public string Value { get; set; } 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 private class MasterViewModel : NotifyingBase

1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs

@ -116,7 +116,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(0, target.SelectedIndex); Assert.Equal(0, target.SelectedIndex);
Assert.Equal("bar", target.SelectedItem); Assert.Equal("bar", target.SelectedItem);
Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes);
} }
private static FuncControlTemplate Template() private static FuncControlTemplate Template()

1596
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

File diff suppressed because it is too large

249
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -5,6 +5,7 @@ using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core; using Avalonia.Data.Core;
@ -219,7 +220,7 @@ namespace Avalonia.Controls.UnitTests
Assert.True(fromContainer.IsSelected); Assert.True(fromContainer.IsSelected);
ClickContainer(toContainer, KeyModifiers.Shift); ClickContainer(toContainer, KeyModifiers.Shift);
AssertChildrenSelected(target, rootNode); AssertAllChildContainersSelected(target, rootNode);
} }
[Fact] [Fact]
@ -238,7 +239,7 @@ namespace Avalonia.Controls.UnitTests
Assert.True(fromContainer.IsSelected); Assert.True(fromContainer.IsSelected);
ClickContainer(toContainer, KeyModifiers.Shift); ClickContainer(toContainer, KeyModifiers.Shift);
AssertChildrenSelected(target, rootNode); AssertAllChildContainersSelected(target, rootNode);
} }
[Fact] [Fact]
@ -255,7 +256,7 @@ namespace Avalonia.Controls.UnitTests
ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(fromContainer, KeyModifiers.None);
ClickContainer(toContainer, KeyModifiers.Shift); ClickContainer(toContainer, KeyModifiers.Shift);
AssertChildrenSelected(target, rootNode); AssertAllChildContainersSelected(target, rootNode);
ClickContainer(fromContainer, KeyModifiers.None); ClickContainer(fromContainer, KeyModifiers.None);
Assert.True(fromContainer.IsSelected); Assert.True(fromContainer.IsSelected);
@ -975,7 +976,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent); target.RaiseEvent(keyEvent);
AssertChildrenSelected(target, rootNode); AssertAllChildContainersSelected(target, rootNode);
} }
[Fact] [Fact]
@ -1005,7 +1006,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent); target.RaiseEvent(keyEvent);
AssertChildrenSelected(target, rootNode); AssertAllChildContainersSelected(target, rootNode);
} }
[Fact] [Fact]
@ -1035,7 +1036,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent); target.RaiseEvent(keyEvent);
AssertChildrenSelected(target, rootNode); AssertAllChildContainersSelected(target, rootNode);
} }
[Fact] [Fact]
@ -1047,7 +1048,7 @@ namespace Avalonia.Controls.UnitTests
target.SelectAll(); target.SelectAll();
AssertChildrenSelected(target, data[0]); AssertAllChildContainersSelected(target, data[0]);
Assert.Equal(5, target.SelectedItems.Count); Assert.Equal(5, target.SelectedItems.Count);
_mouse.Click(target.Presenter!.Panel!.Children[0], MouseButton.Right); _mouse.Click(target.Presenter!.Panel!.Children[0], MouseButton.Right);
@ -1259,6 +1260,174 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact]
public void Can_Bind_Initial_Selected_State_Via_ItemContainerTheme()
{
using var app = Start();
var data = CreateTestTreeData();
var selected = new[] { data[0], data[0].Children[1] };
foreach (var node in selected)
node.IsSelected = true;
var itemTheme = new ControlTheme(typeof(TreeViewItem))
{
BasedOn = CreateTreeViewItemControlTheme(),
Setters =
{
new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
}
};
var target = CreateTarget(data: data, itemContainerTheme: itemTheme, multiSelect: true);
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(selected, target.SelectedItems);
}
[Fact]
public void Can_Bind_Initial_Selected_State_Via_Style()
{
using var app = Start();
var data = CreateTestTreeData();
var selected = new[] { data[0], data[0].Children[1] };
foreach (var node in selected)
node.IsSelected = true;
var style = new Style(x => x.OfType<TreeViewItem>())
{
Setters =
{
new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
}
};
var target = CreateTarget(data: data, multiSelect: true, styles: new[] { style });
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(selected, target.SelectedItems);
}
[Fact]
public void Selection_State_Is_Updated_Via_IsSelected_Binding()
{
using var app = Start();
var data = CreateTestTreeData();
var selected = new[] { data[0], data[0].Children[1] };
selected[0].IsSelected = true;
var itemTheme = new ControlTheme(typeof(TreeViewItem))
{
BasedOn = CreateTreeViewItemControlTheme(),
Setters =
{
new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
}
};
var target = CreateTarget(data: data, itemContainerTheme: itemTheme, multiSelect: true);
selected[1].IsSelected = true;
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(selected, target.SelectedItems);
}
[Fact]
public void Selection_State_Is_Updated_Via_IsSelected_Binding_On_Expand()
{
using var app = Start();
var data = CreateTestTreeData();
var selected = new[] { data[0], data[0].Children[1] };
foreach (var node in selected)
node.IsSelected = true;
var itemTheme = new ControlTheme(typeof(TreeViewItem))
{
BasedOn = CreateTreeViewItemControlTheme(),
Setters =
{
new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
}
};
var target = CreateTarget(
data: data,
expandAll: false,
itemContainerTheme: itemTheme,
multiSelect: true);
var rootContainer = Assert.IsType<TreeViewItem>(target.ContainerFromIndex(0));
// Root TreeViewItem isn't expanded so selection for child won't have been picked
// up by IsSelected binding yet.
AssertContainerSelection(target, new[] { selected[0] });
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(new[] { selected[0] }, target.SelectedItems);
rootContainer.IsExpanded = true;
Layout(target);
// Root is expanded so now all expected items will be selected.
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(selected, target.SelectedItems);
}
[Fact]
public void Selection_State_Is_Updated_Via_IsSelected_Binding_On_Expand_Single_Select()
{
using var app = Start();
var data = CreateTestTreeData();
var selected = new[] { data[0], data[0].Children[1] };
foreach (var node in selected)
node.IsSelected = true;
var itemTheme = new ControlTheme(typeof(TreeViewItem))
{
BasedOn = CreateTreeViewItemControlTheme(),
Setters =
{
new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
}
};
var target = CreateTarget(
data: data,
expandAll: false,
itemContainerTheme: itemTheme);
var rootContainer = Assert.IsType<TreeViewItem>(target.ContainerFromIndex(0));
// Root TreeViewItem isn't expanded so selection for child won't have been picked
// up by IsSelected binding yet.
AssertContainerSelection(target, new[] { selected[0] });
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(new[] { selected[0] }, target.SelectedItems);
rootContainer.IsExpanded = true;
Layout(target);
// Root is expanded and newly revealed selected node will replace current selection
// given that we're in SelectionMode == Single.
selected = new[] { selected[1] };
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
Assert.Equal(selected[0], target.SelectedItem);
Assert.Equal(selected, target.SelectedItems);
}
private static TreeView CreateTarget(Optional<IList<Node>?> data = default, private static TreeView CreateTarget(Optional<IList<Node>?> data = default,
bool expandAll = true, bool expandAll = true,
ControlTheme? itemContainerTheme = null, ControlTheme? itemContainerTheme = null,
@ -1465,17 +1634,61 @@ namespace Avalonia.Controls.UnitTests
_mouse.Click(container, modifiers: modifiers); _mouse.Click(container, modifiers: modifiers);
} }
private void AssertChildrenSelected(TreeView treeView, Node rootNode) private void AssertContainerSelection(TreeView treeView, params Node[] expected)
{
static void Evaluate(Control container, HashSet<Node> remaining)
{
var treeViewItem = Assert.IsType<TreeViewItem>(container);
var node = (Node)container.DataContext!;
Assert.Equal(remaining.Contains(node), treeViewItem.IsSelected);
remaining.Remove(node);
foreach (var child in treeViewItem.GetRealizedContainers())
{
Evaluate(child, remaining);
}
}
var remaining = expected.ToHashSet();
foreach (var container in treeView.GetRealizedContainers())
Evaluate(container, remaining);
Assert.Empty(remaining);
}
private void AssertAllChildContainersSelected(TreeView treeView, Node node)
{ {
Assert.NotNull(rootNode.Children); Assert.NotNull(node.Children);
foreach (var child in rootNode.Children) foreach (var child in node.Children)
{ {
var container = Assert.IsType<TreeViewItem>(treeView.TreeContainerFromItem(child)); var container = Assert.IsType<TreeViewItem>(treeView.TreeContainerFromItem(child));
Assert.True(container.IsSelected); Assert.True(container.IsSelected);
} }
} }
private void AssertDataSelection(IEnumerable<Node> data, params Node[] expected)
{
static void Evaluate(Node rootNode, HashSet<Node> remaining)
{
Assert.Equal(remaining.Contains(rootNode), rootNode.IsSelected);
remaining.Remove(rootNode);
if (rootNode.Children is null)
return;
foreach (var child in rootNode.Children)
{
Evaluate(child, remaining);
}
}
var remaining = expected.ToHashSet();
foreach (var node in data)
Evaluate(node, remaining);
Assert.Empty(remaining);
}
private IDisposable Start() private IDisposable Start()
{ {
return UnitTestApplication.Start( return UnitTestApplication.Start(
@ -1492,6 +1705,7 @@ namespace Avalonia.Controls.UnitTests
private class Node : NotifyingBase private class Node : NotifyingBase
{ {
private IAvaloniaList<Node> _children = new AvaloniaList<Node>(); private IAvaloniaList<Node> _children = new AvaloniaList<Node>();
private bool _isSelected;
public string? Value { get; set; } public string? Value { get; set; }
@ -1504,6 +1718,21 @@ namespace Avalonia.Controls.UnitTests
RaisePropertyChanged(nameof(Children)); RaisePropertyChanged(nameof(Children));
} }
} }
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
RaisePropertyChanged();
}
}
}
public override string ToString() => Value ?? string.Empty;
} }
private class TestTreeDataTemplate : ITreeDataTemplate private class TestTreeDataTemplate : ITreeDataTemplate

Loading…
Cancel
Save