Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Generic Property Window

0.00/5 (No votes)
16 Feb 2006 1  
Article that demostrates how to create generic property windows.

Introduction

Programmers are always looking for ways to make their work easier and faster. At least, I always do. One thing that always makes me lazy is the following scene. Suppose that your application (as most applications do) needs to collect data from the user. What do you do? Most likely, you create a class whose properties will be filled with user's information. Then you create a class derived from System.Windows.Form and put a label with the property's name, and a text box, from where you'll be retrieving/showing the data. You add an OK and CANCEL button, and within the OK button's Clicked event, you validate the entered data. Tedious, isn't it?

I always thought that this procedure could be automated. When C# and .NET came into scene, I found that this could be possible using reflection. Yet it remained the problem of using generic types. When C# 2.0 appeared - with generics, of course - my prayers where answered.

In this article, I create a generic window property. All you've got to do is create a class that uses a few attributes, and a generic window shall appear to collect the data, without writing a single extra line of code.

Background

As the reader probably knows, reflection provides metadata information at runtime, about a particular type. This came to solve many problems when interacting with libraries.

It all begins with the type. The class System.Type provides information about a particular type, such as the name of the properties or methods. It represents type declarations (class types, interface types, array types, generic type definitions, etc.). Type class is the root of the reflection functionality, and is the primary way to access metadata. A Type object that represents a type is unique.

The Type class provides several methods and properties that provide information about a particular type. Probably, the most important methods are GetMethods and GetProperties. GetMethods returns all the public methods of the current type. It returns an array of MethodInfo objects. The MethodInfo class provides information about a particular method of the current type, such as the attributes, its name, the parameters, the return type, whether the method is virtual, private, public, static, final or generic.

GetProperties returns all the public properties of the current type. It returns an array of PropertyInfo objects. The PropertyInfo class provides information about a particular property of the current type, such as the attributes, its name, the get/set value, and whether it is for reading, writing or both.

In this brief introduction, we give only a glimpse of what reflection can do. Yet it is enough for our purposes, and hopefully the reader realized - if you haven't yet noticed- the importance and power of reflection.

The concept

Let us land the ideas. We want a class that given a particular type it must be able to create a dialog window, which will contain labels and text boxes where the information can be displayed to or retrieved from.

For such purposes, we need to access the metadata of the given type. We achieve this with reflection, of course. Indeed, by getting the type's information, we can get all the public properties. For each property, we create a label and a textbox, which will be displayed in the dialog window. Two buttons are provided: an OK and Cancel buttons. When OK is clicked, it will be validated, according to each property type, and the entered value belongs to the property's type. For example, if the property is a long, and the user entered a string, an exception will be thrown.

If one of the property's name is, say, Name, then the label will be displayed as "Name". However, when using long names - say, FullName - It is incorrect to display the label as "FullName". Moreover, it is often desirable to display an alternative name than the property's name itself. For example, rather than display "Name", you ought to display "Employee Name". Another issue is to determine which fields must be mandatory. In our previous example, Name might be mandatory, whereas Age property isn't.

Both problems might be solved by using an attribute class, so that at compile time it is decided the name of the property to be displayed, as well as whether it is a required field or not.

Implementation

As an example, let us think of a class that we want to be displayed in the window. Here's an example:

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; }
    }
}

First, we need to create a class attribute for solving the problems of the display name and mandatory fields. Here's the class:

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; }
    }
}

The class inherits from System.Attribute class. Notice the m_bChangeDisplayName member. This one will determine whether there is a qualified name, or simply use the property's name itself. With this, we can now modify our Employee class as shown below:

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; }
    }
}

Now, we have to implement the main property window class. Yet, we first need the following structure:

public struct DisplayItem
{
    public PropertyInfo propertyInfo;
    public bool bRequired;
    public string strDisplayName;
    public Label lblName;
    public TextBox txtValue;
}

