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

在 WPF 中显示用户友好的枚举值

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (25投票s)

2011年11月30日

CPOL

13分钟阅读

viewsIcon

111713

downloadIcon

1742

提供一个辅助类,可以轻松显示用户友好的枚举值表示,包括可自定义和本地化的文本、图像以及任意 XAML 内容。该类还可以用作枚举和非枚举类的通用 XAML switch 语句。

EnumItemList/EnumItemList.GIF

引言

枚举为程序员提供了一种方便的方式来限制和描述字段或参数允许的值,但如何将它们暴露给用户呢?一个常见的场景是将值显示在列表或组合框中,但直接暴露代码名称通常不合适且无法本地化。

本文介绍了一个小巧但功能强大的辅助类,可以更轻松地以用户友好且可本地化的方式显示枚举中的允许值,包括不仅显示文本,还显示任意图形 WPF 内容。

背景

一种常用推荐的方法是使用 ObjectDataProvider 调用静态 Enum.GetValues 方法

<ObjectDataProvider MethodName="GetValues"
                    ObjectType="{x:Type sys:Enum}"
                    x:Key="FontStylesValues">
    <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="e:FontStyles" />
    </ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

<ComboBox  ItemsSource="{Binding Source={StaticResource FontStylesValues}}" />

这种方法的优点是它不需要任何代码隐藏。缺点是它冗长且无法自定义或本地化文本。仅使用 XAML 按字母顺序对项目进行排序似乎是不可能的。

与之前在 The Code Project 上由 David VeenemanTom F WrightSacha Barber 提出的解决方案相比,下面提出的解决方案不依赖于绑定值转换器(IValueConverter)。取而代之的是,枚举值及其相应的视觉表示存储在 EnumItemList 类型的集合中,该集合会自动填充。枚举表示可以是本地化的字符串、来自资源文件的位图或图标,甚至是任意 XAML 内容。由于 EnumItemList 也可用于非枚举类型,因此它通常可用作 XAML switch 语句。

使用代码

基本用法

让我们从最简单的用例开始,即仅显示程序员命名的所有枚举值,而无需任何自定义。

<ComboBox SelectedValue="{Binding FontStyle}" SelectedValuePath="Value" >
  <ComboBox.ItemsSource>
    <e:EnumItemList EnumType=”{x:Type e:FontStyles}” />
  </ComboBox.ItemsSource> 
</ComboBox>

EnumItemList 类表示一个枚举值-名称对的集合,通过指定 EnumType 属性来填充。值-名称对由 EnumItem 类表示,该类在 Value 属性中保存枚举值。由于组合框项不是枚举值本身,因此重要的是将 SelectedValuePath 设置为 Value,并将底层属性绑定到 SelectedValue(而不是 SelectedItem)。

在这个基本示例中,EnumItemList 实例直接作为此特定组合框的 ItemsSource 创建,但在大多数应用程序中,通过将其定义为资源来重用相同的列表会更有效率且易于维护,如下面的示例所示。在 Model-View-ViewModel (MVVM) 应用程序中,可以有选择地以编程方式创建 EnumItemList,并通过绑定到静态 ViewModel 属性来访问它。

在这种情况下,使用 ObjectDataSource 也可以获得类似的结果,但这需要更多的 XAML 代码,并且不提供下一节中描述的本地化和自定义支持。使用 EnumItemList,这些项默认也会按字母顺序自动排序 - 稍后将对此进行更多介绍。

XAML 中的文本自定义

如前所述,EnumItemListEnumItems 的集合。要自定义显示的文本,您可以直接在 XAML 代码中添加新的 EnumItems,如下所示

<e:EnumItemList EnumType="{x:Type e:FontStyles}" >
    <e:EnumItem Value="BoldItalic">Bold and Italic</e:EnumItem>
    <e:EnumItem Value="{x:Null}">(Select style)</e:EnumItem>
</e:EnumItemList>

在第二行中,我们将 BoldItalic 值更改为一个更友好的形式。这将替换此特定枚举值的旧显示文本。其他枚举值将像以前一样显示其代码名称。在此示例中,我们还为 null 值添加了一个新的命名列表选项,以支持可空的后端属性。这在使用 ObjectDataSource 时不容易实现。

