C# – In ASP.NET MVC/Razor, how to add initializer JavaScript to a “control”

asp.net-mvccjavascriptrazor

Actually, I already have at least 3 different solutions for the problem, I just don't like any of them for various reasons.

In ASP.NET MVC/Razor, there are no controls anymore, in the sense as they were in ASP.NET. An ASCX control used to be view+controller intertwined, so I get it. Instead we have html helpers that emit strings, html fragments. Let's say I want to create a helper method in a library that creates a jQuery UI date picker.

First solution. I just write a method that first creates a textbox (actually I can just call TextBoxFor because it does the same thing for me), then it creates a small JavaScript code fragment that calls $('#textboxid').datepicker(). Then I can call that in my Razor views and it works. I don't like the fact that view-related stuff is done in code, and not in Razor, but all default editor templates are like that, sadly. I don't like the fact that there will be lots of small script fragments after each textbox in the output html. And my code fragment can get complex, if that initializer JS had many parameters, it would look ugly, there would be string escape issues, etc.

Microsoft is pushing unobtrusive JavaScript architecture. I could just put a CSS marker class on my textbox, add some data-attributes if needed, and emit only one JavaScript that turns all my textboxes to date pickers, based on the CSS marker class. This one last part is the main problem, I do need an imperative JS code to do that. Unobtrusive AJAX and validation work well because they don't need that at all, they just respond to user interaction events. This is not the case, I need to turn my textboxes to date pickers whenever they appear. And they can appear dynamically in an editable grid, or after an AJAX call, whenever.

Third solution, view should be done in Razor, so let's do it in Razor, as an editor template. Problem is, we're talking about a class library here. In the past, I have seen ugly hacks to embed .cshtml files as embedded resources or compile them to actual C# code during compilation. Last I checked, they seemed like ugly hacks and weren't supported.

Is there another solution? If not, which of the above is the nicest, the one that most conforms to the state of the art coding conventions, or Microsoft's idea, etc?

Best Answer

I'm not entirely sure I've identified the right problem, but if I have, this may help:

We created a class called ScriptManager which lets us add arbitrary javascript files and/or functions to the bottom of all our pages when they are rendered. Give this a try:

public class ScriptManager
{
    public List<string> scripts = new List<string>();
    public List<string> scriptFiles = new List<string>();
}
public static class HtmlExtensions
{

    [ThreadStatic]
    private static ControllerBase pageDataController;
    [ThreadStatic]
    private static ScriptManager pageData;

    public static ScriptManager ScriptManager(this HtmlHelper html)
    {
        ControllerBase controller = html.ViewContext.Controller;

        // This makes sure we get the top-most controller
        while (controller.ControllerContext.IsChildAction)
        {
            controller = controller.ControllerContext.ParentActionViewContext.Controller;
        }
        if (pageDataController == controller)
        {
            // We've been here before
            return pageData;
        }
        else
        {
            // Initial setup
            pageDataController = controller;
            pageData = new ScriptManager();
            return pageData;
        }
    }
}

Then, at the bottom of your _Layout.cshtml page:

@foreach (var script in Html.ScriptManager().scriptFiles.Distinct())
{
    @Html.Script(script);
}
@foreach (var script in Html.ScriptManager().scripts)
{
    @Html.Raw("<script type='text/javascript'>")
    @Html.Raw(script)
    @Html.Raw("</script>")
}

Finally, you use it in a partial view like:

@{
  Html.ScriptManager().scriptFiles.Add(Url.Content("~/Scripts/jquery.blockUI.js"));
 }

You lose the ability to tell the client about the scripts in the header of the page, but they will be included on all pages you use a partial view that uses the blockUI (for example).