refactor(module: table): reorganize the selection of rows (#3502)

* refactor(module: selection): reorganize the selection of rows

* hold the selected rows

* fix select all

* fix select all

* fix header checkbox Indeterminate status

* fix children selection

* select all within children

* fix CheckStrictly

* fix children cache

* use hashcode as the key of cache

* fix refrence loop

* fix circular reference expand

* clean up

* fix invoke table selected rows after check

* clean up

* prevent selected rows was changed multiple times

* update bunit
This commit is contained in:
James Yeung 2023-11-16 13:05:43 +08:00 committed by GitHub
parent 1409c277b5
commit dbb1978563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 203 additions and 201 deletions

View File

@ -4,10 +4,16 @@ namespace AntDesign
{
public interface ISelectionColumn : IColumn
{
public string Type { get; set; }
public bool Disabled { get; }
public string Key { get; }
public bool Selected { get; }
public bool CheckStrictly { get; set; }
public IList<ISelectionColumn> RowSelections { get; }
public void StateHasChanged();

View File

@ -54,7 +54,7 @@ namespace AntDesign
internal bool HasRowTemplate { get; }
internal void SelectionChanged();
//internal void SelectionChanged();
internal void OnExpandChange(RowData rowData);

View File

@ -31,6 +31,8 @@ namespace AntDesign
private bool IsHeaderDisabled => RowSelections.Any() && RowSelections.All(x => x.Disabled);
public bool Selected => DataItem.Selected;
private bool? _selected;
private void OnCheckedChange(bool selected)
@ -54,7 +56,7 @@ namespace AntDesign
}
else
{
DataItem.Selected = selected;
RowData.SetSelected(selected, CheckStrictly);
}
}
}
@ -76,6 +78,7 @@ namespace AntDesign
else if (IsBody)
{
Table?.Selection?.RowSelections.Add(this);
DataItem.Disabled = Disabled;
}
}

View File

