Introduction
There are certain practical difficulties we face during the development of certain controls. In our project, we wanted to show
a List of Value control for the selection of a particular record. That list can be of customers, vendors, items, or it could be anything. We required three facilities in it:
- Some part of data was in an Oracle database and some of it in a SQL Server database.
- We can always use a dataset to get data and bind it to a grid view control, but if possible, we wanted to avoid this due to performance issues.
- Also, we wanted to update/insert only selected data in the database (i.e., according to user preferences).
So, to resolve the above mentioned issues, we used the CodeDom technology to generate dynamic types, and also implemented the INotifyChanged
interface.
If required, we could also implement any other interface and override its method and properties.
The attached code file consists of a CodeDom class which you can include in your project, and use as it is, or add/delete features as per your requirements.
Limitations
If we have to do the same thing in Silverlight, we cannot use the CodeDom technology as it is not available yet in Silverlight;
in that case, we have to use Reflection.Emit (it becomes more useful in Silverlight as Silverlight does not support datasets).
Process
Problem I: Creating a Dynamic Type
When we generate a dynamic type, we can divide the entire process into certain steps:
- Create a namespace
- Import all the required namespaces and reference all the assemblies required
- Generate a new class
- Implement
INotifyPropertyChanged
- Create constructors
- Create properties
- Create functions
- Compile the code
- Get the object of the created type
If we have to create a type using CodeDom, then we have to import these namespaces:
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
using Microsoft.CSharp;
using System.IO;
Namespace Creation
CodeNamespace cnsCodeDom = new CodeNamespace(NameSpaceName);
Importing and Referencing Assemblies
Here, I have created the FuncImportNameSpaces
and FuncReferencedAssemblies
functions to import namespaces and reference assemblies
from a collection, respectively (the onsumer of this class has to specify which classes he/she would like to import and/or reference).
cnsCodeDom.Imports.Add(new CodeNamespaceImport(item));
cp.ReferencedAssemblies.Add(item);
Creating the Type and Implementing INotifyPropertyChanged
The GenerateNewClass
function is used to create a new type. Here, I have created that custom type and then implemented INotifyPropertyChanged
if
the consumer sets the true
for the Property parameter (i.e., IsImplementPropChanged
).
To implement InotifyPropertyChanged
, I have imported the System.ComponentModel
class and then added an interface name into
the BaseTypes
collection of our class object.
Here, you can implement your custom interface as well, but in that case, you have to specify the Path of your assembly which consists of your custom interface.
For example, if you have an interface ICustomer
, the DLL of which exists at "D:\Customer\Customer.dll", then we can use it as below:
cp.ReferencedAssemblies.Add(Entire Path Of Assembly);
Now, it requires to implement the "PropertyChanged
" event, and that is possible by using the CodeMemberEvent
class.
We can also create our custom event like this:
CodeMemberEvent cme = new CodeMemberEvent();
cme.Name = "DynamicEvent";
cme.Type = new CodeTypeReference("System.EventHandler");
cme.Attributes = MemberAttributes.Public;
clsDecl.Members.Add(cme);
Generate Constructor
CodeConstructor clsConstructor = new CodeConstructor();
If it requires to add a parameter in the constructor, we can use the below mentioned method:
clsConstructor.Parameters.Add(
new CodeParameterDeclarationExpression(GetType(Int32),"CustomerName");
Whatever custom code we would like to write inside the constructor, we can use:
CodeSnippetStatement csn = new CodeSnippetStatement("String Statement");
Inside that statement, we can even use the parameters which we have passed:
"MessageBox.Show(" + ((char)34).ToString() + CustomerName + ((char)34).ToString() + ");"
Generate Properties
- First, add a variable for the property:
CodeMemberField clsMember = new CodeMemberField();
clsMember.Name = "_" + item.PropName;
- Add a
get
statement:
property.GetStatements.Add(new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(newCodeThisReferenceExpression(), "_" +
item.PropName + ";" )));
- Add any custom statements you want to execute after reading your property; the below mentioned statements have to be written before
your "
GetStatements.Add
" function if you want to execute it prior to your get
statement:
CodeSnippetStatement csn = new CodeSnippetStatement(GetStatements);
property.GetStatements.Add(csn);
There is not much difference between get
and set
statements; in your custom statement, you can provide any legitimate C# statement,
but as C# is case sensitive, please ensure case sensitivity when you pass on your string.
Examples:
"if (DynamicEvent != null){DynamicEvent(this,null);â€
"MessageBox.Show(" + ((char)34).ToString() + "Test" + ((char)34).ToString() + ");"
Create Method
Below is the code to generate a method through the CodeDom technology:
CodeMemberMethod cmm = new CodeMemberMethod();
cmm.Name = "NotifyPropertyChanged";
cmm.Parameters.Add(new CodeParameterDeclarationExpression(
newCodeTypeReference("System.String"),"info"));
cmm.Attributes = MemberAttributes.Public;
cmm.Statements.Add(new CodeSnippetStatement("if (PropertyChanged != null)" +
"{PropertyChanged(this, new PropertyChangedEventArgs(info));}"));
clsDecl.Members.Add(cmm);
Here, if a return
statement is required, then we have to specify its data type:
cmm.ReturnType = GetType(bool);
Compile your Code
CompilerResults result = cscp.CompileAssemblyFromSource(cp, source[0]);
Here, the result
variable is very important especially when there is some mistake in your custom code, i.e., if your custom string is not proper
C# code, or if you have inherited an interface but its member is not implemented; you can find out the exact error from the result
variable.
codeGenerator.GenerateCodeFromNamespace(cnsCodeDom, codeWriter, cgo);
This statement is generating code from the namespace. If you want to save the entire code as a class or a cs file, then it is also possible through the following statements:
Get the Object
Just provide the namapespace name and class name to get the object:
Object o = Activator.CreateInstance(
result.CompiledAssembly.GetType(NameSpaceName + "." + ClassName));
That's it; your object is ready. You can now provide it to any data context, as well as you can do all LINQ operations on this object.
Consume Created Object
Below is the code to set all the properties and get the required object:
CodeDomLibrary.DataClass dc = new CodeDomLibrary.DataClass();
dc.AssemblyName = "Customers";
dc.ClassName = "Customer";
dc.NameSpaceName = "Project";
dc.ImportNameSpaces = new
List<string>(){"System.Xml","System","System.Windows.Forms",
"System.Collections.ObjectModel"};
dc.ReferencedAssemblies = new List<string>() { "System.Xml.dll",
"System.Windows.Forms.dll","System.dll" };
CodeDomLibrary.Properties p = new CodeDomLibrary.Properties();
p.PropName = "CustomerName";
p.PropType = "System.String";
p.CustomSetCodeStatements = new List<string>()
{ "MessageBox.Show(" + ((char)34).ToString() +
"Inside Customer Name" + ((char)34).ToString() + ");"
};
dc.PropertyCollection = new List<CodeDomLibrary.Properties>() { p };
dc.IsImplementPropChanged = true;
Object o = dc.CreateObject();
We have to use Reflection to set the properties or invoke any method of the created object.
If we have implemented a custom interface, then we can consume it without Reflection.
o.GetType().GetProperty("CustomerName").SetValue(o, "Mike", null);
To consume a custom event:
EventInfo e = o.GetType().GetEvent("DynamicEvent");
MethodInfo RRMeth = typeof(CodeDomLibrary).GetMethod("Create",
System.Reflection.BindingFlags.Static |BindingFlags.NonPublic);
Delegate peDel2 = Delegate.CreateDelegate(e.EventHandlerType, RRMeth);
e.AddEventHandler(o, peDel2);
static private void Create(Object Sender, EventArgs e)
{
}
As we have completed our first issue, we will move on to the second.
Problem II: Make a Generic Class for Data
Now, as I had mentioned earlier, I want to use the same code and want to get data from
a SQL Server / Oracle database, so it is not possible to create an object of:
SqlConnection
/ OracleConnection
SqlCommond
/ OracleComand
SqlDataReader
/ OracleDataReader
The solution is simple; declare objects of the following interfaces in place of the above mentioned class:
private System.Data.IDbConnection cn;
private System.Data.IDbCommand cmd;
private System.Data.IDataReader sdrdr;
Now, just pass on a flag to show which database you would like to connect: Oracle or SQL Server.
if (flag == "Oracle")
{
cn = new OracleConnection(ConnString);
cmd = new OracleCommand (sql,(OracleConnection) cn);
cmd.CommandType = CommandType.Text;
cn.Open();
}
else
{
cn = new SqlConnection(ConnString);
cmd = new SqlCommand(sql, (SqlConnection) cn);
cmd.CommandType = CommandType.Text;
cn.Open();
}
Rest of the code will remain the same, as we may use a Stored Procedure or simple SQL statements to get or update/insert data.
Conclusion
By creating a dynamic type by inheriting INotifyPropertyChanged
, we can use that object to bind with any UI Element; also, it is possible
to apply LINQ (in memory) to a specified object. Overheads of creating a dataset can also be reduced.
The trick which we have used to create a generic class for Oracle / SQL Server is very simple, and this simple fundamental principle can be used at several places.
Any suggestions / critics / feedback are most welcome.