かずきのBlog@hatena

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

Prism for Windows RuntimeをUniversal appで使えるようにしてみました

Prism for Windows Runtimeって個人的に結構気に入ってて、Universal appでも使いたい…!と思うのですが、いかんせんフットワークが重く感じてしまうんですよね、Prismのチーム。なのでUniversal appが発表されて数日でPrismも対応しました!とかいう感じはなさそう。

なら作ればいいじゃない?ということで、新しいWindows Phone applicationはWindows store appと、ライブラリがほぼ同じということで、Prism for Windows Runtimeのdll自体を参照しても動きます。さらに、Unity(DIコンテナのほう)も動きます。これだけ動けば上等!ということで、サポート系ライブラリ等をそろえてNuGetに放流。もとにしたのは、Prismを使うけどページの継承はしなくていいように作ったPrismAdapter。

使い方

プロジェクトの作成

空のアプリケーション(ユニバーサルアプリ)を作成します。名前は、ここではHelloUniAppにしました。そして、NuGetパッケージマネージャを使ってPhoneとWindowsのプロジェクトにPrismAdapterをインストールします。

PM> Install-Package PrismAdapter -ProjectName HelloUniApp.Windows
依存関係 'Prism.StoreApps (≥ 1.1.0)' の解決を試みています。
'Prism.StoreApps 1.1.0' をインストールしています。
Prism.StoreApps を Microsoft からダウンロードします。このライセンス条項は http://prismwindowsruntime.codeplex.com/license で参照できます。固有のライセンス条項がある追加の依存関係がパッケージに含まれていないかどうかを確認してください。パッケージおよび依存関係を使用することで、該当するライセンス条項に同意したものとみなされます。ライセンス条項に同意しない場合は、関係のあるコンポーネントをデバイスから削除してください。
'Prism.StoreApps 1.1.0' が正常にインストールされました。
'PrismAdapter 0.1.0' をインストールしています。
PrismAdapter を okazuki からダウンロードします。このライセンス条項は https://prismadapter.codeplex.com/license で参照できます。固有のライセンス条項がある追加の依存関係がパッケージに含まれていないかどうかを確認してください。パッケージおよび依存関係を使用することで、該当するライセンス条項に同意したものとみなされます。ライセンス条項に同意しない場合は、関係のあるコンポーネントをデバイスから削除してください。
'PrismAdapter 0.1.0' が正常にインストールされました。
'Prism.StoreApps 1.1.0' を HelloUniApp.Windows に追加しています。
'Prism.StoreApps 1.1.0' が HelloUniApp.Windows に正常に追加されました。
'PrismAdapter 0.1.0' を HelloUniApp.Windows に追加しています。
'PrismAdapter 0.1.0' が HelloUniApp.Windows に正常に追加されました。

PM> Install-Package PrismAdapter -ProjectName HelloUniApp.WindowsPhone
'PrismAdapter 0.1.0' は既にインストールされています。
'PrismAdapter 0.1.0' を HelloUniApp.WindowsPhone に追加しています。
'PrismAdapter 0.1.0' が HelloUniApp.WindowsPhone に正常に追加されました。

上記はコマンドでの例ですが、もちろんGUIでもOKです。ソリューションのパッケージを管理するところから一括で入れるのが個人的にはおすすめです。

アプリクラスの変更

次にApp.xaml.csの中身をPrismAdapter用に書き換えます。PrismではMvvmAppBaseを継承しないといけませんでしたが、PrismAdapterは、継承じゃなくて移譲になるようにしています。

使用するクラスはPrismAdapterBootstrapperクラスで、こいつのSetupメソッドにOnLaunchedのイベント引数を渡して初期化したのちに、Runメソッドで実行します。Runメソッドの引数は、初期画面をどこにするかを決めるためのAction<INavigationService>を渡します。INavigationServiceはPrismのクラスになります。

using PrismAdapter;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Navigation;

// 空のアプリケーション テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234227 を参照してください

namespace HelloUniApp
{
    /// <summary>
    /// 既定の Application クラスに対してアプリケーション独自の動作を実装します。
    /// </summary>
    public sealed partial class App : Application
    {
#if WINDOWS_PHONE_APP
        private TransitionCollection transitions;
#endif

