Cómo vincular una enumeración a un control de cuadro combinado en WPF?

Estoy tratando de encontrar un ejemplo simple donde las enumeraciones se muestran tal cual. Todos los ejemplos que he visto intentan agregar cadenas de visualización atractivas, pero no quiero esa complejidad.

Básicamente, tengo una clase que contiene todas las propiedades que ato, primero configurando el DataContext para esta clase, y luego especificando el enlace como este en el archivo xaml:

 

Pero esto no muestra los valores enum en el ComboBox como elementos.

Puede hacerlo desde el código colocando el siguiente código en el controlador de eventos de Window Loaded , por ejemplo:

 yourComboBox.ItemsSource = Enum.GetValues(typeof(EffectStyle)).Cast(); 

Si necesita vincularlo en XAML, debe usar ObjectDataProvider para crear un objeto disponible como fuente de enlace:

             

Llamar la atención sobre el próximo código:

 xmlns:System="clr-namespace:System;assembly=mscorlib" xmlns:StyleAlias="clr-namespace:Motion.VideoEffects" 

Guíe cómo mapear el espacio de nombres y el ensamblaje que puede leer en MSDN .

Me gusta que todos los objetos que estoy vinculando se definan en mi ViewModel , así que trato de evitar el uso de en el xaml cuando sea posible.

Mi solución no usa datos definidos en la Vista ni código subyacente. Solo un DataBinding, un ValueConverter reutilizable, un método para obtener una colección de descripciones para cualquier tipo de Enum, y una única propiedad en el ViewModel para enlazar.

Cuando quiero vincular un Enum a un ComboBox el texto que quiero mostrar nunca coincide con los valores de Enum , entonces uso el atributo [Description()] para darle el texto que realmente quiero ver en el ComboBox . Si tuviera una enumeración de clases de personajes en un juego, se vería algo como esto:

 public enum PlayerClass { // add an optional blank value for default/no selection [Description("")] NOT_SET = 0, [Description("Shadow Knight")] SHADOW_KNIGHT, ... } 

Primero creé clases de ayuda con un par de métodos para tratar con enums. Un método obtiene una descripción para un valor específico, el otro método obtiene todos los valores y sus descripciones para un tipo.

 public static class EnumHelper { public static string Description(this Enum value) { var attributes = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false); if (attributes.Any()) return (attributes.First() as DescriptionAttribute).Description; // If no description is found, the least we can do is replace underscores with spaces // You can add your own custom default formatting logic here TextInfo ti = CultureInfo.CurrentCulture.TextInfo; return ti.ToTitleCase(ti.ToLower(value.ToString().Replace("_", " "))); } public static IEnumerable GetAllValuesAndDescriptions(Type t) { if (!t.IsEnum) throw new ArgumentException($"{nameof(t)} must be an enum type"); return Enum.GetValues(t).Cast().Select((e) => new ValueDescription() { Value = e, Description = e.Description() }).ToList(); } } 

A continuación, creamos un ValueConverter . Heredar de MarkupExtension hace más fácil de usar en XAML, por lo que no es necesario que lo declare como recurso.

 [ValueConversion(typeof(Enum), typeof(IEnumerable))] public class EnumToCollectionConverter : MarkupExtension, IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return EnumHelper.GetAllValuesAndDescriptions(value.GetType()); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return null; } public override object ProvideValue(IServiceProvider serviceProvider) { return this; } } 

Mi ViewModel solo necesita 1 propiedad a la que se pueda vincular mi View tanto para SelectedValue como para ItemsSource del cuadro combinado:

 private PlayerClass playerClass; public PlayerClass SelectedClass { get { return playerClass; } set { if (playerClass != value) { playerClass = value; OnPropertyChanged(nameof(SelectedClass)); } } } 

Y finalmente vincular la vista ComboBox (usando ValueConverter en el enlace ItemsSource ) …

  

Para implementar esta solución solo necesita copiar mi clase EnumToCollectionConverter y la clase EnumToCollectionConverter . Trabajarán con cualquier enumeración. Además, no lo ValueDescription aquí, pero la clase ValueDescription es simplemente una clase simple con 2 propiedades de objetos públicos, una llamada Value , una llamada Description . Puede crearlo usted mismo o puede cambiar el código para usar un Tuple o KeyValuePair

