Как связать перечисление с элементом управления combobox в WPF?

Я пытаюсь найти простой пример, где перечисления отображаются как есть. Все примеры, которые я видел, пытаются добавить красиво выглядящие строки отображения, но я не хочу этой сложности.

В основном у меня есть класс, который содержит все свойства, которые я связываю, сначала устанавливая DataContext для этого класса, а затем указывая привязку, как это в файле xaml:

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

Но это не показывает значения перечисления в ComboBox как элементы.

Ответ 1

Вы можете сделать это из кода, поместив следующий код в обработчик событий Window Loaded, например:

yourComboBox.ItemsSource = Enum.GetValues(typeof(EffectStyle)).Cast<EffectStyle>();

Если вам нужно связать его в XAML, вам нужно использовать ObjectDataProvider для создания объекта, доступного как источник привязки:

<Window x:Class="YourNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:StyleAlias="clr-namespace:Motion.VideoEffects">
    <Window.Resources>
        <ObjectDataProvider x:Key="dataFromEnum" MethodName="GetValues"
                            ObjectType="{x:Type System:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="StyleAlias:EffectStyle"/>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Window.Resources>
    <Grid>
        <ComboBox ItemsSource="{Binding Source={StaticResource dataFromEnum}}"
                  SelectedItem="{Binding Path=CurrentEffectStyle}" />
    </Grid>
</Window>

Обратите внимание на следующий код:

xmlns:System="clr-namespace:System;assembly=mscorlib"
xmlns:StyleAlias="clr-namespace:Motion.VideoEffects"

Руководство по отображению пространства имен и сборки, которое вы можете прочитать на MSDN.

Ответ 2

Мне нравится, чтобы все объекты, которые я связываю, были определены в моем ViewModel, поэтому я стараюсь по возможности избегать использования <ObjectDataProvider> в xaml.

Мое решение не использует никаких данных, определенных в представлении, и никакого кода. Только DataBinding, повторно используемый ValueConverter, метод для получения коллекции описаний для любого типа Enum и одно свойство в ViewModel для привязки.

Когда я хочу связать Enum с ComboBox, текст, который я хочу отобразить, никогда не соответствует значениям Enum, поэтому я использую атрибут [Description()], чтобы дать ему текст, который я действительно хочу видеть в ComboBox. Если бы у меня было несколько дней недели, это выглядело бы примерно так:

public enum DayOfWeek
{
  // add an optional blank value for default/no selection
  [Description("")]
  NOT_SET = 0,
  [Description("Sunday")]
  SUNDAY,
  [Description("Monday")]
  MONDAY,
  ...
}

Сначала я создал вспомогательный класс с несколькими методами для работы с перечислениями. Один метод получает описание для определенного значения, другой метод получает все значения и их описания для типа.

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

Далее мы создаем ValueConverter. Наследование от MarkupExtension облегчает использование в XAML, поэтому нам не нужно объявлять его как ресурс.

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

Моему ViewModel нужно только 1 свойство, с которым мой View может связываться как для SelectedValue, так и ItemsSource в выпадающем списке:

private DayOfWeek dayOfWeek;

public DayOfWeek SelectedDay
{
  get { return dayOfWeek; }
  set
  {
    if (dayOfWeek != value)
    {
      dayOfWeek = value;
      OnPropertyChanged(nameof(SelectedDay));
    }
  }
}

И, наконец, связать представление ComboBox (используя ValueConverter в привязке ItemsSource)...

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

Для реализации этого решения вам нужно только скопировать мой класс EnumHelper и класс EnumToCollectionConverter. Они будут работать с любыми перечислениями. Кроме того, я не включил его здесь, но класс ValueDescription - это простой класс с двумя открытыми свойствами объекта, один из которых называется Value, другой - Description. Вы можете создать это самостоятельно или изменить код для использования Tuple<object, object> или KeyValuePair<object, object>

Ответ 3

Я использовал другое решение, используя MarkupExtension.

  • Я создал класс, который предоставляет исходный код:

    public class EnumToItemsSource : MarkupExtension
    {
        private readonly Type _type;
    
        public EnumToItemsSource(Type type)
        {
            _type = type;
        }
    
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return Enum.GetValues(_type)
                .Cast<object>()
                .Select(e => new { Value = (int)e, DisplayName = e.ToString() });
        }
    }
    
  • Это почти все... Теперь используйте его в XAML:

        <ComboBox DisplayMemberPath="DisplayName"
              ItemsSource="{persons:EnumToItemsSource {x:Type enums:States}}"
              SelectedValue="{Binding Path=WhereEverYouWant}"
              SelectedValuePath="Value" />
    
  • Измените 'enums: States' на ваш enum