        /// <summary>
        /// 単一アプリケーション オブジェクトを初期化します。これは、実行される作成したコードの
        /// 最初の行であり、main() または WinMain() と論理的に等価です。
        /// </summary>
        public App()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// アプリケーションがエンド ユーザーによって正常に起動されたときに呼び出されます。他のエントリ ポイントは、
        /// アプリケーションが特定のファイルを開くために呼び出されたときに
        /// 検索結果やその他の情報を表示するために使用されます。
        /// </summary>
        /// <param name="e">起動要求とプロセスの詳細を表示します。</param>
        protected async override void OnLaunched(LaunchActivatedEventArgs e)
        {
#if DEBUG
            if (System.Diagnostics.Debugger.IsAttached)
            {
                this.DebugSettings.EnableFrameRateCounter = true;
            }
#endif
            // 初期化して実行
            var b = new PrismAdapterBootstrapper();
            await b.Setup(e);
            b.Run(n => n.Navigate("Main", null));
        }

#if WINDOWS_PHONE_APP
        /// <summary>
        /// アプリを起動した後のコンテンツの移行を復元します。
        /// </summary>
        /// <param name="sender">ハンドラーがアタッチされたオブジェクト。</param>
        /// <param name="e">ナビゲーション イベントの詳細。</param>
        private void RootFrame_FirstNavigated(object sender, NavigationEventArgs e)
        {
            var rootFrame = sender as Frame;
            rootFrame.ContentTransitions = this.transitions ?? new TransitionCollection() { new NavigationThemeTransition() };
            rootFrame.Navigated -= this.RootFrame_FirstNavigated;
        }
#endif
    }
}

ページの作成

次に、ページを作成します。プラットフォームごとに固有の画面を作ることもできますが、ここではSharedプロジェクトに押し込んでいこうと思います。その下準備として、両方のプロジェクトにおいてあるMainPage.xamlを削除します。そして、SharedプロジェクトにViewsというフォルダを作って、その中に、MainPage.xamlを作成します。

これだけで、一応画面が起動するようになります。(Windows PhoneとStore app両方動く)MainPage.xaml.csにPrismの持つ履歴管理機能などを追加していきます。これもVisualStateAwarePageの継承ではなく、PrismAdapterNavigationHelperクラスへの処理の移譲で行います。定型文になるのでNuGetでインストールした場合は、packagesフォルダのPrismAdapterフォルダの中のSnippetフォルダにあるpagesetupという名前のコードスニペットを登録しておくことをお勧めします。ページのコードを決してpagesetupと入力してtab tabでコードが完成します。

public sealed partial class MainPage : Page
{
    // 移譲先
    public PrismNavigationHelper NavigationHelper { get; set; }

    public MainPage()
    {
        this.InitializeComponent();
        // 初期化
        this.NavigationHelper = new PrismNavigationHelper(this);
        this.NavigationHelper.SaveState += this.NavigationHelper_SaveState;
        this.NavigationHelper.LoadState += this.NavigationHelper_LoadState;
        // Windows Phoneのハードキー サポート
        this.NavigationHelper.EnablePhoneSupport();
    }

    private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
    {
    }

    private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
    {
    }

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        this.NavigationHelper.OnNavigatedFrom(e);
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        this.NavigationHelper.OnNavigatedTo(e);
    }

}

なるべく、デフォルトのプロジェクトテンプレートの作りとあわせるようにしています。

ViewModelの設定

ViewModelの設定はPrismの機能をそのまま使います。ViewModelsフォルダに以下のようなMainPageViewModelクラスを作成します。

public class MainPageViewModel : ViewModel
{
    private int counter;

    public int Counter
    {
        get { return this.counter; }
        set { this.SetProperty(ref this.counter, value); }
    }

    public DelegateCommand CountupCommand { get; private set; }

    public MainPageViewModel()
    {
        this.CountupCommand = new DelegateCommand(() => this.Counter++);
    }
}

そして、ViewとViewModelの自動紐付け機能を使うために、ページにViewModelLocatorを設定します。

<!-- Pageタグの属性 -->
xmlns:prism="using:Microsoft.Practices.Prism.StoreApps"
prism:ViewModelLocator.AutoWireViewModel="True"

お好みに応じてデザイン時のDataContextを設定してGUIでやるか手書きXAMLでやるかは置いておいて、画面中央にTextBlock、AppBarにボタンを置いて、それぞれVMのプロパティにバインドします。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:HelloUniApp.Views"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:prism="using:Microsoft.Practices.Prism.StoreApps"
    xmlns:ViewModels="using:HelloUniApp.ViewModels"
    x:Class="HelloUniApp.Views.MainPage"
    prism:ViewModelLocator.AutoWireViewModel="True"
    mc:Ignorable="d" d:DataContext="{d:DesignInstance ViewModels:MainPageViewModel, IsDesignTimeCreatable=True}">
    <Page.BottomAppBar>
        <CommandBar>
            <AppBarButton Icon="Add" Label="カウントアップ" Command="{Binding CountupCommand, Mode=OneWay}"/>
        </CommandBar>
    </Page.BottomAppBar>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock TextWrapping="Wrap" Text="{Binding Counter}" HorizontalAlignment="Center" VerticalAlignment="Center" Style="{StaticResource HeaderTextBlockStyle}" FontFamily="Global User Interface"/>

    </Grid>
</Page>

実行して動作確認

これで、基本的な機能を使ったアプリが出来ました。実行してみると以下のようになります。

f:id:okazuki:20140406021039j:plain

大したことないアプリですが、ほぼ100%(ライブラリの中も含めて)ソースを共有してるってあたりが個人的にぐっときます。

DIコンテナとの組み合わせ

Prismで画面遷移するためにINavigationServiceなどをViewModelで使おうと思ったらDIしてもらうのが一番簡単です。というかPrismAdapterは、DIしかサポートしてません。ということで簡単に使い方を…。まず、NuGetからUnityをインストールします。現時点ではWindows Phone 8.1に対応してないのでストアアプリだけになります。

しかし、ストアアプリ用のdllをWP8.1で参照してもエラーにならないので、手動で参照設定に追加します。

Bootstrapperクラスには、いろいろなインスタンスの生成をカスタマイズできる機能があります。****Factoryプロパティにインスタンス生成のための処理を設定することでデフォルトを置き換えることができます。今回使用するResolveメソッドはViewやViewModelの生成に使われる処理になります。そこをUnity経由で行うようにさしかえます。

そしてSetupメソッドのあとで必要なインスタンスの登録を行います。Bootstrapperには、Prismのクラスが大体登録されていてb.NavigationService.Valueなどのようにして取得できます。今回は画面遷移だけできるようにしたいので、以下のようなApp.xaml.csにしました。

// インスタンス管理をするコンテナ
private UnityContainer container = new UnityContainer();

/// <summary>
/// アプリケーションがエンド ユーザーによって正常に起動されたときに呼び出されます。他のエントリ ポイントは、
/// アプリケーションが特定のファイルを開くために呼び出されたときに
/// 検索結果やその他の情報を表示するために使用されます。
/// </summary>
/// <param name="e">起動要求とプロセスの詳細を表示します。</param>
protected async override void OnLaunched(LaunchActivatedEventArgs e)
{
    // 初期化して実行
    var b = new PrismAdapterBootstrapper
    {
        // インスタンスの生成方法をUnity経由で行うように変更
        Resolve = type => this.container.Resolve(type)
    };
    await b.Setup(e);
    // コンテナに必要なものを登録していく
    this.container.RegisterInstance(b.NavigationService.Value);

    b.Run(n => n.Navigate("Main", null));
}

MainPage.xaml.csには、Unityから値を注入してもらうための目印のプロパティを作っておきいます。

[Dependency]
public INavigationService NavigationService { get; set; }

今回は、10回を超えたらNextPageに遷移するような処理にしてみたいと思うのでMainPage.xaml.csは以下のようになります。

this.CountupCommand = new DelegateCommand(() =>
{
    if (++this.Counter > 10)
    {
        this.NavigationService.Navigate("Next", null);
    }
});

NextPageは適当に作って用意しておけばOKです。

実行してみる

見た目いまいちだけど動く…。

Windows 8.1

Windows Phone 8.1