MVVMでメッセンジャーなしに画面遷移を実現する- 画面遷移を画面状態としてモデル化する


□導入

今回のブログでは、「MVVMでメッセンジャーなしに画面遷移を実現する - 画面遷移を画面状態としてモデル化する」と題しまして、メッセンジャーなしに画面遷移を実現する方法のひとつをご紹介します。

MVVMをご提案すると「画面遷移とダイアログ表示はどうするのか?」と、多くの技術者が聞いてきます。どうやら自分でも調べてこのあたりがMVVMの鬼門だと感じ取るようです。

「どうやら画面遷移とダイアログ表示のためには、メッセンジャーを使うらしい。」 → 「調べたけどなんだかメッセンジャーが難しい、分からん。] → 「そもそもこんなことまでして、どうしてMVVMを使わなければならないんだ?」となります。

こうなると、画面遷移やダイアログ表示がMVVMでの問題であることを前提として、MVVMのメリット面を説明していく必要があり、このマイナスからの説明で私はいつも息切れしてしまいます。どうやら、MVVM入門者にはメッセンジャーはハードルが高すぎるようです(そして私自身メッセンジャーをあまりよく思っていません :p )MVVMを保ちつつも、シンプルに画面遷移やダイアログ表示を実現する代替の方法が求められていると感じます。

ダイアログ表示については、以前の私のブログで「MVVMでダイアログ表示のためにメッセンジャーを使用するのはおかしいのでは?」と題して、実績の薄い方法ながらご説明しましたので、今回は画面遷移についてご説明します。

実はここでご紹介する画面遷移の方法は、世の中的にはメジャーなはずなのですが、なぜか日本語のコンテンツを探してもなかなか見つけることができなかった方法です。なお、DataTemplateのDataTypeを使用しますので、Silverlightユーザはご注意ください。
 


□サンプル画面の画面仕様

サンプルとして以下のような単純な画面を考えましょう。


 Aページの画面


Bページの画面


メイン画面があり、そこにはページを切り替えるトグルボタンを持っています(ちょっとわかりにくいですが、ウィンドウの上部にある「TogglePageCommand」と表示された細長いボタンがトグルボタンです)。そして、トグルボタンを押下することでA→BそしてB→Aへのページ遷移を行うというものです。

なお、Aページにはテキストボックスを持つことにしましょう。


□ViewModelのクラス図を作成する

ViewModelはクラス図作ってちゃんとモデリングすることを推している私の立場もあるので、次に画面遷移をViewModelのクラス図としてモデル化してみます。




MainViewModelをメインウィンドウ用のViewModelとし、APageViewModelおよびBPageViewModelをそれぞれAページ用とBページ用のViewModelとします。

MainViewModelが保持しているCurrentPage関連で現在のページを表現しています。これはViewModelBaseを指していますが、具体的にはそのサブクラスであるAPageViewModelインスタンスを保持している時にはAページが表示され、BPageViewModelインスタンスを保持している時にはBページが表示されることとします。

つまり、このCurrentPage関連に保持するViewModelインスタンスを切り替えることで画面遷移を発生させることができるということです。このようにViewModelの状態を変更することで画面を制御できるのがMVVMの醍醐味ですね。

また、MainViewModelのAPage関連とBPage関連には、それぞれAページとBページのためのViewModelインスタンスをあらかじめ作成して保持しておくものとします。


□ViewModelの実装

ViewModelの設計も終わったので、ViewModelの実装を以下に示します。なお、ViewModelBaseとコマンドのために、MVVM Light Toolkitを使用した実装になっていますが、その他のライブラリを使用している方は適宜読み替えてください。

まずは、MainViewModelのコードです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GalaSoft.MvvmLight;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;

namespace MvvmTransition.ViewModel
{
    public class MainViewModel:ViewModelBase
    {
        public MainViewModel()
        {
            APage = new APageViewModel();
            BPage = new BPageViewModel();
            CurrentPage = APage; 
        }

