Introduction
This project is to programmatically discover data members from custom message classes and display the value of each data member. Suppose I am writing an application to receive data messages from a remote connection and display the value of each data field within each message. With the assumption that the remote connection does not support object serialization. To do so I need to define a class for each message type and its data members as well as a windows form for each. To generalize the problem I can write a generic form that programatically discovers all data fields of a message class and programmatically construct the form controls to display the value of each data field.
Background
Programmatically discovery of message fields eliminiates much hand editting of windows form. Imagine there are hundreds of different messages and you need one windows form for each message it will take a long time to finish up.
To simplify the problem the remote connection is omitted. Only one message and one form is defined in this project. Once a program paradigm is established adding more messages and forms will be easy done by following the same concept.
Project DynamicForm
Create a new project from Visual C# namely DynamicForm. Select Windows Form Project.
MessageFieldAttribute
Define a custome attribute "MessageFieldAttribute
" and use it for data fields that you want to display. The MessageFieldAttribute
only has one property "Description" however it can be expanded to include other properties to let you have informaiton about the data field such as data format and size. For simplicity of this project only description property is provided.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DynamicForm.Messages
{
public sealed class MessageFieldAttr : System.Attribute
{
public string Description { get; set; }
public MessageFieldAttr()
{
Description = "Message Field Description";
}
}
}
Messages
Add a class ClientDataMessage1
that has 2 data fields to be displayed namely "DataField1
" and "DataField2
" and non displayable members "noneDisplayData
". The displable data field has custom attribute "MessageFieldAttr
". The intent here is to only show DataField1
and DataField2
on the display. noneDisplayData
is meant for book keeping purpose of ClientDataMessage1
. It is declared here for the sake of non displayable demonstration purpose.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DynamicForm.Messages
{
public class ClientDataMessage1
{
[MessageFieldAttr(Description = "DataField1")]
public uint DataField1;
[MessageFieldAttr(Description = "DataField2")]
public double DataField2;
private int noneDisplayData;
public string[] ToString()
{
string[] s = new string[2];
s[0] = DataField1.ToString();
s[1] = DataField2.ToString();
return s;
}
}
}
member method ToString
return the values of displayable data fields in the order they are declared in ClientDataMessage1
class.
Add MessageDiscovery
class to the project, MessageDiscovery
class takes in the class name of the message of which data fields will be displayed in a form. MessageDiscovery
retreives the Type of this message then retrieves any data fields that has MessageFieldAttr
custom attribute. It stores the data fields in a List<MemberInfo>. The Module name is "Dynamicform.exe" which is the name of the project (DynamicForm).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace DynamicForm
{
class MessageDiscovery
{
public List<MemberInfo> DataFields { get; set; }
public MessageDiscovery(string className)
{
DataFields = new List<MemberInfo>();
Module myModule = typeof(MessageDiscovery).Assembly.GetModule("DynamicForm.exe");
Type[] myTypes = myModule.FindTypes(Module.FilterTypeNameIgnoreCase, className);
foreach (Type type in myTypes)
{
MemberInfo[] members = type.GetMembers();
foreach (MemberInfo member in members)
{
object[] attrs = member.GetCustomAttributes(false);
foreach (object obj in attrs)
{
if (string.Compare(obj.ToString(), "DynamicForm.Messages.MessageFieldAttr") == 0)
{
DataFields.Add(member);
}
}
}
}
}
}
}
Using the Code
Add a form to the project namely Form1. The method constructDataFields
creates columns of labels. Column 1 shows data field name and column 2 (dataFieldLabels) shows data field value.
The method ShowData
takes in the message object and set the text of column 2 labels (dataFieldLabels)
to the values of data fields from the message object. To test it out I instantiate a ClientDataMessage1
in the constructor Form1()
and call ShowData(msg) to demonstrate that it works.
This form is generic enough that when the message ClientDataMessage1 changes the form source does not need to be updated. it will still work.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
namespace DynamicForm
{
public partial class Form1 : Form
{
private Dictionary<string,Label> dataFieldLabels;
private MessageDiscovery messageD = new MessageDiscovery("ClientDataMessage1");
public Form1()
{
InitializeComponent();
constructDataFields();
Messages.ClientDataMessage1 msg = new Messages.ClientDataMessage1();
msg.DataField1 = 1;
msg.DataField2 = 2;
ShowData(msg);
}
private void constructDataFields()
{
Label[] l = new Label[messageD.DataFields.Count];
dataFieldLabels = new Dictionary<string, Label>();
Console.WriteLine(messageD.DataFields.Count);
for (int i = 0; i < messageD.DataFields.Count; i++)
{
l[i] = new Label();
l[i].Text = messageD.DataFields[i].Name;
l[i].Size = new System.Drawing.Size(100, 25);
l[i].Location = new System.Drawing.Point(25, 25 + i * 25 + 5);
Console.WriteLine("new Label {0} {1} ", messageD.DataFields[i].Name, l[i].Text);
dataFieldLabels[messageD.DataFields[i].Name] = new Label();
dataFieldLabels[messageD.DataFields[i].Name].Size = new System.Drawing.Size(100, 25);
dataFieldLabels[messageD.DataFields[i].Name].Location = new System.Drawing.Point(200, 25 + i * 25 + 5);
dataFieldLabels[messageD.DataFields[i].Name].Text = "N/A";
this.Controls.Add(dataFieldLabels[messageD.DataFields[i].Name]);
}
this.Controls.AddRange(l);
}
public void ShowData(Messages.ClientDataMessage1 msg)
{
string[] values = msg.ToString();
for (int i = 0; i < values.Length; i++)
{
dataFieldLabels.ElementAt(i).Value.Text = values[i];
}
}
}
}
Points of Interest
Imagine that there are 20 data fields in each message and you dont have to manually edit the form to display each data field? how about there are 100 messages each has 20 or more data fields? one can further generalize the Form1 such that it takes in a parameter "class name" and construct the form according the class of interest? that is up for my next challenge or yours. Happy coding.
Revision History
Added links to source zip files