Browse Source

Make TreeViewItem IsSelected bindings work.

pull/10944/head
Steven Kirk 3 years ago
parent
commit
487fe9ed77
  1. 79
      src/Avalonia.Controls/TreeView.cs
  2. 5
      src/Avalonia.Controls/TreeViewItem.cs
  3. 162
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

79
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
/// </summary>
static TreeView()
{
// HACK: Needed or SelectedItem property will not be found in Release build.
SelectingItemsControl.IsSelectedChangedEvent.AddClassHandler<TreeView>((x, e) =>
x.ContainerSelectionChanged(e));
}
/// <summary>
@ -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,15 +488,32 @@ 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 not set on the container: update the container
// selection based on the current selection as understood by this control.
MarkContainerSelected(container, SelectedItems.Contains(item));
}
else
{
// 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);
if (containerIsSelected != SelectedItems.Contains(item))
{
if (containerIsSelected)
SelectedItems.Add(item);
else
SelectedItems.Remove(item);
}
}
}
@ -863,27 +881,44 @@ namespace Avalonia.Controls
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// Called when a container raises the
/// <see cref="SelectingItemsControl.IsSelectedChangedEvent"/>.
/// </summary>
/// <param name="container">The container.</param>
/// <param name="selected">Whether the control is selected</param>
private void MarkContainerSelected(Control? container, bool selected)
/// <param name="e">The event.</param>
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;
}
}
/// <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>
/// Makes a list of objects equal another (though doesn't preserve order).
/// </summary>

5
src/Avalonia.Controls/TreeViewItem.cs

@ -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);
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{

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

@ -5,6 +5,7 @@ using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
@ -219,7 +220,7 @@ namespace Avalonia.Controls.UnitTests
Assert.True(fromContainer.IsSelected);
ClickContainer(toContainer, KeyModifiers.Shift);
AssertChildrenSelected(target, rootNode);
AssertAllChildContainersSelected(target, rootNode);
}
[Fact]
@ -238,7 +239,7 @@ namespace Avalonia.Controls.UnitTests
Assert.True(fromContainer.IsSelected);
ClickContainer(toContainer, KeyModifiers.Shift);
AssertChildrenSelected(target, rootNode);
AssertAllChildContainersSelected(target, rootNode);
}
[Fact]
@ -255,7 +256,7 @@ namespace Avalonia.Controls.UnitTests
ClickContainer(fromContainer, KeyModifiers.None);
ClickContainer(toContainer, KeyModifiers.Shift);
AssertChildrenSelected(target, rootNode);
AssertAllChildContainersSelected(target, rootNode);
ClickContainer(fromContainer, KeyModifiers.None);
Assert.True(fromContainer.IsSelected);
@ -975,7 +976,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent);
AssertChildrenSelected(target, rootNode);
AssertAllChildContainersSelected(target, rootNode);
}
[Fact]
@ -1005,7 +1006,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent);
AssertChildrenSelected(target, rootNode);
AssertAllChildContainersSelected(target, rootNode);
}
[Fact]
@ -1035,7 +1036,7 @@ namespace Avalonia.Controls.UnitTests
target.RaiseEvent(keyEvent);
AssertChildrenSelected(target, rootNode);
AssertAllChildContainersSelected(target, rootNode);
}
[Fact]
@ -1047,7 +1048,7 @@ namespace Avalonia.Controls.UnitTests
target.SelectAll();
AssertChildrenSelected(target, data[0]);
AssertAllChildContainersSelected(target, data[0]);
Assert.Equal(5, target.SelectedItems.Count);
_mouse.Click(target.Presenter!.Panel!.Children[0], MouseButton.Right);
@ -1259,6 +1260,87 @@ 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);
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, 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);
selected[1].IsSelected = true;
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,
bool expandAll = true,
ControlTheme? itemContainerTheme = null,
@ -1465,17 +1547,61 @@ namespace Avalonia.Controls.UnitTests
_mouse.Click(container, modifiers: modifiers);
}
private void AssertChildrenSelected(TreeView treeView, Node rootNode)
private void AssertContainerSelection(TreeView treeView, params Node[] expected)
{
Assert.NotNull(rootNode.Children);
static void Evaluate(Control container, HashSet<Node> remaining)
{
var treeViewItem = Assert.IsType<TreeViewItem>(container);
var node = (Node)container.DataContext!;
foreach (var child in rootNode.Children)
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(node.Children);
foreach (var child in node.Children)
{
var container = Assert.IsType<TreeViewItem>(treeView.TreeContainerFromItem(child));
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()
{
return UnitTestApplication.Start(
@ -1492,6 +1618,7 @@ namespace Avalonia.Controls.UnitTests
private class Node : NotifyingBase
{
private IAvaloniaList<Node> _children = new AvaloniaList<Node>();
private bool _isSelected;
public string? Value { get; set; }
@ -1504,6 +1631,21 @@ namespace Avalonia.Controls.UnitTests
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

Loading…
Cancel
Save