しっぽを追いかけて

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

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

Prism を MVPVM 化する(4)アプリケーション基底クラスを差し替える

Prism を MVPVM 化する(3)の続きです!

最後にアプリケーションクラスの基底クラス MvvmAppBase を MVPVM に対応した MvpvmAppBase に作り替えます

/// <summary>
/// MVPVM アプリケーション抽象クラス
/// </summary>
public abstract class MvpvmAppBase : Application
{
    #region Privates

    /// <summary>
    /// Terminated からの復帰フラグ
    /// </summary>
    private bool isRestoringFromTermination;

    #endregion //Privates

    /// <summary>
    /// コンストラクタ
    /// </summary>
    protected MvpvmAppBase()
    {
        this.Suspending += this.OnSuspending;
    }

    /// <summary>
    /// セッション状態
    /// </summary>
    protected ISessionStateService SessionStateService { get; set; }

    /// <summary>
    /// 画面遷移サービス
    /// </summary>
    public static INavigationService NavigationService { get; set; }

    /// <summary>
    /// 拡張スプラッシュ画面生成クラス
    /// </summary>
    protected Func<SplashScreen, Page> ExtendedSplashScreenFactory { get; set; }

    /// <summary>
    /// 中断状態フラグ
    /// </summary>
    public bool IsSuspending { get; private set; }

    /// <summary>
    /// アプリケーション起動処理
    /// </summary>
    /// <param name="args"><see cref="LaunchActivatedEventArgs"/> の起動イベント引数</param>
    /// <returns>Task</returns>
    protected abstract Task OnLaunchApplication(LaunchActivatedEventArgs args);

    /// <summary>
    /// 画面トークンから画面クラスを取得する
    /// </summary>
    /// <param name="pageToken">画面トークン</param>
    /// <returns>トークンが示す画面クラスの型</returns>
    protected virtual Type GetPageType(string pageToken)
    {
        var assemblyQualifiedAppType = this.GetType().GetTypeInfo().AssemblyQualifiedName;

        var pageNameWithParameter = assemblyQualifiedAppType.Replace(this.GetType().FullName, this.GetType().Namespace + ".Views.{0}Page");

        var viewFullName = string.Format(CultureInfo.InvariantCulture, pageNameWithParameter, pageToken);
        var viewType = Type.GetType(viewFullName);

        if (viewType == null)
        {
            var resourceLoader = ResourceLoader.GetForCurrentView(Constants.StoreAppsInfrastructureResourceMapId);
            throw new ArgumentException(
                string.Format(CultureInfo.InvariantCulture, resourceLoader.GetString("DefaultPageTypeLookupErrorMessage"), pageToken, this.GetType().Namespace + ".Views"),
                "pageToken");
        }

        return viewType;
    }

    /// <summary>
    /// シリアライズ用の KnownTypes の登録処理
    /// </summary>
    protected virtual void OnRegisterKnownTypesForSerialization()
    { 
    }

    /// <summary>
    /// アプリケーション初期化処理
    /// </summary>
    /// <param name="args">The <see cref="IActivatedEventArgs"/> のイベント引数</param>
    /// <returns>Task</returns>
    protected abstract Task OnInitializeAsync(IActivatedEventArgs args);

    /// <summary>
    /// 指定したクラスをインスタンス化する
    /// </summary>
    /// <param name="type">クラスの型</param>
    /// <returns>インスタンス化した指定クラス</returns>
    protected virtual object Resolve(Type type)
    {
        return Activator.CreateInstance(type);
    }

    /// <summary>
    /// ユーザーによって通常起動された場合のアプリケーション起動処理
    /// ファイルアクティベーションやプロトコルアクティベーションなどは含まない
    /// </summary>
    /// <param name="args">起動要求と処理の詳細情報</param>
    protected override async void OnLaunched(LaunchActivatedEventArgs args)
    {
        var rootFrame = await this.InitializeFrameAsync(args);

        // プライマリタイルから起動された場合、TileId と アプリケーションIDが一致するので確認する
        // 参考 http://go.microsoft.com/fwlink/?LinkID=288842
        string tileId = AppManifestHelper.GetApplicationId();

        if (rootFrame != null && (!this.isRestoringFromTermination || (args != null && args.TileId != tileId)))
        {
            await this.OnLaunchApplication(args);
        }

        // 現在のウィンドウがアクティブであることを確認
        Window.Current.Activate();
    }

