10

JavaScript Interop

In this chapter, we will take a look at JavaScript. In specific scenarios, we still need to use JavaScript, or we will want to use an existing library that relies on JavaScript. Blazor uses JavaScript to update the Document Object Model (DOM), download files, and access local storage on the client.

So, there are, and always will be, cases when we need to communicate with JavaScript or have JavaScript communicate with us. Don’t worry. The Blazor community is an amazing one, so chances are someone has already built the interop we need.

In this chapter, we will cover the following topics:

  • Why do we need JavaScript?
  • .NET to JavaScript
  • JavaScript to .NET
  • Implementing an existing JavaScript library
  • JavaScript interop in WebAssembly

Technical requirements

Ensure you have followed the previous chapters or use the Chapter09 folder as a starting point.

You can find the source code for this chapter’s result at https://github.com/PacktPublishing/Web-Development-with-Blazor-Second-Edition/tree/main/Chapter10.

If you are jumping into this chapter using the code from GitHub, make sure you have added Auth0 account information in the settings files. You can find the instructions in Chapter 8, Authentication and Authorization.

Why do we need JavaScript?

Many say Blazor is the JavaScript killer, but the truth is that Blazor needs JavaScript to work. Some events only get triggered in JavaScript, and if we want to use those events, we need to make an interop.

I jokingly say that I have never written so much JavaScript as when I started developing with Blazor. Calm down… it’s not that bad.

I have written a couple of libraries that require JavaScript to work. They are called Blazm.Components and Blazm.Bluetooth.

The first one is a grid component that uses JavaScript interop to trigger C# code (JavaScript to .NET) when the window is resized, to remove columns if they can’t fit inside the window.

When that is triggered, the C# code calls JavaScript to get the size of the columns based on the client width, which only the web browser knows, and based on that answer, it removes columns if needed.

The second one, Blazm.Bluetooth, makes it possible to interact with Bluetooth devices using Web Bluetooth, which is a web standard accessible through, you guessed it, JavaScript.

It uses two-way communication; Bluetooth events can trigger C# code, and C# code can iterate over devices and send data to them. They are both open source, so if you are interested in looking at a real-world project, you can check them out on my GitHub: https://github.com/EngstromJimmy.

In most cases, I would argue that we won’t need to write JavaScript ourselves. The Blazor community is very big, so chances are that someone has already written what we need. But we don’t need to be afraid of using JavaScript, either. Next, we will look at different ways to add JavaScript calls to our Blazor project.

.NET to JavaScript

Calling JavaScript from .NET is pretty simple. There are two ways of doing that:

  • Global JavaScript
  • JavaScript Isolation

We will go through both ways to see what the difference is.

Global JavaScript (the old way)

To access the JavaScript method, we need to make it accessible. One way is to define it globally through the JavaScript window object. This is a bad practice since it is accessible by all scripts and could replace the functionality in other scripts (if we accidentally use the same names).

What we can do is, for example, use scopes, create an object in the global space, and put our variables and methods on that object so that we lower the risk a bit at least.

Using a scope could look something like this:

window.myscope = {};
window.myscope.methodName = () => { ... }

We create an object with the name myscope. Then we declare a method on that object called methodName. In this example, there is no code in the method; this only demonstrates how it could be done.

Then, to call the method from C#, we would call it using JSRuntime like this:

@inject IJSRuntime jsRuntime
await jsRuntime.InvokeVoidAsync("myscope.methodName");

There are two different methods we can use to call JavaScript:

  • InvokeVoidAsync, which calls JavaScript, but doesn’t expect a return value
  • InvokeAsync<T>, which calls JavaScript and expects a return value of type T

We can also send in parameters to our JavaScript method if we want. We also need to refer to JavaScript, and JavaScript must be stored in the wwwroot folder.

The other way is JavaScript Isolation, which uses the methods described here, but with modules.

JavaScript Isolation

In .NET 5, we got a new way to add JavaScript using JavaScript Isolation, which is a much nicer way to call JavaScript. It doesn’t use global methods, and it doesn’t require us to refer to the JavaScript file.

