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:
<uc:BreadcrumbTree x:Name="btree" Height="30" ItemsSource="{Binding Entries.All}">
<uc:BreadcrumbTree.ItemContainerStyle>
<Style TargetType="{x:Type uc:BreadcrumbTreeItem}"
BasedOn="{StaticResource BreadcrumbTreeItemStyle}" >
-->
<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}" />
-->
<Setter Property="IsOverflowed" Value="{Binding IsOverflowed, Mode=OneWayToSource}" />
</Style>
</uc:BreadcrumbTree.ItemContainerStyle>
<uc:BreadcrumbTree.MenuItemTemplate>
<DataTemplate>
-->
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</uc:BreadcrumbTree.MenuItemTemplate>
<uc:BreadcrumbTree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Entries.All}">
-->
<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
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).
public class TreeViewModel : INotifyPropertyChanged
{
public TreeViewModel()
{
...
Entries = new EntriesHelper<TreeNodeViewModel>();
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(() => { )));
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)
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
- The header is depended on the BreadcrumbTree.ItemTemplate.
- 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
- 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 -
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.
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 -
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 -
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.