Udostępnij za pośrednictwem


Turn your Razor helpers into reusable libraries

Note: the generator has evolved since this post. Although the post is still worth reading, please go to https://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):

  1. Custom code in a library project: this makes it easy to produce a binary that can be used in multiple projects without having to keep source files around.  But on the downside, it’s painful to author rendering logic without a view engine language
  2. Using a non-precompiled markup file: in the WebForms world, that meant an ascx file, while in the Razor world, it means a .cshtml file.  This makes it easy to write rendering logic using ascx or Razor syntax.  But the drawback is that it’s hard to turn into a reusable library.

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.

 

The quick ‘getting started’ guide

 

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.

  • Go to the VS Extension Gallery and install the Razor Single File Generator.

image

 

  • Here you may need to restart VS
  • In a Library project, create a Razor file (.cshtml extension)
  • Under its properties, set the Build Action to None, and set the custom tool to RazorClassGenerator

image

  • Define various @helper methods in the cshtml file.  When you save it, it’ll produce a nested .cs file, e.g.

image

  • Reference the library from your Razor view (in an MVC3 or Web Pages app) and use the helpers!

And if you’re looking for the source code, it’s all on CodePlex: https://razorgenerator.codeplex.com/

 

What are Razor declarative helpers?

 

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.

 

A VS Single File Generator to the rescue

 

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: https://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.

 

Possible areas of improvement

 

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.

