共用方式為


自訂 WebView

Xamarin.FormsWebView是在您的應用程式中顯示 Web 和 HTML 內容的檢視。 本文說明如何建立自定義轉譯器,以擴充 WebView 以允許從 JavaScript 叫用 C# 程序代碼。

每個 Xamarin.Forms 檢視都有每個平臺的隨附轉譯器,可建立原生控件的實例。 WebView當 應用程式在 Xamarin.Forms iOS 上轉譯 時,類別WkWebViewRenderer就會具現化,進而具現化原生WkWebView控件。 在 Android 平台上,WebViewRenderer 類別會具現化原生 WebView 控制項。 在通用 Windows 平台 (UWP) 上,WebViewRenderer 類別會具現化原生 WebView 控制項。 如需控件對應之轉譯器和原生控件類別 Xamarin.Forms 的詳細資訊,請參閱 轉譯器基類和原生控件

下圖說明 View 和實作它之對應原生控制項間的關聯性:

WebView 類別與其實作原生類別之間的關聯性

轉譯程式可用來實作平臺自定義,方法是在每個平臺上建立的 WebView 自定義轉譯器。 執行這項作業的程序如下:

  1. 建立HybridWebView 自訂控制項。
  2. 從取用 Xamarin.FormsHybridWebView
  3. 在每個平台上建立HybridWebView 的自訂轉譯器。

現在將討論每個專案,以實 HybridWebView 作可增強 Xamarin.FormsWebView 的轉譯器,以允許從 JavaScript 叫用 C# 程式代碼。 HybridWebView 執行個體會用以顯示 HTML 網頁,要求使用者輸入其名稱。 然後,當使用者按一下 HTML 按鈕時,JavaScript 函式會叫用 C# Action,其顯示包含使用者名稱的快顯視窗。

如需從 JavaScript 叫用 C# 的程式詳細資訊,請參閱 從 JavaScript 叫用 C# 。 如需 HTML 頁面的詳細資訊,請參閱 建立網頁

注意

WebView可以從 C# 叫用 JavaScript 函式,並將任何結果傳回呼叫的 C# 程式代碼。 如需詳細資訊,請參閱 叫用 JavaScript

建立 HybridWebView

自訂 HybridWebView 控制項可以藉由子類別 WebView 化來建立:

public class HybridWebView : WebView
{
    Action<string> action;

    public static readonly BindableProperty UriProperty = BindableProperty.Create(
        propertyName: "Uri",
        returnType: typeof(string),
        declaringType: typeof(HybridWebView),
        defaultValue: default(string));

    public string Uri
    {
        get { return (string)GetValue(UriProperty); }
        set { SetValue(UriProperty, value); }
    }

    public void RegisterAction(Action<string> callback)
    {
        action = callback;
    }

    public void Cleanup()
    {
        action = null;
    }

    public void InvokeAction(string data)
    {
        if (action == null || data == null)
        {
            return;
        }
        action.Invoke(data);
    }
}

HybridWebView 自訂控制項會在 .NET Standard 程式庫專案中建立,並定義下列控制項的 API:

  • 指定要載入之網頁位址的 Uri 屬性。
  • 向控制項登錄 ActionRegisterAction 方法。 已註冊的動作會從 JavaScript 叫用,此 JavaScript 包含在透過 Uri 屬性參考的 HTML 檔案中。
  • 移除已註冊 Action 之參考的 CleanUp 方法。
  • 叫用已註冊 ActionInvokeAction 方法。 這個方法將會從每個平台專案中的自定義轉譯器呼叫。

使用 HybridWebView

您可以宣告控制項的位置命名空間並使用自訂控制項上的命名空間前置詞,在 .NET Standard 程式庫專案的 XAML 中參考 HybridWebView 自訂控制項。 下列程式碼範例示範 XAML 頁面如何使用 HybridWebView 自訂控制項:

<ContentPage ...
             xmlns:local="clr-namespace:CustomRenderer;assembly=CustomRenderer"
             x:Class="CustomRenderer.HybridWebViewPage"
             Padding="0,40,0,0">
    <local:HybridWebView x:Name="hybridWebView"
                         Uri="index.html" />
</ContentPage>

local 命名空間前置詞沒有命名限制。 不過,clr-namespaceassembly 值必須符合自訂控制項的詳細資料。 一旦宣告命名空間,即會使用前置詞來參考自訂控制項。

