Wednesday, November 5, 2008

Developing a Visual Studio Custom Tool

  • This post contains generic code that's ready for use.
  • The code was tested and debugged.
  • Full solution is available at the end of the post.

This post will walk you through the entire process of creating and registering a 'Custom Tool' for visual studio .NET.

A 'Custom Tool' can be attached to any xml file within your C# project (csproj) - once attached, it creates nested .cs file within which it maintains code that is based on the xml content.

Abstract

Those of you who developed a DataSet using visual studio .net had probably noticed that every time that the DataSet is changed and saved - the .Designer.cs file that is associated with it is automatically refilled with new auto-generated classes that reflect the structure and content (schema) of the DataSet. The generated code allows storing structured data, provides easy access to the DB etc.

In order to auto-generate code based on the DataSet (xsd file) schema - visual studio designer uses build-in 'Custom Tool' called 'MSDataSetGenerator', take a look at the properties of the DataSet (xsd file) - you will see that the 'Custom Tool' field is set to 'MSDataSetGenerator'.

 image 

MSDataSetGenerator is actually a special class that contains the logic for generating code according to DataSet schema. This class is packed in 'Custom Tool' assembly that is installed along with Visual Studio .NET.

Creating 'Custom Tool' for Visual Studio .NET

In order to understand how a 'Custom Tool' works, we'll walk through the implementation of a 'Custom Tool' that creates a simple wrapper class for basic XML.

image

By setting the 'Custom Tool' property of the xml file to XMLWrapperGenerator we've attached our 'Custom Tool' to the xml. Now, every time the xml file is saved - the XMLWrapperGenerator 'Custom Tool' parses it and generates new 'SimpleXmlFile class that is based on its new content.

XMLWrapperGenerator  'Custom Tool' implementation

The XMLWrapperGenerator class inherits from BaseCodeGeneratorWithSite (Microsoft.VisualStudio.BaseCodeGeneratorWithSite). The overridden method 'GenerateCode' is being called by visual studio every time the attached file is saved.

[Guid("52A7B6C6-E3DA-4bfa-A27C-8F1CEFA3DDC8")]
[ComVisible(true)]
public class XMLWrapperGenerator : BaseCodeGeneratorWithSite
{
    private MetadataReader m_MetadataReader;

    // This method is being called every time the attached xml is saved.
    protected override byte[] GenerateCode(
        string inputFileName, string inputFileContent)
    {
        try
        {
            // Try to generate the wrapper file.
            return GenerateCodeSafe(inputFileName);
        }
        catch(Exception ex)
        {
            // In case of a failure - print the exception 
            // as a comment on the .cs file.
            return GenerateExceptionMessage(ex);
        }
    }

    private byte[] GenerateCodeSafe(string inputFileName)
    {
        CodeCompileUnit code = new CodeCompileUnit();

        // Create Namespce
        CodeNamespace codeNamespace = CreateNamespace(code);

        // Read XML
        m_MetadataReader = new MetadataReader(inputFileName);

        // Create the Wrapper Class
        string wrapperClassName = Path.GetFileNameWithoutExtension(inputFileName);
        CreateWrapperClass(codeNamespace, wrapperClassName, m_MetadataReader.Metadata);

        string stringcode = WriteCode(CodeProvider, code);

        return Encoding.ASCII.GetBytes(stringcode);
    }

    private CodeNamespace CreateNamespace(CodeCompileUnit code)
    {
        CodeNamespace codeNamespace = new CodeNamespace(FileNameSpace);
        code.Namespaces.Add(codeNamespace);

        codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
        return codeNamespace;
    }

    private CodeTypeDeclaration CreateWrapperClass(CodeNamespace codeNamespace, 
        string className, Metadata metadata)
    {

        List<ParameterMetadata> parameters = metaData.Parameters;
        // Create the header class
        CodeTypeDeclaration wrapperClass = new CodeTypeDeclaration(className);

        CodeStatementCollection statementCollection = new CodeStatementCollection();
        CodeTypeReference[] parameterReferences = new CodeTypeReference[parameters.Count];
        string[] parameterNames = new string[parameters.Count];

        for (int i = 0; i < parameters.Count; i++)
        {
            ParameterMetadata parameterMetadata = parameters[i];

            CodeTypeReference reference =
                new CodeTypeReference(parameterMetadata.TypeMetadata.Type);

            parameterReferences[i] = reference;
            parameterNames[i] = parameterMetadata.Name;

            // Generate field and add to class
            CodeMemberField memberField = CodeGenHellper.GenerateField(
                wrapperClass, parameterMetadata.Name, reference, false);

            // Add field assingment statement to constructor
            CodeGenHellper.AddAssignStatement(
                statementCollection, memberField.Name, parameterMetadata.Name);

            // Generate property and add to class
            CodeGenHellper.GenerateProperty(
                wrapperClass, memberField, parameterMetadata.Name, true);

        }

        CodeGenHellper.GeneratePublicConstructor(
            wrapperClass,
            wrapperClass.Name,
            parameterReferences,
            parameterNames,
            new string[0],
            statementCollection);

        codeNamespace.Types.Add(wrapperClass);

        return wrapperClass;

    }

