使用 MVVM 取消 WPF 中的组合框选择

发布于 2024-12-11 00:29:33 字数 1348 浏览 0 评论 0原文

我的 WPF 应用程序中有一个组合框:

<ComboBox  ItemsSource="{Binding CompetitorBrands}" DisplayMemberPath="Value" 
   SelectedValuePath="Key" SelectedValue="{Binding Path=CompMfgBrandID, Mode=TwoWay,
   UpdateSourceTrigger=PropertyChanged}" Text="{Binding CompMFGText}"/>

绑定到 KeyValuePair 的集合

这是我的 ViewModel 中的 CompMfgBrandID 属性:

public string CompMfgBrandID
{
    get { return _compMFG; }
    set
    {    
        if (StockToExchange != null && StockToExchange.Where(x => !string.IsNullOrEmpty(x.EnteredPartNumber)).Count() > 0)
        {
            var dr = MessageBox.Show("Changing the competitor manufacturer will remove all entered parts from the transaction.  Proceed?",
                "Transaction Type", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
            if (dr != DialogResult.Yes)
                return;
        }

        _compMFG = value;
        StockToExchange.Clear();

        ...a bunch of other functions that don't get called when you click 'No'...
        OnPropertyChanged("CompMfgBrandID");
    }
}

如果您选择“yes”,它的行为将按预期进行。项目被清除并调用剩余的函数。如果我选择“否”,它会返回并且不会清除我的列表或调用任何其他函数,这很好,但组合框仍然显示新的选择。当用户选择“否”时,我需要它恢复到原始选择,就好像什么都没有改变一样。我怎样才能做到这一点?我还尝试在代码隐藏中添加 e.Handled = true ,但无济于事。

I've got a combobox in my WPF application:

<ComboBox  ItemsSource="{Binding CompetitorBrands}" DisplayMemberPath="Value" 
   SelectedValuePath="Key" SelectedValue="{Binding Path=CompMfgBrandID, Mode=TwoWay,
   UpdateSourceTrigger=PropertyChanged}" Text="{Binding CompMFGText}"/>

Bound to a collection of KeyValuePair<string, string>

Here is the CompMfgBrandID property in my ViewModel:

public string CompMfgBrandID
{
    get { return _compMFG; }
    set
    {    
        if (StockToExchange != null && StockToExchange.Where(x => !string.IsNullOrEmpty(x.EnteredPartNumber)).Count() > 0)
        {
            var dr = MessageBox.Show("Changing the competitor manufacturer will remove all entered parts from the transaction.  Proceed?",
                "Transaction Type", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
            if (dr != DialogResult.Yes)
                return;
        }

        _compMFG = value;
        StockToExchange.Clear();

        ...a bunch of other functions that don't get called when you click 'No'...
        OnPropertyChanged("CompMfgBrandID");
    }
}

If you choose "yes", it behaves as expected. Items are cleared and the remaining functions are called. If I choose 'No', it returns and doesn't clear my list or call any of the other functions, which is good, but the combobox still displays the new selection. I need it to revert back to the original selection, as if nothing had changed, when the user picks 'No'. How can I accomplish this? I also tried adding e.Handled = true in codebehind, to no avail.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

黎夕旧梦 2024-12-18 00:29:34

我的做法与上面的 splintor 类似。

您的视图:

<ComboBox  
ItemsSource="{Binding CompetitorBrands}" 
DisplayMemberPath="Value" 
SelectedValuePath="Key" 
SelectedValue="{Binding Path=CompMfgBrandID, 
Mode=TwoWay,
UpdateSourceTrigger=Explicit}" //to indicate that you will call UpdateSource() manually to get the property "CompMfgBrandID" udpated 
SelectionChanged="ComboBox_SelectionChanged"  //To fire the event from the code behind the view
Text="{Binding CompMFGText}"/>

下面是视图后面的代码文件中的事件处理程序“ComboBox_SelectionChanged”的代码。例如,如果您的视图是 myview.xaml,则此事件处理程序的代码文件名应为 myview.xaml.cs