Usé otra solución usando MarkupExtension.

  1. Hice clase que proporciona fuente de elementos:

     public class EnumToItemsSource : MarkupExtension { private readonly Type _type; public EnumToItemsSource(Type type) { _type = type; } public override object ProvideValue(IServiceProvider serviceProvider) { return Enum.GetValues(_type) .Cast() .Select(e => new { Value = (int)e, DisplayName = e.ToString() }); } } 
  2. Eso es casi todo … Ahora úsalo en XAML:

       
  3. Change ‘enums: States’ to your enum

Use ObjectDataProvider:

      

y luego enlazar al recurso estático:

 ItemsSource="{Binding Source={StaticResource enumValues}}" 

La respuesta de Nick realmente me ha ayudado, pero me di cuenta de que podría modificarse ligeramente para evitar una clase adicional, ValueDescription. Recordé que ya existe una clase KeyValuePair en el marco, por lo que puede usarse en su lugar.

El código cambia solo ligeramente:

 public static IEnumerable> GetAllValuesAndDescriptions() where TEnum : struct, IConvertible, IComparable, IFormattable { if (!typeof(TEnum).IsEnum) { throw new ArgumentException("TEnum must be an Enumeration type"); } return from e in Enum.GetValues(typeof(TEnum)).Cast() select new KeyValuePair(e.ToString(), e.Description()); } public IEnumerable> PlayerClassList { get { return EnumHelper.GetAllValuesAndDescriptions(); } } 

y finalmente el XAML:

  

Espero que esto sea útil para otros.

Tendrá que crear una matriz de valores en la enumeración, que se puede crear llamando a System.Enum.GetValues ​​() , pasándole el Type de la enumeración de la que desea los elementos.

Si especifica esto para la propiedad ItemsSource , debe rellenarse con todos los valores de la enumeración. Es probable que desee vincular SelectedItem a EffectStyle (suponiendo que sea una propiedad de la misma enumeración y contenga el valor actual).

Todas las publicaciones anteriores han perdido un truco simple. Es posible desde el enlace de SelectedValue para averiguar cómo llenar automáticamente la ItemsSource AUTOMAGICALLY para que su marca XAML sea justa.

  

Por ejemplo, en mi ViewModel tengo

 public enum FoolEnum { AAA, BBB, CCC, DDD }; FoolEnum _Fool; public FoolEnum Fool { get { return _Fool; } set { ValidateRaiseAndSetIfChanged(ref _Fool, value); } } 

ValidateRaiseAndSetIfChanged es mi gancho INPC. El tuyo puede diferir

La implementación de EnumComboBox es la siguiente, pero primero necesitaré un pequeño ayudante para obtener mis cadenas y valores de enumeración

  public static List> EnumToList(Type t) { return Enum .GetValues(t) .Cast() .Select(x=>Tuple.Create(x, x.ToString(), (int)x)) .ToList(); } 

y la clase principal (Nota, estoy usando ReactiveUI para enganchar cambios de propiedad a través de WhenAny)

 using ReactiveUI; using ReactiveUI.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Windows; using System.Windows.Documents; namespace My.Controls { public class EnumComboBox : System.Windows.Controls.ComboBox { static EnumComboBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(EnumComboBox), new FrameworkPropertyMetadata(typeof(EnumComboBox))); } protected override void OnInitialized( EventArgs e ) { base.OnInitialized(e); this.WhenAnyValue(p => p.SelectedValue) .Where(p => p != null) .Select(o => o.GetType()) .Where(t => t.IsEnum) .DistinctUntilChanged() .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(FillItems); } private void FillItems(Type enumType) { List> values = new List>(); foreach (var idx in EnumUtils.EnumToList(enumType)) { values.Add(new KeyValuePair(idx.Item1, idx.Item2)); } this.ItemsSource = values.Select(o=>o.Key.ToString()).ToList(); UpdateLayout(); this.ItemsSource = values; this.DisplayMemberPath = "Value"; this.SelectedValuePath = "Key"; } } } 

También necesitas establecer el estilo correctamente en Generic.XAML o tu caja no renderizará nada y te arrancarás el pelo.

  

