I have recently done some line of business application development and I really needed a nullable combobox, by this I mean a combobox which can also be set to an empty value which is represented by null in the underlying model.
For example I have Student class and it can have a relationship with a Coach class. A student can have a Coach but it is not mandatory. If i want to display the possible Coaches in a combobox in my userinterface it should be possible to select an "empty" / "null" item which corresponds to null.
It's unfortunate that this kind of frequently used functionality in LOB applications is not build into the standard combobox. After searching the internet I was surprised that I could not find any nice solutions except for inserting an extra (sentinel) dummy item into the list witch represents the empty / null value. I don't like this solution because you have to add these values into the lists and filter them out afterwards or even store them in the database! I wanted a nice clean solution which supports 2-way binding without much effort.
Well I first enhanced the normal combobox with the SelectedValuePath and SelectedValue properties like in WPF (you can find some examples on the internet):
public class EnhancedComboBox : ComboBox
{
public static readonly DependencyProperty SelectedValuePathProperty = DependencyProperty.Register("SelectedValuePath", typeof(string), typeof(EnhancedComboBox), new PropertyMetadata(new PropertyChangedCallback(SelectedValuePathPropertyChanged)));
public static readonly DependencyProperty SelectedValueProperty = DependencyProperty.Register("SelectedValue", typeof(object), typeof(EnhancedComboBox), new PropertyMetadata(new PropertyChangedCallback(SelectedValuePropertyChanged)));
public EnhancedComboBox()
: base()
{
base.SelectionChanged += new SelectionChangedEventHandler(ComboBoxClassic_SelectionChanged);
}
protected void ComboBoxClassic_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (SelectedItem != null && !string.IsNullOrEmpty(SelectedValuePath))
{
SetValue(EnhancedComboBox.SelectedValueProperty, SelectedItem.GetType().GetProperty(SelectedValuePath).GetValue(SelectedItem, null));
}
}
static void SelectedValuePathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
static void SelectedValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EnhancedComboBox cmb = (EnhancedComboBox)d;
foreach (var item in cmb.Items)
{
var currentValue = item.GetType().GetProperty(cmb.SelectedValuePath).GetValue(item, null);
if (currentValue == null && e.NewValue == null)
{
cmb.SelectedItem = item;
return;
}
if (currentValue != null)
{
if (currentValue.Equals(e.NewValue))
{
cmb.SelectedItem = item;
return;
}
}
}
cmb.SelectedIndex = -1;
}
public string SelectedValuePath
{
get { return (string)GetValue(EnhancedComboBox.SelectedValuePathProperty); }
set { SetValue(EnhancedComboBox.SelectedValuePathProperty, value); }
}
public object SelectedValue
{
get { return GetValue(SelectedValueProperty); }
set { SetValue(EnhancedComboBox.SelectedValueProperty, value); }
}
[/code]
Now the trick is to make an new List of Name/Value pairs in which the Name is displayed in the combobox and the value is the underlying class. If we use this construction we can add an extra item to this list which containts the null item (set to null) with a corresponding name. For the namevalue pairs I use a class ComboBoxItem and for the complete list I use a ComboBoxItemsList class.
[code:c#]
namespace NullableComboBoxExample.Controls
{
/// <summary>
/// Item om in combobox te tonen bij nullable lijsten
/// </summary>
public class ComboBoxItem
{
/// <summary>
/// The string show in the listbox (DisplayMemberPath)
/// </summary>
public string DisplayString { get; set; }
/// <summary>
/// The value used when selected (SelectedValuePath)
/// </summary>
public object ItemValue { get; set; }
}
}
using System.Collections.Generic;
namespace NullableComboBoxExample.Controls
{
/// <summary>
/// Lijst van items om in combobox te tonen bij nullable lijsten
/// </summary>
/// <typeparam name="T"></typeparam>
public class ComboBoxItemList<T> : List<ComboBoxItem>
{
public delegate string GetDisplayString(T t);
public ComboBoxItemList(bool addEmptyItem, IEnumerable<T> items, GetDisplayString getDisplayString)
{
if (addEmptyItem)
{
Add(new ComboBoxItem { DisplayString = "<None>", ItemValue = null });
}
if (items != null)
{
foreach (T item in items)
{
Add (new ComboBoxItem { DisplayString = getDisplayString(item), ItemValue = item});
}
}
}
}
}
[/code]
Well the binding in XAML should be done to the properties of the new ComboBoxItem class like this:
[code:c#]
<controls:EnhancedComboBox x:Name="cmbCoach"
DisplayMemberPath="DisplayString"
SelectedValuePath="ItemValue"
SelectedValue="{Binding Path=Coach, Mode=TwoWay}">
[/code]
The ComboBoxListItem class containt a nice constructor in which you can specify if you want to insert a nullable value and how you want to display the string in the combobox:
[code:c#]
// Build a new list to bind to combobox, with DisplayStrings and Values
ComboBoxItemList<Coach> comboListCoaches = new ComboBoxItemList<Coach>(true,
coaches,
c => c.LastName + ", " + c.FirstName);
// Setup itemsource of combobox
cmbCoach.ItemsSource = comboListCoaches;
[/code]
That's all you need to do, to have fully 2-way binding with nullable items. You can even insert extra nullable items into the list. I have attached a fully working example in VS2008.
NullableComboBoxExample.zip (34.76 kb)