Search code examples
javascriptc#blazorwebassembly

Blazor webassembly (WASM) move razor javascript invokes to a class


Description

Hi, I have a simple Blazor wasm project where I have javascript invoke / interop's. In a .razor page this works. I have a small illustration project on Github, the page Pages/TestJS.razor invokes a .js function which I print out to the user. I have to capsulate and add that javascript invoke's now to a separate class, so I can standardize and reuse the functions on different pages.

Problem

The new class I want to build in my invoke's is in Classes/JSFunctionHandler_Shared.cs accessing the javascript function in TestEmbed.js. This function I am integrating on the page Pages/TestJSEmbed.razor for test. But now it returns a exception when I initialize the javascript part in the new class.

Blazor page direct js access (works):

@page "/TestJS"
@inject IJSRuntime JsRuntime

<h3>Test JS (in razor component)</h3>

<button @onclick="onTestJS">Test JS</button>

@code {

    public async Task onTestJS()
    {
       await JsRuntime.InvokeAsync<object>("TestJS"); // Test.js
    }
   
}

Blazor page new with js in a class(not wirking):

@page "/TestJSEmbed"
@using Classes
@inject IJSRuntime JsRuntime

<h3>Test JS Embed (interop by class)</h3>

<button @onclick="onTestJSEmbed">Test JS</button>

@code {

    JSFunctionHandler JSTest = new JSFunctionHandler();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            await JSTest.Init();
    }

    public async Task onTestJSEmbed()
    {
        await JSTest.TestFunction_JSTestEmbed();
    }
 
}

Class with javascript handling:

using Microsoft.JSInterop;
using Microsoft.AspNetCore.Components;

namespace Classes
{
    public partial class JSFunctionHandler
    {

        [Inject]
        public IJSRuntime JSRuntime { get; set; }
        private IJSObjectReference _jsModule;


        public async Task Init()
        {
            _jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./JS/TestEmbed.js");
        }

        public async Task TestFunction_JSTestEmbed()
        {
            await _jsModule.InvokeAsync<object>("JSTestEmbed");
        }

    }
}

Note: The javascript file I would like to access in the class "JSFunctionHandler" I have not added to the index.html because I am loading it in the init() method. This just because I have not found any other example do this for example over index.html assignment.. This could be changed of course.

A small example project just for illustration of the two scenarios is available in Github

Exception (when opening page TestJSEmbed.razor)

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Value cannot be null. (Parameter 'jsRuntime') System.ArgumentNullException: Value cannot be null. (Parameter 'jsRuntime') at Microsoft.JSInterop.JSRuntimeExtensions.InvokeAsync[IJSObjectReference](IJSRuntime jsRuntime, String identifier, Object[] args) at Classes.JSFunctionHandler.Init() in C:\Users\Operator\Documents\GitHub\testdata\BlazorWASM_JSInvoke\ExperimentalTest\Classes\JSFunctionHandler_Shared.cs:line 16 at ExperimentalTest.Pages.TestJSEmbed.OnAfterRenderAsync(Boolean firstRender) in C:\Users\Operator\Documents\GitHub\testdata\BlazorWASM_JSInvoke\ExperimentalTest\Pages\TestJSEmbed.razor:line 16


Solution

  • Try making the javascript exportable, so instead of

    window.JSTestEmbed = async () => {
        alert("JSTestEmbed Js interop works!");
    }
    

    use

    export function JSTestEmbed = () => {// this doesn't really need to be async, right?
        alert("JSTestEmbed Js interop works!"); 
    }
        
    

    Also, if you don't expect a return value, simply use InvokeVoidAsync instead of InvokeAsync<object>

    edit

    Since the class you added is not a component and from your exception message it seems JSRuntime is null, inject the JSRuntime like this:

    In the blazor page

    // other code, html
    @code {
    [Inject]
    public JSFunctionHandler JSTest {get; set;}
    
    // rest of the code
    }
    

    For this to work, you have to add it to the Dependency System in your Program.cs

    `builder.Services.AddSingleton<JSFunctionHandler, JSFunctionHandler >();
    

    And finally in your handler class inject it into your constructor as such:

    private readonly IJSRuntime _jsRuntime;
    
    public JSFunctionHandler (IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }
    
    public async Task Init()
    {
        _jsModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./JS/TestEmbed.js");
    }
    

    Hth