Value 属性的值将在设计时和运行时进行检查,因此如果枚举值未被识别,Visual Studio WPF 设计器将报告错误。

通过如上所示在 EnumItems 中指定要显示的枚举值的文本,可以像所有其他 XAML 内容一样方便地本地化它们,如 MSDN 页面 WPF Globalization and Localization Overview 所述。

通过代码属性进行文本自定义

与 Sacha Barber 的文章中所述的方法相同,EnumItemList 还会查找枚举值上的 DescriptionAttribute,并在找到时使用其描述作为枚举文本,而不是枚举条目代码名称。

public enum FontStyles
{
    Normal,
    Bold,
    Italic,
        
    [Description("Bold and italic")]
    BoldItalic
}

XAML 中指定的枚举值文本优先于 DescriptionAttribute 中的文本。可以使用 LocalizedDescriptionAttribute 实现通过代码属性指定的枚举描述的本地化,如 Sacha 的文章中所述。

基于属性的自定义枚举值可视化的技术的限制是,如果枚举类型在 .NET Framework 或第三方库中声明,则您无法对其进行自定义。另一方面,使用 EnumItemList 在 XAML 中指定自定义和本地化文本将适用于任何枚举类型,包括框架和第三方库中定义的枚举类型。在这些情况下,您可以像上面所示那样在 XAML 中指定所有自定义项文本,或者将枚举表示指定为程序集资源,如下文所述。

通过自定义类型转换器自定义枚举文本

EnumItemList 还支持通过为枚举类型使用自定义 TypeConverter 来指定自定义字符串转换。

[TypeConverter(typeof(MyEnumConverter))]
public enum MyEnum { … }

自定义转换器(MyEnumConverter)必须继承自 System.ComponentModel.TypeConverter,并且应在其 ConvertTo 方法中返回自定义枚举文本。这样,可以从其他源读取本地化名称。但是,EnumItemList 已经支持一种更简单的方法来从程序集资源文件中读取本地化枚举文本,如下一节所述。

从程序集资源中读取枚举项

EnumItemList 本质上支持从程序集资源文件中读取枚举值表示,并且这不仅限于文本。图标和图像资源也开箱即用。要启用此功能,您只需在 Visual Studio 中创建一个类型安全的资源文件,其中资源名称为 EnumType 和枚举项名称,中间用下划线分隔,例如 FontStyles_Bold。资源可以是字符串、图标或图像。要激活此功能,您只需将 ResourceType 属性设置为资源类型。

<e:EnumItemList EnumType="e:FontStyles" ResourceType="res:Resources" />

找到的任何资源将替换 EnumItem 的当前 DisplayValue,并且由于 EnumItem 类有一个自定义类型转换器,因此非文本资源枚举表示将正确显示,而无需数据模板。缺少资源不会导致运行时错误,但在调试版本中,会发出调试跟踪消息以告知此事实。

显示绘图和图像

通过简单地将 EnumItems 的内容属性 DisplayValue 设置为 Drawing 派生类型,可以显示线条图(GeometryDrawing)、图像(ImageDrawing)甚至视频(VideoDrawing)作为枚举项表示。

<e:EnumItem Value="Italic" Text="Italic" >
    <GeometryDrawing Geometry="M 5,0 L 0,16" >
        <GeometryDrawing.Pen>
            <Pen Brush="Black" Thickness="2" />
        </GeometryDrawing.Pen>
    </GeometryDrawing>
</e:EnumItem>

在自定义数据模板中使用 EnumItems

在现代 WPF 应用程序中,您可能希望以比纯文本或图像更丰富的方式来显示每个枚举值。EnumItem 的内容属性 DisplayValue 可以设置为任意对象,并绑定到自定义 DataTemplate 中的任意依赖项属性。例如,如果您需要为每个枚举值显示不同的颜色,则可能出现这种情况。

