前回はデバイス固有の電話機能を使ってみました。

Problem

Xamarin.Formsの2.2から各OS固有のコントロール (Native Control)をSharedプロジェクトから生成して、ContentView.ContentやLayout.Childrenに埋め込むことができるようになりました。
このあたりの説明は、P3PPP様の下記の記事が参考になります。

で、ポイントは、Sharedプロジェクトで**#ifディレクティブ**で分岐するとかする必要があるとのこと。
でも、思うわけですよ。C++ならともかく、モダンなC#でそんなことしたくないわけですよ。
ディレクティブだらけのソースなんか保守性低下するのは目に見えていますから。

で、思いました。
PCLから、Native Controlを使えないか?って。

Resolution

仕組みは単純で、前回話したDependencyServiceを上手く使うだけです。
各プロジェクトでNative Controlを生成して、それをPCL側で使用するだけです。

ただし、当然Native Controlは、そのままでは、Xamarin.FormsのUIに追加できません。
Xamarin.Forms.Viewに変換する必要があります。

この変換は拡張メソッドのXamarin.Forms.Platform.iOS.LayoutExtensions.ToViewXamarin.Forms.Platform.Android.LayoutExtensions.ToViewXamarin.Forms.Platform.UWP.LayoutExtensions.ToViewを使用して、Native ControlをViewに変換します。
今回のソースは下記になります。

PCL

最初にPCLの説明をします。

Serviceのinterface

3つのデバイスのNative Controlを生成するためのinterfaceであるINativeControlServiceを定義します。
INativeControlService.cs

1
2
3
4
5
6
7
8
9
10
namespace Xamarin.Forms.Portable9.Services
{
public interface INativeControlService
{

View Create();

}

}

各プロジェクトはPCLを参照しているので、このinterfaceを実装して各固有機能へのアクセスを提供します。

View

Viewです。
今回はソースから追加するので、

MainPage.xaml

1
2
3
4
5
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Xamarin.Forms.Portable9.Views.MainPage">
</ContentPage>

Contentを空にしました。
このMainPageの生成は、PCLのApp.csで実施します。

App.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using Xamarin.Forms.Portable9.Services;
using Xamarin.Forms.Portable9.Views;

namespace Xamarin.Forms.Portable9
{
public class App : Application
{
public App()
{
var service = DependencyService.Get();
var view = service.Create() as View;
view.HorizontalOptions = LayoutOptions.Center;
view.VerticalOptions = LayoutOptions.Center;

// The root page of your application
MainPage = new MainPage
{
Content = view
};
}

protected override void OnStart()
{
// Handle when your app starts
}

protected override void OnSleep()
{
// Handle when your app sleeps
}

protected override void OnResume()
{
// Handle when your app resumes
}
}
}

iOS

PCLで定義したインターフェースを実装します。

NativeControlService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using UIKit;
using Xamarin.Forms.Platform.iOS;
using Xamarin.Forms.Portable9.iOS.Services;
using Xamarin.Forms.Portable9.Services;

[assembly: Xamarin.Forms.Dependency(typeof(NativeControlService))]
namespace Xamarin.Forms.Portable9.iOS.Services
{
public sealed class NativeControlService : INativeControlService
{

public View Create()
{
var label = new UILabel
{
MinimumFontSize = 14f,
Lines = 0,
LineBreakMode = UILineBreakMode.WordWrap,
Text = "iOS",
};

return label.ToView();
}

}

}

Android

NativeControlService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Android.Widget;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms.Portable9.Droid.Services;
using Xamarin.Forms.Portable9.Services;

[assembly: Xamarin.Forms.Dependency(typeof(NativeControlService))]
namespace Xamarin.Forms.Portable9.Droid.Services
{
public sealed class NativeControlService : INativeControlService
{
public View Create()
{
var view = new TextView(Forms.Context)
{
Text = "Android",
TextSize = 14,
};

return view.ToView();
}

}

}

UWP

NativeControlService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Windows.UI.Xaml.Controls;
using Xamarin.Forms.Platform.UWP;
using Xamarin.Forms.Portable9.Services;
using Xamarin.Forms.Portable9.UWP.Services;

[assembly: Xamarin.Forms.Dependency(typeof(NativeControlService))]
namespace Xamarin.Forms.Portable9.UWP.Services
{
public sealed class NativeControlService : INativeControlService
{

public View Create()
{
var label = new TextBlock
{
Text = "UWP",
};

return label.ToView();
}

}

}

実行してみる

iOS

UILabel
UILabel

Android

TextView
TextView

UWP

**Layout cycle detected. Layout could not complete.**という例外を投げたため実行できませんでした。何故だ….

無慈悲な例外
無慈悲な例外

Conclusion

Sharedプロジェクトでしか使えない、というのはディレクティブの関係なので、固有のUI生成を各プロジェクトに任せてしまえば、どうとでもなります。
XAMLで使えるように工夫次第でできるはずです。
ContentViewの派生クラスのコンストラクタでDependencyService経由で生成したNative Controlをラップすれば、それをXAMLから使うことができるでしょう。

Source Code

https://github.com/takuya-takeuchi/Demo/tree/master/Xamarin.Forms.Portable9