Click here to Skip to main content
Licence LGPL3
First Posted 29 Aug 2009
Views 4,720
Downloads 0
Bookmarked 5 times

MetaGen: A project metadata generator for Visual Studio using T4

By Daniel Vaughan | 29 Aug 2009 | Technical Blog
I have used T4 to build a metadata generator for your Silverlight and Desktop CLR projects. It can be used as a replacement for static reflection (expression trees), reflection (walking the stack), and various other means for deriving the name of a property, method, or field.

1

2
1 vote, 33.3%
3

4
2 votes, 66.7%
5
4.50/5 - 3 votes
μ 4.50, σa 2.02 [?]
A Technical Blog article. View original blog here.[]

I am rather excited to share with you something that I have been working on in my spare time for the last couple of days. I have used T4 to build a metadata generator for your Silverlight and Desktop CLR projects. It can be used as a replacement for static reflection (expression trees), reflection (walking the stack), and various other means for deriving the name of a property, method, or field.

There has been much discussion around removing the property name string code smell from INotifyPropertyChanged implementations. Reflection is slow, and various techniques using reflection have been proposed, but have been criticized for contributing to decreased application performance. It now seems reasonable that a language extension for property change notification might be in order. But, as we don't have that yet, I have created the next best thing: a generator.

A couple of days ago, I began exploring T4 (Text Template Transformation Toolkit), and I'm loving it. T4, if you don't already know is versatile templating system that allows you to generate classes, SQL Scripts, etc. from within Visual Studio. If you have Visual Studio 2008, then you already have T4 ready to go! To find out more about T4, visit http://msdn.microsoft.com/en-us/library/bb126445.aspx.

How To Use It

To use MetaGen, simply include the attached MetaGen.tt file in your project. That’s it!

The MetaGen.tt has a number of customizable constants to prevent name collisions in your project.

<!-- code formatted by http://manoli.net/csharpformat/ -->
/// <summary>
/// The modifier to use when outputting classes.
/// </summary>
const string generatedClassAccessModifier = "internal";

/// <summary>
/// The prefix to use for output class and interface names. 
/// The combination of this and <see cref="generatedClassSuffix"/> provides 
/// MetaGen with the ability to identify those classes etc., 
/// for which it should generated metadata, and to ignore MetaGen generated classes.
/// </summary>
const string generatedClassPrefix = "";

/// <summary>
/// The suffix to use for output class and interface names. 
/// The combination of this and <see cref="generatedClassSuffix"/> provides 
/// MetaGen with the ability to identify those classes etc., 
/// for which it should generated metadata, and to ignore MetaGen generated classes.
/// </summary>
const string generatedClassSuffix = "Metadata";

/// <summary>
/// The child namespace in which to place generated items.
/// If there is a class in MyNamespace namespace, 
/// the metadata class will be generated
/// in the MyNamespace.[generatedClassSuffix] namespace. 
/// This string can be null or empty, in which case a subnamesapce 
/// will not be created, and generated output will reside 
/// in the original classes namespace.
/// </summary>
const string generatedNamespace = "Metadata";

/// <summary>
/// The number of spaces to insert for a one step indent.
/// </summary>
const int tabSize = 4;

Template Implementation

The template consists of a procedural portion of code that retrieves the Visual Studio EnvDTE.DTE instance. This allows us to manipulate the Visual Studio automation object model, and to retrieve file, class, and method information and so on. Thanks go out to Oleg Sych for the T4 Toolbox which demonstrated how to retrieve the EnvDTE.DTE from the template hosting environment.

Once we object the relevant EnvDTE.Project we are able to process the EnvDTE.ProjectItems (files and directories in this case) as the following excerpt shows:

<!-- code formatted by http://manoli.net/csharpformat/ -->
IServiceProvider hostServiceProvider = (IServiceProvider)Host;
EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
EnvDTE.ProjectItem containingProjectItem = 
	dte.Solution.FindProjectItem(Host.TemplateFile);
Project project = containingProjectItem.ContainingProject;

/* Build the namespace representations, which contain class etc. */
Dictionary<string, NamespaceBuilder> namespaceBuilders = 
			new Dictionary<string, NamespaceBuilder>();
