かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ポータブルライブラリで、ポータブルじゃない処理をうまいこと分離する方法

Reactive ExtensionsがOSSになりましたね!!そこで、気になってたものを見てみました。RxのポータブルってPlatform固有の機能をうまいことラッピングしてくれてたりします。
そのやり方をちょいと覗いてみました。
Schedulerクラスが該当のクラスで、こいつは、NewThreadとかプラットフォームごとに実態が異なるので一時は、プラットフォーム固有のAPI用意したから、そっちつかうようにしてね!!みたいになったと思ったら、リリース版では、元に戻ってポータブルライブラリでも****.Platformというプラットフォーム依存のアセンブリを参照に追加しておけば動くという素敵仕様になってました。

おかげで、コードは従来とおなじように書けるようになってます。素敵!!アセンブリ追加しただけで、同じAPI(API自体はポータブルライブラリに定義されてる)なのに、プラットフォームごとに内部動作が違うというのはどうやって実現してるのだろう?ということで真似してみました。
タネは簡単で、****.Platformというアセンブリの中の特定の名前のクラスをリフレクションでインスタンス化してるだけでした。

劣化実装

本物は、リフレクションとかの処理を必要とされるまで評価しないようにLazyを使って遅延初期化とかやってますが、今回やってみる劣化実装はそんな小難しいことはやりません。お題としては、ダイアログを表示するAPIをポータブルライブラリで定義してWinRTとWPFで使ってみようという魂胆です。ダイアログを表示する実態のコードはポータブルライブラリじゃない別のアセンブリで定義してます。

ポータブルライブラリのプロジェクト作成

PortableDialogという名前でプロジェクトを作って以下のようなクラスを追加します。

/// <summary>
/// ダイアログを表示するAPI
/// </summary>
public interface IDialog
{
    Task Show(string message);
}

/// <summary>
/// 何も無かった時のデフォルト実装
/// </summary>
class DefaultDialog : IDialog
{
    public Task Show(string message)
    {
        throw new NotSupportedException();
    }
}

そして、このIDialogの実装を提供するクラスを用意します。

public static class DialogService
{
    public static IDialog Dialog { get; private set; }
}

従来であれば、public static Initialize(IDialog dialog)みたいなメソッドを定義して、何処かで一回呼んでねってスタンスだったのですが、これをPortableDialog.Platformというアセンブリを置くだけでDialogプロパティから適切な実装が返されるようにしたいと思います。とりあえず、staticコンストラクタにリフレクションで型を生成する処理を書いておきます。処理内容は、コメントの通り。

static DialogService()
{
    // DialogServiceクラスの型情報を取得
    var typeInfo = typeof(DialogService).GetTypeInfo();

    // 生成したい型のフルネームを生成
    var fullName = "PortableDialog.Platform.Dialog, " + 
        // DialogServiceの定義されてるアセンブリをもとにAssemblyNameを
        // 作成して名前だけPortableDialog.Platformに差し替える
        new AssemblyName(typeInfo.Assembly.FullName) 
        { 
            Name = "PortableDialog.Platform" 
        };

    // 型情報を取得する
    var type = Type.GetType(fullName, false);
    if (type == null)
    {
        // 型情報の取得に失敗したらデフォルト実装を設定する
        Dialog = new DefaultDialog();
        return;
    }

    // 型情報がとれたらインスタンス化する
    Dialog = (IDialog)Activator.CreateInstance(type);
}
プラットフォーム固有のアセンブリを作成

PortableDialog.Platform.WPFという名前でWPF用の実装クラスを持ったアセンブリを作成します。プロジェクトのプロパティから、アセンブリ名と名前空間をPortableDialog.Platformになおしてから、以下のクラスを実装します。

namespace PortableDialog.Platform
{
    public class Dialog : IDialog
    {
        public async Task Show(string message)
        {
            MessageBox.Show(message);
        }
    }
}

これだけです。次にWinRT用のPortableDialog.Platform.WinRTという名前でクラスライブラリを作って、WPFと同じようにアセンブリ名と名前空間をPortableDialog.Platformに変更して以下のクラスを作成します。

namespace PortableDialog.Platform
{
    public class Dialog : IDialog
    {
        public async Task Show(string message)
        {
            var dlg = new MessageDialog(message);
            await dlg.ShowAsync();
        }
    }
}
使ってみよう

WPFアプリケーションを作ってPortableDialogとPortableDialog.Platform.WPFを参照に追加して、適当なボタンクリックイベントあたりで以下のコードを書いてみます。

private await void Button_Click_1(object sender, RoutedEventArgs e)
{
    async DialogService.Dialog.Show("OK");
}

同じようにWindows ストア アプリを作ってPortableDialogとPortableDialog.Platform.WinRTを参照に追加して、適当なボタンクリックイベントに以下のようなコードを書きます。

private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    await DialogService.Dialog.Show("Hello world");
}

実行すると、どちらもダイアログが無事表示されました。

まとめ

Visual Stuio 2012や、Microoft.Bcl.Asyncの登場によってポータブルライブラリでも.NET Framework 4.5相当のことがSL4やWP7.5を含めた場合でも出来ることが増えてきました。これから、クロスプラットフォーム対応する必要のあるものは、がんがんポータブルライブラリで作っていってもいいのかな?と思います。その時に、どうしてもプラットフォーム固有の機能があるんだ…。という場合は、今回のような手を使えば、うまいこと逃げれることもあるということを頭の片隅に入れておくといいかも。

ソースコード

以下からダウンロードできます。
PortableSample.zip