Closed ztanczos closed 7 months ago
Hi @ztanczos , in order for JS injection to take effect in a new window, it should be called after e.NewWindow = newWebView.CoreWebView2;
. Here is some doc.
Maybe it's worth revising the doc, as it currently says:
Changes to settings should be made before put_NewWindow is called to ensure that those settings take effect for the newly setup WebView.
Which would imply that the setting for AddScriptToExecuteOnDocumentCreatedAsync
should be made before you set NewWindow.
@psmulovics, sorry for confusion. We have further The methods which should affect the new web contents like AddScriptToExecuteOnDocumentCreated and add_WebResourceRequested have to be called after setting NewWindow.
- we will see how to make it more clear.
Not related to the documentation, rather to make it more foolproof - could it give a warning in debug if you set it at the wrong time?
Most interesting information, this enabled me to remove an ugly workaround where our integration actually had to reload ininitially loaded content for window.open to make all hooks and injected scripts work.
Hi @vbryh-msft,
Thank you for your answer but unfortunately it still doesn't work for me, at least not reliably. This is the full source code for the NewWindowRequested
event handler:
private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
try
{
var deferral = e.GetDeferral();
Window window = new Window();
var newWebView = new WebView2();
window.Content = newWebView;
window.Show();
await newWebView.EnsureCoreWebView2Async();
e.NewWindow = newWebView.CoreWebView2;
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("alert('hello')");
e.Handled = true;
deferral.Complete();
}
catch (Exception ex)
{
}
}
and this is the code which triggers the new window request:
<button onclick="window.open('https://www.google.com')">click me</button>
With this event handler most of the time the popup window is just blank (i.e.: not even google.com is displayed), but sometimes - mostly when I put a breakpoint at AddScriptToExecuteOnDocumentCreatedAsync
I see the page properly displayed and my 'hello' alert message as well.
Also if I put a Thread.Sleep(3000)
before AddScriptToExecuteOnDocumentCreatedAsync
it starts to work more often.
Am I doing something wrong or there's a synchronization issue in WV2?
tagging @liminzhu for tracking
For our Win32 integration I avoid extensive processing in the event handler. Instead I get the deferral and use PostMessage to create window and integrate with new WebView2 instance from the message pump.
Hi @pontusn, I'm not sure how would that be possible with the C# .NET interface: my understanding is that I'd need to set eventArgs.NewWindow to the new CoreWebView2 instance in the event handler and creating that instance can only be done asynchronously and it is already a relatively heavy operation.
Hi @vbryh-msft, Just wondering if you had a chance to check my last message.
Use the deferral and avoid lengthly processing in the event handler.
Hi @ztanczos - I was able to repro the issue in .Net, but was unable to quickly solve it - created a bug for somebody to have more time to look into it.
@ztanczos - I have tried your sample one more time - do you need to have alert
in AddScriptToExecuteOnDocumentCreatedAsync? Could you try console.log
instead and see how it behaves for you?
@vbryh-msft - I don't need alert
, let me try to explain what I'm looking for: we have custom Javascript libraries which provide all sorts of functionalities for the hosted web applications. These libraries should be already present when loading a page. This works fine for the 'main' window but if a web application tries to open a new window by using window.open
we'd like to intercept that call and inject the same set of JS libs before the window
object is available to the caller.
Consider this sample HTML:
<html>
<head>
<title>test</title>
<script type="text/javascript">
var openedWindow;
console.log(window['TESTDATA'])
function openWindow() {
openedWindow = window.open('https://www.google.com')
console.log('window opened')
console.log(openedWindow['TESTDATA'])
}
</script>
</head>
<body>
<h1>hello world</h1>
<button onclick="openWindow()">click me</button>
</body>
</html>
And in .NET I'm trying to inject the following JS:
public async Task InitializeAsync()
{
await webView.EnsureCoreWebView2Async();
webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this works'");
}
private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
try
{
_deferral = e.GetDeferral();
Window window = new Window();
var newWebView = new WebView2();
window.Content = newWebView;
window.Show();
await newWebView.EnsureCoreWebView2Async();
e.NewWindow = newWebView.CoreWebView2;
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this doesn't work'");
e.Handled = true;
_deferral.Complete();
}
catch (Exception ex)
{
Debug.Fail(ex.ToString());
}
}
When I click on the button undefined
is logged to the console instead of this doesn't work.
Not sure if this makes sense, let me know if you have further questions. Thank you
@ztanczos There are could be two issues with your example:
doesn't
causes Uncaught SyntaxError: Unexpected identifier
- could you try without it, like just does
console.log(openedWindow['TESTDATA'])
matters - could you try to open google.com from google.com? Hope it makes sense. Or just check window['TESTDATA']
in DevTools(right-click -> Inspect) of the new window?
@vbryh-msft thanks, I didn't notice the apostrophe.. 🤦
I changed the test URLs so there's no cross-origin issue and now I see the same behavior, i.e.: undefined
is logged when the window is opened but later I see TESTDATA
populated.
I'll check whether this behavior would work for us.
What exactly is the purpose of calling deferral.Complete()
? I commented out that line so that I never call .Complete()
and I don't see any change in behavior.
Hi @ztanczos - you should use GetDeferral
if you want to defer event completion at a later time. [Here is an example](https://github.com/MicrosoftEdge/WebView2Samples/blob/main/SampleApps/WebView2WpfBrowser/MainWindow.xaml.cs#:~:text=CoreWebView2Deferral%20deferral%20%3D,.Complete()%3B) how it can be used - you can search for more examples in that file. We suggest to always complete deferral if it was taken in order for event to be in proper state and be able to complete successfully.
[This is](https://github.com/MicrosoftEdge/WebView2Samples/blob/main/SampleApps/WebView2APISample/AppWindow.cpp#:~:text=wil%3A%3Acom_ptr%3CICoreWebView2Deferral,deferral%2D%3EComplete()) how you would use deferral particularly in new window requested(c++ code)
Is injecting JS script works in new window as expected?
What I observe when using the Deferral in the NewWindowRequested event handler is that once I set .NewWindow
then completing or not-completing the deferral doesn't matter: once .NewWindow
is set the Javascript window.open call returns with a navigated window object.
Injecting JS works although not as expected. What we're trying to achieve is to inject custom Javascript into the new window before the window object is returned to the caller. The reason for this is that we provide a large set of custom functionalities for hosted web applications via JS bindings which should be available to child windows as well. With the current behavior there's a timing issue: JS bindings will be available at some point but we have no control over when exactly, so code which relies on JS objects being available in child windows as soon as the window object is ready would break.
Originally I thought we should use the Deferral to signal when .NewWindow
is ready to be used but apparently it is not the case.
Hi @ztanczos, I have tried some tests based on WPF_GettingStarted
and it seems fine. My code is as follows:
public async Task InitializeAsync()
{
await webView.EnsureCoreWebView2Async();
webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
//await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this works'");
}
private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
var _deferral = e.GetDeferral();
Window window = new Window();
var newWebView = new WebView2();
window.Content = newWebView;
window.Show();
await newWebView.EnsureCoreWebView2Async();
e.NewWindow = newWebView.CoreWebView2;
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this does work'; alert('hello')");
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("function square(x) { return x * x; }");
e.Handled = true;
_deferral.Complete();
}
You will get the alert
and the function works.
Hi @plantree1995 , Your code sample doesn't work for me: I don't see any alert, in fact the new window opens as completely blank.
Hi @plantree1995 , Your code sample doesn't work for me: I don't see any alert, in fact the new window opens as completely blank.
Same for me.
Hi @ztanczos @psmulovics, I followed this project, and all source code is posted below:
namespace WPFSample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
webView.NavigationStarting += EnsureHttps;
InitializeAsync();
}
async void InitializeAsync()
{
await webView.EnsureCoreWebView2Async(null);
webView.CoreWebView2.WebMessageReceived += UpdateAddressBar;
webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.chrome.webview.postMessage(window.document.URL);");
await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window.chrome.webview.addEventListener(\'message\', event => alert(event.data));");
}
void UpdateAddressBar(object sender, CoreWebView2WebMessageReceivedEventArgs args)
{
String uri = args.TryGetWebMessageAsString();
addressBar.Text = uri;
webView.CoreWebView2.PostWebMessageAsString(uri);
}
private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
var _deferral = e.GetDeferral();
Window window = new Window();
var newWebView = new WebView2();
window.Content = newWebView;
window.Show();
await newWebView.EnsureCoreWebView2Async();
e.NewWindow = newWebView.CoreWebView2;
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("window['TESTDATA'] = 'this does work'; alert('hello')");
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("function square(x) { return x * x; }");
e.Handled = true;
_deferral.Complete();
}
void EnsureHttps(object sender, CoreWebView2NavigationStartingEventArgs args)
{
String uri = args.Uri;
if (!uri.StartsWith("https://"))
{
webView.CoreWebView2.ExecuteScriptAsync($"alert('{uri} is not safe, try an https link')");
args.Cancel = true;
}
}
private void ButtonGo_Click(object sender, RoutedEventArgs e)
{
if (webView != null && webView.CoreWebView2 != null)
{
webView.CoreWebView2.Navigate(addressBar.Text);
}
}
}
}
Compile and run it in debug mode, open any links in new window, I would get the alert
and function square()
. If you have any other questions, please let me know.
Hi @plantree1995,
This sample is slightly different from what we're trying to achieve. We're trying to open a new window using Javascript's window.open()
this way:
<html>
<head>
<title>test</title>
<script type="text/javascript">
var openedWindow;
console.log(window['TESTDATA'])
function openWindow() {
openedWindow = window.open('http://mytesturl/wv2test.html')
console.log(openedWindow['TESTDATA']);
}
</script>
</head>
<body>
<h1>hello world</h1>
<button onclick="openWindow()">click me</button>
</body>
</html>
When I open this HTML with the sample code from WPF_GettingStarted I see the main page rendered correctly but when I click on the click me
button I only get a blank window and no alert
pop-up + undefined
is logged to the console indicating that TESTDATA
is not available on openedWindow
.
Hi @ztanczos, I tried this way.
private async void CoreWebView2_NewWindowRequested(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
{
try
{
var _deferral = e.GetDeferral();
Window window = new Window();
var newWebView = new WebView2();
window.Content = newWebView;
window.Show();
await newWebView.EnsureCoreWebView2Async();
e.NewWindow = newWebView.CoreWebView2;
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("function square(x) { return x * x; }; function cube(x) { return x * x * x; }");
e.Handled = true;
_deferral.Complete();
}
catch (Exception )
{
}
}
and the html just like this:
<html>
<head>
<title>test</title>
<script type="text/javascript">
var openedWindow;
console.log(window['TESTDATA'])
function openWindow() {
openedWindow = window.open('http://bing.com', 'hello')
}
</script>
</head>
<body>
<h1>hello world</h1>
<button onclick="openWindow()">click me</button>
</body>
</html>
When you click on the button to open a new window, you could use function square()
and function cube()
in the Console.
Hi @plantree1995,
Thanks for looking into this but I know that eventually the injected Javascript code will be there but I have no control over when and I'd need to ensure that the returned Window object already has the injected JS functionality right after the window.open()
call.
Our use-case: we're hosting web applications in our container which provides rich functionality for these web applications via JS + interop objects. This functionality is required in windows opened by these web applications too without timing issues. In the past we could achieve the same with CEF + CefSharp by using V8Extensions but now we're struggling with WebView2.
Hi @ztanczos, Do you mean that you want to get the attribute of child window' JS functionality in the parent window?
The other way around. E.g.: let's say we inject something into the parent window so that window.MyCustomCode
is accessible & works. Right after openedWindow = window.open(...)
I'd like to ensure that openedWindow.MyCustomCode
also accessible & works.
In reality it is a bit more complex than that (multiple JS libraries with multiple properties, classes, etc.) but this is the gist of it.
Hi @ztanczos,
I almost understand what you want. It seems that you can read this docs to known more about what effects will Window.open()
have.
Note that remote URLs won't load immediately. When window.open() returns, the window always contains about:blank. The actual fetching of the URL is deferred and starts after the current script block finishes executing. The window creation and the loading of the referenced resource are done asynchronously
It's not synchronous, so the output undefined
is as expected.
Hi @plantree1995,
Thanks for the link + explanation - I'll re-work my test code to better illustrate understand the problem we're facing: once the web app opens a child window then within the child window - so once it is loaded - the injected JS is not yet available.
Maybe we can better illustrate this issue: the function of open returns although the resource has not yet been loaded completely. So perhaps you can use in this way:
function openWindow() {
openedWindow = await window.open('http://127.0.0.1:8010/foo.html');
openedWindow.onload = function () {
console.log(openedWindow['TESTDATA']);
};
}
Hi @ztanczos, have you already solved this problem? If so, I will close this issue.
Hi @plantree1995, Sorry for the late response, I was on holiday. I'll still need a few days, please don't close this issue yet.
Hi @ztanczos, If you come cross any problems, please let me know. Perhaps we could tackle them together.
Hi @plantree1995, Please bear with me, our actual problem spans across multiple applications and libraries, some of which are open-source, some are not so it is difficult to describe the problem without sharing non-public information.
With Cef + CefSharp we "injected" (or rather registered) the custom Javascript libraries by using V8Extensions (CefSettings.RegisterExtension()
method). That way the custom Javascript is always there, even for newly opened windows, immediately after calling window.open
(so it is never undefined
).
Our framework calls window.open
through desktopJS: https://github.com/morganstanley/desktopJS/blob/main/packages/desktopjs/src/Default/default.ts#L299 - we might be able to tweak that for our needs.
You wrote that injecting JS is not synchronous - what would be your suggestion to check when exactly a custom JS script is injected & ready to be consumed?
Hi @ztanczos, follow this link, Window.open(), we could know that:
The window creation and the loading of the referenced resource are done asynchronously.
In a previous issue, I have this suggestion.
Maybe we can better illustrate this issue: the function of open returns although the resource has not yet been loaded completely. So perhaps you can use in this way:
function openWindow() { openedWindow = window.open('http://127.0.0.1:8010/foo.html'); // remove the await openedWindow.onload = function () { console.log(openedWindow['TESTDATA']); }; }
I don't know have you ever tried in this way. You can use your custom JS scripts freely after the load
event is fired. The reference is below: https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event#syntax .
Hi @ztanczos / @vbryh-msft / @plantree1995 ,
I also believe there is a race condition between setting CoreWebView2NewWindowRequestedEventArgs.NewWindow
and having scripts loaded (via CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync
) prior to navigation / document loading.
Based on the above, I have put together a full reproduction of the issue in this repo along with steps for (and recordings of) expected and actual behavior.
I would very much appreciate if people could validate this issue and/or provide a work around.
FWIW, my hunch is that WebView2 is scheduling a navigation to the destination URL on the UI thread as soon as CoreWebView2NewWindowRequestedEventArgs.NewWindow
is assigned. This causes the navigation to take place as soon as the currently executing code yields the UI thread (via a Task.Delay
or - I expect - somewhere in CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync
), potentially before the scripts have been added. If this is the case, a simple fix for this issue would be to wait for the deferral to be completed prior to scheduling the navigation.
I have pushed a workaround for this issue to this branch.
Short answer: Use CoreWebView2.NavigationStarting
to cancel any navigations that occur prior to CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync
completing then schedule the desired navigation manually.
Long answer: The issue was that setting CoreWebView2NewWindowRequestedEventArgs.NewWindow caused WebView2 to immediately (regardless of whether a deferral was held or not) schedule a navigation to CoreWebView2NewWindowRequestedEventArgs.Uri on the UI thread. Subsequent calls to CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync yielded the UI thread establishing a race-condition condition between the navigation and the script loading (hence why there were different outcomes based on timing / CPU load / etc).
Regarding a "fix" for WebView2 to be discussed with MS, I would suggest:
Navigate
boolean which allows you to instruct WebView2 whether or not to schedule a navigation.Hi @ibebbs, Our use-case is a bit special because we're in fact opening a new blank window, i.e.: about:blank, and once opened we're directly manipulating and projecting DOM elements to the new window.
Because a blank window is special we ended up using ExecuteScriptAsync
instead of AddScriptToExecuteOnDocumentCreatedAsync
and also we triggered an "extra" navigate on the newly opened window and then we're essentially waiting for the injected scripts / interop objects to become available on the Javascript side. We're using desktopJS and here instead of returning newWindow
we return a Promise.
This works although we'd really appreciate a more straightforward way of achieving the same.
Hi @ibebbs - thank you for providing the repro repo. I have checked it - it should work if you will remove alert
from the script and put console.log
instead. Could you please try that? Do you really need to have alert in script or it was just for demo purpose? As mentioned before in general flow with adding script for new window should work unless you have some corner case also - then we would like to learn about what are you trying to achieve.
@vbryh-msft I've just tried @ibebbs repro with alerts replaced with console.log
s, with the same outcome 🤔
@RendijsSmukulis could you please check this comment https://github.com/MicrosoftEdge/WebView2Feedback/issues/2491#issuecomment-1211604701 - it summarizes the issue, thanks.
@RendijsSmukulis @ibebbs also to reiterate - this is correct order of calls, which should work, do not set NewWindow after adding the script:
window.Content = newWebView;
window.Show();
await newWebView.EnsureCoreWebView2Async();
e.NewWindow = newWebView.CoreWebView2;
await newWebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync("console.log('hello')");
e.Handled = true;
_deferral.Complete();
Thanks for the looking into this @vbryh-msft.
I do not believe the use of alert
is the issue here. While using console.log
may have appeared to resolve the issue for you, as demonstrated by @RendijsSmukulis, it does not work reliably due to what appears to be an underlying race condition (navigation occuring as soon as the UI thread is relinquished).
To demonstrate this I have added logging to the master branch of the reproduction repository and changed the script to use console.log
rather than alert
per your suggestion.
If you run this app, ensure "Set NewWindow" is checked and "Set Source" is not, then click the "Open Window From Target..." link, you we see the following log:
[02:56:41.3058] Assigning NewWindow
[02:56:41.3134] Assigned NewWindow
[02:56:41.3135] Start Loading Scripts
[02:56:41.3623] NavigationStarting - 'about:blank'
[02:56:41.6923] ContentLoading - 'https://ian.bebbs.co.uk/'
[02:56:41.6984] Completed Loading Scripts
As you can see, content loading has been started prior to script loading being complete. This is the core of the issue. However, this is exacerbated by anything that might cause the UI thread to be relinquished. For example, if you clear the log and set "Delay" to "2000" then click the "Open Window From Target..." link again, you will see the following log:
[03:08:37.2107] Assigning NewWindow
[03:08:37.2179] Assigned NewWindow
[03:08:37.2816] NavigationStarting - 'about:blank'
[03:08:37.5168] ContentLoading - 'https://ian.bebbs.co.uk/'
[03:08:39.2501] Start Loading Scripts
[03:08:39.2747] Completed Loading Scripts
As you can see, content has loaded before the scripts have even started loading.
I believe this demonstrates the core of the issue and why navigation should not be scheduled prior to the deferral being completed.
Thanks again for you help here.
Hi @ibebbs, my current conclusion is similar to yours: the sequence of Navigation
arising from SetNewWindow
and AddScriptToExecuteOnDocumentCreatedAsync
could not be guaranteed.
We have put together a workaround by using PostMessage()
and a second navigation to keep the order of execution.
Your workaround is very interesting and suggestions about fix seem to work.
We've documented it internally and are working on it.
Thanks for you suggesions so much.
No problem @plantree, glad to be of help.
Could you provide more information about your PostMessage()
workaround? The workaround we're currently employing - cancelling navigations until script loading is complete - has a serious issue (navigation via form POST no longer works correctly) and we will need to find a better solution if MS are not able to provide a fix for this issue in the short-term.
Thanks!
Hi @ibebbs,
I prepared a demo project to show the workaround, which could be found here. If you have any questions, please let me know.
Besides, your suggestions are very helpful, and we are working on solving the root cause.
Thanks for you advice.
Thanks for the demo project @plantree
I see what you're doing and how it might be a viable workaround for some. Unfortunately I don't think we'll be able to use this approach as:
openWindow
function; andDo you have an ETA on when a fix for the root cause might be available?
Thanks again for all your help.
Hi @ibebbs,
We do not have ETA currently, but I'll synchronize with you when we have.
As for you question, if it is possible, could you provide a demo project that we could try to solve together?
Sorry for the delay in getting back to you @plantree.
Unfortunately we don't have a demo project but the repo provided above should be sufficient to address the issue.
Thanks!
Hi @ibebbs,
Thanks for the demo project you provide. I'll try to repoduce your issue locally and synchronize with you later.
Hi,
I'm trying to ensure that custom JavaScript is always injected to new windows which are requested by e.g.: window.open. For this I'm subscribing to NewWindowRequested event handler the following way:
What I'm experiencing is that the new window pops up fine but the custom script is never executed.
How can I ensure that custom JavaScript is always executed when a new window is requested by window.open before the window object is returned to the JavaScript caller?
Thank you, Zoltan
AB#43367417