11

Is there a way to add callback to Javascript and get the result in Blazor? Apart from JS Promises.

for example, let say i want to load a file

Javascript Code

window.readFile = function(filePath, callBack) {
    var reader = new FileReader();
    reader.onload = function (evt) {
        callBack(evt.target.result);
    };
    reader.readAsText(filePath);
}

Can i have something like this in Blazor C#

    // read file content and output result to console
    void GetFileContent() {
        JsRuntime.InvokeAsync<object>("readFile", "file.txt", (string text) => {
            Console.Write(text);
        });
    }

Or Maybe something like this

    // read with javascript
    void ReadFileContent() {
        JsRuntime.InvokeAsync<object>("readFile", "file.txt", "resultCallbackMethod");
    }

    // output result callback to console
    void resultCallbackMethod(string text) {
        Console.Write(text);
    }

Thanks

2
  • I don't know about the callback, but couldn't you use a promise instead?
    – Silvermind
    Commented Jun 17, 2019 at 9:07
  • i can, but i have this bunch of codes with callback only.
    – samtax01
    Commented Jun 17, 2019 at 9:36

4 Answers 4

7

UPDATE 1:

After re-reading your question, I think this would cover your 2nd example

I think you have the option of implementing a JS proxy function that handle the calling. Something like this:

UPDATE 2:

Code was updated with a functional (but not deeply tested) version, you can also find a working example in blazorfiddle.com

JAVASCRIPT CODE

// Target Javascript function
window.readFile = function (filePath, callBack) {

    var fileInput = document.getElementById('fileInput');
    var file = fileInput.files[0];

    var reader = new FileReader();

    reader.onload = function (evt) {
        callBack(evt.target.result);
    };

    reader.readAsText(file);

}

// Proxy function
// blazorInstance: A reference to the actual C# class instance, required to invoke C# methods inside it
// blazorCallbackName: parameter that will get the name of the C# method used as callback
window.readFileProxy = (instance, callbackMethod, fileName) => {

    // Execute function that will do the actual job
    window.readFile(fileName, result => {
        // Invoke the C# callback method passing the result as parameter
        instance.invokeMethodAsync(callbackMethod, result);
    });

}

C# CODE

@page "/"

@inject IJSRuntime jsRuntime

<div>
    Select a text file:
    <input type="file" id="fileInput" @onchange="@ReadFileContent" />
</div>
<pre>
    @fileContent
</pre>

Welcome to your new app.

@code{

    private string fileContent { get; set; }

    public static object CreateDotNetObjectRefSyncObj = new object();

    public async Task ReadFileContent(UIChangeEventArgs ea)
    {
        // Fire & Forget: ConfigureAwait(false) is telling "I'm not expecting this call to return a thing"
        await jsRuntime.InvokeAsync<object>("readFileProxy", CreateDotNetObjectRef(this), "ReadFileCallback", ea.Value.ToString()).ConfigureAwait(false);
    }


    [JSInvokable] // This is required in order to JS be able to execute it
    public void ReadFileCallback(string response)
    {
        fileContent = response?.ToString();
        StateHasChanged();
    }

    // Hack to fix https://github.com/aspnet/AspNetCore/issues/11159    
    protected DotNetObjectRef<T> CreateDotNetObjectRef<T>(T value) where T : class
    {
        lock (CreateDotNetObjectRefSyncObj)
        {
            JSRuntime.SetCurrentJSRuntime(jsRuntime);
            return DotNetObjectRef.Create(value);
        }
    }

}
3
  • You tried bro but it didn't work, Buh i am sure with little edit, it should. I am still looking for something more smart. Maybe with delegate or other means.
    – samtax01
    Commented Jun 17, 2019 at 16:46
  • @samtax01, I updated my answer: the pseudo-code was removed and replaced with working code, also a link to a working example was included. Commented Jun 17, 2019 at 19:36
  • Nowadays you can even use nameof(ReadFileCallback) to avoid hardcoding "ReadFileCallback".
    – user276648
    Commented Feb 17, 2023 at 14:05
3

Using the tips on this page I came up with a more generic version that works with nearly any callback based function.

Update:

You can now call any function whose last argument is a callback. You can pass any number of arguments to the function and the callback can have any number of arguments returned.

The function InvokeJS returns an instance of CallbackerResponse which can be used to get the typed value of any of the response arguments. See examples and code for more information.

Based on OP callback (fileContents (string)):

