Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for calling async methods on host objects (.NET, WinRT) #75

Closed
kekyo opened this issue Nov 20, 2019 · 35 comments
Closed

Add support for calling async methods on host objects (.NET, WinRT) #75

kekyo opened this issue Nov 20, 2019 · 35 comments
Labels
feature request feature request tracked We are tracking this work internally.

Comments

@kekyo
Copy link

kekyo commented Nov 20, 2019

@david-risney I experienced constructing full-duplex asynchronous interoperable functions between javascript's Promise based and .NET's Task based helper function sets on only WebView1 window.notify() infrastructure.

Interface C# side:

// In WebView1
public Task<T> InvokeFunctionAsync<T>(string functionName, params object[] args)
{
  // * Serialize arguments and deserialize result with Json.
  // * Manage asynchronous life time (will cause throwing CancellationException when not response from JavaScript.)
}

Interface JavaScript side:

// invokeMethod(string methodName, ...) : Promise   // contains variable arguments next to methodName
function invokeMethod(methodName)
{
  // * Construct arg[] from variable arguments.
  // * Serialize arguments and deserialize result with Json.
  // * Manage asynchronous life time (will cause calling Promise.reject() when not response from .NET)
}

Improved our project implementation stability with asynchronous architecture on both Task and Promise (and can use async/await on both worlds) and shareable how apply asynchronous on our team technical discussion. I received feedback with very good.

Unfortunaterly I can't share it, these implementations are closed source, I strongly recommend more powerful interop ability on WebView2 when there're supported :)

AB#26646526

@david-risney
Copy link
Contributor

Our current AddRemoteObject method uses COM / IDispatch which doesn't really have an async notion. For .NET / WinRT we'll support async methods as promises in JS.

@lostobject
Copy link

Cross posting from #532
Is there currently an ETA for this support? Eg do you think it would make it in to initial 1.0 release?

Some more context in case it helps:
The application I am working on will eventually be exposing a large API set to JS through WebView2. Some of these APIs will themselves need to call cross process or cross machine boundaries to other parts of the application, and return values to the JS code. Also, there will be many html pages loaded and visible at once. This, with the way WebView2 is structured, will result in a lot of call traffic being funneled into the main WPF exe. Currently the parts at either end are async, but the part in the middle (WPF exe) has to be synchronous. It is not going to scale very well.

Alternatively, I could use WebMessageReceived and window.chrome.webview.postMessage which is inherently asynchronous. But to do this over a large API would require designing and implementing a robust RPC protocol that works over this channel, which is going to be quite a lot of work. Hence the HostObject approach is much more attractive to me.

@champnic
Copy link
Member

champnic commented Oct 14, 2020

