-
Notifications
You must be signed in to change notification settings - Fork 54
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
API Review: CoreWebView2Frame.FrameCreated #4982
base: main
Are you sure you want to change the base?
Changes from 8 commits
132f3f6
cc43133
44eedbe
8d67f77
6e42f58
1fd53bc
04b1b94
2e237f6
4d00028
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,215 @@ | ||||||||||||
CoreWebView2Frame.FrameCreated API | ||||||||||||
=== | ||||||||||||
|
||||||||||||
# Background | ||||||||||||
At present, WebView2 enables developers to track only first-level | ||||||||||||
iframes, which are the direct child iframes of the main frame. | ||||||||||||
However, we see that WebView2 customers want to manage nested | ||||||||||||
iframes, such as recording the navigation history for a second | ||||||||||||
level iframe. To address this, we will introduce the | ||||||||||||
`CoreWebView2Frame.FrameCreated` API. This new API will allow | ||||||||||||
developers to subscribe to the nested iframe creation event, | ||||||||||||
giving them access to all properties, methods, and events of | ||||||||||||
[CoreWebView2Frame](https://learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2frame) | ||||||||||||
for the nested iframe. | ||||||||||||
|
||||||||||||
To prevent unnecessary performance implication, WebView2 does | ||||||||||||
not track any nested iframes by default. It only tracks a nested | ||||||||||||
iframe if its parent iframe (`CoreWebView2Frame`) has subscribed | ||||||||||||
to the `CoreWebView2Frame.FrameCreated` API. For a page with | ||||||||||||
multi-level iframes, developers can choose to track only the | ||||||||||||
main page and first-level iframes (the default behavior), a | ||||||||||||
partial WebView2 frames tree with specific iframes of interest, | ||||||||||||
or the full WebView2 frames tree. | ||||||||||||
|
||||||||||||
# Examples | ||||||||||||
### C++ Sample | ||||||||||||
```cpp | ||||||||||||
wil::com_ptr<ICoreWebView2> m_webview; | ||||||||||||
std::map<int, std::vector<std::wstring>> m_frame_navigation_urls; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Why not just use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fix. |
||||||||||||
// In this example, a WebView2 application wants to manage the | ||||||||||||
// navigation of third-party content residing in second-level iframes | ||||||||||||
// (Main frame -> First-level frame -> Second-level third-party frames). | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe also a sample showing full depth frame tracking? void TrackAllFrameNavigations()
{
webView.CoreWebView2.FrameCreated += (_, args) =>
{
OnFrameCreated(args.Frame);
};
}
void OnFrameCreated(CoreWebView2Frame frame)
{
frame.FrameCreated += (_, e) => OnFrameCreated(e.Frame);
frame.NavigationStarting += OnFrameNavigationStarting;
}
void OnFrameNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e)
{
// as before
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please consider adding an additional sample. |
||||||||||||
HRESULT RecordThirdPartyFrameNavigation() { | ||||||||||||
auto webview2_4 = m_webView.try_query<ICoreWebView2_4>(); | ||||||||||||
// Track the first-level webview frame. | ||||||||||||
webview2_4->add_FrameCreated( | ||||||||||||
Callback<ICoreWebView2FrameCreatedEventHandler>( | ||||||||||||
[this](ICoreWebView2* sender, ICoreWebView2FrameCreatedEventArgs* args) | ||||||||||||
-> HRESULT { | ||||||||||||
// [AddFrameCreated] | ||||||||||||
wil::com_ptr<ICoreWebView2Frame> webviewFrame; | ||||||||||||
CHECK_FAILURE(args->get_Frame(&webviewFrame)); | ||||||||||||
// Track nested (second-level) webview frame. | ||||||||||||
auto frame7 = webviewFrame.try_query<ICoreWebView2Frame7>(); | ||||||||||||
frame7->add_FrameCreated( | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need a null check on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fix. |
||||||||||||
Callback<ICoreWebView2FrameChildFrameCreatedEventHandler>( | ||||||||||||
[this]( | ||||||||||||
ICoreWebView2Frame* sender, | ||||||||||||
ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT | ||||||||||||
{ | ||||||||||||
wil::com_ptr<ICoreWebView2Frame> webviewFrame; | ||||||||||||
CHECK_FAILURE(args->get_Frame(&webviewFrame)); | ||||||||||||
wil::com_ptr<ICoreWebView2Frame2> frame2 = | ||||||||||||
webviewFrame.try_query<ICoreWebView2Frame2>(); | ||||||||||||
if (frame2) | ||||||||||||
{ | ||||||||||||
// Subscribe to nested (second-level) webview frame navigation | ||||||||||||
// starting event. | ||||||||||||
frame2->add_NavigationStarting( | ||||||||||||
Callback<ICoreWebView2FrameNavigationStartingEventHandler>( | ||||||||||||
[this]( | ||||||||||||
ICoreWebView2Frame* sender, | ||||||||||||
ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Fail fast if an exception comes out of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fix. |
||||||||||||
// Manage the navigation, e.g. cancel the | ||||||||||||
// navigation if it's on block list. | ||||||||||||
UINT32 frameId = 0; | ||||||||||||
auto frame5 = wil::com_ptr<ICoreWebView2Frame>(sender) | ||||||||||||
.try_query<ICoreWebView2Frame5>(); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check from try_query. Maybe we should use ICoreWebView2Frame5 in the outer lambda, so that we don't even get here if v5 is not supported. Then that would allow us to use query() instead of try_query(). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please consider simple fix of null check for simple sample code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be simplified to auto frame5 = wil::try_com_query<ICoreWebView2Frame5>(sender); avoids an unnecessary temporary com_ptr. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fix. |
||||||||||||
CHECK_FAILURE(frame5->get_FrameId(&frameId)); | ||||||||||||
wil::unique_cotaskmem_string uri; | ||||||||||||
CHECK_FAILURE(args->get_Uri(&uri)); | ||||||||||||
// Log the navigation history per frame Id. | ||||||||||||
m_frame_navigation_urls[(int)frameId].push_back(uri.get()); | ||||||||||||
return S_OK; | ||||||||||||
}) | ||||||||||||
.Get(), | ||||||||||||
nullptr); | ||||||||||||
} | ||||||||||||
return S_OK; | ||||||||||||
}) | ||||||||||||
.Get(), | ||||||||||||
nullptr); | ||||||||||||
// [AddFrameCreated] | ||||||||||||
return S_OK; | ||||||||||||
}) | ||||||||||||
.Get(), | ||||||||||||
nullptr); | ||||||||||||
} | ||||||||||||
``` | ||||||||||||
### C# Sample | ||||||||||||
```c# | ||||||||||||
var _frameNavigationUrls = new Dictionary<UINT32, List<string>>(); | ||||||||||||
// In this example, a WebView2 application wants to manage the | ||||||||||||
// navigation of third-party content residing in second-level iframes | ||||||||||||
// (Main frame -> First-level frame -> second-level third-party frames). | ||||||||||||
void RecordThirdPartyFrameNavigation() { | ||||||||||||
webView.CoreWebView2.FrameCreated += (sender, args) => | ||||||||||||
{ | ||||||||||||
// Track nested (second-level) webview frame. | ||||||||||||
args.Frame.FrameCreated += (frameCreatedSender, frameCreatedArgs) => | ||||||||||||
{ | ||||||||||||
CoreWebView2Frame childFrame = frameCreatedArgs.Frame; | ||||||||||||
childFrame.NavigationStarting += HandleChildFrameNavigationStarting; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fix. |
||||||||||||
} | ||||||||||||
|
||||||||||||
void HandleChildFrameNavigationStarting(object sender, | ||||||||||||
CoreWebView2NavigationStartingEventArgs args) | ||||||||||||
{ | ||||||||||||
// Manage the navigation, e.g. cancel the navigation | ||||||||||||
// if it's on block list. | ||||||||||||
CoreWebView2Frame frame = (CoreWebView2Frame)sender; | ||||||||||||
if (!_frameNavigationUrls.ContainsKey(frame.FrameId)) | ||||||||||||
{ | ||||||||||||
_frameNavigationUrls[frame.FrameId] = new List<string>(); | ||||||||||||
} | ||||||||||||
// Log the navigation history per frame Id. | ||||||||||||
_frameNavigationUrls[frame.FrameId].Add(args.Uri); | ||||||||||||
} | ||||||||||||
``` | ||||||||||||
|
||||||||||||
# API Details | ||||||||||||
## C++ | ||||||||||||
```C++ | ||||||||||||
/// Receives `FrameCreated` events. | ||||||||||||
interface ICoreWebView2FrameChildFrameCreatedEventHandler : IUnknown { | ||||||||||||
/// Provides the event args for the corresponding event. | ||||||||||||
HRESULT Invoke( | ||||||||||||
[in] ICoreWebView2Frame* sender, | ||||||||||||
[in] ICoreWebView2FrameCreatedEventArgs* args); | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// This is the ICoreWebView2Frame interface. | ||||||||||||
interface ICoreWebView2Frame7 : IUnknown { | ||||||||||||
/// Adds an event handler for the `FrameCreated` event. | ||||||||||||
/// Raised when a new direct descendant iframe is created. | ||||||||||||
/// Handle this event to get access to `ICoreWebView2Frame` objects. | ||||||||||||
/// Use `ICoreWebView2Frame::add_Destroyed` to listen for when this | ||||||||||||
/// iframe goes away. | ||||||||||||
/// | ||||||||||||
/// \snippet ScenarioWebViewEventMonitor.cpp AddFrameCreated | ||||||||||||
HRESULT add_FrameCreated( | ||||||||||||
[in] ICoreWebView2FrameChildFrameCreatedEventHandler* eventHandler, | ||||||||||||
[out] EventRegistrationToken* token); | ||||||||||||
|
||||||||||||
/// Removes an event handler previously added with `add_FrameCreated`. | ||||||||||||
HRESULT remove_FrameCreated( | ||||||||||||
[in] EventRegistrationToken token); | ||||||||||||
} | ||||||||||||
``` | ||||||||||||
|
||||||||||||
## C# | ||||||||||||
```c# | ||||||||||||
namespace Microsoft.Web.WebView2.Core | ||||||||||||
{ | ||||||||||||
runtimeclass CoreWebView2Frame | ||||||||||||
{ | ||||||||||||
[interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2Frame7")] | ||||||||||||
{ | ||||||||||||
event Windows.Foundation.TypedEventHandler<CoreWebView2Frame, CoreWebView2FrameCreatedEventArgs> FrameCreated; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
``` | ||||||||||||
|
||||||||||||
# Appendix | ||||||||||||
## Impacted API | ||||||||||||
### `CoreWebView2Frame.PermissionRequested` and `CoreWebView2Frame.ScreenCaptureStarting` | ||||||||||||
In the current case of nested iframes, the [PermissionRequested](https://learn.microsoft.com/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2frame#permissionrequested) | ||||||||||||
and [ScreenCaptureStarting](https://learn.microsoft.com/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2frame#screencapturestarting) | ||||||||||||
events will be raised from the top-level iframe. With the support | ||||||||||||
of tracking nested iframes, we can now handle these requests directly | ||||||||||||
within the nested iframe. Specifically, these requests are raised to | ||||||||||||
the nearest tracked frame, which is the `CoreWebView2Frame` closest | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could lead to a problem if an app combines multiple components, and one of them adds a FrameCreated handler and accidentally messes up the other component which expected the event from the top-level frame. Is the answer "Yeah, you can't really componentize your app like that because features used by one app might have adverse consequences far away." So it means I can't have a "app usage telemetry" component that hooks all frame activity and logs it, because that would accidentally break my "permission manager" component. From the Windows Runtime design guidelines:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For
But I think this kind of shared behavior should be avoid by developers. Even if we, align with the browser, track all iframes by default and the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No issue after discussion: event bubbles out in existing event bubbling pattern. |
||||||||||||
to the frame that initiates the request (from bottom to top). | ||||||||||||
``` | ||||||||||||
// Example: | ||||||||||||
// A (main frame/CoreWebView2) | ||||||||||||
// | | ||||||||||||
// B (first-level iframe/CoreWebView2Frame) | ||||||||||||
// | | ||||||||||||
// C (nested iframe) | ||||||||||||
// | | ||||||||||||
// D (nested iframe) | ||||||||||||
``` | ||||||||||||
Suppose there's a `PermissionRequest` comes from D. | ||||||||||||
* If D is a tracked frame (`CoreWebView2Frame`), then D is the | ||||||||||||
closet tracked frame from which the request will be raised from. | ||||||||||||
* If D is not being tracked, and C is a tracked frame. Then C | ||||||||||||
is the closet tracked frame from which the request will be | ||||||||||||
raised from. | ||||||||||||
* If neither C nor D is tracked, then B is the closet tracked | ||||||||||||
frame from which the request will be raised. This case applies | ||||||||||||
to current `PermissionRequested` developers, as they haven't | ||||||||||||
subscribe to the `CoreWebView2Frame.FrameCreated` event. | ||||||||||||
Therefore, requests originating from iframes will still be | ||||||||||||
raised from the first-level iframe. | ||||||||||||
|
||||||||||||
If the `PermissionRequested` event is not handled in the current | ||||||||||||
tracked frame, the request will propagate to its parent | ||||||||||||
`CoreWebView2Frame`, or to `CoreWebView2` if the parent frame | ||||||||||||
is the main frame. For example, if frame D is tracked but does | ||||||||||||
not handle the request, the request will bubble up to frame C. | ||||||||||||
If frame C handles the request, it will not propagate further | ||||||||||||
to its parent frame B. | ||||||||||||
|
||||||||||||
### `CoreWebView2.ProcessFailed` | ||||||||||||
With the support of tracking nested iframes, the processes | ||||||||||||
which support these nested iframes will be also tracked by | ||||||||||||
[ProcessFailed](https://learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2.processfailed). | ||||||||||||
As we only track processes running tracked iframes, existing | ||||||||||||
developers will not receive any process failed events specific | ||||||||||||
to nested iframes as they haven't subscribe to the | ||||||||||||
`CoreWebView2Frame.FrameCreated` event. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to violate Windows Runtime guidelines, because it means that you don't get a CoreWebView2.FrameNavigationStarting unless you subscribe to the FrameCreated event (even if the event handler does nothing).
In this case, the app can observe the side effects (through the FrameNavigationStarting event).
We could have a new Boolean property on the frame
ShouldRaiseNavigationEvents
which defaults to true for first-level frames and false for deeper frames. (But see the componentization problem later.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to clarify the documentation here. The
CoreWebView2.FrameNavigationStarting
event will still be triggered for all child iframes, regardless of whether we subscribe to theCoreWebView2Frame.FrameCreated
event or not. In this context, we are referring to the APIs in CoreWebView2Frame. We think that event handler matters because if developers do not subscribe to it, there is no need to expose any CoreWebView2Frame APIsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please clarify that this is an implementation note. This is only about an internal perf optimization - no observable behavior changes described here.