y eso es eso. Obviamente, esto podría ampliarse para admitir i18n pero haría que la publicación sea más larga.

 public class EnumItemsConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (!value.GetType().IsEnum) return false; var enumName = value.GetType(); var obj = Enum.Parse(enumName, value.ToString()); return System.Convert.ToInt32(obj); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return Enum.ToObject(targetType, System.Convert.ToInt32(value)); } } 

Debería extender la respuesta de Rogers y Greg con ese tipo de convertidor de valores Enum, si está vinculando directamente a las propiedades del modelo de objetos enum.

Las aplicaciones universales parecen funcionar de forma un poco diferente; no tiene todo el poder de XAML con todas las funciones. Lo que funcionó para mí es:

  1. Creé una lista de los valores enum como las enumeraciones (no convertidas en cadenas o en enteros) y vinculé el ComboBox ItemsSource a ese
  2. Entonces podría enlazar el elemento de ComboBox seleccionado a mi propiedad pública cuyo tipo es la enumeración en cuestión

Solo por diversión, abrí una pequeña clase de plantillas para ayudar con esto y lo publiqué en las páginas de MSDN Samples . Los bits adicionales me permiten opcionalmente anular los nombres de las enumeraciones y permitirme ocultar algunas de las enumeraciones. Mi código se parece mucho a como el de Nick (arriba), que desearía haber visto antes.

Ejecutando la muestra; incluye múltiples enlaces twoway a la enumeración

Si está vinculando a una propiedad de enumeración real en su ViewModel, no a una representación int de una enumeración, las cosas se vuelven complicadas. Encontré que es necesario enlazar a la representación de cadena, NO el valor int como se espera en todos los ejemplos anteriores.

Puede decir si este es el caso vinculando un simple cuadro de texto a la propiedad a la que desea enlazar en su ViewModel. Si muestra texto, agréguese a la cadena. Si muestra un número, ligue al valor. Tenga en cuenta que he usado Mostrar dos veces, lo que normalmente sería un error, pero es la única forma en que funciona.

  

Greg

Me gustó la respuesta de tom.maruska , pero necesitaba admitir cualquier tipo de enumeración que mi plantilla pudiera encontrar en tiempo de ejecución. Para eso, tuve que usar un enlace para especificar el tipo a la extensión de marcado. Pude trabajar en esta respuesta de nicolay.anykienko para llegar a una extensión de marcado muy flexible que funcionaría en cualquier caso que se me ocurriera. Se consume así:

  

El origen de la extensión de marcado purificado arriba mencionada:

 class EnumToObjectArray : MarkupExtension { public BindingBase SourceEnum { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget; DependencyObject targetObject; DependencyProperty targetProperty; if (target != null && target.TargetObject is DependencyObject && target.TargetProperty is DependencyProperty) { targetObject = (DependencyObject)target.TargetObject; targetProperty = (DependencyProperty)target.TargetProperty; } else { return this; } BindingOperations.SetBinding(targetObject, EnumToObjectArray.SourceEnumBindingSinkProperty, SourceEnum); var type = targetObject.GetValue(SourceEnumBindingSinkProperty).GetType(); if (type.BaseType != typeof(System.Enum)) return this; return Enum.GetValues(type) .Cast() .Select(e => new { Value=e, Name = e.ToString(), DisplayName = Description(e) }); } private static DependencyProperty SourceEnumBindingSinkProperty = DependencyProperty.RegisterAttached("SourceEnumBindingSink", typeof(Enum) , typeof(EnumToObjectArray), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits)); ///  /// Extension method which returns the string specified in the Description attribute, if any. Oherwise, name is returned. ///  /// The enum value. ///  public static string Description(Enum value) { var attrs = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false); if (attrs.Any()) return (attrs.First() as DescriptionAttribute).Description; //Fallback return value.ToString().Replace("_", " "); } } 

Hay muchas respuestas excelentes a esta pregunta y humildemente presento la mía. Encuentro que el mío es algo más simple y más elegante. Requiere solo un convertidor de valor.

Dado un enum …

 public enum ImageFormat { [Description("Windows Bitmap")] BMP, [Description("Graphics Interchange Format")] GIF, [Description("Joint Photographic Experts Group Format")] JPG, [Description("Portable Network Graphics Format")] PNG, [Description("Tagged Image Format")] TIFF, [Description("Windows Media Photo Format")] WDP } 