    /// <summary>
    /// Frame と Content を初期化する
    /// </summary>
    /// <param name="args"><see cref="IActivatedEventArgs"/> のイベント引数</param>
    /// <returns>Task</returns>
    protected async Task<Frame> InitializeFrameAsync(IActivatedEventArgs args)
    {
        var rootFrame = Window.Current.Content as Frame;

        // ウィンドウアクティブ化の際にすでに Content が含まれる場合は初期化しない
        if (rootFrame == null)
        {
            // 初期画面の生成と遷移のため Frame を生成する
            rootFrame = new Frame();

            if (this.ExtendedSplashScreenFactory != null)
            {
                Page extendedSplashScreen = this.ExtendedSplashScreenFactory.Invoke(args.SplashScreen);
                rootFrame.Content = extendedSplashScreen;
            }

            var frameFacade = new FrameFacadeAdapter(rootFrame);

            // セッション状態サービスを初期化する
            this.SessionStateService = new SessionStateService();

            // VisualStateAwarePage がセッション状態を取得できるように設定
            VisualStateAwarePage.GetSessionStateForFrame =
                frame => this.SessionStateService.GetSessionStateForFrame(frameFacade);

            // Frame にキーを関連付ける
            this.SessionStateService.RegisterFrame(frameFacade, "AppFrame");

            NavigationService = this.CreateNavigationService(frameFacade, this.SessionStateService);

            SettingsPane.GetForCurrentView().CommandsRequested += this.OnCommandsRequested;

            // ViewModelLocator の名前解決メソッドを設定する
            ViewModelLocator.SetDefaultViewModelFactory(this.Resolve);

            this.OnRegisterKnownTypesForSerialization();
            if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
            {
                await this.SessionStateService.RestoreSessionStateAsync();
            }

            await this.OnInitializeAsync(args);
            if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
            {
                // 必要な場合のみ、保存されたセッション状態を復元
                try
                {
                    this.SessionStateService.RestoreFrameState();
                    NavigationService.RestoreSavedNavigation();
                    this.isRestoringFromTermination = true;
                }
                catch (SessionStateServiceException)
                {
                    //状態の復元に何か問題があった場合
                    //状態がないものとして続行する
                }
            }

            // Frame を現在のウィンドウに配置
            Window.Current.Content = rootFrame;
        }

        return rootFrame;
    }

    /// <summary>
    /// 画面遷移サービスを生成する
    /// </summary>
    /// <param name="rootFrame">Frame</param>
    /// <param name="sessionStateService">セッション状態サービス</param>
    /// <returns>初期化後の画面遷移サービス</returns>
    private INavigationService CreateNavigationService(IFrameFacade rootFrame, ISessionStateService sessionStateService)
    {
        var navigationService = new PageNavigationService(rootFrame, this.GetPageType, sessionStateService);
        return navigationService;
    }

    /// <summary>
    /// アプリケーションの実行が中断されたときに呼び出されます。アプリケーションの状態は、
    /// アプリケーションが終了されるのか、メモリの内容がそのままで再開されるのか
    /// わからない状態で保存されます。
    /// </summary>
    /// <param name="sender">中断要求の送信元。</param>
    /// <param name="e">中断要求の詳細。</param>
    private async void OnSuspending(object sender, SuspendingEventArgs e)
    {
        this.IsSuspending = true;
        try
        {
            var deferral = e.SuspendingOperation.GetDeferral();

            //Bootstrap inform navigation service that app is suspending.
            NavigationService.Suspending();

            // アプリケーションの状態を保存
            await this.SessionStateService.SaveAsync();

            deferral.Complete();
        }
        finally
        {
            this.IsSuspending = false;
        }
    }

    /// <summary>
    /// 設定チャームのメニュー生成
    /// </summary>
    /// <returns>設定チャームに表示するコマンドのリスト</returns>
    protected virtual IList<SettingsCommand> GetSettingsCommands()
    {
        return new List<SettingsCommand>();
    }

    /// <summary>
    /// チャーム表示要求イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="args"><see cref="SettingsPaneCommandsRequestedEventArgs"/> のイベント引数</param>
    private void OnCommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
    {
        if (args == null || args.Request == null || args.Request.ApplicationCommands == null)
        {
            return;
        }

        var applicationCommands = args.Request.ApplicationCommands;
        var settingsCommands = this.GetSettingsCommands();

        foreach (var settingsCommand in settingsCommands)
        {
            applicationCommands.Add(settingsCommand);
        }
    }
}

基本オリジナルの MvvmAppBase を踏襲しますが例によって以下の処理は書き換えています

    /// <summary>
    /// 画面遷移サービスを生成する
    /// </summary>
    /// <param name="rootFrame">Frame</param>
    /// <param name="sessionStateService">セッション状態サービス</param>
    /// <returns>初期化後の画面遷移サービス</returns>
    private INavigationService CreateNavigationService(IFrameFacade rootFrame, ISessionStateService sessionStateService)
    {
        var navigationService = new PageNavigationService(rootFrame, this.GetPageType, sessionStateService);
        return navigationService;
    }

前回作成した PageNavigationService クラスを画面遷移に利用するように設定しているだけです

実際にアプリを作る際にはこの MvpvmAppBase クラスを継承して下記のように App.xaml.cs を記述します

/// <summary>
/// アプリケーション
/// </summary>
public sealed partial class App : MvpvmAppBase
{
    /// <summary>
    /// コンストラクタ
    /// </summary>
    public App()
    {
        this.InitializeComponent();
    }

    /// <summary>
    /// アプリケーション起動処理
    /// </summary>
    /// <param name="args"><see cref="LaunchActivatedEventArgs"/> の起動イベント引数</param>
    /// <returns>Task</returns>
    protected override Task OnLaunchApplication(LaunchActivatedEventArgs args)
    {
        NavigationService.Navigate("Top", null);
        return Task.FromResult<object>(null);
    }

