Skip to content

Commit 7183cdc

Browse files
[Windows] Fixed : WebView CanGoBack Returns true when it Shouldn't (#37648)
<!-- Please keep the note below for people who find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment whether this change resolves your issue. Thank you! This pull request addresses issues with the accuracy of the `CanGoBack` and `CanGoForward` properties in the Windows WebView handler, ensuring that navigation state is correctly reflected and not stale. It also adds a regression test to prevent future issues with navigation history. ### Description of Change **Fixes and improvements to WebView navigation state:** * Updated the logic in `WebViewHandler.Windows.cs` to check `CoreWebView2.CanGoBack` and `CoreWebView2.CanGoForward` instead of relying solely on the WebView2 control properties, which could become stale and cause incorrect navigation behavior. * Enhanced the `UpdateCanGoBackForward` extension method in `WebViewExtensions.cs` to prefer the `CoreWebView2` navigation state for `CanGoBack` and `CanGoForward`, falling back to the control's properties only when necessary. **Testing and regression prevention:** * Added a new test, `CanGoBackIsFalseAfterReturningToFirstPage`, to verify that `CanGoBack` is set to false after navigating back to the first page, preventing regressions related to navigation history. <!-- Enter description of the fix in this section --> ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #37534 ### Tested the behavior in the following platforms - [x] Windows - [ ] Android - [ ] iOS - [ ] Mac | Before Issue Fix | After Issue Fix | |----------|----------| | <video src="https://github.com/user-attachments/assets/c6c4e68b-21ac-49cd-ad96-3f628c889674"> | <video src="https://github.com/user-attachments/assets/1a0fcee7-32d4-42a6-8003-25b89087e580"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> ---------
1 parent e39fe7c commit 7183cdc

4 files changed

Lines changed: 110 additions & 63 deletions

File tree

.github/skills/release-readiness/SKILL.md

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -144,59 +144,6 @@ preserving their different readiness semantics. It does not read or write Loop
144144
or SharePoint. Do not copy private source-page content into the evidence file;
145145
unknown fields must remain `TBD`.
146146

147-
### Preview: local net11 official-build health
148-
149-
For net11 preview runs through this skill from a local checkout, invoke
150-
`Get-PreviewReadiness.ps1 '-PublicSafe:$false'`. The script then automatically
151-
queries the internal official `dotnet-maui` pipeline (Azure DevOps definition
152-
`1095`, org `dnceng`, project `internal`) when the current Azure CLI identity has
153-
access. No build ID is required. It independently checks:
154-
155-
1. `refs/heads/net11.0` — the inflight source/survey lane.
156-
2. `refs/heads/release/11.0.1xx-previewN` — the evaluated release branch, when
157-
that branch exists.
158-
159-
Candidate mode still checks `net11.0`; it adds the prospective release ref only
160-
after that ref exists. Identical refs are queried once. The local report includes
161-
each branch's health classification, build ID and number, pipeline status/result,
162-
source SHA, and internal build URL. A failed or canceled current build is `red`;
163-
a partially successful build is `partial-success`; a build behind the newest
164-
trigger-eligible commit is `stale`; a queued/running build is `in-progress`;
165-
missing or malformed evidence is `unknown`.
166-
167-
Discovery examines a bounded five-build window. It prefers a build at exact branch
168-
HEAD, then scans by queue time and skips only candidates proven stale before
169-
accepting one proven current. Indeterminate candidates are buffered: disagreeing
170-
possible outcomes remain `unknown`, while a later proven-current failure remains
171-
`red` only when every buffered candidate is also a completed failure/cancellation.
172-
A terminal window containing only same-branch failed/canceled indeterminate builds
173-
also remains blocking as `failed-or-stale` because every candidate is either red
174-
or stale. Its rendering preserves that uncertainty and requires restoring currency
175-
evidence before choosing failure repair versus a current-HEAD rerun. This prevents
176-
both false readiness upgrades and loss of certain blocking evidence.
177-
178-
The internal check is intentionally fail-open:
179-
180-
- `GITHUB_ACTIONS=true` skips it before any Azure command runs.
181-
- Missing Azure CLI, expired login, or inaccessible dnceng/internal access yields
182-
`skipped` and does not downgrade the public-data verdict.
183-
- Azure CLI and GitHub branch queries have bounded execution; a timeout yields
184-
`unknown` rather than hanging the local readiness run.
185-
- Local `red`/`stale`/`failed-or-stale` maps to `BLOCKED`,
186-
`in-progress`/`partial-success` to `WATCH`, and `unknown` to `UNKNOWN`.
187-
For `failed-or-stale`, restore build-currency evidence first; then either repair
188-
the failed build if it is current or run the official build at current HEAD if
189-
it is stale.
190-
- `-PublicSafe:$true` omits all internal IDs, SHAs, URLs, and branch rows. The
191-
public workflow uses this behavior and never receives internal credentials.
192-
193-
The script remains public-safe by default. This skill and the release-readiness
194-
agent explicitly pass `'-PublicSafe:$false'` for enriched local net11 reports;
195-
never reuse those artifacts in a public tracker issue. `-IncludeInternal`
196-
remains an explicit compatibility override when a caller requests a sanitized
197-
internal classification, and `-InternalBuildId` remains a diagnostic override
198-
for the evaluated release branch.
199-
200147
### Preview: authoritative blessed-build source (.NET Release Tracker)
201148

