Cancel combobox selection in WPF with MVVM

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

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



        
12条回答
  •  遥遥无期
    2020-12-08 10:59

    I would like to complete splintor's answer because I stumbled upon a problem with the delayed initialization in OnSelectedItemChanged:

    When OnSelectedItemChanged is raised before AssociatedObject is assigned, using the System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke can have unwanted side effects, such as trying to initialize the newValue with the default value of the combobox selection.

    So even if your ViewModel is up to date, the behaviour will trigger a change from the ViewModel's SelectedItem current value to the default selection of the ComboBox stored in e.NewValue. If your code triggers a Dialog Box, the user will be warned of a change although there is none. I can't explain why it happens, probably a timing issue.

    Here's my fix

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Interactivity;
    
    namespace MyApp
    {
        internal class CancellableSelectionBehaviour : Behavior
        {
            protected override void OnAttached()
            {
                base.OnAttached();
    
                if (MustPerfomInitialChange)
                {
                    OnSelectedItemChanged(this, InitialChangeEvent);
                    MustPerfomInitialChange = false;
                }
    
                AssociatedObject.SelectionChanged += OnSelectionChanged;
            }
    
            protected override void OnDetaching()
            {
                base.OnDetaching();
    
                AssociatedObject.SelectionChanged -= OnSelectionChanged;
            }
    
            public static readonly DependencyProperty SelectedItemProperty =
                DependencyProperty.Register("SelectedItem", typeof(object), typeof(CancellableSelectionBehaviour),
                    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
    
            public object SelectedItem
            {
                get { return GetValue(SelectedItemProperty); }
                set { SetValue(SelectedItemProperty, value); }
            }
    
            public static readonly DependencyProperty IgnoreNullSelectionProperty =
                DependencyProperty.Register("IgnoreNullSelection", typeof(bool), typeof(CancellableSelectionBehaviour), new PropertyMetadata(true));
    
            /// 
            /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
            /// True by default.
            /// 
            public bool IgnoreNullSelection
            {
                get { return (bool)GetValue(IgnoreNullSelectionProperty); }
                set { SetValue(IgnoreNullSelectionProperty, value); }
            }
    
            /// 
            /// OnSelectedItemChanged can be raised before AssociatedObject is assigned so we must delay the initial change. 
            /// Using System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke has unwanted side effects.
            /// So we use this bool to know if OnSelectedItemChanged must be called afterwards, in OnAttached
            /// 
            private bool MustPerfomInitialChange { get; set; }
    
            /// 
            /// OnSelectedItemChanged can be raised before AssociatedObject is assigned so we must delay the initial change. 
            /// Using System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke has unwanted side effects.
            /// So we use this DependencyPropertyChangedEventArgs to save the argument needed to call OnSelectedItemChanged.
            /// 
            private DependencyPropertyChangedEventArgs InitialChangeEvent { get; set; }
    
            /// 
            /// Called when the SelectedItem dependency property is changed.
            /// Updates the associated selector's SelectedItem with the new value.
            /// 
            private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var behavior = (CancellableSelectionBehaviour)d;
    
                // OnSelectedItemChanged can be raised before AssociatedObject is assigned so we must delay the initial change.
                if (behavior.AssociatedObject == null)
                {
                    behavior.InitialChangeEvent = e;
                    behavior.MustPerfomInitialChange = true;               
                }
                else
                {
                    var selector = behavior.AssociatedObject;
                    selector.SelectedValue = e.NewValue;               
                }
            }
    
            /// 
            /// Called when the associated selector's selection is changed.
            /// Tries to assign it to the  property.
            /// If it fails, updates the selector's with   property's current value.
            /// 
            private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                if (IgnoreNullSelection && (e.AddedItems == null || e.AddedItems.Count == 0)) return;
                SelectedItem = AssociatedObject.SelectedItem;
                if (SelectedItem != AssociatedObject.SelectedItem)
                {
                    AssociatedObject.SelectedItem = SelectedItem;
                }
            }
        }
    }
    

提交回复
热议问题