From effccb524e9a066c452a61bb9a121d97de99ab0b Mon Sep 17 00:00:00 2001 From: Chetan Pandey Date: Wed, 20 May 2026 14:00:58 +0530 Subject: [PATCH 1/3] Add WebView2 best practices folder with UWP guide Introduces a best-practices/ folder with a README and an initial UWP best practices document covering controller shutdown and external URI scheme handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- best-practices/README.md | 33 +++++++++++ best-practices/uwp-best-practices.md | 87 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 best-practices/README.md create mode 100644 best-practices/uwp-best-practices.md diff --git a/best-practices/README.md b/best-practices/README.md new file mode 100644 index 000000000..9e1490a6a --- /dev/null +++ b/best-practices/README.md @@ -0,0 +1,33 @@ +# WebView2 Best Practices + +This folder documents best practices that WebView2 developers should follow when +building applications that host the WebView2 control. The goal is to capture +practical, API‑level guidance — informed by real issues that developers have run +into — so that new and existing WebView2 apps can avoid common pitfalls around +lifecycle management, navigation, security, performance, and platform +integration. + +Each document focuses on a specific app model, platform, or scenario, and is +organized around two questions for every recommendation: + +- **Best practice** — what you should do. +- **Why?** — the reasoning, and what can go wrong if you don't. + +## Available guides + +- [UWP best practices](./uwp-best-practices.md) — guidance for hosting WebView2 + inside Universal Windows Platform (UWP) applications. + +## Contributing + +If you have hit a problem in your own app that you think other WebView2 +developers would benefit from knowing about, feel free to open an issue or a +pull request proposing a new best practice. Please keep entries: + +- **API‑level and product‑agnostic** — describe the WebView2 API behavior and + the recommended pattern, not implementation details of any specific app or + internal product. +- **Actionable** — readers should be able to apply the guidance directly in + their own code. +- **Justified** — always include the *Why?* so readers understand the trade‑off + and can decide how it applies to their scenario. diff --git a/best-practices/uwp-best-practices.md b/best-practices/uwp-best-practices.md new file mode 100644 index 000000000..427a9d7e5 --- /dev/null +++ b/best-practices/uwp-best-practices.md @@ -0,0 +1,87 @@ +# UWP Best Practices for WebView2 + +Guidance for hosting WebView2 inside Universal Windows Platform (UWP) apps. +Each entry uses the format: + +- **Best practice** — what to do (with a code snippet). +- **Why?** — the problem this prevents. + +--- + +## 1. Explicitly close the WebView2 controller on app shutdown + +### Best practice + +Call `Close()` on every `CoreWebView2Controller` you own before your UWP app +exits, and release your references afterwards. + +```cpp +// C++/WinRT +void App::OnClosed() +{ + if (m_controller) + { + m_controller.Close(); + m_controller = nullptr; + m_webview = nullptr; + } +} +``` + +```csharp +// C# +private void OnClosed(object sender, WindowEventArgs e) +{ + if (_controller != null) + { + _controller.Close(); + _controller = null; + _webView = null; + } +} +``` + +### Why? + +Without an explicit `Close()`, the WebView2 instance is torn down without +going through the normal browser shutdown procedure. As a result, state such +as cookies is not flushed correctly, which can lead to data loss or +inconsistent state on the next launch. + +--- + +## 2. Handle external URI schemes via the OS launcher + +### Best practice + +Subscribe to `CoreWebView2.LaunchingExternalUriScheme`, cancel the default +launch, and forward the URI to `Windows.System.Launcher.LaunchUriAsync` so +that the OS performs the activation. + +```csharp +// C# +_webView.CoreWebView2.LaunchingExternalUriScheme += async (s, e) => +{ + e.Cancel = true; + await Windows.System.Launcher.LaunchUriAsync(new Uri(e.Uri)); +}; +``` + +```cpp +// C++/WinRT +m_webview.LaunchingExternalUriScheme([](auto&&, auto const& args) +{ + args.Cancel(true); + Windows::System::Launcher::LaunchUriAsync(Uri{ args.Uri() }); +}); +``` + +### Why? + +WebView2 inside a UWP app does not automatically activate external protocol +handlers (for example `mailto:`, `tel:`, `ms-settings:`, store, or custom +`myapp:` schemes). Without this handler, clicks on such links silently do +nothing, which appears as a broken link to the user. `LaunchUriAsync` is the +supported UWP activation path, and `LaunchingExternalUriScheme` is the +purpose‑built event for this hand‑off — it fires only for external schemes, +so the app does not need to filter `http`/`https` navigations itself. From 24e56e8283a006492c49a77fa7b8e87bcaa21b16 Mon Sep 17 00:00:00 2001 From: Chetan Pandey Date: Wed, 20 May 2026 15:17:33 +0530 Subject: [PATCH 2/3] Address PR review: use UWP Suspending event and validate external URI launches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- best-practices/uwp-best-practices.md | 56 +++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/best-practices/uwp-best-practices.md b/best-practices/uwp-best-practices.md index 427a9d7e5..26b4b9b55 100644 --- a/best-practices/uwp-best-practices.md +++ b/best-practices/uwp-best-practices.md @@ -12,12 +12,18 @@ Each entry uses the format: ### Best practice -Call `Close()` on every `CoreWebView2Controller` you own before your UWP app -exits, and release your references afterwards. +Call `Close()` on every `CoreWebView2Controller` you own from your UWP app's +suspend handler (for example `Application.Suspending`), and release your +references afterwards. ```cpp -// C++/WinRT -void App::OnClosed() +// C++/WinRT — App.xaml.cpp +App::App() +{ + Suspending({ this, &App::OnSuspending }); +} + +void App::OnSuspending(IInspectable const&, SuspendingEventArgs const&) { if (m_controller) { @@ -29,8 +35,13 @@ void App::OnClosed() ``` ```csharp -// C# -private void OnClosed(object sender, WindowEventArgs e) +// C# — App.xaml.cs +public App() +{ + this.Suspending += OnSuspending; +} + +private void OnSuspending(object sender, SuspendingEventArgs e) { if (_controller != null) { @@ -56,14 +67,31 @@ inconsistent state on the next launch. Subscribe to `CoreWebView2.LaunchingExternalUriScheme`, cancel the default launch, and forward the URI to `Windows.System.Launcher.LaunchUriAsync` so -that the OS performs the activation. +that the OS performs the activation. Gate the hand‑off on +`IsUserInitiated` and, where appropriate, an allow‑list of schemes and +initiating origins so that web content cannot silently activate other apps. ```csharp // C# +private static readonly HashSet AllowedSchemes = + new(StringComparer.OrdinalIgnoreCase) { "mailto", "tel", "ms-settings" }; + _webView.CoreWebView2.LaunchingExternalUriScheme += async (s, e) => { e.Cancel = true; - await Windows.System.Launcher.LaunchUriAsync(new Uri(e.Uri)); + + if (!e.IsUserInitiated) return; + if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri)) return; + if (!AllowedSchemes.Contains(uri.Scheme)) return; + + try + { + await Windows.System.Launcher.LaunchUriAsync(uri); + } + catch + { + // Optional: surface a fallback UI to the user. + } }; ``` @@ -72,7 +100,17 @@ _webView.CoreWebView2.LaunchingExternalUriScheme += async (s, e) => m_webview.LaunchingExternalUriScheme([](auto&&, auto const& args) { args.Cancel(true); - Windows::System::Launcher::LaunchUriAsync(Uri{ args.Uri() }); + + if (!args.IsUserInitiated()) return; + + Uri uri{ nullptr }; + try { uri = Uri{ args.Uri() }; } catch (...) { return; } + + auto scheme = uri.SchemeName(); + if (scheme != L"mailto" && scheme != L"tel" && scheme != L"ms-settings") + return; + + Windows::System::Launcher::LaunchUriAsync(uri); }); ``` From 658373bccf1ecf0579ae7c1b2370257e0e4d3292 Mon Sep 17 00:00:00 2001 From: Chetan Pandey Date: Thu, 21 May 2026 10:11:04 +0530 Subject: [PATCH 3/3] Use suspend deferral with BrowserProcessExited for WebView2 shutdown Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- best-practices/uwp-best-practices.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/best-practices/uwp-best-practices.md b/best-practices/uwp-best-practices.md index 26b4b9b55..d12cfbe54 100644 --- a/best-practices/uwp-best-practices.md +++ b/best-practices/uwp-best-practices.md @@ -14,7 +14,10 @@ Each entry uses the format: Call `Close()` on every `CoreWebView2Controller` you own from your UWP app's suspend handler (for example `Application.Suspending`), and release your -references afterwards. +references afterwards. Because browser process teardown is asynchronous, take +a suspend deferral and complete it from +`CoreWebView2Environment.BrowserProcessExited` so the OS does not tear the +app down before the runtime has finished shutting down. ```cpp // C++/WinRT — App.xaml.cpp @@ -23,8 +26,15 @@ App::App() Suspending({ this, &App::OnSuspending }); } -void App::OnSuspending(IInspectable const&, SuspendingEventArgs const&) +void App::OnSuspending(IInspectable const&, SuspendingEventArgs const& e) { + auto deferral = e.SuspendingOperation().GetDeferral(); + + m_environment.BrowserProcessExited([deferral](auto&&, auto&&) + { + deferral.Complete(); + }); + if (m_controller) { m_controller.Close(); @@ -43,6 +53,16 @@ public App() private void OnSuspending(object sender, SuspendingEventArgs e) { + var deferral = e.SuspendingOperation.GetDeferral(); + + void OnExited(object s, CoreWebView2BrowserProcessExitedEventArgs args) + { + _environment.BrowserProcessExited -= OnExited; + deferral.Complete(); + } + + _environment.BrowserProcessExited += OnExited; + if (_controller != null) { _controller.Close();