202149
For **Previews**, this skill's public survey (CI health + regression classification on `net<major>.0` or the preview branch) tells you whether the code is *ready*, but it **cannot on its own name which staged build is the official, blessed preview** — that designation lives in the private **.NET Release Tracker** plugin. So when answering *"run release readiness … is net11 preview6 ready?"* / *"which build is the official preview6?"*, consult that authoritative source **in addition to** running `Get-PreviewReadiness.ps1`:

src/Core/src/Handlers/WebView/WebViewHandler.Windows.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,18 +104,38 @@ internal static void MapBackground(IWebViewHandler handler, IWebView webView)
104104

105105
public static void MapGoBack(IWebViewHandler handler, IWebView webView, object? arg)
106106
{
107-
if (handler.PlatformView.CanGoBack && handler is WebViewHandler w)
107+
var platformView = handler.PlatformView;
108+
if (platformView is null)
109+
{
110+
return;
111+
}
112+
113+
// Use CoreWebView2.CanGoBack rather than the WebView2 control's CanGoBack,
114+
// which can be stale (see UpdateCanGoBackForward).
115+
if ((platformView.CoreWebView2?.CanGoBack ?? platformView.CanGoBack) && handler is WebViewHandler w)
116+
{
108117
w.CurrentNavigationEvent = WebNavigationEvent.Back;
118+
}
109119

110-
handler.PlatformView?.UpdateGoBack(webView);
120+
platformView.UpdateGoBack(webView);
111121
}
112122

113123
public static void MapGoForward(IWebViewHandler handler, IWebView webView, object? arg)
114124
{
115-
if (handler.PlatformView.CanGoForward && handler is WebViewHandler w)
125+
var platformView = handler.PlatformView;
126+
if (platformView is null)
127+
{
128+
return;
129+
}
130+
131+
// Use CoreWebView2.CanGoForward rather than the WebView2 control's CanGoForward,
132+
// which can be stale (see UpdateCanGoBackForward).
133+
if ((platformView.CoreWebView2?.CanGoForward ?? platformView.CanGoForward) && handler is WebViewHandler w)
134+
{
116135
w.CurrentNavigationEvent = WebNavigationEvent.Forward;
136+
}
117137

118-
handler.PlatformView?.UpdateGoForward(webView);
138+
platformView.UpdateGoForward(webView);
119139
}
120140

121141
public static void MapReload(IWebViewHandler handler, IWebView webView, object? arg)

