for WPF developers
Home Profile Tips 全記事一覧

WPF で画面遷移する方法 2

(2017/05/18 22:08:43 created.)

(2017/05/24 15:10:13 modified.)

前回までで画面遷移することはできましたが、View のインスタンスを誰も保持していなかったため、同じ画面に戻ってきたつもりでも、再びコンストラクタから再構築された別のインスタンスになってしまっていたため、チェックボックスの状態やスクロールバーの状態はすべて初期化されてしまうといった現象が起こってしまいました。今回は View のインスタンスを保持することでこのような現象を回避してみたいと思います。

というわけでさっそく View のインスタンスを App クラスで保持します。

App.xaml.cs
  1. namespace WpfApplication1
  2. {
  3.     using System.Collections.Generic;
  4.     using System.Windows;
  5.     using System.Windows.Controls;
  6.     using WpfApplication1.ViewModels;
  7.     using WpfApplication1.Views;
  8.  
  9.     /// <summary>
  10.     /// App.xaml の相互作用ロジック
  11.     /// </summary>
  12.     public partial class App : Application
  13.     {
  14.         protected override void OnStartup(StartupEventArgs e)
  15.         {
  16.             base.OnStartup(e);
  17.  
  18.             Instance = this;
  19.  
  20.             var w = new MainView() { DataContext = new MainViewModel() };
  21.             w.Show();
  22.         }
  23.  
  24.         private Dictionary<string, Control> _viewDictionary = new Dictionary<string, Control>()
  25.         {
  26.             { "Content01", new Content01View() { DataContext = new Content01ViewModel() } },
  27.             { "Content02", new Content02View() { DataContext = new Content02ViewModel() } },
  28.             { "Content03", new Content03View() { DataContext = new Content03ViewModel() } },
  29.         };
  30.  
  31.         public static App Instance { get; private set; }
  32.  
  33.         public Control GetView(string key)
  34.         {
  35.             Control control;
  36.             return this._viewDictionary.TryGetValue(key, out control) ? control : null;
  37.         }
  38.     }
  39. }

View のインスタンスを扱うので同じく View で保持したくなりますが、これらに対する DataContext である ViewModel のインスタンスも登場してしまうため、必然的に View と ViewModel の両方を知り得る App クラスが保持することになります。

View は ViewModel に依存するが、そのインスタンスを知る必要はない、ということです。

保持した情報を外部から取得できるように GetView() メソッドを定義しています。このメソッドを利用してコンテンツを切り替えられるように TransitionPanel の内部実装を少し変更します。

TransitionPanel.xaml.cs
  1. public static readonly DependencyProperty ContentSelectorProperty = DependencyProperty.Register("ContentSelector", typeof(string), typeof(TransitionPanel), new PropertyMetadata(null, OnContentSelectorPropertyChanged));
  2.  
  3. public string ContentSelector
  4. {
  5.     get { return (string)GetValue(ContentSelectorProperty); }
  6.     set { SetValue(ContentSelectorProperty, value); }
  7. }
  8.  
  9. private static void OnContentSelectorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  10. {
  11.     var control = d as TransitionPanel;
  12.     control.Content = App.Instance.GetView(control.ContentSelector);
  13. }

変更箇所はただ一つ、ContentSelector 依存関係プロパティを追加定義します。ContentSelector プロパティ変更イベントハンドラで先ほど App クラスで定義した GetView() メソッドを使い、自身のコンテンツを自分で変更するようにします。

このように変更したため、コンテンツを指定するにはコンテンツの名前を渡すようにする必要があるため、MainViewModel と MainView を次のように変更します。

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 _viewModels = new List()
  9.         //{
  10.         //    new Content01ViewModel(),
  11.         //    new Content02ViewModel(),
  12.         //    new Content03ViewModel(),
  13.         //};
  14.         //public List ViewModels
  15.         //{
  16.         //    get { return this._viewModels; }
  17.         //}
  18.  
  19.         private DelegateCommand _changeContentCommand;
  20.         public DelegateCommand ChangeContentCommand
  21.         {
  22.             get
  23.             {
  24.                 return this._changeContentCommand ?? (this._changeContentCommand = new DelegateCommand(
  25.                 p =>
  26.                 {
  27.                     this.ContentName = p as string;
  28.                 }));
  29.             }
  30.         }
  31.  
  32.         private string _contentName;
  33.         public string ContentName
  34.         {
  35.             get { return this._contentName; }
  36.             private set { SetProperty(ref this._contentName, value); }
  37.         }
  38.     }
  39. }
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.         <UniformGrid DockPanel.Dock="Bottom" Columns="3">
  10.             <Button Grid.Column="0" Content="Content01" Command="{Binding ChangeContentCommand}" CommandParameter="Content01" />
  11.             <Button Grid.Column="1" Content="Content02" Command="{Binding ChangeContentCommand}" CommandParameter="Content02" />
  12.             <Button Grid.Column="2" Content="Content03" Command="{Binding ChangeContentCommand}" CommandParameter="Content03" />
  13.         </UniformGrid>
  14.         <vw:TransitionPanel ContentSelector="{Binding ContentName}" />
  15.     </DockPanel>
  16. </YK:Window>

前回は MainViewModel がコンテンツを切り替える元となる ViewModel を持っていたので MainViewModel が主導権を握っているような印象でしたが、今回は MainViewModel はあくまでも View からの情報を橋渡しするだけで、ContentName に指定されるコンテンツの名前は View から渡されるパラメータを横流しするだけとなっています。ViewModel が View の名前を知り得るはずもないため、あえてこのような作りになります。ただし、ContentName プロパティを変更するかどうかは相変わらず MainViewModel が判定できるので、画面遷移できるかどうかは ViewModel で判断できるようになっています。

このように実装すると、Content01View、Content02View、Content03View のインスタンスは App クラスが保持しているため、コンストラクタは 1 回だけしか処理されなくなります。このことから、画面遷移後も View の表示状態が保持されるようになります。

画面遷移するために UserControl 派生の独自のコントロールを定義して仰々しく実装していましたが、次回は TabControl を触ってみます。