A .NET MAUI class library providing an enhanced WebView control with JavaScript interop, navigation events, cookie management, and URL change detection for Android and iOS.
- Execute JavaScript from C# via
EvaluateJavaScriptAsync - Receive JavaScript calls in C# via
invokeCSharpAction(data)bridge - Navigation events:
Navigating,Navigated,WebViewNavigated - URL change detection for SPAs and PWAs via
UrlChanged(addresses dotnet/maui#19232) - Cookie management:
SetCookieAsync,UpdateCookieAsync,RemoveCookieAsync,RemoveAllCookiesAsync - Native back navigation:
CanGoBack,GoBack(),HistoryPosition - Multi-window / popup support on Android and iOS
- Force reload via
Refresh()
| Platform | Minimum OS Version |
|---|---|
| Android | 21.0 |
| iOS | 12.2 |
Target Frameworks: net10.0-android, net10.0-ios (MAUI 10.0.41)
dotnet add package Com.Bnotech.ExtendedWebViewRegister the handler in your MauiProgram.cs:
using Com.Bnotech.ExtendedWebView;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseExtendedWebView(); // <-- Add this
return builder.Build();
}
}<!-- In your XAML page -->
<controls:ExtWebView x:Name="webView"
Source="https://example.com" />using Com.Bnotech.ExtendedWebView;Or set the source in code:
webView.Source = new UrlWebViewSource { Url = "https://example.com" };var request = new EvaluateJavaScriptAsyncRequest("document.title");
await webView.EvaluateJavaScriptAsync(request);The control automatically injects a invokeCSharpAction(data) function into every page. Subscribe to the event in C#:
webView.JavaScriptAction += (sender, e) =>
{
string payload = e.Payload;
// Handle the data sent from JavaScript
};Then call it from your web page:
invokeCSharpAction("Hello from JavaScript!");webView.Navigating += (sender, e) =>
{
// Fires before navigation starts
};
webView.Navigated += (sender, e) =>
{
// Fires after navigation completes
};Detects route changes in single-page applications that don't trigger traditional navigation events:
webView.UrlChanged += (sender, e) =>
{
string newUrl = e.Url;
};// Set a cookie
await webView.SetCookieAsync(
name: "session",
value: "abc123",
domain: ".example.com",
path: "/",
expires: DateTimeOffset.UtcNow.AddDays(7),
isSecure: true,
isHttpOnly: true);
// Update a cookie (overwrites by name/domain/path)
await webView.UpdateCookieAsync(
name: "session",
value: "newvalue",
domain: ".example.com",
path: "/");
// Remove a specific cookie
await webView.RemoveCookieAsync("session", ".example.com", "/");
// Remove all cookies
await webView.RemoveAllCookiesAsync();On iOS, SetCookieAsync (and therefore UpdateCookieAsync) awaits the
WKHttpCookieStore set-cookie completion callback. RemoveAllCookiesAsync awaits
cookie enumeration and every native deletion callback, including the empty-store
case. Both methods also finish their corresponding NSHttpCookieStorage.SharedStorage
updates before returning. No delays are used.
Call these methods after the native handler is connected; a missing handler or UI synchronization context produces an exception rather than a successful no-op. Do not start login/navigation until initialization has completed, and do not run cookie initialization concurrently with navigation:
await webView.RemoveAllCookiesAsync();
await webView.SetCookieAsync("session", sessionToken, ".example.com", "/",
isSecure: true, isHttpOnly: true);
webView.Source = new UrlWebViewSource { Url = loginUrl };The iOS data store is shared: removing all cookies also affects other WebViews using
the default store. The new completion guarantee applies to the two methods above
on iOS, not to RemoveCookieAsync or Android cookie operations.
User-initiated window.open(...) and target="_blank" requests open a separate
native WebView: a dialog on Android and an overlay with an X button on iOS.
For example, a checkout's payment button may open a bank's verification page this
way, or submit a POST form with target="_blank". Ordinary links and redirects
without a new-window target continue to use the existing window. Not every payment
provider opens a popup; some use redirects, iframes, or a separate app instead.
HTTP(S) links and redirects inside a popup stay in the child WebView, including
navigation to another domain where platform policy permits it. Opening and closing
the popup does not replace or reload the checkout. On iOS, WebKit receives the child
created with its supplied configuration and loads the original request itself,
preserving POST bodies. Standard browser opener rules still apply: noopener,
noreferrer, and browser defaults for target="_blank" can make window.opener
unavailable. The library does not override these protections.
Close a popup with X or the page's window.close(); Android also supports dialog
dismissal with the system back button. Nested popups are supported. Closing a parent
popup closes its children, and disconnecting the handler releases all open popups.
Cookies use the native shared store, subject to platform cookie policies. Popups
receive no injected invokeCSharpAction function or native C# message handler on
either platform. The main control's JavaScript API, history properties, and
navigation/URL events continue to refer only to the main WebView. Normal web
communication between windows remains subject to browser origin and opener rules.
Closing a popup is not a payment-success signal. The checkout and payment provider must determine the outcome through their normal verified flow; the library does not mark a payment successful or automatically reload the checkout.
Native popup-blocking defaults remain in place: scripts opening windows without a user gesture are not automatically allowed. iOS popups use a fresh content controller without the app bridge and do not enforce the main WebView's app-bound-domain restriction, allowing external payment-provider domains. The main WebView's restriction remains unchanged; any return URL loaded in the main window must still satisfy its policy. TLS, transport security, and other platform restrictions remain in effect. iOS displays the current host and main-document navigation failures in the popup. This support does not add handling for external-app URL schemes.
Device acceptance is still required for each payment provider on Android and iOS:
| Scenario | Expected result |
|---|---|
Tap a target="_blank" link or a button calling window.open |
A visible child opens; the checkout remains unchanged underneath |
Open a POST form with target="_blank" |
The child receives the submitted data |
| Follow a cross-domain HTTP(S) link and redirect in the child | Content remains in the child, subject to platform domain policy |
| Open an external payment domain on iOS with app-bound domains configured for checkout | The popup can navigate externally; the main window's policy is unchanged |
| Navigate to an unreachable URL or a site with an invalid certificate | The failure is visible; certificate errors are not bypassed |
Check invokeCSharpAction and the native bridge in the popup |
No library bridge is installed |
Close via X or window.close() (also Android system back) |
The child disappears and the original checkout state is retained |
| Open nested or successive popups | Each has working close handling; closing a parent releases its children |
| Leave the host page while a popup is open | No popup or dialog remains after handler disconnection |
| Close or cancel verification without completing payment | The library does not report payment success |
These are acceptance criteria, not a claim of device/provider verification.
Read CanGoBack and HistoryPosition on the UI thread. On iOS they query
WKWebView.CanGoBack and WKWebView.BackForwardList directly, with no JavaScript
and no cached state. HistoryPosition is the number of entries in BackList:
the first entry is position 0, back navigation reduces it, forward navigation
increases it, and reload does not add an entry. It returns -1 if no native entry
is available (including before handler connection). Android uses its native
back/forward list index.
For completed document navigation, read the properties in Navigated with
WebNavigationResult.Success; the native history is already updated then.
GoBack() requests navigation asynchronously; do not read the destination position
immediately after calling it. It does nothing when the connected native WebView
cannot go back and throws if no handler is connected. URL observation can precede
a completed history update: UrlChanged alone is not a document-navigation
completion signal, and same-document/SPA history changes do not necessarily raise
Navigated.
An application can record HistoryPosition once, after it recognizes the
completed checkout entry navigation (after login and its redirects). For its back
button, it can use the following pattern:
// Application-owned state; set after the checkout entry has finished loading.
int? checkoutEntryPosition = null;
bool TryGoBackWithinCheckout()
{
if (checkoutEntryPosition is not int entry ||
webView.HistoryPosition <= entry || !webView.CanGoBack)
return false;
webView.GoBack();
return true;
}When this returns false, the application decides whether to close its MAUI page.
The library never closes pages or identifies checkout URLs. Serialize back requests
until navigation completes so repeated button presses cannot cross the boundary.
The position is a relative index, not a durable entry ID: if WebKit prunes history,
the application replaces the native WebView, or the site replaces history entries,
the application must reassess its boundary.
The application can enable WebKit's native back/forward swipe gesture through the
connected handler. Do not leave it enabled at the recorded entry: otherwise the
gesture can navigate to the preceding login page, bypassing the application back
button. For example, refresh this setting when recording the entry, after each
successful Navigated event, and after HandlerChanged:
void UpdateNativeNavigationGestures()
{
#if IOS
if (webView.Handler?.PlatformView is WebKit.WKWebView nativeWebView)
{
nativeWebView.AllowsBackForwardNavigationGestures =
checkoutEntryPosition is int entry &&
webView.HistoryPosition > entry &&
webView.CanGoBack;
}
#endif
}AllowsBackForwardNavigationGestures enables/disables both directions. At the
entry it must be disabled; a disabled WebKit gesture does not close the MAUI page.
Page closing (including any application-level back gesture) stays with the app.
For SPA changes, refresh the setting after the native history change has settled
as well. This toggle is not a native per-entry navigation policy: applications
requiring an unconditional boundary during overlapping/interactive navigations
should leave WebKit gestures disabled and route their own back action through the
boundary check.
For a genuinely new checkout, initialize the page/session anew and record a new
entry. A checkout page that is only temporarily covered should retain its WebView,
session, and recorded boundary; do not clear cookies or reset the entry on every
OnAppearing.
Force a reload of the current source:
webView.Refresh();| Property | Type | Description |
|---|---|---|
Source |
WebViewSource |
The URL or HTML content to display. Default: about:blank |
CanGoBack |
bool |
Whether the native WebView can navigate back (read on the UI thread) |
HistoryPosition |
int |
Zero-based native history index, or -1 if unavailable (read on the UI thread) |
| Method | Description |
|---|---|
EvaluateJavaScriptAsync(request) |
Executes JavaScript in the WebView |
SetCookieAsync(...) |
Sets a cookie in the platform cookie store |
UpdateCookieAsync(...) |
Updates (overwrites) an existing cookie |
RemoveCookieAsync(name, domain, path) |
Removes a specific cookie |
RemoveAllCookiesAsync() |
Removes all cookies |
Refresh() |
Forces a reload of the current source |
GoBack() |
Requests native back navigation; observe Navigated for completion |
| Event | EventArgs | Description |
|---|---|---|
JavaScriptAction |
JavaScriptActionEventArgs |
JS called invokeCSharpAction(data) |
Navigating |
WebNavigatingEventArgs |
Navigation is about to start |
Navigated |
WebNavigatedEventArgs |
Navigation completed |
UrlChanged |
UrlChangedEventArgs |
Displayed URL changed (SPA/PWA) |
SourceChanged |
SourceChangedEventArgs |
Source property was changed |
See LICENSE.