Powiązanie danych właściwości enum z ComboBox w WPF


256

Jako przykład weź następujący kod:

public enum ExampleEnum { FooBar, BarFoo }

public class ExampleClass : INotifyPropertyChanged
{
    private ExampleEnum example;

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

Chcę, aby powiązać dane właściwość ExampleProperty z ComboBox, aby wyświetlał opcje „FooBar” i „BarFoo” i działa w trybie TwoWay. Optymalnie chcę, aby moja definicja ComboBox wyglądała mniej więcej tak:

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

Obecnie mam moduły obsługi zdarzeń ComboBox.SelectionChanged i ExampleClass.PropertyChanged zainstalowane w moim oknie, w którym ręcznie wykonuję powiązanie.

Czy istnieje lepszy lub jakiś kanoniczny sposób? Czy zwykle używałbyś Konwerterów i jak wypełniłbyś ComboBox odpowiednimi wartościami? Nie chcę nawet teraz zacząć korzystać z i18n.

Edytować

Tak więc odpowiedziano na jedno pytanie: Jak wypełnić ComboBox odpowiednimi wartościami.

Pobierz wartości Enum jako listę łańcuchów za pomocą ObjectDataProvider ze statycznej metody 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>

Tego mogę użyć jako ItemsSource dla mojego ComboBox:

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

4
Zbadałem to i mam rozwiązanie, którego możesz użyć (wraz z lokalizacją) w WPF znajdującym się tutaj .
ageektrapped

Odpowiedzi:


208

Możesz utworzyć niestandardowe rozszerzenie znaczników.

Przykład użycia:

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

U góry XAML:

    xmlns:my="clr-namespace:namespace_to_enumeration_extension_class

i wtedy...

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

I wdrożenie ...

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?
joshua

14
@Crown „my” to przedrostek przestrzeni nazw, który deklarujesz na górze pliku xaml: np. Xmlns: my = "clr-namespace: namespace_to_enumeration_extension_class. Wyliczenie jest skrótem od EnumerationExtension, w xaml nie musisz pisać całej nazwy klasy rozszerzenia .
Gregor Slavec

33
+1, ale ilość kodu wymagana przez WPF do osiągnięcia najprostszej rzeczy jest naprawdę niesamowita
Konrad Morawski

1
Nie podoba mi się sposób, w jaki sprawia, że ​​używasz odwołania do części modelu - typu wyliczenia - w widoku, w ItemsSourceparametrze. Aby zachować widok i model oddzielony, musiałbym utworzyć kopię wyliczenia w ViewModel i kod ViewModel w celu przetłumaczenia między tymi dwoma ... Co sprawiłoby, że rozwiązanie nie byłoby już takie proste. Czy jest sposób na dostarczenie samego typu z ViewModel?
lampak

6
Kolejnym ograniczeniem jest to, że nie możesz tego zrobić, jeśli masz wiele języków.
River-Claire Williamson

176

W viewmodel możesz mieć:

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

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

W XAML ItemSourcewiąże się MyEnumTypeValuesi SelectedItemwiąże się z SelectedMyEnumType .

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

Działa to bajecznie w mojej aplikacji Universal i było bardzo łatwe do wdrożenia. Dziękuję Ci!
Nathan Strutz

96

Wolę nie używać nazwy enum w interfejsie użytkownika. Wolę użyć innej wartości dla user ( DisplayMemberPath) i innej wartości (enum w tym przypadku) ( SelectedValuePath). Te dwie wartości można spakować KeyValuePairi zapisać w słowniku.

XAML

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

DO#

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

EDYCJA: Kompatybilny ze wzorem MVVM.


14
Myślę, że twoja odpowiedź jest niedoceniana, wydaje się najlepszą opcją biorąc pod uwagę to, czego oczekuje sam ComboBox. Być może możesz użyć konstruktora słowników do gettera, używając Enum.GetValues, ale nie rozwiązałoby to części nazw do wyświetlenia. Na koniec, a zwłaszcza jeśli I18n jest zaimplementowany, i tak będziesz musiał ręcznie zmieniać rzeczy, jeśli zmienia się wyliczenie. Ale wyliczenia nie powinny się często zmieniać, jeśli w ogóle, prawda? +1
heltonbiker

2
Ta odpowiedź jest niesamowita ORAZ pozwala zlokalizować opisy wyliczeń ... Dzięki za to!
Shay

2
To rozwiązanie jest bardzo dobre, ponieważ obsługuje zarówno wyliczanie, jak i lokalizację z mniejszym kodem niż inne rozwiązania!
hfann

2
Problem ze słownikiem polega na tym, że klucze są uporządkowane według wartości skrótu, więc kontrola nad tym jest niewielka. Chociaż trochę bardziej szczegółowy, użyłem zamiast tego List <KeyValuePair <enum, string >>. Dobry pomysł.
Kevin Brock

3
@CoperNick @Pragmateek nowa poprawka: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

Nie wiem, czy jest to możliwe tylko w XAML, ale spróbuj wykonać następujące czynności:

Nadaj ComboBoxowi nazwę, abyś mógł uzyskać do niej dostęp w codebehind: „typesComboBox1”

Teraz spróbuj wykonać następujące czynności

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

24

W oparciu o zaakceptowaną, ale teraz usuniętą odpowiedź udostępnioną przez ageektrapped , stworzyłem wersję uproszczoną bez niektórych bardziej zaawansowanych funkcji. Cały kod jest tu zawarty, abyś mógł go skopiować i wkleić, a nie zostać zablokowany przez link-rot.

Używam tego, System.ComponentModel.DescriptionAttributeco naprawdę jest przeznaczone do opisu czasu projektowania. Jeśli nie lubisz używać tego atrybutu, możesz stworzyć swój własny, ale myślę, że użycie tego atrybutu naprawdę wykona zadanie. Jeśli nie użyjesz tego atrybutu, nazwa domyślnie przyjmuje nazwę wartości wyliczonej w kodzie.

public enum ExampleEnum {

