Skip to content

Commit 368e484

Browse files
authored
feat(TreeView): improve AutoCheckParent logic (#8188)
* refactor: 脚本增加保护逻辑 * doc: 更新文档注释 * feat: 更新切换 AutoCheck 参数逻辑 * test: 更新单元测试 * chore: bump version 10.7.4-beta01 * chore: bump version 10.8.0
1 parent 16aca10 commit 368e484

7 files changed

Lines changed: 411 additions & 27 deletions

File tree

‎src/BootstrapBlazor.Server/Locales/en-US.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5745,8 +5745,8 @@
57455745
"TreeViewAccordionModelTitle": "Accordion mode",
57465746
"TreeViewCheckboxAddButtonText": "Add",
57475747
"TreeViewCheckboxButtonText": "Refresh",
5748-
"TreeViewCheckboxCheckBoxDisplayText1": "Automatically select child nodes",
5749-
"TreeViewCheckboxCheckBoxDisplayText2": "Automatically select parent node",
5748+
"TreeViewCheckboxCheckBoxDisplayText1": "Automatically update child nodes state",
5749+
"TreeViewCheckboxCheckBoxDisplayText2": "Automatically update parent node state (checked/indeterminate)",
57505750
"TreeViewCheckboxIntro": "Suitable for use when you need to select a level",
57515751
"TreeViewCheckboxTips1": "By setting the <code>OnTreeItemChecked</code> property to monitor the event when the tree control node is checked, when the check box in front of the tree control node is selected, the following log displays the data of the selected node",
57525752
"TreeViewCheckboxTips2": "The <code>Tree</code> component will keep the status of each node when loading data. The <b>Refresh</b> button will update the data source <code>Items</code> and reinitialize the component. When you click the <code>Append Node</code> button, only the data inside the data source will be changed without reassigning values, so the status of each node will remain unchanged.",

‎src/BootstrapBlazor.Server/Locales/zh-CN.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5745,8 +5745,8 @@
57455745
"TreeViewAccordionModelTitle": "手风琴模式",
57465746
"TreeViewCheckboxAddButtonText": "追加节点",
57475747
"TreeViewCheckboxButtonText": "刷新",
5748-
"TreeViewCheckboxCheckBoxDisplayText1": "自动选中子节点",
5749-
"TreeViewCheckboxCheckBoxDisplayText2": "自动选中父节点",
5748+
"TreeViewCheckboxCheckBoxDisplayText1": "自动更新子节点状态",
5749+
"TreeViewCheckboxCheckBoxDisplayText2": "自动更新父节点状态(全选/半选)",
57505750
"TreeViewCheckboxIntro": "适用于需要选择层级时使用",
57515751
"TreeViewCheckboxTips1": "通过设置 <code>OnTreeItemChecked</code> 属性监控树形控件节点被勾选时的事件,选中树形控件节点前复选框时下面日志显示选中节点的数据",
57525752
"TreeViewCheckboxTips2": "<code>Tree</code> 组件数据加载内部将会保持各个节点状态,<b>刷新</b> 按钮将更新数据源 <code>Items</code>,组件重新初始化,点击 <code>追加节点</code> 按钮时,仅更改数据源内部数据未重新赋值,所以各个节点状态保持不变",

‎src/BootstrapBlazor/BootstrapBlazor.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>10.7.3</Version>
4+
<Version>10.8.0</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

‎src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs‎

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -311,15 +311,15 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
311311
public Func<TItem, string?, Task<bool>>? OnUpdateCallbackAsync { get; set; }
312312

313313
/// <summary>
314-
/// <para lang="zh">获得/设置 节点状态变化时是否自动更新子节点,默认为 false</para>
315-
/// <para lang="en">Gets or sets whether to automatically update child nodes when the node state changes. Default is false</para>
314+
/// <para lang="zh">获得/设置 节点状态变化时是否自动更新子节点状态 子节点状态与本节点保持一致,默认为 false</para>
315+
/// <para lang="en">Gets or sets whether to automatically update child nodes state when the node state changes. Child nodes keep the same state as this node. Default is false</para>
316316
/// </summary>
317317
[Parameter]
318318
public bool AutoCheckChildren { get; set; }
319319

320320
/// <summary>
321-
/// <para lang="zh">获得/设置 节点状态变化时是否自动更新父节点,默认为 false</para>
322-
/// <para lang="en">Gets or sets whether to automatically update parent nodes when the node state changes. Default is false</para>
321+
/// <para lang="zh">获得/设置 节点状态变化时是否自动更新父节点状态 父节点状态由子节点状态派生 全部选中时为选中 全部未选中时为未选中 否则为半选,默认为 false。由 true 更改为 false 时 半选节点将重置为未选中</para>
322+
/// <para lang="en">Gets or sets whether to automatically update parent nodes state when the node state changes. Parent state is derived from children: checked when all children are checked, unchecked when none are checked, otherwise indeterminate. Default is false. When changed from true to false, indeterminate nodes are reset to unchecked</para>
323323
/// </summary>
324324
[Parameter]
325325
public bool AutoCheckParent { get; set; }
@@ -358,6 +358,10 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
358358
private bool _shouldRender = true;
359359
private bool _init;
360360
private bool _scrollIntoView = false;
361+
private List<TreeViewItem<TItem>>? _lastItems;
362+
private bool _lastShowCheckbox;
363+
private bool _lastAutoCheckParent;
364+
private bool _lastAutoCheckChildren;
361365

362366
/// <summary>
363367
/// <inheritdoc/>
@@ -403,9 +407,16 @@ protected override async Task OnParametersSetAsync()
403407
_rows = null;
404408
}
405409

