Click here to Skip to main content
15,885,980 members
Articles / Desktop Programming / WPF

WPF BreadcrumbTree control

Rate me:
Please Sign up or sign in to vote.
4.60/5 (6 votes)
25 Nov 2013MIT6 min read 23.7K   511   27  
This article provides a implementation Breadcrumb that inherited from TreeView

  Image 1

Introduction  

There have been a number WPF breadcrumb implementation around, even I have written this control twice (UserControls.Breadcrumb is also available in the demo), both times I have to rely in reflection to retrieve the Hierarchy (e.g. .typeof(T).GetProperty(subEntriesPath)), this is because the breadcrumb component is a List object that contains a number of BreadcrumbItems.

In many aspect a breadcrumb is very similar to TreeView, it has an item source, a selected value and all items can expand to show it's sub-items. Making BreadcrumbTree a TreeView also allow it easier to bindable to ViewModel.  Because it's a TreeView, the items are loaded based on your HierarchicalDataTemplate instead of using a home-grown method like using System.Reflection or using Binding from UI.

Using the code 

The BreadcrumbTree is the tree section only and does not include the expander.  Noticed that there's no HierarchicalPath in the xaml code:  

XML
<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>

 

Development 

I found two problems when developing BreadcrumbTree, and here's my solution for these problems:

TreeView Selection support 

 Image 2


The reason that most breadcrumb is not a TreeView is that the TreeView does not provide selection support, and it's logical because TreeView is just a group of ListViews.  In the earlier days of WPF people do lookup using the UI thread from root TreeViewItem and find the selected item, this doesn't work well because the lookup jammed the UI thread.

Then I developed another approach (Bounty in FileExplorer2, please find in the documentation inside the control), which do the lookup in ViewModel, by setting TreeNodeViewModel.IsExpaned to true, it force the UI to load the sub-contents of the ViewModel when the TreeViewItem is loaded, and continue search when sub-TreeViewItem is loaded,  resulting very smooth tree view expand and selection.

But the problem is that all code has to be done in ViewModel, and the ViewModel is not reusable, so under Single responsibility principle, I have refactored the code to IEntriesHelper (which control the loading of sub-entries), ITreeSelector and ITreeNodeSelector. Because the availability of async/await, this approach use Task instead of previous method.


BreadcrumbTree require the view model implement these ISupportTreeSelector<ViewModelType, ValueType>, where ViewModelType is your tree node View Model Type, ValueType is used to determine the hierarchy (using the compareFunc).

C#
 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 contains the entries of the current node, it's loaded on demand (e.g. when IsExpanded = true), or by code (IEntriesHelper.LoadAsync()).
TreeSelector enable looking up the hierarchy (using ParentSelector and RootSelector), it also contains a number of properties for binding (e.g. IsSeleted, IsChildSelected and SelectedChild), and code for reporting when these properties are changed.
The default implementation of TreeSelector uses ITreeSelector.SelectAsync(), it is async/await based instead of previous method (e.g. setting IsExpand), for examples, the SelectAsync calls the following:

 await LookupAsync(value, RecrusiveSearch<VM, T>.LoadSubentriesIfNotLoaded,
                    SetSelected<VM, T>.WhenSelected, SetChildSelected<VM, T>.ToSelectedChild);

It involve a value (which is string in this case),  ITreeLookup (RecrusiveSearch) and ITreeLookupProcessor (SetChild, SetChildSelected).  SetChildSelected is required because BreadcrumbTreeItem contains a themed ComboBox, which has it's SelectedValue binded to Selector's SelectedValue property, so it has to be updated as well.

RecrusiveSerch lookup the hierarchy, and call the processors whenever it's along the path.  LookupAsync does not return anything.