It won't be available in the 1.0 release. Looking at our backlog and it's relative priority, I would guess Q2 2021 is when it might land. :(

Instead of a return value, you could have the value returned asynchronously using a callback - there's an example of this in our sample app. This would make the JS look like:

                external.TestAsync(message, (res) => {
                  document.getElementById("result-TestMessage").value = res;
                });

I'm not exactly sure what this would look like in .NET though, so might require some experimentation.

@lostobject
Copy link

Ok thanks that's a much better alternative. I spent some time on it and was able to get it working in C#. It required an IDispatch::Invoke helper for C#. It's a bit clunky and requires unsafe code but I can use this as an interim solution. I used this helper and made the following changes:

Then implement a HostObject method to take a callback argument:

		public void TestAsyncViaCallback(string something, object callback)
		{
			string message = $"***TestAsyncViaCallback({something}) Called***";
			Debug.Print(message);

			// Simulate an async call to a remote machine
			Task.Run(() =>
			{
				string result = "Asynchronous test via callback succeeded";
				bool success = true;

				InvokeMemberHelper.InvokeDispMethod(callback, null, success, result);
			});
		}

And the JS code to call and process the return value:

    async function SendTestMessageAsyncViaCallback() {
      try {
        let message = document.getElementById("input-TestMessage").value;
        let external = await window.chrome.webview.hostObjects.external;

        await external.TestAsyncViaCallback(message, (success, res) => {
          let output = `Succeeded: ${success}\nResult: ${res}`;
          document.getElementById("result-TestMessage").value = output;
        });
      } catch (e) {
        document.getElementById("result-TestMessage").value = `Error: ${e.toString()}`;
      }
    }

Note the JS function takes a success parameter as well as the result, in order to correctly propagate errors back up the chain.

@champnic
Copy link
Member

Glad you got it working! I'll keep you updated on when we have a fix for .NET async functions in general. Thanks!

@RickStrahl
Copy link

Just to be clear async methods will work even when you make a sync method call.

You can also return values back to JavaScript if you use the sync object. Which means pretty much if you need to return values from .NET into JavaScript sync is the only option.

The problem is that you can't return a value when you use the async object in JavaScript. The result will be pending promise that never resolves.

This seems like a pretty big problem especially given that calling into the JavaScript the other way requires async code. So any method in .NET that is called from JavaScript and returns a value, and also needs to access the DOM from the .NET code, can't do it without a pretty good chance of deadlocking.

I've gotten stopped out by this quite a bit. Just had a long Twitter thread on this and literally the answer (from David Fowler) was: "You're screwed. Wrap in Task.Run() and hope for the best".

@champnic
Copy link
Member

Hey @RickStrahl - I just gave this a try in our sample app and a separate .NET app with a host object that returns a value (bool in my test). When I call var a = await window.chrome.webview.hostObjects.external.Test() the variable a was correctly set to the return value, true. This is with 1.0.865-prerelease SDK and latest runtime. Can you share the code you were testing this with? Might be worth opening a separate issue to track that potential bug.

@RickStrahl
Copy link

Ok... I'll take a look. A bit hard now as I've worked around the async issue in a different way.

To be honest it's been a couple of updates back since I last tested as I needed a workaround at the time. In that case the result was a pending promise that never resolved.

I gave up on this at the time because you mentioned that this was a known issue and not likely to get fixed at the time. I'd like to be pleasantly surprise though :-)

@champnic champnic changed the title Provide full-duplex interop methods between .NET side and JavaScript side (improve window.notify() topic) Add support for calling async methods on host objects (.NET, WinRT) Jun 2, 2021
@dimmelsw
Copy link

@champnic
Are there any news on this?

@champnic
Copy link
Member

@dimmelsw Yes, we are starting work on this very soon :)

@dimmelsw
Copy link

@champnic
do you have an ETA?

@champnic
Copy link
Member

My guess would be early 2022, but maybe sooner if it's less complex than we think.

@michael-hawker
Copy link

I didn't realize this wasn't supported. I had just converted my WebView1 project to be more async methods required for it to be out-of-process. Going to make porting to WebView2 harder.

@dimmelsw
Copy link

dimmelsw commented Mar 7, 2022

@champnic
Are there any news on this?

@champnic
Copy link
Member

champnic commented Mar 8, 2022

@dimmelsw This work has just been checked in last week :) It should be available in the next prerelease SDK in about a week or two. Thanks!

@chohoo89
Copy link

chohoo89 commented Mar 8, 2022

Thanks for working 👍

@kekyo
Copy link
Author

kekyo commented Mar 10, 2022

(I am the one who posted the original thread.)

Unfortunaterly I can't share it, these implementations are closed source, I strongly recommend more powerful interop ability on WebView2 when there're supported :)

Today, I released DupeNukem NuGet package. This is a full-duplex asynchronous messaging infrastructure that I have experienced, rewritten from the ground up in a way that is independent of the browser component.

https://github.com/kekyo/DupeNukem

If you are waiting for the interoperability code and are interested, give it a try.

@champnic
Copy link
Member

champnic commented Mar 23, 2022

Hey all - just wanted to confirm that this support is now available in the 1.0.1189-prerelease SDK, using any runtime version 100.0.1183.0+. Thanks!

@mzb986
Copy link

mzb986 commented Mar 29, 2022

How do I install it, runtime version 100.0.1183.0+

@champnic
Copy link
Member

champnic commented Mar 29, 2022

You'll need to use Microsoft Edge Canary/Dev/Beta:
https://docs.microsoft.com/en-us/microsoft-edge/webview2/how-to/set-preview-channel

Let me know if that doesn't answer your question. Thanks!

