perf(TreeView): improve cache for TreeViewNode (#4677)
Some checks are pending
Build project / build (push) Waiting to run

* 优化TreeView组件 虚拟滚动性能,更换List为HashSet,降低时间复杂度

* refactor: 重构代码

* refactor: 使用主构造函数精简代码

* refactor: 改用私有变量反射

* refactor: 增加 HashSetComparer 比较器

* refactor: 重构节点缓存类

* refactor: TreeView 组件更新比较器

* refactor: Table 组件更新比较器

* refactor: 改造缓存类

* test: 更新单元测试

* test: 补充 HashSetComparer 单元测试

---------

Co-authored-by: Argo Zhang <argo@live.ca>
This commit is contained in:
Diego2098 2024-11-17 19:38:06 +08:00 committed by GitHub
parent 313ccb7e99
commit c66f7dc283
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 261 additions and 203 deletions

View File

@ -761,7 +761,7 @@ public partial class Table<TItem> : ITable, IModelEqualityComparer<TItem> where
base.OnInitialized();
// 初始化节点缓存
TreeNodeCache ??= new(Equals);
TreeNodeCache ??= new(this);
OnInitLocalization();
// 设置 OnSort 回调方法

View File

@ -338,7 +338,7 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
base.OnInitialized();
// 初始化节点缓存
TreeNodeStateCache ??= new(Equals);
TreeNodeStateCache ??= new(this);
NotSetOnTreeExpandErrorMessage = Localizer[nameof(NotSetOnTreeExpandErrorMessage)];
}

View File

