WPF 面包屑树控件






4.60/5 (6投票s)
本文提供了一个继承自 TreeView 的 Breadcrumb 实现。
介绍
市面上已经有许多 WPF 的面包屑实现,我自己也写过两次这个控件(演示中也提供了 UserControls.Breadcrumb),但每次我都不得不依赖反射来检索层次结构(例如 typeof(T).GetProperty(subEntriesPath)),这是因为面包屑组件是一个包含多个 BreadcrumbItems 的 List 对象。
在许多方面,面包屑与 TreeView 非常相似,它有一个项源,一个选定的值,并且所有项都可以展开以显示其子项。将 BreadcrumbTree 做成 TreeView 也使其更容易绑定到 ViewModel。 因为它是一个 TreeView,所以项是根据您的 HierarchicalDataTemplate 加载的,而不是使用像 System.Reflection 或 UI 绑定 这样的自制方法。
使用代码
BreadcrumbTree 仅包含树形部分,不包含展开器。 注意 XAML 代码中没有 HierarchicalPath:
<uc:BreadcrumbTree x:Name="btree" Height="30" ItemsSource="{Binding Entries.All}">
<uc:BreadcrumbTree.ItemContainerStyle>
<Style TargetType="{x:Type uc:BreadcrumbTreeItem}"
BasedOn="{StaticResource BreadcrumbTreeItemStyle}" >
<!-- Bind to your ViewModel -->
<Setter Property="ValuePath" Value="Selection.Value" />
<Setter Property="IsExpanded" Value="{Binding Entries.IsExpanded, Mode=TwoWay}" />
<Setter Property="IsCurrentSelected" Value="{Binding Selection.IsSelected, Mode=TwoWay}" />
<Setter Property="SelectedChild" Value="{Binding Selection.SelectedChild, Mode=TwoWay}" />
<Setter Property="IsChildSelected" Value="{Binding Selection.IsChildSelected, Mode=OneWay}" />
<!--Updated by BreadcrumbTreeItem and it's OverflowableStackPanel-->
<Setter Property="IsOverflowed" Value="{Binding IsOverflowed, Mode=OneWayToSource}" />
</Style>
</uc:BreadcrumbTree.ItemContainerStyle>
<uc:BreadcrumbTree.MenuItemTemplate>
<DataTemplate>
<!-- Icon -->
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</uc:BreadcrumbTree.MenuItemTemplate>
<uc:BreadcrumbTree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Entries.All}">
<!-- Icon -->
<TextBlock Text="{Binding Header}" />
</HierarchicalDataTemplate>
</uc:BreadcrumbTree.ItemTemplate>
</uc:BreadcrumbTree>
开发
在开发 BreadcrumbTree 时,我遇到了两个问题,这是我的解决方案:
TreeView 选择支持
大多数面包屑不是 TreeView 的原因在于 TreeView 不提供选择支持,这也是合乎逻辑的,因为 TreeView 只是 ListViews 的一个集合。 在 WPF 的早期,人们通过从根 TreeViewItem 在 UI 线程上查找 来找到选定的项,但这效果不好,因为查找会阻塞 UI 线程。
然后我开发了另一种方法(在 FileExplorer2 中有奖励,请在控件内的 文档中查找),该方法在 ViewModel 中进行查找,通过设置 TreeNodeViewModel.IsExpaned 为 true,它迫使 UI 在 TreeViewItem 加载时加载 ViewModel 的子内容,并在子 TreeViewItem 加载时继续搜索,从而实现非常流畅的树视图展开和选择。
但问题是所有代码都必须在 ViewModel 中完成,而 ViewModel 是不可重用的,所以根据单一职责原则,我将代码重构为 IEntriesHelper(控制子项的加载)、ITreeSelector 和 ITreeNodeSelector。 由于 async/await 的可用性,此方法使用 Task 而不是之前的方法。
BreadcrumbTree 要求 ViewModel 实现这些 ISupportTreeSelector<ViewModelType, ValueType>,其中 ViewModelType 是您的树节点 View Model 类型,ValueType 用于确定层次结构(使用 compareFunc)。
public class TreeViewModel : INotifyPropertyChanged
{
public TreeViewModel()
{
...
Entries = new EntriesHelper<TreeNodeViewModel>();
//Value is based on string
Selection = new TreeRootSelector<TreeNodeViewModel, string>(Entries, compareFunc);
Entries.SetEntries(new TreeNodeViewModel("", "Root", this, null));
}
public ITreeSelector<TreeNodeViewModel, string> Selection { get; set; }
public IEntriesHelper<TreeNodeViewModel> Entries { get; set; }
HierarchicalResult compareFunc(string path1, string path2)
{
if (path1.Equals(path2, StringComparison.CurrentCultureIgnoreCase))
return HierarchicalResult.Current;
if (path1.StartsWith(path2, StringComparison.CurrentCultureIgnoreCase))
return HierarchicalResult.Parent;
if (path2.StartsWith(path1, StringComparison.CurrentCultureIgnoreCase))
return HierarchicalResult.Child;
return HierarchicalResult.Unrelated;
}
}
public class TreeNodeViewModel : INotifyPropertyChanged
{
public TreeNodeViewModel(TreeViewModel root, TreeNodeViewModel parentNode)
{
...
Entries = new EntriesHelper<TreeNodeViewModel>(() =>
Task.Run(() => { /* Load your subentries (when needed) */ )));
Selection = new TreeSelector<TreeNodeViewModel, string>(value, this,
parentNode == null ? root.Selection : parentNode.Selection, Entries);
}
public ITreeSelector<TreeNodeViewModel, string> Selection { get; set; }
public IEntriesHelper<TreeNodeViewModel> Entries { get; set; }
}
EntriesHelper 包含当前节点的条目,它按需加载(例如,当 IsExpanded = true 时),或通过代码(IEntriesHelper.LoadAsync())。TreeSelector 允许查找层次结构(使用 ParentSelector 和 RootSelector),它还包含一些用于绑定的属性(例如,IsSeleted、IsChildSelected 和 SelectedChild),以及在这些属性更改时进行报告的代码。
TreeSelector 的默认实现使用 ITreeSelector.SelectAsync(),它是基于 async/await 的,而不是之前的方法(例如,设置 IsExpand)。例如,SelectAsync 调用以下内容:
await LookupAsync(value, RecrusiveSearch<VM, T>.LoadSubentriesIfNotLoaded,
SetSelected<VM, T>.WhenSelected, SetChildSelected<VM, T>.ToSelectedChild);
它涉及一个值(在这种情况下是字符串)、ITreeLookup(RecrusiveSearch)和 ITreeLookupProcessor(SetChild、SetChildSelected)。 需要 SetChildSelected 是因为 BreadcrumbTreeItem 包含一个主题 ComboBox,其 SelectedValue 绑定到 Selector 的 SelectedValue 属性,因此也必须更新。
RecrusiveSerch 查找层次结构,并在沿途调用处理器。 LookupAsync 不返回任何内容。
包括其他 ITreeLookups 和 ITreeLookupProcessors:
- SearchNextLevel - 类似于递归搜索,但只工作到下一级别,这在 ComboBox 值更改时使用,它在下一级别找到适当的项并将其 IsSelected 设置为 true。
- RecrusiveSearch - 递归搜索以查找所需值,当调用 SelectAsync(value) 时使用。
- RecrusiveBroadcast - 递归,但与递归搜索不同,它广播到所有节点。
- SearchNextUsingReverseLookup - 根据您提供的值以及与之关联的 ITreeSelector 在下一级别搜索项。 这是必需的,因为在 DirectoryInfoEx 中,可能有不同的方法来达到一个路径(例如,桌面 \ 此电脑 \ 文档和 C:\Users\{用户}\文档),以便在项不再被选择时更新 SelectedValue。
- SetSelected - 当 HierarchicalResult 是 Current 时,将 Selector.IsSelected 更新为 true。
- SetNotSelected - 当 HierarchicalResult 是/不是 Current 时,将 Selector.IsSelected 更新为 true。
- SetExpanded - 当 HierarchicalResult 是 Child 时,将 Entries.IsExpanded 更新为 true。
- SetChildSelected - 当 HierarchicalResult 是 Current 或 Child 时,将 Selector.SelectedValue 更新为子项的值。
- SetChildNotSelected - 当 HierarchicalResult 是 Current 或 Child 时,将 Selector.SelectedValue 更新为 null。
这些辅助类可用于任何 TreeView 继承项。
BreadcrumbTree 溢出支持(展开器)

