WPF TreeListView条件自动展开优化:移除Task.Delay及相关改进
WPF树形控件自动展开优化问题
我在WPF视图中实现了带搜索过滤的树形控件(使用TreeListView,行为与TreeView一致),需求是当过滤条件或搜索字符串变更时,自动展开所有匹配节点。由于自定义ControlTemplate会忽略原TreeView的ItemsSource,我用了以下临时方案:
现有代码
ControlTemplate定义
<ControlTemplate x:Key="TreeControlTemplate"> <Border> <treeList:TreeListView ItemsSource="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type treeList:TreeListView}}}" <!-- 其他模板内容 --> </Border> </ControlTemplate>
TreeListView使用代码
<treeList:TreeListView Template="{StaticResource TreeControlTemplate}" DataContext="{Binding RootItem.FilteredChildren}" />
后台展开逻辑
private ViewModel _viewModel; public View() { InitializeComponent(); DataContextChanged += OnDataContextChanged; Tree.DataContextChanged += Tree_DataContextChanged; } private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (DataContext is ViewModel viewModel) { DataContextChanged -= OnDataContextChanged; _viewModel = viewModel; } } private async void Tree_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { await Task.Delay(500); if (_viewModel.ShouldExpandTreesAfterUpdate()) ExpandTree(Tree); } private void ExpandTree(DependencyObject parent) { for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is TreeListViewItem treeListViewItem) treeListViewItem.ExpandSubtree(); else ExpandTree(child); } }
当前实现仅在使用Task.Delay时生效,存在三个待解决问题:
- 必须依赖
Task.Delay(否则UI未就绪),如何移除该依赖? - 如何避免在View后台代码中访问ViewModel?
- 因绑定
ItemsSource导致Items属性为空,需遍历整个视觉树查找节点,如何优化?
解决方案
1. 移除Task.Delay依赖
问题根源是DataContext变更后,UI容器尚未生成完成,直接调用展开逻辑会找不到节点。可以监听ItemContainerGenerator的StatusChanged事件,等待容器生成完毕后再执行展开:
private void Tree_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (_viewModel?.ShouldExpandTreesAfterUpdate() != true) return; void OnGeneratorStatusChanged(object s, EventArgs args) { if (Tree.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { Tree.ItemContainerGenerator.StatusChanged -= OnGeneratorStatusChanged; ExpandTree(Tree); } } Tree.ItemContainerGenerator.StatusChanged += OnGeneratorStatusChanged; }
这样就不需要依赖延迟,而是在UI容器真正准备好后再执行展开操作。
2. 避免View后台访问ViewModel
可以用Blend行为封装展开逻辑,通过绑定ViewModel的属性触发操作,完全解耦View和ViewModel:
自定义行为类
using System.Windows; using System.Windows.Controls; using System.Windows.Media; using Microsoft.Xaml.Behaviors; public class AutoExpandTreeBehavior : Behavior<TreeListView> { public static readonly DependencyProperty ShouldExpandProperty = DependencyProperty.Register("ShouldExpand", typeof(bool), typeof(AutoExpandTreeBehavior), new PropertyMetadata(false, OnShouldExpandChanged)); public bool ShouldExpand { get => (bool)GetValue(ShouldExpandProperty); set => SetValue(ShouldExpandProperty, value); } private static void OnShouldExpandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var behavior = (AutoExpandTreeBehavior)d; if ((bool)e.NewValue) { behavior.TriggerExpand(); } } private void TriggerExpand() { void OnGeneratorReady(object s, EventArgs args) { if (AssociatedObject.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { AssociatedObject.ItemContainerGenerator.StatusChanged -= OnGeneratorReady; ExpandAllNodes(AssociatedObject); } } AssociatedObject.ItemContainerGenerator.StatusChanged += OnGeneratorReady; } private void ExpandAllNodes(DependencyObject parent) { for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) { var child = VisualTreeHelper.GetChild(parent, i); if (child is TreeListViewItem item) { item.ExpandSubtree(); } else { ExpandAllNodes(child); } } } }
XAML中引用行为
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" <treeList:TreeListView Template="{StaticResource TreeControlTemplate}" DataContext="{Binding RootItem.FilteredChildren}"> <i:Interaction.Behaviors> <local:AutoExpandTreeBehavior ShouldExpand="{Binding ShouldExpandTreesAfterUpdate}" /> </i:Interaction.Behaviors> </treeList:TreeListView>
之后View后台无需再引用ViewModel,完全通过绑定触发展开。
3. 优化视觉树遍历问题
由于ControlTemplate嵌套了一个内部TreeListView,外部控件的Items属性为空。可以直接定位到内部的TreeListView,利用它的ItemContainerGenerator操作节点,避免遍历整个视觉树:
private TreeListView GetInnerTree(TreeListView outerTree) { // 根据ControlTemplate结构,直接获取内部TreeListView var border = VisualTreeHelper.GetChild(outerTree, 0) as Border; return border?.Child as TreeListView; } // 修改展开方法 private void ExpandTree(TreeListView outerTree) { var innerTree = GetInnerTree(outerTree); if (innerTree == null) return; foreach (var item in innerTree.Items) { if (innerTree.ItemContainerGenerator.ContainerFromItem(item) is TreeListViewItem container) { container.ExpandSubtree(); ExpandChildNodes(container); } } } private void ExpandChildNodes(TreeListViewItem parentItem) { foreach (var childItem in parentItem.Items) { if (parentItem.ItemContainerGenerator.ContainerFromItem(childItem) is TreeListViewItem childContainer) { childContainer.ExpandSubtree(); ExpandChildNodes(childContainer); } } }
这样直接操作内部TreeListView的Items和容器生成器,遍历效率更高,逻辑也更清晰。
内容的提问来源于stack exchange,提问作者horotab
相关产品推荐
相关产品推荐

