Skip to content

Commit 34d848a

Browse files
SuthiYuvarajCopilotStephaneDelcroixSubhikshaSf4851
authored andcommitted
Fix for BlazorWebView Back Navigation Issues on Android 13+ After Predictive Back Gesture Changes (#33213)
<!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Description BlazorWebView back navigation stopped working in .NET 10 on Android. When the user presses the back button while viewing a BlazorWebView with navigable history, the entire page is popped instead of navigating back within the WebView content ### RootCause The AndroidLifecycle.OnBackPressed lifecycle event system introduced for Android 13+ predictive back gesture support intercepts back navigation. Since BlazorWebView doesn't register a handler to check WebView.CanGoBack(), the system pops the entire page instead of allowing the WebView to navigate its internal history first, breaking the back navigation behavior that worked in .NET 9. ### Description of Change PR #32461 introduced distributed back navigation handling for Android 13+ predictive back gesture support. This PR extends that implementation to include BlazorWebView **BlazorWebViewHandler.Android.cs:** Added an `AndroidLifecycle.OnBackPressed` handler in ConnectHandler. The handler checks `WebView.CanGoBack()` to determine whether the BlazorWebView maintains internal navigation history. If history exists, it calls `WebView.GoBack()` and returns true, ensuring the user navigates backwards within the WebView instead of closing the page. If no internal history exists, it returns false, allowing the standard MAUI navigation stack to handle the back action. **LifecycleEventService.cs**: Added `RemoveEvent<TDelegate>` method to properly cleanup lifecycle event handlers and prevent memory leaks when handlers are disconnected. ### Issues Fixed Fixes #32767 ### Tested the behaviour on the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Output Screenshot Before Issue Fix | After Issue Fix | |----------|----------| |<video width="300" height="150" alt="Before Fix" src="https://github.com/user-attachments/assets/0d6550f8-115a-4893-bb0c-ee24bff38378">|<video width="300" height="150" alt="After Fix" src="https://github.com/user-attachments/assets/49bf38a5-f7be-4790-8fbb-b29ab96b1ed3">| --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stephane Delcroix <stephane@delcroix.org> Co-authored-by: Subhiksha Chandrasekaran <subhiksha.c@syncfusion.com>
1 parent a2e3969 commit 34d848a

File tree

5 files changed

+262
-0
lines changed

5 files changed

+262
-0
lines changed

src/BlazorWebView/src/Maui/Android/BlazorAndroidWebView.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
99
/// </summary>
1010
internal class BlazorAndroidWebView : AWebView
1111
{
12+
internal bool BackNavigationHandled { get; set; }
13+
1214
/// <summary>
1315
/// Initializes a new instance of <see cref="BlazorAndroidWebView"/>
1416
/// </summary>
@@ -22,8 +24,10 @@ public override bool OnKeyDown(Keycode keyCode, KeyEvent? e)
2224
if (keyCode == Keycode.Back && CanGoBack() && e?.RepeatCount == 0)
2325
{
2426
GoBack();
27+
BackNavigationHandled = true;
2528
return true;
2629
}
30+
BackNavigationHandled = false;
2731
return false;
2832
}
2933
}

src/BlazorWebView/src/Maui/Android/BlazorWebViewHandler.Android.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Threading.Tasks;
3+
using Android.Window;
34
using Android.Webkit;
45
using Android.Widget;
56
using Microsoft.Extensions.DependencyInjection;
@@ -9,6 +10,7 @@
910
using Microsoft.Maui;
1011
using Microsoft.Maui.Dispatching;
1112
using Microsoft.Maui.Handlers;
13+
using Microsoft.Maui.LifecycleEvents;
1214
using static global::Android.Views.ViewGroup;
1315
using AWebView = global::Android.Webkit.WebView;
1416
using Path = System.IO.Path;
@@ -21,10 +23,27 @@ public partial class BlazorWebViewHandler : ViewHandler<IBlazorWebView, AWebView
2123
private WebChromeClient? _webChromeClient;
2224
private AndroidWebKitWebViewManager? _webviewManager;
2325
internal AndroidWebKitWebViewManager? WebviewManager => _webviewManager;
26+
private AndroidLifecycle.OnBackPressed? _onBackPressedHandler;
27+
BlazorWebViewPredictiveBackCallback? _predictiveBackCallback;
2428

2529
private ILogger? _logger;
2630
internal ILogger Logger => _logger ??= Services!.GetService<ILogger<BlazorWebViewHandler>>() ?? NullLogger<BlazorWebViewHandler>.Instance;
2731

