Scenario

A user selects a new value from a WPF combo box. You ask if they are sure they want to do this. User says “No”.

WPF app is built with MVVM such that the combo box’s SelectedItem is bound to a property on the ViewModel. In the setter, you prompt the user and attempt to cancel the selection by discarding the new selected value.

Here’s the UI XAML

<Window x:Class="WpfComboBoxCancelSelect.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Cancellable Combo Box Demo" Height="158" Width="242"
>
    <Grid>
        <StackPanel>
            <Label>Normal - This doesn't work</Label>
            <ComboBox 
                HorizontalAlignment="Left" 
                VerticalAlignment="Top"
                ItemsSource="{Binding People}"
                SelectedItem="{Binding CurrentPerson, Mode=TwoWay}"
                DisplayMemberPath="Name"
            />
            <Label>Cancellable Combo - This works.</Label>
            <ComboBox 
                HorizontalAlignment="Left" 
                Name="comboBox2" 
                VerticalAlignment="Top"
                ItemsSource="{Binding People}"
                SelectedItem="{Binding CurrentPersonCancellable, Mode=TwoWay}"
                DisplayMemberPath="Name"
            />
        </StackPanel>
    </Grid>
</Window>

 

Here are screen shots of my sample UI

image

image 

Attempt #1 - The way you’d think it would work

You’d think that you could do something like this:

 

private Person _CurrentPerson;
public Person CurrentPerson
{
    get
    {
        Debug.WriteLine("Getting CurrentPerson.");
        return _CurrentPerson;
    }
    set
    {
        if (value == _CurrentPerson)
            return;

        if (
            MessageBox.Show(
                "Allow change of selected item?", 
                "Continue", 
                MessageBoxButton.YesNo
            ) != MessageBoxResult.Yes
        )
        {
            Debug.WriteLine("Selection Cancelled.");

            // You would think this should work, but it doesn't.
            // This is called within the same binding "context" 
            //  as that of the original setter.
            // The value needs to be changed back in a subsequent setter.
            OnPropertyChanged("CurrentPerson");
            return;
        }

        Debug.WriteLine("Selection applied.");
        _CurrentPerson = value;
        OnPropertyChanged("CurrentPerson");
    }
}

 

This doesn’t work. The ViewModel will have the values that you desire, but the UI will look like it applied the changes.

Attempt #2 – BeginInvoke() the PropertyChanged event

Based on info from this Stack Overflow question, I tried the following. This still didn’t work for me. (Note that I’m working with WPF 4.0 RTM).

private Person _CurrentPersonTry2;
public Person CurrentPersonTry2
{
    get
    {
        Debug.WriteLine("Getting CurrentPersonTry2.");
        return _CurrentPersonTry2;
    }
    set
    {
        if (value == _CurrentPersonTry2)
            return;

        if (
            MessageBox.Show(
                "Allow change of selected item?",
                "Continue",
                MessageBoxButton.YesNo
            ) != MessageBoxResult.Yes
        )
        {
            Debug.WriteLine("Selection Cancelled.");

            // Tell the UI that it should re-bind the value.
            // Do this after the UI has finished it's 
            // current context operation.
            Application.Current.Dispatcher.BeginInvoke(
                    new Action(() =>
                    {
                        Debug.WriteLine(
                            "Dispatcher BeginInvoke " +
                            "Setting CurrentPersonTry2."
                        );

                        OnPropertyChanged("CurrentPersonTry2");
                    }),
                    DispatcherPriority.ContextIdle,
                    null
                );

            return;
        }

        Debug.WriteLine("Selection applied.");
        _CurrentPersonTry2 = value;
        OnPropertyChanged("CurrentPersonTry2");
    }
}

The above code works just like the first version.

Attempt #3 – The final solution

On a whim, I figured I’d try another tweak. I tried letting the underlying value appear to change so that I could change it back to the original value on a BeginInvoke operation. For whatever reason, this does work.

private Person _CurrentPersonCancellable;
public Person CurrentPersonCancellable
{
    get
    {
        Debug.WriteLine("Getting CurrentPersonCancellable.");
        return _CurrentPersonCancellable;
    }
    set
    {
        // Store the current value so that we can 
        // change it back if needed.
        var origValue = _CurrentPersonCancellable;

        // If the value hasn't changed, don't do anything.
        if (value == _CurrentPersonCancellable)
            return;

        // Note that we actually change the value for now.
        // This is necessary because WPF seems to query the 
        //  value after the change. The combo box
        // likes to know that the value did change.
        _CurrentPersonCancellable = value;

        if (
            MessageBox.Show(
                "Allow change of selected item?", 
                "Continue", 
                MessageBoxButton.YesNo
            ) != MessageBoxResult.Yes
        )
        {
            Debug.WriteLine("Selection Cancelled.");

            // change the value back, but do so after the 
            // UI has finished it's current context operation.
            Application.Current.Dispatcher.BeginInvoke(
                    new Action(() =>
                    {
                        Debug.WriteLine(
                            "Dispatcher BeginInvoke " + 
                            "Setting CurrentPersonCancellable."
                        );

                        // Do this against the underlying value so 
                        //  that we don't invoke the cancellation question again.
                        _CurrentPersonCancellable = origValue;
                        OnPropertyChanged("CurrentPersonCancellable");
                    }),
                    DispatcherPriority.ContextIdle,
                    null
                );

            // Exit early. 
            return;
        }

        // Normal path. Selection applied. 
        // Raise PropertyChanged on the field.
        Debug.WriteLine("Selection applied.");
        OnPropertyChanged("CurrentPersonCancellable");
    }
}

 

The important change was to Actually change the underlying value and then change it back. It was also important to run this “undo” on a separate dispatcher operation.

Closing Thoughts

I’ve attached the sample solution if you want to see this in action.

 

I’m not sure what I think about this overall. It sort of makes sense once you get it working, but this is definitely not intuitive. I would think that simply raising PropertyChanged would tell WPF that to re-query the value. It should detect this and undo the change in the combo box. Doing this on a separate Dispatcher operation is tolerable, but having to make it look like the value actually took and then undoing it seems like it crosses the line of sanity.

Hopefully someone can show me a better way. For now, this works.

Cheers!