Example 1 (C# Blazor with await):

var response = await Callbacker.InvokeJS("window.readFile", filename);
var fileContents = response.GetArg<string>(0);
// fileContents available here

Example 2 (C# Blazor with callback):

Callbacker.InvokeJS((response) => { 
    var fileContents = response.GetArg<string>(0);
    // fileContents available here
}, "window.readFile", filename);

Based on common callback (error (string), data (object)):

Example 3 (C# Blazor with await):

// To call a javascript function with the arguments (arg1, arg2, arg3, callback)
// and where the callback arguments are (err, data)
var response = await Callbacker.InvokeJS("window.myObject.myFunction", arg1, arg2, arg3);
// deserialize callback argument 0 into C# string
var err = response.GetArg<string>(0);
// deserialize callback argument 1 into C# object
var data = response.GetArg<MyObjectType>(1);

In your Blazor Program.cs Main add singleton (or scoped if desired) Callbacker

builder.Services.AddSingleton<Services.Callbacker>();

Add Callbacker service in your Blazor page. Example: MyPage.razor.cs

[Inject]
public Callbacker Callbacker { get; set; }

C#

using Microsoft.JSInterop;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Home.Services
{
    public class CallbackerResponse
    {
        public string[] arguments { get; private set; }
        public CallbackerResponse(string[] arguments)
        {
            this.arguments = arguments;
        }
        public T GetArg<T>(int i)
        {
            return JsonConvert.DeserializeObject<T>(arguments[i]);
        }
    }

    public class Callbacker
    {
        private IJSRuntime _js = null;
        private DotNetObjectReference<Callbacker> _this = null;
        private Dictionary<string, Action<string[]>> _callbacks = new Dictionary<string, Action<string[]>>();

        public Callbacker(IJSRuntime JSRuntime)
        {
            _js = JSRuntime;
            _this = DotNetObjectReference.Create(this);
        }

        [JSInvokable]
        public void _Callback(string callbackId, string[] arguments)
        {
            if (_callbacks.TryGetValue(callbackId, out Action<string[]> callback))
            {
                _callbacks.Remove(callbackId);
                callback(arguments);
            }
        }

        public Task<CallbackerResponse> InvokeJS(string cmd, params object[] args)
        {
            var t = new TaskCompletionSource<CallbackerResponse>();
            _InvokeJS((string[] arguments) => {
                t.TrySetResult(new CallbackerResponse(arguments));
            }, cmd, args);
            return t.Task;
        }

        public void InvokeJS(Action<CallbackerResponse> callback, string cmd, params object[] args)
        {
            _InvokeJS((string[] arguments) => {
                callback(new CallbackerResponse(arguments));
            }, cmd, args);
        }

        private void _InvokeJS(Action<string[]> callback, string cmd, object[] args)
        {
            string callbackId;
            do
            {
                callbackId = Guid.NewGuid().ToString();
            } while (_callbacks.ContainsKey(callbackId));
            _callbacks[callbackId] = callback;
            _js.InvokeVoidAsync("window._callbacker", _this, "_Callback", callbackId, cmd, JsonConvert.SerializeObject(args));
        }
    }
}

JS

window._callbacker = function(callbackObjectInstance, callbackMethod, callbackId, cmd, args){
    var parts = cmd.split('.');
    var targetFunc = window;
    var parentObject = window;
    for(var i = 0; i < parts.length; i++){
        if (i == 0 && part == 'window') continue;
        var part = parts[i];
        parentObject = targetFunc;
        targetFunc = targetFunc[part];
    }
    args = JSON.parse(args);
    args.push(function(e, d){ 
        var args = [];
        for(var i in arguments) args.push(JSON.stringify(arguments[i]));
        callbackObjectInstance.invokeMethodAsync(callbackMethod, callbackId, args); 
    });
    targetFunc.apply(parentObject, args);
};
1
  • An elegant solution. Just one think I needed to add it as a scoped service - services.AddScoped<Services.Callbacker>(); - as opposed to a singleton. I suspect that might be because I am doing blazor server. Other than that, works perfectly for me.
    – Jonny
    Commented Jul 26, 2021 at 12:11
2

I believe that you are looking for the info on the documentation here: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.0#invoke-net-methods-from-javascript-functions

It shows how to call the Razor.Net from Javascript. The documentation has more information, but essentially you will need the [JSInvokable] attribute on the method in razor, and calling via DotNet.invokeMethod in javascript.

1
  • Thanks, But Not really, Just a direct Callback. Something like passing a Result Callback Closure to JsRuntime.InvokeAsync straight.
    – samtax01
    Commented Jun 17, 2019 at 11:00
1

Thanks for that @Henry Rodriguez. I created something out of it, and i believed it might be helpful as well.

Note that DotNetObjectRef.Create(this) still works fine in other method. It is only noted to have problem with Blazor lifecycle events in preview6. https://github.com/aspnet/AspNetCore/issues/11159.

This is my new Implementation.

<div>
    Load the file content
    <button @click="@ReadFileContent">Get File Content</button>
</div>

<pre>
    @fileContent
</pre>

Welcome to your new app.

@code{
string fileContent;

//The button onclick will call this.
void GetFileContent() {
     JsRuntime.InvokeAsync<object>("callbackProxy", DotNetObjectRef.Create(this), "readFile", "file.txt", "ReadFileCallback");
}


//and this is the ReadFileCallback

[JSInvokable] // This is required for callable function in JS
public void ReadFileCallback(string filedata) {
    fileContent = filedata;
    StateHasChanged();
}

And in blazor _Host.cshtml or index.html, include the callback proxy connector

// Proxy function that serves as middlemen
 window.callbackProxy =  function(dotNetInstance, callMethod, param, callbackMethod){
    // Execute function that will do the actual job
    window[callMethod](param, function(result){
          // Invoke the C# callback method passing the result as parameter
           return dotNetInstance.invokeMethodAsync(callbackMethod, result);
     });
     return true;
 };



// Then The Javascript function too

 window.readFile = function(filePath, callBack) {
    var reader = new FileReader();
    reader.onload = function (evt) {
        callBack(evt.target.result);
    };
    reader.readAsText(filePath);
}

This works perfectly for what i needed, and it is re-usable.

Not the answer you're looking for? Browse other questions tagged or ask your own question.