From 7f1f8728319c86711f21a5cf66c1f4d79e0d9441 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 5 Aug 2026 19:47:20 +0800 Subject: [PATCH 1/4] feat(TreeView): redesign reset search logic --- .../Components/TreeView/TreeView.razor.cs | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs index 01cca014889..265cc73d77c 100644 --- a/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs +++ b/src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs @@ -124,8 +124,8 @@ public partial class TreeView : IModelEqualityComparer public string? ClearSearchIcon { get; set; } /// - /// 获得/设置 搜索回调方法,默认为 null - /// Gets or sets the search callback method. Default is null + /// 获得/设置 搜索回调方法,默认为 null。重置搜索时以 null 作为搜索条件再次调用 + /// Gets or sets the search callback method. Default is null. It is invoked again with a null search term when the search is reset /// /// Enabled by setting to true. [Parameter] @@ -738,42 +738,25 @@ private async Task OnEnterAsync(string? searchText) private async Task OnClickSearch() { - if (OnSearchAsync != null) + _searchItems = OnSearchAsync == null ? null : await OnSearchAsync(_searchText); + _rows = null; + if (_activeItem != null) { - _rows = null; - _searchItems = await OnSearchAsync(_searchText); - if (_searchItems == null || _activeItem == null) + var item = Rows.FirstOrDefault(i => Equals(i.Value, _activeItem.Value)); + if (item != null) { - StateHasChanged(); + SetActiveItem(item); return; } - - var val = _searchItems.GetAllItems().FirstOrDefault(i => Equals(i.Value, _activeItem.Value)); - if (val == null) - { - StateHasChanged(); - return; - } - - _activeItem = val; - SetActiveItem(val.Value); } + + StateHasChanged(); } - private Task OnClickResetSearch() + private async Task OnClickResetSearch() { _searchText = null; - _searchItems = null; - _rows = null; - if (_activeItem != null && Items != null) - { - SetActiveItem(_activeItem.Value); - } - else - { - StateHasChanged(); - } - return Task.CompletedTask; + await OnClickSearch(); } /// From 5f292e39de48c88e0de8bba8c4a5eeaf32fb3277 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 5 Aug 2026 19:47:31 +0800 Subject: [PATCH 2/4] doc: update sample code --- .../Components/Samples/TreeViews.razor.cs | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs index 32855a103d1..d3c24590968 100644 --- a/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs +++ b/src/BootstrapBlazor.Server/Components/Samples/TreeViews.razor.cs @@ -61,11 +61,11 @@ public sealed partial class TreeViews private List>? AsyncItems { get; set; } - private List>? MaxItems { get; set; } = TreeFoo.GetTreeItems(); + private List> MaxItems { get; set; } = TreeFoo.GetTreeItems(); - private List>? SearchItems1 { get; set; } = TreeFoo.GetTreeItems(); + private List> SearchItems1 { get; set; } = TreeFoo.GetTreeItems(); - private List>? SearchItems2 { get; set; } = TreeFoo.GetTreeItems(); + private List> SearchItems2 { get; set; } = TreeFoo.GetTreeItems(); private List> VirtualizeItems { get; } = TreeFoo.GetVirtualizeTreeItems(); @@ -269,21 +269,57 @@ private Task SelectedItemOnChanged(SelectedItem selectedItem) return ret; } - private static async Task>?> OnSearchAsync(string searchText) + private async Task>?> OnSearchAsync(string? searchText) { await Task.Delay(20); List>? items = null; if (!string.IsNullOrEmpty(searchText)) { - items = - [ - new TreeViewItem(new TreeFoo() { Text = searchText }) { Text = searchText }, - ]; + items = GetSearchItems(searchText, SearchItems1); } return items; } + private static List> GetSearchItems(string searchText, IEnumerable> source) + { + var items = new List>(); + foreach (var item in source) + { + // 递归过滤子节点,保留层次结构 + var children = GetSearchItems(searchText, item.Items); + + // 节点自身匹配或其后代存在匹配时保留该节点 + var isMatch = !string.IsNullOrEmpty(item.Text) && item.Text.Contains(searchText, StringComparison.OrdinalIgnoreCase); + if (isMatch || children.Count > 0) + { + // 克隆节点,避免污染原始数据源 + var node = CloneItem(item); + node.Items = children; + foreach (var child in children) + { + child.Parent = node; + } + items.Add(node); + } + } + return items; + } + + private static TreeViewItem CloneItem(TreeViewItem item) => new(item.Value) + { + Text = item.Text, + Icon = item.Icon, + ExpandIcon = item.ExpandIcon, + CssClass = item.CssClass, + IsActive = item.IsActive, + IsDisabled = item.IsDisabled, + IsExpand = true, + HasChildren = item.HasChildren, + CheckedState = item.CheckedState, + Template = item.Template + }; + private static async Task>> OnExpandVirtualNodeAsync(TreeViewItem node) { await Task.Delay(500); From 06a2297062532616ff8ae0167dd627ae7c8c04ae Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 5 Aug 2026 19:47:46 +0800 Subject: [PATCH 3/4] =?UTF-8?q?test:=20=E6=9B=B4=E6=96=B0=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/SelectTreeTest.cs | 4 +++- test/UnitTest/Components/TreeViewTest.cs | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/test/UnitTest/Components/SelectTreeTest.cs b/test/UnitTest/Components/SelectTreeTest.cs index 8f15dc54864..eaefcc5d7b9 100644 --- a/test/UnitTest/Components/SelectTreeTest.cs +++ b/test/UnitTest/Components/SelectTreeTest.cs @@ -235,7 +235,9 @@ public async Task OnSearchAsync_Ok() builder.Add(a => a.OnSearchAsync, new Func>?>>(v => { key = v; - return Task.FromResult>?>([new TreeViewItem(new TreeFoo()) { Text = v }]); + return Task.FromResult>?>(string.IsNullOrEmpty(v) + ? null + : [new TreeViewItem(new TreeFoo()) { Text = v }]); })); builder.Add(a => a.Items, items); }); diff --git a/test/UnitTest/Components/TreeViewTest.cs b/test/UnitTest/Components/TreeViewTest.cs index 27d53e00013..69cf25bc724 100644 --- a/test/UnitTest/Components/TreeViewTest.cs +++ b/test/UnitTest/Components/TreeViewTest.cs @@ -1513,7 +1513,9 @@ public async Task Enter_Ok() pb.Add(a => a.OnSearchAsync, new Func>?>>(v => { key = v; - return Task.FromResult>?>([new TreeViewItem(new TreeFoo()) { Text = v }]); + return Task.FromResult>?>(string.IsNullOrEmpty(v) + ? null + : [new TreeViewItem(new TreeFoo()) { Text = v }]); })); pb.Add(a => a.Items, items); }); @@ -1651,11 +1653,18 @@ public async Task Search_NullBranches_Ok() { var items = TreeFoo.GetTreeItems(); List>? searchResult = [items[0]]; + var searchCount = 0; + string? searchText = string.Empty; var cut = Context.Render>(pb => { pb.Add(a => a.ShowSearch, true); pb.Add(a => a.Items, items); - pb.Add(a => a.OnSearchAsync, _ => Task.FromResult(searchResult)); + pb.Add(a => a.OnSearchAsync, text => + { + searchCount++; + searchText = text; + return Task.FromResult>?>(searchResult); + }); }); var input = cut.FindComponent>(); @@ -1669,6 +1678,8 @@ public async Task Search_NullBranches_Ok() await cut.InvokeAsync(() => input.Instance.OnEscAsync!(string.Empty)); Assert.Equal(9, cut.FindAll(".tree-content").Count); + Assert.Equal(3, searchCount); + Assert.Null(searchText); cut.Render(pb => { From 267808933e7d33cf3e8cfd588c857aff3c508bc2 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Wed, 5 Aug 2026 19:50:21 +0800 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=E9=87=8D=E6=9E=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/UnitTest/Components/TreeViewTest.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/UnitTest/Components/TreeViewTest.cs b/test/UnitTest/Components/TreeViewTest.cs index 69cf25bc724..52b634a04b0 100644 --- a/test/UnitTest/Components/TreeViewTest.cs +++ b/test/UnitTest/Components/TreeViewTest.cs @@ -1586,25 +1586,26 @@ public async Task Search_CheckedItem_Ok() var checkbox = cut.FindComponent>>(); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Single(checkedItems!); + Assert.NotNull(checkedItems); + Assert.Single(checkedItems); Assert.Equal(items[0].Value.Id, checkedItems[0].Value.Id); Assert.Equal(CheckboxState.Checked, items[0].CheckedState); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Empty(checkedItems!); + Assert.Empty(checkedItems); Assert.Equal(CheckboxState.UnChecked, items[0].CheckedState); searchItems = [new(new TreeFoo { Id = "NotFound", Text = "NotFound" }) { Text = "NotFound" }]; await cut.InvokeAsync(() => input.Instance.OnEnterAsync!(string.Empty)); checkbox = cut.FindComponent>>(); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Empty(checkedItems!); + Assert.Empty(checkedItems); searchItems = [items[0]]; await cut.InvokeAsync(() => input.Instance.OnEnterAsync!(string.Empty)); checkbox = cut.FindComponent>>(); await cut.InvokeAsync(checkbox.Instance.OnToggleClick); - Assert.Single(checkedItems!); + Assert.Single(checkedItems); Assert.Same(items[0], checkedItems[0]); }