13. Nov 2023
Lesedauer 14 Min.
Die Brücken von MAUI
C#-JavaScript-InterOp
Native und Webtechnologien werden gern kombiniert: Das geht auch mit .NET MAUI.

Bei der Entwicklung von mobilen Anwendungen wird oft der hybride Ansatz gewählt, der sowohl native als auch Webtechnologien verwendet. Hybride Apps setzen die Nutzeroberfläche mit Technologien wie HTML, CSS und JavaScript um und stellen im nativen Bereich der Anwendung Funktionen bereit, die als Webtechnologien entweder nicht zur Verfügung stehen oder nur mit großem Aufwand zu bewältigen sind.Mit Xamarin und nun auch mit .NET MAUI lässt sich der JavaScript-Anteil zugunsten von C# stark verringern. Der hybride Ansatz eignet sich also auch dafür, Webtechnologien für Nutzeroberflächen und C# für die UI-Logik einzusetzen.Hybride Apps verwenden ein Web-View-Control, das eine Art eingebettetes Browserfenster innerhalb einer nativen App ist. Die Web-App kann dabei entweder über einen URL von einem Server geladen werden oder als eingebettete Ressource in der nativen App vorhanden sein.Je nach gewählter Architektur treibt entweder der JavaScript-Code der Web-App die Anwendung im Web-View-Control voran und kommuniziert bei Bedarf mit dem nativen Teil der App – beispielsweise um einen Barcode zu scannen oder eine Signatur zu erfassen –, oder der C#-Code in der nativen Umgebung führt die App und das Web-View-Control dient lediglich der Darstellung der Oberfläche.Unabhängig davon, wie hoch der Anteil von JavaScript oder C# ist, ist eine Kommunikation zwischen diesen Technologien in beide Richtungen erforderlich. Die nativen Web-View-Controls bei Android, iOS und Windows haben eine Vielzahl von Einstellungsmöglichkeiten, die mehrheitlich früher oder später benötigt werden, und sie stellen vor allem Möglichkeiten bereit, um aus C# heraus JavaScript aufzurufen und umgekehrt.In Xamarin wurde die Interoperabilität zwischen C# und JavaScript durch direkte Nutzung nativer, plattformspezifischer Steuerelemente wie Android.Webkit.WebView, WKWebView und WebView2 erreicht. Bei .NET MAUI liegt der Fokus jedoch auf einer höheren Abstraktionsebene. Die plattformspezifischen Controls werden in .NET MAUI gekapselt und als XAML-Controls bereitgestellt.Die Abstraktionsebene in .NET MAUI bietet Entwicklern den Vorteil, dass sie sich weniger um die plattformspezifischen Details kümmern müssen und stattdessen auf ein einheitliches API zugreifen können. Das erleichtert die Entwicklung und Wartung der Anwendung für verschiedene Plattformen.Allerdings bringt diese höhere Abstraktionsebene auch Einschränkungen mit sich. Das Web-View-Control von .NET MAUI unterstützt das Aufrufen von JavaScript-Code, aber es gibt keine nativen Mechanismen, um aus JavaScript heraus direkt auf C# zuzugreifen. Das bedeutet, dass sich bestimmte Szenarien, die einen bidirektionalen Austausch von Daten oder Funktionen zwischen JavaScript und C# erfordern, mit den nativen Funktionen des Web-View-Control von .NET MAUI allein nicht umsetzen lassen.Es gibt jedoch eine Lösung, um auch unter .NET MAUI die nativen Steuerelemente zu verwenden und somit die oben genannten Szenarien zu verwirklichen. Das folgende Beispiel zeigt, wie aus einer Web-App heraus eine im nativen Teil der App umgesetzte Signaturerfassung aufgerufen und die erfasste Signatur als SVG an die Webanwendung zurückgegeben wird.
beeinflussen.Vorausgesetzt, dass die Bridge im nativen Teil der App das folgende Interface implementiert, lässt sich der Web-App-Teil der kleinen Beispielanwendung bereits vollständig umsetzen, siehe Listing 1:
Brückentechnologie
Das Bridge-Muster ist ein strukturelles Designmuster, das dazu dient, die Abstraktionsschicht von ihrer Implementierung zu trennen [1]. Es ermöglicht, dass beide unabhängig voneinander verändert werden können, ohne dass sich Änderungen in einer Komponente auf die andere auswirken.Das Muster basiert auf dem Konzept einer Brücke, die eine Verbindung zwischen der Abstraktion und der Implementierung herstellt. Die Abstraktion definiert die Schnittstelle, mit der der Client interagiert, während die Implementierung die konkrete Funktionalität bereitstellt. Im Kontext der Web-View-Bridge bedeutet dies, dass die Brücke als Vermittler zwischen der Web-App und dem nativen Teil der Anwendung fungiert. Sie ermöglicht die Kommunikation und den Datenaustausch zwischen JavaScript und C#.Die Bridge wird in die laufende Anwendung im Web-View-Element injiziert und stellt eine Schnittstelle bereit, über welche die Webanwendung auf native Funktionalitäten zugreifen kann. Sie bietet Methoden, die von der Web-App aufgerufen werden können, um Aktionen im nativen Teil auszuführen.Das Entwurfsmuster fördert eine klare Trennung der Verantwortlichkeiten zwischen der Abstraktion (Web-App) und der Implementierung (nativer Teil der App). Dadurch werden die Flexibilität und die Erweiterbarkeit des Systems verbessert, da Änderungen in einer Komponente isoliert bleiben.Im Fall der Web-View-Bridge ermöglicht das Muster, dass die Web-App die Initiative ergreifen und den nativen Teil der App aufrufen kann, um beispielsweise die Signaturerfassung zu öffnen. Die Bridge ermöglicht dabei den reibungslosen Datenaustausch zwischen den beiden Technologien. Das Muster ermöglicht eine flexible und erweiterbare Architektur, bei der Änderungen in einer Komponente die andere Komponente nichtbeeinflussen.Vorausgesetzt, dass die Bridge im nativen Teil der App das folgende Interface implementiert, lässt sich der Web-App-Teil der kleinen Beispielanwendung bereits vollständig umsetzen, siehe Listing 1:
Listing 1: Der Inhalt der Datei Index.html
<span class="hljs-meta">&lt;!DOCTYPE html&gt;</span> <br/><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span> <br/><span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Signature capture<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span></span><br/><span class="hljs-tag"> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span> /&gt;</span> <br/><span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span> <br/><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Signature capture<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span> <br/> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Click the button to view a message.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span> <br/> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"showMessage()"</span>&gt;</span>Show message<br/> <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span> <br/> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Click the button to capture a signature.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span> <br/> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"captureSignature()"</span>&gt;</span><br/> Capture signature<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span> <br/> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"signature"</span>&gt;</span> <br/> <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> <br/> <br/> <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript"> </span><br/><span class="javascript"> </span><br/><span class="javascript"> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">showMessage</span>(<span class="hljs-params"></span>) </span>{ </span><br/><span class="javascript"> <span class="hljs-built_in">window</span>.webViewBridge?.alert(<span class="hljs-string">"Hello world!"</span>); </span><br/><span class="javascript"> } </span><br/><span class="javascript"> </span><br/><span class="javascript"> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">captureSignature</span>(<span class="hljs-params"></span>) </span>{ </span><br/><span class="javascript"> <span class="hljs-built_in">window</span>.webViewBridge?.captureSignature(</span><br/><span class="javascript"> <span class="hljs-built_in">JSON</span>.stringify(</span><br/><span class="javascript"> { <span class="hljs-attr">penWidth</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">penColor</span>: <span class="hljs-string">"#0000FF"</span> })); </span><br/><span class="javascript"> } </span><br/><span class="javascript"> </span><br/><span class="javascript"> <span class="hljs-built_in">window</span>.webViewBridgeTarget = { </span><br/><span class="javascript"> <span class="hljs-attr">provideSignature</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">result</span>) </span>{ </span><br/><span class="javascript"> <span class="hljs-keyword">if</span> (result.success) { </span><br/><span class="javascript"> <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"signature"</span>)</span><br/><span class="javascript"> .innerHTML = result.signature; </span><br/><span class="javascript"> } <span class="hljs-keyword">else</span> { </span><br/><span class="javascript"> <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"signature"</span>)</span><br/><span class="javascript"> .innerHTML = <span class="hljs-string">"Signature capture aborted"</span>; </span><br/><span class="javascript"> } </span><br/><span class="javascript"> } </span><br/><span class="javascript"> }; </span><br/><span class="javascript"> </span><br/><span class="javascript"> </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <br/><span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span> <br/><span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IWebViewBridge</span>
{
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">Alert</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> message</span>)</span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">CaptureSignature</span>(</span>
<span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">string</span> instructions</span>)</span>;
}
Der Web-App-Teil der Anwendung zeigt zwei Schaltflächen (Bild 1). Die erste Schaltfläche dient als Beispiel und ruft die Bridge-Methode alert() auf. Das ermöglichte während der Entwicklung eine Art Testumgebung, um die Bridge-Funktionalität zu überprüfen. Die zweite Schaltfläche ruft die Bridge-Methode captureSignature() auf und übergibt dabei einige Optionen als JSON-String. Diese Optionen definieren beispielsweise das Aussehen und Verhalten des Eingabefelds für die Signatur oder das Speichern der erfassten Signatur.

Die Oberfläche der Web-App mit den zwei Schaltflächen (Bild 1))
Autor
Durch den Aufruf dieser Bridge-Methoden können in der Web-App bestimmte Aktionen im nativen Teil der App ausgelöst werden, was zu einer nahtlosen Integration von Web-App und nativem Code führt. Dies eröffnet zahlreiche Möglichkeiten.Um es der nativen Seite zu ermöglichen, ihre Ergebnisse an die Web-App zu übergeben, wird im window-Objekt der Web-App das Objekt webViewBridgeTarget hinterlegt, das ebenfalls als eine Bridge fungiert. In dem Beispiel soll das Ergebnis der Signaturerfassung durch Aufruf der Methode provideSignature() bereitgestellt werden:
<span class="hljs-selector-tag">window</span><span class="hljs-selector-class">.webViewBridgeTarget</span>?<span class="hljs-selector-class">.provideSignature</span>(...));
Da sich JavaScript aus C# heraus problemlos aufrufen lässt, stellt dieser „Rückweg“ somit kein Problem dar. Es gilt nur sicherzustellen, dass das Skript korrekt aufgebaut wird.Die Bridge auf der nativen Seite ist allerdings eine Herausforderung, denn das vorhandene Web-View-Control in .NET MAUI kapselt die nativen APIs vollständig [2]. Es scheint, dass das Steuerelement hauptsächlich für einfache Szenarien gedacht ist und nicht für komplexe Interoperabilität zwischen JavaScript und C#.Bei genauerer Betrachtung wird jedoch klar, dass das Control selbst nicht allzu viel Logik enthält [3]. Die eigentliche Magie liegt in den sogenannten Handlern.
Das Handler-Konzept in .NET MAUI
Frameworks, die sich insbesondere im Bereich der Nutzeroberfläche auf Abstraktion konzentrieren, stellen häufig alternative Lösungswege für spezielle Fälle zur Verfügung. Xamarin Forms bot zu diesem Zweck das sogenannte Renderer-Konzept an, das auch unter .NET MAUI weiterhin eingesetzt werden kann. In .NET MAUI wurde aber auch ein neues Konzept für Erweiterungen entwickelt, die sogenannten Handler [4]. Diese sollen verschiedene Nachteile der Renderer im strukturellen Bereich sowie hinsichtlich des Laufzeitverhaltens beseitigen.Die Idee hinter dem Handler-Konzept besteht darin, plattformübergreifende Controls/Views, die als „virtual view“ bezeichnet werden, mithilfe einer Schnittstelle und über die Handler mit den passenden plattformspezifischen Views, den sogenannten „platform views“, zu verbinden (Bild 2).
Das konkrete Architekturkonzept der .NET-MAUI-Handler für die Beispielanwendung (Bild 3)
Autor
Es ist schön zu sehen, wie beim Design der Handler-Architektur ebenfalls das Bridge-Pattern verwendet wurde, siehe Listing 2. Besonders der Aufbau der Klasse ViewHandler lässt erahnen, wie sich die internen Abläufe der Handler gestalten.
Listing 2: Der abstrakte Code der Handler-Architektur
<span class="hljs-keyword">namespace</span> Microsoft.Maui.Controls <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> View : VisualElement, IView, ... <br/> { <br/> ... <br/> } <br/>} <br/> <br/><span class="hljs-keyword">namespace</span> Microsoft.Maui.Handlers <br/>{ <br/> <span class="hljs-keyword">public</span> abstract <span class="hljs-keyword">class</span> ViewHandler&lt;TVirtualView,<br/> TPlatformView&gt; : ViewHandler,<br/> IPlatformViewHandler, IViewHandler,<br/> IElementHandler <br/> where TVirtualView : <span class="hljs-keyword">class</span>, IView <br/> where TPlatformView : View <br/> { <br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-title">ViewHandler</span><span class="hljs-params">(IPropertyMapper mapper,</span></span><br/><span class="hljs-function"><span class="hljs-params"> CommandMapper? commandMapper = null)</span></span>; <br/> <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Func&lt;ViewHandler&lt;TVirtualView,<br/> TPlatformView&gt;, TPlatformView&gt;?<br/> PlatformViewFactory { get; <span class="hljs-built_in">set</span>; } <br/> <span class="hljs-keyword">public</span> Context Context { get; } <br/> <span class="hljs-keyword">public</span> TPlatformView PlatformView {<br/> get; <span class="hljs-keyword">private</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">set</span>; } <br/> <span class="hljs-keyword">public</span> TVirtualView VirtualView {<br/> get; <span class="hljs-keyword">private</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">set</span>; } <br/> <br/> ... <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">SetVirtualView</span><span class="hljs-params">(IView view)</span></span>; <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> sealed override <span class="hljs-keyword">void</span> <span class="hljs-title">SetVirtualView</span><span class="hljs-params">(</span></span><br/><span class="hljs-function"><span class="hljs-params"> IElement view)</span></span>; <br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ConnectHandler</span><span class="hljs-params">(</span></span><br/><span class="hljs-function"><span class="hljs-params"> TPlatformView platformView)</span></span>; <br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> abstract TPlatformView</span><br/><span class="hljs-function"> <span class="hljs-title">CreatePlatformView</span><span class="hljs-params">()</span></span>; <br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">virtual</span> <span class="hljs-keyword">void</span> <span class="hljs-title">DisconnectHandler</span><span class="hljs-params">(</span></span><br/><span class="hljs-function"><span class="hljs-params"> TPlatformView platformView)</span></span>; <br/> <br/> ... <br/> } <br/>}
Basierend auf diesem Konzept wird für das Beispielprogramm eine .NET-MAUI-Klassenbibliothek WebViewInterop mit einem neuen plattformübergreifenden Control BridgedWebView erstellt. Die konkrete Architektur sieht nun wie in Bild 3 dargestellt aus.

Das Architekturkonzept der .NET-MAUI-Handler (Bild 2)
Autor
Der Quellcodeausschnitt in Listing 3 veranschaulicht in verkürzter Form, wie innerhalb der Factory-Methode CreatePlatformView() die entsprechenden nativen Web-View-Controls erzeugt und zurückgegeben werden und wie die Web-App aus den Ressourcen der nativen App geladen wird. Des Weiteren ist zu sehen, dass die Handler eine Bridge erstellen, um die Interoperabilität zwischen JavaScript und C# zu ermöglichen. Diese Bridge wird hier noch detailliert betrachtet.
Listing 3: Der Quellcode der konkreten Handler-Architektur der Beispielanwendung (Teil 1)
// Plattformunabhängiger Code <br/> <br/>namespace WebViewInterop; <br/> <br/>public <span class="hljs-keyword">interface</span> IBridgedWebView : <span class="hljs-type">IView</span> <br/>{ <br/>} <br/> <br/>public class BridgedWebView : <span class="hljs-type">View</span>, IBridgedWebView <br/>{ <br/> <br/>} <br/> <br/>// Android-Handler <br/> <br/>using Android.Views; <br/>using Android.Webkit; <br/>using Microsoft.Maui.Handlers; <br/>using static Android.Views.ViewGroup; <br/> <br/>namespace WebViewInterop.Handlers; <br/> <br/>public class BridgedWebViewHandler : <span class="hljs-type">ViewHandler</span>&lt;IBridgedWebView, Android.Webkit.WebView&gt; <br/>{ <br/> ... <br/> <br/> <span class="hljs-keyword">private</span> Bridge _bridge; <br/> <br/> public BridgedWebViewHandler() :<br/> <span class="hljs-type">base</span>(BridgedWebViewMapper) <br/> { <br/> } <br/> <br/> <span class="hljs-keyword">protected</span> override Android.Webkit.WebView<br/> CreatePlatformView() <br/> { <br/> var webView = <span class="hljs-keyword">new</span> Android.Webkit.WebView(Context) <br/> { <br/> LayoutParameters = <span class="hljs-keyword">new</span> LayoutParams(<br/> LayoutParams.MatchParent,<br/> LayoutParams.MatchParent) <br/> }; <br/> _bridge = <span class="hljs-keyword">new</span> Bridge(); <br/> InitializeWebView(webView); <br/> <span class="hljs-keyword">return</span> webView; <br/> } <br/> <br/> public void InitializeWebView(<br/> Android.Webkit.WebView webView) <br/> { <br/> ... <br/> } <br/> <span class="hljs-keyword">protected</span> override void ConnectHandler(<br/> Android.Webkit.WebView platformView) <br/> { <br/> base.ConnectHandler(platformView); <br/> <br/> _bridge.Connect(platformView); <br/> <br/> platformView.LoadUrl(<br/> <span class="hljs-string">"file:///android_asset/WebApp/Index.html"</span>); <br/> } <br/> <br/> <span class="hljs-keyword">protected</span> override void DisconnectHandler(<br/> Android.Webkit.WebView platformView) <br/> { <br/> base.DisconnectHandler(platformView); <br/> _bridge.Disconnect(platformView); <br/> } <br/>} <br/> <br/>// iOS-Handler <br/> <br/>using CoreGraphics; <br/>using Foundation; <br/>using Microsoft.Maui.Handlers; <br/>using WebKit; <br/> <br/>namespace WebViewInterop.Handlers; <br/> <br/>public class BridgedWebViewHandler :<br/> <span class="hljs-type">ViewHandler</span>&lt;IBridgedWebView, WKWebView&gt; <br/>{ <br/> ... <br/> <br/> <span class="hljs-keyword">private</span> Bridge _bridge; <br/> <br/> public BridgedWebViewHandler() :<br/> <span class="hljs-type">base</span>(BridgedWebViewMapper) <br/> { } <br/> <br/> <span class="hljs-keyword">protected</span> override WKWebView CreatePlatformView() <br/> { <br/> var config = <span class="hljs-keyword">new</span> WKWebViewConfiguration(); <br/> config.SetValueForKey(NSObject.FromObject(<span class="hljs-literal">true</span>),<br/> <span class="hljs-keyword">new</span> NSString(<br/> <span class="hljs-string">"allowUniversalAccessFromFileURLs"</span>)); <br/> config.Preferences.SetValueForKey(<br/> NSNumber.FromBoolean(<span class="hljs-literal">true</span>),<br/> <span class="hljs-keyword">new</span> NSString(<span class="hljs-string">"allowFileAccessFromFileURLs"</span>)); <br/> var webView = <span class="hljs-keyword">new</span> WKWebView(CGRect.Empty, config); <br/> InitializeWebView(webView); <br/> _bridge = <span class="hljs-keyword">new</span> Bridge(); <br/> <span class="hljs-keyword">return</span> webView; <br/> } <br/>
Einen Gesamtüberblick über die Struktur der Beispielanwendung TheBridgesOfMaui liefert Bild 4. Das Projekt ist die eigentliche .NET-MAUI-Anwendung. Die Web-App ist als Raw-Asset in der nativen App enthalten. Die MainPage der App besteht im Wesentlichen aus dem plattformunabhängigen Control BridgedWebView im Projekt WebViewInterop. Im Platforms-Ordner dieses Projekts befinden sich die Implementierungen der plattformspezifischen Handler und Bridges.

Die Struktur der Beispielanwendung „TheBridgesOfMaui“ (Bild 4)
Autor
Wenn es zur Erfassung der Signatur kommt, wird die in der App enthaltene Page SignatureCapturePage angezeigt.Ein wichtiges Detail der Handler-Architektur ist die Registrierung der Handler. Dieser Schritt erfolgt in der Methode CreateMauiApp() in der Datei MauiProgram.cs, siehe Listing 4. Diese Registrierung stellt sicher, dass immer dann, wenn das Control BridgedWebView in der App verwendet wird, der angegebene native BridgedWebViewHandler für die Darstellung auf der jeweiligen Plattform genutzt wird.
Listing 3: Der Quellcode der konkreten Handler-Architektur der Beispielanwendung (Teil 2)
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">InitializeWebView</span>(<span class="hljs-params">WKWebView webView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> ... <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ConnectHandler</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> WKWebView platformView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">base</span>.ConnectHandler(platformView); <br/> _bridge.Connect(platformView); <br/> <br/> <span class="hljs-keyword">string</span> path = Path.Combine(<br/> NSBundle.MainBundle.BundlePath, <span class="hljs-string">"WebApp"</span>); <br/> <span class="hljs-keyword">var</span> uri = <span class="hljs-keyword">new</span> NSUrl(<span class="hljs-string">$"file://<span class="hljs-subst">{path}</span>/Index.html"</span>); <br/> <span class="hljs-keyword">var</span> webAppPath = <span class="hljs-keyword">new</span> NSUrl(<span class="hljs-string">$"file://<span class="hljs-subst">{path}</span>"</span>); <br/> platformView.LoadFileUrl(uri, webAppPath); <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">DisconnectHandler</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> WKWebView platformView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">base</span>.DisconnectHandler(platformView); <br/> _bridge.Disconnect(platformView); <br/> } <br/>} <br/> <br/><span class="hljs-comment">// Windows-Handler </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment">using Microsoft.Maui.Handlers; </span><br/><span class="hljs-comment">using Microsoft.UI.Xaml.Controls; </span><br/><span class="hljs-comment">using Microsoft.Web.WebView2.Core; </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment">namespace WebViewInterop.Handlers; </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment">public class BridgedWebViewHandler :</span><br/><span class="hljs-comment"> ViewHandler&lt;IBridgedWebView, WebView2&gt; </span><br/><span class="hljs-comment">{ </span><br/><span class="hljs-comment"> ... </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> private Bridge _bridge; </span><br/><span class="hljs-comment"> public BridgedWebViewHandler() :</span><br/><span class="hljs-comment"> base(BridgedWebViewMapper) </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> protected override WebView2 CreatePlatformView() </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> var webView = new WebView2(); </span><br/><span class="hljs-comment"> _bridge = new Bridge(); </span><br/><span class="hljs-comment"> InitializeWebView(webView); </span><br/><span class="hljs-comment"> return webView; </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> private void InitializeWebView(WebView2 webView) </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> protected override async void ConnectHandler(</span><br/><span class="hljs-comment"> WebView2 platformView) </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> base.ConnectHandler(platformView); </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> await _bridge.Connect(platformView); </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> platformView.CoreWebView2</span><br/><span class="hljs-comment"> .SetVirtualHostNameToFolderMapping("wwwroot",</span><br/><span class="hljs-comment"> "WebApp", CoreWebView2HostResourceAccessKind</span><br/><span class="hljs-comment"> .Allow); </span><br/><span class="hljs-comment"> platformView.CoreWebView2.Navigate(</span><br/><span class="hljs-comment"> "https://wwwroot/Index.html"); </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment"> </span><br/><span class="hljs-comment"> protected override void DisconnectHandler(</span><br/><span class="hljs-comment"> WebView2 platformView) </span><br/><span class="hljs-comment"> { </span><br/><span class="hljs-comment"> base.DisconnectHandler(platformView); </span><br/><span class="hljs-comment"> _bridge.Disconnect(platformView); </span><br/><span class="hljs-comment"> } </span><br/><span class="hljs-comment">}</span>
Die Umsetzung der Bridges
Weiter geht es mit der Implementierung der Web-View-Bridges. Dazu werden in einer partiellen Klasse mit dem Namen Bridge direkt auf der obersten Ebene des Projekts WebViewInterop der gemeinsame, plattformübergreifende Code und die Logik abgelegt. Für den jeweils plattformspezifischen Code werden weitere partielle Bridge-Klassen für jede Plattform angelegt (Bild 4).
Die Struktur der Beispielanwendung „TheBridgesOfMaui“ (Bild 4)
Autor
Das Injizieren der Bridges in die nativen Web-View-Steuerelemente kann nicht gleichzeitig mit deren Erstellung erfolgen. Stattdessen geschieht dies in .NET MAUI, wenn die Platform View mit der Virtual View verbunden wird. Zu diesem Zeitpunkt wird die Methode ConnectHandler() der Klasse ViewHandler aufgerufen. Die Methoden ConnectHandler() und DisconnectHandler() in ViewHandler werden also überschrieben und darin die entsprechenden Methoden Connect() und Disconnect() der Bridges aufgerufen, siehe Listing 3.
Listing 3: Der Quellcode der konkreten Handler-Architektur der Beispielanwendung (Teil 1)
// Plattformunabhängiger Code <br/> <br/>namespace WebViewInterop; <br/> <br/>public <span class="hljs-keyword">interface</span> IBridgedWebView : <span class="hljs-type">IView</span> <br/>{ <br/>} <br/> <br/>public class BridgedWebView : <span class="hljs-type">View</span>, IBridgedWebView <br/>{ <br/> <br/>} <br/> <br/>// Android-Handler <br/> <br/>using Android.Views; <br/>using Android.Webkit; <br/>using Microsoft.Maui.Handlers; <br/>using static Android.Views.ViewGroup; <br/> <br/>namespace WebViewInterop.Handlers; <br/> <br/>public class BridgedWebViewHandler : <span class="hljs-type">ViewHandler</span>&lt;IBridgedWebView, Android.Webkit.WebView&gt; <br/>{ <br/> ... <br/> <br/> <span class="hljs-keyword">private</span> Bridge _bridge; <br/> <br/> public BridgedWebViewHandler() :<br/> <span class="hljs-type">base</span>(BridgedWebViewMapper) <br/> { <br/> } <br/> <br/> <span class="hljs-keyword">protected</span> override Android.Webkit.WebView<br/> CreatePlatformView() <br/> { <br/> var webView = <span class="hljs-keyword">new</span> Android.Webkit.WebView(Context) <br/> { <br/> LayoutParameters = <span class="hljs-keyword">new</span> LayoutParams(<br/> LayoutParams.MatchParent,<br/> LayoutParams.MatchParent) <br/> }; <br/> _bridge = <span class="hljs-keyword">new</span> Bridge(); <br/> InitializeWebView(webView); <br/> <span class="hljs-keyword">return</span> webView; <br/> } <br/> <br/> public void InitializeWebView(<br/> Android.Webkit.WebView webView) <br/> { <br/> ... <br/> } <br/> <span class="hljs-keyword">protected</span> override void ConnectHandler(<br/> Android.Webkit.WebView platformView) <br/> { <br/> base.ConnectHandler(platformView); <br/> <br/> _bridge.Connect(platformView); <br/> <br/> platformView.LoadUrl(<br/> <span class="hljs-string">"file:///android_asset/WebApp/Index.html"</span>); <br/> } <br/> <br/> <span class="hljs-keyword">protected</span> override void DisconnectHandler(<br/> Android.Webkit.WebView platformView) <br/> { <br/> base.DisconnectHandler(platformView); <br/> _bridge.Disconnect(platformView); <br/> } <br/>} <br/> <br/>// iOS-Handler <br/> <br/>using CoreGraphics; <br/>using Foundation; <br/>using Microsoft.Maui.Handlers; <br/>using WebKit; <br/> <br/>namespace WebViewInterop.Handlers; <br/> <br/>public class BridgedWebViewHandler :<br/> <span class="hljs-type">ViewHandler</span>&lt;IBridgedWebView, WKWebView&gt; <br/>{ <br/> ... <br/> <br/> <span class="hljs-keyword">private</span> Bridge _bridge; <br/> <br/> public BridgedWebViewHandler() :<br/> <span class="hljs-type">base</span>(BridgedWebViewMapper) <br/> { } <br/> <br/> <span class="hljs-keyword">protected</span> override WKWebView CreatePlatformView() <br/> { <br/> var config = <span class="hljs-keyword">new</span> WKWebViewConfiguration(); <br/> config.SetValueForKey(NSObject.FromObject(<span class="hljs-literal">true</span>),<br/> <span class="hljs-keyword">new</span> NSString(<br/> <span class="hljs-string">"allowUniversalAccessFromFileURLs"</span>)); <br/> config.Preferences.SetValueForKey(<br/> NSNumber.FromBoolean(<span class="hljs-literal">true</span>),<br/> <span class="hljs-keyword">new</span> NSString(<span class="hljs-string">"allowFileAccessFromFileURLs"</span>)); <br/> var webView = <span class="hljs-keyword">new</span> WKWebView(CGRect.Empty, config); <br/> InitializeWebView(webView); <br/> _bridge = <span class="hljs-keyword">new</span> Bridge(); <br/> <span class="hljs-keyword">return</span> webView; <br/> } <br/>
Android-Bridge
Das Control Android.Webkit.WebView bietet von Natur aus eine Möglichkeit, Objekte in das Web-View-Control zu injizieren, was die Aufgabe einfach macht. Das zu injizierende Objekt muss von Java.Lang.Object abgeleitet sein. Die Methoden, die aus JavaScript heraus erreichbar sein müssen, müssen mit den Attributen [JavascriptInterface] und [Export] gekennzeichnet werden (Listing 5).Listing 4: Registrierung der Handler
<span class="hljs-keyword">using</span> Microsoft.Extensions.Logging; <br/><span class="hljs-keyword">using</span> WebViewInterop; <br/><span class="hljs-keyword">using</span> CommunityToolkit.Maui; <br/><span class="hljs-keyword">using</span> WebViewInterop.Handlers; <br/> <br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">TheBridgesOfMaui</span>; <br/> <br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MauiProgram</span> <br/>{ <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> MauiApp <span class="hljs-title">CreateMauiApp</span>(<span class="hljs-params"></span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">var</span> builder = MauiApp.CreateBuilder(); <br/> builder <br/> .UseMauiApp&lt;App&gt;() <br/> .UseMauiCommunityToolkit() <br/> .ConfigureFonts(fonts =&gt; <br/> { <br/> fonts.AddFont(<span class="hljs-string">"OpenSans-Regular.ttf"</span>,<br/> <span class="hljs-string">"OpenSansRegular"</span>); <br/> fonts.AddFont(<span class="hljs-string">"OpenSans-Semibold.ttf"</span>,<br/> <span class="hljs-string">"OpenSansSemibold"</span>); <br/> }) <br/> .ConfigureMauiHandlers(handlers =&gt; <br/> { <br/> handlers.AddHandler(<span class="hljs-keyword">typeof</span>(BridgedWebView),<br/> <span class="hljs-keyword">typeof</span>(BridgedWebViewHandler)); <br/> }); <br/> <br/> <span class="hljs-meta">#<span class="hljs-meta-keyword">if</span> DEBUG </span><br/><span class="hljs-meta"> builder.Logging.AddDebug(); </span><br/><span class="hljs-meta"> #<span class="hljs-meta-keyword">endif</span> </span><br/><span class="hljs-meta"> </span><br/><span class="hljs-meta"> return builder.Build(); </span><br/><span class="hljs-meta"> } </span><br/><span class="hljs-meta">}</span>
In der Connect()-Methode erledigt AddJavascriptInterface() die Injektion. Dabei werden die Bridge selbst und der Name angegeben, unter dem sie im JavaScript-window-Objekt zu erreichen ist. Hier lautet er webViewBridge.Die Methoden der nativen Web-View müssen aus dem UI-Thread heraus aufgerufen werden; aus diesem Grund werden die Aufrufe synchronisiert. Die exportierten Methoden Alert() und CaptureSignature() delegieren die Arbeit an die Implementierungen, die im gemeinsamen Teil der Bridge definiert sind.EvaluateJavascriptAsync() bietet einen generischen Ansatz, um die Ergebnisse an die Web-App zu übergeben.
iOS-Bridge
Das WKWebView-Control verfolgt einen anderen Ansatz, um aus JavaScript heraus auf C# zuzugreifen. Dabei wird ein sogenannter WebViewScriptMessageHandler installiert. Er stellt die Methode postMessage() bereit, der ein JavaScript-Objekt als Parameter übergeben werden kann:
windows.webkit.messageHanders.webViewBridgeHandler.
postMessage(<span class="hljs-meta">{...}</span>);
Um die Nachricht zu verarbeiten, wird von der abstrakten Klasse WebViewScriptMessageHandler geerbt und die Methode DidReceiveScriptMessage() überschrieben. Das JavaScript-Objekt, das beim Aufruf von postMessage() übergeben wurde, steht unter message.Body zur Verfügung. Das Casten in den Typ NSDictionary ermöglicht den Zugriff auf die einzelnen Eigenschaften und deren Werte, siehe Listing 6. Das Konzept erinnert an Reflection, da es ermöglicht, zur Laufzeit dynamisch auf die Eigenschaften des JavaScript-Objekts zugreifen zu können.
Listing 5: Der native Teil der Android-Bridge
<span class="hljs-keyword">using</span> Android.Webkit; <br/><span class="hljs-keyword">using</span> Java.Interop; <br/> <br/><span class="hljs-keyword">namespace</span> WebViewInterop; <br/> <br/><span class="hljs-keyword">public</span> partial <span class="hljs-keyword">class</span> <span class="hljs-built_in">Bridge</span> : Java.Lang.Object <br/>{ <br/> ... <br/> <br/> <span class="hljs-keyword">private</span> Android.Webkit.WebView _webView; <br/> <br/> <span class="hljs-keyword">private</span> Android.App.Activity Context <br/> { <br/> <span class="hljs-built_in">get</span> { <span class="hljs-built_in">return</span> Microsoft.Maui.ApplicationModel<br/> .Platform.CurrentActivity; } <br/> } <br/> <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Connect(Android.Webkit.WebView webView) <br/> { <br/> _webView = webView; <br/> Context.RunOnUiThread(() =&gt; <br/> { <br/> webView.AddJavascriptInterface(<span class="hljs-keyword">this</span>,<br/> BRIDGE_NAME); <br/> }); <br/> } <br/> <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Disconnect(<br/> Android.Webkit.WebView webView) <br/> { <br/> Context.RunOnUiThread(() =&gt; <br/> { <br/> webView.RemoveJavascriptInterface(BRIDGE_NAME); <br/> }); <br/> _webView = null; <br/> } <br/> <br/> [JavascriptInterface] <br/> [Export(<span class="hljs-string">"alert"</span>)] <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Alert(Java.Lang.<span class="hljs-keyword">String</span> message) <br/> { <br/> AlertImplementation(message.ToString()); <br/> } <br/> <br/> [JavascriptInterface] <br/> [Export(<span class="hljs-string">"captureSignature"</span>)] <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> CaptureSignature(<br/> Java.Lang.<span class="hljs-keyword">String</span> options) <br/> { <br/> CaptureSignatureImplementation(<br/> options.ToString()); <br/> } <br/> <br/> <span class="hljs-keyword">private</span> async <span class="hljs-built_in">Task</span>&lt;<span class="hljs-keyword">string</span>&gt; EvaluateJavascriptAsync(<br/> <span class="hljs-keyword">string</span> script) <br/> { <br/> var javascriptResult = <span class="hljs-keyword">new</span> StringCallback(); <br/> <br/> Context.RunOnUiThread(() =&gt; <br/> { <br/> _webView.EvaluateJavascript(script,<br/> javascriptResult); <br/> }); <br/> <br/> var result = await javascriptResult.<span class="hljs-built_in">Task</span>; <br/> <span class="hljs-built_in">return</span> result; <br/> } <br/>}
Um sicherzustellen, dass die Web-App nicht an die spezifischen Besonderheiten von iOS angepasst werden muss, gibt es einen kleinen Trick:Die eigentliche Bridge wird in ein Skript injiziert, das beim Laden der Web-App ausgeführt wird (siehe die Methode GetJavascriptForBridge()).Die Aufgabe der Bridge besteht darin, die fachlichen Methoden alert() und captureSignature() in eine spezifische Form für WKWebView zu übersetzen und die Methode postMessage() so aufzurufen, dass die Nachricht auf der C#-Seite in der Implementierung der Methode DidReceiveScriptMessage() verarbeitet werden kann. Dort wird die Nachricht ausgewertet und es werden die entsprechenden Implementierungen aufgerufen, die im gemeinsamen Teil der Bridge definiert sind.Für das Ausführung von JavaScript sorgt die Methode EvaluateJavascriptAsync(). Auf diese Weise lässt sich JavaScript-Code innerhalb des WKWebView-Objekts ausführen und Ergebnisse können an die Web-App übergeben werden.Die nachrichtenbasierte Kommunikation in der iOS-Bridge zeigt, dass man bei der Definition der Schnittstelle der Bridge für die Web-App nicht vollständig frei ist, sondern vielmehr die Plattformspezifika berücksichtigen muss. Hier sind einige grobe Anhaltspunkte, die beachtet werden sollten:
- Die Methoden sollten keine Werte zurückgeben, da die Kommunikation auf Ereignissen und Benachrichtigungen basiert – ohne direkte Rückgabe von Werten.
- Die Parameter sollten primitive Datentypen sein, um eine reibungslose Kommunikation zwischen JavaScript und dem nativen Code zu gewährleisten. Komplexe Objekte oder nutzerdefinierte Datentypen könnten in einfachere Datenstrukturen umgewandelt werden.
Windows-Bridge
Unter Windows dient das Control WebView2 dazu, die Web-App zu hosten. Um auf weitere Einstellungsmöglichkeiten und das Injektions-API zugreifen zu können, wird die CoreWebView2-Eigenschaft benötigt. Diese Eigenschaft ist null und muss durch folgenden Aufruf initialisiert werden:
await webView.EnsureCoreWebView2Async()<span class="hljs-comment">;</span>
Das Injektions-API bietet die Methode AddHostObjectToScript(), die wie folgt definiert ist:
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">AddHostObjectToScript</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> name,</span></span>
<span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">object</span> rawObject</span>)</span>;
In der Dokumentation der Methode [5] findet sich dazu ein Beispiel:
webView.CoreWebView2.AddHostObjectToScript(<span class="hljs-string">"bridge"</span>,
<span class="hljs-keyword">new</span> <span class="hljs-built_in">Bridge</span>());
Es scheint auf den ersten Blick sehr einfach zu sein, jedoch müssen die mit dem Parameter rawObject übergebenen Objekte gemäß den Anmerkungen in der Dokumentation für COM sichtbar sein und das Interface IDispatch implementieren. Das ist für normale .NET-Objekte nicht der Fall.Auch das Anwenden der entsprechenden Attribute wie in anderen Beispielen [6] führt zu einer Laufzeit-Ausnahme mit der Meldung „The group or resource is not in the correct state to perform the requested operation“, wie etwa hier:
[<span class="hljs-name">ClassInterface</span>(<span class="hljs-name">ClassInterfaceType.AutoDual</span>)]
[<span class="hljs-name">ComVisible</span>(<span class="hljs-name">true</span>)]
public class Bridge
{
...
}
Den Aufruf der Funktion AddHostObjectToScript() ist auch nicht im Quellcode von .NET MAUI zu finden, sodass es hier keine Vorlage gibt, an der man sich orientieren könnte.Hilfreiche Hinweise sind im GitHub-Forum von Microsoft Edge zu finden [7]. Dort wird auf ein Tool namens wv2winrt verwiesen, das eine WinRTAdapter-Bibliothek mit einem DispatchAdapter erstellen kann, der ein Objekt so ausstatten kann, dass es sich als Bridge verwenden lässt [8]. Folgt man der Anleitung, ergibt sich ein WinRTAdapter, der lediglich statische Klassen aus dem Windows-Namensraum anbietet.Das genügt aber nicht, denn hier soll ja die eigene Bridge injiziert werden. Die Bibliothek WebViewInterop ist nicht zu verwenden, da deren Projekttyp nicht mit WinRTAdapter kompatibel ist; hier bedarf es einer sogenannten „WinRT Component“. Also erstellen Sie zu diesem Zweck eine passende Klassenbibliothek nach Anleitung [9]. Diese Bibliothek soll nur das Interface IWebViewBridge enthalten. Anschließend wird dieses Projekt dem WinRTAdapter hinzugefügt.Eine Hürde gilt es noch zu überwinden, denn beim Übersetzen des WinRTAdapter beschwert sich der Compiler zunächst mit einer sehr nichtssagenden Fehlermeldung: „midl : error MIDL9008: internal compiler problem“. Diese Meldung verschwindet, sobald dem Projekt eine leere Klasse EmptyHelper hinzugefügt wird. Es scheint, dass die Compiler-Tools Probleme mit einem komplett leeren Projekt haben. Die leere Klasse kann in den Filtereinstellungen des Adapters hinzugefügt werden, um keinen unnötigen Overhead zu verursachen (Bild 5).