406-
if (ShowCheckbox && (AutoCheckParent || AutoCheckChildren))
410+
// 仅在数据源引用或级联勾选相关参数变化时重算勾选状态 普通重渲染不重算 防止覆盖用户显式设置的节点状态
411+
if (ShowCheckbox && (AutoCheckParent || AutoCheckChildren) && IsCheckedParameterChanged())
407412
{
408-
_treeNodeStateCache.IsChecked(Items);
413+
_treeNodeStateCache.IsChecked(Items, AutoCheckParent);
414+
}
415+
416+
// 关闭 AutoCheckParent 后半选状态失去派生来源 将半选节点重置为未选中
417+
if (ShowCheckbox && _lastAutoCheckParent && !AutoCheckParent)
418+
{
419+
ResetIndeterminateNodes();
409420
}
410421

411422
if (_activeItem != null)
@@ -421,6 +432,28 @@ protected override async Task OnParametersSetAsync()
421432
_scrollIntoView = true;
422433
}
423434
}
435+
436+
_lastItems = Items;
437+
_lastShowCheckbox = ShowCheckbox;
438+
_lastAutoCheckParent = AutoCheckParent;
439+
_lastAutoCheckChildren = AutoCheckChildren;
440+
}
441+
442+
private bool IsCheckedParameterChanged() => !ReferenceEquals(_lastItems, Items)
443+
|| _lastShowCheckbox != ShowCheckbox
444+
|| _lastAutoCheckParent != AutoCheckParent
445+
|| _lastAutoCheckChildren != AutoCheckChildren;
446+
447+
private void ResetIndeterminateNodes()
448+
{
449+
foreach (var node in Items.GetAllItems())
450+
{
451+
if (node.CheckedState == CheckboxState.Indeterminate)
452+
{
453+
node.CheckedState = CheckboxState.UnChecked;
454+
_treeNodeStateCache.ToggleCheck(node);
455+
}
456+
}
424457
}
425458

426459
/// <summary>
@@ -527,13 +560,24 @@ public Task<List<CheckboxState>> GetParentsState(List<int> items)
527560
var rows = Rows;
528561
var result = items.Select(i =>
529562
{
530-
var item = rows[i];
563+
// 客户端回调期间数据源可能已更新 索引越界或无子节点时不重算 返回值数量与请求保持一一对应
564+
var item = i >= 0 && i < rows.Count ? rows[i] : null;
565+
if (item == null)
566+
{
567+
return CheckboxState.UnChecked;
568+
}
569+
if (item.Items.Count == 0)
570+
{
571+
return item.CheckedState;
572+
}
573+
531574
var checkedState = item.Items[0].CheckedState;
532575
if (item.Items.Any(s => s.CheckedState != checkedState))
533576
{
534577
checkedState = CheckboxState.Indeterminate;
535578
}
536579
item.CheckedState = checkedState;
580+
_treeNodeStateCache.ToggleCheck(item);
537581
return checkedState;
538582
}).ToList();
539583
return Task.FromResult(result);
@@ -763,6 +807,9 @@ private async Task OnToggleNodeAsync(TreeViewItem<TItem> node)
763807
// 手风琴效果逻辑
764808
node.IsExpand = !node.IsExpand;
765809

810+
// 展开节点无子项时将触发懒加载 新增子节点后需要同步级联勾选状态
811+
var lazyLoad = node.IsExpand && node.HasChildren && node.Items.Count == 0;
812+
766813
if (IsAccordion && !IsVirtualize)
767814
{
768815
await _treeNodeStateCache.ToggleNodeAsync(node, GetChildrenRowAsync);
@@ -788,18 +835,17 @@ private async Task OnToggleNodeAsync(TreeViewItem<TItem> node)
788835
_rows = null;
789836
}
790837