This is awesome for component vendors and end users because JavaScript will be loaded when needed. It will only be loaded once (Blazor handles that for us), and we don’t need to add a reference to the JavaScript file, which makes it easier to start and use a library.

So, let’s implement that instead.

Isolated JavaScript can be stored in the wwwroot folder, but since an update in .NET 6, we can add them in the same way we add isolated CSS. Add them to your component’s folder and name it js at the end (mycomponent.razor.js).

Let’s do just that!

In our project, we can delete categories and components. Let’s implement a simple JavaScript call to reveal a prompt to make sure that the user wants to delete the category or tag:

  1. In the Components project, select the RazorComponents/ItemList.razor file, create a new JavaScript file, and name the file ItemList.razor.js.
  2. Open the new file and add the following code:
    export function showConfirm(message) {
        return confirm(message);
    }
    

    JavaScript Isolation uses the standard ES modules and can be loaded on demand. The methods it exposes are only accessible through that object and not globally, as with the old way.

  1. Open ItemList.razor and inject IJSRuntime at the top of the file:
    @inject IJSRuntime jsRuntime
    
  2. In the code section, let’s add a method that will call JavaScript:
    IJSObjectReference jsmodule;
    private async Task<bool> ShouldDelete()
    {
        jsmodule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "/_content/Components/RazorComponents/ItemList.razor.js");
        return await jsmodule.InvokeAsync<bool> ("showConfirm", "Are you sure?");
    }
    

    IJSObjectReference is a reference to the specific script that we will import further down. It has access to the exported methods in our JavaScript, and nothing else.

    We run the Import command and send the filename as a parameter. This will run the JavaScript command let mymodule = import("/_content/Components/RazorComponents/ItemList.razor.js") and return the module.

    Now we can use that module to access our showConfirm method and send in the argument "Are you sure?".

  1. Change the Delete button we have in the component to the following:
    <td><button class="btn btn-danger" @onclick="@(async ()=>{ if (await ShouldDelete()) { await DeleteEvent.InvokeAsync(item); } })">Delete</button></td>
    

Instead of just calling our Delete event callback, we first call our new method. Let JavaScript confirm that you really want to delete it, and if so, then run the Delete event callback.

This is a simple implementation of JavaScript.

JavaScript to .NET

What about the other way around? I would argue that calling .NET code from JavaScript isn’t a very common scenario, and if we find ourselves in that scenario, we might want to think about what we are doing.

As Blazor developers, we should avoid using JavaScript as much as possible.

I am not bashing JavaScript in any way, but I see this often happen where developers use what they used before and kind of shoehorn it into their Blazor project.

They are solving things with JavaScript that are easy to do with an if statement in Blazor. So that’s why I think it’s essential to think about when to use JavaScript and when not to use JavaScript.

There are, of course, times when JavaScript is the only option, and as I mentioned earlier, Blazm uses communication both ways.

There are three ways of doing a callback from JavaScript to .NET code:

  • A static .NET method call
  • An instance method call
  • A component instance method call

Let’s take a closer look at them.

Static .NET method call

To call a .NET function from JavaScript, we can make the function static, and we also need to add the JSInvokable attribute to the method.

We can add a function such as this in the code section of a Razor component, or inside a class:

[JSInvokable]
public static Task<int[]> ReturnArrayAsync()
{
   return Task.FromResult(new int[] { 1, 2, 3 });
}

In the JavaScript file, we can call that function using the following code:

DotNet.invokeMethodAsync('BlazorWebAssemblySample', 'ReturnArrayAsync')
      .then(data => {
        data.push(4);
          console.log(data);
      });

The DotNet object comes from the Blazor.js or blazor.server.js file.

BlazorWebAssemblySample is the name of the assembly, and ReturnArrayAsync is the name of the static .NET function.

It is also possible to specify the name of the function in the JSInvokeable attribute if we don’t want it to be the same as the method name like this:

[JSInvokable("DifferentMethodName")]

In this sample, JavaScript calls back to .NET code, which returns an int array.

