-1

Under certain conditions if the user selects an item in a combobox, it automatically must be changed to another item

ViewModel

public class VM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    private string selected;
    public string Selected
    {
        get { return selected; }
        set
        {
            if (selected != value)
            {
                selected = value;
                OnPropertyChanged("Selected");
            }
        }
    }

    private ObservableCollection<string> collection;
    public ObservableCollection<string> Collection
    {
        get { return collection; }
        set
        {
            collection = value;
            OnPropertyChanged("Collection");
        }
    }

    public VM()
    {
        this.Collection = new ObservableCollection<string>(new string[] { "A", "B", "C" });
        this.Selected = "A";
        this.PropertyChanged += VM_PropertyChanged;
    }

    void VM_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        this.Selected = "C";
    }
}

View

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<StackPanel>
    <Grid>
      <ComboBox ItemsSource="{Binding Collection}" SelectedValue="{Binding Selected}"/>
    </Grid>
    <Label Content="{Binding Selected, UpdateSourceTrigger=PropertyChanged}"/>
 </StackPanel>
</Window>

So, in this example, no matter what do I select, it should show "C" both, on the combobox and Label, but "C" only shows on Label, it means that the ViewModel is updated but not the view.

It seems the problem here is to try to change the property from the PropertyChanged method.

enter image description here

What could be wrong?

The One
  • 4,560
  • 5
  • 36
  • 52
  • 1
    Was it just a copy/paste error that your viewmodel uses the property `Collection` whereas your view binds to `Coleccion`? – wablab Apr 25 '17 at 18:17
  • Why did you add `UpdateSourceTrigger=PropertyChanged` to bindings on properties that *cannot* update the source? Why do you expect your ComboBox to assign a new ObservableCollection to `Collection`? – 15ee8f99-57ff-4f92-890c-b56153 Apr 25 '17 at 18:19
  • @wablab Yes, original code is in spanish, thanks – The One Apr 25 '17 at 18:55
  • I know, you're right, I'm so used to write it like this, that it has become in a bad practice – The One Apr 25 '17 at 18:56
  • 1
    Not sure what your ultimate goal is, but perhaps you could implement and bind to `INotifyPropertyChanging.PropertyChanging` and perhaps set `Selected` in the event handler for that. I haven't tried this, so I don't know if it would help or not. And actually, you may not even need to bind to it; you might be able to handle it all within the viewmodel itself. (But if you're going to do that, then it might make sense to just change the value of `Selected` within the setter itself.) – wablab Apr 25 '17 at 19:05
  • Thanks @wablab, I didn't know about INotifyPropertyChanging, I followed the example in this [link](http://stackoverflow.com/questions/8577207/better-propertychanged-and-propertychanging-event-handling), it cancels the change correctly, I can set the Selected property to a new value, but the UI doesn't update – The One Apr 25 '17 at 19:36

2 Answers2

2

Under certain conditions if the user selects an item in a combobox, it automatically must be changed to another item

For what it's worth, I think it would be a good idea to revisit that design choice. It is likely to be confusing to users, and there is probably a better way to present that state of affairs to the user, than to ignore input they give the program. There's not enough context in your question to fully understand how you got into this situation in the first place, so I can't offer anything more than to suggest it's likely better to fix the design, than to finagle the code into doing what you want.

That said…

The issue you are running into is that WPF ignores property-changed events for the source of a binding it is currently already updating. In your scenario, the binding is updating the Selected value from its binding, and so changes to that property will be ignored until that binding update is completed.

There are a variety of ways to get the code to work the way you want. Probably the easiest is to simply defer the update of the source property until the handling of the user input has completed. You can do that by using the Dispatcher.InvokeAsync() method:

void VM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    Dispatcher.CurrentDispatcher.InvokeAsync(() => this.Selected = "C");
}

I'm not a big fan of the above, because of the fact that it takes what ideally should be a view-agnostic object, the view model, and injects knowledge of your specific view API, i.e. use of the Dispatcher object. That said, there are other mechanisms you could use which are similar, and which don't rely on the Dispatcher object (e.g. using an asynchronous timer).

There are a variety of other examples of ways to address this on Stack Overflow. For example, you might look at this answer for inspiration. I don't think it will do exactly what you want "straight out of the box", but the attached property approach might be something you find more appropriate, by moving the logic from view model to view code, and thus a place where it's more appropriate to use Dispatcher. (And arguably, if you are going to do something like this, the logic probably belongs in the view anyway…it's weird enough there, but I see no compelling reason this should be inherent in the view model.)

Another approach can be seen in the question Coerce a WPF TextBox not working anymore in .NET 4.0. I.e. manually force the view's state to be updated after the fact.

Community
  • 1
  • 1
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Just to explain a little further, I was showing a Popup dialog asking the user for information only after selecting certain item from the combobox only under certain conditions, if the user cancels the dialog then the selected item should revert to the previous one. You answer is excellent and I will redesign the logic in order to avoid leaving the right design track – The One Apr 25 '17 at 19:46
  • @Tuco: I see. Yes, at least if you are presenting the user with some notice that the setting is being reverted, that would avoid a lot of the user-confusion potential. – Peter Duniho Apr 25 '17 at 21:16
1

Here's how I would most likely do it, but the BeginInvoke() call that does the magic could just as easily be called from your PropertyChanged handler.

What it's doing is essentially queueing the action to happen after the entire property-set business has fully completed. The DispatcherPriority.ApplicationIdle flag is a key point.

As you've found, it's useless for the PropertyChanged handler and the property setter to raise PropertyChanged while the ComboBox is still in the process of changing its selection. This code lets that whole thing finish, and then immediately changes Selected to something else. At that point, the ComboBox will be at leisure to take notice of your PropertyChanged event and update its own selection.

private string selected;
public string Selected
{
    get { return selected; }
    set
    {
        if (selected != value)
        {
            //  Don't let them select "B". 
            if (value == "B")
            {
                Dispatcher.CurrentDispatcher.
                    BeginInvoke(new Action(() => this.Selected = "C"),
                                DispatcherPriority.ApplicationIdle);
                return;
            }
            selected = value;
            OnPropertyChanged("Selected");
        }
    }
}