Enlace de datos de una propiedad enum a un ComboBox en WPF

256

Como ejemplo, tome el siguiente código:

public enum ExampleEnum { FooBar, BarFoo }

public class ExampleClass : INotifyPropertyChanged
{
    private ExampleEnum example;

    public ExampleEnum ExampleProperty 
    { get { return example; } { /* set and notify */; } }
}

Quiero un enlace de datos de la propiedad ExampleProperty a un ComboBox, para que muestre las opciones "FooBar" y "BarFoo" y funcione en modo TwoWay. De manera óptima, quiero que mi definición de ComboBox se vea así:

<ComboBox ItemsSource="What goes here?" SelectedItem="{Binding Path=ExampleProperty}" />

Actualmente tengo controladores para los eventos ComboBox.SelectionChanged y ExampleClass.PropertyChanged instalados en mi ventana donde hago el enlace manualmente.

¿Existe alguna forma canónica mejor o de algún tipo? ¿Usualmente usaría Convertidores y cómo llenaría el ComboBox con los valores correctos? Ni siquiera quiero comenzar con i18n en este momento.

Editar

Entonces, se respondió una pregunta: ¿Cómo lleno el ComboBox con los valores correctos?

Recupere los valores de Enum como una lista de cadenas a través de un ObjectDataProvider del método estático Enum.GetValues:

<Window.Resources>
    <ObjectDataProvider MethodName="GetValues"
        ObjectType="{x:Type sys:Enum}"
        x:Key="ExampleEnumValues">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="ExampleEnum" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>

Esto lo puedo usar como ItemsSource para mi ComboBox:

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"/>
Maximiliano
fuente
44
Exploré esto y tengo una solución que puedes usar (completa con localización) en WPF que se encuentra aquí .
ageektrapped el

Respuestas:

208

Puede crear una extensión de marcado personalizada.

Ejemplo de uso:

enum Status
{
    [Description("Available.")]
    Available,
    [Description("Not here right now.")]
    Away,
    [Description("I don't have time right now.")]
    Busy
}

En la parte superior de tu XAML:

    xmlns:my="clr-namespace:namespace_to_enumeration_extension_class

y entonces...

<ComboBox 
    ItemsSource="{Binding Source={my:Enumeration {x:Type my:Status}}}" 
    DisplayMemberPath="Description" 
    SelectedValue="{Binding CurrentStatus}"  
    SelectedValuePath="Value"  /> 

Y la implementación ...

public class EnumerationExtension : MarkupExtension
  {
    private Type _enumType;


    public EnumerationExtension(Type enumType)
    {
      if (enumType == null)
        throw new ArgumentNullException("enumType");

      EnumType = enumType;
    }

    public Type EnumType
    {
      get { return _enumType; }
      private set
      {
        if (_enumType == value)
          return;

        var enumType = Nullable.GetUnderlyingType(value) ?? value;

        if (enumType.IsEnum == false)
          throw new ArgumentException("Type must be an Enum.");

        _enumType = value;
      }
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
      var enumValues = Enum.GetValues(EnumType);

      return (
        from object enumValue in enumValues
        select new EnumerationMember{
          Value = enumValue,
          Description = GetDescription(enumValue)
        }).ToArray();
    }

    private string GetDescription(object enumValue)
    {
      var descriptionAttribute = EnumType
        .GetField(enumValue.ToString())
        .GetCustomAttributes(typeof (DescriptionAttribute), false)
        .FirstOrDefault() as DescriptionAttribute;


      return descriptionAttribute != null
        ? descriptionAttribute.Description
        : enumValue.ToString();
    }

    public class EnumerationMember
    {
      public string Description { get; set; }
      public object Value { get; set; }
    }
  }
Gregor Slavec
fuente
77
@ Gregor S. ¿Cuál es mi: enumeración?
joshua
14
@Crown 'my' es el prefijo del espacio de nombres que declara en la parte superior del archivo xaml: por ejemplo, xmlns: my = "clr-namespace: namespace_to_enumeration_extension_class. La enumeración es la abreviatura de EnumerationExtension, en xaml no tiene que escribir el nombre completo de la clase de extensión .
Gregor Slavec
33
+1, pero la cantidad de código requerida por WPF para lograr las cosas más simples es realmente sorprendente
Konrad Morawski
1
Realmente no me gusta la forma en que te hace usar una referencia a una parte de tu modelo, el tipo de enumeración, en la vista, en el ItemsSourceparámetro. Para mantener la vista y el modelo desacoplados, necesitaría crear una copia de la enumeración en ViewModel y codificar ViewModel para traducir entre los dos ... Lo que haría que la solución ya no sea tan simple. ¿O hay una manera de suministrar el tipo en sí desde ViewModel?
lampak
66
Otra limitación es que no puede hacer esto si tiene varios idiomas.
River-Claire Williamson
176