@ -10,26 +10,32 @@ namespace BootstrapBlazor.Components;
/// </summary>
/// <typeparam name="TNode"></typeparam>
/// <typeparam name="TItem"></typeparam>
/// <remarks>
/// 构造函数
/// </remarks>
public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer) where TNode : IExpandableNode<TItem>
public class ExpandableNodeCache<TNode, TItem> where TNode : IExpandableNode<TItem>
{
/// <summary>
/// 所有已展开行集合 作为缓存使用
/// </summary>
protected List<TItem> ExpandedNodeCache { get; } = new(50);
protected HashSet<TItem> ExpandedNodeCache { get; }
/// <summary>
/// 所有已收缩行集合 作为缓存使用
/// </summary>
protected List<TItem> CollapsedNodeCache { get; } = new(50);
protected HashSet<TItem> CollapsedNodeCache { get; }
/// <summary>
/// 对象比较器
/// </summary>
protected IEqualityComparer<TItem> EqualityComparer { get; } = new ModelComparer<TItem>(comparer);
protected IEqualityComparer<TItem> EqualityComparer { get; }
/// <remarks>
/// 构造函数
/// </remarks>
public ExpandableNodeCache(IModelEqualityComparer<TItem> comparer)
{
EqualityComparer = new ModelHashSetComparer<TItem>(comparer);
ExpandedNodeCache = new(50, EqualityComparer);
CollapsedNodeCache = new(50, EqualityComparer);
}
/// <summary>
/// 节点展开收缩状态切换方法
/// </summary>
@ -42,13 +48,10 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
if (node.IsExpand)
{
// 展开节点缓存增加此节点
if (!ExpandedNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
{
ExpandedNodeCache.Add(node.Value);
}
ExpandedNodeCache.Add(node.Value);
// 收缩节点缓存移除此节点
CollapsedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
CollapsedNodeCache.Remove(node.Value);
// 无子项时通过回调方法延时加载
if (!node.Items.Any())
@ -64,13 +67,10 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
else
{
// 展开节点缓存移除此节点
ExpandedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
ExpandedNodeCache.Remove(node.Value);
// 收缩节点缓存添加此节点
if (!CollapsedNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
{
CollapsedNodeCache.Add(node.Value);
}
CollapsedNodeCache.Add(node.Value);
}
}
@ -85,20 +85,18 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
if (node.IsExpand)
{
// 已收缩
if (CollapsedNodeCache.Contains(node.Value, EqualityComparer))
if (CollapsedNodeCache.Contains(node.Value))
{
node.IsExpand = false;
}
else if (!ExpandedNodeCache.Contains(node.Value, EqualityComparer))
{
// 状态为 展开
ExpandedNodeCache.Add(node.Value);
}
// 状态为 展开
ExpandedNodeCache.Add(node.Value);
}
else
{
var needRemove = true;
if (ExpandedNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
if (ExpandedNodeCache.Contains(node.Value))
{
// 原来是展开状态,
if (node.HasChildren)
@ -119,7 +117,7 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
}
if (needRemove)
{
ExpandedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
ExpandedNodeCache.Remove(node.Value);
}
}
}
@ -132,7 +130,7 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
/// <param name="ret">查询结果 查无资料时为 null</param>
/// <returns>是否存在 <paramref name="target"/></returns>
/// <remarks>采广度优先搜寻</remarks>
public bool TryFind(IEnumerable<TNode> items, TItem target, [MaybeNullWhen(false)] out TNode ret)
public bool TryFind(List<TNode> items, TItem target, [MaybeNullWhen(false)] out TNode ret)
{
ret = Find(items, target);
return ret != null;
@ -145,7 +143,7 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
/// <param name="target"></param>
/// <returns>查询结果 查无资料时为 null</returns>
/// <remarks>采广度优先搜寻</remarks>
private TNode? Find(IEnumerable<TNode> items, TItem target) => Find(items, target, out _);
private TNode? Find(List<TNode> items, TItem target) => Find(items, target, out _);
/// <summary>
/// 在全部树状结构 <paramref name="source"/> 中寻找指定 <paramref name="target"/>
@ -155,14 +153,14 @@ public class ExpandableNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer
/// <param name="degree">树状阶层起始为0</param>
/// <returns>查询结果 查无资料时为 null</returns>
/// <remarks>采广度优先搜寻</remarks>
public TNode? Find(IEnumerable<TNode> source, TItem target, out int degree)
public TNode? Find(List<TNode> source, TItem target, out int degree)
{
degree = -1;
var ret = source.FirstOrDefault(item => EqualityComparer.Equals(item.Value, target));
if (ret == null)
{
var children = source.SelectMany(e => e.Items.OfType<TNode>());
if (children.Any())
var children = source.SelectMany(e => e.Items.OfType<TNode>()).ToList();
if (children.Count != 0)
{
ret = Find(children, target, out degree);
}

View File

@ -1,52 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
namespace BootstrapBlazor.Components;
/// <summary>
/// 模型比较器
/// </summary>
/// <typeparam name="TItem"></typeparam>
public class ModelComparer<TItem> : IEqualityComparer<TItem>
{
private readonly Func<TItem, TItem, bool> _comparer;
/// <summary>
/// 构造函数
/// </summary>
public ModelComparer(Func<TItem, TItem, bool> comparer)
{
_comparer = comparer;
}
/// <summary>
/// Equals 方法
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool Equals(TItem? x, TItem? y)
{
bool ret;
if (x != null && y != null)
{
// 均不为空时走 comparer 方法判断
ret = _comparer(x, y);
}
else
{
// 有一个为空时 判断是否均为空
// 均为空时为 true 否则 false
ret = x == null && y == null;
}
return ret;
}
/// <summary>
/// GetHashCode 方法
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int GetHashCode([DisallowNull] TItem obj) => obj.GetHashCode();
}

View File

@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
namespace BootstrapBlazor.Components;
/// <summary>
/// 模型比较器
/// </summary>
/// <typeparam name="TItem"></typeparam>
public class ModelHashSetComparer<TItem>(IModelEqualityComparer<TItem> comparer) : IEqualityComparer<TItem>
{
/// <summary>
/// Equals 方法
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public bool Equals(TItem? x, TItem? y) => comparer.Equals(x, y);
/// <summary>
/// GetHashCode 方法
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int GetHashCode([DisallowNull] TItem obj)
{
var keyValue = Utility.GetKeyValue<TItem, object>(obj, comparer.CustomKeyAttribute);
return keyValue?.GetHashCode() ?? obj.GetHashCode();
}
}

View File

@ -10,65 +10,61 @@ namespace BootstrapBlazor.Components;
/// </summary>
/// <typeparam name="TNode"></typeparam>
/// <typeparam name="TItem"></typeparam>
public class TreeNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer) : ExpandableNodeCache<TNode, TItem>(comparer) where TNode : ICheckableNode<TItem>
public class TreeNodeCache<TNode, TItem> : ExpandableNodeCache<TNode, TItem> where TNode : ICheckableNode<TItem>
{
/// <summary>
/// 获得 所有选中节点集合 作为缓存使用
/// </summary>
protected List<TItem> CheckedNodeCache { get; } = new(50);
private readonly HashSet<TItem> _checkedNodeCache;
/// <summary>
/// 获得 所有未选中节点集合 作为缓存使用
/// </summary>
protected List<TItem> UncheckedNodeCache { get; } = new(50);
private readonly HashSet<TItem> _uncheckedNodeCache;
/// <summary>
/// 获得 所有未选中节点集合 作为缓存使用
/// </summary>
protected List<TItem> IndeterminateNodeCache { get; } = new(50);
private readonly HashSet<TItem> _indeterminateNodeCache;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="comparer"></param>
public TreeNodeCache(IModelEqualityComparer<TItem> comparer) : base(comparer)
{
_checkedNodeCache = new(50, EqualityComparer);
_uncheckedNodeCache = new(50, EqualityComparer);
_indeterminateNodeCache = new(50, EqualityComparer);
}
/// <summary>
/// 切换选中状态方法
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public virtual void ToggleCheck(TNode node)
public void ToggleCheck(TNode node)
{
if (node.CheckedState == CheckboxState.Checked)
{
// 未选中节点缓存移除此节点
UncheckedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
IndeterminateNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
_uncheckedNodeCache.Remove(node.Value);
_indeterminateNodeCache.Remove(node.Value);
// 选中节点缓存添加此节点
if (!CheckedNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
{
CheckedNodeCache.Add(node.Value);
}
_checkedNodeCache.Add(node.Value);
}
else if (node.CheckedState == CheckboxState.UnChecked)
{
// 选中节点缓存添加此节点
CheckedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
IndeterminateNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
_checkedNodeCache.Remove(node.Value);
_indeterminateNodeCache.Remove(node.Value);
// 未选中节点缓存移除此节点
if (!UncheckedNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
{
UncheckedNodeCache.Add(node.Value);
}
_uncheckedNodeCache.Add(node.Value);
}
else
{
// 不确定节点缓存添加此节点
CheckedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
UncheckedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
_checkedNodeCache.Remove(node.Value);
_uncheckedNodeCache.Remove(node.Value);
// 未选中节点缓存移除此节点
if (!IndeterminateNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
{
IndeterminateNodeCache.Add(node.Value);
}
_indeterminateNodeCache.Add(node.Value);
}
}
@ -79,46 +75,46 @@ public class TreeNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer) : Ex
/// <returns></returns>
private void IsChecked(TNode node)
{
// 当前节点状态为未确定状态
var nodes = node.Items.OfType<ICheckableNode<TItem>>();
if (CheckedNodeCache.Any(i => EqualityComparer.Equals(i, node.Value)))
var nodes = node.Items.OfType<ICheckableNode<TItem>>().ToList();
if (_checkedNodeCache.Contains(node.Value))
{
node.CheckedState = CheckboxState.Checked;
}
else if (UncheckedNodeCache.Contains(node.Value, EqualityComparer))
else if (_uncheckedNodeCache.Contains(node.Value))
{
node.CheckedState = CheckboxState.UnChecked;
}
else if (IndeterminateNodeCache.Contains(node.Value, EqualityComparer))
else if (_indeterminateNodeCache.Contains(node.Value))
{
node.CheckedState = CheckboxState.Indeterminate;
}
CheckChildren(nodes);
void CheckChildren(IEnumerable<ICheckableNode<TItem>> nodes)
CheckChildren(nodes, node);
}
private void CheckChildren(List<ICheckableNode<TItem>> nodes, TNode node)
{
if (nodes.Count != 0)
{
if (nodes.Any())
{
CheckedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
UncheckedNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
IndeterminateNodeCache.RemoveAll(i => EqualityComparer.Equals(i, node.Value));
_checkedNodeCache.Remove(node.Value);
_uncheckedNodeCache.Remove(node.Value);
_indeterminateNodeCache.Remove(node.Value);
// 查看子节点状态
if (nodes.All(i => i.CheckedState == CheckboxState.Checked))
{
node.CheckedState = CheckboxState.Checked;
CheckedNodeCache.Add(node.Value);
}
else if (nodes.All(i => i.CheckedState == CheckboxState.UnChecked))
{
node.CheckedState = CheckboxState.UnChecked;
UncheckedNodeCache.Add(node.Value);
}
else
{
node.CheckedState = CheckboxState.Indeterminate;
IndeterminateNodeCache.Add(node.Value);
}
// 查看子节点状态
if (nodes.All(i => i.CheckedState == CheckboxState.Checked))
{
node.CheckedState = CheckboxState.Checked;
_checkedNodeCache.Add(node.Value);
}
else if (nodes.All(i => i.CheckedState == CheckboxState.UnChecked))
{
node.CheckedState = CheckboxState.UnChecked;
_uncheckedNodeCache.Add(node.Value);
}
else
{
node.CheckedState = CheckboxState.Indeterminate;
_indeterminateNodeCache.Add(node.Value);
}
}
}
@ -127,27 +123,27 @@ public class TreeNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer) : Ex
/// 重置是否选中状态
/// </summary>
/// <param name="nodes"></param>
public void IsChecked(IEnumerable<TNode> nodes)
public void IsChecked(List<TNode> nodes)
{
if (nodes.Any())
if (nodes.Count != 0)
{
ResetCheckNodes(nodes);
}
}
void ResetCheckNodes(IEnumerable<TNode> items)
private void ResetCheckNodes(List<TNode> items)
{
// 恢复当前节点状态
foreach (var node in items)
{
// 恢复当前节点状态
foreach (var node in items)
// 恢复子节点
if (node.Items.Any())
{
// 恢复子节点
if (node.Items.Any())
{
IsChecked(node.Items.OfType<TNode>());
}
// 设置本节点
IsChecked(node);
IsChecked(node.Items.OfType<TNode>().ToList());
}
// 设置本节点
IsChecked(node);
}
}
@ -157,18 +153,18 @@ public class TreeNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer) : Ex
/// <param name="nodes">数据集合</param>
/// <param name="node">指定节点</param>
/// <returns></returns>
public TNode? FindParentNode(IEnumerable<TNode> nodes, TNode node)
public TNode? FindParentNode(List<TNode> nodes, TNode node)
{
TNode? ret = default;
foreach (var treeNode in nodes)
{
var subNodes = treeNode.Items.OfType<TNode>();
var subNodes = treeNode.Items.OfType<TNode>().ToList();
if (subNodes.Any(i => EqualityComparer.Equals(i.Value, node.Value)))
{
ret = treeNode;
break;
}
if (ret == null && subNodes.Any())
if (ret == null && subNodes.Count != 0)
{
ret = FindParentNode(subNodes, node);
}
@ -181,9 +177,10 @@ public class TreeNodeCache<TNode, TItem>(Func<TItem, TItem, bool> comparer) : Ex
/// </summary>
public void Reset()
{
UncheckedNodeCache.Clear();
CheckedNodeCache.Clear();
IndeterminateNodeCache.Clear();
_uncheckedNodeCache.Clear();
_checkedNodeCache.Clear();
_indeterminateNodeCache.Clear();
ExpandedNodeCache.Clear();
CollapsedNodeCache.Clear();
}

View File

@ -609,6 +609,9 @@ public class TreeViewTest : BootstrapBlazorTestBase
[Fact]
public void CascadeSetCheck_Ok()
{
var cut = Context.RenderComponent<TreeView<TreeFoo>>();
var comparer = cut.Instance;
var items = new List<TreeFoo>()
{
new() { Text = "Test1", Id = "01" },
@ -619,7 +622,7 @@ public class TreeViewTest : BootstrapBlazorTestBase
var node = TreeFoo.CascadingTree(items).First();
// 设置当前几点所有子项选中状态
var cache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(Comparer);
var cache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(comparer);
node.CheckedState = CheckboxState.Checked;
node.SetChildrenCheck(cache);
Assert.True(node.GetAllTreeSubItems().All(i => i.CheckedState == CheckboxState.Checked));
@ -628,6 +631,9 @@ public class TreeViewTest : BootstrapBlazorTestBase
[Fact]
public void SetParentCheck_Ok()
{
var cut = Context.RenderComponent<TreeView<TreeFoo>>();
var comparer = cut.Instance;
var items = new List<TreeFoo>()
{
new() { Text = "Test1", Id = "01" },
@ -638,7 +644,7 @@ public class TreeViewTest : BootstrapBlazorTestBase
Assert.Equal("Test3", node.Value.Text);
// 设置当前节点所有父项选中状态
var cache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(Comparer);
var cache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(comparer);
node.CheckedState = CheckboxState.Checked;
node.SetParentCheck(cache);

View File

@ -11,7 +11,7 @@ public class ModelEqualityComparerTest
public void Equals_Ok()
{
Assert.True(IModelEqualityComparerExtensions.Equals<Foo>(null!, null, null));
Assert.False(IModelEqualityComparerExtensions.Equals<Foo>(null!, null, new Foo()));
Assert.False(IModelEqualityComparerExtensions.Equals<Foo>(null!, new Foo(), null));
Assert.False(IModelEqualityComparerExtensions.Equals(null!, null, new Foo()));
Assert.False(IModelEqualityComparerExtensions.Equals(null!, new Foo(), null));
}
}

View File

@ -5,13 +5,16 @@
namespace UnitTest.Misc;
public class TreeNodeCacheTest
public class TreeNodeCacheTest : BootstrapBlazorTestBase
{
[Fact]
public void ToggleCheck_Ok()
{
var cut = Context.RenderComponent<TreeView<TreeFoo>>();
var comparer = cut.Instance;
var items = TreeFoo.GetTreeItems();
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(Comparer);
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(comparer);
nodeCache.IsChecked(items);
// 设置 1010 节点为选中状态
@ -204,8 +207,11 @@ public class TreeNodeCacheTest
[Fact]
public void FindParentNode_Ok()
{
var cut = Context.RenderComponent<TreeView<TreeFoo>>();
var comparer = cut.Instance;
var items = TreeFoo.GetTreeItems();
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(Comparer);
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(comparer);
var targetId = nodeCache.FindParentNode(items, new TreeViewItem<TreeFoo>(new TreeFoo() { Id = "1110" }))?.Value?.Id;
Assert.Equal("1080", targetId);
}
@ -213,8 +219,11 @@ public class TreeNodeCacheTest
[Fact]
public void SetChildrenCheck_Ok()
{
var cut = Context.RenderComponent<TreeView<TreeFoo>>();
var comparer = cut.Instance;
var items = TreeFoo.GetTreeItems();
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(Comparer);
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(comparer);
var count = GetUncheckItemCount(nodeCache);
Assert.Equal(0, count);
@ -231,7 +240,10 @@ public class TreeNodeCacheTest
[Fact]
public void Reset_Ok()
{
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(Comparer);
var cut = Context.RenderComponent<TreeView<TreeFoo>>();
var comparer = cut.Instance;
var nodeCache = new TreeNodeCache<TreeViewItem<TreeFoo>, TreeFoo>(comparer);
// 设置 1070 节点为选中状态
var node = new TreeViewItem<TreeFoo>(new TreeFoo()
@ -292,8 +304,8 @@ public class TreeNodeCacheTest
{
var count = 0;
var type = treeNodeCache.GetType();
var pi = type.GetProperty("CheckedNodeCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (pi != null && pi.GetValue(treeNodeCache) is List<TreeFoo> data)
var pi = type.GetField("_checkedNodeCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (pi != null && pi.GetValue(treeNodeCache) is HashSet<TreeFoo> data)
{
count = data.Count;
}
@ -304,8 +316,8 @@ public class TreeNodeCacheTest
{
var count = 0;
var type = treeNodeCache.GetType();
var pi = type.GetProperty("UncheckedNodeCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (pi != null && pi.GetValue(treeNodeCache) is List<TreeFoo> data)
var pi = type.GetField("_uncheckedNodeCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (pi != null && pi.GetValue(treeNodeCache) is HashSet<TreeFoo> data)
{
count = data.Count;
}
@ -316,8 +328,8 @@ public class TreeNodeCacheTest
{
var count = 0;
var type = treeNodeCache.GetType();
var pi = type.GetProperty("IndeterminateNodeCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (pi != null && pi.GetValue(treeNodeCache) is List<TreeFoo> data)
var pi = type.GetField("_indeterminateNodeCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (pi != null && pi.GetValue(treeNodeCache) is HashSet<TreeFoo> data)
{
count = data.Count;
}

View File

@ -0,0 +1,93 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using System.ComponentModel.DataAnnotations;
namespace UnitTest.Utils;
public class HasSetComparerTest : BootstrapBlazorTestBase
{
[Fact]
public void GetHashCode_NoKey_Ok()
{
var comparer = new ModelHashSetComparer<Dummy>(new MockModelEqualityComparer<Dummy>() { });
var obj = new Dummy() { Id = 1 };
// 未提供 KeyAttribute 标签内部等同 obj.GetHashCode()
Assert.Equal(obj.GetHashCode(), comparer.GetHashCode(obj));
// 空值相等
Assert.True(comparer.Equals(null, null));
// 未提供 ModelEqualityComparer 两个对象不相等
Assert.False(comparer.Equals(new Dummy() { Id = 1 }, new Dummy() { Id = 1 }));
// 空与非空比较
Assert.False(comparer.Equals(new Dummy(), null));
Assert.False(comparer.Equals(null, new Dummy()));
}
[Fact]
public void GetHashCode_Key_Ok()
{
var comparer = new ModelHashSetComparer<Dog>(new MockModelEqualityComparer<Dog>() { });
var obj = new Dog() { Id = 1 };
// 提供 KeyAttribute 标签内部不等同 obj.GetHashCode()
Assert.NotEqual(1, obj.GetHashCode());
Assert.Equal(1, comparer.GetHashCode(obj));
Assert.True(comparer.Equals(new Dog() { Id = 1 }, new Dog() { Id = 1 }));
// 空值相等
Assert.True(comparer.Equals(null, null));
// 空与非空比较
Assert.False(comparer.Equals(new Dog(), null));
Assert.False(comparer.Equals(null, new Dog()));
}
[Fact]
public void GetHashCode_Comparer_Ok()
{
var comparer = new ModelHashSetComparer<Dummy>(new MockModelEqualityComparer<Dummy>() { ModelEqualityComparer = (x, y) => x.Id == y.Id });
var obj = new Dummy() { Id = 1 };
// 未提供 KeyAttribute 标签内部等同 obj.GetHashCode()
Assert.Equal(obj.GetHashCode(), comparer.GetHashCode(obj));
// 空值相等
Assert.True(comparer.Equals(null, null));
// 提供 ModelEqualityComparer 两个对象相等
Assert.True(comparer.Equals(new Dummy() { Id = 1 }, new Dummy() { Id = 1 }));
// 空与非空比较
Assert.False(comparer.Equals(new Dummy(), null));
Assert.False(comparer.Equals(null, new Dummy()));
}
class Dummy
{
public int Id { get; set; }
}
class Dog
{
[Key]
public int Id { get; set; }
}
class MockModelEqualityComparer<TModel> : IModelEqualityComparer<TModel>
{
public Func<TModel, TModel, bool>? ModelEqualityComparer { get; set; }
public Type CustomKeyAttribute { get; set; } = typeof(KeyAttribute);
public bool Equals(TModel? x, TModel? y) => this.Equals<TModel>(x, y);
}
}

View File

@ -1,28 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
namespace UnitTest.Utils;
public class TItemComparerTest
{
[Fact]
public void GetHashCode_Ok()
{
var comparer = new ModelComparer<Dummy>((x, y) => x.Id == y.Id);
var obj = new Dummy();
Assert.Equal(obj.GetHashCode(), comparer!.GetHashCode(obj));
Assert.True(comparer.Equals(null, null));
Assert.True(comparer.Equals(new Dummy() { Id = 1 }, new Dummy() { Id = 1 }));
Assert.False(comparer.Equals(new Dummy(), null));
Assert.False(comparer.Equals(null, new Dummy()));
}
class Dummy
{
public int Id { get; set; }
}
}