Click here to Skip to main content
15,879,535 members
Articles / Web Development / Blazor
Tip/Trick

Blazor: Using JavaScript in a More Structured Way by Using Extension Methods

Rate me:
Please Sign up or sign in to vote.
5.00/5 (21 votes)
19 Jan 2022CPOL3 min read 15.9K   16   9
Using extension methods to make working with JavaScript in .NET Blazor easier to work with
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:

JavaScript
@inject IJSRuntime JSRuntime;

and then calling the methods:

JavaScript
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:

JavaScript
// Focus a control by its ID
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:

C#
@inject IJSRuntime JSRuntime;

and call it like this:

C#
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:

C#
public static class JSHelper
{
     public const string FocusControlByID = "FocusControlByID";
}

Now we can use the constant in our JS call:

C#
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:

C#
public static class JSHelper
{
    /// <summary>
    /// Focus a control by its ID
    /// </summary>
    /// <param name="JS"></param>
    /// <param name="ControlID">The ID of the Control to set focus to</param>
    /// <returns></returns>
    [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:

C#
await JSRuntime.FocusControlByID("txtName");

NOTE: If you have your code in a separate file from the HTML and inject IJSRuntime like this:

C#
[Inject]
public IJSRuntime JSRuntime { get; set; }

and don't forget to add a using statement for the JSHelper with any namespaces:

C#
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.

image

This also helps with return types. Here is some JavaScript that gets SelectionStart and SelectionLength from an input text control.

JavaScript
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:

C#
/// <summary>
/// Get the Selection start position for an input control
/// </summary>
/// <param name="JS"></param>
/// <param name="p_ControlID">Control ID</param>
/// <returns></returns>
[DebuggerHidden]
public static async ValueTask<int> InputGetSelectionStart
                    (this IJSRuntime JS, string p_ControlID)
{
    return await JS.InvokeAsync<int>("InputGetSelectionStart", p_ControlID);
}

/// <summary>
/// Get the Selection Length position for an input control
/// </summary>
/// <param name="JS"></param>
/// <param name="p_ControlID">Control ID</param>
/// <returns></returns>
[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:

C#
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:

image

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:

JavaScript
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:

C#
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:

C#
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:

C#
/// <summary>
/// Get the Position of a control on screen
/// </summary>
/// <param name="JS"></param>
/// <param name="p_strControlID">Control ID</param>
/// <returns>a clsRect with the control bounds info, 
/// or Null if the control is not found</returns>
[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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNice article, but is the JS -> JSON -> C# necessary Pin
suryapratap00021-Jan-22 16:17
suryapratap00021-Jan-22 16:17 
AnswerRe: Nice article, but is the JS -> JSON -> C# necessary Pin
Scott Ward22-Jan-22 14:34
Scott Ward22-Jan-22 14:34 
Thanks for the tip Surya
I was not aware you could do that, less conversion code is better
I'll give it a try
GeneralRe: Nice article, but is the JS -> JSON -> C# necessary Pin
suryapratap00024-Jan-22 6:01
suryapratap00024-Jan-22 6:01 
PraiseExcellent! Pin
onelopez21-Jan-22 11:26
onelopez21-Jan-22 11:26 
PraiseFive star Pin
Matthew Johnson 202221-Jan-22 7:46
Matthew Johnson 202221-Jan-22 7:46 
GeneralMy vote of 5 Pin
dkurok21-Jan-22 1:12
dkurok21-Jan-22 1:12 
PraiseMy vote of 5 Pin
Juan Antonio Vizcaino21-Jan-22 1:12
professionalJuan Antonio Vizcaino21-Jan-22 1:12 
GeneralMy vote of 5 Pin
Fielding1620-Jan-22 22:30
Fielding1620-Jan-22 22:30 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA19-Jan-22 4:37
professionalȘtefan-Mihai MOGA19-Jan-22 4:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.