Cancel combobox selection in WPF with MVVM

后端 未结 12 1450
你的背包
你的背包 2020-12-08 10:23

I\'ve got a combobox in my WPF application:



        
相关标签:
12条回答
  • 2020-12-08 11:00

    I found a much simpler answer to this question by user shaun on another thread: https://stackoverflow.com/a/6445871/2340705

    The basic problem is that the property changed event gets swallowed. Some would called this a bug. To get around that use BeginInvoke from the Dispatcher to force the property changed event to be put back onto the end of UI event queue. This requires no change to the xaml, no extra behavior classes, and a single line of code changed to the view model.

    0 讨论(0)
  • 2020-12-08 11:00

    --Xaml

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

    --ViewModel

    private object _SelectedItem;
    public object SelectedItem 
    {
        get { return _SelectedItem;}
        set {
               if(_SelectedItem == value)// avoid rechecking cause prompt msg
                { 
                   return;
                } 
                MessageBoxResult result = MessageBox.Show
                        ("Continue change?", MessageBoxButton.YesNo);
                if (result == MessageBoxResult.No)
                {
                    ComboBox combo = (ComboBox)sender;
                    handleSelection = false;
                    combo.SelectedItem = e.RemovedItems[0];
                    return;
                }
                _SelectedItem = value;
                RaisePropertyChanged(); 
            }
    }
    
    0 讨论(0)
  • 2020-12-08 11:04

    The problem is that once WPF updates the value with the property setter, it ignores any further property changed notifications from within that call: it assumes that they will happen as a normal part of the setter and are of no consequence, even if you really have updated the property back to the original value.

    The way I got around this was to allow the field to get updated, but also queue up an action on the Dispatcher to "undo" the change. The action would set it back to the old value and fire a property change notification to get WPF to realize that it's not really the new value it thought it was.

    Obviously the "undo" action should be set up so it doesn't fire any business logic in your program.

    0 讨论(0)
  • 2020-12-08 11:04

    Here is the general flow that I use (doesn't need any behaviors or XAML modifications):

    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));
              }
          }
      }
      
    0 讨论(0)
  • 2020-12-08 11:05

    I think the problem is that the ComboBox sets the selected item as a result of the user action after setting the bound property value. Thus the Combobox item changes no matter what you do in the ViewModel. I found a different approach where you don't have to bend the MVVM pattern. Here's my example (sorry that it is copied from my project and does not exactly match the examples above):

    public ObservableCollection<StyleModelBase> Styles { get; }
    
    public StyleModelBase SelectedStyle {
      get { return selectedStyle; }
      set {
        if (value is CustomStyleModel) {
          var buffer = SelectedStyle;
          var items = Styles.ToList();
          if (openFileDialog.ShowDialog() == true) {
            value.FileName = openFileDialog.FileName;
          }
          else {
            Styles.Clear();
            items.ForEach(x => Styles.Add(x));
            SelectedStyle = buffer;
            return;
          }
        }
        selectedStyle = value;
        OnPropertyChanged(() => SelectedStyle);
      }
    }
    

    The difference is that I completely clear the items collection and then fill it with the items stored before. This forces the Combobox to update as I'm using the ObservableCollection generic class. Then I set the selected item back to the selected item that was set previously. This is not recommended for a lot of items because clearing and filling the combobox is kind of expensive.

    0 讨论(0)
  • 2020-12-08 11:08

    To achieve this under MVVM....

    1] Have an attached behavior that handles the SelectionChanged event of the ComboBox. This event is raised with some event args that have Handled flag. But setting it to true is useless for SelectedValue binding. The binding updates source irrespective of whether the event was handled.

    2] Hence we configure the ComboBox.SelectedValue binding to be TwoWay and Explicit.

    3] Only when your check is satisfied and messagebox says Yes is when we perform BindingExpression.UpdateSource(). Otherwise we simply call the BindingExpression.UpdateTarget() to revert to the old selection.


    In my example below, I have a list of KeyValuePair<int, int> bound to the data context of the window. The ComboBox.SelectedValue is bound to a simple writeable MyKey property of the Window.

    XAML ...

        <ComboBox ItemsSource="{Binding}"
                  DisplayMemberPath="Value"
                  SelectedValuePath="Key"
                  SelectedValue="{Binding MyKey,
                                          ElementName=MyDGSampleWindow,
                                          Mode=TwoWay,
                                          UpdateSourceTrigger=Explicit}"
                  local:MyAttachedBehavior.ConfirmationValueBinding="True">
        </ComboBox>
    

    Where MyDGSampleWindow is the x:Name of the Window.

    Code Behind ...

    public partial class Window1 : Window
    {
        private List<KeyValuePair<int, int>> list1;
    
        public int MyKey
        {
            get; set;
        }
    
        public Window1()
        {
            InitializeComponent();
    
            list1 = new List<KeyValuePair<int, int>>();
            var random = new Random();
            for (int i = 0; i < 50; i++)
            {
                list1.Add(new KeyValuePair<int, int>(i, random.Next(300)));
            }
    
            this.DataContext = list1;
        }
     }
    

    And the attached behavior

    public static class MyAttachedBehavior
    {
        public static readonly DependencyProperty
            ConfirmationValueBindingProperty
                = DependencyProperty.RegisterAttached(
                    "ConfirmationValueBinding",
                    typeof(bool),
                    typeof(MyAttachedBehavior),
                    new PropertyMetadata(
                        false,
                        OnConfirmationValueBindingChanged));
    
        public static bool GetConfirmationValueBinding
            (DependencyObject depObj)
        {
            return (bool) depObj.GetValue(
                            ConfirmationValueBindingProperty);
        }
    
        public static void SetConfirmationValueBinding
            (DependencyObject depObj,
            bool value)
        {
            depObj.SetValue(
                ConfirmationValueBindingProperty,
                value);
        }
    
        private static void OnConfirmationValueBindingChanged
            (DependencyObject depObj,
            DependencyPropertyChangedEventArgs e)
        {
            var comboBox = depObj as ComboBox;
            if (comboBox != null && (bool)e.NewValue)
            {
                comboBox.Tag = false;
                comboBox.SelectionChanged -= ComboBox_SelectionChanged;
                comboBox.SelectionChanged += ComboBox_SelectionChanged;
            }
        }
    
        private static void ComboBox_SelectionChanged(
            object sender, SelectionChangedEventArgs e)
        {
            var comboBox = sender as ComboBox;
            if (comboBox != null && !(bool)comboBox.Tag)
            {
                var bndExp
                    = comboBox.GetBindingExpression(
                        Selector.SelectedValueProperty);
    
                var currentItem
                    = (KeyValuePair<int, int>) comboBox.SelectedItem;
    
                if (currentItem.Key >= 1 && currentItem.Key <= 4
                    && bndExp != null)
                {
                    var dr
                        = MessageBox.Show(
                            "Want to select a Key of between 1 and 4?",
                            "Please Confirm.",
                            MessageBoxButton.YesNo,
                            MessageBoxImage.Warning);
                    if (dr == MessageBoxResult.Yes)
                    {
                        bndExp.UpdateSource();
                    }
                    else
                    {
                        comboBox.Tag = true;
                        bndExp.UpdateTarget();
                        comboBox.Tag = false;
                    }
                }
            }
        }
    }
    

    In the behavior I use ComboBox.Tag property to temporarily store a flag that skips the rechecking when we revert back to the old selected value.

    Let me know if this helps.

    0 讨论(0)
提交回复
热议问题