    /// <summary>
    /// 中断時に復元用に退避する ViewModel を登録する
    /// </summary>
    protected override void OnRegisterKnownTypesForSerialization()
    {
        // セッションデータに保存する可能性のある ViewModel をすべて登録する
        this.SessionStateService.RegisterKnownType(typeof(PhotoViewModel));
        this.SessionStateService.RegisterKnownType(typeof(ItemContainerViewModel));
        this.SessionStateService.RegisterKnownType(typeof(GroupContainerViewModel));
        this.SessionStateService.RegisterKnownType(typeof(ObservableCollection<ItemContainerViewModel>));
        this.SessionStateService.RegisterKnownType(typeof(ObservableCollection<GroupContainerViewModel>));
    }

    /// <summary>
    /// アプリケーション初期化処理
    /// </summary>
    /// <param name="args"><see cref="IActivatedEventArgs"/> のイベント引数</param>
    /// <returns>Task</returns>
    protected async override Task OnInitializeAsync(IActivatedEventArgs args)
    {
        // View から対応する ViewModel を取得するロジックを設定する
        ViewModelLocator.SetDefaultViewTypeToViewModelTypeResolver(viewType =>
        {
            var viewName = viewType.FullName;
            viewName = viewName.Replace(".Views.", ".ViewModels.");
            var viewAssemblyName = typeof(ViewModelBase).GetTypeInfo().Assembly.FullName;
            var viewModelName = string.Format(CultureInfo.InvariantCulture, "{0}ViewModel, {1}", viewName, viewAssemblyName);
            return Type.GetType(viewModelName);
        });

        // 明示的に ViewModel の生成ロジックを指定する場合はここに記載する
    }
}

いろいろ追加していますがとりあえず動かすだけなら必要なのは次の2カ所

    /// <summary>
    /// アプリケーション起動処理
    /// </summary>
    /// <param name="args"><see cref="LaunchActivatedEventArgs"/> の起動イベント引数</param>
    /// <returns>Task</returns>
    protected override Task OnLaunchApplication(LaunchActivatedEventArgs args)
    {
        NavigationService.Navigate("Top", null);
        return Task.FromResult<object>(null);
    }

起動後の初期画面遷移の部分、戻り値は await 用なので特に意味はないです

    /// <summary>
    /// アプリケーション初期化処理
    /// </summary>
    /// <param name="args"><see cref="IActivatedEventArgs"/> のイベント引数</param>
    /// <returns>Task</returns>
    protected async override Task OnInitializeAsync(IActivatedEventArgs args)
    {
        // View から対応する ViewModel を取得するロジックを設定する
        ViewModelLocator.SetDefaultViewTypeToViewModelTypeResolver(viewType =>
        {
            var viewName = viewType.FullName;
            viewName = viewName.Replace(".Views.", ".ViewModels.");
            var viewAssemblyName = typeof(ViewModelBase).GetTypeInfo().Assembly.FullName;
            var viewModelName = string.Format(CultureInfo.InvariantCulture, "{0}ViewModel, {1}", viewName, viewAssemblyName);
            return Type.GetType(viewModelName);
        });

        // 明示的に ViewModel の生成ロジックを指定する場合はここに記載する
    }

ViewModelLocator.SetDefaultViewTypeToViewModelTypeResolver は View から ViewModel インスタンスを 解決するロジックを設定します

この場合は単純に View のクラス名に ViewModel を付けたクラス名を対応させる感じです TopPage → TopPageViewModel というように

あとは忘れやすい下記の記述ですが、これは中断時に ViewModel のデータをファイルにシリアライズで 退避しておくために必要な宣言になります

    /// <summary>
    /// 中断時に復元用に退避する ViewModel を登録する
    /// </summary>
    protected override void OnRegisterKnownTypesForSerialization()
    {
        // セッションデータに保存する可能性のある ViewModel をすべて登録する
        this.SessionStateService.RegisterKnownType(typeof(PhotoViewModel));
        this.SessionStateService.RegisterKnownType(typeof(ItemContainerViewModel));
        this.SessionStateService.RegisterKnownType(typeof(GroupContainerViewModel));
        this.SessionStateService.RegisterKnownType(typeof(ObservableCollection<ItemContainerViewModel>));
        this.SessionStateService.RegisterKnownType(typeof(ObservableCollection<GroupContainerViewModel>));
    }

ViewModel の [RestorableData] 属性を付けたプロパティに利用している 基本型でないクラス名(内部の子クラスも含む)をすべて登録しておかないと 中断時のシリアライズに失敗して例外が発生するので必要です

とりあえずこれで Prism で MVPVM が利用できるようになりました

おそらくまだ Xamarin の PCL では ObservableCollection が使えないのではないか? と思うのですぐに Xamarin 対応はできないと思いますが将来的なクロスプラットフォーム対応は しやすくなるのではないでしょか

次回はさらに!発表されたばかり?のユニバーサルアプリへの対応を進めてみたいと思います