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

# Xamarin 無償化！

## //build/ 2016にて

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

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

## Xamarin.Forms

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

さて、今回は基本的にUWPでの話で、サンプルソースは[GitHub](https://github.com/yakumo/XamarinNativeViewTest)に置いています。

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

## Xamarin.FormsでNative Viewを使う

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

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

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

### サンプル（[Test1.uwp/MainPage.xaml.cs](https://github.com/yakumo/XamarinNativeViewTest/blob/master/Test1.uwp/MainPage.xaml.cs)）

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

まずは8行目、

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

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

次に11行目、

```csharp
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行目で宣言しています。

```csharp
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行目、

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

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

```csharp
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](https://github.com/yakumo/XamarinNativeViewTest/blob/master/Test2.uwp/MainPage.xaml.cs)）

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

```csharp
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からレンダラーを取得して子要素として付け加えるとコントロールツリーの中に追加されて使えるようにはなります。

```csharp
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でデバッグ実行してライブビジュアルツリーで見ていただくと、以下のようになります。

![ライブビジュアルツリー表示.PNG](https://qiita-image-store.s3.amazonaws.com/0/59270/a4520af5-a990-7c9f-e3a1-fff2d19e0add.png)

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

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