Click here to Skip to main content
15,867,488 members
Articles / Desktop Programming / WPF
Article

Creating a Nullable WPF ComboBox

Rate me:
Please Sign up or sign in to vote.
4.90/5 (7 votes)
8 Apr 2011CPOL10 min read 65.3K   1.5K   24   13
This article shows how to subclass the standard WPF ComboBox to provide custom features.

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:

XML
<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:

XML
<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.

C#
public class ComboBoxItemEx : ComboBoxItem
{
    /// <summary>
    /// Gets or sets a <see cref="bool"/> value that indicates if this item is highlighted.
    /// </summary>
    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.

C#
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.

C#
private static readonly DependencyPropertyKey HighlightedItemPropertyKey =
    DependencyProperty.RegisterReadOnly("HighlightedItemProperty", 
			typeof(object), typeof(ComboBox),
                          	new FrameworkPropertyMetadata(null));

/// <summary>
/// Identifies the <see cref="HighlightedItem"/> dependency property.
/// </summary>
public static readonly DependencyProperty HighlightedItemProperty =
    HighlightedItemPropertyKey.DependencyProperty;

/// <summary>
/// Gets a <see cref="bool"/> value that indicates if the null item is highlighted.
/// </summary>
[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.

XML
<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.

C#
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.

C#
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.
C#
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.

C#
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.

C#
/// <summary>
/// Identifies the <see cref="NullValueText"/> dependency property.
/// </summary>
public static readonly DependencyProperty NullValueTextProperty =
    DependencyProperty.Register("NullValueText", typeof (string), typeof (ComboBoxEx),
                                new FrameworkPropertyMetadata("None"));

/// <summary>
/// Gets or sets the text that is used to represent a null value 
/// in the dropdown portion of the combobox.
/// This is a dependency property.
/// </summary>
[Category("Common")]
public string NullValueText
{
    get { return (string)GetValue(NullValueTextProperty); }
    set { SetValue(NullValueTextProperty, value); }
}

/// <summary>
/// Identifies the <see cref="SelectionBoxNullValueText"/> dependency property.
/// </summary>
public static readonly DependencyProperty SelectionBoxNullValueTextProperty =
    DependencyProperty.Register("SelectionBoxNullValueText", 
		typeof(string), typeof(ComboBoxEx),
                  	new FrameworkPropertyMetadata("The value is null"));

/// <summary>
/// Gets or sets the text that is used to represent 
/// a null value in selectionbox of the combobox.
/// This is a dependency property.
/// </summary>
[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.

C#
/// <summary>
/// Identifies the <see cref="NullItemTemplate"/> dependency property.
/// </summary>
public static readonly DependencyProperty NullItemTemplateProperty =
    DependencyProperty.Register("NullItemTemplate", 
		typeof (DataTemplate), typeof (ComboBoxEx));

/// <summary>
/// Gets or sets the <see cref="DataTemplate"/> that is used to 
/// visualize a null value in the dropdown.
/// This is a dependency property.
/// </summary>
[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.

XML
<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:

XML
<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.

C#
/// <summary>
/// Identifies the <see cref="SelectionBoxTemplate"/> dependency property.
/// </summary>
public static readonly DependencyProperty SelectionBoxTemplateProperty =
    DependencyProperty.Register("SelectionBoxTemplate", 
	typeof(DataTemplate), typeof(ComboBoxEx));

/// <summary>
/// Gets or sets the <see cref="DataTemplate"/> that is used to 
/// visualize a item in the selection box
/// This is a dependency property.
/// </summary>
[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.

XML
<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.

XML
<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:

XML
<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:

C#
/// <summary>
/// Identifies the <see cref="SelectionBoxNullItemTemplate"/> dependency property.
/// </summary>
public static readonly DependencyProperty SelectionBoxNullItemTemplateProperty =
    DependencyProperty.Register
	("SelectionBoxNullItemTemplate", typeof(DataTemplate), typeof(ComboBoxEx));

/// <summary>
/// Gets or sets the <see cref="DataTemplate"/> that is used to 
/// visualize a item in the selection box
/// This is a dependency property.
/// </summary>
[Category("Common")]
public DataTemplate SelectionBoxNullItemTemplate
{
    get { return (DataTemplate)GetValue(SelectionBoxNullItemTemplateProperty); }
    set { SetValue(SelectionBoxNullItemTemplateProperty, value); }
}

/// <summary>
/// Identifies the <see cref="SelectionBoxNullValueText"/> dependency property.
/// </summary>
public static readonly DependencyProperty SelectionBoxNullValueTextProperty =
    DependencyProperty.Register("SelectionBoxNullValueText", 
		typeof(string), typeof(ComboBoxEx),
                  	new FrameworkPropertyMetadata("The value is null"));

/// <summary>
/// Gets or sets the text that is used to represent 
/// a null value in selectionbox of the combobox.
/// This is a dependency property.
/// </summary>
[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.

XML
<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.

XML
<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:

NullItemTemplateThe template used to visualize a NULL value in the dropdown
NullValueTextThe text used to identify a null value in the dropdown
SelectionBoxNullItemTemplateThe template used to visualize a NULL value in the selection box.
SelectionBoxNullValueTextThe text used to identify a null value in the selection box.
HighlightedItemThe 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Norway Norway
I'm a 39 year old software developer living in Norway.
I'm currently working for a software company making software for the retail industry.

Comments and Discussions

 
QuestionThanks! Pin
Gary Wheeler22-Jul-14 5:08
Gary Wheeler22-Jul-14 5:08 
BugNot Windows theme-aware Pin
floele19-Nov-12 1:53
floele19-Nov-12 1:53 
QuestionIsEditable set true is not working as expected Pin
Member 235031112-Oct-12 11:34
Member 235031112-Oct-12 11:34 
Questionmissing c# file Pin
BlayneLack11-Apr-11 0:02
BlayneLack11-Apr-11 0:02 
AnswerRe: missing c# file Pin
seesharper11-Apr-11 2:50
seesharper11-Apr-11 2:50 
GeneralRe: missing c# file Pin
BlayneLack11-Apr-11 3:33
BlayneLack11-Apr-11 3:33 
GeneralMy vote of 4 Pin
SledgeHammer019-Apr-11 8:12
SledgeHammer019-Apr-11 8:12 
GeneralRe: My vote of 4 Pin
seesharper11-Apr-11 3:05
seesharper11-Apr-11 3:05 
GeneralMy vote of 5 Pin
Daniel Brännström9-Apr-11 5:48
Daniel Brännström9-Apr-11 5:48 
GeneralRe: My vote of 5 Pin
seesharper11-Apr-11 2:59
seesharper11-Apr-11 2:59 
Thanks a lot!!!

Regards

Bernhard Richter
NewsI think you have posted the same article twice! Pin
Venkatesh Mookkan8-Apr-11 16:37
Venkatesh Mookkan8-Apr-11 16:37 
GeneralRe: I think you have posted the same article twice! Pin
seesharper11-Apr-11 3:06
seesharper11-Apr-11 3:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.