Creating a ComboBox Style AutoCompleteBox Control in Silverlight

Silverlight’s AutoCompleteBox control provides a nice way to filter a list of items as the user types into a TextBox.  If you haven’t used it before you can see an example here.  In a previous post I showed how you could customize AutoCompleteBox to make it look and feel more like an editable ComboBox so that users can click a down arrow to see the list of items and also filter the items by typing into the TextBox. 

image

The solution I wrote about in the previous post relied upon a custom control template which works fine but you may want to encapsulate that functionality into a re-useable control so that you don’t have to worry about adding the template into each application project.  Fortunately, it’s not too hard to derive from AutoCompleteBox and create a custom control.  Here are the steps I went through to build a derived version of AutoCompleteBox that I call AutoCompleteComboBox.

Step 1: Create a Silverlight Class Library

Create a new Silverlight class library and add a class named AutoCompleteComboBox into it.  Derive the class from AutoCompleteBox:

public partial class AutoCompleteComboBox : AutoCompleteBox

Once the class is created add a folder into the project named Themes that contains a file named Generic.xaml in it.  Right-click on Generic.xaml, select Properties and ensure that the Build Action is set to Resource.

Step 2: Define the Control’s Template

Since AutoCompleteComboBox needs to look like a ComboBox I took a template available in one of the AutoCompleteBox toolkit samples that added the drop down arrow (normally seen on the ComboBox control) and tweaked it a little bit.  The template code needs to go in the Generic.xaml file that was created in the previous step.  Here’s what my Generic.xaml file looks like.  You’ll see that there’s a ComboToggleButton style which is referenced by the AutoCompleteComboBox style.

