[C#] Come associare un enum a un controllo a combobox in WPF?


Answers

Mi piace per tutti gli oggetti che sono vincolante da definire nel mio ViewModel , quindi cerco di evitare l'uso di <ObjectDataProvider> nella xaml quando possibile.

La mia soluzione non utilizza dati definiti nella vista e senza code-behind. Solo un DataBinding, un ValueConverter riutilizzabile, un metodo per ottenere una raccolta di descrizioni per qualsiasi tipo di Enum e una singola proprietà nel ViewModel a cui eseguire il binding.

Quando voglio associare un Enum a un ComboBox il testo che voglio visualizzare non corrisponde mai ai valori Enum , quindi uso l'attributo [Description()] per dargli il testo che voglio effettivamente vedere nel ComboBox . Se avessi un enum di classi di personaggi in un gioco, sarebbe simile a questo:

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

Per prima cosa ho creato una classe helper con un paio di metodi per gestire le enumerazioni. Un metodo ottiene una descrizione per un valore specifico, l'altro metodo ottiene tutti i valori e le loro descrizioni per 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<ValueDescription> GetAllValuesAndDescriptions(Type t)
  {
    if (!t.IsEnum)
      throw new ArgumentException($"{nameof(t)} must be an enum type");

    return Enum.GetValues(t).Cast<Enum>().Select((e) => new ValueDescription() { Value = e, Description = e.Description() }).ToList();
  }
}

Successivamente, creiamo un ValueConverter . Ereditare da MarkupExtension rende più facile l'utilizzo in XAML, quindi non è necessario dichiararlo come risorsa.

[ValueConversion(typeof(Enum), typeof(IEnumerable<ValueDescription>))]
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;
  }
}

My ViewModel richiede solo 1 proprietà a cui può collegarsi la mia View sia per SelectedValue che per ItemsSource della casella combinata:

private PlayerClass playerClass;

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

E infine per associare la vista ComboBox (usando ValueConverter nel binding ValueConverter ) ...

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

Per implementare questa soluzione è sufficiente copiare la mia classe EnumToCollectionConverter e la classe EnumToCollectionConverter . Lavoreranno con qualsiasi enumerazione. Inoltre, non l'ho incluso qui, ma la classe ValueDescription è solo una semplice classe con 2 proprietà dell'oggetto pubblico, una chiamata Value , una chiamata Description . Puoi crearlo da solo oppure puoi cambiare il codice per usare una Tuple<object, object> o KeyValuePair<object, object>

Question

Sto cercando di trovare un semplice esempio in cui le enumerazioni sono mostrate così come sono. Tutti gli esempi che ho visto cercano di aggiungere stringhe di visualizzazione di aspetto piacevole, ma non voglio quella complessità.

Fondamentalmente ho una classe che contiene tutte le proprietà che leghiamo, impostando prima DataContext su questa classe e quindi specificando l'associazione come questa nel file xaml:

<ComboBox ItemsSource="{Binding Path=EffectStyle}"/>

Ma questo non mostra i valori enum nel ComboBox come oggetti.




Se si sta vincolando a una proprietà enum effettiva sul ViewModel, non a una rappresentazione int di un enum, le cose si complicano. Ho trovato che è necessario associare alla rappresentazione stringa, NON il valore int come è previsto in tutti gli esempi di cui sopra.

Puoi capire se questo è il caso legando una semplice casella di testo alla proprietà che vuoi associare a ViewModel. Se mostra del testo, si lega alla stringa. Se mostra un numero, esegui il bind al valore. Nota Ho usato Display due volte che normalmente sarebbe un errore, ma è l'unico modo in cui funziona.

<ComboBox SelectedValue="{Binding ElementMap.EdiDataType, Mode=TwoWay}"
                      DisplayMemberPath="Display"
                      SelectedValuePath="Display"
                      ItemsSource="{Binding Source={core:EnumToItemsSource {x:Type edi:EdiDataType}}}" />

Greg




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 legarsi alla risorsa statica:

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



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

Dovresti estendere Rogers e la risposta di Greg con questo tipo di convertitore di valori Enum, se stai vincolando direttamente alle proprietà del modello di oggetti enum.




Usando ReactiveUI , ho creato la seguente soluzione alternativa. Non è una soluzione all-in-one elegante, ma per lo meno è leggibile.

Nel mio caso, legare una lista di enum a un controllo è un caso raro, quindi non ho bisogno di scalare la soluzione attraverso il code-code. Tuttavia, il codice può essere reso più generico modificando EffectStyleLookup.Item in un Object . L'ho provato con il mio codice, non sono necessarie altre modifiche. Ciò significa che l'unica classe di supporto potrebbe essere applicata a qualsiasi elenco di enum . Anche se ciò ridurrebbe la sua leggibilità - ReactiveList<EnumLookupHelper> non ha un grande ReactiveList<EnumLookupHelper> .

Utilizzando la seguente classe helper:

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

Nel ViewModel, converti l'elenco di enumerazioni e esponilo come una proprietà:

public ViewModel : ReactiveObject
{
  private ReactiveList<EffectStyleLookup> _effectStyles;
  public ReactiveList<EffectStyleLookup> 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<EffectStyle>)Enum.GetValues(typeof(EffectStyle))
      .Select( x => new EffectStyleLookup() { 
        Item = x, 
        Display = x.ToString()
      });

    EffectStyles = new ReactiveList<EffectStyle>( list );
  }
}

Nel ComboBox , utilizzare la proprietà SelectedValuePath , per eseguire il bind al valore enum originale:

<ComboBox Name="EffectStyle" DisplayMemberPath="Display" SelectedValuePath="Item" />

Nella vista, questo ci consente di associare l' enum originale a SelectedEffectStyle nel ViewModel, ma di visualizzare il valore ToString() nel controllo 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) );
});



La risposta di Nick mi ha davvero aiutato, ma ho capito che potrebbe essere leggermente modificato, per evitare una classe extra, ValueDescription. Mi sono ricordato che esiste già una classe KeyValuePair nel framework, quindi può essere utilizzata al suo posto.

Il codice cambia leggermente:

public static IEnumerable<KeyValuePair<string, string>> GetAllValuesAndDescriptions<TEnum>() 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<Enum>()
               select new KeyValuePair<string, string>(e.ToString(),  e.Description());
    }


public IEnumerable<KeyValuePair<string, string>> PlayerClassList
{
   get
   {
       return EnumHelper.GetAllValuesAndDescriptions<PlayerClass>();
   }
}

e infine lo XAML:

<ComboBox ItemSource="{Binding Path=PlayerClassList}"
          DisplayMemberPath="Value"
          SelectedValuePath="Key"
          SelectedValue="{Binding Path=SelectedClass}" />

Spero che questo sia utile per gli altri.




Links