How can i get Javascript Callback in .Net Blazor? - c#

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

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);
}
}
}

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.

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);
};

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.

Related

How to call Angular2 Function from C# code

Hi I want to invoke a method in my angular app from C# code. My angular app resides inside WPF WebBrowser control. Below are the code snippets from C# & Angular.
C# Code snippet:
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
[ComVisible(true)]
public partial class WebBrowserView : UserControl
{
public WebBrowserView()
{
InitializeComponent();
myBrowser.Unloaded += myBrowser_Unloaded;
}
private void myBrowser_Unloaded(object sender, RoutedEventArgs e)
{
// This works:
myBrowser.InvokeScript("execScript", new object[] { "this.alert(123)", "JavaScript" });
// This is what I actually want, but it doesn't work:
// For both of these I get the Script Error:
// System.Runtime.InteropServices.COMException: 'Exception from HRESULT: 0x80020101'
myBrowser.InvokeScript("eval", "alertExample()");
string javascript = "CoreHelper.alertExample();";
myBrowser.InvokeScript("eval", new object[] { javascript });
}
}
Angular 10 snippet:
Global function in Global.ts file:
export function alertExample(){
alert('test');
}
Inside an abstract class CoreHelper.ts
export class CoreHelper {
public static alertExample(){
alert('happy');
}
}
Definitely my alertExample is intended to do a lot more than alerting. I do not want to put any scripts inside index.html.
What is that I am missing/doing wrong here?
I also tried adding the script directly in index.html:
Angular 10 index.html:
<script type="text/javascript">
function test(params) {
alert('index.html');
}
</script>
C#
// This works:
myBrowser.InvokeScript("test");
// This doesn't work:
myBrowser.InvokeScript("eval", new object[] { "test" });
Also tried this:
Angular 10 index.html:
<script type="text/javascript" src="./assets/scripts/global-scripts.js">
</script>
global-scripts.js
function test() {
alert('You have arrived: ');
}
C#
// None of these work:
myBrowser.InvokeScript("test");
myBrowser.InvokeScript("eval", new object[] { "test" });
Thanks,
RDV
Posting my solution for this issue:
C# end:
WebBrowserView (XAML View) code:
We can use WebBrowser Navigating event, I needed more control, hence using explicit API to shutdown the connection.
public Tuple<bool, string> OnShutdownEvent(string scriptName)
{
Tuple<bool, string> result = new Tuple<bool, string>(true, "");
if (scriptName.IsNullOrEmpty())
{
return result;
}
try
{
DependencyObject sender = VisualTreeHelper.GetChild(myBrowser, 0);
if (sender != null)
{
if ((sender is WebBrowser) && ((sender as WebBrowser).Document != null))
{
//If the script is not present, we fall in catch condition
//No way to know if the script worked fine or not.
(sender as WebBrowser).InvokeScript(scriptName);
result = new Tuple<bool, string>(true, string.Format("Successfully executed script- {0}", scriptName));
}
else
{
result = new Tuple<bool, string>(true, "No script invoked.");
}
}
}
catch (Exception e)
{
result = new Tuple<bool, string>(false, e.Message);
}
return result;
}
WebBrowserViewModel code snippet:
public bool OnHandleShutdown()
{
try
{
Tuple<bool, string> result = (this.View as XbapPageView).OnShutdownEvent("closeConnection");
}
catch (Exception) { }
finally
{
XBAP_URI = "about:blank";
}
return true;
}
Now in the angular app, following changes are required:
index.html
<script type="text/javascript">
function closeConnection(params) {
window.webSocketServiceRef.zone.run(
function () {
window.webSocketServiceRef.closeConnection(params);
});
}
webSocketService.ts:
constructor(
private _webSocketService: TestWebsocketService,
private _ngZone: NgZone) {
window['webSocketServiceRef'] = {
component: this,
zone: this._ngZone,
closeConnection: (params) => this.onCloseConnectionEvent(params)
};
}
onCloseConnectionEvent(params) {
this._msgSubscription.unsubscribe();
this._webSocketMsgSubject.complete();
return true;
}
Now WebSocket connection is properly closed.
Thanks,
RDV

Blazor pass a javascript object as an argument for IJSRuntime

