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

通用属性窗口

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.43/5 (21投票s)

2005年12月8日

CPOL

8分钟阅读

viewsIcon

50809

downloadIcon

337

演示如何创建通用属性窗口的文章。

引言

程序员们总是在寻找让工作更轻松、更快捷的方法。至少我是这样。有一件事总是让我变得懒惰,那就是下面的场景。假设你的应用程序(大多数应用程序都是如此)需要从用户那里收集数据。你会怎么做?很可能,你会创建一个类,其属性将由用户信息填充。然后,你会创建一个派生自System.Windows.Form的类,并放置一个带有属性名称的标签和一个文本框,用于获取/显示数据。你还会添加一个“确定”和“取消”按钮,并在“确定”按钮的Click事件中验证输入的数据。很繁琐,不是吗?

我一直认为这种程序可以自动化。当C#和.NET出现时,我发现可以使用反射来实现这一点。然而,使用泛型的问题仍然存在。当C# 2.0出现时——当然,带着泛型——我的祈祷得到了回应。

在这篇文章中,我创建了一个通用的属性窗口。你只需要创建一个使用一些特性的类,一个通用的窗口就会出现,用于收集数据,而无需编写任何额外的代码。

背景

正如读者可能知道的,反射在运行时提供了关于特定类型的元数据信息。这解决了与库交互的许多问题。

这一切都始于类型。System.Type类提供了关于特定类型的信息,例如属性或方法的名称。它代表类型声明(类类型、接口类型、数组类型、泛型类型定义等)。Type类是反射功能的核心,也是访问元数据的主要方式。代表一个类型的Type对象是唯一的。

Type类提供了几个方法和属性,用于提供关于特定类型的信息。其中最重要的可能是GetMethodsGetPropertiesGetMethods返回当前类型的*所有* public方法。它返回一个MethodInfo对象的数组。MethodInfo类提供了关于当前类型的特定方法的信息,例如特性、名称、参数、返回类型,以及该方法是否是virtualprivatepublicstatic、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日
    • 对*文章*进行*了*小*修改*。
© . All rights reserved.