Wednesday, November 10, 2010

Static reflection... or T4 with EnvDte?

So I like static reflection for getting Class, Property or Method names for databinding, but a lot of people aren't comfortable with the possible performance implications, or for whatever reason (usually FUD).  I've found an alternative using T4+EnvDte.

Code is included in the link...






    <#@ template language="C#" hostspecific="True" debug="True" #>
    <#@ output extension="cs" #>
    <#@ assembly name="System.Core" #>
    <#@ assembly name="System.Xml" #>
    <#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
    <#@ assembly name="EnvDTE" #>
    <#@ assembly name="EnvDTE80" #>
    <#@ import namespace="System.Collections.Generic" #>
    <#@ import namespace="System.IO" #>
    <#@ import namespace="System.Linq" #>
    <#@ import namespace="System.Text" #>
    <#@ import namespace="System.Text.RegularExpressions" #>
    <#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
    <#@ import namespace="EnvDTE" #>
    <#@ import namespace="EnvDTE80" #>
   
        <#
    var serviceProvider = Host as IServiceProvider;
        if (serviceProvider != null) {
            Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
        }

        // Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe
        if (Dte == null) {
            throw new Exception("T4Generator can only execute through the Visual Studio host");
        }
       
        foreach(var project in Dte.Solution.Projects.Cast<Project>()
            .Where(p=> p.CodeModel!=null && p.Name!="Solution Items"))
        {
            //var appRoot= Path.GetDirectoryName(project.FullName) + '\\';
            var rootNamespace = project.Properties.Item("RootNamespace").Value.ToString();
            WriteLine("//Project:"+project.Name);
            #><#
        WriteLine(string.Empty);#>namespace <#= rootNamespace #>.T4Helper{
        <#
           
                WriteLine("#region "+project.Name+ " properties");
                WriteLine(string.Empty);
            foreach(var prop in project.Properties.Cast<EnvDTE.Property>())
            {
           
                Write("//"+prop.Name+" : ");
                try
                    {           
                        WriteLine(prop.Value.ToString());
                    }
                    catch (Exception ex)
                    {
                        WriteLine("Exception : "+ex.Message);
                    }
            }
            WriteLine("#endregion");
            WriteLine(string.Empty);
            ProcessFileCodeModel(project);
            WriteLine(string.Empty);
            WriteLine("}");
        }
       
    #>


<#+
    //const string Kind_PhysicalFolder = "{6BB5F8EF-4483-11D3-8BCF-00C04F8EC28C}";
    bool AlwaysKeepTemplateDirty = true;
    static DTE Dte;

       
void ProcessFileCodeModel(Project project)
    {
        foreach(var item in project.ProjectItems.Cast<ProjectItem>())
        {
            WriteLine("//Found item:"+item.Name);
            if(item.FileCodeModel!=null )
            {
                WriteLine("//Found CodeModel:"+item.FileCodeModel.GetType().FullName);
                RecurseFindElements(item.FileCodeModel.CodeElements);
           
            }
           
        }
       
       
    }
    void WriteMapping(CodeClass codeClass)
    {
        WriteLine("///"+codeClass.FullName);
        WriteLine("public static class "+MakeIntoValidIdentifier(codeClass.Name)+"{");
        PushIndent("\t");
       
        WriteLine("public static string MetaName{get{return \""+codeClass.Name+"\";}}");
       
        RecurseFindElements(codeClass.Children);       
        PopIndent();
            WriteLine("}");
        WriteLine(string.Empty);
    }
    void WriteMapping(CodeProperty codeProperty)
    {
        WriteLine("///"+codeProperty.FullName);
        WriteLine("public static string Meta"+MakeIntoValidIdentifier(codeProperty.Name)+"{ get{ return \""+
            codeProperty.Name+"\";}}");
        //WriteLine("public static string MetaBinding"+MakeIntoValidIdentifier(codeProperty.Name)+"{ get{ return \""
            //+codeProperty.Parent.Name+"."+codeProperty.Name+"\";}}");
    }
    void WriteMapping(CodeFunction codeFunction)
    {
        WriteLine("///"+codeFunction.FullName);
        WriteLine("public static string Meta"+codeFunction.Name+"{ get{ return \""+
            codeFunction.Name+"\";}}");
    }
 private void RecurseFindElements(CodeElements codeElements)
    {
     
      var q=codeElements.Cast<CodeElement>( )
        //.Where(x =>x is CodeInterface||x is CodeClass || x is CodeNamespace) //&& ((CodeInterface)x).Attributes.Count>0)
        //.Where(x => x.Name.StartsWith("System")==false)
        //.Where(x => x.Name.StartsWith("Infragistics")==false)
        //.Where(x => x.Name.StartsWith("Microsoft")==false)
        //.Where(x => x.Name.StartsWith("ICSharpCode")==false)
        ;
        var functions=new List<string>();
      foreach (var element in q)
      {
       
        if (element is CodeFunction && functions.Contains(element.Name)==false)
        {
           
            WriteMapping(element as CodeFunction);
            functions.Add(element.Name);
        }
        if(element is CodeProperty)
        {
            WriteMapping(element as CodeProperty);
        }
        if (element is CodeInterface)
        {
          var c = (CodeInterface)element;
          RecurseFindElements(c.Members);

        }
        if (element is CodeClass)
        {
          var c = (CodeClass)element;
            //verify element is a project element not an external
          if (c.InfoLocation==vsCMInfoLocation.vsCMInfoLocationProject)
            {
                WriteMapping(c);
            }
        
        }
        if (element is CodeNamespace)
        {
           
            var ns=(CodeNamespace)element;
           
          RecurseFindElements(ns.Members);
        }
      }
    }

