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

从枚举填充组合框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (30投票s)

2016 年 6 月 10 日

CPOL

7分钟阅读

viewsIcon

32851

downloadIcon

942

一种快速将组合框从枚举(可选资源字符串)中填充的方法。

引言

我一直认为,最优秀的程序员是懒惰的程序员,因为他们讨厌重复的代码,并希望找到自动化和减少代码量的途径。其中一个应用场景就是固定值的组合框,通常需要将其转换为代码中可以处理的值。大多数程序员会编写各种字符串列表和KeyValuePairs 来实现这一点,并且会反复进行。

EnumFunctions 模块及其附带的演示窗体旨在自动化此过程,并以一种独立于语言的方式进行。顺便说一下,代码和示例同时提供 VB.Net 和 C# 版本。

背景

我通常会尝试编写独立于语言和文化的应用和产品,以便能够翻译到不同的国家和语言。这意味着,实际上,我不会在数据库中存储人类可读的值(例如字符串),而是依赖于有限范围的字节值,并在代码中使用枚举。许多程序员只是存储下拉列表中选择的字符串,这很快,但使应用程序难以翻译,并可能因尝试进行字符串比较而出错。相比之下,使用枚举很简单,并且可以在编译时和运行时进行类型检查,以提高代码的质量和可靠性。本文将展示如何轻松地“国际化”您的应用程序,以支持组合框上的不同语言。

Using the Code

首先,从您想要处理的值集开始定义一个枚举。我通常使用 Byte 值,因为这对于将其存储在数据库中很方便,而超过 128 个项目的下拉列表则是一个糟糕的设计。您也可以使用 16 位和 32 位整数。仅仅使用枚举的名称和值的问题是,它们对于最终用户来说看起来不太好。一个像这样的列表

Public Enum PreferredContactMethodEnum As Byte
    HomePhone = 0
    WorkPhone = 1
    CellPhone = 2
    EMail = 3
    Mail = 4
    FAX = 5
    Other = 6
End Enum
public enum PreferredContactMethodEnum : byte
{
    HomePhone = 0,
    WorkPhone = 1,
    CellPhone = 2,
	EMail = 3,
	Mail = 4,
	FAX = 5,
	Other = 6
}

如果我们只是提取值和名称列表并将其分配给 ComboBox 的 Item 列表,那么在下拉列表中会产生相当难看的选择集。事实证明,微软已经考虑到了这一点,并提供了一种很好的机制,可以通过 DisplayAttribute 提供更多信息。要使用它,您需要向项目中添加对 System.ComponentModel.DataAnnotations 的引用。我们现在可以在每个项目前添加一个属性,提供一个漂亮的、人类可读的文本。

<Display(Name:="Work Phone")>
WorkPhone = 1
[Display(Name:="Work Phone")]
WorkPhone = 1,

这样在我们的下拉列表中看起来会好一些,但我们需要做一些额外的工作来提取显示文本,这将在文章后面讨论。然而,如果我们可以使列表独立于语言,那就更好了。DisplayAttribute 也支持这一点,因为它允许您指定本地资源文件和文件中的资源名称。

<Display(ResourceType:=GetType(My.Resources.Resources), name:="ContactMethodWorkPhone")>
WorkPhone = 1
[Display(ResourceType:=GetType(Properties.Resources), name:="ContactMethodWorkPhone")]
WorkPhone = 1,

我们现在可以将所有定义放在本地资源文件中,它会自动加载特定语言和文化的正确版本(前提是您已创建它们)。所以这是一个好的开始,我们现在可以在代码中创建人类可读的、独立于语言的枚举。下一步是将它们加载到 ComboBoxDataGridViewComboBoxColumn 中。本文附带的代码使用了我在 EnumFunctions 模块中编写的一组函数,并且包含在示例代码和演示中。 VB 和 C# 版本都包含在内。