Die Einstellungen des WinRTAdapter (Bild 5)
Autor
Nach diesen Schritten lässt sich die Bridge endlich fertigstellen. Dazu ist WinRTAdapter als Referenz zum Projekt WebViewInterop hinzuzufügen (nur für Windows) und die Bridge muss das Interface IWebViewBridge implementieren. Die Methode Connect() wird um den Aufruf des Injektions-API ergänzt, siehe Listing 7.
Listing 6: Der native Teil der iOS-Bridge (Teil 1)
<span class="hljs-keyword">using</span> Foundation; <br/><span class="hljs-keyword">using</span> WebKit; <br/> <br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">WebViewInterop</span>; <br/> <br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Bridge</span> <br/>{ <br/> <span class="hljs-keyword">private</span> WKWebView _webView; <br/> <br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">class</span> <span class="hljs-title">WebViewBridgeMessageHandler</span> :<br/> <span class="hljs-title">WKScriptMessageHandler</span> <br/> { <br/> <span class="hljs-keyword">private</span> Bridge _bridge; <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">WebViewBridgeMessageHandler</span>(<span class="hljs-params">Bridge bridge</span>) </span><br/><span class="hljs-function"> </span>{ <br/> _bridge = bridge; <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">DidReceiveScriptMessage</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> WKUserContentController userContentController,</span></span><br/><span class="hljs-function"><span class="hljs-params"> WKScriptMessage message</span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">var</span> dict = message.Body <span class="hljs-keyword">as</span> NSDictionary; <br/> NSObject methodName; <br/> <span class="hljs-keyword">if</span> (dict.TryGetValue(<span class="hljs-keyword">new</span> NSString(<span class="hljs-string">"MethodName"</span>),<br/> <span class="hljs-keyword">out</span> methodName)) <br/> { <br/> <span class="hljs-keyword">switch</span> (methodName.ToString()) <br/> { <br/> <span class="hljs-keyword">case</span> <span class="hljs-string">"alert"</span>: <br/> { <br/> <span class="hljs-keyword">var</span> args = (dict[<span class="hljs-string">"MethodArguments"</span>]<br/> <span class="hljs-keyword">as</span> NSDictionary); <br/> _bridge.AlertImplementation(<br/> args[<span class="hljs-string">"message"</span>].ToString()); <br/> <span class="hljs-keyword">break</span>; <br/> } <br/> <span class="hljs-keyword">case</span> <span class="hljs-string">"captureSignature"</span>: <br/> { <br/> <span class="hljs-keyword">var</span> args = (dict[<span class="hljs-string">"MethodArguments"</span>]<br/> <span class="hljs-keyword">as</span> NSDictionary); <br/> _bridge.CaptureSignatureImplementation(<br/> args[<span class="hljs-string">"options"</span>].ToString()); <br/> <span class="hljs-keyword">break</span>; <br/> } <br/> } <br/> } <br/> } <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Connect</span>(<span class="hljs-params">WKWebView platformView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> _webView = platformView; <br/> <span class="hljs-keyword">var</span> script = <span class="hljs-keyword">new</span> WKUserScript(<span class="hljs-keyword">new</span> NSString(<br/> GetJavascriptForBridge()),<br/> WKUserScriptInjectionTime.AtDocumentEnd, <span class="hljs-literal">false</span>); <br/> platformView.Configuration.UserContentController<br/> .AddUserScript(script); <br/> platformView.Configuration.UserContentController<br/> .AddScriptMessageHandler(<br/> <span class="hljs-keyword">new</span> WebViewBridgeMessageHandler(<span class="hljs-keyword">this</span>),<br/> <span class="hljs-string">"webViewBridgeHandler"</span>); <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> <span class="hljs-title">GetJavascriptForBridge</span>(<span class="hljs-params"></span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">var</span> js = <span class="hljs-string">@"{ </span><br/><span class="hljs-string"> alert: function(message) { </span><br/><span class="hljs-string"> window.webkit.messageHandlers</span><br/><span class="hljs-string"> .webViewBridgeHandler.postMessage({</span><br/><span class="hljs-string"> 'MethodName': 'alert',</span><br/><span class="hljs-string"> 'MethodArguments': {'message': message} }); </span><br/><span class="hljs-string"> }, </span><br/><span class="hljs-string"> captureSignature: function(options) { </span><br/><span class="hljs-string"> window.webkit.messageHandlers</span><br/><span class="hljs-string"> .webViewBridgeHandler.postMessage({</span><br/><span class="hljs-string"> 'MethodName': 'captureSignature',</span><br/><span class="hljs-string"> 'MethodArguments': {'options': options} }); </span><br/><span class="hljs-string"> } </span><br/><span class="hljs-string"> }; </span><br/><span class="hljs-string"> "</span>; <br/> <span class="hljs-keyword">return</span> <span class="hljs-string">$"window.webViewBridge = <span class="hljs-subst">{js}</span>"</span>; <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Disconnect</span>(<span class="hljs-params">WKWebView platformView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> _webView = <span class="hljs-literal">null</span>; <br/> platformView.Configuration.UserContentController<br/> .RemoveAllUserScripts(); <br/> platformView.Configuration.UserContentController<br/> .RemoveScriptMessageHandler(<br/> <span class="hljs-string">"webViewBridgeHandler"</span>); <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">EvaluateJavascriptAsync</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">string</span> script</span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">var</span> strResult =<br/> <span class="hljs-keyword">await</span> _webView.EvaluateJavaScriptAsync(script); <br/> <span class="hljs-keyword">if</span> (strResult != <span class="hljs-literal">null</span>) <br/> { <br/> <span class="hljs-keyword">return</span> strResult.ToString(); <br/> } <br/> <span class="hljs-keyword">else</span> <br/> { <br/> <span class="hljs-keyword">return</span> <span class="hljs-keyword">string</span>.Empty; <br/> } <br/> } <br/>}
Der Code der WebView2-Bridge ist vergleichsweise klein, aber der Aufwand, um eine funktionierende Lösung zu erreichen, ist aufgrund der spärlichen und teilweise unvollständigen Dokumentation enorm.Jetzt ist es aber geschafft. Alle relevanten Plattformen verfügen über eine eigene Bridge Sie können sich endlich der eigentlichen Aufgabe widmen: dem Erfassen der Signatur.
Das Erfassen der Signatur
Um die gewünschten Funktionen der Web-View-Bridges umzusetzen, befassen Sie sich zunächst mit der Implementierung der Methoden AlertImplementation() und CaptureSignatureImplementation(). Diese befinden sich im allgemeinen Teil der Bridge, siehe Listing 8.Listing 7: Der native Teil der Windows-Bridge
<span class="hljs-keyword">using</span> Maui.Windows.Interfaces; <br/><span class="hljs-keyword">using</span> Microsoft.UI.Xaml.Controls; <br/> <br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">WebViewInterop</span>; <br/> <br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Bridge</span> : <span class="hljs-title">IWebViewBridge</span> <br/>{ <br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">string</span> _currentStartupId; <br/> <br/> <span class="hljs-keyword">private</span> WebView2 _webView; <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">Connect</span>(<span class="hljs-params">WebView2 webView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> _webView = webView; <br/> <span class="hljs-keyword">await</span> webView.EnsureCoreWebView2Async(); <br/> <br/> webView.CoreWebView2.Settings<br/> .AreDevToolsEnabled = <span class="hljs-literal">true</span>; <br/> webView.CoreWebView2.Settings<br/> .AreHostObjectsAllowed = <span class="hljs-literal">true</span>; <br/> webView.CoreWebView2.Settings<br/> .AreDefaultScriptDialogsEnabled = <span class="hljs-literal">true</span>; <br/> webView.CoreWebView2.Settings<br/> .IsScriptEnabled = <span class="hljs-literal">true</span>; <br/> webView.CoreWebView2.Settings<br/> .IsWebMessageEnabled = <span class="hljs-literal">true</span>; <br/> <br/> <span class="hljs-keyword">var</span> dispatchAdapter =<br/> <span class="hljs-keyword">new</span> WinRTAdapter.DispatchAdapter(); <br/> <br/> webView.CoreWebView2.AddHostObjectToScript(<br/> <span class="hljs-string">"bridge"</span>, dispatchAdapter.WrapObject(<span class="hljs-keyword">this</span>,<br/> dispatchAdapter)); <br/> _currentStartupId = <span class="hljs-keyword">await</span> webView.CoreWebView2<br/> .AddScriptToExecuteOnDocumentCreatedAsync(<br/> <span class="hljs-string">@"window.webViewBridge =</span><br/><span class="hljs-string"> chrome.webview.hostObjects.sync.bridge;"</span>); <br/> <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Disconnect</span>(<span class="hljs-params">WebView2 webView</span>) </span><br/><span class="hljs-function"> </span>{ <br/> _webView = <span class="hljs-literal">null</span>; <br/> webView.CoreWebView2<br/> .RemoveScriptToExecuteOnDocumentCreated(<br/> _currentStartupId); <br/> webView.CoreWebView2.RemoveHostObjectFromScript(<br/> <span class="hljs-string">"bridge"</span>); <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Alert</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> message</span>) </span><br/><span class="hljs-function"> </span>{ <br/> AlertImplementation(message); <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CaptureSignature</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> options</span>) </span><br/><span class="hljs-function"> </span>{ <br/> CaptureSignatureImplementation(options); <br/> } <br/> <br/> <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">EvaluateJavascriptAsync</span>(</span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">string</span> script</span>) </span><br/><span class="hljs-function"> </span>{ <br/> <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> _webView.CoreWebView2<br/> .ExecuteScriptAsync(script); <br/> <span class="hljs-keyword">return</span> result; <br/> } <br/>}
AlertImplementation() kann direkt an ihrem Ort implementiert werden und zeigt lediglich eine MessageBox an. CaptureSignatureImplementation()hingegen verwendet das Nachrichtensystem [10] aus dem Community MVVM Toolkit [11], um eine SignatureCaptureMessage zu versenden. Diese Nachricht enthält die übergebenen Optionen, die zuvor aus einem String extrahiert wurden.Um den Rückweg zu ermöglichen, ist die Registrierung für SignatureCaptureResultMessage nötig. Das Ergebnis wird der Web-App bereitgestellt, und zwar durch Aufruf der Methode provideSignature() des webViewBridgeTarget-Objekts, die von der Web-App bereitgestellt wird. Technisch gesehen erfolgt dies als Aufruf von JavaScript aus C# heraus.Diese Implementierung gewährleistet eine reibungslose Kommunikation zwischen der Web-View-Web-App und der nativen Anwendung. Die WebViewInterop-Bibliothek ist in der Lage, Nachrichten auszutauschen und Aktionen auf
beiden Seiten anzustoßen. Das ermöglicht es, auf Ereignisse in der Web-App zu reagieren und die Ergebnisse an sie zurückzugeben.Die Verwendung des Nachrichtensystems und der JavaScript-Aufrufe erlaubt eine saubere und entkoppelte Architektur. Die WebViewInterop-Bibliothek muss nicht über Details der Signaturerfassung oder anderer spezifischer Implementierungen Bescheid wissen. Dadurch bleibt sie flexibel und kann einfach an andere technische Anforderungen angepasst werden.Insgesamt bieten die implementierten Methoden der Web-View-Bridges eine solide Grundlage für die Interaktion zwischen der Web-View-Web-App und der nativen Anwendung. Sie eröffnen Möglichkeiten, um Nutzerinteraktionen zu behandeln, Daten auszutauschen und Webinhalte nahtlos in native Anwendungen einzubinden.Die konkrete Umsetzung der Signaturerfassung erfolgt durch die Registrierung der SignatureCaptureMessage in der App-Klasse. Sobald eine solche Nachricht empfangen wird, wird die SignatureCapturePage angezeigt, siehe Bild 6 links.
beiden Seiten anzustoßen. Das ermöglicht es, auf Ereignisse in der Web-App zu reagieren und die Ergebnisse an sie zurückzugeben.Die Verwendung des Nachrichtensystems und der JavaScript-Aufrufe erlaubt eine saubere und entkoppelte Architektur. Die WebViewInterop-Bibliothek muss nicht über Details der Signaturerfassung oder anderer spezifischer Implementierungen Bescheid wissen. Dadurch bleibt sie flexibel und kann einfach an andere technische Anforderungen angepasst werden.Insgesamt bieten die implementierten Methoden der Web-View-Bridges eine solide Grundlage für die Interaktion zwischen der Web-View-Web-App und der nativen Anwendung. Sie eröffnen Möglichkeiten, um Nutzerinteraktionen zu behandeln, Daten auszutauschen und Webinhalte nahtlos in native Anwendungen einzubinden.Die konkrete Umsetzung der Signaturerfassung erfolgt durch die Registrierung der SignatureCaptureMessage in der App-Klasse. Sobald eine solche Nachricht empfangen wird, wird die SignatureCapturePage angezeigt, siehe Bild 6 links.

Das Erfassen (links) und die Anzeige (rechts) der Signatur in den beiden „Welten“ der Beispielanwendung (Bild 6)
Autor
Die SignatureCapturePage erlaubt es dem Nutzer, die Signatur per Touch zu erfassen und zu bestätigen. Sobald dies abgeschlossen ist, wird die Seite durch den Aufruf der übergebenen Rückruffunktion whenFinished() geschlossen. Das Ergebnis der Signaturerfassung wird anschließend durch den Versand der SignatureCaptureResultMessage an die Bridge und somit an die Web-App weitergeleitet.Die Web-App wertet das Ergebnis aus und zeigt die erfasste Signatur an, wie es Bild 6 rechts zeigt.

Das Erfassen (links) und die Anzeige (rechts) der Signatur in den beiden „Welten“ der Beispielanwendung (Bild 6)
Autor
Fazit
Die Implementierung von Web-View-Bridges in .NET MAUI eröffnet eine Vielzahl von Möglichkeiten zur Interaktion zwischen nativen Anwendungen und Webtechnologien. Durch den Einsatz von Bridge-Mustern und plattformspezifischen Handlern lassen sich die nativen und die Web-Teile einer Anwendung sauber trennen.Das Pattern ermöglicht es, die Komplexität der Interoperabilität zwischen JavaScript und den nativen Plattformen zu abstrahieren. Die gemeinsame Logik lässt sich in einer plattformunabhängigen Bridge einrichten, spezifische Implementierungen können für jede Plattform erstellt werden.Die verschiedenen Plattformen, wie Android, iOS und Windows, haben unterschiedliche Ansätze zur Einbindung von Web-Views und zur Kommunikation zwischen JavaScript und nativem Code. Bei Android sind dies die Funktionen von Android.Webkit.WebView, während iOS dazu auf WKWebView setzt; Windows verwendet das WebView2-Element für diesen Zweck.Es ist jedoch zu beachten, dass die vorgestellte Architektur lediglich ein Grundgerüst darstellt und an die spezifischen Anforderungen angepasst werden muss. Je nach den Anforderungen der Anwendung lassen sich weitere Funktionen, Schnittstellen und Kommunikationsmechanismenimplementieren.Die vorgestellte Architektur dient somit als Ausgangspunkt und Leitfaden, um Web-View-Bridges einfacher in .NET MAUI einbinden zu können.Die Umsetzung der Web-View-Bridges erfordert jedoch eine gründliche Recherche und Experimentierfreudigkeit, da die Dokumentation in einigen Bereichen lückenhaft oder unvollständig ist. Es ist oft nötig, verschiedene Ansätze und das Kombinieren von Informationen aus unterschiedlichen Quellen auszuprobieren, um zu einer funktionierenden Lösung zu gelangen. Die Unterstützung durch Community-Foren und die Bereitschaft zur Erforschung weiterführender Ressourcen sind dabei von großem Nutzen.Insgesamt bietet die Verwendung von Web-View-Bridges in .NET MAUI eine leistungsstarke und flexible Methode, um die Vorteile von Webtechnologien in nativen Anwendungen zu nutzen und gleichzeitig die verschiedenen Plattformen sauber zu trennen.
Fussnoten
- [1] Erich Gamma u. a., Design Patterns: Elements of Reusable Object-Oriented Software, Addison Wesley, 1995, ISBN 978-0-201-63361-0,
- [2] .NET MAUI WebView Class, http://www.dotnetpro.de/SL2312MAUIBridge1
- [3] .NET Multi-platform App UI (.NET MAUI), http://www.dotnetpro.de/SL2312MAUIBridge2
- [4] Handler, http://www.dotnetpro.de/SL2312MAUIBridge3
- [5] CoreWebView2.AddHostObjectToScript(String, Object) Method, http://www.dotnetpro.de/SL2312MAUIBridge4
- [6] CoreWebView2 Class, http://www.dotnetpro.de/SL2312MAUIBridge5
- [7] GitHub, CoreWebView2.AddHostObjectToScript throws System.Exception #2529, http://www.dotnetpro.de/SL2312MAUIBridge6
- [8] Call native-side WinRT code from web-side code, http://www.dotnetpro.de/SL2312MAUIBridge7
- [9] Walkthrough – Create a C#/WinRT component, and consume it from C++/WinRT, http://www.dotnetpro.deSL2312MAUIBridge8
- Introduction to the MVVM Toolkit, http://www.dotnetpro.de/SL2312MAUIBridge9
- Messenger (des MVVM Toolkit), http://www.dotnetpro.de/SL2312MAUIBridge10