<Window.Resources>
   <e:EnumItemList x:Key="valueStatusEnumList" EnumType="{x:Type e:ValueStatus}" >
        <e:EnumItem Value="Ok" DisplayValue="Green" Text="Ok" />
        <e:EnumItem Value="Warning" DisplayValue="Orange" Text="Warning" />
        <e:EnumItem Value="Error" DisplayValue="Red" Text="Error" />
    </e:EnumItemList>
</Window.Resources> 
...
<ComboBox ItemsSource="{StaticResource valueStatusEnumList}" 
        SelectedValue="{Binding Status}" SelectedValuePath="Value" >
    <ComboBox.ItemTemplate>
        <DataTemplate DataType="{x:Type e:EnumItem}" >
            <TextBlock Text="{Binding Text, Mode=OneTime}" 
                 Background="{Binding DisplayValue, Mode=OneTime}" />
        </DataTemplate>
    </ComboBox.ItemTemplate>            
</ComboBox>

这里有一个名为 ValueStatus 的枚举类型,其值为 OkWarningError。我们不只是定义一个文本,而是使用 EnumItemDisplayValue 属性来定义要使用的颜色,然后重新定义 ItemTemplate 以将该值用于 Background 属性。结果如下所示。

EnumItemList/ColoredItems.GIF

当使用 EnumItemList 显示非文本内容时,EnumItemText 属性非常方便,可以为每个项提供替代文本表示。在上面的示例中,Text 属性在数据模板中用于显示它。设置 Text 还允许用户在非文本 DisplayValues 中键入第一个字母来使用键盘选择一个项。

将任意 UIElement 作为枚举表示

在某些情况下,您可能需要为每个枚举值显示一个由多个用户界面元素(例如,图标、文本和描述)组成的复杂视图。这也可以通过为每个值指定 DataTemplate 来由 EnumItemList 支持,如下所示。

<Windows.Resources>
   <e:EnumItemList x:Key="fontStylesList" EnumType="{x:Type e:FontStyles}" >
        <e:EnumItem Value="Bold" Text="Bold" >
            <DataTemplate>
                <TextBlock FontWeight="Bold" 
                   Text="{Binding Text, Mode=OneTime}"></TextBlock>
            </DataTemplate>
        </e:EnumItem>
        <e:EnumItem Value="BoldItalic" Text="Bold and Italic" >
            <DataTemplate>
            <TextBlock FontWeight="Bold" 
              FontStyle="Italic">Bold and Italic</TextBlock>
            </DataTemplate>
        </e:EnumItem>
        <e:EnumItem Value="Italic" Text="Italic" >
            <DataTemplate>
            <TextBlock FontStyle="Italic">Italic</TextBlock>
            </DataTemplate>
        </e:EnumItem>
        <e:EnumItem Value="Normal" Text="Normal" >
            <DataTemplate>
                <TextBlock>Normal</TextBlock>                   
            </DataTemplate> 
        </e:EnumItem>
    </e:EnumItemList>
</Windows.Resources>

<ComboBox ItemsSource="{StaticResource fontStylesList}" 
          SelectedValue="{Binding FontStyle}" 
          SelectedValuePath="Value" >
    <!--  <ComboBox.ItemTemplate>
        <DataTemplate >
            <ContentPresenter ContentTemplate="{Binding DisplayValue, Mode=OneTime}" />
        </DataTemplate> 
    </ComboBox.ItemTemplate>  -->
</ComboBox>

在这种情况下,我们可以将 EnumItemDisplayValue 绑定到 ComboBoxItemTemplate 中的 ContentPresenterContentTemplate。如代码片段中的注释所示,实际上无需显式指定。由于 EnumItem 类有一个自定义类型转换器,因此会自动应用任何 DataTemplate,使用包含 ContentPresenter 的默认内容模板。也可以跳过数据模板部分,直接将 UIElement 指定为 EnumItem 的内容,但这将无法使用绑定到 EnumItem 属性以从资源文件获取本地化文本。此外,当需要重用枚举项时,必须克隆 UIElement,这不如使用 DataTemplate 快速。结果如下所示。

EnumItemList/EnumItemList.GIF

