Creating a Custom BuildProvider


Creating a Custom BuildProvider

When you write an ASP.NET page and save the page to your computer's file system, the ASP.NET page gets compiled dynamically into a .NET class in the background. The page is compiled dynamically by a BuildProvider.

The ASP.NET Framework includes a number of BuildProviders. Each BuildProvider is responsible for compiling a file with a particular extension that is located in a particular type of folder. For example, there are BuildProviders for Themes, Master Pages, User Controls, and Web Services.

When a BuildProvider builds, it builds a new class in the Temporary ASP.NET Files folder. Any class added to the folder becomes available to your application automatically. When you use Visual Web Developer, any public properties and methods of the class appear in Intellisense.

You can create your own BuildProviders. This can be useful in a variety of different scenarios. For example, imagine that you find yourself building a lot of ASP.NET pages that display forms. You can tediously build each ASP.NET page by hand by adding all the necessary form and validation controls. Alternatively, you can create a new BuildProvider that takes an XML file and generates the form pages for you automatically.

Or, imagine that you are spending a lot of time building data access components. For example, every time you need to access a database table, you create a new component that exposes properties that correspond to each of the columns in the database table. In this case, it would make sense to create a custom BuildProvider that generates the data access component automatically.

Creating a Simple BuildProvider

Let's start by creating a really simple BuildProvider. The new BuildProvider will be named the SimpleBuildProvider. Whenever you create a file that has the extension .simple, the SimpleBuilderProvider builds a new class with the same name as the file in the background. The dynamically compiled class also includes a single method named DoSomething() that doesn't actually do anything.

The SimpleBuildProvider is contained in Listing 25.1.

Listing 25.1. App_Code\CustomBuildProviders\SimpleBuildProvider.vb

Imports System Imports System.Web.Compilation Imports System.CodeDom Imports System.IO Namespace AspNetUnleashed     Public Class SimpleBuildProvider         Inherits BuildProvider         Public Overrides Sub GenerateCode(ByVal ab As AssemblyBuilder)           Dim fileName As String = Path.GetFileNameWithoutExtension(Me.VirtualPath)           Dim snippet As String = "Public Class " & fileName & vbNewLine             snippet &= "  Public Shared Sub DoSomething()" & vbNewLine             snippet &= "  End Sub" & vbNewLine             snippet &= "End Class"           ab.AddCodeCompileUnit(Me, New CodeSnippetCompileUnit(snippet))         End Sub     End Class End Namespace 

All BuildProviders must inherit from the base BuildProvider class. Typically, you override the BuildProvider class GenerateCode() method. This method is responsible for generating the class that gets added to the Temporary ASP.NET Files folder.

An instance of the AssemblyBuilder class is passed to the GenerateCode() method. You add the class that you want to create to this AssemblyBuilder by calling the AssemblyBuilder.AddCodeCompileUnit() method.

In Listing 25.1, a CodeSnippetCompileUnit is used to represent the source code for the class. Any code that you represent with the CodeSnippetCompileUnit is added, verbatim, to the dynamically generated class. This approach is problematic.

Unfortunately, you can use the SimpleBuildProvider in Listing 25.1 only when building a Visual Basic .NET application. It doesn't work with a C# application. Because the code represented by the CodeSnippetCompileUnit is Visual Basic .NET code, using the SimpleBuildProvider with a C# application would result in compilation errors. The SimpleBuildProvider would inject Visual Basic .NET code into a C# assembly.

The proper way to write the SimpleBuildProvider class would be to use the CodeDom. The CodeDom enables you to represent .NET code in a language neutral manner. When you represent a block of code with the CodeDom, the code can be converted to either C# or Visual Basic .NET code automatically. You'll learn how to use the CodeDom when we build a more complicated BuildProvider in the next section. For now, just realize that we are taking a shortcut to keep things simple.

When you add the SimpleBuildProvider to your project, it is important that you add the file to a separate subfolder in your App_Code folder and you mark the folder as a separate code folder in the web configuration file. For example, in the sample code on the CD that accompanies this book, the SimpleBuildProvider is located in the App_Code\CustomBuildProviders folder.

You must add a BuildProvider to a separate subfolder because a BuildProvider must be compiled into a different assembly than the other code in the App_Code folder. This makes sense because a BuildProvider is actually responsible for compiling the other code in the App_Code folder.

The web configuration file in Listing 25.2 defines the CustomBuildProviders folder and registers the SimpleBuildProvider.

Listing 25.2. Web.Config

<?xml version="1.0"?> <configuration>     <system.web>       <compilation>         <codeSubDirectories>           <add directoryName="CustomBuildProviders"/>         </codeSubDirectories>         <buildProviders>           <add extension=".simple" type="AspNetUnleashed.SimpleBuildProvider" />         </buildProviders>        </compilation>            </system.web> </configuration> 

