dotnet / AspNetCore.Docs

Documentation for ASP.NET Core
https://docs.microsoft.com/aspnet/core
Creative Commons Attribution 4.0 International
12.6k stars 25.29k forks source link

Is calling a JS method or dereferencing JS object possible? #30311

Closed macias closed 2 months ago

macias commented 1 year ago

[EDIT by guardrex to add the metadata]

The documentation: https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-7.0 or https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-7.0#javascript-interop-calls-without-a-circuit

Describe the new topic

I fail to see how they address the issue of calling a JS method (instance one, non-static), or if not possible -- how to dereference obtained reference. The only scenarios I found is disposing JS reference and calling a function within module.

Consider such case:

  1. create instance of a class in JS
  2. return the reference of this instance to C#
  3. somehow call a method on this reference

If this is not possible, it would be good to mention it in documentation. If it is possible, small example would be helpful.

The stopping step for me is (3). I tried two approaches -- calling a method, like here, JS part:

class Dummy{
   dummyMethod = function()
   {
   }
}

function dummyCall() {
  let dummy = new Dummy();
  let result = DotNet.createJSObjectReference(dummy);
  return  result;
}

and C#:

  var instance = await JsRuntime.InvokeAsync<IJSObjectReference>("dummyCall");
  await instance.InvokeVoidAsync("dummyMethod");

This gives me an error:

Microsoft.JSInterop.JSException: Could not find 'dummyMethod' ('dummyMethod' was undefined).

The second approach would be to call a function via JsRuntime and pass one parameter to it -- obtained reference (instance in the above code). However here I have no idea how to dereference it on JS side -- I have the reference, and I need to obtain JS object out of it, in other words "undo" what createJSObjectReference did.

Lack of documentation about the proper method call is troublesome for me here.


Document Details

Do not edit this section. It is required for learn.microsoft.com ➟ GitHub issue linking.

guardrex commented 1 year ago

Hello @macias ...

I'm a bit buried in .NET 8 release work at the moment, so I might need to get back to you later in the week or early next week.

In passing ...

If you're just trying to call a JS method of a JS class, you should only need to use dot notation ({JS CLASS NAME}.{JS METHOD NAME}) with IJSRuntime. The class is just part of the method's scope in window.someScope.someFunction covered where IJSRuntime is explained at the top of the article. In a basic use case, the approach is more along the lines of your second approach, but I'm not sure why you're focused on dereferencing. There's nothing to dereference because createJSObjectReference isn't used to create an object that requires disposal. An example from the article that's based on calling a JS class method is the long-running JS example at ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-7.0#abort-a-long-running-javascript-function

... of course without paying attention to the long-running JS aspects ... just looking at how the JS class/method is referenced by the calling code on the .NET side ...

<script>
  class Helpers {
    static async longRunningFn() {
        ...
      }
    }
  }

  window.Helpers = Helpers;
</script>
await JS.InvokeVoidAsync("Helpers.longRunningFn");

... BUT, I can't tell from your issue exactly what you're trying to do. You'd have to explain out further what you're trying to do if you're trying to do something different than the simplest case.

If we need to discuss this further, I'll need to get back to you ... hopefully by the end of the week, but perhaps next week. Also, you can discuss subjects like this further with community devs in the meantime. We recommend the usual spots ...

WRT a more explicit example of how a JS class is part of the method's scope for a JS call, Yes! I feel that's something that this article could use as an early example on how to set up the call in that basic case. I don't want readers to have to sift through all of this coverage and only see it as a part of that latter example on calling a long-running JS FN. I can certainly add something near where the method scope is described.

... and btw just for future reference, use the This page feedback button and form at the bottom of the English-US topic. Use of the This page feedback form adds metadata to your GitHub issue that cross-links the topic and automatically pings the author.

Capture

I'll add the metadata manually to this issue.

macias commented 1 year ago

@guardrex Thank you very much for your answer and pointing out how to better post an issue/feedback.

I didn't try to call static method, but it looks like the approach you describe should work (for static methods). However I try to accomplish something different -- a call to JS method per given instance (I already updated my original post, I am sorry if this was not clear).

So pretty much I would like to cover this scenario:

  1. C# calls JS to create instance of some JS class -- I know how to do this
  2. JS creates the instance and returns it to C# -- I hope dummyCall from my example covers this
  3. C# calls given method on this particular instance (so the method is not static) -- in my example I try to call dummyMethod on instance of Dummy type
guardrex commented 1 year ago

Did you try invoking it with dot notation, like this ...

await instance.InvokeVoidAsync("Dummy.dummyMethod");

