Usually you do not create an instance of the BuildProvider.
What you do is create a new class that derives from the BuildProvider class.
After you create this class you will need to configure this BuildProvider in
the web.config in the Compilation section.
There are several methods and properties in the
BuildProvider class that you will need to override or use while developing the
new BuildProvider.
The main method that you will need to override is the
GeneratCode method. This method takes as input parameter an AssemblyBuilder
instance. Therefore, the code inside the GenerateCode method should create a
new CodeCompileUnit then add it to the AssemblyBuilder. The BuildProvider
generates source code for the different files registered in the appropriate
language and the AssemblyBuilder combines the source code generated by each
BiuldProvider into the assembly.
There is a property on the BiuldProvider class called
CodeCompilerType. This property is a read-only property that retrieves the
language of the generated source code. Usually this information is sent by the
ASP.NET environment through the AssemblyBuilder, however, you can implement
this property explicitly and an example of implementing it can be checked in
the BuildProvder Class MSDN Documentation.
The CodeCompileUnit, that provides a container for a CodeDOM
program graph, will include the namespace, the class(s) and all the needed
properties and methods. Hence, the need to add that compile unit into the
current running assembly to be able to compile the generated class and add it
to the current assembly and have the ability to access the class and all its
properties and methods at design time in your classes!
By default, ASP.NET 2.0 environment passes an instance of
the AssemblyBuilder based on the preferred compiler language and the context of
the file(s) to the BuildProvider methods to be able to generated code for those
files given their paths and the programming language to use in generating the
source code.
Type of the BuildProvider to build
In this article we will assume we have an XML file that
represents a settings XML file for the web application, the structure of the
file is as follows.
Listing 1
<?xml version="1.0" encoding="utf-8" ?>
<Settings namespace="SettingsNamespace" classname="bhSettings">
<Setting type="System.String" name="Name" value="Bilal Haidar" />
<Setting type="System.Int32" name="Age" value="26" />
</Settings>
As you can see, we have a Settings XML file with a Setting
record. Each setting record has three attributers, the Data Type, Name, and
Value attributes. Therefore, the source code generated for such a file should
contain two properties, Name and Age, whose data types are String and Int32,
and their values are “Bilal Haidar” and 26 respectively.
The BuildProvider that we are going to build in this article
shall parse this XML file and generate a C# class (since we will create a new
Web Site – C#) that has two properties as mentioned above. This way, developers
working on the web site will have a very nice intellisense for this XML file
without having to write a single line of code to parse the above XML file. They
will only be responsible for accessing the different settings of the XML file
through a well structured C# class. This is why BuildProviders are so cool!
Constructing the SettingsBuildProvider
The following code shows the main structure of the
SettingsBuildProvider class deriving from the BuildProvider.
Listing 2
public class SettingsBuildProvider: BuildProvider
{
public override void GenerateCode(AssemblyBuilder assemblyBuilder)
{
// Get the path to the filename
string fileName = base.VirtualPath;
SettingsGenLib settingsGenLib = new SettingsGenLib();
// Create a new CodeCompileUnit
CodeCompileUnit codeUnit = settingsGenLib.GenerateSettingsClass(fileName,
true);
// Add the unit to be compiled and added to the current assembly
assemblyBuilder.AddCodeCompileUnit(this, codeUnit);
}
}
You can see a property called VirtualPath, which is defined
on the base class that is the BuildProvider. This property represents the
context of the file for which the source code is to be generated and, hence, it
contains the path to that file in the current web application and mainly in the
App_Code folder inside your application.
Then we define a new instance of an internal class that we
added to handle all the details of generating the source code and is called
SettingsGenLib. The SettingsGenLib has a single public method called GenerateSettingsClass
that takes as input the following two parameters.
FileName: File name to generate the source code to.
IsVirtualInputFile: A Boolean specifying that the file name
is read from the VirtualPath property of the BuildProvider or not. It will affect
the way we access and parse the XML file.
The GenerateSettingsClass method returns an instance of the
CodeCompileUnit which is then added to the instance of the AssemblyBuilder that
has been passed by the ASP.Net environment to be able to compile and generate
the class(s) out of the CodeCompileUnit created.
The SettingsGenLib class sand GenerateSettingsClass
method
The SettingsGenLib is a helper class we have created to
include all the details of implementing the BuildProvider. This way it will be
easier for you to focus on the main class that is deriving from the
BiuldProvider and the methods that need to be overridden.
The GenerateSettingsClass method is a long method, so we
will split it into pieces and explain each separately.
Listing 3
public CodeCompileUnit GenerateSettingsClass(string fileName, bool isVirtualInputFile)
{
// Create a new compile unit
CodeCompileUnit compileUnit = new CodeCompileUnit();
The above code snippet shows the method header and the input
parameters.
First of all, a new instance of the CodeCompileUnit is
created; this instance will hold the namespace and class declarations to be
compiled later on and added to the AssemblyBuilder.
Listing 4
// Hold the contents of the XML file
XmlDocument doc = new XmlDocument();
if (isVirtualInputFile)
using(Stream inFile = VirtualPathProvider.OpenFile(fileName))
{
doc.Load(inFile);
}
else
using(Stream inFile = File.Open(fileName, FileMode.Open))
{
doc.Load(inFile);
}
The code above loads the XML file into an XMLDocument
instance. The isVirtualInputFile says whether we are loading a virtual path
from the App_Code, if true, then we can use the VirtualPathProvider OpenFile
static method. Hopefully, in the near future we will be delivering an article
on the VirtualPathProvider and showing you how to create a custom
VirtualPathProvider.
Listing 5
// Get a node list representing the root
XmlNodeList nodeList = doc.SelectNodes("/Settings");
// Get the namespace of the generated class
string ns = "";
if (nodeList[0].Attributes["namespace"].InnerText != "")
ns = nodeList[0].Attributes["namespace"].InnerText;
// Define the namespace name
if (ns == string.Empty)
ns = "DefaultNamespace";
// Get the class name
string className = "";
if (nodeList[0].Attributes["classname"].InnerText != "")
className = nodeList[0].Attributes["classname"].InnerText;
// Define the class name
if (className == string.Empty)
className = "DefaultClass";
else
className = FormatPropertyName(className);
In the code above, we have selected the root node since it
contains two important attributes, the Namespace and Class name. We select the
root node and set the namespace and class name respectively using some useful
methods on the XmlDocument and with the help of XPath.
Now that we have both the namespace and class name, it is
time to add them to the CodeCompileUnit we created above.
Listing 6
// Create a new namespace
CodeNamespace namespaceUnit = new CodeNamespace(ns);
// Add the namespace to the compile unit
compileUnit.Namespaces.Add(namespaceUnit);
// Create a new class
CodeTypeDeclaration settingsClass = new CodeTypeDeclaration(className);
// Add it to the namespace types
namespaceUnit.Types.Add(settingsClass);
First of all, we create a new namespace using CodeNamespace
object giving it the namespace name we retrieved from the XML file then add it
to the Namespace collection of the CodeCompileUnit.
After that, we create a new instance of the
CodeTypeDeclaration, which represents the new class to be generated, and add it
to the Types collection of the newly added namespace.
Listing 7
// Get the list of setting fields
nodeList = doc.SelectNodes("//Settings/Setting");
foreach (XmlNode node in nodeList)
{
string typename = node.Attributes["type"].InnerText;
string propName = node.Attributes["name"].InnerText;
string propValue = node.Attributes["value"].InnerText;
XmlRecord xmlRecord = new XmlRecord(typename, propName, propValue);
// Create field
CodeMemberField itemField = CreateField(xmlRecord);
settingsClass.Members.Add(itemField);
// Create Property
CodeMemberProperty itemProperty = CreateProperty(xmlRecord);
settingsClass.Members.Add(itemProperty);
}
In the code above we used an XPath expression to retrieve
all the Setting records from the XML file. Each Setting record will be
transformed into a property in the new generated class.
The code is pretty simple; we get all the Setting records,
loop through them one by one and access the three main attributes used for each
record: type, name, and value. The type and name will be used for the type of
the member and property created for this record and the value will be used to
set the initial value of the member mentioned in above.
The code creates a new CodeMemberField instance by calling the
helper method CreateField, which we will check very soon. The created field is
added to the members' collection of the class to be generated.
In addition to the field created, a new property is created
by using the CodeMemberProperty object. Another helper method is used,
CreateProperty.
The above code is repeated for each Setting record inside
the XML file, thus creating a new property and field for each Setting record.
Finally, the GenerateSettingsClass returns the
CodeCompileUint we have created early in the method, where we added the
namespace, class, members and properties.
The CodeCompileUnit returned is added to the AssemblyBuilder
instance to compile and generate the class and add it to the current assembly.
CreateField method
This method is a helper method used to create a new field.
Listing 8
private CodeMemberField CreateField(XmlRecord record)
{
// Create a new field with the specified data type in the XML file
CodeMemberField cfield = new CodeMemberField(record.typeName, FormatFieldName
(record.propName));
// Specify that the field should be private and static
cfield.Attributes = MemberAttributes.Private | MemberAttributes.Static;
// Assign the value found in the XML file to this field
string valueToAssign = "";
switch (record.typeName)
{
case "System.String":
case "System.Guid":
valueToAssign = string.Format("\"{0}\"", record.propValue);
break;
case "System.Char":
valueToAssign = string.Format("'{0}'", record.propValue);
break;
default:
valueToAssign = string.Format("{0}", record.propValue);
break;
}
cfield.InitExpression = new CodeSnippetExpression(valueToAssign);
return cfield;
}
The CreateField method takes as input an XmlRecord
parameter. The XmlRecord is a structure we have created to join the type, name,
and value of each Setting record.
First of all, a new instance of the CodeMemberField is
created with the type and name of the field. The field is set to be a private
and static field. Finally, we assign a value for this field by using the
InitExpression property. We are taking special consideration for the string,
guid, and char data types to include required double and single quotes
respectively.
CreateProperty method
This method is similar to the above method in which we
create a new public and static property.
Listing 9
private CodeMemberProperty CreateProperty(XmlRecord record)
{
// Create a new property
CodeMemberProperty cproperty = new CodeMemberProperty();
// Set the name, type, and access of the proprety
cproperty.Name = FormatPropertyName(record.propName);
cproperty.Type = new CodeTypeReference(record.typeName);
cproperty.Attributes = MemberAttributes.Public | MemberAttributes.Static;
// We only need Read-Only properties
cproperty.GetStatements.Add(new CodeMethodReturnStatement(new
CodeFieldReferenceExpression(null, FormatFieldName(record.propName))));
return cproperty;
}
A new instance of the CodeMemberProperty is created
following this the name, type and attributes are set respectively. Since the
XML file contains all the data and no need to set any of the properties, we
will only be creating read-on properties. Therefore, we need to add a Get
section for the new property created and this is done by using the
GetStatements property which contains the statements in the “get” section of a
property. The “get” section is simple, we are just returning the member we
created above.
This way, we have finished creating the BuildProvider. Now,
we create a new Web application and then add a reference to this BuildProvider.
The BuildProvider in this article was created in a separate Class Library.
After adding the reference to the SettingsBuildProvider, we
need to configure the BuildProvider in the Web.config.
Configuring the SettingsBuildProvider in the Web.config
We will need to tell the ASP.NET application to use the new
BuildProvider we created. To do so, we need to add this section into the
web.config.
Listing 10
<system.web>
<compilation debug="true">
<buildProviders>
<add extension=".settings" type="BuildProviders.SettingsBuildProvider"/>
</buildProviders>
</compilation>
</system.web>
As you can see, we have added a new buildProviders entry. In
the entry we specify the extension of the file that the newly created
BiuldProvider will generate the source code for. The file extension is a custom
one of your choice and we have chosen it to be “.settings.” The type specifies
the full path to the BuildProvider we created.
Once you configure your web application as above, you still
need to add the XML file we displayed into the App_Code folder. This is a must do
step! Create a new XML file with the extension of “.settings” and place the
file in the App_Code folder.
Now that you have finished all the configurations required,
you can open any web form, go to the code-behind and try to write down the name
of the namespace you have chosen in the XML file. Then you will see the
inteliisense is active, the class name will be shown, and all the properties
within the class are accessible as read-only properties as shown below.
Listing 11
string name = SettingsNamespace.BhSettings.Name;
int age = SettingsNamespace.BhSettings.Age;
You can see that the properties are type-safe so no need to
convert any data type, simply use them as they are!
Finally, this is the class that has been dynamically generated
for you at the design time.
Listing 12
namespace SettingsNamespace
{
public class BhSettings
{
private static string name = "Bilal Haidar";
private static int age = 26;
public static string Name
{
get
{
return name;
}
}
public static int Age
{
get
{
return age;
}
}
}
}