6

I have something like it will pop to the user for getting confirmation of changes. If he clicks no I am setting the selectedValue in view model to the previous selection. But its not getting displayed correctly in view. Please help.

gowri
  • 61
  • 1
  • 2
  • .xaml Code : – gowri May 04 '11 at 07:28
  • .cs (ViewModel) ComSelectedValue = SomeCodeoftheItemsSource; Its not chaning the selected value in display – gowri May 04 '11 at 07:29
  • Do u call OnPropertyChanged after your set in the ViewModel? Your binding is not specifying an UpdateSourceTrigger? – Willem May 04 '11 at 07:39
  • I had called OnPropertyChanged and even with update source trigger its not working. One more info I am setting the same property to previous value on its property changed event. – gowri May 04 '11 at 09:08

8 Answers8

12

Very simple solution for .NET 4.5.1+:

<ComboBox SelectedItem="{Binding SelectedItem, Delay=10}" ItemsSource="{Binding Items}"  />

It's works for me in all cases. Just fire NotifyPropertyChanged without value assignment to rollback.

Dvor_nik
  • 1,091
  • 1
  • 10
  • 13
9

If the user clicks no and you try to revert the value and then call OnPropertyChanged, WPF will swallow the event since it is already responding to that event. One way to get around this is to use the dispatcher and call the event outside of the current event context.

System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => { OnPropertyChanged("ComSelectedValue"); }), null);
shaun
  • 91
  • 2
  • This worked, but I can't believe you have to do this. Surely there's a better method of doing this?? – Dan May 28 '15 at 18:32
  • Thank you! I can't believe I lost a day of work to this. I'm sorry I can only give one upvote! – Ben Jan 28 '19 at 12:56
4

WPF seems to validate that the bound property has changed before updating the UI. So simply invoking an NotifyPropertyChanged()/OnPropertyChanged() doesn't do the trick.

The problem is that since the property hasn't changed, WPF doesn't think it needs to refresh the combo box.

here is the incredibly hackish way I handle it in my ViewModels;

private int _someProperty = -1;
public int SomeProperty
{
    if (_someProperty != -1 && doYesNo() == Yes)
    {
       _someProperty = value;
    }
    else if (_someProperty != -1)
    {
       int oldValue = _someProperty;
       _someProperty = -1; // Set to a different/invalid value so WPF sees a change
       Dispatcher.BeginInvoke(new Action(() => { SomeProperty = oldValue; }));
    }
    else 
    {
       _someProperty = value;
    }
    NotifyPropertyChanged("SomeProperty");
}

Not pretty but it works reliably.

NebulaSleuth
  • 833
  • 1
  • 7
  • 18
0

What if you tried to raise the property changed event asynchronously? This is similar the examples from shaun and NebulaSleuth.

  public int SomeProperty
  {
     get { return m_someProperty; }
     set
     {
        if (value == m_someProperty)
           return;

        if (doYesNo() == No)
        {
           // Don't update m_someProperty but let the UI know it needs to retrieve the value again asynchronously.
           Application.Current.Dispatcher.BeginInvoke((Action) (() => NotifyOfPropertyChange("SomeProperty")));
        }
        else
        {
           m_someProperty = value;
           NotifyOfPropertyChange("SomeProperty");
        }
     }
  }
Joe
  • 1,043
  • 2
  • 12
  • 21
0

Here is the general flow that I use:

  1. I just let the change pass through the ViewModel and keep track of whatever's passed in before. (If your business logic requires the selected item to not be in an invalid state, I suggest moving that to the Model side). This approach is also friendly to ListBoxes that are rendered using Radio Buttons as making the SelectedItem setter exit as soon as possible will not prevent radio buttons from being highlighted when a message box pops out.
  2. I immediately call the OnPropertyChanged event regardless of the value passed in.
  3. I put any undo logic in a handler and call that using SynchronizationContext.Post() (BTW: SynchronizationContext.Post also works for Windows Store Apps. So if you have shared ViewModel code, this approach would still work).

    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        public List<string> Items { get; set; }
    
        private string _selectedItem;
        private string _previouslySelectedItem;
        public string SelectedItem
        {
            get
            {
                return _selectedItem;
            }
            set
            {
                _previouslySelectedItem = _selectedItem;
                _selectedItem = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
                }
                SynchronizationContext.Current.Post(selectionChanged, null);
            }
        }
    
        private void selectionChanged(object state)
        {
            if (SelectedItem != Items[0])
            {
                MessageBox.Show("Cannot select that");
                SelectedItem = Items[0];
            }
        }
    
        public ViewModel()
        {
            Items = new List<string>();
            for (int i = 0; i < 10; ++i)
            {
                Items.Add(string.Format("Item {0}", i));
            }
        }
    }
    
