I want to make a ComboBox in WPF that has one null
item on the top, when this gets selected, the SelectedItem should be set to null (reset to default state). I\
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
It is possible to reset the selection if you select an item.
<ComboBox x:Name="cb">
<ComboBox.Items>
<ComboBoxItem Content="(None)">
<ComboBoxItem.Triggers>
<EventTrigger RoutedEvent="Selector.Selected">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="cb" Storyboard.TargetProperty="SelectedItem">
<ObjectAnimationUsingKeyFrames Duration="0:0:0">
<DiscreteObjectKeyFrame Value="{x:Null}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ComboBoxItem.Triggers>
</ComboBoxItem>
<ComboBoxItem>First Item</ComboBoxItem>
<ComboBoxItem>Second Item</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
Unfortunately this will not work with ItemsSource
and a CompositeCollection to add this reset item to an arbitrary list. The reason is WPF can't resolve the Storyboard.TargetName
in this scope.
But maybe this helps you go on with retemplating the ComboBox
.
Here's the ultimate super-simple solution to this problem:
Instead of having an item with a value of null in your ItemsSource, use DbNull.Value as item or as the item's value property.
That's all. You're done. No value converters, no code-behind, no xaml triggers, no wrappers, no control descendants...
It simply works!
Here's a short example for binding enum values including a "null item":
Create your ItemsSource like this:
var enumValues = new ArrayList(Enum.GetValues(typeof(MyEnum)));
enumValues.Insert(0, DBNull.Value);
return enumValues;
Bind this to the ItemsSource of the ComboBox.
Bind the SelectedValue of your ComboBox to any Property having a Type of MyEnum? (i.e. Nullable<MyEnum>).
Done!
Background: This approach works because DbNull.Value is not the same like a C# null value, while on the other hand the framework includes a number of coercion methods to convert between those two. Eventually, this is similar to the mentioned "Null object pattern", but without the need for creating an individual null object and without the need for any value converters.
Think about implementing a Null Object Pattern for the "None" combobox item and add this item to your items list. Then implement custom logic for saving null object in that class, or just check if selected item is of NullItem type.
Please use the following code.
<ComboBoxItem IsSelected="{Binding ClearSelectedItems}">(None)</ComboBoxItem>
In the viewmodel, catch the "ClearSelectedItems" change notification and clear the SelectedItems of ItemsControl.
I used the following solution for a similar problem. It makes use of the Converter property of the binding to go back and forth between the internal representation (that null is a reasonable value) and what I want to appear in the ComboBox. I like that there's no need to add an explicit list in a model or viewmodel, but I don't like the fragile connection between the string literal in the converter and that in the ComboBox.
<ComboBox SelectedValue="{Binding MyProperty, Converter={x:Static Converters:MyPropertySelectionConverter.Instance}}" >
<ComboBox.ItemsSource>
<CompositeCollection>
<sys:String>(none)</sys:String>
<CollectionContainer Collection="{Binding Source={x:Static Somewhere}, Path=ListOfPossibleValuesForMyProperty}" />
</CompositeCollection>
</ComboBox.ItemsSource>
</ComboBox>
and then the converter looks like:
public class MyPropertySelectionConverter : IValueConverter
{
public static MyPropertySelectionConverter Instance
{
get { return s_Instance; }
}
public const String NoneString = "(none)";
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Object retval = value as MyPropertyType;
if (retval == null)
{
retval = NoneString;
}
return retval;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Object retval = null;
if (value is MyPropertyType)
{
retval = value;
}
else if (String.Equals(NoneString, value as String, StringComparison.OrdinalIgnoreCase))
{
retval = null;
}
else
{
retval = DependencyProperty.UnsetValue;
}
return retval;
}
private static MyPropertySelectionConverter s_Instance = new MyPropertySelectionConverter();
}