I want to pass a javascript object that is already existed in a javascript file as a parameter to InvokeVoidAsync method :
myJs.js :
var myObject= {a:1,b:2};
MyComponent.razor :
#inject IJSRuntime js
#code {
private async Task MyMethod()
{
await js.InvokeVoidAsync("someJsFunction", /*how to add myObject here? */);
}
}
How to pass myObject to the someJsFunction function?
(Note that the function may also should accept c# objects too)
I finally found the trick, simply use the eval function :
private async Task MyMethod()
{
object data = await js.InvokeAsync<Person>("eval","myObject");
await js.InvokeVoidAsync("someJsFunction", data );
}
in the example below, "someJsFunction" accepts an object of type Person :
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
in my javascript file
var myObject = { name: "Pat", age: 38 }; //a person
//this function provides access to myObject
window.getMyObject = () =>
{
return myObject;
};
window.someJsFunction = (person) =>
{
console.log(person);
}
In the component :
#page "/"
#inject IJSRuntime js
<div>
<button #onclick="MyMethod" >Click</button>
</div>
#code{
Person Person;
protected async override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
//retrieve myOject and assign to the field Person
Person = await js.InvokeAsync<Person>("getMyObject");
}
}
private async Task MyMethod()
{
if(Person != null)
//pass Person to someJsFunction
await js.InvokeVoidAsync("someJsFunction", Person);
}
}
You could define a new JS function
someJsFunctionWithObj(){
someJsFunction(myObject);
}
Then call that instead...
await js.InvokeVoidAsync("someJsFunctionWithObj");
Or create a JS function that returns the object -> call that from C# to get it -> then pass it back, but that seems quite convoluted.
It sounds like probably you should rethink your design really... Maybe if you explained why you are actually trying to do this we can suggest better idea?

Asynchronous console program hangs on completion of task using CefSharp

In my quest to create the perfect string result = browser.Browse(url) method, I have created a simple class library to demonstrate CefSharp. The code for that is:
public class CefSharpHeadlessBrowser
{
public CefSharpHeadlessBrowser()
{
Cef.Initialize(new CefSettings { CachePath = "cache" }, false, true);
}
public string Browse(string url)
{
Task<string> result;
var browserSettings = new BrowserSettings { WindowlessFrameRate = 1 };
using (var browser = new ChromiumWebBrowser(url, browserSettings))
{
browser.WaitForBrowserToInitialize();
browser.LoadPageAsync();
// Wait awhile for Javascript to finish executing.
Thread.Sleep(2000);
result = browser.GetSourceAsync();
Thread.Sleep(100);
}
return result.Result;
}
}
public static class CefExtensions
{
public static void WaitForBrowserToInitialize(this ChromiumWebBrowser browser)
{
while (!browser.IsBrowserInitialized)
{
Task.Delay(100);
}
}
public static Task LoadPageAsync(this IWebBrowser browser)
{
var tcs = new TaskCompletionSource<bool>();
EventHandler<LoadingStateChangedEventArgs> handler = null;
handler = (sender, args) =>
{
if (!args.IsLoading)
{
browser.LoadingStateChanged -= handler;
tcs.TrySetResult(true);
}
};
browser.LoadingStateChanged += handler;
return tcs.Task;
}
}
This is the test harness, in a separate console project that references the CefSharpHeadlessBrowser project:
class Program
{
static void Main(string[] args)
{
const string searchUrl = "https://www.google.com";
var browser = new CefSharpHeadlessBrowser();
var result = browser.Browse(searchUrl);
Console.Write(result);
}
}
This actually works; it gets the HTML page source properly and displays it in the console window, just as it should. But here's the problem: the console program hangs after displaying the page source. It should exit immediately. Which must mean that I'm doing something wrong with the asynchronous operations and causing a deadlock.
What could be the issue?
CefSharp has a Shutdown command; I was able to solve the problem by adding the following method to the CefSharpHeadlessBrowser class:
public void Shutdown()
{
Cef.Shutdown();
}
And then changing the Test Harness to:
class Program
{
static void Main(string[] args)
{
const string searchUrl = "https://www.google.com";
var browser = new CefSharpHeadlessBrowser();
var result = browser.Browse(searchUrl);
Console.WriteLine(result);
browser.Shutdown(); // Added
}
}
This undoubtedly frees up any remaining threads that are running.
I'll probably make the class IDisposable and wrap the calling code in a using statement.

Output a variable name before the Json result

