-
Notifications
You must be signed in to change notification settings - Fork 1
DotNetInstanceCallbackHandler
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.
DotNetInstanceCallbackHandler : IDisposable
Please note! The
DotNetInstanceCallbackHandler
class implements theIDisposable
interface, you need to dispose of the object instance when you are done with it to prevent memory leaks and other unwanted side effects.
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.
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.
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.
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.
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.