Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

AutoComplete ComboBox for Silverlight

0.00/5 (No votes)
16 Feb 2010 3  
How to customize an AutoCompleteBox as a type-ahead ComboBox for Line of Business applications.

Demo application

Table of Contents

Introduction

The built-in ComboBox in Silverlight is not powerful enough to cater to the requirements of enterprise applications. In LOB (Line of Business) applications, sometimes we need type-ahead behavior for combo boxes so that the available choices are filtered as the user starts typing. The AutoCompleteBox (previously included in Silverlight Toolkit, but now a part of Silverlight 3 native controls) is a very powerful and flexible control, and in this article, we will look at how we can customize the AutoCompleteBox to be used as a replacement for Combo Box/Drop Down.

What will We Cover?

To begin the article, let's first define what functionalities we need to add to the AutoCompleteBox to make it behave like a type-ahead customizable drop down combo box. Here are our requirements:

  1. There should be a down-arrow like button that pops up the drop down with the available choices. Currently, the AutoCompleteBox is a simple textbox in its original shape.
  2. When an item is selected, bringing the drop down should show all the available choices. Currently, it only displays the selected one.
  3. If an item is selected and the drop down is opened, the selected item should be highlighted and focused (brought into view).
  4. The custom AutoCompleteBox should be usable for concrete object-to-object relationships (e.g., the SalesOrderDetail object containing a reference to the Project object, also called associations or navigational properties).
  5. The custom implementation should be usable in foreign key relationships (e.g., the SalesOrderDetail object containing a field called ProductID).

To address all these, we will create an AutoCompleteComboBox control that inherits the AutoCompleteBox and start adding the above features to it.

Requirement 1

There should be a down-arrow like button that pops up the drop down with the available choices.

AutoCompleteBox with drop down button

Thanks to the Windows Presentation Framework, this can be achieved using styles. This is explained by Tim Hieuer here, and used in the Silverlight 3 toolkit demo on this page. So, we will copy the following style inside Themes\Generic.xaml.

<!-- 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="myCtrls:AutoCompleteComboBox">

  <Setter Property="MinimumPopulateDelay" Value="1" />

  <!-- 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="0" />


  <!-- 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="myCtrls:AutoCompleteComboBox">
        <Grid Margin="{TemplateBinding Padding}">
          <TextBox IsTabStop="True" x:Name="Text" 
              Style="{TemplateBinding TextBoxStyle}" Margin="0" />
          <ToggleButton 
              x:Name="DropDownToggle"            
              HorizontalAlignment="Right"
              VerticalAlignment="Center"
              Style="{StaticResource ComboToggleButton}"
              Margin="0"
              HorizontalContentAlignment="Center" 
              Background="{TemplateBinding Background}" 
              BorderThickness="0" 
              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>
            </ToggleButton.Content>
          </ToggleButton>
          
          <Popup x:Name="Popup">
            <Border x:Name="PopupBorder" HorizontalAlignment="Stretch" 
                Opacity="1.0" BorderThickness="0" 
                CornerRadius="3">
              <Border.RenderTransform>
                <TranslateTransform X="2" Y="2" />
              </Border.RenderTransform>
              <Border.Background>
                <SolidColorBrush Color="#11000000" />
              </Border.Background>
              <Border HorizontalAlignment="Stretch" BorderThickness="0" 
                 CornerRadius="3">
                <Border.Background>
                  <SolidColorBrush Color="#11000000" />
                </Border.Background>
                <Border.RenderTransform>
                  <TransformGroup>
                    <ScaleTransform />
                    <SkewTransform />
                    <RotateTransform />
                    <TranslateTransform X="-1" Y="-1" />
                  </TransformGroup>
                </Border.RenderTransform>
                <Border HorizontalAlignment="Stretch" 
                    Opacity="1.0" Padding="2" 
                    BorderThickness="2" 
                    BorderBrush="{TemplateBinding BorderBrush}" 
                    CornerRadius="3">
                  <Border.RenderTransform>
                    <TransformGroup>
                      <ScaleTransform />
                      <SkewTransform />
                      <RotateTransform />
                      <TranslateTransform X="-2" Y="-2" />
                    </TransformGroup>
                  </Border.RenderTransform>
                  <Border.Background>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                      <GradientStop Color="#FFDDDDDD" Offset="0"/>
                      <GradientStop Color="#AADDDDDD" Offset="1"/>
                    </LinearGradientBrush>
                  </Border.Background>
                  <ListBox x:Name="Selector" 
                    Height="200"
                    ScrollViewer.HorizontalScrollBarVisibility="Auto" 
                    ScrollViewer.VerticalScrollBarVisibility="Auto" 
                    ItemTemplate="{TemplateBinding ItemTemplate}" />
                </Border>
              </Border>
            </Border>
          </Popup>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