在提供的窗体中,我有两个记录,一个是作为数据集表中的记录创建的,另一个是作为可能使用实体框架存储的对象。它们之间的主要区别在于,实体框架(及类似的数据库)允许我保存和恢复对象,然后这些对象可以拥有将数字字段转换为枚举的属性。数据集表定义仅限于使用各种大小的整数,因此我们需要翻译我们的枚举并使用匹配的数据类型,但仍然支持可空值。

我们的联系人记录看起来像这样,我们保持非常简单,MaritalStatus 是可选的,而 PreferredContactMethod 是必需的。

Public Class Contact
    Public Property Name As String
    Public Property MaritalStatus As MaritalStatusEnum?
    Public Property PreferredContactMethod As PreferredContactMethodEnum = PreferredContactMethodEnum.HomePhone
End Class        
public class Contact
{
    public string Name { get; set; }
    public MaritalStatus? { get; set; }
	public PreferredContactMethodEnum PreferredContactMethod = PreferredContactMethodEnum.HomePhone { get; set; }
}

我们的数据表记录仅使用字节来存储这两个枚举,其中 MaritalStatus 可为空,PreferredContactMethod 为非空。我们希望确保我们的枚举被翻译成正确的数据类型,否则我们的列表将不显示任何内容,或者选择的值在被选中后将不会“保留”。要使用数据表行的 Enum 函数,以下行包含在 Form_Load 中,并用于填充绑定到数据集的 MaritalStatus 字段的组合框。

EnumFunctions.PopulateDropdownlistFromEnum(Of Byte?, MaritalStatusEnum)(DSMaritalStatusComboBox, True)
EnumFunctions.PopulateDropdownlistFromEnum<byte?, MaritalStatusEnum>(DSMaritalStatusComboBox, True);

泛型参数告诉函数我们正在使用一个允许可选值的列表。MaritalStatusEnum 用于填充列表,而 AddEmptyValue 参数设置为 True 告诉函数在第一个位置添加一个额外的空行,值为 null。此函数读取枚举值,查找资源字符串,并使用映射到 ComboBoxDisplayMemberValueMemberKeyValuePair 对象来填充组合框。对于必填字段,情况更简单。

EnumFunctions.PopulateDropdownlistFromEnum(Of Byte, PreferredContactMethodEnum)(DSPreferredContactMethodComboBox)
EnumFunctions.PopulateDropdownlistFromEnum<byte, PreferredContactMethodEnum>(DSPreferredContactMethodComboBox);

在这种情况下,由于字段是必需的,不允许空值,因此不会生成可选行。使用对象时,情况甚至更简单,因为我们可以编写所有代码而无需指定类型。

EnumFunctions.PopulateDropdownlistFromEnum(Of MaritalStatusEnum?)(EFMaritalStatusComboBox, True)
EnumFunctions.PopulateDropdownlistFromEnum(Of PreferredContactMethodEnum)(EFPreferredContactMethodComboBox)
EnumFunctions.PopulateDropdownlistFromEnum<MaritalStatusEnum?>(EFMaritalStatusComboBox, True);
EnumFunctions.PopulateDropdownlistFromEnum<PreferredContactMethodEnum>(EFPreferredContactMethodComboBox);

我们可以将枚举直接传递给函数,它将使用该类型,因为我们的实体框架类属性已经使用相同的枚举进行了声明。我们的组合框必须将其 SelectedValue 属性绑定到数据字段,因为它期望一个值并使用该值从数组中选择适当的文本。

内部工作原理

该代码使用反射从指定的枚举类型中提取值和属性,将它们放入 KeyValuePairs 数组,然后使用该数组来初始化 CombBox 控件或 DataGridViewComboBoxColumnDisplayMember 设置为“Key”,ValueMember 设置为“Value”。

为了创建列表,我利用枚举的函数来获取列表中的值和名称。代码类似于以下内容,但枚举是硬编码的,而不是作为泛型类型传递的。

Dim values As System.Array = [Enum].GetValues(GetType(MaritalStatusEnum))
Dim list As New List(Of [Enum])(values.Length)
For Each value As [Enum] In values
    list.Add(value)
Next
System.Array values = Enum.GetValues(typeof(MaritalStatusEnum));
List <enum> list = new List>Enum>(values.Length);
foreach (Enum value in values) {
   	list.Add(value);
}

