Click here to Skip to main content
Email Password   helpLost your password?

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.

Sample application

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.

/// <summary>
/// Base class for all view models (from the Model-View-ViewModel pattern).
/// </summary>
public abstract class ViewModel : INotifyPropertyChanged
{
    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Raises the <see cref="PropertyChanged"/> event.
    /// </summary>
    /// <param name="propertyName">Name of the property whose value is changed.</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

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 <see cref="ICommand"/>
/// which runs an event handler when it is invoked.
/// </summary>
public class DelegateCommand : ICommand
{
    private readonly Action<object> _executeAction;
    private readonly 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>
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    /// <summary>
    /// Creates a new instance of <see cref="DelegateCommand"/>
    /// and assigns the given action to it.
    /// </summary>
    /// <param name="executeAction">Event handler to assign to the command.</param>
    public DelegateCommand(Action<object> executeAction) : this(executeAction, null)
    {
    }

    /// <summary>
    /// Creates a new instance of <see cref="DelegateCommand"/>
    /// and assigns the given action and predicate to it.
    /// </summary>
    /// <param name="executeAction">Event handler to assign to the command.</param>
    /// <param name="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>
    /// <param name="parameter">Data used by the command.
    /// If the command does not require data 
    /// to be passed, this object can be set to null.</param>
    public bool 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>
    /// <param name="parameter">Data used
    /// by the command. If the command does not require data 
    /// to be passed, this object can be set to null.</param>
    public void 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:

internal class SearchViewModel<T> : ViewModel where T : class
{
    private enum SearchType
    {
        Forward,
        ForwardSkipCurrent,
        Backward
    }

    private readonly Func<T, string, bool> _itemMatch;
    private bool _noResults;
    private string _searchTerm = String.Empty;

    /// <summary>
    /// Creates a new instance of <see cref="SearchViewModel{T}"/> class.
    /// </summary>
    /// <param name="collectionView">Collection to search for items.</param>
    /// <param name="itemMatch">Delegate to perform item matching.</param>
    public SearchViewModel(ICollectionView collectionView, 
                           Func<T, string, bool> itemMatch)
    {
        CollectionView = collectionView;
        CollectionView.CollectionChanged += (sender, e) => RebuildSearchIndex();
        RebuildSearchIndex();

        _itemMatch = itemMatch;

        NextCommand = new DelegateCommand(
            p => FindItem(SearchType.ForwardSkipCurrent), 
            x => !String.IsNullOrEmpty(SearchTerm) && !NoResults);
        PreviousCommand = new DelegateCommand(
            p => FindItem(SearchType.Backward), 
            x => !String.IsNullOrEmpty(SearchTerm) && !NoResults);
    }

    protected ICollectionView CollectionView { get; private set; }
    protected IList<T> SearchIndex { get; private set; }

    public ICommand NextCommand { get; private set; }
    public ICommand PreviousCommand { get; private set; }

    public bool NoResults
    {
        get { return _noResults; }
        set
        {
            if (_noResults == value) return;
            _noResults = value;
            OnPropertyChanged("NoResults");
        }
    }

    public string SearchTerm
    {
        get { return _searchTerm; }
        set
        {
            if (_searchTerm == value) return;
            _searchTerm = value;
            OnPropertyChanged("SearchTerm");
            NoResults = false;
            FindItem(SearchType.Forward);
        }
    }

