[食譜好菜] CefSharp 在 JavaScript 與 .NET 之間互相呼叫方法並且傳遞參數及回傳值

上一篇文章我們簡單介紹了一下 CefSharp 的基本使用方式,直接在 WinForms/WPF 內嵌一個瀏覽器,基本上我們所有的程式邏輯都可以引入 JavaScript 套件,用 JavaScript 來開發,直接在瀏覽器端執行,但是這不代表我們不能把工作丟給 .NET 來做,必要時我們還是可以搭配 .NET 來平衡一下應用程式的工作負載,因此在 JavaScript 與 .NET 之間傳遞參數及回傳值就很重要了,這篇文章我們就來探究一下使用 CefSharp 套件,如何在 JavaScript 與 .NET 之間傳遞參數及回傳值?以及能夠傳遞的參數及回傳值的類型有哪些?

.NET 呼叫 JavaScript 方法

第一步我們先來了解在 CefSharp 中,JavaScript 與 .NET 之間如何互相呼叫方法,.NET 要呼叫 JavaScript 的方法相對簡單一些,分為兩種:無回傳值有回傳值

var browser = new ChromiumWebBrowser { Dock = DockStyle.Fill };

// 無回傳值
browser.ExecuteScriptAsync("goToHome();");
browser.ExecuteScriptAsync("goTo", "/");

// 有回傳值
var res1 = await browser.EvaluateScriptAsync("getNestedObjectList();");
var res2 = await browser.EvaluateScriptAsync("add", 1, 2);
var res3 = await browser.EvaluateScriptAsPromiseAsync("return addAsPromise(1, 2);"); // Promise Function

何時能開始呼叫 JavaScript 方法?

JavaScript 的方法也不是隨時能呼叫的,必須等到相關資源載入後才能,而要知道何時能開始呼叫 JavaScript 方法,官網的說明介紹了三種方法:

  1. 實作 IRenderProcessMessageHandler 介面,將其實例指定給 ChromiumWebBrowser.RenderProcessMessageHandler 屬性,在 OnContextCreated() 方法被呼叫時,始可執行 JavaScript。
  2. 註冊 ChromiumWebBrowser.LoadingStateChanged 事件,在 LoadingStateChangedEventArgs.IsLoading 為 false 時,始可執行 JavaScript。
  3. 註冊 ChromiumWebBrowser.FrameLoadEnd 事件,在 FrameLoadEndEventArgs.Frame.IsMain 屬性為 true 時,始可執行 JavaScript。

不過在這邊我要介紹一個更簡單的方法 - IChromiumWebBrowserBase.ExecuteScriptAsyncWhenPageLoaded(),這個擴充方法可以保證 JavaScript 會在頁面已載入的時候執行。

JavaScript 呼叫 .NET 方法

而 JavaScript 要呼叫 .NET 的方法,我們要使用 JSB(JavaScript Binding)的方式,簡單來說就是將 .NET 類別的實例,綁定到 JavaScript 的一個全域變數上,中間由 CefSharp 幫我們透過原生的 Chromium IPC(Inter-process Communication)進行呼叫,假定我有一個類別叫 IndexViewBinding,裡面有一個兩數相加的方法。

public class IndexViewBinding
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

而綁定的動作是由 JavaScript 這一端來驅動,我們使用 CefSharp 提供的 CefSharp.BindObjectAsync() 方法,參數則是傳入全域變數的名稱,完成綁定之後,我們就能進行呼叫。

(async function () {
    await CefSharp.BindObjectAsync("indexViewBinding");

    const result = await window.indexViewBinding.add(1, 2);

    alert(result);
})();

在 .NET 這一端我們需要註冊 ChromiumWebBrowser.JavascriptObjectRepository.ResolveObject 事件,透過傳進來的全域變數名稱來判斷要綁定的類別實例。

var browser = new ChromiumWebBrowser { Dock = DockStyle.Fill };

browser.JavascriptObjectRepository.ResolveObject += (sender, e) =>
    {
        if (e.ObjectName == "indexViewBinding")
        {
            e.ObjectRepository.Register(e.ObjectName, new IndexViewBinding());
        }
    };

這樣子就把 JavaScript 跟 .NET 串起來了,只不過我們需要額外注意一件事,無論在 JavaScript 或是 .NET,綁定的物件預設都會被快取一份起來,所以這裡要當心 Memory Leak 的風險,必要時使用 JavaScript Binding APIIgnoreCache 選項或 RemoveObjectFromCache() 方法,加上 JavascriptBindingEventArgs.ObjectRepository.UnRegisterAll() 方法來釋放記憶體空間。

傳遞基礎型別

互相呼叫方法我們會了之後,傳遞參數及回傳值就簡單多了,其中常見的基礎型別 intdoubledateboolstring 是完全支援的。

// .NET pass/receive primitive data types to/from JavaScript
// in .NET
var browser = new ChromiumWebBrowser { Dock = DockStyle.Fill };

browser.LoadingStateChanged += async (sender, args) =>
    {
        if (args.IsLoading == false)
        {
            var response = await browser.EvaluateScriptAsync("passPrimitiveDataTypes", 1, 0.1, DateTime.Now, true, "str");

            var result = (int)response.Result;
        }
    };