@michael-hawker
Copy link

michael-hawker commented Apr 1, 2022

@champnic I seem to be getting an System.Execution.EngineException (with no other message or details) on 1189-prerelease

>	EmbeddedBrowserWebView.dll!embedded_browser_webview_current::EmbeddedBrowserHost::PostMethodCall(int,class std::__1::basic_string<char,struct std::__1::char_traits<char>,class std::__1::allocator<char> > const &,class std::__1::basic_string<char,struct std::__1::char_traits<char>,class std::__1::allocator<char> > const &,class absl::optional<class std::__1::basic_string<char,struct std::__1::char_traits<char>,class std::__1::allocator<char> > > const &,int)	Unknown
 	EmbeddedBrowserWebView.dll!embedded_browser::mojom::HostStubDispatch::Accept(class embedded_browser::mojom::Host *,class mojo::Message *)	Unknown
 	EmbeddedBrowserWebView.dll!mojo::InterfaceEndpointClient::HandleIncomingMessageThunk::Accept(class mojo::Message *)	Unknown
 	EmbeddedBrowserWebView.dll!mojo::MessageDispatcher::Accept(class mojo::Message *)	Unknown
 	EmbeddedBrowserWebView.dll!mojo::InterfaceEndpointClient::HandleIncomingMessage(class mojo::Message *)	Unknown
 	EmbeddedBrowserWebView.dll!mojo::internal::MultiplexRouter::ProcessIncomingMessage()	Unknown
 	EmbeddedBrowserWebView.dll!mojo::internal::MultiplexRouter::Accept(class mojo::Message *)	Unknown
 	EmbeddedBrowserWebView.dll!mojo::MessageDispatcher::Accept(class mojo::Message *)	Unknown
 	EmbeddedBrowserWebView.dll!base::internal::Invoker<base::internal::BindState<void (*)(const base::RepeatingCallback<void (unsigned int)> &, unsigned int, const mojo::HandleSignalsState &),base::RepeatingCallback<void (unsigned int)>>,void (unsigned int, const mojo::HandleSignalsState &)>::Run()	Unknown
 	EmbeddedBrowserWebView.dll!mojo::SimpleWatcher::OnHandleReady()	Unknown
 	EmbeddedBrowserWebView.dll!base::TaskAnnotator::RunTaskImpl(struct base::PendingTask &)	Unknown
 	EmbeddedBrowserWebView.dll!base::TaskAnnotator::RunTask<>()	Unknown
 	EmbeddedBrowserWebView.dll!embedded_browser_webview::internal::AppTaskRunner::DoWork()	Unknown
 	EmbeddedBrowserWebView.dll!embedded_browser_webview::internal::AppTaskRunner::MessageCallback()	Unknown
 	EmbeddedBrowserWebView.dll!base::win::MessageWindow::WindowProc()	Unknown
 	EmbeddedBrowserWebView.dll!base::win::WrappedWindowProc<&base::win::MessageWindow::WindowProc>()	Unknown

I've been also trying to use the WebView2 JavaScript Debugger in VS, but it doesn't like me setting a break point in html, so it's hard to catch on load. Going to move my code to a button to see if I can try the Dev Tools debugger and inspect the native object.

Here's my test project:

WebView2Testing.zip

@champnic
Copy link
Member

champnic commented Apr 2, 2022

@michael-hawker I was able to repro using your test app. Could you move this to a new issue and I'll get it opened as a bug. Thanks!

@rsegner
Copy link

rsegner commented Apr 11, 2022

@champnic I have tested @michael-hawker example using 1189-prerelease with various webview runtimes (Beta / Dev and Canary). None of these resolve the original issue. While I do not get the System.Execution.EngineException with these updated runtimes, await anobject.MyLongFunction(); at line 34 of WebRoot\TestPage.html always returns null after the await in the C# code. I would expect that we would get a result of "Done!" after 5 seconds

@champnic champnic reopened this Apr 11, 2022
@champnic
Copy link
Member

@rsegner We've had to revert the code for now as it was causing regressions when the SDK + runtime pairing was off. We'll be fixing the root cause and then checking the code back in, and will use @michael-hawker's sample app to help validate. Thanks!

