Build Providers for Windows Forms
In ASP.NET and Windows Forms projects, some file types are treated differently than others. For example, ASPX and ASCX files are dynamically parsed and compiled to an assembly on the fly. The contents of an XML Schema Definition (XSD) file are used to create a new, strongly typed DataSet-based object at design time. When you add a reference to an external Web service in Visual Studio, you download the Web Services Description Language (WSDL) file and Visual Studio creates a proxy class to let you place calls to it through a special API. These special features apply to any kind of .NET application—ASP.NET, Windows Forms, and even console applications.
In ASP.NET 2.0, the parse-and-compile model is taken to the next level. Each and every file in a project can be associated with a build provider (a component that parses the contents of the file and produces a compilable unit of code) and parsed at build time, and executable code can be generated on the fly. This build provider model allows you to extend the build engine of ASP.NET with custom code generators, and enables you to use custom syntax (typically, but not necessarily, XML-based syntax) to describe certain application-specific contents that will be translated into C# or Visual Basic and then compiled. Build providers are a great option if you want to avoid writing boilerplate C# or Visual Basic code.
Unfortunately, Windows Forms projects don't support build providers just yet. In this column, after a brief overview of the build provider model in ASP.NET, I'll explore a couple of ways to extend Windows Forms projects in Visual Studio 2005 using a feature that simulates the build providers offered by ASP.NET 2.0.
ASP.NET Build Providers
The compilation model has undergone a significant refactoring, and in ASP.NET 2.0 it can now parse and dynamically compile a number of new file types in addition to pages, user controls, handlers, and Web services. Newly supported file types include XSD for strongly typed DataSets, WSDL for describing external Web services, resource files, themes, and master pages. Furthermore, the model is extensible and you can add your own file types to a list of those recognized and automatically handled by the host environment—be it Visual Studio 2005 or the Web server. The new compilation model eliminates the need for an explicit compilation step handled by Visual Studio when deployed files undergo changes.
Build providers serve two main purposes. They let you deploy application files that are always compiled on demand and automatically recompiled whenever necessary (for example, after a file has been changed). Of course, if you don't like leaving source files lying around the Web server, you can simply choose to precompile the site for deployment and install only binaries and fictitious endpoints for publicly accessible resources.
The second benefit is that you can use build providers as user-defined code generators. Assume, for example, that you place an XML file with a given extension in your project and bind it to a custom provider. At build time, that provider will parse the source file and generate a helper class that will be automatically compiled to an assembly. Plus, this helper class is a legitimate part of the project and enjoys full IntelliSense support in Visual Studio 2005.
Build Providers in Practice
Now, let's look at the sample XML file in Figure 1. It describes mappings between database tables and classes. After parsing this code, a build provider can generate a C# or Visual Basic file that contains a class named Employee with as many public properties as there are columns in the Select command. The class will belong to the specified namespace and, optionally, will be marked as partial.
Figure 1 Mapping Database Tables with Classes
<mappings namespace="MyApp.Entities"> <mapping connectionString= "SERVER=.;DATABASE=northwind;Integrated Security=SSPI" tableName="Employees" className="Employee" selectCommand="SELECT firstname AS FirstName, lastname AS LastName, title As Title FROM employees" allowPartialClass="false" allowCollectionClass="false" collectionClassName="EmployeeCollection" allowGatewayClass="false" gatewayClassName="Customers"> </mapping> <mapping connectionString= "SERVER.;DATABASE=northwind;Integrated Security=SSPI" tableName="Customers" className="Customer" selectCommand="SELECT companyname AS CompanyName, contactname AS Contact, country As Country FROM customers" allowPartialClass="true" allowCollectionClass="true" collectionClassName="CustomerCollection" allowGatewayClass="true" gatewayClassName="Customers"> </mapping> </mappings>
A provider working on the file in Figure 1 could also generate an EmployeeCollection class and even the skeleton of a gateway class if you wanted to build your data access layer in accordance with the principles of the Table Data Gateway design pattern.
Figure 2 Dynamically Created Class
You add the XML file in Figure 1 to the project and have ASP.NET generate the underlying set of classes dynamically. You'll never see the source code of these files, in much the same way you are never exposed to the real source code used to serve a request for an ASP.NET page. You will see the programming interface of the classes created from the XML description through IntelliSense. Figure 2 shows the Customer class created from the code of Figure 1 in action in a Visual Basic project in Visual Studio 2005.
Build Providers in Detail
It's important to know that build providers are an integral part of the build engine of ASP.NET. Build providers are not simply a way to extend the build engine with custom components—they are the only way to build code for ASP.NET applications. There's a build provider behind ASPX and ASCX files. And build providers are used to serve C#, Visual Basic, and many other types of files that may appear in ASP.NET applications. All these build providers are non-public, sealed classes defined in the System.Web.Compilation namespace inside the System.Web assembly. And they all derive from one common root: the BuildProvider class.
To create your own build provider, therefore, you must define a class that inherits from BuildProvider, compile it to an assembly, and override the <compilation> section of the Web.config file to add a new entry. You can register a build provider at various levels—machine, site, or application. Here's what a sample build provider might look like when registered:
<compilation> <buildProviders> <add extension=".map" type="MsdnMag.MapBuildProvider" /> ... </buildProviders> </compilation>
The structure of a build provider looks like the following:
Public Class MyBuildProvider Inherits BuildProvider Public Sub New() End Sub Public Overrides Sub GenerateCode(ByVal ab As AssemblyBuilder) Dim fileName As String = MyBase.VirtualPath Dim code As CodeCompileUnit = BuildCodeTree(fileName) ab.AddCodeCompileUnit(this, code) End Sub '... End Class
The GenerateCode method is virtual but has an empty body in the base class. At the very minimum, you need to override it to build a provider. The VirtualPath property on the base class provides the virtual name of the source file. The BuildCodeTree is a provider-specific function that parses the source file and returns a CodeDOM tree representing the class to create. The CodeDOM tree is then passed as an argument to the AddCodeCompileUnit method of the AssemblyBuilder object. The assembly builder object is a helper class passed to GenerateCode that hides all the details involved in generating the source code for the CodeDOM in the right language and compiling the assembly in a valid location. CodeDOM is the Microsoft .NET Framework API that you use to represent any piece of code as an object graph, similar in form to an Abstract Syntax Tree for those of you familiar with compiler construction. The graph, in turn, can be passed to a language-specific code provider object and translated into compilable code.
Once you have built your own code generator to return a CodeDOM hierarchy, you're pretty much finished if you're working with ASP.NET. Note that you are in no way bound to CodeDOM to generate code on the fly for an ASP.NET build provider. By overriding a different set of methods, you can also pass the code to compile through a TextWriter object. In this case, you just output code as plain strings.
Comparing Build Engines
As of the .NET Framework 2.0, build providers are an exclusive feature of ASP.NET. Period. But it would be amazing to have compilers that accept external plug-ins to preprocess file types and perform some conversion into the proper language. In this scenario, you declaratively set a list of associations between file types, and build plug-ins to pass this information to the compiler. When the C# compiler meets, say, a .map file on its command line, it knows that it has to invoke a code provider component to get the real C# source to work on. (There was a plan for something like this in the early days of the .NET Framework 2.0. Unfortunately, this model is not here yet.)
ASP.NET build providers extend the tailor-made compilation engine of ASP.NET applications. You can't really get anything like that in Windows Forms until the compilers are refactored or a brand new compilation model for Windows applications is invented and implemented. That said, you can quite easily simulate build providers for Windows Forms with a little help from the Visual Studio environment. After all, both Visual Studio .NET 2003 and Visual Studio 2005 let you add an XSD file to a project and have the IDE generate a corresponding C# or Visual Basic .NET reference file. While this is radically different than using ASP.NET 2.0 build providers, the final effect is very similar. You still have an external and reusable component to generate some good code for you. The key differences are in the timing and the actor.
For Windows Forms applications, you take advantage of the characteristics of the Visual Studio build machinery. The file is created locally and then added to the project. It is then passed on to the language compiler as part of the project. In ASP.NET 2.0, this same sequence of steps is abstracted by the compiler infrastructure. To an external observer, in ASP.NET you really compile, say, .map files to assemblies. In Windows Forms, you rely on Visual Studio to generate C# or Visual Basic files with respect to a .map input file. This code generation can happen both at design and build time.
In both Visual Studio .NET 2003 and Visual Studio 2005, content files such as the .map file shown in the code in Figure 1 can be preprocessed using a special component known as a custom tool. In Visual Studio 2005, you have another option: using an MSBuild custom task in the project file.
What is MSBuild?
MSBuild is the new build system integrated in Visual Studio 2005 and also the first release of a new build system designed for the Microsoft platform. Built entirely in managed code, MSBuild processes a new XML-based project file format and allows developers to describe what items need to be built and how to build them. MSBuild doesn't require Visual Studio 2005, so you can build applications on machines even if those machines don't have Visual Studio 2005 installed. The project file format supports a syntax that enables developers to define reusable build rules in separate files. These files, known as targets, can be imported in different projects.
An in-depth look at MSBuild is far beyond the scope of this column, but is worth tackling in the future. For a primer on MSBuild, take a look at the wiki located at Channel9 Wiki: HomePage. A variety of blogs discuss useful tips for MSBuild development. My favorite is the development team's blog located at MSBuild Team Blog.
For this discussion, the key point I want to express about MSBuild is that when you create a Visual Studio 2005 project, the source code of the .csproj or .vbproj file is nothing more than an MSBuild script. This means that if you want Visual Studio 2005 to treat certain content files in a particular way, you must use the proper MSBuild syntax.
Custom Tasks with MSBuild
An MSBuild project file contains references to things like targets, property groups, item groups, and tasks. A target is any final result you expect from the build process. For example, if you're going to compile a few C# files, an assembly is the expected target. Each project file can have one or more targets. Targets refer to a collection of tasks, each with its own set of properties to carry out a particular operation. A group of items represents a collection of related input files to be processed by the system. Finally, a property group defines project-wide properties that can be used to configure tasks and targets.
Figure 3 shows the MSBuild file that compiles a couple of Visual Basic files and generates an assembly named MyApp.exe. The contents of a Visual Studio 2005 .vbproj file are more complex and articulated, but follow the same principles and schema.
Figure 3 Sample MSBuild Project File
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <AssemblyName>MyApp</AssemblyName> </PropertyGroup> <ItemGroup> <Compile Include="test1.vb" /> <Compile Include="test2.vb" /> </ItemGroup> <Target Name="Build" Inputs="@(Compile)" Outputs="$(OutputPath)$(AssemblyName).exe"> <Vbc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" /> </Target> </Project>
The visible part of the MSBuild engine is a console application (msbuild.exe) that is a thin wrapper around the Build API. This API is implemented in the Microsoft.Build.* assemblies in the installation path of the .NET Framework 2.0.
A task represents an action you want to accomplish at some point during the build process. Predefined Visual Studio 2005 targets for C# and Visual Basic already define a number of tasks and properties that you can use in your own scripts. An MSBuild task is a class that inherits from the Task class defined in the Microsoft.Build.Utilities namespace.
Figure 4 illustrates the source code of a sample task. The task, named MakeDirectories, creates a set of directories in the current folder. The names of the directories to be created are specified through a public property. The following code snippet shows one possible use of the MakeDirectories task:
Figure 4 MakeDirectories Sample MSBuild Task
<ItemGroup> <PublishDir Include="c:\temp\publish" /> </ItemGroup> <Target Name="AfterBuild"> <MakeDirectories Condition="!Exists('@(PublishDir)')" Directories="@(PublishDir)" /> <Copy SourceFiles="$(TargetPath)" DestinationFolder="@(PublishDir)" /> </Target>
Imports System.IO Imports Microsoft.Build.Utilities Public Class MakeDirectories Inherits Task Private _directories() As String Public Property Directories() As String() Get Return _directories End Get Set(ByVal value As String()) _directories = value End Set End Property Public Overrides Function Execute() As Boolean For Each directory As String In _directories directory.CreateDirectory(directory) Next Return True End Function End Class
To preprocess a custom file to a C# or Visual Basic class, you need a custom task that takes input and output file paths and converts the custom file to a class. Figure 5 shows the source code of the custom task named MapGeneratorTask. The task has a required property, FileName, which indicates the name of the source file to be preprocessed and another attribute flagged with the <Output()> attribute. The output property indicates an element that represents the task's contribution to the output of the parent target.
Figure 5 MapGeneratorTask
Imports System Imports System.CodeDom Imports System.CodeDom.Compiler Imports System.Text Imports System.IO Imports Microsoft.Build.Framework Imports Microsoft.Build.Utilities Namespace MsdnMag Public Class MapGeneratorTask Inherits Task Private _language As String = "CS" Private _fileName As String Private _outFileName As String Public Property Language() As String Get Return _language End Get Set(ByVal value As String) _language = value End Set End Property <Required()> _ Public Property FileName() As String Get Return _fileName End Get Set(ByVal value As String) _fileName = value End Set End Property <Output()> _ Public Property OutputFileName() As String Get Return _outFileName End Get Set(ByVal value As String) _outFileName = value End Set End Property Public Overrides Function Execute() As Boolean Log.LogMessage("Transforming into a " & Language & _ " file: " & FileName) ' Read the file contents Dim reader As StreamReader = New StreamReader(FileName) Dim inputFileContent As String = reader.ReadToEnd() reader.Close() ' Build a CodeDOM tree of the file to return Dim unit As CodeCompileUnit = _ CodeDomHelpers.BuildCodeTreeFromMapFile(inputFileContent) ' Output to the current language Dim writer As StreamWriter = New StreamWriter(OutputFileName) Dim provider As CodeDomProvider = _ CodeDomProvider.CreateProvider(_language) provider.GenerateCodeFromCompileUnit(unit, writer, Nothing) writer.Close() ' Success Return True End Function End Class End Namespace
The task takes, as input, a custom file and transforms it into a Visual Basic or C# class. This will be added to the output of the target and can be used as input to another MSBuild target. In Figure 5, the CodeDomHelpers static class contains the logic necessary to create a CodeDOM tree out of the input file. There are still a few points to clarify, the first of which is to associate the task with .map files. In addition, you'll need to know how to register the custom task with MSBuild and how to pass the newly created files to the core build engine for actual compilation.
Figure 6 shows a sample MSBuild project file that preprocesses all .map files in the current directory using the sample MapGeneratorTask and then passes the results to the Visual Basic compiler. The <UsingTask> node declares the new task and indicates full class name and assembly. The project file contains two targets: Build and Mapping. The Build target compiles the Visual Basic files listed through a <Compile> node and all the input it receives from the Mapping target. The Inputs attribute of the target lists the input sources. The DependsOnTarget attribute indicates that the Mapping target must be built before proceeding with the core compilation. All input that the main target receives is passed to the Vbc task (the Csc task for C# sources) for actual file compilation.
Figure 6 Using a Custom Task for .map Files
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <UsingTask TaskName="MsdnMag.MapGeneratorTask" AssemblyFile="c:\MyMsBuild\Bin\MyBuildTasks.dll" /> <PropertyGroup> <AssemblyName>MyAsm</AssemblyName> </PropertyGroup> <ItemGroup> <Compile Include="test.vb" /> </ItemGroup> <ItemGroup> <Content Include="*.map" /> </ItemGroup> <Target Name="Build" Inputs="@(Mapping);@(Compile)" Outputs="$(OutputPath)$(AssemblyName).exe" DependsOnTargets="Mapping"> <Vbc Sources="@(Compile);@(Mapping)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" /> </Target> <Target Name="Mapping"> <MapGeneratorTask Language="VB" FileName="%(Content.FullPath)" OutputFileName="$(OutputPath)%(Content.FullPath).vb"> <Output TaskParameter="OutputFileName" ItemName="Mapping"/> </MapGeneratorTask> </Target> </Project>
The following code snippet shows a realistic usage of the MapGeneratorTask. The FileName property owes its value to the FullPath metadata property of the Content node where .map files are bound. The OutputFileName property is created in the project's output path and has the same name as the input file plus the language-specific extension:
<Target Name="Mapping"> <MapGeneratorTask Language="VB" FileName="%(Content.FullPath)" OutputFileName="$(OutputPath)%(Content.FullPath).vb"> <Output TaskParameter="OutputFileName" ItemName="Mapping"/> </MapGeneratorTask> </Target>
The Output node indicates which of the task's properties are used to form the target's output and the name used to reference it in other targets. Figure 7 shows the msbuild.exe application at work on the project file you saw in Figure 6. In the end, the final executable has been built from one source class and a couple of .map files. The build task embedded in the MSBuild project file actually works on three source files—the original file and two additional files that were created from .map files. Note that by writing a slightly more complex MSBuild script, you can make these source classes invisible or temporary by generating them in a separate folder or removing them through a post-build target with a RemoveFile task.
Figure 7 Msbuild.exe in Action on .map Files
Using Custom Files in Visual Studio 2005
For the example I just discussed, I utilized a custom project file. Another option, however, is to extend the project file that Visual Studio 2005 creates for a Visual Basic or C# project, enabling it to support .map files via a custom task. This is a two-step procedure.
First, register the new task with the <UsingTask> element. Next, create a <Target> section to manage .map files; the Mapping target shown earlier is just fine. You don't need to explicitly modify the project file to add <Content> sections for .map files. It's sufficient to add .map files to the Visual Studio 2005 project and select a Content build action for them. This will automatically create the following markup underneath the ItemGroup section with files to process:
One more thing. You still need to inform the principal target about the new dependency. Given the default structure of the .csproj and .vbproj files, here's what you have to enter:
<PropertyGroup> <CoreBuildDependsOn> $(CoreBuildDependsOn); [YourTarget] </CoreBuildDependsOn> </PropertyGroup>
Of course, the YourTarget placeholder will be replaced by the actual name of the target. Note that when you edit project files outside Visual Studio 2005, you will see a security dialog warning that informs you the project has been customized. Simply choose to load the project normally and go.
Custom Tools in Visual Studio
The second possible route for processing custom .map files in Windows Forms is to use custom tools. Custom Tool support is a little known feature of Visual Studio .NET (available since version 2002) and basically refers to the IDE's ability to use external components to preprocess non-class files. Figure 8 shows where you can set the Custom Tool of any item added to the project in the Windows Forms designer of Visual Studio 2005. Built-in custom tools exist for a variety of content files including XSD, RESX, and WSDL. And these represent the chief technology that enables the creation of reference files for certain well-known resources. Basically, whenever you get a typed DataSet in response to the addition of an XSD file, there's a custom tool working behind the scenes.
Figure 8 Setting the Custom Tool
In the October 2002 issue of MSDNMagazine, Jon Flanders and Chris Sells wrote the article "Top Ten Cool Features of Visual Studio .NET Help You Go From Geek to Guru". In the article, custom tools were number two on the list. Custom tools work nearly the same way in Visual Studio 2005.
A Visual Studio custom tool is a COM object that is expected to implement a few interfaces, including IVsSingleFileGenerator and IObjectWithSite. If you want to write custom tools with managed code, you need to employ the BaseCodeGeneratorWithSite class. The class is defined in the newest Microsoft.VisualStudio.Design system assembly, but it is marked as internal and cannot be used to derive from. So what do you do? Well, the development team provided equivalent code in an assembly that I've included in this month's sample code. Say, for example, that a custom tool is a class that inherits from the BaseCodeGeneratorWithSite class. The code in Figure 9 shows a Visual Studio custom tool that uses the same CodeDOM generator employed in the MSBuild task to create a class file based on a .map file.
Figure 9 Visual Studio Custom Tool for .map Files
Imports System Imports System.Collections.Generic Imports System.Runtime.InteropServices Imports Microsoft.CustomTool Imports System.CodeDom Imports System.CodeDom.Compiler Imports System.IO Namespace MsdnMag <ComVisible(True)> _ <Guid("DB336537-7F85-441b-909B-97595D6E9590")> _ <CLSCompliant(False)> _ Public Class TableMappingGenerator Inherits BaseCodeGeneratorWithSite Public Overrides Function GetDefaultExtension() As String Return ".map" + MyBase.GetDefaultExtension() End Function Protected Overrides Function GenerateCode( _ ByVal inputFileName As String, _ ByVal inputFileContent As String) As Byte() ' Build a CodeDOM tree of the file to return Dim unit As CodeCompileUnit = CodeDomHelpers.BuildCodeTreeFromMapFile(inputFileContent) ' Output to the current language Dim writer As New StringWriter CodeProvider.GenerateCodeFromCompileUnit(unit, writer, Nothing) Dim code As String = writer.ToString() writer.Close() Return System.Text.Encoding.ASCII.GetBytes(code) End Function End Class End Namespace
You don't have to be a rocket scientist to understand how the code of a custom tool works. It consists of overriding the GenerateCode method to return an array of bytes and optionally the GetDefaultExtension method to specify the extension of the generated file. Note that, unlike MSBuild tasks, the Visual Studio Custom Tool feature is specifically designed to transform one file into another with different content. More importantly, you don't have to do anything to force the compilation of these dynamically created reference files. Visual Studio automatically tweaks the project file to request compilation.
The trickiest part of the story is how you make a custom tool visible to the Visual Studio 2005 IDE. First and foremost, you need to register the custom tool class as a COM object. You do this with a call to regasm.exe.
regasm /codebase YourCustomTool.dll
If you don't install the assembly that you are registering into the global assembly cache, you'll need to use the /codebase switch. Next, you need to list your custom tool with the built-in custom tools in the registry. The entry to look for is: HKLM\SOFTWARE\Microsoft\VisualStudio\8.0\Generators. You'll find three subkeys, each with a GUID. These GUIDs correspond to installed languages (in other words, you could opt to register a custom tool for one language but not another). For brevity, I'm not showing the entire GUID here but will limit it to the first three characters. The GUID that starts with "164" is for VB; "e6f" is for J#; "fae" is for C#. You choose your language (or languages) and create a key with the name of the custom tool. This name is important as it represents the string to type in the dialog box shown in Figure 8. The key requires two entries—CLSID and GeneratesDesignTimeSource. The former is a string that points to the GUID of the custom tool class—the value of the [Guid] attribute in Figure 9. The latter is a DWORD entry and must be set to 1.
Putting It All Together
You're now all set and can start working with a sample Windows Forms project with a form and the nwind.map file shown in Figure 1. Make sure the .map file has a build action of Content and remember to set Custom Tool to the name of the generator—for example, TableMappingGenerator. If you double-click the .map file and click Run Custom Tool, you should see a child node appear beneath the .map file. If not, make sure you have the Show All Files feature turned on (see Figure 2).
Figure 10 offers a preview of the code you get from either the custom tool or the custom build action. Incidentally, it is the same code you can obtain in ASP.NET 2.0 using a build provider. Actually, all that you need is a made-to-measure CodeDOM library to describe the code you want to obtain with respect to a given input file. Once you have such a library up and running, you can create a variety of wrappers for it—build providers, custom tools, and MSBuild tasks. And note that the .map file can generate a partial class. When this happens, you can extend the code of the dynamically generated class through other partial classes throughout the project.
Figure 10 Class Behind the .map File
'------------------------------------------------------------------------- ' <auto-generated> ' This code was generated by a tool. ' Runtime Version:2.0.50215.44 ' ' 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.Collections.Generic Namespace MsdnMag Partial Public Class Customer Private _companyname As String Private _contact As String Private _country As String Public Overridable Property CompanyName() As String Get Return Me._companyname End Get Set(value As String) Me._companyname = value End Set End Property Public Overridable Property Contact() As String Get Return Me._contact End Get Set(value As String) Me._contact = value End Set End Property Public Overridable Property Country() As String Get Return Me._country End Get Set(value As String) Me._country = value End Set End Property End Class Public Class CustomerCollection Inherits List(Of Customer) End Class Partial Public Class Customers Public Shared ReadOnly Property ConnectionString() As String Get Return "SERVER=.;DATABASE=northwind;Integrated Security=SSPI" End Get End Property End Class End Namespace
Dino Esposito is the co-author of “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) and “Programming ASP.NET MVC 5” (Microsoft Press, 2014). A technical evangelist for the .NET Framework and Android platforms at JetBrains and frequent speaker at industry events worldwide, Esposito shares his vision of software at software2cents.wordpress.com and on Twitter at twitter.com/despos.
Thanks to the following Microsoft technical expert for reviewing this article: James McCaffrey