Introduction
This article describes a WPF AJAX style ComboBox custom control. The control demonstrates implementing custom RoutedEvents. There is also a section on coding RoutedEvents using VB.NET.
I've have been using AJAX enabled DropDownList controls in my ASP.NET projects for quite some time. You'll find these cool controls being utilized all over the web. Basically, the controls dynamically load themselves based on some user input, as opposed to standard DropDownList controls that are populated by the developer in markup or code and typically have a fixed item set.
The main reason a custom control is necessary is that the standard behavior of the ComboBox is to generate a SelectionChanged
event every time the selection changes. In our case, this event wouldn't work because users expect to use their arrow keys to navigate through the records before selecting them. That would raise the SelectionChanged
event every time they pressed the up or down arrow key. Our custom control changes the ComboBox by adding two custom RoutedEvents to enable the required behavior.
I have used this control over the Internet to get data through a VPN tunnel, and also from a publicly exposed Web Service. The source database had over 100,000 rows in the custom table. The control worked great.
Functional Requirements
- Allow user to enter some text, press <ENTER>, and the ComboBox will populate itself.
- Once the item list is visible, allow user to enter text, and the ComboBox will find the matching rows.
- Allow users to use the arrow keys to navigate through the displayed records.
- Allow user to press <ESC> to clear the ComboBox.
- Allow user to select a record with the mouse, or by pressing the <ENTER> or <TAB> keys.
- Author control so that developers can use any data source.
- Author control so that developers can customize the display of data.
Demo Program

