Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow binding DataGridRow.IsSelected #16520

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
Expand Down Expand Up @@ -564,7 +563,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 @@ -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();
Expand Down Expand Up @@ -1067,7 +1067,6 @@ internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight)
}
}


protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == DataContextProperty)
Expand All @@ -1086,6 +1085,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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Avalonia.Controls.Utils;
using Xunit;

namespace Avalonia.Controls.DataGrid.UnitTests.Utils
namespace Avalonia.Controls.DataGridTests.Utils
{
public class ReflectionHelperTests
{
Expand Down
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
Loading