WebView2Feedback icon indicating copy to clipboard operation
WebView2Feedback copied to clipboard

Support for async functions/Promises in ExecuteScript

Open Kitiara opened this issue 5 years ago • 23 comments

Is there a way to read data as text from a blob url ? I've tried executing async/await javascript but i couldn't make it work for blobs.

AB#28965236

Kitiara avatar Aug 31 '20 06:08 Kitiara

Can you share some more details about your scenario/code? Are you doing this entirely on the javascript side? Does you code work in the browser (not in a webview)?

champnic avatar Aug 31 '20 18:08 champnic

Can you share some more details about your scenario/code? Are you doing this entirely on the javascript side? Does you code work in the browser (not in a webview)?

wstring script(L"(() => {"
L"async function fnc() {"
    L"let bl = await fetch(document.getElementById('test').href).then(r => r.blob());"
    L"var reader = new FileReader;"
    L"reader.readAsText(bl);"
    L"let promise = new Promise((res, rej) => {"
        L"setTimeout(() => res(reader.result), 100)"
    L"});"

    L"return await promise;"
L"}"

L"return fnc().then(function(value) {"
    L"return value; });"
L"})();");

webview->ExecuteScript(script.c_str(), Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
	[](HRESULT errorCode, PCWSTR result) -> HRESULT {
		MessageBox(nullptr, result, L"ExecuteScript Result", MB_OK);
	return S_OK;
}).Get());

The result is always '{}'. I have tested the javascript on the console side of browser and it seems to be working just fine but not in webview2 ??

Kitiara avatar Aug 31 '20 20:08 Kitiara

A quirk of ExecuteScript (and the underlying CDP call) is that objects are often just returned as "{}". Try wrapping your return value in a .toString().

champnic avatar Aug 31 '20 20:08 champnic

We're tracking improvements here as part of #105.

champnic avatar Aug 31 '20 20:08 champnic

A quirk of ExecuteScript (and the underlying CDP call) is that objects are often just returned as "{}". Try wrapping your return value in a .toString().

Async functions always return promises, .toString has just proved that. I couldn't get any data no matter what i try. But i managed to get some data by using ajax. Check this out:

    wstring jquery(L"var jqry = document.createElement('script');"
    L"jqry.src = 'https://code.jquery.com/jquery-3.3.1.min.js';"
    L"document.getElementsByTagName('head')[0].appendChild(jqry);");
    webview->ExecuteScript(jquery.c_str(), Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
        [webview](HRESULT errorCode, PCWSTR result) -> HRESULT {
        return S_OK;
    }).Get());
    Sleep(500);
    wstring script(L"jQuery.noConflict();"
    L"function testAjax() {"
    L"var result='';"
    L"jQuery.ajax({"
        L"url:document.getElementById('test').href,"
            L"async: false,"
            L"success:function(data) {"
                L"result = data; "
            L"}"
            L"});"
        L"return result;"
    L"}"
    L"(() => {"
        L"return testAjax()"
    L"})();");
    webview->ExecuteScript(script.c_str(), Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
        [webview](HRESULT errorCode, LPCWSTR result) -> HRESULT {
            wcout << wstring(result).c_str() << endl;

        return S_OK;
    }).Get());

This way i'm getting some data but not all of it. On the other hand, i have tested this ajax script on the console side of the browser and it worked there. Any ideas ?

Kitiara avatar Sep 01 '20 05:09 Kitiara

Ah, I understand now. We don't currently support async function results in ExecuteScript. It looks like there's a path forwards for implementing support there, so I'll open a feature request on our side to track that.

As a workaround, you should be able to use window.chrome.webview.postMessage(data) and add_WebMessageReceived to get out the data from inside your "then" function with the result. Let me know if that work.

champnic avatar Sep 02 '20 00:09 champnic

The ajax method is working. I noticed that certain unicodes are causing wcout to fail. I managed to print whole data by using wprintf(L"%ls\n", result); But i've found something strange. If you remove or replace null characters from javascript side then result of ExecuteScript become completely NULL.

Kitiara avatar Sep 02 '20 07:09 Kitiara

I believe the ajax is working because you are making it a synchronous call - I don't think we'd recommend this, as it will block the web code while executing. postMessage when the async calls are complete is preferred.

Can you share the code that is doing the character replacement? When you say NULL do you mean the return string is "null" or that the return string is a nullptr/NULL/0x0?

champnic avatar Sep 02 '20 15:09 champnic

I believe the ajax is working because you are making it a synchronous call - I don't think we'd recommend this, as it will block the web code while executing. postMessage when the async calls are complete is preferred.

I don't need async for the project i'm working on, so it's ok for me.

Can you share the code that is doing the character replacement? When you say NULL do you mean the return string is "null" or that the return string is a nullptr/NULL/0x0?

I mean the return string is "null". Replacing or removing any '\u0000' is causing this phenomenon.

return result.replace(/[\u0000]/g, 'X');

Kitiara avatar Sep 02 '20 17:09 Kitiara

A "null" string can be returned when the javascript object is undefined, there's an exception in the script, or if there's an error in trying to serialize the result using JSON. I'm not sure why removing the null character would cause those. Can you confirm that code works in the console/browser?

champnic avatar Sep 02 '20 18:09 champnic

A "null" string can be returned when the javascript object is undefined, there's an exception in the script, or if there's an error in trying to serialize the result using JSON. I'm not sure why removing the null character would cause those. Can you confirm that code works in the console/browser?

Yes, it is working without any problem.