在上面的示例中,枚举项模板之间唯一不同的地方是 FontStyleFontWeight 属性值。在这种情况下,我们可以改为为每个枚举项定义一个样式,并为这些属性设置 setter。EnumItem 本质上通过将 Style 应用于显示 Text 内容的 TextBlock 来支持 Style 内容,而无需指定 ItemTemplate

前提是复杂的枚举列表仅用于单个列表控件,您可以跳过 EnumItemList,通过为每个枚举值创建一个控件并使用 DataContext 绑定到该值来获得相同的结果。

<ComboBox SelectedValuePath="DataContext" SelectedValue="{Binding FontStyle}">
    <TextBlock DataContext="Normal">Normal</TextBlock>
    <TextBlock DataContext="Bold" FontWeight="Bold">Bold</TextBlock>
    <TextBlock DataContext="Italic" FontStyle="Italic">Italic</TextBlock>
    <TextBlock DataContext="BoldItalic" FontStyle="Italic" 
        FontWeight="Bold">Bold And Italic</TextBlock>
</ComboBox>

此解决方案需要更少的代码,但这些项在其他上下文中不容易重用。另一个限制是这些项不能轻松地按照本地化文本进行排序。使用 EnumItemList,默认情况下按本地化文本排序,这可以如下一节所述进行自定义。

排序项

在大多数情况下,按字母顺序对枚举项进行排序是最用户友好的。使用 EnumItemList,这是默认行为。要自定义排序,您可以设置 XAML 中的 SortBy 属性,以按 TextDisplayValueValue 属性排序。通过将 Comparer 属性设置为您自己的 IComparer<EnumItem> 实现,可以实现更高级的排序。

将 EnumItemList 用作值转换器

到目前为止,所有示例都是为了自定义组合框枚举显示。当然,同样的方法可以用于任何其他 ItemsControl,包括 ListBox。有时在列表控件上下文之外可视化枚举值很有用。为了支持此类场景,EnumItemList 能够作为绑定的值转换器。在下面的示例中,枚举属性 Status 的值被可视化为静态(非交互式)控件,每个枚举值具有不同的背景颜色。

<ContentControl Content="{Binding Status,Converter={StaticResource valueStatusEnumList} }" >
    <ContentControl.ContentTemplate>
        <DataTemplate DataType="{x:Type e:EnumItem}" >
            <TextBlock Text="{Binding Text, Mode=OneTime}" 
               Background="{Binding DisplayValue, Mode=OneTime}" />
        </DataTemplate>
    </ContentControl.ContentTemplate>
</ContentControl>

您可能已经注意到,DataTemplate 与之前组合框项使用的模板相同。当 EnumList 被指定为绑定的转换器时,它会将枚举值转换为相应的 EnumItem 实例,因此其所有属性都可以在数据模板中使用。

注意:正如论坛中报道的那样,Visual Studio 中的 WPF 设计器在使用数据模板中的 EnumItemList 作为值转换器以及其他特定情况下时可能会报告错误。但是,这不会影响生成的能力和运行时功能。这似乎与此处报告的 WPF 设计器问题 有关。通过重写 EnumItemList 使其不继承自 ObservableCollection(即不实现 IList),可能可以解决这个小的设计时问题,但这样大的返工可能会破坏其他用法。

将 EnumItemList 用于非枚举类型

有趣的是,EnumItemList 的实现也可用于未声明为枚举的类型,包括整数、字符串和自定义对象。例如,来自某个外部系统的错误代码,无法或不适合将其包装在枚举类型中。要直接在 XAML 中将错误代码映射到更用户友好且可本地化的描述,可以使用 EnumItemList,如下所示。

<Window.Resources>
    <e:EnumItemList x:Key="errorCodeList" 
              EnumType="{x:Type sys:Int32}" SortBy="Value" >
        <e:EnumItem Value="1001">File not found.</e:EnumItem>
        <e:EnumItem Value="1002">Directory not found.</e:EnumItem>
        <e:EnumItem Value="1003">Not sufficient disc space.</e:EnumItem>
        <e:EnumItem Value="1004">General read error.</e:EnumItem>
        <e:EnumItemList.DefaultItem>
            <e:EnumItem >Unrecognized error.</e:EnumItem>
        </e:EnumItemList.DefaultItem>
    </e:EnumItemList>
    <DataTemplate x:Key=errorCodeTemplate >
       <StackPanel Orientation="Horizontal" >
                <ContentPresenter Content="{Binding Value, Mode=OneTime}" Width="35" />
                <ContentPresenter Content="{Binding DisplayValue, Mode=OneTime}" />
            </StackPanel>
    </DataTemplate>