private int previousSelection = 0; //Give it a default selection value

private bool promptUser true; //to be replaced with your own property which will indicates whether you want to show the messagebox or not.

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ComboBox comboBox = (ComboBox) sender;
            BindingExpression be = comboBox.GetBindingExpression(ComboBox.SelectedValueProperty);

            if (comboBox.SelectedValue != null && comboBox.SelectedIndex != previousSelection)
            {
                if (promptUser) //if you want to show the messagebox..
                {
                    string msg = "Click Yes to leave previous selection, click No to stay with your selection.";
                    if (MessageBox.Show(msg, "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) //User want to go with the newest selection
                    {

                        be.UpdateSource(); //Update the property,so your ViewModel will continue to do something
                        previousSelection = (int)comboBox.SelectedIndex;  
                    }
                    else //User have clicked No to cancel the selection
                    {
                        comboBox.SelectedIndex = previousSelection; //roll back the combobox's selection to previous one
                    }
                }
                else //if don't want to show the messagebox, then you just have to update the property as normal.
                {
                    be.UpdateSource();
                    previousSelection = (int)comboBox.SelectedIndex;
                }
            }
        }

I did it in a similar way to what splintor has above.

Your view:

<ComboBox  
ItemsSource="{Binding CompetitorBrands}" 
DisplayMemberPath="Value" 
SelectedValuePath="Key" 
SelectedValue="{Binding Path=CompMfgBrandID, 
Mode=TwoWay,
UpdateSourceTrigger=Explicit}" //to indicate that you will call UpdateSource() manually to get the property "CompMfgBrandID" udpated 
SelectionChanged="ComboBox_SelectionChanged"  //To fire the event from the code behind the view
Text="{Binding CompMFGText}"/>

Below is the code for the event handler "ComboBox_SelectionChanged" from the code file behind the view. For example, if you view is myview.xaml, the code file name for this event handler should be myview.xaml.cs

private int previousSelection = 0; //Give it a default selection value

private bool promptUser true; //to be replaced with your own property which will indicates whether you want to show the messagebox or not.

private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ComboBox comboBox = (ComboBox) sender;
            BindingExpression be = comboBox.GetBindingExpression(ComboBox.SelectedValueProperty);

            if (comboBox.SelectedValue != null && comboBox.SelectedIndex != previousSelection)
            {
                if (promptUser) //if you want to show the messagebox..
                {
                    string msg = "Click Yes to leave previous selection, click No to stay with your selection.";
                    if (MessageBox.Show(msg, "Confirm", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) //User want to go with the newest selection
                    {

                        be.UpdateSource(); //Update the property,so your ViewModel will continue to do something
                        previousSelection = (int)comboBox.SelectedIndex;  
                    }
                    else //User have clicked No to cancel the selection
                    {
                        comboBox.SelectedIndex = previousSelection; //roll back the combobox's selection to previous one
                    }
                }
                else //if don't want to show the messagebox, then you just have to update the property as normal.
                {
                    be.UpdateSource();
                    previousSelection = (int)comboBox.SelectedIndex;
                }
            }
        }
苍白女子 2024-12-18 00:29:34