<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:system="clr-namespace:System;assembly=mscorlib"
  xmlns:custom="clr-namespace:MyCustomControls">
    
    <!-- AutoCompleteBox Styles -->

    <!-- Custom toggle button template -->
    <Style x:Name="ComboToggleButton" TargetType="ToggleButton">
        <Setter Property="Foreground" Value="#FF333333"/>
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="Background" Value="#FF1F3B53"/>
        <Setter Property="Padding" Value="0"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToggleButton">
                    <Grid>
                        <Rectangle Fill="Transparent" />
                        <ContentPresenter
                            x:Name="contentPresenter"
                            Content="{TemplateBinding Content}"
                            ContentTemplate="{TemplateBinding ContentTemplate}"
                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                            Margin="{TemplateBinding Padding}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- Custom control template used for the IntelliSense sample -->
    <Style TargetType="custom:AutoCompleteComboBox">

        <!-- ComboBox should not perform text completion by default -->
        <Setter Property="IsTextCompletionEnabled" Value="False" />

        <!-- The minimum prefix length should be 0 for combo box scenarios -->
        <Setter Property="MinimumPrefixLength" Value="1" />

        <!-- Regular template values -->
        <Setter Property="Background" Value="#FF1F3B53"/>
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="HorizontalContentAlignment" Value="Left"/>
        <Setter Property="BorderBrush">
            <Setter.Value>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#FFA3AEB9" Offset="0"/>
                    <GradientStop Color="#FF8399A9" Offset="0.375"/>
                    <GradientStop Color="#FF718597" Offset="0.375"/>
                    <GradientStop Color="#FF617584" Offset="1"/>
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="custom:AutoCompleteComboBox">
                    <Grid Margin="{TemplateBinding Padding}">
                        <TextBox IsTabStop="True" x:Name="Text" Style="{TemplateBinding TextBoxStyle}" Margin="0,0,0,2" />
                        <ToggleButton x:Name="ToggleButton"
                                HorizontalAlignment="Right"
                                VerticalAlignment="Center"
                                Style="{StaticResource ComboToggleButton}"
                                Margin="0" BorderBrush="Black" BorderThickness="1,0,0,0"
                                HorizontalContentAlignment="Center" 
                                Background="LightGray" 
                                Height="16" Width="16">
                            <ToggleButton.Content>
                                <Path x:Name="BtnArrow" Height="4" Width="8" Stretch="Uniform" 
                                      Data="F1 M 301.14,-189.041L 311.57,-189.041L 306.355,-182.942L 301.14,-189.041 Z " 
                                          Margin="0,0,6,0" HorizontalAlignment="Right">
                                    <Path.Fill>
                                        <SolidColorBrush x:Name="BtnArrowColor" Color="#FF333333"/>
                                    </Path.Fill>
                                    <Path.RenderTransform>
                                        <ScaleTransform x:Name="ToggleButtonScale" />
                                    </Path.RenderTransform>
                                </Path>
                            </ToggleButton.Content>
                        </ToggleButton>
                        <Popup x:Name="Popup">
                            <Border x:Name="PopupBorder" HorizontalAlignment="Stretch" Opacity="1.0" BorderThickness="0">
                                <Border.RenderTransform>
                                    <TranslateTransform X="2" Y="-2" />
                                </Border.RenderTransform>
                                <Border.Background>
                                    <SolidColorBrush Color="#11000000" />
                                </Border.Background>
                                <ListBox x:Name="Selector" ScrollViewer.HorizontalScrollBarVisibility="Auto" 
                                                 ScrollViewer.VerticalScrollBarVisibility="Auto" 
                                                 ItemTemplate="{TemplateBinding ItemTemplate}" />
                            </Border>
                        </Popup>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="FocusStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.2" To="ToggleButtonOver" />
                                    <VisualTransition GeneratedDuration="0:0:0.2" To="ToggleButtonOut" />
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="ToggleButtonOver">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="ToggleButtonScale" Storyboard.TargetProperty="ScaleX" To="1.3" />
                                        <DoubleAnimation Storyboard.TargetName="ToggleButtonScale" Storyboard.TargetProperty="ScaleY" To="1.3" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="ToggleButtonOut">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="ToggleButtonScale" Storyboard.TargetProperty="ScaleX" To="1" />
                                        <DoubleAnimation Storyboard.TargetName="ToggleButtonScale" Storyboard.TargetProperty="ScaleY" To="1" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="PopupStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:0.4" 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>
                            <VisualStateGroup x:Name="ValidationStates">
                                <VisualState x:Name="Valid" />
                                <VisualState x:Name="InvalidUnfocused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ValidationErrorElement" Storyboard.TargetProperty="Visibility">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidFocused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ValidationErrorElement" Storyboard.TargetProperty="Visibility">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="validationTooltip" Storyboard.TargetProperty="IsOpen">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <system:Boolean>True</system:Boolean>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Step 3: Add a TemplatePart Attribute

Go back to the AutoCompleteComboBox class created in Step 1 and add a TemplatePart attribute on it that references the ToggleButton control name (which will represent the drop down arrow) defined in the control template.  Template parts are used to be able to reference the different controls defined in the template.  Although TemplatePart’s Name property can be given a direct string value, I followed the convention seen in Silverlight controls and used a constant to define the name value.

[TemplatePart(Name = AutoCompleteComboBox.ElementToggleButton, Type = typeof(ToggleButton))]
public partial class AutoCompleteComboBox : AutoCompleteBox
{
    private const string ElementToggleButton = "ToggleButton";
}

Step 4: Add the ToggleButton Property

Add a ToggleButton property into the AutoCompleteComboBox class as shown next.  You can call it whatever you want but I like to keep things simple and call it what it really is….a ToggleButton.  The event handlers referenced in the code below are shown in Step 6.

#region Template Parts

ToggleButton _ToggleButton;

ToggleButton ToggleButton
{
    get { return _ToggleButton; }
    set
    {
        if (_ToggleButton != null)
        {
            _ToggleButton.Click -= new RoutedEventHandler(ToggleButton_Click);
            _ToggleButton.MouseEnter -= new MouseEventHandler(ToggleButton_MouseEnter);
            _ToggleButton.MouseLeave -= new MouseEventHandler(ToggleButton_MouseLeave);
        }

        _ToggleButton = value;

        if (_ToggleButton != null)
        {
            _ToggleButton.Click += new RoutedEventHandler(ToggleButton_Click);
            _ToggleButton.MouseEnter += new MouseEventHandler(ToggleButton_MouseEnter);
            _ToggleButton.MouseLeave += new MouseEventHandler(ToggleButton_MouseLeave);
        }
    }
}

