From 3b5274a3151ffe9c2d5c1f1bcaa317972ce9feca Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Mon, 15 Jul 2024 13:37:09 +0800 Subject: [PATCH] feat(Table): add RowTemplate (#3854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 精简代码 * feat: 增加 TableRowContext 上下文类 * refactor: 精简代码 * feat: 增加 RowTemplate 模板参数 * refactor: 增加 RowTemplate 模板逻辑 * doc: 增加 CustomTableRow 组件 * doc: 增加 RowTemplate 文档 * revert: 撤销代码写法 * test: 增加单元测试 * doc: 更新文档 * doc: 增加描述信息 * test: 更新单元测试 * refactor: 更新单元测试 * test: 更新单元测试 * test: 更新单元测试 * test: 更新单元测试 --- .../Components/CustomTableRow.razor | 6 + .../Components/CustomTableRow.razor.cs | 38 +++ .../Components/Samples/Table/TablesRow.razor | 20 ++ .../Samples/Table/TablesRow.razor.cs | 10 + src/BootstrapBlazor.Server/Locales/en-US.json | 5 +- src/BootstrapBlazor.Server/Locales/zh-CN.json | 5 +- .../Components/Table/Table.razor | 225 ++++++++++-------- .../Components/Table/Table.razor.cs | 6 + .../Components/Table/TableColumnContext.cs | 5 +- .../Components/Table/TableRowContext.cs | 24 ++ .../Attributes/AutoGenerateClassTest.cs | 4 +- test/UnitTest/Components/ButtonTest.cs | 1 + test/UnitTest/Components/ContextMenuTest.cs | 7 +- test/UnitTest/Components/TableColumnTest.cs | 4 + test/UnitTest/Components/TableTest.cs | 43 ++++ .../Extensions/ITableColumnExtensionsTest.cs | 14 +- 16 files changed, 296 insertions(+), 121 deletions(-) create mode 100644 src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor create mode 100644 src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor.cs create mode 100644 src/BootstrapBlazor/Components/Table/TableRowContext.cs diff --git a/src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor b/src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor new file mode 100644 index 000000000..40f96840a --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor @@ -0,0 +1,6 @@ + + + +@Context.Row.Name +@Context.Row.Address +@Context.Row.Count diff --git a/src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor.cs b/src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor.cs new file mode 100644 index 000000000..80645232a --- /dev/null +++ b/src/BootstrapBlazor.Server/Components/Components/CustomTableRow.razor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.Server.Components.Components; + +/// +/// 自定义行组件 +/// +public partial class CustomTableRow +{ + /// + /// 获得/设置 行上下文数据实例 + /// + [Parameter] + [NotNull] + public TableRowContext? Context { get; set; } + + /// + /// 获得/设置 值改变回调方法 + /// + [Parameter] + public Func, Task>? OnValueChanged { get; set; } + + private async Task OnDateTimeChanged(DateTime? dt) + { + // 通知数据源数据已更新 + Context.Row.DateTime = dt; + Context.Row.Count = Random.Shared.Next(1, 100); + if (OnValueChanged != null) + { + await OnValueChanged(Context); + } + + // 仅更新本组件不更新父 Table 组件 + StateHasChanged(); + } +} diff --git a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor index b448409d7..1602e43e5 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor @@ -142,3 +142,23 @@ + + +
@((MarkupString)Localizer["RowTemplateDesc"].Value)
+ + + + + + + + + + + +
+
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor.cs index 2362052af..43659e284 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Table/TablesRow.razor.cs @@ -98,4 +98,14 @@ public partial class TablesRow } private static string? SetRowClassFormatter(Foo item) => item.Count > 60 ? "row-highlight" : null; + + private Task UpdateRowValue(TableRowContext context) + { + // 触发内部数据变化通知 + // 本例中 context.Row 就是数据源内数据,此处不需要进行数据处理 + // 如果使用数据库此处可以根据数据变化项进行数据库更新即可 + // 此处不需要 StateHasChanged 更新 UI + + return Task.CompletedTask; + } } diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 607b76254..d498cc2aa 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -5422,7 +5422,10 @@ "RowNumberText": "Number", "ClickToSelectP3": "Currently selected row:", "ClickToSelectNoneText": "None", - "PlaceHolder": "Cannot be empty, within 50 characters" + "PlaceHolder": "Cannot be empty, within 50 characters", + "RowTemplateTitle": "Row Template", + "RowTemplateIntro": "By setting the RowTemplate template, you can implement custom row cell linkage logic by sub-packaging components to achieve performance optimization and avoid the problem of refreshing the entire table component after linkage due to cell data refresh.", + "RowTemplateDesc": "In this example, a custom component is used to select a date, link the quantity column, randomly generate a random number, and save it to the original data, so there is no need to refresh the entire Table component." }, "BootstrapBlazor.Server.Components.Samples.Table.TablesDynamic": { "TablesDynamicTitle": "Table Dynamic", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 84ac8862f..bd795d35b 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -5422,7 +5422,10 @@ "RowNumberText": "序号", "ClickToSelectP3": "当前选中行:", "ClickToSelectNoneText": "无", - "PlaceHolder": "不可为空,50字以内" + "PlaceHolder": "不可为空,50字以内", + "RowTemplateTitle": "行模板", + "RowTemplateIntro": "通过设置 RowTemplate 模板,可通过分装组件的方式实现自定义行内单元格联动逻辑,达到性能最优化,避免单元格数据刷新导致联动后需要刷新整个表格组件的问题,可用于销售类软件,调整单价时总价列变化需求", + "RowTemplateDesc": "本例中通过自定义组件,实现选择日期后,联动数量列,随机生成一个随机数字,并且保存到原始数据中,从而不需要刷新整个 Table 组件" }, "BootstrapBlazor.Server.Components.Samples.Table.TablesDynamic": { "TablesDynamicTitle": "Table 表格", diff --git a/src/BootstrapBlazor/Components/Table/Table.razor b/src/BootstrapBlazor/Components/Table/Table.razor index cd1c75e64..008afac43 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor +++ b/src/BootstrapBlazor/Components/Table/Table.razor @@ -249,38 +249,46 @@ { @RenderExtendButtons(item) } - @foreach (var col in GetVisibleColumns()) + @if (RowTemplate != null) { - var cellClass = ""; - string? value = null; - RenderFragment? valueTemplate = null; -
- - @if (col.OnCellRender != null) - { - var cell = new TableCellArgs { Row = item, ColumnName = col.GetFieldName() }; - col.OnCellRender(cell); - cellClass = cell.Class; - value = cell.Value; - valueTemplate = cell.ValueTemplate; - } - - @if (valueTemplate != null) + var columns = GetVisibleColumns(); + @RowTemplate(new (item, columns)); + } + else + { + @foreach (var col in GetVisibleColumns()) + { + var cellClass = ""; + string? value = null; + RenderFragment? valueTemplate = null; +
+ + @if (col.OnCellRender != null) { - @valueTemplate + var cell = new TableCellArgs { Row = item, ColumnName = col.GetFieldName() }; + col.OnCellRender(cell); + cellClass = cell.Class; + value = cell.Value; + valueTemplate = cell.ValueTemplate; } - else if (value != null) - { - @value - } - else - { - @GetValue(col, item) - } - -
+ + @if (valueTemplate != null) + { + @valueTemplate + } + else if (value != null) + { + @value + } + else + { + @GetValue(col, item) + } + +
+ } } @if (ShowExtendButtons && !IsExtendButtonsInRowHeader) { @@ -656,14 +664,15 @@ RenderFragment RenderRow => item => @ @if (ShowDetails()) {
- @if (ShowDetailRow == null || ShowDetailRow.Invoke(item)) + @if (ShowDetailRow == null || ShowDetailRow(item)) { } @@ -696,88 +705,96 @@ { @RenderRowExtendButtons(item) } - @foreach (var col in GetVisibleColumns()) + @if (RowTemplate != null) { - if (CheckShownWithBreakpoint(col)) + var columns = GetVisibleColumns(); + @RowTemplate(new (item, columns)); + } + else + { + @foreach (var col in GetVisibleColumns()) { - if (colIndex > 1) + if (CheckShownWithBreakpoint(col)) { - colIndex--; - continue; - } - var cellClass = ""; - var colspan = 0; - string? value = null; - RenderFragment? valueTemplate = null; - if (col.OnCellRender != null) - { - var cell = new TableCellArgs { Row = item, ColumnName = col.GetFieldName() }; - col.OnCellRender(cell); - cellClass = cell.Class; - colspan = cell.Colspan; - valueTemplate = cell.ValueTemplate; - value = cell.Value; - colIndex = colspan; - } - - @{ - var isFirstColOfTree = IsTree && index++ == 0; - var degree = 0; - var isExpend = false; - var hasChildren = false; - if (isFirstColOfTree) - { - var treeItem = TreeNodeCache.Find(TreeRows, item, out degree); - if(treeItem != null) - { - isExpend = treeItem.IsExpand; - hasChildren = treeItem.HasChildren; - } - } - var hesTreeChildren = isFirstColOfTree && hasChildren; + if (colIndex > 1) + { + colIndex--; + continue; } - - @if (isFirstColOfTree) - { -
- @if (hesTreeChildren) + var cellClass = ""; + var colspan = 0; + string? value = null; + RenderFragment? valueTemplate = null; + if (col.OnCellRender != null) + { + var cell = new TableCellArgs { Row = item, ColumnName = col.GetFieldName() }; + col.OnCellRender(cell); + cellClass = cell.Class; + colspan = cell.Colspan; + valueTemplate = cell.ValueTemplate; + value = cell.Value; + colIndex = colspan; + } + + @{ + var isFirstColOfTree = IsTree && index++ == 0; + var degree = 0; + var isExpend = false; + var hasChildren = false; + if (isFirstColOfTree) { - + var treeItem = TreeNodeCache.Find(TreeRows, item, out degree); + if(treeItem != null) + { + isExpend = treeItem.IsExpand; + hasChildren = treeItem.HasChildren; + } } -
+ var hesTreeChildren = isFirstColOfTree && hasChildren; } - @if (IsExcel) - { - @RenderExcelCell(col, item, ItemChangedType.Update) - } - else if(isInCell) - { - @RenderCell(col, EditModel, AddInCell ? ItemChangedType.Add : ItemChangedType.Update) - } - else - { - var triggerDoubleClick = OnDoubleClickCellCallback != null; - - @if (valueTemplate != null) + + @if (isFirstColOfTree) + { +
+ @if (hesTreeChildren) { - @valueTemplate + } - else if (value != null) - { - @value - } - else - { - @GetValue(col, item) - } - - } - - +
+ } + @if (IsExcel) + { + @RenderExcelCell(col, item, ItemChangedType.Update) + } + else if(isInCell) + { + @RenderCell(col, EditModel, AddInCell ? ItemChangedType.Add : ItemChangedType.Update) + } + else + { + var triggerDoubleClick = OnDoubleClickCellCallback != null; + + @if (valueTemplate != null) + { + @valueTemplate + } + else if (value != null) + { + @value + } + else + { + @GetValue(col, item) + } + + } +
+ + } } } @if (ShowExtendButtons && !IsExtendButtonsInRowHeader) diff --git a/src/BootstrapBlazor/Components/Table/Table.razor.cs b/src/BootstrapBlazor/Components/Table/Table.razor.cs index 46f8ad8ab..becd4198f 100644 --- a/src/BootstrapBlazor/Components/Table/Table.razor.cs +++ b/src/BootstrapBlazor/Components/Table/Table.razor.cs @@ -450,6 +450,12 @@ public partial class Table : ITable, IModelEqualityComparer where [Parameter] public RenderFragment? DetailRowTemplate { get; set; } + /// + /// 获得/设置 行模板 + /// + [Parameter] + public RenderFragment>? RowTemplate { get; set; } + /// /// 获得/设置 TableHeader 实例 /// diff --git a/src/BootstrapBlazor/Components/Table/TableColumnContext.cs b/src/BootstrapBlazor/Components/Table/TableColumnContext.cs index 8bcac7a63..e2ce14e2d 100644 --- a/src/BootstrapBlazor/Components/Table/TableColumnContext.cs +++ b/src/BootstrapBlazor/Components/Table/TableColumnContext.cs @@ -7,9 +7,6 @@ namespace BootstrapBlazor.Components; /// /// TableColumn 上下文类 /// -/// -/// 构造方法 -/// /// /// public class TableColumnContext(TItem model, TValue value) @@ -23,5 +20,5 @@ public class TableColumnContext(TItem model, TValue value) /// /// 获得/设置 当前绑定字段数据实例 /// - public TValue Value { get; } = value; + public TValue Value => value; } diff --git a/src/BootstrapBlazor/Components/Table/TableRowContext.cs b/src/BootstrapBlazor/Components/Table/TableRowContext.cs new file mode 100644 index 000000000..a22c47d04 --- /dev/null +++ b/src/BootstrapBlazor/Components/Table/TableRowContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.Components; + +/// +/// TableRow 上下文类 +/// +/// +/// +public class TableRowContext(TItem model, IEnumerable columns) +{ + /// + /// 获得/设置 行数据实例 + /// + [NotNull] + public TItem Row { get; } = model ?? throw new ArgumentNullException(nameof(model)); + + /// + /// 获得/设置 当前绑定字段数据实例 + /// + public IEnumerable Columns => columns; +} diff --git a/test/UnitTest/Attributes/AutoGenerateClassTest.cs b/test/UnitTest/Attributes/AutoGenerateClassTest.cs index 21d2cb7a2..c6522bd82 100644 --- a/test/UnitTest/Attributes/AutoGenerateClassTest.cs +++ b/test/UnitTest/Attributes/AutoGenerateClassTest.cs @@ -150,10 +150,10 @@ public class AutoGenerateClassTest attrInterface.IsReadonlyWhenEdit = true; Assert.True(attrInterface.IsReadonlyWhenEdit); - attrInterface.GetTooltipTextCallback = new Func>(_ => Task.FromResult((string?)"Test")); + attrInterface.GetTooltipTextCallback = _ => Task.FromResult((string?)"Test"); Assert.NotNull(attrInterface.GetTooltipTextCallback); - attrInterface.CustomSearch = new Func((_, _) => new SearchFilterAction("test", "test")); + attrInterface.CustomSearch = (_, _) => new SearchFilterAction("test", "test"); Assert.NotNull(attrInterface.CustomSearch); attrInterface.Searchable = null; diff --git a/test/UnitTest/Components/ButtonTest.cs b/test/UnitTest/Components/ButtonTest.cs index 00ed29d37..fdbe4fe96 100644 --- a/test/UnitTest/Components/ButtonTest.cs +++ b/test/UnitTest/Components/ButtonTest.cs @@ -274,6 +274,7 @@ public class ButtonTest : BootstrapBlazorTestBase await cut.InvokeAsync(() => { + var button = cut.FindComponent