Preface
This article demonstrates a first version of a DataTable Generator. This generator uses information found in custom attributes on any class, and transforms this class, or several instances of it, to a System.Data.DataTable
.
Problem
In any modern application, you deal with a lot of custom classes, like Employee
, Article
, or Manager
- each of them having their own properties, member variables, and methods.
Often, you need a quick way to capture the current values of the class at once, view the current data contained in them, or simply transform all values to an XML file.
Luckily, there is class that is able to do all of this already: the DataTable
. A DataTable
can contain thousands of rows, dozens of columns, can be easily viewed by using a grid, and finally, using XmlSerializer
, can be turned easily to an XML file.
The only question is how a custom class can transformed to a DataTable
? Well, this is exactly where Xteq.Data.DataTableGenerator
jumps in.
Xteq.Data.DataTableGenerator
The first solution that might come to mind is to define a custom interface, and that each class that should be transmogrified to a DataTable
must implement it. Within this method, it should define which columns should be added, which data types they have, and finally, fill the data table with rows.
However, for the project I needed this generator, this would have been too bloated, and also I simply didn't wanted to add another piece of code for each class.
What I wanted to have was something like this:
public class CustomClass1
{
[DataTableInclude]
private string _name = string.Empty;
}
That way, I only need to apply the custom attribute [DataTableInclude]
to the members that should be included in the DataTable
, and the DataTableGenerator
(DTG in short) should do the rest.
Creating this custom attribute was the easy part. Just create a new class, derive it from System.Attribute
, and apply the AttributeUsage
on the members this attribute can be applied.
As we want to be able to transform properties, member variables, and functions, we add the following on our custom class DataTableIncludeAttribute
:
[AttributeUsage(AttributeTargets.Property |
AttributeTargets.Field | AttributeTargets.Method)]
public sealed class DataTableIncludeAttribute : System.Attribute
....
Reflect and create data columns
With this Attribute
class in place, we can start to create the DTG. The first thing we need to do is to define the columns on our DataTable
. Only with columns in place, we can add rows where the data will be shown.
To create a DataColumn
, we basically only need to know two things: the name of the column, and the data type.
To retrieve this information, we will use Reflection. Reflection is the method in the .NET Framework to retrieve information about an object (actually, only the type of the object) and use it. For our purpose, we need to know on which parts of a given class (properties, members, or methods) our custom attribute has been applied.
Since DTG will use the retrieved information to define the columns, the function is called DefineColumns
and takes a Type
:
public void DefineColumns(Type type)
To make the life for all lazy programmers easier, we also define a second method that can be used on an existing instance:
public void DefineColumns(object obj)
{
Type type = obj.GetType();
DefineColumns(type);
}
Within DefineColumns()
, we need to retrieve all methods, fields, and properties from the given type, and check if on any of these our attribute has been applied. To do this, we retrieve the fields using type.GetFields()
, the properties using type.GetProperties()
, and the methods with type.GetMethods()
.
Each of them return an array of FieldInfo
, PropertyInfo
, or MethodInfo
that we pass to EnumerateMembers()
. EnumerateMembers()
uses the helper function GetAttribute()
to retrieve the DataTableInclude
attribute. If the attribute exists, it extracts it and passes the MemberInfo
(the field, property, or method we are currently checking) and the DataTableInclude
attribute to DefineColumn()
.
Finally, DefineColumn()
DefineColumn()
will first check if the given DataTableInclude
attribute contains a name. If not, it will use the name of the MemberInfo
(the name of the field, property, or method) as the name for the column.
Next, it needs to know the data type that this member encapsulates. Depending on which member it is working on, this information can be found at:
- For a property:
PropertyType
- For a field:
FieldType
- For a method:
ReturnType
This type (e.g., string
, int64
, long
etc.) will then be assigned to the DataType
property of the data column.
It then adds this newly created column as well as the current MemberInfo
to the internal Dictionary
collection (_list
) so we can easily access it later.
Sorting
One thing that was requested 10 minutes after other people started using DTG was "How do I control the order of the columns?". My first reaction was to say that this sorting should be done in the front end. But as DTG should make lives easier, it should also be able to control the order in which the columns appear.
This sorting is done through a temporary list (_sort_list
) that is used by DefineColumns()
and DefineColumn()
. The list stores the created data columns as well as the DataTableInclude
attribute. Once DefineColumns()
has finished enumerating all members, it sorts _sort_list
and then adds the contained columns to the data table.
This sorting takes the SortID
property of each DataTableInclude
attribute and sorts it ascending. For example, if you want to have a member to appear in the first place, simple apply the DataTableInclude
attribute like this:
[DataTableInclude(SortID=1)]
private string _name = string.Empty;
As _sort_list
contains both the created data column as well as the DataTableInclude
attribute class, you can easily implement your own sorting method by changing DataColumnAndDataTableIncludeAttributeComparer
.
Adding data
Once all columns are added, we should add rows with data from our custom objects. This is achieved by using the method:
public void Add(object o)
Rather simple, isn't it? And indeed, the function itself is very simple since it only needs to do the following:
- Create a new
DataRow
- Enumerate over all columns assigned to the data table
For each column retrieved, the function does the following:
- Check if this column is in the list
DefineColumns()
has created
- If so, retrieve the
MemberInfo
and use it to retrieve the current value
- For a property or a field, use
GetValue()
, for a method invoke it using Invoke()
- Assign this returned value to the cell of the current column
And that's it.
For your convenience, there is also a second Add()
method that takes an IEnumerable
and adds all objects that this enumerator returns. A quick example:
ArrayList list = new ArrayList();
for (int i = 0; i < 30; i++)
{
CustomClass1 cc = new CustomClass1();
cc.Age = i;
cc.Name = "John Doe " + i;
list.Add(cc);
}
generator.Add(list);
Example
After all this, what is the result actually? Well, here's some code to demonstrate how easy DTG can be used. This is a custom class we want to display in a data grid:
using System;
using Xteq.Data;
namespace TestAppDataTableGen
{
class CustomClassSimple
{
[DataTableInclude]
private long _myField=13;
[DataTableInclude(SortID=10)]
public string Prop
{
get
{
return (_myField + 10).ToString();
}
}
[DataTableInclude("Ticks")]
protected long GetCurrentTickCount()
{
return DateTime.Now.Ticks;
}
}
}
Using the DataTableInclude
attribute we have defined to include the private field _myField
, to include the property "Prop
", which should appear on position 10, and also we want to have the value from GetCurrentTickCount()
, but the column should be named "Ticks".
To display this class, we only need to write:
CustomClassSimple ccs = new CustomClassSimple();
DataTableGenerator generator = new DataTableGenerator();
generator.DefineColumns(ccs);
generator.Add(ccs);
grid1.DataSource = generator.DataTable;
The data grid will then display:
Private members note
Although DTG is able to retrieve private members (see _myField
) in the example above, this will only work at the level that declares this private member.
For example, if you create CustomClassSimple2
that is derived from the example class above, _myField
will not show up since it is private to CustomClassSimple2
and thus not accessible.
Search method
When creating a new instance of DTG, you can also pass in one or more values from the DataTableGeneratorSearch
enumeration. Since reflecting the passed object takes some time, you might decide that only fields should be checked and included. This can be done by using this constructor:
DataTableGenerator generator =
new DataTableGenerator(DataTableGeneratorSearch.Fields);
You can also OR several options together like this:
DataTableGenerator generator =
new DataTableGenerator(DataTableGeneratorSearch.Fields|
DataTableGeneratorSearch.Properties);
DataTable tricks
Since DTG provides full access to its underlying DataTable
using the DataTable
property, you can add new columns, remove existing ones, or do whatever you want with it.
Conclusion
As said at the beginning of this article, this is only the first version of the DataTableGenerator
. It currently can't handle indexed properties, methods with parameters, can't resolve any internal collections (for example, class A
has a field _collection
that contains a collection of class B
) or private members of the base classes.
But using this code as the basis, it should be easy to implement these or any other features.
Enjoy!
Disclaimer
THE SOFTWARE AND ALL ACCOMPANYING FILES, DATA AND MATERIALS, ARE DISTRIBUTED AND PROVIDED "AS IS" AND WITH NO WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED. THE USER ACKNOWLEDGES THAT GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM, INCLUDING THE SOFTWARE, MUST BE THOROUGHLY TESTED WITH NON-CRITICAL DATA BEFORE THE USER RELIES ON IT, AND THE USER HEREBY ASSUME THE ENTIRE RISK OF USING THE PROGRAM.
IN ADDITION, IN NO EVENT XTEQ SYSTEMS, OR ITS PRINCIPALS, SHAREHOLDERS, OFFICERS, EMPLOYEES, AFFILIATES, CONTRACTORS, SUBSIDIARIES, OR PARENT ORGANIZATIONS, WILL BE LIABLE FOR ANY INDIRECT, INCIDENTAL, CONSEQUENTIAL OR PUNITIVE DAMAGES WHATSOEVER RELATING TO THE USE OF THE SOFTWARE OR TO THE RELATIONSHIP OF THE USER WITH XTEQ SYSTEMS. THIS INCLUDES, BUT IS NOT LIMITED TO, MERCHANTABILITY AND FITNESS FOR A PARTICULAR FUNCTION.