This structure's purpose is to gather all that we need for creating the display. propertyInfo member holds a reference to a PropertyInfo object, obtained with reflection. bRequired and strDisplayName are obtained from the attribute of the class. Finally, lblName and txtValue holds a reference to the controls being displayed in the window. Now, let us see the declaration of the property window class:

public class PropertyWindow<T> : Form
    where T:new()
{ ... }

Notice the following. PropertyWindow inherits from System.Windows.Form, because it is a window after all. We use the template parameter T for holding the type whose public properties shall be displayed. There is one constrain, to indicate that the type must have a default constructor. This is needed, because we might want to create a new instance of T. Indeed, we have to hold this instance somewhere, so we create a private member:

private T m_tProperty;

We also need two variables to hold the OK and Cancel buttons. Here they are:

private Button m_cmdOK;
private Button m_cmdCancel;

The following member is for saving each DisplayItem:

private List<DisplayItem> m_lstItems;

Now, let us see the constructors. We have two options. A default constructor, which shall create a new T instance, and a constructor with a T parameter, so we can also use objects already created (this is useful when showing already gathered data):

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();
}

Both constructors call InitializeComponent, a function that - surprise surprise - initializes all the window's components. This was created by the window editor:

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);
}

We need a property that will let us get and set the instanced T object. Here we go:

public T Property
{
    get { return m_tProperty; }
    set { m_tProperty = value; }
}

The process goes as follows. When the dialog is loaded, we must first gather the information about the type's argument being showed, as well as the metadata information from the PropertyWindowAttribute attributes. Let's take a look:

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);
            }
        }
    }
}

As we can see, we use reflection to get all the properties from the T type. Then, we iterate over each property to get its custom attribute, and hence its display name and whether it is required or not. Finally, we add each property into a m_lstItems list. After this, we call CreateLayout method, whose purpose is to create the label and the textbox for each element. Here's the code:

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;
    }
}

As you can see, for each element in m_lstItems, a label and textbox are created. There is a difference of 26 pixels between each element. An important line is the following:

item.txtValue.Text = 
    item.propertyInfo.GetValue(m_tProperty, null).ToString();

The previous line takes the PropertyInfo instance and invokes the get property and converts it into a string, so it can be displayed within the textbox. Again, we're using reflection here.

Now, we need a function that gathers data from the textboxes, to do the validation (i.e. required fields and formatting issues). This one is 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;
}

The method returns true if everything was alright, false otherwise. The method iterates over each DisplayItem element. If the current item is required and no data was entered, it displays an error message by calling ShowRequiredErrorMessage. Else, it gathers the data, converts it to the property's type (if an invalid format is detected - i.e. a string was entered instead of an integer - a message is shown to the user by calling ShowFormatErrorMessage) and sets the value to the property (using reflection, again).

The two methods that display the error message perform that: displaying the error message. They are declared as virtual, so if you don't like the message... create a class, inherit from PropertyWindow and override the methods. The parameter will give you all the information that you need. Here is the code for both the methods:

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);
}

Finally, we just have to add a method that receives the Form.Load event, and a method that receives the m_cmdOK.Click event. Here they are:

private void PropertyWindow_Load(object sender, EventArgs e)
{
    GatherPropertyInfo();
    CreateLayout();
}

private void m_cmdOK_Click(object sender, EventArgs e)
{
    if (CollectData()) 
    {
        Hide();
    }
}

The method PropertyWindow_Load calls GatherPropertyInfo and CreateLayout. The method m_cmdOK_Click calls CollectData, and if it succeeded, it will hide the dialog.

Using the classes

Now the only thing left is to use the class. For displaying the Employee, we only have to write this code:

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)
);

Points of interest

I hope you're pleased with this, and I hope you will find it useful. Yet, there is space for improvements. For example, rather than using a string for the display name of each property, you could use an integer code, and load a string from the resource with such code. In this way, there won't be problem with translations. Another improvement could be to add validations for read-only and write-only properties. But I think that for most purposes, it fills the requirements. Enjoy!

History

  • 8th Dec, 2005
    • Main release of the article.
  • 15th Feb, 2006
    • Minor changes to the article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here