Skip to content

DotNetInstanceCallbackHandler

Mika Berglund edited this page Feb 25, 2022 · 5 revisions

DotNetInstanceCallbackHandler Class

This class is designed to support advanced interop scenarios combining calling JavaScript functions from .NET with calling .NET methods from JavaScript. For instance, if you want to leverage a JavaScript library that returns its data using callback methods or Promise objects, then this class will help you a lot.

Inheritance

DotNetInstanceCallbackHandler : IDisposable

Please note! The DotNetInstanceCallbackHandler class implements the IDisposable interface, you need to dispose of the object instance when you are done with it to prevent memory leaks and other unwanted side effects.

Examples

Using IJSRuntime

This sample shows you how you can benefit from the DotNetInstanceCallbackHandler class in a more traditional way where you have plain JavaScript functions that you invoke from your .NET code using an implementation of the IJSRuntime interface.

This requires that you have added a script reference to all script files you need to your page template. This is normally done in Blazor Server applications to Pages/_Host.cshtml, and to wwwroot/index.html in Blazor WebAssembly applications.

First, we'll take a look at the JavaScript.

function getDelayedData(args) {
    setTimeout(() => {
        args.successCallback.target
            .invokeMethodAsync(
                args.successCallback.methodName,
                args.data.data
            );
    }, 500);
}

Here, we simulate using libraries that require callbacks by using the setTimeout() function. The success callback will be called when the timeout has elapsed.

The data that we pass as argument to the callback is just something that we sent from our .NET code. Typically, that would be something that the library you use would return to the callback that we sent to the setTimeout() function in the sample above.

Then, in your Blazor application, you write something like this.

@page "/callbacks"
@inject IJSRuntime jsRuntime

@code{

    [Parameter]
    public string Data { get; set; }


    private async Task ButtonClickHandlerAsync(MouseEventArgs args)
    {
        this.Data = null;
        var input = new Dictionary<string, object>
        {
            { "data", Guid.NewGuid() }
        };

        using (var handler = new DotNetInstanceCallbackHandler<string>(this.jsRuntime, "getDelayedData", input))
        {
            this.Data = await handler.GetResultAsync();
            this.StateHasChanged();
        }
    }
}
<button @onclick="this.ButtonClickHandlerAsync">Click to get data</button>
<p>
    Data: @this.Data
</p>

Here, we just generate a new Guid that we pass to the JavaScript function, and then display it when it is returned back to our .NET code. In a real-world scenario you would of course use something else as input and get something else back. This is just a simple example to demonstrate how the DotNetInstanceCallbackHandler class works.

Using Imported JavaScript Module

The code sample below demonstrates how you use the DotNetInstanceCallbackHandler class in your own code with an imported JavaScript module. The JavaScript code is simplified as much as possible to give a better overview, and just uses the setTimeout() function where the callback back to .NET is called after the timeout has elapsed.

The JavaScript code then looks like this.

export function getDelayedTimeString(args) {
    try {
        setTimeout(() => {
            args.successCallback.target
                .invokeMethodAsync(
                    args.successCallback.methodName, 
                    new Date().toLocaleTimeString()
                );
        }, 100);
    }
    catch(err) {
        args.failureCallback.target
            .invokeMethodAsync(
                args.failureCallback.methodName,
                err
            );
    }
}

Note that instead of setTimeout(), you would call whatever JavaScript SDK or library the requires you to pass in a callback function, which then is executed when the data you request is ready.

Then, in you component class you would need to have the following using statements.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using Blazorade.Core.Components;
using Blazorade.Core.Interop;

Now, your component class could define the following members.

public class MyComponent : BlazoradeComponentBase
{
    [Parameter]
    public string Data { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if(firstRender)
        {
            using(var handler = await this.GetHandlerAsync())
            {
                this.Data = await handler.GetResultAsync();
            }
        }
    }

    private async Task<DotNetInstanceCallbackHandler> GetHandlerAsync()
    {
        var module = await this.GetJsModuleAsync();
        return new DotNetInstanceCallbackHandler<string>(
            module, 
            "getDelayedTimeString"
        );
    }

    Task<IJSObjectReference> jsModule;
    private Task<IJSObjectReference> GetJsModuleAsync()
    {
        return this.jsModule ??=
            this.jsRuntime
                .InvokeAsync<IJSObjectReference>(
                    "import",
                    "./js/your-js-module.js"
                )
                .AsTask();
    }

}

The GetHandlerAsync and GetJsModuleAsync methods are just helper methods that you can easily refactor out to a base class or a helper class.

The real action happens in this example in the OnAfterRenderAsync method. That method creates an instance of the DotNetInstanceCallbackHandler<string> class and then just calls the GetResultAsync() method. That's all. Everything else is taken care of by the DotNetInstanceCallbackHandler class.

Handling Exceptions

The following sample shows you how tyou can use the DotNetInstanceCallbackHandler class to handle errors that occur in your JavaScript.

First, the JavaScript implementation.

function doStuff(args) {
    try {
        let result = new Date().toLocaleTimeString();
        // Do something that will produce the result you need.

        args.successCallback.target
            .invokeMethodAsync(
                args.successCallback.methodName, 
                result
            );
    }
    catch(err) {
        // In case of an exception, we signal that back to
        // our Blazor code with the failureCallback.

        args.failureCallback.target
            .invokeMethodAsync(
                args.failureCallback.methodName,
                { error: err }
            );
    }
}

Then in your Blazor code you would write something like shown below.

private async Task CallJavaScriptAsync(IJSObjectReference module)
{
    // Get the module as shown in the samples above.

    string result = null;
    var handler = new DotNetInstanceCallbackHandler<string> (
        module, 
        "doStuff"
    );
    try {
        result = await handler.GetResultAsync();
    }
    catch (FailureCallbackException ex) {
        // An exception will be thrown if your JavaScript
        // code calls into the failure callback.
    }
    finally {
        // You must make sure that you call the Dispose method
        // on the handler when you are done with it.
        handler.Dispose();
    }
}

The point to note here is that if your JavaScript code will be calling into the failureCallback method, then that will throw an exception in your Blazor code that you need to catch. The exception that is thrown is always FailureCallbackException. This exception contains the data that your JavaScript code sent to the failure callback.

Timeout

If your JavaScript function does not call either the successCallback or failureCallback methods, the GetResultAsync() method would never complete. That's why the GetResultAsync() method defines the timeout argument that specifies the number of milliseconds the method will wait for your JavaScript to call back.

The default is 3000 milliseconds, but you can specify any timeout, as long as you specify a value. If you try to set timeout to null, it will default to 3000 ms.

If the call times out, the InteropTimeoutException exception will be thrown.

Summary

The benefit of using this class over the DotNetInstanceMethod class is that you don't have to synchronize the execution of the method where you need the data with the method that you specify with the DotNetInstanceMethod class and that will be called by your JavaScript code.