This is a pattern that I'm not quite familiar with. We have a bit of guidance on passing a created JS object reference from JS to .NET, but it passes the JS obj on a call from JS to .NET, not returned from a JS FN call to a .NET method caller. However, I don't see why what you're doing would be a problem. It seems like it would work the way that you've structured it. The guidance I'm referring to is at ...

https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-7.0#create-javascript-object-and-data-references-to-pass-to-net

Based on that, it seems like what you're trying to do should be supported.

First, check on the scope and see if prefixing with the class name works, like ...

await instance.InvokeVoidAsync("Dummy.dummyMethod");

If that still throws, I'll ping a Programmer Wiz™ 😄 on the product unit to help.

macias commented 1 year ago

JS code:

class Dummy{
    dummyMethod = function()
    {
        console.log("Can you see me");
    }
}

window.dummyCall = function () {
    console.log("Creating dummy")
    let dummy = new Dummy();
    dummy.dummyMethod();
    console.log("JS creating ref")
    let result = DotNet.createJSObjectReference(dummy);
    console.log("JS returning ref")
    return result;
};

C# code:

        var instance = await JsRuntime.InvokeAsync<IJSObjectReference>("dummyCall");
        await instance.InvokeVoidAsync("Dummy.dummyMethod"); // throws exception

The exception says:

Microsoft.JSInterop.JSException: Could not find 'Dummy.dummyMethod' ('Dummy' was undefined).

Of out curiosity I moved "Dummy" class within "window" scope, and tried again. It throws again at the same point with the same message.

Btw. this reference instance is just a mere identifier holder, there is nothing with it, so to make such call, dereferencing it is needed. If the check is made directly, it is no surprise nothing can be found within it.

guardrex commented 1 year ago

Pinging @MackinnonBuck to take a look at this scenario to see if it's supported ... if so, how is it composed?

Stand-by ....... The product unit is a bit swamped today getting .NET 8 RC1 out. It might take a day (or a few) for Mackinnon to get free.

guardrex commented 1 year ago

UPDATE (10/2): AFAICT, the product unit is swamped with work for .NET 8. @macias, I'll leave it to you if you want to open a product unit support issue for them on their repo at ...

https://github.com/dotnet/aspnetcore/issues

If you do, leave this issue open and please add ...

cc: @guardrex https://github.com/dotnet/AspNetCore.Docs/issues/30311

... to the bottom of your opening comment so that I can follow along. If they identify that this pattern isn't supported and offer a different approach, I can work that into the docs. If they show/explain what's wrong with your approach, I can work that in. If they find out that this is just a 🐞 in the framework, then we might end up closing this because we don't usually document bugs ... they just fix them and that's the end of it. In any case, leave this issue open if you open an issue for them on their repo.

You'll need to cut-'n-paste the content out of here into your opening comment there, as they won't work the issue by looking at this one. You'll need to explain out there the scenario and show the code example(s) like you did here.

If you want to wait until .NET 8 launches, when I think they won't be quite so busy, I can ping them in late November to look at this docs issue.

There's also always the possibility of discussing this pattern with the community gurus on support chats. We recommend the usual places ...

macias commented 1 year ago

@guardrex Thank you for the update. "If you want to wait until .NET 8 launches," -- yes, there is no point in adding burden to the team, I already wrote my small workaround. As for asking -- I did it even before posting this issue, no answers, so maybe something is broken/missing indeed. Let's wait for after-Net 8 time :-).

guardrex commented 1 year ago

@macias ... Ok, sounds good. We'll just leave this open. When you open for them, use that CC: line ☝️ to cross-link this to that issue. If I don't see over there how they finally resolved it, you can ping me back here to take a look at the PU issue to find out what they want to do/say about it.

Yannike commented 9 months ago

@macias Did you have any luck with this issue, I think I am getting a similar problem : A javascript object is create inside a static function and returned to .net using DotNet.createJSObjectReference.

I then try to call a function from that JavaScript object from C# using InvokeVoidAsync and get the error "Could not find "

Here are some small generic snippets of want I am trying to do

On the Js (typescript) side

