MVVM: ¿Enlazar botones de radio para ver un modelo?

EDITAR: el problema se corrigió en .NET 4.0.

He intentado vincular un grupo de botones de radio a un modelo de vista con el botón IsChecked . Después de revisar otras publicaciones, parece que la propiedad IsChecked simplemente no funciona. He creado una breve demostración que reproduce el problema, que he incluido a continuación.

Aquí está mi pregunta: ¿Existe una manera directa y confiable de enlazar botones de radio usando MVVM? Gracias.

Información adicional: la propiedad IsChecked no funciona por dos motivos:

  1. Cuando se selecciona un botón, las propiedades IsChecked de otros botones en el grupo no se establecen en falso .

  2. Cuando se selecciona un botón, su propiedad IsChecked no se establece después de la primera vez que se selecciona el botón. Supongo que WPF arruinará el enlace en el primer clic.

Proyecto de demostración: Aquí está el código y el marcado para una demostración simple que reproduce el problema. Cree un proyecto WPF y reemplace el marcado en Window1.xaml con lo siguiente:

       

Reemplace el código en Window1.xaml.cs con el siguiente código (un truco), que establece el modelo de vista:

 using System.Windows; namespace WpfApplication1 { ///  /// Interaction logic for Window1.xaml ///  public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { this.DataContext = new Window1ViewModel(); } } } 

Ahora agregue el siguiente código al proyecto como Window1ViewModel.cs :

 using System.Windows; namespace WpfApplication1 { public class Window1ViewModel { private bool p_ButtonAIsChecked; ///  /// Summary ///  public bool ButtonAIsChecked { get { return p_ButtonAIsChecked; } set { p_ButtonAIsChecked = value; MessageBox.Show(string.Format("Button A is checked: {0}", value)); } } private bool p_ButtonBIsChecked; ///  /// Summary ///  public bool ButtonBIsChecked { get { return p_ButtonBIsChecked; } set { p_ButtonBIsChecked = value; MessageBox.Show(string.Format("Button B is checked: {0}", value)); } } } } 

Para reproducir el problema, ejecute la aplicación y haga clic en el botón A. Aparecerá un cuadro de mensaje que IsChecked propiedad IsChecked del botón A se ha establecido en verdadero . Ahora seleccione el Botón B. Aparecerá otro cuadro de mensaje que IsChecked propiedad IsChecked del Botón B se ha configurado como verdadera , pero no hay ningún cuadro de mensaje que indique que la propiedad IsChecked del Botón A se haya establecido en falso, la propiedad no se ha modificado.

Ahora haz clic en el Botón A nuevamente. El botón se seleccionará en la ventana, pero no aparecerá ningún cuadro de mensaje: la propiedad IsChecked no se ha modificado. Finalmente, haga clic en el Botón B nuevamente – el mismo resultado. La propiedad IsChecked no se actualiza en absoluto para ninguno de los botones después de que se hace clic en el botón por primera vez.

Si comienzas con la sugerencia de Jason, entonces el problema se convierte en una única selección encuadernada de una lista que se traduce muy bien en un ListBox . En ese punto, es trivial aplicar estilo a un control ListBox para que aparezca como una lista de RadioButton .

      

Parece que arreglaron el enlace a la propiedad IsChecked en .NET 4. Un proyecto que se rompió en VS2008 funciona en VS2010.

Para el beneficio de cualquiera que investigue esta pregunta en el futuro, esta es la solución que finalmente implementé. Se basa en la respuesta de John Bowen, que seleccioné como la mejor solución al problema.

Primero, creé un estilo para un cuadro de lista transparente que contenía botones de opción como elementos. Luego, creé los botones para ir al cuadro de lista: mis botones están fijos, en lugar de leerlos en la aplicación como datos, así que los codifiqué en el marcado.

Yo uso una enumeración llamada ListButtons en el modelo de vista para representar los botones en el cuadro de lista, y utilizo la propiedad Tag cada botón para pasar un valor de cadena del valor enum para usar para ese botón. La propiedad ListBox.SelectedValuePath me permite especificar la propiedad Tag como fuente para el valor seleccionado, que vinculo al modelo de vista usando la propiedad SelectedValue . Pensé que necesitaría un convertidor de valor para convertir entre la cadena y su valor enum, pero los convertidores incorporados de WPF manejaron la conversión sin problemas.

Aquí está el marcado completo para Window1.xaml :

                       Button A Button B    

El modelo de vista tiene una sola propiedad, SelectedButton, que usa una enumeración ListButtons para mostrar qué botón está seleccionado. La propiedad llama a un evento en la clase base que uso para ver modelos, lo que aumenta el evento PropertyChanged :

 namespace RadioButtonMvvmDemo { public enum ListButtons {ButtonA, ButtonB} public class Window1ViewModel : ViewModelBase { private ListButtons p_SelectedButton; public Window1ViewModel() { SelectedButton = ListButtons.ButtonB; } ///  /// The button selected by the user. ///  public ListButtons SelectedButton { get { return p_SelectedButton; } set { p_SelectedButton = value; base.RaisePropertyChangedEvent("SelectedButton"); } } } } 

En mi aplicación de producción, el SelectedButton SelectButton llamará a un método de clase de servicio que tomará la acción requerida cuando se seleccione un botón.

Y para ser completo, aquí está la clase base:

 using System.ComponentModel; namespace RadioButtonMvvmDemo { public abstract class ViewModelBase : INotifyPropertyChanged { #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion #region Protected Methods ///  /// Raises the PropertyChanged event. ///  /// The name of the changed property. protected void RaisePropertyChangedEvent(string propertyName) { if (PropertyChanged != null) { PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName); PropertyChanged(this, e); } } #endregion } } 

¡Espero que ayude!

Una solución es actualizar ViewModel para los botones de opción en el setter de las propiedades. Cuando el Botón A se establece en True, establece el Botón B en falso.

Otro factor importante cuando se vincula a un objeto en DataContext es que el objeto debe implementar INotifyPropertyChanged. Cuando cualquier propiedad vinculada cambia, el evento debe activarse e incluir el nombre de la propiedad modificada. (Se omite la verificación nula en la muestra por brevedad).

 public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected bool _ButtonAChecked = true; public bool ButtonAChecked { get { return _ButtonAChecked; } set { _ButtonAChecked = value; PropertyChanged(this, new PropertyChangedEventArgs("ButtonAChecked")); if (value) ButtonBChecked = false; } } protected bool _ButtonBChecked; public bool ButtonBChecked { get { return _ButtonBChecked; } set { _ButtonBChecked = value; PropertyChanged(this, new PropertyChangedEventArgs("ButtonBChecked")); if (value) ButtonAChecked = false; } } } 

Editar:

El problema es que, al hacer clic primero en el botón B, el valor de IsChecked cambia y el enlace se transfiere, pero el botón A no pasa a través de su estado no verificado a la propiedad ButtonAChecked. Al actualizar manualmente en el código, se llamará al ajustador de propiedades ButtonAChecked la próxima vez que se haga clic en el botón A.

No estoy seguro acerca de cualquier error detectado en IsCheck, un posible refactorizador que podría hacer a su modelo de vista: la vista tiene una serie de estados mutuamente excluyentes representados por una serie de RadioButtons, solo se puede seleccionar uno de ellos en un momento dado. En el modelo de vista, solo tiene 1 propiedad (por ejemplo, una enumeración) que represente los estados posibles: stateA, stateB, etc. De esta forma no necesitaría todos los ButtonAIsChecked individuales, etc.

Aquí hay otra manera de hacerlo

VER:

        

ViewModel:

  private string selectedTitle; public string SelectedTitle { get { return selectedTitle; } set { SetProperty(ref selectedTitle, value); } } public RelayCommand TitleCommand { get { return new RelayCommand((p) => { selectedTitle = (string)p; }); } } 

Una pequeña extensión de la respuesta de John Bowen: no funciona cuando los valores no implementan ToString() . Lo que necesita en lugar de configurar el Content de RadioButton en un TemplateBinding, simplemente ponga un ContentPresenter en él, así:

      

De esta forma, también puede usar DisplayMemberPath o ItemTemplate según corresponda. El RadioButton simplemente “envuelve” los artículos, proporcionando la selección.

Debes agregar el nombre del grupo para el botón de opción

      

Sé que esta es una vieja pregunta y el problema original se resolvió en .NET 4. y honestamente, esto está ligeramente fuera de tema.

En la mayoría de los casos en los que he querido usar RadioButtons en MVVM es para seleccionar entre elementos de una enumeración , esto requiere vincular una propiedad bool en el espacio VM a cada botón y usarlos para establecer una propiedad enum global que refleje la selección real, esto se pone muy tedioso muy rápido. Así que se me ocurrió una solución que es reutilizable y muy fácil de implementar, y no requiere ValueConverters.

La Vista es prácticamente la misma, pero una vez que tiene su enumeración en su lugar, el lado VM se puede hacer con una sola propiedad.

MainWindowVM

 using System.ComponentModel; namespace EnumSelectorTest { public class MainWindowVM : INotifyPropertyChanged { public EnumSelectorVM Selector { get; set; } private string _colorName; public string ColorName { get { return _colorName; } set { if (_colorName == value) return; _colorName = value; RaisePropertyChanged("ColorName"); } } public MainWindowVM() { Selector = new EnumSelectorVM ( typeof(MyColors), MyColors.Red, false, val => ColorName = "The color is " + ((MyColors)val).ToString() ); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } } 

La clase que hace todo el trabajo hereda de DynamicObject. Visto desde el exterior crea una propiedad bool para cada elemento en la enumeración prefijada con ‘Is’, ‘IsRed’, ‘IsBlue’, etc. a los que se puede vincular desde XAML. Junto con una propiedad de valor que contiene el valor enum real.

 public enum MyColors { Red, Magenta, Green, Cyan, Blue, Yellow } 

EnumSelectorVM

 using System; using System.ComponentModel; using System.Dynamic; using System.Linq; namespace EnumSelectorTest { public class EnumSelectorVM : DynamicObject, INotifyPropertyChanged { //------------------------------------------------------------------------------------------------------------------------------------------ #region Fields private readonly Action _action; private readonly Type _enumType; private readonly string[] _enumNames; private readonly bool _notifyAll; #endregion Fields //------------------------------------------------------------------------------------------------------------------------------------------ #region Properties private object _value; public object Value { get { return _value; } set { if (_value == value) return; _value = value; RaisePropertyChanged("Value"); _action?.Invoke(_value); } } #endregion Properties //------------------------------------------------------------------------------------------------------------------------------------------ #region Constructor public EnumSelectorVM(Type enumType, object initialValue, bool notifyAll = false, Action action = null) { if (!enumType.IsEnum) throw new ArgumentException("enumType must be of Type: Enum"); _enumType = enumType; _enumNames = enumType.GetEnumNames(); _notifyAll = notifyAll; _action = action; //do last so notification fires and action is executed Value = initialValue; } #endregion Constructor //------------------------------------------------------------------------------------------------------------------------------------------ #region Methods //--------------------------------------------------------------------- #region Public Methods public override bool TryGetMember(GetMemberBinder binder, out object result) { string elementName; if (!TryGetEnumElemntName(binder.Name, out elementName)) { result = null; return false; } try { result = Value.Equals(Enum.Parse(_enumType, elementName)); } catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) { result = null; return false; } return true; } public override bool TrySetMember(SetMemberBinder binder, object newValue) { if (!(newValue is bool)) return false; string elementName; if (!TryGetEnumElemntName(binder.Name, out elementName)) return false; try { if((bool) newValue) Value = Enum.Parse(_enumType, elementName); } catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException) { return false; } if (_notifyAll) foreach (var name in _enumNames) RaisePropertyChanged("Is" + name); else RaisePropertyChanged("Is" + elementName); return true; } #endregion Public Methods //--------------------------------------------------------------------- #region Private Methods private bool TryGetEnumElemntName(string bindingName, out string elementName) { elementName = ""; if (bindingName.IndexOf("Is", StringComparison.Ordinal) != 0) return false; var name = bindingName.Remove(0, 2); // remove first 2 chars "Is" if (!_enumNames.Contains(name)) return false; elementName = name; return true; } #endregion Private Methods #endregion Methods //------------------------------------------------------------------------------------------------------------------------------------------ #region Events public event PropertyChangedEventHandler PropertyChanged; protected virtual void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion Events } } 

Para responder a los cambios, puede suscribirse al evento NotifyPropertyChanged o pasar un método anónimo al constructor como se hizo anteriormente.

Y finalmente, MainWindow.xaml

    Red Magenta Blue Cyan Green Yellow     

Espero que alguien más lo encuentre útil, porque creo que esto va en mi caja de herramientas.

Tengo un problema muy similar en VS2015 y .NET 4.5.1

XAML:

          

….

Como puede ver en este código, tengo una vista de lista, y los elementos en la plantilla son botones de radio que pertenecen a un nombre de grupo.

Si agrego un nuevo elemento a la colección con la propiedad Seleccionado establecido en Verdadero, aparece marcado y el rest de botones permanecen marcados.

Lo resuelvo obteniendo el botón checked primero y configurándolo en falso manualmente, pero esta no es la forma en que se supone que debe hacerse.

código detrás:

 `.... lstInCallList.ItemsSource = ContactCallList AddHandler ContactCallList.CollectionChanged, AddressOf collectionInCall_change ..... Public Sub collectionInCall_change(sender As Object, e As NotifyCollectionChangedEventArgs) 'Whenever collection change we must test if there is no selection and autoselect first. If e.Action = NotifyCollectionChangedAction.Add Then 'The solution is this, but this shouldn't be necessary 'Dim seleccionado As RadioButton = getCheckedRB(lstInCallList) 'If seleccionado IsNot Nothing Then ' seleccionado.IsChecked = False 'End If DirectCast(e.NewItems(0), PhoneCall).Selected = True ..... End sub 

`

 Male Female 

Debajo está el código para IValueConverter

 public class GenderConvertor : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return !(bool)value; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return !(bool)value; } } 

esto funcionó para mí. Incluso el valor se combinó tanto en la vista como en el modelo de vista de acuerdo con el clic del botón de radio. Verdadero -> Masculino y Falso -> Femenino