Introduction

In order to build the code, you'll need Visual Studio 2008 SP1. To run the sample, .NET Framework 3.5 SP1 is required.

Recently, my team had a customer request for searching items in a WPF DataGrid control. Search should be done automatically as the user types in letters. We decided to create a more generic implementation that works similar to Firefox's search. Besides the incremental search, there are Next and Previous buttons. The result can be seen below.

Background

Before describing the solution, let's first provide some information about the sample infrastructure. The user interface for the application we are working on (and also for this sample) is based on the Model-View-ViewModel pattern. You can find more about this pattern by following the links at the bottom of this article.

ViewModel

All ViewModel classes, including the SearchViewModel, inherit from the base ViewModel class. This class implements only the INotifyPropertyChanged interface.

DelegateCommand

Another important ingredient of the user interface is DelegateCommand. DelegateCommand is a class implementing the WPF ICommand interface. It doesn't encapsulate any command code but uses a delegate (an Action<T> instance) to run some external code. The second, optional delegate (of type Predicate<T>) can be used to enable or disable a command, providing a nice feedback to the user.

///<summary>/// Represents an <seecref="ICommand"/>/// which runs an event handler when it is invoked.
///</summary>publicclass DelegateCommand : ICommand
{
privatereadonly Action<object> _executeAction;
privatereadonly Predicate<object> _canExecute;
///<summary>/// Raised when changes occur that affect whether or not the command should execute.
///</summary>///<remarks>/// The trick to integrate into WPF command manager found on:
/// http://joshsmithonwpf.wordpress.com/2008/06/17/
/// allowing-commandmanager-to-query-your-icommand-objects/
///</remarks>publicevent EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
///<summary>/// Creates a new instance of <seecref="DelegateCommand"/>/// and assigns the given action to it.
///</summary>///<paramname="executeAction">Event handler to assign to the command.</param>public DelegateCommand(Action<object> executeAction) : this(executeAction, null)
{
}
///<summary>/// Creates a new instance of <seecref="DelegateCommand"/>/// and assigns the given action and predicate to it.
///</summary>///<paramname="executeAction">Event handler to assign to the command.</param>///<paramname="canExecute">Predicate
/// to check whether the command can be executed.</param>public DelegateCommand(Action<object> executeAction, Predicate<object> canExecute)
{
_executeAction = executeAction;
_canExecute = canExecute;
}
///<summary>/// Defines the method that determines whether
/// the command can execute in its current state.
///</summary>///<returns>/// true if this command can be executed; otherwise, false.
///</returns>///<paramname="parameter">Data used by the command.
/// If the command does not require data
/// to be passed, this object can be set to null.</param>publicbool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute.Invoke(parameter);
}
///<summary>/// Defines the method to be called when
/// the command is invoked. The method will invoke the
/// attached event handler.
///</summary>///<paramname="parameter">Data used
/// by the command. If the command does not require data
/// to be passed, this object can be set to null.</param>publicvoid Execute(object parameter)
{
_executeAction.Invoke(parameter);
}
}

Search "component"

SearchViewModel

OK, now that we have covered some basic infrastructure, it's time to move to the actual search implementation. The search logic is implemented in SearchViewModel. Let's first see the code, and then we'll comment it:

The first thing you'll note is that this class is generic, which allows us to avoid type casting when matching items. Also, since we have several null checks, it's constrained as a 'class'.

The search logic is in the FindItem methods. The main FindItem method accepts a SearchType enumeration. There are three types of search. When a search is performed by typing characters to a TextBox, the current item is also matched. This search type is SearchType.Forward. When a search is performed by clicking on the Next and Previous buttons, the current item is skipped and we use SearchType.ForwardSkipCurrent and SearchType.Backward, respectively.

One of the most important classes in the WPF data binding model is the CollectionView class / ICollectionView interface. All item controls use an object of this type to store their ItemsSource property, so it is the best way to provide a search upon them. The problem with the ICollectionView is that it doesn't have a Count property or an indexer. Actually, we could use the CollectionView class instead, because it has a Count property, but the missing indexer prevents us from efficiently iterating backwards or forwards from a previous position. To allow iterating with the for loop, we added the SearchIndex property of type IList<T>. The SearchIndex is created in the constructor and at any time the CollectionView changes (items added or removed, sorting, etc.). It holds all the items from the CollectionView, is strongly typed and in the current sort order. This means that the search will work correctly even if you change the sort order.

The FindItem method itself doesn't know if a single item matches the search term. It loops through all the items and calls the itemMatch delegate (of type Func<T, string, bool>) for every item. This delegate is the second parameter of the SearchViewModel constructor. Having the match as a delegate allows us to have custom match logic whenever we use this component and for any custom item type.

SearchUserControl

SearchViewModel also has several properties that are data bound to SearchUserControl.

The SearchTerm is bound to a TextBox. Whenever the user types in a character, a search is performed. The NoResults property is bound to a No resultsTextBlock. The TextBlock is displayed when NoResults is true. PreviousCommand and NextCommand are bound to Previous and Next buttons. Commands are instances of DelegateCommand. Whenever the user clicks on one of these buttons, a forward or backward search is performed. When SearchTerm is empty or there are no results, both buttons are disabled. The disabled state is controlled by the second Predicate<T> parameter of the DelegateCommand constructor.

Using the search component

ProductsViewModel

Now that we have a component ready, it's time to see how it can be used. The following is the ProductsViewModel class. It's used to data bind a list of Product objects (you can find the Product class in the accompanied source code) to ProductsPage.

SearchViewModel is also exposed as a property of ProductsViewModel. It's initialized in the ProductsViewModel constructor with a list of Products and an ItemMatch method delegate. ItemMatch matches Products whose Code or Barcode starts with the search term, or whose Name contains the search term. You could write any custom logic here, i.e., to match Products whose quantity is greater than the search term, to match the Name with multiple words in the search term, etc.

ProductsPage has only a DataGrid and a SearchUserControl. Both are bound to ProductsViewModel. In order for the search to work, the IsSynchronizedWithCurrentItem property of the DataGrid (or any other items control) needs to be set to true, so that the control picks up whenever the CurrentItem is changed.

You'll also notice that the DataGrid is scrolled to display the current item which is not the default behavior of DataGrid. The Infrastructure.DataGridExtenders class is in charge of auto scrolling. It's a variation of the solution found here: Autoscroll ListBox in WPF. It could be made more generic to support all WPF item controls.

PersonsViewModel and PersonsPage from the sample application are similar. PersonsPage uses a ListBox as an item control to reflect the fact that the search is independent of the controls.

Points of Interest

The SearchUserControl can be further improved with shortcut keys, restyled to a floating transparent window, hidden until the user tries to type in something in the grid, etc.

I hope this article will be helpful to you, to understand the power and elegance behind the M-V-VM pattern.