After this, we need to set our control's default style to the one described above and wire up the dropdown button's Click event.

public AutoCompleteComboBox() : base()
{
    SetCustomFilter();
    this.DefaultStyleKey = typeof(AutoCompleteComboBox);
}

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    ToggleButton toggle = (ToggleButton)GetTemplateChild("DropDownToggle");
    if (toggle != null)
    {
        toggle.Click += DropDownToggle_Click;
    }
}

private void DropDownToggle_Click(object sender, RoutedEventArgs e)
{
    FrameworkElement fe = sender as FrameworkElement;
    AutoCompleteBox acb = null;
    while (fe != null && acb == null)
    {
        fe = VisualTreeHelper.GetParent(fe) as FrameworkElement;
        acb = fe as AutoCompleteBox;
    }
    if (acb != null)
    {
        acb.IsDropDownOpen = !acb.IsDropDownOpen;
    }
}

Requirement 2

When an item is selected, bringing the drop down should show all the available choices.

AutoCompleteBox with all the items shown even an item is selected

At this point, our custom AutoCompleteBox has gained visuals like a typical combo box, but if we select an item and then bring the drop down, it will only display the selected item. To view all the items, we need to set a custom ItemFilter predicate like this:

protected virtual void SetCustomFilter()
{
    //custom logic: how to autocomplete
    this.ItemFilter = (prefix, item) =>
    {
        //return all items for empty prefix
        if (string.IsNullOrEmpty(prefix))
            return true;

        //return all items if a record is already selected
        if (this.SelectedItem != null)
            if (this.SelectedItem.ToString() == prefix)
                return true;

        //else return items that contains prefix
        return item.ToString().ToLower().Contains(prefix.ToLower());
    };
}

Notice that the above code assumes that you have overridden the ToString() method of the object. That is, the AutoCompleteComboBox filters on the ToString() value of the lookup object. Also note that I am using a StartsWith filter, but you can change the behavior as per your business needs.

Requirement 3

If an item is selected and the drop down is opened, the selected item should be highlighted and focused (brought into view).

AutoCompleteBox with the selected item highlighted

We are now able to popup the correct items, but notice that our selected item is not highlighted when the list pops up. Also, we need to scroll the list to the selected item for a better user experience. For this, we need to override the OnPopulated event and select the item from the underlying ListBox. Thanks to nangua for discovering in this thread that the ListBox.ScrollIntoView() method needs to be run on the UI thread to work properly.

//highlighting logic
protected override void OnPopulated(PopulatedEventArgs e)
{
    base.OnPopulated(e);
    ListBox listBox = GetTemplateChild("Selector") as ListBox;
    if (listBox != null)
    {
        //highlight the selected item, if any
        if (this.ItemsSource != null && this.SelectedItem != null)
        {
            listBox.SelectedItem = this.SelectedItem;
            
            //now scroll the selected item into view
            listBox.Dispatcher.BeginInvoke(delegate
            {
                listBox.UpdateLayout();
                listBox.ScrollIntoView(listBox.SelectedItem);
            });
        }
    }
}

Requirement 4

The custom AutoCompleteBox should be usable for concrete object-to-object relationships.

Till now, our AutoCompleteComboBox looks and behaves like we expected for a type-ahead combo box. But, if we are using data binding, and following the MVVM (Model View ViewModel) pattern, and why shouldn't we, then we need to have a Dependency Property that is automatically updated when a new item is selected. The most obvious choice is to use the SelectedItem Dependency Property, but there is a small problem. If we are using down arrow to cycle through the choices, the SelectedItem Dependency Property (and hence the property to which the Dependency Property is bound) changes. This may not be desirable (as this could invoke unnecessary code in case we have hooked the PropertyChanged event), and we typically want to update our data object only after the user finishes making his decision. For this, we will add a custom Dependency Property (I am calling it SeletedItemBinding) that is updated once the user has finished selecting an entry (when the drop down is closed). So, we declare a new Dependency Property and set the selected item when that Dependency Property is changed.

#region SelectedItemBinding
public static readonly DependencyProperty SelectedItemBindingProperty =
    DependencyProperty.Register("SelectedItemBinding",
            typeof(object),
            typeof(AutoCompleteComboBox),
            new PropertyMetadata
        (new PropertyChangedCallback(OnSelectedItemBindingChanged))
            );

public object SelectedItemBinding
{
    get { return GetValue(SelectedItemBindingProperty); }
    set { SetValue(SelectedItemBindingProperty, value); }
}

