Note: the generator has evolved since this post. Although the post is still worth reading, please go to http://razorgenerator.codeplex.com/ for the most up to date doc.
The first blog post I ever wrote was titled “Turning an ascx user control into a redistributable custom control”. It was almost exactly five years ago, and it still gets a lot of hits today. And interestingly, this new blog post is about solving essentially the same problem, but with a much nicer Razor based solution than was available at the time.
The general issue we’re trying to solve is to encapsulate reusable pieces of UI. Unfortunately, this has typically meant choosing between two approaches, each having their pros and cons (this mirrors the intro from my old post):
Here, I will show you have you can have the best of both worlds in the Razor world: write your helpers using the powerful Razor declarative helpers syntax, while still being able to build them into a reusable library.
If you don’t care about the details of how this works and just want to use it, here is what you need to know to run it.
And if you’re looking for the source code, it’s all on CodePlex: http://razorgenerator.codeplex.com/
Scottgu introduced the concept in his Razor post, under the “Declarative HTML Helpers” section. Here is an example:
@helper WriteList(string[] items) { <ul> @foreach (var s in items) { <li> @s </li> } </ul> }
Here, we are using the powerful Razor syntax to define what our helper will output. The code in the method is basically the same thing as you write in regular Razor rendering logic, but the fact that it’s inside an @helper method turns it into a declarative helper.
Normally, those @helper methods must live either in your view itself, or in a .cshtml file in App_Code. When it’s in the view itself, it’s only usable within that one view, while when it’s in App_Code, it’s usable from anywhere in your app. But in either case, it really isn’t very reusable in the sense that you can’t easily turn it into a library of helpers that you can use in any app without having to carry the .cshtml file.
If you’ve ever used T4 (which I’ve blogged quite a bit about), then you pretty much know what a Single File Generator is. It’s something that you can attach to a file in your project, such that it generates another file underneath it.
In this case, the file we attach a SingleFileGenerator to is the .cshtml file, and what it generates is the source code that the Razor engine produces from it.
Writing a VS Single File Generator may seem scary, but luckily there is a good sample in the SDK: http://code.msdn.microsoft.com/sfgdd. In fact, most of the code in my Razor generator is directly copied from this. The only place that has interesting code that’s specific to Razor is the RazorClassGenerator.GenerateCode() method. Here is the key code (simplified for brevity):
// Determine the project-relative path string projectRelativePath = InputFilePath.Substring(appRoot.Length); // Turn it into a virtual path by prepending ~ and fixing it up string virtualPath = VirtualPathUtility.ToAppRelative("~" + projectRelativePath); // Create the same type of Razor host that's used to process Razor files in App_Code var host = new WebCodeRazorHost(virtualPath, InputFilePath); // Set the namespace to be the same as what's used by default for regular .cs files host.DefaultNamespace = FileNameSpace; // Create a Razor engine nad pass it our host var engine = new RazorTemplateEngine(host); // Generate code GeneratorResults results = null; using (TextReader reader = new StringReader(inputFileContent)) { results = engine.GenerateCode(reader); } // Then results.GeneratedCode has the CodeDom CodeCompileUnit that the generator needs
So it’s all pretty simple. In essence, all it does is give the content of the Razor file to the Razor engine and asks it to generate the right code for it.
The most obvious pain point when using this is that you need to manually set the custom tool to RazorClassGenerator. It should be relatively easy to add behavior to the VSIX that would add a right click option on .cshtml files that would set this up.
Another potentially very cool thing is to not only use this to precompile @helpers, but also real Views. This is trickier because it requires some logic that will allow the view engine to find the precompiled Views, but it can certainly be done.