从枚举填充组合框
一种快速将组合框从枚举(可选资源字符串)中填充的方法。
引言
我一直认为,最优秀的程序员是懒惰的程序员,因为他们讨厌重复的代码,并希望找到自动化和减少代码量的途径。其中一个应用场景就是固定值的组合框,通常需要将其转换为代码中可以处理的值。大多数程序员会编写各种字符串列表和KeyValuePair
s 来实现这一点,并且会反复进行。
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,
我们现在可以将所有定义放在本地资源文件中,它会自动加载特定语言和文化的正确版本(前提是您已创建它们)。所以这是一个好的开始,我们现在可以在代码中创建人类可读的、独立于语言的枚举。下一步是将它们加载到 ComboBox
或 DataGridViewComboBoxColumn
中。本文附带的代码使用了我在 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。此函数读取枚举值,查找资源字符串,并使用映射到 ComboBox
的 DisplayMember
和 ValueMember
的 KeyValuePair
对象来填充组合框。对于必填字段,情况更简单。
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
属性绑定到数据字段,因为它期望一个值并使用该值从数组中选择适当的文本。
内部工作原理
该代码使用反射从指定的枚举类型中提取值和属性,将它们放入 KeyValuePair
s 数组,然后使用该数组来初始化 CombBox
控件或 DataGridViewComboBoxColumn
。DisplayMember
设置为“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 - 添加了对作为第二个泛型类型传递的可空枚举的检查。