#endregion


Step 5: Set the DefaultStyleKey for the AutoCompleteComboBox Control

Next you need to tell the AutoCompleteComboBox to use the Style you defined in Generic.xaml instead of the default style used by AutoCompleteBox.  Do this by adding the following constructor into the class (which also adds code to call the ApplyTemplate class shown in the next step):

public AutoCompleteComboBox()
{
    this.DefaultStyleKey = typeof(AutoCompleteComboBox);
    Loaded += (sender, e) => ApplyTemplate();
}


Step 6: Override ApplyTemplate and Handle Events

We’re almost there! The next thing that needs to be done is to override the control’s OnApplyTemplate method.  We need to do this so that the ToggleButton can be injected into the process so that we can handle the user clicking on it to show all of the items like a normal ComboBox would do.  Here’s the remaining code that needs to be added into the class. 

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    ToggleButton = GetTemplateChild(ElementToggleButton) as ToggleButton;
}

void ToggleButton_MouseEnter(object sender, MouseEventArgs e)
{
    VisualStateManager.GoToState(this, "ToggleButtonOver", true);
}

void ToggleButton_MouseLeave(object sender, MouseEventArgs e)
{
    VisualStateManager.GoToState(this, "ToggleButtonOut", true);
}

void ToggleButton_Click(object sender, RoutedEventArgs e)
{
    if (String.IsNullOrEmpty(this.SearchText))
    {
        this.Text = String.Empty;
    }
    this.IsDropDownOpen = !this.IsDropDownOpen;
}


This code accesses the ToggleButton control defined in the Generic.xaml template using the base class’s GetTemplateChild method.  Once the ToggleButton control is found it’s assigned to the ToggleButton property of our control which handles hooking up the Click, MouseEnter and MouseLeave events.  The ToggleButton_Click event handler does the magic of making the AutoCompleteComboBox control look and feel like a standard ComboBox.


Step 7: Use the AutoCompleteComboBox Control

Once the custom control is finished and you successfully build it you can use the control in a Silverlight application.  To do that, create a Silverlight application project and reference the control project created in Step 1 (or reference the dll that’s generated by the project).  Within the XAML for your Silverlight application add a reference to the control’s namespace and assembly:

<Usercontrol x:Class="SilverlightApplication1"
    xmlns:controls="clr-namespace:YourCustomControlNamespace;assembly=YourCustomControlAssemblyName">
</UserControl>


Once the assembly and namespace are referenced you can define the control and bind items to it using it’s ItemSource property:

<controls:AutoCompleteComboBox ItemsSource="{Binding Jobs}" 
    Margin="5,0,5,10" Width="150"  HorizontalAlignment="Left"
    MinimumPopulateDelay="0" MinimumPrefixLength="0" />

If I had to pick out the key parts from these steps they’d be creating the Generic.xaml file, adding the TemplatePart attribute, setting the control’s DefaultStyleKey, overriding ApplyTemplate and calling GetTemplateChild to locate the ToggleButton control.  These changes allow the AutoCompleteComboBox to look like a regular ComboBox while still allowing the user to type into it to filter the items. 

Download Code

 

 

Logo

For more information about onsite, online and video training, mentoring and consulting solutions for .NET, SharePoint or Silverlight please visit http://www.thewahlingroup.com/.

comments powered by Disqus

2 Comments

  • Hi,

    I am trying to use this control in Silverlight 3 application, but I have problems with it - clicking on button do nothing...
    Any workarounds?

    Thanks for answer,

    Tomas

  • Tomas: Hard to say without seeing any code. Have you tried the sample that's available? I'm using that code in a client application and it's working great so far so I'm guessing something else is going on to cause the problem.

Comments have been disabled for this content.