Dissecting Razor, part 8: Static Helpers

Razor helpers can be extremely useful, but they are designed to be used only by the page that created them.

To create reusable helpers, you can create CSHTML pages in the App_Code directory.  The WebRazorHostFactory will check for pages in App_Code and create a WebCodeRazorHost instead of the normal WebPageRazorHost.

This happens before the virtual CreateHost method; in order to change this behavior, one must create an inherited RazorBuildProvider and override the CreateHost method; for more details, see the source for more details.

The WebCodeRazorHost compiles helper methods as public static methods by setting the RazorEngineHost.StaticHelpers property to true.  It also overrides PostProcessGeneratedCode to remove the Execute() method and make the Application property static. 

Static helper pages inherit the HelperPage class, a very stripped-down base class which contains the static WriteTo methods used by the helpers and some static members that expose the Model, Html, and other members from the currently executing page (using the WebPageContext.Current.Page property).

Because the generated Execute() method is removed, all content outside helpers and @functions  blocks is not seen by the compiler.  It is seen by the Razor parser, so it must contain valid Razor syntax (eg, no stray @ characters).

Here is an example:

@helper Menu(params string[][] items) {
    <ul>
        @foreach (var pair in items) {
            <li><a href="@Href(pair[1])">@pair[0]</a></li>
        }
    </ul>
}

This text and @code is not compiled.

This helper can then be called from any other code by writing PageName.Menu(...), where PageName is the filename of the CHSTML page in App_Code.  In Razor pages, it can be called like any other helper.  In normal C# code, it returns a HelperResult instance; call ToString() or WriteTo(TextWriter) to get the HTML source.

For example:  (assuming that the helper is defined in ~/App_Code/Helpers.cshtml)

@Helpers.Menu(
    new[] { "Home",    "~/"        },
    new[] { "About",   "~/About"   },
    new[] { "Contact", "~/Contact" }
)

In order to see the source, I need to insert a #error directive inside the helper block; putting it in the page itself will have no effect, since the page is not compiled.  Since the contents of the helper block are code, not markup, I don’t need to wrap it in @{ ... }.

The above helper is transformed into the following C#: (As usual, @line directives have been removed)

public class Helpers : System.Web.WebPages.HelperPage {
    public static System.Web.WebPages.HelperResult Menu(params string[][] items) {
        return new System.Web.WebPages.HelperResult(__razor_helper_writer => {

            WriteLiteralTo(@__razor_helper_writer, "    <ul>\r\n");
            foreach (var pair in items) {
                WriteLiteralTo(@__razor_helper_writer, "            <li><a href=\"");
                WriteTo(@__razor_helper_writer, Href(pair[1]));
                WriteLiteralTo(@__razor_helper_writer, "\">");
                WriteTo(@__razor_helper_writer, pair[0]);
                WriteLiteralTo(@__razor_helper_writer, "</a></li>\r\n");
            }
            WriteLiteralTo(@__razor_helper_writer, "    </ul>\r\n");
#error
        });
    }
    public Helpers() {
    }
    protected static System.Web.HttpApplication ApplicationInstance {
        get {
            return ((System.Web.HttpApplication)(Context.ApplicationInstance));
        }
    }
}

Note that the page-level content does not show up anywhere.  The helper itself is compiled exactly like a normal helper, except that it’s static.

Although there aren’t any in this example, @functions blocks will also be emitted normally into the generated source.

Next Time: Inline Helpers

0 comments:

Post a Comment