Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編)

Xamarin.Forms ではイベント絡みが隠蔽化されていて、ボタンのクリックイベントやテキストボックスの変更イベントぐらいしか発生しません。このため、パズルゲームでは画像(Imageタグ)のタップイベントを Xamarin.Froms でパズルゲームを作る(iOS/Android版) | Moonmile Solutions Blog のように TapGestureRecognizer を使っています。(今後はどうか分からないのですが)TapGestureRecognizer クラスでは、情報が何もわたってこなくてタップなりスワイプなりの操作をしようとすると、ネイティブな UI コントロールに切り替える必要が出てきます。なので、さっくっと作れそうなページであれば Xamarin.Forms で、複雑なコントロールの組み合わせであればネイティブで、ってことになるんでしょうが…いや、ちょっと待てよ。それぞれのプラットフォームでは、Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this); のように、Xamarin.Forms の XAML から各プラットフォームへのコンバータが動いています。Windows Phone の場合は ConvertPageToUIElement、iOS の場合は CreateViewController が使われています。Android の場合は SetPage が直接呼び出されていて中身が不明ですが、たぶん内容は似た感じになっているハズです。

これは、Xamarin.Forms 製の XAML から、MS 製の XAML にコンバートしていることを示しているわけで、何等かの形で内部で Windows Phone 特有のコントロールにして持っています。少し調べていて、レンダリングの部分で Xamarin.Forms の Label から iOS の UILabel を取り出す – Qiita のように LabelRenderer などで変更できることが解りました。更に調べていくと、なんとか Renderer のようなものがいっぱいあります。

image

実は、直前のバージョンでは EntryRenderer が none public になっていて手が出せなかったのですが(iOS と Android の Renderer は public になっていました)。何故か、つい最近公開された、1.1.0.6201 では、ここの Renderer が公開になっていました。ええ、ついでに内部でペアでもっていてる Element プロパティと Control プロパティも public になっています(直前のバージョンでは、名前すら違っていたのは内緒です)。ここで、情報を整理すると、

  • Xamarin.Forms.ContentPage が Xamarin.Forms の XAML ツリーを持っている。
  • ConvertPageToUIElement 等を呼び出すと、MS 製の XAML ツリーを作成する。
  • 同時に、Renderer を含む UIElement のツリーを作成し、Xamarin 製と MS 製の対応をツリー状にして保持する。
  • Xamarin.Forms 製のコントロールは、Element プロパティで取得する。
  • MS 製のコントロールは、Control プロパティで取得する。

のような感じになっています。おおまかに書くとこんな感じです。中身をみるとテキストボックスの場合には、TextBox と Password の2つのコントロールを持っているので、Xamarin.Forms 側では Entry 、MS XAML では Canvs になっています。

image

なので、Xamarin.Forms 側のコントロールから、うまく MS XAML のコントロールを見つけてやれば、Tap や Mouse イベント等のネイティブなイベントを設定できるはずです。

■Renderer のツリー を覗いてみる

ためしに、Windows Phone の MainPage クラスのコンストラクタを弄って、ツリーを書き出してみます。

public MainPage()
{
    InitializeComponent();

    Forms.Init();
    Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this);
    Disp(Content);
}

 

void Disp(UIElement el, string spc = "")
{
    var pa = el as Panel;
    if (pa == null)
    {
        Debug.WriteLine("{0}{1}", spc, el.GetType().Name);
    }
    else
    {
        Debug.WriteLine("{0}{1} '{2}'", spc, pa.GetType().Name, pa.Name);
        foreach (var it in pa.Children)
        {
            Disp(it, spc + " ");
        }
    }
}

これを実行すると、こんな感じに Renderer のツリーが取得できます。Name プロパティを出力してみたのですが、残念ながら空になっています。名前は別途 FindName で検索しないと駄目っぽいです。

