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.
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.
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.
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:
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:
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.
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.
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.
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
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:
This is simplified implementation, complete implementation can be found at ScriptRenderTagHelper.cs
@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.
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.
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 :)