Ответ 4

Использовать ObjectDataProvider:

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

а затем привязать к статическому ресурсу:

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

на основе этой статьи

Ответ 5

Ответ на Nick действительно помог мне, но я понял, что его можно немного подправить, чтобы избежать дополнительного класса ValueDescription. Я вспомнил, что в классе существует класс KeyValuePair, поэтому его можно использовать вместо него.

Код изменяется незначительно:

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

и, наконец, XAML:

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

Я надеюсь, что это полезно для других.

Ответ 6

Вам нужно создать массив значений в перечислении, который можно создать, вызвав System.Enum.GetValues ​​(), передав ему Type перечисления, что вы хотите элементы.

Если вы укажете это для свойства ItemsSource, то он должен быть заполнен всеми значениями перечисления. Вероятно, вы хотите привязать SelectedItem к EffectStyle (считая, что это свойство того же перечисления и содержит текущее значение).

Ответ 7

Все вышеуказанные сообщения пропустили простой трюк. Возможно, из привязки SelectedValue узнать, как заполнить элемент ItemsSource AUTOMAGICALLY, чтобы ваша разметка XAML была просто.

<Controls:EnumComboBox SelectedValue="{Binding Fool}"/>

Например, в моей модели ViewModel у меня есть

public enum FoolEnum
    {
        AAA, BBB, CCC, DDD

    };


    FoolEnum _Fool;
    public FoolEnum Fool
    {
        get { return _Fool; }
        set { ValidateRaiseAndSetIfChanged(ref _Fool, value); }
    }

ValidateRaiseAndSetIfChanged - мой крючок INPC. Ваш может отличаться.

Реализация EnumComboBox выглядит следующим образом, но сначала мне понадобится небольшой помощник, чтобы получить строки и значения перечисления

    public static List<Tuple<object, string, int>> EnumToList(Type t)
    {
        return Enum
            .GetValues(t)
            .Cast<object>()
            .Select(x=>Tuple.Create(x, x.ToString(), (int)x))
            .ToList();
    }

и основной класс (обратите внимание, что я использую ReactiveUI для подключения изменений свойств через WhenAny)

using ReactiveUI;
using ReactiveUI.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Documents;

namespace My.Controls
{
    public class EnumComboBox : System.Windows.Controls.ComboBox
    {
        static EnumComboBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(EnumComboBox), new FrameworkPropertyMetadata(typeof(EnumComboBox)));
        }

        protected override void OnInitialized( EventArgs e )
        {
            base.OnInitialized(e);

            this.WhenAnyValue(p => p.SelectedValue)
                .Where(p => p != null)
                .Select(o => o.GetType())
                .Where(t => t.IsEnum)
                .DistinctUntilChanged()
                .ObserveOn(RxApp.MainThreadScheduler)
                .Subscribe(FillItems);
        }

        private void FillItems(Type enumType)
        {
            List<KeyValuePair<object, string>> values = new List<KeyValuePair<object,string>>();

            foreach (var idx in EnumUtils.EnumToList(enumType))
            {
                values.Add(new KeyValuePair<object, string>(idx.Item1, idx.Item2));
            }

            this.ItemsSource = values.Select(o=>o.Key.ToString()).ToList();

            UpdateLayout();
            this.ItemsSource = values;
            this.DisplayMemberPath = "Value";
            this.SelectedValuePath = "Key";

        }
    }
}

Вам также нужно правильно установить стиль в Generic.XAML, или ваше поле ничего не сделает, и вы вытащите свои волосы.

<Style TargetType="{x:Type local:EnumComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
</Style>

и это все. Это, очевидно, может быть расширено для поддержки i18n, но сделает сообщение более длинным.

Ответ 8

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

Вы должны расширить ответ Роджерса и Грега с таким конвертером значений Enum, если вы привязываетесь прямо к свойствам объектной модели перечисления.

Ответ 9

