2.7 Custom Build Tools

In C#, J#, and VB.NET projects, all source files have a Custom Tool property. This can be used to process a file at design time, optionally generating another file to be compiled into the project. The most common application of this in VS.NET projects is to generate a type-safe wrapper for the DataSet class from an XML schema file (.xsd). (See Chapter 5 for more information on type-safe DataSet wrappers.) However, this system is extensible, allowing you to add your own custom tools to generate code.

A custom tool is a COM component that VS.NET will run every time the source file changes and is saved. It must implement the IVsSingleFileGenerator COM interface. The main interesting method on this interface is Generate. VS.NET will call this each time the source file is saved, passing in the filename and the contents of the input file. The Generate method returns an array of bytes that will contain either C#, J#, or Visual Basic .NET source code, depending on the type of project. VS.NET saves these bytes to a file, which it compiles when the project is next built. (You can see this file in the Solution Explorer by pressing the Show All Files button.) Because the generated file is compiled as part of the project, IntelliSense will be available during development time for all of the types it defines.

While you could implement the IVsSingleFileGenerator COM interface directly, a managed base class provided in Visual Studio .NET 2002Microsoft.VSDesigner.CodeGenerator.BaseCodeGeneratorWithSiteis much easier to use. To use it, just import the Microsoft.VSDesigner.dll assembly in the Common7\IDE directory of the VS.NET program directory. Your class must be decorated with the Guid attribute to determine its CLSID, but apart from that, the only thing you have to do is write the Generate method itself. The following code shows the implementation of a simple code generator.

[Guid("A0B5E5E9-3DF8-48bc-A6BA-E0DFD35C6237")]

public class MyGenerator : BaseCodeGeneratorWithSite

{

    public override byte[  ] GenerateCode(string file, string contents)

    {

        string code = "public class Foo { }";

        return System.Text.Encoding.ASCII.GetBytes(code);

    }

}

This particular example isn't very interestingit always generates the same code and doesn't bother to examine its input. A more useful tool would generate code based on the input provided.

Once you've built your custom tool, it must be registered as a COM class. (You can do this by running the regasm command-line tool.) You must add certain keys to the registry to let Visual Studio .NET know about your custom tool. Figure 2-14 shows a typical example.

Figure 2-14. Custom tool registry entries
figs/mvs_0214.gif

As you can see, you must add entries under this key:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\Generators

(For VS.NET 2002, use 7.0 instead of 7.1.) Underneath here you will find several GUIDs. These are package IDs, which are listed in Table 2-1, and they determine which languages the custom tool will be available with. (See Chapter 10 for more information about VS.NET packages.) The example in Figure 2-14 shows a generator registered for C#.

Table 2-1. Package IDs used with custom tools

Package ID

Package

{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}

C#

{164B10B9-B200-11D0-8C61-00A0C91E29D5}

VB.NET

{E6FDF8B0-F3D1-11D4-8576-0002A516ECE8}

J#

{20D4826B-C6FA-45db-90F4-C717570B9F32}

Embedded C#

{54307750-4C48-4d2d-B523-A3B42F5C3837}

Embedded VB.NET

To add your own tool, create a new key underneath the relevant package. (So if your tool generates C#, place it under the C# package ID.) The name of the key will be the name the user types into the Custom Tool property in Visual Studio .NET. Set the key's default property to a string describing the tool. Next, add a string value called CLSIDthis must contain the CLSID of your tool (as specified in its Guid attribute; you can generate a new GUID with Tools Create GUID). Finally, add a DWORD value called GeneratesDesignTimeSource, and set it to 1this tells VS.NET that the tool generates source code at design time and that it should be given the opportunity to do so every time the user saves the input file.

Once your custom tool has been registered, using it is just a matter of setting the relevant file's Custom Tool property. You can either set this manually or create a wizard that will do it for you programmatically. (See Chapter 9 for more information on Wizards.)

Unfortunately, with the release of Visual Studio .NET 2003, all of the types in Microsoft.VSDesigner.dll were made private. Not only does this mean that you can no longer derive from BaseCodeGeneratorWithSite, it also hides the implementation of the IVsSingleFileGenerator COM interface. (This is not defined in any type libraries that ship with VS.NETthe only definition for it is the one inside Microsoft.VSDesigner.dll.) This makes it tricky to write a custom tool in VS.NET 2003, as the documentation states that you must implement this interface despite not providing a definition. Fortunately, it doesn't make it impossiblethe COM interface definitions you require are simple, and are shown in Example 2-1.

Example 2-1. Custom tool COM interface definitions
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

[Guid("3634494C-492F-4F91-8009-4541234E4E99")]

public interface IVsSingleFileGenerator

{

    [return:MarshalAs(UnmanagedType.BStr)]

    string GetDefaultExtension(  );

    void Generate([In, MarshalAs(UnmanagedType.LPWStr)] string  wszInputFilePath,

        [In, MarshalAs(UnmanagedType.BStr)] string  bstrInputFileContents,

        [In, MarshalAs(UnmanagedType.LPWStr)] string  wszDefaultNamespace,

        out IntPtr pbstrOutputFileContents,

        [MarshalAs(UnmanagedType.U4)] out int pbstrOutputFileContentsSize,

        [In, MarshalAs(UnmanagedType.Interface)] 

           IVsGeneratorProgress  pGenerateProgress);

   

}

   

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

[Guid("BED89B98-6EC9-43CB-B0A8-41D6E2D6669D")]

public interface IVsGeneratorProgress

{

    [return:MarshalAs(UnmanagedType.U4)]

    void GeneratorError(

        [In, MarshalAs(UnmanagedType.Bool)] bool fWarning,

        [In, MarshalAs(UnmanagedType.U4)] int dwLevel,

        [In, MarshalAs(UnmanagedType.BStr)] string bstrError,

        [In, MarshalAs(UnmanagedType.U4)] int dwLine,

        [In, MarshalAs(UnmanagedType.U4)] int dwColumn);

   

    [return:MarshalAs(UnmanagedType.U4)]

    void Progress(

        [In, MarshalAs(UnmanagedType.U4)] int nComplete,

        [In, MarshalAs(UnmanagedType.U4)] int nTotal);

}

You can then implement the IVsSingleFileGenerator directly. This is slightly more work than it was under VS.NET 2002, because we must now deal with the interop issues that were previously handled by the BaseCodeGeneratorWithSite base class. But this it not too onerous, as shown in Example 2-2.

Example 2-2. Implementing IVsSingleFileGenerator by hand
[Guid("A0B5E5E9-3DF8-48bc-A6BA-E0DFD35C6237")]

public class MyCustomTool : IVsSingleFileGenerator

{

    public byte[  ] GenerateCode(string file, string contents)

    {

        string code = "public class Foo { }";

        return System.Text.Encoding.ASCII.GetBytes(code);

    }

   

    public void Generate(string wszInputFilePath,

        string bstrInputFileContents, string wszDefaultNamespace,

        out IntPtr pbstrOutputFileContents, out int pbstrOutputFileContentsSize,

        IVsGeneratorProgress pGenerateProgress)

    {

        pbstrOutputFileContents = new IntPtr (  );

        pbstrOutputFileContentsSize = 0;

   

        if (bstrInputFileContents =  = null)

            throw new ArgumentNullException(  );

   

        byte[  ] codeBytes = GenerateCode(wszInputFilePath, bstrInputFileContents);

   

        int len = codeBytes.Length;

        pbstrOutputFileContents = Marshal.AllocCoTaskMem(len);

        pbstrOutputFileContentsSize = len;

   

        Marshal.Copy(codeBytes, 0, pbstrOutputFileContents, len);

   

    }

   

    public string GetDefaultExtension(  )

    {

        return ".cs";

    }

}

As you can see, the GenerateCode method here looks exactly the same as beforewe have simply had to supply our own implementation of IVsSingleFileGenerator. This custom tool will work in both VS.NET 2002 and VS.NET 2003.

Although the BaseCodeGeneratorWithSite class was made private with the release of VS.NET 2003, you can still use this class if you want to, instead of using the code in Example 2-1 and Example 2-2. Microsoft has made the source code for this class available for download at http://www.gotdotnet.com/userarea/keywordsrch.aspx?keyword=BaseCodeGeneratorWithSite.