string MakeIntoValidIdentifier(string arbitraryString)
{
    var validIdentifier = Regex.Replace(arbitraryString, @"[^A-Za-z0-9-._]", " ");
    validIdentifier = ConvertToPascalCase(validIdentifier);
    if (Regex.IsMatch(validIdentifier, @"^\d")) validIdentifier = "_" + validIdentifier;
    return validIdentifier;
}

string ConvertToPascalCase(string phrase)
{
    string[] splittedPhrase = phrase.Split(' ', '-', '.');
    var sb = new StringBuilder();

    sb = new StringBuilder();

    foreach (String s in splittedPhrase)
    {
        char[] splittedPhraseChars = s.ToCharArray();
        if (splittedPhraseChars.Length > 0)
        {
            splittedPhraseChars[0] = ((new String(splittedPhraseChars[0], 1)).ToUpper().ToCharArray())[0];
        }
        sb.Append(new String(splittedPhraseChars));
    }
    return sb.ToString();
}

void EmitNamesInnerClass(List<string> names)
{
    if(names.Any())
    {
        WriteLine("\r\n\t\tpublic static class Names");
        WriteLine("\t\t{");
        foreach(var name in names)
            WriteLine(string.Format("\t\t\tpublic const string {0} = \"{0}\";", name));
        WriteLine("\t\t}");

        names.Clear();
    }
}

Project GetProjectContainingT4File(DTE dte) {

    // Find the .tt file's ProjectItem
    ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile);

    // If the .tt file is not opened, open it
    if (projectItem.Document == null)
        projectItem.Open(Constants.vsViewKindCode);

    if (AlwaysKeepTemplateDirty) {
        // Mark the .tt file as unsaved. This way it will be saved and update itself next time the
        // project is built. Basically, it keeps marking itself as unsaved to make the next build work.
        // Note: this is certainly hacky, but is the best I could come up with so far.
        projectItem.Document.Saved = false;
    }

    return projectItem.ContainingProject;
}

#>

6 comments:

  1. hi,
    thanks for the post is is very useful like example to combine t4 and envdte.
    only one question
    in the line

    "Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
    "

    what is it "SDTE" could be substituted by EnvDTE.DTE
    thanks again

    ReplyDelete
  2. This should be Microsoft.VisualStudio.Shell.Interop.SDTE

    http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.shell.interop.sdte.aspx

    ReplyDelete
  3. thanks again
    I'm not able to find it

    ReplyDelete
  4. sorry for my english, I mean "I wasn't able" until your explanation.
    and thanks for the post.

    ReplyDelete
  5. Hi,

    Thanks for post! Its great!!! Helps a lot. Really good peace of work!

    ReplyDelete
  6. Thanks for the post, it's was really helpful!

    But this algo doesn't work when the project contains folders because the files in the folders is not detected.

    I have resolved the problem by replacing the function ProcessFileCodeModel by :

    void ProcessFileCodeModel(Project project)
    {
    ProcessProjectItems(project.ProjectItems.Cast());
    }

    void ProcessProjectItems(IEnumerable projectItems)
    {
    foreach(var item in projectItems)
    {
    if(item.FileCodeModel!=null )
    {
    RecurseFindElements(item.FileCodeModel.CodeElements);
    }

    if(item.ProjectItems != null)
    {
    ProcessProjectItems(item.ProjectItems.Cast());
    }
    }
    }

    Ps: sry for the indentation

    ReplyDelete