Browse Source

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.
pull/16568/head
Steven Kirk 2 years ago
committed by GitHub
parent
commit
b1489aaca2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  2. 47
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  3. 6
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  4. 1
      tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj
  5. 2
      tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs
  6. 177
      tests/Avalonia.Controls.DataGrid.UnitTests/DataGridRowTests.cs
  7. 2
      tests/Avalonia.Controls.DataGrid.UnitTests/Utils/ReflectionHelperTests.cs
  8. 2
      tests/Avalonia.UnitTests/TestRoot.cs

12
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;

47
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);
}

6
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
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" />

2
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
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);
}
}
}

2
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
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)
{
}

Loading…
Cancel
Save