It is returned as a promise in the JavaScript file that we are waiting for, and then (using the then operator) we continue with the execution, adding a 4 to the array and then outputting the values in the console.

Instance method call

This method is a bit tricky; we need to pass an instance of the .NET object to call it (this is the method that Blazm.Bluetooth is using).

First, we need a class that will handle the method call:

using Microsoft.JSInterop;
public class HelloHelper
{
    public HelloHelper(string name)
    {
        Name = name;
    }
    public string Name { get; set; }
    [JSInvokable]
    public string SayHello() => $"Hello, {Name}!";
}

This class takes a string (a name) in the constructor and a method called SayHello that returns a string containing "Hello,", and the name we supplied when we created the instance.

So, we need to create an instance of that class, supply a name, and create DotNetObjectReference<T>, which will give JavaScript access to the instance.

But first, we need JavaScript that can call the .NET function:

export function sayHello (hellohelperref) {
    return hellohelperref.invokeMethodAsync('SayHello').then(r => console.log(r));
}

In this case, we are using the export syntax, and we export a function called sayHello, which takes an instance of DotNetObjectReference called dotnetHelper.

In that instance, we invoke the SayHello method, which is the SayHello method on the .NET object. In this case, it will reference an instance of the HelloHelper class.

We also need to call the JavaScript method, and we can do that from a class or, in this case, from a component:

@page "/interop"
@inject IJSRuntime jsRuntime
@implements IDisposable
<button type="button" class="btn btn-primary" @onclick="async ()=> { await TriggerNetInstanceMethod(); }">    Trigger .NET instance method HelloHelper.SayHello </button>
@code {
    private DotNetObjectReference<HelloHelper> objRef;
    
    IJSObjectReference jsmodule;
    public async ValueTask<string>
      TriggerNetInstanceMethod()
    {
        objRef = DotNetObjectReference.Create(new HelloHelper("Bruce Wayne"));
        jsmodule = await jsRuntime. InvokeAsync<IJSObjectReference>("import", "/_content/MyBlog.Shared/Interop.razor.js");
        return await jsmodule.InvokeAsync<string>("sayHello", objRef);
    }
    public void Dispose()
    {
        objRef?.Dispose();
    }
}

Let’s go through the class. We inject IJSRuntime because we need one to call the JavaScript function. To avoid any memory leaks, we also have to make sure to implement IDiposable, and toward the bottom of the file, we make sure to dispose of the DotNetObjectReference instance.

We create a private variable of the DotNetObjectReference<HelloHelper> type, which is going to contain our reference to our HelloHelper instance. We create IJSObjectReference so that we can load our JavaScript function.

Then we create an instance of DotNetObjectReference.Create(new HelloHelper("Bruce Wayne")) of our reference to a new instance of the HelloHelper class, which we supply with the name "Bruce Wayne".

Now we have objRef, which we will send to the JavaScript method, but first, we load the JavaScript module, and then we call JavaScriptMethod and pass in the reference to our HelloHelper instance. Now, the JavaScript sayHello method will run hellohelperref.invokeMethodAsync('SayHello'), which will make a call to SayHelloHelper and get back a string with "Hello, Bruce Wayne".

There are two more ways that we can use to call .NET functions from JavaScript. We can call a method on a component instance where we can trigger an action, and it is not a recommended approach for Blazor Server. We can also call a method on a component instance by using a helper class.

Since calling .NET from JavaScript is rare, we won’t go into the two examples. Instead, we’ll dive into things to think about when implementing an existing JavaScript library.

Implementing an existing JavaScript library

The best approach, in my opinion, is to avoid porting JavaScript libraries. Blazor needs to keep the DOM and the render tree in sync, and having JavaScript manipulate the DOM can jeopardize that.

Most component vendors, such as Telerik, Synfusion, Radzen, and, of course, Blazm, have native components. They don’t just wrap a JavaScript but are explicitly written for Blazor in C#. Even though the components use JavaScript in some capacity, the goal is to keep that to a minimum.

So, if you are a library maintainer, my recommendation would be to write a native Blazor version of the library, keep JavaScript to a minimum, and, most importantly, not force Blazor developers to write JavaScript to use your components.

