Introduction
In this article, I am going to share my experience in creating an AutoSuggest
/AutoComplete
control in WPF which follows the MVVM pattern.
Why Do We Need an AutoSuggest Control?
The dynamic development of the web browsers highlighted the true benefits from the AutoSuggest
control’s functionality for the first time- users not only needed to browse, they needed help, they needed instant feedback in a form of a list of suggestions after typing part of a word or phrase. On the other hand, the amount of the presented data made the simple task of choosing from a list without help from an application tedious. And finally, the shortened find-and-choose time in touch screen interfaces where drop-downs are difficult to use made the AutoSuggest
controls irreplaceable.
Background
I am currently involved in a WPF desktop application development which manages contracts.
In order to improve the user’s experience and speed up form entry, I decided to use controls in the form of TextBox
es where text is entered by the keyboard. Some of the controls although require picking from a list of choices and the most natural way to implement those without using a ComboBox
is to use an AutoSuggest
/AutoComplete
control.
I did try to find a ready out of the box user control but unfortunately almost all of the controls I found based their implementation on the Microsoft’s ComboBox
and just added simple implementation for filtering the items. After not finding what I was looking for, I started talking about the best way of building such an AutoSuggest
/AutoComplete
control to Orlin Petrov, who is a good friend of mine and is a really outstanding software developer. And this is how we decided to work on it together- him taking the leading architectural role and me doing most of the implementation. This is how KOControls
was born.
The code I am providing contains a package of the KOControls
library which, as of this writing, consists of generic WPF utilities, implementation of AutoSuggest
/AutoComplete
control in WPF and a sample project demonstration on how to use it.
Implementation and Usage of the Code
User Requirements
We started with creating a list of high level requirements that our control should meet:
- The control must be MVVM compliant and the
ViewModel
should be completely functional without user interface
- It should be able to hook up to any
TextBox
and allow AutoSuggest
/AutoComplete
functionality
- The control should have an option allowing submission of “free text” and upon confirmation the “free text” to be converted to a valid value
- It should have a way to inject commands in the suggestion’s window (this is useful when you want to allow the users to edit or create new suggestions from within the suggestion’s control)
- It should work with copy/paste
- The control presenting the suggestion should be WPF
DataGrid
or ListView
- The control's functionality should be controlled through properties of the
ViewModel
. (Note that the control would work only if its DataContext
is of a particular ViewModel
type)
- It should have an option to insert a filtering algorithm. This will allow third party users to implement an algorithm which fetches the suggested values from a webservice or from a database, to do cleverer filtering overcoming spelling mistakes and auto-correction, etc. A default implementation of the filtering algorithm should be provided.
- The control should support walking up and down the suggestions with the arrow keys and that should be done without losing
TextBox
focus
- It should support the following options for “
Cancel
” and “Select
” behavior:
- Space may act as selection command or exit command. If it acts as a selection command, it should only select an item if it is the last item in the suggestions list and if the option to allow “free text” values is off.
- Tab may act as a “
Select
” or a “Cancel
” command. Lost focus is also considered Tab in that sense.
- Enter may act as a “
Select
” or a “Cancel
” command.
- The arrow keys may act as a “
Select
” command.
- It should support delay in milliseconds before invoking the filtering algorithm. This is useful if the filtering algorithm is heavy and you do not want to invoke it every time the user types a character but want the user to pause and slow down before you invoke the algorithm.
- The control should have an option to allow or disallow empty values. Also by default, an empty value is the “
null
” value but users may supply a different empty value.
- Should have an
AutoComplete
feature which highlights the completed text after the point where user stopped typing.
There is a screen shot of the AutoSuggestControl
in action below:
Project Structure
KO.Controls.Core
– Common non UI utilities and core classes and interfaces
KO.Controls.GUI.Core
– Common UI utilities like ICommand
implementations, converters and utility extension method
KO.Controls.GUI
– User controls of theKO.Controls
library UI and ViewModels
. Currently it has the AutoSuggest
user control implementation.
KO.Controls.AutoSuggestTest
– Test project demonstrating the usage and helping us with testing the AutoSuggestControl
s’ functionality (there will be others based on the current control).
KO.Controls.Samples.Core
– Common library which contains dummy test data and is to be shared with other Test projects.
KOControls.GUI.Tests
- A nice to have project.
Overview of the Class Structure and Implementation
Before I start, I want to give credit to Orlin Petrov’s whose guidance and input on building this nice solution were invaluable.
After discussing which existing .NET controls we should extend/reuse to suite our purpose, we decided on using the WPF TextBox
, Selector
(ListView
, DataG
rid
, etc.) and popup controls, because they give the most flexibility. The AutoSuggestControl
is a user control which you put inside a popup control. The AutoSuggestControl
contains a selector control, hooks onto any TextBox
, listens to the user’s input, manipulates the selector control’s items and drives the AutoSuggestViewModel
.
Below is the view of the AutoSuggestControl
’s default templates:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d"
xmlns:GUI="clr-namespace:KOControls.GUI"
xmlns:Core="clr-namespace:KOControls.GUI.Core;assembly=KOControls.GUI.Core">
-->
<ControlTemplate x:Key="AutoSuggestControl_Default_SuggestionsTemplate">
<DataGrid x:Name="PART_Selector"
ItemsSource="{Binding Suggestions}" SelectionMode="Single">
</DataGrid>
</ControlTemplate>
-->
<ControlTemplate x:Key="AutoSuggestControl_Default_CommandsTemplate">
<ItemsControl IsTabStop="False" ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="25"
Command="{Binding}"
Content="{Binding Header}">
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel x:Name="buttonMenuPanel"
Orientation="Horizontal"
VerticalAlignment="Top">
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ControlTemplate>
-->
<ControlTemplate x:Key="AutoSuggestControl_DefaultTemplate"
TargetType="{x:Type GUI:AutoSuggestControl}">
<Border
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Control x:Name="_suggestionsContentPresenter"
Grid.Row="0"
Template="{TemplateBinding
SuggestionsTemplate}"
DataContext="{Binding Suggestions}"/>
<Control x:Name="_commandsContentPresenter"
Grid.Row="1"
Template="{TemplateBinding CommandsTemplate}"
DataContext="{Binding Commands}"/>
</Grid>
</Border>
</ControlTemplate>
</ResourceDictionary>
The view consists of a grid with two rows. Inside the first row, there is a suggestions’ presenter template which has a default implementation of a DataGrid
bound to the suggestions collection in the ViewModel
. The second row implies the commands’ template which has a default implementation listing the commands as buttons. The custom commands allow the user to add “new”, “edit” and “details” functionality for any of the suggestions given by the AutoSuggestControl
.
The AutoSuggestControl.cs class contains logic which drives and keeps the ViewModel
, the Popup
, the TextBox
and the Selector
(DataGrid
or ListView
) which presents the suggestions in sync. The AutoSuggestControl
only works with the AutoSuggestViewModel
and you cannot give it any other DataContext
.
The AutoSuggestViewModel
contains all the business logic for finding suggestions as well as all the options and commands which the user has injected. The actual finding of the suggestions is done by inserting a class which implements the simple ISelector
Interface shown below:
public interface ISelector
{
IEnumerable Select(object filter);
}
The filter is the entered text in the TextBox
and the IEnumerable
are the found suggestions. We provide a default implementation for the ISelector
interface which works with a collection of cities and returns the matching cities based on the filter text.
There are two other important dependency properties of the AutoSuggestViewModel
: the Suggestion
and the SuggestionPreview
properties. The Suggestion
property represents the currently selected Suggestion
. The SuggestionPreview
property represents the suggestion the user is about to select (has focus on), but has not yet selected.
How to Use the Control in your Applications
For a comprehensive understanding of the usage of the AutoSuggestControl
, I advise you to examine the latest source code at http://code.google.com/p/kocontrols/downloads/list.
I have described three cases below which are very easy to implement using the AutoSuggestControl
:
- You have a list of cities and you would like to provide the user with a simple and fast way of selecting a city by typing just the first few letters of the city.
- You have a list of cities and you would like to provide the user with simple and fast way of selecting a city by typing the first few letters. If the city does not exist in your list, you would like to add it automatically or to pop up an entry window allowing the user to add the city in the list.
- You have a list of cities and you would like to provide the user with simple and fast way of selecting a city by typing the first few letters. If the city does not exist in your list, you would like to invoke the “Add New City” window where the new city can be added. If you want to edit any of the existing cities you would like to quickly invoke the “Edit City” window where the cities can be edited.
I’ll cover in detail how to achieve the first case.
The Model
Create a class called “city
” which has “Name
” and “Country
” properties as below:
private string name = "";
private Country country = null;
public string Name { get { return name; } set { if(name != value) { name = value;} } }
public Country Country { get { return country; } set { if(country != value)
{ country = value; } } }
Create a class called Country
which has a name
property as below:
private string name = "";
public string Name { get { return name; } set { if (name != value) { name = value;} } }
Create AutoSuggestConsumerViewModel
which has a Collection of cities and a AutoSuggestViewModel
property as below:
public class AutoSuggestConsumerViewModelBase : DependencyObject
{
public AutoSuggestViewModel AutoSuggestVM { get; protected set; }
public IList<City> AllCities { get; set; }
public AutoSuggestConsumerViewModelBase()
{
AllCities = TestDataService.GetCities();
IValueConverter valueConverter =
new ValueConverter(x => x == null ? "" : ((City)x).Name);
ISelector selector = new AutoSuggestViewModel.DefaultSelector
(valueConverter, AllCities);
AutoSuggestVM = new AutoSuggestViewModel(selector, valueConverter);
}
}
Create a view as a UserControl
which contains the following XAML snippet:
...
<Label Grid.Column="0" Content="City:" HorizontalAlignment="Right" />
<TextBox Grid.Column="1" Width="140" Height="22" Padding="0, 3, 0, 0"
VerticalAlignment="Top"
x:Name="_cityTextBox"/>
<GUI:Popup x:Name="_popup" Placement="Bottom"
PlacementTarget="{Binding ElementName=_cityTextBox}">
<GUI:AutoSuggestControl x:Name="autoSuggest" Focusable="False"
OwnerPopup="{Binding ElementName=_popup}"
TargetTextBox="{Binding ElementName=_cityTextBox,
Mode=OneTime}"
FrameworkElement.DataContext="{Binding AutoSuggestVM}"
TaboutTrigger="All"
ConfirmTrigger="SpaceTabArrows">
<GUI:AutoSuggestControl.SuggestionsTemplate>
<ControlTemplate>
<DataGrid x:Name="PART_Selector"
CanUserReorderColumns="False"
CanUserSortColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeColumns="True"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="None">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
<DataGridTextColumn
Header="Country Name"
Binding="{Binding Country.Name}" />
</DataGrid.Columns>
</DataGrid>
</ControlTemplate>
</GUI:AutoSuggestControl.SuggestionsTemplate>
</GUI:AutoSuggestControl>
</GUI:Popup>
…
The above XAML creates a label
, a TextBox
and a popup
with AutoSuggestControl
functionality (i.e., when the user types the first few letters of the city, the popup window opens with the available options).
The most recent source code for the KOControls
library and information on implementation of the AutoSuggestControl
for the WPF DataGrid
could be found at http://code.google.com/p/kocontrols.
The attached files contain a binary with a text application demonstrating how the AutoSuggestControl
works as well as the latest source (as of this writing) code of KOControls
.
I hope you’ve enjoyed the article. If you are interested in this control, we strongly recommend you to download the source code and play with it. The source code is very simple to figure out.
I will very much appreciate your comments and suggestions and especially ideas and feedback on how to improve the user control.
Ah and if you find a bug, please definitely let me know.
History
- 30th November, 2011: Initial version