65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中绑定和使用友好枚举

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (73投票s)

2008年9月19日

CPOL

4分钟阅读

viewsIcon

357672

downloadIcon

4255

在 WPF 中绑定和使用友好枚举。

引言

作为 .NET 开发者,我们知道并且可能已经使用过 `Enum`。对于那些之前没有使用过 `enum` 的人,MSDN 对它们的介绍是这样的:

"enum 关键字用于声明一个枚举,这是一种包含一组命名常数(称为枚举数列表)的独立类型。每个枚举类型都有一个基础类型,可以是除 char 之外的任何整型。"

所以当我们使用 `enum` 时,实际上我们可以做一些像这样的事情:

enum Hardware {DiskDrive=1, Keyboard, GraphicsCard, Monitor};

这都没问题,但是想象一下,如果我们想在一个列表中显示 `enum` 的列表,并且我们希望有更具描述性的值,比如更友好的名称,但仍然在需要时保留选定的基础 `enum` 值。

本文将展示如何使用 WPF 完成以下操作:

  • 绑定到 `enum` 值的枚举
  • 为 `enum` 显示友好的名称,以改善用户体验

绑定到枚举值的枚举

我们可能想做的第一件事是显示所有可能的 `enum` 值列表,以便可以在此列表中选择当前值,或者允许用户选择一个新的 `enum` 值。使用以下技术很容易实现这一点:

<ObjectDataProvider x:Key="foodData"
                    MethodName="GetValues" 
                    ObjectType="{x:Type sys:Enum}">
    <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="local:FoodTypes" />
    </ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

然后我们可以像这样在 XAML 中使用它进行绑定:

<ComboBox x:Name="cmbFoodType"  
    ItemsSource="{Binding Source={StaticResource foodData}}"
	....
	....
</ComboBox>

在这里,我声明了一个演示 `enum`(如果您能暂时忽略 `LocalizableDescriptionAttribute`,稍后会详细介绍):

public enum FoodTypes : int
{
    Pizza = 1,
    Burger = 2,
    SpagBol = 3
}

这将产生以下结果:

为枚举显示友好的名称,以改善用户体验

现在这只完成了工作的一半,但从用户的角度来看,一些更具描述性的文本实际上可以改善用户体验,那么我们可以为此做些什么呢?好消息是,反射和特性提供了答案。我们可以用一个特殊的特性来装饰我们的 `enum`,即一个派生的 `LocalizableDescriptionAttribute`,它继承自 `DescriptionAttribute`,可以如下使用:

public enum FoodTypes : int
{
    [LocalizableDescription(@"Pizza", typeof(Resource))]
    Pizza = 1,

    [LocalizableDescription(@"Burger", typeof(Resource))]
    Burger = 2,

    [LocalizableDescription(@"SpagBol", typeof(Resource))]
    SpagBol = 3
}

我应该指出,本文的原始内容使用了 `EnumMember`,正如一些读者指出的那样,它无法针对不同文化进行本地化。幸运的是,其中一位读者是才华横溢的 Uwe Keim,他给了我一些处理 `enum` 本地化的代码。以下是 `LocalizableDescriptionAttribute` 的代码。谢谢 Uwe。

using System;
using System.Resources;
using System.Reflection;
using System.Globalization;
using System.ComponentModel;

namespace FriendlyEnumValues
{
    /// <summary>
    /// Attribute for localization.
    /// </summary>
    [AttributeUsage(AttributeTargets.All,Inherited = false,AllowMultiple = true)]
    public sealed class LocalizableDescriptionAttribute : DescriptionAttribute
    {
        #region Public methods.
        // ------------------------------------------------------------------

        /// <summary>
        /// Initializes a new instance of the 
        /// <see cref="LocalizableDescriptionAttribute"/> class.
        /// </summary>
        /// <param name="description">The description.</param>
        /// <param name="resourcesType">Type of the resources.</param>
        public LocalizableDescriptionAttribute
		(string description,Type resourcesType) : base(description)
        {
            _resourcesType = resourcesType;
        }

        #endregion

        #region Public properties.

        /// <summary>
        /// Get the string value from the resources.
        /// </summary>
        /// <value></value>
        /// <returns>The description stored in this attribute.</returns>
        public override string Description
        {
            get
            {
                if (!_isLocalized)
                {
                    ResourceManager resMan =
                         _resourcesType.InvokeMember(
                         @"ResourceManager",
                         BindingFlags.GetProperty | BindingFlags.Static |
                         BindingFlags.Public | BindingFlags.NonPublic,
                         null,
                         null,
                         new object[] { }) as ResourceManager;

                    CultureInfo culture =
                         _resourcesType.InvokeMember(
                         @"Culture",
                         BindingFlags.GetProperty | BindingFlags.Static |
                         BindingFlags.Public | BindingFlags.NonPublic,
                         null,
                         null,
                         new object[] { }) as CultureInfo;

                    _isLocalized = true;

                    if (resMan != null)
                    {
                        DescriptionValue =
                             resMan.GetString(DescriptionValue, culture);
                    }
                }

                return DescriptionValue;
            }
        }
        #endregion

        #region Private variables.

