Encapsulated JavaScript Components in ASP.NET Razor
ASP.NET MVC is my go-to stack for web projects and has been for years. I preferred it greatly over all the other frameworks I'd tried before working with it, like ASP.NET Web Forms, PHP smarty templates or whatever WordPress is doing. That said working with newer frameworks like React over the last 4-5 years has shown me that there's smarter ways to accomplish certain tasks.
Specifically here I'm looking at the front-end, in this case at Razor views. The ability to write C# code that is executed before serving to the client is pretty slick and it makes adding more complex display logic a breeze. Razor uses Partial Views, View Components, Layout Views and Layout Sections as forms of UI composition to reuse individual components.
One of the greatest joys of coding in React is creating components that are reusable, isolated and encapsulated right off the bat with no extra work needed. This makes UI composition in React extremely easy and intuitive. When returning to .NET's Razor View system I wondered if its possible to do that same style of UI composition.
The Razor tools listed above work great for the problem that they were meant to solve -- reducing the duplication of C# display logic and HTML code. Or as Microsoft states:
• Break up large markup files into smaller components.
• Reduce the duplication of common markup content across markup files.
However JavaScript is notably not markup, and this is where the issues begin.
The Problem
JavaScript not being markup leaves something to be desired when in comes to adding client-side interactions to Razor generated views. JavaScript can absolutely be added to Razor files, be they Partials or otherwise, however JavaScript is not treated as a first-class citizen and attempting to use it as such will quickly run into frustration. While there are reasons for this (which I will get to later) lets further explore the problem through an example.
There are multiple pain points that programmers will run into while in this space, however where most users encounter this problems is when trying to use a @section
that is defined in a Layout file within a Partial View. For instance if there a _Layout.cshtml
file:
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
</head>
<body>
@RenderBody()
<partial name="_ExamplePartial" />
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
And the programmer attempts to use the defined section "Scripts" from within a _ExamplePartial.cshtml
Partial View like so:
<!-- _ExamplePartial.cshtml -->
<p>This is a partial.</p>
@section Scripts {
<script type="text/javascript">
alert("I'm a bit partial to reuse.");
</script>
}
Then nothing will happen, regardless of if you include _ExamplePartial.cshtml
directly in the _Layout.cshtml
View or some intermediate View that uses a Layout View. The code within the @section Scripts
implementation in the Partial View will never make it on to the rendered page.
Should Have Read the Manual
This doesn't work as Partials cannot participate in the parent views' sections. Indeed Microsoft states this as well in the documentation for Layout Sections:
Sections defined in a page or view are available only in its immediate layout page. They cannot be referenced from partials, view components, or other parts of the view system.
Well then now we have an issue on our hands. One of the main reasons for using Layout Sections is to provide a way to easily add JavaScript to a page, Microsoft's own documentation on Layout Sections literally uses JavaScript as the example. If they can't be used in Partials though then they are of no use, ruling out both Layout Views and Layout Sections.
Despair growing. Everything sucks. The call of the void beckons. Maybe we should just give up now.
The Solution to Everything
Partials and View Components haven't been discounted yet though as <script>
tags can still be added directly into the Partial's code:
<!-- _ExamplePartial.cshtml -->
<p>This is a partial.</p>
<script type="text/javascript">
alert("I'm a bit partial to reuse.");
</script>
The above code works great, the Partial has rendered the JavaScript right to the page and the alert pops up. While a Partial is used here every reference could just as easily be replaced with a View Component. In any case the Partial is now an encapsulated component that reliably delivers a piece of functionality. This solves everything!
Inevitable Disaster
Wait, hold up.
If this is the way to write JavaScript in View files then why have Layout Sections at all? Why does Microsoft recommend their usage for JavaScript, what boon did Layout Sections add again?
Layout Sections add a way to specify where in the Layout parts of the code are injected.
This is especially important for JavaScript because control over where JavaScript is injected into the DOM is super important. Scripts that are dependant on a library will fail if loaded before their dependancy, but perhaps even worse, script tags are loaded synchronously by default which means they block DOM construction. Cue the elevator music.
Lets see just how these limitations can effect us with an example:
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
</head>
<body>
@RenderBody()
<partial name="_ExamplePartial" />
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
To quickly explain the above:
the body is rendered first,
then the _ExamplePartial.cshtml
,
next the external jQuery script is loaded, and
lastly the rest of the scripts that might have been added by any number of (non-partial) Views.
Note that there is no need to async the jQuery <script>
tag as it is only blocking other JavaScript (that might even depend on jQuery being loaded) within the @RenderSectionAsync
.
And here's the changes to _ExamplePartial.cshtml
<!-- _ExamplePartial.cshtml -->
<p>This is a partial.</p>
<button id="btn-send">Send Alert</button>
<script type="text/javascript">
$("#btn-send").click(function(){
alert("I'm a bit partial to reuse.");
});
</script>
We add a button with an id attribute, and some jQuery that sends an alert when the user clicks the button. Alright, hot dog lets test it.
Disaster. The error "Uncaught ReferenceError: $ is not defined
" is a clear indication that the jQuery library is not loaded before it is attempted to be used. A quick look at the generated HTML shows that this is clearly the case, the two script tags need to be swapped in load order. A simple fix.
A Short Reprieve
The fix here is to ensure fundamental libraries are added before they are used. Easy enough. One problem, as we learnt before: script tags are loaded synchronously by default and therefore block the rendering of the page until they have been download and parsed. The solution then is to ensure all external libraries are put in the head and have the async attribute present.
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" async></script>
</head>
<body>
@RenderBody()
<partial name="_ExamplePartial" />
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
So close, unfortunately now there is a race condition between the async script tag loading first or the partial JavaScript code running first. In our case we still get the Uncaught ReferenceError: $ is not defined
error:
The question now is: how to wait until the async script has finished loading? We could use $(document).ready()
if jQuery had loaded, however in this case jQuery itself is the script we are trying to load. Thus, a VanillaJS approach is needed:window.addEventListener('load', function(event) {}
Lets add that around the Partial's JavaScript code.
<!-- _ExamplePartial.cshtml -->
<p>This is a partial.</p>
<button id="btn-send">Send Alert</button>
<script type="text/javascript">
window.addEventListener('load', function(event) {
$("#btn-send").click(function(){
alert("I'm a bit partial to reuse.");
});
});
</script>
Well there's no errors, but at what cost? We have coupled all Partial JavaScript code to the load event and we're going to be duplicating a lot of "onload" boilerplate.
It is not all bad news however, while this works great for externally loaded scripts, we can actually do the exact same procedure for JavaScript that is loaded by the View into Layout Section. So things are looking up and we finally have a workable solution to provide encapsulated components to Razor Views...
Right?
Isolation Was Never An Option
To have a truely composition based system the components need to be entirely self-sufficent, or require their dependancies on construction (in classic Dependancy-Injection fashion). That line was a bit theoretical so let me explain what I mean through an example:
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" async></script>
</head>
<body>
@RenderBody()
<partial name="_ExamplePartial" />
<partial name="_ExamplePartial" />
<partial name="_ExamplePartial" />
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
What happens when multiple partials of the same type (3 in this case) are added to a View? Pandamonium, that's what. It initially looks fine:
However clicking on the buttons puts the truth on full display:
Would you look at that! One click on the top button will trigger all 3 alerts. Of course the sadness doesn't stop there, clicking on either of the other 2 buttons does nothing. The reason for this is because there is no isolation of code within the Partials.
Some change of the existing code can remedy the situation but this is the reason that the complete isolation of components can never be done by Razor. It is only a pipe-dream.
Razor itself cannot guarantee isolation of components because Razaor doesn't write the JavaScript. This is in stark contrast to React where the JSX written is transpiled into JavaScript. React can force each component to run in its own context scope and can enforce that when transpiling. As far as Razor is concerned all JavaScript is just an HTML tag to display, the contents obscured and ultimatley disregarded.
Throw In the Towel Grumpy Man
Just because Razor cannot guarantee isolation doesn't mean that the programmer cannot. Lets make the Partials completely isolated and self-sufficent.
First step is to remove all HTML element id attributes as it is invalid HTML to have multiple id attributes with the same value. id attributes are meant to be unique identifiers and if Partials are for reuse and composition then they might actually be reused in composition (funny that).
<!-- _ExamplePartial.cshtml -->
<p>This is a partial.</p>
<button class="btn-send">Send Alert</button>
<script type="text/javascript">
window.addEventListener('load', function(event) {
$(".btn-send").click(function(){
alert("I'm a bit partial to reuse.");
});
});
</script>
Quick and easy, simply change the id attribute to class and the id selector in the JavaScript to a class selector. Now all buttons can be clicked, even if all of the buttons trigger each other's events. Next step is to isolate the event listeners from each other.
To do that the Partials need to be passed a model. The model it needs depends on the what the reusable component the Partial is implementing. A Button component will need a completely different model than an Input component for example. There is one property that will be consistent across all components and that is a "component id".
//ButtonPartialModel.cs
public class ButtonPartialModel
{
public string ID { get; set; }
}
For this example we are starting simple and only defining the ID. We can add more properties to the Button model later. Next make sure that the Partial uses the model:
<!-- _ExamplePartial.cshtml -->
@model ButtonPartialModel;
<p>This is a partial.</p>
<button class="btn-send">Send Alert</button>
<script type="text/javascript">
window.addEventListener('load', function(event) {
$(".btn-send").click(function(){
alert("I'm a bit partial to reuse.");
});
});
</script>
The @model
right at the top fixes that issue. Next the Layout file must be altered to include a model to each Partial call:
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" async></script>
</head>
<body>
@RenderBody()
<partial
name="_ExamplePartial"
model='new ButtonPartialModel(){ ID = "a" }'>
<partial
name="_ExamplePartial"
model='new ButtonPartialModel(){ ID = "b" }'>
<partial
name="_ExamplePartial"
model='new ButtonPartialModel(){ ID = "c" }'>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Now the Partial is getting its model and can use the ID property. Those IDs are currently hardcoded but that can be dealt with later, for the moment lets use that ID to enforce some isolation:
//ButtonPartialModel.cs
@model ButtonPartialModel;
<p>This is a partial.</p>
<button id="btn-@Model.ID" class="btn-send">Send Alert</button>
<script type="text/javascript">
window.addEventListener('load', function(event) {
$("#btn-@Model.ID").click(function(){
alert("I'm a bit partial to reuse.");
});
});
</script>
The button tag's id now uses @Model.ID
and the jQuery selector needed to be updated to suit. Of course this could be used in a class instead of the id attribute, while the id attribute does make the most sense the majority of the time, it really depends on your use case. Bug fixed, component isolation enforced - not by Razor but by the programmer themselves.
A viable solution at last!
You Handsome Devil
While the solution is sound it lacks some of the finer features. Lets make it shiny and more beautiful. First port of call is to remove the need to hardcode the ID when calling the Partial. Passing in an ID is certainly not a requirement in React so lets remove it here too.
This is something that should happen automatically and shouldn't need to be worried about by the programmer. As such hiding it away in a Base class for all ViewModels is how I would deal with this issue:
//BaseViewModel.cs
public class BaseViewModel
{
private readonly Lazy<string> _ID = new Lazy<string>(BaseViewModel.GenerateID());
public string ID => _ID.Value;
//This method could go anywhere, such as a helper class
public static string GenerateID(){
return String.Concat(Guid.NewGuid().ToString("N").Select(c => char.IsDigit(c) ? (char)(c + 17) : c));
}
}
This takes care of ID creation by lazily creating Guid, removing all hyphens and then changing any numbers to capital letters. This is based off a stack overflow answer and isn't cryptographically safe - but it doesn't need to be! I would actually argue this is likely overkill for the use case. We are generating a 32-character id that needs to be unique only for a single page request. Since this is being done by a base class we can remove it all from the inheriting class:
//ButtonPartialModel.cs
public class ButtonPartialModel : BaseViewModel
{ }
By adding in logic that generates an ID we now no longer need to pass in anything in the Layout file. This looks familiar to the original implementation:
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" async></script>
</head>
<body>
@RenderBody()
<partial name="_ExamplePartial" />
<partial name="_ExamplePartial" />
<partial name="_ExamplePartial" />
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
More the Merrier
Now that ID has been removed from the model properties for each thing that is customisable for the component should be added. By defining a model, the contract upon what is customisable is made available to each user of the component. Take this example of a more customisable Button:
//ButtonPartialModel.cs
public class ButtonPartialModel
{
public string BackgroundColour { get; set; }
public bool HasRoundedCorners { get; set; }
public string ButtonText { get; set; }
}
<!-- _ExamplePartial.cshtml -->
@model ButtonPartialModel;
<p>This is a partial.</p>
<button id="btn-@Model.ID" class='btn-send
@(Model.HasRoundedCorners ? " rounded " : "")'
style='@(!string.IsNullOrEmpty(Model.BackgroundColour) ?
"background-color: " + Model.BackgroundColour :
"")'
>
@Model.ButtonText
</button>
<script type="text/javascript">
window.addEventListener('load', function(event) {
$("#btn-@Model.ID").click(function(){
alert("I'm a bit partial to reuse.");
});
});
</script>
<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
<title>@ViewData["Title"]</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" async></script>
</head>
<body>
@RenderBody()
<partial
name="_ExamplePartial"
model='new ButtonPartialModel(){
BackgroundColour = "#aa3",
HasRoundedCorners = true,
ButtonText = "First"
}'
>
<partial
name="_ExamplePartial"
model='new ButtonPartialModel(){
BackgroundColour = "rgb(200,100,0)",
HasRoundedCorners = true,
ButtonText = "Second"
}'
>
<partial
name="_ExamplePartial"
model='new ButtonPartialModel(){
BackgroundColour = "#a3a",
HasRoundedCorners = true,
ButtonText = "Third"
}'
>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Note I actually prefer the older @await Html.PartialAsync()
HTML Helper when going down this path of component composition because it feels a lot more like classic C# code. Here's how that would look:
<!-- _Layout.cshtml -->
@await Html.PartialAsync(
"_ExamplePartial",
new ButtonPartialModel(){
BackgroundColour = "#aa3",
HasRoundedCorners = true,
ButtonText = "First"
})
@await Html.PartialAsync(
"_ExamplePartial",
new ButtonPartialModel(){
BackgroundColour = "rgb(200, 100, 0)",
HasRoundedCorners = true,
ButtonText = "Second"
})
@await Html.PartialAsync(
"_ExamplePartial",
new ButtonPartialModel(){
BackgroundColour = "#a3a",
HasRoundedCorners = false,
ButtonText = "Last"
})
Parting Words
This problem has been rearing its nasty head for great many years. I am certainly not the first to explore its solutions. I particularly liked Johnny Oshika's solution as a catch-all that queues up each Partial/View Component's JavaScript in very clever way and runs them in order once the page has rendered.
In any case this problem was one I've been thinking about for a while and coming up with a scalable solution was hard yet straight forward. With this piece of the puzzle solved I may one day write up a comparison post between .NET web technologies and React.
But for now this will have to do.