In-Depth

Generate Code from Custom File Formats

Create a custom tool to provide a flexible and easy way to generate code on demand.

TECHNOLOGY TOOLBOX: VB.NET, C#

You're writing the same code over and over again with only minor changes, and you're about to do it again. Stop. If you're creating a standalone class (either a class that you'll call from your application's code or a class that you can inherit from), build a custom tool that writes the code for you now and every time you'll need it.

You're already using a custom tool every time you add a dataset to an application. VS sets the CustomTool property of the dataset's .XSD to MSDataSetGenerator. Whenever you close the .XSD file -- or, in Visual Studio 2008 (VS 2008), whenever you switch away from the file -- VS passes MSDataSetGenerator the contents of the .XSD file, which MSDataSetGenerator then converts to code for the .XSD's code file and hands back to Visual Studio.

You can design your own input format and write your own custom tool to generate code for you. Once you build your custom tool, you simply have to enter its name in the CustomTool property of the file with your input. VS takes care of the rest (see Figure 1).

Using a tool to generate repetitive code ensures consistency and reliability. It guarantees that your code generates the same way every time, reducing the amount of testing you have to do. Using a custom tool can also make you more productive, provided that entering the specifications is faster than writing the code. If you decide you need to enhance your generated code, just modify your custom tool to incorporate your insights. Your class will be regenerated automatically with the new version of the code the next time you build your project.

Installing to the GAC Automatically
Creating a custom tool (also called a "single file generator") presents one problem. VS is a Windows application that calls a custom tool directly, so your custom tool must integrate with Windows and COM. You can create your custom tool in .NET, but you must make your tool available from Windows.

To enable VS to call your tool, you must register it with Windows, add at least one registry key to let VS know your custom tool exists, and install your tool in the Global Assembly Cache (GAC). This means you must give your DLL a strong name. The good news is that you can configure VS so that all of these operations happen every time you compile your application. More good news: Most of the support for creating a custom tool is boilerplate and is identical from one custom tool to another. You only have to write it once and then copy and paste it to any other tool you create.

You can build a custom tool in a variety of ways. I use the Microsoft.VisualStudio.Shell.Interop.IVsSingleFileGenerator and Microsoft.VisualStudio.OLE.Interop.IObjectWithSite interfaces. To create a custom tool, begin by creating a class library in the language of your choice and then add to it references to the Microsoft.VisualStudio.OLE.Interop and Microsoft.VisualStudio.Shell.Interop libraries.

Next, configure your project to have VS call the gacutil utility to put your DLL in the GAC after your application compiles. First, find the full path to wherever gacutil.exe is installed on your computer. In VS, open the Project Properties for your application. If you're working in Visual Basic, select the Compile tab and click on the Build Events button. If you're working in C#, simply select the Build Events tab. Add these lines to the PostBuild event textbox (replace <full path to gacutil> with the path to your copy of gacutil.exe):

"<full path to gacutil>\gacutil.exe" -u "$(TargetName)"
"<full path to gacutil>\gacutil.exe" -i  "$(TargetPath)"

The first line (which uses the TargetName parameter) removes any existing version of your tool from the GAC. The second line (which uses the TargetPath parameter) installs the current version of your tool into the GAC.

When I build a project, I sometimes get an error message that states Microsoft.VisualStudio.Shell.Interop is "not registered for COM interop." This doesn't stop the project from building or interfere with the custom tool executing, so if you get that message, ignore it. However, to ensure that the PostBuild event script executes every time you recompile your DLL, set the dropdown list underneath the PostBuild textbox to "When the build updates the project output."

To install your project in the GAC successfully, you must generate an .SNK file that holds a digital signature (a "strong name"). Go to the Signing tab and check the "Sign the assembly" option. From the dropdown list you just enabled, select <New…> to display the Create a Strong Name Key dialog. Enter a valid file name in the "Key file name" textbox. The resulting file can be read with Notepad, so if you want to ensure that no one but you has access to the file, enter a password in the dialog; if you're not worried about that, then uncheck the password option before clicking on the OK button.

Integrating with Windows and COM
The next step is to configure your project to have your DLL registered with COM/Windows every time you build it. Still in Project Properties, find the "Register for COM interop" checkbox on the Build tab (for C#) or Compile tab (for Visual Basic) and check it. You'll need to revisit this option if you change the project's configuration setting (for example, if you switch from Debug to Release), because the option is cleared when the configuration changes. As part of supporting COM, you also must go to the Application tab and click on the Assembly Information button. Check the "Make assembly COM-Visible" option on the resulting dialog.

Your custom tool must implement two interfaces: IVsSingleFileGenerator and IObjectWithSite. You must assign your class a unique GUID to identify it to Windows. Simply add a System.Runtime.InteropServices.Guid attribute to your class and pass it a GUID. In VS 2008, you can generate a GUID by selecting Create GUID from the Tools menu. In VS 2005, open the VS Command window and run the guidgen utility. Both methods open the Create GUID dialog. Regardless of how you start the tool, pick option 4 to generate a GUID compatible with the Guid attribute (the result will be enclosed in braces that you'll need to delete after pasting the GUID into your code).

Once you're done, your class declaration will look like this in Visual Basic:

<System.Runtime.InteropServices.Guid( _
  "B2429B91-452D-4a95-A355-435337729EFB")> _
  Public Class TextGenerator
  Implements _
   Microsoft.VisualStudio.Shell.Interop. _
   IVsSingleFileGenerator
  Implements Microsoft.VisualStudio.OLE.Interop. _
   IObjectWithSite

For VS to know about your custom tool, you need to add at least one key to the Windows registry. The easiest way to do this is to add two methods to your application: one to add the key and one to delete it. With the right attributes on those methods, they will be called automatically when your custom tool is registered or deregistered with Windows. (The changes you make to your project ensure that your tool is registered and deregistered each time you build it.)

The code in these two methods is mostly -- but not completely -- boilerplate. You need to specify the version of VS that the custom tool works with, provide a GUID that identifies the language of the projects that your tool will work with, provide the name you'll use in the CustomTools property and give a description. In addition, you have to copy the GUID you generated for the GUID attribute into this code.

Rather than rewrite these methods each time you create a custom tool, use a set of variables to hold the values and just update those variables from one tool to another. This VB code creates a tool for C# projects in VS 2008 called TextGenerator:

Shared VSVersion As String = "9.0"
Shared CSLangGUID As String = _
  "{fae04ec1-301f-11d3-bf4b-00c04f79efbc}"
Shared ToolName As String = "TextGenerator"
Shared ToolDesc As String =  _
  "Generates a class from text input"
Shared ToolGUID As String = _
  " B2429B91-452D-4a95-A355-435337729EFB "

<System.Runtime.InteropServices.ComRegisterFunction()> _
  Public Shared Sub RegisterClass(ByVal typ As Type)
  Dim key As Microsoft.Win32.RegistryKey
  key = Microsoft.Win32.Registry.LocalMachine. _
    CreateSubKey( _
    "SOFTWARE\Microsoft\VisualStudio\" & VSVersion & _
    "\Generators\{" & CSLangGUID + "}\" & ToolName & "\")
  key.SetValue("", ToolDesc)
  key.SetValue("CLSID", "{" + ToolGUID + "}")
  key.SetValue("GeneratesDesignTimeSource", 1)
End Sub

<System.Runtime.InteropServices.ComUnregisterFunction> +
Public Shared Sub UnregisterClass(ByVal type As Type)
  Microsoft.Win32.Registry.LocalMachine.DeleteSubKey( _
   "SOFTWARE\Microsoft\VisualStudio\" & VSVersion & _
   "\Generators\" & CSLangGUID & "\" & ToolName & "\", _
   False)
End Sub

If you want to support more than one language (for instance, if your tool works equally well in both C# and VB projects), just write out two keys: one for each language. The language GUID to use for VB is 164B10B9-B200-11D0-8C61-00A0C91E29D5.

Using Standard Code
For the four methods required by the interfaces your class has to implement, you can use the same code for every custom tool you create. For example, the code for the GetSite and SetSite methods never changes:

Private _site As Object

Public Sub GetSite( _
  ByRef riid As System.Guid, ByRef ppvSite As _
  System.IntPtr) Implements _
  Microsoft.VisualStudio.OLE.Interop. _
  IObjectWithSite.GetSite

If Me._site = Nothing Then
  Throw New _
   System.ComponentModel.Win32Exception(-2147467259)
End If

Dim objectPointer As IntPtr = _
  System.Runtime.InteropServices.Marshal. _
  GetIUnknownForObject(Me._site)
Try
  System.Runtime.InteropServices.Marshal. _
   QueryInterface(objectPointer, riid, ppvSite)
  If ppvSite = IntPtr.Zero Then
  Throw New _
   System.ComponentModel.Win32Exception(-2147467262)
  End If
  Catch
  End Try

End Sub

Public Sub SetSite(ByVal pUnkSite As Object) Implements _
  Microsoft.VisualStudio.OLE.Interop. _
  IObjectWithSite.SetSite
  Me._site = pUnkSite
End Sub 

The GetDefaultExtension method provides the extension that VS will add to the file containing your generated code, so you may need to change it. This example causes VS to create a file with the format filename.generated.cs:

Public Function DefaultExtension( _
  ByRef pbstrDefaultExtension As _
  String) As Integer Implements _
  Microsoft.VisualStudio.Shell.Interop. _
  IVsSingleFileGenerator.DefaultExtension
  pbstrDefaultExtension = ".generator.cs"
  Return 0
End Function

When VS is ready to generate your code, it calls the IVsSingleFileGenerator interface's Generate method. VS passes the method the name of the file, the project's namespace, the file's contents, and a progress bar. You must update the method's two ByRef/out parameters with an array of bytes containing your generated code and the length of your array. You should set the array parameter to nothing and the length parameter to 0 if your code generation fails.

Rather than rewrite the Generate method for every custom tool, have the Generate method create a class called CodeGenerator and call a method on it named GenerateCode. Always pass the GenerateCode method the name of the file, the project Namespace, the file contents and the progress bar that VS passes to the Generate method. As a result, the Generate method becomes a set of boilerplate code that looks the same in every custom tool:

Dim generatedCode() As Byte

Try
  Dim cg As SiteGenerator = New CodeGenerator
  generatedCode = cg.GenerateCode(wszInputFilePath, _
   wszDefaultNamespace, bstrInputFileContents, _
   pGenerateProgress)
  rgbOutputFileContents(0) = _
   System.Runtime.InteropServices.Marshal. _
   AllocCoTaskMem(generatedCode.Length)
  System.Runtime.InteropServices.Marshal.Copy( _
   generatedCode, 0, rgbOutputFileContents(0), _
   generatedCode.Length)
   pcbOutput = generatedCode.Length
Catch
  pcbOutput = 0
  rgbOutputFileContents(0) = IntPtr.Zero
End Try

Generating Code
You're now finally ready to start writing the code that's unique to your code generator. Start with a standard version of your CodeGenerator class, which writes to the output file the para­meters passed to it from the Generate method. This code lets you check that your custom tool works and is getting passed what you expect:

Public Function GenerateCode(ByVal FilePath As String, _
  ByVal Namespc As String, ByVal FileContents As String, _
  ByVal Progress As _
  Microsoft.VisualStudio.Shell.Interop. _
  IVsGeneratorProgress) _
  As Byte()

  If FileContents = "" Then
   Throw New Exception("No content")
  End If
  Progress.Progress(0, 100)
  Dim generatedCode As String
  generatedCode = "Namespace: " & Namespc & ", " & _
   "File name:" & FilePath & ", " & _
   "File contents:" & FileContents
  Progress.Progress(100, 100)
  Return System.Text.Encoding.UTF8.GetBytes( _
    generatedCode)

End Function

You must start a new copy of VS to test your custom tool. After the new copy starts, create a project, add a file, fill it with some text, and set the file's CustomTool property to the name of your custom tool. In VS 2008, your custom tool runs as soon as you switch focus away from the file; in VS 2005, you can run your custom tool by closing the file. In either version of VS, you can also right-click on the file in Solution Explorer and select "Run custom tool." Whatever method you choose, you should see a new file added under your existing file with the file extension you specified in the GetDefaultExtension method and holding the output from your GenerateCode method.

Of course, it might not work the first time. Problems usually are related to registering your custom tool with Windows. If this is the case, the message you'll get will typically say that your custom tool cannot be found. A number of typical problems can occur.

First, the GUID you used in the GUID attribute on your class might be different than the Guid you set the ToolGUID variable to. Second, the two methods that add and remove the keys to the Windows registry might be wrong. Check that you've got the right version number for VS and the right GUID for the language you're testing in. For example, did you create a key for C# but then test your custom tool in a VB project? It's helpful to open RegEdit and look at the keys you're generating to see if they look like the other keys in the Generators section.

Finally, your custom tool might not have gotten into the GAC. If a problem exists with your PostBuild event, you'll see a message in your Output window. You can also try removing your custom tool from the GAC by using gacutil –u nameofyourproject. If you get a message stating that your assembly couldn't be found, then you'll know that you have a problem getting your tool into the GAC.

When you're ready to start generating code, replace the line in your GenerateCode method that sets the generatedCode variable with a line that sets the variable to a string containing your code. This example creates a "Hello, World" class:

generatedCode = "namespace " & Namespc & _
  "{public class NewClass{public string" & _
  " SayHello(string Name){return ""Hello, "" + Name;}}}";

You now have a working custom tool. True, the interesting work is still left to do: reading your file's input and turning it into real code. But that's also the fun part.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube