使用MVVM取消WPF中的combobox选择

我的WPF应用程序中有一个combobox:

 

绑定到KeyValuePair的集合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"); } } 

如果选择“是”,则表现如预期。 清除项目并调用其余function。 如果我选择“否”,它将返回并且不会清除我的列表或调用任何其他function,这很好,但combobox仍然显示新选择。 当用户选择“否”时,我需要它恢复原始选择,好像什么都没有改变。 我怎么能做到这一点? 我也尝试在代码隐藏中添加e.Handled = true ,但无济于事。

在MVVM下实现这一目标….

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

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

3]只有当你的检查得到满足并且messagebox表示Yes ,当我们执行BindingExpression.UpdateSource() 。 否则,我们只需调用BindingExpression.UpdateTarget()来恢复旧的选择。


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

XAML ……

    

其中MyDGSampleWindow是x: Window名称。

代码背后……

 public partial class Window1 : Window { private List> list1; public int MyKey { get; set; } public Window1() { InitializeComponent(); list1 = new List>(); var random = new Random(); for (int i = 0; i < 50; i++) { list1.Add(new KeyValuePair(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) 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属性临时存储一个标志,当我们恢复到旧的选定值时,该标志会跳过重新检查。

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

这可以使用Blend的通用行为以通用和紧凑的方式实现。

该行为定义了一个名为SelectedItem的依赖项属性,您应该将绑定放在此属性中,而不是放在ComboBox的SelectedItem属性中。 该行为负责将依赖项属性中的更改传递给ComboBox(或更一般地,传递给Selector),并且当Selector的SelectedItem更改时,它会尝试将其分配给自己的SelectedItem属性。 如果赋值失败(可能是因为绑定的VM proeprty setter拒绝了赋值),则该行为会使用其SelectedItem属性的当前值更新Selector的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 { 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)); ///  /// 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); } } ///  /// 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 = (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; } } ///  /// 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; } } } } 

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

          

这是VM属性的示例:

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

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

  

在大多数情况下,它对我有用。 您可以在combobox中回滚选择,只需在没有赋值的情况下触发NotifyPropertyChanged。

用户shaun在另一个post上找到了一个更简单的答案: https ://stackoverflow.com/a/6445871/2340705

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

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

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

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

我有同样的问题,UI线程的原因和biding的工作方式。 检查此链接: ComboBox上的SelectedItem

示例中的结构使用了代码,但MVVM完全相同。

我更喜欢“splintor”的代码示例而不是“AngelWPF”。 他们的方法非常相似。 我已经实现了附加的行为,CancellableSelectionBehavior,它的工作方式与广告一致。 也许只是splintor示例中的代码更容易插入我的应用程序。 AngelWPF的附加行为中的代码引用了KeyValuePair类型,该类型将调用更多代码更改。

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

对于那些在看到Blend和System.Windows.Interactivity的引用时吓跑的人,您不必安装Microsoft Expression Blend。 您可以下载Blend SDK for .NET 4(或Silverlight)。

Blend SDK for .NET 4

Blend SDK for Silverlight 4

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

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

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

  1. 我只是让更改通过ViewModel并跟踪之前传递的内容。 (如果您的业务逻辑要求所选项目不处于无效状态,我建议将其移至模型侧)。 这种方法对使用单选按钮呈现的ListBox也很友好,因为尽快使SelectedItem setter出口不会阻止单选按钮在弹出消息框时突出显示。
  2. 无论传入的值如何,我都会立即调用OnPropertyChanged事件。
  3. 我在处理程序中放置了任何撤消逻辑,并使用SynchronizationContext.Post()调用它(BTW:SynchronizationContext.Post也适用于Windowsapp store应用。所以,如果你有共享的ViewModel代码,这种方法仍然可以工作)。

     public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public List 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(); for (int i = 0; i < 10; ++i) { Items.Add(string.Format("Item {0}", i)); } } } 

我用与上面的splintor类似的方式做到了。

你的观点:

  

下面是视图后面的代码文件中的事件处理程序“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; } } } 

我认为问题是ComboBox在设置绑定属性值后,作为用户操作的结果设置了所选项。 因此,无论您在ViewModel中执行什么操作,Combobox项都会更改。 我发现了一种不同的方法,你不必弯曲MVVM模式。 这是我的例子(很遗憾,它是从我的项目中复制而来,并不完全符合上面的例子):

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

不同之处在于我完全清除了项目集合,然后用之前存储的项目填充它。 当我使用ObservableCollectiongenerics类时,这会强制Combobox更新。 然后我将所选项目设置回先前设置的所选项目。 这不建议用于很多项目,因为清理和填充combobox有点贵。

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

在分配AssociatedObject之前引发OnSelectedItemChanged时,使用System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke可能会产生不必要的副作用,例如尝试使用combobox选择的默认值初始化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 { 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; } } } }