Canvas ''
 PageRenderer ''
  ViewRenderer ''
   LabelRenderer ''
    TextBlock
   ViewRenderer ''
    EntryRenderer ''
     Canvas ''
      PhoneTextBox
      PasswordBox
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
   ViewRenderer ''
    ImageRenderer ''
     Image
    ImageRenderer ''
     Image
    ImageRenderer ''
     Image

■ Entry コントロールを見つけ出して、イベントを設定する

しかし、Element プロパティを調べれば、対応する Control プロパティで Windows Phone のコントロールが見つかることが分かったので、試しに Entry コントロールだけチェックしてみます。

public MainPage()
{
    InitializeComponent();

    Forms.Init();
    this.page = TMPuzzleXForms.App.GetMainPage() as TMPuzzleXForms.MainPage;
    // Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this);
    this.Content = page.ConvertPageToUIElement(this);
    Disp(Content);

    SetNamePageToUIelement("textUserName", this.page);
    var obj = this.FindName("textUserName") as UIElement;
    obj.LostFocus += obj_LostFocus;
}

void obj_LostFocus(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("lost focus");
}

Xamarin.Forms 側で textUserName と名前つけたコントロールに対応する Windows Phone のコントロールを見つけ出します。そして、LostFocus 時にデバッグ出力するコードです。

ツリーから探し出すコードはこんな風になります。コントロールを直接返すのではなく、いちど Xamarin.Forms で付けた名前と同じものを Windows Phone のコントロールにもつけています。こうすると、後から FindName で見つけられるので汎用性があります。

void SetNamePageToUIelement(string name, Xamarin.Forms.Page page )
{
    var el = page.FindByName<Entry>(name);
    if (el != null)
    {
        var rend = FindRenderer( el );
        if (rend != null)
        {
            var en = rend as EntryRenderer;
            en.Control.Name = name;
        }
    }
}

UIElement FindRenderer(Entry ent)
{
    return Search( this.Content, ent );
}
UIElement Search(UIElement el, Entry ent)
{
    var pa = el as Panel;
    if (pa != null)
    {
        var en = pa as EntryRenderer;
        if (en != null && en.Element == ent)
        {
            return en;
        }
        foreach (var it in pa.Children)
        {
            var ret = Search(it, ent);
            if (ret != null)
            {
                return ret;
            }
        }
    }
    return null;
}

まあ、いちいち探索をすると遅くなってしまうので、一度 Xamarin.Forms のツリーを Renderer で探索してしまってから名前を付けるとよいでしょう。このあたりは、後日やる予定。

ネイティブのコントロールイベントが取れるので Image コントロールの Tap イベントも付加できます。おそらく、iOS/Android も同じ方式でできると思うので、このあたりは共通して使えるようにしていきます。ちょっとやっかいなのは、EntryRenderer クラスは共通のインターフェースから継承されていなくて、ViewRenderer<Entry, System.Windows.Controls.Canvas> な感じでひとつひとつジェネリックが使われてるってところですね。取得したいところが、Control プロパティと Element プロパティなので、共通に持っている Canvas では駄目という…ここは各コントロールごとに書くしかないのかな。あるいはリフレクションを使うとうまく作れるかも。

    public class VisualElementRenderer<TElement, TNativeElement> : Canvas, IVisualElementRenderer, IRegisterable
        where TElement : Xamarin.Forms.VisualElement
        where TNativeElement : System.Windows.FrameworkElement
    {
        public VisualElementRenderer();

        protected bool AutoPackage { get; set; }
        protected bool AutoTrack { get; set; }
        public UIElement ContainerElement { get; }
        public TNativeElement Control { get; }
        public TElement Element { get; }
        protected VisualElementTracker Tracker { get; set; }

このテクニックを、月曜日のコンテストに間に合わせるか…どうかは不明。どうせならば汎用的に作っておきたいし、Image コントロールのタップは3機種同じように作りたいので。

カテゴリー: Windows Phone, Xamarin パーマリンク