Some components will be unable to use JavaScript implementations since they need to manipulate the DOM.

Blazor is pretty smart when syncing the DOM and render tree, but try to avoid manipulating the DOM. If we need to use JavaScript for something, make sure to put a tag outside the manipulation area, and Blazor will then keep track of that tag and not think about what is inside the tag.

Since we started with Blazor at my workplace very early, many vendors had not yet completed their Blazor components. We needed a graph component fast. On our previous website (before Blazor), we used a component called Highcharts.

Highcharts is not a free component but is free to use for non-commercial projects. When building our wrapper, we had a couple of things we wanted to ensure. We wanted the component to work in a similar way to the existing one, and we wanted it to be as simple to use as possible.

Let’s walk through what we did.

First, we added a reference to the Highcharts JavaScript:

<script src="https://code.highcharts.com/highcharts.js"></script>

And then we added a JavaScript file as follows:

export function loadHighchart(id, json) {
var obj = looseJsonParse(json);
    Highcharts.chart(id, obj);
};
export function looseJsonParse(obj) {
    return Function('"use strict";return (' + obj + ')')();
}

The loadHighchart method takes id of the div tag, which should be converted to a chart and the JSON for configuration.

There is also a method that converts the JSON to a JSON object so that it can be passed into the chart method.

The Highchart Razor component looks like this:

@inject Microsoft.JSInterop.IJSRuntime jsruntime
<div>
    <div id="@id.ToString()"></div>
</div>
@code
{
    [Parameter] public string Json { get; set; }
    private string id { get; set; } = "Highchart" + Guid.NewGuid().ToString();
    protected override void OnParametersSet()
    {
        StateHasChanged();
        base.OnParametersSet();
    }
    IJSObjectReference jsmodule;
    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (!string.IsNullOrEmpty(Json))
        {
            jsmodule = await jsruntime.InvokeAsync<IJSObjectReference>("import", "/_content/Components/RazorComponents/HighChart.razor.js");
            await jsmodule.InvokeAsync<string>("loadHighchart", new object[] { id, Json });
        }
        await base.OnAfterRenderAsync(firstRender);
    }
}

The important thing to notice here is that we have two nested div tags: one on the outside that we want Blazor to track and one on the inside that Highchart will add things to.

We pass a JSON parameter in the JSON for the configuration and then call our JavaScript function. We run our JavaScript interop in the OnAfterRenderAsync method because otherwise, it would throw an exception, as you may recall from Chapter 4, Understanding Basic Blazor Components.

Now, the only thing left to do is to use the component, and that looks like this:

@page "/HighChartTest"
<HighChart Json="@chartjson">
</HighChart>
@code {
    string chartjson = @" {
    chart: { type: 'pie'},
    series: [{
        data: [{
            name: 'Does not look like Pacman',
            color:'black',
            y: 20,
        }, {
            name: 'Looks like Pacman',
            color:'yellow',
            y: 80
        }]
    }]
}";
}

This test code will show a pie chart that looks like Figure 10.1:

Figure 10.1 – Chart example

Figure 10.1: Chart example

We have now gone through how we got a JavaScript library to work with Blazor, so this is an option if there is something we need.

As mentioned, the component vendors are investing in Blazor, so chances are that they have what we need, so we might not need to invest time in creating our own component library.

JavaScript interop in WebAssembly

All the things mentioned so far in this chapter will work great for Blazor Server and Blazor WebAssembly.