@michael-hawker
Copy link

Cool, note originally, I had the order as:

            document.getElementById("prop1").innerHTML = await anobject.SomePropertyString;
            document.getElementById("prop2").innerHTML = await anobject.SomeInteger;
            document.getElementById("prop3").innerHTML = await anobject.MyEnumProp;

            var result = await anobject.MyLongFunction();
            console.log(result);
            document.getElementById("wait").innerHTML = result;

            document.getElementById("func1").innerHTML = await anobject.MyFirstFunction("true");
            document.getElementById("func2").innerHTML = await anobject.MyFirstFunction("boo");
            document.getElementById("func3").innerHTML = await anobject.MySecondFunction(7);

As I'd expect there to be a delay in seeing the results for func* until the 5 second delay was processed while awaiting the async C# task.

Just wanted to call this out for a more well-rounded test. 🙂

I think to clarify too, based on my understanding of the docs, even the properties and the synchronous function calls need to have await prefixed them in JavaScript as everything is called asynchronously in the context of the WebView communicating to the C# host object, correct? It's just in the case when it's an async function in C# where we've hit these extra issues, I think.

Good to know it should be handy for validation though! 🎉

@rsegner
Copy link

rsegner commented Apr 26, 2022

Realize the issue has not been marked as fixed, however, just tried @michael-hawker s app with WebView SDK v1.0.1222-prerelease and Edge runtime Canary (at the time of writing v102.0.1236.0).

No longer getting an AccessViolation error, however also not getting the expected result of "Done!" from the async operation.

@champnic
Copy link
Member

@rsegner That's expected. Unfortunately we had to rollback the change for async host object functions because it was causing the access violation errors.

@champnic champnic added the tracked We are tracking this work internally. label May 10, 2022
@rsegner
Copy link

rsegner commented May 30, 2022

@champnic Any news on this one? I am very keen on using this for a new development for my company. Happy to be an early adopter and provide any feedback.

@champnic
Copy link
Member

champnic commented Jun 1, 2022

We are working on relanding the change. It's currently in a Pull Request and will hopefully be checked in soon!

@champnic
Copy link
Member

champnic commented Jun 9, 2022

Hey all - we've fixed and relanded this change. It should be available in the next SDK pre-release package. Thanks!

@Vampire2008
Copy link

What is current status?
Is it available in any pre-release?

@champnic
Copy link
Member

This is available in 1.0.1305-prerelease. Please give it a try and let us know if it's working for you. Thanks!

@raymeskhoury
Copy link

I tested this and it's working with Beta channel (104) and 1.0.1305-prerelease. It wasn't working with stable (103) yet.

@raymeskhoury
Copy link

raymeskhoury commented Jul 20, 2022

@champnic I'd also like to verify that it's safe to run a nested message loop (e.g. by showing a dialog) prior to the final result being returned asynchronously to JS. https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#reentrancy talks about it not being safe to show dialogs inside the event handler, however ideally this would be safe as long as it wasn't done synchronously.

For example, is this safe?

public async Task<string> PickDirectory() {
  await Task.Yield();  // PickDirectory finishes synchronously
  return dialog.ShowAndReturnResult();
}

I could it see it being safe or unsafe depending on how the webview code is waiting for the result, e.g. if webview is waiting in a nested message loop for the async result then I could see this being dicey. Ideally that wouldn't be happening.

@movedoa
Copy link

movedoa commented Jun 8, 2023

@champnic I'd also like to verify that it's safe to run a nested message loop (e.g. by showing a dialog) prior to the final result being returned asynchronously to JS. https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#reentrancy talks about it not being safe to show dialogs inside the event handler, however ideally this would be safe as long as it wasn't done synchronously.

For example, is this safe?

public async Task<string> PickDirectory() {
  await Task.Yield();  // PickDirectory finishes synchronously
  return dialog.ShowAndReturnResult();
}

I could it see it being safe or unsafe depending on how the webview code is waiting for the result, e.g. if webview is waiting in a nested message loop for the async result then I could see this being dicey. Ideally that wouldn't be happening.

Did you find out if this is safe to do?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request feature request tracked We are tracking this work internally.
Projects
None yet
Development

No branches or pull requests