しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin で Prism EventAggregator を使って Suspending 時の処理をする

前回の投稿 でとりあえず各プラットフォームでのバックグラウンド遷移の通知の仕方がわかったので、Xamarin.Forms の画面側に Suspending が通知されるようにしたいと思います

普通に App クラスにイベントハンドラを追加してもよいですが、せっかくなのでイベントの発信から受信を仲介する Prism の EventAggregator というライブラリを使っていこうと思います

もちろんライセンスは Apache 2.0 で依存関係なしなので Xamarin でも使えます!

f:id:matatabi_ux:20141109153212p:plain

いつものように Nuget パッケージマネージャを開いて Prism で検索すると Prism.PubSubEvents というライブラリがヒットするのでこれを Xamarin.Forms と各プラットフォームプロジェクトにインストールします

f:id:matatabi_ux:20141109211648p:plain

まずは EventAggregator でハンドリングするイベント関係のクラスを追加します

/// <summary>
/// アプリケーション実行状態
/// </summary>
public enum AppState
{
    /// <summary>
    /// 未起動
    /// </summary>
    NotRunning = 0,

    /// <summary>
    /// 実行中
    /// </summary>
    Running = 10,

    /// <summary>
    /// 中断中
    /// </summary>
    Suspended = 20,

    /// <summary>
    /// シャットダウン
    /// </summary>
    Terminated = 30,
}

/// <summary>
/// iOS アプリケーション実行状態
/// </summary>
public enum AppStateIOS
{
    /// <summary>
    /// 未起動
    /// </summary>
    NotRunning = 0,

    /// <summary>
    /// 非表示中
    /// </summary>
    InActive = 5,

    /// <summary>
    /// 利用中
    /// </summary>
    Active = 10,

    /// <summary>
    /// バックグラウンド実行中
    /// </summary>
    Background = 20,

    /// <summary>
    /// 停止中
    /// </summary>
    Suspended = 30,
}

/// <summary>
/// Windows Phone アプリケーション実行状態
/// </summary>
public enum AppStatePhone
{
    /// <summary>
    /// 未起動
    /// </summary>
    NotRunning = 0,

    /// <summary>
    /// 実行中
    /// </summary>
    Running = 10,

    /// <summary>
    /// 中断中
    /// </summary>
    Dormant = 20,

    /// <summary>
    /// シャットダウン
    /// </summary>
    Tombstoned = 30,
}

AppState~ はアプリの実行状態を表すただの enum です

Xamarin.Forms で受信した場合と、各プラットフォームで受信した場合を用意してます

/// <summary>
/// アプリケーション実行状態の変更イベント
/// </summary>
public class AppStateChangedEvent : PubSubEvent<ChangedAppState>
{
}

/// <summary>
/// アプリケーション実行状態の変更イベント引数
/// </summary>
public class ChangedAppState : EventArgs
{
    /// <summary>
    /// 新しい実行状態
    /// </summary>
    public AppState AppState { get; protected set; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="state">アプリケーション実行状態</param>
    public ChangedAppState(AppState state)
    {
        this.AppState = state;
    }
}

/// <summary>
/// iOS アプリケーション実行状態の変更イベント引数
/// </summary>
public class ChangedAppStateIOS : ChangedAppState
{
    /// <summary>
    /// AppState の変換表
    /// </summary>
    private static readonly Dictionary<AppStateIOS, AppState> ConvertToAppState = new Dictionary<AppStateIOS, AppState>()
    {
        {AppStateIOS.NotRunning, AppState.NotRunning},
        {AppStateIOS.InActive, AppState.Running},
        {AppStateIOS.Active, AppState.Running},
        {AppStateIOS.Background, AppState.Suspended},
        {AppStateIOS.Suspended, AppState.Terminated},
    };

