通用属性窗口






3.43/5 (21投票s)
演示如何创建通用属性窗口的文章。
引言
程序员们总是在寻找让工作更轻松、更快捷的方法。至少我是这样。有一件事总是让我变得懒惰,那就是下面的场景。假设你的应用程序(大多数应用程序都是如此)需要从用户那里收集数据。你会怎么做?很可能,你会创建一个类,其属性将由用户信息填充。然后,你会创建一个派生自System.Windows.Form
的类,并放置一个带有属性名称的标签和一个文本框,用于获取/显示数据。你还会添加一个“确定”和“取消”按钮,并在“确定”按钮的Click事件中验证输入的数据。很繁琐,不是吗?
我一直认为这种程序可以自动化。当C#和.NET出现时,我发现可以使用反射来实现这一点。然而,使用泛型的问题仍然存在。当C# 2.0出现时——当然,带着泛型——我的祈祷得到了回应。
在这篇文章中,我创建了一个通用的属性窗口。你只需要创建一个使用一些特性的类,一个通用的窗口就会出现,用于收集数据,而无需编写任何额外的代码。
背景
正如读者可能知道的,反射在运行时提供了关于特定类型的元数据信息。这解决了与库交互的许多问题。
这一切都始于类型。System.Type
类提供了关于特定类型的信息,例如属性或方法的名称。它代表类型声明(类类型、接口类型、数组类型、泛型类型定义等)。Type类是反射功能的核心,也是访问元数据的主要方式。代表一个类型的Type
对象是唯一的。
Type
类提供了几个方法和属性,用于提供关于特定类型的信息。其中最重要的可能是GetMethods
和GetProperties
。GetMethods
返回当前类型的*所有* public
方法。它返回一个MethodInfo
对象的数组。MethodInfo
类提供了关于当前类型的特定方法的信息,例如特性、名称、参数、返回类型,以及该方法是否是virtual
、private
、public
、static
、final或泛型。
GetProperties
返回当前类型的*所有* public
属性。它返回一个PropertyInfo
对象的数组。PropertyInfo
类提供了关于当前类型的特定属性的信息,例如特性、名称、get/set值,以及它是仅读、仅写还是读写。
在这篇简短的介绍中,我们只展示了反射功能的一小部分。但这足以满足我们的目的,希望读者能够意识到——如果你还没有注意到的话——反射的重要性和强大功能。
概念
让我们落地这些想法。我们想要一个类,它能够根据给定的类型创建一个对话窗口,该窗口包含标签和文本框,用于显示或检索信息。
为此,我们需要访问给定类型的元数据。当然,我们通过反射来实现这一点。事实上,通过获取类型的*信息,我们可以得到所有* public
*属性。对于每个属性,我们创建一个标签和一个文本框,它们将显示在对话窗口中。提供了两个按钮:“确定”和“取消”。当点击“确定”时,它将根据每个属性的类型进行验证,并且输入的*值*属于该属性的*类型*。例如,如果该属性是*一个* long
*,而用户输入了一个* string
*,*将会抛出一个*异常。
如果某个属性的*名称*是*例如* Name,那么*标签*将显示为“Name”。但是,当使用*长*名称*时*——*例如* FullName——*显示*标签*为*“FullName”*是*不*正确的*。*此外,*显示*一个*不同*于*属性*本身*的*名称*是很*常见*的*。*例如*,*与其*显示*“Name”,*不如*显示*“Employee Name”。*另一个*问题*是*确定*哪些*字段*必须*是*必填*项。*在我们*之前的*示例*中,*Name*可能*是*必填*项,*而* Age*属性*不是*。
这两个问题都可以通过使用*特性*类来*解决*,*以便*在*编译*时*决定*要*显示*的*属性*的*名称*,*以及*它*是否*是*必需*字段*。
实现
例如,让我们*考虑*一个*我们*想要*在*窗口*中*显示*的*类。*以下*是*一个*示例*:
public class Employee
{
private string m_strName;
private string m_strLastName;
private int m_iAge;
public Employee()
{
m_strName = "";
m_strLastName = "";
m_iAge = 0;
}
public string Name
{
get { return m_strName; }
set { m_strName = value; }
}
public string LastName
{
get { return m_strLastName; }
set { m_strLastName = value; }
}
public int Age
{
get { return m_iAge; }
set { m_iAge = value; }
}
}
首先,我们需要创建一个*特性*类来*解决*显示*名称*和*必填*字段*的问题*。*这是*该*类*:
public class PropertyWindowAttribute : Attribute
{
private string m_strDisplayName;
private bool m_bRequiredField;
private bool m_bChangeDisplayName;
public PropertyWindowAttribute()
{
m_strDisplayName = "";
m_bChangeDisplayName = false;
m_bRequiredField = false;
}
public string DisplayName
{
get { return m_strDisplayName; }
set
{
m_bChangeDisplayName = value.Length != 0;
m_strDisplayName = value;
}
}
public bool Required
{
get { return m_bRequiredField; }
set { m_bRequiredField = value; }
}
}
该类*继承*自*System.Attribute
*类*。*注意* `m_bChangeDisplayName`*成员*。*这个*成员*将*决定*是*否*存在*一个*限定*名称*,*或者*简单*地*使用*属性*本身*的*名称*。*有了*这个*,*我们*现在*可以*修改*我们的*Employee
*类*,*如下*所示*:
public class Employee
{
private string m_strName;
private string m_strLastName;
private int m_iAge;
public Employee()
{
m_strName = "";
m_strLastName = "";
m_iAge = 0;
}
[PropertyWindowAttribute(DisplayName="First Name",
Required=true)]
public string Name
{
get { return m_strName; }
set { m_strName = value; }
}
[PropertyWindowAttribute(DisplayName="Last Name",
Required=true)]
public string LastName
{
get { return m_strLastName; }
set { m_strLastName = value; }
}
[PropertyWindowAttribute(DisplayName="Age",
Required=false)]
public int Age
{
get { return m_iAge; }
set { m_iAge = value; }
}
}
现在,*我们*必须*实现*主*属性*窗口*类*。*但是*,*我们*首先*需要*以下*结构*:
public struct DisplayItem
{
public PropertyInfo propertyInfo;
public bool bRequired;
public string strDisplayName;
public Label lblName;
public TextBox txtValue;
}
这个*结构*的*目的*是*收集*我们*创建*显示*所需*的*一切*。* `propertyInfo`*成员*持*有*一个*PropertyInfo
*对象的*引用*,*该*对象*是通过*反射*获取*的*。* `bRequired`*和* `strDisplayName`*是从*该*类*的*特性*中*获取*的*。*最后*,* `lblName`*和* `txtValue`*持有*对*窗口*中*显示*的*控件*的*引用*。*现在*,*让我们*看看*属性*窗口*类*的*声明*:
public class PropertyWindow<T> : Form
where T:new()
{ ... }
请*注意*以下*几点*。* `PropertyWindow`*继承*自* `System.Windows.Form`*,*因为*它*毕竟*是一个*窗口*。*我们*使用*模板*参数* `T`*来*持有*其* public
*属性*将被*显示*的*类型*。*有一个*约束*,*用于*指示*该*类型*必须*有一个*默认*构造函数*。*这是*必需*的*,*因为*我们*可能*想*创建*一个* `T`*的新*实例*。*事实上*,*我们*必须* somewhere*持有*这个*实例*,*所以*我们*创建*了一个* private
*成员*:
private T m_tProperty;
我们*还*需要*两个*变量*来*保存*OK*和*Cancel*按钮*。*它们*是*:
private Button m_cmdOK;
private Button m_cmdCancel;
以下*成员*用于*保存*每个* `DisplayItem`*:
private List<DisplayItem> m_lstItems;
现在*,*让我们*看看*构造函数*。*我们*有*两个*选项*。*一个*默认*构造函数*,*它*将*创建*一个*新*的* `T`*实例*,*以及*一个*带*有* `T`*参数*的*构造函数*,*所以*我们*也可以*使用*已经*创建*的*对象*(*这*在*显示*已*收集*数据*时*很有*用*):
public PropertyWindow()
{
m_tProperty = new T();
m_lstItems = new List<DisplayItem>();
InitializeComponent();
}
public PropertyWindow(T tProperty)
{
m_tProperty = tProperty;
m_lstItems = new List<DisplayItem>();
InitializeComponent();
}
这两个*构造函数*都*调用* `InitializeComponent`*,*一个*函数*——*出乎*意料*——*初始化*了*所有*的*窗口*组件*。*这个*是由*窗口*编辑器*创建*的*:
private void InitializeComponent()
{
this.m_cmdOK = new System.Windows.Forms.Button();
this.m_cmdCancel = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// m_cmdOK
//
this.m_cmdOK.Location = new System.Drawing.Point(233, 12);
this.m_cmdOK.Name = "m_cmdOK";
this.m_cmdOK.Size = new System.Drawing.Size(75, 23);
this.m_cmdOK.TabIndex = 0;
this.m_cmdOK.Text = "OK";
this.m_cmdOK.UseVisualStyleBackColor = true;
this.m_cmdOK.Click +=
new System.EventHandler(this.m_cmdOK_Click);
//
// m_cmdCancel
//
this.m_cmdCancel.DialogResult =
System.Windows.Forms.DialogResult.Cancel;
this.m_cmdCancel.Location = new System.Drawing.Point(233, 41);
this.m_cmdCancel.Name = "m_cmdCancel";
this.m_cmdCancel.Size = new System.Drawing.Size(75, 23);
this.m_cmdCancel.TabIndex = 1;
this.m_cmdCancel.Text = "Cancel";
this.m_cmdCancel.UseVisualStyleBackColor = true;
//
// PropertyWindow
//
this.AcceptButton = this.m_cmdOK;
this.CancelButton = this.m_cmdCancel;
this.ClientSize = new System.Drawing.Size(320, 266);
this.Controls.Add(this.m_cmdCancel);
this.Controls.Add(this.m_cmdOK);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "PropertyWindow";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition =
System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Properties";
this.Load += new System.EventHandler(this.PropertyWindow_Load);
this.ResumeLayout(false);
}
我们需要*一个*属性*来*允许*我们*获取*和*设置*被*实例*化*的* `T`*对象*。*完成*:
public T Property
{
get { return m_tProperty; }
set { m_tProperty = value; }
}
过程*如下*。*当*对话*框*加载*时*,*我们*必须*首先*收集*要*显示*的*参数*类型*的*信息*,*以及*来自* `PropertyWindowAttribute`*特性的*元数据*信息*。*让我们*看*看*:
private void GatherPropertyInfo()
{
Type typeProp;
m_lstItems.Clear();
typeProp = m_tProperty.GetType();
foreach (PropertyInfo propInfo in typeProp.GetProperties())
{
foreach (
Attribute attribute in Attribute.GetCustomAttributes(propInfo))
{
if (attribute.GetType() == typeof(PropertyWindowAttribute))
{
PropertyWindowAttribute propWinAttr;
DisplayItem item;
propWinAttr = (PropertyWindowAttribute)attribute;
item = new DisplayItem();
item.propertyInfo = propInfo;
item.bRequired = propWinAttr.Required;
item.strDisplayName = propWinAttr.DisplayName;
m_lstItems.Add(item);
}
}
}
}
如*我们*所*见*,*我们*使用*反射*来*获取* `T`*类型*的*所有*属性*。*然后*,*我们*遍历*每个*属性*以*获取*其*自定义*特性*,*从而*获取*其*显示*名称*以及*它*是否*是*必填*项*。*最后*,*我们将*每个*属性*添加*到*一个* `m_lstItems`*列表中*。*在此*之后*,*我们*调用* `CreateLayout`*方法*,*其*目的*是*为*每个*元素*创建*标签*和*文本框*。*这是*代码*:
private void CreateLayout()
{
Point ptLabel;
Point ptTextbox;
Size szLabel;
Size szTextbox;
// startup
ptLabel = new Point(13, 12);
ptTextbox = new Point(102, 9);
szLabel = new Size(85, 13);
szTextbox = new Size(125, 20);
for (int i = 0; i < m_lstItems.Count; i++)
{
DisplayItem item = m_lstItems[i];
// create the label
item.lblName = new Label();
item.lblName.Text = item.strDisplayName;
item.lblName.Location = ptLabel;
item.lblName.Size = szLabel;
// create the textbox
item.txtValue = new TextBox();
item.txtValue.Text =
item.propertyInfo.GetValue(m_tProperty, null).ToString();
item.txtValue.Location = ptTextbox;
item.txtValue.Size = szTextbox;
// add it to the window
this.Controls.Add(item.lblName);
this.Controls.Add(item.txtValue);
// update for the new location
ptLabel.Y += 26;
ptTextbox.Y += 26;
m_lstItems[i] = item;
}
}
如*你*所*见*,*对于* `m_lstItems`*中的*每个*元素*,*都*创建*了一个*标签*和*一个*文本框*。*每个*元素*之间*有*26*像素*的*间距*。*有一*行*很重要*:
item.txtValue.Text =
item.propertyInfo.GetValue(m_tProperty, null).ToString();
上一行*获取* `PropertyInfo`*实例*并*调用*get*属性*,*将其*转换*为*字符串*,*以便*在*文本框*中*显示*。*再次*,*我们*在这里*使用*反射*。
现在*,*我们需要*一个*函数*来*从*文本框*收集*数据*,*进行*验证*(*即*必填*字段*和*格式*问题*)。*这个*是* `CollectData`*:
private bool CollectData()
{
bool bRet = true;
foreach (DisplayItem item in m_lstItems)
{
Type propType = item.propertyInfo.PropertyType;
if (item.bRequired && item.txtValue.Text.Length == 0)
{
ShowRequiredErrorMessage(item);
bRet = false;
break;
}
try
{
if (propType == typeof(int))
{
item.propertyInfo.SetValue(m_tProperty,
int.Parse(item.txtValue.Text), null);
}
else if (propType == typeof(long))
{
item.propertyInfo.SetValue(m_tProperty,
long.Parse(item.txtValue.Text), null);
}
else if (propType == typeof(short))
{
item.propertyInfo.SetValue(m_tProperty,
short.Parse(item.txtValue.Text), null);
}
else if (propType == typeof(float))
{
item.propertyInfo.SetValue(m_tProperty,
float.Parse(item.txtValue.Text), null);
}
else if (propType == typeof(double))
{
item.propertyInfo.SetValue(m_tProperty,
double.Parse(item.txtValue.Text), null);
}
else if (propType == typeof(decimal))
{
item.propertyInfo.SetValue(m_tProperty,
int.Parse(item.txtValue.Text), null);
}
else if (propType == typeof(string))
{
item.propertyInfo.SetValue(m_tProperty,
item.txtValue.Text, null);
}
else if (propType == typeof(DateTime))
{
item.propertyInfo.SetValue(m_tProperty,
DateTime.Parse(item.txtValue.Text), null);
}
else
{
item.propertyInfo.SetValue(m_tProperty,
(object)item.txtValue.Text, null);
}
}
catch (FormatException)
{
ShowFormatErrorMessage(item);
bRet = false;
break;
}
}
return bRet;
}
该*方法*返回* true
*如果*一切*都*没*问题*,*否则*返回* false
*。*该*方法*遍历*每个* `DisplayItem`*元素*。*如果*当前*项*是*必填*项*且*没有*输入*数据*,*它*会*通过*调用* `ShowRequiredErrorMessage`*显示*一个*错误*消息*。*否则*,*它*会*收集*数据*,*将其*转换*为*属性*的*类型*(*如果*检测*到*无效*格式*——*即*输入*了*字符串*而不是*整数*——*则*通过*调用* `ShowFormatErrorMessage`*向*用户*显示*消息*)*并*将*值*设置*为*属性*(*再次*使用*反射*)。
显示*错误*消息*的*两个*方法*执行*此*操作*:*显示*错误*消息*。*它们*被*声明*为* virtual
*,*所以*如果你*不喜欢*这个*消息*……*创建一个*类*,*继承*自* `PropertyWindow`*并*重写*这些*方法*。*参数*将*为你*提供*所有*你*需要*的*信息*。*这是*两个*方法*的代码*:
protected virtual void ShowFormatErrorMessage(DisplayItem itemError)
{
string strMsg = string.Format(
"The following data was in invalid format.\n" +
"Field: {0}\n" +
"Expected type: {1}\n",
itemError.strDisplayName,
itemError.propertyInfo.PropertyType.Name
);
MessageBox.Show(this, strMsg, "Invalid data",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
protected virtual void ShowRequiredErrorMessage(
DisplayItem itemError)
{
string strMsg = string.Format(
"The field {0} is required.",
itemError.strDisplayName
);
MessageBox.Show(this, strMsg, "Invalid data",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
最后*,*我们*只需*添加*一个*接收* `Form.Load`*事件*的*方法*,*以及*一个*接收* `m_cmdOK.Click`*事件*的*方法*。*它们*是*:
private void PropertyWindow_Load(object sender, EventArgs e)
{
GatherPropertyInfo();
CreateLayout();
}
private void m_cmdOK_Click(object sender, EventArgs e)
{
if (CollectData())
{
Hide();
}
}
方法* `PropertyWindow_Load`*调用* `GatherPropertyInfo`*和* `CreateLayout`*。*方法* `m_cmdOK_Click`*调用* `CollectData`*,*如果*成功*,*它*将*隐藏*对话*框*。
使用这些类
现在*唯一*剩下*的*就是*使用*这个*类*。*要*显示* `Employee`*,*我们*只需*编写*以下*代码*:
Employee emp = new Employee();
emp.Name = "Kith";
emp.LastName = "Kanan";
emp.Age = 22;
PropertyWindow<Employee> wndProp =
new PropertyWindow<Employee>(emp);
wndProp.ShowDialog(this);
MessageBox.Show(string.Format(
"After modifying Employee:\nName: {0}\nLastName: {1}\nAge: {2}",
emp.Name, emp.LastName, emp.Age)
);
关注点
我*希望*你*对*此*感到*满意*,*我*也*希望*你*觉得*它*有用*。*然而*,*还有*改进*的*空间*。*例如*,*与其*使用*字符串*作为*每个*属性*的*显示*名称*,*不如*使用*整数*代码*,*然后*从*资源*中*加载*具有*该*代码*的*字符串*。*这样*,*翻译*就不*会有*问题*了*。*另*一个*改进*是*添加*只读*和*只写*属性*的*验证*。*但是*我认为*对于*大多数*目的*来说*,*它*已经*满足*了*要求*。*尽情*享*受*吧*!
历史
- 2005年12月8日
- 文章*主*发布*。
- 2006年2月15日
- 对*文章*进行*了*小*修改*。