// in JavaScript
function passPrimitiveDataTypes(a, b, c, d, e) {
    return a;
}

// JavaScript pass/receive primitive data types to/from .NET
// in .NET
public class IndexViewBinding
{
    // ...
    
    public int PassPrimitiveDataTypes(int a, double b, DateTime c, bool d, string e)
    {
        return a;
    }
}

// in JavaScript
(async function () {
    await CefSharp.BindObjectAsync("indexViewBinding");

    const result = await window.indexViewBinding.passPrimitiveDataTypes(1, 0.1, new Date(), true, "str");
})();

傳遞物件

傳遞物件的話就有一點變化了,由於 ExecuteScriptAsync() 及 EvaluateScriptAsync() 無法支援複雜型別,所以傳遞物件到 JavaScript 無法很直接地傳遞過去,需要轉一手。

// .NET pass/receive object to/from JavaScript
// in .NET
var browser = new ChromiumWebBrowser { Dock = DockStyle.Fill };

browser.LoadingStateChanged += async (sender, args) =>
    {
        if (args.IsLoading == false)
        {
            var o = new TestData { Id = 1, Name = "Johnny", Test = new TestData { Id = 2, Name = "Amy" } };

            var response = await browser.EvaluateScriptAsync($"passObject({JsonSerializer.Serialize(o, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })});");

            var result = (dynamic)response.Result;
        }
    };

// in JavaScript
function passObject(o) {
    return o;
}

而 JavaScript 傳遞物件到 .NET 則一律使用 dynamic 型別接收下來,所以基本上比較沒有什麼問題。

// JavaScript pass/receive object to/from .NET
// in .NET
public class IndexViewBinding
{
    // ...
    
    public TestData PassObject(dynamic o)
    {
        return new TestData { Id = o.id, Name = o.name, Test = new TestData { Id = o.test.id, Name = o.test.name } };
    }
}

// in JavaScript
(async function () {
    await CefSharp.BindObjectAsync("indexViewBinding");

    const result = await window.indexViewBinding.passObject({ id: 1, name: "Johnny", test: { id: 2, name: "Mary" } });
})();

使用 JSB,如果習慣 Camel Case 命名方式的話,將 NameConverter 指定為 CamelCaseJavascriptNameConverter 即可。

var browser = new ChromiumWebBrowser { Dock = DockStyle.Fill };

browser.JavascriptObjectRepository.NameConverter = new CamelCaseJavascriptNameConverter();

傳遞二進位資料

二進位資料除了可以用 Base64 編碼來表示之外,還可以用八進位數值陣列來表示,所以關於傳遞二進位資料就有兩種資料型別可以用:字串數值陣列,我這邊就只展示後者。

// .NET pass/receive binary to/from JavaScript
// in .NET
var browser = new ChromiumWebBrowser { Dock = DockStyle.Fill };

browser.LoadingStateChanged += async (sender, args) =>
    {
        if (args.IsLoading == false)
        {
            var bin = Encoding.UTF8.GetBytes("abc123");

            var response = await browser.EvaluateScriptAsync($"passBinary([{string.Join(',', bin)}]);");

            var result = ((IDictionary<string, object>)response.Result).Select(kv => Convert.ToByte(kv.Value)).ToArray();
        }
    };

// in JavaScript
function passBinary(bin) {
    return new Uint8Array(bin);
}

// JavaScript pass/receive object to/from .NET
// in .NET
public class IndexViewBinding
{
    // ...
    
    public byte[] PassBinary(dynamic bin)
    {
        return ((IDictionary<string, object>)bin).Select(kv => Convert.ToByte(kv.Value)).ToArray();
    }
}

// in JavaScript
(async function () {
    await CefSharp.BindObjectAsync("indexViewBinding");

    const result = await window.indexViewBinding.passBinary(new TextEncoder("utf-8").encode("abc123"));
})();

傳遞 JavaScript Callback

Function 在 JavaScript 來說也算是物件,所以被當作參數值傳來傳去也是正常的事情,因此 JavaScript 呼叫 .NET 方式時一樣可以傳入 Function 當參數,在適當的時機點進行呼叫。

JavaScript 的 Function 傳入 .NET 會被轉成 IJavascriptCallback,呼叫 ExecuteAsync() 就可以執行它,而且還能代入參數。

// JavaScript pass function to .NET
// in .NET
public class IndexViewBinding
{
    // ...
    
    public void PassFunction(IJavascriptCallback callback)
    {
        callback.ExecuteAsync(new TestData { Id = 1, Name = "Johnny", Test = new TestData { Id = 2, Name = "Mary" } });
    }
}

// in JavaScript
(async function () {
    await CefSharp.BindObjectAsync("indexViewBinding");

    window.indexViewBinding.passFunction(function (o) {
        alert(JSON.stringify(o));
    });
})();

JavaScript 跟 .NET 這兩個截然不同的生態,透過 CefSharp 彷彿融合了在一起,彼此獨立又互相合作,頓時覺得 CefSharp 的開發者真是不簡單,相信開發的過程應該遭遇到不少大大小小的問題,以上,使用 CefSharp 套件要在 JavaScript 與 .NET 之間,互相呼叫方法並且傳遞參數及回傳值的方法,就分享給各位朋友,希望對大家有一點幫助。

參考資料

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學