The web configuration file in Listing 25.2 associates the SimpleBuildProvider with the file extension .simple. Whenever you add a file with the .simple extension to the App_Code folder, the SimpleBuildProvider automatically compiles a new class based on the file.

For example, adding the file in Listing 25.3 to your App_Code folder causes the SimpleBuildProvider to create a new class named Mike.

Listing 25.3. App_Code\Mike.simple

Hello! Hello! Hello! 

The actual content of the file that you create doesn't matter. The SimpleBuildProvider ignores everything about the file except for the name of the file.

You can see the new file created by the SimpleBuildProvider by navigating to the Sources_App_Code folder contained in the folder that corresponds to your application in the Temporary ASP.NET Files folder. The contents of the auto-generated file are contained in Listing 25.4.

Listing 25.4. mike.simple.72cecc2a.vb

#ExternalChecksum("C:\Websites\MyApp\Code\VB\App_Code\Mike.simple", "{406ea660-64cf-4c82-b6f0-42d48172a799}","E3A7385F5DCED38D7B52DBB3CBCA0B40") Public Class Mike   Public Shared Sub DoSomething()   End Sub End Class 

Any class added to the Temporary ASP.NET Files folder is available in your application automatically. For example, the page in Listing 25.5 uses the Mike class.

Listing 25.5. ShowSimpleBuildProvider.aspx

<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server">     Sub Page_Load()         Mike.DoSomething()     End Sub      </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title>Show SimpleBuildProvider</title> </head> <body>     <form  runat="server">     <div>          </div>     </form> </body> </html> 

The Mike class appears in Intellisense. For example, if you type Mike followed by a period, the DoSomething() method appears (see Figure 25.1).

Figure 25.1. Using a BuildProvider to generate a class dynamically.


Creating a Data Access Component BuildProvider

In the previous section, we created a simple but useless BuildProvider. In this section, we create a complicated but useful BuildProvider.

In this section, we create a DataBuildProvider. The DataBuildProvider generates a data access component automatically from an XML file. For example, if you add the XML file in Listing 25.6 to your project, then the DataBuildProvider generates the class in Listing 25.7 automatically.

Listing 25.6. App_Code\Movie.data

<Movies>   <add name="Title" />   <add name="Director" />   <add name="BoxOfficeTotals" type="Decimal" /> </Movies> 

Listing 25.7. movie.data.72cecc2a.vb

[View full width]

#ExternalChecksum("C:\Websites\MyApp\App_Code\Movie.data", "{406ea660-64cf-4c82-b6f0-42d48172a799}","E043FB9DD82756F3156AD9FC5090A8BD") '------------------------------------------------------------------------------ ' <auto-generated> '     This code was generated by a tool. '     Runtime Version:2.0.50727.42 ' '     Changes to this file may cause incorrect behavior and will be lost if '     the code is regenerated. ' </auto-generated> '------------------------------------------------------------------------------ Option Strict Off Option Explicit On Imports System Namespace Data     Partial Public Class Movie                  Private _Title As String         Private _Director As String         Private _BoxOfficeTotals As [Decimal]                  Public Sub New()             MyBase.New         End Sub                  Public Overridable Property Title() As String             Get                 Return Me._Title             End Get             Set                 Me._Title = value             End Set         End Property                  Public Overridable Property Director() As String             Get                 Return Me._Director             End Get             Set                 Me._Director = value             End Set         End Property                  Public Overridable Property BoxOfficeTotals() As [Decimal]             Get                 Return Me._BoxOfficeTotals             End Get             Set                 Me._BoxOfficeTotals = value             End Set         End Property                  '''<summary>Returns List of Movie</summary>         Public Overloads Shared Function [Select](ByVal con As System.Data.SqlClient .SqlConnection) As System.Collections.Generic.List(Of Movie)             Dim results As System.Collections.Generic.List(Of Movie) = New System .Collections.Generic.List(Of Movie)             Dim cmd As System.Data.SqlClient.SqlCommand = New System.Data.SqlClient.SqlCommand             cmd.Connection = con             Dim cmdText As String = "SELECT Title,Director,BoxOfficeTotals FROM Movies"             cmd.CommandText = cmdText             Dim reader As System.Data.SqlClient.SqlDataReader = cmd.ExecuteReader             Dim counter As Integer             counter = 0             Do While reader.Read                 Dim record As Movie = New Movie                 record.Title = CType(reader("Title"),String)                 record.Director = CType(reader("Director"),String)                 record.BoxOfficeTotals = CType(reader("BoxOfficeTotals"),[Decimal])                 results.Add(record)                 counter = (counter + 1)             Loop             Return results         End Function                  '''<summary>Returns List of Movie</summary>         Public Overloads Shared Function [Select](ByVal connectionStringName As String) As  System.Collections.Generic.List(Of Movie)             Dim results As System.Collections.Generic.List(Of Movie) = New System .Collections.Generic.List(Of Movie)             Dim conStringSettings As System.Configuration.ConnectionStringSettings =  System.Web.Configuration.WebConfigurationManager.ConnectionStrings(connectionStringName)             Dim conString As String = conStringSettings.ConnectionString             Dim con As System.Data.SqlClient.SqlConnection = New System.Data.SqlClient .SqlConnection             con.ConnectionString = conString             Try                  con.Open                 results = Movie.Select(con)             Finally                 con.Close             End Try             Return results         End Function     End Class End Namespace 