Kitiara avatar Sep 02 '20 19:09 Kitiara

Are you able to share what the result string is before and after the replacement? If you just use a static string (not from the server) is there some minimal string that reproduces the problem so we can take a look on our end?

champnic avatar Sep 02 '20 19:09 champnic

It's not a static string. The result i'm getting is constantly changing but it's pattern is always the same and i'm always getting null after the replacement of the null character. So you should be able to reproduce the problem. Here it is: Original text: before.txt Expected outcome: after.txt

Kitiara avatar Sep 02 '20 19:09 Kitiara

Working on getting a repro. Are you able to share the url you are using in you ajax request? Does the problem still happen without the .replace(...) call?

champnic avatar Sep 02 '20 21:09 champnic

The url isn't static either. The problem is happening with the replace call, otherwise it is fine.

Kitiara avatar Sep 02 '20 22:09 Kitiara

I think this is very important. Since WebView2 is rolled out with the new Office 365 it will be more interesting as a replacement for CEF/CefSharp. Maybe you can have a look at their interface. Solving the problem similar would probably help when migrating the code from CEF to WebView2!

https://github.com/cefsharp/CefSharp/wiki/General-Usage#2-how-do-you-call-a-javascript-method-that-returns-a-result

killerfurbel avatar Jun 07 '21 14:06 killerfurbel

I've been struggling all day with trying to await ExecuteScriptAsync on an async javascript function. As soon as it hits the await inside the JS function, it returns to the .NET code. Is there no way to wait for the async JS code to finish before moving on?

TonyLugg avatar Jan 13 '22 00:01 TonyLugg

Currently no - the ExecuteScript function doesn't wait on async javascript functions. That's what this feature request is tracking.

You can use host objects or postMessage/WebMessageReceived to message a result back to the host app if needed.

champnic avatar Jan 13 '22 00:01 champnic

OK, hopefully it gets added soon. Using a callback/post-back defeats the purpose of "await".

TonyLugg avatar Jan 13 '22 15:01 TonyLugg

I wonder if anyone could provide a complete working example for this problem using WebView2 postMessage communication instead of awaiting the result of an asynchronous JavaScript function on the host side?

We are working with the WinForms/C# integration and we plan to embed a serverside Blazor application. Like described in the docs we shall use invokeMethodAsync on a DotNetObjectReference object inside the JavaScript world, to execute Blazor Serverside functions. One of these functions we want to support is called bool HasUnsavedData(). It can be called from the host to see if the Blazor application has unsaved data and react accordingly (for example ask the user to save his data before closing the complete application).

For sake of simplicity let's use the JavaScript setTimeout function to simulate the async call to invokeMethodAsync done by the host system to ask the web application if it has unsaved data. Since the main thread of the .NET application is not allowed to be blocked i tried something (hacky) like

private async bool ExecuteHasUnsavedData()
{
    var received = false;
    var result = false;

    void OnMessageReceived(object _, CoreWebView2WebMessageReceivedEventArgs args)
    {
        var msg = args.TryGetWebMessageAsString();
        if (msg.StartsWith("RETURN|"))
        {
            result = bool.Parse(msg.Split('|')[1]);
            received = true;
        }
    }
    
    _webView2.CoreWebView2.WebMessageReceived += OnMessageReceived;

    // Assume this is our async JS call which was rewritten so that it uses postMessage to populate it's result
    var jsCode = "setTimeout(() => window.chrome.webview.postMessage('RETURN|true'), 2000)";
    await _webView2.ExecuteScriptAsync(jsCode);
   
     while(!received)
        Application.DoEvents();

    _webView2.CoreWebView2.WebMessageReceived -= OnMessageReceived;
    return result;
}

but that didn't work.

Would be really nice if anyone has a suggestion otherwise i'm afraid we're not able to use WebView2 currently 😢

R0Wi avatar Mar 17 '22 14:03 R0Wi

Technically this is possible already via the DevTools Protocol.

For ease of use I've just released WebView2.DevTools.Dom to nuget.org

It's a DevTools Protocol based framework for JavaScript execution, DOM access/manipulation. (Direct connection to browser via CoreWebView2 instance, doesn't open a remote debugging port). More details in the Readme

 await webView.EnsureCoreWebView2Async();
 
 // Create one instance per CoreWebView2
 // Reuse devToolsContext if possible, dispose (via DisposeAsync) before creating new instance
 // Make sure to call DisposeAsync when finished or await using as per this example
// Add using WebView2.DevTools.Dom; to access the CreateDevToolsContextAsync extension method
 await using var devToolsContext = await webView2Browser.CoreWebView2.CreateDevToolsContextAsync();

 var meaningOfLifeAsInt = await devToolsContext.EvaluateFunctionAsync<int>("() => Promise.resolve(42)");

amaitland avatar Mar 21 '22 06:03 amaitland

If anyone is just interested in invoking async Javascript via DevTools interface, i just created a little helper class which might point you to the right direction.

If you need extended functionallity i can really recommend the library WebView2.DevTools.Dom, thanks @amaitland for creating this! And also thanks for the great hint 🚀

R0Wi avatar Mar 31 '22 12:03 R0Wi

Using @amaitland package [WebView2.DevTools.Dom](https://www.nuget.org/packages/WebView2.DevTools.Dom/) absolutely does the job. However this issue was created 5 years ago: what is the status and roadmap? Will there be a built-in way to do this?

CocoDico78 avatar Apr 11 '25 11:04 CocoDico78