</Windows.Resources>
…
<ComboBox SelectedValuePath="Value" SelectedValue="{Binding ErrorCode}" 
          ItemsSource="{StaticResource errorCodeList}"
          ItemTemplate=”{StaticResource errorCodeTemplate}” />

<ContentControl Content="{Binding ErrorCode, Converter={StaticResource errorCodeList}}" 
                ContentTemplate=”{StaticResource errorCodeTemplate}” />

这样,EnumItemList 可以作为一个非常通用的 XAML switch/case 语句。如上面的代码片段所示,还有一个 DefaultItem EnumList 项,它作为在找不到匹配项时返回的项的模板。返回项中的 Value 属性将是实际值,因此如果 ErrorCode 设置为未列出的值,则会显示默认消息。结果如下所示。

EnumItemList/NonEnumItems.GIF

实现细节

EnumItemList

为了允许将 EnumItemList 实例直接用作 ItemSource 并使用 XAML 添加项,EnumItemList 必须实现 IList。我们还想在添加项时进行一些类型检查。我选择简单地派生 EnumItemList 类自 ObservableCollection 以获得更改通知支持,从而支持在绑定发生后添加或更改项。当设置 EnumType 属性时,列表会填充,以支持在 XAML 中定义它。

public class EnumItemList : ObservableCollection<EnumItem>, IValueConverter
{
    public Type EnumType
    {
        get { return m_enumType; }
        set
        {
            m_enumType = value;
                
            // Cache type converter to increase performance.
            m_converter = TypeDescriptor.GetConverter(m_enumType);

            // Convert any existing item values to type.
            foreach (EnumItem item in this)
            {
                ConvertValueToEnumType(item);
            }

            //For enum types, fill list with values and their names 
            if (m_enumType.IsEnum)
            {
                // For all public static fields, i.e. all enum values
                foreach (FieldInfo field in m_enumType.GetFields(
                         BindingFlags.Public | BindingFlags.Static))
                {
                    object fieldValue = field.GetValue(null);

                    // Look for DescriptionAttributes.
                    object[] descriptions = 
                      field.GetCustomAttributes(typeof(DescriptionAttribute), true);

                    string displayName;
                    if (descriptions.Length > 0)
                    {
                        displayName = ((DescriptionAttribute)descriptions[0]).Description;
                    }
                    else
                    {
                        try
                        {
                            // Use type converter to support enums with custom TypeConverters.
                            displayName = m_converter.ConvertToString(fieldValue);
                        }
                        catch (Exception ex)
                        {
                            displayName = field.Name;
                        }
                    }
                    EnumItem item = new EnumItem() { Value = fieldValue, DisplayValue = displayName };
                        
                    if (IndexOfValue(item.Value) < 0)
                    // Do not add item if it already exists.
                    {
                        Add(item);
                    }
                }
            }
        }
    }

    public void ConvertValueToEnumType(EnumItem item)
    {
        if (m_enumType != null && item.Value != null && 
           (m_enumType.IsAssignableFrom(item.Value.GetType()) == false))
        {
            item.Value = 
              m_converter.ConvertFrom(null, CultureInfo.InvariantCulture, item.Value);
        }
    }
…
}

EnumItem

EnumItem 类是一个纯容器类,具有 ValueDisplayValueText 属性。

[TypeConverter(typeof(EnumItemTypeConverter))]
[Serializable()]
[ContentProperty("DisplayValue")]
public class EnumItem
{
    [Localizable(false)]
    public Object Value { get; set; }

    [Localizable(true)]
    public Object DisplayValue { get; set; }

    private String m_text;

    [Localizable(true)]
    public String Text
    {
        get {
            return m_text ?? ((DisplayValue != null) ? DisplayValue.ToString() : 
                     ((Value != null ? Value.ToString() : null)));
        }
        set { m_text = value; }
    }

