diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 63f47c3e4dd..c314d71d492 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -1183,7 +1183,7 @@ private void OnHeadersVisibilityChanged(AvaloniaPropertyChangedEventArgs e) row.EnsureHeaderStyleAndVisibility(null); if (newValueRows) { - row.UpdatePseudoClasses(); + row.ApplyState(); row.EnsureHeaderVisibility(); } } @@ -1720,7 +1720,7 @@ internal int? MouseOverRowIndex // 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 @@ internal int? MouseOverRowIndex Debug.Assert(newMouseOverRow != null); if (newMouseOverRow != null) { - newMouseOverRow.UpdatePseudoClasses(); + newMouseOverRow.ApplyState(); } } } @@ -4177,7 +4177,7 @@ void SetValidationStatus(ICellEditBinding binding) if (editingRow.IsValid) { editingRow.IsValid = false; - editingRow.UpdatePseudoClasses(); + editingRow.ApplyState(); } } @@ -4368,7 +4368,7 @@ private void ExitEdit(bool keepFocus) //IsTabStop = true; if (IsSlotVisible(EditingRow.Slot)) { - EditingRow.UpdatePseudoClasses(); + EditingRow.ApplyState(); } ResetEditingRow(); if (keepFocus) @@ -6224,7 +6224,7 @@ private void ResetValidationStatus() cell.UpdatePseudoClasses(); } } - EditingRow.UpdatePseudoClasses(); + EditingRow.ApplyState(); } } IsValid = true; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 146461f2a79..60acfb7f03e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -63,6 +63,7 @@ public class DataGridRow : TemplatedControl 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 @@ public object Header set { SetValue(HeaderProperty, value); } } + public static readonly DirectProperty IsSelectedProperty = + AvaloniaProperty.RegisterDirect( + 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 IsValidProperty = AvaloniaProperty.RegisterDirect( nameof(IsValid), @@ -352,20 +365,6 @@ internal bool IsRecyclable } } - 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 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) RootElement = e.NameScope.Find(DATAGRIDROW_elementRoot); if (RootElement != null) { - UpdatePseudoClasses(); + ApplyState(); } bool updateVerticalScrollBar = false; @@ -650,11 +649,12 @@ internal void ApplyHeaderStatus() } } - 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 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight) } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == DataContextProperty) @@ -1086,6 +1085,18 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } } + else if (change.Property == IsSelectedProperty) + { + var value = change.GetNewValue(); + + if (OwningGrid != null && Slot != -1) + { + OwningGrid.SetRowSelection(Slot, value, false); + } + + PseudoClasses.Set(":selected", value); + } + base.OnPropertyChanged(change); } diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index c9a348d172a..15a14a61c23 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -677,7 +677,7 @@ private void ApplyDisplayedRowsState(int startSlot, int endSlot) { if (DisplayData.GetDisplayedElement(slot) is DataGridRow row) { - row.UpdatePseudoClasses(); ; + row.ApplyState(); } slot = GetNextVisibleSlot(slot); } @@ -1513,7 +1513,7 @@ private void LoadRowVisualsForDisplay(DataGridRow row) if (row.IsSelected || row.IsRecycled) { - row.UpdatePseudoClasses(); + row.ApplyState(); } // Show or hide RowDetails based on DataGrid settings @@ -1927,7 +1927,7 @@ private void SelectDisplayedElement(int slot) Control element = DisplayData.GetDisplayedElement(slot); if (element is DataGridRow row) { - row.UpdatePseudoClasses(); + row.ApplyState(); EnsureRowDetailsVisibility(row, raiseNotification: true, animate: true); } else diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj index 885386238df..e532f50e8d4 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj @@ -3,6 +3,7 @@ $(AvsCurrentTargetFramework) Library true + Avalonia.Controls.DataGridTests diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs index 04d7ce3fc78..bd6313943ab 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs @@ -4,7 +4,7 @@ using Avalonia.Collections; using Xunit; -namespace Avalonia.Controls.DataGrid.UnitTests.Collections +namespace Avalonia.Controls.DataGridTests.Collections { public class DataGridSortDescriptionTests diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/DataGridRowTests.cs b/tests/Avalonia.Controls.DataGrid.UnitTests/DataGridRowTests.cs new file mode 100644 index 00000000000..241f437b8ea --- /dev/null +++ b/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