    private void FindItem(SearchType type)
    {
        if (String.IsNullOrEmpty(SearchTerm)) return;

        T item;
        switch (type)
        {
            case SearchType.Forward:
                // Search from the current position
                // to end and loop from start if nothing found
                item = FindItem(CollectionView.CurrentPosition, SearchIndex.Count - 1) ??
                       FindItem(0, CollectionView.CurrentPosition);
                break;
            case SearchType.ForwardSkipCurrent:
                // Search from the next item position
                // to end and loop from start if nothing found
                item = FindItem(CollectionView.CurrentPosition + 1, SearchIndex.Count - 1) ??
                       FindItem(0, CollectionView.CurrentPosition);
                break;
            case SearchType.Backward:
                // Search backwards from the current position
                // to start and loop from end if nothing found
                item = FindItemReverse(CollectionView.CurrentPosition - 1, 0) ??
                       FindItemReverse(SearchIndex.Count - 1, 
                       CollectionView.CurrentPosition);
                break;
            default:
                throw new ArgumentOutOfRangeException("type");
        }

        if (item == null)
            NoResults = true;
        else
            CollectionView.MoveCurrentTo(item);
    }

    private T FindItem(int startIndex, int endIndex)
    {
        for (var i = startIndex; i <= endIndex; i++)
        {
            if (_itemMatch(SearchIndex[i], SearchTerm))
                return SearchIndex[i];
        }
        return null;
    }

    private T FindItemReverse(int startIndex, int endIndex)
    {
        for (var i = startIndex; i >= endIndex; i--)
        {
            if (_itemMatch(SearchIndex[i], SearchTerm))
                return SearchIndex[i];
        }
        return null;
    }

    private void RebuildSearchIndex()
    {
        SearchIndex = new List<T>();

        foreach (var item in CollectionView)
        {
            SearchIndex.Add((T) item);
        }
    }
}

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.

<UserControl x:Class="CodeMind.FirefoxLikeSearch.Views.SearchUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:ClassModifier="internal">
    
    <StackPanel Orientation="Horizontal">
        
        <TextBox
            Text="{Binding Path=SearchTerm, UpdateSourceTrigger=PropertyChanged}"
            Width="200" />
        
        <Button
            Command="{Binding Path=NextCommand}"
            Margin="5,0,0,0" 
            Width="70">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Next" />
                <Image Width="10" Source="/Resources/arrow_down.png" />
            </StackPanel>
        </Button>
        
        <Button
            Command="{Binding Path=PreviousCommand}"
            Margin="5,0,0,0"
            Width="70">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Previous" />
                <Image Width="10" Source="/Resources/arrow_up.png" />
            </StackPanel>
        </Button>
            
        <TextBlock
            FontWeight="Bold"
            Foreground="Red"
            Margin="5,0,0,0"
            Text="No results"
            Visibility="{Binding Path=NoResults, 
                        Converter={StaticResource booleanToVisibilityConverter}}"
            VerticalAlignment="Center" />
        
    </StackPanel>
    
</UserControl>

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 results TextBlock. 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.

internal class ProductsViewModel : ViewModel
{
    public ProductsViewModel()
    {
        Products = CollectionViewSource.GetDefaultView(Product.GetTestProducts());
        Search = new SearchViewModel<Product>(Products, ItemMatch);
    }

    public ICollectionView Products { get; private set; }
    public SearchViewModel<Product> Search { get; private set; }

    private static bool ItemMatch(Product item, string searchTerm)
    {
        searchTerm = searchTerm.ToLower();

        return item.Code.ToLower().StartsWith(searchTerm) ||
               item.Barcode.ToLower().StartsWith(searchTerm) ||
               item.Name.ToLower().Contains(searchTerm);
    }
}

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

<Page x:Class="CodeMind.FirefoxLikeSearch.Views.ProductsPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:dg="http://schemas.microsoft.com/wpf/2008/toolkit"
    xmlns:Views="clr-namespace:CodeMind.FirefoxLikeSearch.Views"
    xmlns:Infrastructure="clr-namespace:CodeMind.FirefoxLikeSearch.Infrastructure"
    Title="ProductsPage"
    x:ClassModifier="internal">
    <Grid>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>

        <dg:DataGrid 
            AlternatingRowBackground="#FFF2F5F1" 
            AutoGenerateColumns="False"
            Grid.Row="0" 
            GridLinesVisibility="None" 
            Infrastructure:DataGridExtenders.IsAutoScroll="True"
            IsReadOnly="True"
            IsSynchronizedWithCurrentItem="True"
            ItemsSource="{Binding Path=Products}"
            Margin="5,5,5,5"
            RowHeight="20" 
            SelectionMode="Single"
            VerticalAlignment="Stretch">

            <dg:DataGrid.Columns>
                <dg:DataGridTextColumn Header="Code" Binding="{Binding Path=Code}"/>
                <dg:DataGridTextColumn Header="Barcode" Binding="{Binding Path=Barcode}"/>
                <dg:DataGridTextColumn Header="Name" Binding="{Binding Path=Name}"/>
                <dg:DataGridTextColumn Header="Price" Binding="{Binding Path=Price}"/>
                <dg:DataGridTextColumn Header="Quantity" Binding="{Binding Path=Quantity}"/>
            </dg:DataGrid.Columns>

        </dg:DataGrid>

        <Views:SearchUserControl
            DataContext="{Binding Path=Search}"
            Grid.Row="1"
            Margin="5,0,5,5" />
    </Grid>
</Page>

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.

You can find more about this pattern here:

Other useful links:

And, of course, all WPF articles from CodeProject gurus:

History

02-01-2009

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
Question"Next" by Cursor down?
ChrDressler
8:45 21 Nov '09  
Hi,

thanx for this fine article!

I want to fire the NextCommand, if the Key.Down is pressed.
I try'd this, but NextCommand is not "in the scope":
   
private void TextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Down)
{
NextCommand.Execute(null);
}
}


Can you help me?

Best regards from Dresden (Germany)
AnswerRe: "Next" by Cursor down?
Miroslav Popovic
9:18 21 Nov '09  
Hello,

You're welcome. Yes, the NextCommand is not in scope, but the button is. Assign a name to the next button, like x:Name="nextButton", and then use it in code behind to access its Command property like this: nextButton.Command.Execute(null);

This would be the easy way, but it will violate the no-code-behind principle that MVVM pattern is all about. The better option would be to define InputBindings for keyboard shortcuts and mouse gestures, but there's a problem with that too. If you write the code like this:

<TextBox
Text="{Binding Path=SearchTerm, UpdateSourceTrigger=PropertyChanged}"
Width="200">
<TextBox.InputBindings>
<KeyBinding Command="{Binding NextCommand}" Key="Down" />
</TextBox.InputBindings>
</TextBox>

you'll get the following error: A 'Binding' cannot be set on the 'Command' property of type 'KeyBinding'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.

Unfortunately, there is no support for InputBindings Command binding in current WPF version. However, this should be possible in the upcoming WPF 4, if I remember correctly.

For now, there are some workarounds you can use: http://stackoverflow.com/questions/612966/keyboard-events-in-a-wpf-mvvm-application[^].

Also, some existing MVVM frameworks have workarounds implemented, check them out here: http://www.japf.fr/2009/10/a-quick-tour-of-existing-mvvm-frameworks/[^]

Long story short, if you're OK with having some code in code-behind, just ignore the previous text and call nextButton.Command.Execute(null) in TextBox.PreviewKeyDown handler - not KeyDown, since Up, Down, Left and Right are special keys handled by the TextBox itself.

Regards,
Miroslav
AnswerRe: "Next" by Cursor down?
ChrDressler
10:22 22 Nov '09  
Hi Miroslav,

thanx, your workaround with "nextButton.Command..." works fine.

I have tested the "InputBinding Command..." in VS2010 and .NET 4.0 and in deed: it works! Big Grin

Regards,
Christoph
GeneralRe: "Next" by Cursor down?
Miroslav Popovic
10:24 22 Nov '09  
Glad I could help Smile

Cheers,
Miroslav
GeneralAwesome Work
David Roh
11:54 30 Apr '09  
Thanks for sharing!
David Roh
GeneralRe: Awesome Work
Miroslav Popovic
12:08 30 Apr '09  
David Roh wrote:
Thanks for sharing!
David Roh