另一个问题是溢出。Breadcrumb 应该隐藏最左边的 BreadcrumbItem(如果空间不足),并在展开器中显示它们。 这可以通过编写一个面板来解决,但当它是树时,问题会更复杂,例如:
BreadcrumbTree 是一个经过重新设计的 TreeView,它有一个标题,一个展开器 combobox 和一个对齐在水平堆栈面板中的项列表。
BreadcrumbTree 是根 BreadcrumbItem 的容器,它又是更多 BreadcrumbItems 的容器,一个 BreadcrumbItem 有三个部分:
- 标题取决于 BreadcrumbTree.ItemTemplate。
- 展开器 combobox (DropDownList) 绑定到 Entries.All (ObservableCollection),当 Entries.IsExpanded (也绑定到 combobox) 时加载,它的项使用 BreadcrumbTree.MenuItemTemplate 进行渲染。
- itemList,虽然是一个列表,但只显示一项。只有被选中(Selection.IsSelected)或沿途被选中(Selection.IsChildSelected)的项才可见,此列表的面板是 OneItemPanel。
其中 1 和 2 应该渲染为不可见,而 3 在项溢出时应该可见,因为所有可见的 BreadcrumbItems 都位于不同的分支,因此不同的面板,溢出必须单独处理。
为了解决这个问题,1、2 和 3 被放置在一个 OverflowableStackPanel 内。当空间不足时,此面板将折叠 OverflowableStackPanel.CanOverflow 的项,并将 OverflowableStackPanel.OverflowItemCount 设置为 true,该值绑定到特定的 BreadcrumbItem.OverflowItemCount。 当 OverflowItemCount > 0 时,BreadcrumbItem 将 IsOverflowed 设置为 true,从而 TreeNodeViewModel.Selector.IsOveflowed 为 true。
一旦 ViewModel 通知其已溢出,展开的项就可以显示在主题 ComboBox (DropDownList) 中。
<bc:DropDownList x:Name="bexp" DockPanel.Dock="Left" ItemsSource="{Binding Selection.OverflowedAndRootItems}" SelectedValuePath="Selection.Value" ItemTemplate="{Binding ItemTemplate, ElementName=btree}"> <bc:DropDownList.Header> <Path x:Name="path" Stroke="Black" StrokeThickness="1.2" Data="{StaticResource ExpanderArrow}" HorizontalAlignment="Center" VerticalAlignment="Center" IsHitTestVisible="True" /> </bc:DropDownList.Header> <bc:DropDownList.ItemContainerStyle> <Style TargetType="{x:Type ComboBoxItem}" BasedOn="{StaticResource ComboboxNullAsSeparatorStyle}"> <Setter Property="Visibility" Value="{Binding IsOverflowedOrRoot, Mode=OneWay, Converter={StaticResource btvc}}" /> </Style> </bc:DropDownList.ItemContainerStyle> </bc:DropDownList>
BreadcrumbTree 不包含 SuggestBox,您需要手动组合展开器 DropDownList、BreadcrumbTree 和 SuggestBox。
可重用控件
在开发 BreadcrumbTree 时,我开发了许多可重用控件,它们在开发其他控件时可能有用。
Breadcrumb -
Breadcrumb 控件是 BreadcrumbTree 的列表版本,它是对原始版本 这里 找到的版本的更新。
这个 Breadcrumb 比本文描述的 BreadcrumbTree 更易于使用且功能更丰富(也带有 SuggestBox),您只需要设置 Parent/Value 和 Subentries 路径。
<uc:Breadcrumb x:Name="breadcrumb2" Height="30" ParentPath="Parent" ValuePath="Value" SubentriesPath="SubDirectories" SelectedPathValue="{Binding SelectedPathValue, ElementName=breadcrumb1, Mode=TwoWay}" IconTemplate="{StaticResource FakeVMIconTemplate}" IsProgressbarVisible="True" IsIndeterminate="False" HeaderTemplate="{StaticResource FakeVMHeaderTemplate}" />
然而,它不如 BreadcrumbTree 灵活,因为用户 ViewModel 无法控制大多数内部工作。
SuggestBoxBase / SuggestBox

