|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionProgrammers 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 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. BackgroundAs 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 The
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 conceptLet 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 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. ImplementationAs 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 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. public class PropertyWindow<T> : Form
where T:new()
{ ... }
Notice the following. 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 private List<DisplayItem> m_lstItems;
Now, let us see the constructors. We have two options. A default constructor, which shall create a new 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 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 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 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 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 item.txtValue.Text =
item.propertyInfo.GetValue(m_tProperty, null).ToString();
The previous line takes the 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 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 The two methods that display the error message perform that: displaying the error message. They are declared as 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 private void PropertyWindow_Load(object sender, EventArgs e)
{
GatherPropertyInfo();
CreateLayout();
}
private void m_cmdOK_Click(object sender, EventArgs e)
{
if (CollectData())
{
Hide();
}
}
The method Using the classesNow the only thing left is to use the class. For displaying the 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 interestI 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||