y un convertidor de valor …

 public class ImageFormatValueConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is ImageFormat format) { return GetString(format); } return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (value is string s) { return Enum.Parse(typeof(ImageFormat), s.Substring(0, s.IndexOf(':'))); } return null; } public string[] Strings => GetStrings(); public static string GetString(ImageFormat format) { return format.ToString() + ": " + GetDescription(format); } public static string GetDescription(ImageFormat format) { return format.GetType().GetMember(format.ToString())[0].GetCustomAttribute().Description; } public static string[] GetStrings() { List list = new List(); foreach (ImageFormat format in Enum.GetValues(typeof(ImageFormat))) { list.Add(GetString(format)); } return list.ToArray(); } } 

recursos …

   

Declaración XAML …

   

Ver modelo …

  private ImageFormat _imageFormat = ImageFormat.JPG; public ImageFormat Format { get => _imageFormat; set { if (_imageFormat != value) { _imageFormat = value; OnPropertyChanged(); } } } 

Cuadro combinado resultante …

ComboBox enlazado a enum

Usando ReactiveUI , he creado la siguiente solución alternativa. No es una solución todo en uno elegante, pero creo que al menos es legible.

En mi caso, vincular una lista de enum a un control es un caso raro, por lo que no es necesario escalar la solución a través de la base de código. Sin embargo, el código puede hacerse más genérico al cambiar EffectStyleLookup.Item en un Object . Lo probé con mi código, no son necesarias otras modificaciones. Lo que significa que la clase de un solo ayudante podría aplicarse a cualquier lista enum . Aunque eso reduciría su legibilidad, ReactiveList no tiene un buen ReactiveList .

Usando la siguiente clase de ayuda:

 public class EffectStyleLookup { public EffectStyle Item { get; set; } public string Display { get; set; } } 

En ViewModel, convierta la lista de enumeraciones y exhíbala como una propiedad:

 public ViewModel : ReactiveObject { private ReactiveList _effectStyles; public ReactiveList EffectStyles { get { return _effectStyles; } set { this.RaiseAndSetIfChanged(ref _effectStyles, value); } } // See below for more on this private EffectStyle _selectedEffectStyle; public EffectStyle SelectedEffectStyle { get { return _selectedEffectStyle; } set { this.RaiseAndSetIfChanged(ref _selectedEffectStyle, value); } } public ViewModel() { // Convert a list of enums into a ReactiveList var list = (IList)Enum.GetValues(typeof(EffectStyle)) .Select( x => new EffectStyleLookup() { Item = x, Display = x.ToString() }); EffectStyles = new ReactiveList( list ); } } 

En el ComboBox , utilice la propiedad SelectedValuePath para enlazar con el valor enum original:

  

En la Vista, esto nos permite vincular el enum original al SelectedEffectStyle en el ViewModel, pero muestra el valor de ToString() en el ComboBox :

 this.WhenActivated( d => { d( this.OneWayBind(ViewModel, vm => vm.EffectStyles, v => v.EffectStyle.ItemsSource) ); d( this.Bind(ViewModel, vm => vm.SelectedEffectStyle, v => v.EffectStyle.SelectedValue) ); }); 

Estoy agregando mi comentario (en VB, lamentablemente, pero el concepto se puede replicar fácilmente en C # en un abrir y cerrar de ojos), porque solo tuve que hacer referencia a esto y no me gustó ninguna de las respuestas, ya que eran demasiado complejas. No debería ser tan difícil.

Así que se me ocurrió una manera más fácil. Enlaza los enumeradores a un diccionario. Enlace ese diccionario al Combobox.

Mi combobox:

  

Mi código detrás. Con suerte, esto ayuda a alguien más.

 Dim tDict As New Dictionary(Of Integer, String) Dim types = [Enum].GetValues(GetType(Helper.Enumerators.AllowedType)) For Each x As Helper.Enumerators.AllowedType In types Dim z = x.ToString() Dim y = CInt(x) tDict.Add(y, z) Next cmbRole.ClearValue(ItemsControl.ItemsSourceProperty) cmbRole.ItemsSource = tDict 

Explicación simple y clara: http://brianlagunas.com/a-better-way-to-data-bind-enums-in-wpf/

 xmlns:local="clr-namespace:BindingEnums" xmlns:sys="clr-namespace:System;assembly=mscorlib"