Quantcast
Viewing all articles
Browse latest Browse all 18

Building a Nullable ComboBox

I like WPF. It has a bit of a learning curve, but once you get over it, you have an extremely extensible UI model that you can tweak very deeply. Unfortunately, this mindset made Microsoft leave many of the built-in controls with some missing functionality, apparently with the intention that people extend it if they felt like it.

One painfully missing piece, for me, was the ability to clear a ComboBox’s selection back to null, for a non-mandatory selection. You can add a Null value to the ComboBox’s ItemSource, but you can’t select it – selecting a null-backed value will simply return the last-selected value.

After viewing some less than optimal options on StackOverflow, I decided to roll my own clearable, nullable ComboBox, using a combination of two techniques – editing the ComboBox’s ControlTemplate to add a little “x” button to the SelectionBox, and a set of Attached Properties to apply the logic to the ComboBox in question.


1. Styling the ComboBox

The first step is messing with the control’s template. If you haven’t done this before, you should probably start with a Primer on the subject.

Anyway, this is a part of the default template for a ComboBox, which I extracted using Blend (Visual Studio 2012 and up can do it too. This is the main template:

1 <ControlTemplate TargetType="{x:Type ComboBox}"> 2 <Grid x:Name="MainGrid" SnapsToDevicePixels="true"> 3 <Grid.ColumnDefinitions> 4 <ColumnDefinition Width="*"/> 5 <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/> 6 </Grid.ColumnDefinitions> 7 <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom"> 8 <!-- Pop-related stuff --> 9 </Popup> 10 <ToggleButton BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/> 11 <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> 12 </Grid> 13 </ControlTemplate>

What you can see here is that the ComboBox is comprised of three parts. The Popup, which is irrelevant to us, a ToggleButton, which is what expands and collapse the combobox, and the ContentPresenter which displays the current selection. All of this is inside a Grid.

Now we’ll add our little Clear button into that mix. We’ll add a third GridColumn for the button to fit into, and put our button there:

1 <ControlTemplate TargetType="{x:Type ComboBox}"> 2 <Grid x:Name="MainGrid" SnapsToDevicePixels="true"> 3 <Grid.ColumnDefinitions> 4 <ColumnDefinition Width="*"/> 5 <ColumnDefinition Width="25"/> 6 <ColumnDefinition MinWidth="{DynamicResource {x:Static SystemParameters.VerticalScrollBarWidthKey}}" Width="0"/> 7 </Grid.ColumnDefinitions> 8 <Popup x:Name="PART_Popup" AllowsTransparency="true" Grid.ColumnSpan="2" IsOpen="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" PopupAnimation="{DynamicResource {x:Static SystemParameters.ComboBoxPopupAnimationKey}}" Placement="Bottom"> 9 <!-- Pop-related stuff --> 10 </Popup> 11 <ToggleButton Grid.ColumnSpan="3" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ComboBoxReadonlyToggleButton}"/> 12 <Button Grid.Column="1" 13 Style="{StaticResource ClearButtonStyle}" 14 Command="{TemplateBinding NullableSelector.NullifyCommand}" 15 Visibility="{TemplateBinding IsNullable, Converter={StaticResource BooleanToVisibilityConverter}}"/> 16 <ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" Content="{TemplateBinding SelectionBoxItem}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> 17 </Grid> 18 </ControlTemplate>

My changes here: the new ColumnDefinition, setting the existing ToggleButton’s ColumnSpan to the entire, expanded grid, and our new little Button, with a nice little style, which actually nullifies the combobox. We’ll ignore the two bindings there, for Command and Visibility, and focus on the styling, again changing the ControlTemplate for an unintrusive experience:

1 <ControlTemplate> 2 <Canvas Height="20" Width="20"> 3 <Rectangle Height="20" Width="20" Fill="White" Opacity="0.01"></Rectangle> 4 <Line x:Name="Line1" X1="6" Y1="6" X2="14" Y2="14" Stroke="DarkGray" StrokeThickness="2"></Line> 5 <Line x:Name="Line2" X1="14" Y1="6" X2="6" Y2="14" Stroke="DarkGray" StrokeThickness="2"></Line> 6 </Canvas> 7 <ControlTemplate.Triggers> 8 <Trigger Property="IsMouseOver" Value="True"> 9 <Setter Property="Shape.Stroke" TargetName="Line1" Value="Gray" /> 10 <Setter Property="Shape.Stroke" TargetName="Line2" Value="Gray" /> 11 </Trigger> 12 </ControlTemplate.Triggers> 13 </ControlTemplate>

