Preprocessed Razor Templates

Wednesday, January 5, 2011

Preprocessed T4 templates generate all the code you need to execute a template at runtime.

You can do the same "preprocessing" with Razor, but it takes a little more work.

The first step is creating a custom tool for Visual Studio. Visual Studio will feed the template into this tool and expect you to generate the contents of a code file to include in the compilation phase of a project. Visual Studio communicates with the tool using the IVsSingleFileGenerator interface.

[Guid("2678AC94-69B1-4B2B-8939-F15BE231C468")]
public class RazorGenerator : IVsSingleFileGenerator
{
    public int DefaultExtension(out string pbstrDefaultExtension)
    {
        pbstrDefaultExtension = ".cs";
        return pbstrDefaultExtension.Length;
    }

    public int Generate(string wszInputFilePath,
                        string bstrInputFileContents,
                        string wszDefaultNamespace,
                        IntPtr[] rgbOutputFileContents,
                        out uint pcbOutput,
                        IVsGeneratorProgress pGenerateProgress)
    {
        var engine = InitializeEngine(wszDefaultNamespace, 
                                      wszInputFilePath);
        return GenerateCode(wszInputFilePath, engine, 
                            rgbOutputFileContents, out pcbOutput);
    }

    private int GenerateCode(string wszInputFilePath, 
                             RazorTemplateEngine engine, 
                             IntPtr[] rgbOutputFileContents, 
                             out uint pcbOutput)
    {
        using (var file = File.Open(wszInputFilePath, FileMode.Open))
        using (var reader = new StreamReader(file))
        {
            var result = engine.GenerateCode(reader);
            var provider = new CSharpCodeProvider();

            using (var writer = new StringWriter())
            {
                provider.GenerateCodeFromCompileUnit(
                    result.GeneratedCode, writer, 
                    new CodeGeneratorOptions());
                var bytes = Encoding.UTF8.GetBytes(writer.ToString());
                var length = bytes.Length;
                rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(length);
                Marshal.Copy(bytes, 0, rgbOutputFileContents[0], length);
                pcbOutput = (uint)length;
                return 0;
            }
        }
    }

    private RazorTemplateEngine InitializeEngine(
                  string wszDefaultNamespace, 
                  string wszInputFilePath)
    {
        var host = new RazorEngineHost(new CSharpRazorCodeLanguage())
        {
          DefaultNamespace = wszDefaultNamespace,
          DefaultClassName = Path.GetFileNameWithoutExtension(
                                             wszInputFilePath)
        };
        return new RazorTemplateEngine(host);
    }
}

Hungarian naming - yuck, but when in COM interop, do as the Hungarians do.

The above class goes into a strong named assembly that you'll GAC and regasm. You also need to make a registry key under HKEY_LOCAL_MACHINE -> SOFTWARE -> Wow6432Node -> Microsoft -> VisualStudio -> 10.0 -> Generators (omit the Wow part on 32 bit systems). Dmitri Nesteruk has a good overview of the process in Custom Tools Explained.

One way to use the template is to provide some base classes:

public class LetterModel
{
    public string FirstName { get; set; }
    public string ShowNumber { get; set; }
}

public abstract class LetterTemplateWriter : RazorWrite
{
    public LetterModel Model { get; set; }        
}

public abstract class RazorWriter
{        
    protected RazorWriter()
    {
        _builder = new StringBuilder();
    }

    public void WriteLiteral(string literal)
    {
        _builder.Append(literal);
    }

    public void Write(object value)
    {
        _builder.Append(value);
    }

    public string GenerateOutput()
    {
        Execute();
        return _builder.ToString();
    }

    public abstract void Execute();
    StringBuilder _builder;
}

And a template:

@inherits LetterTemplateWriter
              

Hi @Model.FirstName,
Thank you for the email. Although our schedules 
are very busy, we decided to take some time and 
write you a personal reply. 

We appreciate the thoughtful feedback on 
show @Model.ShowNumber, and we want to promise 
you, @Model.FirstName, that we will try harder. 

Sincerely, 

Make sure the CustomTool in the file properties window matches your registry key named under Generators:

image

And voila!

var template = new LetterTemplate();
template.Model = new LetterModel() 
{ 
    FirstName = "...", ShowNumber = "..."
};
Console.WriteLine(template.GenerateOutput());

Not as simple as Preprocessed T4 templates, but you can work with the vastly superior Razor syntax. In addition, there is no mucking around with appdomains and runtime compilation. Instead, razor generates the C# code to compile into your project.


Comments
Stan Wednesday, January 5, 2011
get rid of the hugarian naming crap - it's terrible.
gravatar scott Wednesday, January 5, 2011
@Stan - agreed. The only reason I left it in was because if you generate the members of the inteface in VS, those are the parameter names you'll get. I figured the code might be easier to follow that way, but probably not :(
mcduff Wednesday, January 5, 2011
Its not actually hungarian. It's the notation denoted as hungarian. The original notation had quite a different intent.
gravatar David Ebbo Thursday, January 6, 2011
Check out a couple related post, one by myself and the other by Chris Van de Steeg:

blogs.msdn.com/...

www.chrisvandesteeg.nl/...
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!