かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

WPFでFormの継承(Windowの継承 or 見た目の継承?) その2

1つ前の記事:http://d.hatena.ne.jp/okazuki/20091025/1256472760

前回の記事で、無理矢理?独自ロジックとフォーマットを定義したWindowを継承させる方法を書きました。ただ、この方法も出来るっちゃ出来るけど、個人的には好きじゃありません。
それに、恐らく親Windowで定義したButtonのイベントを拾いたいとかあたりで、つまづいたりしそうな予感もします。これは、ApplyTemplateメソッドあたりをオーバーライドして、Buttonのインスタンスを取得して、ごにょごにょすればいけるはずかな・・・?(未検証)

ということで、今回は、上記の方法ではなくMVVMパターンでちょっぴりやってみようと思います。

ViewModelBaseの作成

WpfMVVMBaseという名前でWPFアプリケーションを作成して、そこにViewModelの基本クラスを作ります。こいつはINotifyPropertyChangedを実装しただけのシンプルなものにしました。

using System.ComponentModel;

namespace WpfMVVMBase
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged メンバ
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string name)
        {
            var h = PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(name));
            }
        }
        #endregion
    }
}

基本となるViewModelを作成

次に、ViewModelBaseを継承してMyViewModelBaseクラスを作成します。イメージとしては、こいつが親ウィンドウにあたります。

namespace WpfMVVMBase
{
    public class MyViewModelBase : ViewModelBase
    {
        private ViewModelBase _contentModel;
        // 差分
        public ViewModelBase ContentModel
        {
            get { return _contentModel; }
            set
            {
                _contentModel = value;
                OnPropertyChanged("ContentModel");
            }
        }
    }
}

こいつに紐づくViewをApp.xamlでDataTemplateを使用して定義します。

<DataTemplate DataType="{x:Type local:MyViewModelBase}">
	<DockPanel>
		<TextBlock DockPanel.Dock="Top" Text="へっだー" />
		<TextBlock DockPanel.Dock="Bottom" Text="ふったー" />
		<!-- 真ん中に差分を表示する -->
		<ContentPresenter Content="{Binding ContentModel, Mode=TwoWay}"/>
	</DockPanel>
</DataTemplate>

差分のViewModelを作成

差分のViewModelといってもMyViewModelBaseを拡張するわけではなくて、ViewModelBaseを継承して作ります。

public class Page1ViewModel : ViewModelBase
{
}

public class Page2ViewModel : ViewModelBase
{
}

Page1ViewModelとPage2ViewModelに対応するViewをDataTemplateでApp.xamlに定義します。今回は、簡単にするためにApp.xamlに全て書いてますが、UserControlにViewを定義して、DataTemplateの中は、UserControlを書くだけにするのがいいと思います。

<DataTemplate DataType="{x:Type local:Page1ViewModel}">
	<Button Content="AAAAAA" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Page2ViewModel}">
	<Button Content="BBBBBB" />
</DataTemplate>

App.xamlのStartupUriを削除してStartupイベントを定義します。ここで最初にViewModelを組み立ててViewを表示させます。

using System.Windows;

namespace WpfMVVMBase
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            // viewmodel組み立てて
            var viewModel = new MyViewModelBase
            {
                ContentModel = new Page1ViewModel()
            };
            // windowと紐付けて
            var window = new Window1
            {
                Content = viewModel
            };
            // 表示
            window.Show();
        }
    }
}

実行すると、以下のような画面になります。


次に、別画面を表示する場合を考えて見ます。とりあえず、コマンドが必要なので、コマンドを作成します。今回は手抜きです。すいません。

// 超簡易コマンド実装
public class RelayCommand : ICommand
{
    private Action _execute;

    public RelayCommand(Action execute)
    {
        _execute = execute;
    }

    #region ICommand メンバ

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _execute();
    }

    #endregion
}

Page1ViewModelに以下のコマンドの定義を追加します。

public class Page1ViewModel : ViewModelBase
{
    private ICommand _showPage2Command;
    public ICommand ShowPage2Command
    {
        get
        {
            return _showPage2Command = _showPage2Command ?? 
                new RelayCommand(ShowPage2);
        }
    }

    public void ShowPage2()
    {
        // ViewModelでViewを作るのは合法か・・・?
        // というのは永遠のテーマ
        // Viewに依存するのが嫌なら、何かしらインターフェースを1枚かまそう
        var window = new Window1
        {
            // 中身がPage2ViewModelのMyViewModelBaseをContentに
            Content = new MyViewModelBase
            {
                ContentModel = new Page2ViewModel()
            }
        };
        // 表示
        window.Show();
    }
}

DataTemplateに定義したボタンにCommandをBindします。

<DataTemplate DataType="{x:Type local:Page1ViewModel}">
	<Button Content="AAAAAA" Command="{Binding ShowPage2Command, Mode=OneTime}"  />
</DataTemplate>

これでAAAAAAボタンをクリックすると、別ウィンドウで以下のようなBBBBBBBと表示された画面が表示されます。

ん〜もうちょっと整理しないといけないかな・・・。
とりあえず30分くらい考えた感じだとこんなの。