himanago

Azure・C#などのMS系技術やLINE関連技術など、好きな技術について書くブログ

【Uno Platform】WebAssembly 実行時の JavaScript 相互運用

Uno Platform は、UWP を iOS/Android や WebAssembly で動くようにするものですが、WebAssembly で Uno を動かすときには、C#/XAML で足りない部分を補ったり、既存の JavaScript ライブラリ/コードを併用したりするために、C#JavaScript を相互に呼び合いたい場面が出てきます。

同じく C# で WebAssembly を実現する Blazor でも JavaScript との相互運用機能 が用意されていますが、Uno Platform でもできるようなので試してみました。

JavaScript ファイルの使用

Wasm のプロジェクトにある WasmScripts フォルダに js ファイルを「埋め込みリソース」として配置すれば、読み込んでくれます。既存のライブラリなどを読み込むこともできますね。

f:id:himanago:20191229120655p:plain

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 だとこうなりますが、

f:id:himanago:20191229145334p:plain

WebAssembly で実行するとこのようになります。

f:id:himanago:20191229144325p:plain

ちゃんと JavaScript コードが呼ばれていますね。

特定要素に対する処理を JS にやらせる

JavaScript のコードを書く際、C#/XAML で生成した特定のコントロールJavaScript に操作させたくなります。

そのために使えるのがコントロールHtmlId プロパティで、生成された HTML 要素の id 属性として使われる値がとれます。これを JavaScript に渡せば特定のコントロールに対する DOM 操作などを簡単に書けます。

たとえば、TextBoxIsReadOnly プロパティは 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 すると、そこで登録したイベントハンドラJavaScriptdispatchEvent を受け取れます。

気を付けたいのは、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#クロスプラットフォーム対応できるという利点が薄れてしまうので多用しすぎないように注意したいところです。

上記検証に使ったサンプルは下記に置きました。

GitHub - himanago/UnoJavaScriptInteropSample