Explorar o código

Allow binding `DataGridRow.IsSelected` (#16520)

* Change namespace to prevent conflicts.

The `DataGrid` in the namespace name was hiding the `DataGrid` type.

* Initial impl of bindable DataGridRow.IsSelected.

* Make DataGridRow.IsSelected two-way bindable.
Steven Kirk hai 1 ano
pai
achega
b1489aaca2

+ 6 - 6
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -1183,7 +1183,7 @@ namespace Avalonia.Controls
                             row.EnsureHeaderStyleAndVisibility(null);
                             if (newValueRows)
                             {
-                                row.UpdatePseudoClasses();
+                                row.ApplyState();
                                 row.EnsureHeaderVisibility();
                             }
                         }
@@ -1720,7 +1720,7 @@ namespace Avalonia.Controls
                     // State for the old row needs to be applied after setting the new value
                     if (oldMouseOverRow != null)
                     {
-                        oldMouseOverRow.UpdatePseudoClasses();
+                        oldMouseOverRow.ApplyState();
                     }
 
                     if (_mouseOverRowIndex.HasValue)
@@ -1732,7 +1732,7 @@ namespace Avalonia.Controls
                             Debug.Assert(newMouseOverRow != null);
                             if (newMouseOverRow != null)
                             {
-                                newMouseOverRow.UpdatePseudoClasses();
+                                newMouseOverRow.ApplyState();
                             }
                         }
                     }
@@ -4177,7 +4177,7 @@ namespace Avalonia.Controls
                             if (editingRow.IsValid)
                             {
                                 editingRow.IsValid = false;
-                                editingRow.UpdatePseudoClasses();
+                                editingRow.ApplyState();
                             }
                         }
 
@@ -4368,7 +4368,7 @@ namespace Avalonia.Controls
             //IsTabStop = true;
             if (IsSlotVisible(EditingRow.Slot))
             {
-                EditingRow.UpdatePseudoClasses();
+                EditingRow.ApplyState();
             }
             ResetEditingRow();
             if (keepFocus)
@@ -6224,7 +6224,7 @@ namespace Avalonia.Controls
                             cell.UpdatePseudoClasses();
                         }
                     }
-                    EditingRow.UpdatePseudoClasses();
+                    EditingRow.ApplyState();
                 }
             }
             IsValid = true;

+ 29 - 18
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@@ -63,6 +63,7 @@ namespace Avalonia.Controls
         private Control _detailsContent;
         private IDisposable _detailsContentSizeSubscription;
         private DataGridDetailsPresenter _detailsElement;
+        private bool _isSelected;
 
         // Locally cache whether or not details are visible so we don't run redundant storyboards
         // The Details Template that is actually applied to the Row
@@ -85,6 +86,18 @@ namespace Avalonia.Controls
             set { SetValue(HeaderProperty, value); }
         }
 
+        public static readonly DirectProperty<DataGridRow, bool> IsSelectedProperty =
+            AvaloniaProperty.RegisterDirect<DataGridRow, bool>(
+                nameof(IsSelected),
+                o => o.IsSelected,
+                (o, v) => o.IsSelected = v);
+
+        public bool IsSelected
+        {
+            get => _isSelected;
+            set => SetAndRaise(IsSelectedProperty, ref _isSelected, value);
+        }
+
         public static readonly DirectProperty<DataGridRow, bool> IsValidProperty =
             AvaloniaProperty.RegisterDirect<DataGridRow, bool>(
                 nameof(IsValid),
@@ -352,20 +365,6 @@ namespace Avalonia.Controls
             }
         }
 
-        internal bool IsSelected
-        {
-            get
-            {
-                if (OwningGrid == null || Slot == -1)
-                {
-                    // The Slot can be -1 if we're about to reuse or recycle this row, but the layout cycle has not
-                    // passed so we don't know the outcome yet.  We don't care whether or not it's selected in this case
-                    return false;
-                }
-                return OwningGrid.GetRowSelection(Slot);
-            }
-        }
-
         internal int? MouseOverColumnIndex
         {
             get
@@ -564,7 +563,7 @@ namespace Avalonia.Controls
             RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot);
             if (RootElement != null)
             {
-                UpdatePseudoClasses();
+                ApplyState();
             }
 
             bool updateVerticalScrollBar = false;
@@ -650,11 +649,12 @@ namespace Avalonia.Controls
             }
         }
 
