しっぽを追いかけて

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

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

Xamarin で Prism 風に戻る遷移の際も遷移パラメータを参照できるようにする

Xamarin の画面遷移でも Prism のようにセッション情報を保持するようにしようかとも思ったんですが、どうやらそんなことをしてもメリットがなさそうなのでやめました;

iOSWindows Phone 8.0 もプラットフォームとしては画面遷移時に View もデータも丸ごとメモリに保持するらしく、またアプリ中断からの起動で見ていた画面、状態を復元するような仕組みになっていないようです

f:id:matatabi_ux:20141108210702p:plain

例えば iOS には Suspended の状態がありますが、これは Windows ランタイムでいう Suspended や Terminated とは異なり、アプリをメモリ上に確保はしていますが、次回の起動をスピードアップできるだけで、Active 状態で見ていた画面を即座に復元することはできないということです

これだと画面遷移の履歴を View から分離して保存しても意味がないですよね・・・Windows Phone 8.1 は View と状態データの分離ができているので電池の持ちがいいんでしょうか

そんなこんなでセッション情報の保存機能はあきらめて、代わりに戻る遷移で戻った際にも当時画面遷移で渡ってきていた遷移パラメータを参照できるようにしてみようと思います

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

    /// <summary>
    /// 画面遷移履歴情報
    /// </summary>
    private readonly static Stack<NavigatitonHistory> NavigationStack = new Stack<NavigatitonHistory>();

    /// <summary>
    /// デフォルトの Page 生成メソッド
    /// </summary>
    private static Func<Type, object> defaultPageFactory = type => Activator.CreateInstance(type);

    #endregion //Privates

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

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public NavigationService()
    {
        RootPage = new NavigationPage();
    }

    /// <summary>
    /// デフォルトの Page 生成メソッドを設定します
    /// </summary>
    /// <param name="factory">デフォルトの Page 生成メソッド</param>
    public static void SetPageFactiory(Func<Type, object> factory)
    {
        NavigationService.defaultPageFactory = factory;
    }

    /// <summary>
    /// 画面遷移用ルート画面を設定します
    /// </summary>
    /// <param name="rootPage">画面遷移用ルート画面</param>
    public static void SetRootPage(NavigationPage rootPage)
    {
        RootPage = rootPage;

        if (rootPage.CurrentPage == null)
        {
            return;
        }

        PrepareNavigation(rootPage.CurrentPage, null, NavigationMode.Pushed);
    }

    /// <summary>
    /// 画面遷移の前処理をします
    /// </summary>
    /// <param name="page">遷移先画面</param>
    /// <param name="parameter">遷移パラメータ</param>
    /// <param name="mode">遷移種別</param>
    private static void PrepareNavigation(Page page, object parameter, NavigationMode mode)
    {
        switch (mode)
        {
            case NavigationMode.Pushed:
                NavigationStack.Push(new NavigatitonHistory(page.GetType(), parameter, mode));
                break;

            case NavigationMode.Poped:
                NavigationStack.Pop();
                break;

            case NavigationMode.PopedToRoot:
                for (var i = 0; i < NavigationStack.Count - 1; i++)
                {
                    NavigationStack.Pop();
                }
                break;
        }

        var vm = page.BindingContext as INavigationAware;
        if (vm != null)
        {
            // 遷移時の処理
            vm.OnNavigatedTo(parameter, mode);
        }

        EventHandler onDisappearing = null;

        onDisappearing = new EventHandler(
            (sender, e) =>
            {
                // 画面を離れる際の処理
                page.Disappearing -= onDisappearing;

                if (vm == null)
                {
                    return;
                }

                //TODO:中断状態の判定
                vm.OnNavigatedFrom(false);
            });

        page.Disappearing += onDisappearing;
    }

    /// <summary>
    /// 画面遷移します
    /// </summary>
    /// <param name="pageType">遷移先クラスの型</param>
    /// <param name="parameter">遷移パラメータ</param>
    /// <returns>遷移に成功した場合 <code>true</code>、それ以外は<code>false</code></returns>
    public async Task<bool> NavigateAsync(Type pageType, object parameter = null)
    {
        var page = NavigationService.defaultPageFactory(pageType) as Page;

        if (page == null)
        {
            return false;
        }

        PrepareNavigation(page, parameter, NavigationMode.Pushed);
        await RootPage.PushAsync(page);

        return true;
    }

    /// <summary>
    /// 戻る遷移
    /// </summary>
    public async Task GoBack()
    {
        await RootPage.PopAsync();

        var history = NavigationStack.Skip(1).FirstOrDefault();
        if (history != null)
        {
            PrepareNavigation(RootPage.CurrentPage, history.Parameter, NavigationMode.Poped);
        }
    }

    /// <summary>
    /// ホームへ戻る遷移
    /// </summary>
    public async Task GoHome()
    {
        await RootPage.PopToRootAsync();

        var history = NavigationStack.LastOrDefault();
        if (history != null)
        {
            PrepareNavigation(RootPage.CurrentPage, history.Parameter, NavigationMode.PopedToRoot);
        }
    }

    /// <summary>
    /// 戻る遷移可能フラグ
    /// </summary>
    /// <returns>戻り遷移可能な場合 <code>true</code>、それ以外は<code>false</code></returns>
    public bool CanGoBack()
    {
        return NavigationStack.Count() > 1;
    }

    /// <summary>
    /// 中断します
    /// </summary>
    public void Suspending()
    {
        throw new NotImplementedException();
    }
}

