c# - wpf combobox selectionchanged
Cancele la selección de combo box en WPF con MVVM (11)
Tengo un cuadro combinado en mi aplicación WPF:
<ComboBox ItemsSource="{Binding CompetitorBrands}" DisplayMemberPath="Value"
SelectedValuePath="Key" SelectedValue="{Binding Path=CompMfgBrandID, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" Text="{Binding CompMFGText}"/>
Vinculado a una colección de KeyValuePair<string, string>
Aquí está la propiedad CompMfgBrandID en mi 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");
}
}
Si elige "sí", se comporta como se esperaba. Los elementos se borran y se llaman las funciones restantes. Si elijo ''No'', vuelve y no borra mi lista ni llama a ninguna de las otras funciones, lo cual es bueno, pero el cuadro combinado todavía muestra la nueva selección. Necesito que regrese a la selección original, como si nada hubiera cambiado, cuando el usuario selecciona ''No''. ¿Cómo puedo lograr esto? También traté de agregar e.Handled = true
en el código subyacente, pero fue en vano.
Aquí está el flujo general que uso (no necesita ningún comportamiento o modificaciones XAML):
- Solo dejo que el cambio pase a través del ViewModel y hago un seguimiento de lo que haya pasado antes. (Si su lógica comercial requiere que el elemento seleccionado no esté en un estado inválido, le sugiero que lo mueva al lado del Modelo). Este enfoque también es amigable con ListBoxes que se representan con los botones de opción, ya que hacer que el setter de SelectedItem salga tan pronto como sea posible no evitará que los botones de opción se resalten cuando aparece un cuadro de mensaje.
- Inmediatamente llamo al evento OnPropertyChanged independientemente del valor pasado.
Pongo cualquier lógica de deshacer en un controlador y llamo a eso usando SynchronizationContext.Post () (BTW: SynchronizationContext.Post también funciona para las aplicaciones de la tienda de Windows. Por lo tanto, si usted compartió el código de ViewModel, este enfoque funcionaría).
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)); } } }
Creo que el problema es que ComboBox establece el elemento seleccionado como resultado de la acción del usuario después de establecer el valor de la propiedad vinculada. Por lo tanto, el elemento de Combobox cambia independientemente de lo que haga en ViewModel. Encontré un enfoque diferente donde no tienes que doblar el patrón MVVM. Aquí está mi ejemplo (siento que se haya copiado de mi proyecto y no coincida exactamente con los ejemplos anteriores):
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);
}
}
La diferencia es que borro completamente la colección de artículos y luego la lleno con los artículos almacenados anteriormente. Esto obliga al Combobox a actualizarse mientras uso la clase genérica ObservableCollection. Luego configuré el elemento seleccionado nuevamente al elemento seleccionado que se configuró previamente. Esto no se recomienda para muchos artículos porque borrar y llenar el cuadro combinado es un poco caro.
El problema es que una vez que WPF actualiza el valor con el creador de propiedades, ignora cualquier otra propiedad de notificaciones modificadas dentro de esa llamada: asume que pasarán como una parte normal del organismo de creación de contenido y que no tienen consecuencias, incluso si usted realmente tiene actualizó la propiedad nuevamente al valor original.
La forma en que solucioné esto fue permitir que el campo se actualizara, pero también poner en cola una acción en el Dispatcher para "deshacer" el cambio. La acción volvería al valor anterior y activaría una notificación de cambio de propiedad para que WPF se dé cuenta de que realmente no es el nuevo valor que pensó que era.
Obviamente, la acción "deshacer" debe configurarse para que no active ninguna lógica comercial en su programa.
Encontré una respuesta mucho más simple a esta pregunta por parte del usuario shaun en otro hilo: https://.com/a/6445871/2340705
El problema básico es que el evento de propiedad cambiada se traga. Algunos llamarían esto un error. Para evitarlo, utilice BeginInvoke desde Dispatcher para forzar el evento de cambio de propiedad para que se vuelva a colocar al final de la cola de eventos de UI. Esto no requiere ningún cambio en el xaml, no hay clases de comportamiento extra, y una sola línea de código cambió al modelo de vista.
Esto se puede lograr de una manera genérica y compacta usando el comportamiento genérico de Blend.
El comportamiento define una propiedad de dependencia llamada SelectedItem
, y debe poner su enlace en esta propiedad, en lugar de en la propiedad SelectedItem
de ComboBox. El comportamiento es el encargado de pasar los cambios en la propiedad de dependencia al ComboBox (o más generalmente, al Selector), y cuando el Elemento Seleccionado del Selector cambia, intenta asignarlo a su propia propiedad SelectedItem
. Si la asignación falla (probablemente debido a que el responsable de establecer la VM obligado rechazó la asignación), el comportamiento actualiza el elemento seleccionado del Selector con el valor actual de su propiedad SelectedItem
.
Por todo tipo de razones, es posible que encuentre casos en los que se borre la lista de elementos del Selector y el elemento seleccionado se vuelva nulo (consulte esta pregunta ). Por lo general, no desea que su propiedad de VM se vuelva nula en este caso. Para esto, agregué la propiedad de dependencia IgnoreNullSelection, que es verdadera de manera predeterminada. Esto debería resolver tal problema.
Esta es la clase 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;
}
}
}
}
Esta es la forma de usarlo en 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>
y esta es una muestra de la propiedad de VM:
private string _selected;
public string Selected
{
get { return _selected; }
set
{
if (IsValidForSelection(value))
{
_selected = value;
}
}
}
Lo hice de manera similar a lo que splintor tiene arriba.
Su punto de vista:
<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}"/>
A continuación se muestra el código para el controlador de eventos "ComboBox_SelectionChanged" del archivo de código detrás de la vista. Por ejemplo, si ve myview.xaml, el nombre del archivo de código para este controlador de eventos debe ser 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;
}
}
}
Me gustaría completar la respuesta de splintor porque me encontré con un problema con la inicialización demorada en OnSelectedItemChanged
:
Cuando OnSelectedItemChanged se genera antes de que se asigne AssociatedObject, el uso de System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke
puede tener efectos secundarios no deseados, como intentar inicializar el newValue con el valor predeterminado de la selección del cuadro combinado.
Por lo tanto, incluso si su ViewModel está actualizado, el comportamiento activará un cambio del valor actual del SelectedItem
de ViewModel a la selección predeterminada del ComboBox almacenado en e.NewValue
. Si su código activa un cuadro de diálogo, se advertirá al usuario de un cambio, aunque no lo haya. No puedo explicar por qué sucede, probablemente sea un problema de tiempo.
Aquí está mi solución
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;
}
}
}
}
Para lograr esto en MVVM ....
1] Tener un comportamiento adjunto que maneje el evento SelectionChanged
del ComboBox. Este evento se plantea con algunos eventos args que tienen la bandera de Handled
. Pero establecerlo en true es inútil para el enlace SelectedValue
. La fuente de actualizaciones de enlace independientemente de si el evento fue manejado.
2] Por lo tanto, configuramos el enlace ComboBox.SelectedValue
para que sea TwoWay
y Explicit
.
3] Solo cuando su cheque está satisfecho y MessageBox dice que Yes
es cuando realizamos BindingExpression.UpdateSource()
. De lo contrario, simplemente llamamos a BindingExpression.UpdateTarget()
para volver a la selección anterior.
En mi ejemplo a continuación, tengo una lista de KeyValuePair<int, int>
vinculada al contexto de datos de la ventana. ComboBox.SelectedValue
está vinculado a una propiedad MyKey
de la Window
escribir.
XAML ...
<ComboBox ItemsSource="{Binding}"
DisplayMemberPath="Value"
SelectedValuePath="Key"
SelectedValue="{Binding MyKey,
ElementName=MyDGSampleWindow,
Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
local:MyAttachedBehavior.ConfirmationValueBinding="True">
</ComboBox>
Donde MyDGSampleWindow
es la x: Nombre de la Window
.
Código detrás ...
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;
}
}
Y el comportamiento adjunto
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;
}
}
}
}
}
En el comportamiento, uso la propiedad ComboBox.Tag
para almacenar temporalmente un indicador que omite la nueva verificación cuando volvemos al valor seleccionado anterior.
Déjeme saber si esto ayuda.
Prefiero la muestra del código "splintor" sobre "AngelWPF''s". Sin embargo, sus enfoques son bastante similares. Implementé el comportamiento adjunto, CancellableSelectionBehavior, y funciona según lo anunciado. Quizás fue simplemente que el código en el ejemplo de splintor era más fácil de conectar a mi aplicación. El código en el comportamiento adjunto de AngelWPF tenía referencias a un tipo KeyValuePair que habría requerido una mayor alteración del código.
En mi aplicación, tenía un ComboBox donde los elementos que se muestran en un DataGrid se basan en el elemento seleccionado en el ComboBox. Si el usuario realizó cambios en el DataGrid, luego seleccionó un nuevo elemento en el ComboBox, le pedirá al usuario que guarde los cambios con los botones Sí | NO | Cancelar como opciones. Si presionaron Cancelar, quería ignorar su nueva selección en el ComboBox y mantener la selección anterior. Esto funcionó como un campeón!
Para aquellos que atemorizan el momento en que ven las referencias a Blend and System.Windows.Interactivity, no es necesario tener instalado Microsoft Expression Blend. Puede descargar Blend SDK para .NET 4 (o Silverlight).
Oh, sí, en mi XAML, realmente uso esto como mi declaración de espacio de nombres para Blend en este ejemplo:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Solución muy simple para .NET 4.5.1+:
<ComboBox SelectedItem="{Binding SelectedItem, Delay=10}" ItemsSource="{Binding Items}" />
Me funciona en la mayoría de los casos. Puede deshacer la selección en el cuadro combinado, simplemente active NotifyPropertyChanged sin asignación de valores.
Tuve el mismo problema, las causas por el hilo de la interfaz de usuario y la forma en que funcionan las opciones. Verifique este enlace: SelectedItem en ComboBox
La estructura en la muestra utiliza el código detrás pero el MVVM es exactamente el mismo.