        private ViewModelBase _CurrentPage;
        public ViewModelBase CurrentPage
        {
            get { return _CurrentPage; }
            set
            {
                if (_CurrentPage != value)
                {
                    _CurrentPage = value;
                    RaisePropertyChanged("CurrentPage");
                }
            }
        }

        private APageViewModel _APage;
        public APageViewModel APage
        {
            get { return _APage; }
            set
            {
                if (_APage != value)
                {
                    _APage = value;
                    RaisePropertyChanged("APage");
                }
            }
        }

        private BPageViewModel _BPage;
        public BPageViewModel BPage
        {
            get { return _BPage; }
            set
            {
                if (_BPage != value)
                {
                    _BPage = value;
                    RaisePropertyChanged("BPage");
                }
            }
        }

        private ICommand _TogglePageCommand;
        public ICommand TogglePageCommand
        {
            get { 
                if (_TogglePageCommand == null)
                {
                    _TogglePageCommand = new RelayCommand(TogglePage);
                }

                return _TogglePageCommand;
            }
        }

        private void TogglePage()
        {
            if (CurrentPage == APage) // AページだったらBページに切り替え
            {
                CurrentPage = BPage;
            }
            else if (CurrentPage == BPage) // BページだったらAページに切り替え
            {
                CurrentPage = APage;
            }
        }
    }
}
 
以上で、MainViewModelのコードですが、当然ながらクラス図の通りですね。なお、TogglePage()にトグルボタンを押下した際に実行されるコードが記述されていますが、CurrentPageのViewModelのインスタンスをA用とB用で切り替えているだけです。

次に、Aページ用のViewModelとBページ用のViewModelです。

AページのViewModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GalaSoft.MvvmLight;

namespace MvvmTransition.ViewModel
{
    public class APageViewModel:ViewModelBase
    {
        private string _AText;
        public string AText
        {
            get { return _AText; }
            set
            {
                if (_AText != value)
                {
                    _AText = value;
                    RaisePropertyChanged("AText");
                }
            }
        }
    }
}

 
BページのViewModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GalaSoft.MvvmLight;

namespace MvvmTransition.ViewModel
{
    public class BPageViewModel:ViewModelBase
    {
    }
}

 
サンプルでは単純すぎて、Aページ用のViewModelとBページ用のViewModelを説明する必要もないでしょう。実際の開発ではAページとBページは大きく異なる画面となるでしょうから、それぞれのページに適したプロパティを追加することになります。

これでViewModelの実装は終わりました。簡単ですね。


□Viewの実装

次にViewの実装を説明します。まずは、単純なAページ用のViewとBページ用のViewです。

AページのViewのコード


<UserControl x:Class="MvvmTransition.APageView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:localvm="clr-namespace:MvvmTransition.ViewModel"
        xmlns:localview="clr-namespace:MvvmTransition" Background="Red">
    <StackPanel>
        <Label Content="A Page"/>
        <TextBox Text="{Binding AText}" Width="150"/>
    </StackPanel>
</UserControl> 

BページのViewのコード

<UserControl x:Class="MvvmTransition.BPageView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:localvm="clr-namespace:MvvmTransition.ViewModel"
        xmlns:localview="clr-namespace:MvvmTransition"  Background="Blue">
    <StackPanel>
        <Label Content="B Page"/>
    </StackPanel>
</UserControl>
 

これらのViewは単純なので説明も不要でしょう。

次はメイン画面のViewのコードです。ポイントは2つあります。1つ目は、AページとBページのViewとViewModelのマッピングを実施している点、そして2つ目は、CurrentPageのViewModelを使用して現在の画面を表示を実現している点です。