    /// <summary>
    /// 新しい実行状態
    /// </summary>
    public AppStateIOS NativeAppState { get; protected set; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="state">アプリケーション実行状態</param>
    public ChangedAppStateIOS(AppStateIOS state)
        : base(ConvertToAppState[state])
    {
        this.NativeAppState = state;
    }
}

/// <summary>
/// Windows Phone アプリケーション実行状態の変更イベント引数
/// </summary>
public class ChangedAppStatePhone : ChangedAppState
{
    /// <summary>
    /// AppState の変換表
    /// </summary>
    private static readonly Dictionary<AppStatePhone, AppState> ConvertToAppState = new Dictionary<AppStatePhone, AppState>()
    {
        {AppStatePhone.NotRunning, AppState.NotRunning},
        {AppStatePhone.Running, AppState.Running},
        {AppStatePhone.Dormant, AppState.Suspended},
        {AppStatePhone.Tombstoned, AppState.Terminated},
    };

    /// <summary>
    /// 新しい実行状態
    /// </summary>
    public AppStatePhone NativeAppState { get; protected set; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="state">アプリケーション実行状態</param>
    public ChangedAppStatePhone(AppStatePhone state)
        : base(ConvertToAppState[state])
    {
        this.NativeAppState = state;
    }
}

こちらは EventAggregator でハンドリングするイベントクラスと Payload クラスです

これも Xamarin.Forms で受信した場合と、各プラットフォームで受信した場合を用意してます

使い方はかんたんで

    var eventAggregator = new EventAggregator();

    // イベントの受信登録
    eventAggregator.GetEvent<AppStateChangedEvent>().Subscribe(
    (s, e) =>
    {
        // 受信後の処理
    ]);

    // イベントの発行
    eventAggregator.GetEvent<AppStateChangedEvent>()
        .Publish(new ChangedAppStateIOS(AppState.Suspended));

こんな感じに受信と発行を記述するだけ!

EventAggregator がシングルトンとして仲介役をするので、多対多のイベントの送受信がかんたんに実装できます(あまり使いすぎると管理しきれなくなりますが;)

今回の場合はまず App クラスを次のように修正して UnityContainer に EventAggregator を登録します

/// <summary>
/// 共通 アプリケーションクラス
/// </summary>
public class App
{
    /// <summary>
    /// アプリケーションクラス参照
    /// </summary>
    public static readonly App Current;

    /// <summary>
    /// アプリ内で管理するモジュールのコンテナ
    /// </summary>
    public static readonly UnityContainer Container = new UnityContainer();

    /// <summary>
    /// コンストラクタ
    /// </summary>
    static App()
    {
        Current = new App();
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    private App()
        : base()
    {
        // Prism.Mvvm.Xamarin アセンブリを読み込み可能にするおまじない
        var autoWired = ViewModelLocator.AutoWireViewModelProperty.DefaultValue;

    }

    /// <summary>
    /// アプリケーション起動処理
    /// </summary>
    public void OnLaunchApplication()
    {
        // EventAggregator の生成に UnityContainer を使います
        Container.RegisterType<IEventAggregator, EventAggregator>(new ContainerControlledLifetimeManager());

        // ViewModel をインスタンス化するデフォルトメソッドを指定します
        ViewModelLocationProvider.SetDefaultViewModelFactory((type) => Container.Resolve(type));

        // 画面遷移サービスの生成方法を注入します
        Container.RegisterType<INavigationService, NavigationService>(new ContainerControlledLifetimeManager());

        // Page クラスの生成に UnityContainer を使います
        NavigationService.SetPageFactiory(type => Container.Resolve(type));
        NavigationService.SetRootPage(new NavigationPage(new FirstPage()));

        Container.RegisterType<SecondPageViewModel>(new ContainerControlledLifetimeManager());
    }

    /// <summary>
    /// メイン画面を取得します
    /// </summary>
    /// <returns>メイン画面</returns>
    public static Page GetMainPage()
    {
        return NavigationService.RootPage;
    }
}

その次は Suspended を検知するために NavigationService を改修

/// <summary>
/// 画面遷移サービス
/// </summary>
public class NavigationService : INavigationService
{
    #region Privates

    ~ 中略 ~

