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.
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:
- 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.
- When an item is selected, bringing the drop down should show all the available choices. Currently, it only displays the selected one.
- If an item is selected and the drop down is opened, the selected item should be highlighted and focused (brought into view).
- 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).
- 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.
There should be a down-arrow like button that pops up the drop down with the available choices.
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.
<!---->
<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>
<!---->
<Style TargetType="myCtrls:AutoCompleteComboBox">
<Setter Property="MinimumPopulateDelay" Value="1" />
<!---->
<Setter Property="IsTextCompletionEnabled" Value="False" />
<!---->
<Setter Property="MinimumPrefixLength" Value="0" />
<!---->
<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;
}
}
When an item is selected, bringing the drop down should show all the available choices.
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()
{
this.ItemFilter = (prefix, item) =>
{
if (string.IsNullOrEmpty(prefix))
return true;
if (this.SelectedItem != null)
if (this.SelectedItem.ToString() == prefix)
return true;
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.
If an item is selected and the drop down is opened, the selected item should be highlighted and focused (brought into view).
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.
protected override void OnPopulated(PopulatedEventArgs e)
{
base.OnPopulated(e);
ListBox listBox = GetTemplateChild("Selector") as ListBox;
if (listBox != null)
{
if (this.ItemsSource != null && this.SelectedItem != null)
{
listBox.SelectedItem = this.SelectedItem;
listBox.Dispatcher.BeginInvoke(delegate
{
listBox.UpdateLayout();
listBox.ScrollIntoView(listBox.SelectedItem);
});
}
}
}
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()
{
this.isUpdatingDPs = true;
if (this.SelectedItem != null || this.Text == string.Empty)
{
SetValue(SelectedItemBindingProperty, GetValue(SelectedItemProperty));
}
else
{
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; }
}
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();
}
public void SetSelectemItemUsingSelectedValueDP()
{
if (this.ItemsSource != null)
{
if (this.SelectedValue == null)
{
this.SelectedItem = null;
}
else if (this.SelectedItem == null)
{
object selectedValue = GetValue(SelectedValueProperty);
string propertyPath = this.SelectedValuePath;
if (selectedValue != null && !(string.IsNullOrEmpty(propertyPath)))
{
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()
{
this.isUpdatingDPs = true;
if (this.SelectedItem != null || this.Text == string.Empty)
{
SetValue(SelectedItemBindingProperty,
GetValue(SelectedItemProperty));
string propertyPath = this.SelectedValuePath;
if (!string.IsNullOrEmpty(propertyPath))
{
if (this.SelectedItem != null)
{
PropertyInfo propertyInfo =
this.SelectedItem.GetType().GetProperty(propertyPath);
object propertyValue =
propertyInfo.GetValue(this.SelectedItem, null);
this.SelectedValue = propertyValue;
}
else {
BindingExpression bindingExpression =
this.GetBindingExpression(SelectedValueProperty);
Binding dataBinding = bindingExpression.ParentBinding;
object dataItem = bindingExpression.DataItem;
string propertyPathForSelectedValue = dataBinding.Path.Path;
Type propertyTypeForSelectedValue =
dataItem.GetType().GetProperty(
propertyPathForSelectedValue).PropertyType;
object defaultObj = null;
if (propertyTypeForSelectedValue.IsValueType)
defaultObj =
Activator.CreateInstance(propertyTypeForSelectedValue);
this.SelectedValue = defaultObj;
}
}
}
else
{
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).
We are done, and our MVVM compatible AutoCompleteComboBox
control is ready to be used. Here's how the control can be used:
- Object to object association (typical association that is available with Entity Framework 3.5):
<custom:AutoCompleteComboBox
SelectedItemBinding="{Binding Product, Mode=TwoWay}"
ItemsSource="{Binding Path=Products, Source={StaticResource ViewModel}}"
/>
- Foreign key association (the new association type introduced with Entity Framework 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:
- Object to Object association
- Foreign key association with Integer foreign keys
- Foreign key association with String foreign keys
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.
- Version 1.0
- 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