Silverlight で Model-View-ViewModel パターン

はじめに

WPF では Model-View-ViewModel (以下 MVVM) パターンが主流になりつつあるみたい。

MVVM パターンには Command が不可欠ですが、Silverlight にはありません。でも SLExtensions ライブラリを使えば問題無し。

今更ながら挑戦してみました。

Model を作成

商品情報を格納するデータクラスにしてみました。

Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

WCF のデータコントラクトを Model にしてもいいですね。

ViewModel を作成

これが MVVM パターンのキモです。

ProductViewModel.cs
public class ProductViewModel : NotifyingObject
{
    private Command _findCommand;
    private ObservableCollection<Product> _model;
    private int _beginId;
    private int _endId;

    public ProductViewModel()
    {
        _model = new ObservableCollection<Product>();
        _beginId = 1;
        _endId = 9999;

        _findCommand = new Command("Find");
        _findCommand.Executed += new EventHandler<ExecutedEventArgs>(findCommand_Executed);
    }

    /// <summary>
    /// 検索コマンドを取得します。
    /// </summary>
    public Command FindCommand
    {
        get { return _findCommand; }
    }

    /// <summary>
    /// モデルを取得または設定します。
    /// </summary>
    public ObservableCollection<Product> Model
    {
        get { return _model; }
        set
        {
            if (_model == value)
            {
                return;
            }
            _model = value;
            OnPropertyChanged("Model");
        }
    }

    /// <summary>
    /// 検索する製品 ID の範囲の開始値を取得または設定します。
    /// </summary>
    public int BeginId
    {
        get { return _beginId; }
        set
        {
            if (_beginId == value)
            {
                return;
            }
            _beginId = value;
            OnPropertyChanged("BeginId");
        }
    }

    /// <summary>
    /// 検索する製品 ID の範囲の終了値を取得または設定します。
    /// </summary>
    public int EndId
    {
        get { return _endId; }
        set
        {
            if (_endId == value)
            {
                return;
            }
            _endId = value;
            OnPropertyChanged("EndId");
        }
    }

    /// <summary>
    /// データを取得して Model にセットします。
    /// </summary>
    private void findCommand_Executed(object sender, ExecutedEventArgs e)
    {
        // Model に適当なデータをセットする
        ObservableCollection<Product> products = new ObservableCollection<Product>();
        int min = Math.Min(BeginId, EndId);
        int max = Math.Max(BeginId, EndId);
        for (int i = min; i <= max; i++)
        {
            products.Add(new Product()
            {
                Id = i,
                Name = string.Format("製品{0}", i),
                Price = 1000,
            });
        }
        Model = products;
    }
}

SLExtensions に用意されている NotifyingObject クラスを継承しています。このクラスは INotifyPropertyChanged インタフェースを実装しただけのクラス。既にあるものは利用しちゃいますw
ViewModel は UI から入力された日付や検索結果を保持しています。検索を行う FindCommand も公開しています。この Command は View から呼び出されます。
FindCommand に割り当てている処理自体は適当なデータを生成して Model にセットする簡易的なものです。本来なら WCF サービスを呼び出したりするでしょう。

View を作成

Page.xaml
<UserControl x:Class="MVVMSample.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    xmlns:mscontrols="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"
    xmlns:msinput="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls.Input"
    xmlns:theming="clr-namespace:Microsoft.Windows.Controls.Theming;assembly=Microsoft.Windows.Controls.Theming"
    xmlns:slinput="clr-namespace:SLExtensions.Input;assembly=SLExtensions">
    <mscontrols:DockPanel x:Name="LayoutRoot" Background="White" LastChildFill="True">
        <StackPanel Orientation="Horizontal" mscontrols:DockPanel.Dock="Top" theming:ImplicitStyleManager.ApplyMode="Auto">
            <StackPanel.Resources>
                <Style TargetType="msinput:NumericUpDown">
                    <Setter Property="Width" Value="120"/>
                    <Setter Property="Minimum" Value="1"/>
                    <Setter Property="Maximum" Value="9999"/>
                </Style>
                <Style TargetType="mscontrols:Label">
                    <Setter Property="VerticalContentAlignment" Value="Center"/>
                </Style>
            </StackPanel.Resources>
            <mscontrols:Label Content="検索範囲:"/>
            <mscontrols:Label Content="開始ID"/>
            <msinput:NumericUpDown  Value="{Binding Path=BeginId, Mode=TwoWay}"/>
            <mscontrols:Label Content=" 〜 "/>
            <mscontrols:Label Content="終了ID"/>
            <msinput:NumericUpDown Value="{Binding Path=EndId, Mode=TwoWay}"/>
            <Button Content="検索" slinput:CommandService.Command="Find"/>
        </StackPanel>
        <data:DataGrid ItemsSource="{Binding Path=Model}" AutoGenerateColumns="False">
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Binding="{Binding Path=Id}" Header="商品ID"/>
                <data:DataGridTextColumn Binding="{Binding Path=Name}" Header="商品名"/>
                <data:DataGridTextColumn Binding="{Binding Path=Price}" Header="価格"/>
            </data:DataGrid.Columns>
        </data:DataGrid>
    </mscontrols:DockPanel>
</UserControl>

Silverlight Toolkit にある NumericUpDown や Label を使っています。

注目すべきは Button の部分。CommandService クラスの Command 添付プロパティを使って、 "Find" という名前の Command を設定しています。これが ViewModel で登録した FineCommand です。

Page.xaml.cs

そうそう。

public partial class Page : UserControl
{
    public Page()
    {
        var viewModel = new ProductViewModel();
        DataContext = viewModel;

        InitializeComponent();
    }
}

View の DataContext に ViewModel を設定するのを忘れてはいけません。WPF では DataTemplate を使うみたいですが、Silverlight では出来ないので、コードで設定します。

実行結果

表示直後

f:id:griefworker:20090318152830p:image

検索ボタンクリック後

f:id:griefworker:20090318152909p:image

まとめ

MVVM パターンを使えば、画面とロジックを上手く分離できそうです。View のコードがすっきりするし、ViewModel の単体テストも楽になりそうだ。