メイン画面のViewのコード

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:localvm="clr-namespace:MvvmTransition.ViewModel"
        xmlns:localview="clr-namespace:MvvmTransition"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" x:Class="MvvmTransition.MainWindow"
        Title="MainWindow" Height="350" Width="525">

     <!-- AページとBページのViewとViewModelのマッピング -->
    <Window.Resources>
        <DataTemplate DataType="{x:Type localvm:APageViewModel}">
            <localview:APageView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type localvm:BPageViewModel}">
            <localview:BPageView />
        </DataTemplate>
    </Window.Resources>
    
    <Window.DataContext>
        <localvm:MainViewModel/>
    </Window.DataContext>

    <DockPanel LastChildFill="True" >
        <Button DockPanel.Dock="Top" Content="TogglePageCommand" Command="{Binding TogglePageCommand}"/>
        <!-- CurrentPageのViewModelを使用して現在の画面を表示 -->
        <ContentControl DockPanel.Dock="Bottom" Content="{Binding CurrentPage}" />
    </DockPanel>
</Window>


「AページとBページのViewとViewModelのマッピング」では、DataTemplateの機能を利用しています。このDataTemplateを使用したViewとViewModelのマッピングは、WPFを使用したMVVMでは一般的な方法の1つです。DataTempleteの機能により、ViewModelというある種のデータを画面に表示する際に、自動的に対応するViewが適用されます。 そしてこの場合、ViewとViewModel間のDataContextの設定まで自動的に行われます。これは私がMVVMで開発している時には、最もよく使用するViewとViewModelのマッピング方法です。

ちなみに私のViewとViewModelのマッピングの基本的なスタンスとしては、メイン画面などの頂点のウィンドウには、

<Window.DataContext>
     <localvm:MainViewModel />
</Window.DataContext>


のようにXaml内でDataCotextを指定したり、コードビハインドでViewModelをViewのDataContextに設定するようにしています。そして、頂点のウィンドウ以外の場所では、(なるべく)このDataTemplateを用いたViewとViewModelのマッピングを行うようにしています。

なお、DataTempleteでのマッピングは、システムに対して1つだけグローバルな定義があれば良いので、ResourceDictionaryの別出しのXamlファイルとして定義して、そのファイルをApp.xamlにマージすると、さらにメイン画面のViewをシンプルに保つことができます。

次に、 「CurrentPageのViewModelを使用して現在の画面を表示」では、ContentControlのContentプロパティに、MainViewModelのCurrentPageプロパティをバインディングしています。

ContentにViewModelの値を直接指定していますが、この場合には上記のDataTempleが適用されますので、AページのViewModelが含まれている場合にはAページのViewとなりますし、BページのViewModelが含まれている場合にはBページのViewとなります。当然、ViewModelが変更されれば内容が切り替わりますので、これによって画面遷移が実現できるというわけです。

ちなみに、このContentContorlを利用してViewModelをViewに変換するというのはMVVMではよく使うテクニックの一つです。また、同様にItemsControlのItemsSourceではViewModelのリストを取り扱うこともできますのでこちらも覚えておくと良いと思います。


□まとめ

これで、MVVMでメッセンジャーなしに画面遷移をシンプルに実現する方法のご紹介は終了です。

今回ご紹介した画面遷移の方法はシンプルですしViewModel内でしっかりと画面遷移を制御できるため個人的にお勧めです。

また、ViewModelはこのままの構造としておいて、View側でCurrentPageを確認しながらVisibilityやZIndexを変更するような方法も(ちょっとがんばる必要がありますが)ありえるかと思います

ちょっと話がそれますが、実はこのアプリでは、画面遷移してAページから再びAページへ戻ってきても、テキストの内 容が保持されたままになっています。もしも画面遷移時にデータを削除したいような場合には、トグルボタン押下時に毎度ViewModelをnewしてしま うようにコードを修正することで実現可能です。このようにViewModelはセッションのような役割を持つことができるのも特徴です。

一方で、メッセンジャーを使用して画面遷移を行う方法はフレームワークに多く、フレームワーク側で、ViewModelのライフサイクル管理やセッション管理のようなことをやってくれるようなものも存在しますので、どのような画面遷移の方法を採用するかは、やはりプロジェクトの要件に応じて検討する必要があります。
  
今回は、画面遷移をシンプルに実現する方法をご説明しました。メッセンジャーによってMVVMの食わず嫌いとなることが多少なり緩和され、少しでも多くの方にMVVMの興味をもっていただければ幸いです。

以上。

人気の投稿