-        internal void UpdatePseudoClasses()
+        internal void ApplyState()
         {
             if (RootElement != null && OwningGrid != null && IsVisible)
             {
-                PseudoClasses.Set(":selected", IsSelected);
+                var isSelected = Slot != -1 && OwningGrid.GetRowSelection(Slot);
+                IsSelected = isSelected;
                 PseudoClasses.Set(":editing", IsEditing);
                 PseudoClasses.Set(":invalid", !IsValid);
                 ApplyHeaderStatus();
@@ -1067,7 +1067,6 @@ namespace Avalonia.Controls
             }
         }
 
-
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
             if (change.Property == DataContextProperty)
@@ -1086,6 +1085,18 @@ namespace Avalonia.Controls
                     }
                 }
             }
+            else if (change.Property == IsSelectedProperty)
+            {
+                var value = change.GetNewValue<bool>();
+
+                if (OwningGrid != null && Slot != -1)
+                {
+                    OwningGrid.SetRowSelection(Slot, value, false);
+                }
+
+                PseudoClasses.Set(":selected", value);
+            }
+
             base.OnPropertyChanged(change);
         }
 

+ 3 - 3
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@@ -677,7 +677,7 @@ namespace Avalonia.Controls
                 {
                     if (DisplayData.GetDisplayedElement(slot) is DataGridRow row)
                     {
-                        row.UpdatePseudoClasses(); ;
+                        row.ApplyState();
                     }
                     slot = GetNextVisibleSlot(slot);
                 }
@@ -1513,7 +1513,7 @@ namespace Avalonia.Controls
 
             if (row.IsSelected || row.IsRecycled)
             {
-                row.UpdatePseudoClasses();
+                row.ApplyState();
             }
 
             // Show or hide RowDetails based on DataGrid settings
@@ -1927,7 +1927,7 @@ namespace Avalonia.Controls
             Control element = DisplayData.GetDisplayedElement(slot);
             if (element is DataGridRow row)
             {
-                row.UpdatePseudoClasses();
+                row.ApplyState();
                 EnsureRowDetailsVisibility(row, raiseNotification: true, animate: true);
             }
             else

+ 1 - 0
tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj

@@ -3,6 +3,7 @@
     <TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
     <OutputType>Library</OutputType>
     <IsTestProject>true</IsTestProject>
+    <RootNamespace>Avalonia.Controls.DataGridTests</RootNamespace>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />

+ 1 - 1
tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs

@@ -4,7 +4,7 @@ using System.Linq;
 using Avalonia.Collections;
 using Xunit;
 