Other ITreeLookups and ITreeLookupProcessors included :
  • SearchNextLevel - just like recrusive search but only work until next level, this is used when ComboBox value changes, it find the appropriate item in next level and set it's IsSelected to true.
  • RecrusiveSearch - Recrusive search to find the required value, this is used when SelectAsync(value) is called
  • RecrusiveBroadcast - Recrusive, but unlike Recrusive search, broadcast to all nodes.
  • SearchNextUsingReverseLookup - Search item in next level, based on your supplied value and an ITreeSelector that's associated with it.  This is required because in DirectoryInfoEx, there may be different ways to reach a path (e.g. Desktop \ This PC \ Documents and C:\Users\{User}\Documents), to update the SelectedValue when an item no longer selected.
  • SetSelected - Update Selector.IsSelected to true when  HierarchicalResult is Current.
  • SetNotSelected - Update Selector.IsSelected to true when  HierarchicalResult is / is not Current.
  • SetExpanded - Update Entries.IsExpanded to true when  HierarchicalResult is Child.
  • SetChildSelected - Update Selector.SelectedValue to child's value when  HierarchicalResultis Current or Child.
  • SetChildNotSelected - Update Selector.SelectedValue to null when  HierarchicalResultis Current or Child.

These helper class can be used in any TreeView inherited items.  

 

BreadcrumbTree Overflow support (Expander) 

Image 3

The another problem is overflowing, Breadcrumb should hide the leftmost BreadcrumbItem if not enough space is available, and show them in the expander.  This can and should be solved by writing a panel, but the problem is more complicate when it's a tree, e.g:

BreadcrumbTree is a restyled TreeView, it has a header, a expander comboBox and item list all align in a horizontal stack panel.

BreadcrumbTree is a container of root's BreadcrumbItem, which is a container of more BreadcrumbItems, a BreadcrumbItem has three parts

  1. The header is depended on the BreadcrumbTree.ItemTemplate.
  2. The expander combobox (DropDownList) binded to Entries.All (ObservableCollection), which is loaded when Entries.IsExpanded (also binded to the combobox), it's items are render using BreadcrumbTree.MenuItemTemplate
  3. The itemList, although is a list, display one items only, only the item that is selected (Selection.IsSelected) or is along the path to selected (Selection.IsChildSelected) is visible, the panel of this list is OneItemPanel.

Where 1 and 2 should render invisible while 3 should be visible when an item is overflowed, because all visible BreadcrumbItems are in different branch, thus different panel, overflow have to be handled separately.

To solve the problem, 1, 2 and 3 is placed inside an OverflowableStackPanel, this panel, when there's not sufficient space, it will collapse items that's OverflowableStackPanel.CanOverflow, and set OverflowableStackPanel.OverflowItemCount to true, which is binded to specific BreadcrumbItem.OverflowItemCount.  When OverflowItemCount is >0, BreadcrumbItem set it's IsOverflowed to true, and thus TreeNodeViewModel.Selector.IsOveflowed is true.

Once the ViewModel is notified it's Overflowed, expanded items can be display in a themed 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 doesn't include SuggestBox, you have to combine the expander DropDownList, BreadcrumbTree and SuggestBox manually. 

 

 

Reusable controls

When developing the BreadcrumbTree I have developed a number of reusable controls, it may be useful when developing other controls:

Breadcrumb -

Image 4
Breadcrumb control is a List version of BreadcrumbTree, it's an updated from the original version found here

This Breadcrumb is easier to use and more featured (also with SuggestBox) than the BreadcrumbTree described in this article, all you need is to set the Parent/Value and Subentries path.  

<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}"
/>

However, it's less flexible than BreadcrumbTree, because most inner working is not controllable by user's ViewModel.


SuggestBoxBase / SuggestBox

Image 5
SuggestBox display a popup suggestion based on your input, suggestion are query from HierarchyHelper and SuggestSource. 
SuggestBoxBase is the base class of SuggestBox, it allows developer to handle the suggestions (by setting SuggestBoxBase.Suggestions) themselves.
<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 -  

Image 6

HotTrack is a re-styled Border that highlight itself when IsMouseOver, IsDragging and 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  -

Image 7

DropDown is a button that create a drop down menu, you can use any header or content.  DropDownList is a DropDown that contains a list.

<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> 

Reference 

History  

26/11/13 - Initial version.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Founder
Hong Kong Hong Kong

Comments and Discussions

 
-- There are no messages in this forum --