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

32

我在我的WPF应用程序中有一个下拉框:

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

绑定到一个 KeyValuePair<string, string> 集合。

这是我的 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");
    }
}

如果您选择“是”,它会按预期行事。项目将被清除并调用其余功能。如果我选择“否”,它将返回并不清除我的列表或调用任何其他函数,这很好,但组合框仍然显示新的选择。当用户选择“否”时,我需要它恢复到原始选择状态,就像没有发生任何更改一样。我该如何实现这一点?我还尝试在代码后台添加e.Handled = true,但没有效果。

2
将GUI放在属性设置器中是一个不好的想法,特别是在MVVM中。 - H H
我建议考虑在属性设置器或属性更改事件中发布消息,并利用中介者模式处理对话框UI的显示。然后,对话框选择将发布响应消息,您的视图模型中介者正在监听该消息。 - Oppositional
请注意,如果您可以将代码更改为使用“SelectedItem”而不是“SelectedValue”,则问题可以解决。显然,这不是一个简单的替换,因此您需要进行一些思考。 - Richardissimo
12个回答

43

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

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

这在所有情况下都对我起作用。您可以在组合框中回滚选择,只需触发NotifyPropertyChanged而无需进行值分配。


19
为了在 MVVM 框架下实现这个功能...
1] 创建一个附加的行为来处理 ComboBox 的 SelectionChanged 事件。该事件会引发带有 Handled 标志的某些事件参数。但将其设置为 true 对于 SelectedValue 绑定是无用的。无论事件是否被处理,绑定都会更新源。
2] 因此,我们将 ComboBox.SelectedValue 绑定配置为 TwoWay 和 Explicit。
3] 只有当您的检查得到满足并且消息框显示 "Yes" 时,才执行 BindingExpression.UpdateSource()。否则,我们只需调用 BindingExpression.UpdateTarget() 来恢复旧的选择。
在我的下面的示例中,我有一个 KeyValuePair<int, int> 的列表,它绑定到窗口的数据上下文。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属性临时存储一个标志,以便在回退到旧的选定值时跳过重新检查。

如果这有帮助,请让我知道。


6
这个很好用,谢谢。但这让我更加后悔使用MVVM模式。我讨厌这个最简单的概念(恢复下拉框的值)需要所有这些额外的代码、时间和疑难解答。 - drowned
3
@drowned,MVVM和WPF是密不可分的。我曾经参与过许多WPF项目,其中大部分都使用了MVVM...相信我,当它变得越来越庞大时,使用MVVM会更加可管理。 - WPF-it
2
我无法决定这是天才还是疯狂。 - Mark
我发现这个答案很有用:https://dev59.com/I2Mm5IYBdhLWcg3wHMN0#17861647 - Eric Scherrer

18

使用Blend的通用行为 (Generic Behavior)可以以一种通用且简洁的方式实现此目标。

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

由于各种原因,您可能会遇到选择器中的项目列表被清空,所选项目变为空的情况(请参见此问题)。在这种情况下,您通常不希望您的视图模型属性变为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>

这是虚拟机属性的示例:

private string _selected;

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

6
我在另一个帖子上由用户shaun发现了一个更简单的答案: https://dev59.com/SlbUa4cB1Zd3GeqPCNTF#6445871 基本问题是属性更改事件被吞噬了。有些人会认为这是一个错误。为了解决这个问题,可以使用来自Dispatcher的BeginInvoke强制将属性更改事件放回UI事件队列的末尾。这不需要更改XAML,也不需要额外的行为类,只需要更改视图模型中的一行代码即可。

4
问题在于一旦WPF使用属性设置器更新了值,它会忽略任何来自该调用中的进一步属性更改通知:它假定它们将作为setter的正常部分发生,并且并不重要,即使你确实已经将属性更新回原始值。
我解决这个问题的方法是允许字段得到更新,但也将一个操作排入Dispatcher队列中来“撤消”更改。该操作会将其设置回旧值并触发属性更改通知以使WPF意识到它实际上并非先前认为的新值。
显然,“撤消”操作应该被设置为不在程序中触发任何业务逻辑。

我不完全确定这是正确的:我有一个ToggleButton,将IsChecked绑定到一个bool。如果在setter中强制将其设置为false并通知属性更改,它将保持未选中状态。这表明它会注意到进一步的属性更改通知。 - Kieren Johnstone
1
当我尝试使用ComboBox绑定时,即使在执行了这些步骤后,它仍然会停留在用户选择的值上。 - RandomEngy

2

我曾经遇到过相同的问题,是由UI线程和绑定方式引起的。请查看此链接:ComboBox上的SelectedItem

示例中使用了代码后台,但MVVM完全相同。


这个解决方案更简单。很好! - dizel3d
是的,我更喜欢这个,而不是与System.Windows.Interactivity.Behavior跳舞。 - dizel3d

1
我用的方法与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;
                }
            }
        }

非常感谢,这救了我的一天。一直在寻找类似的解决方案,它按预期工作了。从视图模型中是否也可以实现相同的功能? - Mohanvel V

1
我更喜欢"splintor"的代码示例,而不是"AngelWPF"的。虽然他们的方法相当相似。我已经实现了附加行为"CancellableSelectionBehavior",并且它的功能与广告一样。也许只是因为splintor示例中的代码更容易插入我的应用程序。AngelWPF的附加行为中的代码引用了一个KeyValuePair类型,这将需要更多的代码修改。
在我的应用程序中,我有一个ComboBox,在其中显示在DataGrid中显示的项目基于ComboBox中选择的项目。如果用户对DataGrid进行更改,然后选择ComboBox中的新项目,则我将提示用户使用Yes | NO | Cancel按钮保存更改。如果他们按下取消,我想忽略他们在ComboBox中的新选择并保留旧选择。这像个冠军一样工作!
对于那些一看到Blend和System.Windows.Interactivity的引用就感到害怕的人,您不必安装Microsoft Expression Blend。您可以下载.NET 4(或Silverlight)的Blend SDK。

.NET 4的Blend SDK

Silverlight 4的Blend SDK

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

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

1

这是我通常使用的基本流程(不需要任何行为或XAML修改):

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

    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("无法选择该项");
                SelectedItem = Items[0];
            }
        }
    
        public ViewModel()
        {
            Items = new List<string>();
            for (int i = 0; i < 10; ++i)
            {
                Items.Add(string.Format("项目 {0}", i));
            }
        }
    }
    

0

我认为问题在于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);
  }
}

区别在于我完全清除了项目集合,然后用之前存储的项目填充它。这强制Combobox更新,因为我使用ObservableCollection泛型类。然后我将选定的项目设置回先前设置的选定的项目。对于许多项目来说,这并不推荐,因为清除和填充组合框有点昂贵。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接