Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / WPF

The Perils of Canceling WPF ComboBox Selection

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
21 Jun 2012CPOL8 min read 34.9K   475   9   9
How to deal with a stubborn combo box that won't let you cancel invalid user selection

"We meant to do better, but it came out as always."
V. Chernomyrdin , former Prime Minister of Russia

Summary

In our application we have a combo box that switches between different modes. Any unsaved work will be lost during the switch. So, if there are unsaved changes and the user selects new mode in the combo box, we want to

  1. Remember user's choice and return the selection back to current mode.
  2. Ask the user whether he indeed wants to switch mode and lose the changes.
  3. If the answer is no, pretend nothing happened. If the answer is yes, change combo box selection and switch the mode.

Reverting using selection on step 1 makes sense, because the application did not officialy switch modes until the user said "yes", and we want the combo box to correctly reflect current mode. Unfortunately, it turns out that if one uses MVVM and data binding, reverting user seletion in a combo box is difficult in .NET 3.5 and next to impossible in .NET 4.0.

More Details

If we use MVVM, we have three entities related to the combo box selection:

  1. Actual visual state displayed on screen.
  2. Value of ComboBox.SelectedItem property.
  3. Value of ViewModel.SelectedItem property tied to the combo box via binding.

In an ideal world all three should be synchronized at all times, with possible exception of very brief transition states. Unfortunately, the synchronization is broken in different ways in .NET 3.5. and .NET 4.0.

.NET 3.5

In .NET 3.5 the combo box will correctly synchronize the visual state and ComboBox.SelectedItem property. It will, however, ignore any updates to the ViewModel.SelectedItem made while processing user selection. This was presumably done to avoid an infinite loop of updates. The end result is that if the view model attempts to "correct" selected item, ViewModel.SelectedItem will be out of sync with ComboBox.SelectedItem and the actual visual state.

Binding behavior in .NET 3.5

.NET 4.0

In .NET 4.0 Microsoft tried to make our lives easier. Now the combo box will listen to view model changes, but unfortunately it will "forget" to update the actual visual state. In my opinion, this is worse than before, since by looking at the values the program can no longer detect that something is amiss. The combo box will report one thing to the application and show different thing to the user. This is not a Good Thing to do.

Binding behavior in .NET 4.0

Workaround for .NET 3.5 using BeginInvoke()

Evidently, the combo box usually does listen to view model updates, even in .NET 3.5, otherwise an MVVM application would never be able to set the selection programmatically. Roughly, the combo box turns "deaf" to view model updates for the duration of "selection changed" windows message, probably to prevent infinite loops. Once the selection message processing is finished, the combo box is willing to listen to updates again. Thus, possible work around is to defer the reversal of user selection until after curent windows message is processed via Dispatcher.BeginInvoke() call. I used this technique in my application until it was ported to .NET 4.

This workaround stops working in .NET 4, because the combo box now pretends to listen to current view model value. When BeginInvoke() is dispatched and the view model once again signals an update, the combo box sees that the view model state is the same as its internal state and does nothing, still leaving the visual state out of sync.

Demo Application

The demo application demonstrates the relationship between the visual state, the combo box object state and the view model state, which varies depending on the .NET version and what is done in the property setter. I used to it to research the issue and understand how the internals work.

Download ComboBoxSelectionCancel.zip (30K)

Application Screenshot

