Liaison de données d'une propriété enum à un ComboBox dans WPF

256

À titre d'exemple, prenez le code suivant:

public enum ExampleEnum { FooBar, BarFoo }

public class ExampleClass : INotifyPropertyChanged
{
    private ExampleEnum example;

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

Je veux un pour lier la propriété ExampleProperty à un ComboBox, afin qu'il affiche les options "FooBar" et "BarFoo" et fonctionne en mode TwoWay. Idéalement, je veux que ma définition ComboBox ressemble à ceci:

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

Actuellement, j'ai des gestionnaires pour les événements ComboBox.SelectionChanged et ExampleClass.PropertyChanged installés dans ma fenêtre où je fais la liaison manuellement.

Existe-t-il une meilleure ou une sorte de méthode canonique? Utiliseriez-vous habituellement des convertisseurs et comment rempliriez-vous le ComboBox avec les bonnes valeurs? Je ne veux même pas commencer avec i18n pour le moment.

Éditer

Il a donc été répondu à une question: comment remplir le ComboBox avec les bonnes valeurs.

Récupérez les valeurs Enum sous forme de liste de chaînes via un ObjectDataProvider à partir de la méthode statique 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>

Cela, je peux l'utiliser comme un ItemsSource pour mon ComboBox:

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"/>
Maximilian
la source
4
J'ai exploré cela et j'ai une solution que vous pouvez utiliser (avec localisation) dans WPF situé ici .
ageektrapped le

Réponses:

208

Vous pouvez créer une extension de balisage personnalisée.

Exemple d'utilisation:

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

En haut de votre XAML:

    xmlns:my="clr-namespace:namespace_to_enumeration_extension_class

puis...

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

Et la mise en œuvre ...

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
la source
7
@Gregor S. quel est mon: énumération?
joshua
14
@Crown 'my' est le préfixe d'espace de noms que vous déclarez au-dessus de votre fichier xaml: par exemple xmlns: my = "clr-namespace: namespace_to_enumeration_extension_class. L'énumération est l'abréviation de EnumerationExtension, en xaml vous n'avez pas à écrire le nom complet de la classe d'extension .
Gregor Slavec
33
+1, mais la quantité de code requise par WPF pour accomplir les choses les plus simples est vraiment époustouflante
Konrad Morawski
1
Je n'aime pas vraiment la façon dont cela vous fait utiliser une référence à une partie de votre modèle - le type d'énumération - dans la vue, dans le ItemsSourceparam. Afin de garder la vue et le modèle découplés, je devrais créer une copie de l'énumération dans le ViewModel et coder ViewModel pour traduire entre les deux ... Ce qui rendrait la solution plus simple. Ou existe-t-il un moyen de fournir le type lui-même à partir de ViewModel?
lampak
6
Une autre limitation est que vous ne pouvez pas le faire si vous avez plusieurs langues.
River-Claire Williamson
176

Dans le viewmodel, vous pouvez avoir:

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, le ItemSourcese lie à MyEnumTypeValueset SelectedItemse lie à SelectedMyEnumType.

<ComboBox SelectedItem="{Binding SelectedMyEnumType}" ItemsSource="{Binding MyEnumTypeValues}"></ComboBox>
user659130
la source
Cela a fonctionné fabuleusement dans mon application universelle et a été très facile à mettre en œuvre. Je vous remercie!
Nathan Strutz
96

Je préfère ne pas utiliser le nom d'énumération dans l'interface utilisateur. Je préfère utiliser une valeur différente pour user ( DisplayMemberPath) et différente pour value (enum dans ce cas) ( SelectedValuePath). Ces deux valeurs peuvent être regroupéesKeyValuePair et stockées dans le dictionnaire.

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: Compatible avec le modèle MVVM.

CoperNick
la source
14
Je pense que votre réponse est sous-estimée, elle semble la meilleure option compte tenu de ce que ComboBox attend lui-même. Vous pourriez peut-être mettre un générateur de dictionnaire dans le getter, en utilisant Enum.GetValues, mais cela ne résoudrait pas la partie des noms à afficher. En fin de compte, et surtout si I18n est implémenté, vous devrez de toute façon modifier manuellement les éléments si l'énumération change. Mais les énumérations ne sont pas censées changer souvent, voire pas du tout, n'est-ce pas? +1
heltonbiker
2
Cette réponse est géniale ET elle permet de localiser les descriptions des énumérations ... Merci pour cela!
Shay
2
Cette solution est très bonne car elle gère à la fois l'énumération et la localisation avec moins de code que les autres solutions!
hfann
2
Le problème avec Dictionary est que les clés sont classées par valeur de hachage, donc il y a peu de contrôle sur cela. Bien qu'un peu plus verbeux, j'ai plutôt utilisé List <KeyValuePair <enum, string >>. Bonne idée.
Kevin Brock
3
@CoperNick @Pragmateek nouveau correctif: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

Je ne sais pas si c'est possible en XAML uniquement, mais essayez ce qui suit:

Donnez un nom à votre ComboBox pour pouvoir y accéder dans le code derrière: "typesComboBox1"

Maintenant, essayez ce qui suit

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

Sur la base de la réponse acceptée mais maintenant supprimée fournie par ageektrapped, j'ai créé une version allégée sans certaines des fonctionnalités les plus avancées. Tout le code est inclus ici pour vous permettre de le copier-coller et de ne pas être bloqué par link-rot.

J'utilise le System.ComponentModel.DescriptionAttributequi est vraiment destiné aux descriptions de temps de conception. Si vous n'aimez pas utiliser cet attribut, vous pouvez créer le vôtre, mais je pense que l'utilisation de cet attribut fait vraiment le travail. Si vous n'utilisez pas l'attribut, le nom sera par défaut le nom de la valeur d'énumération dans le code.

public enum ExampleEnum {

  [Description("Foo Bar")]
  FooBar,

  [Description("Bar Foo")]
  BarFoo

}

Voici la classe utilisée comme source des éléments:

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

}

Vous pouvez l'utiliser en XAML comme ceci:

<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
la source
23

Utilisez ObjectDataProvider:

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

puis liez à la ressource statique:

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

Trouvez cette solution sur ce blog

druss
la source
Bonne réponse. Soit dit en passant, cela vous évite d'avoir à vous soucier d'un problème de type Converterénumération chaîne.
DonBoitnott
1
La solution liée semble morte (texte coréen ou japonais?). Si je mets votre code dans mes ressources XAML, cela indique qu'Enum n'est pas pris en charge dans un projet WPF.
Sebastian
6

Ma façon préférée de le faire est de ValueConverterfaire en sorte que ItemsSource et SelectedValue se lient tous les deux à la même propriété. Cela ne nécessite aucune propriété supplémentaire pour garder votre ViewModel agréable et propre.

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

Et la définition du convertisseur:

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

Ce convertisseur fonctionnera avec n'importe quelle énumération. ValueDescriptionest juste une classe simple avec une Valuepropriété et une Descriptionpropriété. Vous pouvez tout aussi facilement utiliser un Tupleavec Item1et Item2, ou KeyValuePairavec Keyet Valueau lieu de Valeur et description ou toute autre classe de votre choix tant qu'il a peut contenir une valeur enum et une description de chaîne de cette valeur enum.

pseudo
la source
Bonne réponse! Pour la ValueDescriptionclasse, la Descriptionpropriété peut être omise si elle n'est pas nécessaire. Une classe simple avec seulement Valuepropriété fonctionne aussi!
pogosama
De plus, si vous souhaitez vous lier à un RadioButton, la méthode Convert doit renvoyer une liste de chaînes, c'est .Select(e => e.ToString())-à- dire au lieu d'utiliser la ValueDescriptionclasse.
pogosama
Au lieu de ValueDescriptionaussi, un KeyValuePairpourrait être utilisé, comme indiqué ici
Apfelkuacha
5

Voici une solution générique utilisant une méthode d'assistance. Cela peut également gérer une énumération de tout type sous-jacent (octet, sbyte, uint, long, etc.)

Méthode d'assistance:

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

Voir le modèle:

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

Boîte combo:

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

vous pouvez envisager quelque chose comme ça:

  1. définissez un style pour le bloc de texte ou tout autre contrôle que vous souhaitez utiliser pour afficher votre énumération:

    <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. définir votre style pour ComboBoxItem

    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <TextBlock Tag="{Binding}" Style="{StaticResource enumStyle}"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  3. ajoutez une zone de liste déroulante et chargez-la avec vos valeurs d'énumération:

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

si votre énumération est volumineuse, vous pouvez bien sûr faire de même dans le code, en épargnant beaucoup de dactylographie. j'aime cette approche, car elle facilite la localisation - vous définissez tous les modèles une fois, puis vous ne mettez à jour que vos fichiers de ressources de chaîne.

Greg
la source
le SelectedValuePath = "Content" m'a aidé ici. J'ai mes ComboBoxItems en tant que valeurs de chaîne et je n'arrive pas à convertir ComboBoxItem en mon type d'énumération. Merci
adriaanp
2

Si vous utilisez un MVVM, sur la base de la réponse @rudigrobler, vous pouvez effectuer les opérations suivantes:

Ajoutez la propriété suivante à la classe ViewModel

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

Ensuite, dans le XAML, procédez comme suit:

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

Il s'agit d'une DevExpressréponse spécifique basée sur la réponse la plus votée de Gregor S.(actuellement, elle compte 128 voix).

Cela signifie que nous pouvons garder le style cohérent dans toute l'application:

entrez la description de l'image ici

Malheureusement, la réponse originale ne fonctionne pas avec un ComboBoxEdit de DevExpress sans quelques modifications.

Tout d'abord, le XAML pour 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 de dire que vous devrez pointer xamlExtensionsvers l'espace de noms qui contient la classe d'extension XAML (définie ci-dessous):

xmlns:xamlExtensions="clr-namespace:XamlExtensions"

Et nous devons pointer myEnumvers l'espace de noms qui contient l'énumération:

xmlns:myEnum="clr-namespace:MyNamespace"

Ensuite, l'énumération:

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

Le problème avec le XAML est que nous ne pouvons pas l'utiliser SelectedItemValue, car cela génère une erreur car le setter est inaccessible (peu d'oubli de votre part, DevExpress). Nous devons donc modifier notre ViewModelpour obtenir la valeur directement à partir de l'objet:

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

Pour être complet, voici l'extension XAML de la réponse d'origine (légèrement renommée):

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

Avertissement: je n'ai aucune affiliation avec DevExpress. Telerik est également une excellente bibliothèque.

Contango
la source
Pour mémoire, je ne suis pas affilié à DevExpress. Telerik possède également de très belles bibliothèques, et cette technique pourrait même ne pas être nécessaire pour leur bibliothèque.
Contango
0

Essayez d'utiliser

<ComboBox ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"
    SelectedValue="{Binding Path=ExampleProperty}" />
rudigrobler
la source
Ça ne marche pas. La zone de liste déroulante affichera simplement un texte vide et sa modification ne fera rien. Je suppose que jeter un convertisseur ici serait la meilleure solution.
Maximilian
0

J'ai créé un projet CodePlex open source qui fait cela. Vous pouvez télécharger le package NuGet ici .

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