Databinding di una proprietà enum in un ComboBox in WPF


256

Ad esempio, prendi il seguente codice:

public enum ExampleEnum { FooBar, BarFoo }

public class ExampleClass : INotifyPropertyChanged
{
    private ExampleEnum example;

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

Voglio un database per associare la proprietà ExampleProperty a un ComboBox, in modo che mostri le opzioni "FooBar" e "BarFoo" e funzioni in modalità TwoWay. Voglio che la mia definizione ComboBox sia simile a questa:

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

Attualmente ho gestori per gli eventi ComboBox.SelectionChanged e ExampleClass.PropertyChanged installati nella mia finestra in cui eseguo manualmente l'associazione.

Esiste un modo canonico migliore o un po '? Useresti normalmente i convertitori e come popoleresti ComboBox con i giusti valori? Non voglio nemmeno iniziare subito con i18n.

modificare

Quindi è stata data una risposta: come faccio a popolare il ComboBox con i giusti valori.

Recupera i valori Enum come un elenco di stringhe tramite un ObjectDataProvider dal metodo statico 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>

Questo posso usarlo come ItemsSource per il mio ComboBox:

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"/>

4
Ho esplorato questo e ho una soluzione che puoi usare (completa con la localizzazione) in WPF che si trova qui .
travolto il

Risposte:


208

È possibile creare un'estensione di markup personalizzata.

Esempio di utilizzo:

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

Nella parte superiore della tua XAML:

    xmlns:my="clr-namespace:namespace_to_enumeration_extension_class

e poi...

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

E l'implementazione ...

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; }
    }
  }

7
@Gregor S. what my: Enumeration is?
Giosuè,

14
@Crown 'my' è il prefisso dello spazio dei nomi che dichiari in cima al tuo file xaml: ad esempio xmlns: my = "clr-namespace: namespace_to_enumeration_extension_class. L'enumerazione è l'abbreviazione di EnumerationExtension, in xaml non devi scrivere l'intero nome della classe di estensione .
Gregor Slavec

33
+1, ma la quantità di codice richiesta da WPF per realizzare le cose più semplici è davvero fondamentale
Konrad Morawski,

1
Non mi piace molto il modo in cui ti fa usare un riferimento a una parte del tuo modello - il tipo di enumerazione - nella vista, nel ItemsSourceparametro. Per mantenere la vista e il modello disaccoppiati avrei bisogno di creare una copia dell'enumerazione in ViewModel e codificare ViewModel da tradurre tra i due ... Il che renderebbe la soluzione non più così semplice. O c'è un modo per fornire il tipo stesso da ViewModel?
lampak,

6
Un'altra limitazione è che non puoi farlo se hai più lingue.
River-Claire Williamson,

176

Nel modello di visualizzazione puoi avere:

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

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

In XAML si ItemSourcelega MyEnumTypeValuese si SelectedItemlega a SelectedMyEnumType.

<ComboBox SelectedItem="{Binding SelectedMyEnumType}" ItemsSource="{Binding MyEnumTypeValues}"></ComboBox>

Funzionava magnificamente nella mia app Universal ed era molto facile da implementare. Grazie!
Nathan Strutz,

96

Preferisco non usare il nome di enum nell'interfaccia utente. Preferisco usare un valore diverso per user ( DisplayMemberPath) e diverso per value (enum in questo caso) ( SelectedValuePath). Questi due valori possono essere compressiKeyValuePair e archiviati nel dizionario.

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 */; }
}

EDIT: compatibile con il modello MVVM.


14
Penso che la tua risposta sia sottovalutata, sembra l'opzione migliore dato quello che si aspetta ComboBox. Forse potresti mettere un generatore di dizionari nel getter, usando Enum.GetValues, ma ciò non risolverebbe la parte dei nomi da visualizzare. Alla fine, e specialmente se I18n è implementato, dovrai cambiare manualmente le cose se l'enum cambia, comunque. Ma gli enum non dovrebbero cambiare spesso, se non del tutto, vero? +1
heltonbiker

2
Questa risposta è fantastica E permette di localizzare le descrizioni degli enum ... Grazie per questo!
Shay,

2
Questa soluzione è ottima perché gestisce sia enum sia localizzazione con meno codice rispetto ad altre soluzioni!
hfann,

2
Il problema con Dictionary è che le chiavi sono ordinate in base al valore hash, quindi c'è poco controllo su questo. Sebbene un po 'più prolisso, ho usato invece List <KeyValuePair <enum, string >>. Bella idea
Kevin Brock,

3
@CoperNick @Pragmateek nuova correzione: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

Non so se è possibile solo in XAML, ma prova quanto segue:

Dai un nome al tuo ComboBox in modo che tu possa accedervi nel codebehind: "typesComboBox1"

Ora prova quanto segue

typesComboBox1.ItemsSource = Enum.GetValues(typeof(ExampleEnum));

24

Basato sulla risposta accettata ma ora eliminata fornita da ageektrapped ho creato una versione ridotta senza alcune delle funzionalità più avanzate. Tutto il codice è incluso qui per consentirti di copiarlo e incollarlo e non essere bloccato da link-rot.

Uso quello System.ComponentModel.DescriptionAttributeche è veramente destinato alle descrizioni dei tempi di progettazione. Se non ti piace usare questo attributo, puoi crearne uno tuo ma penso che usare questo attributo faccia davvero il lavoro. Se non si utilizza l'attributo, il nome verrà impostato automaticamente sul nome del valore enum nel codice.

