我的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
,但无济于事。
答案 0 :(得分:18)
这可以使用Blend的Generic Behavior以通用和紧凑的方式实现。
该行为定义了一个名为SelectedItem
的依赖项属性,您应该将绑定放在此属性中,而不是放在ComboBox的SelectedItem
属性中。该行为负责将依赖项属性中的更改传递给ComboBox(或更一般地,传递给选择器),当选择器的SelectedItem
发生更改时,它会尝试将其分配给自己的SelectedItem
属性。如果赋值失败(可能是因为绑定的VM proeprty setter拒绝了赋值),则行为会使用其SelectedItem
属性的当前值更新Selector的SelectedItem
。
由于种种原因,您可能会遇到清除选择器中的项目列表并且所选项目变为空的情况(请参阅this question)。在这种情况下,您通常不希望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;
}
}
}
答案 1 :(得分:17)
在MVVM下实现这一目标....
1]具有处理ComboBox的SelectionChanged
事件的附加行为。引发此事件的是一些具有Handled
标志的事件参数。但是将它设置为true对SelectedValue
绑定毫无用处。无论事件是否得到处理,绑定都会更新源。
2]因此,我们将ComboBox.SelectedValue
绑定配置为TwoWay
和Explicit
。
3]只有当您的检查得到满足并且消息框显示Yes
时才会执行BindingExpression.UpdateSource()
。否则,我们只需调用BindingExpression.UpdateTarget()
即可恢复旧选择。
在下面的示例中,我有一个绑定到窗口数据上下文的KeyValuePair<int, int>
列表。 ComboBox.SelectedValue
绑定到MyKey
的简单可写Window
属性。
XAML ......
<ComboBox ItemsSource="{Binding}"
DisplayMemberPath="Value"
SelectedValuePath="Key"
SelectedValue="{Binding MyKey,
ElementName=MyDGSampleWindow,
Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
local:MyAttachedBehavior.ConfirmationValueBinding="True">
</ComboBox>
其中MyDGSampleWindow
是x:Window
的名称。
代码背后......
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
属性临时存储一个标志,当我们恢复到旧的选定值时,该标志会跳过重新检查。
如果有帮助,请告诉我。
答案 2 :(得分:17)
.NET 4.5.1 +的非常简单的解决方案:
<ComboBox SelectedItem="{Binding SelectedItem, Delay=10}" ItemsSource="{Binding Items}" />
在大多数情况下,它对我有用。 您可以在组合框中回滚选择,只需在没有赋值的情况下触发NotifyPropertyChanged。
答案 3 :(得分:6)
我在另一个帖子上找到了用户shaun对这个问题的一个更简单的答案: https://stackoverflow.com/a/6445871/2340705
基本问题是属性更改事件被吞下。有人会称这是一个错误。为了解决这个问题,请使用Dispatcher中的BeginInvoke来强制将属性更改事件放回到UI事件队列的末尾。这不需要更改xaml,也不需要额外的行为类,并且只需将一行代码更改为视图模型。
答案 4 :(得分:4)
问题是,一旦WPF使用属性设置器更新了值,它就会忽略该调用中的任何其他属性更改通知:它假定它们将作为setter的正常部分发生并且无关紧要,即使您确实已将该属性更新回原始值。
我解决这个问题的方法是允许字段更新,但也可以在Dispatcher上排队操作以“撤消”更改。该操作会将其设置回旧值并触发属性更改通知,以使WPF意识到它并不是它认为的新值。
显然应该设置“撤消”操作,这样就不会触发程序中的任何业务逻辑。
答案 5 :(得分:2)
我有同样的问题,由UI线程引起的原因和出价的方式。请查看此链接:SelectedItem on ComboBox
示例中的结构使用了代码,但MVVM完全相同。
答案 6 :(得分:1)
我更喜欢“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)。
哦,是的,在我的XAML中,我实际上在本例中使用它作为Blend的命名空间声明:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
答案 7 :(得分:1)
以下是我使用的一般流程(不需要任何行为或XAML修改):
我在处理程序中放置了任何撤消逻辑,并使用SynchronizationContext.Post()调用它 (顺便说一句:SynchronizationContext.Post也适用于Windows应用商店应用。所以,如果你有共享的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("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));
}
}
}
答案 8 :(得分:0)
我的方式与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;
}
}
}
答案 9 :(得分: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);
}
}
不同之处在于我完全清除了项目集合,然后用之前存储的项目填充它。当我使用ObservableCollection泛型类时,这会强制Combobox更新。然后我将所选项目设置回先前设置的所选项目。这不建议用于很多项目,因为清理和填充组合框有点贵。
答案 10 :(得分:0)
我想完成splintor's answer,因为我偶然发现了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;
}
}
}
}
答案 11 :(得分:0)
-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();
}
}