В качестве примера возьмем следующий код:
public enum ExampleEnum { FooBar, BarFoo }
public class ExampleClass : INotifyPropertyChanged
{
private ExampleEnum example;
public ExampleEnum ExampleProperty
{ get { return example; } { /* set and notify */; } }
}
Я хочу привязать свойство ExampleProperty к ComboBox, чтобы оно отображало параметры «FooBar» и «BarFoo» и работало в режиме TwoWay. Оптимально я хочу, чтобы мое определение ComboBox выглядело примерно так:
<ComboBox ItemsSource = "What goes here?" SelectedItem = "{Binding Path=ExampleProperty}" />
В настоящее время у меня есть обработчики событий ComboBox.SelectionChanged и ExampleClass.PropertyChanged, установленные в моем окне, где я выполняю привязку вручную.
Есть ли способ лучше или какой-то канонический? Вы бы обычно использовали конвертеры и как бы вы заполняли ComboBox правильными значениями? Я даже не хочу сейчас начинать работать с i18n.
Редактировать
Итак, был дан ответ на один вопрос: как заполнить ComboBox правильными значениями.
Получить значения Enum в виде списка строк через ObjectDataProvider из статического метода 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>
Это я могу использовать как ItemsSource для моего ComboBox:
<ComboBox ItemsSource = "{Binding Source = {StaticResource ExampleEnumValues}}"/>