foreach (ProjectItem projectItem in project.ProjectItems)
{
    ProcessProjectItem(projectItem, namespaceBuilders);
}

We then recursively process EnvDTE.CodeElements and directories in order to create an object model representing the project.

<!-- code formatted by http://manoli.net/csharpformat/ -->
public void ProcessProjectItem
  (ProjectItem projectItem, Dictionary<string, NamespaceBuilder> namespaceBuilders)
{
    FileCodeModel fileCodeModel = projectItem.FileCodeModel;
    
    if (fileCodeModel != null)
    {
        foreach (CodeElement codeElement in fileCodeModel.CodeElements)
        {
            WalkElements(codeElement, null, null, namespaceBuilders);
        }
    }
    
    if (projectItem.ProjectItems != null)
    {
        foreach (ProjectItem childItem in projectItem.ProjectItems)
        {
            ProcessProjectItem(childItem, namespaceBuilders);
        }
    }
}

int indent;

public void WalkElements(CodeElement codeElement, CodeElement parent, 
    BuilderBase parentContainer, Dictionary<string, NamespaceBuilder> namespaceBuilders)
{
    indent++;
    CodeElements codeElements;
    
    if (parentContainer == null)
    {
        NamespaceBuilder builder;
        string name = "global";
        if (!namespaceBuilders.TryGetValue(name, out builder))
        {
            builder = new NamespaceBuilder(name, null, 0);    
            namespaceBuilders[name] = builder;
        }
        parentContainer = builder;
    }
    
    switch(codeElement.Kind)
    {
        /* Handle namespaces */
        case vsCMElement.vsCMElementNamespace:
        {
            CodeNamespace codeNamespace = (CodeNamespace)codeElement;
            string name = codeNamespace.FullName;
            if (!string.IsNullOrEmpty(generatedNamespace) && 
				name.EndsWith(generatedNamespace))
            {
                break;
            }                
            
            NamespaceBuilder builder;
                            
            if (!namespaceBuilders.TryGetValue(name, out builder))
            {
                builder = new NamespaceBuilder(name, null, 0);    
                namespaceBuilders[name] = builder;
            }
                            
            codeElements = codeNamespace.Members;
            foreach (CodeElement element in codeElements)
            {
                WalkElements(element, codeElement, builder, namespaceBuilders);
            }
            break;
        }
        /* Process classes */
        case vsCMElement.vsCMElementClass:
        {
            CodeClass codeClass = (CodeClass)codeElement;
            string name = codeClass.Name;
            if (string.IsNullOrEmpty(generatedNamespace) 
                && name.StartsWith(generatedClassPrefix) 
                && name.EndsWith(generatedClassSuffix))
            {
                break;
            }
            
            List<string> comments = new List<string>();
            comments.Add(string.Format("/// <summary>Metadata for class 
		<see cref=\"{0}\"/></summary>", codeClass.FullName));
            
            BuilderBase builder;
            if (!parentContainer.Children.TryGetValue(name, out builder))
            {
                builder = new ClassBuilder(name, comments, indent);
                parentContainer.Children[name] = builder;
            }
            codeElements = codeClass.Members;
            if (codeElements != null)
            {
                foreach (CodeElement ce in codeElements)
                {
                    WalkElements(ce, codeElement, builder, namespaceBuilders);
                }
            }
            break;    
        }
        /* Process interfaces. */
        case vsCMElement.vsCMElementInterface:
        {
            CodeInterface codeInterface = (CodeInterface)codeElement;    
            string name = codeInterface.Name;
            if (name.StartsWith(generatedClassPrefix) && 
				name.EndsWith(generatedClassSuffix))
            {
                break;
            }
            List<string> comments = new List<string>();
            string commentName = FormatTypeNameForComment(codeInterface.FullName);
            comments.Add(string.Format("/// <summary>Metadata for 
		interface <see cref=\"{0}\"/></summary>", commentName));
            InterfaceBuilder builder = new InterfaceBuilder(name, comments, indent);
            parentContainer.AddChild(builder);
            
            codeElements = codeInterface.Members;
            if (codeElements != null)
            {
                foreach (CodeElement ce in codeElements)
                {
                    WalkElements(ce, codeElement, builder, namespaceBuilders);
                }
            }
            break;
        }
        /* Process methods */
        case  vsCMElement.vsCMElementFunction:
        {
            CodeFunction codeFunction = (CodeFunction)codeElement;
            if (codeFunction.Name == parentContainer.Name 
                || codeFunction.Name == "ToString"
                || codeFunction.Name == "Equals"
                || codeFunction.Name == "GetHashCode"
                || codeFunction.Name == "GetType"                    
                || codeFunction.Name == "MemberwiseClone"
                || codeFunction.Name == "ReferenceEquals")
            {
                break;
            }
            
            string name = codeFunction.Name.Replace('.', '_');
            List<string> comments = new List<string>();
            string commentName = FormatTypeNameForComment(codeFunction.FullName);
            comments.Add(string.Format("/// <summary>Name of method 
			<see cref=\"{0}\"/></summary>", commentName));
            MemberBuilder builder = new MemberBuilder(name, comments, indent);
            parentContainer.AddChild(builder);
            break;
        }
        /* Process properties. */
        case vsCMElement.vsCMElementProperty:
        {
            CodeProperty codeProperty = (CodeProperty)codeElement;
            
            string name = codeProperty.Name.Replace('.', '_');
            if (name != "this")
            {
                List<string> comments = new List<string>();
                string commentName = FormatTypeNameForComment(codeProperty.FullName);
                comments.Add(string.Format("/// <summary>Name of property 
				<see cref=\"{0}\"/></summary>", commentName));
                MemberBuilder builder = new MemberBuilder(name, comments, indent);
                parentContainer.AddChild(builder);
            }
            break;
        }
        /* Process fields. */
        case vsCMElement.vsCMElementVariable:
        {
            CodeVariable codeVariable = (CodeVariable)codeElement;    
            string name = codeVariable.Name;
            List<string> comments = new List<string>();
            string commentName = FormatTypeNameForComment(codeVariable.FullName);
            comments.Add(string.Format("/// <summary>Name of field 
			<see cref=\"{0}\"/></summary>", commentName));
            MemberBuilder builder = new MemberBuilder(name, comments, indent);
            parentContainer.AddChild(builder);
            break;
        }
    }
    indent--;
}

