Files
ShrlAlgoToolkit/Melskin/Controls/MultiTreeView.xaml.cs
2026-02-12 21:29:00 +08:00

300 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Specialized;
namespace Melskin.Controls;
/// <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)
{
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)
{
var children = GetGeneratedChildren(item);
if (children.Count == 0) return;
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)
.Select(i => parent.ItemContainerGenerator.ContainerFromIndex(i) as MultiTreeViewItem)
.Where(c => c != null)];
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)
{
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;
}