Adding page up and down support to AutoCompleteBox

15 December 2008

Supporting the page up and page down keys for browsing through auto complete suggestions is one feature that we decided not to include in the production control to improve the flexibility of the control, plus the actual code is a little hacky.

If you have an application where your users are going to be exploring large sets of items in the auto complete drop down, it makes sense to add this functionality back. This is a place where Silverlight can add value: most standard AJAX auto complete controls do not support the paging keys, but with Silverlight, we can find a way.

In this post, I'm going to walk you through creating a derived AutoCompleteBox control that supports the page up and down keys. The control will be a little more specific than the shipping control in that it will be explicitly tied to a ListBox and a custom ListBoxSelectionAdapter.

Overview

To accomplish this work, we'll:

  • Create a new class library for the custom control
  • Create a new selection adapter that derives from ListBox, and modify the navigation logic to support the paging keys
  • Create a derived auto complete control
  • Setup the default style and template for the control in generic.xaml

If you followed my post yesterday on customizing the cycle navigation behavior of the control, then you've already worked through most of the steps in this guide: both posts share the same source code actually. The source is attached to the post for download. Feel free to leave any questions that you may have.

Why isn't this "in the box"?

The shipping AutoCompleteBox doesn't make assumptions about the drop-down selection control that you use: in fact, it doesn't even care if the control derives from ListBox, Selector, or ItemsControl. All that you need is either for the control itself to derive from ISelectionAdapter, or to write a wrapper/adapter that derives from this interface.

This opens up the door for some great experiences, since you can use a tree view, data grid, or build your own rich control like a carousel. So not only can you re-template the auto complete control - but you can also use your own selection controls while keeping the nitty-gritty suggestion and items management to our stable production code.

To give you an idea of what we're talking about, in theory the only bounding box is ISelectionAdapter when it comes to what the AutoCompleteBox can interact with:

the ISelectionAdapter interface

Anyway, the reason we weren't able to ship this is that it effectively would only allow us to offer page up and down support to ListBox controls, and the experience that we shipped actually supports all Selector controls in a consistent manner, without any special casing for ListBox.

Creating a new custom control class library

Open up Visual Studio with the Silverlight Tools installed and create a new Visual C# "Silverlight Class Library" project. I named my project "CustomAutoComplete".

Hacking away at page up and down support

Another reason we didn't ship with this is that the technique for easily intercepting and managing page up and down controls is a little complex to be done properly. In this example, instead we're going to intercept page down events and pass them into the ListBox - where the list box already has logic for paging down.

And then, for page up, we'll do some hacking to eventually pass it to the list box. However, TextBox actually handles the page up and will move the selection to the start of the text box - and then never send the key argument to our control. Our strategy here will be to intercept the key up event with the Key.PageUp, and then pass it directly to the list box's key down event handler.

This isn't a recommended strategy for control development, but gosh, it works here to enable this scenario.

Making a new ISelectionAdapter

A selection adapter is a concept that the auto complete control uses for handing off the suggestion navigation, presentation, and selection to another control.

Instead of requiring that the auto complete control always use a ListBox to work, we opted for a design with the ISelectionAdapter interface. This means that you can create a wrapper for any control, regardless of whether the control derives from Selector or not. This allows you to use DataGrid, for instance.

I copied the contents of the SelectorSelectionAdapter.cs file in the Source\Controls\AutoCompleteBox\ directory of the toolkit source download.

Although a good starting place for our adapter, we are going to switch from the "wrapper" style of selection adapter and instead make ours an actual control by deriving directly from the Silverlight ListBox control.

This is not particularly important for this scenario, but I'll be re-using this project for some other posts where it does make a difference.

Changes I've made: named the file and class ListBoxSelectionAdapter. Derived from ListBox:

namespace CustomAutoCompleteBox
{
    /// <summary>
    /// A selection adapter that is also a ListBox, for use with the
    /// AutoCompleteBox control.
    /// </summary>
    public partial class ListBoxSelectionAdapter : ListBox, ISelectionAdapter
    {

Next, I've modified the standard HandleKeyDown method by adding support for Key.PageDown. This code handles the situation where no item is selected, then passes the key arguments directory to the base.OnKeyDown method (the ListBox itself, that then handles the right amount of navigation). It then marks the arguments as handled, just to be safe:

case Key.PageDown:
    if (SelectedItem == null && Items.Count > 0)
    {
        SelectedIndex = 0;
    }
    base.OnKeyDown(e);
    e.Handled = true;
    break;

Then, to handle the key up, we need to get a little more creative. I've added an internal method called HandleKeyUp, that is very similar, and will be called by our custom auto complete control:

/// <summary>
/// Workaround that will take the key up and send it as a key down to 
/// the list box. This is because the text box will handle the first 
/// page up and use it to move the caret on the text box up.
/// </summary>
/// <param name="e">The event data.</param>
internal void HandleKeyUp(KeyEventArgs e)
{
    if (e.Key == Key.PageUp)
    {
        base.OnKeyDown(e);
    }
}

Yes, that's not a typo: in the case of a page up, the key up handler should call the key down handler.

Derive CustomizedAutoCompleteBox from AutoCompleteBox

Now that the selection adapter is ready, we should create our own AutoCompleteBox control. We'll derive from the toolkit control and add a new dependency property for the cyclic navigation.

First off, create the new code file, CustomizedAutoCompleteBox.cs:

// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.Windows.Controls;

namespace CustomAutoCompleteBox
{
    /// <summary>
    /// An AutoCompleteBox control implementation that has a few additional 
    /// features, as a demonstration. "Polished" with features such as page up
    /// and page down support, and cyclic navigation settings for the list box.
    /// </summary>
    /// <remarks>This is a demonstration only and is strongly tied to the use of
    /// a ListBox as the selection adapter.</remarks>
    public class CustomizedAutoCompleteBox : AutoCompleteBox
    {

The standard implementation of AutoCompleteBox handles most of the interaction with the SelectionAdapter, but we still need to find the template part in the OnApplyTemplate method, so that we can call the new HandleKeyUp method that we created. So, we add a private or internal automatic property for our ListBoxSelectionAdapter:

