Thursday, September 7, 2017

Incremental Search

It is surprisingly easy to implement incremental searches using MVVM. This is a feature that re-evaluates the search results as the user enters the search parameters instead of waiting for the user to click the [Search] button. Obviously it's only a good feature when the search is extremely fast.

In this example, the full search results are already loaded and all we are doing is filtering that result set in memory. This project targets framework 4.5.2, but it will work with much earlier frameworks.

Start a new C# WPF project and call it Incremental Search.


Now make the XAML for MainWindow look like this...
<Window x:Class="IncrementalSearch.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:IncrementalSearch"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"></ColumnDefinition>
            <ColumnDefinition Width="auto"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Search:"></TextBlock>
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SearchTerm, UpdateSourceTrigger=PropertyChanged}" Width="100"></TextBox>
        <TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding SearchResults.Count, StringFormat='Match Count:{0:#}'}"></TextBlock>
        <DataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsReadOnly="True" ItemsSource="{Binding SearchResults}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding}" Width="*"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

The XAML to look at is the binding of the TextBox and DataGrid.
The TextBox.Text is bound to a property called SearchTerm and the UpdateSourceTrigger is set to PropertyChanged. This means the property's setter will be called whenever the user types in the text box (and a lot of other times too).

The DataGrid uses a property called SearchResults as its ItemsSource. Whenever our code calls NotifyPropertyChanged on SearchResults, the getter for SearchResults will be called and the DataGrid will be updated.

Let's look at the code behind now...

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;

namespace IncrementalSearch
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string _SearchTerm = "";
        public string SearchTerm
        {
            get { return _SearchTerm; }
            set
            {
                if (_SearchTerm != value)
                {
                    _SearchTerm = value;
                    NotifyPropertyChanged("SearchTerm");
                    NotifyPropertyChanged("SearchResults");
                }
            }
        }

        private List<String> WordList = new List<string> { "Here", "is", "a", "list", "of", "words", "we", "will", "search", "when", "the", "user", "enters", "a", "search", "term" };
        private List<String> _SearchResults;
        public List<String> SearchResults
        {
            get
            {
                _SearchResults = WordList.Where((w) => w.Contains(_SearchTerm)).ToList<String>();
                return _SearchResults;
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

The class implements INotifyPropertyChanged as all MVVM applications do. It's implemented in the code after the constructor. The rest of the code is concerned with implementing the SearchResults and the SearchTerm properties.

The setter for SearchTerm is called whenever a property of the bound textbox changes. The first thing we do is make sure the text actually changed and, if it did, we note the new value, call NotifyPropertyChanged for SearchTerm (which is technically not necessary), and then call NotifyPropertyChanged for SearchResults. This causes the getter for SearchResults to be called.

The getter for SearchResults applies a where filter to the full results list and returns a list of matching words. It stores them in a private field which is not actually used in this implementation.

Now let's look at the XAML again. There is a textblock after the textbox that displays the number of matches. The binding has a compound path that causes SearchResults to be evaluated a second time. This is not efficient. If this approach caused performance issues, you could bind to a new property that gets updated in the SearchResults getter.

Try running the solution now. You will initially see a list of words that gets filtered as you type in the Search textbox. It was pretty easy to implement.


No comments:

Post a Comment