原文:Implementing a HybridWebView
呈现一个特定于平台的视图
Xamarin.Forms自定义用户界面控件应该来自视图类(View class),用于在屏幕上放置布局和控件。本文演示了如何为HybridWebView(混合webview)自定义控件创建自定义渲染器,该控件演示了如何增强特定平台的web控件,以允许从JavaScript调用c#代码。
每一个Xamarin.Forms视图为每个创建本地控件实例的平台提供了相应的渲染器。当一个视图被Xamarin.Forms在iOS中渲染时,ViewRenderer类被实例化,从而实例化一个本机的UIView控件。
在Android平台上,ViewRenderer类实例化一个视图控件。在Windows Phone和Universal Windows平台(UWP)上,ViewRenderer类实例化了一个本机FrameworkElement控件。有关使用Xamarin.Forms控件映射到渲染器和本地控件类的更多信息,请参考:Renderer Base Classes and Native Controls(渲染器基类和本地控件)。
下面的图表说明了视图与实现它的相应本机控件之间的关系:
通过为每个平台上的视图创建自定义渲染器,可以使用呈现过程实现特定于平台的定制。这样做的过程如下:
1.创建HybridWebView自定义控件。
2.使用来自Xamarin.Forms的HybridWebView。
3.在每个平台上为HybridWebView创建自定义渲染器。
现在,每个项目都将依次讨论,以实现一个HybridWebView渲染器,它增强了特定于平台的web控件,以允许从JavaScript调用c#代码。HybridWebView实例将被用来显示一个HTML页面,让用户输入他们的名字。然后,当用户单击HTML按钮时,一个JavaScript函数将调用一个C#操作,该操作将显示一个包含用户名的弹出窗口。
有关从JavaScript调用C#的过程的更多信息,请参见 Invoking C# from JavaScript(从JavaScript调用C#)。有关HTML页面的更多信息,请参见Creating the Web Page(创建Web页面)。
创建HybridWebView
通过子类化View类,可以创建HybridWebView自定义控件,如下面的代码示例所示:
1 public class HybridWebView : View 2 { 3 Action<string> action; 4 public static readonly BindableProperty UriProperty = BindableProperty.Create ( 5 propertyName: "Uri", 6 returnType: typeof(string), 7 declaringType: typeof(HybridWebView), 8 defaultValue: default(string)); 9 10 public string Uri { 11 get { return (string)GetValue (UriProperty); } 12 set { SetValue (UriProperty, value); } 13 } 14 15 public void RegisterAction (Action<string> callback) 16 { 17 action = callback; 18 } 19 20 public void Cleanup () 21 { 22 action = null; 23 } 24 25 public void InvokeAction (string data) 26 { 27 if (action == null || data == null) { 28 return; 29 } 30 action.Invoke (data); 31 } 32 }
HybridWebView自定义控件是在可移植类库(PCL)项目中创建的,并为控件定义了以下API:
●一个Uri属性,用于指定要加载web页面的地址。
●一个RegisterAction方法,用控件注册一个动作。通过Uri属性引用的HTML文件中包含的JavaScript调用注册的动作。
●一个CleanUp方法,移除注册 Action 的引用。
●一个InvokeAction方法,调用注册动作。这个方法将从每个平台特定项目中的自定义渲染器调用。
使用HybridWebView
通过为其位置声明一个命名空间并使用自定义控件上的命名空间前缀,可以在PCL项目中引用XAML中的HybridWebView自定义控件。下面的代码示例展示了如何使用XAML页面来使用HybridWebView自定义控件:
1 <ContentPage ... 2 xmlns:local="clr-namespace:CustomRenderer;assembly=CustomRenderer" 3 x:Class="CustomRenderer.HybridWebViewPage" 4 Padding="0,20,0,0"> 5 <ContentPage.Content> 6 <local:HybridWebView x:Name="hybridWebView" Uri="index.html" 7 HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" /> 8 </ContentPage.Content> 9 </ContentPage>
本地命名空间前缀可以命名为任何东西。但是,clr命名空间和程序集值必须与自定义控件的细节相匹配。声明命名空间后,将使用前缀引用自定义控件。
下面的代码示例展示了如何通过C#页面来使用HybridWebView自定义控件:
1 public class HybridWebViewPageCS : ContentPage 2 { 3 public HybridWebViewPageCS () 4 { 5 var hybridWebView = new HybridWebView { 6 Uri = "index.html", 7 HorizontalOptions = LayoutOptions.FillAndExpand, 8 VerticalOptions = LayoutOptions.FillAndExpand 9 }; 10 ... 11 Padding = new Thickness (0, 20, 0, 0); 12 Content = hybridWebView; 13 } 14 }
HybridWebView实例将用于在每个平台上显示本机web控件。它的Uri属性设置为一个HTML文件,该文件存储在每个平台特定的项目中,并由本机web控件显示。呈现的HTML要求用户输入他们的名字,JavaScript函数在响应HTML按钮时调用C#操作。
HybridWebViewPage注册从JavaScript调用的操作,如下面的代码示例所示:
1 public partial class HybridWebViewPage : ContentPage 2 { 3 public HybridWebViewPage () 4 { 5 ... 6 hybridWebView.RegisterAction (data => DisplayAlert ("Alert", "Hello " + data, "OK")); 7 } 8 }
这个操作调用DisplayAlert方法来显示一个模式弹出,它显示了由HybridWebView实例显示的HTML页面中的名称。
现在可以将自定义渲染器添加到每个应用程序项目中,以便通过允许从JavaScript调用c#代码来增强特定于平台的web控件。
在每个平台上创建自定义渲染器
创建自定义渲染类的过程如下:
1.创建ViewRenderer < T1,T2 >类,以呈现自定义控件的子类。
第一个类型参数应该是自定义控件,在本例中是HybridWebView。
第二个类型参数应该是实现自定义视图的本机控件。
2.重写显示自定义控件的OnElementChanged方法,并编写逻辑来自定义它。
这个方法在相应的Xamarin.Forms自定义控件创建时调用。
3.向自定义渲染器类添加一个ExportRenderer属性,以指定它将用于呈现Xamarin.Forms自定义控件。此属性用于将自定义渲染器与Xamarin.Forms进行注册。
对大多数Xamarin.Forms元素,可以在每个平台项目中提供自定义渲染器。如果未注册自定义渲染器,则将使用控件基类的默认渲染器。然而,在呈现视图元素时,每个平台项目都需要自定义渲染器。
下图说明了示例应用程序中每个项目的职责,以及它们之间的关系:
HybridWebView自定义控件是由特定于平台的呈现类提供的,这些类都来自于每个平台的ViewRendererclass。这个结果在每个HybridWebView的自定义控件中都显示了特定于平台的web控件,如下面的截图所示:
ViewRenderer类公开了OnElementChanged方法,它是在创建Xamarin.Forms自定义控件时调用的,以呈现相应的本机web控件。该方法采用ElementChangedEventArgs参数,其中包含了OldElement和NewElement属性。这些属性表示渲染器附加到的Xamarin.Forms元素,以及渲染器所依附的Xamarin.Forms元素。在示例应用程序中,OldElement属性将为null,NewElement属性将包含对混合式webviewinstance的引用。
在每个平台特定的renderer类中,OnElementChanged方法的一个重写版本是执行本机web控件实例化和自定义的位置。SetNativeControl方法应该用于实例化本机web控件,此方法还将为控件属性分配控件引用。此外,对于正在呈现的Xamarin.Forms控件的引用可以通过元素属性获得。
在某些情况下,OnElementChanged方法可以多次调用,因此在实例化新的本机控件时必须小心,以防止内存泄漏。在自定义渲染器中实例化一个新的本机控件时使用的方法如下面的代码示例所示:
1 protected override void OnElementChanged (ElementChangedEventArgs<NativeListView> e) 2 { 3 base.OnElementChanged (e); 4 5 if (Control == null) { 6 // Instantiate the native control and assign it to the Control property with 7 // the SetNativeControl method 8 } 9 10 if (e.OldElement != null) { 11 // Unsubscribe from event handlers and cleanup any resources 12 } 13 14 if (e.NewElement != null) { 15 // Configure the control and subscribe to event handlers 16 } 17 }
当控件属性为空时,应该只实例化一个新的本机控件。只有当自定义渲染器附加到新的Xamarin.Forms元素时,才应该配置该控件并订阅事件处理程序。类似地,任何订阅到的事件处理程序都应该只在呈现元素时取消订阅。采用这种方法将有助于创建一个不受内存泄漏影响的性能自定义渲染器。
每个自定义渲染器类都使用一个ExportRenderer属性来修饰,该属性将渲染器注册为Xamarin.Forms。该属性包含两个参数——呈现的Xamarin.Forms自定义控件的类型名称,以及自定义渲染器的类型名称。属性的装配(assembly )前缀指定该属性应用于整个程序集。
下面的部分将讨论由每个本地web控件加载的web页面的结构,以及从JavaScript调用C#的过程,以及在每个特定于平台的自定义renderer类中的实现。
创建Web页面
下面的代码示例显示了通过混合webview自定义控件显示的web页面:
1 <html> 2 <body> 3 <script src="http://code.jquery.com/jquery-1.11.0.min.js"></script> 4 <h1>HybridWebView Test</h1> 5 <br/> 6 Enter name: <input type="text" id="name"> 7 <br/> 8 <br/> 9 <button type="button" onclick="javascript:invokeCSCode($(‘#name‘).val());">Invoke C# Code</button> 10 <br/> 11 <p id="result">Result:</p> 12 <script type="text/javascript"> 13 function log(str) 14 { 15 $(‘#result‘).text($(‘#result‘).text() + " " + str); 16 } 17 18 function invokeCSCode(data) { 19 try { 20 log("Sending Data:" + data); 21 invokeCSharpAction(data); 22 } 23 catch (err){ 24 log(err); 25 } 26 } 27 </script> 28 </body> 29 </html>
这个Web页面允许用户在input元素中输入他们的名字,并提供一个button元素,当单击时将调用C#代码。实现这一目标的过程如下:
●当用户单击button元素,invokeCSCode JavaScript函数被调用时,input元素的值被传递给函数。
●invokeCSCode函数调用log函数,以显示数据发送到C# Action。然后调用invokeCSharpAction方法调用C# Action,传递从input元素接收的参数。
invokeCSharpAction JavaScript函数没有在web页面中定义,并将由每个自定义渲染器注入它。
从JavaScript调用C#
从JavaScript调用C#的过程在每个平台上都是相同的:
●自定义渲染器创建一个本地网络控制和加载指定的HTML文件HybridWebView.Uri属性。
●一旦加载web页面,invokeCSharpAction JavaScript函数自定义渲染器注入到web页面。
●当用户输入他们的名字,点击HTML的button元素,invokeCSCode函数被调用时,反过来调用invokeCSharpAction函数。
●invokeCSharpAction函数调用自定义渲染器的方法,进而调用HybridWebView.InvokeAction方法。
●HybridWebView.InvokeAction方法调用注册过的Action。
下面的部分将讨论如何在每个平台上实现这个过程。
在iOS上创建自定义渲染器
下面的代码示例显示了iOS平台的自定义渲染器:
1 [assembly: ExportRenderer (typeof(HybridWebView), typeof(HybridWebViewRenderer))] 2 namespace CustomRenderer.iOS 3 { 4 public class HybridWebViewRenderer : ViewRenderer<HybridWebView, WKWebView>, IWKScriptMessageHandler 5 { 6 const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}"; 7 WKUserContentController userController; 8 9 protected override void OnElementChanged (ElementChangedEventArgs<HybridWebView> e) 10 { 11 base.OnElementChanged (e); 12 13 if (Control == null) { 14 userController = new WKUserContentController (); 15 var script = new WKUserScript (new NSString (JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false); 16 userController.AddUserScript (script); 17 userController.AddScriptMessageHandler (this, "invokeAction"); 18 19 var config = new WKWebViewConfiguration { UserContentController = userController }; 20 var webView = new WKWebView (Frame, config); 21 SetNativeControl (webView); 22 } 23 if (e.OldElement != null) { 24 userController.RemoveAllUserScripts (); 25 userController.RemoveScriptMessageHandler ("invokeAction"); 26 var hybridWebView = e.OldElement as HybridWebView; 27 hybridWebView.Cleanup (); 28 } 29 if (e.NewElement != null) { 30 string fileName = Path.Combine (NSBundle.MainBundle.BundlePath, string.Format ("Content/{0}", Element.Uri)); 31 Control.LoadRequest (new NSUrlRequest (new NSUrl (fileName, false))); 32 } 33 } 34 35 public void DidReceiveScriptMessage (WKUserContentController userContentController, WKScriptMessage message) 36 { 37 Element.InvokeAction (message.Body.ToString ()); 38 } 39 } 40 }
HybridWebViewRenderer类将HybridWebView.Uri属性中指定的web页面加载到原生WKWebView控件中,并将invokeCSharpAction JavaScript函数注入到web页面中。一旦用户输入他们的名字并单击HTML按钮元素,执行invokeCSharpAction JavaScript函数,DidReceiveScriptMessage方法被称为接收到消息后从web页面。反过来,该方法调用杂交 HybridWebView.InvokeAction 方法,它将调用注册的操作以显示弹出窗口。
该功能的实现如下:
●提供控制属性为空,进行以下操作:
○WKUserContentController实例被创建时,它允许用户发布信息和注射到web页面的脚本。
○WKUserScript实例被创建以注入invokeCSharpAction JavaScript函数后的网页加载web页面。
○WKUserContentController.AddScript方法将WKUserScript实例添加到内容控制器。
○ WKUserContentController.AddScriptMessageHandler 方法添加一个消息处理程序脚本名为invokeAction WKUserContentController实例,这将导致JavaScript函数window.webkit.messageHandlers.invokeAction.postMessage(data)中定义的所有帧在所有web视图将使用WKUserContentController实例。
○WKWebViewConfiguration实例被创建,与WKUserContentController实例被设置为内容的控制器。
○WKWebView控制被实例化,SetNativeControl方法被调用时指定的引用WKWebView控制控制财产。
●提供自定义渲染器是连接到一个新的Xamarin.Forms元素:
○WKWebView.LoadRequest方法加载指定的HTML文件HybridWebView.Uri属性。
该代码指定该文件存储在项目的内容文件夹中。
一旦web页面显示出来,invokeCSharpAction JavaScript函数就会被注入到web页面中。
●当元素渲染器附加到变化:
○资源被释放。
WKWebView类只在ios8和以后的版本中得到支持。
在Android上创建自定义渲染器
下面的代码示例显示了Android平台的自定义渲染器:
1 [assembly: ExportRenderer (typeof(HybridWebView), typeof(HybridWebViewRenderer))] 2 namespace CustomRenderer.Droid 3 { 4 public class HybridWebViewRenderer : ViewRenderer<HybridWebView, Android.Webkit.WebView> 5 { 6 const string JavaScriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}"; 7 8 protected override void OnElementChanged (ElementChangedEventArgs<HybridWebView> e) 9 { 10 base.OnElementChanged (e); 11 12 if (Control == null) { 13 var webView = new Android.Webkit.WebView (Forms.Context); 14 webView.Settings.JavaScriptEnabled = true; 15 SetNativeControl (webView); 16 } 17 if (e.OldElement != null) { 18 Control.RemoveJavascriptInterface ("jsBridge"); 19 var hybridWebView = e.OldElement as HybridWebView; 20 hybridWebView.Cleanup (); 21 } 22 if (e.NewElement != null) { 23 Control.AddJavascriptInterface (new JSBridge (this), "jsBridge"); 24 Control.LoadUrl (string.Format ("file:///android_asset/Content/{0}", Element.Uri)); 25 InjectJS (JavaScriptFunction); 26 } 27 } 28 29 void InjectJS (string script) 30 { 31 if (Control != null) { 32 Control.LoadUrl (string.Format ("javascript: {0}", script)); 33 } 34 } 35 } 36 }
HybridWebViewRenderer类加载将HybridWebView.Uri属性中指定的web页面加载到一个原生WebView控件中,而invokec锐动作JavaScript函数被注入到web页面中,在web页面加载之后,使用了InjectJS方法。
一旦用户输入他们的名字并点击HTML按钮元素,就会执行invokeCSharpAction JavaScript函数。
该功能的实现如下:
●提供控制属性为空,进行以下操作:
○原生WebView实例被创建,启用了JavaScript的控制。
○SetNativeControl方法称为引用分配给本地WebView控件控制属性。
●提供自定义渲染器是连接到一个新的Xamarin.Forms元素:
○WebView.AddJavascriptInterface方法注入一个新的JSBridge实例主框架的WebView JavaScript上下文,JSBridge命名它。
这允许从JavaScript访问JSBridge类中的方法。
○WebView.LoadUrl方法加载指定的HTML文件HybridWebView.Uri属性。
该代码指定该文件存储在项目的内容文件夹中。
○InjectJS方法是为了注入invokeCSharpAction JavaScript函数调用web页面。
●当元素渲染器附加到变化:
○资源被释放。
当invokeCSharpAction JavaScript函数被执行时,它依次调用JSBridge.InvokeActio方法,如下代码示例所示:
1 public class JSBridge : Java.Lang.Object 2 { 3 readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer; 4 5 public JSBridge (HybridWebViewRenderer hybridRenderer) 6 { 7 hybridWebViewRenderer = new WeakReference <HybridWebViewRenderer> (hybridRenderer); 8 } 9 10 [JavascriptInterface] 11 [Export ("invokeAction")] 12 public void InvokeAction (string data) 13 { 14 HybridWebViewRenderer hybridRenderer; 15 16 if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget (out hybridRenderer)) { 17 hybridRenderer.Element.InvokeAction (data); 18 } 19 } 20 }
该类必须从 Java.Lang.Object 派生,而暴露于JavaScript的方法必须使用[JavascriptInterface]和[导出]属性来修饰。因此,当invokeCSharpAction JavaScript函数被注入到web页面并被执行时,它将调用由于[JavascriptInterface]和[Export(" invokeAction "])属性修饰的JSBridge.InvokeAction方法。反过来,InvokeAction方法调用混合HybridWebView.InvokeAction方法,它将调用注册的操作以显示弹出窗口。
使用[Export]属性的项目必须包含对Mono.Android.Export的引用,否则将导致编译错误。
注意,JSBridge类对混合webviewrenderer类保持了弱引用。这是为了避免在两个类之间创建循环引用。有关更多信息,请参见MSDN上的 Weak References (弱引用)。
在Windows Phone和UWP上创建自定义渲染器
下面的代码示例显示了Windows Phone和UWP的自定义渲染器:
1 [assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))] 2 namespace CustomRenderer.WinPhone81 3 { 4 public class HybridWebViewRenderer : ViewRenderer<HybridWebView, Windows.UI.Xaml.Controls.WebView> 5 { 6 const string JavaScriptFunction = "function invokeCSharpAction(data){window.external.notify(data);}"; 7 8 protected override void OnElementChanged(ElementChangedEventArgs<HybridWebView> e) 9 { 10 base.OnElementChanged(e); 11 12 if (Control == null) 13 { 14 SetNativeControl(new Windows.UI.Xaml.Controls.WebView()); 15 } 16 if (e.OldElement != null) 17 { 18 Control.NavigationCompleted -= OnWebViewNavigationCompleted; 19 Control.ScriptNotify -= OnWebViewScriptNotify; 20 } 21 if (e.NewElement != null) 22 { 23 Control.NavigationCompleted += OnWebViewNavigationCompleted; 24 Control.ScriptNotify += OnWebViewScriptNotify; 25 Control.Source = new Uri(string.Format("ms-appx-web:///Content//{0}", Element.Uri)); 26 } 27 } 28 29 async void OnWebViewNavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args) 30 { 31 if (args.IsSuccess) 32 { 33 // Inject JS script 34 await Control.InvokeScriptAsync("eval", new[] { JavaScriptFunction }); 35 } 36 } 37 38 void OnWebViewScriptNotify(object sender, NotifyEventArgs e) 39 { 40 Element.InvokeAction(e.Value); 41 } 42 } 43 }
HybridWebViewRenderer类加载将HybridWebView.Uri 属性中指定的web页面加载到一个原生WebView控件中,并且在web页面加载后,通过WebView.InvokeScriptAsync方法将invokeCSharpAction JavaScript函数注入到web页面中。一旦用户输入他们的名字并点击HTML按钮元素,invokeCSharpAction JavaScript函数就会被执行,当从web页面接收到通知后,将调用OnWebViewScriptNotify方法。反过来,该方法调用混合HybridWebView.InvokeAction方法,它将调用注册的操作以显示弹出窗口。
该功能的实现如下:
●提供控制属性为空,进行以下操作:
○SetNativeControl方法实例化一个新的本地WebView控制和引用分配给控制财产。
●提供自定义渲染器是连接到一个新的Xamarin.Forms元素:
○NavigationCompleted和ScriptNotify事件的事件处理程序注册。
当本机WebView控件已完成加载当前内容或导航失败时,NavigationCompleted事件触发。
当本机WebView控件中的内容使用JavaScript将字符串传递给应用程序时,ScriptNotify事件触发。
在传递字符串参数时,web页面通过调用window.external.notify触发ScriptNotify事件。
○WebView.Source属性设置为指定的HTML文件的URI HybridWebView.Uri属性。代码假定该文件存储在项目的内容文件夹中。一旦web页面显示,NavigationCompleted事件将触发和OnWebViewNavigationCompleted方法将被调用。
当导航成功完成时,invokeCSharpAction JavaScript函数将被注入WebView.InvokeScriptAsync方法的web页面中。
●当元素渲染器附加到变化:
事件没订阅。
总结
本文演示了如何为混合视图自定义控件创建自定义渲染器,演示了如何增强特定于平台的web控件,以允许从JavaScript调用C#代码。