The problem
Imagine a ComboBox bound to some business objects:
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding Item, Mode=TwoWay}">
public ObservableCollection<Animal> Items
{
get { return new ObservableCollection<Animal>(Some animals…)}
}
public Animal Item{get;set;}
The state of the ComboBox is initially “no selection”. When the user selects an Item, the SelectedItem is set, but now the user can’t go back to “no selection” because no such item exists in the collection of business objects.
Having to add extra items to the ItemsSource in the ViewModel is a drag. I want to do that in XAML using a generic Converter.
ItemsSource="{Binding Items,Converter={StaticResource AddEmptyItemConverter}}"
My initial thought was to just add a Null value to the collection, but this results in a conversion error in the SelectedItem binding when trying to save the single Item back down to the ViewModel. This could be solved with another converter, but I want to do this with just one.
Using only one simple converter
The solution I found was to use dynamics and reflection to instantiate an Object of the correct type and add that to the collection.
public class AddEmptyItemConverter:IValueConverter
{
public object Convert(dynamic value, Type targetType, object parameter, CultureInfo culture)
{
if(value!=null)
if(value.GetType().IsGenericType)
if(value.Count>0)
value.Insert(0,Activator.CreateInstance(value[0].GetType()));
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
(You will need to import the CSharp.dll to get the dynamic keyword to work in Silverlight)
If the converter is used on an empty collection, nothing is added. This is great, because in that case the user can’t go away from “no selection” in the first place. (But it’s also reeeally lucky that this is the desired behavior, because I don’t know how I would figure out the type in that case :-) )
This "no selection" will appear as blank in the ComboBox regardless of the ItemTemplate or DisplayMemberPath because all of its properties are Null (or whatever the default constructor of the Business object feels like).
In the ViewModel, the empty object is passed around like a first class citizen and can be bound to other XAML controls. Obviously the logic needs to recognize a “blank” object as a “no selection” if needed.
How to display text in the "empty" Item
If you want some text in the “empty” option, you could use the Converter Parameter to get the converter to inject some text into the object, but that makes the converter less generic and I don’t want the text to get into the object might be pushed back into the ViewModel.
A better solution is to use the fact that we know a property will contain Null. For example the Name property and replace the Null value with some nice string in XAML. In my case, I want to change the Name of the animal in my ItemTemplate to the text "No animal at all!".
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Height="25" Text="{Binding Name}"/>
<TextBlock Height="25" Foreground="Blue" Text="{Binding Color}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
A nice and clean way of doing that (but WPF only) is by giving you textbox a style with a DataTrigger:
<TextBlock Height="25" Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Name}" Value="{x:Null}">
<Setter Property="Text" Value="No animal at all!"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
If you are using Silverlight, the datatriggers are not available, but Expression Blends standard “ChangePropertyAction” behavior can be used as an excellent substitute.
Use expression to drag the behavior onto a textbox in the ComboBox’s ItemTemplate and set the properties or import
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
and type the interaction trigger in the textbox by hand:
<TextBlock Height="25" Text="{Binding Name}">
<i:Interaction.Triggers>
<i:EventTrigger>
<i:Interaction.Behaviors>
<ei:ConditionBehavior>
<ei:ConditionalExpression>
<ei:ComparisonCondition LeftOperand="{Binding Name}" RightOperand="{x:Null}"/>
</ei:ConditionalExpression>
</ei:ConditionBehavior>
</i:Interaction.Behaviors>
<ei:ChangePropertyAction PropertyName="Text" Value="No animal at all"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
Tags: IValueConverter, silverlight, dynamics, wpf, reflection, xaml, combobox