以下是我使用的一般流程(不需要任何行为或 XAML 修改):

  1. 我只是让更改通过 ViewModel 并跟踪之前传入的内容。
    (如果您的业务逻辑要求所选项目不处于无效状态,我建议将其移至模型端)。此方法对于使用单选按钮呈现的列表框也很友好,因为使 SelectedItem setter 尽快退出不会阻止单选按钮在弹出消息框时突出显示。
  2. 无论传入的值如何,我都会立即调用 OnPropertyChanged 事件。
  3. 我将所有撤消逻辑放入处理程序中,并使用 SynchronizationContext.Post() 调用该事件
    (顺便说一句:SynchronizationContext.Post 也适用于 Windows 应用商店应用程序。因此,如果您共享 ViewModel 代码,此方法仍然有效)。

    公共类 ViewModel :INotifyPropertyChanged
    {
        公共事件 PropertyChangedEventHandler PropertyChanged;
    
        公共列表<字符串>项目{得到;放; }
    
        私有字符串_selectedItem;
        私有字符串_previouslySelectedItem;
        公共字符串 SelectedItem
        {
            得到
            {
                返回_selectedItem;
            }
            放
            {
                _previouslySelectedItem = _selectedItem;
                _selectedItem = 值;
                if (属性更改!= null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
                }
                SynchronizationContext.Current.Post(selectionChanged, null);
            }
        }
    
        私有无效选择更改(对象状态)
        {
            if (所选项目!= 项目[0])
            {
                MessageBox.Show("无法选择该");
                所选项目=项目[0];
            }
        }
    
        公共视图模型()
        {
            Items = new List();
            for (int i = 0; i < 10; ++i)
            {
                Items.Add(string.Format("项目 {0}", i));
            }
        }
    }
    

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));
            }
        }
    }
    
吾性傲以野 2024-12-18 00:29:34

我认为问题在于 ComboBox 在设置绑定属性值后将所选项目设置为用户操作的结果。因此,无论您在 ViewModel 中执行什么操作,Combobox 项都会发生变化。我发现了一种不同的方法,您不必改变 MVVM 模式。这是我的示例(抱歉,它是从我的项目复制的,与上面的示例不完全匹配):

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);
  }
}

不同之处在于我完全清除了项目集合,然后用之前存储的项目填充它。当我使用 ObservableCollection 泛型类时,这会强制 Combobox 进行更新。然后我将所选项目设置回之前设置的所选项目。对于很多项目不建​​议这样做,因为清除和填充组合框有点昂贵。

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.

新人笑 2024-12-18 00:29:34

我想完成 splintor 的回答,因为我偶然发现了 OnSelectedItemChanged 中延迟初始化的问题:

在分配 AssociatedObject 之前引发 OnSelectedItemChanged 时,使用 System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke 可以具有不需要的副作用,例如尝试使用组合框选择的默认值初始化 newValue。

因此,即使您的 ViewModel 是最新的,该行为也会触发从 ViewModel 的 SelectedItem 当前值更改为存储在 e.NewValue 中的 ComboBox 的默认选择。如果您的代码触发对话框,则会警告用户有更改,尽管没有更改。我无法解释为什么会发生这种情况,可能是时间问题。

这是我的修复方法

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;

namespace MyApp
{
    internal class CancellableSelectionBehaviour : Behavior<Selector>
    {
        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));

        /// <summary>
        /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
        /// True by default.
        /// </summary>
        public bool IgnoreNullSelection
        {
            get { return (bool)GetValue(IgnoreNullSelectionProperty); }
            set { SetValue(IgnoreNullSelectionProperty, value); }
        }

        /// <summary>
        /// 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
        /// </summary>
        private bool MustPerfomInitialChange { get; set; }

        /// <summary>
        /// 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.
        /// </summary>
        private DependencyPropertyChangedEventArgs InitialChangeEvent { get; set; }

        /// <summary>
        /// Called when the SelectedItem dependency property is changed.
        /// Updates the associated selector's SelectedItem with the new value.
        /// </summary>
        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;               
            }
        }

        /// <summary>
        /// Called when the associated selector's selection is changed.
        /// Tries to assign it to the <see cref="SelectedItem"/> property.
        /// If it fails, updates the selector's with  <see cref="SelectedItem"/> property's current value.
        /// </summary>
        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;
            }
        }
    }
}

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<Selector>
    {
        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));

        /// <summary>
        /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
        /// True by default.
        /// </summary>
        public bool IgnoreNullSelection
        {
            get { return (bool)GetValue(IgnoreNullSelectionProperty); }
            set { SetValue(IgnoreNullSelectionProperty, value); }
        }

        /// <summary>
        /// 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
        /// </summary>
        private bool MustPerfomInitialChange { get; set; }

        /// <summary>
        /// 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.
        /// </summary>
        private DependencyPropertyChangedEventArgs InitialChangeEvent { get; set; }

        /// <summary>
        /// Called when the SelectedItem dependency property is changed.
        /// Updates the associated selector's SelectedItem with the new value.
        /// </summary>
        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;               
            }
        }

        /// <summary>
        /// Called when the associated selector's selection is changed.
        /// Tries to assign it to the <see cref="SelectedItem"/> property.
        /// If it fails, updates the selector's with  <see cref="SelectedItem"/> property's current value.
        /// </summary>
        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;
            }
        }
    }
}
雪花飘飘的天空 2024-12-18 00:29:34

