Files
ShrlAlgoToolkit/NeoUI/Melskin/Controls/MultiTreeView.xaml.cs

300 lines
11 KiB
C#
Raw Normal View History

2025-08-20 12:10:13 +08:00
using System.Collections;
using System.Collections.Specialized;
2026-01-02 17:30:30 +08:00
namespace VariaStudio.Controls;
2025-08-20 12:10:13 +08:00
/// <summary>
/// 多选树形视图控件。
/// </summary>
public sealed class MultiTreeView : TreeView
{
// 内部标志位用于防止在控件处理逻辑时响应外部对SelectedItems集合的修改避免冲突
private bool isUpdatingSelection;
/// <summary>
/// 多选树形视图控件,允许用户选择多个节点。
/// 继承自TreeView类并扩展了多选功能。
/// </summary>
public MultiTreeView()
{
// 为SelectedItems属性提供一个默认的集合实例
SetValue(SelectedItemsProperty, new List<object>());
}
#region SelectedItems Dependency Property
/// <summary>
/// 表示 MultiTreeView 控件中当前选中的项的集合的依赖属性。
/// 该属性用于绑定到外部数据源,以反映用户在树形视图中的选择状态。
/// 当此属性值更改时会触发相关联的逻辑来同步UI的勾选状态并更新内部对新集合的事件监听。
/// </summary>
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(
nameof(SelectedItems),
typeof(IList),
typeof(MultiTreeView),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));
/// <summary>
/// 获取或设置 MultiTreeView 控件中当前选中的项的集合。
/// 该属性允许外部数据源绑定到控件,以便反映用户在树形视图中的选择状态。
/// 当此属性值更改时会触发相关联的逻辑来同步UI的勾选状态并更新内部对新集合的事件监听。
/// </summary>
public IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
#endregion
#region Item Container Overrides
/// <inheritdoc />
protected override DependencyObject GetContainerForItemOverride() => new MultiTreeViewItem();
/// <inheritdoc />
protected override bool IsItemItsOwnContainerOverride(object item) => item is MultiTreeViewItem;
#endregion
#region Event Handling and Logic
// 当绑定的SelectedItems属性本身发生变化时例如从ViewModel中设置了一个新的集合
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var treeView = (MultiTreeView)d;
if (treeView.isUpdatingSelection)
return;
// 移除对旧集合的事件监听
if (e.OldValue is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= treeView.OnSelectedItemsCollectionChanged;
}
// 添加对新集合的事件监听
if (e.NewValue is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += treeView.OnSelectedItemsCollectionChanged;
}
// 根据新集合的内容完全同步UI的勾选状态
treeView.SyncUiFromSelectedItems();
}
// 当绑定的SelectedItems集合内部发生Add/Remove/Clear等操作时
private void OnSelectedItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
2025-08-20 12:10:13 +08:00
{
if (isUpdatingSelection)
return;
SyncUiFromSelectedItems();
}
// 核心方法: 由MultiTreeViewItem调用表示一个节点的IsChecked状态被用户改变
internal void OnItemCheckChanged(MultiTreeViewItem item)
{
isUpdatingSelection = true;
// 1. 向下更新:更新所有子孙节点的选中状态,使其与当前节点一致
var descendants = GetDescendants(item, true);
foreach (var descendant in descendants)
{
SetItemIsChecked(descendant, item.IsChecked == true);
}
// 2. 向上更新:逐级更新所有父节点的选中状态(可能变为全选、不选或半选)
var ancestor = GetParentItem(item);
while (ancestor != null)
{
UpdateParentItemCheckState(ancestor);
ancestor = GetParentItem(ancestor);
}
// 3. 数据同步根据当前UI的勾选状态更新SelectedItems集合
SyncSelectedItemsFromUi();
isUpdatingSelection = false;
}
#endregion
#region Synchronization Methods
// 从SelectedItems集合出发反向同步整个UI树的勾选状态
private void SyncUiFromSelectedItems()
{
isUpdatingSelection = true;
var allItems = GetAllGeneratedItems();
foreach (var item in allItems)
{
var shouldBeChecked = SelectedItems?.Contains(item.DataContext) ?? false;
SetItemIsChecked(item, shouldBeChecked);
}
// 更新所有父节点的半选状态确保UI一致性
foreach (var item in allItems.Where(i => !i.HasItems))
{
var parent = GetParentItem(item);
while (parent != null)
{
UpdateParentItemCheckState(parent);
parent = GetParentItem(parent);
}
}
isUpdatingSelection = false;
}
// 从UI树的当前勾选状态出发同步更新SelectedItems集合
private void SyncSelectedItemsFromUi()
{
var selectedDataItems = GetAllGeneratedItems()
.Where(item => item.IsChecked == true)
.Select(item => item.DataContext)
.ToList();
// SelectedItems ??= new List<object>();
// 高效地比较并更新集合,避免不必要的事件触发
var currentSelected = SelectedItems.OfType<object>().ToList();
var toAdd = selectedDataItems.Except(currentSelected).ToList();
var toRemove = currentSelected.Except(selectedDataItems).ToList();
foreach (var item in toRemove) SelectedItems.Remove(item);
foreach (var item in toAdd) SelectedItems.Add(item);
}
#endregion
#region Helper Methods
// 安全地设置一个MultiTreeViewItem的IsChecked状态同时抑制其事件通知
private static void SetItemIsChecked(MultiTreeViewItem item, bool isChecked)
{
if (item.IsChecked == isChecked) return;
item.IsUpdatingCheckState = true;
item.IsChecked = isChecked;
item.IsUpdatingCheckState = false;
}
// 根据子项的勾选状态,更新父项的状态(可能是 true, false, or null for indeterminate
private static void UpdateParentItemCheckState(MultiTreeViewItem item)
2025-08-20 12:10:13 +08:00
{
var children = GetGeneratedChildren(item);
if (children.Count == 0) return;
2025-08-20 12:10:13 +08:00
bool? newCheckState;
if (children.All(c => c?.IsChecked == true))
newCheckState = true;
else if (children.All(c => c is { IsChecked: false or null }))
newCheckState = false;
else
newCheckState = null;
item.IsUpdatingCheckState = true;
item.IsChecked = newCheckState;
item.IsUpdatingCheckState = false;
}
// 获取一个节点的所有已生成的子孙节点 (UI-based)
private static List<MultiTreeViewItem> GetDescendants(ItemsControl parent, bool includeSelf)
{
var descendants = new List<MultiTreeViewItem>();
if (includeSelf && parent is MultiTreeViewItem self)
{
descendants.Add(self);
}
// ItemContainerGenerator是WPF中获取项容器的标准方式
for (var i = 0; i < parent.Items.Count; i++)
{
if (parent.ItemContainerGenerator.ContainerFromIndex(i) is MultiTreeViewItem child)
{
descendants.AddRange(GetDescendants(child, true));
}
}
return descendants;
}
private List<MultiTreeViewItem> GetAllGeneratedItems() => GetDescendants(this, false);
private static List<MultiTreeViewItem?> GetGeneratedChildren(ItemsControl parent) =>
[.. Enumerable.Range(0, parent.Items.Count)
2025-08-20 12:10:13 +08:00
.Select(i => parent.ItemContainerGenerator.ContainerFromIndex(i) as MultiTreeViewItem)
.Where(c => c != null)];
2025-08-20 12:10:13 +08:00
private static MultiTreeViewItem? GetParentItem(TreeViewItem item) => ItemsControl.ItemsControlFromItemContainer(item) as MultiTreeViewItem;
#endregion
}
/// <summary>
/// 用于多选树形视图中的树形视图项。
/// </summary>
public class MultiTreeViewItem : TreeViewItem
{
// 内部标志位用于防止在父控件更新IsChecked状态时再次触发不必要的通知
internal bool IsUpdatingCheckState { get; set; }
/// <summary>
/// 表示 MultiTreeViewItem 是否被选中的依赖属性。
/// 该属性支持三态true 表示选中false 表示未选中null 表示半选状态(部分子项被选中)。
/// 当此属性值更改时,会触发相关联的逻辑来更新其所有子孙节点以及父节点的选中状态,
/// 并同步更新 SelectedItems 集合以反映最新的用户选择。
/// </summary>
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register(
nameof(IsChecked),
typeof(bool?),
typeof(MultiTreeViewItem),
new FrameworkPropertyMetadata(false, OnIsCheckedChanged));
/// <summary>
/// 获取或设置当前 MultiTreeViewItem 是否被选中。
/// 该属性支持三态true 表示选中false 表示未选中null 表示半选状态(部分子项被选中)。
/// 当此属性值更改时,会触发相关联的逻辑来更新其所有子孙节点以及父节点的选中状态,
/// 并同步更新 SelectedItems 集合以反映最新的用户选择。
/// </summary>
/// <value>类型为 <see cref="bool"/> 的值,指示当前项是否被选中。</value>
public bool? IsChecked
{
get => (bool?)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
static MultiTreeViewItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiTreeViewItem), new FrameworkPropertyMetadata(typeof(MultiTreeViewItem)));
}
private static void OnIsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var item = (MultiTreeViewItem)d;
// 如果是父控件的内部逻辑在更新状态,则直接返回,避免无限循环
if (item.IsUpdatingCheckState)
return;
// 向上查找父控件MultiTreeView并通知它状态发生了用户驱动的变化
var parentTreeView = GetParentTreeView(item);
parentTreeView?.OnItemCheckChanged(item);
}
// 辅助方法用于在可视化树中向上查找父MultiTreeView
private static MultiTreeView? GetParentTreeView(DependencyObject item)
{
var parent = VisualTreeHelper.GetParent(item);
while (parent != null && parent is not MultiTreeView)
2025-08-20 12:10:13 +08:00
{
parent = VisualTreeHelper.GetParent(parent);
}
return parent as MultiTreeView;
}
// 重写这两个方法是自定义TreeViewItem的标准做法
/// <inheritdoc />
protected override DependencyObject GetContainerForItemOverride() => new MultiTreeViewItem();
/// <inheritdoc />
protected override bool IsItemItsOwnContainerOverride(object item) => item is MultiTreeViewItem;
}