public enum ExampleEnum {

  [Description("Foo Bar")]
  FooBar,

  [Description("Bar Foo")]
  BarFoo

}

Ecco la classe utilizzata come sorgente degli elementi:

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;
  }

}

Puoi usarlo in XAML in questo modo:

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

23

Usa ObjectDataProvider:

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

e quindi associare a risorsa statica:

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

Trova questa soluzione su questo blog


Bella risposta. Per inciso, ti evita di doverti preoccupare di un Converterproblema enum-to-string.
DonBoitnott,

1
La soluzione collegata sembra morta (testo coreano o giapponese?). Se inserisco il tuo codice nelle mie risorse XAML, dice che Enum non è supportato in un progetto WPF.
Sebastian,

6

Il mio modo preferito per farlo è con un ValueConvertermodo in cui ItemsSource e SelectedValue si associno entrambi alla stessa proprietà. Ciò non richiede proprietà aggiuntive per mantenere ViewModel bello e pulito.

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

E la definizione del convertitore:

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;
  }
}

Questo convertitore funzionerà con qualsiasi enum. ValueDescriptionè solo una classe semplice con una Valueproprietà e una Descriptionproprietà. Puoi usare altrettanto facilmente aTuple con Item1e Item2, oppure un KeyValuePaircon Keye Valueinvece di Valore e Descrizione o qualsiasi altra classe di tua scelta purché possa contenere un valore enum e una descrizione stringa di quel valore enum.


Bella risposta! Per la ValueDescriptionclasse, la Descriptionproprietà può essere omessa se non necessaria. Una classe semplice con solo Valueproprietà funziona anche!
pogosama,

Inoltre, se si desidera associare un RadioButton, il metodo Convert deve restituire un elenco di stringhe, ovvero .Select(e => e.ToString())anziché utilizzare la ValueDescriptionclasse.
pogosama,

Invece di ValueDescriptionanche una KeyValuePairpotrebbe essere utilizzato, come mostrato qui
Apfelkuacha

5

Ecco una soluzione generica che utilizza un metodo di supporto. Questo può anche gestire un enum di qualsiasi tipo sottostante (byte, sbyte, uint, long, ecc.)

Metodo di supporto:

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

Vedi modello:

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

Combo box:

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

5

puoi considerare qualcosa del genere:

  1. definisci uno stile per textblock o qualsiasi altro controllo che desideri utilizzare per visualizzare il tuo enum:

    <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. definisci il tuo stile per ComboBoxItem

    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <TextBlock Tag="{Binding}" Style="{StaticResource enumStyle}"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  3. aggiungi una casella combinata e caricala con i tuoi valori enum:

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

se la tua enum è grande, puoi ovviamente fare lo stesso con il codice, risparmiando molto digitando. mi piace questo approccio, poiché semplifica la localizzazione: definisci tutti i modelli una volta, quindi aggiorni solo i file delle risorse delle stringhe.


the SelectedValuePath = "Content" mi ha aiutato qui. Ho i miei ComboBoxItems come valori di stringa e continuavo a ottenere non riesco a convertire ComboBoxItem nel mio tipo Enum. Grazie
adriaanp,

2

Se si utilizza un MVVM, in base alla risposta di @rudigrobler, è possibile effettuare le seguenti operazioni:

Aggiungi la seguente proprietà alla classe ViewModel

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

Quindi in XAML procedi come segue:

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

1

Questa è una DevExpressrisposta specifica basata sulla risposta più votata daGregor S. (attualmente ha 128 voti).

Ciò significa che possiamo mantenere lo stile coerente nell'intera applicazione:

inserisci qui la descrizione dell'immagine

Sfortunatamente, la risposta originale non funziona con un ComboBoxEditDevExpress senza alcune modifiche.

Innanzitutto, XAML per 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"/>

Inutile dire che dovrai indicare xamlExtensionslo spazio dei nomi che contiene la classe di estensione XAML (che è definita di seguito):

xmlns:xamlExtensions="clr-namespace:XamlExtensions"

E dobbiamo puntare myEnumallo spazio dei nomi che contiene l'enum:

xmlns:myEnum="clr-namespace:MyNamespace"

Quindi, l'enum:

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
    }
}

Il problema con XAML è che non possiamo usare SelectedItemValue, in quanto ciò genera un errore poiché il setter è inaccessibile (un po 'di svista da parte vostra DevExpress). Quindi dobbiamo modificare il nostro ViewModelper ottenere il valore direttamente dall'oggetto:

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");
    }
}

Per completezza, ecco l'estensione XAML dalla risposta originale (leggermente rinominata):

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
    }
}

Disclaimer: non ho alcuna affiliazione con DevExpress. Telerik è anche un'ottima biblioteca.


Per la cronaca, non sono affiliato con DevExpress. Telerik ha anche librerie molto belle, e questa tecnica potrebbe non essere nemmeno necessaria per la loro biblioteca.
Contango,

0

Prova a usare

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"
    SelectedValue="{Binding Path=ExampleProperty}" />

Questo non funziona La casella combinata mostrerà solo un testo vuoto e cambiarlo non farà nulla. Immagino che inserire un convertitore qui sarebbe la soluzione migliore.
Massimiliano

0

Ho creato un progetto CodePlex open source che lo fa. È possibile scaricare il pacchetto NuGet da qui .

<enumComboBox:EnumComboBox EnumType="{x:Type demoApplication:Status}" SelectedValue="{Binding Status}" />
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.