More Related Content Similar to T119_5年間の試行錯誤で進化したMVPVMパターン (20) T119_5年間の試行錯誤で進化したMVPVMパターン3. わんくま同盟 東京勉強会 #119
はじめに
• 2014/06/07 東京勉強会 #90
「きっと怖くないMVVM&MVPVM」から5年
• 仕事でWPFの経験を積みながら
• 何かか作りたいアプリが出来た時の為に
MVPVMフレームワークだけでも作っておこうと思って
作り始めるも、なかなかうまくいかなかったり、他の勉強を
始めたりで中断
という事を繰り返してきました
• その試行錯誤の成果がまとまりつつあるので
紹介したいと思います。
4. わんくま同盟 東京勉強会 #119
MVPVMとは
• Model-View-Presenter-ViewModel
• MVVMパターンにMVPパターンを合体させた
もの
• MVVMでは,ViewとViewModelに分散してい
た画面遷移に関する処理をPresenter一つに
まとめることができるため処理が追いやすい
5. わんくま同盟 東京勉強会 #119
Model
• ビジネスロジック
• View,Presenter,ViewModel以外の部分
View
• ユーザーインターフェース
• UIへの出力とUIからの入力を担当する。
• FrameworkElementの派生クラス
• XAMLの記述+対応する partial class
6. わんくま同盟 東京勉強会 #119
ViewModel
• Viewの情報を保持(データバインド)
• Viewから受け取った入力やコマンドをPresenterに渡す
Presenter
• View,ViewModelを保持、その接続を管理する
• ViewModelから受け取った入力やコマンドを処理し
Modelを呼び出す。
• ViewModelのプロパティを通してViewを変更する。
• ナビゲーション・画面遷移の管理
9. わんくま同盟 東京勉強会 #119
画面遷移時にどちらのコントロールを使うか
• ContentControl
ひとつのコントロールを表示するだけ
シンプルで扱いやすい
• Frame,NavigationWindow (UWPではFrameのみ)
ナビゲーション前・後イベント
履歴(戻る、進む)
Pageインスタンスのキャッシュ等
画面遷移に関する機能を持つ
正直、履歴機能とかあまり使わないので
ContentControlで良い気がする
11. わんくま同盟 東京勉強会 #119
{x:Bind}マークアップ
• UWPで追加されたバインド方法
• {Binding}より高速
• PathのルートはDataContext ではなく、Page
• コンパイル時にバインド式が検証されコードが生成される
(obj/*.g.cs に生成)
• バインド式が間違っていたらコンパイルエラーが発生
12. わんくま同盟 東京勉強会 #119
{x:Bind}マークアップ
• Pageを表すクラスからバインディング ソース(ViewModel)
クラスを公開する必要がある
public sealed partial class Page1 : Page
{
public Page1()
{
this.InitializeComponent();
this.ViewModel = new Page1ViewModel();
}
public Page1ViewModel ViewModel { get; set; }
}
<Page x:Class=“Page1" ... >
<TextBlock Text="{x:Bind Path=ViewModel.Text, Mode=OneWay}" ... />
</Page>
13. わんくま同盟 東京勉強会 #119
obj/Page1.g.cs ({x:bind})
partial class Page1 :…
{
private static class XamlBindingSetters
{
public static void Set_Windows_UI_Xaml_Controls_TextBlock_Text
(TextBlock obj, String value, string targetNullValue)
{
if (value == null && targetNullValue != null)
{value = targetNullValue;}
obj.Text = value ?? String.Empty;
}
};
private class Page1_obj1_Bindings : …
{
public void Update()
{
this.Update_(this.dataRoot, NOT_PHASED);
this.initialized = true;
}
private void Update_(Views.Page1 obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | (1 << 0))) != 0)
{this.Update_ViewModel(obj.ViewModel, phase);}
}
}
private void Update_ViewModel(Page1ViewModel obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | (1 << 0))) != 0)
{this.Update_ViewModel_Text(obj.Text, phase);}
}
}
private void Update_ViewModel_Text(System.String obj, int phase)
{
if ((phase & ((1 << 0) | NOT_PHASED )) != 0)
{
if (!isobj2TextDisabled)
{
XamlBindingSetters.
Set_Windows_UI_Xaml_Controls_TextBlock_Text(
this.obj2, obj, null);
}
}
}
}
}
14. わんくま同盟 東京勉強会 #119
obj/Page1.g.cs ({Binding})
partial class Page1 :…
{
private class Page1_obj1_Bindings : …
{
public void Update()
{
this.Update_(this.dataRoot, NOT_PHASED);
this.initialized = true;
}
private void Update_(Views.Page1 obj, int phase)
{
if (obj != null)
{
}
}
}
}
15. わんくま同盟 東京勉強会 #119
{x:Bind}とイベント、関数
• コマンドだけでなく、イベント(と関数)のバインドが可能
• <Button Click="{x:Bind ViewModel.OnClick}" />
• RoutedEventも使用可
<Button Click="{x:Bind ViewModel.Clicked}" />
• Bind構文内で関数を呼び出して
Converterのように使うことができる。
<TextBlock Text=“{x:Bind ViewModel.ConvertFunc(ViewModel.Text) }” />
18. わんくま同盟 東京勉強会 #119
最終的にこんな感じに
public sealed partial class Page1 : Page
{
public Page1ViewModel ViewModel
{
set { this.DataContext = value; }
get { return this.DataContext as Page1ViewModel; }
}
}
21. わんくま同盟 東京勉強会 #119
サンプル
• UWP
• Frame(NavigationWindow)で
ボタンを押すとPage1からPage2,Page2から
Page1へと遷移する
• https://github.com/akatukisiden/MVPVM
23. わんくま同盟 東京勉強会 #119
アプリケーションクラス
public sealed partial class App : Application
{
public WindowPresenter WindowPresenter { get; set; }
public App()
{
// 事前起動有効化
CoreApplication.EnablePrelaunch(true);
this.InitializeComponent();
this.EnteredBackground += App_EnteredBackground;
this.LeavingBackground += App_LeavingBackground;
this.Resuming += App_Resuming;
this.Suspending += App_Suspending;
this.UnhandledException += App_UnhandledException;
WindowPresenter = new WindowPresenter();
}
24. わんくま同盟 東京勉強会 #119
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{ WindowPresenter.Initialize(); }
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{ /* 以前中断したアプリケーションから状態を読み込みます*/ }
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
WindowPresenter.Navigate(typeof(Page1));
}
WindowPresenter.Activate();
}
else
{ /* 事前起動 */ }
}
初期化処理
事前起動済みの時は本起動時
は実行しない
本起動時のみ実行
画面を表示する
26. わんくま同盟 東京勉強会 #119
WindowPresenterBase.cs
public abstract class WindowPresenterBase
{
public WindowPresenterBase()
{ }
public Window Window { set; get; }
public virtual void Initialize()
{
if (Window == null)
{
Window = Window.Current;
Window.Activated += Window_Activated;
Window.Closed += Window_Closed;
Window.SizeChanged += Window_SizeChanged;
Window.VisibilityChanged += Window_VisibilityChanged;
}
}
public virtual void Cleanup()
{
Window.Activated -= Window_Activated;
Window.Closed -= Window_Closed;
Window.SizeChanged -= Window_SizeChanged;
Window.VisibilityChanged -= Window_VisibilityChanged;
Window = null;
}
27. わんくま同盟 東京勉強会 #119
WindowPresenter.cs
public class WindowPresenter :WindowPresenterBase
{
public RootFramePresenter RootFramePresenter { get; set; }
public WindowPresenter()
{
RootFramePresenter = new RootFramePresenter();
}
public override void Initialize()
{
base.Initialize();
// ナビゲーション コンテキストとして動作するフレームを作成
var root = new Frame();
this.Window.Content = root;
this.RootFramePresenter.View = root;
this.RootFramePresenter.Initialize();
}
public bool Navigate(Type key) { return RootFramePresenter.Navigate(key);}
public override void Cleanup() { base.Cleanup(); }
28. わんくま同盟 東京勉強会 #119
Frame.Navigate(Type, object,NavigationTransitionInfo)
• 遷移先ページを指すType,パラメータ、ナビゲーション時のア
ニメの種類を引数に取る
• WPFではアニメの種類は無しでTypeの代わりにUriを用いる
– Pageのインスタンスを使うオーバーロードもあるが、ナビ
ゲーションのキャンセル時等は、
作成したインスタンスが使われないままになるので個人的
には非推奨
• Navigating(キャンセル可),Navigatedイベントが呼
び出される
29. わんくま同盟 東京勉強会 #119
Frame.Navigate(Type, object,NavigationTransitionInfo)
• Navigatingイベントでキャンセルされた場合
NavigationStoppedイベントが発火
– WPFではキャンセル時には発生しないがStopLoading()
を呼び出すことで手動で発生させることができる
– ナビゲーション中に別のナビゲーションを開始しても
発生するらしいが詳しい条件は不明
• Navigatedイベント中に例外がが発生すると
NavigationFailedイベントが発火
– WPFだとIOExceptionじゃないといけないので注意
30. わんくま同盟 東京勉強会 #119
RootFramePresenter.cs
public class RootFramePresenter: FramePresenterBase<Frame,EmptyViewModel>
{
public RootFramePresenter()
{
AddChild(typeof(Page1), new Page1Presenter());
AddChild(typeof(Page2), new Page2Presenter());
}
public override void Initialize()
{
base.Initialize();
this.IsNavigationStackEnabled = true;
}
public override void Cleanup()
{
base.Cleanup();
}
}
最初に、Presenterのインスタンスを作成
Viewの型とセットで登録
31. わんくま同盟 東京勉強会 #119
Presenterの作成タイミング
• Presenter,とViewModelは最初にまとめて作成して
おく。
• Viewのインスタンスが作られるタイミングで
Presenterを作ろうとしたこともあるのだが、
気が付いたらFrameのもつ履歴やインスタンス
キャッシュの機能をPresenterに再実装しようとして
しまったり、
Viewの型に応じたPresenterを作成するしくみが必
要になったりしたため、
これなら最初に作成しておいた方が楽だと判断した
32. わんくま同盟 東京勉強会 #119
FramePresenterBase
public abstract class FramePresenterBase<TView, TViewModel>
: ParentPresenterBaseCore
where TView : Windows.UI.Xaml.Controls.Frame, new()
where TViewModel : ViewModelBase, new()
{
public FramePresenterBase() { ViewModel = new TViewModel(); }
public bool IsNavigationStackEnabled
{
get { return View.IsNavigationStackEnabled; }
set { View.IsNavigationStackEnabled = value; }
}
public TView View { get { return this.ViewBase as TView; } set { this.ViewBase = value; } }
public TViewModel ViewModel
{ get { return this.ViewModelBase as TViewModel; } set { this.ViewModelBase = value; } }
ナビゲーション履歴を使うかどうか
Viewの同名プロパティにアクセス
非ジェネリック基底クラス(Core)の
プロパティをキャスト
33. わんくま同盟 東京勉強会 #119
FramePresenterBase
public override void Initialize()
{
base.Initialize();
IsNavigationStackEnabled = false;
View.Navigating += View_Navigating;
View.Navigated += View_Navigated;
View.NavigationFailed += View_NavigationFailed;
View.NavigationStopped += View_NavigationStopped;
}
public override void Cleanup()
{
View.Navigating -= View_Navigating;
View.Navigated -= View_Navigated;
View.NavigationFailed -= View_NavigationFailed;
View.NavigationStopped -= View_NavigationStopped;
base.Cleanup();
}
34. わんくま同盟 東京勉強会 #119
FramePresenterBase
public override bool Navigate
(Type key, object extraData = null, NavigationTransitionInfo infoOrverride =null)
{ return View.Navigate(key, extraData, infoOrverride); }
public override void GoForward()
{
View.GoForward();
}
public override void GoBack(NavigationTransitionInfo infoOrverride = null)
{ View.GoBack(infoOrverride); }
private void View_Navigated(object sender, NavigationEventArgs e)
{ View_NavigatedCore(sender, e); }
}
画面遷移関係の関数
Frameの同名関数を呼び出し
通常の遷移,進む,戻る
遷移時のアニメーションの種類
ParentPresenterBaseCore
の関数を呼び出し
35. わんくま同盟 東京勉強会 #119
ParentPresenterBaseCore.cs
public abstract class ParentPresenterBaseCore : PresenterBaseCore
{
protected
Dictionary<Type, ChildPagePresenterBaseCore> Children { get; private set; };
public void AddChild(Type key, ChildPagePresenterBaseCore child)
{
Children.Add(key, child);
}
public void RemoveChild(Type key)
{
Children.Remove(key);
}
本体はDictionary
子Presenterの登録
子Presenterの解除
36. わんくま同盟 東京勉強会 #119
ParentPresenterBaseCore.cs
public ParentPresenterBaseCore()
{ Children = new Dictionary<Type, ChildPagePresenterBaseCore>(); }
public ChildPagePresenterBaseCore CurrentChild { get; internal set; };
public override void Initialize()
{ base.Initialize(); }
public override void Cleanup()
{
if (CurrentChild != null)
{
CurrentChild.Cleanup();
CurrentChild = null;
}
base.Cleanup();
}
表示中の子(Page)Presenter
37. わんくま同盟 東京勉強会 #119
ParentPresenterBaseCore.cs
protected void View_NavigationStopped(object sender, NavigationEventArgs e)
{
CurrentChild.InvokeOnNavigationStopped(e);
}
protected void View_Navigating(object sender, NavigatingCancelEventArgs e)
{
if (CurrentChild != null)
{ CurrentChild.InvokeOnNavigatingFrom(e); }
}
protected void View_NavigationFailed(object sender, NavigationFailedEventArgs e)
{ CurrentChild.InvokeOnNavigationFailed(e); }
Internal 関数を通してイベントに対応するPagePresenterの関数を呼び出し
38. わんくま同盟 東京勉強会 #119
ParentPresenterBaseCore.cs
protected internal void View_NavigatedCore(object sender, NavigationEventArgs e)
{
if (e != null)
{
if (CurrentChild != null)
{
CurrentChild.InvokeOnNavigatedFrom(e);
CurrentChild.Cleanup();
CurrentChild = null;
}
Type key = e.Content.GetType();
var childPresenter = Children[key];
var element = e.Content as FrameworkElement;
childPresenter.Parent = this;
childPresenter.ViewBase = element;
CurrentChild = childPresenter;
childPresenter.Initialize();
childPresenter.InvokeOnNavigatedTo(e);
}
}
表示中のページの終了処理
遷移先Pageに対応したPagePresenterを取得
遷移先Presenterの初期化処理
internal関数を介して
PagePresenter.OnNavigatedFrom(e)
の呼び出し
PagePresenter.OnNavigatedTo(e)
の呼び出し
39. わんくま同盟 東京勉強会 #119
• UWPのPageには3つのオーバーライド用関数が存在
– OnNavigatedTo (Pageへ遷移してきたときに実行)
– OnNavigatingFrom (Pageから遷移するときに実行,キャンセル可)
– OnNavigatedFrom (Pageから遷移したときに実行)
• MVPVMではできるだけViewにコードを書きたくないため同
名の関数をPagePresenterに用意して同じような感覚で使え
るようにしている
40. わんくま同盟 東京勉強会 #119
Page1Presenter
public class Page1Presenter : PagePresenterBase<Views.Page1, Page1ViewModel>
{
public override void Initialize()
{
base.Initialize();
ViewModel.ClickEvent += Click;
this.ViewModel.Text = "Page1";
}
public void Click(object sender, RoutedEventArgs args)
{ this.Parent.Navigate(typeof(Page2), null); }
public override void Cleanup()
{
ViewModel.ClickEvent -= Click;
base.Cleanup();
}
x:bindで呼び出すイベント
41. わんくま同盟 東京勉強会 #119
PagePresenterBase
public abstract class PagePresenterBase<TView, TViewModel> : ChildPagePresent
erBaseCore
where TView : Page, new()
where TViewModel : ViewModelBase, new()
{
public PagePresenterBase()
{ ViewModel = new TViewModel(); }
public TView View
{ get { return this.ViewBase as TView; } set { this.ViewBase = value; } }
public TViewModel ViewModel
{
get { return this.ViewModelBase as TViewModel; }
set { this.ViewModelBase = value; }
}
42. わんくま同盟 東京勉強会 #119
PagePresenterBase
public override void Initialize()
{
base.Initialize();
View.Loaded += OnPageLoaded;
}
protected virtual void OnPageLoaded(object sender, RoutedEventArgs e)
{}
public override void Cleanup()
{
View.Loaded -= OnPageLoaded;
base.Cleanup();
}
}
43. わんくま同盟 東京勉強会 #119
ChildPresenterBaseCore
public abstract class ChildPagePresenterBaseCore : PresenterBaseCore
{
public ParentPresenterBaseCore Parent { get; set; }
public override void Initialize()
{
base.Initialize();
}
public override void Cleanup()
{
Parent = null;
base.Cleanup();
}
親FramePresenter
参照外し
44. わんくま同盟 東京勉強会 #119
ChildPresenterBaseCore
internal void InvokeOnNavigatedTo(NavigationEventArgs e)
{ OnNavigatedTo(e);}
internal void InvokeOnNavigatingFrom(NavigatingCancelEventArgs e)
{ OnNavigatingFrom(e); }
internal void InvokeOnNavigatedFrom(NavigationEventArgs e)
{ OnNavigatedFrom(e); }
internal void InvokeOnNavigationStopped(NavigationEventArgs e)
{OnNavigationStopped (e); }
internal void InvokeOnNavigationFailed(NavigationFailedEventArgs e)
{ OnNavigationFailed(e); }
45. わんくま同盟 東京勉強会 #119
ChildPresenterBaseCore
protected virtual void OnNavigatedTo(NavigationEventArgs e)
{ }
protected virtual void OnNavigatingFrom(NavigatingCancelEventArgs e)
{}
protected virtual void OnNavigatedFrom(NavigationEventArgs e)
{}
protected virtual void OnNavigationFailed(NavigationFailedEventArgs e)
{}
protected virtual void OnNavigationStopped(NavigationEventArgs e)
{}
}
46. わんくま同盟 東京勉強会 #119
PresenterBaseCore
public abstract class PresenterBaseCore
{
protected TaskScheduler UIThreadTaskScheduler { get; private set; }
public bool IsEnable { get; private set; }
internal FrameworkElement ViewBase { get; set; }
internal ViewModelBase ViewModelBase { get; set; }
public PresenterBaseCore()
{
this.IsEnable = false;
}
Presenterが有効かどうか
Initialize()でtrue
CleanUp()でfalse
View,ViewModel
非ジェネリック基底クラス用
47. わんくま同盟 東京勉強会 #119
PresenterBaseCore
public virtual void Initialize()
{
this.UIThreadTaskScheduler
= TaskScheduler.FromCurrentSynchronizationContext();
ViewBase.DataContext = ViewModelBase;
IsEnable = true;
}
public virtual void Cleanup()
{
if (ViewBase != null)
{
ViewBase.DataContext = null;
ViewBase = null;
}
IsEnable = false;
}
ViewとViewModelを接続
接続解除
Viewの参照外し
破壊ではなく解放
管理をViewフレームワークに
返すイメージ
48. わんくま同盟 東京勉強会 #119
まとめ
• UWPでは画面遷移アニメーションがあるので
ContentControlではなくてFrameを使う
• Presenter,ViewModelは最初にまとめて作る
• View(Page)の作成はできる限りフレームワー
クに任せ、
使用後は参照を解除し
管理権をフレームワークに返すことでViewの
機能を最大限生かす