But with Blazor WebAssembly we have direct access to the JSRuntime (since all the code is running inside the browser. Direct access will give us a really big performance boost. For most applications, we are doing one or two JavaScript calls. Performance is not really going to be a problem. Some applications are more JavaScript-heavy though and would benefit from using the JSRuntime directly.

We have had direct access to the JSRuntime using the IJSInProcessRuntime and IJSUnmarshalledRuntime. But with .NET 7, both are now obsolete, and we have gotten a nicer syntax.

In the GitHub repo, I have added a couple of files to the BlazorWebAssembly.Client project if you want to try the code.

We will start by looking at calling JavaScript from .NET.

To be able to use these features, we need to enable them in the project file by enabling AllowUnsafeBlocks.

<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

.NET to JavaScript

To show the difference, the sample below is the same ShowAlert function as earlier in the chapter.

The Razor file looks like this:

@page "/nettojswasm"
@using System.Runtime.InteropServices.JavaScript
<h3>This is a demo how to call JavaScript from .NET</h3>
<button @onclick="ShowAlert">Show Alert</button>
@code {
    protected async void ShowAlert()
    {
        ShowAlert("Hello from .NET");
    }
    protected override async Task OnInitializedAsync()
    {
        await JSHost.ImportAsync("nettojs", "../JSInteropSamples/NetToJS.razor.js");
    }
}

We are using JSHost to import the JavaScript and give it the name "nettojs". A Source Generator generates the implementation for calling the JavaScript, and to be sure that it can pick up what it should do, we need to add some code in a code-behind.

The code-behind it looks like this:

using System.Runtime.InteropServices.JavaScript;
namespace BlazorWebAssembly.Client.JSInteropSamples;
public partial class NetToJS
{
    [JSImport("showAlert", "nettojs")]
    internal static partial string ShowAlert(string message);
}

The JavaScript file looks like this:

export function showAlert(message) {
    return alert(message);
}

We add a JSImport attribute to a method, which will automatically be mapped to the JavaScript call.

This is a much nicer implementation, I think, and a lot faster.

Next, we will look at calling .NET from JavaScript.

JavaScript to .NET

When calling a .NET method from JavaScript, a new attribute makes that possible called JSExport.

The Razor file implementation looks like this:

@page "/jstostaticnetwasm"
@using System.Runtime.InteropServices.JavaScript
<h3>This is a demo how to call .NET from JavaScript</h3>
<button @onclick="ShowMessage">Show alert with message</button>
@code {
    protected override async Task OnInitializedAsync()
    {
        await JSHost.ImportAsync("jstonet", "../JSInteropSamples/JSToStaticNET.razor.js");
    }
}

Calling JSHost.ImportAsync is not necessary for the JSExport part of the demo, but we need it to call JavaScript so that we can make the .NET call from JavaScript.

Similarly, here we need to have the methods in a code behind class that looks like this:

using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
namespace BlazorWebAssembly.Client.JSInteropSamples;
[SupportedOSPlatform("browser")]
public partial class JSToStaticNET
{
    [JSExport]
    internal static string GetAMessageFromNET()
    {
        return "This is a message from .NET";
    }
    [JSImport("showMessage", "jstonet")]
    internal static partial void ShowMessage();
}

Here we are using the SupportedOSPlatform attribute to ensure that this code can only run on a browser.

The JavaScript portion of this demo looks like this:

export async function setMessage() {
    const { getAssemblyExports } = await globalThis.getDotnetRuntime(0);
    var exports = await getAssemblyExports("BlazorWebAssembly.Client.dll");
    alert(exports.BlazorWebAssembly.Client.JSInteropSamples.JSToStaticNET.GetAMessageFromNET());
}
export async function showMessage() {
    await setMessage();
}

We call the showMessage JavaScript function from .NET, and it will then call the setMessage function.

The setMessage function uses the globalThis object to access the .NET runtime and get access to the getAssemblyExports method.

It will retrieve all the exports for our assembly and then run the method. The .NET method will return the string "This is a message from .NET" and show the string in an alert box.

Even though I prefer not to make any JavaScript calls in my Blazor applications, I love having the power to bridge between .NET code and JavaScript code with ease.

Summary

This chapter taught us about calling JavaScript from .NET and calling .NET from JavaScript. In most cases, we won’t need to do JavaScript calls, and chances are that the Blazor community or component vendors have solved the problem for us.

We also looked at how we can port an existing library if needed.

In the next chapter, we will continue to look at state management.

Join our community on Discord 

Join our community’s Discord space for discussions with the author and other readers: 

https://packt.link/WebDevBlazor2e

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset