Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Source code generator for any data type

0.00/5 (No votes)
5 May 2008 1  
How to extend Visual Studio so it can generate code for any data type.

Introduction

Do you sometimes need a class for a complex data type? I do, but I think it too much work to type it myself. Did you know that .NET Framework has a tool to generate classes from XSD-files? It does, but it requires a command prompt, and every time you change the XML-schema, you have to run it again. Did you know that Visual Studio can work with Custom Tools? It does, but how to create one can be quite a mystery, at least, it was to me.

This article will describe how to create a custom tool, install it, and use it. The example can generate classes from XML-schemas which you can incorporate right into Visual Studio 2005 and 2008 (tested).

Background

Visual Studio uses xsd.exe internally when creating datasets. However, it is also capable of generating strongly typed classes from other XSD-files, which I will demonstrate in this article.

In order to create a Custom Tool, you need to create a class that inherits from Microsoft.VisualStudio.Design.BaseCodeGeneratorWithSite, which, for some reason, is internal, and therefore cannot be inherited from. Gert Servranckx created a class called Microsoft.CustomTool.BaseCodeGeneratorWithSite, which can be inherited from.

Using the code

The project

First of all, I created a new class library project, which I called "xsd2class". Because a Custom Tool is a COM object, I checked COM-Visible (in Properties > Application > Assembly Information). Copy the project's GUID, because you'll need it again when we define the Custom Tool's class.

This particular COM object is, in fact, a .NET assembly that Visual Studio should be able to find, so it needs to be in the GAC. In order to do that, it needs to be signed, so I created a new key.snk and signed the assembly with it. It saves some work if the assembly is added to the GAC automatically, so I set up a Post-build event with the command line:

"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm" /codebase "$(TargetPath)" 

The Custom Tool-class skeleton

As stated before, we need a class that is COM-Visible and inherits from Microsoft.CustomTool.BaseCodeGeneratorWithSite, so let's create one. Let's start off by adding a reference to the assembly "Microsoft.VisualStudio.BaseCodeGeneratorWithSite.dll". Then, add a new class as follows. (The value for the Guid attribute is the same as the GUID you copied from the Assembly info.)

namespace Xsd2Class
{
    [ComVisible(true)]
    [Guid("0e439c89-b1be-489d-a0f4-c2d191db6f9b")]
    public class Xsd2Class : Microsoft.CustomTool.BaseCodeGeneratorWithSite
    {
    }
}

The Microsoft.CustomTool.BaseCodeGeneratorWithSite.GenerateCode method is abstract, so we have to override it:

protected override byte[] GenerateCode(string inputFileName, string inputFileContent)
{
    return new byte[] { };
}

In order for the Custom Tool to be recognized by Visual Studio, it needs to be registered in the Registry (where else):

  • "HKLM\SOFTWARE\Microsoft\VisualStudio\8.0\Generators" for Visual Studio 2005 and
  • "HKLM\SOFTWARE\Microsoft\VisualStudio\9.0\Generators" for Visual Studio 2008.

The Generators key has subkeys with GUIDs as names. This is what they mean:

  • {164B10B9-B200-11D0-8C61-00A0C91E29D5}: Visual Basic
  • {E6FDF8B0-F3D1-11D4-8576-0002A516ECE8}: J#
  • {FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}: C#
  • Other: Don't know.

We've got to register our Custom Tool for all three languages. So, in each key, we create another key with the name "Xsd2Class" and the values:

  • (Default): String: Xsd2Class (This is the name of our custom tool)
  • CLSID: String: {0e439c89-b1be-489d-a0f4-c2d191db6f9b} (The GUID you copied from the Project's assembly information) and
  • GeneratesDesignTimeSource: DWORD: 1

For Visual Studio 2005, the reg file would be:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\
      Generators\{164B10B9-B200-11D0-8C61-00A0C91E29D5}\Xsd2Class]
@="Xsd2Class"
"CLSID"="{0e439c89-b1be-489d-a0f4-c2d191db6f9b}"
"GeneratesDesignTimeSource"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\
      Generators\{E6FDF8B0-F3D1-11D4-8576-0002A516ECE8}\Xsd2Class]
@="Xsd2Class"
"CLSID"="{0e439c89-b1be-489d-a0f4-c2d191db6f9b}"
"GeneratesDesignTimeSource"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\8.0\
      Generators\{FAE04EC1-301F-11d3-BF4B-00C04F79EFBC}\Xsd2Class]
@="Xsd2Class"
"CLSID"="{0e439c89-b1be-489d-a0f4-c2d191db6f9b}"
"GeneratesDesignTimeSource"=dword:00000001

For Visual Studio 2008, simply replace "8.0" with "9.0".

And now, we've got ourselves a working Custom Tool, doing absolutely nothing!

Doing something

Xsd.exe

As said before, xsd.exe is a command line tool. So, let's create a wrapper for it.

private void GenerateCodeFile(string inputFileName)
{
    FileInfo FI = new FileInfo(inputFileName);

    Process proc = new Process();
    proc.StartInfo.FileName = @"C:\Program Files\" + 
         @"Microsoft Visual Studio 8\SDK\v2.0\Bin\xsd.exe";
    string Namespace = FI.Name.Substring(0, FI.Name.Length - FI.Extension.Length);
    string Language = GetLanguage();
    proc.StartInfo.Arguments =
                String.Format(@"/c /l:{0} /n:{1} ""{2}"" /out:""{3}""", 
                Language, Namespace, inputFileName, FI.DirectoryName);
    proc.StartInfo.UseShellExecute = false;
    proc.StartInfo.CreateNoWindow = true;
    proc.StartInfo.RedirectStandardError = true;
    proc.Start();
    proc.WaitForExit();
    if (proc.ExitCode != 0)
    {
        using (StreamReader SR = proc.StandardError)
        {
            string Errors = SR.ReadToEnd();
            if (!(string.IsNullOrEmpty(Errors)))
            {
                throw new Exception(Errors);
            }
        }
    }
}

We create a new Process and let the StartInfo's FileName point to xsd.exe. xsd.exe has a few arguments:

  • /c: Create a Class (not a DataSet),
  • /l: Language (VB for Visual Basic, CS for C#, etc.),
  • /n: namespace
  • Filename
  • /out: the directory in which to write the generated source code.

Usually, the Language parameter has the same value as the return value of the base class' GetDefaultExtension method. However, I noticed that if I create a new class in J#, that file has a .vjs extension, while the language attribute should be "jsl". The GetLanguage method makes this mapping.

If we were to have a Books collection with Book elements where every Book has an Author and a Title, and if we omitted the namespace parameter, the Books and Book objects would be created in the default namespace, which gets messy if you work with many objects. So, we'd better provide one. If the XSD file is called Books.xsd, I think "Books" is a nice namespace.

The filename is the inputFilename parameter of the GenerateCode method, and the output directory is the directory of the inputFile.

Because we don't want to see the command prompt appearing every time, we set UseShellExecute to false and CreateNoWindow to true.

Because we want to know if something goes wrong, we redirect the standard error.

Returning the results

The GenerateCode method needs the results as a byte array, so let's just read the XSD's output and return it as a byte array:

private byte[] GetGeneratedFileContent(string inputFileName)
{
    string DefaultExtension = GetDefaultExtension();
    string outputFilename = inputFileName.Replace(".xsd", DefaultExtension);
    FileInfo FI = new FileInfo(outputFilename);
    using (StreamReader FS = FI.OpenText())
    {
        string Contents = FS.ReadToEnd();
        return System.Text.Encoding.ASCII.GetBytes(Contents);
    }
}

Putting it together

Now, putting it together...

protected override byte[] GenerateCode(string inputFileName, 
                          string inputFileContent)
{
    GenerateCodeFile(inputFileName);
    byte[] GeneratedFileContent = 
           GetGeneratedFileContent(inputFileName);
    return GeneratedFileContent;
}

... and we've got ourselves a working Custom Tool that can generate source code from an XSD file!

User manual

That's all very nice, but how do I use it? Simple :-)

  1. Add a new XML schema to your project.
  2. addnewitem.png

  3. Write the schema.
  4. schema.png

    <?xml version="1.0" encoding="utf-8"?>
    <xs:schema 
        id="Books" 
        targetNamespace="http://tempuri.org/Books.xsd" 
        elementFormDefault="http://tempuri.org/Books.xsd" 
        xmlns:mstns="http://tempuri.org/Books.xsd" 
        xmlns:xs="http://www.w3.org/2001/XMLSchema">
      <xs:element name="Books" type="Books">
        <xs:annotation>
          <xs:documentation>
            Collection of Books.
          </xs:documentation>
        </xs:annotation>
      </xs:element>
      <xs:complexType name="Books">
        <xs:sequence>
          <xs:element name="Book" type="Book">
            <xs:annotation>
              <xs:documentation>
                A book with a title and an author.
              </xs:documentation>
            </xs:annotation>
          </xs:element>
        </xs:sequence>
      </xs:complexType>
      <xs:complexType name="Book">
        <xs:sequence>
          <xs:element name="Title" type="xs:string">
            <xs:annotation>
              <xs:documentation>
                The Book's main title.
              </xs:documentation>
            </xs:annotation>
          </xs:element>
          <xs:element name="Author" type="xs:string">
            <xs:annotation>
              <xs:documentation>
                The name of the main writer.
              </xs:documentation>
            </xs:annotation>
          </xs:element>
        </xs:sequence>
      </xs:complexType>
    </xs:schema>
  5. Set the Custom Tool to use.
  6. properties.png

    customtool.png

  7. Run the custom tool (or save your schema).
  8. runcustomtool.png

Et voila: your strongly typed Books class:

result.png

Conclusion

First of all, I hope you find this tool useful :-). I also hope this will inspire you to write your own Custom Tools, which you hopefully will write an article about.

History

  • 2008-04-11: Initial article.
  • 2008-05-05: Changed the GAC remark.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here