MVPVM の Prism をユニバーサルアプリに対応させる(2)
MVPVM の Prism をユニバーサルアプリに対応させる(1)の続きです!
Windows Phone 8.1 とコードを共有することで変更が必要になった部分を修正します
まずはアプリケーションの基底クラス
namespace UniversalMVPVMApp.Common { using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Threading.Tasks; using Microsoft.Practices.Prism.StoreApps; using Microsoft.Practices.Prism.StoreApps.Interfaces; using UniversalMVPVMApp.Services; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Resources; #if WINDOWS_APP using Windows.UI.ApplicationSettings; #endif using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media.Animation; using Windows.UI.Xaml.Navigation; /// <summary> /// MVPVM アプリケーション抽象クラス /// </summary> public abstract class MvpvmAppBase : Application { #region Privates /// <summary> /// Terminated からの復帰フラグ /// </summary> private bool isRestoringFromTermination; #if WINDOWS_PHONE_APP /// <summary> /// 遷移効果 /// </summary> private TransitionCollection transitions; #endif #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); #if WINDOWS_APP SettingsPane.GetForCurrentView().CommandsRequested += this.OnCommandsRequested; #endif // 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) { //状態の復元に何か問題があった場合 //状態がないものとして続行する } } #if WINDOWS_PHONE_APP // スタートアップのターンスタイル ナビゲーションを削除します。 if (rootFrame.ContentTransitions != null) { this.transitions = new TransitionCollection(); foreach (var c in rootFrame.ContentTransitions) { this.transitions.Add(c); } } rootFrame.ContentTransitions = null; rootFrame.Navigated += this.OnPhoneFirstNavigated; #endif // Frame を現在のウィンドウに配置 Window.Current.Content = rootFrame; } return rootFrame; } #if WINDOWS_PHONE_APP /// <summary> /// アプリを起動した後のコンテンツの移行を復元します。 /// </summary> /// <param name="sender">ハンドラーがアタッチされたオブジェクト。</param> /// <param name="e">ナビゲーション イベントの詳細。</param> private void OnPhoneFirstNavigated(object sender, NavigationEventArgs e) { var rootFrame = sender as Frame; rootFrame.ContentTransitions = this.transitions ?? new TransitionCollection() { new NavigationThemeTransition() }; rootFrame.Navigated -= this.OnPhoneFirstNavigated; } #endif /// <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; } } #if WINDOWS_APP /// <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); } } #endif } }
Windows Phone アプリでは画面遷移の効果の付け方が Windows アプリとは異なるので下記を #if ディレクティブで追加しています
#if WINDOWS_PHONE_APP /// <summary> /// 遷移効果 /// </summary> private TransitionCollection transitions; #endif ・・・ /// <summary> /// Frame と Content を初期化する /// </summary> /// <param name="args"><see cref="IActivatedEventArgs"/> のイベント引数</param> /// <returns>Task</returns> protected async Task<Frame> InitializeFrameAsync(IActivatedEventArgs args) { ・・・ #if WINDOWS_PHONE_APP // スタートアップのターンスタイル ナビゲーションを削除します。 if (rootFrame.ContentTransitions != null) { this.transitions = new TransitionCollection(); foreach (var c in rootFrame.ContentTransitions) { this.transitions.Add(c); } } rootFrame.ContentTransitions = null; rootFrame.Navigated += this.OnPhoneFirstNavigated; #endif // Frame を現在のウィンドウに配置 Window.Current.Content = rootFrame; } return rootFrame; } #if WINDOWS_PHONE_APP /// <summary> /// アプリを起動した後のコンテンツの移行を復元します。 /// </summary> /// <param name="sender">ハンドラーがアタッチされたオブジェクト。</param> /// <param name="e">ナビゲーション イベントの詳細。</param> private void OnPhoneFirstNavigated(object sender, NavigationEventArgs e) { var rootFrame = sender as Frame; rootFrame.ContentTransitions = this.transitions ?? new TransitionCollection() { new NavigationThemeTransition() }; rootFrame.Navigated -= this.OnPhoneFirstNavigated; } #endif
さらに Windows Phone アプリには設定チャームがないので下記を #if ディレクティブで Windows アプリ専用にしています
namespace UniversalMVPVMApp.Common { using System; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Threading.Tasks; using Microsoft.Practices.Prism.StoreApps; using Microsoft.Practices.Prism.StoreApps.Interfaces; using UniversalMVPVMApp.Services; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Resources; #if WINDOWS_APP using Windows.UI.ApplicationSettings; #endif ・・・ /// <summary> /// Frame と Content を初期化する /// </summary> /// <param name="args"><see cref="IActivatedEventArgs"/> のイベント引数</param> /// <returns>Task</returns> protected async Task<Frame> InitializeFrameAsync(IActivatedEventArgs args) { ・・・ #if WINDOWS_APP SettingsPane.GetForCurrentView().CommandsRequested += this.OnCommandsRequested; #endif ・・・ #if WINDOWS_APP /// <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); } } #endif
アプリの基底クラスの対応はこれで大丈夫でした
次は画面遷移の PageNavigationService を下記のように変更します
/// <summary> /// 画面遷移サービス /// </summary> public class PageNavigationService : INavigationService { #region Privates /// <summary> /// 最終遷移先パラメータキー /// </summary> private const string LastNavigationParameterKey = "LastNavigationParameter"; /// <summary> /// 最終遷移先画面キー /// </summary> private const string LastNavigationPageKey = "LastNavigationPageKey"; /// <summary> /// Frame ファサード /// </summary> private readonly IFrameFacade frame; /// <summary> /// 遷移先解決処理 /// </summary> private readonly Func<string, Type> navigationResolver; /// <summary> /// セッション管理サービス /// </summary> private readonly ISessionStateService sessionStateService; #endregion //Privates /// <summary> /// コンストラクタ /// </summary> /// <param name="frame">Frame</param> /// <param name="navigationResolver">遷移先解決処理</param> /// <param name="sessionStateService">セッション管理サービス</param> public PageNavigationService(IFrameFacade frame, Func<string, Type> navigationResolver, ISessionStateService sessionStateService) { this.frame = frame; this.navigationResolver = navigationResolver; this.sessionStateService = sessionStateService; if (frame == null) { return; } #if WINDOWS_PHONE_APP Windows.Phone.UI.Input.HardwareButtons.BackPressed += this.OnHardwareButtonsBackPressed; #endif this.frame.Navigating += this.OnNavigating; this.frame.Navigated += this.OnNavigated; } /// <summary> /// 指定画面に遷移する /// </summary> /// <param name="pageToken">画面トークン</param> /// <param name="parameter">遷移パラメータ</param> /// <returns>成功した場合 <c>true</c>、それ以外は <c>false</c></returns> public bool Navigate(string pageToken, object parameter) { Type pageType = this.navigationResolver(pageToken); if (pageType == null) { var resourceLoader = ResourceLoader.GetForCurrentView(Constants.StoreAppsInfrastructureResourceMapId); var error = string.Format(CultureInfo.CurrentCulture, resourceLoader.GetString("FrameNavigationServiceUnableResolveMessage"), pageToken); throw new ArgumentException(error, "pageToken"); } // 全く同じ遷移でないか確認するため画面の型とパラメータを取得する var lastNavigationParameter = this.sessionStateService.SessionState.ContainsKey(LastNavigationParameterKey) ? this.sessionStateService.SessionState[LastNavigationParameterKey] : null; var lastPageTypeFullName = this.sessionStateService.SessionState.ContainsKey(LastNavigationPageKey) ? this.sessionStateService.SessionState[LastNavigationPageKey] as string : string.Empty; if (lastPageTypeFullName != pageType.FullName || !AreEquals(lastNavigationParameter, parameter)) { return this.frame.Navigate(pageType, parameter); } return false; } /// <summary> /// 戻り遷移する /// </summary> public void GoBack() { this.frame.GoBack(); } #if WINDOWS_PHONE_APP /// <summary> /// 戻るボタン押下イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnHardwareButtonsBackPressed(object sender, Windows.Phone.UI.Input.BackPressedEventArgs e) { if (this.CanGoBack()) { e.Handled = true; this.GoBack(); } } #endif /// <summary> /// 戻り遷移できるかどうか取得する /// </summary> /// <returns>遷移できる場合 <c>true</c>、それ以外は <c>false</c></returns> public bool CanGoBack() { return this.frame.CanGoBack; } ・・・以降省略
変更したのは Windows Phone には戻るハードボタンがあるのでその部分の対応だけです
上記変更だけして Windows アプリと Windows Phone アプリを起動してみると
問題なく起動できました!
思った以上にコードを共有できて簡単にマルチデバイス対応ができそうです