No Ordinary Love
  • 569
  • 1
  • 4
  • 15
0

I realize this is an old post but it seems no one has done this the right way. I used System.Interactivity.Triggers and Prism to process the SelectionChanged event and manually trigger the SelectedItem. This will prevent undesired Selected Item Changes in both the UI and the View-Model.

My view:

<Window x:Class="Lind.WPFTextBlockTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:vm="clr-namespace:Lind.WPFTextBlockTest"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Interactivity;assembly=Microsoft.Practices.Prism.Interactivity"
    Title="MainWindow" Height="649" Width="397">
<Window.DataContext>
    <vm:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
    <ComboBox ItemsSource="{Binding Data}" SelectedItem="{Binding SelectedData, Mode=OneWay}">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectionChanged">
                <prism:InvokeCommandAction Command="{Binding TryChangeSelectedData}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </ComboBox>
</StackPanel>

My View-Model (BindeableBase and DelegateCommand from Prism 5):

public class MainWindowViewModel : BindableBase
{
    public ObservableCollection<string> Data { get; private set; }
    private string selectedData;
    public string SelectedData
    {
        get { return selectedData; }
        set
        {
            SetProperty(ref selectedData, value);
        }
    }
    public DelegateCommand<SelectionChangedEventArgs> TryChangeSelectedData { get; private set; }
    public MainWindowViewModel()
    {
        Data = new ObservableCollection<string>() { "Foo", "Bar", "Dick", "Head" };
        SelectedData = Data.First();
        TryChangeSelectedData = new DelegateCommand<SelectionChangedEventArgs>(args =>
        {
            var newValue = args.AddedItems.Cast<string>().FirstOrDefault();
            if (newValue == "Dick")
                this.OnPropertyChanged(() => this.SelectedData);
            else
                SelectedData = newValue;
        });
    }
}
JasonLind
  • 370
  • 2
  • 13
0

Assumptions:
- You show a dialog box (with a message and OKCancel buttons) when a user selects some value from ComboBox.
- If user presses OK, everything is OK. :)
- If user presses Cancel, you say vmPropSelectedValue=previousValue.

This won't work. Why?

Don't have exact answer, but I believe when you show the dialog the system has just changed the selected value and has just notified the Source (via binding infrastructure) about the changed value . If at this moment (when source has control) you now change the value of ViewModel property from your VM code, which you expect would trigger OnPropertyChanged of INotifyPropertyChanged, which you expect would ask the WPF to update the target with your requested value. However, the WPF has not yet completed the cycle - its still waiting for the Source to return the control back to it. So it simply rejects your request (otherwise it would go in infinite loop).

If this was confusing, try this:
Cycle starts:
1. User changes value on UI. WPF changes target.
2. binding infrastructure requests Source to update itself.
3. Source updates itself (VM property).
4. Source returns control back to binding infra.
Cycle End.

Experts: Can't find some documentation in this regard. Above is my belief how things work. Please rectify if incorrect.


Short Answer:
AFAIK, this can't be done via pure VM code alone. You will have to put some code-behind code.
Here's one way: http://www.amazedsaint.com/2008/06/wpf-combo-box-cancelling-selection.html

publicgk
  • 3,170
  • 1
  • 26
  • 45
  • I second what publicgk says. You cannot change the value in the middle of a binding transaction. Even if you manage change the value (say through a OnPropertyChanging event) the UI will go ahead and ignore you anyway unless you unset and reset the DataContext which is nasty. So you have to get a bit dirty like in the link above which is actually as clean as it will get I think. – Dominic May 04 '11 at 09:54
0

In most WPF applications you bind a view model to the user interface with a TwoWay mode and then you're set to go.

However this goes against the typical user experience, where when you edit something and you don't save, you don't see that editing reflected throughout your entire application, even if you don't save your changes to the Database.

The mechanism available in WPF is the UpdateSourceTrigger property of the Binding. With this property you can control when the User Interface updates the ViewModel that it is bound to. This allows you to update only when the user saves what he's editing or something similar.

An example of a XAML Binding with the UpdateSourceTrigger set to Explicit:

"{Binding Path=Privado, UpdateSourceTrigger=Explicit, Mode=TwoWay}"

And when you want to really save to the ViewModel you call:

UpdateSource();
David Rodrigues
  • 622
  • 5
  • 8