  [Description("Foo Bar")]
  FooBar,

  [Description("Bar Foo")]
  BarFoo

}

Oto klasa używana jako źródło przedmiotów:

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

}

Możesz go używać w XAML w następujący sposób:

<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

Użyj ObjectDataProvider:

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

a następnie powiąż z zasobem statycznym:

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

Znajdź to rozwiązanie na tym blogu


Niezła odpowiedź. Nawiasem mówiąc, dzięki temu nie musisz się martwić o Converterproblem z wyliczaniem do łańcucha.
DonBoitnott

1
Połączone rozwiązanie wydaje się martwe (koreański czy japoński tekst?). Jeśli umieściłem kod w moich zasobach XAML, oznacza to, że Enum nie jest obsługiwany w projekcie WPF.
Sebastian

6

Moim ulubionym sposobem na to jest ValueConverter, aby zarówno ItemsSource, jak i SelectedValue były powiązane z tą samą właściwością. Nie wymaga to żadnych dodatkowych właściwości, aby Twój ViewModel był ładny i czysty.

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

I definicja konwertera:

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

Ten konwerter będzie działał z dowolnym wyliczeniem. ValueDescriptionto tylko prosta klasa z Valuewłaściwością i Descriptionwłaściwością. Możesz równie łatwo użyć Tuplez Item1i Item2, lub KeyValuePairz Keyi Valuezamiast wartości i opisu lub dowolnej innej wybranej przez ciebie klasy, pod warunkiem, że może ona zawierać wartość wyliczenia i opis ciągu tej wartości wyliczenia.


Niezła odpowiedź! W przypadku ValueDescriptionklasy Descriptionwłaściwość można pominąć, jeśli nie jest potrzebna. ValueDziała również prosta klasa z tylko właściwością!
pogosama

Ponadto, jeśli chcesz utworzyć powiązanie z RadioButton, wówczas metoda Convert musi zwrócić listę ciągów, tzn. .Select(e => e.ToString())Zamiast używać ValueDescriptionklasy.
pogosama