32+
/// <summary>
33+
/// Gets the concrete LifecycleEventService to access internal RemoveEvent method.
34+
/// RemoveEvent is internal because it's not part of the public ILifecycleEventService contract,
35+
/// but is needed for proper cleanup of lifecycle event handlers.
36+
/// </summary>
37+
private LifecycleEventService? TryGetLifecycleEventService()
38+
{
39+
var services = MauiContext?.Services;
40+
if (services != null)
41+
{
42+
return services.GetService<ILifecycleEventService>() as LifecycleEventService;
43+
}
44+
return null;
45+
}
46+
2847
protected override AWebView CreatePlatformView()
2948
{
3049
Logger.CreatingAndroidWebkitWebView();
@@ -60,10 +79,89 @@ protected override AWebView CreatePlatformView()
6079
return blazorAndroidWebView;
6180
}
6281

82+
/// <summary>
83+
/// Connects the handler to the Android <see cref="AWebView"/> and registers platform-specific
84+
/// back navigation handling so that the WebView can consume back presses before the page is popped.
85+
/// </summary>
86+
/// <param name="platformView">The native Android <see cref="AWebView"/> instance associated with this handler.</param>
87+
/// <remarks>
88+
/// This override calls the base implementation and then registers an <see cref="AndroidLifecycle.OnBackPressed"/>
89+
/// lifecycle event handler. The handler checks <see cref="AWebView.CanGoBack"/> and, when possible, navigates
90+
/// back within the WebView instead of allowing the back press (or predictive back gesture on Android 13+)
91+
/// to propagate and pop the containing page.
92+
/// <para>
93+
/// When multiple BlazorWebView instances exist, the handler includes focus and visibility checks to ensure
94+
/// only the currently visible and focused WebView handles the back navigation, preventing conflicts between instances.
95+
/// </para>
96+
/// Inheritors that override this method should call the base implementation to preserve this back navigation
97+
/// behavior unless they intentionally replace it.
98+
/// </remarks>
99+
protected override void ConnectHandler(AWebView platformView)
100+
{
101+
base.ConnectHandler(platformView);
102+
103+
// Register OnBackPressed lifecycle event handler to check WebView's back navigation
104+
// This ensures predictive back gesture (Android 13+) checks WebView.CanGoBack() before popping page
105+
var lifecycleService = TryGetLifecycleEventService();
106+
if (lifecycleService != null)
107+
{
108+
// Create a weak reference to avoid memory leaks
109+
var weakPlatformView = new WeakReference<AWebView>(platformView);
110+
111+
AndroidLifecycle.OnBackPressed handler = (activity) =>
112+
{
113+
// Check if WebView is still alive, attached to window, and has focus
114+
// This prevents non-visible or unfocused BlazorWebView instances from
115+
// incorrectly intercepting back navigation when multiple instances exist
116+
if (weakPlatformView.TryGetTarget(out var webView) &&
117+
webView.IsAttachedToWindow &&
118+
webView.HasWindowFocus &&
119+
webView.CanGoBack())
120+
{
121+
webView.GoBack();
122+
return true; // Prevent back propagation - handled by WebView
123+
}
124+
125+
return false; // Allow back propagation - let page be popped
126+
};
127+
128+
// Register with lifecycle service - will be invoked by HandleBackNavigation in MauiAppCompatActivity
129+
lifecycleService.AddEvent(nameof(AndroidLifecycle.OnBackPressed), handler);
130+
_onBackPressedHandler = handler;
131+
}
132+
133+
if (OperatingSystem.IsAndroidVersionAtLeast(33) && _predictiveBackCallback is null)
134+
{
135+
if (Microsoft.Maui.ApplicationModel.Platform.CurrentActivity is not null)
136+
{
137+
_predictiveBackCallback = new BlazorWebViewPredictiveBackCallback(this);
138+
Microsoft.Maui.ApplicationModel.Platform.CurrentActivity?.OnBackInvokedDispatcher?.RegisterOnBackInvokedCallback(0, _predictiveBackCallback);
139+
}
140+
}
141+
}
142+
63143
private const string AndroidFireAndForgetAsyncSwitch = "BlazorWebView.AndroidFireAndForgetAsync";
64144

