Xamarin無償化をよそにXamarin.FormsビューとUWPネイティブビューの融合の話


Xamarin 無償化!

//build/ 2016にて

Xamarinが無償化しましたね。
待ちに待ってた情報です。
昨年Windows Phone関連でライセンスもらってたんですが…。
これからも使える!というのは大きいです。

ま、今回はそれはそれとして…。

Xamarin.Forms

Xamarin.Formsというのは、iOS / Android / WindowsPhoneで共通に使える画面表示の仕組みを実装したものです。
以前は上記3系統でしたが、今ではWindows 8 Store / UWPも対応しているので、デスクトップからモバイルまでまさに1コードで同一の操作感を実現できる仕組みです。
詳しくは、メインサイト日本語情報等を参考にしてください。
このあたりは先刻ご存知、ということで話を進めます。

さて、今回は基本的にUWPでの話で、サンプルソースはGitHubに置いています。

Xamarin.FormsビューとUWPネイティブビューの融合

Xamarin.FormsでNative Viewを使う

Xamarin.Formsのビューは、UWPの標準ビューを「Xamarin.Formsのビュークラス」と「レンダラークラス」という薄皮二枚かませて他のプラットフォームとの互換性を持たせています。
実際に使用者側が使うのがビュークラス、 ライブラリ作成者側が苦労する 使用者から隠蔽されているのがレンダラークラス、になり、Nativeのビューはレンダラーが作成と保持、ビュークラスに設定したプロパティをNativeのビューに反映させる、という動作をします。

レンダラーはViewRenderer<>を継承したクラスをしています。
これはパラメータに2つのTypeを指定し、前がXamarin.FormsのViewクラス、次がNative Viewクラスです。
この設定によってこのレンダラーがViewとNative Viewを関連づけている、ということを宣言しています。

また、ソースファイルの最初に assembly: ExportRenderer を書かなければいけません。
これは引数にXamarin.FormsのViewとレンダラーのTypeを取ります。
この宣言によって、Viewとレンダラーを関連づけています。

サンプル(Test1.uwp/MainPage.xaml.cs

このサンプルはXamarin.FormsでNative Viewを使うだけのものになっています。

まずは8行目、

[assembly: ExportRenderer(typeof(Test1.uwp.XamarinView.NativeView), typeof(Test1.uwp.Native.NativeViewRenderer))]

です。
assembly: は、ソースの最初に書かなければいけない、ということでここに書いてあります。
この宣言で Test1.uwp.XamarinView.NativeView のレンダラーは Test1.uwp.Native.NativeViewRenderer であることを宣言しています。

次に11行目、

public class NativeViewRenderer : ViewRenderer<Test1.uwp.XamarinView.NativeView, Windows.UI.Xaml.Controls.TextBlock>

これで NativeViewRenderer クラスは Test1.uwp.XamarinView.NativeView を表示する際に Windows.UI.Xaml.Controls.TextBlock を使うことを宣言しています。

そして、Xamarin.FormsのViewの NativeView は72行目で宣言しています。

public class NativeView : View
{
    public static readonly BindableProperty TextProperty = BindableProperty.Create("ItemTextWidth", typeof(string), typeof(string), String.Empty, BindingMode.OneWay, null, null, null, null);

    public string Text
    {
        get { return (string)base.GetValue(NativeView.TextProperty); }
        set { base.SetValue(NativeView.TextProperty, value); }
    }
}

Viewを継承してTextプロパティを宣言しているだけのクラスですね。

このクラスを実際に作っているのは106行目、

Content = new Test1.uwp.XamarinView.NativeView()
{
    Text = "Hello, world !!",
},

NativeViewを作ると、自動的にレンダラーが作られ、レンダラーの OnElementChanged 関数が呼ばれます。
これは15行目にあり、その中の19行目からの

if (Control == null)
{
     nativeView = new Windows.UI.Xaml.Controls.TextBlock()
     {
        FontSize = 60,
     };
     SetNativeControl(nativeView);
}

ここでNative Viewが設定されていなければ作って設定しています。

Xamarin.FormsのViewの子要素としてNative Viewを配置する

これは先の NativeView を、そのまま子要素を持てるXamarin.Formsのビューの子要素として追加するだけです。

サンプル(Test2.uwp/MainPage.xaml.cs

今度はTest1.uwpから NativeViewNativeViewRenderer はそのまま、106行からのViewの定義のみ変更しています。

Content = new StackLayout()
{
    HorizontalOptions = LayoutOptions.Center,
    VerticalOptions = LayoutOptions.Center,
    Padding = new Thickness(50),
    BackgroundColor = Color.Aqua,
    Children = {
        new Test2.uwp.XamarinView.NativeView()
        {
            Text = "Hello, world !!",
            BackgroundColor = Color.White,
        },
    },
}

Test1.uwpのViewの作成時に NativeView の親要素として StackLayout を追加しました。
これでウインドウ中央、50pxのボーダーに囲まれたテキスト表示になります。

Native Viewの子要素としてXamarin.FormsのViewを配置する

さて。
すでにあるNative Viewや、上記で作ったレンダラーが保持しているNative View に直接Xamarin.FormsのViewを配置するにはどうしたらいいでしょうか。

実は、ViewRendererは Windows.UI.Xaml.Controls.Panel を継承しているので、Viewからレンダラーを取得して子要素として付け加えるとコントロールツリーの中に追加されて使えるようにはなります。

StackPanel basePanel = new StackPanel()
{
    Name = "TextBase",
    HorizontalAlignment = HorizontalAlignment.Stretch,
    VerticalAlignment = VerticalAlignment.Stretch,
};
Label textLabel = new Label()
{
    HorizontalOptions = LayoutOptions.FillAndExpand,
    VerticalOptions = LayoutOptions.Start,
    HorizontalTextAlignment = Xamarin.Forms.TextAlignment.Center,
    VerticalTextAlignment = Xamarin.Forms.TextAlignment.Start,
    FontSize = 40,
    Text = "Hello, world",
};
LabelRenderer renderer = (LabelRenderer)textLabel.GetOrCreateRenderer();
basePanel.Children.Add(renderer);

こんな感じですね。

ところで、なんで「使えるように なります」、というような書き方をしているかといいますと。
実はこのままではtextLabelは表示されません。
Windows10 + Visual Studio 2015 Update 2でデバッグ実行してライブビジュアルツリーで見ていただくと、以下のようになります。

TextBase [StackPanel] の子要素の [Panel] つまり、LabelRendererのRenderSizeが0x0になっています。
ViewRendererはArrangeOverrideやMeasureOverrideのメソッドを持っており、中でオーバーライドしているはずなのですが、大きさ計算が想定される値を計算していないようです。
nativeView.LayoutUpdated 内でrendererやtextLabelをArrangeしてやるとちゃんと表示されるので、計算するようにしてしまえば動作はさせられますが、せっかく 手抜きのために Xamarin.Formsを使っているのにコードが増えてしまったら本末転倒です。

本当はこれがやりたかったんですが、どうもそういうわけで一旦あきらめたのでした。