Zamiast ValueDescriptionrównież KeyValuePairmożna użyć, jak pokazano tutaj
Apfelkuacha

5

Oto ogólne rozwiązanie z wykorzystaniem metody pomocniczej. Może to również obsługiwać wyliczanie dowolnego typu bazowego (bajt, sbyte, uint, long itp.)

Metoda pomocnicza:

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

Zobacz model:

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

ComboBox:

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

5

możesz rozważyć coś takiego:

  1. zdefiniuj styl dla bloku tekstowego lub innego elementu sterującego, którego chcesz użyć do wyświetlenia wyliczenia:

    <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. zdefiniuj swój styl dla ComboBoxItem

    <Style TargetType="{x:Type ComboBoxItem}">
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <TextBlock Tag="{Binding}" Style="{StaticResource enumStyle}"/>
                </DataTemplate>
            </Setter.Value>
        </Setter>
    </Style>
  3. dodaj combobox i załaduj go wartościami wyliczenia:

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

jeśli twoje wyliczenie jest duże, możesz oczywiście zrobić to samo w kodzie, oszczędzając dużo pisania. Podoba mi się to podejście, ponieważ ułatwia lokalizację - wszystkie szablony definiuje się raz, a następnie aktualizuje się tylko pliki zasobów ciągów.


SelectedValuePath = "Content" pomogło mi tutaj. Mam ComboBoxItems jako wartości ciągów i ciągle nie mogę przekonwertować ComboBoxItem na mój typ Enum. Dzięki
adriaanp,

2

Jeśli używasz MVVM, na podstawie odpowiedzi @rudigrobler możesz wykonać następujące czynności:

Dodaj następującą właściwość do klasy ViewModel

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

Następnie w XAML wykonaj następujące czynności:

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

1

To jest DevExpresskonkretna odpowiedź na podstawie najczęściej głosowanej odpowiedzi Gregor S.(obecnie ma 128 głosów).

Oznacza to, że możemy zachować spójność stylizacji w całej aplikacji:

wprowadź opis zdjęcia tutaj

Niestety oryginalna odpowiedź nie działa z ComboBoxEditDevExpress bez pewnych modyfikacji.

Po pierwsze, XAML dla 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"/>

Nie trzeba dodawać, że trzeba będzie wskazać xamlExtensionsprzestrzeń nazw, która zawiera klasę rozszerzeń XAML (zdefiniowaną poniżej):

xmlns:xamlExtensions="clr-namespace:XamlExtensions"

I musimy wskazać myEnumprzestrzeń nazw, która zawiera wyliczenie:

xmlns:myEnum="clr-namespace:MyNamespace"

Następnie wyliczenie:

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

Problem z XAML polega na tym, że nie możemy użyć SelectedItemValue, ponieważ powoduje to błąd, ponieważ konfigurator jest niedostępny (trochę niedopatrzenia z twojej strony DevExpress). Musimy więc zmodyfikować nasz, ViewModelaby uzyskać wartość bezpośrednio z obiektu:

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

Dla kompletności, oto rozszerzenie XAML z oryginalnej odpowiedzi (nieco zmieniona):

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

Uwaga: Nie mam powiązań z DevExpress. Telerik to także świetna biblioteka.


Dla przypomnienia, nie jestem związany z DevExpress. Telerik ma również bardzo dobre biblioteki, a ta technika może nawet nie być konieczna dla ich biblioteki.
Contango,

0

Spróbuj użyć

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

To nie działa Combobox pokaże tylko pusty tekst, a zmiana go nic nie da. Wydaje mi się, że najlepszym rozwiązaniem byłoby wprowadzenie tutaj konwertera.
Maximilian

0

Stworzyłem projekt CodePlex typu open source , który to robi. Możesz pobrać pakiet NuGet stąd .

<enumComboBox:EnumComboBox EnumType="{x:Type demoApplication:Status}" SelectedValue="{Binding Status}" />
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.