4

I have started dabbling in T4 and first got along pretty well, but then ran into an issue that's actually pretty obvious and might not be solvable, but maybe there is a way that I just lack the experience to know or see.

Given the following class:

public class T4Test : CodeActivity
{
    protected override void Execute(CodeActivityContext context)
    {
    }

    [Input("InX")]
    public InArgument<string> InX { get; set; }

    [Output("OutX")]
    public OutArgument<string> OutX { get; set; }
}

I want this as the output:

public class ActivityWrapper
{
    private readonly T4Test _activity;
    private readonly ActivityContext _context;

    public ActivityWrapper(T4Test activity, ActivityContext context)
    {
        this._activity = activity;
        this._context = context;
    }

    public string InX
    {
        get { return this._activity.InX.Get(this._context); }
    }

    public string OutX
    {
        get { return this._activity.OutX.Get(this._context); }
        set { this._activity.OutX.Set(this._context, value); }
    }
}

I have figured out the Reflection stuff I need, and I know what the T4 code should look like, but there's one problem: I need it in the same project as the T4Test class. However, to load the assembly and reflect over it, it needs to be compiled - but of course that's a bit difficult if I intend to modify that same assembly's code. (And I guess NCrunch doesn't simplify things.)

Now here's the things that I hope might still make it possible to solve this:

  • The project will compile without the generated class. This is because the class will implement interfaces that will be auto-registered/-resolved by an IoC container. It is also not testable anyway, because ActivityContext can't be mocked.
  • For that reason it doesn't have to be there or correct all the time. I just need to be able to say "generate this now" before actually delivering the DLL.
  • For the same reason I also don't care whether the T4 template actually sits in the project - as long as the generated file ends up in the project (though without needing another project for the template and constructing PostBuild events to copy a .cs file around).
  • To be precise, it doesn't even need to be T4. If there's any other feasible way to do it, I'll be happy to use that as well.

Is there any way to achieve this? (And was that even clear enough?)

TeaDrivenDev
  • 6,591
  • 33
  • 50
  • 2
    Have you considered generating the entire class in the T4 template? I.e., define a few helper methods to create a small DSL and then list the class members to be generated in the template? – dtb Mar 25 '13 at 23:05
  • You mean defining the properties in a different form (XML or so) and then generating both classes from the template? That might actually work, given the limited variation I'm dealing with. What's behind the "helper methods" thing; what would those be for? – TeaDrivenDev Mar 26 '13 at 00:15
  • Yes, generate both classes from the template. I'll post an example to illustrate the helper methods. – dtb Mar 26 '13 at 01:32
  • A more complicated answer is using roslyn to parse the C# syntax tree but honestly @dtb suggestion is better. – Just another metaprogrammer Mar 26 '13 at 18:08

2 Answers2

6

I would like to propose an alternative to reflecting the generated assembly, since transforming the T4 only works when the project successfully built and generates proper output iff the assembly is not outdated.

If you use a hostspecific T4 template you gain access to the Visual Studio automation model through the EnvDTE interfaces. Using this you can walk the CodeModel of your currently loaded Visual Studio solution without the need to building it first.

Have a look at my answer to this SO question: Design Time Reflection. Using the aid of the free template from tangible's Template Gallery you could easily "reflect" your existing classes at design time and detect properties decorated with the desired attributes:

<#
var project = VisualStudioHelper.CurrentProject;

// get all class items from the code model
var allClasses = VisualStudioHelper.GetAllCodeElementsOfType(project.CodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false);

// iterate all classes
foreach(EnvDTE.CodeClass codeClass in allClasses)
{
    // iterate all properties
    var allProperties = VisualStudioHelper.GetAllCodeElementsOfType(codeClass.Members, EnvDTE.vsCMElement.vsCMElementProperty, true);
    foreach(EnvDTE.CodeProperty property in allProperties)
    {
        // check if it is decorated with an "Input"-Attribute
        if (property.Attributes.OfType<EnvDTE.CodeAttribute>().Any(a => a.FullName == "Input"))
        {
            ...
        }
    }
}
#>
Community
  • 1
  • 1
Nico
  • 2,120
  • 16
  • 20
  • Thank you. This is pretty much what I was originally looking for, though apparently, given the nature of my generation input, I wasn't looking for the right thing. – TeaDrivenDev Mar 28 '13 at 00:31
3

T4Test.tt

<#@ include file="Activities.tt" #>
<#
var t4test = new Activity("T4Test")
{
    Input("InX"),
    Output("OutX"),
};
GenerateCode(t4test);
#>

Activities.tt

<#@ template language="C#" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#+
class Activity : IEnumerable<Property>
{
    private string name, wrapper;
    private List<Property> properties;
    public Activity(string name, string wrapper = null)
    {
        this.name = name;
        this.wrapper = wrapper ?? name + "Wrapper";
        this.properties = new List<Property>();
    }
    public void Add(Property property)
    {
        this.properties.Add(property);
    }
    public IEnumerator<Property> GetEnumerator()
    {
        return this.properties.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    public void GenerateCode()
    {
        // ...
    }
}
class Property
{
    private bool output;
    private string name, type;
    public Property(bool output, string name, string type)
    {
        this.output = output;
        this.name = name;
        this.type = type;
    }
}
Property Input(string name, string type = "string")
{
    return new Property(false, name, type);
}
Property Output(string name, string type = "string")
{
    return new Property(true, name, type);
}
void GenerateCode(params Activity[] activities)
{
    WriteLine("namespace Foo");
    WriteLine("{");
    PushIndent("   ");
    foreach (var activity in activities)
    {
        WriteLine("class " + activity.name);
        WriteLine("{");
        PushIndent("   ");
        // ...
        PopIndent();
        WriteLine("}");
    }
    PopIndent();
    WriteLine("}");
}
#>
dtb
  • 213,145
  • 36
  • 401
  • 431
  • Thanks; I'll have to look at this in a better condition tomorrow. What I seem to understand now is that in everyday use, this would be a `.tt` file that lacks the second code block (`var t4test` etc.) when I drop it into a project, upon which I only need to complete it by adding that declaration, right? That might actually save even more work than just generating the wrapper class, as the activity class itself is already nearly all boilerplate. – TeaDrivenDev Mar 26 '13 at 01:53
  • Yes. You can even split the template into two files. I've updated the example accordingly. – dtb Mar 26 '13 at 05:39
  • Thanks; this seems to be the most flexible and efficient option - but how do I actually generate the code? How do I write to the output file from `GenerateCode()`? – TeaDrivenDev Mar 27 '13 at 23:58
  • It seems you can't generate code from nested classes; I've updated the example accordingly. – dtb Mar 28 '13 at 00:07
  • I see now; the `WriteLine` stuff was what I was looking for. I figured it should work this way but wasn't aware of the method name. Stuff starts falling into place now. :-) – TeaDrivenDev Mar 28 '13 at 00:12
  • I started actually implementing my real requirements with this and found that I don't even need the `WriteLine` calls - I can just toggle between the `GenerateCode` function and real template text with `#>` and `<#+` tags at any point in the code. So, what I ultimately needed was simply the idea to just generate everything from a single terse "DSL" definition as the underlying task lends itself to that very well. – TeaDrivenDev Apr 05 '13 at 13:48