I decided I should start titling my posts with XAML instead of WPF as they are applicable regardless of whether you are developing in Windows 7 with WPF or Windows 8. I’m going to take the BreadCrumb design pattern sample and extend it to include the Live Filter design pattern.
The premise behind the Live Filter design pattern is pretty simple, show the user everything and then allow them to filter the results, immediately showing how the filter affects the results. As noted in the BreadCrumb post dense trees can easily become overwhelming to users. Providing a simple, easy to use live filter makes the experience significantly better.
In the above image you can see there we have a search text box with two filter parameters. As the user types into the search box the results are immediately filtered.
<Grid Grid.Row="0" Grid.ColumnSpan="3" Margin="5">
<xctk:WatermarkTextBox Margin="2" Watermark="Search" Text="{Binding SearchValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button Margin="5 0 5 0" Width="16" Height="16" HorizontalAlignment="Right" FontFamily="Segoe UI Symbol" Opacity=".75" ToolTip="Clear" Command="{Binding ClearCommand}">
<TextBlock Margin="0 -3.5 0 0" Foreground="#FF63A3B2">👽</TextBlock>
</Button>
</Grid>
<StackPanel Grid.Row="1" Grid.ColumnSpan="3" Margin="5" Orientation="Horizontal">
<CheckBox Margin="5" IsChecked="{Binding SearchProperty2, Mode=TwoWay}" Content="Include Property 2 In Search" />
<CheckBox Margin="5" IsChecked="{Binding SearchProperty3, Mode=TwoWay}" Content="Include Property 3 In Search" />
</StackPanel>
Above is the XAML for the search controls. Most of it is standard MVVM binding. The problem with standard MVVM binding is that for textbox it only updates the underlying view model property on lost focus. That doesn’t help us as we want to update the results in real-time. To fix this the UpdateSourceTrigger is set to PropertyChanged. This way as soon as the user types a letter the property is changed and we can perform a search. (I’m actually using the nuget package for the Extended WPF Toolkit to provide a watermark textbox but it works like a regular textbox).
<TreeView Grid.Row="2" Grid.RowSpan="2" Margin="5"
ItemsSource="{Binding ChildNodes}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Visibility" Value="{Binding Visibility, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel>
<TextBlock Text="{Binding Property1}" FontSize="18" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The first important part is the TreeViewItemContainerStyle. The nodes implement the properties IsExpanded and Visibility. This is extremely important. We don’t want to have to deal with the ItemContainerGenerator. This can be a major pain the rear and will make the code extremely sluggish. If we’re going to go with binding the nodes in the tree let’s take full advantage of it. The other important part is setting up the VirtualizingStackPanel values. The search is really rather trivial and usually works extremely fast. The slow part is updating the UI. We leave that to the masters at Microsoft by binding everything and letting them figure out how to render it. But we can help things out a bit. By setting up the VirtualizingStackPanel the control doesn’t have to render all the children in the tree. Now use this with some caution. Because the nodes will only be updated as the user drags the scroll bar you can get some choppy and sluggish operations as the control updates.
string searchValue;
public string SearchValue
{
get { return searchValue; }
set
{
if (SetProperty(ref searchValue, value))
{
PerformSearch();
}
}
}
bool searchProperty2 = false;
public bool SearchProperty2
{
get { return searchProperty2; }
set
{
if (SetProperty(ref searchProperty2, value))
{
PerformSearch();
}
}
}
bool searchProperty3 = false;
public bool SearchProperty3
{
get { return searchProperty3; }
set
{
if(SetProperty(ref searchProperty3, value))
{
PerformSearch();
}
}
}
These are the properties in the view model. We’re using BindableBase from Prism for the view model. Thus SetProperty returns a true if there was a change. We need to minimize extraneous calls to PerformSearch as much as possible.
private static object lockObj = new object();
private CancellationTokenSource CancellationTokenSource { get; set; }
private void PerformSearch()
{
//if we're doing a search then we probably have some new search term
clearCommand.RaiseCanExecuteChanged();
lock (lockObj)
{
if (CancellationTokenSource != null)
CancellationTokenSource.Cancel();
CancellationTokenSource = new CancellationTokenSource();
resultCount = 0;
}
Task.Run(() =>
{
DateTime now = DateTime.Now;
try
{
if (string.IsNullOrEmpty(SearchValue))
{
ClearAllNodes(ChildNodes, Visibility.Visible, CancellationTokenSource.Token);
return;
}
else
{
ClearAllNodes(ChildNodes, Visibility.Collapsed, CancellationTokenSource.Token);
}
var options = new ParallelOptions { CancellationToken = CancellationTokenSource.Token };
try
{
Parallel.ForEach(ChildNodes, options, (childNode) =>
{
PerformSearch(childNode, options);
});
}
catch (OperationCanceledException)
{
//Noop
}
}
finally
{
LastSearchTookInMilliseconds = (DateTime.Now - now).Milliseconds;
OnPropertyChanged(() => ResultCount);
}
}, CancellationTokenSource.Token);
}
private void PerformSearch(RandomValueNode childNode, ParallelOptions options)
{
if (options.CancellationToken.IsCancellationRequested)
return;
if (childNode.Property1.StartsWith(SearchValue) ||
(SearchProperty2 && childNode.Property2.StartsWith(SearchValue)) ||
(SearchProperty3 && childNode.Property3.StartsWith(SearchValue)))
{
Interlocked.Increment(ref resultCount);
childNode.IsExpanded = true;
childNode.Visibility = Visibility.Visible;
}
foreach (RandomValueNode node in childNode.Children)
{
if (options.CancellationToken.IsCancellationRequested)
break;
PerformSearch(node, options);
}
}
And finally we do the search. To make this as responsive as possible we need to cancel the search each time as quickly as possible. Thus I’m using a CancellationToken in the Task.Run and the Parallel.ForEach to do the search. Remember that it is only effective to spin off threads from the thread pool if you can give them enough work. In my case there will only be ten nodes but I give each of those plenty to do. I’m passing the options into the PerformSearch so that we kill the search as quick as possible. As the code recurses I want to kill everything. It may seem like overkill to check for options.CancellationToken.IsCancellationRequested at the top of the method and in the foreach, and it may very well be.
So this pattern is pretty straightforward, simply update live results as the user changes the filter parameters. The key here is taking into account the result set size and to stop any existing search quickly, really try and take advantage of all that the TPL provides. As you can see in the image at the top I can iterate over 700,000 nodes pretty quickly and the UI is still pretty responsive. Of course that is really more because of the Task.Run and using VirtualizingStackPanel.
There is, of course, another way to do this. If you find that things are moving much too sluggish the other alternative is to show a minimal amount of results (like when doing a Google search and it begins to suggest terms). Then in the background have a thread that starts to update the results.
I would encourage you to get the code from the “Download” button at the top.
Thanks for reading,
Brian