src/Core/src/Platform/Windows/WebViewExtensions.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ public static void UpdateUserAgent(this WebView2 platformWebView, IWebView webVi
7474

7575
public static void UpdateGoBack(this WebView2 platformWebView, IWebView webView)
7676
{
77-
if (platformWebView == null)
77+
if (!platformWebView.IsValid())
78+
{
7879
return;
80+
}
7981

8082
if (platformWebView.CoreWebView2.CanGoBack)
8183
platformWebView.CoreWebView2.GoBack();
@@ -85,8 +87,10 @@ public static void UpdateGoBack(this WebView2 platformWebView, IWebView webView)
8587

8688
public static void UpdateGoForward(this WebView2 platformWebView, IWebView webView)
8789
{
88-
if (platformWebView == null)
90+
if (!platformWebView.IsValid())
91+
{
8992
return;
93+
}
9094

9195
if (platformWebView.CoreWebView2.CanGoForward)
9296
platformWebView.CoreWebView2.GoForward();
@@ -101,8 +105,21 @@ public static void UpdateReload(this WebView2 platformWebView, IWebView webView)
101105

102106
internal static void UpdateCanGoBackForward(this WebView2 platformWebView, IWebView webView)
103107
{
104-
webView.CanGoBack = platformWebView.CanGoBack;
105-
webView.CanGoForward = platformWebView.CanGoForward;
108+
// The WebView2 XAML control's CanGoBack/CanGoForward properties can become stale
109+
// (e.g. remain true even when no history is left), so prefer CoreWebView2's values,
110+
// which reflect the current navigation state synchronously. Guard with IsValid()
111+
// since CoreWebView2 can throw if accessed after the control has been closed.
112+
if (platformWebView.IsValid())
113+
{
114+
var coreWebView2 = platformWebView.CoreWebView2;
115+
webView.CanGoBack = coreWebView2.CanGoBack;
116+
webView.CanGoForward = coreWebView2.CanGoForward;
117+
}
118+
else
119+
{
120+
webView.CanGoBack = platformWebView.CanGoBack;
121+
webView.CanGoForward = platformWebView.CanGoForward;
122+
}
106123
}
107124

108125
public static void Eval(this WebView2 platformWebView, IWebView webView, string script)

src/Core/tests/DeviceTests/Handlers/WebView/WebViewHandlerTests.Windows.cs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,6 @@ await InvokeOnMainThreadAsync(async () =>
328328

329329
var handler = CreateHandler(webView);
330330

331-
var platformView = handler.PlatformView;
332-
333331
// Setup the view to be displayed/parented and run our tests on it
334332
await AttachAndRun(webView, async (handler) =>
335333
{
@@ -388,6 +386,71 @@ await AttachAndRun(webView, async (handler) =>
388386
});
389387
}
390388

389+
[Fact(DisplayName = "CanGoBack Is False After Navigating Back To The First Page (Issue #37534)")]
390+
public async Task CanGoBackIsFalseAfterReturningToFirstPage()
391+
{
392+
var pageLoadTimeout = TimeSpan.FromSeconds(5);
393+
394+
await InvokeOnMainThreadAsync(async () =>
395+
{
396+
var webView = new WebViewStub()
397+
{
398+
Width = 100,
399+
Height = 100,
400+
Source = new HtmlWebViewSourceStub { Html = "<h1>Page 1</h1>" }
401+
};
402+
403+
var handler = CreateHandler(webView);
404+
405+
var platformView = handler.PlatformView;
406+
407+
// Setup the view to be displayed/parented and run our tests on it
408+
await AttachAndRun(webView, async (handler) =>
409+
{
410+
async Task WaitForNavigationAsync(Action navigate)
411+
{
412+
var tcsNavigated = new TaskCompletionSource<bool>();
413+
using var ctsTimeout = new CancellationTokenSource(pageLoadTimeout);
414+
ctsTimeout.Token.Register(() => tcsNavigated.TrySetException(new TimeoutException("Failed to navigate")));
415+
416+
webView.NavigatedDelegate = (evnt, url, result) =>
417+
{
418+
if (result == WebNavigationResult.Success)
419+
tcsNavigated.TrySetResult(true);
420+
};
421+
422+
navigate();
423+
424+
Assert.True(await tcsNavigated.Task);
425+
}
426+
427+
// Wait for the first page to finish loading
428+
await WaitForNavigationAsync(() => { });
429+
430+
// Navigate to a second page, creating back history
431+
await WaitForNavigationAsync(() =>
432+
{
433+
webView.Source = new HtmlWebViewSourceStub { Html = "<h1>Page 2</h1>" };
434+
handler.UpdateValue(nameof(IWebView.Source));
435+
});
436+
437+
Assert.True(webView.CanGoBack, "CanGoBack should be true after navigating to a second page.");
438+
439+
// Navigate back to the first page
440+
await WaitForNavigationAsync(() =>
441+
{
442+
handler.Invoke(nameof(IWebView.GoBack), null);
443+
});
444+
445+
// Regression test for https://github.com/dotnet/maui/issues/37534:
446+
// CanGoBack should become false once there is no more back history left,
447+
// instead of remaining stale as true (which caused GoBack() to repeatedly
448+
// reload the same page instead of falling back to app-level navigation).
449+
Assert.False(webView.CanGoBack, "CanGoBack should be false once there is no more back history.");
450+
});
451+
});
452+
}
453+
391454
WebView2 GetNativeWebView(WebViewHandler webViewHandler) =>
392455
webViewHandler.PlatformView;
393456

0 commit comments

Comments
 (0)