Browse Source

Respect single-select with IsSelected bindings.

Use the existing `UpdateSelection` methods to correctly respect the current `SelectionMode`.
pull/10944/head
Steven Kirk 3 years ago
parent
commit
2aca946a71
  1. 48
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  2. 27
      src/Avalonia.Controls/TreeView.cs
  3. 48
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  4. 93
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

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

@ -309,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
{
@ -495,6 +484,17 @@ namespace Avalonia.Controls.Primitives
}
}
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.ContainerForItemPreparedOverride(container, item, index);
@ -513,14 +513,7 @@ namespace Avalonia.Controls.Primitives
// container theme which has bound the IsSelected property. Update our selection
// based on the selection state of the container.
var containerIsSelected = GetIsSelected(container);
if (containerIsSelected != Selection.IsSelected(index))
{
if (containerIsSelected)
Selection.Select(index);
else
Selection.Deselect(index);
}
UpdateSelection(index, containerIsSelected, toggleModifier: true);
}
}
@ -907,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)

27
src/Avalonia.Controls/TreeView.cs

@ -494,27 +494,18 @@ namespace Avalonia.Controls
// 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))
{
// 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
if (container.IsSet(SelectingItemsControl.IsSelectedProperty))
{
// 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);
}
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/>
@ -681,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))
{

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

@ -976,6 +976,54 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.False(items[0].IsSelected);
}
[Fact]
public void Selection_Is_Updated_On_Container_Realization_With_IsSelected_Binding()
{
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;
var itemTheme = new ControlTheme(typeof(ContentPresenter))
{
Setters =
{
new Setter(SelectingItemsControl.IsSelectedProperty, new Binding("IsSelected")),
new Setter(Control.HeightProperty, 100.0),
}
};
// Create a SelectingItemsControl with a virtualizing stack panel.
var target = CreateTarget(itemsSource: items, itemContainerTheme: itemTheme, virtualizing: true);
var panel = Assert.IsType<VirtualizingStackPanel>(target.ItemsPanelRoot);
var scroll = panel.FindAncestorOfType<ScrollViewer>()!;
// 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);
// Scroll item 15 into view.
scroll.Offset = new(0, 1000);
Layout(target);
Assert.Equal(10, panel.FirstRealizedIndex);
Assert.Equal(19, panel.LastRealizedIndex);
// 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 Selection_State_Change_On_Unrealized_Item_Is_Respected_With_IsSelected_Binding()
{

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

@ -1279,7 +1279,7 @@ namespace Avalonia.Controls.UnitTests
}
};
var target = CreateTarget(data: data, itemContainerTheme: itemTheme);
var target = CreateTarget(data: data, itemContainerTheme: itemTheme, multiSelect: true);
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
@ -1305,7 +1305,7 @@ namespace Avalonia.Controls.UnitTests
}
};
var target = CreateTarget(data: data, styles: new[] { style });
var target = CreateTarget(data: data, multiSelect: true, styles: new[] { style });
AssertDataSelection(data, selected);
AssertContainerSelection(target, selected);
@ -1331,7 +1331,7 @@ namespace Avalonia.Controls.UnitTests
}
};
var target = CreateTarget(data: data, itemContainerTheme: itemTheme);
var target = CreateTarget(data: data, itemContainerTheme: itemTheme, multiSelect: true);
selected[1].IsSelected = true;
@ -1341,6 +1341,93 @@ namespace Avalonia.Controls.UnitTests
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,
bool expandAll = true,
ControlTheme? itemContainerTheme = null,

Loading…
Cancel
Save