In this tip, you will learn how to use extension methods to make working with JavaScript in .NET Blazor more structured and easier to work with.
Introduction
Blazor is designed to allow writing web code without using JavaScript, but once you start getting into a more complex project, it usually still involves using some JavaScript for more advanced things like control focus, checking dom control properties, etc.
JavaScript is called by first injecting it:
@inject IJSRuntime JSRuntime;
and then calling the methods:
JSRuntime.InvokeAsync<> or JSRuntime.InvokeVoidAsync()
This uses strings for the JavaScript method name and the parameters are loosely typed, so have to be remembered or you need to go to the JavaScript files to check what they are each time as you won't get any errors until you are running the code and call them incorrectly.
Background
I recently watched a nice online stream "Modern Web Dev with Blazor and .NET 6 with Jason Taylor" organized by SSW.
One great tip that I got from this video was using constants for JavaScript function call names, Time index.
I have taken this idea and expanded on it to make working with JavaScript so much friendlier. It worked so well that I wanted to share my idea.
Using the Code
The main idea is to make wrapper extension function for my JavaScript calls.
Here is some simple JavaScript we use:
function FocusControlByID(ControlID) {
var ctrl = document.getElementById(ControlID);
if (ctrl !== null) { ctrl.focus(); }
}
Now to call this is Blazor, we need to inject the JS interface:
@inject IJSRuntime JSRuntime;
and call it like this:
await JSRuntime.InvokeVoidAsync("FocusControlByID", "txtName");
This is OK, but using the tip on making constants of the JavaScript function names is a step better. Make a new static
class as follows:
public static class JSHelper
{
public const string FocusControlByID = "FocusControlByID";
}
Now we can use the constant in our JS call:
await JSRuntime.InvokeVoidAsync(JSHelper.FocusControlByID, "txtName");
That way, we don't have to go back to the JS file every time we want to use this function again to check the name, and also makes it easier to find all references to see where it is used.
This is where I decided to take it a little further. The other thing that would be nice is if it helped us with JS parameters and return types. To do this, we can use extension methods, change the JSHelper to have this instead:
public static class JSHelper
{
[DebuggerHidden]
public static async ValueTask FocusControlByID(this IJSRuntime JS, string ControlID)
{
await JS.InvokeVoidAsync("FocusControlByID", ControlID);
}
}
Now, you can call the JS function like this:
await JSRuntime.FocusControlByID("txtName");
NOTE: If you have your code in a separate file from the HTML and inject IJSRuntime
like this:
[Inject]
public IJSRuntime JSRuntime { get; set; }
and don't forget to add a using
statement for the JSHelper with any namespaces:
using Shared.JSHelper;
The best bit is we can add comments explaining the function and how to use it as well as comments for parameters, we no longer need to go back to the JS to find the function names, and how many parameters and what type they take.
It also adds the benefit of strongly typing the parameters, so we don't accidentally pass a string
to an int
parameter, etc. All JS calls are enumerated after we type JSRuntime.
This also helps with return types. Here is some JavaScript that gets SelectionStart
and SelectionLength
from an input text control.
function InputGetSelectionStart(p_ControlID) {
var objControl = document.getElementById(p_ControlID);
if (objControl != null) {
return objControl.selectionStart;
}
}
function InputGetSelectionLength(p_ControlID) {
var objControl = document.getElementById(p_ControlID);
if (objControl != null) {
return objControl.selectionEnd - objControl.selectionStart;
}
}
Add wrappers for them to the JSHelper
:
[DebuggerHidden]
public static async ValueTask<int> InputGetSelectionStart
(this IJSRuntime JS, string p_ControlID)
{
return await JS.InvokeAsync<int>("InputGetSelectionStart", p_ControlID);
}
[DebuggerHidden]
public static async ValueTask<int> InputGetSelectionLength
(this IJSRuntime JS, string p_ControlID)
{
return await JS.InvokeAsync<int>("InputGetSelectionLength", p_ControlID);
}
Now calling them is as simple as this:
int SelStart = await JSRuntime.InputGetSelectionStart("txtName");
int SelLen = await JSRuntime.InputGetSelectionLength("txtName");
We can see the return type is an int
in the tooltip we get while typing the code in:
Now if we ever decide to add an extra parameter or change the name of a JS call, as long as we update the wrapper to match Visual Studio will mark any calls missing the new parameter or with the old name as errors which makes it super easy to find and fix them.
One more benefit is when you have extra code to convert JS objects or strings to dotnet objects, you can do this in your wrapper rather than every time you call the JS function.
Consider this JavaScript for getting a controls bounds:
function GetControlBounds(p_strControlID) {
var objCtrl = document.getElementById(p_strControlID);
if (objCtrl !== null)
{
var Rect = objCtrl.getBoundingClientRect();
var strJson = "";
strJson = "{ \"left\":" + Rect.left +
", \"topAbsolute\":" + (window.scrollY + Rect.top) +
", \"topScrolled\":" + Rect.top +
", \"width\":" + Rect.width +
", \"height\":" + Rect.height + " }";
return strJson;
}
else {
return null;
}
}
I have a dotnet class that I de-serialize the result to:
public class clsRect
{
public float left { get; set; }
public float topScrolled { get; set; }
public float topAbsolute { get; set; }
public float width { get; set; }
public float height { get; set; }
}
This is normally used like this:
string boundsJson = await JSRuntime.InvokeAsync<string>("GetControlBounds", "txtName");
if (boundsJson == null) { return; }
clsRect myRect = System.Text.Json.JsonSerializer.Deserialize<clsRect>(boundsJson);
Now, I can wrap it up into one nice function, and include the clsRect
in the JSHelper
class:
[DebuggerHidden]
public static async ValueTask<clsRect>
GetControlBounds(this IJSRuntime JS, string p_strControlID)
{
string strBounds = await JS.InvokeAsync<string>("GetControlBounds", p_strControlID);
if (strBounds == null) { return null; }
return System.Text.Json.JsonSerializer.Deserialize<clsRect>(strBounds);
}
which makes calling it as easy as:
JSHelper.clsRect myCalRect = await JSRuntime.GetControlBounds("txtName");
This has helped us make our Blazor code a lot more structured and easier to use when using JavaScript.
Hope it helps some other Blazor users out there.
Thanks to Jason Taylor for kicking off this idea.
History
- 18th January, 2022: Initial version