    public override string ToString()
    {
        return Text;
    }
}

ContentProperty 属性用于定义 DisplayValue 是设置为 XML 元素内容。ToString() 被重写,以便在绑定到 String 类型属性以及执行文本搜索时返回文本表示。

EnumItemTypeConverter

为了支持显示非文本内容而不必指定 ItemTemplateEnumItem 有一个自定义类型转换器,该转换器能够在 ContentPresenter 上下文中提供要显示的 UIElement,其中包括默认控件模板的 ComboBoxItems。如果您没有指定模板,ContentPresenter 将询问转换器是否提供 UIElement。根据 DisplayValue 的类型,转换器将返回一个合适的 UIElement,如下面的代码片段所示。

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
    return destinationType == typeof(UIElement) || destinationType == typeof(String);
}

public override object ConvertTo(ITypeDescriptorContext context, 
       CultureInfo culture, object value, Type destinationType)
{
    EnumItem item = value as EnumItem;
    if (item != null)
    {
        if (destinationType == typeof(String))
        {
            return item.ToString();
        }
        if (destinationType == typeof(UIElement))
        {
            object displayValue = item.DisplayValue;
            if (displayValue == null || displayValue is String)
            {
                TextBlock textBlock = new TextBlock();
                textBlock.Text = item.ToString();
                return textBlock;
            }
            else if (displayValue is UIElement)
            {
                if (VisualTreeHelper.GetParent((UIElement)displayValue) != null)
                {
                    // Clone UIElement to allow it to be used several times.
                    string str = XamlWriter.Save(displayValue);
                    StringReader sr = new StringReader(str);
                    XmlReader xr = XmlReader.Create(sr);
                    UIElement ret = (UIElement)XamlReader.Load(xr);
                    return ret;
                }
                else
                {
                    return displayValue;
                }
            }
            if (displayValue is DataTemplate)
            {
                ContentPresenter presenter = new ContentPresenter();
                presenter.Content = item;
                presenter.ContentTemplate = (DataTemplate)displayValue;
                return presenter;
            }
            else if (displayValue is Style)
            {
                TextBlock textBlock = new TextBlock();
                textBlock.Style = (Style)displayValue;
                textBlock.Text = item.ToString();
                return textBlock;
            }
            // Support for other DisplayValue types not shown in article.
        }
        throw new InvalidOperationException(
              "Unable to convert item value to destination type.");
    }
    return null;
}

如上面的代码片段所示,支持将 UIElement 作为直接显示值。为了能够在多个位置使用它,UIElement 使用 XamlWriter 进行必要的克隆。如果性能很重要,建议将其包装在 DataTemplate 中。

结论

这里介绍的 EnumItemList 的设计易于开发人员使用,并涵盖了许多枚举值需要呈现的情况,同时提供良好的本地化支持和对非文本内容的支持。它甚至可以作为具有非枚举类型的通用 XAML switch 语句。

我特别高兴找到了支持显示非文本内容(如 DataTemplates、Drawings 和图标资源)的方法,而无需在 XAML 中指定自定义模板。解决方案是一个自定义 TypeConverter,它在 ContentPresenter 上下文中显示时提供一个合适的 UIElement

EnumItemList 不能直接在 Silverlight 中使用,因为它使用了 TypeDescriptor 并支持 Silverlight 不支持的非文本资源。然而,通过剥离这些功能,应该很容易使其在 Silverlight 中也能正常工作,至少对于本地化的文本枚举表示。

希望它能帮助您创建用户希望看到枚举值显示方式的应用程序。

历史

2011年11月30日

  • 初版发布。

2012年7月7日

  • 更新了源代码,增加了对 Image 控件和 ImageSource 属性的支持,正如在下面的文章论坛中讨论的那样。还进行了一些小的 bug 修复。

2014年3月3日

  • 根据 Thoits (版本 1.2),更新了代码,修复了 WPF 设计器错误和转换器改进。

2014年3月12日

  • 更新了文章源代码(版本 1.3),并添加了关于 WPF 设计器错误的说明。

© . All rights reserved.