Once this is complete, we output our namespace representations to the resulting MetaGen.cs file as follows:

<!-- code formatted by http://manoli.net/csharpformat/ -->
/* Finally, write them to the output. */
foreach (object item in namespaceBuilders.Values)
{
     WriteLine(item.ToString());
}

What results is a file containing various namespace blocks that include static classes representing our non-metadata classes and interfaces with the project. Property names, method names, and field names are represented as constants. Inner classes are represented as nested static classes.

I have included with this post the MetaGen.tt template file, and also a demo WPF application. If you have any suggestions such as ideas for other metadata information etc., please let me know.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)

About the Author

Daniel Vaughan

Software Developer (Senior)

Switzerland Switzerland

Member

Follow on Twitter Follow on Twitter
Daniel Vaughan is a consultant with a decade of commercial experience across a wide range of industries including finance, e-commerce, and multimedia. While originally from Australia and the UK, Daniel is currently based in Geneva Switzerland; working with WPF, Silverlight, Windows Phone, WCF, and WF within the finance industry. Daniel is a member of the XAML Disciples, and is a Silverlight and WPF Insider.
 
In his spare time Daniel likes to spend time concocting novel ideas, such as employing neural networks to predict user navigation behaviour in WPF applications, and a grid computing framework for Silverlight. Daniel is also the creator of a number of open-source projects including Calcium SDK, and Clog.


Would you like Daniel to bring value to your organisation? Please contact

 
Daniel's Blog | Microsoft MVP profile | Daniel on Twitter



Windows Phone Experts




Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Mobile
Web01 | 2.5.120210.1 | Last Updated 29 Aug 2009
Article Copyright 2009 by Daniel Vaughan
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid