更新:我为 Windows 开发了一个解决方法。见下文。
TL;博士-
https://github.com/nmoschkin/MAUIWebViewExample
我提出了一个适用于 iOS 和 Android 的 MAUI 解决方案,使用新的 Handler 模式,如下所述:
Porting Custom Renderers To Handlers
上面的文档有些差,并且没有针对 iOS 版本的实现。我在这里提供。
这种改编也使 Source 属性成为可绑定属性.与上面链接中的示例不同,我愿意不是实际上将属性添加到属性映射器以传统方式在平台处理程序中。相反,我们将监听由可绑定属性的属性更改通知方法触发的事件。
这个例子实现了一个 100% 自定义的 WebView。如果您想从本机组件移植其他属性和方法,则必须自己添加这些附加功能。
共享代码:
在共享代码文件中,您希望通过以下方式实现上述链接中描述的类和接口来创建自定义视图(为我们将提供给消费者的事件提供额外的类):
public class SourceChangedEventArgs : EventArgs
{
public WebViewSource Source
{
get;
private set;
}
public SourceChangedEventArgs(WebViewSource source)
{
Source = source;
}
}
public class JavaScriptActionEventArgs : EventArgs
{
public string Payload { get; private set; }
public JavaScriptActionEventArgs(string payload)
{
Payload = payload;
}
}
public interface IHybridWebView : IView
{
event EventHandler<SourceChangedEventArgs> SourceChanged;
event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
void Refresh();
WebViewSource Source { get; set; }
void Cleanup();
void InvokeAction(string data);
}
public class HybridWebView : View, IHybridWebView
{
public event EventHandler<SourceChangedEventArgs> SourceChanged;
public event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
public HybridWebView()
{
}
public void Refresh()
{
if (Source == null) return;
var s = Source;
Source = null;
Source = s;
}
public WebViewSource Source
{
get { return (WebViewSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly BindableProperty SourceProperty = BindableProperty.Create(
propertyName: "Source",
returnType: typeof(WebViewSource),
declaringType: typeof(HybridWebView),
defaultValue: new UrlWebViewSource() { Url = "about:blank" },
propertyChanged: OnSourceChanged);
private static void OnSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as HybridWebView;
bindable.Dispatcher.Dispatch(() =>
{
view.SourceChanged?.Invoke(view, new SourceChangedEventArgs(newValue as WebViewSource));
});
}
public void Cleanup()
{
JavaScriptAction = null;
}
public void InvokeAction(string data)
{
JavaScriptAction?.Invoke(this, new JavaScriptActionEventArgs(data));
}
}
然后您必须为每个平台声明处理程序,如下所示:
安卓实现:
public class HybridWebViewHandler : ViewHandler<IHybridWebView, Android.Webkit.WebView>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
private JSBridge jsBridgeHandler;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
}
private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override Android.Webkit.WebView CreatePlatformView()
{
var webView = new Android.Webkit.WebView(Context);
jsBridgeHandler = new JSBridge(this);
webView.Settings.JavaScriptEnabled = true;
webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
webView.AddJavascriptInterface(jsBridgeHandler, "jsBridge");
return webView;
}
protected override void ConnectHandler(Android.Webkit.WebView platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += VirtualView_SourceChanged;
}
protected override void DisconnectHandler(Android.Webkit.WebView platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= VirtualView_SourceChanged;
VirtualView.Cleanup();
jsBridgeHandler?.Dispose();
jsBridgeHandler = null;
}
private static void LoadSource(WebViewSource source, Android.Webkit.WebView control)
{
try
{
if (source is HtmlWebViewSource html)
{
control.LoadDataWithBaseURL(html.BaseUrl, html.Html, null, "charset=UTF-8", null);
}
else if (source is UrlWebViewSource url)
{
control.LoadUrl(url.Url);
}
}
catch { }
}
}
public class JavascriptWebViewClient : WebViewClient
{
string _javascript;
public JavascriptWebViewClient(string javascript)
{
_javascript = javascript;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
view.EvaluateJavascript(_javascript, null);
}
}
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
internal JSBridge(HybridWebViewHandler hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewHandler hybridRenderer;
if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
hybridRenderer.VirtualView.InvokeAction(data);
}
}
}
iOS 实现:
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WKWebView>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
private WKUserContentController userController;
private JSBridge jsBridgeHandler;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
}
private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WKWebView CreatePlatformView()
{
jsBridgeHandler = new JSBridge(this);
userController = new WKUserContentController();
var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(jsBridgeHandler, "invokeAction");
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(CGRect.Empty, config);
return webView;
}
protected override void ConnectHandler(WKWebView platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += VirtualView_SourceChanged;
}
protected override void DisconnectHandler(WKWebView platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= VirtualView_SourceChanged;
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
jsBridgeHandler?.Dispose();
jsBridgeHandler = null;
}
private static void LoadSource(WebViewSource source, WKWebView control)
{
if (source is HtmlWebViewSource html)
{
control.LoadHtmlString(html.Html, new NSUrl(html.BaseUrl ?? "http://localhost", true));
}
else if (source is UrlWebViewSource url)
{
control.LoadRequest(new NSUrlRequest(new NSUrl(url.Url)));
}
}
}
public class JSBridge : NSObject, IWKScriptMessageHandler
{
readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
internal JSBridge(HybridWebViewHandler hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
}
public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
HybridWebViewHandler hybridRenderer;
if (hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
hybridRenderer.VirtualView?.InvokeAction(message.Body.ToString());
}
}
}
如您所见,我正在监听事件以更改源,然后它将执行更改它所需的特定于平台的步骤。
另请注意,在这两种实现中JSBridge我正在使用一个弱引用跟踪控制。我不确定处置可能会陷入僵局的任何情况,但我这样做是出于非常谨慎的考虑。
窗口实现
所以。根据我阅读的各种文章,当前的 WebView2 for MAUI 的 WinUI3 迭代还不允许我们调用AddHostObjectToScript.他们计划在将来的版本中进行此操作。
但是,后来我记得它是 Windows,所以我创建了一个变通方案,可以肯定地模拟相同的行为并达到相同的结果,但使用了一个有点非正统的解决方案:通过使用HttpListener.
public class HybridSocket
{
private HttpListener listener;
private HybridWebViewHandler handler;
bool token = false;
public HybridSocket(HybridWebViewHandler handler)
{
this.handler = handler;
CreateSocket();
}
private void CreateSocket()
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:32000/");
}
public void StopListening()
{
token = false;
}
private void Fling(string json)
{
handler.VirtualView.InvokeAction(json);
}
public void Listen()
{
var s = listener;
try
{
token = true;
s.Start();
while (token)
{
HttpListenerContext ctx = listener.GetContext();
using HttpListenerResponse resp = ctx.Response;
resp.AddHeader("Access-Control-Allow-Origin", "null");
resp.AppendHeader("Access-Control-Allow-Headers", "content-type");
var req = ctx.Request;
Stream body = req.InputStream;
Encoding encoding = req.ContentEncoding;
using (StreamReader reader = new StreamReader(body, encoding))
{
if (req.ContentType != null)
{
Console.WriteLine("Client data content type {0}", req.ContentType);
}
var json = reader.ReadToEnd();
if (!string.IsNullOrEmpty(json))
{
Fling(json);
}
}
resp.StatusCode = (int)HttpStatusCode.OK;
resp.StatusDescription = "Status OK";
}
CreateSocket();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WebView2>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = @"function invokeCSharpAction(data)
{
var http = new XMLHttpRequest();
var url = 'http://localhost:32000';
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/json');
http.send(JSON.stringify(data));
}";
static SynchronizationContext sync;
private HybridSocket socketer;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
sync = SynchronizationContext.Current;
socketer = new HybridSocket(this);
Task.Run(() => socketer.Listen());
}
~HybridWebViewHandler()
{
socketer.StopListening();
}
private void OnWebSourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WebView2 CreatePlatformView()
{
sync = sync ?? SynchronizationContext.Current;
var webView = new WebView2();
webView.NavigationCompleted += WebView_NavigationCompleted;
return webView;
}
private void WebView_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
var req = new EvaluateJavaScriptAsyncRequest(JavascriptFunction);
PlatformView.EvaluateJavaScript(req);
}
protected override void ConnectHandler(WebView2 platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += OnWebSourceChanged;
}
protected override void DisconnectHandler(WebView2 platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= OnWebSourceChanged;
VirtualView.Cleanup();
}
private static void LoadSource(WebViewSource source, WebView2 control)
{
try
{
if (control.CoreWebView2 == null)
{
control.EnsureCoreWebView2Async().AsTask().ContinueWith((t) =>
{
sync.Post((o) =>
{
if (source is HtmlWebViewSource html)
{
control.CoreWebView2.NavigateToString(html.Html);
}
else if (source is UrlWebViewSource url)
{
control.CoreWebView2.Navigate(url.Url);
}
}, null);
});
}
else
{
if (source is HtmlWebViewSource html)
{
control.CoreWebView2.NavigateToString(html.Html);
}
else if (source is UrlWebViewSource url)
{
control.CoreWebView2.Navigate(url.Url);
}
}
}
catch { }
}
}
最后,您需要通过添加初始化 MAUI 应用程序配置MauiHandlers应用程序生成器:
在 MauiProgram.cs 中初始化 MAUI 应用程序
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(HybridWebView), typeof(HybridWebViewHandler));
});
return builder.Build();
}
将控件添加到 XAML
<controls:HybridWebView
x:Name="MyWebView"
HeightRequest="128"
HorizontalOptions="Fill"
Source="{Binding Source}"
VerticalOptions="FillAndExpand"
WidthRequest="512"
/>
最后,我将以上所有内容添加到 GitHub 存储库中的完整示例 MAUI 项目中:
https://github.com/nmoschkin/MAUIWebViewExample
GitHub 存储库示例还包括一个 ViewModel,其中包含WebViewSource控件在标记中绑定到的。