Универсальные приложения, похоже, работают по-другому; он не обладает всеми возможностями полнофункционального XAML. Что для меня работало:

  • Я создал список значений перечисления как перечисления (не преобразованные в строками или целыми числами) и связал элемент Items ComboBox с этим
  • Затем я мог привязать ComboBox ItemSelected к моей публичной собственности   тип которого является рассматриваемым перечислением

Просто для удовольствия я взломал небольшой шаблонный класс, чтобы помочь с этим, и опубликовал его на страницах MSDN. Дополнительные биты позволяют мне опционально переопределять имена перечислений и позволять мне скрывать некоторые из перечислений. Мой код выглядит ужасно, как Ник (выше), чего я бы хотел увидеть раньше.

Запуск образца, он включает множественные привязки twoway к перечислению

Ответ 10

На этот вопрос есть много отличных ответов, и я смиренно отвечаю на свой. Я считаю, что мое несколько проще и элегантнее. Требуется только конвертер значений.

Учитывая перечисление...

public enum ImageFormat
{
    [Description("Windows Bitmap")]
    BMP,
    [Description("Graphics Interchange Format")]
    GIF,
    [Description("Joint Photographic Experts Group Format")]
    JPG,
    [Description("Portable Network Graphics Format")]
    PNG,
    [Description("Tagged Image Format")]
    TIFF,
    [Description("Windows Media Photo Format")]
    WDP
}

и конвертер значений...

public class ImageFormatValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is ImageFormat format)
        {
            return GetString(format);
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is string s)
        {
            return Enum.Parse(typeof(ImageFormat), s.Substring(0, s.IndexOf(':')));
        }
        return null;
    }

    public string[] Strings => GetStrings();

    public static string GetString(ImageFormat format)
    {
        return format.ToString() + ": " + GetDescription(format);
    }

    public static string GetDescription(ImageFormat format)
    {
        return format.GetType().GetMember(format.ToString())[0].GetCustomAttribute<DescriptionAttribute>().Description;

    }
    public static string[] GetStrings()
    {
        List<string> list = new List<string>();
        foreach (ImageFormat format in Enum.GetValues(typeof(ImageFormat)))
        {
            list.Add(GetString(format));
        }

        return list.ToArray();
    }
}

ресурсы...

    <local:ImageFormatValueConverter x:Key="ImageFormatValueConverter"/>

декларация XAML...

    <ComboBox Grid.Row="9" ItemsSource="{Binding Source={StaticResource ImageFormatValueConverter}, Path=Strings}"
              SelectedItem="{Binding Format, Converter={StaticResource ImageFormatValueConverter}}"/>

Посмотреть модель...

    private ImageFormat _imageFormat = ImageFormat.JPG;
    public ImageFormat Format
    {
        get => _imageFormat;
        set
        {
            if (_imageFormat != value)
            {
                _imageFormat = value;
                OnPropertyChanged();
            }
        }
    }

Результирующий комбинированный список...

ComboBox bound to enum

Ответ 11

Если вы привязываетесь к фактическому свойству enum на вашем ViewModel, а не в представлении enum перечисления, все становится сложным. Я обнаружил, что необходимо привязать к строковому представлению, а не значение int, как ожидается во всех приведенных выше примерах.

Вы можете определить, действительно ли это так, привязывая простой текстовый блок к свойству, которое вы хотите привязать к вашей ViewModel. Если он показывает текст, привяжите его к строке. Если он показывает число, привяжите его к значению. Примечание. Я дважды использовал Display, который обычно был ошибкой, но это единственный способ, которым это работает.

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

Грег

Ответ 12

Мне понравился tom.maruska answer, но мне нужно было поддерживать любой тип перечисления, с которым мой шаблон мог столкнуться во время выполнения. Для этого мне пришлось использовать привязку, чтобы указать тип расширения разметки. Я смог работать в этом ответе от nicolay.anykienko, чтобы придумать очень гибкое расширение разметки, которое будет работать в любом случае, о котором я могу думать. Он потребляется следующим образом:

<ComboBox SelectedValue="{Binding MyEnumProperty}" 
          SelectedValuePath="Value"
          ItemsSource="{local:EnumToObjectArray SourceEnum={Binding MyEnumProperty}}" 
          DisplayMemberPath="DisplayName" />

Источник для разметки разметки, упомянутый выше:

class EnumToObjectArray : MarkupExtension
{
    public BindingBase SourceEnum { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        DependencyObject targetObject;
        DependencyProperty targetProperty;

        if (target != null && target.TargetObject is DependencyObject && target.TargetProperty is DependencyProperty)
        {
            targetObject = (DependencyObject)target.TargetObject;
            targetProperty = (DependencyProperty)target.TargetProperty;
        }
        else
        {
            return this;
        }

        BindingOperations.SetBinding(targetObject, EnumToObjectArray.SourceEnumBindingSinkProperty, SourceEnum);

        var type = targetObject.GetValue(SourceEnumBindingSinkProperty).GetType();

        if (type.BaseType != typeof(System.Enum)) return this;

        return Enum.GetValues(type)
            .Cast<Enum>()
            .Select(e => new { Value=e, Name = e.ToString(), DisplayName = Description(e) });
    }

    private static DependencyProperty SourceEnumBindingSinkProperty = DependencyProperty.RegisterAttached("SourceEnumBindingSink", typeof(Enum)
                       , typeof(EnumToObjectArray), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits));

    /// <summary>
    /// Extension method which returns the string specified in the Description attribute, if any.  Oherwise, name is returned.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <returns></returns>
    public static string Description(Enum value)
    {
        var attrs = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
        if (attrs.Any())
            return (attrs.First() as DescriptionAttribute).Description;

        //Fallback
        return value.ToString().Replace("_", " ");
    }
}

Ответ 13

Простое и понятное объяснение: http://brianlagunas.com/a-better-way-to-data-bind-enums-in-wpf/

xmlns:local="clr-namespace:BindingEnums"
xmlns:sys="clr-namespace:System;assembly=mscorlib"

...

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

...

<Grid>
    <ComboBox HorizontalAlignment="Center" VerticalAlignment="Center" MinWidth="150"
              ItemsSource="{Binding Source={StaticResource dataFromEnum}}"/>
</Grid>

Ответ 14

Я добавляю свой комментарий (к сожалению, в VB, но концепция может быть легко воспроизведена в С# в одно мгновение), потому что мне просто нужно было сослаться на это, и мне не понравились какие-либо ответы, поскольку они были слишком сложными. Это не должно быть так сложно.

Поэтому я придумал более простой способ. Привязать перечислители к словарю. Свяжите этот словарь с Combobox.

Мой комбинированный список:

<ComboBox x:Name="cmbRole" VerticalAlignment="Stretch" IsEditable="False" Padding="2" 
    Margin="0" FontSize="11" HorizontalAlignment="Stretch" TabIndex="104" 
    SelectedValuePath="Key" DisplayMemberPath="Value" />

Мой код позади. Надеюсь, это поможет кому-то еще.

Dim tDict As New Dictionary(Of Integer, String)
Dim types = [Enum].GetValues(GetType(Helper.Enumerators.AllowedType))
For Each x As Helper.Enumerators.AllowedType In types
    Dim z = x.ToString()
    Dim y = CInt(x)
    tDict.Add(y, z)
Next

cmbRole.ClearValue(ItemsControl.ItemsSourceProperty)
cmbRole.ItemsSource = tDict

Ответ 15

Используя ReactiveUI, я создал следующее альтернативное решение. Это не элегантное решение "все-в-одном", но я думаю, что, по крайней мере, он читаем.

В моем случае привязка списка enum к элементу управления является редким случаем, поэтому мне не нужно масштабировать решение по базе кода. Однако код можно сделать более общим, изменив EffectStyleLookup.Item на Object. Я тестировал его с помощью моего кода, никаких других изменений не требуется. Это означает, что один класс-помощник может быть применен к любому списку enum. Хотя это уменьшит его читаемость - ReactiveList<EnumLookupHelper> не имеет большого кольца для него.

Используя следующий вспомогательный класс:

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

В ViewModel преобразует список перечислений и выставляет его как свойство:

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

В ComboBox используйте свойство SelectedValuePath для привязки к исходному значению enum:

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

В представлении это позволяет нам привязать исходный enum к SelectedEffectStyle в ViewModel, но отобразить значение ToString() в 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) );
});

Ответ 16

Ник Solutuion может быть упрощен еще больше, ничего особенного, вам понадобится только один конвертер:

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

Затем вы используете его там, где хотите, чтобы ваше поле со списком появлялось:

<ComboBox ItemsSource="{Binding PagePosition, Converter={converter:EnumToCollectionConverter}, Mode=OneTime}"  SelectedItem="{Binding PagePosition}" />