Script Capture Tag Helper

Razor sections allows us to write a block of code in one view and render it later in another. The typical example would be the scripts section that takes a script block from the page and renders it in the layout after all common scripts. This works fine until you try to use sections from the partial view or from the display template.

Why even bother

I wanted to add charts to the Ether reporting tool that I wrote to track KPI metrics for one of my projects at work. The tool generates a few different report types, each of which is a separate .NET class, but they all rendered by one Razor Page. To avoid me having to write switch/case to distinguish between different report types, I use Display Template feature of Razor to do this for me. Naturally, different report types will require different charts to be displayed, some might not have them at all, so, I quickly dismissed the idea of including all necessary scripts into a base page. Those scripts need to be in specific display template, but here is a problem, sections do not work inside partial view or display template.

Looking for the solution

I quickly remembered that I came across somewhat similar problem back in 2015 when working with Sitecore CMS. There too, you cannot use sections as Sitecore does its custom rendering that completely overrides standard MVC rendering. The solution was found by my colleague at the time, Derek Hunziker, he proposed to write an HtmlHelper that will capture the markup on the page and render it on the layout. While this works fine in Sitecore or in regular AspNet MVC application, it would not work in AspNet Core application as it relays on System.Web features, like ViewDataContainer and WebViewPage.OutputStack that are no longer available and hardly would be in the “spirit” of AspNet Core application. While not having WebViewPage.OutputStack exposed as an API, AspNet Core has one feature that can actually capture markup, Tag Helpers.

Will tag helpers help?

Tag Helpers is an adequate addition to the Razor markup. In essence, they meant to replace HtmlHelper class with the more robust way of combining C# and Html. Here is a simple example of a tag helper:

<form asp-page-handler="Report" method="post">
  @* form markup *@
</form>
    

The tag helper part is in asp-page-handler attribute that is added to the form html tag. This attribute is a hint to the Razor engine that this tag needs additional processing. The final result will look like this:

<form method="post" action="/?handler=Report">
  // form markup
</form>
    

Tag helpers are an ideal solution in this case, as they are easy to use, elegant and certainly AspNet Core friendly. In fact, I think, it is hard to find a solution that would be easier to use, since it is literally just adding an attribute to an html tag. To solve this, there need to be two tag helpers, one that will capture the script block and the second one that will render it.

Script Capture Tag Helper

Authoring custom tag helpers is remarkably easy, it can be done in three steps. The first step is to create a class that inherits base TagHelper class. Second, is to mark this class with HtmlTargetElement attribute specifying what html element to target, in this case, it is script, and which attributes to look for. This is needed for Razor to know when to apply tag helper. The third step is to override Process or ProcessAsync methods that will hold a tag helper logic.

[HtmlTargetElement("script", Attributes = "capture")]
public class ScriptCaptureTagHelper : TagHelper
{
    /// 
    /// Unique id of the script block
    /// 
    [HtmlAttributeName("capture")]
    public string Capture { get; set; }

    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        // Actual code that will capture the script
    }
}
    

Tag helper can bind attribute value to a C# property, this is done by marking the property with HtmlAttributeName and specifying attribute name. In the example above there is “Capture” property being bind to the corresponding html attribute. If ViewContext instance needs to be accessed by the tag helper, it is done by adding a property to a tag helper class and decorating it with “ViewContext” attribute. this is specially injected as it needs to represent the current instance of the “ViewContext”, other dependencies can be injected through normal dependency injection. The content of the tag can be accessed through the output.GetChildContentAsync method, it returns an instance of TagHelperContent, to get a string representation of a content, GetContent method of TagHelperContent needs to be called. Since the content of the script block is captured for future rendering, it cannot be left as is, because it will be rendered in the middle of a page. To prevent this output.SuppressOutput method needs to be called.

var content = await output.GetChildContentAsync();
var key = $"Script_{Capture}";
ViewContext.HttpContext.Items.Add(key, capture);
output.SuppressOutput();
    

Here I store captured content in HttpContext.Items dictionary using capture attribute value as a key. This is simplified implementation, complete implementation can be found at ScriptCaptureTagHelper.cs

Script Render Tag Helper

Similar to capture tag helper, render tag helper is implemented in its own class that inherits from base TagHelper class, but instead of storing current content it reads the value from HttpContext.Items dictionary, using the same key value and calls SetHtmlContent on the output object:

var key = $"Script_{Render}";
var content = ViewContext.HttpContext.Items[key].ToString();
output.Content.SetHtmlContent(content);
    

This is simplified implementation, complete implementation can be found at ScriptRenderTagHelper.cs

Using it

@addTagHelper *, ScriptCaptureTagHelper needs to be added to the _ViewImports.cshtml file to register tag helpers. ScriptCaptureTagHelper is the name of the assembly where tag helpers are located. To actually use tag helpers add a capture tag on the script block in the display template or in the partial view.

<script capture="@nameof(ReportResult)">
    function createChart() {
      // TODO: create a chart
    }

    $(document).ready(function () { createChart(); });
</script>
    

In the view where the script needs to be rendered, add an empty script tag with the render attribute, specifying the same id as for capture attribute.

<script render="@nameof(ReportResult)">
</script>
    

Where can I get it

The complete implementation is available on the ScriptCaptureTagHelper GitHub page. Also, there is a ScriptCaptureTagHelper NuGet package available for download. Complete implementation has more features then just capturing single script block. It allows capture of multiple blocks under ther same ID with ordering and preserves attributes, which makes possible to capture script reference tag.

Happy coding :)