Search code examples
c#wpfbindingpropertychanged

WPF TwoWay Binding and PropertyChanged


So suppose there is an enum

    enum SampleEnum 
    {
        Item1,
        Item2
    }

Then there is a ComboBox

    <ComboBox ItemsSource="{Binding SomeItemSource}"    
        SelectedItem="{Binding 
            Path=ItemX,
            Mode=TwoWay,
            UpdateSourceTrigger=PropertyChanged,
            Converter={StaticResource ResourceKey=SomeConverter}}">

Combo box has a ViewModel as its DataContext

    ViewModel : INotifyPropertyChanged, ...
    {
        ...

        public SampleEnum ItemX
        {
            get => model.GetItemX();
            set
            {
                model.SetItemX(value);
                RaisePropertyChanged();
            }
        }

        ...
    }

And RaisePropertyChanged([CallerMemberName] string caller = "") invokes PropertyChanged with property's name.

But.

When I run my code, open my CheckBox, select one item, I get following behaviour: my code enters setter, sets value of the model, raises PropertyChanged, then my getter is invoked, value is retrieved, but it never reaches ComboBox. ComboBox displays the value I chose by hand, not the value returned by accessor.

E.g. if you rewrite get => SampleEnum.Item2 to return always the same value, ComboBox will still display the value I picked by my hand in UI, not the one that is returned by accessor, even though I am 100% sure that getter is invoked, than the value travels to Converter and Convrter also returns proper value.

But if RaisePropertyChanged(nameof(ItemX)) is invoked from any other place, ComboBox immediately retrieves the value from accessor and displays it.

In short, ComboBox ignores PropertyChanged if invoked from setter, but in any other case it works perfectly fine. Specifying directly the name of property (instead of relying on compiler services) or invoking several RasiePropertyChanged in a row in setter does not work.

In general, one should expect that the value selected in combo box and the one returned by getter are the same, but sometimes model can reject provided value and instead return a default one. Not the best behaviour, but it is possible. And in this case the user will be misinformed which item of a ComboBox is actually selected.

I am just wondering what is so special about this accessor that ComboBox ignores it.


Solution

  • tl;dr: What you're trying to do is data validation. That's a solved problem: You can implement validation in your viewmodel with IDataErrorInfo, or in your view with ValidationRules. Either one of those works with WPF instead of against it. Working against WPF is almost invariably losing proposition.

    You could also write an ItemContainerStyle for the ComboBox that disables invalid items, or your viewmodel could update the ComboBox item collection to exclude any items that are currently unselectable. I prefer that approach: Rather than "here, you can choose any of these options -- BZZZT, LOL, WRONG CHOICE!", it seems friendlier to present them only with the options that they can choose.

    And if you'll know which options are valid after they make their choice, you can almost certainly know beforehand as well.


    ComboBox ignores PropertyChanged if invoked from setter, but in any other case it works perfectly fine.

    That's correct. The ComboBox is still handling the change it got from the user, and won't be finished doing so until some time after your setter is complete. The ComboBox will ignore any PropertyChanged events on a property it's currently updating. This is by design.

    The standard workaround is to call BeginInvoke() with ApplicationIdle priority, and raise PropertyChanged in the delegate. That will have the effect of raising PropertyChanged again after the ComboBox is entirely finished with the current selection-change event. But that's nothing a viewmodel should ever be concerning itself with.

    As you say, "Not the best behaviour". It would be preferable to write validation which rejects the wrong value in the first place, rather than writing a strange workaround like the above.