C# + WPFで、複数のViewModelで1個のViewを使いまわす

引き続き、Microsoft.TeamFoundation.MVVM名前空間を使って作る、WPFアプリの話です。

例えば、こんな仕様があったということにします。

  • 入庫と出庫で同じ画面構成
  • 画面に表示する項目名や、在庫数を加減するロジックだけが異なる

 
その例として、

  • View: 入庫/出庫で共通の一つ
  • ViewModel
    • 入庫/出庫で共通している部分のViewModelBase
    • 入庫/出庫ごとに、上のViewModelBaseを継承したViewModel

をやってみました。

 
2015/12/10 追記 ここから

公式Blogに以下の記事が掲載されましたので、Microsoft.TeamFoundation.MVVMの使用前に記事を確認してみてください。
Microsoft.TeamFoundation.MVVM 名前空間の利用について - Visual Studio サポート チーム blog - Site Home - MSDN Blogs

2015/12/10 追記 ここまで

 

仕様

  • メニュー画面に2つのボタン置く
  • 片方のボタンでは入庫画面を、もう一方のボタンでは出庫画面を表示する
  • WindowのタイトルとDataGridのDataGridTextColumn.Headerが異なるだけという手抜きをして、在庫数を加減するロジックは実装せず

 

画面イメージ

メニュー画面

f:id:thinkAmi:20140917114306p:plain

 

入庫画面

f:id:thinkAmi:20140911175234p:plain

 

出庫画面

f:id:thinkAmi:20140911175243p:plain

 

メニュー画面

MenuViewのViewやViewModelは今までやってきたことと同じです。

View (MenuView.xaml)
<Window.Resources>
    <mvvm:RegisterWindow x:Key="Receiving" Type="local:SharedView" />
    <mvvm:RegisterWindow x:Key="Shiping" Type="local:SharedView" />
</Window.Resources>

のようなWindow.Resourcesを用意しておきます。

 

ViewModel (MenuViewModel.cs)

ボタンのコマンドに紐づくExecuteReceivingCommand()メソッドなどに

WindowDisplayService.ShowDialog("Receiving", new ReceivingViewModel());

のような感じで、Window.Resourcesで定義したKeyと、入庫/出庫画面向けのViewModelを渡して表示します。

 

入庫/出庫画面

使いまわすView (SharedView.xaml)
Windowのタイトルへのデータバインディング

通常のデータバインディングします。

<Window x:Class="SharedViews.SubWindowView"
        ...
        Title="{Binding Path=WindowTitle}">

 

DataGridのDataGridTextColumn.Headerへのデータバインディング

こちらは、以下を参考にしてデータバインディングします。

 
ただ、このままだと忘れそうなので、自分の理解を残しておきます。

まず、使うところ(今回はDataGrid)のResourcesとして、FrameworkElementのDataContextに、現在のDataContextのオブジェクト(ViewModel)をセットします。

<DataGrid.Resources>
    <FrameworkElement x:Key="proxyElement" DataContext="{Binding}"/>
</DataGrid.Resources>

 
なお、ここではBindingのPathを省略していますが、この書き方の場合でDataContextのオブジェクトがデータバインディングされていることになります。

上の例では、空のバインディング構文 ({Binding}) を使用しています。 この場合、ListBox は、親の DockPanel 要素から DataContext を継承します (この例には示されていません)。 パスを指定しない場合、既定でオブジェクト全体にバインドされます。 つまり、この例では、ItemsSource プロパティをオブジェクト全体にバインドしているため、パスが省略されたことになります
データ バインドの概要 - MSDN

 
次に、画面には見えないContentControlのContentとして、DataGrid.Resourcesを持っておきます。

<ContentControl Visibility="Collapsed" Content="{StaticResource ResourceKey=proxyElement}"/>

 
最後に、DataGridTextColumn.Headerへデータバインディングします。

<DataGridTextColumn Header="{Binding Path=DataContext.QuantityTitle, Source={StaticResource ResourceKey=proxyElement}}"/> 

 

入庫/出庫共通のViewModel (SharedViewModelBase.cs)

データバインディング向けのプロパティを用意します。

 
また、WindowのタイトルやDataGridTextColumn.Header向けのプロパティは継承先のクラスでオーバーライドできるようにしておきます。

public class SharedViewModelBase : ViewModelBase
{
    public ObservableCollection<Slip> SlipSource { get; set; }

    // 継承先のクラスで実装
    public virtual string WindowTitle { get { throw new System.NotImplementedException(); } }
    public virtual string QuantityTitle { get { throw new System.NotImplementedException(); } }
}

 
あとは、継承先のクラス(ReceivingViewModel.cs, ShipingViewModel.cs)でWindowTitle・QuantityTitleのプロパティを実装します。

 

感想など

作ってみたものの、

  • MVVMの作法では、複数のViewModelで1個のViewを使いまわしていいのか
  • 使いまわして良い場合、ベースとなるViewModelから継承したViewModelを、ViewのDataContextとして使う方法でいいのか

というあたりが分かりませんでした。

 

ソースコード

GitHubに上げました。
CSharp-Sample/MVVMApp/SharedViewMVVM at master · thinkAmi/CSharp-Sample · GitHub