Running the Program
The above program has two sections: Default ComboBox and AJAX Style ComboBox.
The top section demonstrates the default behavior of a standard WPF ComboBox when its item selection is changed. Open the program and use the arrow keys to change the selection. The TextBlock on the right changes each time the arrow keys or mouse is used to change the selection.
The bottom section demonstrates our AJAX style ComboBox. Requirements of our program include, user should be able to enter some text and press <ENTER> which will cause the ComboBox to be filled with data. Once populated, the user can either navigate records with the arrow keys or the mouse. If using the keyboard, the user will navigate to the record they want using the arrow keys, and press <ENTER> or <TAB> to select the desired record. If using the mouse, just click on the record they want to select.
After running the program, you can see the difference in behavior between the two ComboBoxes.
Again, this is the main reason we need a custom control, because you would not want to go back to the database for a record every time the user changes selection with the arrow keys. Also, we need an event to listen for to load records when required.
AJAX Style Custom Control Code
When writing a custom control, there are two approaches. One is to add a new Custom Control to your project. Using this method, you will author your code and control template. The approach we are using is to add a new class to the project and simply inherit from ComboBox
. In this case, we only need to author the code and not the control template. This control will use the WPF ComboBox
default control template, with no action required on our part.
Imports System.ComponentModel
Public Class AjaxStyleComboBox
Inherits ComboBox
#Region " Private Declarations "
Private _bolKeyBoardActivity As Boolean = False
Private _intMinimunSearchTextLength As Integer = 1
#End Region
#Region " Properties "
<Description("What is the minimum number of characters" & _
"the user must enter before they are allowed to request" & _
" a record search"), Category("Custom"), DefaultValue(1)> _
Public Property MinimunSearchTextLength() As Integer
Get
Return _intMinimunSearchTextLength
End Get
Set(ByVal Value As Integer)
_intMinimunSearchTextLength = Value
End Set
End Property
#End Region
#Region " RoutedEvents "
Public Shared ReadOnly LoadItemsSourceRoutedEvent As RoutedEvent _
= EventManager.RegisterRoutedEvent( _
"LoadItemsSource", RoutingStrategy.Bubble, _
GetType(LoadItemsSourceRoutedEventHandler), _
GetType(AjaxStyleComboBox))
Public Shared ReadOnly RecordSelectedRoutedEvent As RoutedEvent _
= EventManager.RegisterRoutedEvent( _
"RecordSelected", RoutingStrategy.Bubble, _
GetType(RecordSelectedRoutedEventHandler), _
GetType(AjaxStyleComboBox))
Public Custom Event LoadItemsSource As _
LoadItemsSourceRoutedEventHandler
AddHandler(ByVal value As _
LoadItemsSourceRoutedEventHandler)
Me.AddHandler( _
AjaxStyleComboBox.LoadItemsSourceRoutedEvent, value)
End AddHandler
RemoveHandler(ByVal value As _
LoadItemsSourceRoutedEventHandler)
Me.RemoveHandler( _
AjaxStyleComboBox.LoadItemsSourceRoutedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, _
ByVal e As LoadItemsSourceRoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
Public Custom Event RecordSelected As _
RecordSelectedRoutedEventHandler
AddHandler(ByVal value As _
RecordSelectedRoutedEventHandler)
Me.AddHandler( _
AjaxStyleComboBox.RecordSelectedRoutedEvent, value)
End AddHandler
RemoveHandler(ByVal value As _
RecordSelectedRoutedEventHandler)
Me.RemoveHandler( _
AjaxStyleComboBox.RecordSelectedRoutedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, _
ByVal e As RecordSelectedRoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
#End Region
#Region " Methods "
Protected Overrides Sub OnPreviewKeyDown(ByVal e As _
System.Windows.Input.KeyEventArgs)
_bolKeyBoardActivity = True
If Me.IsDropDownOpen = True _
AndAlso Me.SelectedIndex > -1 _
AndAlso (e.Key = Key.Enter _
OrElse e.Key = Key.Tab) Then
OnRecordSelected(New RecordSelectedRoutedEventArgs( _
AjaxStyleComboBox.RecordSelectedRoutedEvent, Me, _
Me.SelectedValue))
Me.IsDropDownOpen = False
If Not e.Key = Key.Tab Then
e.Handled = True
End If
ElseIf e.Key = Key.Escape Then
Me.ItemsSource = Nothing
Me.Text = String.Empty
Me.IsDropDownOpen = False
e.Handled = True
Else
MyBase.OnPreviewKeyDown(e)
End If
End Sub
Protected Overrides Sub OnKeyDown(ByVal e As _
System.Windows.Input.KeyEventArgs)
_bolKeyBoardActivity = True
If Me.Text.Trim.Length >= Me.MinimunSearchTextLength _
AndAlso e.Key = Key.Enter Then
OnLoadItemsSource(New LoadItemsSourceRoutedEventArgs( _
AjaxStyleComboBox.LoadItemsSourceRoutedEvent, Me, _
Me.Text))
Me.IsDropDownOpen = True
e.Handled = True
Else
MyBase.OnKeyDown(e)
End If
End Sub
Protected Overrides Sub OnPreviewMouseLeftButtonDown( _
ByVal e As System.Windows.Input.MouseButtonEventArgs)
_bolKeyBoardActivity = False
MyBase.OnPreviewMouseLeftButtonDown(e)
End Sub
Protected Overrides Sub OnSelectionChanged( _
ByVal e As System.Windows.Controls.SelectionChangedEventArgs)
MyBase.OnSelectionChanged(e)
If _bolKeyBoardActivity = False Then
OnRecordSelected(New RecordSelectedRoutedEventArgs( _
AjaxStyleComboBox.RecordSelectedRoutedEvent, Me, _
Me.SelectedValue))
End If
End Sub
Protected Overridable Sub OnRecordSelected( _
ByVal e As RecordSelectedRoutedEventArgs)
RaiseEvent RecordSelected(Me, e)
End Sub
Protected Overridable Sub OnLoadItemsSource( _
ByVal e As LoadItemsSourceRoutedEventArgs)
RaiseEvent LoadItemsSource(Me, e)
End Sub
#End Region
End Class
How the Control Works
The control operates in two different states: when IsDropDownOpen
is False
(popup is not visible) and when IsDropDownOpen
is True
(popup is visible).
When IsDropDownOpen
is False
, the following occurs:
- User begins keying characters into the control.
OnPreviewKeyDown
inspects each character. If the <ESC> key is pressed, the ItemsSource
is removed and the text is cleared from the control.
- If the key is not handled by
OnPreviewKeyDown
, OnKeyDown
inspects each character, and if the search text is at least as long as the MinimunSearchTextLength
property and the <ENTER> key is pressed, the LoadItemsSource
RoutedEvent is raised, requesting the program to provide data to the ComboBox.
When IsDropDownOpen
is True
, the following occurs:
- User keys characters into the control.
OnPreviewKeyDown
inspects each character. If the <ESC> key is pressed, the ItemsSource
is removed and the text is cleared from the control. If the <ENTER> or <TAB> key is pressed, the RecordSelected
RoutedEvent is raised, informing the program to take the required action.
- If the key is not handled by
OnPreviewKeyDown
, the base ComboBox
control takes over key processing and will look up items in the ComboBox as the user types. Try this: open the program, enter "smi" in the AJAX style ComboBox, and then press <ENTER>. The text will change to "Smith", and the ComboBox will open, displaying the records. Now, press the <END> key and type "-". This will select the record with the last name of "Smith-Bates". This functionality is built into the ComboBox when the IsEditable
property is True
. You can read up on this in the MDSN help topic: ComboBox.IsEditable Property.
- If the user uses their mouse and clicks on a
ComboBoxItem
, the RecordSelected
RoutedEvent is raised, informing the program to take the required action.
That is really all this control does. It simply process keystrokes and raises the appropriate events.
MinimunSearchTextLength Property
The MinimunSearchTextLength
property controls how many characters the user must enter before they can begin a search. If you had 100,000 names in your database, you may want to limit the returned rows by requiring a certain number of characters be entered. You can also limit the number of rows returned by the SQL statement you use. For example, using the TOP
predicate in your SELECT
statement.
Now is a good time to mention an additional way to limit the rows returned. Let's say that your database is rather large and you want to limit the rows returned to customers in a certain state as well as last name. Super easy. Place an additional ComboBox with states in it, and use the state and the partial last name in your look up SQL statement or Stored Procedure parameters. You can see how simple it can be to provide additional features to this solution. This solution is not a one size fits all search solution. A WPF ComboBox
is probably not a choice for large database record paging. So, if it fits, use it; if not, bust out the search form with a ListView
.
The control exposes two RoutedEvents: LoadItemsSource
and RecordSelected
.
LoadItemsSource RoutedEvent
Just like the standard ASP.NET AJAX enabled DropDownList
, this control raises a LoadItemsSource
RoutedEvent when the control needs items. It is the consuming program's responsibility to take the search string and fill the ComboBox with search results.
RecordSelected RoutedEvent
When the user selects a record, this control raises the RecordSelected
RoutedEvent. The consuming program should listen for this event and take the appropriate action. Normally, this means loading a form with data.
VB.NET Notes on RoutedEvent
For programmers new to RoutedEvents, confusion can occur because VB.NET RoutedEvent methods have the same names as some statements in the VB.NET language. Also, since most of the RoutedEvent examples are written in C#, VB.NET programmers don't have a one for one example when declaring their own custom RoutedEvents since the VB.NET syntax is different. I'll try to explain all this below.
Public Shared ReadOnly RecordSelectedRoutedEvent As RoutedEvent _
= EventManager.RegisterRoutedEvent( _
"RecordSelected", RoutingStrategy.Bubble, _
GetType(RecordSelectedRoutedEventHandler), _
GetType(AjaxStyleComboBox))
Public Custom Event LoadItemsSource As _
LoadItemsSourceRoutedEventHandler
AddHandler(ByVal value As _
LoadItemsSourceRoutedEventHandler)
Me.AddHandler( _
AjaxStyleComboBox.LoadItemsSourceRoutedEvent, value)
End AddHandler
RemoveHandler(ByVal value As _
LoadItemsSourceRoutedEventHandler)
Me.RemoveHandler( _
AjaxStyleComboBox.LoadItemsSourceRoutedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, _
ByVal e As LoadItemsSourceRoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
There are two pieces to creating a custom RoutedEvent: a RoutedEvent declaration, and the corresponding Custom Event code.
The RoutedEvent declaration is straightforward. Using the EventManger.RegisterRoutedEvent
method, describe your event to WPF.
When it comes to the Handler Type parameter, you'll need to provide the delegate type for your RoutedEventHandler
. If you do not require any special properties in your RoutedEventArgs
, then you should use the RoutedEventHandler
delegate supplied by the .NET 3.0 framework. If you need to supply special properties in your RoutedEventArgs
, you will need to derive a class from RoutedEventArgs
and supply your own delegate. In the included project download, there are two extended RoutedEventArgs
classes and the corresponding delegates.
Custom Event
The above VB.NET Custom Event has three pieces: AddHandler
, RemoveHandler
, and RaiseEvent
. The same code in C# only has the AddHandler
and RemoveHandler
sections, and does not use the word "Custom." The Visual Studio editor will template the Custom Event code for you after you enter the first line.
AddHandler and RemoveHandler
In the above example, AddHandler
is written in three different ways. The first, AddHandler(ByVal value As LoadItemsSourceRoutedEventHandler)
, is a VB.NET language statement. In the Visual Studio editor, "AddHander
" appears in blue. Notice that the AddHandler
statement here has a different syntax than the AddHandler
statement used to add handlers for standard events.
Me.AddHandler(AjaxStyleComboBox.LoadItemsSourceRoutedEvent, value)
is a method inherited from the UIElement
class.
[AddHandler](AjaxStyleComboBox.LoadItemsSourceRoutedEvent, value)
is a method inherited from the UIElement
class, and is the same as Me.AddHandler
.
You can see that it is important to understand what is going on. C# does not have this problem.
RaiseEvent
RaiseEvent
has the same naming issues as the above AddHandler
and RemoveHander
. In addition, when raising a custom RoutedEvent, the developer can do it in two different places.
First, the developer can make use of the Custom Event RaiseEvent
by using the following syntax in their program:
OnRecordSelected(New RecordSelectedRoutedEventArgs( _
AjaxStyleComboBox.RecordSelectedRoutedEvent, Me, Me.SelectedValue))
...
Protected Overridable Sub OnRecordSelected( _
ByVal e As RecordSelectedRoutedEventArgs)
RaiseEvent RecordSelected(Me, e)
End Sub
The same RoutedEvent can also be raised like this:
Me.RaiseEvent(New RecordSelectedRoutedEventArgs( _
AjaxStyleComboBox.RecordSelectedRoutedEvent, _
Me, Me.SelectedValue))
Your development team should decide which pattern to use and stick with it. The first is probably preferred because the OnRecordSelected
method can be overridden by consumers of your control. If you use the second method, the consumers of the control won't have a method to override, or worse, your developer ignores the pattern and calls the event directly.
XAML Markup
<Window.Resources>
<DataTemplate x:Key="searchByContactLastName" >
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="5,2,0,0"/>
<Setter Property="Width" Value="100"/>
</Style>
</StackPanel.Resources>
<TextBlock Text="{Binding Path=LastName}" Margin="0,2,0,0"/>
<TextBlock Text="{Binding Path=FirstName}"/>
<TextBlock Text="{Binding Path=City}"/>
<TextBlock Text="{Binding Path=State}"/>
<TextBlock Text="{Binding Path=ContactId}" Width="50"/>
</StackPanel>
</DataTemplate>
<ItemsPanelTemplate x:Key="cboVirtualizingPanelTemplate">
<VirtualizingStackPanel IsItemsHost="True" IsVirtualizing="True" />
</ItemsPanelTemplate>
</Window.Resources>
Other XAML markup...
<WPFComboBoxAjaxStyle:AjaxStyleComboBox
HorizontalAlignment="Stretch"
x:Name="cboLastNameSearch"
VerticalAlignment="Top"
Grid.ColumnSpan="1"
Grid.Row="8"
IsEditable="True"
SelectedValuePath = "ContactId"
TextSearch.TextPath = "LastName"
ItemsPanel="{StaticResource cboVirtualizingPanelTemplate}"
ItemTemplate="{DynamicResource searchByContactLastName}"
IsSynchronizedWithCurrentItem="True"
MaxDropDownHeight="200"
MinimunSearchTextLength="2"/>
The XAML markup is rather simple, and familiar to most.
DataTemplate
The power of this solution is really exploited here. You can have one or more DataTemplate
s defined for your AJAX style ComboBox. For example, maybe you will allow users to look up customers by their last name, first name, or invoice number. All that is required is to swap out the DataTemplate
, and the ComboBox takes on a whole new look with respect to the display of data.
ItemsPanelTemplate
For ComboBoxes that will have more than a few records, it is important for performance reasons to set the ItemsPanelTemplate
to a VirutalizingStackPanel
. See the MSDN topic, "VirutalizingStackPanel Class".
You will also need to set the IsItemsHost
property to True
. This indicates that this panel is a container for user interface (UI) items that are generated by an ItemsControl
. See the MSDN topic, "Panel.IsItemsHost Property".
AjaxStyleComboBox
I'll cover the properties that are critical for this solution to work.
I have mentioned the IsEditable
property. Setting this to True
will allow you to enter text in the ComboBox's TextBox.
The SelectedValuePath
property tells the ComboBox what value to place in the SelectValue
property when a record is selected. If you are using multiple DataTemplate
s, you may have to change this property if the new DataTemplate
key is different from the previous DataTemplate
key.
The TextSearch.TextPath
attached property tells the ComboBox which property in the items collection will be displayed in the ComboBox TextBox. If you are using multiple DataTemplate
s, you will have to change this property to match the new path you are performing your look by. Example, "LastName", "FirstName", etc.
Window1 Code-Behind
Private Sub cboLastNameSearch_LoadItemsSource( _
ByVal sender As Object, ByVal e As LoadItemsSourceRoutedEventArgs) _
Handles cboLastNameSearch.LoadItemsSource
Me.cboLastNameSearch.ItemsSource = _
DataAccessLayer.GetContactSearchResults(e.SearchString)
End Sub
Private Sub cboLastNameSearch_RecordSelected( _
ByVal sender As Object, ByVal e As RecordSelectedRoutedEventArgs) _
Handles cboLastNameSearch.RecordSelected
Me.gridContactForm.DataContext = _
DataAccessLayer.GetContact(CType(e.Value, Integer))
End Sub
Here is where a custom control really shines. Almost no code in the Window code-behind file. Two event handlers with one line of code each. How simple!
Using the AJAX Style ComboBox in Your Projects
The image below shows a directory in the included solution with four files. When using this control, you will need to include these four files in your project.

Closing
Hope this article can help someone learn a little more about WPF.
History
- 11 November 2007: Initial release.