for WPF developers
Home Profile Tips 全記事一覧

WPF で画面遷移する方法 1 についての考察

(2017/05/18 9:35:22 created.)

(2017/05/24 16:50:21 modified.)

前回では WPF で画面遷移する方法を UserControl 派生の TransitionPanel を使って実現しました。また、コンテンツの指定方法は DataTemplate を利用した方法を使いました。ここではその具体的なコードについて紹介し、ちょっとした特徴をまとめます。

まずはプロジェクトのファイル構造から。

Views フォルダに MainView を始め、前回作成した TransitionPanel と、コンテンツとして表示する Content01View、Content02View、Content03View というユーザコントロールがあります。

ViewModel フォルダにはそれぞれの View に対応した ViewModel があります。ただし、Content01ViewModel、Content02ViewModel、Content03ViewModel は ViewModelBase クラスの派生クラスとしています。

App.xaml では次のように View と ViewModel を紐付けるための DataTemplate をリソースとして定義しています。

App.xaml
  1. <Application x:Class="WpfApplication1.App"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:vw="clr-namespace:WpfApplication1.Views"
  5.              xmlns:vm="clr-namespace:WpfApplication1.ViewModels">
  6.     <Application.Resources>
  7.         <DataTemplate DataType="{x:Type vm:Content01ViewModel}">
  8.             <vw:Content01View />
  9.         </DataTemplate>
  10.         <DataTemplate DataType="{x:Type vm:Content02ViewModel}">
  11.             <vw:Content02View />
  12.         </DataTemplate>
  13.         <DataTemplate DataType="{x:Type vm:Content03ViewModel}">
  14.             <vw:Content03View />
  15.         </DataTemplate>
  16.     </Application.Resources>
  17. </Application>

各 DataTemplate に x:Key 属性を指定していないため、コンテンツとして各 ViewModel が指定されると自動的に各 DataTemplate にしたがって対応する View に変換されて表示されるようになります。ちなみに、変換された View の DataContext は変換前のデータ、すなわちここでは ViewModel となるため、Content01View などに明示的に DataContext を指定する必要はありません。

MainViewModel ではコンテンツを入れ替えるために各コンテンツの ViewModel を保持します。

MainViewModel.cs
  1. namespace WpfApplication1.ViewModels
  2. {
  3.     using System.Collections.Generic;
  4.     using YKToolkit.Bindings;
  5.  
  6.     internal class MainViewModel : NotificationObject
  7.     {
  8.         private List<ViewModelBase> _viewModels = new List<ViewModelBase>()
  9.         {
  10.             new Content01ViewModel(),
  11.             new Content02ViewModel(),
  12.             new Content03ViewModel(),
  13.         };
  14.         public List<ViewModelBase> ViewModels
  15.         {
  16.             get { return this._viewModels; }
  17.         }
  18.     }
  19. }

MainViewModel が公開するプロパティを使って MainView を次のように定義します。

MainView.xaml
  1. <YK:Window x:Class="WpfApplication1.Views.MainView"
  2.            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.            xmlns:YK="clr-namespace:YKToolkit.Controls;assembly=YKToolkit.Controls"
  5.            xmlns:vw="clr-namespace:WpfApplication1.Views"
  6.            Title="MainView"
  7.            Width="300" Height="200">
  8.     <DockPanel>
  9.         <ComboBox x:Name="combobox" DockPanel.Dock="Bottom" ItemsSource="{Binding ViewModels}" DisplayMemberPath="Caption" SelectedIndex="0" />
  10.         <vw:TransitionPanel Content="{Binding SelectedItem, ElementName=combobox}" />
  11.     </DockPanel>
  12. </YK:Window>

ComboBox で ViewModel のリストを表示し、選択された ViewModel を TransitionPanel のコンテンツとして表示するようにしています。ちなみに Content01ViewModel クラスの中身は次のようになっています。

Content01ViewModel.cs
  1. namespace WpfApplication1.ViewModels
  2. {
  3.     internal class Content01ViewModel : ViewModelBase
  4.     {
  5.         public string Caption { get { return "Content01"; } }
  6.     }
  7. }

Content02ViewModel と Content03ViewModel も同様になっています。

Content01View は例えば次のようにチェックボックスを配置しておきます。

Content01View.xaml
  1. <UserControl x:Class="WpfApplication1.Views.Content01View"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
  6.              mc:Ignorable="d" 
  7.              d:DesignHeight="300" d:DesignWidth="300">
  8.     <Grid>
  9.         <CheckBox Content="Content01" />
  10.     </Grid>
  11. </UserControl>

Content02View と Content03View も適当にコントロールを配置しておきます。

それでは実行してみましょう。

無事画面遷移できました。

ところで、せっかく配置したチェックボックスにチェックを入れて画面遷移してみましょう。

なんと、チェックを入れた状態で画面遷移して戻ってくると、チェック状態がリセットされています。もう少し解析するために、Content01View のコードビハインドに次のようなコードを追加します。

Content01View.xaml.cs
  1. namespace WpfApplication1.Views
  2. {
  3.     using System.Windows.Controls;
  4.  
  5.     /// <summary>
  6.     /// Content01View.xaml の相互作用ロジック
  7.     /// </summary>
  8.     public partial class Content01View : UserControl
  9.     {
  10.         public Content01View()
  11.         {
  12.             System.Diagnostics.Debug.WriteLine("Content01View() コンストラクタ");
  13.  
  14.             InitializeComponent();
  15.         }
  16.     }
  17. }

Content02View、Content03View も同様にコンストラクタでデバッグ出力をおこなうようにしてからもう一度実行してみましょう。

Content02View、Content03View は初めて表示されるのでコンストラクタが走るのは当然ですが、その後に一度表示したはずの Content01View がまたコンストラクタから処理されていることがわかります。

実は、これは DataTemplate を使って View と ViewModel を紐付けていることが原因です。DataTemplate は指定された型のデータを指定されたテンプレートに変換して表示するためのテンプレートです。つまり、データが指定される度にテンプレートからコンテンツを毎回構築してしまいます。

このサンプルのようにチェックボックスの状態くらいなら ViewModel 側でデータを保持し、それをデータバインディングすることで解決できそうですが、それ以外にも、例えば ListBox の選択状態だったり、そのアイテムを表示するためのスクロールバーの状態だったり、そのすべてを ViewModel 側で保持しようとすると、技術的には可能かもしれませんが、あまり現実的ではありません。

今回の現象の肝は、ViewModel のインスタンスは MainViewModel クラスが保持しているのに対し、Content01View などの View のインスタンスは誰も保持していないことにあります。DataTemplate によって View と ViewModel を紐付けることで実現する画面遷移はお手軽ではありますが、システム要件によってはちょっと使い勝手が悪くなってしまうので注意が必要です。

というわけで View のインスタンスもちゃんと保持する画面遷移について後日紹介します。