Wednesday, December 19, 2012

Auto-Generating EventArgs classes with T4

T4 is a code generator built into Visual Studio. In this post, I will present a T4 template file which I've used to help auto-generate Event Arguments classes in C#.

Normally, event arguments classes always follow the same structure. You typically have a set of public properties in a class which extends EventArgs and a constructor which takes in the same number of formal arguments.

Here's a typical event arguments class:

public class OperationCompletedEventArgs : EventArgs
{
    public bool Result { get; private set; }

    public OperationCompletedEventArgs(bool result)
    {
        Result = result;
    }
}

Given the repetitive nature of how typical event arguments classes are structured, they make a perfect fit for an example of code generation.



The intent of my template file for generating event arguments classes is to be able to define a declaration of what classes I want to be generated and then let the template generate those classes for me.

The declaration part of the template looks something like this:

var collection = new List<EventArgumentsRepresentation>
                 {
                     new EventArgumentsRepresentation("OperationCompleted")
                     {
                         Members = new Dictionary<string, string> { 
                             {"Result", "bool"}
                         }
                     },
                     new EventArgumentsRepresentation("GenericAndPartial")
                         {
                             AccessModifier = "internal",
                             IsPartial = true,
                             GenericTypes = new List<string>() { "T", "TK"},
                             Members = new Dictionary<string, string> { 
                                 {"GenericMember1", "T"},
                                 {"GenericMember2", "TK"}
                             }
                         }
                 };


Given the above structure, the template would then generate the following two classes for me:

public class OperationCompletedEventArgs : EventArgs
{
    public bool Result { get; private set; }

    public OperationCompletedEventArgs(bool result) 
    {
        Result = result;
    }
}

internal partial class GenericAndPartialEventArgs<T, TK> : EventArgs
{
    public T GenericMember1 { get; private set; }
    public TK GenericMember2 { get; private set; }

    public GenericAndPartialEventArgs(T genericMember1, TK genericMember2) 
    {
        GenericMember1 = genericMember1;
        GenericMember2 = genericMember2;
    }
}

The next I would want a new event arguments class, I would just add a new instance of EventArgumentsRepresentation to the list and let the template do the work.



And now, for the entire template file. If you want to use it for your own projects, create a new "Text Template" (.tt file) in Visual Studio and copy the below template into it. Once you then save the template file, the classes will be generated into a .cs file.

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

<#
var namespaceName = "EventArgumentsGenerationDemo";
var usings = new List<string> {
    "System",
};

var collection = new List<EventArgumentsRepresentation>
                 {
                     new EventArgumentsRepresentation("OperationCompleted")
                     {
                         Members = new Dictionary<string, string> { 
                             {"Result", "bool"},
                         },
                     },
                     new EventArgumentsRepresentation("GenericAndPartial")
                         {
                             AccessModifier = "internal",
                             IsPartial = true,
                             GenericTypes = new List<string>() { "T", "TK"},
                             Members = new Dictionary<string, string> { 
                                 {"GenericMember1", "T"},
                                 {"GenericMember2", "TK"},
                             },
                         },
                     
                 };
#>
namespace <#=namespaceName#>
{
<#
foreach (var usingDirective in usings) {
#>
    using <#=usingDirective#>;
<#
}
#>

    /*
     * This file is autogenerated.  
     *
     * If you want to edit anything from here, use the EventArgumentsClasses.tt file.
     */

<#
foreach (var eventArgsRepresentation in collection) {
#>
    <#=eventArgsRepresentation.ClassSignature#>
    {
<#
foreach (var member in eventArgsRepresentation.Members) {
#>
        public <#=member.Value#> <#=EventArgumentsRepresentation.ToPascalCase(member.Key)#> { get; private set; }
<#
}
#>

        public <#=eventArgsRepresentation.ClassName#>(<#=eventArgsRepresentation.GetConstructorArguments()#>) 
        {
<#
foreach (var member in eventArgsRepresentation.Members) {
#>
            <#=EventArgumentsRepresentation.ToPascalCase(member.Key)#> = <#=EventArgumentsRepresentation.ToCamelCase(member.Key)#>;
<#
}
#>
        }
    }

<#
}
#>
}

<#+
internal class EventArgumentsRepresentation
{
    public string ClassName { get; private set; }

    public string AccessModifier { get; set; }
    public bool IsPartial { get; set; }
    public List<string> GenericTypes {get; set;}
    public Dictionary<string, string> Members { get; set; }
    public string ClassSignature {
        get {
            return String.Format("{0} {1}class {2}{3} : EventArgs", AccessModifier, IsPartial ? "partial " : "", ClassName, GetGenericTypesSignature());
        }
    }

    public EventArgumentsRepresentation(string name)
    {
        ClassName = String.Format("{0}EventArgs", ToPascalCase(name));
        AccessModifier = "public";
        GenericTypes = new List<string>();
        Members = new Dictionary<string, string>();
    }

    public string GetGenericTypesSignature() {
        if (GenericTypes.Count < 1) {
            return "";
        }

        return String.Format("<{0}>", string.Join(", ", GenericTypes.ToArray()));
    }

    public string GetConstructorArguments() {
        if (Members == null || Members.Count < 1) {
            return "";
        }

        return String.Join(", ", Members.Select(k => String.Format("{0} {1}", k.Value, ToCamelCase(k.Key))).ToArray());
    }

    public static string ToPascalCase(string text) {
        return char.ToUpper(text[0]) + text.Substring(1);
    }

    public static string ToCamelCase(string text) {
        return char.ToLower(text[0]) + text.Substring(1);
    }
}
#>