2010年9月26日日曜日

MVVMパターンでSilverlightアプリを開発する その1

WPFかSilverlightで開発する場合に非常に有用なデザインパターンがMVVMだ。MVVMが最初に提唱され始めたのが2006年ごろらしいので、かれこれ4年ほどたち、その間にMVVM Light ToolkitやPrismなどかなりこなれたライブラリ群が多数出たおかげで、現在はMVVMにのっとった開発が容易になっている。今回はPrismよりも簡易に使い始められるMVVM Light ToolkitでMVVMでの開発方法を紹介する。

MVVM Light Toolkitの導入
MVVM Light Toolkitの公式サイト
MVVM Light Toolkitのインストール方法
上記2つ目のリンクから「Binaries (WPF3.5SP1/SL3/WPF4/SL4/Windows Phone 7)」のリンクをクリックしてDLL群をダウンロードしよう。解凍するとProgram Files配下のフォルダができるのでそれをコピーしてProgram Files配下へと配置する。


あわせて
「Templates for Visual Studio 2010 (WPF3.5SP1/SL3/WPF4/SL4/Windows Phone 7)」
「Snippets for Visual Studio 2008, Visual Studio 2010 and Visual Studio 2010 Express (WPF3.5SP1/SL3/WPF4/SL4/Windows Phone 7)」
からテンプレートとコードスニペットをダウンロードしておくと良いだろう(私の環境はVS2010なのでそれようのファイルをダウンロードしている。自分の環境にあったものを選択してほしい)。配置方法はZipファイルを解凍後、それぞれ所定の位置にコピーすればよい。所定の位置は下図を参考にしてほしい。

例)C:\Users\Yoo\Documents\Visual Studio 2010\Templates

例)C:\Users\Yoo\Documents\Visual Studio 2010\Code Snippets\Visual C#\My Code Snippets


セットアップが完了したらVisual Studio 2010を起動し、新規プロジェクトの作成を選択し、Silverlightの項目を選択しよう。テンプレートをコピーしている場合は下図のようにMvvm Lightという項目があるはずなのでSL4のほうを選択しよう。


プロジェクトのスケルトンは下図のようになる。


MVVMについて
ここでMVVMパターンについて簡単に説明しておこう。

MVVMはModel View ViewModelの略称でMVC(Model View Controller), MVP(Model View Presenter)の流れから来ている。このMVVMを理解するうえで最も重要なのはVMの部分で、ViewModelがなんぞや、というのを理解すればおのずとMVVMがどういうものか理解できる。


※ここでViewModelのやりとりしている矢印がData Access Layerだけなのは、ViewとのやりとりはBindingを介して行うのでMVVMパターンの建前上ViewとViewModelの間にはやり取りが発生しない(Code Behindにイベントハンドラを記述する必要がなくなる)。

ここで上の微妙な図を使って図解する。図を見てもらうと分かると思うが、ViewModelはViewとModelの中間に位置する。ViewModelの存在意義はいくつがあるが、そのうちもっとも重要なのは、ModelとViewの間にある必要な情報の差を埋める部分にある。アプリケーションは多種多様な情報を扱うが、大体バックエンドのデータストアにはSQL ServerなりOracleなどのデータベースを使うのが一般的だろう。そのため、バックエンドにデータを流し込んだり、取得する単位はテーブルエンティティ毎になりやすい。しかし、RDBのテーブル構成はデータ保持の観点からは大変優秀だが、UIにそのまま表示する情報の形式としては足りなかったり、過剰であったりする。そこで、View(ここでいうUI)とModel(ここでいうテーブルエンティティ)の間を取り持つのにViewModelが存在する。つまりViewModelには、多様なデータを取得しViewに必要な情報を形成する役割とユーザのインプットをModelにしデータアクセスレイヤーに渡す役割という2つの大きな役割がある。

また、ViewModelにはそれ以外にもCommandとしてユーザのアクションハンドラーを公開することが求められる。ユーザのアクションハンドラー(要するにイベントハンドラー)をCommandとして公開する理由は下記の通り。
  • ViewのUI要素のCommand(Button.Commandなど)とViewModel.CommandをBindingすることが可能となり、Code Behindにイベントハンドラを記述する必要がなくなる(保守性がアップ)
  • イベントハンドラも含めてビジネスロジックはすべてViewModel上にあるので単体テストを行うことが容易になる(Testabilityがアップ)

以下に簡単にまとめる。
  • View:UI要素の集合。プレゼンテーション層
  • Model:テーブルエンティティだったり、DTOだったり、ビジネスエンティティだったり
  • ViewModel:ViewとModelの中間層。ビジネスロジックはここ。Commandもここ

MVVM Light Toolkitの説明
ここで簡単にMVVM Light Toolkitの説明をする。基本となる要素は下記の通りだ。
  • ViewModelBase
  • Messenger
  • RelayCommand
