Monday, December 15, 2008

Design Patterns: Visitor vs. Strategy (with C# .NET Sample)

Both visitor and strategy design patterns offer a way of separating an algorithm from an object, in this post we'll review the implementation of the patterns in a simple car wash application and try to figure out in which cases each pattern should be applied.  

Basic Object Model

The Car object is composed out of wheel, engine and body, each implements ICarElement.

image

As for now, we know that the application should support 2 kind of operations:

1. washing the car by steam.

2. washing by water.

however, the design should support more operations that will be added in the future.

Straight forward Implementation

With the most simple and straight forward approach the operations are implemented in the car elements body.

image

image

image The good thing is that it's all right in front of you, if you want to browse through the operations you don't have to look around, you go directly to the basic object model.

image The problem with the straight forward approach is that the basic object model has to be changed with each new operation that will be required in the future, as for each new operation all the elements of the car (wheel, engine and body) have to be added with an extra function. Also, the tight coupling between the basic object model and the operation makes it harder to replace operation implementation of the fly.

Strategy

With the Strategy approach there's a separate strategy object for each one of the operations. image

The element is assigned with the proper strategy, and when it is requested to do a wash - it delegates the request to the strategy that performs the wash. 

 image

imageWith this approach the basic object model is not required to change with every new operation, and operation can be gracefully replaced on the fly.

image The problem is that there are too many strategy objects, in order to replace a washing technic from 'by water' to 'by  stream' - three strategies have to be replaced. Also, with each new operation three strategies (one for each element) have to be added to the application.

Visitor

With the Visitor approach there's one visitor object for each kind of operation ('wash by water', 'wash by steam'); the visitor object contains operation implementation for all the elements of the car (wheel, engine and body). For example, the 'stream wash' operation kind has SteamWashVisitor that contains steam washing implementation for all the elements of the car.

image

When the element is requested to do a wash - it calls the appropriate method on the visitor. 

image

imageWith this approach the basic object model is not required to changed with every new operation, and operation implementations for all the elements are bundled in one Visitor that can be replaced as a whole. For example, in order to replace a washing technic from 'by water' to 'by  stream' - all we have to do is to replace the Visitor object from WaterWashVisitor to SteamWashVisitor.

image When ever the basic object model changes all the Visitors have to be changed as well, for example, if a new 'Door' object is added to the car - all the visitors have to be added with an extra 'VisitDoor' method. This is why the Visitor pattern is applicable only if the basic object model doesn't tend to change.

Summery

In most cases the operations are implemented in the body of the object on which they operate.

Often, operations are implemented in an external package or have to be replaced of the fly according to application rules, for this strategies can be used to allow external operations injection and graceful operations replacement.

In other cases (not that often) the same kind of operation has to be executed on several elements (that usually share interface) in the object model, and there's a need to easily replace the entire set of operations on the fly. Only in condition that the object model is steady and doesn't tend to change - visitors can be used to group set of operations in one object that can be  easily replaced.

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/