Showing posts with label razor-helpers. Show all posts
Showing posts with label razor-helpers. Show all posts

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

Dissecting Razor, part 7: Helpers

We’ll continue our trek into Razor’s class-level features with helpers.

Helpers are one of Razor’s unique features.  They encapsulate blocks of HTML and server-side logic into reusable page-level methods. 

You can define a helper by writing @helper MethodName(parameters) { ... }.  Inside the code block, you can put any markup or server-side code.  The contents of a helper are parsed as a code block (like the contents of a loop or if block), so any non-HTML-like markup must be surrounded by <text> tags or prefixed by the @: escape.

Here is a simple example:

<!DOCTYPE html>
<html>
    <body>
        @helper NumberRow(int num) {
            var square = num * num;
            <tr>
                <td>@num</td>
                <td>@square</td>
                <td>@(num % 2 == 0 ? "Even" : "Odd")</td>
            </tr>
        }
        <table>
            <thead>
                <tr>
                    <th>Number</th>
                    <th>Square</th>
                    <th>Eveness</th>
                </tr>
            </thead>
            <tbody>
                @for (int i = 0; i < 10; i++) {
                    @NumberRow(i)
                }
            </tbody>
        </table>
    </body>
</html>

Note that code statements (such as the square declaration) can go directly inside the helper body without being wrapped in code blocks – the direct contents of the helper is a code block, not markup.  Like any other code block, HTML-like markup is automatically treated as markup instead of code.

Like @functions blocks, helper methods can go anywhere in the source file; the physical location of the block is ignored.