下列程式碼範例示範 C# 頁面如何使用 HybridWebView 自訂控制項:

public HybridWebViewPageCS()
{
    var hybridWebView = new HybridWebView
    {
        Uri = "index.html"
    };
    // ...
    Padding = new Thickness(0, 40, 0, 0);
    Content = hybridWebView;
}

HybridWebView 執行個體會用於顯示每個平台的原生 Web 控制項。 Uri屬性會設定為儲存在每個平台專案中的 HTML 檔案,並由原生 Web 控件顯示。 轉譯的 HTML 會要求使用者輸入其名稱,以 JavaScript 函式叫用 C# Action 以回應 [HTML] 按鈕的按一下動作。

HybridWebViewPage 註冊要從 JavaScript 叫用的動作,如下列程式碼範例所示:

public partial class HybridWebViewPage : ContentPage
{
    public HybridWebViewPage()
    {
        // ...
        hybridWebView.RegisterAction(data => DisplayAlert("Alert", "Hello " + data, "OK"));
    }
}

這個動作會呼叫 DisplayAlert 方法,顯示強制回應快顯,此快顯可呈現 HybridWebView 執行個體所顯示之 HTML 頁面中輸入的名稱。

自定義轉譯器現在可以新增至每個應用程式專案,藉由允許從 JavaScript 叫用 C# 程式代碼來增強平臺 Web 控件。

在每個平臺上建立自定義轉譯器

建立自訂轉譯器類別的流程如下:

  1. 在 iOS 上建立 類別的 WkWebViewRenderer 子類別,以及在 WebViewRenderer Android 和 UWP 上建立類別,以轉譯自定義控件。
  2. 覆寫轉 OnElementChangedWebView 和 寫入邏輯的方法,以自定義它。 建立物件時 HybridWebView 會呼叫這個方法。
  3. ExportRenderer將屬性新增至自定義轉譯器類別或AssemblyInfo.cs,以指定將用來轉Xamarin.Forms譯自定義控件。 這個屬性是用來向 Xamarin.Forms註冊自定義轉譯器。

注意

對於大多數 Xamarin.Forms 元素,您可以選擇在每個平台專案中提供自定義轉譯器。 如果自訂轉譯器未註冊,則會使用控制項基底類別的預設轉譯器。 不過,轉譯 View 項目時,每個平台專案都必須要有自訂轉譯器。

下圖說明應用程式範例中每個專案的責任,以及這些專案之間的關聯性:

HybridWebView 自訂轉譯器專案責任

自定義 HybridWebView 控件是由平台轉譯器類別轉譯,其衍生自 WkWebViewRenderer iOS 上的 類別,以及 WebViewRenderer Android 和 UWP 上的 類別。 這會導致使用原生 Web 控件轉譯每個 HybridWebView 自定義控件,如下列螢幕快照所示:

每個平台上的 HybridWebView

WkWebViewRendererWebViewRenderer 類別會公開 OnElementChanged 方法,這個方法會在建立自定義控件以轉譯對應的原生Web控件時Xamarin.Forms呼叫。 這個方法會採用 VisualElementChangedEventArgs 包含 OldElementNewElement 屬性的參數。 這些屬性代表Xamarin.Forms轉譯器附加的專案,以及Xamarin.Forms轉譯器分別附加至的專案。 在範例應用程式中,OldElement 屬性會是 null,而 NewElement 屬性會包含 HybridWebView 執行個體的參考。

在每個平台轉譯器類別中,方法的 OnElementChanged 覆寫版本是執行原生 Web 控件自定義的位置。 可以透過屬性取得所轉譯之控件的Element參考Xamarin.Forms。

每個自定義轉譯器類別都會以 ExportRenderer 向 註冊轉譯器 Xamarin.Forms的屬性裝飾。 屬性會採用兩個參數 :要轉譯之 Xamarin.Forms 自定義控件的類型名稱,以及自定義轉譯器的型別名稱。 屬性的 assembly 前置詞會指定套用至整個組件的屬性。

下列各節將討論每個原生 Web 控制項所載入的網頁結構、從 JavaScript 叫用 C# 的程式,以及每個平臺自定義轉譯器類別的實作。

建立網頁

下列程式碼範例示範將由 HybridWebView 自訂控制項顯示的網頁:

<html>
<body>
    <script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
    <h1>HybridWebView Test</h1>
    <br />
    Enter name: <input type="text" id="name">
    <br />
    <br />
    <button type="button" onclick="javascript: invokeCSCode($('#name').val());">Invoke C# Code</button>
    <br />
    <p id="result">Result:</p>
    <script type="text/javascript">function log(str) {
            $('#result').text($('#result').text() + " " + str);
        }

        function invokeCSCode(data) {
            try {
                log("Sending Data:" + data);
                invokeCSharpAction(data);
            }
            catch (err) {
                log(err);
            }
        }</script>
</body>
</html>

網頁可讓使用者在 input 項目中輸入他們的姓名,並提供按一下即可叫用 C# 程式碼的 button 項目。 執行這項作業的程序如下:

  • 當使用者按一下 button 項目時,即會呼叫 invokeCSCode JavaScript 函式,並將 input 項目的值傳遞至函式。
  • invokeCSCode 函式會呼叫 log 函式,顯示它傳送到 C# Action 的資料。 然後,它會呼叫 invokeCSharpAction 方法來叫用 C# Action,傳遞從 input 項目收到的參數。

invokeCSharpAction JavaScript 函式不是在網頁中定義,而是由每個自訂轉譯器插入至網頁。

在 iOS 上,此 HTML 檔案位於具有 BundleResource 建置動作的平台專案 [內容] 資料夾中。 在 Android 上,此 HTML 檔案則位於具有 AndroidAsset 建置動作的平台專案 [資產/內容] 資料夾中。

從 JavaScript 叫用 C#

每個平台從 JavaScript 叫用 C# 的程序都完全相同:

  • 自訂轉譯器會建立原生 Web 控制項,並載入 HybridWebView.Uri 屬性指定的 HTML 檔案。
  • 一旦載入網頁,自訂轉譯器就會將 invokeCSharpAction JavaScript 函式插入至網頁。
  • 當使用者輸入其名稱並按一下 HTML button 項目時,即會叫用 invokeCSCode 函式,然後接著叫用 invokeCSharpAction 函式。
  • invokeCSharpAction 函式會在自訂轉譯器中叫用方法,再接著叫用 HybridWebView.InvokeAction 方法。
  • HybridWebView.InvokeAction 方法叫用已註冊的 Action

下列各節會討論如何在每個平台上實作此程序。

在 iOS 上建立自定義轉譯器

下列程式碼範例示範適用於 iOS 平台的自訂轉譯器:

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace CustomRenderer.iOS
{
    public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
    {
        const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
        WKUserContentController userController;

        public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
        {
        }

        public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
        {
            userController = config.UserContentController;
            var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
            userController.AddUserScript(script);
            userController.AddScriptMessageHandler(this, "invokeAction");
        }

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                userController.RemoveAllUserScripts();
                userController.RemoveScriptMessageHandler("invokeAction");
                HybridWebView hybridWebView = e.OldElement as HybridWebView;
                hybridWebView.Cleanup();
            }

            if (e.NewElement != null)
            {
                string filename = Path.Combine(NSBundle.MainBundle.BundlePath, $"Content/{((HybridWebView)Element).Uri}");
                LoadRequest(new NSUrlRequest(new NSUrl(filename, false)));
            }
        }

        public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            ((HybridWebView)Element).InvokeAction(message.Body.ToString());
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ((HybridWebView)Element).Cleanup();
            }
            base.Dispose(disposing);
        }        
    }
}

HybridWebViewRenderer 類別會將 HybridWebView.Uri 屬性中所指定網頁載入到原生的 WKWebView 控制項中,而 invokeCSharpAction JavaScript 函式會插入至網頁。 一旦使用者輸入其名稱並按一下 HTML button 項目,即會使用網頁收到訊息後呼叫的 DidReceiveScriptMessage 方法來執行 invokeCSharpAction JavaScript 函式。 接著,此方法會叫用 HybridWebView.InvokeAction 方法,這樣會叫用已註冊的動作來顯示快顯。

執行此功能的程序如下:

  • 轉譯器建構函式會 WkWebViewConfiguration 建立 物件,並擷取其 WKUserContentController 物件。 對象 WkUserContentController 允許張貼訊息,並將使用者腳本插入網頁。
  • 轉譯器建構函式會 WKUserScript 建立 物件,在載入網頁之後,將JavaScript函式插入 invokeCSharpAction 網頁。
  • 轉譯器建構函式會呼叫 WKUserContentController.AddUserScript 方法,將 物件新增 WKUserScript 至內容控制器。
  • 轉譯器建構函式會呼叫 WKUserContentController.AddScriptMessageHandler 方法,將名為 invokeAction 的腳本訊息處理程式新增至 WKUserContentController 物件,這會導致 JavaScript 函window.webkit.messageHandlers.invokeAction.postMessage(data)式定義於所有使用 WKUserContentController 物件之實例的所有WebView框架中。
  • 前提是自定義轉譯器已附加至新 Xamarin.Forms 元素:
    • WKWebView.LoadRequest 方法會載入 HybridWebView.Uri 屬性所指定的 HTML 檔案。 程式碼指定該檔案會儲存在專案的 Content 資料夾中。 一旦顯示網頁,即將 invokeCSharpAction JavaScript 函式插入至網頁。
  • 當轉譯器附加至變更的專案時,就會釋放資源。
  • 處置轉譯器時,會 Xamarin.Forms 清除專案。

注意

僅 iOS 8 和更新版本支援 WKWebView 類別。

此外,必須更新 Info.plist 以包含下列值:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

在Android上建立自定義轉譯器

下列程式碼範例示範適用於 Android 平台的自訂轉譯器:

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace CustomRenderer.Droid
{
    public class HybridWebViewRenderer : WebViewRenderer
    {
        const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
        Context _context;

        public HybridWebViewRenderer(Context context) : base(context)
        {
            _context = context;
        }

        protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                Control.RemoveJavascriptInterface("jsBridge");
                ((HybridWebView)Element).Cleanup();
            }
            if (e.NewElement != null)
            {
                Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
                Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
                Control.LoadUrl($"file:///android_asset/Content/{((HybridWebView)Element).Uri}");
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ((HybridWebView)Element).Cleanup();
            }
            base.Dispose(disposing);
        }        
    }
}

HybridWebViewRenderer 類別會將 HybridWebView.Uri 屬性中所指定網頁載入到原生的 WebView 控制項中,並在網頁載入完成後,使用 JavascriptWebViewClient 類別的 OnPageFinished 覆寫將 invokeCSharpAction JavaScript 函式插入至網頁:

public class JavascriptWebViewClient : FormsWebViewClient
{
    string _javascript;

    public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
    {
        _javascript = javascript;
    }

    public override void OnPageFinished(WebView view, string url)
    {
        base.OnPageFinished(view, url);
        view.EvaluateJavascript(_javascript, null);
    }
}

一旦使用者輸入其名稱並按一下 HTML button 項目,即會執行 invokeCSharpAction JavaScript 函式。 執行此功能的程序如下:

  • 前提是自定義轉譯器已附加至新 Xamarin.Forms 元素:
    • 方法會將 SetWebViewClient 新的 JavascriptWebViewClient 物件設定為的實作 WebViewClient
    • WebView.AddJavascriptInterface 方法會將新的 JSBridge 執行個體插入至 WebView JavaScript 內容的主框架,將它命名為 jsBridge。 以便從 JavaScript 存取 JSBridge 類別中的方法。
    • WebView.LoadUrl 方法會載入 HybridWebView.Uri 屬性所指定的 HTML 檔案。 程式碼指定該檔案會儲存在專案的 Content 資料夾中。
    • JavascriptWebViewClient 類別中,invokeCSharpAction JavaScript 函式會在頁面載入完成後插入至網頁。
  • 當轉譯器附加至變更的專案時,就會釋放資源。
  • 處置轉譯器時,會 Xamarin.Forms 清除專案。

invokeCSharpAction JavaScript 函式執行時,它會依序叫用 JSBridge.InvokeAction 方法,如下列程式碼範例所示:

public class JSBridge : Java.Lang.Object
{
    readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer;

    public JSBridge(HybridWebViewRenderer hybridRenderer)
    {
        hybridWebViewRenderer = new WeakReference<HybridWebViewRenderer>(hybridRenderer);
    }

    [JavascriptInterface]
    [Export("invokeAction")]
    public void InvokeAction(string data)
    {
        HybridWebViewRenderer hybridRenderer;

        if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
        {
            ((HybridWebView)hybridRenderer.Element).InvokeAction(data);
        }
    }
}

類別必須衍生自 Java.Lang.Object,而向 JavaScript 公開的方法則必須使用 [JavascriptInterface][Export] 屬性裝飾。 因此,當 invokeCSharpAction JavaScript 函式插入至網頁且執行時,它會因為正使用 [JavascriptInterface][Export("invokeAction")] 屬性裝飾而呼叫 JSBridge.InvokeAction 方法。 接著, InvokeAction 方法會叫 HybridWebView.InvokeAction 用 方法,這會叫用已註冊的動作以顯示快顯。

重要

使用 屬性的 [Export] Android 項目必須包含 的 Mono.Android.Export參考,否則會產生編譯程序錯誤。

請注意,JSBridge 類別會將 WeakReference 維持為 HybridWebViewRenderer 類別。 這是為了避免建立兩個類別之間的循環參考。 如需詳細資訊,請參閱 弱式參考

在UWP上建立自定義轉譯器

下列程式碼範例示範適用於 UWP 的自訂轉譯器:

[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace CustomRenderer.UWP
{
    public class HybridWebViewRenderer : WebViewRenderer
    {
        const string JavaScriptFunction = "function invokeCSharpAction(data){window.external.notify(data);}";

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement != null)
            {
                Control.NavigationCompleted -= OnWebViewNavigationCompleted;
                Control.ScriptNotify -= OnWebViewScriptNotify;
            }
            if (e.NewElement != null)
            {
                Control.NavigationCompleted += OnWebViewNavigationCompleted;
                Control.ScriptNotify += OnWebViewScriptNotify;
                Control.Source = new Uri($"ms-appx-web:///Content//{((HybridWebView)Element).Uri}");
            }
        }

        async void OnWebViewNavigationCompleted(Windows.UI.Xaml.Controls.WebView sender, WebViewNavigationCompletedEventArgs args)
        {
            if (args.IsSuccess)
            {
                // Inject JS script
                await Control.InvokeScriptAsync("eval", new[] { JavaScriptFunction });
            }
        }

        void OnWebViewScriptNotify(object sender, NotifyEventArgs e)
        {
            ((HybridWebView)Element).InvokeAction(e.Value);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ((HybridWebView)Element).Cleanup();
            }
            base.Dispose(disposing);
        }        
    }
}

HybridWebViewRenderer 類別會將 HybridWebView.Uri 屬性中所指定網頁載入到原生的 WebView 控制項中,並在網頁載入後,使用 WebView.InvokeScriptAsync 方法將 invokeCSharpAction JavaScript 函式插入至網頁。 一旦使用者輸入其名稱並按一下 HTML button 項目,即會使用網頁收到通知後呼叫的 OnWebViewScriptNotify 方法來執行 invokeCSharpAction JavaScript 函式。 接著,此方法會叫用 HybridWebView.InvokeAction 方法,這樣會叫用已註冊的動作來顯示快顯。

執行此功能的程序如下:

  • 前提是自定義轉譯器已附加至新 Xamarin.Forms 元素:
    • 則會註冊 NavigationCompletedScriptNotify 事件的事件處理常式。 當原生 WebView 控制項已完成載入目前的內容或導覽失敗時,會引發 NavigationCompleted 事件。 當原生 WebView 控制項使用 JavaScript 將字串傳遞至應用程式時,會引發 ScriptNotify 事件。 網頁在傳遞 string 參數時呼叫 window.external.notify,藉以引發 ScriptNotify 事件。
    • WebView.Source 屬性設為 HybridWebView.Uri 屬性所指定之 HTML 檔案的 URI。 程式碼假設該檔案會儲存在專案的 Content 資料夾中。 一旦顯示網頁,就會引發 NavigationCompleted 事件並叫用 OnWebViewNavigationCompleted 方法。 然後使用 WebView.InvokeScriptAsync 方法將 invokeCSharpAction JavaScript 函式插入至網頁,假設導覽已順利完成。
  • 當轉譯器附加至變更的專案時,事件會取消訂閱。
  • 處置轉譯器時,會 Xamarin.Forms 清除專案。