En el modelo de vista puede tener:

public MyEnumType SelectedMyEnumType 
{
    get { return _selectedMyEnumType; }
    set { 
            _selectedMyEnumType = value;
            OnPropertyChanged("SelectedMyEnumType");
        }
}

public IEnumerable<MyEnumType> MyEnumTypeValues
{
    get
    {
        return Enum.GetValues(typeof(MyEnumType))
            .Cast<MyEnumType>();
    }
}

En XAML, se ItemSourceune MyEnumTypeValuesy se SelectedItemune a SelectedMyEnumType.

<ComboBox SelectedItem="{Binding SelectedMyEnumType}" ItemsSource="{Binding MyEnumTypeValues}"></ComboBox>
usuario659130
fuente
Esto funcionó fabulosamente en mi aplicación Universal y fue muy fácil de implementar. ¡Gracias!
Nathan Strutz
96

Prefiero no usar el nombre de enum en la interfaz de usuario. Prefiero usar un valor diferente para user ( DisplayMemberPath) y diferente para value (enum en este caso) ( SelectedValuePath). Esos dos valores se pueden empaquetar KeyValuePairy almacenar en el diccionario.

XAML

<ComboBox Name="fooBarComboBox" 
          ItemsSource="{Binding Path=ExampleEnumsWithCaptions}" 
          DisplayMemberPath="Value" 
          SelectedValuePath="Key"
          SelectedValue="{Binding Path=ExampleProperty, Mode=TwoWay}" > 

C#

public Dictionary<ExampleEnum, string> ExampleEnumsWithCaptions { get; } =
    new Dictionary<ExampleEnum, string>()
    {
        {ExampleEnum.FooBar, "Foo Bar"},
        {ExampleEnum.BarFoo, "Reversed Foo Bar"},
        //{ExampleEnum.None, "Hidden in UI"},
    };


private ExampleEnum example;
public ExampleEnum ExampleProperty
{
    get { return example; }
    set { /* set and notify */; }
}

EDITAR: Compatible con el patrón MVVM.

CoperNick
fuente
14
Creo que su respuesta está subestimada, parece la mejor opción dado lo que ComboBox espera. Tal vez podría poner un generador de diccionarios en el getter, usando Enum.GetValues, pero eso no resolvería la parte de los nombres que se mostrarán. Al final, y especialmente si se implementa I18n, tendrá que cambiar manualmente las cosas si la enumeración cambia, de todos modos. Pero no se supone que las enumeraciones cambien a menudo, si es que lo hacen, ¿verdad? +1
heltonbiker
2
Esta respuesta es increíble Y permite localizar las descripciones de las enumeraciones ... ¡Gracias por esto!
Shay
2
¡Esta solución es muy buena porque maneja tanto la enumeración como la localización con menos código que otras soluciones!
hfann
2
El problema con Dictionary es que las claves están ordenadas por valor hash, por lo que hay poco control sobre eso. Aunque un poco más detallado, usé List <KeyValuePair <enum, string >> en su lugar. Buena idea.
Kevin Brock
3
@CoperNick @Pragmateek nueva solución:public Dictionary<ExampleEnum, string> ExampleEnumsWithCaptions { get; } = new Dictionary<ExampleEnum, string>() { {ExampleEnum.FooBar, "Foo Bar"}, {ExampleEnum.BarFoo, "Reversed Foo Bar"}, //{ExampleEnum.None, "Hidden in UI"}, };
Jinjinov
40

No sé si es posible solo en XAML, pero intente lo siguiente:

Dé un nombre a su ComboBox para que pueda acceder a él en el código detrás: "typesComboBox1"

Ahora intenta lo siguiente

typesComboBox1.ItemsSource = Enum.GetValues(typeof(ExampleEnum));
rudigrobler
fuente
24

Basado en la respuesta aceptada pero ahora eliminada proporcionada por ageektrapped , creé una versión reducida sin algunas de las características más avanzadas. Todo el código se incluye aquí para permitirle copiarlo y pegarlo y no ser bloqueado por link-rot.

Utilizo el System.ComponentModel.DescriptionAttributeque realmente está destinado a descripciones de tiempo de diseño. Si no le gusta usar este atributo, puede crear el suyo propio, pero creo que usar este atributo realmente hace el trabajo. Si no usa el atributo, el nombre será por defecto el nombre del valor de enumeración en el código.

public enum ExampleEnum {

  [Description("Foo Bar")]
  FooBar,

  [Description("Bar Foo")]
  BarFoo

}

Aquí está la clase utilizada como fuente de elementos:

public class EnumItemsSource : Collection<String>, IValueConverter {

  Type type;

  IDictionary<Object, Object> valueToNameMap;

  IDictionary<Object, Object> nameToValueMap;

  public Type Type {
    get { return this.type; }
    set {
      if (!value.IsEnum)
        throw new ArgumentException("Type is not an enum.", "value");
      this.type = value;
      Initialize();
    }
  }

  public Object Convert(Object value, Type targetType, Object parameter, CultureInfo culture) {
    return this.valueToNameMap[value];
  }

  public Object ConvertBack(Object value, Type targetType, Object parameter, CultureInfo culture) {
    return this.nameToValueMap[value];
  }

  void Initialize() {
    this.valueToNameMap = this.type
      .GetFields(BindingFlags.Static | BindingFlags.Public)
      .ToDictionary(fi => fi.GetValue(null), GetDescription);
    this.nameToValueMap = this.valueToNameMap
      .ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
    Clear();
    foreach (String name in this.nameToValueMap.Keys)
      Add(name);
  }

  static Object GetDescription(FieldInfo fieldInfo) {
    var descriptionAttribute =
      (DescriptionAttribute) Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute));
    return descriptionAttribute != null ? descriptionAttribute.Description : fieldInfo.Name;
  }

}