I'm outputting the localization key/value pairs present in the JS localization resource into lang.js like this:
[Route("js/lang.js")]
public ActionResult Lang()
{
ResourceManager manager = new ResourceManager("Normandy.App_GlobalResources.JsLocalization", System.Reflection.Assembly.GetExecutingAssembly());
ResourceSet resources = manager.GetResourceSet(CultureInfo.CurrentCulture, true, true);
Dictionary<string, string> result = new Dictionary<string, string>();
IDictionaryEnumerator enumerator = resources.GetEnumerator();
while (enumerator.MoveNext())
result.Add((string)enumerator.Key, (string)enumerator.Value);
return Json(result);
}
The contents of /js/lang.js are (I include the file with a normal <script> tag):
{"Test":"test","_Lang":"en"}
Is there any way to make them be:
var LANG = {"Test":"test","_Lang":"en"}
You could use JSONP. Write a custom action result:
public class JsonpResult: ActionResult
{
public readonly object _model;
public readonly string _callback;
public JsonpResult(object model, string callback)
{
_model = model;
_callback = callback;
}
public override void ExecuteResult(ControllerContext context)
{
var js = new JavaScriptSerializer();
var jsonp = string.Format(
"{0}({1})",
_callback,
js.Serialize(_model)
);
var response = context.HttpContext.Response;
response.ContentType = "application/json";
response.Write(jsonp);
}
}
and then in your controller action return it:
[Route("js/lang.js")]
public ActionResult Lang()
{
...
return new JsonpResult(result, "cb");
}
and finally define the callback to capture the json before including the script:
<script type="text/javascript">
function cb(json) {
// the json argument will represent the JSON data
// {"Test":"test","_Lang":"en"}
// so here you could assign it to a global variable
// or do something else with it
}
</script>
<script type="text/javascript" src="js/lang.js"></script>
It looks like you want to return a script, instead of a json object. in that case I would do one of 2 things
return an action result that encapsulates a script rather than json (not sure if one exists)
return json and then set that json to a local variable on the client (typical ajax call)

How to keep retrying the reactive method until it succeeds?

Here is my async download reactive extension for WebClient.
What is the best way to recall "DownloadStringAsync" again and again till the operation succeeds?
Something like this but in reactive way:
while (true)
{
var result = DownloadStringAsync();
if (result)
{
return;
}
}
MY CODE:
[Serializable]
public class WebClientException : Exception
{
public WebClientResponse Response { get; set; }
public WebClientException()
{
}
public WebClientException(string message)
: base(message)
{
}
public WebClientException(string message, Exception innerException)
: base(message, innerException)
{
}
protected WebClientException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
public class WebClientResponse
{
public WebHeaderCollection Headers { get; set; }
public HttpStatusCode StatusCode { get; set; }
public string Result { get; set; }
public WebException Exception { get; set; }
}
public static IObservable<WebClientResponse> DownloadStringAsync(this WebClient webClient, Uri address, WebHeaderCollection requestHeaders)
{
var asyncResult =
Observable.FromEventPattern<DownloadStringCompletedEventHandler, DownloadStringCompletedEventArgs>
(ev => webClient.DownloadStringCompleted += ev, ev => webClient.DownloadStringCompleted -= ev)
.ObserveOn(Scheduler.TaskPool)
.Select(o =>
{
var ex = o.EventArgs.Error as WebException;
if (ex == null)
{
var wc = (WebClient) o.Sender;
return new WebClientResponse {Headers = wc.ResponseHeaders, Result = o.EventArgs.Result};
}
var wcr = new WebClientResponse {Exception = ex};
var r = ex.Response as HttpWebResponse;
if (r != null)
{
wcr.Headers = r.Headers;
wcr.StatusCode = r.StatusCode;
var s = r.GetResponseStream();
if (s != null)
{
using (TextReader tr = new StreamReader(s))
{
wcr.Result = tr.ReadToEnd();
}
}
}
throw new WebClientException {Response = wcr};
})
.Take(1);
if (requestHeaders != null)
{
foreach (var key in requestHeaders.AllKeys)
{
webClient.Headers.Add(key, requestHeaders[key]);
}
}
webClient.DownloadStringAsync(address);
return asyncResult;
}
Your method produces a hot observable, which means that it has already started loading when it returns and each new subscription does not create a new request to the web server. You need to wrap your method in another and use Observable.Create (in order to create a cold observable which does create a new request upon each subscription):
public static IObservable<WebClientResponse> DownloadStringAsync(this WebClient webClient, Uri address, WebHeaderCollection requestHeaders)
{
return Observable
.Create(observer =>
{
DownloadStringAsyncImpl(webClient, address, requestHeaders)
.Subscribe(observer);
return () => { webClient.CancelAsync(); };
});
}
Here, DownloadStringAsyncImpl is your previous implementation of DownloadStringAsync, while the public method has been replaced.
Now you can retry the async method until it succeeds as follows:
myWebClient
.DownloadStringAsync( /* args... */)
.Retry()
.Subscribe(result => {
/* now I've got a result! */
});
I think you have at least one decent "here is some code" answer, so I will focus on a more general hand holding.
The first thing I would look at is the design guidelines for Rx. It is a short (34 page) PDF document that helps change paradigm from pull "subscriptions" to push, or moving from IEnumerable to IObservable.
If you want to go a bit further, there are PDF HOLs (hands on labs) for both .NET and JavaScript. You can find other resources on the Rx pages (start here).
If it is an async function. Doing a repetitive checking means you turned it into a sync function call. Is this something you really want to do?
You can have a dedicated thread calling this async function and block itself after calling this function. When create this thread, pass it a delegate that should be called after the async function returns. Upon completion, call the delegate with error code.
Hope this answers your question.

Categories