791-
if (ShowCheckbox)
838+
// 仅在懒加载新增子节点后同步级联勾选状态 普通展开/折叠不重算 防止覆盖用户显式设置的节点状态
839+
if (ShowCheckbox && lazyLoad && node.Items.Count > 0)
792840
{
793841
if (AutoCheckChildren)
794842
{
843+
// 新增子节点继承本节点勾选状态
795844
node.SetChildrenCheck(_treeNodeStateCache);
796845
}
797846
if (AutoCheckParent)
798847
{
799-
node.SetParentCheck(_treeNodeStateCache);
800-
}
801-
if (!AutoCheckChildren && AutoCheckParent && node.Items.Count > 0)
802-
{
848+
// 由新增子节点状态派生本节点及其祖先节点状态
803849
node.Items[0].SetParentCheck(_treeNodeStateCache);
804850
}
805851
}

‎src/BootstrapBlazor/Components/TreeView/TreeView.razor.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,12 @@ export function toggleLoading(id, index, state) {
173173

174174
export function setChildrenState(id, index, state) {
175175
const el = document.getElementById(id);
176+
if (el === null) {
177+
return;
178+
}
176179
const node = el.querySelector(`[data-bb-tree-view-index="${index}"]`);
177-
const level = parseInt(node.style.getPropertyValue('--bb-tree-view-level'));
178180
if (node) {
181+
const level = parseInt(node.style.getPropertyValue('--bb-tree-view-level'));
179182
let next = node.nextElementSibling;
180183
while (next) {
181184
const currentLevel = parseInt(next.style.getPropertyValue('--bb-tree-view-level'));
@@ -195,9 +198,12 @@ export function setChildrenState(id, index, state) {
195198

196199
export async function setParentState(id, invoke, method, index) {
197200
const el = document.getElementById(id);
201+
if (el === null) {
202+
return;
203+
}
198204
const node = el.querySelector(`[data-bb-tree-view-index="${index}"]`);
199-
let level = parseInt(node.style.getPropertyValue('--bb-tree-view-level'));
200205
if (node) {
206+
let level = parseInt(node.style.getPropertyValue('--bb-tree-view-level'));
201207
const parents = [];
202208
let prev = node.previousElementSibling;
203209
while (prev) {

‎src/BootstrapBlazor/Misc/TreeNodeCache.cs‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ public void ToggleCheck(TNode node)
7878
/// <para lang="en">Check whether current node is checked</para>
7979
/// </summary>
8080
/// <param name="node"></param>
81-
private void IsChecked(TNode node)
81+
/// <param name="autoCheckParent"><para lang="zh">是否由子节点状态派生本节点状态</para><para lang="en">Whether to derive the node state from its children</para></param>
82+
private void IsChecked(TNode node, bool autoCheckParent)
8283
{
83-
var nodes = node.Items.OfType<ICheckableNode<TItem>>().ToList();
8484
if (_checkedNodeCache.Contains(node.Value))
8585
{
8686
node.CheckedState = CheckboxState.Checked;
@@ -94,7 +94,11 @@ private void IsChecked(TNode node)
9494
node.CheckedState = CheckboxState.Indeterminate;
9595
}
9696

97-
CheckChildren(nodes, node);
97+
if (autoCheckParent)
98+
{
99+
var nodes = node.Items.OfType<ICheckableNode<TItem>>().ToList();
100+
CheckChildren(nodes, node);
101+
}
98102
}
99103

100104
private void CheckChildren(List<ICheckableNode<TItem>> nodes, TNode node)
@@ -129,27 +133,28 @@ private void CheckChildren(List<ICheckableNode<TItem>> nodes, TNode node)
129133
/// <para lang="en">Reset checked state</para>
130134
/// </summary>
131135
/// <param name="nodes"></param>
132-
public void IsChecked(List<TNode> nodes)
136+
/// <param name="autoCheckParent"><para lang="zh">是否由子节点状态派生父节点状态 默认 true</para><para lang="en">Whether to derive parent state from children. Default is true</para></param>
137+
public void IsChecked(List<TNode> nodes, bool autoCheckParent = true)
133138
{
134139
if (nodes.Count != 0)
135140
{
136-
ResetCheckNodes(nodes);
141+
ResetCheckNodes(nodes, autoCheckParent);
137142
}
138143
}
139144

140-
private void ResetCheckNodes(List<TNode> items)
145+
private void ResetCheckNodes(List<TNode> items, bool autoCheckParent)
141146
{
142147
// 恢复当前节点状态
143148
foreach (var node in items)
144149
{
145150
// 恢复子节点
146151
if (node.Items.Any())
147152
{
148-
IsChecked(node.Items.OfType<TNode>().ToList());
153+
IsChecked(node.Items.OfType<TNode>().ToList(), autoCheckParent);
149154
}
150155

151156
// 设置本节点
152-
IsChecked(node);
157+
IsChecked(node, autoCheckParent);
153158
}
154159
}
155160

0 commit comments

Comments
 (0)