        private readonly Type _resourcesType;
        private bool _isLocalized;

        #endregion
    }
}

这里的基本思想是,这个 `LocalizableDescriptionAttribute` 允许你传入一个键和一个资源类型来查找,所以键值将索引到资源文件并获取资源文件的值。这在附加的演示代码中包含的小资源文件中有所展示。

既然我们知道 `enum` 可以这样做,那么如何在 XAML 中将其用作 `ComboBox` 呢?嗯,幸运的是,还有另一个 WPF 技巧可以提供帮助,那就是 `IValueConverter`。让我们看看修改后的 XAML:

<Window.Resources>
    <local:EnumToFriendlyNameConverter x:Key="enumItemsConverter"/>

</Window.Resources>

<StackPanel>

    <!-- Enum Combobox picker -->
    <StackPanel Orientation="Vertical" Margin="2" Grid.Row="0" Grid.Column="0"   >

        <Label Height="Auto" Content="Food Types"/>
        <ComboBox x:Name="cmbFoodType"  
            ItemsSource="{Binding Source={StaticResource foodData}}"
	   		  ....
	    	  ....
            <ComboBox.ItemTemplate>

                <DataTemplate>
                    <Label  Content="{Binding   Path=.,Mode=OneWay, 
                                        Converter={StaticResource enumItemsConverter}}"
                            Height="Auto"
                            Margin="0" 
                            VerticalAlignment="Center"/>

                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </StackPanel>

</StackPanel>

其中 `EnumToFriendlyNameConverter` 如下所示:

/// <summary>
using System;
using System.Windows.Data;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;

namespace FriendlyEnumValues
{
    /// <summary>
    /// This class simply takes an enum and uses some reflection to obtain
    /// the friendly name for the enum. Where the friendlier name is
    /// obtained using the LocalizableDescriptionAttribute, which holds the localized
    /// value read from the resource file for the enum
    /// </summary>
    [ValueConversion(typeof(object), typeof(String))]
    public class EnumToFriendlyNameConverter : IValueConverter
    {
        #region IValueConverter implementation

        /// <summary>
        /// Convert value for binding from source object
        /// </summary>
        public object Convert(object value, Type targetType,
                object parameter, CultureInfo culture)
        {
            // To get around the stupid WPF designer bug
            if (value != null)
            {
                FieldInfo fi = value.GetType().GetField(value.ToString());

                // To get around the stupid WPF designer bug
                if (fi != null)
                {
                    var attributes =
                        (LocalizableDescriptionAttribute[]) 
			fi.GetCustomAttributes(typeof 
			(LocalizableDescriptionAttribute), false);

                    return ((attributes.Length > 0) &&
                            (!String.IsNullOrEmpty(attributes[0].Description)))
                               ?
                                   attributes[0].Description
                               : value.ToString();
                }
            }

            return string.Empty;
        }

        /// <summary>
        /// ConvertBack value from binding back to source object
        /// </summary>
        public object ConvertBack(object value, Type targetType, 
			object parameter, CultureInfo culture)
        {
            throw new Exception("Cant convert back");
        }
        #endregion
    }
}

实际的魔法是通过使用一些反射来实现的。所以如果你需要在 XBAP 中运行它,你需要确保它在 `FullTrust` 模式下运行。

拼图的最后一步是确保选定的值能够传回可能使用 `enum` 值之一的源对象。我正在使用一个简单的测试设置,包括一个 `ViewModel` 和一个测试类。从附加的演示代码中应该很清楚。

总之,确保测试类接收实际 `enum` 值而不是它不知道如何处理的友好名称的部分,仅仅是 XAML 中更多的数据绑定。如下所示:

<ComboBox x:Name="cmbFoodType"  
    ItemsSource="{Binding Source={StaticResource foodData}}"
    SelectedItem="{Binding Path=TestableClass.FoodType, Mode=TwoWay}" Height="Auto">
    <ComboBox.ItemTemplate>

        <DataTemplate>
            <Label  Content="{Binding   Path=.,Mode=OneWay, 
                                Converter={StaticResource enumItemsConverter}}"
                    Height="Auto"
                    Margin="0" 
                    VerticalAlignment="Center"/>

        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

这里现在包括了一个到 `SelectedItem` 的绑定,这是一个到测试类中实际 `enum` 值的 `TwoWay` 绑定。

所以把所有东西放在一起,我们现在有一个绑定的 `ComboBox`,它向用户显示友好的值,但为选定项在绑定对象中维护正确的 `enum` 值。

这是测试类的选定值,请注意,它是正确的 `enum` 值。

我认为这在一定程度上改善了用户体验,希望它能帮助到你,正如它帮助了我一样。

替代方法

自从我写了这篇文章以来,传奇人物 Andrew Smith(Infragistics……(连 Josh Smith 都称他为大师))给我发了一封电子邮件,其中提供了一种替代方法,他创建了一个 `MarkupExtension` 来实现与此相同的功能,所以你可能想在他的博客上看看。该帖子可以通过链接访问:http://agsmith.wordpress.com/2008/09/19/accessing-enum-members-in-xaml/,谢谢 Andrew。

© . All rights reserved.