ViewModelBaseは名前の通りにViewModelの基底クラスだ。いくつか便利な関数が用意されているので特別な理由が無い限りViewModelはこのクラスから派生することをお勧めする。

MessengerはViewModel間に限らず、ありとあらゆる場所への通知が可能だ。画面遷移やViewModel間の値変更の通知などはこのクラスが担うことになる。

RelayCommandはMVVM Light Toolkitが提供するCommandクラスだ。これの他にもEventToCommandというかなり強力なCommandクラスがあるので、SelectionChangedイベントなどCommandとしてハンドルできないUI要素のイベントがある場合はこちらを使うことになる。

と、ここまでで結構長くなってしまったのでコードを使っての解説は次回以降に持ち越すことにする。

2010年9月16日木曜日

Silverlightで複数ページを印刷する PrintDocument

Silverlightで単一ページを印刷するのは至極簡単でPrintDocumentを使用する。コード例は下記になる。

var printDoc = new PrintDocument();
printDoc.PrintPage += (s, eArgs) =>
{
 eArgs.PageVisual = this;
};
printDoc.Print("Print page");

PrintPageイベント引数のPageVisualプロパティにVisual要素を渡すとその内容をそのまま印刷してくれる。ここで一点注意が必要なのだが、Print関数はユーザのアクション(ボタンクリックなど)をハンドルした関数内で呼び出さないとSecurityExceptionがThrowされる。

ついで複数ページを印刷する方法を解説する。元ネタは下記から。
Silverlight Business Apps: Module 6.2 - Multi Page Printing

var printDoc = new PrintDocument();
bool firstPage = true;
var index = 0;
var itemsSource = new List<Item>{ 項目多数 };

printDoc.PrintPage += (s, eArgs) =>
{
 var itemHost = new StackPanel { Width = eArgs.PrintableArea.Width };
 if (firstPage)
 {
    itemHost.Children.Add(new HeaderControl());
    firstPage = false;
 }

 while (index < itemsSource.Count)
 {
    var item = itemsSource[index];
    var control = new ResultControl { Text = item.Text };
    control.Measure(new Size(eArgs.PrintableArea.Width, double.PositiveInfinity));

    var desiredHeight = control.DesiredSize.Height;
    var insideStack = new StackPanel { Height = desiredHeight };
    insideStack.Children.Add(control);
    itemHost.Children.Add(insideStack);

    itemHost.Measure(new Size(eArgs.PrintableArea.Width, double.PositiveInfinity));
    if (itemHost.DesiredSize.Height > eArgs.PrintableArea.Height)
    {
        itemHost.Children.Remove(insideStack);
        eArgs.HasMorePages = true;
        break;
    }
    index++;
 }
 eArgs.PageVisual = itemHost;
};
printDoc.Print("Print Page");

PrintPageイベント引数にPrintableAreaに印刷可能範囲の縦横サイズが入っている。このサイズより外にあるものは印刷されないので、こちら側で制御してやる必要があり、二枚目以降の印刷が必要な場合は再度PrintPageイベントが発生しないといけないのでHasMorePagesにtrueを設定しフレームワークに印刷がすべて終わっていないことを伝えておく必要がある。

Measure関数で実際に描画されるコントロールのサイズを計測する。Measure関数を呼び出しておくとDesiredSizeにコントロールに必要なサイズが設定されるのでそれを参照して、印刷可能範囲との比較を行っていく。

一点はまった点としてinsideStackの存在理由を説明しておく。一見無駄なコーディングに見えるが、これがないと実際に印刷されるサイズとDesiredSizeで取得されるサイズに差異が生じる場合があり、予期せぬ印刷結果につながることがある。

というのも、ResultControlは内部的にGridを保持し、さらにRowDefinitionで3行保持している。ただ、ある特定の場合は最後の1行にデータが設定されないので上2行分のHeight40ピクセルほどで事足りる。しかしDesiredSizeでは毎回3行分のHeight60ピクセルが返却され、さらに始末の悪いことに描画は40ピクセル分で行われる。そのためプログラム上はHeight60ピクセルで計算している部分が実はHeight40ピクセルが正しく、その20ピクセル分の差異がitemsSourceの項目が多くなるほど積もり積もって大きくなり、正しい印刷制御が行えなくなってしまう。それを防ぐためにinsideStackを使用している。insideStackのHeightにDesiredHeightで取得したHeightを設定し、必ずその高さを保証することにより、実際に印刷されるものと、プログラム上で取得できるサイズの差異が発生しないようにしている。

2010年9月15日水曜日

Dynamic LinqをLinq To Xmlで使うときの注意点 in Silverlight

Linq To SQLでDynamic LINQを良く使っていたのだが、先日Silverlight上でLinq To Xmlを同じように使ったところ結構はまったので紹介しておく。

問題のコードは下記。