65145
protected override void DisconnectHandler(AWebView platformView)
66146
{
147+
if (OperatingSystem.IsAndroidVersionAtLeast(33) && _predictiveBackCallback is not null)
148+
{
149+
Microsoft.Maui.ApplicationModel.Platform.CurrentActivity?.OnBackInvokedDispatcher?.UnregisterOnBackInvokedCallback(_predictiveBackCallback);
150+
_predictiveBackCallback.Dispose();
151+
_predictiveBackCallback = null;
152+
}
153+
154+
// Clean up lifecycle event handler to prevent memory leaks
155+
if (_onBackPressedHandler != null)
156+
{
157+
var lifecycleService = TryGetLifecycleEventService();
158+
if (lifecycleService != null)
159+
{
160+
lifecycleService.RemoveEvent(nameof(AndroidLifecycle.OnBackPressed), _onBackPressedHandler);
161+
_onBackPressedHandler = null;
162+
}
163+
}
164+
67165
platformView.StopLoading();
68166

69167
if (_webviewManager != null)
@@ -182,5 +280,43 @@ public virtual async Task<bool> TryDispatchAsync(Action<IServiceProvider> workIt
182280

183281
return await _webviewManager.TryDispatchAsync(workItem);
184282
}
283+
284+
sealed class BlazorWebViewPredictiveBackCallback : Java.Lang.Object, IOnBackInvokedCallback
285+
{
286+
WeakReference<BlazorWebViewHandler> _weakBlazorWebViewHandler;
287+
288+
public BlazorWebViewPredictiveBackCallback(BlazorWebViewHandler handler)
289+
{
290+
_weakBlazorWebViewHandler = new WeakReference<BlazorWebViewHandler>(handler);
291+
}
292+
293+
public void OnBackInvoked()
294+
{
295+
// KeyDown for Back button is handled in BlazorAndroidWebView.
296+
// Here we just need to check if it was handled there.
297+
// If not, we propagate the back press to the Activity's OnBackPressedDispatcher.
298+
if (_weakBlazorWebViewHandler is not null && _weakBlazorWebViewHandler.TryGetTarget(out var handler))
299+
{
300+
var webView = handler.PlatformView as BlazorAndroidWebView;
301+
if (webView is not null)
302+
{
303+
var wasBackNavigationHandled = webView.BackNavigationHandled;
304+
// reset immediately for next back event
305+
webView.BackNavigationHandled = false;
306+
307+
if (!wasBackNavigationHandled)
308+
{
309+
if (webView.CanGoBack()) // If we can go back in WeView, Navigate back
310+
{
311+
webView.GoBack();
312+
return;
313+
}
314+
// Otherwise propagate back press to Activity
315+
(Microsoft.Maui.ApplicationModel.Platform.CurrentActivity as AndroidX.AppCompat.App.AppCompatActivity)?.OnBackPressedDispatcher?.OnBackPressed();
316+
}
317+
}
318+
}
319+
}
320+
}
185321
}
186322
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
override Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.ConnectHandler(Android.Webkit.WebView! platformView) -> void
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using Microsoft.AspNetCore.Components.WebView.Maui;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Maui.LifecycleEvents;
6+
using Xunit;
7+
8+
namespace Microsoft.Maui.MauiBlazorWebView.DeviceTests.Elements;
9+
10+
public partial class BlazorWebViewTests
11+
{
12+
#if ANDROID
13+
/// <summary>
14+
/// Verifies that BlazorWebViewHandler registers an OnBackPressed lifecycle event handler
15+
/// when connected on Android. This handler is essential for proper back navigation within
16+
/// the BlazorWebView on Android 13+ with predictive back gestures.
17+
/// See: https://github.com/dotnet/maui/issues/32767
18+
/// </summary>
19+
[Fact]
20+
public async Task BlazorWebViewRegistersOnBackPressedHandler()
21+
{
22+
EnsureHandlerCreated(additionalCreationActions: appBuilder =>
23+
{
24+
appBuilder.Services.AddMauiBlazorWebView();
25+
});
26+
27+
var bwv = new BlazorWebViewWithCustomFiles
28+
{
29+
HostPage = "wwwroot/index.html",
30+
CustomFiles = new Dictionary<string, string>
31+
{
32+
{ "index.html", TestStaticFilesContents.DefaultMauiIndexHtmlContent },
33+
},
34+
};
35+
bwv.RootComponents.Add(new RootComponent { ComponentType = typeof(MauiBlazorWebView.DeviceTests.Components.NoOpComponent), Selector = "#app", });
36+
37+
await InvokeOnMainThreadAsync(async () =>
38+
{
39+
var bwvHandler = CreateHandler<BlazorWebViewHandler>(bwv);
40+
var platformWebView = bwvHandler.PlatformView;
41+
await WebViewHelpers.WaitForWebViewReady(platformWebView);
42+
43+
// Get the lifecycle event service and verify OnBackPressed handler is registered
44+
var lifecycleService = MauiContext.Services.GetService<ILifecycleEventService>() as LifecycleEventService;
45+
Assert.NotNull(lifecycleService);
46+
47+
// Verify the OnBackPressed event has been registered
48+
Assert.True(lifecycleService.ContainsEvent(nameof(AndroidLifecycle.OnBackPressed)),
49+
"BlazorWebViewHandler should register an OnBackPressed lifecycle event handler on Android");
50+
});
51+
}
52+
53+
/// <summary>
54+
/// Verifies that BlazorWebViewHandler properly cleans up the OnBackPressed lifecycle event handler
55+
/// when disconnected. This prevents memory leaks and ensures proper cleanup.
56+
/// See: https://github.com/dotnet/maui/issues/32767
57+
/// </summary>
58+
[Fact]
59+
public async Task BlazorWebViewCleansUpOnBackPressedHandlerOnDisconnect()
60+
{
61+
EnsureHandlerCreated(additionalCreationActions: appBuilder =>
62+
{
63+
appBuilder.Services.AddMauiBlazorWebView();
64+
});
65+
66+
var bwv = new BlazorWebViewWithCustomFiles
67+
{
68+
HostPage = "wwwroot/index.html",
69+
CustomFiles = new Dictionary<string, string>
70+
{
71+
{ "index.html", TestStaticFilesContents.DefaultMauiIndexHtmlContent },
72+
},
73+
};
74+
bwv.RootComponents.Add(new RootComponent { ComponentType = typeof(MauiBlazorWebView.DeviceTests.Components.NoOpComponent), Selector = "#app", });
75+
76+
await InvokeOnMainThreadAsync(async () =>
77+
{
78+
var bwvHandler = CreateHandler<BlazorWebViewHandler>(bwv);
79+
var platformWebView = bwvHandler.PlatformView;
80+
await WebViewHelpers.WaitForWebViewReady(platformWebView);
81+
82+
var lifecycleService = MauiContext.Services.GetService<ILifecycleEventService>() as LifecycleEventService;
83+
Assert.NotNull(lifecycleService);
84+
85+
// Verify handler is registered after connect
86+
Assert.True(lifecycleService.ContainsEvent(nameof(AndroidLifecycle.OnBackPressed)),
87+
"OnBackPressed handler should be registered after ConnectHandler");
88+
89+
// Count the handlers before disconnect
90+
var handlersBefore = lifecycleService.GetEventDelegates<AndroidLifecycle.OnBackPressed>(nameof(AndroidLifecycle.OnBackPressed));
91+
int countBefore = 0;
92+
foreach (var _ in handlersBefore)
93+
countBefore++;
94+
95+
// Disconnect the handler by setting the BlazorWebView's Handler to null
96+
// This triggers DisconnectHandler internally
97+
bwv.Handler = null;
98+
99+
// Count the handlers after disconnect
100+
var handlersAfter = lifecycleService.GetEventDelegates<AndroidLifecycle.OnBackPressed>(nameof(AndroidLifecycle.OnBackPressed));
101+
int countAfter = 0;
102+
foreach (var _ in handlersAfter)
103+
countAfter++;
104+
105+
// Verify the handler count decreased (cleanup happened)
106+
Assert.True(countAfter < countBefore,
107+
$"OnBackPressed handler should be removed after DisconnectHandler. Before: {countBefore}, After: {countAfter}");
108+
});
109+
}
110+
#endif
111+
}

src/Core/src/LifecycleEvents/LifecycleEventService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,15 @@ public IEnumerable<TDelegate> GetEventDelegates<TDelegate>(string eventName)
3939

4040
public bool ContainsEvent(string eventName) =>
4141
_mapper.TryGetValue(eventName, out var delegates) && delegates?.Count > 0;
42+
43+
internal void RemoveEvent<TDelegate>(string eventName, TDelegate action)
44+
where TDelegate : Delegate
45+
{
46+
if (_mapper.TryGetValue(eventName, out var delegates) && delegates != null)
47+
{
48+
if (delegates.Remove(action) && delegates.Count == 0)
49+
_mapper.Remove(eventName);
50+
}
51+
}
4252
}
4353
}

0 commit comments

Comments
 (0)