While I agree there are plenty solutions to WPF ComboBox's null item issue, Andrei Zubov's reference to Null Object Pattern inspired me to try a less overkilling alternative, which consists on wrapping every source item allow with a null value (also wrapped) before injecting the whole wrapped collection into ComboBox.ItemsSource property. Selected item will be available into SelectedWrappedItem property.
So, first you define your generic Wrapper...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ComboBoxWrapperSample
{
/// <summary>
/// Wrapper that adds supports to null values upon ComboBox.ItemsSource
/// </summary>
/// <typeparam name="T">Source combobox items collection datatype</typeparam>
public class ComboBoxNullableItemWrapper<T>
{
string _nullValueText;
private T _value;
public T Value
{
get { return _value; }
set { _value = value; }
}
/// <summary>
///
/// </summary>
/// <param name="Value">Source object</param>
/// <param name="NullValueText">Text to be presented whenever Value argument object is NULL</param>
public ComboBoxNullableItemWrapper(T Value, string NullValueText = "(none)")
{
this._value = Value;
this._nullValueText = NullValueText;
}
/// <summary>
/// Text that will be shown on combobox items
/// </summary>
/// <returns></returns>
public override string ToString()
{
string result;
if (this._value == null)
result = _nullValueText;
else
result = _value.ToString();
return result;
}
}
}
Define your item model...
using System.ComponentModel;
namespace ComboBoxWrapperSample
{
public class Person : INotifyPropertyChanged
{
// Declare the event
public event PropertyChangedEventHandler PropertyChanged;
public Person()
{
}
// Name property
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
// Age property
private int _age;
public int Age
{
get { return _age; }
set
{
_age = value;
OnPropertyChanged("Age");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
// Don't forget this override, since it's what defines ao each combo item is shown
public override string ToString()
{
return string.Format("{0} (age {1})", Name, Age);
}
}
}
Define your ViewModel...
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace ComboBoxWrapperSample
{
public partial class SampleViewModel : INotifyPropertyChanged
{
// SelectedWrappedItem- This property stores selected wrapped item
public ComboBoxNullableItemWrapper<Person> _SelectedWrappedItem { get; set; }
public ComboBoxNullableItemWrapper<Person> SelectedWrappedItem
{
get { return _SelectedWrappedItem; }
set
{
_SelectedWrappedItem = value;
OnPropertyChanged("SelectedWrappedItem");
}
}
// ListOfPersons - Collection to be injected into ComboBox.ItemsSource property
public ObservableCollection<ComboBoxNullableItemWrapper<Person>> ListOfPersons { get; set; }
public SampleViewModel()
{
// Setup a regular items collection
var person1 = new Person() { Name = "Foo", Age = 31 };
var person2 = new Person() { Name = "Bar", Age = 42 };
List<Person> RegularList = new List<Person>();
RegularList.Add(person1);
RegularList.Add(person2);
// Convert regular collection into a wrapped collection
ListOfPersons = new ObservableCollection<ComboBoxNullableItemWrapper<Person>>();
ListOfPersons.Add(new ComboBoxNullableItemWrapper<Person>(null));
RegularList.ForEach(x => ListOfPersons.Add(new ComboBoxNullableItemWrapper<Person>(x)));
// Set UserSelectedItem so it targes null item
this.SelectedWrappedItem = ListOfPersons.Single(x => x.Value ==null);
}
// INotifyPropertyChanged related stuff
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
And, finnaly your View (ok, it's a Window)
<Window x:Class="ComboBoxWrapperSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboBoxWrapperSample"
xmlns:vm="clr-namespace:ComboBoxWrapperSample"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ignore="http://www.ignore.com"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance {x:Type vm:SampleViewModel}, IsDesignTimeCreatable=False}"
Title="MainWindow" Height="200" Width="300">
<StackPanel Orientation="Vertical" Margin="10">
<TextBlock Margin="0,10,0,0">Favorite teacher</TextBlock>
<ComboBox ItemsSource="{Binding ListOfPersons}"
SelectedItem="{Binding SelectedWrappedItem, Mode=TwoWay}">
</ComboBox>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<TextBlock>Selected wrapped value:</TextBlock>
<TextBlock Text="{Binding SelectedWrappedItem }" Margin="5,0,0,0" FontWeight="Bold"/>
</StackPanel>
</StackPanel>
</Window>
Reaching this point, did I mention that you could retrieve unwrapped selected item thru SelectedWrappedItem.Value property ?
Here you can get a working sample
Hope it helps someone else