The XML file in Listing 25.6 contains the name of a database table (Movies) and it contains a list of columns from the database table. When you add the file in Listing 25.6 to your project, the class in Listing 25.7 is generated automatically.

The data access component in Listing 25.7 contains a property that corresponds to each of the columns listed in the Movie.data file. Furthermore, each property has the data type specified in the Movie.data file.

Notice, furthermore, that the Movie data access component includes two Select() methods. You can retrieve all the records from the Movies database table in two ways: by passing an open SqlConnection object to the Select() method or by passing the name of a connection string defined in the web configuration file to the Select() method.

The page in Listing 25.8 illustrates how you can use the Movie data access component within an ASP.NET page (see Figure 25.2).

Figure 25.2. Displaying data returned by a dynamically generated data access component.


Listing 25.8. ShowDataBuildProvider.aspx

<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <script runat="server">     Sub Page_Load()         grdMovies.DataSource = Data.Movie.Select("Movies")         grdMovies.DataBind()     End Sub      </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head  runat="server">     <title>Show DataBuildProvider</title> </head> <body>     <form  runat="server">     <div>          <asp:GridView                  Runat="server" />          </div>     </form> </body> </html> 

Unlike the SimpleBuildProvider created in the previous section, the DataBuildProvider uses the CodeDom to represent code. This means that you can use the DataBuildProvider in both Visual Basic .NET and C# applications. The DataBuildProvider generates the data access component in different languages automatically. For example, if you use the DataBuildProvider in a C# application, the BuildProvider generates the code in Listing 25.6 in C#.

Unfortunately, the code for the DataBuildProvider is much too long to include here. The entire code is included on the CD that accompanies the book. The file in Listing 25.9 contains part of the DataBuildProvider code.

Listing 25.9. DataBuildProvider.vb (Partial)

Namespace AspNetUnleashed     Public Class DataBuildProvider         Inherits BuildProvider         Private _className As String         Public Overrides Sub GenerateCode(ByVal ab As AssemblyBuilder)             ' Load the XML file             Dim xmlData As New XmlDocument()             xmlData.Load(HostingEnvironment.MapPath(Me.VirtualPath))             ' Generate code from XML document             Dim dataCode As CodeCompileUnit = GetDataCode(xmlData)             ' Add the code             ab.AddCodeCompileUnit(Me, dataCode)         End Sub         Private Function GetDataCode(ByVal xmlData As XmlDocument) As CodeCompileUnit             ' Add class             _className = Path.GetFileNameWithoutExtension(Me.VirtualPath)             Dim dataType As New CodeTypeDeclaration(_className)             dataType.IsPartial = True             ' Add constructor             AddConstructor(dataType)             ' Add properties             AddProperties(dataType, xmlData)             ' Add Select method             AddSelect(dataType, xmlData)             ' Add Select with conString overload             AddSelectConString(dataType, xmlData)             ' Create namespace             Dim dataNS As New CodeNamespace("Data")             ' Add class to namespace             dataNS.Types.Add(dataType)             ' Create code unit             Dim dataCode As New CodeCompileUnit()             ' Add namespace to code unit             dataCode.Namespaces.Add(dataNS)             ' Add default namespaces             dataNS.Imports.Add(New CodeNamespaceImport("System"))             Return dataCode         End Function     End Class End Namespace 

The DataBuildProvider's GenerateCode() method loads a .data file into an XmlDocument. Notice that the VirtualPath property represents the path of the file that is being built. For example, if you add a file named Products.data to your project, then the VirtualPath property would represent the path to the Products.data file.

Next, the code for the data access component is created from the XML file by the GeTDataCode() method. The GeTDataCode() method makes heavy use of the CodeDom to generate the code in a language-neutral manner.

Working with the CodeDom is a strange and tedious experience. You must build up a block of code by building a code tree. In Listing 25.9, a CodeCompileUnit named dataCode is created. A CodeNamespace named dataNS that represents a namespace is created and added to the CodeCompileUnit. And, a CodeTypeDeclaration named datatype that represents a class is added to the namespace. After the class is created, the methods and properties are added to the class block by block.




ASP. NET 2.0 Unleashed
ASP.NET 2.0 Unleashed
ISBN: 0672328232
EAN: 2147483647
Year: 2006
Pages: 276

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net