Barring other stylings, we should have a combobox that looks like this:

Image may be NSFW.
Clik here to view.
image

Which is very nice and all, but the button doesn’t actually do anything, does it? Now let’s go handle the logic.

2. Attaching the Logic

Now that we have a Button that’s meant to clear the selection from the ComboBox, we now need to bind it to a Command that actually performs the selection-clearing itself.

Trying to adhere to the MVVM pattern, we try to avoid code-behind as much as possible, so we won’t add any explicit click-handlers for this button. We could inherit the default ComboBox and add the Clear command to bind to, but that can open up other problems (with default styles, for instance) and make life more difficult.

The approach I like is to use Attached Properties to add new logic to existing controls. In this case, we’ll add a new Attached Property called IsNullable, which you might have noticed in our earlier Visibility bindings.

1 public class NullableSelector 2 { 3 public static readonly DependencyProperty IsNullableProperty 4 = DependencyProperty.RegisterAttached( 5 "IsNullable", 6 typeof (bool), 7 typeof (NullableSelector), 8 new PropertyMetadata(default(bool), OnIsNullableChanged)); 9 10 public static void SetIsNullable(DependencyObject element, bool value) 11 { 12 element.SetValue(IsNullableProperty, value); 13 } 14 public static bool GetIsNullable(DependencyObject element) 15 { 16 return (bool)element.GetValue(IsNullableProperty); 17 } 18 }

The first three parts are standard-issue Attached Properties, auto-generated by Resharper. The only manual change is to add the OnIsNullableChanged event handler, which fires when the value changes.

The event handler ensures that we’re dealing with a Selector control (the base class of ComboBox, ListBox and other items with a SelectedItem property) and creates an ICommand implementation which sets the SelectedItem to Null.

Here’s the Command, very straightforward, initialize in constructor, nullify in Execute:

1 private class NullifyCommand : ICommand 2 { 3 private readonly Selector _selectorControl; 4 5 public NullifyCommand(Selector selectorControl) 6 { 7 _selectorControl = selectorControl; 8 } 9 public void Execute(object parameter) 10 { 11 _selectorControl.SelectedItem = null; 12 } 13 14 public bool CanExecute(object parameter) 15 { return true; } 16 17 public event EventHandler CanExecuteChanged; 18 }

And here’s the OnIsNullableChanged handler, which creates the NullifyCommand object for the ComboBox or Selector it’s attached to, and saves it in a second Attached Property called NullifyCommand – the one that the Button’s Command is bound to:

1 private static void OnIsNullableChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) 2 { 3 var selector = dependencyObject as Selector; 4 if (selector == null) 5 return; 6 7 if ((bool)e.NewValue) 8 { 9 selector.SetValue(NullifyCommandProperty, new NullifyCommand(selector)); 10 } 11 else 12 { 13 selector.ClearValue(NullifyCommandProperty); 14 } 15 } 16 17 public static readonly DependencyProperty NullifyCommandProperty 18 = DependencyProperty.RegisterAttached( 19 "NullifyCommand", typeof (ICommand), 20 typeof (NullableSelector), 21 new PropertyMetadata(default(ICommand))); 22 23 public static void SetNullifyCommand(DependencyObject element, ICommand value) 24 { 25 element.SetValue(NullifyCommandProperty, value); 26 } 27 28 public static ICommand GetNullifyCommand(DependencyObject element) 29 { 30 return (ICommand) element.GetValue(NullifyCommandProperty); 31 }

And now all we have to do is add a ComboBox to our WPF app, set its style to our style, and add the attached property:

1 <ComboBox 2 Style="{StaticResource NullableComboBoxStyle}" 3 bits:NullableSelector.IsNullable="True">

3. Summary

Is this solution simple and straightforward? Well, no. Few things in WPF are. It is, I feel, a bit cleaner and more MVVMish than other solutions I’ve found, and in my case, I already had a styled ComboBox ControlTemplate to add it to.

I’d appreciate any comments or improvements I hadn’t thought of.

I’ve also attached the full project file, including the NullableSelector class and the NullableComboBoxStyle. Let me know if you find it useful.

Attachment: NullableComboBoxDemo.zip


Viewing all articles
Browse latest Browse all 18

Trending Articles