--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(); 
        }
}

--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(); 
        }
}
雾里花 2024-12-18 00:29:33

.NET 4.5.1+ 的非常简单的解决方案:

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

它在所有情况下都适合我。
您可以回滚组合框中的选择,只需触发 NotifyPropertyChanged 而不分配值。

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.
You can rollback selection in combobox, just fire NotifyPropertyChanged without value assignment.

最美不过初阳 2024-12-18 00:29:33

要在 MVVM 下实现此目的....

1] 有一个附加行为来处理 ComboBox 的 SelectionChanged 事件。此事件是通过一些具有 Handled 标志的事件参数引发的。但将其设置为 true 对于 SelectedValue 绑定来说是没有用的。无论事件是否被处理,绑定都会更新源。

2] 因此,我们将 ComboBox.SelectedValue 绑定配置为 TwoWayExplicit

3] 只有当您的检查得到满足并且消息框显示时,我们才会执行BindingExpression.UpdateSource()。否则,我们只需调用 BindingExpression.UpdateTarget() 即可恢复到旧的选择。


在下面的示例中,我有一个绑定到窗口数据上下文的 KeyValuePair 列表。 ComboBox.SelectedValue 绑定到Window 的一个简单的可写MyKey 属性。

XAML ...

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

其中 MyDGSampleWindowWindow 的 x:Name。

代码隐藏...

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;
    }
 }

附加行为

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;
                }
            }
        }
    }
}

在行为中我使用ComboBox.Tag属性来暂时存储一个标志,当我们恢复到旧的选定值时,该标志会跳过重新检查。

如果这有帮助,请告诉我。

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.

乄_柒ぐ汐 2024-12-18 00:29:33

这可以使用 Blend 的 一般行为

该行为定义了一个名为 SelectedItem 的依赖属性,您应该将绑定放入此属性中,而不是放在 ComboBox 的 SelectedItem 属性中。该行为负责将依赖属性中的更改传递给 ComboBox(或更一般地说,传递给选择器),并且当选择器的 SelectedItem 更改时,它会尝试将其分配给自己的 SelectedItem 属性。如果分配失败(可能是因为绑定的 VM 属性设置器拒绝分配),该行为将使用其 SelectedItem 属性的当前值更新选择器的 SelectedItem

由于各种原因,您可能会遇到选择器中的项目列表被清除并且所选项目变为空的情况(请参阅这个问题)。在这种情况下,您通常不希望 VM 属性变为 null。为此,我添加了 IgnoreNullSelection 依赖属性,默认情况下该属性为 true。这应该可以解决这样的问题。

这是 CancellableSelectionBehavior 类:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;

namespace MySampleApp
{
    internal class CancellableSelectionBehavior : Behavior<Selector>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectionChanged += OnSelectionChanged;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SelectionChanged -= OnSelectionChanged;
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(CancellableSelectionBehavior),
                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(CancellableSelectionBehavior), new PropertyMetadata(true));

        /// <summary>
        /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
        /// True by default.
        /// </summary>
        public bool IgnoreNullSelection
        {
            get { return (bool)GetValue(IgnoreNullSelectionProperty); }
            set { SetValue(IgnoreNullSelectionProperty, value); }
        }

        /// <summary>
        /// Called when the SelectedItem dependency property is changed.
        /// Updates the associated selector's SelectedItem with the new value.
        /// </summary>
        private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (CancellableSelectionBehavior)d;

            // OnSelectedItemChanged can be raised before AssociatedObject is assigned
            if (behavior.AssociatedObject == null)
            {
                System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
                {
                    var selector = behavior.AssociatedObject;
                    selector.SelectedValue = e.NewValue;
                }));
            }
            else
            {
                var selector = behavior.AssociatedObject;
                selector.SelectedValue = e.NewValue;
            }
        }

        /// <summary>
        /// Called when the associated selector's selection is changed.
        /// Tries to assign it to the <see cref="SelectedItem"/> property.
        /// If it fails, updates the selector's with  <see cref="SelectedItem"/> property's current value.
        /// </summary>
        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;
            }
        }
    }
}

