Introduction
Imagine that we have a list of customers in a standard WPF ComboBox
that allows the user to pick a customer. Selecting a customer is not mandatory, so it can be left blank and still pass whatever validation the user interface might enforce. So the user goes a head and picks a customer and later decides that the field should have been left blank to begin with.
The question is: How do we reset the ComboBox
to its initial state?
Background
Just recently, I was given the task to covert a set of subclassed Winforms controls into their WPF counterparts. These controls could contain standard shortcuts used in our application as well as customizations such as behavior and appearance.
For instance, we have a subclass of TextBox
(TextBoxEx
) that makes sure that the text is selected when the user clicks inside the TextBox
.
I worked my way through the various controls, but when I came as far as the ComboBox
, I needed to take a closer look. The requirement of the control was that it should display a row that represented "no Value". This way, the user could choose to undo a previous selection by selecting the "No Value" row.
This all made sense to me, but how should this be implemented in WPF?
I started to search for solutions online and soon found out that the common approach was to insert a dummy object in the source list that represented the NULL
value.
Various implementations of course, everything from value converters to the use of CompositeCollection
, but the basic idea remained the same.
Modifying the underlying list just to please the user interface seemed like a bad idea, so I started to look for alternatives.
The question now became:
Would it be possible to restyle the ComboBox
so that it is capable of having a selectable NULL
item?
Restyling the WPF ComboBox
It is pretty obvious that we need to look into how the visual tree of a ComboBox
looks like and in order to do that, we need to look at the default template for the ComboBox
.
Obtaining the default template can be done either by using Expression Blend or we can simply download all the default templates for all the standard controls from here.
For convenience, I have included a copy of these templates in the demo project.
Anyhow, we start of by creating a subclass of ComboBox
and call it ComboBoxEx
and give it a default style taken from the downloaded Aero.NormalColor
theme.
The listbox
given a list of employees now looks like this:
What we need to do here is place some content before the first item in the list.
It should be noted that every object displayed in a ComboBox
is wrapped inside a ComboBoxItem
that in turn has its own control template.
The dropdown portion of the ComboBox
contains a ScrollViewer
like this:
<ScrollViewer Name="DropDownScrollViewer">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas Height="0" Width="0" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle
Name="OpaqueRect"
Height="{Binding ElementName=DropDownBorder,Path=ActualHeight}"
Width="{Binding ElementName=DropDownBorder,Path=ActualWidth}"
Fill="{Binding ElementName=DropDownBorder,Path=Background}" />
</Canvas>
<ItemsPresenter Name="ItemsPresenter"
KeyboardNavigation.DirectionalNavigation="Contained"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Grid>
</ScrollViewer>
We need to stack some content above the ItemsPresenter
and we can do that using a StackPanel
.
The first row (Row 0) is where we put the content that is to represent the NULL
value. The question is what should that content be? Well, since everything else is wrapped in a ComboBoxItem
, maybe we should start out just the same. Like this:
<ScrollViewer CanContentScroll="False" Name="DropDownScrollViewer">
<Grid RenderOptions.ClearTypeHint="Enabled">
<Canvas Height="0" Width="0" HorizontalAlignment="Left" VerticalAlignment="Top">
<Rectangle
Name="OpaqueRect"
Height="{Binding ElementName=DropDownBorder,Path=ActualHeight}"
Width="{Binding ElementName=DropDownBorder,Path=ActualWidth}"
Fill="{Binding ElementName=DropDownBorder,Path=Background}" />
</Canvas>
<StackPanel>
<ComboBoxItem Content="This is a null value"></ComboBoxItem>
<ItemsPresenter Name="ItemsPresenter"
KeyboardNavigation.DirectionalNavigation="Contained"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
</Grid>
</ScrollViewer>
This will result in a ComboBox
that looks like this:
Now, that looks exactly like what we want.
The next thing we need to take care of is the highlighting. While all employees are highlighted as expected, we see that hovering the mouse over the NULL
item does not do anything.
So what we need to do is this:
When the mouse enters the ComboBoxItem
representing the NULL
value, we need to remove the highlight from whatever item is currently highlighted. While that seems like a trivial task, is actually not so straight forward given the fact that the ComboBoxItem.IsHighlighted
property is defined as read-only.
Well, let us continue by solving that problem.
public class ComboBoxItemEx : ComboBoxItem
{
public new bool IsHighlighted
{
get { return base.IsHighlighted; }
set { base.IsHighlighted = value; }
}
}
So how do we make sure that this class is used instead of the ComboBoxItem
class as a item container?
The ComboBox
inherits from the ItemsControl
that contains a method made for this purpose.
We add this code to our new ComboBoxEx
class.
protected override DependencyObject GetContainerForItemOverride()
{
var comboBoxItem = new ComboBoxItemEx();
RegisterEventHandlerForWhenIsHighlightedChanges(comboBoxItem);
return comboBoxItem;
}
We simply create our ComboBoxItemEx
instance and return that in place of the ComboBoxItem
instance.
In addition, we also have the opportunity of hooking when an item is highlighted. This alone is a nice feature so let us go ahead and create a dependency property for this. This allows for other controls to bind to this property to support live preview of the highlighted item.
private static readonly DependencyPropertyKey HighlightedItemPropertyKey =
DependencyProperty.RegisterReadOnly("HighlightedItemProperty",
typeof(object), typeof(ComboBox),
new FrameworkPropertyMetadata(null));
public static readonly DependencyProperty HighlightedItemProperty =
HighlightedItemPropertyKey.DependencyProperty;
[Browsable(false)]
public object HighlightedItem
{
get { return GetValue(HighlightedItemProperty); }
private set { SetValue(HighlightedItemPropertyKey, value); }
}
Now, we need to make sure that when the mouse enters the NULL
item, we must remove the highlight from any other highlighted item and at the same time make sure the NULL
item is highlighted.
Since we must hook the mouse event from the NULL ComboBoxItem
, we need to give it a name so that we can obtain a reference to it in the code behind.
<local:ComboBoxItemEx
x:Name="PART_NullValue"
Style="{StaticResource ResourceKey=ComboBoxNullItem}"
Content="This is a null value" >
</local:ComboBoxItemEx>
From the code, we can see that we also set the Style
property to a custom style that only applies to the ComboBoxItems
representing the NULL
value. For now, that style is just a copy of the default ComboBoxItem
style, but this can come in handy later if we want to alter the ContentTemplate
used to visualize the NULL
item.
Remember that altering the ItemsTemplate
of the ComboBox
will not affect the ContentTemplate
of the NULL
item as it is not presented by the ItemsPresenter
. More on this later.
Now that we have a name for it, we can also get a reference to it from the code behind.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
GetComboBoxNullItemFromTemplate();
RegisterEventHandlersForComboBoxNullItem();
}
private void RegisterEventHandlersForComboBoxNullItem()
{
_comboBoxNullItem.AddHandler(MouseEnterEvent,
new MouseEventHandler((o, e) => OnComboBoxNullItemMouseEnter()),
handledEventsToo: true);
}
private void GetComboBoxNullItemFromTemplate()
{
_comboBoxNullItem = GetTemplateChild("PART_NullValue") as ComboBoxItemEx;
}
private void OnComboBoxNullItemMouseEnter()
{
RemoveHighlightFromCurrentlyHighlightedItem();
HighlightNullItem();
}
This code takes care of highlighting the NULL
item and remove the highlight from the currently highlighted item. In the case of another item being highlighted, we just need to remove the highlight from the NULL
item.
private void OnComboBoxItemHighlighted(ComboBoxItemEx comboBoxItem)
{
HighlightedItem = comboBoxItem.DataContext;
RemoveHighlightFromComboBoxNullItem();
}
Now if we fire this thing up, we can see that everything highlights as expected.
The next challenge up is keyboard handling. If we try to navigate the items using the arrow keys, we will soon find out that it is impossible to navigate to and from the NULL
item.
The logic should be something like:
- If the
NULL
item is currently highlighted and we press the down arrow key, we should highlight the first item in the list.
- If the first item in the list is highlighted and we press the up arrow key, we should highlight the
NULL
item.
private void OnScrollViewerKeyDown(KeyEventArgs keyEventArgs)
{
if (ArrowKeyDownWasPressed(keyEventArgs))
HandleScrollViewerArrowKeyDown(keyEventArgs);
if (ArrowKeyUpWasPressed(keyEventArgs))
HandleScrollViewerArrowKeyUp(keyEventArgs);
}
private void HandleScrollViewerArrowKeyDown(KeyEventArgs keyEventArgs)
{
if (IsComboBoxNullItemHighlighted && HasItems)
{
RemoveHighlightFromComboBoxNullItem();
HighlightTheFirstComboBoxItem();
IndicateThatTheKeyEventHasBeenHandled(keyEventArgs);
}
}
private void HandleScrollViewerArrowKeyUp(KeyEventArgs keyEventArgs)
{
if (IsFirstComboBoxItemIsHighLighted)
{
RemoveHighlightFromCurrentlyHighlightedItem();
HighlightNullItem();
IndicateThatTheKeyEventHasBeenHandled(keyEventArgs);
}
}
This is pretty much it for the up and down arrow keys, but there is more keyboard handling to take care of.
If the ComboBox
is closed, we can use the up/down/left/right arrow keys to navigate between the items.
The NULL
item is at the moment ignored so we need to fix that as well.
protected override void OnKeyDown(KeyEventArgs keyEventArgs)
{
if (!IsDropDownOpen)
{
if (ArrowKeyDownWasPressed(keyEventArgs) || ArrowKeyRightWasPressed(keyEventArgs))
HandleArrowKeyDownOrRight(keyEventArgs);
if (ArrowKeyUpWasPressed(keyEventArgs) || ArrowKeyLeftWasPressed(keyEventArgs))
HandleArrowKeyUpOrLeft(keyEventArgs);
}
if (!keyEventArgs.Handled)
base.OnKeyDown(keyEventArgs);
}
private void HandleArrowKeyUpOrLeft(KeyEventArgs keyEventArgs)
{
if (IsFirstItemSelected)
{
ClearSelectedItem();
}
}
private void HandleArrowKeyDownOrRight(KeyEventArgs keyEventArgs)
{
if (IsNothingSelected && HasItems)
{
SelectFirstItem();
IndicateThatTheKeyEventHasBeenHandled(keyEventArgs);
}
}
That should take care of most of the needed keyboard handling and we are ready to move on to the next task.
The Visual Appearance of a NULL Item
As briefly mentioned before, the ItemsTemplate
that we might apply to the ComboBox
will not affect how the NULL
item is visually represented in the dropdown. This gives meaning because such a template (DataTemplate
) is very likely to have bindings to the underlying object. And since the value is NULL
, we can't bind to it either.
The ContentPresenter
within the ComboBoxItem
template already knows how to display a string
so that is why we see the "The value is null
" text.
First of all, "The value is null
" is hardwired into the template itself so we need to do something about that.
Another thing is that it might be a nice feature to be able to customize the NULL
item as it may be presented in a certain way. That way, we can display whatever we want to visualize the NULL
item, such as a bitmap.
Since the representation of the NULL
item is likely to be a string
in most cases, we add a new dependency property to the ComboBoxEx
class that lets the developer specify this.
public static readonly DependencyProperty NullValueTextProperty =
DependencyProperty.Register("NullValueText", typeof (string), typeof (ComboBoxEx),
new FrameworkPropertyMetadata("None"));
[Category("Common")]
public string NullValueText
{
get { return (string)GetValue(NullValueTextProperty); }
set { SetValue(NullValueTextProperty, value); }
}
public static readonly DependencyProperty SelectionBoxNullValueTextProperty =
DependencyProperty.Register("SelectionBoxNullValueText",
typeof(string), typeof(ComboBoxEx),
new FrameworkPropertyMetadata("The value is null"));
[Category("Common")]
public string SelectionBoxNullValueText
{
get { return (string)GetValue(SelectionBoxNullValueTextProperty); }
set { SetValue(SelectionBoxNullValueTextProperty, value); }
}
In addition to the NullValueText
that specifies the text to be displayed in the dropdown, we have also added a SelectionBoxNullValueText
property that lets us specify what should be displayed in the selection box when the value is null
. An example of this would be "Pick an employee".
Let us first define a NullItemTemplate
property that allows customization of the NULL
item in the dropdown.
public static readonly DependencyProperty NullItemTemplateProperty =
DependencyProperty.Register("NullItemTemplate",
typeof (DataTemplate), typeof (ComboBoxEx));
[Category("Common")]
public DataTemplate NullItemTemplate
{
get { return (DataTemplate)GetValue(NullItemTemplateProperty); }
set { SetValue(NullItemTemplateProperty, value); }
}
Next, we need to modify the control template so that this template is used.
<local:ComboBoxItemEx
x:Name="PART_NullValue"
Style="{StaticResource ResourceKey=ComboBoxNullItem}"
Content="{TemplateBinding NullValueText}"
ContentTemplate="{TemplateBinding NullItemTemplate}">
</local:ComboBoxItemEx>
Let us try this out by applying a custom template in the demo project:
<ExtendedControls:ComboBoxEx.NullItemTemplate>
<DataTemplate>
<Image Source="/System.Windows.ExtendedControls.Demo;
component/NoUser.png"></Image>
</DataTemplate>
</ExtendedControls:ComboBoxEx.NullItemTemplate>
As we can see below, the NULL
value is now represented by an image.
That should cover most scenarios in the drop down, but what about the selection box.
A lot of people are asking how to apply a custom template to the selection box. The answer is that we can't. At least not in a straight forward manner, that is.
For some obscure reason, the WPF team has made the SelectionBoxItemTemplate
read-only and it will always use the template defined in the ItemsTemplate
. This is not necessarily always the desired behavior.
But since we are dealing with a full control template for the ComboBox
here, we can do something about this limitation.
As an example, we should be able to display the employee using an italic style font.
Since we can't override the metadata to make a read-only dependency property writeable, we must create a similar property. Let us name the property SelectionBoxTemplate
.
public static readonly DependencyProperty SelectionBoxTemplateProperty =
DependencyProperty.Register("SelectionBoxTemplate",
typeof(DataTemplate), typeof(ComboBoxEx));
[Category("Common")]
public DataTemplate SelectionBoxTemplate
{
get { return (DataTemplate)GetValue(SelectionBoxTemplateProperty); }
set { SetValue(SelectionBoxTemplateProperty, value); }
}
Now we need to make sure we fall back to the SelectionBoxItemTemplate
if this template is null
.
This is done using a trigger on the control template.
<Trigger Property="SelectionBoxTemplate" Value="{x:Null}">
<Setter TargetName="selectionBoxContentPresenter"
Property="ContentTemplate"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type local:ComboBoxEx}},Path=SelectionBoxItemTemplate}">
</Setter>
</Trigger>
In order to achieve this, we must give the ContentPresenter
a name and default the ContentTemplate
to our new SelectionBoxTemplate
property.
<ContentPresenter x:Name="selectionBoxContentPresenter" IsHitTestVisible="false"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}">
</ContentPresenter>
Now we can go ahead and create a custom template for the selectionbox
like this:
<ExtendedControls:ComboBoxEx.SelectionBoxTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock FontStyle="Italic" Text="{Binding FirstName}"></TextBlock>
<TextBlock Margin="5,0,0,0" FontStyle="Italic"
Text="{Binding LastName}"></TextBlock>
</StackPanel>
</DataTemplate>
</ExtendedControls:ComboBoxEx.SelectionBoxTemplate>
The image below shows the result of this customization:
Things are starting to fall into place. The next thing is how to visualize the NULL
item in the selection box.
I think we should try to make this as simple as possible and yet provide a decent level of flexibility.
As mentioned, it would be nice to have the ability to customize how a NULL
item is represented in the selection box.
Let's create a DataTemplate
property and a SelectionBoxNullValueText
property:
public static readonly DependencyProperty SelectionBoxNullItemTemplateProperty =
DependencyProperty.Register
("SelectionBoxNullItemTemplate", typeof(DataTemplate), typeof(ComboBoxEx));
[Category("Common")]
public DataTemplate SelectionBoxNullItemTemplate
{
get { return (DataTemplate)GetValue(SelectionBoxNullItemTemplateProperty); }
set { SetValue(SelectionBoxNullItemTemplateProperty, value); }
}
public static readonly DependencyProperty SelectionBoxNullValueTextProperty =
DependencyProperty.Register("SelectionBoxNullValueText",
typeof(string), typeof(ComboBoxEx),
new FrameworkPropertyMetadata("The value is null"));
[Category("Common")]
public string SelectionBoxNullValueText
{
get { return (string)GetValue(SelectionBoxNullValueTextProperty); }
set { SetValue(SelectionBoxNullValueTextProperty, value); }
}
Next, we provide a default template that simply displays a TextBlock
that binds to the SelectionBoxNullValueText
property.
<Style x:Key="{x:Type local:ComboBoxEx}"
TargetType="{x:Type local:ComboBoxEx}">
<Setter Property="SelectionBoxNullItemTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}"></TextBlock>
</DataTemplate>
</Setter.Value>
</Setter>
.......
Now, we need to make sure that the ContentPresenter
used in the selection box gets this template if the SelectedItem
property is NULL
.
<Trigger Property="SelectedItem" Value="{x:Null}">
<Setter TargetName="selectionBoxContentPresenter"
Property="ContentTemplate"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type local:ComboBoxEx}},
Path=SelectionBoxNullItemTemplate}">
</Setter>
<Setter TargetName="selectionBoxContentPresenter"
Property="Content"
Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type local:ComboBoxEx}},Path=SelectionBoxNullValueText}">
</Setter>
</Trigger>
The image below shows our latest customization:
Or we could choose to give it the same template as the one in the dropdown.
Using the Code
We can use this combobox
in the same way that we use a regular combobox
.
Just to summarize what we have done here, given below is a list of the added properties and their purpose:
NullItemTemplate |
The template used to visualize a NULL value in the dropdown |
NullValueText |
The text used to identify a null value in the dropdown |
SelectionBoxNullItemTemplate |
The template used to visualize a NULL value in the selection box. |
SelectionBoxNullValueText |
The text used to identify a null value in the selection box. |
HighlightedItem |
The currently highlighted item in the ComboBox |
Well, that's it for now. This is my first WPF article, so go gentle on the ratings. :)
History
- 8th April, 2011: Initial version