![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Beginner
License: The Code Project Open License (CPOL)
WPF AJAX Style ComboBoxBy Karl ShifflettAn article describing the WPF AJAX Style ComboBox custom control. The control demonstrates implementing custom RoutedEvents. There is also section on coding RoutedEvents using VB.NET. |
C# 2.0, C# 3.0, VB 8.0, VB 9.0, Windows, .NET 2.0, .NET 3.0, .NET 3.5, WPF, VS2005, VS2008, Architect, Dev, Design
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
This article describes the WPF AJAX Style ComboBox custom control. The control demonstrates implementing custom RoutedEvents. There is also 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 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.

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, 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, 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 changed selection with the arrow keys. Also, we need an event to listen for to load records when required.
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
''' <summary>
''' An extended WPF ComboBox that works like the ASP.NET
''' AJAX enabled ComboBoxes
''' </summary>
''' <remarks></remarks>
Public Class AjaxStyleComboBox
Inherits ComboBox
#Region " Private Declarations "
Private _bolKeyBoardActivity As Boolean = False
Private _intMinimunSearchTextLength As Integer = 1
#End Region
#Region " Properties "
''' <summary>
''' What is the minimum number of characters the
''' user must enter before they are allowed to
''' request a record search
''' </summary>
<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 "
''' <summary>
''' Fires when a record is selected. This replaces the
''' ComboBox SelectedChanged event
''' </summary>
Public Shared ReadOnly LoadItemsSourceRoutedEvent As RoutedEvent _
= EventManager.RegisterRoutedEvent( _
"LoadItemsSource", RoutingStrategy.Bubble, _
GetType(LoadItemsSourceRoutedEventHandler), _
GetType(AjaxStyleComboBox))
''' <summary>
''' Fires when the combo box needs its items loaded
''' </summary>
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 "
''' <summary>
''' Raises the RecordSelected event if the user presses
''' ENTER or TAB
''' </summary>
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
''' <summary>
''' Raises the LoadItemsSource event when the user
''' has entered a search string and pressed ENTER
''' </summary>
Protected Overrides Sub OnKeyDown(ByVal e As _
System.Windows.Input.KeyEventArgs)
_bolKeyBoardActivity = True
'this code only runs if the DropDown is not open
' because if its open the OnPreviewKeyDown code
'will handle the ENTER key
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
''' <summary>
''' Raises the RecordSelected event if the user selects
''' a record with their mouse.
''' </summary>
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
The control operates in two different states, when IsDropDownOpen is False (popup is not visible) and when IsDropDownOpen is True (popup is visible).
When the IsDropDownOpen is False the following occurs:
OnPreviewKeyDown inspects each character. If the <ESC> key is presses, the ItemsSource is removed and the text is cleared from the control. 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 provide data to the ComboBox. When the IsDropDownOpen is True the following occurs:
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. 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", the ComboBox will open displaying the records. Now, press the <END> key and type "-". This will select the record with 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. 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.
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 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 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 this 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.
Just like the standard ASP.NET AJAX enabled DropDownList this control raises a LoadItemsSource RoutedEvent when the controls needs items. It is the consuming programs responsibility to take the search string and fill the ComboBox with search results.
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.
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)
'same as the above line of code
'[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)
'same as the above line of code
'[RaiseEvent](e)
End RaiseEvent
End Event
There are two pieces to creating a custom RoutedEvent, a RoutedEvent declaration and 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 .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 corresponding delegates.
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.
In the above example AddHandler is written 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 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 has the same naming issues as the above AddHandler and RemoveHander. In addition when raising a custom RoutedEvent the developer can do it 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.
<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.
The power of this solution is really exploited here. You can have one or more DataTemplates 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 the data.
For ComboBoxes that will have more than a few records it is important for performance reasons to set the ItemsPanelTemplate to a VirutalizingStackPanel. See 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 MSDN topic, "Panel.IsItemsHost Property."
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 DataTemplates, you may have to change this property if the new DataTemplate key is different from the previous DataTemplate key.
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 DataTemplates, you will have to change this property to match the new path you are performing your look by. Example, "LastName", "FirstName", etc.
''' <summary>
''' this is a custom event fired by our AjaxStyleComboBox
''' it's requesting new data so we provide it
''' </summary>
Private Sub cboLastNameSearch_LoadItemsSource( _
ByVal sender As Object, ByVal e As LoadItemsSourceRoutedEventArgs) _
Handles cboLastNameSearch.LoadItemsSource
Me.cboLastNameSearch.ItemsSource = _
DataAccessLayer.GetContactSearchResults(e.SearchString)
End Sub
''' <summary>
''' this is a custom event fired by our AjaxStyleComboBox
''' a record has been selected, now it's up to the local
''' code to act upon a record selection
''' </summary>
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!
The below image 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.

Hope this article can help someone learn a little more about WPF.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 24 Jan 2008 Editor: |
Copyright 2007 by Karl Shifflett Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |