648 lines
20 KiB
C#
648 lines
20 KiB
C#
/************************************************************************
|
||
*@file FrameworkLocal\UIClient\CenterViews\DataLogs\Core\TreeNode.cs
|
||
* @author Su Liang
|
||
* @Date 2022-08-01
|
||
*
|
||
* @copyright © Sicentury Inc.
|
||
*
|
||
* @brief Reconstructed to support rich functions.
|
||
*
|
||
* @details
|
||
* All functions to operate the TreeView are integrated into one class, which
|
||
* also makes it meet the MVVM coding pattern.
|
||
* *****************************************************************************/
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.Specialized;
|
||
using System.Diagnostics;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
using Sicentury.Core.EventArgs;
|
||
|
||
namespace Sicentury.Core.Tree
|
||
{
|
||
public class TreeNode : BindableBase
|
||
{
|
||
#region Variables
|
||
|
||
/// <summary>
|
||
/// 当末端的节点选择属性发生变化时触发此事件。
|
||
/// </summary>
|
||
public event EventHandler<TreeNodeSelectionChangedEventArgs> TerminalNodeSelectionChanged;
|
||
|
||
/// <summary>
|
||
/// 当子节点中有元素发生变化时触发事件。
|
||
/// </summary>
|
||
public event EventHandler<NotifyCollectionChangedEventArgs> ChildNodesChanged;
|
||
|
||
private bool? _isSelected = false;
|
||
private double _averageValue;
|
||
private double _minValue;
|
||
private double _maxValue;
|
||
private bool _isExpanded;
|
||
private bool _isMatch = true;
|
||
private string _filterKeyword;
|
||
private Visibility _visibility = Visibility.Visible;
|
||
private int _totalTerminalCount;
|
||
private int _selectedTerminalCount;
|
||
|
||
/// <summary>
|
||
/// 挂起更新操作时的变量锁,防止多线程问题。
|
||
/// </summary>
|
||
private readonly object _suspendUpdateLocker;
|
||
|
||
/// <summary>
|
||
/// 时否挂起更新操作。
|
||
/// </summary>
|
||
private bool _isUpdateSuspended;
|
||
|
||
|
||
#endregion
|
||
|
||
#region Constructors
|
||
|
||
public TreeNode(string name)
|
||
{
|
||
_suspendUpdateLocker = new object();
|
||
_isUpdateSuspended = false;
|
||
|
||
Name = name;
|
||
//RawData = new List<ParameterNodePoint>();
|
||
MaxTerminalSelectionAllowed = -1;
|
||
ChildNodes = new TreeNodeCollection(this);
|
||
ChildNodes.TerminalNodeSelectionChanged += OnTerminalNodeSelectionChanged;
|
||
ChildNodes.CollectionChanged += ChildNodesOnCollectionChanged;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Properties
|
||
|
||
/// <summary>
|
||
/// 当前树中被选中的终端节点的数量。
|
||
/// </summary>
|
||
public int SelectedTerminalCount
|
||
{
|
||
get => _selectedTerminalCount;
|
||
private set => Set(ref _selectedTerminalCount, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前树中终端节点的总数量。
|
||
/// </summary>
|
||
public int TotalTerminalCount
|
||
{
|
||
get => _totalTerminalCount;
|
||
private set => Set(ref _totalTerminalCount, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回允许的最大终端节点选中数量。
|
||
/// <para>-1表示可选中所有终端节点。</para>
|
||
/// </summary>
|
||
public int MaxTerminalSelectionAllowed { get; set; }
|
||
|
||
public bool? IsSelected
|
||
{
|
||
get => _isSelected;
|
||
set
|
||
{
|
||
if (value == _isSelected)
|
||
{
|
||
OnPropertyChanged();
|
||
return;
|
||
}
|
||
|
||
var snapshot = _isSelected;
|
||
Set(ref _isSelected, value);
|
||
|
||
// 仅当该节点未最末端节点时触发事件。
|
||
var cancelEventArgs = new TreeNodeSelectionChangedEventArgs(this, snapshot, value);
|
||
RaiseTerminalNodeSelectionChangedEvent(cancelEventArgs);
|
||
if (cancelEventArgs.Cancel)
|
||
{
|
||
_isSelected = snapshot;
|
||
OnPropertyChanged();
|
||
}
|
||
|
||
// 处理子节点的选择状态
|
||
UpdateChildrenSelection();
|
||
|
||
// 处理我的状态。
|
||
UpdateSelectionState(this);
|
||
|
||
// 处理父节点的选择状态
|
||
UpdateParentSelection();
|
||
}
|
||
}
|
||
|
||
public string Name { get; }
|
||
|
||
/// <summary>
|
||
/// 返回包含完整路劲的节点名称。
|
||
/// </summary>
|
||
public string FullName => ToString();
|
||
|
||
public double AverageValue
|
||
{
|
||
get => _averageValue;
|
||
private set => Set(ref _averageValue, value);
|
||
}
|
||
|
||
public double MinValue
|
||
{
|
||
get => _minValue;
|
||
private set => Set(ref _minValue, value);
|
||
}
|
||
|
||
public double MaxValue
|
||
{
|
||
get => _maxValue;
|
||
private set => Set(ref _maxValue, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否展开节点。
|
||
/// </summary>
|
||
public bool IsExpanded
|
||
{
|
||
get => _isExpanded;
|
||
set => Set(ref _isExpanded, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否包含Filter指定的关键字。
|
||
/// </summary>
|
||
public bool IsMatch
|
||
{
|
||
get => _isMatch;
|
||
set => Set(ref _isMatch, value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置或返回节点是否可见。
|
||
/// </summary>
|
||
public Visibility Visibility
|
||
{
|
||
get => _visibility;
|
||
set
|
||
{
|
||
Set(ref _visibility, value);
|
||
|
||
if (IsTerminal)
|
||
UpdateParentVisibility(ParentNode);
|
||
}
|
||
}
|
||
|
||
public TreeNode ParentNode { get; set; }
|
||
|
||
//public List<ParameterNodePoint> RawData { get; }
|
||
|
||
public Visibility IsVisibilityParentNode { get; set; }
|
||
|
||
public TreeNodeCollection ChildNodes { get; set; }
|
||
|
||
/// <summary>
|
||
/// 筛选器的关键字。
|
||
/// </summary>
|
||
public string FilterKeyWord
|
||
{
|
||
get => _filterKeyword;
|
||
set => Set(ref _filterKeyword, value);
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回我是否为最末端节点。
|
||
/// </summary>
|
||
public bool IsTerminal => (ChildNodes == null || ChildNodes.Count == 0);
|
||
|
||
/// <summary>
|
||
/// 返回我是否属于一级节点。
|
||
/// </summary>
|
||
public bool IsTopLevel => ParentNode == null;
|
||
|
||
/// <summary>
|
||
/// 返回我是否属于二级节点。
|
||
/// </summary>
|
||
public bool IsSecondLevel => ParentNode != null && ParentNode.ParentNode == null;
|
||
|
||
/// <summary>
|
||
/// 返回是否有终端节点被选中。
|
||
/// </summary>
|
||
public bool HasTerminalSelected => Flatten(true).FirstOrDefault(x => x.IsSelected == true) != null;
|
||
|
||
#endregion
|
||
|
||
#region Methods
|
||
|
||
/// <summary>
|
||
/// 如果是末端节点,触发事件。
|
||
/// </summary>
|
||
private void RaiseTerminalNodeSelectionChangedEvent(TreeNodeSelectionChangedEventArgs e)
|
||
{
|
||
// 仅当该节点未最末端节点时触发事件。
|
||
if (IsTerminal)
|
||
TerminalNodeSelectionChanged?.Invoke(this, e);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理子节点和父节点的选择状态。
|
||
/// </summary>
|
||
private void UpdateChildrenSelection()
|
||
{
|
||
if (_isSelected.HasValue == false)
|
||
return;
|
||
|
||
// 刷新子节点的选择状态
|
||
if (IsTerminal)
|
||
return;
|
||
|
||
//! 遍历子节点时,本节点的_isSelected属性可能被改写。
|
||
// 例如:某个终端节点的Selected改变时,会反向设置父节点的Selected属性;则接下来所有子节点赋值Selected属性时会使用新值。
|
||
var fixedIsSelected = _isSelected;
|
||
Flatten(true).ToList()
|
||
.ForEach(x =>
|
||
{
|
||
if(x.IsMatch)
|
||
x.SetSelectionProperty(fixedIsSelected);
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新父节点的选择状态。
|
||
/// </summary>
|
||
public void UpdateParentSelection()
|
||
{
|
||
if (ParentNode == null)
|
||
return;
|
||
|
||
// 递归更新IsSelected属性
|
||
UpdateSelectionState(ParentNode);
|
||
ParentNode.UpdateParentSelection();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新指定节点的IsSelected属性。
|
||
/// </summary>
|
||
/// <param name="node"></param>
|
||
private static void UpdateSelectionState(TreeNode node)
|
||
{
|
||
if (node.IsTerminal)
|
||
return;
|
||
|
||
// 刷新父节点的选择状态
|
||
var selectionGroup = node.Flatten(true).GroupBy(x => x.IsSelected);
|
||
var enumerable = selectionGroup.ToList();
|
||
if (!enumerable.Any())
|
||
return;
|
||
|
||
if (enumerable.Count() > 1)
|
||
node.SetSelectionProperty(null);
|
||
else if (enumerable.Count() == 1)
|
||
{
|
||
var ss = enumerable[0].Key;
|
||
if (ss.HasValue)
|
||
node.SetSelectionProperty(ss.Value);
|
||
}
|
||
else
|
||
{
|
||
// 非法情况
|
||
Debugger.Break();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新父级节点的显示属性。
|
||
/// </summary>
|
||
private static void UpdateParentVisibility(TreeNode node)
|
||
{
|
||
while (true)
|
||
{
|
||
// 刷新父节点的选择状态
|
||
var selectionGroup = node.Flatten(true).GroupBy(x => x.Visibility);
|
||
var enumerable = selectionGroup.ToList();
|
||
if (!enumerable.Any())
|
||
return;
|
||
|
||
if (enumerable.Count() > 1)
|
||
// 如果终端节点存在多种情况,则一定显示本节点。
|
||
node.Visibility = Visibility.Visible;
|
||
else if (enumerable.Count == 1)
|
||
{
|
||
// 如果存在一种情况,则判断是全部为显示还是全部为折叠。
|
||
|
||
var ss = enumerable[0].Key;
|
||
if (ss == Visibility.Visible)
|
||
node.Visibility = Visibility.Visible;
|
||
else
|
||
node.Visibility = Visibility.Collapsed;
|
||
}
|
||
else
|
||
{
|
||
// 非法情况
|
||
Debugger.Break();
|
||
}
|
||
|
||
|
||
// 如果本节点已经是顶层节点,则退出
|
||
if (node.ParentNode == null)
|
||
break;
|
||
|
||
node = node.ParentNode;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当终端节点被选则或取消选择时触发此事件。
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void OnTerminalNodeSelectionChanged(object sender, TreeNodeSelectionChangedEventArgs e)
|
||
{
|
||
//! 在树的根部检查终端节点选中数是否超过最大限制
|
||
if (IsTopLevel)
|
||
{
|
||
if (MaxTerminalSelectionAllowed > -1)
|
||
if (GetSelectedTerminalCount() > MaxTerminalSelectionAllowed)
|
||
{
|
||
e.Cancel = true;
|
||
return;
|
||
}
|
||
|
||
UpdateTerminalNodeCountInfo();
|
||
}
|
||
|
||
TerminalNodeSelectionChanged?.Invoke(sender, e);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当ChildNodes元素发生变化时触发此事件。
|
||
/// </summary>
|
||
/// <param name="sender"></param>
|
||
/// <param name="e"></param>
|
||
private void ChildNodesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||
{
|
||
/*
|
||
* 如果当前节点是顶层节点,则统计终端节点总数,否则继续向父级节点传递事件。
|
||
*/
|
||
|
||
if (IsTopLevel)
|
||
UpdateTerminalNodeCountInfo();
|
||
|
||
ChildNodesChanged?.Invoke(this, e);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新终端节点数量信息。
|
||
/// </summary>
|
||
private void UpdateTerminalNodeCountInfo()
|
||
{
|
||
lock (_suspendUpdateLocker)
|
||
{
|
||
if (_isUpdateSuspended)
|
||
return;
|
||
|
||
SelectedTerminalCount = GetSelectedTerminalCount();
|
||
TotalTerminalCount = ChildNodes.Sum(x => x.Flatten(true).ToList().Count);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 该方法用于Tree内部设置<see cref="IsSelected"/>属性的状态。
|
||
/// </summary>
|
||
/// <param name="isSelected"></param>
|
||
private void SetSelectionProperty(bool? isSelected)
|
||
{
|
||
var snapshot = _isSelected;
|
||
_isSelected = isSelected;
|
||
|
||
var args = new TreeNodeSelectionChangedEventArgs(this, snapshot, _isSelected);
|
||
RaiseTerminalNodeSelectionChangedEvent(args);
|
||
if (args.Cancel)
|
||
_isSelected = snapshot;
|
||
|
||
OnPropertyChanged(nameof(IsSelected));
|
||
|
||
if (IsTerminal)
|
||
UpdateParentSelection();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查找指定完整路径名称的末端节点。
|
||
/// </summary>
|
||
/// <param name="fullName"></param>
|
||
/// <returns></returns>
|
||
public TreeNode FindTerminalByFullName(string fullName)
|
||
{
|
||
return Flatten(true).FirstOrDefault(x => x.FullName == fullName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定层级的节点。
|
||
/// </summary>
|
||
/// <param name="level">0-based层级,0表示Tree的根节点。</param>
|
||
/// <returns></returns>
|
||
public TreeNode GetNodeByLevel(int level)
|
||
{
|
||
var list = new List<TreeNode>();
|
||
list.Add(this);
|
||
while (true)
|
||
{
|
||
if (ParentNode != null)
|
||
list.Add(this.ParentNode);
|
||
else
|
||
break;
|
||
}
|
||
|
||
if (level >= list.Count)
|
||
return null;
|
||
|
||
return list[level];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取被选中的终端节点的数量。
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public int GetSelectedTerminalCount()
|
||
{
|
||
return Flatten(true).Where(x => x.IsSelected == true).ToList().Count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将数中的节点转换为节点数组。
|
||
/// </summary>
|
||
/// <param name="terminalOnly">是否仅枚举最末端节点。</param>
|
||
/// <para>更新子节点的Selected属性时,需要同时更新中间节点的属性;更新父节点时,只评估终端节点的Selected属性。</para>
|
||
/// <returns></returns>
|
||
public List<TreeNode> Flatten(bool terminalOnly)
|
||
{
|
||
if (ChildNodes == null || ChildNodes.Count <= 0)
|
||
return new List<TreeNode>(new[] { this });
|
||
|
||
var lst = ChildNodes.SelectMany(x => x.Flatten(terminalOnly)).ToList();
|
||
if(!terminalOnly)
|
||
lst.Add(this);
|
||
|
||
return lst;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除统计数据。
|
||
/// </summary>
|
||
public void ClearStatistic()
|
||
{
|
||
MinValue = double.NaN;
|
||
MaxValue = double.NaN;
|
||
AverageValue = double.NaN;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置统计数据。
|
||
/// </summary>
|
||
/// <param name="min"></param>
|
||
/// <param name="max"></param>
|
||
/// <param name="average"></param>
|
||
public void SetStatistic(double min, double max, double average)
|
||
{
|
||
MinValue = min;
|
||
MaxValue = max;
|
||
AverageValue = average;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消所有节点选择状态。
|
||
/// </summary>
|
||
public void UnselectAll()
|
||
{
|
||
SuspendUpdate();
|
||
Flatten(true).ForEach(x => x.IsSelected = false);
|
||
ResumeUpdate();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选择所有节点。
|
||
/// </summary>
|
||
public void SelectAll()
|
||
{
|
||
SuspendUpdate();
|
||
Flatten(true).ForEach(x => x.IsSelected = true);
|
||
ResumeUpdate();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 折叠所有节点。
|
||
/// </summary>
|
||
public void CollapseAll()
|
||
{
|
||
Flatten(false).ForEach(x => x.IsExpanded = false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 展开所有节点。
|
||
/// </summary>
|
||
public void ExpandAll()
|
||
{
|
||
Flatten(false).ForEach(x => x.IsExpanded = true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示所有节点。
|
||
/// </summary>
|
||
public void ShowAll()
|
||
{
|
||
Flatten(true).ForEach(x => x.Visibility = Visibility.Visible);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示选中的项目。
|
||
/// </summary>
|
||
public void ShowSelectedOnly()
|
||
{
|
||
Flatten(true).Where(x => x.IsSelected == false).ToList()
|
||
.ForEach(x => x.Visibility = Visibility.Collapsed);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用筛选器。
|
||
/// </summary>
|
||
/// <param name="keyWord"></param>
|
||
public void ApplyFilter(string keyWord)
|
||
{
|
||
//! 自顶向下遍历,仅需关心子节点状态。
|
||
|
||
// 如果父节点匹配,则我强制匹配。
|
||
IsMatch = (ParentNode != null && ParentNode.IsMatch) || Name.ToLower().Contains(keyWord.ToLower());
|
||
FilterKeyWord = keyWord;
|
||
|
||
// 如果是终端节点,则仅检查是否匹配关键字,然后退出
|
||
if (IsTerminal)
|
||
return;
|
||
|
||
// 否则检查我的名称是否匹配关键字,并检查我的子节点中是否有匹配关键字的节点,如果有,则我也要显示。
|
||
foreach (var node in ChildNodes)
|
||
{
|
||
node.ApplyFilter(keyWord);
|
||
}
|
||
|
||
var isMatchedNodes = Flatten(false).Where(x => x.IsMatch).ToList();
|
||
if (isMatchedNodes.Any())
|
||
IsMatch = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除筛选器。
|
||
/// </summary>
|
||
public void ClearFilter()
|
||
{
|
||
IsMatch = true;
|
||
FilterKeyWord = "";
|
||
|
||
if (IsTerminal)
|
||
return;
|
||
|
||
foreach (var node in ChildNodes)
|
||
{
|
||
node.ClearFilter();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 挂起TreeView视图更新。
|
||
/// <para>通常在后端对节点Selected属性操作时,挂起视图更新,以提高性能。</para>
|
||
/// </summary>
|
||
public void SuspendUpdate()
|
||
{
|
||
lock (_suspendUpdateLocker)
|
||
{
|
||
_isUpdateSuspended = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 恢复TreeView视图更新。
|
||
/// <para>通常在后端对节点Selected属性操作时,挂起视图更新,以提高性能。</para>
|
||
/// <para>恢复视图更新后,主动更新节点数量统计值。</para>
|
||
/// </summary>
|
||
public void ResumeUpdate()
|
||
{
|
||
lock (_suspendUpdateLocker)
|
||
{
|
||
_isUpdateSuspended = false;
|
||
}
|
||
|
||
UpdateTerminalNodeCountInfo();
|
||
}
|
||
|
||
public override string ToString()
|
||
{
|
||
// 完整路径中删除根节点的名称,以确保FullName匹配系统中的定义。
|
||
return (ParentNode?.ParentNode == null) ? Name : $"{ParentNode}.{Name}";
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|