Puedes usarlo en XAML así:

<Windows.Resources>
  <local:EnumItemsSource
    x:Key="ExampleEnumItemsSource"
    Type="{x:Type local:ExampleEnum}"/>
</Windows.Resources>
<ComboBox
  ItemsSource="{StaticResource ExampleEnumItemsSource}"
  SelectedValue="{Binding ExampleProperty, Converter={StaticResource ExampleEnumItemsSource}}"/> 
Martin Liversage
fuente
23

Utilice ObjectDataProvider:

<ObjectDataProvider x:Key="enumValues"
   MethodName="GetValues" ObjectType="{x:Type System:Enum}">
      <ObjectDataProvider.MethodParameters>
           <x:Type TypeName="local:ExampleEnum"/>
      </ObjectDataProvider.MethodParameters>
 </ObjectDataProvider>

y luego enlazar al recurso estático:

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

Encuentra esta solución en este blog

druss
fuente
Buena respuesta. Por cierto, le ahorra tener que preocuparse por un problema Converterde enumeración a cadena.
DonBoitnott
1
La solución vinculada parece estar muerta (¿texto coreano o japonés?). Si pongo su código en mis recursos XAML, dice que Enum no es compatible con un proyecto WPF.
Sebastian
6

Mi forma favorita de hacer esto es con una de ValueConvertermodo que ItemsSource y SelectedValue se unan a la misma propiedad. Esto no requiere propiedades adicionales para mantener su ViewModel agradable y limpio.

<ComboBox ItemsSource="{Binding Path=ExampleProperty, Converter={x:EnumToCollectionConverter}, Mode=OneTime}"
          SelectedValuePath="Value"
          DisplayMemberPath="Description"
          SelectedValue="{Binding Path=ExampleProperty}" />

Y la definición del convertidor:

public static class EnumHelper
{
  public static string Description(this Enum e)
  {
    return (e.GetType()
             .GetField(e.ToString())
             .GetCustomAttributes(typeof(DescriptionAttribute), false)
             .FirstOrDefault() as DescriptionAttribute)?.Description ?? e.ToString();
  }
}

[ValueConversion(typeof(Enum), typeof(IEnumerable<ValueDescription>))]
public class EnumToCollectionConverter : MarkupExtension, IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return Enum.GetValues(value.GetType())
               .Cast<Enum>()
               .Select(e => new ValueDescription() { Value = e, Description = e.Description()})
               .ToList();
  }
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    return null;
  }
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    return this;
  }
}