前回の投稿 では viewState の Dictionary<string, object> を取り扱っていましたが、それらを思い切って削除しています

代わりに画面遷移の履歴を Stack に保存して、戻る遷移の際も遷移パラメータをさかのぼって渡すようにしました

/// <summary>
/// 画面遷移履歴情報
/// </summary>
public class NavigatitonHistory
{
    /// <summary>
    /// 画面遷移履歴情報
    /// </summary>
    /// <param name="pageType">遷移先画面</param>
    /// <param name="parameter">遷移パラメータ</param>
    /// <param name="mode">遷移種別</param>
    public NavigatitonHistory(Type pageType, object parameter, NavigationMode mode)
    {
        this.PageType = pageType;
        this.Parameter = parameter;
        this.NavigationMode = mode;
    }

    /// <summary>
    /// 遷移先画面
    /// </summary>
    public Type PageType { get; private set; }

    /// <summary>
    /// 遷移パラメータ
    /// </summary>
    public object Parameter { get; private set; }

    /// <summary>
    /// 遷移種別
    /// </summary>
    public NavigationMode NavigationMode { get; private set; }
}

NavigationHistory はただのいれものです

f:id:matatabi_ux:20141108214340p:plain

お試しで前回のサンプルから一つ画面を増やし、First → Second → Third → Second の遷移をできるようにしてみました

/// <summary>
/// 2番目の画面
/// </summary>
public class SecondPageViewModel : ViewModelBase
{
    /// <summary>
    /// 画面遷移サービス
    /// </summary>
    private INavigationService navigationService;

    #region Title プロパティ

    /// <summary>
    /// Title
    /// </summary>
    private string title = string.Empty;

    /// <summary>
    /// Title の取得と設定
    /// </summary>
    public string Title
    {
        get { return this.title; }
        set { this.SetProperty<string>(ref this.title, value); }
    }

    #endregion //Title プロパティ

    #region ButtonLabel プロパティ

    /// <summary>
    /// ButtonLabel
    /// </summary>
    private string buttonLabel = string.Empty;

    /// <summary>
    /// ButtonLabel の取得と設定
    /// </summary>
    public string ButtonLabel
    {
        get { return this.buttonLabel; }
        set { this.SetProperty<string>(ref this.buttonLabel, value); }
    }

    #endregion //ButtonLabel プロパティ

    #region GoForwardCommand

    /// <summary>
    /// 進むコマンド
    /// </summary>
    private ICommand goForwardCommand;

    /// <summary>
    /// 進むコマンド
    /// </summary>
    public ICommand GoForwardCommand
    {
        get { return this.goForwardCommand ?? (this.goForwardCommand = DelegateCommand.FromAsyncHandler(this.GoForward)); }
    }

    /// <summary>
    /// 進む遷移
    /// </summary>
    private async Task GoForward()
    {
        await this.navigationService.NavigateAsync(typeof(ThirdPage), "From Second");
    }

    #endregion //GoForwardCommand

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="navigationService">画面遷移サービス</param>
    [InjectionConstructor]
    public SecondPageViewModel(INavigationService navigationService)
    {
        this.navigationService = navigationService;
        this.title = "Second Page";
        this.buttonLabel = "To Third Page";
    }

    /// <summary>
    /// 画面に遷移したときの処理
    /// </summary>
    /// <param name="navigationParameter">遷移パラメータ</param>
    /// <param name="navigationMode">遷移モード</param>
    public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode)
    {
        base.OnNavigatedTo(navigationParameter, navigationMode);

        this.Title = string.Format("Second Page {0}", navigationParameter.ToString());

        Debug.WriteLine(string.Format("parameter={0}, mode={1}", navigationParameter.ToString(), navigationMode.ToString()));
    }

    /// <summary>
    /// 画面から遷移するときの処理
    /// </summary>
    /// <param name="suspending">中断フラグ</param>
    public override void OnNavigatedFrom(bool suspending)
    {
        base.OnNavigatedFrom(suspending);
    }
}

SecondPage の ViewModel は少し修正して GoBackCommand を GoForwardCommand に変更するなどしています

本当はブレイクポイントを張って OnNavigatedTo イベントハンドラでの変数の値を確認しようと思ったのですが、async メソッドを中継しているからなのか、なぜか iOS だとここにブレイクポイントを張ると例外が発生してしまうので Debug トレースを出力することにしました

ともかく実行して First → Second → Third → Second の画面遷移を実行すると

f:id:matatabi_ux:20141108221854p:plain

ThirdPage から戻っても SecondPage でちゃんと First → Second の遷移状態が再現しています

Debug トレースを見ると

f:id:matatabi_ux:20141108222017p:plain

最初の遷移では First → Second なので Pushed で "FirstPage" が渡っています

次の遷移では Third → Second の戻る遷移ですが Poped で "FirstPage" が渡っています・・・ちゃんと戻る場合も遷移した当時の遷移パラメータが渡されてますね

単純に画面を戻すだけではなく、NavigationMode で Poped であることを伝えているので、細かい画面制御ができそうです!