WPF: Cancel a user selection in a databound ListBox

mvvmwpfwpf-controls

How do I cancel a user selection in a databound WPF ListBox? The source property is set correctly, but the ListBox selection is out of sync.

I have an MVVM app that needs to cancel a user selection in a WPF ListBox if certain validation conditions fail. Validation is triggered by a selection in the ListBox, rather than by a Submit button.

The ListBox.SelectedItem property is bound to a ViewModel.CurrentDocument property. If validation fails, the setter for the view model property exits without changing the property. So, the property to which ListBox.SelectedItem is bound doesn't get changed.

If that happens, the view model property setter does raise the PropertyChanged event before it exits, which I had assumed would be enough to reset the ListBox back to the old selection. But that's not working–the ListBox still shows the new user selection. I need to override that selection and get it back in sync with the source property.

Just in case that's not clear, here is an example: The ListBox has two items, Document1 and Document2; Document1 is selected. The user selects Document2, but Document1 fails to validate. The ViewModel.CurrentDocument property is still set to Document1, but the ListBox shows that Document2 is selected. I need to get the ListBox selection back to Document1.

Here is my ListBox Binding:

<ListBox 
    ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
    SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

I did try using a callback from the ViewModel (as an event) to the View (which subscribes to the event), to force the SelectedItem property back to the old selection. I pass the old Document with the event, and it is the correct one (the old selection), but the ListBox selection doesn't change back.

So, how do I get the ListBox selection back in sync with the view model property to which its SelectedItem property is bound? Thanks for your help.

Best Answer

For future stumblers on this question, this page is what ultimately worked for me: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx

It's for a combobox, but works for a listbox just fine, since in MVVM you don't really care what type of control is calling the setter. The glorious secret, as the author mentions, is to actually change the underlying value and then change it back. It was also important to run this “undo” on a separate dispatcher operation.

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");
    }
}

Note: The author uses ContextIdle for the DispatcherPriority for the action to undo the change. While fine, this is a lower priority than Render, which means that the change will show in the UI as the selected item momentarily changing and changing back. Using a dispatcher priority of Normal or even Send (the highest priority) preempts the display of the change. This is what I ended up doing. See here for details about the DispatcherPriority enumeration.

Related Topic