wpf data-binding enums radio-button

wpf - ¿Cómo enlazar RadioButtons a una enumeración?



data-binding enums (9)

Tengo una enumeración como esta:

public enum MyLovelyEnum { FirstSelection, TheOtherSelection, YetAnotherOne };

Conseguí una propiedad en mi DataContext:

public MyLovelyEnum VeryLovelyEnum { get; set; }

Y tengo tres RadioButtons en mi cliente WPF.

<RadioButton Margin="3">First Selection</RadioButton> <RadioButton Margin="3">The Other Selection</RadioButton> <RadioButton Margin="3">Yet Another one</RadioButton>

Ahora, ¿cómo puedo vincular los RadioButtons a la propiedad para un enlace correcto de dos vías?


Basado en el EnumToBooleanConverter de Scott. Noté que el método ConvertBack no funciona en el código Enum with flags.

He probado el siguiente código:

public class EnumHasFlagToBooleanConverter : IValueConverter { private object _obj; public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { _obj = value; return ((Enum)value).HasFlag((Enum)parameter); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value.Equals(true)) { if (((Enum)_obj).HasFlag((Enum)parameter)) { // Do nothing return Binding.DoNothing; } else { int i = (int)_obj; int ii = (int)parameter; int newInt = i+ii; return (NavigationProjectDates)newInt; } } else { if (((Enum)_obj).HasFlag((Enum)parameter)) { int i = (int)_obj; int ii = (int)parameter; int newInt = i-ii; return (NavigationProjectDates)newInt; } else { // do nothing return Binding.DoNothing; } } } }

Lo único que no puedo ponerme a trabajar es hacer un lanzamiento desde int a targetType por lo que lo hice codificado de forma rígida a NavigationProjectDates , la enumeración que uso. Y, targetType == NavigationProjectDates ...

Editar para más convertidor de banderas Flags genérico:

public class FlagsEnumToBooleanConverter : IValueConverter { private int _flags=0; public object Convert(object value, Type targetType, object parameter, string language) { if (value == null) return false; _flags = (int) value; Type t = value.GetType(); object o = Enum.ToObject(t, parameter); return ((Enum)value).HasFlag((Enum)o); } public object ConvertBack(object value, Type targetType, object parameter, string language) { if (value?.Equals(true) ?? false) { _flags = _flags | (int) parameter; } else { _flags = _flags & ~(int) parameter; } return _flags; } }


Este trabajo para Checkbox también.

public class EnumToBoolConverter:IValueConverter { private int val; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int intParam = (int)parameter; val = (int)value; return ((intParam & val) != 0); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { val ^= (int)parameter; return Enum.Parse(targetType, val.ToString()); } }

Enlace de una sola enumeración a múltiples casillas de verificación.



He creado una nueva clase para manejar RadioButtons y CheckBoxes vinculantes a enumeraciones. Funciona para las enumeraciones marcadas (con varias selecciones de casilla de verificación) y las enumeraciones no marcadas para las casillas de verificación de selección única o los botones de radio. Tampoco requiere ValueConverters en absoluto.

Esto puede parecer más complicado al principio, sin embargo, una vez que haya copiado esta clase en su proyecto, habrá terminado. Es genérico, por lo que se puede reutilizar fácilmente para cualquier enumeración.

public class EnumSelection<T> : INotifyPropertyChanged where T : struct, IComparable, IFormattable, IConvertible { private T value; // stored value of the Enum private bool isFlagged; // Enum uses flags? private bool canDeselect; // Can be deselected? (Radio buttons cannot deselect, checkboxes can) private T blankValue; // what is considered the "blank" value if it can be deselected? public EnumSelection(T value) : this(value, false, default(T)) { } public EnumSelection(T value, bool canDeselect) : this(value, canDeselect, default(T)) { } public EnumSelection(T value, T blankValue) : this(value, true, blankValue) { } public EnumSelection(T value, bool canDeselect, T blankValue) { if (!typeof(T).IsEnum) throw new ArgumentException($"{nameof(T)} must be an enum type"); // I really wish there was a way to constrain generic types to enums... isFlagged = typeof(T).IsDefined(typeof(FlagsAttribute), false); this.value = value; this.canDeselect = canDeselect; this.blankValue = blankValue; } public T Value { get { return value; } set { if (this.value.Equals(value)) return; this.value = value; OnPropertyChanged(); OnPropertyChanged("Item[]"); // Notify that the indexer property has changed } } [IndexerName("Item")] public bool this[T key] { get { int iKey = (int)(object)key; return isFlagged ? ((int)(object)value & iKey) == iKey : value.Equals(key); } set { if (isFlagged) { int iValue = (int)(object)this.value; int iKey = (int)(object)key; if (((iValue & iKey) == iKey) == value) return; if (value) Value = (T)(object)(iValue | iKey); else Value = (T)(object)(iValue & ~iKey); } else { if (this.value.Equals(key) == value) return; if (!value && !canDeselect) return; Value = value ? key : blankValue; } } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }

Y para saber cómo usarlo, digamos que tiene una enumeración para ejecutar una tarea de forma manual o automática, y puede programarse para cualquier día de la semana, y algunas opciones opcionales ...

public enum StartTask { Manual, Automatic } [Flags()] public enum DayOfWeek { Sunday = 1 << 0, Monday = 1 << 1, Tuesday = 1 << 2, Wednesday = 1 << 3, Thursday = 1 << 4, Friday = 1 << 5, Saturday = 1 << 6 } public enum AdditionalOptions { None = 0, OptionA, OptionB }

Ahora, aquí está lo fácil que es usar esta clase:

public class MyViewModel : ViewModelBase { public MyViewModel() { StartUp = new EnumSelection<StartTask>(StartTask.Manual); Days = new EnumSelection<DayOfWeek>(default(DayOfWeek)); Options = new EnumSelection<AdditionalOptions>(AdditionalOptions.None, true, AdditionalOptions.None); } public EnumSelection<StartTask> StartUp { get; private set; } public EnumSelection<DayOfWeek> Days { get; private set; } public EnumSelection<AdditionalOptions> Options { get; private set; } }

Y aquí es lo fácil que es enlazar casillas de verificación y botones de radio con esta clase:

<StackPanel Orientation="Vertical"> <StackPanel Orientation="Horizontal"> <!-- Using RadioButtons for exactly 1 selection behavior --> <RadioButton IsChecked="{Binding StartUp[Manual]}">Manual</RadioButton> <RadioButton IsChecked="{Binding StartUp[Automatic]}">Automatic</RadioButton> </StackPanel> <StackPanel Orientation="Horizontal"> <!-- Using CheckBoxes for 0 or Many selection behavior --> <CheckBox IsChecked="{Binding Days[Sunday]}">Sunday</CheckBox> <CheckBox IsChecked="{Binding Days[Monday]}">Monday</CheckBox> <CheckBox IsChecked="{Binding Days[Tuesday]}">Tuesday</CheckBox> <CheckBox IsChecked="{Binding Days[Wednesday]}">Wednesday</CheckBox> <CheckBox IsChecked="{Binding Days[Thursday]}">Thursday</CheckBox> <CheckBox IsChecked="{Binding Days[Friday]}">Friday</CheckBox> <CheckBox IsChecked="{Binding Days[Saturday]}">Saturday</CheckBox> </StackPanel> <StackPanel Orientation="Horizontal"> <!-- Using CheckBoxes for 0 or 1 selection behavior --> <CheckBox IsChecked="{Binding Options[OptionA]}">Option A</CheckBox> <CheckBox IsChecked="{Binding Options[OptionB]}">Option B</CheckBox> </StackPanel> </StackPanel>

