65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 面包屑树控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (6投票s)

2013年11月26日

MIT

6分钟阅读

viewsIcon

24460

downloadIcon

525

本文提供了一个继承自 TreeView 的 Breadcrumb 实现。

 

介绍  

市面上已经有许多 WPF 的面包屑实现,我自己也写过两次这个控件(演示中也提供了 UserControls.Breadcrumb),但每次我都不得不依赖反射来检索层次结构(例如 typeof(T).GetProperty(subEntriesPath)),这是因为面包屑组件是一个包含多个 BreadcrumbItems 的 List 对象。

在许多方面,面包屑与 TreeView 非常相似,它有一个项源,一个选定的值,并且所有项都可以展开以显示其子项。将 BreadcrumbTree 做成 TreeView 也使其更容易绑定到 ViewModel。  因为它是一个 TreeView,所以项是根据您的 HierarchicalDataTemplate 加载的,而不是使用像 System.ReflectionUI 绑定 这样的自制方法。

使用代码 

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 有三个部分:

  1. 标题取决于 BreadcrumbTree.ItemTemplate。
  2. 展开器 combobox (DropDownList) 绑定到 Entries.All (ObservableCollection),当 Entries.IsExpanded (也绑定到 combobox) 时加载,它的项使用 BreadcrumbTree.MenuItemTemplate 进行渲染。
  3. 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> 

参考文献 

历史  

26/11/13 - 初始版本。

© . All rights reserved.