.NETで作る!

.NETに関するあれこれ(C#、VB.NET)

WPF+Prism 5.0 でMVVMアプリを作る(画面遷移)

私が知ってるWindowsFormアプリの画面遷移は

  • モーダルウインドウを開いて、オーナーウインドウは非表示にし、それっぽくみせる
  • ScreenManagerクラス(非ウインドウクラス)を作って、現在画面を閉じてから次の画面を開く

こんな感じです。

代わってWPFではFrameコントロールを使用するとこで、

  • Webアプリっぽく「画面の戻る」ができる

ようになり、とてもモダンな感じになっています。

使い方はこんな感じ。

  1. FrameコントロールのSourceプロパティに初期表示のUserControlのファイルパスを指定
  2. 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で実装しますので、処理の流れはこのようなイメージとなります。

f:id:mk3008net:20150505153037p:plain

厳密に書くともっと複雑になりますが、遷移という観点ではこれぐらいの粒度で問題ないでしょう。

ここでいう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に表示するデータ。

. .