  1. Cuando se cargue la interfaz de usuario, se seleccionará el botón de opción "Manual" y podrá modificar su selección entre "Manual" o "Automático", pero siempre se deberá seleccionar cualquiera de ellos.
  2. Todos los días de la semana no estarán marcados, pero cualquier número de ellos se puede marcar o no.
  3. "Opción A" y "Opción B" estarán inicialmente desactivadas. Puedes marcar uno o el otro, marcar uno desmarcará el otro (similar a RadioButtons), pero ahora también puedes desmarcar ambos (lo que no puedes hacer con RadioButton de WPF, razón por la cual se usa CheckBox aquí)

Para UWP, no es tan simple: debe saltar a través de un aro adicional para pasar un valor de campo como parámetro.

Ejemplo 1

Válido tanto para WPF como para UWP.

<MyControl> <MyControl.MyProperty> <Binding Converter="{StaticResource EnumToBooleanConverter}" Path="AnotherProperty"> <Binding.ConverterParameter> <MyLibrary:MyEnum>Field</MyLibrary:MyEnum> </Binding.ConverterParameter> </MyControl> </MyControl.MyProperty> </MyControl>

Ejemplo 2

Válido tanto para WPF como para UWP.

... <MyLibrary:MyEnum x:Key="MyEnumField">Field</MyLibrary:MyEnum> ... <MyControl MyProperty="{Binding AnotherProperty, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={StaticResource MyEnumField}}"/>

Ejemplo 3

Válido sólo para WPF!

<MyControl MyProperty="{Binding AnotherProperty, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static MyLibrary:MyEnum.Field}}"/>

UWP no admite x:Static por lo que el Ejemplo 3 está fuera de discusión; Suponiendo que vaya con el Ejemplo 1 , el resultado es un código más detallado. El ejemplo 2 es un poco mejor, pero aun así no es ideal.

Solución

public abstract class EnumToBooleanConverter<TEnum> : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { var Parameter = parameter as string; if (Parameter == null) return DependencyProperty.UnsetValue; if (Enum.IsDefined(typeof(TEnum), value) == false) return DependencyProperty.UnsetValue; return Enum.Parse(typeof(TEnum), Parameter).Equals(value); } public object ConvertBack(object value, Type targetType, object parameter, string language) { var Parameter = parameter as string; return Parameter == null ? DependencyProperty.UnsetValue : Enum.Parse(typeof(TEnum), Parameter); } }

Luego, para cada tipo que desee admitir, defina un convertidor que recuadre el tipo de enumeración.

public class MyEnumToBooleanConverter : EnumToBooleanConverter<MyEnum> { //Nothing to do! }

La razón por la que debe estar recuadrada es porque aparentemente no hay forma de hacer referencia al tipo en el método ConvertBack ; El boxeo se encarga de eso. Si utiliza cualquiera de los dos primeros ejemplos, puede simplemente hacer referencia al tipo de parámetro, eliminando la necesidad de heredar de una clase en caja; Si desea hacerlo todo en una línea y con la menor verbosidad posible, la última solución es ideal.

El uso se parece al Ejemplo 2 , pero es, de hecho, menos detallado.

<MyControl MyProperty="{Binding AnotherProperty, Converter={StaticResource MyEnumToBooleanConverter}, ConverterParameter=Field}"/>

El inconveniente es que debe definir un convertidor para cada tipo que desee admitir.


Para la respuesta de EnumToBooleanConverter: En lugar de devolver DependencyProperty.UnsetValue, considere devolver Binding. No hay nada para el caso en el que el valor de IsChecked del botón de opción se vuelva falso. El primero indica un problema (y puede mostrar al usuario un rectángulo rojo o indicadores de validación similares), mientras que el segundo simplemente indica que no se debe hacer nada, que es lo que se desea en ese caso.

http://msdn.microsoft.com/en-us/library/system.windows.data.ivalueconverter.convertback.aspx http://msdn.microsoft.com/en-us/library/system.windows.data.binding.donothing.aspx


Podrías usar un convertidor más genérico.

public class EnumBooleanConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { string parameterString = parameter as string; if (parameterString == null) return DependencyProperty.UnsetValue; if (Enum.IsDefined(value.GetType(), value) == false) return DependencyProperty.UnsetValue; object parameterValue = Enum.Parse(value.GetType(), parameterString); return parameterValue.Equals(value); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { string parameterString = parameter as string; if (parameterString == null) return DependencyProperty.UnsetValue; return Enum.Parse(targetType, parameterString); } #endregion }

Y en la parte XAML usas:

<Grid> <Grid.Resources> <l:EnumBooleanConverter x:Key="enumBooleanConverter" /> </Grid.Resources> <StackPanel > <RadioButton IsChecked="{Binding Path=VeryLovelyEnum, Converter={StaticResource enumBooleanConverter}, ConverterParameter=FirstSelection}">first selection</RadioButton> <RadioButton IsChecked="{Binding Path=VeryLovelyEnum, Converter={StaticResource enumBooleanConverter}, ConverterParameter=TheOtherSelection}">the other selection</RadioButton> <RadioButton IsChecked="{Binding Path=VeryLovelyEnum, Converter={StaticResource enumBooleanConverter}, ConverterParameter=YetAnotherOne}">yet another one</RadioButton> </StackPanel> </Grid>


Puede simplificar aún más la respuesta aceptada. En lugar de escribir las enumeraciones como cadenas en xaml y hacer más trabajo en su convertidor del necesario, puede pasar explícitamente el valor de enumeración en lugar de una representación de cadena, y como comentó CrimsonX, los errores se lanzan en tiempo de compilación en lugar de en tiempo de ejecución:

ConverterParameter = {x: Local estático: YourEnumType.Enum1}

<StackPanel> <StackPanel.Resources> <local:ComparisonConverter x:Key="ComparisonConverter" /> </StackPanel.Resources> <RadioButton IsChecked="{Binding Path=YourEnumProperty, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static local:YourEnumType.Enum1}}" /> <RadioButton IsChecked="{Binding Path=YourEnumProperty, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static local:YourEnumType.Enum2}}" /> </StackPanel>

Luego simplifica el convertidor:

public class ComparisonConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value?.Equals(parameter); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value?.Equals(true) == true ? parameter : Binding.DoNothing; } }

Nota - NullReferenceException (10 de octubre de 18):

Se actualizó el ejemplo para eliminar la posibilidad de lanzar una NullReferenceException. IsChecked es un tipo anulable, por lo que devolver Nullable<Boolean> parece una solución razonable.

Nota: múltiples grupos de RadioButtons en el mismo contenedor (17 de febrero de 2011):

En xaml, si los botones de opción comparten el mismo contenedor principal, al seleccionar uno se deseleccionará todos los demás dentro de ese contenedor (incluso si están vinculados a una propiedad diferente). Así que trate de mantener sus RadioButton''s que están vinculados a una propiedad común agrupada en su propio contenedor como un panel de pila. En los casos en que sus RadioButtons relacionados no puedan compartir un solo contenedor primario, entonces establezca la propiedad GroupName de cada RadioButton en un valor común para agruparlos lógicamente.

Nota - Tipo de enumeración anidado en una clase (28 de abril de 11):

Si su tipo de enumeración está anidado en una clase (en lugar de hacerlo directamente en el espacio de nombres), es posible que pueda usar la sintaxis ''+'' para acceder a la enumeración en XAML como se indica en una respuesta (no marcada) a la pregunta No se puede encontrar tipo de enumeración para referencia estática en WPF :

ConverterParameter = {x: Local estático: YourClass + YourNestedEnumType.Enum1}

Debido a este problema de Microsoft Connect , sin embargo, el diseñador en VS2010 ya no se cargará indicando que "Type ''local:YourClass+YourNestedEnumType'' was not found." , pero el proyecto sí compila y ejecuta con éxito. Por supuesto, puede evitar este problema si puede mover su tipo de enumeración directamente al espacio de nombres.

Editar (16 de diciembre de 10):

Gracias a anon por sugerir la devolución de Binding.DoNothing en lugar de DependencyProperty.UnsetValue.

Editar (5 de abril de 11):

ConvertBack simplificado, si-else utiliza un operador ternario.

Editar (27 de enero de 12):

Si utiliza las banderas Enum, el convertidor sería el siguiente:

public class EnumToBooleanConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return ((Enum)value).HasFlag((Enum)parameter); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return value.Equals(true) ? parameter : Binding.DoNothing; } }

Edición (7 de mayo de 15):

En el caso de un Nullable Enum (que no se plantea en la pregunta, pero puede ser necesario en algunos casos, por ejemplo, ORM devuelve nulo desde DB o siempre que tenga sentido que en la lógica del programa no se proporcione el valor), recuerde agregar una comprobación inicial nula en el Método de conversión y devuelve el valor bool apropiado, que suele ser falso (si no desea que se seleccione ningún botón de opción), como a continuación:

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value == null) { return false; // or return parameter.Equals(YourEnumType.SomeDefaultValue); } return value.Equals(parameter); }