The applicaion uses MVVM approach (actually, it's just VVM, since the "model" class is not present). The combo box is defined in XAML as follows:

XAML
<ComboBox SelectedItem="{Binding SelectedItem, Mode=TwoWay}" ... />

The view model has SelectedItem property that is bound to the SelectedItem propery of the combo box:

C#
class MainViewModel
{
    public string SelectedItem 
    {
        get { ... }
        set { ... }
    }
}

The controls, from top to bottom are:

ControlComment
Current CLR versionRead only
The combo box 
Current value of ComboBox.SelectedItemRead only
Current value of MainViewModel.SelectedItemRead only
"Ignore value updates in setter" check boxWhen checked, the setter for MainViewModel.SelectedItem property will ignore requests to change the value.
"Use BeginInvoke()" check boxWhen checked, the setter will begin-invoke a deferred "view model selected item changed" notification.
"Throw exception in setter" check boxWhen checked, the setter will throw an exception. Some people alleged that this may cancel the combo box update. It does not.
"Set SelectedItem to" buttonCalls MainViewModel.SelectedItem setter with a value of the adjacent text box.
The log windowShows some events of interest as they occur in the application.
"Clear Log" buttonClears the log window.

.NET 3.5 Log

Here's what happens if we try to change selection from January to February under .NET 3.5 with "ignore value updates in setter" and "use BeginInvoke()" checked:

.NET 3.5 switch log

The first property changed notification (on line 3) is ignored by the combo box, but the one issued on line 5 via BeginInvoke() catches on, and the selection is changed back to January as we intended.

.NET 4 Log

If we do the same in .NET 4, the result is different.

.NET 4 switch log

The property changed notification on line 3 is no longer ignored, it is followed by the get_SelectedItem() call on line 4: the combo box reads selected item property back and sets its own SelectedItem value to January. This is repeated again as a result of BeginInvoke() on lines 5 and 6. So, the view model and the combo box control are now perfectly synchronized, but the actual visual state, as you can see, is still "February". This is, simply speaking, a bug in WPF.

Demo Application Guts

Most of the demo application is relatively straightforward. The most elaborated piece of code is the setter for SelectedItem property of the MainViewModel class that takes into account all the options we specified:

C#
set 
{
    Log.Write("MainViewModel.set_SelectedItem('" + value + "')");
 
    if (ThrowExceptionOnUpdate)
    {
        Log.Write("Throwing exception");
        throw new InvalidOperationException();
    }
 
    if (AreUpdatesIgnored)
    {
        Log.Write("Passed value ignored, MainViewModel.SelectedItem is still '" + _SelectedItem + "'");
    }
    else
    {
        _SelectedItem = value;
        Log.Write("MainViewModel.SelectedItem has been set to '" + _SelectedItem + "'");
    }
 
    if (UseBeginInvoke)
    {
        Action deferred = () => { RaisePropetryChanged("SelectedItem", true); };
        Dispatcher.CurrentDispatcher.BeginInvoke(deferred);
    }
 
    RaisePropetryChanged("SelectedItem", true);
    RaisePropetryChanged("SelectedItemForTextBlockDisplay", false);
}

Another little trick is that we use SelectedItemForTextBlockDisplay property instead of just SelectedItem to show view model selection state on screen. These two properties always return the same value. By having two properties instead of one we can distinguish property reads by the combo box, that go to SelectedItem and less interesting property reads by the auxilliary "ViewModel.SelectedItem is" text block, that go to SelectedItemForTextBlockDisplay.

How to Work Around Issues with Combo Box Selection in .NET 4

I pretty much gave up on canceling the selection as the old trick stopped working. From the other hand, if we just let it change this will have a bad effect on the rest of the application. The solution is to create a "double-buffer" property with two heads: the one facing the UI and the other facing the rest of the application. This complicates the application logic somewhat, but at least allows us to solve the problem. I created another sample for that:

Download ComboBoxSelectionDoubleBuffer.zip (23K)

Double Buffer Sample

DoubleBuffer<T>

The key part of this sample is DoubleBuffer<T> class. It contains two "sides" of a visible value. The UI binds to the UIValue property, while the rest of the application is interested in the Value property. Most of the time the two properties are the same, except for the transition period when the user is deciding whether he wants to go ahead with new selection or not.

C#
class DoubleBuffer<T> : NotifyPropertyChangedImpl
{
    /// <summary>
    /// UI-facing side of the buffer. Typically of no interest to the rest of the application
    /// </summary>
    public T UIValue { get; set; }
 
    /// <summary>
    /// Application facing side of the buffer
    /// </summary>
    /// <remarks>Use Assign() to assign values to this side</remarks>
    public T Value { get; set; }
 
    public event Action<T> UIValueChanged;
    public event Action<T> ValueChanged;
 
    /// <summary>
    /// Forces specific value into Value and UIValue
    /// </summary>
    public void Assign(T value);
 
    public void ConfirmUIChange();
    public void CancelUIChange();
}

Note that we do not actually cancel user selection: we just don't let it penetrate too deep into our application. On the screen shot above selected month is May, but we do not switch the calendar to May yet, and we revert the selection to January if the user says no.

Using The Double Buffered Property

The main combo box is defined in MainWindow.xaml simply as

XAML
<ComboBox ItemsSource="{Binding Months}" SelectedItem="{Binding SelectedMonth.UIValue}" />

The corresponding property in the MainViewModel class is defined as

C#
public DoubleBuffer<string> SelectedMonth { get; private set; } 

and is initialized like this:

C#
SelectedMonth = new DoubleBuffer<string>();
SelectedMonth.UIValueChanged += OnSelectedMonthChanging;
SelectedMonth.ValueChanged += OnSelectedMonthChanged;
SelectedMonth.Assign("January");

The "changing" handler shows user confirmation dialog, and the "changed" handler actually applies the change:

C#
private void OnSelectedMonthChanging(string toWhat)
{
    ConfirmationDialog.Show(
        "Are you sure you want to switch to " + toWhat + "?",
        SelectedMonth.ConfirmUIChange,
        SelectedMonth.CancelUIChange);
}
 
private void OnSelectedMonthChanged(string toWhat)
{
    Calendar = "Calendar for " + toWhat + " goes here";
}

A Remark On Style

The Combo Box Selection Double Buffer sample is a pure MVVM application. However, it is still just a sample. It uses a simplified version of DelegateCommand found in many MVVM toolkits, and a layered ConfirmationDialog. In real life I would probably use a more elaborated commanding solution from some toolkit, or interactions support from System.Interactions library, but I wanted to keep the application self contained. The version of ConfirmationDialog is also somewhat simplified compared to its real-life counterpartsto perform commanding, and , and on a more e

Conclusion

Dealing with something as fundamental as a combo box should not require a degree in quantum physics. Combo box control has always been an Achilles' foot of Windows GUI frameworks, it had bugs since old Win32 days. They way things worked in .NET 3.5 was not ideal, but .NET 4.0 made it even worse, despite good intentions. "Reading back" of the binding value appears to be implemented in a hurry: not only it broke the combo box behavior, it also broke behavior of OneWayToSource bindings. I really wish Microsoft had a better regression testing process.

References

  1. The Case of the Confused ComboBox – A WPF/MVVM Bedtime Story by James Kovacs.
  2. OneWayToSource Broken in .NET 4.0 on connect.microsoft.com.
  3. WPF 4.0 Data Binding Change (great feature) by Karl Shifflett.
    BTW, where is an official announcement on that from Microsoft? I could not find one in the list of WPF 4.0 changes.
  4. Viktor Chernomyrdin - a Wikipedia article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead Thomson Reuters
United States United States
Ivan is a hands-on software architect/technical lead working for Thomson Reuters in the New York City area. At present I am mostly building complex multi-threaded WPF application for the financial sector, but I am also interested in cloud computing, web development, mobile development, etc.

Please visit my web site: www.ikriv.com.

Comments and Discussions

 
QuestionView only solution in .NET 4.5.1 Pin
spasarto2-Sep-14 11:12
spasarto2-Sep-14 11:12 
QuestionIt works only while ConfirmationDialog.Show is not blocking Pin
Edward Pavlov16-Dec-13 22:18
Edward Pavlov16-Dec-13 22:18 
Try to replace it with standard messagebox and solution will fail. Very sorry.
QuestionCancel not working when executed directly in callback for DoubleBuffer.UIValueChanged Pin
Jonas1111-May-13 22:33
Jonas1111-May-13 22:33 
AnswerRe: Cancel not working when executed directly in callback for DoubleBuffer.UIValueChanged Pin
Ivan Krivyakov2-May-13 4:14
Ivan Krivyakov2-May-13 4:14 
QuestionAnother possible solution Pin
FatCatProgrammer22-Jun-12 3:13
FatCatProgrammer22-Jun-12 3:13 
AnswerRe: Another possible solution Pin
Ivan Krivyakov22-Jun-12 3:41
Ivan Krivyakov22-Jun-12 3:41 
GeneralRe: Another possible solution Pin
FatCatProgrammer22-Jun-12 17:24
FatCatProgrammer22-Jun-12 17:24 
AnswerRe: Another possible solution Pin
Narayana Palla29-Mar-13 10:42
Narayana Palla29-Mar-13 10:42 
Questionvery nice Pin
BillW3321-Jun-12 6:22
professionalBillW3321-Jun-12 6:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.