SuggestBox 根据您的输入显示弹出建议,建议从 HierarchyHelper 和 SuggestSource 查询。
SuggestBoxBase 是 SuggestBox 的基类,它允许开发人员自行处理建议(通过设置 SuggestBoxBase.Suggestions)。
<bc:SuggestBox x:Name="suggestBoxAuto2" DisplayMemberPath="Value" Hint="Uses ViewModel, try Sub1\Sub12" Text="{Binding Text, ElementName=txtAuto, UpdateSourceTrigger=Explicit}"/> suggestBoxAuto2.HierarchyHelper = suggestBoxAuto.HierarchyHelper = new PathHierarchyHelper("Parent", "Value", "SubDirectories"); suggestBoxAuto2.RootItem = FakeViewModel.GenerateFakeViewModels(TimeSpan.FromSeconds(0.5)); suggestBoxAuto2.SuggestSources = new List<ISuggestSource>(new[] { new AutoSuggestSource() });
HotTrack -

HotTrack 是一个重新设计的 Border,在 IsMouseOver、IsDragging 和 IsSelected 时会高亮显示自身。
<bc:HotTrack BorderBrush="Gainsboro" BorderThickness="1" IsEnabled="True" SelectedBorderBrush="Black"> <Button Template="{StaticResource BaseButton}" Width="200" Height="70" BorderBrush="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center" >ABC</Button> </bc:HotTrack>
DropDown / DropDownList -

DropDown 是一个创建下拉菜单的按钮,您可以使用任何标题或内容。 DropDownList 是一个包含列表的 DropDown。
<bc:DropDown x:Name="dd" > <bc:HotTrack BorderBrush="Gainsboro" BorderThickness="1" IsEnabled="True" SelectedBorderBrush="Black"> <Button Template="{StaticResource BaseButton}" Width="200" Height="70" BorderBrush="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center" >Popup</Button> </bc:HotTrack> </bc:DropDown>
参考文献
- FastObservableCollection
- 如何同步运行 async task<T> 方法
- WPF 面包屑文件夹文本框 - 我的原始面包屑
- WPF 面包屑栏 - CodeProject 上的另一个面包屑
- Nesher 的面包屑栏 - 另一个面包屑
历史
26/11/13 - 初始版本。