Uno Platform は、UWP を iOS/Android や WebAssembly で動くようにするものですが、WebAssembly で Uno を動かすときには、C#/XAML で足りない部分を補ったり、既存の JavaScript ライブラリ/コードを併用したりするために、C# と JavaScript を相互に呼び合いたい場面が出てきます。
同じく C# で WebAssembly を実現する Blazor でも JavaScript との相互運用機能 が用意されていますが、Uno Platform でもできるようなので試してみました。
JavaScript ファイルの使用
Wasm のプロジェクトにある WasmScripts
フォルダに js ファイルを「埋め込みリソース」として配置すれば、読み込んでくれます。既存のライブラリなどを読み込むこともできますね。
C# コードから JavaScript の呼び出し
JavaScript を配置できるとしても、それを C# 側から呼べないと意味がありません。
C# から呼ぶには、Uno.Foundation
名前空間にある WebAssemblyRuntime.InvokeJS
メソッドを使います。
さきほどの JavaScript ファイルの中身に、こんなかんじの適当な関数を定義しておきます。
function sayHello() { alert('Hello from JS!'); }
XAML にボタンを一つだけ置いて、コードビハインドを以下のように書いてみます。
if ディレクティブで WASM のときだけ JavaScript 呼び出しで、その他は普通に MessageDialog
です。
using System; using System.Threading.Tasks; using Windows.UI.Popups; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; #if __WASM__ using Uno.Foundation; #endif namespace UnoJavaScriptInteropSample { /// <summary> /// An empty page that can be used on its own or navigated to within a Frame. /// </summary> public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } private async void callJsButton_Click(object sender, RoutedEventArgs e) { #if __WASM__ WebAssemblyRuntime.InvokeJS("sayHello();"); #else await new MessageDialog("Hello from MessageDialog").ShowAsync(); #endif } } }
UWP だとこうなりますが、
WebAssembly で実行するとこのようになります。
ちゃんと JavaScript コードが呼ばれていますね。
特定要素に対する処理を JS にやらせる
JavaScript のコードを書く際、C#/XAML で生成した特定のコントロールを JavaScript に操作させたくなります。
そのために使えるのがコントロールの HtmlId
プロパティで、生成された HTML 要素の id
属性として使われる値がとれます。これを JavaScript に渡せば特定のコントロールに対する DOM 操作などを簡単に書けます。
たとえば、TextBox
の IsReadOnly
プロパティは WebAssembly では対応しておらず、True
にしても UWP では当然読み取り専用になるので、これと WebAssembly の動きをそろえたい…という場合に以下のようにテキストボックスを readonly にする関数に対し id を渡すよう、以下のように実装すれば対応可能です。
#if __WASM__ protected override void OnLoaded() { WebAssemblyRuntime.InvokeJS($"toReadonly({textBox.HtmlId});"); } #endif
テキストボックスを readonly にする関数のほうの実装ですが、C# 側の TextBox の HtmlId
でとれる値が、実は input 要素のものではなく、それを囲む外側の div である点を考慮する必要があります。
Uno Platform では、UWP のコントロールと対応する見た目の HTML 要素を WebAssembly で生成していますが、結果的に生成されるのが単一のタグで構成されたものであるとは限りません。そのため、一度どのようなタグの要素が生成されるのかを確認してから実装をする必要があります(そのため JavaScript では以下のように2段階で input 要素を探す)。
function toReadonly(id) { const box = document.getElementById(id); box.getElementsByTagName('input')[0].setAttribute('readonly', 'readonly'); }
イベントの連携
あとおもしろいのはイベントです。以下のドキュメントにも記載がありますが、この機能は JavaScript 側から dispatchEvent
を実行することで C# 側に用意したイベントハンドラを呼び出すことができます。
(Wasm) Handling custom HTML events
C# で using に Uno.Extensions
を追加して、RegisterHtmlEventHandler
/RegisterHtmlCustomEventHandler
すると、そこで登録したイベントハンドラで JavaScript の dispatchEvent
を受け取れます。
気を付けたいのは、JavaScript 側から dispatchEvent
する要素と、C# 側で RegisterHtmlEventHandler
/RegisterHtmlCustomEventHandler
するコントロールを合わせなければならないということです。これも HTML 側でどんな要素が対応しているか確認してしっかり一致させておかないとうまく動きません。
下記のコードは、テキストボックスのダブルクリックで C# 側のイベントを呼ぶ処理です。
外側の div 要素と C#/XAML 側の TextBox コントロールが一致するので、テキストボックスをダブルクリックしてイベントを走らせたければ、JavaScript で input 要素のダブルクリックイベントの処理内で、外側の div から dispatchEvent
するのがポイントです。
#if __WASM__ protected override void OnLoaded() { WebAssemblyRuntime.InvokeJS($"addDblclickEventListener({textBox.HtmlId});"); textBox.RegisterHtmlCustomEventHandler("dblclickEvent", OnDblclickEvent, isDetailJson: false); } private async void OnDblclickEvent(object sender, HtmlCustomEventArgs e) { await new MessageDialog(e.Detail).ShowAsync(); } #endif
function addDblclickEventListener(id) { const box = document.getElementById(id); const tb = box.getElementsByTagName('input')[0]; tb.addEventListener('dblclick', (e) => { box.dispatchEvent(new CustomEvent("dblclickEvent", { detail: e.target.id })); }); }
JavaScript から C# 側に文字列や JSON を渡すことができるので、わりと幅広いことができそうです。
まとめ
Uno における C# と JavaScript の相互運用をみてみました。イベントの連携などでいろいろできそうですが、あまり複雑なことをしすぎるとコードがごちゃごちゃしてしまいますし、せっかく C# でクロスプラットフォーム対応できるという利点が薄れてしまうので多用しすぎないように注意したいところです。
上記検証に使ったサンプルは下記に置きました。