-namespace Avalonia.Controls.DataGrid.UnitTests.Collections
+namespace Avalonia.Controls.DataGridTests.Collections
 {
 
     public class DataGridSortDescriptionTests

+ 177 - 0
tests/Avalonia.Controls.DataGrid.UnitTests/DataGridRowTests.cs

@@ -0,0 +1,177 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+#nullable enable
+
+namespace Avalonia.Controls.DataGridTests;
+
+public class DataGridRowTests
+{
+    [Fact]
+    public void IsSelected_Binding_Works_For_Initial_Rows()
+    {
+        using var app = Start();
+        var items = Enumerable.Range(0, 100).Select(x => new Model($"Item {x}")).ToList();
+        items[2].IsSelected = true;
+        
+        var target = CreateTarget(items, [IsSelectedBinding()]);
+        var rows = GetRows(target);
+
+        Assert.Equal(0, GetFirstRealizedRowIndex(target));
+        Assert.Equal(4, GetLastRealizedRowIndex(target));
+        Assert.All(rows, x => Assert.Equal(x.Index == 2, x.IsSelected));
+    }
+
+    [Fact]
+    public void IsSelected_Binding_Works_For_Rows_Scrolled_Into_View()
+    {
+        using var app = Start();
+        var items = Enumerable.Range(0, 100).Select(x => new Model($"Item {x}")).ToList();
+        items[10].IsSelected = true;
+
+        var target = CreateTarget(items, [IsSelectedBinding()]);
+        var rows = GetRows(target);
+
+        Assert.Equal(0, GetFirstRealizedRowIndex(target));
+        Assert.Equal(4, GetLastRealizedRowIndex(target));
+
+        target.ScrollIntoView(items[10], target.Columns[0]);
+        target.UpdateLayout();
+
+        Assert.Equal(6, GetFirstRealizedRowIndex(target));
+        Assert.Equal(10, GetLastRealizedRowIndex(target));
+
+        Assert.All(rows, x => Assert.Equal(x.Index == 10, x.IsSelected));
+    }
+
+    [Fact]
+    public void Can_Toggle_IsSelected_Via_Binding()
+    {
+        using var app = Start();
+        var items = Enumerable.Range(0, 100).Select(x => new Model($"Item {x}")).ToList();
+        items[2].IsSelected = true;
+
+        var target = CreateTarget(items, [IsSelectedBinding()]);
+        var rows = GetRows(target);
+
+        Assert.Equal(0, GetFirstRealizedRowIndex(target));
+        Assert.Equal(4, GetLastRealizedRowIndex(target));
+        Assert.All(rows, x => Assert.Equal(x.Index == 2, x.IsSelected));
+
+        items[2].IsSelected = false;
+
+        Assert.All(rows, x => Assert.False(x.IsSelected));
+    }
+
+    [Fact]
+    public void Can_Toggle_IsSelected_Via_DataGrid()
+    {
+        using var app = Start();
+        var items = Enumerable.Range(0, 100).Select(x => new Model($"Item {x}")).ToList();
+        items[2].IsSelected = true;
+
+        var target = CreateTarget(items, [IsSelectedBinding()]);
+        var rows = GetRows(target);
+
+        Assert.Equal(0, GetFirstRealizedRowIndex(target));
+        Assert.Equal(4, GetLastRealizedRowIndex(target));
+        Assert.All(rows, x => Assert.Equal(x.Index == 2, x.IsSelected));
+
+        target.SelectedItems.Remove(items[2]);
+
+        Assert.All(rows, x => Assert.False(x.IsSelected));
+        Assert.False(items[2].IsSelected);
+    }
+
+    private static IDisposable Start()
+    {
+        return UnitTestApplication.Start(TestServices.StyledWindow);
+    }
+
+    private static DataGrid CreateTarget(
+        IList items,
+        IEnumerable<Style>? styles = null)
+    {
+        var root = new TestRoot
+        {
+            ClientSize = new(100, 100),
+            Styles =
+            {
+                new StyleInclude((Uri?)null)
+                {
+                    Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml")
+                },
+            }
+        };
+
+        var target = new DataGrid 
+        { 
+            Columns =
+            {
+                new DataGridTextColumn { Header = "Name", Binding = new Binding("Name") }
+            },
+            ItemsSource = items
+        };
+
+        if (styles is not null)
+        {
+            foreach (var style in styles)
+                target.Styles.Add(style);
+        }
+
+        root.Child = target;
+        root.ExecuteInitialLayoutPass();
+        return target;
+    }
+
+    private static int GetFirstRealizedRowIndex(DataGrid target)
+    {
+        return target.GetSelfAndVisualDescendants().OfType<DataGridRow>().Select(x => x.Index).Min();
+    }
+
+    private static int GetLastRealizedRowIndex(DataGrid target)
+    {
+        return target.GetSelfAndVisualDescendants().OfType<DataGridRow>().Select(x => x.Index).Max();
+    }
+
+    private static IReadOnlyList<DataGridRow> GetRows(DataGrid target)
+    {
+        return target.GetSelfAndVisualDescendants().OfType<DataGridRow>().ToList();
+    }
+
+    private static Style IsSelectedBinding()
+    {
+        return new Style(x => x.OfType<DataGridRow>())
+        {
+            Setters = { new Setter(DataGridRow.IsSelectedProperty, new Binding("IsSelected", BindingMode.TwoWay)) }
+        };
+    }
+
+    private class Model : NotifyingBase
+    {
+        private bool _isSelected;
+        private string _name;
+
+        public Model(string name) => _name = name;
+
+        public bool IsSelected 
+        {
+            get => _isSelected;
+            set => SetField(ref _isSelected, value);
+        }
+
+        public string Name 
+        { 
+            get => _name;
+            set => SetField(ref _name, value);
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.Controls.DataGrid.UnitTests/Utils/ReflectionHelperTests.cs

@@ -2,7 +2,7 @@
 using Avalonia.Controls.Utils;
 using Xunit;
 
-namespace Avalonia.Controls.DataGrid.UnitTests.Utils
+namespace Avalonia.Controls.DataGridTests.Utils
 {
     public class ReflectionHelperTests
     {

+ 2 - 0
tests/Avalonia.UnitTests/TestRoot.cs

@@ -90,6 +90,8 @@ namespace Avalonia.UnitTests
             return result.Object;
         }
 
+        public void ExecuteInitialLayoutPass() => LayoutManager.ExecuteInitialLayoutPass();
+
         public void Invalidate(Rect rect)
         {
         }