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
を追加しようとしましたが、役に立ちませんでした。
MVVMでこれを実現するには...
1] ComboBoxのSelectionChanged
イベントを処理する動作を添付します。このイベントは、Handled
フラグを持ついくつかのイベント引数で発生します。ただし、trueに設定しても、SelectedValue
バインディングには役に立ちません。バインディングは、イベントが処理されたかどうかに関係なく、ソースを更新します。
2]したがって、_ComboBox.SelectedValue
_バインディングをTwoWay
およびExplicit
に構成します。
3] checkが満たされ、メッセージボックスに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
はWindow
の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
_プロパティを使用して、選択した古い値に戻ったときに再チェックをスキップするフラグを一時的に保存します。
これが役立つかどうか教えてください。
これは、Blendの Generic Behavior を使用して、一般的でコンパクトな方法で実現できます。
この動作では、SelectedItem
という名前の依存関係プロパティが定義されているため、ComboBoxのSelectedItem
プロパティではなく、このプロパティにバインディングを配置する必要があります。この動作は、依存関係プロパティの変更をComboBox(またはより一般的にはセレクター)に渡すことを担当し、セレクターのSelectedItem
が変更されると、それを独自のSelectedItem
プロパティ。割り当てが失敗した場合(おそらくバウンドVMプロパティセッターが割り当てを拒否したため))、動作はセレクターのSelectedItem
をそのSelectedItem
プロパティの現在の値で更新します。
さまざまな理由で、セレクター内のアイテムのリストがクリアされ、選択されたアイテムがnullになる場合があります( この質問 を参照)。通常、この場合、VMプロパティがnullになることは望ましくありません。このために、デフォルトでtrueであるIgnoreNullSelection依存関係プロパティを追加しました。これでこのような問題が解決するはずです。
これは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;
}
}
}
.NET 4.5.1+の非常にシンプルなソリューション:
<ComboBox SelectedItem="{Binding SelectedItem, Delay=10}" ItemsSource="{Binding Items}" />
ほとんどの場合、それは私にとってはうまくいきます。コンボボックスで選択をロールバックできます。値を割り当てずにNotifyPropertyChangedを起動するだけです。
私は別のスレッドでユーザーshaunによるこの質問へのはるかに簡単な答えを見つけました: https://stackoverflow.com/a/6445871/2340705
基本的な問題は、プロパティ変更イベントが飲み込まれることです。これをバグと呼ぶ人もいます。これを回避するには、ディスパッチャーからBeginInvokeを使用して、プロパティ変更イベントをUIイベントキューの最後に強制的に戻します。これには、xamlの変更、追加の動作クラス、およびビューモデルへの1行のコードの変更は必要ありません。
問題は、WPFがプロパティセッターで値を更新すると、その呼び出し内からのプロパティ変更通知を無視することです。これは、セッターの通常の部分として発生し、実際にプロパティを元の値に更新しました。
これを回避する方法は、フィールドを更新できるようにするだけでなく、変更を「元に戻す」ためにディスパッチャーのアクションをキューに入れることでした。このアクションは、それを古い値に戻し、プロパティ変更通知を起動して、WPFに、それが実際には新しい値ではないと認識させます。
明らかに、「元に戻す」アクションは、プログラムでビジネスロジックを起動しないように設定する必要があります。
同じ問題がありました。UIスレッドと入札の仕組みが原因です。このリンクを確認してください: ComboBoxのSelectedItem
サンプルの構造はコードビハインドを使用していますが、MVVMはまったく同じです。
これが私が使用する一般的なフローです(動作やXAMLの変更は必要ありません):
元に戻すロジックをハンドラーに入れ、SynchronizationContext.Post()を使用して呼び出します(BTW: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));
}
}
}
私はスプリンターが上に持っているのと同じようにそれをしました。
あなたの見解:
<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;
}
}
}
私は「AngelWPF」よりも「splintor」のコードサンプルを好みます。しかし、彼らのアプローチはかなり似ています。添付の動作CancellableSelectionBehaviorを実装しましたが、宣伝どおりに機能します。おそらく、splintorの例のコードが私のアプリケーションにプラグインするのが簡単だったというだけのことでしょう。 AngelWPFの添付動作のコードには、さらにコードの変更を必要とするKeyValuePairタイプへの参照が含まれていました。
私のアプリケーションでは、DataGridに表示されるアイテムがComboBoxで選択されたアイテムに基づいているComboBoxがありました。ユーザーがDataGridに変更を加えてから、ComboBoxで新しい項目を選択した場合、オプションとして[はい]、[いいえ]、[キャンセル]ボタンを使用して変更を保存するようにユーザーに促します。彼らがキャンセルを押した場合、私はComboBoxでの新しい選択を無視し、古い選択を保持したいと思いました。これはチャンピオンのように機能しました!
BlendとSystem.Windows.Interactivityへの参照を目にした瞬間に怖がる人は、Microsoft ExpressionBlendをインストールする必要はありません。 Blend SDK for .NET 4(またはSilverlight)をダウンロードできます。
そうそう、私のXAMLでは、この例のBlendの名前空間宣言として実際にこれを使用しています。
xmlns:i="clr-namespace:System.Windows.Interactivity;Assembly=System.Windows.Interactivity"
完了したい 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;
}
}
}
}
問題は、バインドされたプロパティ値を設定した後、ComboBoxがユーザーアクションの結果として選択されたアイテムを設定することだと思います。したがって、Comboboxアイテムは、ViewModelで何をしても変更されます。 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が強制的に更新されます。次に、選択したアイテムを以前に設定した選択したアイテムに戻します。コンボボックスのクリアと入力にはコストがかかるため、これは多くのアイテムにはお勧めできません。
--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();
}
}