1

I have an application that has a combobox bound to a viewmodel. I would like to perform validation on the selection, and if the user selects a choice that I don't want them to, I would like to provide feedback that the selection is invalid, and return the SelectedItem to a default value.

I am using IPropertyChanged, the combobox gets its items and selection from a ComboItem class via ObservableCollection, and the binding is working as expected.

The combobox has three items, each with an item number 0-2, and a description.
ComboBox Form Image:
ComboBox Form Image

The button on my WPF Test Form returns the value of the SelectedComboItem stored in the view model.

When an item is selected, validation is performed, and if the selected item number is 2 (Description Invalid), then an error window is displayed, and the SelectedComboItem is set back to its default value (hard coded as ComboItems[0] for simplicity).

        public ObservableCollection<ComboItem> ComboItems
    {
        get { return _comboItems; }
        set { _comboItems = value; OnPropertyChanged("ComboItems"); }
    }

    public ComboItem SelectedComboItem
    {
        get { return _selectedComboItem; }
        set { _selectedComboItem = ComboValidation(value); OnPropertyChanged("SelectedComboItem"); }
    }

//************************

    private ComboItem ComboValidation(ComboItem item)
    {
        if(item.Item == 2)
        {
            MessageBox.Show("This is an invalid selection", "ComboTest");
            return ComboItems[0];
        }
        return item;

When I select the valid items (Item 1, Item 2), the combobox reacts and the viewmodel data is as expected.
Item 2 Combo = ViewModel Selection:
Item 2 Combo = ViewModel Selection

However, when I select the invalid item (Item 3), the viewmodel SelectedComboItem property changes as expected, but the WPF combobox that it is bound to retains the invalid item.
WPF Combobox Shows Invalid, ViewModel Shows Default Value:
WPF Combobox Shows Invalid, ViewModel Shows Default Value

I assume what is happening is that the initial value is being stored for use by the form refresh, even though it happens after the validation and value change. How can I refresh the form to ensure that it refreshes to the validated data?

Test Application Here: https://github.com/jtprichard/WPFComboTest

EldHasp
  • 6,079
  • 2
  • 9
  • 24

1 Answers1

0

If you chose to implement validation your way you have to pay attention to some behind the scenes action. The fact that your solution is not that straightforward shows that you should go down the framework's predefined route, which is to implement INotifyDataErrorInfo on your view model.

The recommended pattern to design input validation is

  1. prevent invalid user input by e.g. grey-out/remove invalid selection options
  2. show the invalid input and indicate its invalidity by e.g. showing a red box around the input and, most important, let the user fix it. There is nothing worse than magically changing input (from user perspective). It feels like a bug. And in case the user didn't realize that the value has changed unexpectedly, the behavior can lead to weird application behavior (from a user perspective) at best or may damage some hardware (assuming the selected value is used to control a machine etc.) at worst.

Let's hope Tesla's autopilot configurator is not using your logic.

Strong recommendation is to implement INotifyDataErrorInfo!

Talking about TwoWay binding, you must know that the Selector (implementor of ItemsControl and provider of the SelectedItem property) will ignore the PropertyChanged event after setting the SelectedItem value. If it wouldn't, then this would introduce an infinite loop at worst but a redundant binding feedback at best: when the Selector sets the SelectedItem => trigger PropertyChanged on view model => Selector updates itself with the most likely same value - if value differs then the feedback loop continues.

In fact the Selector ignores the PropertyChanged after it has updated the binding source via TwoWay binding. The flow is: update Binding.Source by calling Set method and since it is a TwoWay binding, the Get method is invoked too, but the result is ignored.

To implement your not recommended validation logic, you therefore must wait until the full binding update pass Set->Get has completed.
You can do this by setting the validation result asynchronously using the Dispatcher:

public ComboItem SelectedComboItem
{
    get => _selectedComboItem; 
    set 
    { 
        _selectedComboItem = value;
        OnPropertyChanged(nameof(SelectedComboItem)); 
        Validate(value); 
    }
}

private bool Validate(ComboItem item)
{
    if(item.Item == 2)
    {
        Application.Current.Dispatcher.InvokeAsync(
            () => SelectedComboItem = ComboItems.FirstOrDefault());
        return false;
    }

    return true;
}
BionicCode
  • 1
  • 4
  • 28
  • 44