Как связать перечисление с элементом управления 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}" />