WPF+Prism 5.0 でMVVMアプリを作る(画面遷移)
私が知ってるWindowsFormアプリの画面遷移は
- モーダルウインドウを開いて、オーナーウインドウは非表示にし、それっぽくみせる
- ScreenManagerクラス(非ウインドウクラス)を作って、現在画面を閉じてから次の画面を開く
こんな感じです。
代わってWPFではFrameコントロールを使用するとこで、
- Webアプリっぽく「画面の戻る」ができる
ようになり、とてもモダンな感じになっています。
使い方はこんな感じ。
- FrameコントロールのSourceプロパティに初期表示のUserControlのファイルパスを指定
- UserControl内でHyperlink(遷移したいUserControlのファイルパスを指定)を記述
MainWindow.xaml
<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Frame Source="/Views/Top.xaml" NavigationUIVisibility="Visible" > </Frame> </Window>
/Views/Top.xaml
<UserControl x:Class="Top" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid Background="LightYellow"> <TextBlock><Hyperlink NavigateUri="/Views/Info.xaml">クリックすると情報ページへ遷移します</Hyperlink></TextBlock> </Grid> </UserControl>
/Views/Info.xaml
<UserControl x:Class="Info" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid Background="LightBlue"> <TextBlock><Hyperlink NavigateUri="/Views/Top.xaml">クリックするとトップページへ遷移します</Hyperlink></TextBlock> </Grid> </UserControl>
とても簡単です!と言いたいところですが、静的(固定)ページへのリンクしかできず、「選択したアイテムを次のページの初期値として渡したい(引数渡し)」場合はこの方法は使用できません。*1
引数を渡したい場合はFrameコントロール+画面遷移サービスクラス(Navigator)で対応させましょう。
画面遷移をするサービスを作る
MVVMで実装しますので、処理の流れはこのようなイメージとなります。
厳密に書くともっと複雑になりますが、遷移という観点ではこれぐらいの粒度で問題ないでしょう。
ここでいうCurrentView、CurrentViewModel、NextView、NextViewModelは画面遷移の本質ではありませんので省略*2。Frameは標準のコントロールなので特に説明なし。問題はNavigator。
Navigatorは標準では提供されていませんので、内製します。といっても、ほぼ定型文なのでこんな感じで書いておしまい。
''' <summary> ''' フレームナビゲータ ''' </summary> ''' <remarks> ''' NavigationServiceのラッパークラス ''' メインウインドウのメインコンテンツ表示用 ''' </remarks> Public NotInheritable Class FrameNavigator Public Sub New() Me.New("MainFrame") End Sub Public Sub New(frameName As String) _frameName = frameName _frame = GetDescendantFromName(Of Frame)(Application.Current.MainWindow, Me.FrameName) AddHandler Me.Frame.LoadCompleted, AddressOf Me.Frame_LoadCompleted Me.Service = Me.Frame.NavigationService End Sub Private _frameName As String ''' <summary> ''' フレーム名 ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public ReadOnly Property FrameName As String Get Return _frameName End Get End Property Private _frame As Frame Public ReadOnly Property Frame As Frame Get Return _frame End Get End Property ''' <summary> ''' メインウィンドウナビゲーションサービス ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Private Property Service As NavigationService Public Function Navigate(uri As Uri) As Boolean Implements IFrameNavigator.Navigate Dim r = ServiceLocator.Current.GetInstance(Of IMessenger)() If r IsNot Nothing Then r.Send(New InfoMessage(String.Empty)) Return Me.Service.Navigate(uri) End Function Public Function Navigate(uri As Uri, extraData As Object) As Boolean Implements IFrameNavigator.Navigate Dim r = ServiceLocator.Current.GetInstance(Of IMessenger)() If r IsNot Nothing Then r.Send(New InfoMessage(String.Empty)) Return Me.Service.Navigate(uri, extraData) End Function Private Sub Frame_LoadCompleted(sender As Object, e As NavigationEventArgs) If e.ExtraData IsNot Nothing Then '引数がある場合、VMに渡す Dim vm = TryCast(DirectCast(e.Content, UserControl).DataContext, IExtraDataReceivable) If vm IsNot Nothing Then vm.SetExtraData(e.ExtraData) End If End Sub ''' <summary> ''' 要素検索 ''' </summary> ''' <typeparam name="T"></typeparam> ''' <param name="parent">検査対象オブジェクト</param> ''' <param name="name">検索対象要素名</param> ''' <returns></returns> ''' <remarks></remarks> Private Shared Function GetDescendantFromName(Of T)(parent As DependencyObject, name As String) As T Dim cnt = VisualTreeHelper.GetChildrenCount(parent) If cnt = 0 Then Return Nothing For i As Integer = 0 To cnt - 1 Dim element = TryCast(VisualTreeHelper.GetChild(parent, i), FrameworkElement) If element Is Nothing Then Continue For If element.Name = name Then Return DirectCast(CType(element, Object), T) Dim innerElement = GetDescendantFromName(Of T)(element, name) If innerElement IsNot Nothing Then Return innerElement Next Return Nothing End Function End Class
ポイント
- メインウインドウクラスに「MainFrame」と名前を付けたFrameコントロールを配置しておく。 実体はGetDescendantFromNameメソッドにて、勝手に検索、特定されます。
- FrameNavigatorクラスはシングルトンで。(シングルトンでなくてもかまいませんけど、1つインスタンスがあれば十分)
- 引数がある場合は、VM側が受け取り口(インターフェイス)を作っておく。 対したコードではないですが、受け取り口インターフェイスはこんな感じ。Navigatorクラスは次に遷移するVのVMは具体的には知り得ませんので、Object型で流し込み。
''' <summary> ''' データ受け取りインターフェイス ''' </summary> ''' <remarks></remarks> Public Interface IExtraDataReceivable ''' <summary> ''' 遷移元が指定したデータを渡します ''' </summary> ''' <param name="data"></param> ''' <remarks></remarks> Sub SetExtraData(data As Object) End Interface
さらなる改良
上記のコードで基本的な画面遷移は実行可能。あとは
- ホーム画面への遷移(戻る履歴の除去)
- ログアウト(戻る履歴の除去、ログインページへの遷移)
- 遷移時にログを残す
- 遷移前、編集中であれば確認メッセージを出す。キャンセルされた場合は画面遷移を中止する。
なんかがあるとよさそうです。
*1:できるかもしれませんが、詳細不明。stackoverflowでもhyperlinkのみで解決している風情なし。
*2:Viewは単なるUI、ViewModelはViewに表示するデータ。