Here is the generated source for the above example: (with blank lines and #line directives stripped for clarity)

public class _Page_Razor_Helpers_cshtml : System.Web.WebPages.WebPage {

    public System.Web.WebPages.HelperResult NumberRow(int num) {
        return new System.Web.WebPages.HelperResult(__razor_helper_writer => {

            var square = num * num;

            WriteLiteralTo(@__razor_helper_writer, "            <tr>\r\n                <td>");
            WriteTo(@__razor_helper_writer, num);

            WriteLiteralTo(@__razor_helper_writer, "</td>\r\n                <td>");
            WriteTo(@__razor_helper_writer, square);

            WriteLiteralTo(@__razor_helper_writer, "</td>\r\n                <td>");
            WriteTo(@__razor_helper_writer, num % 2 == 0 ? "Even" : "Odd");

            WriteLiteralTo(@__razor_helper_writer, "</td>\r\n            </tr>\r\n");

        });
    }
    public _Page_Razor_Helpers_cshtml() {
    }
    protected ASP.global_asax ApplicationInstance {
        get {
            return ((ASP.global_asax)(Context.ApplicationInstance));
        }
    }
    public override void Execute() {
        WriteLiteral("<!DOCTYPE html>\r\n<html>\r\n    <body>\r\n        ");
        WriteLiteral("\r\n        <table>\r\n            <thead>\r\n                <tr>\r\n                   " +
        " <th>Number</th>\r\n                    <th>Square</th>\r\n                    <th>E" +
        "veness</th>\r\n                </tr>\r\n            </thead>\r\n            <tbody>\r\n");

        for (int i = 0; i < 10; i++) {
            Write(NumberRow(i));
        }

        WriteLiteral("            </tbody>\r\n        </table>\r\n    </body>\r\n</html>\r\n");
#error

    }
}

Helpers are compiled as class-level methods that take a parameter set and return a System.Web.WebPages.HelperResult.  (This class name is configured by the RazorHostFactory)

Notice the the contents of the helper method are inside a lambda expression that takes a parameter named __razor_helper_writer.  This construction allows the helper to write directly to the HTTP response stream instead of assembling a giant string and then writing the string all at once.

The HelperResult constructor takes an Action<TextWriter> which contains the contents of the helper block.  The class implements IHtmlString and calls the action from the constructor to generate HTML.  However, under normal circumstances, this IHtmlString implementation is never called.

Calls to helper methods (@NumberRow(i)) are passed to the Write(HelperResult) overload.  This overload calls HelperResult.WriteTo(writer), which passes the page’s TextWriter directly to the helper’s lambda expression.  Thus, the lambda expression can write directly to the page’s output stream, without passing the output as a parameter to the helper method.

Looking inside the helper, we see that all content is passed to WriteTo and WriteLiteralTo methods, as opposed to the Write and WriteLiteral methods used by the rest of the page.

Helper methods cannot call the normal Write* methods since they aren’t necessarily writing to the current output (even though they usually do).  Therefore, they call these Write*To methods, which accept the TextWriter as a parameter.  These static methods are inherited from the WebPageExecutingBase class; their names are also configured by the RazorHostFactory.  The @ in the parameter is a little-used C# syntactictal feature that allows keywords to be used as identifiers; it has nothing to do with Razor’s use of the @ character.

Since helpers are compiled as normal methods, they can do almost anything that a normal method can.  However, because their contents are compiled inside a lambda expression, they have some limitations.  For example, helpers cannot use ref or out parameters, since they cannot be used inside lambda expressions.  Helpers can take params arrays or optional parameters.

Also, Razor’s C# code parser doesn’t support generic helper methods, although there is no reason that they couldn’t be supported in a later release.

The VB.Net code parser also doesn’t explicitly support generic helpers.  However, because VB.Net generics use parentheses, a VB.Net helper can declare a generic type parameter instead of a normal parameter list.

For example:

<!DOCTYPE html>
<html>

    <body>
        @Helper PrintType(Of T)
            @GetType(T)
        End Helper
        @PrintType(Of Dictionary(Of Integer, List(Of String)))()
    </body>

</html>

This trick is not very useful.

Next Time: Static Helpers

Writing output in Razor helpers using code

The new ASP.Net WebPages view engine (formerly Razor) allows you to create reusable parameterized blocks of HTML called helpers

For example:

@helper Fibonacci(int count) {
    int current = 1, prev = 0;
    for (int i = 0; i < count; i++) {
        @:@current, 
        int t = current;
        current += prev;
        prev = t;
    }
}

This helper will write out the first count Fibonacci numbers.  It can be called by writing @Fibonacci(30) in the page that defines the helper.

Using Razor syntax in this code looks strange.  Razor syntax is designed to write HTML tags.  Since I’m printing plain text, I need to use the  @: escape (or the <text> tag) in order to output my text.  This small bit of code looks confusing and can get lost inside larger blocks of server-side code.

Instead, I can use an undocumented hack.  Razor helpers are implemented by compiling a lambda expression as an Action<TextWriter>.  The lambda expression receives a TextWriter parameter named __razor_helper_writer.  (You can see this by writing a Razor page with a compilation error and clicking Show Complete Compilation Source)  There is nothing preventing me from using this parameter yourself.  (it even shows up in IntelliSense!)

Therefore, I can rewrite the helper as follows:

@helper Fibonacci(int count) {
    int current = 1, prev = 0;
    for (int i = 0; i < count; i++) {
        __razor_helper_writer.Write(current + ", ");
        int t = current;
        current += prev;
        prev = t;
    }
}

Remember to correctly escape your text by calling Html.Encode.  Since this writes directly to the output stream, it doesn’t get the benefit of Razor’s automatic escaping.

Note: This relies on undocumented implementation details of the Razor compiler, and may change in future releases. I would not recommend doing this.
Instead, you can write a function:

@functions{ 
    string Fibonacci(int count) {
        var builder = new System.Text.StringBuilder();
        int current = 1, prev = 0;
        for (int i = 0; i < count; i++) {
            builder.Append(current).Append(", ");

            int t = current;
            current += prev;
            prev = t;
        }
        return builder.ToString();
    }
}
This can be called the same way as the helper.  Instead of using the special helper support, the call will just print a string, the same you print any other string.  Therefore, the function’s output will be HTML-escaped.  To prevent that, you can change the function to return an HtmlString.