diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index b924a3763d..663a315732 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/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)
diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs
index b2a90229f4..e3a9a05951 100644
--- a/src/Avalonia.Controls/TreeView.cs
+++ b/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));
}
///
@@ -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))
{
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
index f34d090122..c308a9cc92 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
+++ b/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(target.ItemsPanelRoot);
+ var scroll = panel.FindAncestorOfType()!;
+
+ // 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()
{
diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
index b2cf51629a..baf8ad5c0e 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/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(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(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?> data = default,
bool expandAll = true,
ControlTheme? itemContainerTheme = null,