12 Comments

Here's an ASP.NET MVC HTML Helper that helps in the following scenario. Let's say you have a partial that can be included a number of times in a view but has a bit of common code that need only be included once. Typically that bit of common code would be some script.

In ASP.NET such a scenario is addressed by RegisterClientScriptBlock. In the Spark View Engine it's taken care of by the once attribute.

So, inspired by Phil Haack's Templated Razor Delegates post from earlier this year I knew you could write helper functions for Razor that could take arbitrary markup. The key to it is taking a Func argument that returns a HelperResult.

To use it you invoke it like so:

@Html.Once("some unique key", @<div>arbitrary markup that gets rendered just once</div>)

More typically, for including script, e.g.:

@Html.Once("TABLE_SORTER_INIT_SCRIPT", @<script type="text/javascript">
    $(function() {
        $('table.sortable th').each(function(){
            // . . .
        });
    });
</script>)

Here's the implementation that adds the Once extension method to HtmlHelper:

using System;
using System.Web.Mvc;
using System.Web.WebPages;

namespace Foo
{
    public static class HtmlUtils
    {
        public static HelperResult Once(this HtmlHelper html, string key, Func<object, HelperResult> template)
        {
            var httpContextItems = html.ViewContext.HttpContext.Items;
            var contextKey = "HtmlUtils.Once." + key;
            if (!httpContextItems.Contains(contextKey))
            {
                // Render and record the fact in HttpContext.Items
                httpContextItems.Add(contextKey, null);
                return template(null);
            }
            else
            {
                // Do nothing, already rendered something with that key
                return new HelperResult(writer => { /*no-op*/ });
            }
        }
    }
}

Hope that helps!

Comments

Comment by Duncan Smart

What error? Which line? Also, I assume you are calling it from a Razor .cshtml file?

Comment by Bart

Yes it's a Razor .cshtml file. The error "No overload for method 'Once' takes 2 arguments" occurs on the call to @Html.Once("...."). I created an overload that accepts 2 arguments and then passes the original 2 arguments and a null value for the 3rd "template" argument to the original "Once" method but then I get "The best overloaded method match for...has some invalid arguments".

Bart
Comment by Bart

Here is the method I created that accepts two arguments:

public static void Once(this HtmlHelper html, string key)
{
Func template = null;

Once(html, key, template);

}

Bart
Comment by Bart

Looks like some of my formatting was lost (angle brackets and the content inside) but as you can see I take the two arguments and add a third and pass those to the original method.

Bart
Comment by Duncan Smart

Hmm, I don't know. Mine works as-is, just tried it in a new project. The code is just in a regular *.cs file, not in App_Code.

Comment by Duncan Smart

Also I don't know what you're doing with a null template. The whole point of this is to render a template once.

Comment by Bart

Thanks...I'll keep looking at it. The reason I pass in the null is because I didn't know what else to pass in as the third method argument.

Bart
Comment by Duncan Smart

"I didn’t know what else to pass in as the third method argument" - I think you may have missed the point of what this is for :-)

Comment by Bart

Well MVC is a little outside of my comfort zone :-) For some reason it won't compile unless I pass in something for the third parameter. I will try later today to move the code out of the app_code directory and see if that is the problem. Thanks!

Bart
Comment by Bart

I think I may see the source of my misunderstanding. The first parameter is implied and only the last two parameters need to be included in the call to the method: key and template...which is how your code is set up. I found another similar example and it got me clued in.
But apparently my project is looking for me to supply a value for the first argument: "this HtmlHelper html".(???) At least now I know what is causing the issue and it should be easy to track down why it isn't picking up that scope.

Bart
Comment by Bart

I'm getting an error at the "var httpContextItems = html.ViewContext.HttpContext.Items" line. Should it work if put this as a class in the App_Code directory?

Bart
Comment by Bart

I got past the initial error but then got an error that there was no overload for 2 arguments. I created one that accepts two arguments and then passes those to the original method but it doesn't like what I am providing as the third argument (using type Func).

Bart