You're welcome. Glad you like it.
Miroslav
GeneralExcellent Article
Rick Dean
2:21 14 Feb '09  
Trying to get a handle on M-V-VM. Your work really helped a lot-- Smile simple, straightforward. Thanks for your efforts.
GeneralRe: Excellent Article
Miroslav Popovic
2:23 14 Feb '09  
Glad I could help Smile

Regards,
Miroslav
GeneralSvaka čast !
Branko.P
1:33 2 Feb '09  
I još jednom, svaka čast! Smile
GeneralRe: Svaka čast !
Miroslav Popovic
1:52 2 Feb '09  
Hvala Branko.
Thank you Branko.
GeneralGoob article ... could be great.
Colin Eberhardt
22:13 1 Feb '09  
Hi,

This is a good article that provides some useful functionality. A few things that I would really like to see explained ...

You have a class called 'ViewModel' but what is it about this class that makes it a view model?
What are the alternatives to the View Model approach?
What benefits has MVVM given you?

Finally, Your code requires a concrete view model for your specific business object. Can you generalize this so that any business object could be searched? This would make it a much more useful little component.

Do the above .. and you get my 5 Wink

Don't get me wrong, it is a good article and well written. I just think that with a little more effort it could be excellent. Keep up the good (or excellent) work!

Regards,
Colin E.
GeneralRe: Goob article ... could be great.
Miroslav Popovic
1:51 2 Feb '09  
Hi Colin,

Colin Eberhardt wrote:
You have a class called 'ViewModel' but what is it about this class that makes it a view model?
What are the alternatives to the View Model approach?
What benefits has MVVM given you?


Actually, I didn't want to focus that much on what the M-V-VM is, but more on how to use it in some circumstances. There are far better resources explaining M-V-VM than I would ever write, like Josh's MSDN article from the links above.

Colin Eberhardt wrote:
Finally, Your code requires a concrete view model for your specific business object. Can you generalize this so that any business object could be searched? This would make it a much more useful little component.


The code doesn't require that you have a concrete view model. You'll see that the SearchViewModel class has a generic constraint on object, not on ViewModel inherited class, so you could use SearchViewModel/SearchUserControl in some other context (like a regular non-mvvm application). Although, in that case it would be better to collapse the view model and control to a single control file, and have the CollectionView and SearchTerm as DependencyProperties of that control... But, that's already a case for another article Smile. This one focuses more on M-V-VM.

Anyway, thank you very much for your useful suggestions. I'll see to update the article sometime and made it more clear on some points.

Regards,
Miroslav

GeneralGreat Article
Nedim Sabic
8:36 1 Feb '09  
Odlican clanak Miroslave.
Great article Miroslav.

=)
GeneralRe: Great Article
Miroslav Popovic
8:39 1 Feb '09  
Hvala druze Nedime Smile
Thank you, mate.
GeneralGood job
Sacha Barber
7:46 1 Feb '09  
I can see the Josh touch in here like the RelayCommand which you are calling DelegateCommand, its a nice article, well done sir.

Sacha Barber
  • Microsoft Visual C# MVP 2008
  • Codeproject MVP 2008
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralRe: Good job
Miroslav Popovic
7:58 1 Feb '09  
Thank you!

Yes, DelegateCommand is the name I picked up from somewhere when I first started learning M-V-VM. It's code was updated several times since then, and it's quite similar to Josh's RelayCommand.

And please don't underestimate the influence from you and other WPF masters Smile
GeneralRe: Good job
Sacha Barber
8:05 1 Feb '09  
Its a good article like I say, it doesn't matter where you get information from as long as you get it.

Sacha Barber
  • Microsoft Visual C# MVP 2008
  • Codeproject MVP 2008
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net


Last Updated 1 Feb 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010