Este convertidor funcionará con cualquier enumeración. ValueDescriptiones solo una clase simple con una Valuepropiedad y una Descriptionpropiedad. Podrías usar fácilmente un Tuplecon Item1y Item2, o un KeyValuePaircon Keyy en Valuelugar de Valor y Descripción o cualquier otra clase de tu elección, siempre que pueda contener un valor de enumeración y una descripción de cadena de ese valor de enumeración.

Mella
fuente
¡Buena respuesta! Para la ValueDescriptionclase, la Descriptionpropiedad puede omitirse si no es necesaria. ¡Una clase simple con solo Valuepropiedad también funciona!
pogosama
Además, si desea enlazar a un RadioButton, entonces el método Convert debe devolver una lista de cadenas, es decir .Select(e => e.ToString()), en lugar de usar la ValueDescriptionclase.
pogosama
En lugar de ValueDescriptiontambién se KeyValuePairpodría usar a, como se muestra aquí
Apfelkuacha
5

Aquí hay una solución genérica que utiliza un método auxiliar. Esto también puede manejar una enumeración de cualquier tipo subyacente (byte, sbyte, uint, long, etc.)

Método de ayuda:

static IEnumerable<object> GetEnum<T>() {
    var type    = typeof(T);
    var names   = Enum.GetNames(type);
    var values  = Enum.GetValues(type);
    var pairs   =
        Enumerable.Range(0, names.Length)
        .Select(i => new {
                Name    = names.GetValue(i)
            ,   Value   = values.GetValue(i) })
        .OrderBy(pair => pair.Name);
    return pairs;
}//method

Ver modelo:

public IEnumerable<object> EnumSearchTypes {
    get {
        return GetEnum<SearchTypes>();
    }
}//property

Caja combo:

<ComboBox
    SelectedValue       ="{Binding SearchType}"
    ItemsSource         ="{Binding EnumSearchTypes}"
    DisplayMemberPath   ="Name"
    SelectedValuePath   ="Value"
/>
Jack
fuente
5

puedes considerar algo así:

  1. defina un estilo para el bloque de texto o cualquier otro control que desee usar para mostrar su enumeración:

    <Style x:Key="enumStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="Text" Value="&lt;NULL&gt;"/>
        <Style.Triggers>
            <Trigger Property="Tag">
                <Trigger.Value>
                    <proj:YourEnum>Value1<proj:YourEnum>
                </Trigger.Value>
                <Setter Property="Text" Value="{DynamicResource yourFriendlyValue1}"/>
            </Trigger>
            <!-- add more triggers here to reflect your enum -->
        </Style.Triggers>
    </Style>
  2. define tu estilo para ComboBoxItem

    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <TextBlock Tag="{Binding}" Style="{StaticResource enumStyle}"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  3. agregue un cuadro combinado y cárguelo con sus valores de enumeración:

    <ComboBox SelectedValue="{Binding Path=your property goes here}" SelectedValuePath="Content">
        <ComboBox.Items>
            <ComboBoxItem>
                <proj:YourEnum>Value1</proj:YourEnum>
            </ComboBoxItem>
        </ComboBox.Items>
    </ComboBox>

Si su enumeración es grande, por supuesto, puede hacer lo mismo en el código, ahorrando mucho tipeo. Me gusta ese enfoque, ya que facilita la localización: define todas las plantillas una vez y luego solo actualiza los archivos de recursos de cadena.

Greg
fuente
SelectedValuePath = "Content" me ayudó aquí. Tengo mis ComboBoxItems como valores de cadena, y seguí obteniendo no puedo convertir ComboBoxItem a mi tipo de enumeración. Gracias
adriaanp
2

Si está utilizando un MVVM, basado en la respuesta @rudigrobler, puede hacer lo siguiente:

Agregue la siguiente propiedad a la clase ViewModel

public Array ExampleEnumValues => Enum.GetValues(typeof(ExampleEnum));

Luego, en XAML, haga lo siguiente:

<ComboBox ItemsSource="{Binding ExampleEnumValues}" ... />
MotKohn
fuente
1

Esta es una DevExpressrespuesta específica basada en la respuesta más votada por Gregor S.(actualmente tiene 128 votos).

Esto significa que podemos mantener el estilo consistente en toda la aplicación:

ingrese la descripción de la imagen aquí

Desafortunadamente, la respuesta original no funciona con un ComboBoxEditDevExpress sin algunas modificaciones.

Primero, el XAML para ComboBoxEdit:

<dxe:ComboBoxEdit ItemsSource="{Binding Source={xamlExtensions:XamlExtensionEnumDropdown {x:myEnum:EnumFilter}}}"
    SelectedItem="{Binding BrokerOrderBookingFilterSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    DisplayMember="Description"
    MinWidth="144" Margin="5" 
    HorizontalAlignment="Left"
    IsTextEditable="False"
    ValidateOnTextInput="False"
    AutoComplete="False"
    IncrementalFiltering="True"
    FilterCondition="Like"
    ImmediatePopup="True"/>

No hace falta decir que deberá apuntar xamlExtensionsal espacio de nombres que contiene la clase de extensión XAML (que se define a continuación):

xmlns:xamlExtensions="clr-namespace:XamlExtensions"

Y tenemos que señalar myEnumel espacio de nombres que contiene la enumeración:

xmlns:myEnum="clr-namespace:MyNamespace"

Entonces, la enumeración:

namespace MyNamespace
{
    public enum EnumFilter
    {
        [Description("Free as a bird")]
        Free = 0,

        [Description("I'm Somewhat Busy")]
        SomewhatBusy = 1,

        [Description("I'm Really Busy")]
        ReallyBusy = 2
    }
}

El problema con el XAML es que no podemos usarlo SelectedItemValue, ya que esto arroja un error ya que el configurador es inaccesible (un poco de supervisión por su parte DevExpress). Entonces tenemos que modificar nuestro ViewModelpara obtener el valor directamente del objeto:

private EnumFilter _filterSelected = EnumFilter.All;
public object FilterSelected
{
    get
    {
        return (EnumFilter)_filterSelected;
    }
    set
    {
        var x = (XamlExtensionEnumDropdown.EnumerationMember)value;
        if (x != null)
        {
            _filterSelected = (EnumFilter)x.Value;
        }
        OnPropertyChanged("FilterSelected");
    }
}

Para completar, aquí está la extensión XAML de la respuesta original (ligeramente renombrada):

namespace XamlExtensions
{
    /// <summary>
    ///     Intent: XAML markup extension to add support for enums into any dropdown box, see http://bit.ly/1g70oJy. We can name the items in the
    ///     dropdown box by using the [Description] attribute on the enum values.
    /// </summary>
    public class XamlExtensionEnumDropdown : MarkupExtension
    {
        private Type _enumType;


        public XamlExtensionEnumDropdown(Type enumType)
        {
            if (enumType == null)
            {
                throw new ArgumentNullException("enumType");
            }

            EnumType = enumType;
        }

        public Type EnumType
        {
            get { return _enumType; }
            private set
            {
                if (_enumType == value)
                {
                    return;
                }

                var enumType = Nullable.GetUnderlyingType(value) ?? value;

                if (enumType.IsEnum == false)
                {
                    throw new ArgumentException("Type must be an Enum.");
                }

                _enumType = value;
            }
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var enumValues = Enum.GetValues(EnumType);

            return (
                from object enumValue in enumValues
                select new EnumerationMember
                       {
                           Value = enumValue,
                           Description = GetDescription(enumValue)
                       }).ToArray();
        }

        private string GetDescription(object enumValue)
        {
            var descriptionAttribute = EnumType
                .GetField(enumValue.ToString())
                .GetCustomAttributes(typeof (DescriptionAttribute), false)
                .FirstOrDefault() as DescriptionAttribute;


            return descriptionAttribute != null
                ? descriptionAttribute.Description
                : enumValue.ToString();
        }

        #region Nested type: EnumerationMember
        public class EnumerationMember
        {
            public string Description { get; set; }
            public object Value { get; set; }
        }
        #endregion
    }
}

Descargo de responsabilidad: no tengo afiliación con DevExpress. Telerik es también una gran biblioteca.

Aplazamiento de pago
fuente
Para el registro, no estoy afiliado a DevExpress. Telerik también tiene bibliotecas muy buenas, y esta técnica podría no ser necesaria para su biblioteca.
Contango
0

Intenta usar

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"
    SelectedValue="{Binding Path=ExampleProperty}" />
rudigrobler
fuente
Esto no funciona El cuadro combinado solo mostrará un texto vacío y cambiarlo no hará nada. Supongo que arrojar un convertidor aquí sería la mejor solución.
Maximiliano
0

He creado un proyecto de código abierto de CodePlex que hace esto. Puede descargar el paquete NuGet desde aquí .

<enumComboBox:EnumComboBox EnumType="{x:Type demoApplication:Status}" SelectedValue="{Binding Status}" />
LawMan
fuente