From debdabce33062c52f9a293977dbd5d56db8b00ba Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 9 Aug 2024 17:14:38 +0800 Subject: [PATCH] feat(TreeView): add MaxSelectedCount OnMaxSelectedCountExceed parameter (#4007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 增加 OnBeforeStateChanged 回调方法 * feat: 增加 MaxSelectedCount 参数 * refactor: 通过 OnBeforeStateChanged 实现逻辑 * refactor: 增加 OnMaxSelectedCountExceed 回调执行逻辑 * refactor: 增加客户端阻止逻辑 * refactor: 增加阻止继承标签 * doc: 更新文档 * refactor: 更新注释 * refactor: 精简代码 * test: 增加单元测试 * refactor: 重构代码 * test: 更新单元测试 * test: 更新单元测试 * doc: 更新文档 * doc: 增加多选最大值示例 --- .../Components/Samples/Checkboxs.razor.cs | 6 +- .../Components/Samples/TreeViews.razor | 8 +++ .../Components/Samples/TreeViews.razor.cs | 40 ++++++++++--- src/BootstrapBlazor.Server/Locales/en-US.json | 31 ++++++---- src/BootstrapBlazor.Server/Locales/zh-CN.json | 33 ++++++----- .../Components/Checkbox/Checkbox.razor.cs | 2 - .../Components/TreeView/TreeView.razor | 1 + .../Components/TreeView/TreeView.razor.cs | 35 ++++++++++- test/UnitTest/Components/TreeViewTest.cs | 59 +++++++++++++++++++ 9 files changed, 173 insertions(+), 42 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/Checkboxs.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Checkboxs.razor.cs index 6a9818e22..92ae66fb4 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/Checkboxs.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/Checkboxs.razor.cs @@ -94,7 +94,8 @@ public sealed partial class Checkboxs ValueList = " — ", DefaultValue = " — " }, - new(){ + new() + { Name = "IsDisabled", Description = Localizer["Att4"], Type = "boolean", @@ -102,14 +103,13 @@ public sealed partial class Checkboxs DefaultValue = "false" }, new() - { Name = "State", Description = Localizer["Att5"], Type = "CheckboxState", ValueList = "Mixed / Checked / UnChecked", DefaultValue = "UnChecked" - }, + } ]; /// diff --git a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor index c9253d844..413a5c29c 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor +++ b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor @@ -182,6 +182,14 @@ + +
@((MarkupString)Localizer["TreeViewMaxSelectedCountDesc"].Value)
+ +
+ diff --git a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs index 1183c9eb7..3da2ca2ce 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs @@ -81,6 +81,14 @@ public sealed partial class TreeViews new("False", "Keep") ]; + [Inject, NotNull] + private ToastService? ToastService { get; set; } + + private Task OnMaxSelectedCountExceed() + { + return ToastService.Information(Localizer["OnMaxSelectedCountExceedTitle"], Localizer["OnMaxSelectedCountExceedContent", 2]); + } + private Task OnTreeItemChecked(List> items) { Logger2.Log($"当前共选中{items.Count}项"); @@ -119,7 +127,7 @@ public sealed partial class TreeViews { var ret = TreeFoo.GetCheckedTreeItems(); ret[0].IsExpand = true; - ret[0].Items= TreeFoo.GetCheckedTreeItems(); + ret[0].Items = TreeFoo.GetCheckedTreeItems(); return ret; } @@ -235,7 +243,7 @@ public sealed partial class TreeViews builder.AddAttribute(2, nameof(Button.Text), "Click"); builder.AddAttribute(3, nameof(Button.OnClick), EventCallback.Factory.Create(this, e => { - ToastService.Warning("自定义 TreeItem", "测试 TreeItem 按钮点击事件"); + ToastService.Warning("自定义 TreeViewItem", "测试 TreeViewItem 按钮点击事件"); })); builder.CloseComponent(); } @@ -251,9 +259,9 @@ public sealed partial class TreeViews { Name = "Items", Description = "menu data set", - Type = "IEnumerable", + Type = "IEnumerable", ValueList = " — ", - DefaultValue = "new List(20)" + DefaultValue = "new List(20)" }, new() { @@ -291,7 +299,7 @@ public sealed partial class TreeViews { Name = nameof(TreeView.OnTreeItemClick), Description = "Callback delegate when tree control node is clicked", - Type = "Func", + Type = "Func", ValueList = " — ", DefaultValue = " — " }, @@ -299,7 +307,7 @@ public sealed partial class TreeViews { Name = nameof(TreeView.OnTreeItemChecked), Description = "Callback delegate when tree control node is selected", - Type = "Func", + Type = "Func", ValueList = " — ", DefaultValue = " — " }, @@ -307,7 +315,7 @@ public sealed partial class TreeViews { Name = nameof(TreeView.OnExpandNodeAsync), Description = "Tree control node expand callback delegate", - Type = "Func", + Type = "Func", ValueList = " — ", DefaultValue = " — " }, @@ -326,6 +334,22 @@ public sealed partial class TreeViews Type = "bool", ValueList = "true|false", DefaultValue = "false" + }, + new() + { + Name = nameof(TreeView.MaxSelectedCount), + Description = "The maximum count of selected node", + Type = "int", + ValueList = " — ", + DefaultValue = "0" + }, + new() + { + Name = nameof(TreeView.OnMaxSelectedCountExceed), + Description = "Select the callback method when the maximum number of nodes is reached", + Type = "Func", + ValueList = " — ", + DefaultValue = " — " } ]; @@ -335,7 +359,7 @@ public sealed partial class TreeViews { Name = nameof(TreeViewItem.Items), Description = "Child node data source", - Type = "List>", + Type = "List>", ValueList = " — ", DefaultValue = "new ()" }, diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json index 67f5a10e2..5b11df67b 100644 --- a/src/BootstrapBlazor.Server/Locales/en-US.json +++ b/src/BootstrapBlazor.Server/Locales/en-US.json @@ -630,15 +630,15 @@ "TreeViewsTitle": "Tree tree control", "TreeViewsDescription": "Present information in a clear hierarchy that can be expanded or collapsed", "TreeViewsTips1": "If the component is a generic component, you need to use TItem to specify the bound data model. In this example, the model is TreeFoo and needs to be set", - "TreeViewsTips2": "Set TreeItem its IsExpand parameter to control whether the current child node is expanded", + "TreeViewsTips2": "Set TreeViewItem its IsExpand parameter to control whether the current child node is expanded", "TreeViewsTips3": "When clicking on the small arrow to expand the child item, obtain the child item data collection through the OnExpandNodeAsync callback delegate method", "TreeViewsTips4": "Keep the node state fallback mechanism, ModelEqualityComparer CustomKeyAttribute IEqualityComparer<TItem> Equals overloaded method", "TreeViewsTips5": "The component will remain in the expanded collapsed selected state", - "TreeViewsTips6": "Set whether the node is expanded state through TreeItem<TItem>.IsExpand", - "TreeViewsTips7": "Set whether the node is selected state through TreeItem<TItem>.IsActive", - "TreeViewsTips8": "Through TreeItem<TItem>.Checked, set whether the node is in checked/single selection state", + "TreeViewsTips6": "Set whether the node is expanded state through TreeViewItem<TItem>.IsExpand", + "TreeViewsTips7": "Set whether the node is selected state through TreeViewItem<TItem>.IsActive", + "TreeViewsTips8": "Through TreeViewItem<TItem>.Checked, set whether the node is in checked/single selection state", "TreeViewsTips9": "Step 1: Set the TItem generic model", - "TreeViewsTips10": "Step 2: Set Items to get the component data source Note The data source type is IEnumerable<TreeItem<TItem>>", + "TreeViewsTips10": "Step 2: Set Items to get the component data source Note The data source type is IEnumerable<TreeViewItem<TItem>>", "TreeViewsTips11": "Step 3: Set the OnExpandNodeAsync callback delegate to expand the response node to get the child data source collection", "TreeViewsTips12": "Step 4: Set ModelEqualityComparer to provide a component identification model comparison delegate method, Note This setting is optional. Perform downgrade search through the fallback mechanism explained above", "TreeViewNormalTitle": "Basic usage", @@ -654,19 +654,19 @@ "TreeViewCheckboxButtonText": "refresh", "TreeViewTreeDisableTitle": "Disabled state", "TreeViewTreeDisableIntro": "Some nodes of the Tree can be set to disabled state", - "TreeViewTreeDisableDescription": "By setting the Disabled property of the data source TreeItem object, you can control whether this node can be checked or not. When set to false, it will not affect the node expansion. /shrink function", + "TreeViewTreeDisableDescription": "By setting the Disabled property of the data source TreeViewItem object, you can control whether this node can be checked or not. When set to false, it will not affect the node expansion. /shrink function", "TreeViewAccordionModelTitle": "Accordion mode", "TreeViewAccordionModelIntro": "For nodes of the same level, only one can be expanded at a time", "TreeViewAccordionModelDescription": "Enable the accordion effect by setting the IsAccordion property of the Tree component", "TreeViewDefaultExpandTitle": "Expanded by default and selected by default", "TreeViewDefaultExpandIntro": "Some nodes of Tree can be set to be expanded or selected by default", - "TreeViewDefaultExpandDescription": "By setting the IsExpand property of the TreeItem object, you can control whether this node is in the expanded state by default. In this example, navigation 2 is in the expanded state by default, and the rest Node defaults to contracted state", + "TreeViewDefaultExpandDescription": "By setting the IsExpand property of the TreeViewItem object, you can control whether this node is in the expanded state by default. In this example, navigation 2 is in the expanded state by default, and the rest Node defaults to contracted state", "TreeViewTreeDisplayIconTitle": "Show icon", "TreeViewTreeDisplayIconIntro": "Control whether the component displays the icon by setting ShowIcon", - "TreeViewTreeDisplayIconDescription": "By setting the ShowIcon property of the TreeItem object, you can control whether this node displays the icon or not", + "TreeViewTreeDisplayIconDescription": "By setting the ShowIcon property of the TreeViewItem object, you can control whether this node displays the icon or not", "TreeViewTreeClickExpandTitle": "Click on the node to expand and contract", "TreeViewTreeClickExpandIntro": "By setting ClickToggleNode to control whether to expand and contract when the node is clicked", - "TreeViewTreeClickExpandDescription": "By setting the ClickToggleNode property of the TreeItem object, you can control whether this node can be expanded and contracted by clicking", + "TreeViewTreeClickExpandDescription": "By setting the ClickToggleNode property of the TreeViewItem object, you can control whether this node can be expanded and contracted by clicking", "TreeViewTreeValidationFormTitle": "The Tree component is built into the validation form", "TreeViewTreeValidationFormIntro": "Tree can be enabled inside the component Checkbox will be displayed when it is built into the validation form DisplayName This function needs to be disabled in the tree component", "TreeViewTreeValidationFormDescription": "Show Checkbox built into validation component ValidateForm by setting ShowCheckbox property does not show DisplayName", @@ -674,9 +674,9 @@ "TreeViewTreeLazyLoadingIntro": "Dynamically add child nodes when expanding a node", "TreeViewTreeLazyLoadingDescription": "By setting the node HasChildNode to control whether to display the small arrow picture of the node. Add nodes through Tree's OnExpandNodeAsync delegate", "TreeViewTreeCustomNodeTitle": "Custom node", - "TreeViewTreeCustomNodeIntro": "Implement your own node template by setting TreeItem Template", + "TreeViewTreeCustomNodeIntro": "Implement your own node template by setting TreeViewItem Template", "TreeViewTreeNodeColorTitle": "Node color", - "TreeViewTreeNodeColorIntro": "Implement your own node style by setting TreeItem CssClass", + "TreeViewTreeNodeColorIntro": "Implement your own node style by setting TreeViewItem CssClass", "TreeViewCheckedItemsTitle": "Get all selected nodes", "TreeViewCheckedItemsIntro": "Get all nodes by setting the OnTreeItemChecked callback delegate", "TreeViewCustomCheckedItemsTitle": "Customize the selected node", @@ -692,9 +692,14 @@ "TreeViewSetActiveTitle": "Set Active Node", "TreeViewSetActiveIntro": "Set the currently active node by calling the SetActiveItem method", "TreeViewSetActiveDisplayText": "Current Active Node", - "TreeViewsAttribute": "TreeItem property", + "TreeViewsAttribute": "TreeViewItem property", "TreeViewsDisableWholeTreeView": "Whole TreeView disable", - "TreeViewsWhetherToExpandWhenDisable": "Whether to expand when the control node is disabled" + "TreeViewsWhetherToExpandWhenDisable": "Whether to expand when the control node is disabled", + "OnMaxSelectedCountExceedTitle": "Maximum Info", + "OnMaxSelectedCountExceedContent": "You can select at most {0} items", + "TreeViewMaxSelectedCountTitle": "MaxSelectedCount", + "TreeViewMaxSelectedCountIntro": "Control the maximum number of selectable items by setting the MaxSelectedCount property, and handle the logic through the OnMaxSelectedCountExceed callback", + "TreeViewMaxSelectedCountDesc": "When more than 2 nodes are selected, a Toast prompt bar will pop up" }, "BootstrapBlazor.Server.Components.Samples.Trees": { "TreeIntro": "

Obsolete,The TreeView provides more functions", diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json index 22c47bdc7..3003c570e 100644 --- a/src/BootstrapBlazor.Server/Locales/zh-CN.json +++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json @@ -630,15 +630,15 @@ "TreeViewsTitle": "Tree 树形控件", "TreeViewsDescription": "用清晰的层级结构展示信息,可展开或折叠", "TreeViewsTips1": "组件为泛型组件需要使用 TItem 指定绑定的数据模型,本例中模型为 TreeFoo 需要设置", - "TreeViewsTips2": "设置 TreeItemIsExpand 参数控制当前子节点是否展开", + "TreeViewsTips2": "设置 TreeViewItemIsExpand 参数控制当前子节点是否展开", "TreeViewsTips3": "点击子项展开小箭头时,通过 OnExpandNodeAsync 回调委托方法获取子项数据集合", "TreeViewsTips4": "保持节点状态回落机制,ModelEqualityComparer CustomKeyAttribute IEqualityComparer<TItem> Equals 重载方法", "TreeViewsTips5": "组件将会保持 展开 收缩 选中 状态", - "TreeViewsTips6": "通过 TreeItem<TItem>.IsExpand 设置节点是否 展开 状态", - "TreeViewsTips7": "通过 TreeItem<TItem>.IsActive 设置节点是否 选中 状态", - "TreeViewsTips8": "通过 TreeItem<TItem>.Checked 设置节点是否 复选/单选 状态", + "TreeViewsTips6": "通过 TreeViewItem<TItem>.IsExpand 设置节点是否 展开 状态", + "TreeViewsTips7": "通过 TreeViewItem<TItem>.IsActive 设置节点是否 选中 状态", + "TreeViewsTips8": "通过 TreeViewItem<TItem>.Checked 设置节点是否 复选/单选 状态", "TreeViewsTips9": "第一步:设置 TItem 泛型模型", - "TreeViewsTips10": "第二步:设置 Items 获得组件数据源 注意 数据源类型为 IEnumerable<TreeItem<TItem>>", + "TreeViewsTips10": "第二步:设置 Items 获得组件数据源 注意 数据源类型为 IEnumerable<TreeViewItem<TItem>>", "TreeViewsTips11": "第三步:设置 OnExpandNodeAsync 回调委托响应节点展开获取子项数据源集合", "TreeViewsTips12": "第四步:设置 ModelEqualityComparer 提供组件识别模型比较委托方法,注意 本设置为可选项 通过上面讲解的回落机制进行降级搜索", "TreeViewNormalTitle": "基础用法", @@ -654,19 +654,19 @@ "TreeViewCheckboxButtonText": "刷新", "TreeViewTreeDisableTitle": "禁用状态", "TreeViewTreeDisableIntro": "可将 Tree 的某些节点设置为禁用状态", - "TreeViewTreeDisableDescription": "通过设置数据源 TreeItem 对象的 Disabled 属性,来控制此节点是否可以进行勾选动作,设置为 false 时不影响节点展开/收缩功能", + "TreeViewTreeDisableDescription": "通过设置数据源 TreeViewItem 对象的 Disabled 属性,来控制此节点是否可以进行勾选动作,设置为 false 时不影响节点展开/收缩功能", "TreeViewAccordionModelTitle": "手风琴模式", "TreeViewAccordionModelIntro": "对于同一级的节点,每次只能展开一个", "TreeViewAccordionModelDescription": "通过设置 Tree 组件的 IsAccordion 属性开启手风琴效果", "TreeViewDefaultExpandTitle": "默认展开和默认选中", "TreeViewDefaultExpandIntro": "可将 Tree 的某些节点设置为默认展开或默认选中", - "TreeViewDefaultExpandDescription": "通过设置 TreeItem 对象的 IsExpand 属性,来控制此节点是否默认为展开状态,本例中 导航二 默认为展开状态,其余节点默认为收缩状态", + "TreeViewDefaultExpandDescription": "通过设置 TreeViewItem 对象的 IsExpand 属性,来控制此节点是否默认为展开状态,本例中 导航二 默认为展开状态,其余节点默认为收缩状态", "TreeViewTreeDisplayIconTitle": "显示图标", "TreeViewTreeDisplayIconIntro": "通过设置 ShowIcon 来控制组件是否显示图标", - "TreeViewTreeDisplayIconDescription": "通过设置 TreeItem 对象的 ShowIcon 属性,来控制此节点是否显示图标", + "TreeViewTreeDisplayIconDescription": "通过设置 TreeViewItem 对象的 ShowIcon 属性,来控制此节点是否显示图标", "TreeViewTreeClickExpandTitle": "点击节点展开收缩功能", "TreeViewTreeClickExpandIntro": "通过设置 ClickToggleNode 来控制点击节点时是否进行展开收缩操作", - "TreeViewTreeClickExpandDescription": "通过设置 TreeItem 对象的 ClickToggleNode 属性,来控制此节点是否通过点击来实现展开收缩操作", + "TreeViewTreeClickExpandDescription": "通过设置 TreeViewItem 对象的 ClickToggleNode 属性,来控制此节点是否通过点击来实现展开收缩操作", "TreeViewTreeValidationFormTitle": "Tree 组件内置到验证表单中", "TreeViewTreeValidationFormIntro": "Tree 组件内部可开启 Checkbox 内置到验证表单时会显示 DisplayName 此功能在树状组件内需要禁止", "TreeViewTreeValidationFormDescription": "通过设置 ShowCheckbox 属性显示 Checkbox 内置到验证组件 ValidateForm 中不显示 DisplayName", @@ -674,9 +674,9 @@ "TreeViewTreeLazyLoadingIntro": "展开节点时动态添加子节点", "TreeViewTreeLazyLoadingDescription": "通过设置节点 HasChildNode 控制是否显示节点小箭头图片 。通过Tree的 OnExpandNodeAsync 委托添加节点", "TreeViewTreeCustomNodeTitle": "自定义节点", - "TreeViewTreeCustomNodeIntro": "通过设置 TreeItem Template 来实现自己的节点模板", + "TreeViewTreeCustomNodeIntro": "通过设置 TreeViewItem Template 来实现自己的节点模板", "TreeViewTreeNodeColorTitle": "节点颜色", - "TreeViewTreeNodeColorIntro": "通过设置 TreeItem CssClass 来实现自己的节点样式", + "TreeViewTreeNodeColorIntro": "通过设置 TreeViewItem CssClass 来实现自己的节点样式", "TreeViewCheckedItemsTitle": "获取所有选中节点", "TreeViewCheckedItemsIntro": "通过设置 OnTreeItemChecked 回调委托获取所有节点", "TreeViewCustomCheckedItemsTitle": "自定义选中节点", @@ -692,9 +692,14 @@ "TreeViewSetActiveTitle": "设置激活节点", "TreeViewSetActiveIntro": "通过调用 SetActiveItem 方法设置当前激活节点", "TreeViewSetActiveDisplayText": "当前激活节点", - "TreeViewsAttribute": "TreeItem 属性", - "TreeViewsDisableWholeTreeView": "是否禁用整个TreeView", - "TreeViewsWhetherToExpandWhenDisable": "禁用时候是否可以展开或折叠子节点" + "TreeViewsAttribute": "TreeViewItem 属性", + "TreeViewsDisableWholeTreeView": "是否禁用整个 TreeView", + "TreeViewsWhetherToExpandWhenDisable": "禁用时候是否可以展开或折叠子节点", + "OnMaxSelectedCountExceedTitle": "可选最大数量提示", + "OnMaxSelectedCountExceedContent": "最多只能选择 {0} 项", + "TreeViewMaxSelectedCountTitle": "最大选择数量", + "TreeViewMaxSelectedCountIntro": "通过设置 MaxSelectedCount 属性控制最大可选数量,通过 OnMaxSelectedCountExceed 回调处理逻辑", + "TreeViewMaxSelectedCountDesc": "选中节点超过 2 个时,弹出 Toast 提示栏" }, "BootstrapBlazor.Server.Components.Samples.Trees": { "TreeIntro": "

本组件已弃用,请使用新组件 TreeView 提供更多功能", diff --git a/src/BootstrapBlazor/Components/Checkbox/Checkbox.razor.cs b/src/BootstrapBlazor/Components/Checkbox/Checkbox.razor.cs index cac4d5ba5..1fe263a1f 100644 --- a/src/BootstrapBlazor/Components/Checkbox/Checkbox.razor.cs +++ b/src/BootstrapBlazor/Components/Checkbox/Checkbox.razor.cs @@ -164,8 +164,6 @@ public partial class Checkbox : ValidateBase ///

/// 触发 OnBeforeStateChanged 回调方法 由 JavaScript 调用 /// - /// - /// [JSInvokable] public async Task TriggerOnBeforeStateChanged() { diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor b/src/BootstrapBlazor/Components/TreeView/TreeView.razor index a27bcb4dc..ff9ed2c1c 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor @@ -63,6 +63,7 @@ else { } diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs index ebac71c23..fb4dc5451 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs @@ -184,6 +184,18 @@ public partial class TreeView : IModelEqualityComparer [Parameter] public bool ShowCheckbox { get; set; } + /// + /// 获得/设置 最多选中数量 + /// + [Parameter] + public int MaxSelectedCount { get; set; } + + /// + /// 获得/设置 超过最大选中数量时回调委托 + /// + [Parameter] + public Func? OnMaxSelectedCountExceed { get; set; } + /// /// 获得/设置 是否显示 Icon 图标 默认 false 不显示 /// @@ -271,6 +283,8 @@ public partial class TreeView : IModelEqualityComparer private string? _searchText; + private Func>? _onBeforeStateChangedCallback; + /// /// /// @@ -332,6 +346,9 @@ public partial class TreeView : IModelEqualityComparer ActiveItem ??= Items.FirstOrDefaultActiveItem(); ActiveItem?.SetParentExpand, TItem>(true); } + + _onBeforeStateChangedCallback = (ShowCheckbox && MaxSelectedCount > 0) ? new Func>(OnBeforeStateChanged) + : null; } async Task CheckExpand(IEnumerable> nodes) @@ -488,6 +505,22 @@ public partial class TreeView : IModelEqualityComparer } } + private async Task OnBeforeStateChanged(CheckboxState state) + { + var ret = true; + if (state == CheckboxState.Checked) + { + var items = GetCheckedItems().Where(i => i.HasChildren == false).ToList(); + ret = items.Count < MaxSelectedCount; + } + + if (!ret && OnMaxSelectedCountExceed != null) + { + await OnMaxSelectedCountExceed(); + } + return ret; + } + /// /// 节点 Checkbox 状态改变时触发此方法 /// @@ -496,8 +529,6 @@ public partial class TreeView : IModelEqualityComparer /// private async Task OnCheckStateChanged(TreeViewItem item, bool shouldRender = false) { - //item.CheckedState = ToggleCheckState(item.CheckedState); - if (AutoCheckChildren) { // 向下级联操作 diff --git a/test/UnitTest/Components/TreeViewTest.cs b/test/UnitTest/Components/TreeViewTest.cs index e8eb4fdc6..9c3c8319d 100644 --- a/test/UnitTest/Components/TreeViewTest.cs +++ b/test/UnitTest/Components/TreeViewTest.cs @@ -206,6 +206,65 @@ public class TreeViewTest : BootstrapBlazorTestBase cut.WaitForAssertion(() => cut.Contains("fa-solid fa-font-awesome")); } + [Fact] + public async Task OnMaxSelectedCountExceed_Ok() + { + bool max = false; + var items = TreeFoo.CascadingTree(new List() + { + new() { Text = "navigation one", Id = "1010", Icon = "fa-solid fa-font-awesome" }, + new() { Text = "Navigation two", Id = "1020", Icon = "fa-solid fa-font-awesome" }, + new() { Text = "Navigation three", Id = "1030", Icon = "fa-solid fa-font-awesome" } + }); + + var cut = Context.RenderComponent>(pb => + { + pb.Add(a => a.ShowCheckbox, true); + pb.Add(a => a.MaxSelectedCount, 2); + pb.Add(a => a.Items, items); + pb.Add(a => a.OnMaxSelectedCountExceed, () => + { + max = true; + return Task.CompletedTask; + }); + }); + var checkboxes = cut.FindComponents>(); + Assert.Equal(3, checkboxes.Count); + + await cut.InvokeAsync(async () => + { + await checkboxes[0].Instance.TriggerOnBeforeStateChanged(); + }); + Assert.Equal(CheckboxState.Checked, checkboxes[0].Instance.State); + + await cut.InvokeAsync(async () => + { + await checkboxes[1].Instance.TriggerOnBeforeStateChanged(); + }); + Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State); + + // 选中第三个由于限制无法选中 + await cut.InvokeAsync(async () => + { + await checkboxes[2].Instance.TriggerOnBeforeStateChanged(); + }); + Assert.Equal(CheckboxState.Checked, checkboxes[0].Instance.State); + Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State); + Assert.Equal(CheckboxState.UnChecked, checkboxes[2].Instance.State); + Assert.True(max); + + // 取消选择第一个 + max = false; + await cut.InvokeAsync(async () => + { + await checkboxes[0].Instance.TriggerOnBeforeStateChanged(); + }); + Assert.Equal(CheckboxState.UnChecked, checkboxes[0].Instance.State); + Assert.Equal(CheckboxState.Checked, checkboxes[1].Instance.State); + Assert.Equal(CheckboxState.UnChecked, checkboxes[2].Instance.State); + Assert.False(max); + } + [Fact] public void Template_Ok() {