The Case of the Confused ComboBox – A WPF/MVVM Bedtime Story | James Kovacs

:

Once upon a time there were four friends – two TextBoxes and two ComboBoxes. They all lived happily on a WPF Window bound together in blissful MVVM harmony. The TextBoxes were bound to the ViewModel’s TextBoxText property and the ComboBoxes to the ComboBoxSelectedItem property.

MVVM ComboBox

<Window x:Class="MvvmComboBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mvvm="clr-namespace:MvvmComboBox"
        Title="MVVM ComboBox" Width="300" Height="150">
    <Window.Resources>
        <mvvm:MainWindowViewModel x:Key="viewModel"/>
    </Window.Resources>
    <StackPanel DataContext="{StaticResource viewModel}">
        <TextBox Text="{Binding TextBoxText.Value}"/>
        <TextBox Text="{Binding TextBoxText.Value}"/>
        <ComboBox ItemsSource="{Binding ComboBoxItems}"
                SelectedItem="{Binding ComboBoxSelectedItem.Value}"/>
        <ComboBox ItemsSource="{Binding ComboBoxItems}"
                SelectedItem="{Binding ComboBoxSelectedItem.Value}"/>
    </StackPanel>
</Window>

TextBoxText and ComboBoxItem are both Observable<T> so that INotifyPropertyChanged notifications are broadcast properly to all interested parties.

public class Observable<T> : INotifyPropertyChanged where T:class {
    public event PropertyChangedEventHandler PropertyChanged = (o,e) => { };

    private T value;
    public T Value {
        get { return value; }
        set {
            if(this.value == value) return;
            this.value = value;
            PropertyChanged(this, new PropertyChangedEventArgs("Value"));
        }
    }
}

ASIDE: Notice the event definition in line 2. By assigned a NOP (no operation) lambda, I don’t have to check whether any listeners are registered for the event. I can simply fire the event and worse case scenario, I execute an empty function body rather than throwing a NullReferenceException. This is tidier in my opinion than the if(PropertyChanged != null) nonsense that we’ve been using for years.

The view model is similarly simple.

public class MainWindowViewModel {
    public MainWindowViewModel() {
        TextBoxText = new Observable<string>();
        ComboBoxSelectedItem = new Observable<string>();
    }

    public Observable<string> TextBoxText { get; set; }
    public Observable<string> ComboBoxSelectedItem { get; set; }

    public IEnumerable<string> ComboBoxItems {
        get {
            yield return "One";
            yield return "Two";
            yield return "Three";
            yield return "Four";
            yield return "Five";
        }
    }
}

With the INotifyPropetyChanged in place, changing one TextBox will cause its twin to display the same value and same for the two ComboBoxes.

MVVM ComboBox synchronized

We get a strange new requirement that every second change should be ignored. (The actual business requirement was more sensible. I was displaying a list of entities for editing. If the previous entity wasn’t saved, the user would be presented with a save dialog and standard “yes/no/cancel” options. On cancel, I wanted to disregard the ComboBox change and revert back to the previously selected entity.)

public class Observable<T> : INotifyPropertyChanged where T:class {
    public event PropertyChangedEventHandler PropertyChanged = (o,e) => { };

    private T value;
    private int count;
    public T Value {
        get { return value; }
        set {
            if(this.value == value) return;

            if(count++ % 2 == 0) return;

            this.value = value;
            PropertyChanged(this, new PropertyChangedEventArgs("Value"));
        }
    }
}

So based on business rules, we’ve ignored a change to the view model. The TextBoxes always remain synchronized – reverting back to the previous value if the change is ignored. Unfortunately the changed ComboBoxes becomes unsynchronized with the view model if the view model ignores a change.

MVVM ComboBox unsynchronized

If you examine the view model in the debugger, the TextBoxText is “Hello, world! Hello, again!” as expected. You can change either TextBox as many times as you want. Every other edit is ignored and causes its changes to revert, as expected. Examining the current state of the view model’s ComboBoxSelectedItem, its value is “Four” and not “Two”. I selected “Two” in the first ComboBox, which was the ignored change. The first ComboBox is now unsynchronized with the view model. If you change the selection again, both ComboBoxes have the correct value. The problem is only with changes ignored by the view model.

Let’s create a new type derived from ComboBox to fix this problem.

namespace JamesKovacs.MvvmComboBox {

 

    public class ComboBox : System.Windows.Controls.ComboBox {
        protected override void OnSelectionChanged(SelectionChangedEventArgs e) {
            base.OnSelectionChanged(e);
            var binding = GetBindingExpression(SelectedItemProperty);
            if(binding != null) {
                binding.UpdateTarget();
            }
        }

 

    }
}

I perform the normal OnSelectionChanged event and then update the ComboBox’s SelectedItemProperty from the bound ViewModel if a binding exists. This ensures that if the view model ignores changes or modifies the selection in some way, the ComboBox displays the correct value. Minor note: I am defining a derived ComboBox in my own namespace, which is also called ComboBox. I do this so that in my XAML, I can apply the fix by prepending <ComboBox/> with my namespace <mvvm:ComboBox/>. With this fix in place, my MVVM ComboBox remains synchronized with its view model.

image

In most cases, MVVM model binding dramatically simplifies your WPF code. Unfortunately there are some cases where bugs in the framework prevent it from working properly. Fortunately in many cases you can work around these limitations by simply deriving your own custom control from the one supplied with WPF to fix these problems.

For those of you who want to play around with the code, you can find it here. Happy coding!

ADDENDUM: As noted by Michael L. Perry in this comment, WPF behaviours can also be used to inject code without subclassing ComboBox. The code sample now includes his behaviour code in addition to the subclassing option. Also to note is that a bug in .NET 4.0 prevents the fix from working. The ComboBox (regulard, subclassed, or behavioured) becomes unsynchronized from the view model. The subclassing code works correctly in .NET 3.5. If there are any WPF gurus out there who can understand why it works in .NET 3.5, but not .NET 4.0, I would love to hear an explanation.