"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
- Remember user's choice and return the selection back to current mode.
- Ask the user whether he indeed wants to switch mode and lose the changes.
- 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:
- Actual visual state displayed on screen.
- Value of
ComboBox.SelectedItem
property.
- 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.
.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.
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)
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:
<ComboBox SelectedItem="{Binding SelectedItem, Mode=TwoWay}" ... />
The view model has SelectedItem
property that is bound to the SelectedItem
propery
of the combo box:
class MainViewModel
{
public string SelectedItem
{
get { ... }
set { ... }
}
}
The controls, from top to bottom are:
Control | Comment |
Current CLR version | Read only |
The combo box | |
Current value of ComboBox.SelectedItem | Read only |
Current value of MainViewModel.SelectedItem | Read only |
"Ignore value updates in setter" check box | When checked, the setter for MainViewModel.SelectedItem property will ignore requests to change the value. |
"Use BeginInvoke()" check box | When checked, the setter will begin-invoke a deferred "view model selected item changed"
notification. |
"Throw exception in setter" check box | When checked, the setter will throw an exception. Some people alleged that this may cancel
the combo box update. It does not. |
"Set SelectedItem to" button | Calls MainViewModel.SelectedItem setter with a value of the adjacent text box. |
The log window | Shows some events of interest as they occur in the application. |
"Clear Log" button | Clears 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:
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.
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:
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)
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.
class DoubleBuffer<T> : NotifyPropertyChangedImpl
{
public T UIValue { get; set; }
public T Value { get; set; }
public event Action<T> UIValueChanged;
public event Action<T> ValueChanged;
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
<ComboBox ItemsSource="{Binding Months}" SelectedItem="{Binding SelectedMonth.UIValue}" />
The corresponding property in the MainViewModel
class is defined as
public DoubleBuffer<string> SelectedMonth { get; private set; }
and is initialized like this:
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:
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
- The Case of the Confused ComboBox – A WPF/MVVM Bedtime Story by
James Kovacs.
- OneWayToSource Broken in .NET 4.0
on
connect.microsoft.com
.
- 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.
- Viktor Chernomyrdin - a Wikipedia article.