デザイン用のViewModelを使ってViewをデザインする

唐突ですが前回のサンプルをViewModel化してみます。

ViewModel化する

まずViewModelです。

namespace Wpf110330Listbox {
    class ViewModel {
        public double X { get; set; }
        public double Y { get; set; }
        public string Text { get; set; }
        public ViewModel() {}
    }
}

ViewModel化と言いつつ手抜きしてINotifyPropertyChangedインターフェイスを実装してません。手抜きの弊害は後述しますが、MVVM Light toolkitなどで真面目に実装して見てください。

次にViewModelにバインドするようにXAMLを改造します。

    <DockPanel >
        <Button Content="追加" DockPanel.Dock="Top"
                HorizontalAlignment="Center"
                Click="Button_Click" />
        
        <ItemsControl Name="DragList"
                      ItemsSource="{Binding}">

            <!--親ContentPresenterの位置をバインディング設定する-->
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding X}"/>
                    <Setter Property="Canvas.Top"  Value="{Binding Y}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
            
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Thumb DragDelta="Thumb_DragDelta" >
                        <Thumb.Template>
                            <ControlTemplate>

                                <Grid Width="100" Height="30">
                                    <Ellipse Fill="LightBlue" Stroke="Blue" />
                                    
                                    <!--バインドするプロパティを変更した-->
                                    <TextBlock Text="{Binding Text}" 
                                               HorizontalAlignment="Center" 
                                               VerticalAlignment="Center"/>
                                </Grid>

                            </ControlTemplate>
                        </Thumb.Template>
                    </Thumb>
                </DataTemplate>
            </ItemsControl.ItemTemplate>

        </ItemsControl>
    </DockPanel>

変更箇所にコメントを入れました。
前回 説明したようにCanvas.LeftとTopは親のContentPresenterに設定する必要があります。これにバインディングを設定するため、今回はItemContainerStyleプロパティを追加しています。


次にコードビハインドです。

        //ViewModelのコレクションを宣言
        private ObservableCollection<ViewModel> ViewModels = new ObservableCollection<ViewModel>();

        public MainWindow() {
            InitializeComponent();
            //DataContextを上書きする
            DragList.DataContext = ViewModels;
        }

        //追加ボタンのイベントハンドラ
        private void Button_Click(object sender, RoutedEventArgs e) {
            var text = (DragList.Items.Count + 1).ToString();
            var model = new ViewModel { Text = text };
            ViewModels.Add(model);
        }

        //Thumbコントロールのドラッグイベント処理
        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e) {
            (前回と変更なし)
        }

プライベート変数にViewModelのリストを宣言して初期化時にItemsControlのDataContextに設定しています。なおListを使うとリストへの要素追加がViewの表示に反映されませんのでObservableCollectionを使います。
また、ボタンのクリックイベントはViewModelを生成するように変更しました。
実行するとめでたく前回と同じ動作をすると思います。

デザイン用のモデルを使う

さて、いよいよ本題です。
まずXAMLのルート要素(Window)のResourcesにデザイン用のビューモデルを宣言しておきます。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:my="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">

    <!--デザイン用のVMを宣言-->
    <Window.Resources>
        <x:Array x:Key="DesignDataSource" Type="my:ViewModel" >
            <my:ViewModel X="10"  Y="40" Text="いちじく"/>
            <my:ViewModel X="100" Y="10" Text="にんじん"/>
            <my:ViewModel X="150" Y="60"  Text="山椒"/>
        </x:Array>
    </Window.Resources>    
  

XAMLの冒頭で名前空間を取り込んでいます。これでmy:ViewModelでユーザクラスをXAML上で利用できます。ここでは3つのインスタンスを作っています。また、ViewModelクラスを入れる箱としてx:Arrayを使っています。Type属性値には中に入れるオブジェクトのクラス(ここではmy:ViewModel)を記述します。この部分のXAMLはViewModel[]型の配列となります。

なお、単純なstringデータで十分であれば 文字列データ のように使えます。(名前空間の宣言をお忘れなく)


次にデザイン用のビューモデルをバインドするようにItemsControlのDataContext属性を追加します。これは実行時の初期化で上書きされるためデザイン時にしか影響を与えません。

        <!--DataContextにデザイン用のVMに接続、実行時に上書きする-->
        <ItemsControl Name="DragList"
                      DataContext="{StaticResource DesignDataSource}"
                      ItemsSource="{Binding}">

ビルドするとデザイン時のプレビュー画面に用意したビューモデルが反映されていると思います。これでいちいちデバッグを実行させること無く、ビューモデルを使ってプレビューを見ながらViewのデザインをチューニングしたり、バインディングが適切に効いているか確かめられるので効率が良くなると思います。

なお、今回はViewModelクラスを手を抜いてINotifyPropertyChangedインターフェイスを実装しませんでしたので、XAML上でViewModelクラスのプロパティを変更してもすぐには反映されません。ビルドすると反映されます。これは手抜きせずインターフェイスを実装すると即座に反映されると思います。

ちなみにこの挙動はデザイン時に限ったものではなく、INotifyPropertyChangedを実装していないプロパティに対する初期化後の値の変更がViewに反映されない現象に遭遇します。バインディングのタネ明かしを知っていれば当然のことなのですが、最初の頃は不可解な現象で戸惑うかも知れません。

正統派のやり方

なお、今回のサンプルは実は手抜き版です。d:DataContextを使うのが正統派のようです。
Visual Studio 2010のデザイナに仕事をさせて楽をする方法 - かずきのBlog@hatena

やり方はリンク先を参照していただくとして、注意点は次の3行を忘れずに入れてください。

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"

特に最後のを忘れていると変なコンパイルエラーが出るようです。