かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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分くらい考えた感じだとこんなの。