var list = doc.Descendants("Row").Select(x => new { 
  Original=x, 
  Text=x.Element("FirstName").Value + x.Element("LastName").Value + x.Element("Address").Value
})
.Where("Text.Contains(@0)", new string[]{ "何か" }).ToList();

上記は意味の無いDynamic Linqの使い方だがサンプル目的なので分かりやすく記述している。上記のコードを実行するとContainsの部分でMethodAccessExceptionがThrowされる。どうやらAnonymousクラスのプロパティに対してReflectionから実行しようとすると駄目なようだ。回避方法は下記の通り。

public class TemporaryDataHolder
{
 public XElement Original { get; set; }
 public string Text { get; set; }
}

var list = doc.Descendants("Row").Select(x => new TemporaryDataHolder { 
  Original=x, 
  Text=x.Element("FirstName").Value + x.Element("LastName").Value + x.Element("Address").Value
})
.Where("Text.Contains(@0)", new string[]{ "何か" }).ToList();

一時的にデータを保持するクラスを定義して、それをAnonymousクラスの代わりにインスタンス化すればよい。なんだかなぁ、という感じだがDynamic Linqが処理の重要な部分を占める場合は致し方ない。

Linq To XmlでXNamespaceを指定する

前回SqlMetalの使い方を説明したが、今回は前回出力したマッピングファイルの書き換えをLinq To XMLで行う方法を紹介する。

マッピングファイルを書き換える理由は次の通り。複数のDBをまたいだクエリをLinq To Sqlで行う場合、マッピングファイルのTable要素は下記のようにServerName、DBNameを含んだ記述にしなければならない。

dbo.users → ServerName.DBName.dbo.users

SQL Server 2008で複数のDBを単一クエリで扱うための設定は下記を参照してほしい。
Linked Serverの設定方法
sp_addlinkedserver (Transact-SQL)

で、Linq To Xmlを使用してのマッピングファイルの読み込みなのだが、下記のようにXNamespaceを指定しないと予期したようにTable要素は取得できない。

var doc = XDocument.Load(mappingFilePath);
XNamespace ns = "http://schemas.microsoft.com/linqtosql/mapping/2007";
var table = doc.Descendants(ns+"Table").Where(x => 条件).FirstOrDefault();
// 省略

Linq To Xmlに限らずXmlDocumentやXPathでもXmlファイルを操作する場合は、名前空間の指定のある要素にはそれぞれXNamespaceを明示的に指定しなければ予期したように動作しないので注意しよう。

SqlMetalでLinq To SQLのマッピング情報を外部ファイル化する

Linq To SQLを使用する際にdbml必要なDBのテーブルをGUI上でD&Dして構築すると、DBとのマッピング情報やソースコードは自動で生成されるので改変できない。しかしSqlMetalを使うと、マッピング情報をマッピングファイルとして外部ファイル化したり、生成されるクラスに指定の名前空間や基底クラスを付与したりと色々細かい操作行えるようになる。

SqlMetalはVisual Studioをインストールするとついてくるコマンドラインツールで、初期では下記にある。
drive:\Program Files\Microsoft SDKs\Windows\vn.nn\bin

ここでいくつか使い方をみてみよう。

DBの環境は下記の通り:
SQL Server名: .\SQLSERVER2008R2
UID: sa
PWD: password
DB: TestDB

SqlMetal /server:.\SQLSERVER2008R2 /database:TestDB /user:sa /password:password /code:"C:\MatsuoSoftware\TestDB.cs" /map:"C:\MatsuoSoftware\TestDB.map" /serialization:Unidirectional /context:TestDBDataContext /namespace:MatsuoSoftware.Data /sprocs /functions

/server, /database, /user, /passwordはDBへの接続情報。/code, /mapで出力先と出力方法を指定する。ここで/dbmlを指定することも可能だが、その場合は/code, /mapオプションとは一緒に使えない。dbmlで出力するか、csファイルとmapファイルで出力するかの二択だ。/serializationでシリアル化の方法を指定する。Unidirectionalを指定するとDataContract, DataMember属性が生成されるクラス、プロパティに付与されるのでWCFサービスの戻り値や引数としてそのまま使用することが可能になる。/context, /namespaceは見ての通りだ。/sprocs, /functionsでストアドプロシージャと関数も出力に含めるよう指定している。

他にも基底クラスを指定する/entitybaseなどがあるので下記を参考に自分の目的にあったオプションを見つけて欲しい。

SqlMetal.exe (Code Generation Tool)

最後に、マッピングファイルの読み込み方法で苦労したので参考までに紹介しておく。

Assembly assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream("TestDB.map");
XmlMappingSource mappingSource = XmlMappingSource.FromStream(stream);
return new TestDBDataContext(connectionString, mappingSource);

マッピングファイルのビルドアクションをResourceにしてからでないとなぜだか読み込めなかったのでGetManifestResourceStreamを使用している。