    /// <summary>
    /// EventAggregator
    /// </summary>
    private static IEventAggregator eventAggregator;

    #endregion //Privates

    /// <summary>
    ///  画面遷移用ルート画面
    /// </summary>
    public static NavigationPage RootPage { get; private set; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    [InjectionConstructor]
    public NavigationService(IEventAggregator eventAggregator)
    {
        RootPage = new NavigationPage();

        NavigationService.eventAggregator = eventAggregator;
        NavigationService.eventAggregator.GetEvent<AppStateChangedEvent>().Subscribe(this.OnAppStateChanged);
    }

    ~ 中略 ~

    /// <summary>
    /// アプリケーション実行状態変更イベントハンドラ
    /// </summary>
    /// <param name="state">アプリケーション実行状態</param>
    private void OnAppStateChanged(ChangedAppState state)
    {
        var vm = RootPage.CurrentPage.BindingContext as INavigationAware;
        if (vm == null)
        {
            return;
        }
        var history = NavigationStack.FirstOrDefault();
        if (history == null)
        {
            return;
        }

        switch (state.AppState)
        {
            case AppState.Running:
                vm.OnNavigatedTo(history.Parameter, history.NavigationMode);
                break;

            case AppState.Suspended:
                vm.OnNavigatedFrom(true);
                break;

            case AppState.NotRunning:
            case AppState.Terminated:
                break;
        }
    }
}

Suspended の検知が目的ですが Running になった時も ViewModel.OnNavigateTo を呼び出すようにしてみました

あとは各プラットフォームごとにアプリ実行状態変更時にイベントを発行するコードを記述

/// <summary>
/// アプリケーション代理クラス
/// </summary>
[Register("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
    /// <summary>
    /// ウィンドウ
    /// </summary>
    protected UIWindow window;

    /// <summary>
    /// 起動完了時の処理
    /// </summary>
    /// <param name="app">アプリケーション</param>
    /// <param name="options">オプション指定</param>
    /// <returns></returns>
    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        Forms.Init();

        window = new UIWindow(UIScreen.MainScreen.Bounds);

        window.RootViewController = App.GetMainPage().CreateViewController();

        window.MakeKeyAndVisible();

        return true;
    }

    /// <summary>
    /// Active に移行した際の処理
    /// </summary>
    /// <param name="application">アプリケーションクラス</param>
    public override void WillEnterForeground(UIApplication application)
    {
        App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>()
            .Publish(new ChangedAppStateIOS(AppStateIOS.Active));
    }

    /// <summary>
    /// Backgroud に移行した際の処理
    /// </summary>
    /// <param name="application">アプリケーションクラス</param>
    public override void DidEnterBackground(UIApplication application)
    {
        App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>()
            .Publish(new ChangedAppStateIOS(AppStateIOS.Background));
    }
}

iOS の場合 AppDelegate に記述

なぜか base.WillEnterForeground や base.DidEnterBackground を実行すると呼んじゃダメ例外が発生するので、基底クラスの同名メソッドは呼ばないようにしています

/// <summary>
/// アプリケーションクラス
/// </summary>
public partial class App : Application
{

    ~ 中略 ~

    /// <summary>
    /// Running に移行した際の処理
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void Application_Activated(object sender, ActivatedEventArgs e)
    {
        PrismXamarin.App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>()
            .Publish(new ChangedAppStatePhone(AppStatePhone.Running));
    }

    /// <summary>
    /// Dormant に移行した際の処理
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void Application_Deactivated(object sender, DeactivatedEventArgs e)
    {
        PrismXamarin.App.Container.Resolve<IEventAggregator>().GetEvent<AppStateChangedEvent>()
            .Publish(new ChangedAppStatePhone(AppStatePhone.Dormant));
    }

}

Windows Phone の方はこんな感じ

試しに iOS で実行!・・・適当に画面遷移してホームボタンでバックグラウンド実行に遷移させると

f:id:matatabi_ux:20141109221050p:plain

無事に NavigationService に Suspended への遷移が通知されました!