Comments

  • Anonymous
    October 27, 2010
    The comment has been removed

  • Anonymous
    October 28, 2010
    @adrian: not sure I understand your T4MVC idea. If you contact me offline we can discuss!

  • Anonymous
    October 28, 2010
    @David: Sorry, my mistake. I thought the problem was to find the views that should be precompiled, whereas you actually meant how to make the view engine use the precompiled views instead of their sources.

  • Anonymous
    November 01, 2010
    Sounds very similar to the view-management concept in Portable Areas within MvcContrib.  I love the idea, and I hope it makes it into the product.

  • Anonymous
    November 01, 2010
    Great job implementing this generator! A small improvement I'd like to suggest is to add the GeneratedCode attribute to the compiled helper class, so that code coverage and code quality tools ignore the generated code.

  • Anonymous
    November 02, 2010
    @Dorin: thanks for the suggestion, I just made that change and pushed a new version to the VS gallery.

  • Anonymous
    November 07, 2010
    This is little bit confusing with the already available helpers. Can you please give one scenario where we can create a helper and use them. I mean some practical application. Thanks, Thani

  • Anonymous
    November 07, 2010
    @Thanigainathan: the project on bitbucket (mentioned above) has a small sample you can try.

  • Anonymous
    November 10, 2010
    Scott Gu wrote that it's possible to place helpers into Views/Helpers directory. But it doesn't seem to work for me...

  • Anonymous
    November 14, 2010
    David, very interesting! I was also looking for some way to precompile razor views. I'm sure MS has such a tool already: just take a look at system.web.webpages.administration.dll It's full of compiled cshtml views. The answer to your problem is in there as well though: they use PageVirtualPathAttribute on the generated class. Apparently the RazorViewEngine takes this attribute into account when searching for views if the virtualpath providers don't return anything. I tried asking Haacked and Scott Guthrie how the WebPages team did this, but did not get an answer as of yet. Perhaps you being closer to the fire (hence, I even thought you were on the same team ;)) could help finding out what tool the WebPages team uses for this precompilation. If there is no such tool, I will take your class and extend it to compile real views somewhere this week (if noone beats me to it :))

  • Anonymous
    November 14, 2010
    @Chris: yes, I actually wrote the code that you're referring to, and in fact the code in this post started from the same base (though I took out the PageVirtualPathAttribute logic).  But note that the PageVirtualPathAttribute logic as it currently exists only works for ASP.NET Web Pages (i.e. WebMatrix), and not for MVC views.  I actually prototyped doing it for MVC views earlier, but haven't had time to play with it since.  But it is definitely something worth looking into :)

  • Anonymous
    November 18, 2010
    Hi David, This looks interesting... Is there an issue with making this Extension available in VS2010 Express editions (C# and WebDev)? (I see that the VSIX Manifest doesn't include Express Editions.) Thanks, Martin

  • Anonymous
    November 18, 2010
    @Martin: I just didn't try that. If simply adding it to the manifest allows it to work, we should change it indeed.  Please let me know.

  • Anonymous
    November 18, 2010
    Hi David, I think Express support can be added as follows to the manifest:    <SupportedProducts>      <VisualStudio Version="10.0">        <Edition>Ultimate</Edition>        <Edition>Premium</Edition>        <Edition>Pro</Edition>        <Edition>Express_All</Edition>     </VisualStudio>    </SupportedProducts> This is my reference: msdn.microsoft.com/.../ee822857.aspx It would be great if this could be added. Thanks, Martin

  • Anonymous
    November 18, 2010
    @Martin: I tried it and unfortunately it doesn't work. When I add that, I can no longer upload the extension to the VS gallery. It fails with "You need to obtain an exception to upload a tool or control that supports the Visual Studio Express SKUs".  I think this is a deliberate limitation that they added to the Express versions.

  • Anonymous
    November 18, 2010
    Ok, got it working for mvc views now. Encountered a lot of Internals again :( I hacked together a t4 template, will now use your code to create a IVsSingleFileGenerator out of that in the next days....

  • Anonymous
    November 18, 2010
    @Chris: it might be worth blogging your results when you are ready!

  • Anonymous
    November 18, 2010
    OK - that's unfortunate. I'm wondering whether there is a way to get this working without the Extension Manager. Looks like there is a requirement to place the DLL in a particular folder and make some Registry settings.

  • Anonymous
    November 18, 2010
    The comment has been removed

  • Anonymous
    November 19, 2010
    Hmm, now I've got it working, I'm starting to wonder if it wouldn't be a better option to just embed the source of the views, and use a VirtualPathProvider to serve those to the normal ViewEngines and the BuildManager to be compiled at runtime. There could be stuff in the web.config for example that changes the behavior of the views for example, that would not work for compiled views. What's your opinon about that? What advantages would pre-compilation have aside from a little performance win at the application startup?

  • Anonymous
    November 19, 2010
    @Jason: it works just like T4 templates.  The idea is that the .cs files are generated at design time while you change and save the cshtml files.  The generated .cs files are then part of the project, so there is no need for the generator at build time.  So this should not be an issue at all.

  • Anonymous
    November 19, 2010
    @Chris: one big potential benefit of pre-compilation is that it could allow you to unit test your views in a much cleaner way than is possible today, since they're just standard classes in your project rather than things that require runtime magic.

  • Anonymous
    November 19, 2010
    Ah, yeah,good one: just needed an extra push to finish it up :)

  • Anonymous
    November 22, 2010
    Ok, finished it: http://goo.gl/KffWq

  • Anonymous
    December 30, 2010
    Of course.  Thanks, that makes perfect sense!

  • Anonymous
    January 16, 2011
    The mapjects engine is implemented on this cshtml, and are they're using azor with this. I am having trouble razor syntax highlighter, can someone help...

  • Anonymous
    February 07, 2011
    The comment has been removed

  • Anonymous
    February 07, 2011
    @Chris: you don't need any @{ } blocks top level in the method, since it starts out being code. You only need this inside tags. For the Html helper, simplest workaround is to pass it explicitly, e.g. @helper RenderAboutLink(System.Web.Mvc.HtmlHelper Html) {    @Html.ActionLink("About link from helper", "About", "Home"); }

  • Anonymous
    February 10, 2011
    Thanks David. It's working well now.

  • Anonymous
    May 04, 2011
    I know I'm a bit late in saying this, but why can't you just create a whole bunch of extension methods and place that class within the System.Web.Mvc.Html namepspace? Then you wouldn't have to include a custom namespace in each view, the extensions could be put in to a library and you wouldn't have to use the @helper syntax any more. Thoughts?

  • Anonymous
    May 05, 2011
    @Vince: that's a valid point. Could you open a bug on razorgenerator.codeplex.com to track this. Thanks!

  • Anonymous
    July 17, 2011
    David,  I have converted the same example that you have mentioned in What are Razor declarative helpers? to a re-usable library ..  you can refer my article in codeproject www.codeproject.com/.../MVC3CustomControl.aspx

  • Anonymous
    July 17, 2011
    @Ranjan: good article. But note that it's mentioning the old version of the generator, and things have changed some, so you may want to update.

  • Anonymous
    September 12, 2012
    Great tool, please update the screen shot and this post content to current version 1.4.3 for RazorGenerator CustomTool=RazorGenerator

  • Anonymous
    September 12, 2012
    @Vairam: I'm too lazy to update the screen shot (it's harder than text!), but I do have a note at the top pointing to the codeplex site for the latest info :)

  • Anonymous
    July 15, 2013
    I am trying to load external CSS file into the main layout and use DLL but it is not working. Do I need to do anything for loading external css or scripts in the precompiled views

  • Anonymous
    July 16, 2013
    @Deepika please post all RazorGenerator questions to razorgenerator.codeplex.com. Thanks!

  • Anonymous
    February 19, 2014
    Hi David, can't thank you enough for this tip, although I have been quite dumb not to have discovered it for all these years I have been learning MVC. I am trying something similar: build the markup generation in helper methods and move them off to a separate class library. I am sure this post will be of tremendous help.

  • Anonymous
    January 21, 2015
    It would be VERY nice if there were some benchmarks comparing DLL-packed vs "simple views" on these metrics:

  1. HTML rendering time. (I guess it should be the same, but better safe than sorry)
  2. Project build time. (This should be longer than usual, I guess) One extra advantage of this approach is that deploying a simple DLL is WAY simpler than deploying hundreds of views with their directory structure. If the same could be done for all resources (JS, images, etc) it would be even better for lazy people like me. Perhaps it even makes defacing a site even more difficult! :D
  • Anonymous
    January 21, 2015
    The comment has been removed
  • Anonymous
    October 28, 2015
    Hi David! Thank you for this! But I was just wondering if it's possible to read the contents of the embedded views from other project? :D