Я не знаю, возможно ли это только в XAML, но попробуйте следующее:
Дайте своему ComboBox имя, чтобы вы могли получить к нему доступ в коде позади: "typesComboBox1"
Теперь попробуйте следующее
typesComboBox1.ItemsSource = Enum.GetValues(typeof(ExampleEnum));
Попробуйте использовать
<ComboBox ItemsSource = "{Binding Source = {StaticResource ExampleEnumValues}}"
SelectedValue = "{Binding Path=ExampleProperty}" />
Это не работает. Поле со списком просто покажет пустой текст, и его изменение ничего не даст. Думаю, лучшим решением было бы добавить сюда конвертер.
вы можете рассмотреть что-то вроде этого:
определите стиль для текстового блока или любого другого элемента управления, который вы хотите использовать для отображения вашего перечисления:
<Style x:Key = "enumStyle" TargetType = "{x:Type TextBlock}">
<Setter Property = "Text" Value = "<NULL>"/>
<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>
определите свой стиль для ComboBoxItem
<Style TargetType = "{x:Type ComboBoxItem}">
<Setter Property = "ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Tag = "{Binding}" Style = "{StaticResource enumStyle}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
добавьте поле со списком и загрузите его значениями перечисления:
<ComboBox SelectedValue = "{Binding Path=your property goes here}" SelectedValuePath = "Content">
<ComboBox.Items>
<ComboBoxItem>
<proj:YourEnum>Value1</proj:YourEnum>
</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
если у вас большое перечисление, вы, конечно, можете сделать то же самое в коде, сэкономив много времени на вводе текста. Мне нравится этот подход, поскольку он упрощает локализацию - вы определяете все шаблоны один раз, а затем обновляете только файлы строковых ресурсов.
Здесь мне помог SelectedValuePath = "Content". У меня есть свои ComboBoxItems как строковые значения, и я не могу преобразовать ComboBoxItem в мой тип Enum. Спасибо
Вы можете создать собственное расширение разметки.
Пример использования:
enum Status
{
[Description("Available.")]
Available,
[Description("Not here right now.")]
Away,
[Description("I don't have time right now.")]
Busy
}
В верхней части вашего XAML:
xmlns:my = "clr-namespace:namespace_to_enumeration_extension_class
а потом...
<ComboBox
ItemsSource = "{Binding Source = {my:Enumeration {x:Type my:Status}}}"
DisplayMemberPath = "Description"
SelectedValue = "{Binding CurrentStatus}"
SelectedValuePath = "Value" />
А реализация ...
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; }
}
}
@ Грегор С. Что у меня: Перечисление?
@Crown 'my' - это префикс пространства имен, который вы объявляете в верхней части файла xaml: например, xmlns: my = "clr-namespace: namespace_to_enumeration_extension_c lass. Перечисление является сокращением от EnumerationExtension, в xaml вам не нужно писать полное расширение название класса.
Можно ли, используя приведенный выше код, сохранить значение выбранного элемента в пользовательских настройках и прочитать его?
Да, просто установите / получите свойство CurrentStatus в вашей модели просмотра.
+1, но количество кода, требуемого WPF для выполнения простейших задач, действительно потрясающе.
Мне не очень нравится, как он заставляет вас использовать ссылку на часть вашей модели - тип перечисления - в представлении в параметре ItemsSource. Чтобы сохранить представление и модель разделенными, мне нужно было бы создать копию перечисления в ViewModel и код ViewModel для перевода между ними ... Что сделало бы решение не таким простым. Или есть способ предоставить сам тип из ViewModel?
Еще одно ограничение заключается в том, что вы не можете этого сделать, если у вас несколько языков.
Если вы столкнулись с ошибкой "EnumerationExtension” does not include a constructor that has the specified number of arguments, попробуйте использовать ItemsSource = "{Binding Source = {my:Enumeration EnumType = {x:Type my:Status}}}"
Не могу получить эту часть пространства имен ... Все выделяется как «не существует».
@RiverWilliamson ваш атрибут может быть чувствительным к культуре, так что вы можете указать имя и тип ресурса
Я сталкивался с этой проблемой несколько раз на протяжении многих лет, и использование вашего MarkupExtension обеспечивает самый чистый механизм, который я видел, не только для автоматического использования прокси-класса вместо значения перечисления, но и для его локализации. Я абстрагировал бит, который выполняет отражение, в помощник, который можно использовать в коде, и теперь все мое описание перечисления (для XAML или отчетов) проходит через один класс, а затем используется MarkupExtension, который у вас есть. Хорошая работа!
У меня была большая проблема с отображением описания, затем я переопределил ToString в классе EnumerationMember, чтобы вернуть описание, и теперь он работает как пуля 7,62 мм.
Вы можете показать, где находится свойство CurrentStatus? Это в файле xaml.cs? Мне не удается привязать к SelectedValue
Я также пытаюсь выяснить, где находится «CurrentStatus», потому что у меня есть атрибут Description с числами и буквами (1120S), который не будет правильно назначать значение при его выборе. Он правильно отображается в раскрывающемся списке, но при выборе он не присваивает значение моей модели просмотра.
В модели просмотра вы можете иметь:
public MyEnumType SelectedMyEnumType
{
get { return _selectedMyEnumType; }
set {
_selectedMyEnumType = value;
OnPropertyChanged("SelectedMyEnumType");
}
}
public IEnumerable<MyEnumType> MyEnumTypeValues
{
get
{
return Enum.GetValues(typeof(MyEnumType))
.Cast<MyEnumType>();
}
}
В XAML ItemSource связывается с MyEnumTypeValues, а SelectedItem - с SelectedMyEnumType.
<ComboBox SelectedItem = "{Binding SelectedMyEnumType}" ItemsSource = "{Binding MyEnumTypeValues}"></ComboBox>
Это прекрасно работало в моем универсальном приложении, и его было очень легко реализовать. Спасибо!
Это отлично работает и требует гораздо меньше кода, чем выбранный ответ.
На основе принятого, но теперь удаленного ответа, предоставленного Ageektrapped, я создал уменьшенную версию без некоторых более продвинутых функций. Весь код включен здесь, чтобы вы могли копировать и вставлять его и не блокировались ссылочной гнилью.
Я использую System.ComponentModel.DescriptionAttribute, который действительно предназначен для описания времени разработки. Если вам не нравится использовать этот атрибут, вы можете создать свой собственный, но я думаю, что использование этого атрибута действительно выполняет свою работу. Если вы не используете атрибут, имя по умолчанию будет соответствовать имени значения перечисления в коде.
public enum ExampleEnum {
[Description("Foo Bar")]
FooBar,
[Description("Bar Foo")]
BarFoo
}
Вот класс, используемый в качестве источника элементов:
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;
}
}
Вы можете использовать его в XAML следующим образом:
<Windows.Resources>
<local:EnumItemsSource
x:Key = "ExampleEnumItemsSource"
Type = "{x:Type local:ExampleEnum}"/>
</Windows.Resources>
<ComboBox
ItemsSource = "{StaticResource ExampleEnumItemsSource}"
SelectedValue = "{Binding ExampleProperty, Converter = {StaticResource ExampleEnumItemsSource}}"/>
Я предпочитаю не использовать имя перечисления в пользовательском интерфейсе. Я предпочитаю использовать другое значение для пользователя (DisplayMemberPath) и другое значение (в данном случае enum) (SelectedValuePath). Эти два значения могут быть упакованы в KeyValuePair и сохранены в словаре.
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 */; }
}
Обновлено: совместим с шаблоном MVVM.
Я думаю, что ваш ответ недооценен, это лучший вариант, учитывая то, чего ожидает сам ComboBox. Возможно, вы могли бы поместить построитель словаря в получатель, используя Enum.GetValues, но это не решило бы часть отображаемых имен. В конце концов, особенно если реализован I18n, вам все равно придется вручную изменить что-то, если перечисление изменится. Но перечисления не должны часто меняться, если они вообще меняются? +1
Позвольте мне исправить это, как вы написали в комментарии: private static readonly Dictionary<ExampleEnum, string> EnumMapping = new Dictionary<ExampleEnum, string>() { {ExampleEnum.FooBar, "Foo Bar"}, {ExampleEnum.BarFoo, "Reversed Foo Bar"}, //{ExampleEnum.None, "Hidden in UI"}, }; public Dictionary<ExampleEnum, string> ExampleEnumsWithCaptions { get { return EnumMapping; } } :)
Этот ответ потрясающий, и он позволяет локализовать описания перечислений ... Спасибо за это!
Это решение очень хорошее, потому что оно обрабатывает как перечисление, так и локализацию с меньшим количеством кода, чем другие решения!
Проблема со словарем состоит в том, что ключи упорядочены по хеш-значению, поэтому контроль над этим ограничен. Хотя я был немного более подробным, вместо этого я использовал List <KeyValuePair <enum, string >>. Хорошая идея.
@CoperNick @Pragmateek новое исправление: public Dictionary<ExampleEnum, string> ExampleEnumsWithCaptions { get; } = new Dictionary<ExampleEnum, string>() { {ExampleEnum.FooBar, "Foo Bar"}, {ExampleEnum.BarFoo, "Reversed Foo Bar"}, //{ExampleEnum.None, "Hidden in UI"}, };
@JinJi Обновлено до C# 6. Ваше исправление делает код короче. Спасибо.
Я использовала этот узор, мне нравится его краткость и изящество. Лично я поменял местами Key и Value, потому что в моей голове словарь будет содержать строковые ключи для значений перечисления. Но я думаю, это не имеет значения.
Вот общее решение с использованием вспомогательного метода. Это также может обрабатывать перечисление любого базового типа (byte, sbyte, uint, long и т. д.)
Вспомогательный метод:
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
Просмотреть модель:
public IEnumerable<object> EnumSearchTypes {
get {
return GetEnum<SearchTypes>();
}
}//property
Поле со списком:
<ComboBox
SelectedValue = "{Binding SearchType}"
ItemsSource = "{Binding EnumSearchTypes}"
DisplayMemberPath = "Name"
SelectedValuePath = "Value"
/>
Используйте 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}}"
Найдите этот решение в этом блоге
Хороший ответ. Между прочим, это избавляет вас от необходимости беспокоиться о Converter для проблемы с enum-to-string.
Связанное решение кажется мертвым (корейский или японский текст?). Если я помещаю ваш код в свои ресурсы XAML, он говорит, что Enum не поддерживается в проекте WPF.
Вам нужно добавить 'xmlns: System = "clr-namespace: System; assembly = mscorlib"' в определения xlmns тега Window
Это конкретный ответ DevExpress, основанный на ответе Gregor S., получившем наибольшее количество голосов (в настоящее время у него 128 голосов).
Это означает, что мы можем сохранить единообразие стиля во всем приложении:

К сожалению, исходный ответ не работает с ComboBoxEdit от DevExpress без некоторых модификаций.
Во-первых, XAML для 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"/>
Излишне говорить, что вам нужно будет указать xamlExtensions в пространстве имен, которое содержит класс расширения XAML (который определен ниже):
xmlns:xamlExtensions = "clr-namespace:XamlExtensions"
И мы должны указать myEnum на пространство имен, содержащее перечисление:
xmlns:myEnum = "clr-namespace:MyNamespace"
Затем перечисление:
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
}
}
Проблема с XAML заключается в том, что мы не можем использовать SelectedItemValue, так как это вызывает ошибку, поскольку сеттер недоступен (немного недосмотра с вашей стороны, DevExpress). Поэтому нам нужно изменить наш ViewModel, чтобы получить значение непосредственно из объекта:
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");
}
}
Для полноты, вот расширение XAML из исходного ответа (слегка переименовано):
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
}
}
Отказ от ответственности: я не имею отношения к DevExpress. Telerik - также отличная библиотека.
Для справки, я не связан с DevExpress. У Telerik также есть прекрасные библиотеки, и этот метод может даже не понадобиться для их библиотеки.
Если вы используете MVVM, основываясь на ответе @rudigrobler, вы можете сделать следующее:
Добавьте следующее свойство в класс ViewModel
public Array ExampleEnumValues => Enum.GetValues(typeof(ExampleEnum));
Затем в XAML сделайте следующее:
<ComboBox ItemsSource = "{Binding ExampleEnumValues}" ... />
Мой любимый способ сделать это - использовать ValueConverter, чтобы ItemsSource и SelectedValue были привязаны к одному и тому же свойству. Для этого требуется без дополнительных свойств, чтобы ваша ViewModel оставалась красивой и чистой.
<ComboBox ItemsSource = "{Binding Path=ExampleProperty, Converter = {x:EnumToCollectionConverter}, Mode=OneTime}"
SelectedValuePath = "Value"
DisplayMemberPath = "Description"
SelectedValue = "{Binding Path=ExampleProperty}" />
И определение Конвертера:
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;
}
}
Этот конвертер будет работать с любым перечислением. ValueDescription - это простой класс со свойством Value и свойством Description. Вы можете так же легко использовать Tuple с Item1 и Item2 или KeyValuePair с Key и Value вместо Value и Description или любой другой класс по вашему выбору, если он может содержать значение перечисления и строковое описание этого значения перечисления.
Хороший ответ! Для класса ValueDescription свойство Description может быть опущено, если оно не требуется. Также работает простой класс со свойством только Value!
Кроме того, если вы хотите выполнить привязку к RadioButton, тогда метод Convert должен возвращать список строк, то есть .Select(e => e.ToString()), вместо использования класса ValueDescription.
Вместо ValueDescription также можно использовать KeyValuePair, например показано здесь
Код
public enum RULE
{
[Description( "Любые, без ограничений" )]
any,
[Description( "Любые если будет три в ряд" )]
anyThree,
[Description( "Соседние, без ограничений" )]
nearAny,
[Description( "Соседние если будет три в ряд" )]
nearThree
}
class ExtendRULE
{
public static object Values
{
get
{
List<object> list = new List<object>();
foreach( RULE rule in Enum.GetValues( typeof( RULE ) ) )
{
string desc = rule.GetType().GetMember( rule.ToString() )[0].GetCustomAttribute<DescriptionAttribute>().Description;
list.Add( new { value = rule, desc = desc } );
}
return list;
}
}
}
XAML
<StackPanel>
<ListBox ItemsSource= "{Binding Source = {x:Static model:ExtendRULE.Values}}" DisplayMemberPath = "desc" SelectedValuePath = "value" SelectedValue = "{Binding SelectedRule}"/>
<ComboBox ItemsSource = "{Binding Source = {x:Static model:ExtendRULE.Values}}" DisplayMemberPath = "desc" SelectedValuePath = "value" SelectedValue = "{Binding SelectedRule}"/>
</StackPanel>
Я изучил это и получил решение, которое вы можете использовать (в комплекте с локализацией) в WPF, расположенном здесь.