    private byte[] GenerateExceptionMessage(Exception ex)
    {
        CodeCompileUnit code = new CodeCompileUnit();
        CodeNamespace codeNamespace = new CodeNamespace(FileNameSpace);
        code.Namespaces.Add(codeNamespace);

        codeNamespace.Comments.Add(new CodeCommentStatement(ex.ToString()));

        string stringcode = WriteCode(CodeProvider, code);

        return Encoding.ASCII.GetBytes(stringcode);
    }

    private static string WriteCode(CodeDomProvider p_provider, CodeCompileUnit code)
    {
        StringWriter stringwriter = new StringWriter();
        p_provider.GenerateCodeFromCompileUnit(code, stringwriter, null);
        string stringcode = stringwriter.ToString();
        stringwriter.Close();

        return stringcode;
    }
}

In 'GenerateCodeSafe' implementation we load the attached xml to MetadataReader class and use it to create the wrapper class (with CodeDome).

Registration 

In the same XMLWrapperGenerator class  we have to include some registration code which will be called with the registration (regasm) of the assembly.

// You have to make sure that the value of this attribute (Guid) 
// is exactly the same as the value of the field 'CustomToolGuid' 
// (in the registration region)
[Guid("52A7B6C6-E3DA-4bfa-A27C-8F1CEFA3DDC8")]
[ComVisible(true)]
public class XMLWrapperGenerator : BaseCodeGeneratorWithSite
{
    #region Registration

    // You have to make sure that the value of this field (CustomToolGuid) is exactly 
    // the same as the value of the Guid attribure (at the top of the class)
    private static Guid CustomToolGuid =
        new Guid("{52A7B6C6-E3DA-4bfa-A27C-8F1CEFA3DDC8}");

    private static Guid CSharpCategory =
        new Guid("{FAE04EC1-301F-11D3-BF4B-00C04F79EFBC}");

    private static Guid VBCategory =
        new Guid("{164B10B9-B200-11D0-8C61-00A0C91E29D5}");


    private const string CustomToolName = "XMLWrapperGenerator";

    private const string CustomToolDescription = "Generate wrapper for XML";

    private const string KeyFormat
        = @"SOFTWARE\Microsoft\VisualStudio\{0}\Generators\{1}\{2}";

    protected static void Register(Version vsVersion, Guid categoryGuid)
    {
        string subKey = String.Format(KeyFormat,
            vsVersion, categoryGuid.ToString("B"), CustomToolName);

        using (RegistryKey key = Registry.LocalMachine.CreateSubKey(subKey))
        {
            key.SetValue("", CustomToolDescription);
            key.SetValue("CLSID", CustomToolGuid.ToString("B"));
            key.SetValue("GeneratesDesignTimeSource", 1);
        }
    }

    protected static void Unregister(Version vsVersion, Guid categoryGuid)
    {
        string subKey = String.Format(KeyFormat,
            vsVersion, categoryGuid.ToString("B"), CustomToolName);

        Registry.LocalMachine.DeleteSubKey(subKey, false);
    }

    [ComRegisterFunction]
    public static void RegisterClass(Type t)
    {
        // Register for both VS.NET 2002 & 2003 (C#) 
        Register(new Version(8, 0), CSharpCategory);

        // Register for both VS.NET 2002 & 2003 (VB) 
        Register(new Version(8, 0), VBCategory);
    }

    [ComUnregisterFunction]
    public static void UnregisterClass(Type t)
    { // Unregister for both VS.NET 2002 & 2003 (C#) 
        Unregister(new Version(8, 0), CSharpCategory);

        // Unregister for both VS.NET 2002 & 2003 (VB) 
        Unregister(new Version(8, 0), VBCategory);
    }

    #endregion
}

In order to register the assembly open visual studio command prompt.

image

Type - '"regasm /codebase [AssemblyFullPath].dll"

image

To un-register type - '"regasm /codebase [AssemblyFullPath].dll /u"

Debugging

Create new solution, create new project and add it to the solution, add the xml file for which you've created the 'Custom Tool' (SimpleXmlFile.xml) , set the 'Custom Tool' property of the xml file (XMLWrapperGenerator), save the solution as 'Sample.sln' and close the solution.

Open the project the contains the 'Custom Tool' (XMLWrapperGenerator.csproj).

From the 'Solution Explorer' select the project and open the properties window -> select the 'Debug' tab -> from 'Start Action' select 'Start external program' -> set the external program to 'C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe' (incase you are using 'Visual Studio 2005') -> set the full path of the solution that you've created at the first step (Sample.sln) in the 'Command line arguments' box -> Insert break-point where ever in your 'Custom Tool' class (for example - in the first line of 'GenerateCode'), Run (F5).

The 'Sample.sln'' solution will be opened, save the xml file (SimpleXmlFile.xml) -> the 'break-point' will hit -> start debugging...

Sample project

Download from here

Usefull Links

http://www.drewnoakes.com/snippets/WritingACustomCodeGeneratorToolForVisualStudio/

3 comments:

  1. Hey man , Thanks , it's very useful for me.

    ReplyDelete
  2. Very great article. Like it.
    In turn I'd like to suggest reading http://techzone.enterra-inc.com/architecture/algorythm-of-defining-plain-polygon-signature-point/

    Worth reading!

    ReplyDelete
  3. Nice! Somebody could use this information to fix the incomplete DataSet generator built into Visual Studio.

    ReplyDelete