private static void OnSelectedItemBindingChanged
    (DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((AutoCompleteComboBox)d).OnSelectedItemBindingChanged(e);
}

protected virtual void OnSelectedItemBindingChanged(
                       DependencyPropertyChangedEventArgs e)
{
    SetSelectemItemUsingSelectedItemBindingDP();
}

public void SetSelectemItemUsingSelectedItemBindingDP()
{
    if (!this.isUpdatingDPs)
        SetValue(SelectedItemProperty, GetValue(SelectedItemBindingProperty));
}

#endregion

We also need to reflect changes back to the DataContext once the user finishes. For this, we can override the OnDropClosed or OnLostFocus event. Let's override the DropDownClosed event like this:

protected override void OnDropDownClosed(RoutedPropertyChangedEventArgs<bool> e)
{
    base.OnDropDownClosed(e);
    UpdateCustomDPs();
}

private void UpdateCustomDPs()
{
    //flag to ensure that we don't reselect the selected item
    this.isUpdatingDPs = true;

    //if a new item is selected or the user blanked out the selection, update the DP
    if (this.SelectedItem != null || this.Text == string.Empty)
    {
        //update the SelectedItemBinding DP
        SetValue(SelectedItemBindingProperty, GetValue(SelectedItemProperty));
    }
    else
    {
        //revert to the originally selected one

        if (this.GetBindingExpression(SelectedItemBindingProperty) != null)
        {
            SetSelectemItemUsingSelectedItemBindingDP();
        }
    }

    this.isUpdatingDPs = false;
}

Notice that the above solution will work for object-to-object associations; e.g., considering the typical relation between the SalesOrderDetail and the Product tables, we have a Product reference in the SalesOrderDetail object like this:

public class SalesOrderDetail
{
    Product product;
    public Product Product
    {
        get { return product; }
        set { product = value; }
    }

Requirement 5

The custom implementation should be usable in foreign key relationships.

The above implementation is going to work for most business applications, but in one of our applications, we did not have concrete object-to-object relationships. Instead, we had a database style foreign key relation with a ProductID property in the SalesOrderDetail table, like this:

public class SalesOrderDetail
{
    int productID;
    public int ProductID
    {
        get { return productID; }
        set { productID = value; }
    }

If you do not have any such scenarios in your application, you can simply skip this part. This requirement needs a bit more work, and we need to create two dependency properties: SelectedValuePath: a string Dependency Property to determine which property of the lookup object to copy, and SelectedValue: an object Dependency Property to determine which property of the DataContext to be updated. Typically, SelectedValue will refer to the primary key of the lookup business object (e.g., ProductID from the Product table) and SelectedValuePath will refer to the foreign key in the current DataContext (e.g., ProductID in the SalesOrderDetail table). Notice how we determine the item to be selected using Reflection in the following code:

#region SelectedValue

public static readonly DependencyProperty SelectedValueProperty =
    DependencyProperty.Register(
            "SelectedValue",
            typeof(object),
            typeof(AutoCompleteComboBox),
            new PropertyMetadata(
              new PropertyChangedCallback(OnSelectedValueChanged))
            );

public object SelectedValue
{
    get { return GetValue(SelectedValueProperty); }
    set { SetValue(SelectedValueProperty, value); }
}

private static void OnSelectedValueChanged
    (DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((AutoCompleteComboBox)d).OnSelectedValueChanged(e);
}

protected virtual void OnSelectedValueChanged(
                  DependencyPropertyChangedEventArgs e)
{
    if (!this.isUpdatingDPs)
        SetSelectemItemUsingSelectedValueDP();
}

//selects the item whose value is given in SelectedValueDP
public void SetSelectemItemUsingSelectedValueDP()
{
    if (this.ItemsSource != null)
    {
        /// if selectedValue is empty, remove the current selection
        if (this.SelectedValue == null)
        {
            this.SelectedItem = null;
        }

        /// if there is no selected item,
        /// select the one given by SelectedValueProperty
        else if (this.SelectedItem == null)
        {
            object selectedValue = GetValue(SelectedValueProperty);
            string propertyPath = this.SelectedValuePath;
            if (selectedValue != null && !(string.IsNullOrEmpty(propertyPath)))
            {
                /// loop through each item in the item source
                /// and see if its 'SelectedValuePathProperty' == SelectedValue
                foreach (object item in this.ItemsSource)
                {
                    PropertyInfo propertyInfo = item.GetType().GetProperty(propertyPath);
                    if (propertyInfo.GetValue(item, null).Equals(selectedValue))
                        this.SelectedItem = item;
                }
            }
        }
    }
}

#endregion

#region SelectedValuePath

public static readonly DependencyProperty SelectedValuePathProperty =
    DependencyProperty.Register(
            "SelectedValuePath",
            typeof(string),
            typeof(AutoCompleteComboBox),
            null
            );

public string SelectedValuePath
{
    get { return GetValue(SelectedValuePathProperty) as string; }
    set { SetValue(SelectedValuePathProperty, value); }
}

#endregion

We also need to update the DataContext's property referred by the SelectedValue property; this is done by modifying our UpdateCustomDPs() method (that we already created) with the following:

private void UpdateCustomDPs()
{
    //flag to ensure that we don't reselect the selected item
    this.isUpdatingDPs = true;

    //if a new item is selected or the user 
    //blanked out the selection, update the DP
    if (this.SelectedItem != null || this.Text == string.Empty)
    {
        //update the SelectedItemBinding DP
        SetValue(SelectedItemBindingProperty, 
                 GetValue(SelectedItemProperty));

        //update the SelectedValue DP
        string propertyPath = this.SelectedValuePath;
        if (!string.IsNullOrEmpty(propertyPath))
        {
            if (this.SelectedItem != null)
            {
                PropertyInfo propertyInfo =
        this.SelectedItem.GetType().GetProperty(propertyPath);

                //get property from selected item
                object propertyValue = 
                  propertyInfo.GetValue(this.SelectedItem, null);

                //update the datacontext
                this.SelectedValue = propertyValue;
            }
            else //user blanked out the selection,
            // so we need to set the default value
            {
                //get the binding for selectedvalue property
                BindingExpression bindingExpression =
                   this.GetBindingExpression(SelectedValueProperty);
                Binding dataBinding = bindingExpression.ParentBinding;

                //get the dataitem (typically the datacontext)
                object dataItem = bindingExpression.DataItem;

                //get the property of that dataitem
                //that's bound to selectedValue property
                string propertyPathForSelectedValue = dataBinding.Path.Path;

                //get the default value for that property
                Type propertyTypeForSelectedValue = 
                  dataItem.GetType().GetProperty(
                    propertyPathForSelectedValue).PropertyType;
                object defaultObj = null;
                //use activator for getting the defaults for value types
                if (propertyTypeForSelectedValue.IsValueType)  
                    defaultObj = 
                      Activator.CreateInstance(propertyTypeForSelectedValue);

                //update the Selected Value property
                this.SelectedValue = defaultObj;
            }
        }
    }
    else
    {
        //revert to the originally selected one
        if (this.GetBindingExpression(SelectedItemBindingProperty) != null)
        {
            SetSelectemItemUsingSelectedItemBindingDP();
        }

        else if (this.GetBindingExpression(SelectedValueProperty) != null)
        {
            SetSelectemItemUsingSelectedValueDP();
        }
    }

    this.isUpdatingDPs = false;
}

Notice that in the above code, we used Activator.CreateInstance() to get the default value (for the SelectedValue Dependency Property) when no item is selected. This was done to make the control more flexible so that it can be used with any data type of foreign key (typically, these are integers or strings).

Using the Code

We are done, and our MVVM compatible AutoCompleteComboBox control is ready to be used. Here's how the control can be used:

  1. Object to object association (typical association that is available with Entity Framework 3.5):
  2. <custom:AutoCompleteComboBox
        SelectedItemBinding="{Binding Product, Mode=TwoWay}"
        ItemsSource="{Binding Path=Products, Source={StaticResource ViewModel}}"
    />
  3. Foreign key association (the new association type introduced with Entity Framework 4):
  4. <custom:AutoCompleteComboBox
        SelectedValue="{Binding ProductID, Mode=TwoWay}"
        SelectedValuePath="ProductID"
        ItemsSource="{Binding Path=Products, Source={StaticResource ViewModel}}"
    />

The demo application follows the Model View ViewModel pattern and demonstrates the use of the AutoCompleteComboBox in three scenarios:

  1. Object to Object association
  2. Foreign key association with Integer foreign keys
  3. Foreign key association with String foreign keys

Conclusion

That's all. This article demonstrated how we can customize the AutoCompleteBox to be used as a replacement for ComboBox in our LOB applications. There are plenty of other customizations, and if any one wants to enhance this control further, I strongly recommend to read the blog posts by Jeff Wilcox. He has written a lot of great posts on the AutoCompleteBox control and its customizations. I hope you enjoyed reading this article.

History

  • Version 1.0
    • Initial version
  • Version 1.1
    • Fixed UI virtualization so the control can efficiently handle thousands of records
    • Scrolled the selected item into view when the popup is opened

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here