上一篇文章我們簡單介紹了一下 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 方法,官網的說明介紹了三種方法:
- 實作 IRenderProcessMessageHandler 介面,將其實例指定給 ChromiumWebBrowser.RenderProcessMessageHandler 屬性,在 OnContextCreated() 方法被呼叫時,始可執行 JavaScript。
- 註冊 ChromiumWebBrowser.LoadingStateChanged 事件,在 LoadingStateChangedEventArgs.IsLoading 為 false 時,始可執行 JavaScript。
- 註冊 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 API 的 IgnoreCache
選項或 RemoveObjectFromCache()
方法,加上 JavascriptBindingEventArgs.ObjectRepository.UnRegisterAll()
方法來釋放記憶體空間。
傳遞基礎型別
互相呼叫方法我們會了之後,傳遞參數及回傳值就簡單多了,其中常見的基礎型別 int
、double
、date
、bool
、string
是完全支援的。
// .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 之間,互相呼叫方法並且傳遞參數及回傳值的方法,就分享給各位朋友,希望對大家有一點幫助。