Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deselecting ComboBoxItems in MVVM

Tags:

mvvm

wpf

I am using a standard wpf/mvvm application where i bind combo boxes to collections on a ViewModel.

I need to be able to de-select an item from the dropdown. Meaning, users should be able to select something, and later decide that they want to un-select it (select none) for it. the problem is that there are no empty elements in my bound collection

my initial thought was simply to insert a new item in the collection which would result having an empty item on top of the collection.

this is a hack though, and it affects all code that uses that collection on the view model.

for example if someone was to write

_myCollection.Frist(o => o.Name == "foo") 

this will throw a null reference exception.

possible workaround is:

_myCollection.Where(o => o != null).First(o => o.Name == "foo");

this will work, but no way to ensure any future uses of that collection won't cause any breaks.

what's a good pattern / solution for being able to adding an empty item so the user can de-select. (I am also aware of CollectionView structure, but that seems like a overkill for something so simple)

Update

went with @hbarck suggestion and implemented CompositeCollection (quick proof of concept)

    public CompositeCollection MyObjects {
        get {
            var col = new CompositeCollection();

            var cc1 = new CollectionContainer();
            cc1.Collection = _actualCollection;

            var cc2 = new CollectionContainer();
            cc2.Collection = new List<MyObject>() { null }; // PROBLEM

            col.Add(cc2);
            col.Add(cc1);
            return col;
        }
    }

this code work with existing bindings (including SelectedItem) which is great.

One problem with this is, that if the item is completely null, the SelectedItem setter is never called upon selecting it.

if i modify that one line to this:

            cc2.Collection = new List<MyObject>() { new MyObject() }; // PROBLEM

the setter is called, but now my selected item is just a basic initialized class instead of null.. i could add some code in the setter to check/reset, but that's not good.

like image 230
Sonic Soul Avatar asked Sep 28 '12 16:09

Sonic Soul


2 Answers

I think the easiest way would be to use a CompositeCollection. Just append your collection to another collection which only contains the empty item (null or a placeholder object, whatever suites your needs), and make the CompositeCollection the ItemsSource for the ComboBox. This is probably what it is intended for.

Update:

This turns out to be more complicated than I first thought, but actually, I came up with this solution:

<Window x:Class="ComboBoxFallbackValue"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:t="clr-namespace:TestWpfDataBinding"
    xmlns:s="clr-namespace:System;assembly=mscorlib"
    xmlns:w="clr-namespace:System.Windows;assembly=WindowsBase"
Title="ComboBoxFallbackValue" Height="300" Width="300">
<Window.Resources>
    <t:TestCollection x:Key="test"/>
    <CompositeCollection x:Key="MyItemsSource">
        <x:Static Member="t:TestCollection.NullItem"/>
        <CollectionContainer Collection="{Binding Source={StaticResource test}}"/>
    </CompositeCollection>
    <t:TestModel x:Key="model"/>
    <t:NullItemConverter x:Key="nullItemConverter"/>
</Window.Resources>
<StackPanel>
    <ComboBox x:Name="cbox" ItemsSource="{Binding Source={StaticResource MyItemsSource}}" IsEditable="true" IsReadOnly="True" Text="Select an Option" SelectedItem="{Binding Source={StaticResource model}, Path=TestItem, Converter={StaticResource nullItemConverter}, ConverterParameter={x:Static t:TestCollection.NullItem}}"/>
    <TextBlock Text="{Binding Source={StaticResource model}, Path=TestItem, TargetNullValue='Testitem is null'}"/>
</StackPanel>

Basically, the pattern is that you declare a singleton NullInstance of the class you use as items, and use a Converter which converts this instance to null when setting the VM property. The converter can be written universally, like this (it's VB, I hope you don't mind):

Public Class NullItemConverter
Implements IValueConverter

Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
    If value Is Nothing Then
        Return parameter
    Else
        Return value
    End If
End Function

Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
    If value Is parameter Then
        Return Nothing
    Else
        Return value
    End If
End Function

End Class

Since you can reuse the converter, you can set this all up in XAML; the only thing that remains to be done in code is to provide the singleton NullItem.

like image 180
hbarck Avatar answered Sep 30 '22 20:09

hbarck


Personally, I tend to add an "empty" version of whatever object is in my collection I'm binding to. So, for example, if you're binding to a list of strings, then in your viewmodel, insert an empty string at the beginning of the collection. If your Model has the data collection, then wrap it with another collection in your viewmodel.

MODEL:

public class Foo
{
    public List<string> MyList { get; set;}
}

VIEW MODEL:

public class FooVM
{
    private readonly Foo _fooModel ;

    private readonly ObservableCollection<string> _col;
    public ObservableCollection<string> Col // Binds to the combobox as ItemsSource
    {
        get { return _col; }
    }

    public string SelectedString { get; set; } // Binds to the view

    public FooVM(Foo model)
    {
        _fooModel = model;
        _col= new ObservableCollection<string>(_fooModel.MyList);
        _col.Insert(0, string.Empty);
    }
}
like image 45
Thelonias Avatar answered Sep 30 '22 21:09

Thelonias