将枚举属性绑定到WPF中的组合框

以下面的代码为例:

public enum ExampleEnum { FooBar, BarFoo }


public class ExampleClass : INotifyPropertyChanged
{
private ExampleEnum example;


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

我想把属性ExampleProperty绑定到一个组合框,这样它就显示了选项“FooBar”和“BarFoo”,并在模式双向工作。最理想的情况下,我希望我的ComboBox定义看起来像这样:

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

目前我有处理程序的组合框。SelectionChanged和ExampleClass。在我手动绑定的窗口中安装了PropertyChanged事件。

有没有更好的或者某种权威的方法?你通常会使用转换器,你将如何填充组合框与正确的值?我现在甚至不想从i18n开始。

编辑

因此,一个问题得到了回答:我如何用正确的值填充组合框。

通过ObjectDataProvider从静态Enum中检索Enum值作为字符串列表。getvalue方法:

<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 ItemsSource="{Binding Source={StaticResource ExampleEnumValues}}"/>
199709 次浏览

我不知道在xml中是否可行,但请尝试以下方法:

给你的组合框一个名字,这样你就可以在代码背后访问它:"typesComboBox1"

现在试试下面的方法

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

试着用

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

你可以这样考虑:

  1. 定义一个文本块的样式,或任何其他控件,你想用来显示你的enum:

    <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. define your style for ComboBoxItem

    <Style TargetType="{x:Type ComboBoxItem}">
    <Setter Property="ContentTemplate">
    <Setter.Value>
    <DataTemplate>
    <TextBlock Tag="{Binding}" Style="{StaticResource enumStyle}"/>
    </DataTemplate>
    </Setter.Value>
    </Setter>
    </Style>
    
  3. add a combobox and load it with your enum values:

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

if your enum is large, you can of course do the same in code, sparing a lot of typing. i like that approach, since it makes localization easy - you define all the templates once, and then, you only update your string resource files.

您可以创建自定义标记扩展。

用法示例:

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) // or IXamlServiceProvider for UWP and WinUI
{
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; }
}
}

在视图模型中,你可以有:

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绑定到MyEnumTypeValuesSelectedItem绑定到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}}"/>

我不喜欢在UI中使用enum的名称。我更喜欢使用不同的值为用户(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模式兼容。

这是一个使用helper方法的通用解决方案。 这也可以处理任何底层类型的枚举(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}}"

找到这个本博客的解决方案

这是一个DevExpress特定的答案,基于Gregor S.投票最多的答案(目前有128票)。

这意味着我们可以在整个应用程序中保持样式一致:

enter image description here

不幸的是,如果没有一些修改,最初的答案不能与来自DevExpress的ComboBoxEdit一起工作。

首先,ComboBoxEdit的XAML:

<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,因为这会抛出一个错误,因为setter是不可访问的(这是你的疏忽,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也是一个很棒的图书馆。

我已经创建了一个开源的CodePlex上项目来做这件事。你可以从在这里下载NuGet包。

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

如果你正在使用MVVM,基于@rudigrobler的答案,你可以做以下事情:

将以下属性添加到视图模型类中

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属性。你可以很容易地使用带有Item1Item2Tuple,或者带有KeyValueKeyValuePair来代替Value和Description,或者任何其他你选择的类,只要它可以保存枚举值和该枚举值的字符串描述。

代码

    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>

看到所有的一切,看到某些过于复杂的解决方案如何成为“标准(反)模式”是一件痛苦的事情。对于最琐碎的问题:应该避免实现MarkupExtension的开销和复杂性,特别是使用属性修饰枚举值。简单地实现一个数据模型。

通常,向用户显示枚举值名称是一个坏主意。枚举不应该显示在UI中。它们是在编程上下文中使用的常量。值名称不用于显示。他们是为了称呼工程师,因此这些名字通常使用特殊的语义和词汇,就像科学词汇一样,不被公众所理解。不要犹豫,为显示的值创建一个专用的源。

当涉及到本地化时,问题变得更加明显。
这就是为什么所有贴出来的答案都是过度设计的。它们使一个非常简单的问题看起来像一个关键问题。
最平凡的解决方案就是最好的解决方案,这是事实。原始问题的主题绝对是异常。
我强烈建议不要使用任何提供的答案。尽管它们可能有效,但它们给一个微不足道的问题增加了不必要的复杂性

注意,你总是可以通过调用静态Enum.GetValuesEnum.GetNames将枚举转换为其值或值名的列表,它们都会返回一个IEnumerable,你可以直接将其赋值给ComboBox.ItemsSource属性,例如通过数据绑定。

IEnumerable<ExampleEnum> values = Enum.GetValues<ExampleEnum>();
IEnumerable<string> names = Enum.GetNames<ExampleEnum>();
通常,当定义一个枚举时,你不会想到UI。
枚举值名称不是基于UI设计规则选择的。
通常,UI标签和文本都是由没有开发人员或程序员背景的人创建的。他们通常提供本地化应用程序所需的所有翻译。
有很多很好的理由不将UI与应用程序混合使用。
你永远不会在设计一个类的时候用UI(例如,DataGrid列)来命名它的属性。你可能希望你的列标题包含空格等 这也是为什么异常消息是针对开发人员而不是用户的原因。你肯定不希望为了在特定的UI上下文中提供一个对用户有意义的显示名称而装饰每个属性、每个异常、枚举或任何数据类型或成员。
你不希望UI设计渗透到你的代码库中,污染你的类。
应用程序及其用户界面——这是两个不同的问题。
添加这种抽象或虚拟的额外分离层允许添加不应该显示的enum值。或者更一般的,修改代码而不必破坏或修改UI 你应该使用简单的 IValueConverter或提供这些显示值的专用类作为绑定源,而不是使用属性和实现额外的逻辑负载来提取它们的值(使用反射)。
坚持最常见的模式,并为ComboBox项实现一个数据模型,其中类有一个enum类型的属性作为成员,帮助你识别ComboBox.SelectedItem(如果你需要enum值):

ExampleEnum.cs

// Define enumeration without minding any UI elements and context
public enum ExampleEnum
{
FooBar = 0,
BarFoo
}

ExampleClass.cs

// Define readable enum display values in the UI context.
// Display names can come from a localizable resource.
public class BindingSource : INotifyPropertyChanged
{
public BindingSource()
{
ItemModels = new List<ItemModel>
{
new ItemModel { Label = "Foo Bar Display", Value = ExampleEnum.FooBar },
new ItemModel { Label = "Bar Foo Display", Value = ExampleEnum.BarFoo }
}
}


public List<ItemModel> ItemModels { get; }


private ItemModel selectedItemModel;
public ItemModel SelectedItemModel { get => selectedItemModel; => set and notify; }
}

ItemModel.cs

public class ItemModel
{
public string Label { get; set; }
public ExampleEnum Value { get; set; }
}

MainWindow.xaml

<Window>
<Window.DataContext>
<BindingSource />
</Window.DataContext>


<ComboBox ItemsSource="{Binding ItemModels}"
DisplayMemberName="DisplayValue"
SelectedItem="{Binding SelectedItemModel}" />
</Window>