Skip to content

Commit

Permalink
Allow binding DataGridRow.IsSelected (#16520)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
grokys committed Aug 2, 2024
1 parent 0d2dad7 commit 55a5672
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 28 deletions.
12 changes: 6 additions & 6 deletions src/Avalonia.Controls.DataGrid/DataGrid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,7 @@ private void OnHeadersVisibilityChanged(AvaloniaPropertyChangedEventArgs e)
row.EnsureHeaderStyleAndVisibility(null);
if (newValueRows)
{
row.UpdatePseudoClasses();
row.ApplyState();
row.EnsureHeaderVisibility();
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -1732,7 +1732,7 @@ internal int? MouseOverRowIndex
Debug.Assert(newMouseOverRow != null);
if (newMouseOverRow != null)
{
newMouseOverRow.UpdatePseudoClasses();
newMouseOverRow.ApplyState();
}
}
}
Expand Down Expand Up @@ -4177,7 +4177,7 @@ void SetValidationStatus(ICellEditBinding binding)
if (editingRow.IsValid)
{
editingRow.IsValid = false;
editingRow.UpdatePseudoClasses();
editingRow.ApplyState();
}
}

Expand Down Expand Up @@ -4368,7 +4368,7 @@ private void ExitEdit(bool keepFocus)
//IsTabStop = true;
if (IsSlotVisible(EditingRow.Slot))
{
EditingRow.UpdatePseudoClasses();
EditingRow.ApplyState();
}
ResetEditingRow();
if (keepFocus)
Expand Down Expand Up @@ -6224,7 +6224,7 @@ private void ResetValidationStatus()
cell.UpdatePseudoClasses();
}
}
EditingRow.UpdatePseudoClasses();
EditingRow.ApplyState();
}
}
IsValid = true;
Expand Down
47 changes: 29 additions & 18 deletions src/Avalonia.Controls.DataGrid/DataGridRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -85,6 +86,18 @@ public object Header
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),
Expand Down Expand Up @@ -347,20 +360,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
Expand Down Expand Up @@ -558,7 +557,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot);
if (RootElement != null)
{
UpdatePseudoClasses();
ApplyState();
}

bool updateVerticalScrollBar = false;
Expand Down Expand Up @@ -644,11 +643,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();
Expand Down Expand Up @@ -1061,7 +1061,6 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight)
}
}


protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == DataContextProperty)
Expand All @@ -1080,6 +1079,18 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
}
}
}
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);
}

Expand Down
6 changes: 3 additions & 3 deletions src/Avalonia.Controls.DataGrid/DataGridRows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Avalonia.Collections;
using Xunit;

namespace Avalonia.Controls.DataGrid.UnitTests.Collections
namespace Avalonia.Controls.DataGridTests.Collections
{

public class DataGridSortDescriptionTests
Expand Down
177 changes: 177 additions & 0 deletions tests/Avalonia.Controls.DataGrid.UnitTests/DataGridRowTests.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions tests/Avalonia.UnitTests/TestRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public IRenderTarget CreateRenderTarget()
return result.Object;
}

public void ExecuteInitialLayoutPass() => LayoutManager.ExecuteInitialLayoutPass();

public void Invalidate(Rect rect)
{
}
Expand Down

0 comments on commit 55a5672

Please sign in to comment.