我们还可以使用类似的函数来获取名称列表。如上所述,我们使用微软的 DisplayAttribute 类来注释我们的枚举项。利用这些属性的关键是使用反射方法来提取每个枚举项中的属性。这通过一个公开的函数(以便您可以使用它)来完成,该函数接受一个枚举值并返回其 DisplayAttribute

Public Function GetEnumDisplayAttribute(Of E)(value As E) As DisplayAttribute
    Dim type As System.Type = value.[GetType]()
    If Not type.IsEnum Then
        Throw New ArgumentException([String].Format("Type '{0}' is not Enum", type))
    End If

    Dim members() As MemberInfo = type.GetMember(value.ToString())
    If members.Length = 0 Then
        Throw New ArgumentException([String].Format("Member '{0}' not found in type '{1}'", value, type.Name))
    End If

    Dim member As MemberInfo = members(0)
    Dim attributes() As Object = member.GetCustomAttributes(GetType(DisplayAttribute),False)
    If attributes.Length = 0 Then
        Return Nothing
    End If

    Dim attribute As DisplayAttribute = DirectCast(attributes(0), DisplayAttribute)
    Return attribute
End Function
public DisplayAttribute GetEnumDisplayAttribute<e>(E value)
{
    System.Type type = value.GetType();
    if (!type.IsEnum) {
	    throw new ArgumentException(String.Format("Type '{0}' is not Enum", type));
    }

    MemberInfo[] members = type.GetMember(value.ToString());
    if (members.Length == 0) {
	    throw new ArgumentException(String.Format("Member '{0}' not found in type '{1}'", value, type.Name));
    }

    MemberInfo member = members(0);
    object[] attributes = member.GetCustomAttributes(typeof(DisplayAttribute), false);
    if (attributes.Length == 0) {
	    return null;
    }

    DisplayAttribute attribute = (DisplayAttribute)attributes(0);
    return attribute;
}

上面代码中棘手的部分是如何从枚举中获取单个项。这可以通过使用反射函数 GetMember 来实现。另外,如果找不到 DisplayAttribute,我会返回 null,以便调用代码可以通过简单地使用枚举项名称来处理它。

DisplayAttribute 本身负责查找资源字符串的繁重工作。一旦我有了它,我只需要问它相关的名称。如果有资源,它将返回资源字符串,如果没有,它将只返回名称。这是通过以下方法完成的。

Dim da As DisplayAttribute = GetEnumDisplayAttribute(Of E)(EnumTypeValue)
If da Is Nothing Then
    Return [Enum].GetName(EnumTypeValue.[GetType](), EnumTypeValue)
Else
    Return da.GetDescription()
End If
DisplayAttribute da = GetEnumDisplayAttribute<e>(EnumTypeValue);
if (da == null) {
    return Enum.GetName(EnumTypeValue.GetType(), EnumTypeValue);
   } else {
    return da.GetDescription();
}

关注点

如果您将 EnumFunctions 模块放在单独的 DLL 中,您必须记住将 Enum 所在项目资源页的访问修饰符设置为 Public

研究和理解属性的工作原理让我对它们更加欣赏,并在我的项目中广泛使用它们。我使用属性来注释记录用于日志记录目的 - 属性告诉我的记录器哪些字段需要比较,哪些需要忽略。我使用它们来装饰我的窗体,在加载时向用户显示额外的信息。我使用实体框架/组件属性来添加验证,例如“必需”。我强烈建议大多数开发人员学习这些内容,并找到方法来利用现有的属性或根据需要创建新的属性。

我使用了 Telerik 的代码转换服务 http://converter.telerik.com/ 来完成从 VB 到 C# 的初步转换。它非常棒,完成了大约 95% 的工作。剩下的需要您自己弄清楚。

如果我遇到负责创建属性概念的 Microsoft 开发人员,我会请他们喝足够多的酒!

历史

V2.0 - 添加了对作为第二个泛型类型传递的可空枚举的检查。

© . All rights reserved.