        /// <summary>
        /// The selection adapter.
        /// </summary>
        internal ListBoxSelectionAdapter SelectionAdapterPart { get; set; }

We override the OnApplyTemplate method to grab the selection adapter instance and set it to our local SelectionAdapterPart property.

/// <summary>
/// Overrides the on apply template method.
/// </summary>
public override void OnApplyTemplate()
{
    SelectionAdapterPart = GetTemplateChild("SelectionAdapter") as ListBoxSelectionAdapter;
    base.OnApplyTemplate();
}

Now, to actually grab the page up key, we'll override the OnKeyUp method of the AutoCompleteBox control:

/// <summary>
/// Overrides the key up method.
/// </summary>
/// <param name="e">The event data.</param>
protected override void OnKeyUp(KeyEventArgs e)
{
    if (e.Key == Key.PageUp && SelectionAdapterPart != null)
    {
        SelectionAdapterPart.HandleKeyUp(e);
    }

    base.OnKeyUp(e);
}

Create the default template (generic.xaml)

Last step: default styling. To do this, we must create our generic.xaml file inside the class library:
  • Create a new folder in the project (right-click on the project in the Solution Explorer > select Add > then New Folder) called Themes
  • Add a new Silverlight use control to the new Themes directory, call it generic.xaml. You can remove any associated generic.xaml.cs file, since there's no code behind for the default style file.
  • Change the file to compile as a Resource: right-click on generic.xaml, select Properties; then, in the property grid, change the Build Action to be Resource instead of Page.

Next, open up the file, delete its contents, and replace it with the standard resource dictionary XML content, declare the local XMLNS for your assembly and namespace:

<!--
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-->

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:controls="clr-namespace:CustomAutoCompleteBox">

</ResourceDictionary>

Now, we just have to add the style for the control. I've copied most of the template from the source download of the toolkit, but did modify the opacity of the list box a little bit. I also place an instance of ListBoxSelectionAdapter, with the x:Name of "SelectionAdapter" (the part name that AutoCompleteBox looks for), where the simple ListBox is in the default template:

<Style TargetType="controls:CustomizedAutoCompleteBox">
    <Setter Property="IsTabStop" Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="controls:CustomizedAutoCompleteBox">
                <Grid Margin="{TemplateBinding Padding}" 
                      Background="{TemplateBinding Background}">
                    <TextBox IsTabStop="True" x:Name="Text" Style="{TemplateBinding TextBoxStyle}" Margin="0" />
                    <Popup x:Name="Popup">
                        <Border x:Name="PopupBorder" HorizontalAlignment="Stretch" Opacity="0.0" BorderThickness="0" CornerRadius="3">
                            <Border.RenderTransform>
                                <TranslateTransform X="1" Y="1" />
                            </Border.RenderTransform>
                            <Border
                                HorizontalAlignment="Stretch" 
                                Opacity="1.0" 
                                Padding="0" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                BorderBrush="{TemplateBinding BorderBrush}" 
                                CornerRadius="3">
                                <Border.RenderTransform>
                                    <TransformGroup>
                                        <TranslateTransform X="-1" Y="-1" />
                                    </TransformGroup>
                                </Border.RenderTransform>
                                <controls:ListBoxSelectionAdapter 
                                    x:Name="SelectionAdapter" 
                                    Background="#99ffffff"
                                    ItemContainerStyle="{TemplateBinding ItemContainerStyle}"
                                    ItemTemplate="{TemplateBinding ItemTemplate}" />
                            </Border>
                        </Border>
                    </Popup>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="PopupStates">
                            <VisualStateGroup.Transitions>
                                <VisualTransition GeneratedDuration="0:0:0.1" To="PopupOpened" />
                                <VisualTransition GeneratedDuration="0:0:0.2" To="PopupClosed" />
                            </VisualStateGroup.Transitions>
                            <VisualState x:Name="PopupOpened">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="PopupBorder" Storyboard.TargetProperty="Opacity" To="1.0" />
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="PopupClosed">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="PopupBorder" Storyboard.TargetProperty="Opacity" To="0.0" />
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

That is a lot of code; here's just the selection adapter part to make that clear. Remember, since we derived ListBoxSelectionAdapter directly from ListBox, this XAML is actually talking directly to a ListBox instance when initialized.

<controls:ListBoxSelectionAdapter 
    x:Name="SelectionAdapter" 
    Background="#99ffffff"
    ItemContainerStyle="{TemplateBinding ItemContainerStyle}"
    ItemTemplate="{TemplateBinding ItemTemplate}" />

Validating the new experience

Ok, build the project. Hopefully you'll receive no errors, and we'll be ready to verify the new behavior.

I added a regular Silverlight Application (with the auto-generated test page option) called 'Demo', and then:

  • Added a reference to Microsoft.Windows.Controls
  • Added a project reference to the CustomAutoCompleteBox project (that contains our CustomizedAutoCompleteBox control, the default template, and the ListBoxSelectionAdapter)

Inside the page you're testing, make sure to add the XMLNS statement for your custom library; you can then place your custom control inside. I'm binding it to the data context. If you run the sample app, search for a few short characters, then press the page up and down keys - you're all set now!

<UserControl x:Class="Demo.WithListBoxSelectionAdapter"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:custom="clr-namespace:CustomAutoCompleteBox;assembly=CustomAutoCompleteBox"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid x:Name="LayoutRoot">
        <StackPanel>
            <custom:CustomizedAutoCompleteBox ItemsSource="{Binding}" x:Name="autoComplete1" />
        </StackPanel>
    </Grid>
</UserControl>

In my example, I set the page's DataContext to a bunch of sample data in the Loaded method. The download includes this data, plus the ability to compare the regular AutoCompleteBox control to the new one.

Download and explore this sample app

Related sample post

Download the December 2008 release of the Silverlight Toolkit

Jeff Wilcox is a Software Engineer at Microsoft in the Open Source Programs Office (OSPO), helping Microsoft engineers use, contribute to and release open source at scale.

comments powered by Disqus