MVPパターンを業務アプリに適用する − 画面遷移
MVPパターンにおける画面遷移のやり方を調べていたが、具体的な情報があまりなく、これだというのが無かったので自分なりに考えてみた。
業務アプリでやるからには極力シンプルで、誰にでもわかるやり方にする必要がある*1。
とりあえず考えたのは、下図のようにプレゼンター側で遷移するビューを取り出し、必要な値を設定し表示するというやり方。
概要図
これくらい単純なのが一番いい。
ビューに値を設定する方法としては、
- コントロールを参照して直接値を設定する。
- プロパティを定義して、間接的に値を設定する。
が考えられるが、プロジェクト的には1.を採用したい。簡単だし、受け入れられ易い。プロパティってなに?って言われちゃうレベルだからね。
しかし、いくつか問題点がある。
- コントロールを直接参照するので、コントロール名を変更されたり、削除されたりするとコンパイルエラー
- 型の不一致(例えばTextBoxのTextプロパティはString型だけど、欲しいのはInteger型)
1.は、画面がころころ変わるのは事前に設計がちゃんとできていないからなので、それ以前の問題
事前の設計がきちんと行われていれば、コントロール名が変更されたり、削除される事は(ほとんど)無い。
2.は、利用する側でその都度変換をかけていては、あちこちに同じ様なコードが記述されてしまうので、
バグの温床になりかねない。
結果として、一番安全なのは、
3.は、プロパティを定義してもらう為の説得材料にもなりそう。こうしないと危険でしょ?みたいな。
実装
ということで、プレゼンターからビューにアクセスする為の変更を稚拙のMVPフレームワークに施していく。
まずは、PresenterBaseにコンテナからビューを取得する為のメソッドを追加する。
PresenterBase.vb(一部省略)
Public MustInherit Class PresenterBase(Of TView As Form) Private _container As IUnityContainer Public Sub SetContainer(ByVal container As IUnityContainer) _container = container End Sub Protected Function GetView(Of T As Form)() As T Return _container.Resolve(Of T)() End Function End Class
コンテナの設定は後でやるとして、GetViewというメソッドを定義した。
このメソッドは単に型パラメータで渡された型のインスタンスをResolveメソッドで取り出して返すだけ。
プレゼンターへのコンテナの設定はPresenterExtensionが行う。
PresenterExtension.vb(一部省略)
Public NotInheritable Class PresenterExtension Inherits UnityContainerExtension Protected Overrides Sub Initialize() Me.Context.Strategies.Add( _ New PresenterInitializeStrategy(Me.Container), UnityBuildStage.Setup _ ) End Sub Public NotInheritable Class PresenterInitializeStrategy Inherits BuilderStrategy Private _container As IUnityContainer Public Sub New(ByVal container As IUnityContainer) _container = container Initialize() End Sub Public Overrides Sub PostBuildUp(ByVal context As IBuilderContext) Dim view = context.Existing Dim viewType = view.GetType() If _presenterDefinitions.ContainsKey(viewType) Then Dim presenter = _container.Resolve(_presenterDefinitions(viewType)) presenter.SetView(view) presenter.SetContainer(_container) End If MyBase.PostBuildUp(context) End Sub End Class End Class
サンプルプログラムには画面遷移が無いので、画面遷移するように下図のメニュー画面を作る。
画面イメージ
コードはこんな感じ
MenuView.vb
Public Class MenuView Public Event MenuClick(ByVal sender As Object, ByVal e As MenuClickEventArgs) Private Sub menu_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnCalc.Click RaiseEvent MenuClick(sender, New MenuClickEventArgs(DirectCast(sender, Button).Tag)) End Sub End Class Public Class MenuClickEventArgs Inherits EventArgs Private _menuKind As MenuKind Public ReadOnly Property MenuKind() As MenuKind Get Return _menuKind End Get End Property Public Sub New(ByVal menuKind As MenuKind) _menuKind = menuKind End Sub End Class
クリックされたメニューはMenuClickEventArgsクラスのMenuKindプロパティで判断できるようになっている。
MenuKind列挙型の定義は以下
Enums.vb
Public Enum MenuKind 計算機 = 1 End Enum
で、このメニュー画面のプレゼンター
MenuViewPresenter.vb
Public Class MenuViewPresenter Inherits PresenterBase(Of MenuView) Protected Overrides Sub OnViewSet(ByVal view As MenuView) If view IsNot Nothing Then AddHandler view.MenuClick, AddressOf view_MenuClick End If MyBase.OnViewSet(view) End Sub Private Sub view_MenuClick(ByVal sender As Object, ByVal e As MenuClickEventArgs) Select Case e.MenuKind Case MenuKind.計算機 Dim calcView = GetView(Of CalcView)() calcView.StartPosition = FormStartPosition.CenterParent calcView.ShowDialog(Me.View) Case Else End Select End Sub End Class
実行して「計算機」ボタンをクリックすると、こんな感じ
スクリーンキャプチャ
これで適当な値を入力して計算したりなんかして、計算画面を閉じて、もう一度「計算機」ボタンをクリックすると前の値は保持されるずに初期状態の計算画面が表示される。
GetViewメソッドはDIコンテナから新しいオブジェクトを取り出すだけなので、毎回違うオブジェクトだからこの動きは当然の結果。
でも、使い捨ての画面ならこれでもいいけど、画面の値をずっと保持していたい場合なんかがあった場合、別途画面のインスタンスをどこかに保持しておくか、毎回画面の値をセットし直すなどのなんらかの対処をする必要がある。
こういう事はあまりしたくないので、DIコンテナから毎回同じインスタンスを取得できる、つまりSingletonなビューを取り出せるメソッドを用意しておく。
PresenterBase.vb(一部省略)
Protected Function GetViewOfSingleton(Of T As Form)(ByVal viewName As String) As T Dim view = _container.Resolve(Of T)(viewName) Dim singletonView = TryCast(view, SingletonViewBase) ' オブジェクトのLifetimeをSingletonで管理するオブジェクト Dim lifetimeMgr = New ContainerControlledLifetimeManager() If singletonView IsNot Nothing Then singletonView.SetLifetime(lifetimeMgr) End If ' Singletonで登録する。 ' 次回以降、コンテナからSingletonで取り出せるようになる。 _container.RegisterInstance(viewName, view, lifetimeMgr) Return view End Function
Protected Function GetViewOfSingleton(Of T As Form)(ByVal viewName As String) As T Dim view = _container.Resolve(Of T)(viewName) Dim lifetimeMgr = _context.GetLifetimeManager(view) If lifetimeMgr Is Nothing Then lifetimeMgr = New ContainerControlledLifetimeManager() ' Singletonで登録する。 ' 次回以降、コンテナからSingletonで取り出せるようになる。 _container.RegisterInstance(viewName, view, lifetimeMgr) Dim singletonView = TryCast(view, SingletonViewBase) ' SingletonViewBaseから継承していれば、Lifetimeの管理を任せる。 If singletonView IsNot Nothing Then singletonView.SetLifetime(lifetimeMgr) End If End If Return view End Function
GetViewOfSingletonメソッドで取得したビューはSingletonインスタンスになる。もちろんviewName引数で指定した名前単位でだけど。
ついでにこういうのも作っておいた。
SingletonViewBase.vb
Public Class SingletonViewBase Private _lifetimeMgr As ILifetimePolicy Friend Sub SetLifetime(ByVal lifetimeMgr As ILifetimePolicy) _lifetimeMgr = lifetimeMgr End Sub <System.Diagnostics.DebuggerNonUserCode()> _ Protected Overrides Sub Dispose(ByVal disposing As Boolean) Try If disposing AndAlso components IsNot Nothing Then components.Dispose() End If ' singletonオブジェクトを破棄 ' これやるとDisposeが呼ばれてStackOverflowを起こす 'If _lifetimeMgr IsNot Nothing Then '_lifetimeMgr.RemoveValue() '_lifetimeMgr = Nothing 'End If Finally MyBase.Dispose(disposing) End Try End Sub End Class
GetViewOfSingletonメソッドで生成したビューがSingletonViewBaseを継承していた場合はILifetimePolicyのインスタンスを設定しておき、SingletonViewBaseがDisposeされたタイミングでILifetimePolicyのRemoveValueメソッドを呼び出す事で、DIコンテナ上からもインスタンスが削除されるようになっている。
これで次からは同じviewNameでも新しいインスタンスが生成されるようになる。
ビューがSingletonになる可能性ができたことでPresenterExtension側にも対応が必要になったけど、その部分は割愛しておく。
とまぁ、こんな感じでビューを生成して画面遷移していけばいいと思う。