export class JavaScriptObject {
    test()
    {
        console.log("test)";
    }
}
static async CreateObject(): Promise<any> {
    let newObject = new JavascriptObject();
    return DotNet.createJSObjectReference(newObject);
}

On the blazor side :

IJSObjectReference jsObjectReference = await this.jsRuntime.InvokeAsync<IJSObjectReference>("app.CreateObject");
jsObjectReference.InvokeVoidAsync("test", this.objectReference)

From my limited understanding, DotNet.createJSObjectReference create a reference to a JsonObject. So the returned IJSObjectReference is not the javascript object itself, but a reference to it. It seems that when the InvokeVoidAsync call is done, it is done on the Reference object, not on the actual object itself, maybe explaining why the function is not found.

guardrex commented 9 months ago

Sorry that this went into a black hole. The PU was very busy late last year with the .NET 8 release, and then the holidays hit and everyone bugged out for a break. Folks should be getting back to work this and next week. They'll need a little time to dig out and get organized, but everyone is getting back to work starting today.

@macias ... I think it best if you open a PU issue for this using your original remarks here and anything else you would like to add. Open the issue at ...

https://github.com/dotnet/aspnetcore/issues

Please add ...

cc: @guardrex https://github.com/dotnet/AspNetCore.Docs/issues/30311

... to the bottom of your opening comment so that I can follow along.

We'll leave this docs issue open until it's resolved to see what the docs will require to cover this.

macias commented 9 months ago

@Yannike yes and no. "No" because the documented way is not working for me, so as I pointed out, I wrote small custom workaround and it works correctly for me. You know, after N failed times I simply gave up and moved on -- so "whatever works" :-).

guardrex commented 9 months ago

Ok, so we have a partial answer. The FN has to be a plain JS FN in the class. I'm not really a total JS jockey, so I'm not surprised at myself for not understanding that up until now. I think it should be part of doc examples. We're very light in the docs on the use of JS classes. We only have perhaps 1-3 examples. I'm going to use this issue to work on new coverage for using JS classes/methods with JS interop.

Idk if you'll get an answer to your other question on ...

What about how to dereference the result of createJSObjectReference from JavaScript?

I'll keep an 👂 open on that PU issue if Javier responds there to your question.

Leave this issue open. It will close later when the PR merges.

I'm totally slammed with .NET 8 issues at the moment ⛰️⛏️😰, so I don't know when I'll reach this. I think it will be later this year ... Q2 ... Q3 ... hard to say. I just know that .NET 8 doc work will be the main priority for at least the next few months. I'll eventually reach this. Thanks again for the issue.

macias commented 9 months ago

@guardrex I've just made a test and indeed what javiercn explained in the other post really works. In other words, tested, and confirmed :-).

"I'm not really a total JS jockey" :-) Me too, and I guess good portion of C#/Blazor devs would not make a proper distinction between JS function within class and actual method. Thus I think it would be useful to have at least remark/comment in the documentation reminding everyone that if they want to call a method, it should be a proper method, and not function inside class.

2024-02-17 update: I made mistake when testing, so I did NOT test it or reproduce.

biosmanager commented 8 months ago

@macias I cannot get this to work.

class VideoStream {
    nextTrack() {
        console.log("test");
    }   
}

function createVideoStream() {
    let videoStream = new VideoStream();
    return DotNet.createJSObjectReference(videoStream);
}

And on C#:

var videoStream = await JS.InvokeAsync<IJSObjectReference>("createVideoStream");
await videoStream.InvokeVoidAsync("nextTrack");

I still get Microsoft.JSInterop.JSException: Could not find 'nextTrack' ('nextTrack' was undefined). I have no idea what is going on.

biosmanager commented 8 months ago

I solved it by actually returning the VideoStream object:

function createVideoStream() {
    let videoStream = new VideoStream();
    return videoStream;
}
macias commented 8 months ago

@biosmanager In this case it works, because the method does not use any instance references and the instance itself is JSON-serializable. So you can go long with this approach but please note if you add members returning JS object will become costly.

biosmanager commented 8 months ago

@macias Thanks! However, I don't understand why it didn't work with DotNet.createJSObjectReference. In this case the function could never be found which kinda makes sense because the underlying JS object only holds a reference id.

macias commented 8 months ago

@biosmanager Thank you very much for bringing this up. After checking why I cannot run your code and I could run my I noticed, that I used now method syntax as suggested, but I created and call this method via my helper function so "of course" it worked because it didn't use Blazor helpers when executing the call.

biosmanager commented 8 months ago

@macias Could you show this helper function? Might be useful for me and others.

macias commented 8 months ago

@biosmanager Sure thing, please take a look at: https://codeberg.org/macias/lala/src/branch/devel/app/Lala.WebElements/wwwroot/js/bare-elements.js line around 132. There are several methods for creating instances, keeping them, and calling methods (both kind of syntax are supported). All crazy usage is shown for example here: https://codeberg.org/macias/lala/src/branch/devel/app/Lala.WebElements/Pages/DummyPage.razor.cs (line 39), but I use them for real of course, in this entire project. This page is simply for experimentation, so maybe it is easier to catch the call.

For further usage/reader: this is live repo, so in time the line numbers or even links can point out into not relevant places.