Przeglądaj źródła

Respect single-select with IsSelected bindings.

Use the existing `UpdateSelection` methods to correctly respect the current `SelectionMode`.
Steven Kirk 2 lat temu
rodzic
commit
2aca946a71

+ 26 - 22
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)

+ 11 - 16
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 - 0
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()
         {

+ 90 - 3
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,