@ -62,7 +62,7 @@
}
</CascadingValue>
</tr>
@body(_showItems, 0)
@body(_showItems)
</tbody>
@tfoot(this)
</table>
@ -83,7 +83,7 @@
}
</CascadingValue>
</tr>
@body(_showItems, 0)
@body(_showItems)
</tbody>
@tfoot(this)
</table>
@ -96,7 +96,7 @@
@colGroup((this, true))
@header(this)
<tbody class="ant-table-tbody">
@body( _showItems, 0)
@body( _showItems)
</tbody>
@tfoot(this)
</table>
@ -182,21 +182,24 @@ RenderFragment<(Table<TItem> table, bool header)> colGroup = ctx =>
<col style="width: @((CssSizeLength)table.ScrollBarWidth); min-width: @((CssSizeLength)table.ScrollBarWidth);" />
}
</colgroup>
</Template>;
</Template>
;
RenderFragment body(IEnumerable<TItem> showItems, int level)
{
if (!_hasInitialized)
RenderFragment body(IEnumerable<TItem> showItems, RowData<TItem> parentRowData = null)
{
return builder => { };
}
var level = (parentRowData?.Level + 1) ?? 0;
if (!_hasInitialized)
{
return builder => { };
}
#if NET5_0_OR_GREATER
if (UseItemsProvider&&!_isVirtualizeEmpty)
{
return @<Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize TItem="RowData<TItem>"
OverscanCount="10"
ItemsProvider="ItemsProvider"
ItemContent="bodyRow()" >
ItemContent="bodyRow()">
<Placeholder>
<tr class="ant-table-row ant-table-row-level-0">
@foreach (var item in ColumnContext.HeaderColumns)
@ -238,15 +241,16 @@ RenderFragment<(Table<TItem> table, bool header)> colGroup = ctx =>
{
return@<ForeachLoop
TItem="RowData<TItem>"
Items="_groups.Select((data,index)=> GetGroupRowData(data,index,level))"
ChildContent="bodyRow()"></ForeachLoop>;
Items="_groups.Select((data,index)=> GetGroupRowData(data,index,level, parentRowData?.Children ))"
ChildContent="bodyRow()"></ForeachLoop>;
}
#if NET5_0_OR_GREATER
else if (EnableVirtualization) {
else if (EnableVirtualization)
{
return @<Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize TItem="RowData<TItem>"
OverscanCount="10"
Items="showItems.Select((data, index) => GetRowData(data, index, level)).ToList()"
Items="showItems.Select((data, index) => GetRowData(data, index, level, parentRowData?.Children )).ToList()"
ItemContent="bodyRow()">
</Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize>;
}
@ -255,9 +259,9 @@ RenderFragment<(Table<TItem> table, bool header)> colGroup = ctx =>
{
{ @*Build will fail without this block*@ }
return
@<ForeachLoop TItem="RowData<TItem>"
Items="showItems.Select((data, index) => GetRowData(data, index, level))"
ChildContent="bodyRow()"/>;
@<ForeachLoop TItem="RowData<TItem>"
Items="showItems.Select((data, index) => GetRowData(data, index, level, parentRowData?.Children))"
ChildContent="bodyRow()" />;
}
return builder => { };
@ -267,8 +271,6 @@ RenderFragment<(Table<TItem> table, bool header)> colGroup = ctx =>
{
return currentRowData =>
{
//var currentRowData = GetRowData(tuple.data, tuple.index, level);
return @<TableRowWrapper RowData="currentRowData">
@{
var currentDataItem = currentRowData.DataItem;
@ -289,7 +291,7 @@ RenderFragment<(Table<TItem> table, bool header)> colGroup = ctx =>
}
else
{
<TableRow @key="currentDataItem.Data">
<TableRow>
@ChildContent(currentDataItem.Data)
</TableRow>
}
@ -297,9 +299,11 @@ RenderFragment<(Table<TItem> table, bool header)> colGroup = ctx =>
</CascadingValue>
</CascadingValue>
// When expand button was clicked, would trigger here to add one more child level
@if (currentDataItem.HasChildren && currentRowData.Expanded)
{
@body(SortFilterChildren(currentDataItem.Children), currentRowData.Level + 1);
currentRowData.Children??=new();
@body(SortFilterChildren(currentDataItem.Children), currentRowData);
}
@if (!currentDataItem.HasChildren && ExpandTemplate != null && RowExpandable(currentRowData) && currentRowData.Expanded)
{

View File

@ -7,8 +7,8 @@ namespace AntDesign
{
public partial class Table<TItem> : ITable
{
private Dictionary<TItem, TableDataItem<TItem>> _dataSourceCache;
private Dictionary<TItem, RowData<TItem>> _rootRowDataCache;
private Dictionary<int, TableDataItem<TItem>> _dataSourceCache;
private Dictionary<int, RowData<TItem>> _rootRowDataCache;
private void FlushCache()
{
@ -23,5 +23,80 @@ namespace AntDesign
_selection?.StateHasChanged();
}
private RowData<TItem> GetGroupRowData(IGrouping<object, TItem> grouping, int index, int level, Dictionary<int, RowData<TItem>> rowCache = null)
{
int rowIndex = index + 1;
if (level == 0)
{
rowIndex += PageSize * (PageIndex - 1);
}
var groupRowData = new RowData<TItem>()
{
Key = grouping.Key.ToString(),
IsGrouping = true,
RowIndex = rowIndex,
DataItem = new TableDataItem<TItem>
{
HasChildren = true,
Table = this,
Children = grouping
},
Children = grouping.Select((data, index) => GetRowData(data, index, level, rowCache)).ToDictionary(x => GetHashCode(x.Data), x => x)
};
return groupRowData;
}
private RowData<TItem> GetRowData(TItem data, int index, int level, Dictionary<int, RowData<TItem>> rowCache = null)
{
int rowIndex = index + 1;
if (level == 0)
{
rowIndex += PageSize * (PageIndex - 1);
}
var dataHashCode = GetHashCode(data);
if (!_dataSourceCache.TryGetValue(dataHashCode, out var currentDataItem) || currentDataItem == null)
{
currentDataItem = new TableDataItem<TItem>(data, this);
currentDataItem.SetSelected(SelectedRows.Contains(data), triggersSelectedChanged: false);
_dataSourceCache.Add(dataHashCode, currentDataItem);
}
// this row cache may be for children rows
rowCache ??= _rootRowDataCache;
if (!rowCache.TryGetValue(dataHashCode, out var currentRowData) || currentRowData == null)
{
currentRowData = new RowData<TItem>(currentDataItem)
{
Expanded = DefaultExpandAllRows && level < DefaultExpandMaxLevel
};
rowCache.Add(dataHashCode, currentRowData);
}
currentRowData.Level = level;
currentRowData.RowIndex = rowIndex;
currentRowData.PageIndex = PageIndex;
if (currentDataItem.HasChildren && (level < DefaultExpandMaxLevel || currentRowData.Expanded))
{
foreach (var (item, i) in currentDataItem.Children.Select((item, index) => (item, index)))
{
currentRowData.Children ??= [];
if (currentRowData.Children.ContainsKey(GetHashCode(item)))
continue;
GetRowData(item, i, level + 1, currentRowData.Children);
}
}
return currentRowData;
}
}
}

View File

@ -11,6 +11,9 @@ namespace AntDesign
{
private IEnumerable<TItem> _outerSelectedRows;
/// <summary>
/// Selected rows across pages
/// </summary>
[Parameter]
public IEnumerable<TItem> SelectedRows
{
@ -25,16 +28,11 @@ namespace AntDesign
public EventCallback<IEnumerable<TItem>> SelectedRowsChanged { get; set; }
private ISelectionColumn _selection;
private readonly HashSet<TItem> _selectedRows = new();
private readonly HashSet<TItem> _selectedRows;
private bool _preventRowDataTriggerSelectedRowsChanged;
internal void DataItemSelectedChanged(TableDataItem<TItem> dataItem, bool selected)
{
if (!RowSelectable(dataItem.Data))
{
dataItem.SetSelected(!selected, triggersSelectedChanged: false);
return;
}
if (selected)
{
_selectedRows.Add(dataItem.Data);
@ -43,11 +41,9 @@ namespace AntDesign
{
_selectedRows.Remove(dataItem.Data);
}
if (!_preventRowDataTriggerSelectedRowsChanged)
{
SelectionChanged();
_selection?.StateHasChanged();
}
SelectionChanged();
_selection?.StateHasChanged();
}
ISelectionColumn ITable.Selection
@ -56,21 +52,23 @@ namespace AntDesign
set => _selection = value;
}
bool ITable.AllSelected => _selectedRows.Count != 0 && _selectedRows.Count == GetAllItemsByTopLevelItems(_showItems, true).Count();
bool ITable.AllSelected => _selectedRows.Any() && _dataSourceCache.Values.All(x => x.Disabled || x.Selected);
bool ITable.AnySelected => _selectedRows.Count > 0;
bool ITable.AnySelected => _dataSourceCache.Values.Any(x => !x.Disabled && x.Selected);
public void SelectAll()
{
_selectedRows.Clear();
foreach (var selectedRow in GetAllItemsByTopLevelItems(_showItems, true))
_preventRowDataTriggerSelectedRowsChanged = true;
foreach (var select in _dataSourceCache.Values)
{
_selectedRows.Add(selectedRow);
}
foreach (TableDataItem<TItem> dataItem in _dataSourceCache.Values)
{
dataItem.SetSelected(_selectedRows.Contains(dataItem.Data));
if (select.Disabled)
continue;
select.SetSelected(true);
_selectedRows.Add(select.Data);
}
_preventRowDataTriggerSelectedRowsChanged = false;
_selection?.StateHasChanged();
SelectionChanged();
@ -78,17 +76,21 @@ namespace AntDesign
private void ClearSelectedRows()
{
_selectedRows.Clear();
foreach (TableDataItem<TItem> dataItem in _dataSourceCache.Values)
{
dataItem.SetSelected(false);
_selectedRows.Remove(dataItem.Data);
}
}
public void UnselectAll()
{
_preventRowDataTriggerSelectedRowsChanged = true;
ClearSelectedRows();
_preventRowDataTriggerSelectedRowsChanged = false;
_selection?.StateHasChanged();
SelectionChanged();
}
@ -99,14 +101,15 @@ namespace AntDesign
/// </summary>
public void SetSelection(ICollection<string> keys)
{
_preventRowDataTriggerSelectedRowsChanged = true;
ClearSelectedRows();
if (keys?.Count > 0)
{
_preventRowDataTriggerSelectedRowsChanged = true;
_selection?.RowSelections.ForEach(x => x.RowData.Selected = keys.Contains(x.Key));
_preventRowDataTriggerSelectedRowsChanged = false;
_selection?.RowSelections.ForEach(x => x.RowData.SetSelected(keys.Contains(x.Key), x.CheckStrictly));
}
_preventRowDataTriggerSelectedRowsChanged = false;
_selection?.StateHasChanged();
SelectionChanged();
}
@ -114,10 +117,11 @@ namespace AntDesign
// Only select the given row (for radio selection)
void ITable.SetSelection(ISelectionColumn selectItem)
{
ClearSelectedRows();
_preventRowDataTriggerSelectedRowsChanged = true;
selectItem.RowData.Selected = true;
ClearSelectedRows();
selectItem.RowData.SetSelected(true, selectItem.CheckStrictly);
_preventRowDataTriggerSelectedRowsChanged = false;
_selection?.StateHasChanged();
@ -126,19 +130,20 @@ namespace AntDesign
private void SelectItem(TItem item)
{
if (!RowSelectable(item))
return;
_preventRowDataTriggerSelectedRowsChanged = true;
_selectedRows.Add(item);
if (_dataSourceCache.TryGetValue(item, out var rowData))
if (_dataSourceCache.TryGetValue(GetHashCode(item), out var rowData))
{
rowData.SetSelected(true);
}
_preventRowDataTriggerSelectedRowsChanged = false;
}
public void SetSelection(IEnumerable<TItem> items)
{
if (ReferenceEquals(items, _selectedRows))
if (items.SequenceEqual(_selectedRows, this))
return;
if (items is not null and not IReadOnlyCollection<TItem>)
@ -146,41 +151,35 @@ namespace AntDesign
// (which would happen when the given enumerable is based on _selectedRows with linq methods)
items = items.ToArray();
_preventRowDataTriggerSelectedRowsChanged = true;
ClearSelectedRows();
items?.ForEach(SelectItem);
_selection?.StateHasChanged();
SelectionChanged();
_preventRowDataTriggerSelectedRowsChanged = false;
}
public void SetSelection(TItem item)
{
_preventRowDataTriggerSelectedRowsChanged = true;
ClearSelectedRows();
if (item != null)
{
SelectItem(item);
}
_preventRowDataTriggerSelectedRowsChanged = false;
_selection?.StateHasChanged();
SelectionChanged();
}
#if NET5_0_OR_GREATER
[MemberNotNull(nameof(_selection))]
#endif
private void EnsureSelection()
{
if (_selection == null)
{
throw new InvalidOperationException("To use the SetSelection method for a table, you should add a Selection component to the column definition.");
}
}
void ITable.SelectionChanged() => SelectionChanged();
//void ITable.SelectionChanged() => SelectionChanged();
private void SelectionChanged()
{
if (SelectedRowsChanged.HasDelegate)
if (SelectedRowsChanged.HasDelegate && !_preventRowDataTriggerSelectedRowsChanged)
{
_preventRender = true;
_outerSelectedRows = _selectedRows;

View File

@ -83,9 +83,6 @@ namespace AntDesign
[Parameter]
public Func<RowData<TItem>, bool> RowExpandable { get; set; } = _ => true;
[Parameter]
public Func<TItem, bool> RowSelectable { get; set; } = _ => true;
[Parameter]
public Func<TItem, IEnumerable<TItem>> TreeChildren { get; set; } = _ => Enumerable.Empty<TItem>();
@ -273,8 +270,9 @@ namespace AntDesign
public Table()
{
_dataSourceCache = new(this);
_rootRowDataCache = new(this);
_dataSourceCache = new();
_rootRowDataCache = new();
_selectedRows = new(this);
}
private List<IFieldColumn> _groupedColumns = [];
@ -473,7 +471,7 @@ namespace AntDesign
if (ServerSide)
{
_showItems = _dataSource;
_showItems = _dataSource ?? Enumerable.Empty<TItem>();
_total = Total;
}
else
@ -510,16 +508,6 @@ namespace AntDesign
{
SetSelection(_outerSelectedRows);
}
else
{
_selectedRows?.Clear();
}
var removedCacheItems = _dataSourceCache.Keys.Except(_showItems).ToArray();
foreach (var item in removedCacheItems)
{
_dataSourceCache.Remove(item);
}
}
_treeMode = (TreeChildren != null && (_showItems?.Any(x => TreeChildren(x)?.Any() == true) == true)) || _groupedColumns.Count > 0;
@ -655,33 +643,6 @@ namespace AntDesign
FieldFilterTypeResolver ??= InjectedFieldFilterTypeResolver;
}
private IEnumerable<TItem> GetAllItemsByTopLevelItems(IEnumerable<TItem> items, bool onlySelectable = false)
{
if (items?.Any() != true) return Array.Empty<TItem>();
if (TreeChildren != null)
{
var itemsSet = new HashSet<TItem>();
AddAllItemsAndChildren(items);
items = itemsSet;
void AddAllItemsAndChildren(IEnumerable<TItem> itemsToAdd)
{
if (itemsToAdd is null)
return;
foreach (TItem item in itemsToAdd)
{
if (!itemsSet.Add(item))
continue;
AddAllItemsAndChildren(TreeChildren(item));
}
}
}
if (onlySelectable) items = items.Where(x => RowSelectable(x));
return items;
}
protected override void OnParametersSet()
{
base.OnParametersSet();
@ -863,76 +824,14 @@ namespace AntDesign
return RowKey(x).Equals(RowKey(y));
}
int IEqualityComparer<TItem>.GetHashCode(TItem obj)
int IEqualityComparer<TItem>.GetHashCode(TItem obj) => GetHashCode(obj);
private int GetHashCode(TItem obj)
{
if (RowKey == null)
RowKey = data => data;
return RowKey(obj).GetHashCode();
}
private RowData<TItem> GetGroupRowData(IGrouping<object, TItem> grouping, int index, int level)
{
var groupRowData = new RowData<TItem>()
{
Key = grouping.Key.ToString(),
IsGrouping = true,
DataItem = new TableDataItem<TItem>
{
HasChildren = true,
Table = this,
Children = grouping
},
Children = grouping.Select((data, index) => GetRowData(data, index, level)).ToDictionary(x => x.Data, x => x)
};
groupRowData.DataItem.RowData = groupRowData;
return groupRowData;
}
private RowData<TItem> GetRowData(TItem data, int index, int level)
{
int rowIndex;
if (level == 0)
{
rowIndex = PageSize * (PageIndex - 1) + index + 1;
}
else
{
rowIndex = index + 1;
}
if (!_dataSourceCache.TryGetValue(data, out var currentDataItem) || currentDataItem == null)
{
currentDataItem = new TableDataItem<TItem>(data, this);
currentDataItem.SetSelected(SelectedRows.Contains(data), triggersSelectedChanged: false);
_dataSourceCache.Add(data, currentDataItem);
}
// this row cache may be for children rows
var rowCache = currentDataItem.RowData?.Children ?? _rootRowDataCache;
if (rowCache!.TryGetValue(data, out var currentRowData) || currentRowData == null)
{
currentRowData = new RowData<TItem>(currentDataItem)
{
Expanded = DefaultExpandAllRows && level < DefaultExpandMaxLevel
};
rowCache[data] = currentRowData;
currentDataItem.RowData ??= currentRowData;
}
if (currentDataItem.HasChildren && currentRowData.Expanded)
{
currentRowData.Children = new(this);
}
currentRowData.Level = level;
currentRowData.RowIndex = rowIndex;
currentRowData.PageIndex = PageIndex;
return currentRowData;
}
}
}

View File

@ -19,7 +19,7 @@ namespace AntDesign.TableModels
/// <summary>
/// hold the state of children rows
/// </summary>
public Dictionary<TItem, RowData<TItem>> Children { get; set; }
public Dictionary<int, RowData<TItem>> Children { get; set; }
public RowData()
{ }
@ -28,6 +28,17 @@ namespace AntDesign.TableModels
{
DataItem = dataItem;
}
protected override void CheckedChildren(bool isSelected, bool checkStrictly)
{
if (Children?.Any() != true)
return;
foreach (var item in Children)
{
item.Value.SetSelected(isSelected, checkStrictly);
}
}
}
/// <summary>
@ -63,7 +74,7 @@ namespace AntDesign.TableModels
public abstract TableDataItem TableDataItem { get; }
public bool Selected { get => TableDataItem.Selected; set => TableDataItem.Selected = value; }
public bool Selected { get => TableDataItem.Selected; }
public event Action<RowData, bool> ExpandedChanged;
@ -71,6 +82,18 @@ namespace AntDesign.TableModels
{
_expanded = expanded;
}
protected abstract void CheckedChildren(bool isSelected, bool checkStrictly);
internal void SetSelected(bool isSelected, bool checkStrictly)
{
TableDataItem.SetSelected(isSelected);
if (checkStrictly)
{
CheckedChildren(isSelected, checkStrictly);
}
}
}
/// <inheritdoc />
@ -82,8 +105,6 @@ namespace AntDesign.TableModels
public IEnumerable<TItem> Children { get; set; }
public RowData<TItem> RowData { get; set; }
public TableDataItem()
{
}
@ -98,7 +119,6 @@ namespace AntDesign.TableModels
protected override void OnSelectedChanged(bool value)
{
base.OnSelectedChanged(value);
Table.DataItemSelectedChanged(this, value);
}
}
@ -111,30 +131,22 @@ namespace AntDesign.TableModels
/// <br/>
/// For row specific data, see <see cref="RowData"/>.
/// </summary>
public class TableDataItem
public abstract class TableDataItem
{
private bool _selected;
public bool Selected
{
get => _selected;
set
{
if (_selected != value)
{
OnSelectedChanged(value);
}
}
}
public bool Disabled { get; set; }
public virtual bool HasChildren { get; set; }
public event Action<TableDataItem, bool> SelectedChanged;
protected virtual void OnSelectedChanged(bool value)
{
SetSelected(value);
}
protected abstract void OnSelectedChanged(bool value);
internal void SetSelected(bool selected, bool triggersSelectedChanged = true)
{
@ -144,6 +156,9 @@ namespace AntDesign.TableModels
}
_selected = selected;
OnSelectedChanged(_selected);
if (triggersSelectedChanged)
SelectedChanged?.Invoke(this, _selected);
}

View File

@ -9,7 +9,9 @@
@bind-PageSize="_pageSize"
@bind-SelectedRows="selectedRows"
OnChange="OnChange"
Size="TableSize.Small">
Size="TableSize.Small"
RowKey="x=>x.Id"
>
<Selection Key="@(context.Id.ToString())" />
<PropertyColumn Property="c=>c.Id" Sortable />
<PropertyColumn Property="c=>c.Date" Format="yyyy-MM-dd" Sortable />

View File

@ -16,7 +16,7 @@
}
</h5>
<Table @ref="table" DataSource="@data" @bind-SelectedRows="selectedRows" RowSelectable="@(x => x.Name!="Disabled User")" RowKey="x=>x.Name">
<Table @ref="table" DataSource="@data" @bind-SelectedRows="selectedRows" RowKey="x=>x.Name">
<Selection Key="@context.Name" Type="@selectionType" Disabled="@(context.Name == "Disabled User")" />
<PropertyColumn Property="c=>c.Name">
<a>@context.Name</a>
@ -26,7 +26,6 @@
</Table>
</div>
@code {
ITable table;
@ -73,7 +72,6 @@
protected override void OnInitialized()
{
selectedRows = data[..1];
}
public void RemoveSelection(string key)

View File

@ -1,7 +1,7 @@
@using System.ComponentModel
<Table DataSource="_data" TreeChildren="item=>item.Children" DefaultExpandAllRows Size="TableSize.Small">
<Selection CheckStrictly />
<Selection />
<PropertyColumn Property="c=>c.Name" />
<PropertyColumn Property="c=>c.Age" Width="12%" Sortable />
<PropertyColumn Property="c=>c.Address" Width="30%" />

View File

@ -27,7 +27,8 @@
OnChange="OnChange"
Total="total"
RemoteDataSource
Bordered>
Bordered
RowKey="x=>x.Name">
<Selection />
<PropertyColumn Property="c=>c.Name" Width="400" />
<PropertyColumn Property="c=>c.Age" Width="150" />