这是在 XAML 中使用它的方法:

<Window x:Class="MySampleApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="My Smaple App" Height="350" Width="525"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:MySampleApp"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
    <StackPanel>
        <ComboBox ItemsSource="{Binding Options}">
            <i:Interaction.Behaviors>
                <local:CancellableSelectionBehavior SelectedItem="{Binding Selected}" />
            </i:Interaction.Behaviors>
        </ComboBox>
    </StackPanel>
</Window>

这是 VM 属性的示例:

private string _selected;

public string Selected
{
    get { return _selected; }
    set
    {
        if (IsValidForSelection(value))
        {
            _selected = value;
        }
    }
}

This can be achieved in a generic and compact way using Blend's Generic Behavior.

The behavior defines a dependency property named SelectedItem, and you should put your binding in this property, instead of in the ComboBox's SelectedItem property. The behavior is in charge of passing changes in the dependency property to the ComboBox (or more generally, to the Selector), and when the Selector's SelectedItem changes, it tries to assign it to the its own SelectedItem property. If the assignment fails (probably because the bound VM proeprty setter rejected the assignment), the behavior updates the Selector’s SelectedItem with the current value of its SelectedItem property.

For all sorts of reasons, you might encounter cases where the list of items in the Selector is cleared, and the selected item becomes null (see this question). You usually don't want your VM property to become null in this case. For this, I added the IgnoreNullSelection dependency property, which is true by default. This should solve such problem.

This is the CancellableSelectionBehavior class:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;

namespace MySampleApp
{
    internal class CancellableSelectionBehavior : Behavior<Selector>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectionChanged += OnSelectionChanged;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SelectionChanged -= OnSelectionChanged;
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(CancellableSelectionBehavior),
                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(CancellableSelectionBehavior), new PropertyMetadata(true));

        /// <summary>
        /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
        /// True by default.
        /// </summary>
        public bool IgnoreNullSelection
        {
            get { return (bool)GetValue(IgnoreNullSelectionProperty); }
            set { SetValue(IgnoreNullSelectionProperty, value); }
        }

        /// <summary>
        /// Called when the SelectedItem dependency property is changed.
        /// Updates the associated selector's SelectedItem with the new value.
        /// </summary>
        private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (CancellableSelectionBehavior)d;

            // OnSelectedItemChanged can be raised before AssociatedObject is assigned
            if (behavior.AssociatedObject == null)
            {
                System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
                {
                    var selector = behavior.AssociatedObject;
                    selector.SelectedValue = e.NewValue;
                }));
            }
            else
            {
                var selector = behavior.AssociatedObject;
                selector.SelectedValue = e.NewValue;
            }
        }

        /// <summary>
        /// Called when the associated selector's selection is changed.
        /// Tries to assign it to the <see cref="SelectedItem"/> property.
        /// If it fails, updates the selector's with  <see cref="SelectedItem"/> property's current value.
        /// </summary>
        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;
            }
        }
    }
}

This is the way to use it in XAML:

<Window x:Class="MySampleApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="My Smaple App" Height="350" Width="525"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:MySampleApp"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
    <StackPanel>
        <ComboBox ItemsSource="{Binding Options}">
            <i:Interaction.Behaviors>
                <local:CancellableSelectionBehavior SelectedItem="{Binding Selected}" />
            </i:Interaction.Behaviors>
        </ComboBox>
    </StackPanel>
</Window>

and this is a sample of the VM property:

private string _selected;

public string Selected
{
    get { return _selected; }
    set
    {
        if (IsValidForSelection(value))
        {
            _selected = value;
        }
    }
}
傻比既视感 2024-12-18 00:29:33

我在另一个线程上找到了用户 shaun 对这个问题的更简单的答案:
https://stackoverflow.com/a/6445871/2340705

基本问题是属性更改事件被吞没。有些人会将此称为错误。要解决此问题,请使用 Dispatcher 中的 BeginInvoke 来强制将属性更改事件放回到 UI 事件队列的末尾。这不需要更改 xaml,不需要额外的行为类,并且只需对视图模型更改一行代码。

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.

旧人九事 2024-12-18 00:29:33

问题是,一旦 WPF 使用属性 setter 更新值,它就会忽略该调用中任何进一步的属性更改通知:它假设它们将作为 setter 的正常部分发生,并且不会产生任何后果,即使您确实有将属性更新回原始值。

我解决这个问题的方法是允许字段更新,但也在调度程序上排队操作以“撤消”更改。该操作会将其设置回旧值并触发属性更改通知,以使 WPF 意识到它实际上并不是它认为的新值。

显然,应该设置“撤消”操作,这样它就不会触发程序中的任何业务逻辑。

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.

俏︾媚 2024-12-18 00:29:33

我遇到了同样的问题,是由 UI 线程和出价工作方式引起的。检查此链接: ComboBox 上的 SelectedItem

示例中的结构使用后面的代码,但MVVM 是完全一样的。

I had the same issue, causes by UI thread and the way that biding works. Check the this link: SelectedItem on ComboBox

The structure in the sample uses code behind but the MVVM is exactly the same.

最偏执的依靠 2024-12-18 00:29:33

我更喜欢“splintor”的代码示例而不是“AngelWPF”。但他们的方法非常相似。我已经实现了附加行为 CancellableSelectionBehavior,并且它的工作原理如广告所示。也许只是 splintor 示例中的代码更容易插入到我的应用程序中。 AngelWPF 附加行为中的代码引用了需要更多代码更改的 KeyValuePair 类型。

在我的应用程序中,我有一个 ComboBox,其中 DataGrid 中显示的项目基于 ComboBox 中选择的项目。如果用户对 DataGrid 进行了更改,然后在组合框中选择了一个新项目,我将提示用户使用 Yes|NO|Cancel 按钮作为选项保存更改。如果他们按下“取消”,我想忽略他们在组合框中的新选择并保留旧选择。这就像冠军一样!

对于那些一看到 Blend 和 System.Windows.Interactivity 就被吓到的人来说,您不必安装 Microsoft Expression Blend。您可以下载适用于 .NET 4(或 Silverlight)的 Blend SDK。

混合.NET 4 SDK

Blend SDK for Silverlight 4

哦,是的,在我的 XAML,实际上在本例中我使用它作为 Blend 的命名空间声明:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

I prefer "splintor's" code sample over "AngelWPF's". Their approaches are fairly similar though. I have implemented the attached behavior, CancellableSelectionBehavior, and it works as advertised. Perhaps it was just that the code in splintor's example was easier to plug into my application. The code in AngelWPF's attached behavior had references to a KeyValuePair Type that would have called for more code alteration.

In my application, I had a ComboBox where the items that are displayed in a DataGrid are based on the item selected in the ComboBox. If the user made changes to the DataGrid, then selected a new item in the ComboBox, I would prompt the user to save changes with Yes|NO|Cancel buttons as options. If they pressed Cancel, I wanted to ignore their new selection in the ComboBox and keep the old selection. This worked like a champ!

For those who frighten away the moment they see references to Blend and System.Windows.Interactivity, you do not have to have Microsoft Expression Blend installed. You can download the Blend SDK for .NET 4 (or Silverlight).

Blend SDK for .NET 4

Blend SDK for Silverlight 4

Oh yeah, in my XAML, I actually use this as my namespace declaration for Blend in this example:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文