12 月号 2008
Team System: プロセス テンプレートでチーム プロジェクトを効率化する
Visual Studio Team System (VSTS) でプロセス テンプレートを使用してチーム プロジェクトを効率化する方法を学んでください。 Brian A. Randell
実用的な WF: Windows Workflow Foundation アプリケーションのベスト プラクティス
Windows Workflow Foundation の中心は宣言型のプログラミング モデルです。WF を使用して実用的なソフトウェア ソリューションを構築する場合に検討すべきベスト プラクティスを紹介します。 Josh Lane
Visual Studio OBA ツール: 相互運用 API 拡張機能を使用して OBA の開発を単純化する
VSTO Power Tools Office 相互運用 API 拡張機能を紹介するために、Outlook、Excel、および Word を自動化するアプリケーションの開発手順を見ていきます。 Andrew Whitechapel、Phillip Hoff、および Vladimir Morozov
SOA データ アクセス: LINQ to SQL と Entity Framework による柔軟なデータ アクセス
LINQ to SQL および ADO.NET Entity Framework を使用してデータ アクセス層を構築することで、使用している保存テクノロジからアプリケーションを分離できます。 Anthony Sneed
Geneva Framework: クレームベースの WCF サービスを構築するためのより優れたアプローチ
今回は、"Geneva" というコード ネームを持つ Microsoft の新しいフレームワークをご紹介します。これは、クレームベースのアプリケーションやサービス、およびフェデレーション セキュリティ シナリオを構築するために使用できます。 Michele Leroux Bustamante
Columns
編集者のメモ: 自分の中のビジネス性: テキサス州オースティンのオープン スペース カンファレンスからお送りします。編集長 Howard Dierking が、ソフトウェア開発とビジネス目標が共存し、相互にメリットがもたらされることについて述べています。Howard Dierking | ツールボックス: .NET 用の静的分析ツール、Matt Berseth のブログ: 今月は、デザイン ルールに準拠するための FxCop および他のツールや、jQuery を取り上げます。Scott Mitchell | CLR 徹底解剖: CLR でのスレッド管理: 同時実行アプリケーションで目的のパフォーマンスを得ることは、考えるほど簡単ではありません。一般的なスレッドの問題がアプリケーションにどのように影響するかを説明します。Erika Fuentes および Eric Eilebrecht
Advanced Basics: ObservableCollection クラス: CollectionChanged イベントを使用して、基になるデータ ソースの変更をバインドされたデータ コントロールに反映する方法について、Ken Getz が説明します。Ken Getz | Cutting Edge: ASP.NET のプレゼンテーション パターン: ASP.NET Web アプリケーションに対して適切な設計パターンを選択すると、プレゼンテーション層とその下位層との間で関心を分離することができます。Dino Esposito | 実践的なパターン: テストの容易性を高める設計: アプリケーションの設計でテストの容易性を高めるということは、より低いコストで作成でき、理解しやすく、実行が速く、デバッグもずっと簡単な、小さなテストを記述することを意味します。Jeremy Miller
テストの実行: Virtual Server を使用した構成テスト (第 2 部): Virtual Server は一連の COM モジュールで構築されているため、バーチャル マシンの作成およびテストに使用できます。ここでは、Windows PowerShell を使用したテストの実行方法を説明します。Dr. James McCaffrey および Paul Despe | 基礎: WPF でパス上にテキストをレンダリングする: Windows Presentation Foundation (WPF) を使用して、パス上にテキストをレイアウトした後、パスを定義する個々の点をアニメーションで動かし、文字がそれに応じて跳ね回るのを見ることができます。Charles Petzold | Windows と C++: 擬似変数と書式指定子を使用した x64 でのデバッグ: 今月は、擬似変数と書式指定子を利用して、デバッグに使用するさまざまな情報を表示する方法について説明します。Kenny Kerr
いろいろな場所へ: 接続マネージャの接続方法: Marcus Perryman が、Windows Mobile アプリケーションでネットワーク データを使用する場合の接続マネージャの適切な使い方を説明します。Marcus Perryman | 実践的なユーザビリティ: ソフトウェアの人間的な顔: 優れたユーザー エクスペリエンスは、表面が美しいだけではありません。この新しいコラムでは、優れたユーザー エクスペリエンスを構築する細部について説明します。Dr. Charles B. Kreitzberg および Ambrose Little | { End Bracket }: 優れたソフトウェア テスト担当者の条件: 今月のコラムでは、James McCaffrey が優れたソフトウェア テスト担当者に求められる資質とスキルについて説明します。Dr. James McCaffrey


Team System
プロセス テンプレートでチーム プロジェクトを効率化する
Brian A. Randell

この記事では、次の内容について説明します。
  • プロセス テンプレートの基礎
  • MSF Agile および MSF CMMI のテンプレート
  • プロセス テンプレートをカスタマイズする
  • 独自のテンプレートを作成する
 この記事では、次のテクノロジを使用しています。
Visual Studio Team Foundation Server 2008
目次
プロセス テンプレート
MSF テンプレートの内部
プロセス テンプレートをカスタマイズする
独自のテンプレートを作成する
カスタマイズされたテンプレートを編集する
現在、Team Foundation Server (TFS) での作業および成果物はすべてチーム プロジェクト内で収集、追跡できます。チーム プロジェクトは単に、開発プロジェクト中に追跡および使用するすべての成果物を格納して分別するためのバケツのようなものです。これには、Team Foundation Client (TFC) の新しいチーム プロジェクト ウィザードが役立ちます。
このウィザードは一見単純です。[ファイル] メニューの [新しいチーム プロジェクト] をクリックすれば、ウィザードが起動します。ウィザードが起動したら、チーム プロジェクトの名前を指定します。[次へ] をクリックし、コンボ ボックスを使用してプロセス メソドロジ テンプレートを選択します。再度 [次へ] をクリックし、チーム プロジェクトの説明を追加します。この説明は、SharePoint でポータルのホーム ページに表示されます。もう一度 [次へ] をクリックすると、バージョン管理の定義方法を決定するオプションが表示されます。新しい樹幹を作成するか、既存の樹幹からの分岐によって樹幹を作成するか、または樹幹を作成しないかを選択できます。オプションを選択して [完了] をクリックすると、わずかな時間で新しいチーム プロジェクトが作成されます。
それですべて終わりであれば、この記事はとても短いものになるでしょう。編集者は原稿料の支払いを保留し、筆者のコラムの今後について再考するかもしれません。しかし、話はそれほど単純ではありません。実際、Visual Studio Team System (VSTS) を使用して開発プロジェクトを良好にスタートさせるには、プロセス テンプレートを選択するウィザードのページが重要な意味を持ちます。なぜなら、プロセス テンプレートは、チーム プロジェクトの最初の構造および内容を定義するものであるからです。

プロセス テンプレート
既に述べたとおり、チーム プロジェクトはチームのすべての成果物に対する収集メカニズムを提供します。一般に、チーム プロジェクトには、ソース ファイルなどのバージョン管理された項目を格納します。また、すべての作業項目および作業項目クエリも管理して格納します。ドキュメントは、チーム プロジェクトのポータルの一部である SharePoint ドキュメント ライブラリに格納します。そして最後に、チームの作業状況を追跡するために、データ ウェアハウスに対してレポートを実行します。
プロセス テンプレートは、チーム プロジェクトに含める既定の成果物テンプレートを定義します。具体的には、チームで使用できるようにする作業項目の種類を定義します。また、SharePoint サイトのユーザー インターフェイスと構造、およびドキュメント ライブラリに読み込まれる既定のドキュメント (ファイルとテンプレートの両方) を定義します。さらに、レポート サービス サイトに対して既定のレポートを提供し、既定のセキュリティ グループ、領域、イテレーション、およびバージョン管理設定を定義します。そして最後に、プロセス テンプレートは、SharePoint サイトの一部として規範的なガイダンスを提供します。
TFS の最初のインストール時には、2 つのプロセス テンプレートが提供されます。TFS の 2008 リリースでのそれらの正式な名前は、Microsoft Solutions Framework (MSF) for Agile Software Development (MSF Agile) および MSF for CMMI Process Improvement (MSF CMMI) です。
MSF は、顧客との頻繁なやり取りによって、反復的な開発プロセスを推進します。MSF の現在のリリースはバージョン 4.0 で、独自の豊富な MSF ガイダンス リソースのセットを備える一方、TFS のプロセス ガイダンスにも影響を与えています。Michael Turner の著書『Microsoft Solutions Framework Essentials』(Microsoft Press、2006 年) には、さらに有用な情報が記載されています。
Microsoft プロセス テンプレートに含まれる規範的なガイダンスは、開発プロジェクトの実行方法およびワークストリームの構成方法に関して提供されます。このガイダンスは、Visual Studio や TFS の動作よりもむしろ、開発のプロセスに重点を置いています。このガイダンスには、チーム プロジェクトの SharePoint ポータル ホーム ページにある [プロセス ガイダンス] リンクからアクセスできます。また、TFS へのオンライン アクセスを行わずにガイダンスをざっと目を通す場合には、MSF Agile および MSF CMMI の両方について、マイクロソフトから直接ダウンロードすることもできます。

MSF テンプレートの内部
MSF Agile プロセス テンプレートと MSF CMMI プロセス テンプレートを簡単に比較すると、"アジャイル" と "フォーマル" の違いということになります。どちらのテンプレートも、7 つの論理領域に分類されるピアのチームに基づいて、異なるロールを定義します。MSF Agile テンプレートは、そのチーム モデルの一部として 8 つのロールを定義します。一方、MSF CMMI は、20 ものロールを定義します。図 1 に示すように、CMMI テンプレートは、各論理領域をさらに具体的なロールによって拡張します。これには、コアとなる開発チームには属していない管理者や他の関係者も含まれます。
図 1 MSF Agile および MSF CMMI のチーム メンバ ロール
領域 | ロール | CMMI | Agile
アーキテクチャ | インフラストラクチャ設計担当者 | Y | N
アーキテクチャ | ソリューション設計担当者 | Y | N
開発 | ビルド エンジニア | Y | N
開発 | 開発マネージャ | Y | N
開発 | 主要開発者 | Y | N
製品管理 | 監査担当者 | Y | N
製品管理 | 製品マネージャ | Y | N
製品管理 | スポンサー | Y | N
製品管理 | 領域の専門家 | Y | N
プログラム管理 | 統合プロジェクト管理 (IPM) 責任者 | Y | N
テスト | テスト マネージャ | Y | N
ユーザー エクスペリエンス | ユーザー教育スペシャリスト | Y | N
ユーザー エクスペリエンス | ユーザー エクスペリエンス設計担当者 | Y | N
開発 | データベース開発者 | Y | Y
開発 | 開発者 | Y | Y
製品管理 | ビジネス アナリスト | Y | Y
プログラム管理 | プロジェクト管理者 | Y | Y
リリース運用 | データベース管理者 | Y | Y
リリース運用 | リリース マネージャ | Y | Y
テスト | テスト担当者 | Y | Y
アーキテクチャ | アーキテクト | N | Y
ユーザー エクスペリエンス | ビジネス アナリスト | N | Y
ロール以外にも、作業項目にはいくつかの違いが見られます (図 2 を参照)。これには、各テンプレートに対して提供される実際の作業項目の種類の違いも含まれます。作業項目の各種類は、それぞれ 4 つの要素から構成されます。目的、フィールド、ワークフロー、およびレイアウトです。したがって、どちらのテンプレートもバグ作業項目を持ちますが、それらは根本的に種類が異なります。実際、MSF Agile と MSF CMMI の作業項目間の主要な違いは、状態および遷移として表現されるそれぞれのワークフローにあります。MSF Agile のバグに関するプロセス ガイダンスを調べ、状態と遷移のセクションを見ると、MSF Agile のバグには 3 つの状態と 23 の遷移があることがわかります (図 3 を参照)。一方、MSF CMMI のバグには、4 つの状態と 17 の遷移があります (図 4 を参照)。
図 2 MSF Agile および MSF CMMI の作業項目の種類
作業項目の種類 | 説明 | CMMI | Agile
変更要求 | 変更要求は、製品またはベースラインの一部に対して提案されている変更を特定します。 | Y | N
問題 | 問題作業項目は、製品での作業を妨げる可能性がある、または作業を現在妨げているイベントや状況を説明します。 | Y | N
要件 | 要求は、顧客の問題を解決するために製品に求められることを把握し、追跡します。 | Y | N
確認 | 確認作業項目は、デザイン レビューまたはコード レビューの結果を文書化します。 | Y | N
バグ | バグは、問題が存在する可能性があることや、問題がシステムに存在することを示す作業項目です。 | Y | Y
リスク | リスクは、将来においてプロジェクトの結果に悪影響を及ぼす可能性があるイベントや条件です。 | Y | Y
タスク | タスク作業項目は、何らかの作業を行う必要性を示します。 | Y | Y
サービス品質要求 | サービス品質要求は、パフォーマンス、読み込み、可用性、負荷、アクセシビリティ、サービス性、保全性など、システムの特徴を文書化します。 | N | Y
シナリオ | シナリオは、ユーザー操作の 1 つのパスをシステムを通じて記録する作業項目の種類です。 | N | Y
図 3 MSF Agile でのバグの状態と遷移
図 4 MSF CMMI でのバグの状態と遷移
たとえば、CMMI のバグは提案済み状態で開始される一方、MSF Agile のバグは作成と同時にアクティブになることがわかります。2 つの種類のバグ間のもう 1 つの違いは、その目的にあります。MSF Agile のバグは、すべての欠陥に対するキャッチオールとなっています。カスタマイズされていない MSF Agile のバグを調べると、Issue という名前のブール型フィールドが含まれているのがわかります。CMMI プロセス テンプレートの場合は、Issue という作業項目の種類が別個に作成されています。これは、違いの一例にすぎません。ただし、このような違いは、チーム プロジェクトを使用する際にチームのワークフローに影響を与えます。
MSF 作業項目のそれぞれの種類について、プロセス ガイダンス ドキュメントでは 1 つのセクションが割り当てられています。プロセス ガイダンスは、2 つのセクションに分かれています。それは、ワークストリームとアクティビティです。ワークストリームは、一般に特定のロールに関連した、アクティビティの論理的な流れです。アクティビティは、特定のタスクに焦点を合わせた手順のグループです。
各アクティビティは、開始基準と終了基準を持ち、1 つまたは複数のロール、および 1 つのワークストリームと関連付けられています。たとえば、MSF Agile の "Reproduce the Bug" (バグの再現) アクティビティを見ると、"Fix a Bug" (バグの修正) ワークストリームの一部であり、関与するロールが開発者だけであることがわかります。MSF Agile テンプレートの "Plan an Iteration" (イテレーションの計画) ワークストリームに含まれる "Determine Iteration Length" (イテレーション長の決定) など、他のアクティビティには、責任を持つロール (プロジェクト管理者) と関与するロール (開発者およびデータベース開発者) の両方があります。
チームおよびプロセスが既にどの程度成熟しているかに応じて、ガイダンスで提供される助言のすべてまたは一部を採用することも、何も採用しないこともできます。ただし、各テンプレートに含まれる作業項目、レポート、およびドキュメントは、各テンプレートのガイダンスのデザイン内で動作するよう作成されていることに注意してください。したがって、ドキュメントを参照し、自分のチームに対して何が機能し何が機能しないのかを確認する必要があります。その後で、提案されたフレームワーク内で作業するか、またはそれを自分のチーム向けに調整するかを決定できます。

プロセス テンプレートをカスタマイズする
チームではマイクロソフトのテンプレートのいずれかをそのまま使用することもできますが、通常は何か変更を加える必要があります。ほとんどのチームで第 1 に挙げられるのが、作業項目のカスタマイズです。それ以外にも、作成やカスタマイズの対象として、チェックイン ポリシー、SharePoint ポータル サイト、レポートなどがあります。
筆者は、MSDN Magazine での最初のコラム以来ずっと、Team System の豊富なカスタマイズ機能セットについて賞賛してきましたが、プロセス テンプレートも例外ではありません。プロセス テンプレートのほとんどすべての部分が変更可能であり、カスタマイズできます。実際、何が可能であるかを知るための良い方法は、スクラム開発手法の利点を活用する 2 つのよく知られたサードパーティ テンプレートを調べてみることです。
Conchango は、筆者が知る限り最初のスクラム パブリック テンプレートを公開していて、TFS 2005 および TFS 2008 用のテンプレート バージョンがあります。この記事の執筆時点で、Conchango はコミュニティに対して両方のテンプレートを無料で提供していますが、ダウンロード リンクを表示するには登録が必要です。
テンプレートをダウンロードしたら、コンテンツを抽出し、セットアップ パッケージを実行します。構成を完了すると、新しいチーム プロジェクト ウィザードの選択肢の 1 つとしてテンプレートが表示されます。チーム プロジェクトを作成すると、TFS 2008 バージョンのテンプレートが 6 種類のカスタム作業項目、16 のチーム作業項目クエリ、15 のレポート、1 つの SharePoint テンプレート、および独自のプロセス ガイダンスを定義していることがわかります。
もう 1 つのよく知られたテンプレートに、VSTS Scrum Process Template (Lightweight Scrum) があります。VSTS MVP である Mike Azocar、Steven Borg、および Willy-Peter Schaub は、これをコミュニティ主導型のプロジェクトとしてスタートさせました。Conchango のテンプレートと同様に、プロジェクトの所有者たちはこのテンプレートを TFS 2005 と TFS 2008 の両方に対して用意しています。また、Microsoft Project Server 2007 VSTS Connector と共に使用できるバージョンも提供されています。
Lightweight Scrum テンプレートの TFS 2008 バージョンには、6 種類のカスタム作業項目、18 のチーム作業項目クエリ、5 つのレポート、1 つの SharePoint テンプレート、および独自のプロセス ガイダンスが含まれています。このテンプレートをインストールするには、最初に SharePoint テンプレートをインストールする必要があります。その後で、プロセス テンプレート マネージャを使用して Lightweight Scrum テンプレートの残り部分をインストールします。プロセス テンプレート マネージャは、Visual Studio の [チーム] メニューの [Team Foundation Server の設定] からアクセスできます。Lightweight Scrum テンプレートをフォルダに解凍してから、プロセス テンプレート マネージャを起動し、[アップロード] ボタンをクリックします。ProcessTemplate.xml ファイルを含むフォルダを参照して、[アップロード] ボタンをクリックします (図 5 を参照)。
図 5 Lightweight Scrum テンプレートをインポートする (画像をクリックすると拡大表示されます)
どちらのスクラム テンプレートも、可能なカスタマイズのレベルについてよい手掛かりを与えてくれます。インターネットを検索すれば、それ以外のテンプレートも見つかります。何人かの VSTS MVP は、コミュニティ主導型のプロセス テンプレート ライブラリを作成するために Templex という CodePlex プロジェクトを立ち上げています。

独自のテンプレートを作成する
ドキュメントを入手して独自のプロセス テンプレートを手作業で構築することも可能ですが、筆者が関わっている顧客のほとんどは、既存のマイクロソフトのテンプレートを利用してそこから始める方が簡単であると感じています。開始の手助けとして、このアプローチについて次に説明します。
まず、マイクロソフトのプロセス テンプレートをダウンロードするためにディレクトリを作成します。この作業を重要視する場合は、ダウンロード後にテンプレート内をチェックできるように、バージョン管理で場所を構成する必要があります。[チーム] メニューの [TFS Settings] (TFS 設定) をクリックすることにより、プロセス テンプレート マネージャを使用して MSF Agile テンプレートを C:\PMT にダウンロードします。プロセス テンプレート マネージャにより、MSF for Agile Software Development–v4.2 という名前のフォルダが作成されます。このフォルダ内には、6 個のサブフォルダと 1 つの XML ファイル ProcessTemplate.xml が含まれています (図 6 を参照)。
図 6 MSF Agile プロセス テンプレートのダウンロード結果 (クリックすると拡大画像が表示されます)
フォルダおよびその内容は、ほとんどがひとめでわかるものです。プロセス テンプレートとその内容は、いくつかの XML ファイルを使用して記述します。これらの XML ファイルは、実際の成果物 (レポート定義ファイルや HTML ドキュメントなど) との組み合わせで、プロセス テンプレートを定義します。図 7 に、構成ファイルとそれぞれの場所および目的を示します。
図 7 プロセス テンプレートの構成ファイル
フォルダ\ファイル | 目的
%ルート%\ProcessTemplate.xml | ProcessTemplate.xml は、テンプレートのコアとなる構造を定義します。このファイルは、FileMapping.xml を除く他のすべてのファイルを参照します。
Classification\Classification.xml | Classification.xml は、テンプレートの領域およびイテレーションを定義します。また、FileMapping.xml を参照します。
Classification\FileMapping.xml | FileMapping.xml は、作業項目フィールドを Microsoft Project のプロジェクトにマップします。
Groups and Permissions\GroupsandPermissions.xml | GroupsandPermissions.xml は、既定のグループとそれらのアクセス許可を定義します。
Reports\ReportsTasks.xml | ReportsTasks.xml は、Reporting Services のレポート定義の一覧を定義します。
Version Control\VersionControl.xml | VersionControl.xml は、バージョン管理リポジトリのアクセス許可、既定のチェックイン メモ、および既定のチェックアウト動作を定義します。
Windows SharePoint Services\WssTasks.XML | WssTasks.xml は、使用する SharePoint サイト テンプレート (既にインストールされているもの) を定義します。また、ドキュメント ライブラリ、フォルダ、およびテンプレートのプロセス ガイダンスを含むファイルを定義します。
WorkItem Tracking\workitems.xml | Workitems.xml は、作業項目の種類およびプロセス テンプレートのチーム作業項目クエリの一覧を定義します。また、チーム プロジェクトの作成時に新しいチーム プロジェクトで作成する必要のある既定の作業項目の定義も含んでいます。
XML ファイルは、いくつかの方法で変更できます。一般には、メモ帳などの任意のテキスト エディタを使用します。XML を手作業で編集することが苦にならず、Visual Studio XML エディタの使用を好む方は、Visual Studio 2008 SDK をダウンロードして XSD ファイルを関連付けることもできます。XSD ファイルは、%SDK インストール フォルダ%\VisualStudioTeamSystemIntegration\Process Templates\ProcessTemplateSchemas にあります。また、作業項目の定義およびグローバル リストに対する XSD ファイルは、%SDK インストール フォルダ%\VisualStudioTeamSystemIntegration\Work Item Tracking\WorkItemTrackingSchemas にあります。MSDN でプロセス テンプレート スキーマのドキュメントも参照できます。
(特に、このプロセス全体に慣れていない場合には) VSTS 2008 TFS Power Tool の一部として提供されるビジュアルなプロセス テンプレート エディタを使用すると便利かもしれません (「重要な Power Tool」で詳しく説明されています)。プロセス テンプレート エディタには、Visual Studio の [ツール] メニューからアクセスできます。
ダウンロード テンプレートからのファイルを開く前に、フォルダ名を、テンプレートに対して使用する名前 (たとえば、"MSDN Magazine Agile 1.0" など) に変更する必要があります。名前の変更後、最新バージョンの Power Tool がインストールされた状態で、Visual Studio の [ツール] メニューから [プロセス テンプレート]、[Open Process Template] (プロセス テンプレートを開く) の順にクリックして、ProcessTemplate.xml ファイルを開きます。テンプレート デザイナが起動します。名前と説明を意味のあるものに変更して (図 8 を参照)、変更を保存します。デザイナを閉じてからファイルを再度開くと、エディタに新しい名前が表示されます。エディタの左側のツリー ビューでは、図 7 に示されたコア ファイル、およびすべての作業項目の種類の定義および作業項目クエリにアクセスできます。
図 8 新しいテンプレートの名前と説明を編集する (画像をクリックすると拡大表示されます)
メインのプロセス テンプレート エディタで公開される項目のほとんどは、編集に際してひとめで内容がわかるようになっています。前述したとおり、一般に作業項目は最初に変更される大きな項目です。ツリー ビューでは、[Work Item Types Definitions] (作業項目の種類の定義) ノードにある [Bug] (バグ) 作業項目を選択します。その作業項目の種類が、作業項目の種類の定義エディタで開かれます。このエディタでは、通常のテキストボックス コントロールを使用して名前と説明を変更できます。
作業項目エディタには、作業項目の種類の定義にアクセスするための 3 つのタブがあります (図 9 を参照)。これは、[フィールド]、[レイアウト]、および [ワークフロー] です。また、[XML の表示] を使用すると、種類の XML 表現を簡単に見ることができます。このボタンはエディタでの作業中常に表示されており、XML ファイルに加えた変更の効果を確認するのに良い方法です。慣れてくれば、プロセス テンプレート エディタを使用するよりも、XML ファイルを直接編集する方が早くて簡単に感じるかもしれません。
図 9 バグ作業項目の種類を編集する (クリックすると拡大画像が表示されます)
[フィールド] タブには、作業項目の種類で使用されるすべてのフィールドの一覧が表示されます。フィールドは、名前、種類、および参照名を持つものとして定義されています。参照名は、フィールドの完全修飾名と考えることができます。これは、ピリオドで区切られた 2 つ以上の部分から構成された名前である必要があります。参照名は、作業項目クエリで列の表示名として使用されます。
[New] (新規作成) ボタンをクリックして、新しいフィールドを追加します。[Field Definition] (フィールド定義) ダイアログ ボックスで、図 10 のようにフィールドを設定し、[OK] をクリックします。名前と種類のフィールドは明確です。他に選択できる有効なデータ型としては、DateTime、Double、History、HTML、Integer、PlainText、String、および TreePath があります。History および TreePath は、それぞれ履歴フィールドと領域およびイテレーション フィールド用に特化されたコントロールです。[ヘルプ テキスト] に指定したテキストは、作業項目インスタンスの使用中にフィールドをポイントしたときに表示されます。
図 10 新しいフィールド定義の値
[Reportable] (レポート可能) フィールドは、後でレポートに含めることができるように TFS でフィールドを TFS データ ウェアハウスにエクスポートするかどうかを制御します。フィールドを [Reportable] (レポート可能) にする場合は、そのフィールドがディメンション、詳細、メジャーのいずれであるかを選択することが重要です。DateTime、Double、Integer、および String 型のフィールドには、ディメンションを使用します。そのフィールドを使ってフィルタ処理を行う場合には、ディメンションを選択します。フィールドで標準値のセットを使用する場合には、これが最適な選択肢です。
レポートに含める大きなテキスト フィールドがある場合には、詳細オプションを使用します。TFS では、データ ウェアハウス キューブに詳細フィールドを含めません。リレーショナル ウェアハウス データベースだけです。詳細フィールドには、DateTime、Double、Integer、および String 型のフィールドを使用できます。
最後に、(sum や count などの) 数式を使用する場合や、フィールドで同様な操作を実行する場合には、メジャー フィールドを使用します。このフィールドは、Double および Integer のデータだけをサポートします。メジャーを選択する場合は、数式フィールドからオプションを選択します。現在のドキュメントでは、sum だけが有効であると書かれていますが、それは誤りです。
フィールドにいったん [Reportable] (レポート可能) 属性を適用すると、以後変更できないことに注意が必要です。ただし、既に使用しているフィールドにこの属性を適用することはできます。フィールドを [Reportable] (レポート可能) にした後でその作業項目に加えた修正は、ウェアハウスに格納されます (該当する場合)。しかし、フィールドを [Reportable] (レポート可能) に設定する前に加えられた変更について、後からウェアハウスにデータが反映されることはありません。
フィールドを作成したら、そのフィールドがフォーム上のどこに表示されるかを指定する必要があります。作業項目の種類エディタで、[レイアウト] タブをクリックします。[フォームのプレビュー] をクリックして、現在の作業項目を調べることができます。ツリー ビューで、[Tab Page—Details] (タブ ページ - 詳細) ノードが表示されるまで下に移動します。そのノードの下にグループ ノードがあり、その下に 2 つの列ノードがあります。
最初の列ノードの [Group—General] (グループ - 全般) ノードの下に、1 つの列ノードがあります。このノードを右クリックして、[New Control] (新しいコントロール) をクリックします。新しく追加されたコントロール ノードを選択し、右側のプロパティ グリッドを使用してフィールド名を MSDN.Mag.Beverage に変更します。ラベルを Beverage に変更して、結果をプレビューします。問題なければ、作業内容を保存し、開いているエディタをすべて閉じます。
プロセス テンプレート エディタを使用し、[アップロード] ボタンをクリックして、変更された ProcessTemplate.xml ファイルを含むフォルダを参照します。テンプレートをアップロードしたら、新しいチーム プロジェクト ウィザードを実行して、MSDNMagDemo という名前のチーム プロジェクトを作成します (前述の Team System コラム「重要な Power Tool」で説明しているように、これを Virtual PC イメージでテストすることをお勧めします)。チーム プロジェクトを作成したら、新しいバグ作業項目を追加して、[詳細] タブに切り替えます。新しいフィールドが表示されます。新しいバグ作業項目を保存せずに閉じます。

カスタマイズされたテンプレートを編集する
この新しく作成されたフィールドに変更を加えて、許可された飲み物のリストを取得するとしましょう。また、このフィールドに対してレポートを実行できるようにもします。この変更は、今作成したチーム プロジェクトだけでなく、任意の新しいチーム プロジェクトに対しても有効にする必要があるため、少し追加の作業が必要になります。
最初に、[Process Editor] (プロセス エディタ)、[Work Item Types] (作業項目の種類)、[Open WIT from File] (ファイルから WIT を開く) の順にクリックして、バグ作業項目テンプレートを再度開きます。C:\PMT\MSDN Magazine Agile 1.0\WorkItem Tracking\TypeDefinitions に移動し、Bug.xml ファイルを選択します。Beverage フィールドを見つけて選択してから、フィールド一覧の最上部にあるツール バーで [開く] コマンドをクリックします。[Reportable] (レポート可能) フィールドの値をディメンションに変更します。
次に、[ルール] タブをクリックし、ツール バーの [New] (新規作成) をクリックします。[Select a rule type] (ルールの種類の選択) ダイアログ ボックスに示されるように、各フィールドにはいくつかのルールを適用できます。[ALLOWEDVALUES] を選択し、[OK] をクリックします。[ALLOWEDVALUES] ダイアログ ボックスで、[New] (新規作成) をクリックします。[List Item Edit] (リスト項目編集) に「Coffee」と入力し、[OK] をクリックします。この操作を 2 回繰り返してリストに "Soda" と "Juice" を追加します (プロジェクトで許される範囲内で、自由に好きな名前をお使いください)。リストが完成したら、[OK] をクリックして [ALLOWEDVALUES] ダイアログ ボックスを閉じます。[OK] をクリックして [Field Definition] (フィールド定義) ダイアログ ボックスを閉じ、Visual Studio の [ファイル] メニューの [保存] コマンドを使用して変更を保存します。
この時点で、ディスク上のファイルが変更されました。前と同様に、テンプレートを再アップロードする必要があります。ProcessTemplate.xml ファイルに追加の変更を加えずにそれを試みると、テンプレートが既に存在するという警告を受け取ります。[はい] をクリックして、テンプレートの古いコピーを上書きします。
これにより、新しいチーム プロジェクトの問題は解決されます。ただし、MSDNMagDemo チーム プロジェクト内の既存のバグ作業項目を修正する必要があります。[Process Editor] (プロセス エディタ)、[Work Item Types] (作業項目の種類)、[Import WIT from File Server] (ファイル サーバーから WIT をインポート) の順にクリックします。[Import Work Item Type Definition] (作業項目の種類の定義のインポート) ダイアログ ボックスで、[参照] ボタンをクリックし、C:\PMT\MSDN Magazine Agile 1.0\WorkItem Tracking\TypeDefinitions の Bug.xml ファイルを選択します。次に、[Project to import to] (インポート先プロジェクト) ツリーのチーム プロジェクトの一覧で、MSDNMagDemo チーム プロジェクトを選択し、[OK] をクリックします。MSDNMagDemo チーム プロジェクトが存在しない場合は、Visual Studio をいったん閉じて再起動する必要があります。
新しいバグを追加する前に、チーム エクスプローラ ウィンドウの MSDNMagDemo チーム プロジェクト ノードで、[作業項目] フォルダを右クリックして [Refresh] (更新) をクリックすることにより、作業項目の定義を更新する必要があります。図 11 のような画面が表示されます。
図 11 新しい Beverage フィールドを含めて完成されたバグ作業項目 (クリックすると拡大画像が表示されます)
マイクロソフトが、単なるツールではなく規範的なガイダンスを備えたツールを提供することに多大な努力をしていることがおわかりいただけたかと思います。もう少し時間をかけて、両方の組み込みテンプレート (またはコミュニティで作成されたテンプレート) を調べてみることをお勧めします。テンプレートの選択がチームおよび開発の進捗全体に対して持つ意味を理解してください。

Brian A. Randell は、MCW Technologies, LLC のシニア コンサルタントです。マイクロソフト テクノロジについての講演や執筆を行っています。Microsoft MVP として、Pluralsight 社の Applied Team System コースの作成にも携わりました。mcwtech.com/cs/blogs/brianr にブログを公開しています。


実用的な WF
Windows Workflow Foundation アプリケーションのベスト プラクティス
Josh Lane
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事では、次の内容について説明します。
  • ワークフローのプログラミング モデル
  • ワークフローとサービス
  • 単体テストとワークフロー
  • WF をソリューションに接続する
 この記事では、次のテクノロジを使用しています。
Microsoft .NET Framework 3.5、Windows Workflow Foundation、Visual Studio 2008
目次
ワークフローのプログラミング モデル
副作用駆動型プログラミング
さまざまなサービス
偶発的な実行
単体テストも有効
ランタイム内のランタイム
二者択一の選択肢ではない
プラグ アンド プレイ
ドメイン モデリングとプログラム デザイン
その他の注意事項
Windows Workflow Foundation (WF) が Microsoft .NET Framework 3.0 の一部としてリリースされて以来、筆者は、WF テクノロジを使用したシステムの実装に多くの時間を費やし、その方法を人にも伝授してきました。その経験から、WF を使用して実用的なソフトウェア ソリューションを構築する場合に検討すべきベスト プラクティス (ベストとまではいかない事例も含めて) に関する洞察が得られました。
その結果、WF は一種のアイデンティティの危機に瀕していると実感しました。話を交わした多くの開発者は、条件付きロジック、フロー制御、アトミック操作など WF の機能をある程度まで理解していました。
「ええ。理解しているつもりです。デザイン画面に図形をドラッグすればいいんでしょう。それだけで、実行する操作のフローチャートのようなシーケンスが表現されます。便利なしくみですよね」でも、そう言った後にやんわりと否定的な反応が返ってくることがあります。「でも、どんな使い道があるんでしょう」とか、「そんなことなら C# や Visual Basic でもできるでしょう」という風に。
こうした言い分にも根拠がないとはいえません。確かに、WF は表面的には Visual Studio から操作するドラッグ アンド ドロップ デザイナです。ただ、このような反応からは、表面上はありふれて見える機能の背後にある WF の真価が認知されていないことがわかります。
WF の真価を理解するには、歴史的背景を考えてみることが有効です。その誕生以来、.NET Framework の主な役割は Windows ソフトウェア プログラムを作成する場合の抽象化のレベルを高めることでした。Windows プログラマは COM、HRESULT、スマート ポインタ、MTA、メッセージ ポンプ、サンク層など理解しなければならない非ドメイン環境固有の概念が多すぎて悪戦苦闘を強いられることがありました。
.NET Framework は、これらの概念の多くを過去のものにすることに成功しました。現在、多くの開発者は、Visual Studio を開いて当面の問題に直接マップするコードの作成をすぐに開始できることを当たり前のことと思っています。メモリ管理、セキュリティ ポリシーの適用、コードの依存関係の実行時解決などの数え切れない問題を .NET ランタイムに任せることができます。その結果、多くの開発者の今日の生産性は飛躍的に向上しました。
WF はプログラミングの抽象化のレベルをさらに一歩高めるロジックです。WF プログラムは .NET CLR 上で動作する特殊なランタイム環境内で実行されます。WF ランタイム環境をターゲットとするプログラムの作成には一定の制限が課されます。代わりに WF では、実行に数日から数か月かかる実行時間の長いコード、プログラム実行の一時停止、再開、取り消し、監査および追跡プログラムの実行、実行中のプログラム ロジックの変更などをサポートする、高機能で柔軟性が高く、拡張性に優れたランタイム サービスを利用できるようになります。
WF の最も重要な特性は宣言型のプログラミング モデルです。WF では、通常は Visual Studio のドラッグ アンド ドロップ WF デザイナを使用してプログラムの処理を記述します。この場合、処理内容を詳細に指定することはありません。WF ランタイムは、プログラムの記述 (制御フローとアクティビティの階層図) を目的のロジックを実行するための詳細仕様として使用します。

ワークフローのプログラミング モデル
Visual Studio のワークフロー デザイナを使用してみれば、WF がコンポーネント テクノロジであることがすぐにわかります。WF のコンポーネントはアクティビティです。もう少し具体的に言うと、Activity 基本クラスのサブクラスです。
アクティビティは WF の構成および実行の単位です。一部のアクティビティ (Activity のサブクラスである CompositeActivity のサブクラス) には、親アクティビティの実行シーケンスの一部として実行される子アクティビティがあります。WF のワークフローは 1 つのルート アクティビティから開始して任意の深さと複雑さで展開されるアクティビティの図にすぎません。図中の各アクティビティの本質的なロジックと、このロジックで操作されるデータによって、実行順序とワークフローの全体的な動作が決まります。
WF のアクティビティはレゴ ブロックのようなものです。レゴの場合、単体ではあまり役に立たないさまざまな色、サイズ、機能を持つブロックを組み合わせて大きくしていくことによって全体を組み立てます。できあがった全体は個々の部品の合計以上の価値を持ちます。これと同様に、WF も個別の機能を実行するアトミック アクティビティを組み合わせて高次のロジックを作成し、そのロジックをさらに組み合わせて、より大きなビジネス プロセスなどの機能を実現します。
コンポーネント プログラミングでは一般に (そして特に WF では)、再利用のために継承より構成の方が好まれるという傾向があります。WF の使用に慣れて、複雑なワークフローを作成するようになると、C# や Visual Basic で作成された複雑なシステムの実例に従って深いクラス階層を作成するのではなく、小さい自律的なアクティビティからプログラムを組み立てる場合が多くなります。
従来どおり、WF での再利用方法としてクラスの継承を使用することも可能です。たとえば、SendEmailActivity から派生して SendEncryptedEmailActivity クラスを作成することに問題はありません。この場合、実行される処理の性質が根本的に変わるため、継承を使用するのは自然な方法です。ところが、条件付きで一定の状況下でのみ電子メールを送信する場合や、電子メールを送信する前後に別の処理を行う場合には、SendEmailActivity を他の CompositeActivity サブクラス (SequenceActivity や IfElseActivity など) の子にして、追加ロジックを SendEmailActivity の外部に定義する方が賢明です。
図 1 は、条件付きの電子メール アクションをカプセル化した複合アクティビティを示しています。このように SendEmailActivity を 1 つの役割 (電子メールの送信) に専念させることにより、他のワークフローで再利用できる可能性が高くなります。また、新しいコードを一から作成せずに、既存の構成要素を WF で再利用することも可能になります (条件分岐やアクティビティの連続実行のサポート)。
図 1 条件付きロジックを使用したカスタム複合アクティビティ

副作用駆動型プログラミング
プログラミングでは、副作用はプログラムの一部を実行した結果生じた共有状態の識別可能な変化として定義されます。たとえば、オブジェクトのプロパティ値の変化や、データベース テーブルの 1 つまたは複数の値の変化などが副作用に当たります。副作用はソフトウェアの処理に常時使用されます。
共有状態を使用せず、データの遷移や操作の主な手段としてメソッドの戻り値を使用する純然たる関数型プログラミングと異なり、ワークフローに関連する処理には何らかの副作用が伴います。ワークフロー プログラムでは戻り値を使用せず、実行前と実行後の状態を比較して変更を確認します。制御フローを指定する場合はメソッドを呼び出すのではなく、複数のアクティビティを組み立てます。アクティビティの入出力は、メソッドのパラメータや戻り値によって明示的に定義せず、アクティビティ クラスのデザイン時にプロパティで暗黙的に定義されます。データは、組み込みサポートされているアクティビティ データ バインドを使用してアクティビティ間を遷移します。共有状態を操作するには、カスタム WF アクティビティのロジック内で構成されたサービスとやり取りする方法もあります。
基本的に、WF のアクティビティは、アクティビティの状態の初期化、アクティビティの実行、アクティビティの状態変更の確認の順序で実行され、すべてのアクティビティについてこの実行シーケンスが繰り返されます。この場合、アクティビティ実行の副作用 ("PurchaseOrderTotal プロパティの新しい値は何か" や "変更されたばかりのデータベース値は何か") は、何が起こったか、また、次に何が起こるかを知る手段になります。アクティビティが他のダウンストリーム アクティビティにとって好ましくない方法で状態を操作する可能性もありますが (この場合のリスクは C# などの命令型言語に固有の問題に比べたらささいなものです)、データをコピーすることによって生じるパフォーマンスの低下を防ぐことができます。

さまざまなサービス
WF は、汎用的なプログラミング環境として、外部のサブシステムやコードと統合するように設計されています。最も直接的な統合方法として、データベースまたは Web サービスの呼び出しや、サードパーティ製ライブラリの呼び出しを行うカスタム ワークフロー アクティビティを作成する方法があります。WF の用語では、このような外部システムのことをサービスと呼びます。
WF サービスの考え方は単純です。ワークフロー内のアクティビティに必要であり、アクティビティ定義に含まれていないライブラリや機能要素をサービスとして WF ランタイムに登録するというものです。
// create an instance of your service type...
  LoggingService logSvc = new LoggingService();

  // register it with the WorkflowRuntime instance...
  workflowRuntime.AddService( logSvc );

  WorkflowInstance instance =
    workflowRuntime.CreateWorkflow( typeof( Workflow1 ) );

  instance.Start();
実行時に、アクティビティは、サービスの有効期間の管理を WF ランタイムとホストに任せ、ActivityExecutionContext (ホスト ランタイムと直接やり取りを行わないアクティビティ) を通じて必要なサービスに対するアクセスを要求し、構成されたサービスを使用して処理を実行できます。
protected override ActivityExecutionStatus Execute(
    ActivityExecutionContext aec ) {
  // ActivityExecutionContext provides access to services...
  LoggingService logSvc = aec.GetService<LoggingService>();

  Debug.Assert( logSvc != null );
  logSvc.LogEvent( EvtType, Message );
  return ActivityExecutionStatus.Closed;
}
一般的なサービスの例としては、外部サブシステムのログ コンポーネント、データ アクセス層、プロキシなどが挙げられますが、他にも可能性は無限大にあります。
WF サービス モデルの真価は、その単純さにあります。あらゆる .NET オブジェクトをサービスとして構成できます。サービスの有効期間管理などの追加動作を WorkflowRuntimeService サブクラスに明示的にデザインされたサービスで使用できます。具象型ではなく、インターフェイスを使用してサービスを公開すると、依存しているアクティビティとは独立してサービスの実装を簡単に変更できるため、便利です。
この場合の重要なベスト プラクティスは、カスタム WF アクティビティのコア ロジックを処理に必要な構成要素から切り離すことです。また、コア ロジックを、ワークフロー (ログなど) の複数のアクティビティ間で共有されるステートフルなサービス ロジックにリファクタリングすることも重要です。実行時にアクティビティ図に従って参照をログ コンポーネントに渡すのでは煩雑です。実行サイクルのある時点でシリアル化されたワークフローの場合はさらに複雑になります。
サービスについて、もう 1 つ言い添えることがあります。Windows WF には、アクティビティから .NET クラス インスタンスを呼び出し、アクティビティ内で .NET イベントを処理するローカル サービスとして知られているインフラストラクチャが付属しています。これらのローカル サービスの操作には、CallExternalMethod アクティビティおよび HandleExternalEvent アクティビティを使用します。実質的に、これらのサービスは、基本的な WF サービス インフラストラクチャの上位層として追加され、SharePoint など WF ホスト環境とより相性の良い宣言型の純正サービスと同じ機能を果たします。ただし、Windows Presentation Foundation (WPF) や ASP.NET などのより汎用的なホストの場合、この追加層は相当する純正サービスの構成よりも使い方が煩雑になる可能性があります。ローカル サービスを使用してソリューションを構築する前に、期待されるホスティング要件をよく考慮してください。
この場合の重要な原則は、WF サービス モデルの機能と柔軟性を活かして適切にファクタリングされた再利用可能なワークフロー アクティビティをデザインすることです。

偶発的な実行
WF を汎用的なプログラミング タスクに適合させる機能の 1 つに、非同期、非ブロックの処理のサポートがあります。サービスに対して要求が出されると、要求側の呼び出しに関連付けられたローカル リソースが他の処理のために解放される最近の Web サービスおよびデータベース指向アプリケーションの性質は、非同期呼び出しに適しています。ディスパッチされた処理が完了したら、実行は停止した箇所に戻ることができます。ASP.NET の非同期ページはこの方法の一例です。
WF ランタイムによって最初に実行されると、アクティビティの Execute メソッドが呼び出されます。アクティビティにより Execute 内のすべての処理が実行されると、Execute は ActivityExecutionStatus.Completed を返します。できる限りの処理を実行し、Web サービスまたはデータベース呼び出しからの応答や、特定のアドレスへの電子メール送信など、処理の続行を促す外部からの作用を待機する状態に入ると、ActivityExecutionStatus.Executing が返されます。
図 2 は、顧客 ID を指定して信用情報を外部サービスに照会するアクティビティの Execute メソッドを示しています。信用情報の取得には数分あるいは数時間かかる可能性もあるため、ワークフローはサービスを非同期に呼び出してアイドル状態で結果を待機します。アイドル状態になる前に、アクティビティは、外部からの刺激を受けたときにランタイムによって呼び出されるデリゲートを登録します。このデリゲートは継続 (文字どおり、"プログラムの残りの部分へのポインタ") と呼ばれます。このデリゲートによって、アクティビティの作成者はアクティビティの処理を継続するために必要なロジックを定義できます。1 つのワークフローの有効期間は、このようなアイドル継続状態の複数の遷移で構成される場合があります。
図 2 Execute メソッド
protected override ActivityExecutionStatus Execute(
  ActivityExecutionContext aec ) {

  if ( string.IsNullOrEmpty( SSN ) ) {
    throw new InvalidOperationException( "SSN property value is invalid." );
  }

  ICreditScoring creditSvc = aec.GetService<ICreditScoring>();
  Debug.Assert( creditSvc != null );

  WorkflowQueuingService queueSvc = aec.GetService<WorkflowQueuingService>();
  Debug.Assert( queueSvc != null );

  Guid queueName = Guid.NewGuid();
  WorkflowQueue queue = queueSvc.CreateWorkflowQueue( queueName, false );
  queue.QueueItemAvailable += CreditScoreComputed;
  creditSvc.ComputeScoreAsync( SSN, queueName );

  return ActivityExecutionStatus.Executing;
}
ランタイムは、ワークフロー インスタンスに関連付けられたワークフロー キューにアイテム (任意の .NET オブジェクト) が到達したことにより、アイドル状態のワークフローをアクティブにするために必要な外部刺激を認識します。非同期 Web サービス呼び出しの場合、応答を待機するコードがキューへの参照を取得し、関連するデータ (最終的に Web サービスから受け取ったあらゆるデータが想定されます) をキューにプッシュします。ランタイムはキューを監視し、登録されたデリゲートを呼び出します。デリゲートのロジックで、キューに登録されたアイテムを取得して必要な処理を実行することができます。
この方法の長所は、アイドル時のワークフローの動作にあります。Execute メソッドで ActivityExecutionStatus.Executing を返すアクティビティ (アイドル状態と判断されるアクティビティ) は、明らかに一定期間何の処理も行いません。外部刺激を待機する期間は数秒、数時間、数日、あるいは数か月かもしれません。この期間には、コンピュータ リソースを消費するメモリ内にワークフローが存在する必要はなく、より有効な用途にメモリを明け渡すことが可能です。そこで、WF ランタイムはこのようなワークフローをメモリから自動的にアンロードし、SqlWorkflowPersistenceService などの永続化サービスを使用して固定記憶域にシリアル化する機能をサポートしています。外部刺激を受けると、WF ランタイムはワークフローを再読み込みし、通常どおりに実行を継続します。

単体テストも有効
単体テストは WF のプログラマにとっても依然として有効です。WF の明示的で複合的な性質は、ソリューションの各部分を他の部分から切り離して実行する包括的テストのバッテリを作成する場合にも適しています。ここでは概要を説明しますが、ワークフローの単体テストの詳細については、MSDN Magazine の 2008 年 11 月号に掲載されている Matt Milner の「基礎」コラム (ワークフローとアクティビティの単体テスト) をご覧ください。
WF で実行できるのはワークフロー全体 (つまり SequentialWorkflowActivity のサブクラス化されたインスタンス) のみだという誤解が一般的にあるようですが、実際には、各アクティビティ インスタンスをスタンドアロン ワークフローとして実行できるため、汎用的なアクティビティ テスト ハーネスを使用して、アクティビティを単体または大きな全体の中の小さいサブセットで実行し、検証することも簡単です。図 3 は、このようなテスト ハーネスを示しています。
図 3 汎用的なワークフロー テスト ハーネス
protected Dictionary<string, object> ExecuteActivity(
  Type activityType,
  IEnumerable<object>
  services,
  params object[] inputs ) {

  Dictionary<string, object> outputs = null;
  Exception ex = null;

  using ( WorkflowRuntime workflowRuntime = new WorkflowRuntime() ) {
    AutoResetEvent waitHandle = new AutoResetEvent( false );

    workflowRuntime.WorkflowCompleted +=
      delegate( object sender, WorkflowCompletedEventArgs e ) {

      outputs = e.OutputParameters;
      waitHandle.Set();
    };

    workflowRuntime.WorkflowTerminated +=
      delegate( object sender, WorkflowTerminatedEventArgs e ) {

      ex = e.Exception;
      waitHandle.Set();
    };

    foreach ( object svc in services ) {
      workflowRuntime.AddService( svc );
    }

    Dictionary<string, object> parms = new Dictionary<string, object>();

    for ( int i = 0; i < inputs.Length; i += 2 ) {
      Debug.Assert( inputs[ i ] is string );

      parms.Add( (string) inputs[ i ], inputs[ i + 1 ] );
    }

    WorkflowInstance instance =
      workflowRuntime.CreateWorkflow( activityType, parms );

    instance.Start();

    waitHandle.WaitOne();

    if ( ex != null ) {
      Assert.True( false, ex.Message );
      return null;
    }
    Else {
      return outputs;
    }
  }
}
"データの入力、コードの実行、データの検証" という基本的な状態指向のテスト パターンは、WF の一般的なテスト ハーネスで通用します。WF ランタイムは、プロパティ名をキーとする汎用の Dictionary を使用した、ルート アクティビティのプロパティ値の自動生成をサポートしています。アクティビティの実行完了後にルート アクティビティのプロパティ値を確認することもできます。アクティビティがワークフロー階層内で複数のレベルにネストし、データ バインドを利用してプロパティ値を初期化している場合でも、アクティビティを他のアクティビティから切り離してテストし、そのアクティビティに期待される状態とエラー状態を全面的にシミュレーションすることができます。
WF では、オブジェクトのモック作成などのさらに詳細な単体テストも有効です。この場合、テスト コードを依存関係から切り離すことと、これらの依存関係に対する期待値を検証すること (たとえば、"このテストのコードを実行した場合、この依存関係では Foo() メソッドが 3 回呼び出されるはずだ" という期待値の検証) に重点を置きます。その例を次に示します。
// mock the IBanking interface...
var bankingMock = new Mock<IBanking>();

// specify that you expect AddCustomer to
// be called with the customer instance...
bankingMock.Expect( bank => bank.AddCustomer( customer ) );

// execute WF harness, configuring the mock as a service...
ExecuteActivity( typeof( AddCustomerActivity ),
  new List<object>() { bankingMock.Object }, "Customer", customer );
単体テストおよびオブジェクトのモック作成に詳しい読者なら、WF サービスが WF アクティビティをテストするモックに最適であることを既にお察しでしょう。WF サービスは本質的に依存関係であり、このような単体テストからは除外する必要があります。

ランタイム内のランタイム
WF は単なるコンポーネント ベースの拡張可能なプログラミング環境ではありません。CLR 上に構築される実行エンジンも取り込んでいます。このエンジンには、作業スケジュールの作成、アイドル状態時の自動シリアル化および逆シリアル化、データ バインドによる親アクティビティから子アクティビティへのデータ伝達など、ワークフロー プログラムに便利な多くのサービスが用意されています。ただし、WF のルールに従う必要があります。ワークフローの実装が未熟だと、この環境で十分に機能しないか、まったく動作しない可能性があります。
WF ランタイムについて理解する必要がある最も重要なことは、ホスト コードに特にスレッド要件がないということでしょう。シングルスレッド、マルチスレッドを問わず、あらゆる既存のスレッド モデルと統合するように設計されています。
ワークフロー実行のスケジューリングは、WF ランタイムに対して構成されたサービスを通じて行われます。DefaultWorkflowSchedulerService はワークフローを .NET ThreadPool で実行しますが、ManualWorkflowSchedulerService では、使用するスレッドを指定できます。後者は、ASP.NET などの ThreadPool に処理をディスパッチする WF ホストを操作する場合に便利です。実際に必要になることはまれですが、独自のカスタム スケジューリングを実装することも可能です。
スレッド モデルを問わず、個々のワークフローが実行するのは一度に 1 スレッドのみです。これは、WF ランタイム実行モデルの設計上の配慮です。ワークフロー内でマルチスレッドのコードを呼び出す必要がある場合には、カスタム WF サービスを作成することをお勧めします。
C# や Visual Basic など、従来のスタック ベースのプログラミング環境と異なり、ワークフローは呼び出し側コードと非同期に実行されます。そのため、ワークフローの実行が完了したとき、制御は自動的にワークフローを呼び出した箇所に戻るわけではありません。各ワークフロー インスタンスがプログラム内の独立したロジック分岐になります。
WF で実行中のワークフローの状態を管理する場合、WF ランタイムを理解していないと問題を引き起こす可能性があります。従来の POCO (Plain Old CLR Object) プログラミングでは、データ ストレージにクラス メンバのフィールドおよびプロパティを使用します。WF のアクティビティも同様の方法で作成しますが、アイドル状態時に永続するために構成された偶発的なワークフローの場合、図 4 のように、1 つの論理的なワークフローの有効期間が複数の物理的なアクティビティ図のインスタンスによって実現されている可能性があります。あるいは、複数のアプリケーションまたは複数のコンピュータによって実現されていることもあり得ます。
図 4 偶発的なワークフローの論理的および物理的ワークフロー有効期間
WF は、アクティビティ図と永続化時の状態の自動シリアル化をサポートしていますが、この機能はデータ型のサブセットに対してのみ有効であることに注意してください (配列はサポートされず、ジェネリックのサポートにも制限があります)。シリアル化サロゲートを使用してカスタム シリアル化を手動で作成することができます。Activity.OnActivityExecutionContextLoad および Activity.OnActivityExecutionContextUnload をオーバーライドして、各インスタンスのアクティビティごとにカスタム処理を実行し、ワークフローのオブジェクト図を破棄することも可能です。
WF 自体が 1 つのランタイムです。WF のスレッド、スケジューリング、シリアル化の基本概念を理解し、陥りやすい誤りを犯さないよう注意してください。

二者択一の選択肢ではない
新しいテクノロジを最初に導入する際に一般的なアンチパターンは、そのテクノロジを全面的に採用し、場合によっては他の選択肢を排除するという姿勢です。ことわざに言うところの "ハンマーを振り下ろすときは何でも釘に見える" 症候群にかかってしまうようです。だれもがある程度はこのような過ちを犯しています。WF についてもこのような誤りを犯す可能性は大いにありますが、多くの統合ポイントや拡張性フックを持つ低水準のプラミング テクノロジという性質を持つ WF は、多くの個別の問題領域に適用できます。
WF は、ASP.NET などの高度なマルチスレッド環境にも、Windows フォームまたは WPF を使用して作成されたデスクトップ クライアント アプリケーションにも有効です。このような環境での一般的な WF の使い方には、ユーザー インターフェイスのイベント処理ロジックを命令型コードではなくワークフロー内に実装する UI 駆動型ロジックの実装、Web 検索結果の取得および処理や、通常は別のスレッドで呼び出されるディスク上での大きなファイルの操作などのタスク指向の非同期バックグラウンド処理、ワークフローのロジックにより ASP.NET やスマート クライアントの UI ナビゲーションを指示するページフローなどがあります。
図 5 は、マイクロソフトのサンプル ページフロー アプリケーションのデザイン画面を示しています。ワークフロー内の最上位の各アクティビティは、表示する画面 (Web ページまたはアプリケーション フォーム) および各画面のカスタム処理を定義します。ここには、許可される画面遷移も示されています。この明快な図と、同じ情報をコード化した場合の C# や Visual Basic の多大なコード行を比較してみれば、WF のメリットは一目瞭然です。
図 5 ユーザー インターフェイスのナビゲーション手順を制御するワークフロー (クリックすると拡大画像が表示されます)
WF と Windows Communication Foundation (WCF) は、サービス指向アプリケーションに最適な互いに相性の良いテクノロジです。ワークフローは特定の機能を実行するために呼び出されるサービスであり、WCF は使用するサービスを公開するためのテクノロジです。.NET Framework 3.5 には、WF と WCF を簡単に統合できるように SendActivity アクティビティおよび ReceiveActivity アクティビティが付属しています。WCF のアドレス、バインド、コントラクト ファサードの裏で宣言によって WF ワークフローを公開するには、ReceiveActivity を使用します。ワークフローは、要求の受信後に WCF 統合プラミングによって呼び出されるサービスの実装です。
実行時間の長いワークフローもこの方法で公開できます。1 つのワークフロー インスタンスで複数のサービス処理を公開し、実行とアイドルのシーケンスを複数処理することが可能です。WCF のバインド型 (BasicHttpContextBinding、WsHttpContextBinding、または NetTcpContextBinding) を使用して、クライアントから送信されたメッセージを特定の処理中のワークフロー インスタンスに関連付けます。
お察しのように、SendActivity を使用すると、ワークフローから外部サービスに送信する WCF クライアント接続を比較的簡単に構成できます。
.NET 統合テクノロジである WF プログラムは .NET コードによって利用可能なあらゆるライブラリやテクノロジと通信することができます。アプリケーション ロジックを WF で実装する場合、既存のテクノロジへの投資を破棄する必要はありません。むしろ、WF テクノロジの真価は、既存のテクノロジ資産の利用価値を高めることができるところにあります。
WF は統合テクノロジであると同時に、それ自体が 1 つの手段です。WF は、ワークフロー製品を採用したマイクロソフト製品の基盤テクノロジとして使用されています。対象マイクロソフト製品には、SharePoint、Dynamics 製品、今後リリースされる Office Communications Server および Office Project などがあります。このように広く採用されることにより、これらのテクノロジと WF アプリケーションとの統合がはるかに容易になりました。WF を全体の中の一部として利用することができます。

プラグ アンド プレイ
統合の可能性を高めるため、WF では拡張性を基本原則にしています。コア インフラストラクチャの主要な部分は構成可能であり、カスタム実装に置き換えることもできます。WF には、制御フロー、トランザクション セマンティクス、サービス通信などを実行する多くの組み込みアクティビティが付属していますが、これらのアクティビティは WF プログラムで使用する場合に必須のものではなく、WF のプログラミング コミュニティに提供されていない特殊な機能を使用しません。独自のカスタム アクティビティやサードパーティ ベンダー製のアクティビティでも、標準アクティビティに劣らない機能を実現できます。
ドメイン固有の動作に対応するアクション指向のアクティビティは、明らかにカスタム実装に適しています。融資を行うモーゲージ会社のワークフローには、洪水保険見積の請求、固定資産税の計算、加入申込者の信用情報の取得などのカスタム アクティビティを実装することが考えられます。これらのカスタム アクティビティは便利で必要なものですが、さらに興味深いケースとして、特定のドメイン関数ではなく、制御フローのカスタマイズに重点を置くカスタム複合アクティビティがあります。WF には、IfElseActivity や WhileActivity などのいくつかの標準的な制御フロー複合アクティビティが用意されています。ただし、WF では、構成可能な優先度に基づく複合的な子アクティビティの実行や、依存関係ネットワークを使用した実行順序の指定などの特殊なケースも実装できます (図 6 を参照)。
図 6 依存関係ネットワークの使用
[Designer( typeof( DependencyNetworkActivityDesigner ) )]
[ActivityValidator( typeof( DependencyNetworkActivityValidator ) )]
public partial class DependencyNetworkActivity : SequenceActivity {

  DependencyProperty.RegisterAttached( "RunAfter",
    typeof( string ), typeof( DependencyNetworkActivity ),
    new PropertyMetadata( DependencyPropertyOptions.Metadata ) );

  public static object GetRunAfter( object depObj ) {
    return ( (DependencyObject) depObj ).GetValue( RunAfterProperty );
  }

  public static void SetRunAfter( object depObj, object value ) {
    ( (DependencyObject) depObj ).SetValue( RunAfterProperty, value );
  }

  public DependencyNetworkActivity() {
    InitializeComponent();
  }

  private IList<Activity> _orderedList = null;
  private int _currentIndex = 0;

  protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec ) {
    if ( this.EnabledActivities.Count == 0 ) {
      return ActivityExecutionStatus.Closed;
    }
    else {
      _orderedList = DependencyNetworkActivityValidator.ComputeOrderedList( this );

      Debug.Assert( _orderedList != null );
      Debug.Assert( _orderedList.Count == this.EnabledActivities.Count );

      ScheduleNextChild( aec );

      return ActivityExecutionStatus.Executing;
    }
  }

  private void ScheduleNextChild( ActivityExecutionContext aec ) {
    Debug.Assert( aec != null );

    Debug.Assert( _orderedList != null );
    Debug.Assert( _currentIndex < _orderedList.Count );

    Activity child = _orderedList[ _currentIndex++ ];

    Debug.Assert( child != null );

    child.Closed += ChildDone;

    aec.ExecuteActivity( child );
  }

  private void ChildDone( object sender, ActivityExecutionStatusChangedEventArgs e ) {
    ActivityExecutionContext aec = ( sender as ActivityExecutionContext );
    e.Activity.Closed -= ChildDone;

    if ( _currentIndex < _orderedList.Count ) {
      ScheduleNextChild( aec );
    }
    else {
      aec.CloseActivity();
    }
  }
}
また、WF に付属する各種ランタイム サービスのほとんどは拡張可能または置き換え可能です。たとえば、他の記憶媒体上に独自のワークフロー永続化層を実装したり、組み込みの .NET ThreadPool の代わりにカスタム スレッド プールの実装を実行するワークフロー スケジューラを実装するなどの処理も自在に実現できます。このようなカスタム サービスの構成方法は他の WF サービスとまったく同じであるため、これは一貫性があり、簡潔なモデルです。
WF の究極の目標は、テクノロジの適用可能性を狭めるような厳格な要件を強いるのではなく、テクノロジやアプリケーション アーキテクチャが進化しても対応できる柔軟性を保つことです。
その他の WF リソース

ワークフローのヒントと秘訣

ワークフロー サービス (Windows Workflow Foundation および Windows Communication Foundation)

データのキャプチャとドキュメント作成のためのワークフローを構築する

WF でワークフロー モデルを読み込む

ドメイン モデリングとプログラム デザイン
WF でソフトウェアを構築する際のさらに大きな難題は、問題領域の要素をワークフローの要素に対応付けることです。主な問題は WF の制御フロー指向のプログラミング モデルにあります。従来の命令型またはオブジェクト指向のプログラミングと異なり、ワークフローのアトミック要素 (アクティビティ) は問題領域のエンティティを表すのではなく、操作または処理手順を表します。これは微妙ながら重要な相違であり、特に WF の初心者にとっては、ただでさえ簡単ではない作業 (ドメイン モデリングおよびプログラム設計) がさらに難しくなります。
WF のプログラミング モデル (アクティビティ、サービス、偶発的な実行など) のしくみを理解することと、機能仕様や数多くのユース ケースを把握して、より効率的で適切にファクタリングされた堅牢なワークフローを作成することとはまったく異なります。どのドメイン概念をアクティビティ、サービス、またはワークフロー全体にマップするのでしょうか。ワークフローを遷移するデータと遷移方法はどうしたらよいでしょうか。並列処理が可能な部分や、ワークフローをアイドル状態にしてユーザー入力を待機する必要がある部分はどこでしょうか。
紙面の都合上、これらの質問に十分な回答をすることはできませんが、経験上役立った一般的なガイドラインを次に示します。モデリングおよびデザインに関する以下の指針は、あくまで初歩的なものであり、万全の処方ではありません。完璧な WF デザインを実現する唯一の方法とは考えないでください。そんなはずがありませんから。
まず、モデリング フェーズは、プロジェクトに適した設計上の考慮事項を念頭に置いて、通常のユース ケース、アクター、ドメイン要素と同様に考えてください。有効な方法は何でも採用してください。WF 固有のしくみを考慮する必要はありません。
次に、アクター (Customer や BankManager など) およびドメイン要素 (PurchaseOrders、InsurancePolicies、AcmeWidgets) をオブジェクトとしてモデリングします。当然のことながら、アイテムごとにカプセル化するデータを十分に考慮してください。必要に応じてデータをプロパティで公開します。アクター メソッドをスタブアウトします。まだ実装はしません。
この時点で決めておくことがあります。それは、すべての重要な動作を 1 つまたは複数のワークフロー内で個別のアクティビティとして表す WF 指向のデザインにするか、それとも、もう少しターゲットを絞った形で WF を使用することにして、主要な部分を WF で実装し、その他の部分には従来の .NET コードを流用するかということです。この場合、全面的に WF を採用するか、まったく WF を使用しないかという二者択一的な考え方をするのではなく、WF をどの程度まで採用するかを熟慮してください。WF の強み (モジュール性、可視性、機能セット) は、効果の大きい部分 (複雑で絶えず変化するビジネス ロジック) と小さい部分 (ユーザー インターフェイスのレイアウトやデータ アクセス層) があります。
次に、各ドメイン要素に定義する操作を考えます。従来のオブジェクト指向プログラミングでは、操作を具象型のパブリック メソッドにマップします。WF では、"銀行口座から預金を引き出す"、"ショッピング カートにアイテムを追加する" などの操作を、1 つまたは複数の定義済みオブジェクト上で操作する個々のカスタム アクティビティにマップします。WF を使い続けているうちに、操作を WF アクティビティとして実装することが多くなり、従来のクラス メソッドの実装は減っていきます。
ユース ケースは、アクターが実行する操作を実行順に定義して、特定の機能を果たし、エラーが発生した場合の対処を行います。C# または Visual Basic では、Strategy パターンまたは Template Method パターンを使用して表し、アクターの型定義からメソッドを生成することができます。WF では、ユース ケースを、ドメイン要素のアクション アクティビティおよびユース ケース内の条件付きロジックを定義する他のアクティビティで構成されるカスタム複合アクティビティにマップすることをお勧めします。図 7 は、コード ベースのロジックが存在しないカスタム複合アクティビティ (AddCustomerWithAuditActivity) を示しています。すべての手順は一連のカスタム アクティビティの呼び出しとして実装されています。
図 7 コード ベースのロジックが存在しないカスタム複合アクティビティ
ユース ケースはカスタム複合アクティビティとして定義されているため、より大きいビジネス プロセス ("月末調整を行う"、"次年度予算を予測する" など) を、これらのカスタム複合アクティビティを集約するワークフローとして定義できます。アクターの動作は、必要なすべての入力を受け取り、適切なワークフローを起動し、必要に応じてワークフローにデータを渡して、結果を取得するクラス メソッドとして定義できます。データを渡すには、データ バインドまたはサービス呼び出しを使用します。
WF のドメイン モデリングおよびコード デザインは、命令型の .NET プログラミングの相当するタスクと類似していますが、大きな違いがいくつかあります。初めて WF プロジェクトを構築する場合に避けられない副産物として、学習曲線とリファクタリングの可能性を想定しておいてください。

その他の注意事項
このセクションでは、重要な点に関する注意事項をまとめます。
まず、CodeActivity の使用は最小限にしてください。アクティビティ間の適切なデータ フローを確保するための 1 回限りの "グルー" コードとしては便利ですが、それ以外の場合には、目的のタスクに固有のカスタム アクティビティで置き換えてください。
ワークフローを他のサブシステムと統合する場合、WF の機能はインターフェイスを使用して間接的に公開することをお勧めします。これにより疎結合が高まり、ワークフロー コードの使用およびテストが容易になります。また、時間を経て、WF ロジックの実装を変更することも容易になります。
カスタム アクティビティのバリデータを実装することも忘れないでください。WF アクティビティの構成モデルは、アクティビティに許可された構成と禁止された構成を確認するオプションの検証ロジックをサポートしています。この検証ロジックを実装するには、ActivityValidator をサブクラス化し、対象のアクティビティ型の属性を設定します。バリデータ実装の例については、付属のダウンロード コードを参照してください。
Visual Studio ウィザードで生成した定型ワークフローのホスティング コードはワークフローの暫定的な呼び出しやテストに便利ですが、それ以外にはあまり使い道はありません。真に優れた、役に立つワークフローはコマンド ラインからは実行できません。
ランタイムの敏捷性を高めるために、依存関係注入コンテナの使用を検討してください。WF カスタム サービスの構成については、StructureMap または Spring.NET を参照してください。
ワークフローで PolicyActivity を使用してビジネス ルールを実行する場合、RuleAction のサブクラスを作成し、有効な文法をドメイン固有のキーワードで拡張することを検討してください。これにより、作成したルールの表現力とわかりやすさが向上します。
ワークフローのホスティング環境をデザインする場合、アイドル状態のワークフローに対する外部システムからのサービス応答が継続的に環境にアクセスできるようにする必要があります。ワークフローはアイドル状態でも、ワークフロー ホストが動作していなければ、外部システムの応答は処理されず、ワークフローがアクティブになることはありません。
WF のような幅広いテクノロジの場合、ベスト プラクティスや主要原則を完全に網羅することは難しく、状況によって適用可能な度合いも異なります。このガイダンスは筆者および筆者の仕事仲間や学生の経験から導き出したものです。すべてとは言わないまでも、かなり幅広いケースに適用可能です。ここに掲載した諸事項が個別のニーズにどの程度適合するか (または適合しないか) を判断する際には、固有の問題領域や制約を検討してください。

Josh Lane は、ビジネス ルール エンジン ベンダー InRule Technology Inc. の主席アーキテクトであり、DevelopMentor の .NET インストラクタも務めています。現在、ジョージア州アトランタに住んでいます。


Visual Studio OBA ツール
相互運用 API 拡張機能を使用して OBA の開発を単純化する
Andrew Whitechapel、Phillip Hoff、および Vladimir Morozov
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事では、次の内容について説明します。
  • VSTO Power Tools と Office 相互運用 API
  • Outlook、Word、および Excel 拡張機能を使用する
  • 拡張機能を拡張する
  • Visual Basic で拡張機能を使用する
 この記事では、次のテクノロジを使用しています。
Office、VSTO Power Tools、LINQ
目次
拡張機能をインストールする
拡張機能を使用する
Word 拡張機能
Excel 拡張機能
Outlook 拡張機能
拡張機能の内部
拡張機能を拡張する
Visual Basic で拡張機能を使用する
まとめ
Microsoft Office クライアント アプリケーション オブジェクト モデルに対して C# でアプリケーションを開発するのは厄介な場合があります。なぜなら、これらのオブジェクト モデルでは、オプションのパラメータやパラメータ化されたプロパティなど、Visual Basic for Applications (VBA) と Visual Basic で内部的に処理され、C# では処理されない機能が使用されるからです。さらに、Office は柔軟に型指定されたパラメータと戻り値を多用しますが、これらは Microsoft .NET Framework などの厳密に型指定されたシステムでは嫌われています。
Microsoft Visual Studio Tools for the Office System (VSTO) Power Tools には、C# 開発者にこれらの機能のサポートを提供する Office 相互運用 API 拡張機能ライブラリのセットが含まれています。Visual Basic を使用している開発者にとっても、タイプセーフな方法で Microsoft Office Outlook アイテムをクエリする場合には特に、これらのライブラリが役立ちます。これらの拡張機能は、より堅牢なコードを作成するためだけでなく、開発者の生産性を上げるのにも役立ちます。
これらの Visual Studio Power Tools API 拡張機能を紹介するために、Power Tools を使用して Office アプリケーションを自動化するアプリケーションの開発手順を見ていきます。拡張機能で採用されている .NET Framework 3.0 機能と、さらにカスタマイズして使用するための拡張機能の拡張についても確認します。Visual Basic コードでの拡張機能の使用についても確認します。

拡張機能をインストールする
VSTO Power Tools は、MSDN に文書化されており、VSTO System Power Tools v1.0.0.0 ダウンロード ページから入手できます。Office Interop API 拡張機能は、VSTO_PTExtLibs.exe という名前のダウンロード ファイルにすべて含まれています。この自己解凍ファイルを実行すると、拡張機能のドキュメントが %PROGRAMFILES%\Microsoft VSTO Power Tools 1.0\Office Interop Extensions の下にインストールされます。拡張機能アセンブリ自体は、グローバル アセンブリ キャッシュ (GAC) にインストールされます。
これらの拡張機能の最初のリリースには、Office 2003 と 2007 Office system 両方の Microsoft Office Excel、InfoPath、Project、Outlook、PowerPoint、Visio、および Word のサポートが含まれています。すべての拡張機能には、Microsoft.Office.Interop.<application>.Extensions.dll というパターンに基づく一貫した名前付け規則があります。たとえば、Excel の拡張機能は Microsoft.Office.Interop.Excel.Extensions という名前空間内で定義され、Microsoft.Office.Interop.Excel.Extensions.dll という名前のアセンブリに組み込まれます。
拡張機能は、基本的に Office プライマリ相互運用機能アセンブリ (PIA) への非常に小さいラッパーであるため、拡張機能が動作するためには適切な Office PIA がインストールされている必要があります。PIA は、2007 Office system では既定でインストールされ、Office 2003 ではオプションでインストールされます。または、PIA 再頒布可能パッケージを使用できます。拡張機能は、VSTO を作成したのと同じチームによって作成されていますが、VSTO ソリューションを構築しているかどうかにかかわらず、Office を使用するすべてのソリューションで拡張機能を使用できます。

拡張機能を使用する
拡張機能を調べるために、Outlook、Excel、Word の 3 つの Office アプリケーションで機能を呼び出すソリューションを構築します。この記事のアプリケーションは VSTO ソリューションではありません。Office アプリケーションを自動化する単純な Windows Presentation Foundation (WPF) アプリケーションです。まず、Outlook から電子メール データをフェッチし、そのデータに対して単純な計算を実行します。続いて、結果を Excel に渡してグラフを生成し、最後にそのグラフを Word 文書に貼り付けて保存します。
この 3 つのタスクは、ユーザーがメイン ウィンドウのボタンを使用して個別に制御できるように分割されています (図 1 を参照)。これは明らかに人工的なメカニズムですが、各操作を分割して個別に検証できるようにするという重要な目的を果たします。また、すべてのタスクは、未加工の PIA オブジェクトまたは拡張機能ライブラリを使用して任意の順序で実行できます。
図 1 サンプル アプリケーションのメイン ウィンドウ
アプリケーションからの最後の出力は、Outlook データから生成された Excel グラフを含む HTML 形式の Word 文書です。対象の Outlook データセットは、選択した電子メール アイテムのセットのサイズと、それらが送受信された日時です。グラフには、送信から受信までの経過時間に対する個々のアイテムのサイズがプロットされます。この文書を図 2 に示します。
図 2 アプリケーションによって生成された HTML 文書 (クリックすると拡大画像が表示されます)
実は、このソリューションの操作の順序は、拡張機能の最も複雑な使用方法から開始し、拡張機能の最も単純な使用方法で終わります。そのため、逆の順序で主要手順を検証してから、すべてを正しい順序でまとめます。図 3 に、ソリューションの要件と、各要件を満たすために使用される拡張機能をまとめます。
図 3 拡張機能にマップしたソリューション機能
ソリューション機能 | Office アプリケーション | 対処される問題 | 拡張機能
選択したデータを電子メールの受信ボックスからフェッチします。 | Outlook | エラーを起こしやすい DASL クエリ文字列を使用しないで、フィルタ処理したクエリを作成します。 | LINQ-to-DASL。
電子メール データを使用してグラフを作成します。 | Excel | パラメータ化された get_Range プロパティを使用しないで Excel セル範囲を指定します。すべての負のセル値を検索し、適切な方法でそれらをゼロに設定します。 | パラメータ化されたプロパティ。パラメータ化されたプロパティを介して公開されているコレクション。LINQ-to-Objects。
グラフを文書に貼り付けて保存します。 | Word | 厳密に型指定されたパラメータを使用して、Office 機能を呼び出します。オプションのパラメータを渡さずに、Office 機能を呼び出します。参照によってパラメータを明示的に渡さずに、Word 機能を使用します。 | 厳密に型指定されたメソッド オーバーロード。厳密に型指定されたオプション パラメータ。null 許容型。オブジェクト初期化子。

Word 拡張機能
この記事のソリューション例では、Word を使用して実行する操作は単純です。いくつかのテキストを挿入してスタイルを設定してから、文書の終わりに移動し、クリップボードから Excel グラフを貼り付けます。Office オブジェクト モデルの厄介な機能の 1 つは、参照パラメータの使用であり、これは Word で特に普及しています。たとえば、Word の Document.Range メソッドは、フェッチする範囲の開始位置と終了位置という 2 つのパラメータを受け取ります。C# では、この両方のパラメータを参照によって明示的な変数として渡す必要があります。
次のコードに、未加工の PIA アプローチを使用して文書の開始位置に相当する範囲を取得する方法を示します。
object startPosition = 0;
object endPosition = 0;
Word.Range r = (Word.Range)doc.Range(ref startPosition,
  ref endPosition);
拡張機能で提供されている最も単純な機能の 1 つは、参照によって渡す必要のない 2 つの単純なパラメータを受け取る Range メソッドのオーバーロードです。拡張機能を使用すると、文書の開始位置を取得する呼び出しははるかに単純になります。この例では、拡張機能は参照によってパラメータを渡す必要性を回避し、厳密に型指定されたパラメータを渡すという要件も適用します。開始位置と終了位置は常に整数ですが、Word オブジェクト モデルでは、柔軟に型指定されたオブジェクトであることのみ指定されます。拡張機能では、メソッドのシグネチャに厳密な型指定が強制されるため、デザイン時の IntelliSense とコンパイル時の型チェックの利点がもたらされます。コードも 3 行から 1 行に削減されます。
Word.Range r = doc.Range(0, 0);
Office オブジェクト モデルの別の小さな不都合は、パラメータ化されたプロパティの使用です。Office では、パラメータを受け取るプロパティの公開が一般的です。COM サーバーから公開されたすべてのプロパティは、実際にはメソッドであることに注意してください。当然、メソッドは複数のパラメータを受け取ることができます。
C# では、メソッドはパラメータを受け取ることができますが、プロパティはできません。そのため、Office PIA は C# でパラメータ化されたプロパティを get_ メソッドと set_ メソッドのペアとして公開します。たとえば、スタイル プロパティを設定する PIA コードでは、オブジェクト パラメータを (この場合も参照によって) 渡す必要があります。次の未加工の PIA コードは、前のスニペットからの続きです。ここでは、現在の Range の後にテキストを挿入し、そのスタイルを設定します。
r.InsertAfter("Email Send Time vs Size");
r.InsertParagraphAfter();
r = (Word.Range)doc.Paragraphs[1].Range;
object style = Word.WdBuiltinStyle.wdStyleTitle;
r.set_Style(ref style);
拡張機能を使用して同じ処理を行うために、コードはさらにわかりやすくなっています。前と同様に、厳密に型指定されたパラメータと単純なメソッド呼び出しを使用します。中間変数と、set_ または get_ プレフィックスは不要です。
r.InsertAfter("Email Send Time vs Size");
r.InsertParagraphAfter();
r = (Word.Range)doc.Paragraphs[1].Range;
r.Style(Word.WdBuiltinStyle.wdStyleTitle);
Office オブジェクト モデルの多くのメソッドは、オプションのパラメータを受け取ります。Visual Basic ランタイムでは、不足しているパラメータに既定値を使用するように Office オブジェクトに指示する値が汎用パラメータに提供されるため、Visual Basic ではオプションのパラメータを指定する必要がありません。C# では、この操作は実行されません。これも、拡張機能が基礎となる操作を処理する領域であり、Visual Basic を使用する場合のエクスペリエンスに非常によく似たエクスペリエンスを C# 開発者に提供します。
たとえば、文書の終わりに移動してクリップボードの内容を貼り付けるために、未加工の PIA を使用しているコードでは、不足パラメータを System.Type.Missing として明示的に渡す必要があります。
object gotoItem = Word.WdGoToItem.wdGoToLine;
object gotoDirection = Word.WdGoToDirection.wdGoToLast;
object missing = Type.Missing;
r = r.GoTo(ref gotoItem, ref gotoDirection,
  ref missing, ref missing);
r.Paste();
一方、拡張機能を使用すると、C# 3.0 で導入されたオブジェクト初期化子機能を使用して、厳密に型指定された名前付きパラメータに指定する値を提供し、既定値を使用するパラメータを完全に省略できます。名前付きパラメータの使用も、C# 開発者に Visual Basic エクスペリエンスをもたらす機能です。
r = r.GoTo(
  new DocumentGoToArgs {
    What = Word.WdGoToItem.wdGoToLine,
    Which = Word.WdGoToDirection.wdGoToLast
  });
r.Paste();
拡張機能のオプション パラメータ機能の便利な点は、Word の Document.SaveAs のようなメソッドでより明らかになります。PIA コードを使用する場合は、16 個のパラメータをすべて指定する必要があります。この記事の例では、文書を HTML ファイルとして保存します。そのためには、Word で HTML ファイル形式のファイル名と列挙値を、どちらも柔軟に型指定されたオブジェクトとして指定する必要があります。残りの 14 個のパラメータについては注目しませんが、いずれにしてもこれらを渡す必要があり、すべてのパラメータを参照によって渡す必要があります。
object saveFormat = Word.WdSaveFormat.wdFormatHTML;
doc.SaveAs(ref fileName, ref saveFormat,
  ref missing, ref missing, ref missing, ref missing,
  ref missing, ref missing, ref missing, ref missing,
  ref missing, ref missing, ref missing, ref missing,
  ref missing, ref missing);
拡張メソッドでは、再び厳密な型指定を使用し、オプションのパラメータを完全に省略できます。
doc.SaveAs(fileName, Word.WdSaveFormat.wdFormatHTML);

Excel 拡張機能
ソリューション例では、Excel を使用してグラフをクリップボードに挿入し、その後で Word 文書に貼り付けられるようにします。まず、ブックの最初のワークシートを取得します。Office オブジェクト モデルでは、汎用オブジェクトとして入力された特定のオブジェクトを返すことが一般的です。たとえば、Sheets コレクションに Worksheet オブジェクトと Chart オブジェクトの両方が含まれることがあるため、コレクションの特定のアイテムを指定する場合、Excel PIA はこれを汎用オブジェクトとして返します。この汎用オブジェクトを、目的の Sheet 型にキャストする必要があります。
Excel.Worksheet sheet = (Excel.Worksheet)book.Sheets[1];
拡張機能の一般的なテーマは、全体を通して厳密な型指定を使用することであり、これは異種コレクションから返されるオブジェクトの型に拡張されます。開発者に、返されたオブジェクトに対して危険なキャストの実行を強制することはありません。開発者のコードの単純化とは別に、これは型チェックをランタイム時からコンパイル時に移動します。これにより、アプリケーションの堅牢性が高まり、テストのコストが削減されます。拡張機能では、整数またはシート名を使用してコレクションにインデックスを付けることができます。
//Excel.Worksheet sheet =
//  book.Sheets.Item<Excel.Worksheet>("Sheet1");
Excel.Worksheet sheet =
  book.Sheets.Item<Excel.Worksheet>(1);
例では、特定のセル範囲を取得し、データを挿入します。2 つのデータ列があります。一方は電子メール メッセージの送信から受信までの経過時間 (ミリ秒単位) 用で、もう一方は電子メールのサイズ (バイト単位) 用です。列見出しを挿入した後で、電子メール データをセルのグリッドに挿入します。
PIA アプローチを使用して、再び Range と Offset にパラメータ化されたプロパティを使用する必要があります。emailData 変数は文字列の配列であり、完全なソリューションでは Outlook の電子メール データを挿入します。
sheet.get_Range("A1", missing).Value2 = "mSec";
sheet.get_Range("B1", missing).Value2 = "Bytes";
Excel.Range firstCell = sheet.get_Range("A2", missing);
Excel.Range lastCell;

for (int r = 0; r < this.emailData.Length; r++) {
  String[] cellValues = this.emailData[r].Split(',');
  for (int c = 0; c < cellValues.Length; c++) {
    lastCell = (Excel.Range)firstCell.get_Offset(r, c);
    lastCell.Value2 = cellValues[c];
  }
}
拡張機能を使用すると、厳密に型指定された Range メソッドを使用できます。拡張機能ライブラリは、ほとんどの Office オブジェクト モデルを扱っていますが、すべてを扱っているわけではありません。そのため、この例では、Range プロパティに拡張機能を使用できますが、拡張機能を拡張しない限り (詳細は後述します)、PIA Offset プロパティを使用する必要があります。
sheet.Range("A1").Value2 = "mSec";
sheet.Range("B1").Value2 = "Bytes";
firstCell = sheet.Range("A2");
次に、シート内の負の値を検索し、それらをゼロに設定します。PIA を使用して、単純な方法で実行できます。
Excel.Range chartData = sheet.get_Range("A2", lastCell);
foreach (Excel.Range cell in chartData) {
  if (((double)cell.Value2) < 0) {
    cell.Value2 = 0;
  }
}
または、拡張機能を使用して、SQL 型のクエリを使用して実行することもできます。これは、拡張機能ライブラリの LINQ-to-Objects 機能を使用します。LINQ では、パフォーマンスが最適化され、結果セットの列挙を開始するまでクエリが実際には実行されない遅延実行モデルを使用します。一部のシナリオでは、アプリケーションのパフォーマンスが飛躍的に向上します。ただし、ToList または ToArray を呼び出してクエリを強制的に実行してから結果を操作する方が効率的な場合があります。たとえば、結果セットを複数回反復する場合などです。この例では、検索条件は非常に単純ですが (ゼロ未満のセル値)、検索条件が複雑になると SQL 型のクエリがより有用になるようすを見ることができます。
Excel.Range chartData = sheet.Range("A2", lastCell);
var cells = (from c in chartData.Items()
             where ((double)c.Value2) < 0
             select c).ToArray();
foreach (Excel.Range cell in cells) {
  cell.Value2 = 0;
}
次に、mSec 列と Bytes 列から単純な折れ線グラフを作成し、クリップボードにコピーします。PIA アプローチでは、柔軟な型指定、明示的なオプション パラメータ、およびパラメータ化されたプロパティを使用する必要があります。
Excel.Shape chart = sheet.Shapes.AddChart(
  Excel.XlChartType.xlLine,
  missing, missing, missing, missing);
chart.Select(missing);
excel.ActiveChart.SetSourceData(
  sheet.get_Range("A1", lastCell), missing);
excel.ActiveChart.ChartArea.Copy();
そのままでは、拡張機能はパラメータ化された Range プロパティでのみ役立ちます。Shapes.AddChart または Chart.SetSourceData のオーバーロードは提供しません。これも、拡張機能を拡張する必要のあるケースです。

Outlook 拡張機能
DAV Searching and Locating (DASL) クエリ言語を使用して、Outlook アイテムを検索、フィルタ処理、およびクエリできます。言語は SQL に似ており、かなり強力です。ただし、1 つの重大な欠点があります。文字列を作成し、これを Outlook Restrict または Find メソッドにフィルタとして渡す必要があることです。また、クエリは文字列であるため、デザイン時またはコンパイル時にはサポートできません。したがって、意図したクエリを文字列が本当に表しているかどうかは、実行するまでわかりません。
文字列が正しい形式のクエリであるかどうかも確認できません。DASL クエリは、プロパティ URN を使用します。これは直感的ではなく、通常はあいまいな 16 進値が含まれています。また、詳しく文書化されておらず、誤入力しやすいものです。クエリで文字列パターン、日付、または時間条件を使用する場合は、さらに誤入力しやすいワイルドカードと書式設定ルールにも注意する必要があります。明らかに、これらのクエリ文字列はすぐに複雑になり、作成と貼り付けの両方が難しく、生産性を低下させ、テストのコストを増やし、コードの保守を難しくします。
この記事の例では、図 4 に示す DASL フィルタを設定して、件名が "RE:" で始まり、過去 30 日間に受信された電子メール メッセージである (つまり、MessageClass が IPM.Note に基づいている) すべての受信ボックス アイテムを検索します。Windows デスクトップ サーチ (WDS) が使用可能な場合はこれを使用します。WDS ではより高速な検索を実行できるからです。WDS は、Windows Vista では既定でインストールされますが、Windows XP と Windows Server 2003 ではオプションです。DASL クエリは、日時比較を常に協定世界時 (UTC) で実行するため、比較文字列で日付のリテラルを使用する場合、比較ではその UTC 値を使用する必要があります。
図 4 電子メール メッセージを検索するための DASL フィルタ
string filter;
if (outlook.Session.DefaultStore.IsInstantSearchEnabled) {
  filter =
    @"@SQL=(((""http://schemas.microsoft.com/mapi/proptag/0x001a001e"""
    + "CI_STARTSWITH 'IPM.Note')"
    + @" AND (""urn:schemas:httpmail:subject"" CI_STARTSWITH 'RE:'))"
    + @" AND (""urn:schemas:httpmail:datereceived"" >= '"
    + (DateTime.Now.ToUniversalTime()
    - new TimeSpan(30, 0, 0, 0)).ToString("g") + @"'))";
}
else {
  filter =
    @"@SQL=(((""http://schemas.microsoft.com/mapi/proptag/0x001a001e"""
    + "LIKE 'IPM.Note%')"
    + @" AND (""urn:schemas:httpmail:subject"" LIKE 'RE:%'))"
    + @" AND (""urn:schemas:httpmail:datereceived"" >= '"
    + (DateTime.Now.ToUniversalTime()
    - new TimeSpan(30, 0, 0, 0)).ToString("g") + @"'))";
}

Outlook.Items folderItems = folder.Items;
Outlook.Items filteredItems = folderItems.Restrict(filter);
StringBuilder builder = new StringBuilder();

foreach (Outlook.MailItem item in filteredItems) {
  builder.AppendLine(String.Format("{0},{1},{2}",
    item.SentOn, item.ReceivedTime, item.Size));
}

CalculateElapsedTime(builder.ToString());
対象のアイテムをすべて取得した後で、電子メールの送信にかかった時間と電子メールのサイズから構成される各アイテムの文字列を作成します (文字列および DateTime 操作を単純に実行するカスタム メソッド CalculateElapsedTime を使用し、Office オブジェクト モデルは使用しません。このメソッドはここでは示しませんが、付属のソースコード ダウンロードで見ることができます)。
拡張機能を使用する場合は、文字列クエリを省略し、代わりに LINQ クエリを使用して同じ検索を実行できます。LINQ クエリは、DASL 文字列クエリのエクスペリエンスとは大きく異なるデザイン時の IntelliSense とコンパイル時の型および構文チェックの利点をすべてもたらします。1 つの制限に注意してください。拡張機能は Outlook アイテムの Size プロパティを公開しないため、Body プロパティの Length を代わりに使用します (これも、拡張機能の拡張に意味があるケースです)。
var filteredItems = (
  from item in folder.Items.AsQueryable<Mail>()
  where item.MessageClass.StartsWith("IPM.Note")
  && item.Subject.StartsWith("RE:")
  && item.DateReceived >=
  DateTime.Now.ToUniversalTime() - new TimeSpan(30, 0, 0, 0)
  select item).ToList();
StringBuilder builder = new StringBuilder();

foreach (Mail item in filteredItems) {
  builder.AppendLine(String.Format("{0},{1},{2}",
    item.Date, item.DateReceived, item.Body.Length));
}

CalculateElapsedTime(builder.ToString());

拡張機能の内部
これまで、拡張機能によって開発者のコーディング エクスペリエンスがどれだけ単純になり、エラーが少なくなるかを見てきました。次に、この単純性と堅牢性が見えない部分でどのように実現されるかを見てみましょう。まず、Word の Document.SaveAs メソッドについて考えます。このメソッドは 16 個のオプション パラメータを受け取りますが、ほとんどの場合に、指定する必要があるのはこれらのパラメータのうち非常にわずかな個数だけです (または、1 つも指定しないこともあります)。
拡張メソッドを作成するには、単純に、拡張するオブジェクトを最初の (または唯一の) パラメータとして受け取る静的メソッドを作成します。したがって、厳密に型指定された SaveAs メソッドに Document オブジェクトを提供するために、Document オブジェクトを最初のパラメータとして受け取る SaveAs というメソッドを作成できます。拡張メソッドのオーバーロードを提供して、各オーバーロードが開発者のコーディング エクスペリエンスの単純化に使用されるようにすることもできますが、実際の機能は最終的に基礎となる PIA Document オブジェクト自体に対して呼び出されます。
このようにして、図 5 の SaveAs メソッドのコード リストに示すように拡張機能が実装されます。Visual Studio でのデザイン時に、拡張メソッドは、開発者からは拡張したオブジェクトのメソッドのように見え、IntelliSense とオートコンプリートを通じて使用できます。IntelliSense は、図 6 に示すように拡張メソッドに "(extension)" というフラグを付けます。
図 5 オーバーロードされた SaveAs メソッド
public static void SaveAs(this Document doc) {
  SaveAs(doc, (string) null);
}

public static void SaveAs(this Document doc, string fileName) {
  SaveAs(doc, fileName, null);
}

public static void SaveAs(this Document doc, string fileName,
   WdSaveFormat? fileFormat) {
  SaveAs(doc, new DocumentSaveAsArgs {
    FileName = fileName, FileFormat = fileFormat });
}

public static void SaveAs(this Document doc, DocumentSaveAsArgs args) {
  doc.SaveAs(
    ref args.FileNameInternal,
    ref args.FileFormatInternal,
    ref args.LockCommentsInternal,
    ref args.PasswordInternal,
    ref args.AddToRecentFilesInternal,
    ref args.WritePasswordInternal,
    ref args.ReadOnlyRecommendedInternal,
    ref args.EmbedTrueTypeFontsInternal,
    ref args.SaveNativePictureFormatInternal,
    ref args.SaveFormsDataInternal,
    ref args.SaveAsAOCELetterInternal,
    ref args.EncodingInternal,
    ref args.InsertLineBreaksInternal,
    ref args.AllowSubstitutionsInternal,
    ref args.LineEndingInternal,
    ref args.AddBiDiMarksInternal);
}
図 6 Visual Studio での拡張メソッドの IntelliSense (クリックすると拡大画像が表示されます)
C# の null 許容型機能を利用することもできます。この機能では、厳密に型指定されたパラメータを null として渡すことができます (System.Type.Missing 値で柔軟に型指定されたオブジェクトを渡すことと同じではありません)。図 5 に示されている第 3 のオーバーロード SaveAs メソッドでは、WdSaveFormat パラメータが (? 構文を使用して宣言された) null 許容型として指定されています。これにより、このメソッドを呼び出して、このパラメータに null を渡すことができます。文書の保存の実際の動作は、DocumentSaveAsArgs オブジェクトを受け取るオーバーロードで最終的に行われます。前に示したように、これはオブジェクト初期化子を使用して、開発者が目的のパラメータの値を名前で提供でき、既定値を使用できるパラメータを省略できるようにします。
DocumentSaveAsArgs クラスは、SaveAsArgs 拡張クラスから派生します。引数値自体は FileNameInternal などのクラスの内部フィールドに格納され、これらは拡張メソッドによって直接使用されます。開発者は、内部フィールドではなく FileName などのパブリック プロパティを使用します。Word オブジェクト モデルのほとんどのメソッドにはパラメータを参照として渡すという要件があるため、このデザインが生まれました。他の Office アプリケーションの拡張機能ライブラリにある引数クラスでは、この追加の層を使用しません。
public abstract class SaveAsArgs : ExtensionArgs {
  internal object FileNameInternal = Type.Missing;

  public string FileName {
    get {
      return ToReference<string>(FileNameInternal);
    }
    set {
      FileNameInternal = ToObject(value);
    }
  }
  // code omitted for brevity.
}
SaveAsArgs は、ExtensionArgs から派生し、実際の型とそれらと同等のボックス化された型を変換するコード、および System.Type.Missing を処理するためのロジックを含みます。
protected T ToReference<T>(object obj) where T : class {
  return (obj != null && obj != Type.Missing) ? (T) obj : null;
}
パラメータ化されたプロパティも、拡張機能ライブラリのオーバーロードされた拡張メソッドによって処理されます。前と同様に、これらは基礎となる PIA オブジェクトを最終的な機能に使用します。たとえば、Excel.Range 拡張機能は、内部的に get_Range プロパティ メソッドを使用する 3 つのオーバーロードを提供します。
public static Range Range(this Worksheet worksheet, string name) {
  return worksheet.get_Range(name, Type.Missing);
}

public static Range Range(this Worksheet worksheet,
  string name1, string name2) {
  return worksheet.get_Range(name1, name2);
}

public static Range Range(this Worksheet worksheet, string range1, Range range2) {
  return worksheet.get_Range(range1, range2);
}
Outlook で LINQ-to-DASL クエリをサポートするために、拡張機能ライブラリには、クエリのデータ ソースを表すための OutlookItemSource という抽象基本クラスが用意されています。このクラスは、LINQ で使用される IQueryable<T> および IQueryProvider インターフェイスを実装します。拡張機能はまた、単一フォルダの Items コレクションに対するクエリおよび複数のフォルダのクエリを実行するために、それぞれ ItemsSource および ApplicationSource という派生クラスも提供します。
public abstract class OutlookItemSource<T> : IQueryProvider,    IQueryable<T> {
  public IQueryable<TElement> CreateQuery<TElement>    (Expression expression) {
    MethodCallExpression call = expression as MethodCallExpression;
    switch (call.Method.Name) {
      case "Where": return new
        OutlookItemSourceWhereHandler<TElement>(
          this, expression, _builder, _search);
      case "Select": return new
        OutlookItemSourceSelectHandler<TElement>(
          this, expression);
    }
  }
  // code omitted for brevity.
}
OutlookItemSource クラスでは、その他の内部拡張クラスを使用して、LINQ クエリから DASL クエリ文字列を構築します。たとえば、ParseMethodCallExpression メソッドは、Contains などの LINQ 式から (LIKE '%') などの DASL クエリ サブ文字列への変換を行います。
private static void ParseMethodCallExpression(MethodCallExpression call) {
  if (call.Method.DeclaringType == typeof(string)) {
    string format = null;
    switch (call.Method.Name) {
      case "Contains": format = @"({0} LIKE '%{1}%')"; break;
      case "StartsWith": format = @"({0} LIKE '{1}%')"; break;
      case "EndsWith": format = @"({0} LIKE '%{1}')"; break;
    }
    // code omitted for brevity.
  }
}
OutlookItemProperty 属性は、DASL プロパティと Outlook アイテム インターフェイス上のプロパティ間のマッピングを提供します。この属性は、OutlookItem クラスから公開されるパブリック プロパティで使用されます。こうして、拡張機能を使用すると、開発者はあいまいな URN/hex タグ文字列の使用を避けてターゲット プロパティを指定できます。
public class OutlookItem : IOutlookItemWrapper {
  [OutlookItemProperty(
    "http://schemas.microsoft.com/mapi/proptag/0x001a001e")]
  public virtual string MessageClass
    { get { return GetProperty("MessageClass") as string; } }
  // code omitted for brevity.
}

拡張機能を拡張する
拡張機能が内部的にどのように実装されているかを知ると、これらの拡張機能を簡単に拡張できます。Office オブジェクト モデル内の一部のオブジェクトとメソッドは、拡張機能では扱われておらず、場合によってはユーザー定義拡張機能を提供することも必要になります。
この記事のアプリケーション例では、最も明白なケースは Excel メソッド Range.get_Offset、Shapes.AddChart、Shape.Select、および Chart.SetSourceData です。これらすべてについて、拡張メソッドを簡単に提供できます。図 7 に示すように、これらは単純に、拡張する型のオブジェクトを最初のパラメータとして受け取る静的メソッドにする必要があることを思い出してください。
図 7 Excel メソッドを拡張する
public static class RangeExtensions {
  public static Excel.Range Offset(
    this Excel.Range cells, int rowOffset, int columnOffset) {
    return cells.get_Offset(rowOffset, columnOffset) as Excel.Range;
  }
}

public static class ShapesExtensions {
  public static Excel.Shape AddChart(
    this Excel.Shapes shapes, Excel.XlChartType chartType) {
    return shapes.AddChart(chartType,
      Type.Missing, Type.Missing, Type.Missing, Type.Missing);
  }
}

public static class ShapeExtensions {
  public static void Select(this Excel.Shape shape) {
    shape.Select(Type.Missing);
  }
}

public static class ChartExtensions {
  public static void SetSourceData(this Excel.Chart chart,
    Excel.Range range) {
    chart.SetSourceData(range, Type.Missing);
  }
}
Shapes.AddChart メソッドを例として考えると、この拡張メソッドは、PIA AddChart メソッド (または任意の拡張機能オーバーロード) からの異なる数の引数を持つことで動作します。同じ引数を持つが、単に厳密な型指定である拡張メソッドを提供しようとした場合、コンパイラはそれを呼び出しません (引数の型の厳密さに関係なくネイティブ メソッドを優先するため)。Word では参照による引数を使用するため、これを Word で実行できます。Word メソッドの値による拡張機能を提供することで、コンパイラは常に 2 つを区別できます。
Excel の拡張機能ライブラリには、実際にはコンパイラがこの方法では到達できないメソッドがいくつか含まれています。コンパイラが使用しないメソッドを提供することは奇妙に思えますが、これらは IntelliSense には表示されるため、ユーザーが引数の実際の型を識別するのに役立ちます。また、開発者は必要に応じてこれらのメソッドを通常の静的メソッド構文で使用できます。これらのカスタム拡張機能を使用して、アプリケーション コードを簡略化できます。
//lastCell = (Excel.Range)firstCell.get_Offset(r, c);
lastCell = (Excel.Range)firstCell.Offset(r, c);

//chart = sheet.Shapes.AddChart(
//    Excel.XlChartType.xlLine, missing, missing, missing, missing);
chart = sheet.Shapes.AddChart(Excel.XlChartType.xlLine);
//chart.Select(missing);
chart.Select();
//excel.ActiveChart.SetSourceData(sheet.Range("A1", lastCell), missing);
excel.ActiveChart.SetSourceData(sheet.Range("A1", lastCell));
開発者が追加の拡張メソッドを提供できるようにすることに加えて、拡張機能ライブラリ自体もある程度の拡張性をサポートします。たとえば、LINQ-to-DASL 機能では、プラグ可能なロギングがサポートされます (図 8 を参照)。ItemsSource クラスは、System.IO.TextWriter から派生した任意のオブジェクトに設定できる Log プロパティを提供します。例では、デバッグ ウィンドウに出力する単純なクラスを作成できます。このクラスは、拡張機能コードが LINQ クエリからの DASL クエリ文字列の作成を完了したときに内部的に使用されます。この文字列が、デバッグ ウィンドウへの出力になります。
図 8 LINQ-to-DASL でのロギング
// Provide a logging class for our LINQ-to-DASL queries.
internal class DebuggerWriter : TextWriter {
  public override Encoding Encoding {
    get { throw new NotImplementedException(); }
  }

  public override void WriteLine(string value) {
    Debug.WriteLine(value);
  }
}

// Subclass the extensions Mail class so that we can expose a Size
// property that maps to the real Outlook size property.
internal class MailEx : Mail {
  [OutlookItemProperty("http://schemas.microsoft.com/mapi/proptag/0x0E080003")]
  public int Size { get { return Item.Size; } }
}

// Create the ItemsSource manually (this is
// what Items.AsQueryable() does implicitly).
var source = new ItemsSource<MailEx>(folder.Items);

// Set the Log property of the ItemsSource to a TextWriter.
// It will be given the DASL query string immediately before
// the query is executed in Outlook.
source.Log = new DebuggerWriter();

//var filteredItems = (from item in folder.Items.AsQueryable<Mail>()
var filteredItems = (from item in source
                     where item.MessageClass.StartsWith("IPM.Note")
                     && item.Subject.StartsWith("RE:")
                     && item.DateReceived >=
                     DateTime.Now.ToUniversalTime() -                        new TimeSpan(30, 0, 0, 0)
                     select item).ToList();

//foreach (Mail item in filteredItems)
foreach (MailEx item in filteredItems) {
  builder.AppendLine(String.Format("{0},{1},{2}",
    //item.Date, item.DateReceived, item.Body.Length));
    item.Date, item.DateReceived, item.Size));
}
同時に、拡張機能 Mail クラスをサブクラス化して、OutlookItemProperty 属性を使用してマッピングを確立することにより、基礎となる Outlook Size プロパティのパブリック プロパティを公開できます。以前に、既定では Size が拡張機能によって公開されないため、Body.Length を対処法として使用したことを思い出してください。
これらの拡張機能の追加の拡張によって、アプリケーション例が完成します。LINQ-to-DASL を使用してフィルタ処理された Outlook メール アイテム データをフェッチし、そのデータを Excel に渡してグラフを生成し、そのグラフを Word 文書にコピーし、最終的に HTML ファイルとして保存します。
Outlook 2007 では、Table オブジェクトが導入されました。このオブジェクトは、フォルダ アイテムを列挙するための、パフォーマンスが最適化されたメカニズムです。これにより、プロパティをテーブル列として指定し、アイテムをテーブルの行としてフィルタ処理および並べ替えできます。Outlook 2007 以降では、アイテムのコレクションを操作する従来の方法に優先して、Table メカニズムを使用する必要があります。
テーブルを取得するには、Restrict または Find メソッドで使用するのと同じフィルタ文字列を使用しますが、これは GetTable メソッドと共に使用します。図 9 に示すコードから、既存のバージョンの LINQ-to-DASL 拡張機能を Outlook Table と共に使用する利点はほとんどないことが明らかになります。依然としてクエリ文字列を使用して Table を取得する必要があり、Table フィルタ処理はテーブル列を指定して行われるためです。拡張機能ライブラリの LINQ-to-DASL アプローチは、Outlook 2003 と Outlook 2007 のいずれでも使用できますが、Outlook 2007 のみを対象としている場合は、データ セットが大きい場合には特に Table アプローチの方がパフォーマンスがよくなります。このアプローチのコード例を図 9 に示します。
図 9 Outlook 2007 で Table を使用する
//Outlook.Items folderItems = folder.Items;
//Outlook.Items filteredItems = folderItems.Restrict(filter);
//foreach (Outlook.MailItem item in filteredItems) {
//  builder.AppendLine(String.Format("{0},{1},{2}",
//    item.SentOn, item.ReceivedTime, item.Size));
//}

// Get a table of mail items, remove the default column set,
// and add the specific columns we're interested in instead.
table = folder.GetTable(filter, Outlook.OlTableContents.olUserItems);
table.Columns.RemoveAll();
table.Columns.Add("SentOn");
table.Columns.Add("ReceivedTime");
table.Columns.Add("Size");

// Iterate the table rows.
while (!table.EndOfTable) {
  Outlook.Row nextRow = table.GetNextRow();
  builder.AppendLine(String.Format("{0},{1},{2}",
    nextRow["SentOn"], nextRow["ReceivedTime"], nextRow["Size"]));
}
VSTO リソース
VSTO を使用した独自の Office ベース アプリケーションの作成の詳細については、MSDN Magazine のアーカイブにある次の記事を参照してください。

VSTO 3.0: Visual Studio 2008 を使用して Office Business Application を開発する (Steve Fox)

VSTO: WPF、WCF、および LINQ を使用して Office ベースのソリューションを構築する (Andrew Whitechapel)

Office アプリケーション: VBA コードを VSTO で拡張する (Paul Stubbs、Kathleen McGrath)

Visual Basic で拡張機能を使用する
拡張機能の主な利点は、C# 開発者に、Visual Basic に似たコーディング エクスペリエンスを提供することです。ただし、いくつかの拡張機能 (特に、LINQ-to-DASL 拡張機能) は、Visual Basic (または、その他のマネージ言語) で作業している開発者にとっても利点があります。たとえば、厳密に型指定された LINQ クエリを文字列ベースの DASL クエリの代わりに使用することは、どの言語でも役立ちます。図 10 に、Outlook クエリ操作の Visual Basic バージョンを示します。
図 10 Visual Basic における Outlook クエリ
Dim source As New ItemsSource(Of MailEx)(folder.Items)
Dim builder As New StringBuilder()

Dim filteredItems = ( _
  From item In source _
  Where item.MessageClass.StartsWith("IPM.Note") _
  AndAlso item.Subject.StartsWith("RE:") _
  AndAlso item.DateReceived >= _
  (DateTime.Now.ToUniversalTime() - New TimeSpan(30, 0, 0, 0)) _
  Select item).ToList()

For Each filteredItem In filteredItems
  builder.AppendLine(String.Format("{0},{1},{2}", _
  filteredItem.Date, filteredItem.DateReceived, filteredItem.Size))
Next
MessageBox.Show(builder.ToString())
C# と Visual Basic が式ツリーを生成する方法には違いが 1 つあります。C# では、MessageClass のマップされたプロパティを定義する基本型 (この記事の例では OutlookItem) を使用します。一方、Visual Basic では、プロパティ マッピングで属性が付けられない MessageClass のオーバーライドを定義する大多数の派生型 (Mail) が使用されます。このため、プロパティ マッピングで属性が付けられる MailEx クラス (図 11 を参照) で MessageClass の別のオーバーライドを定義する必要があります (これも、将来のリリースで改良される可能性のある場所です)。
図 11 Visual Basic の MailEx
Friend Class MailEx
  Inherits Mail

  <OutlookItemProperty(     "http://schemas.microsoft.com/mapi/proptag/0x0E080003")> _
  Public ReadOnly Property Size() As Integer
    Get
      Return MyBase.Item.Size
    End Get
  End Property

  <OutlookItemProperty(     "http://schemas.microsoft.com/mapi/proptag/0x001a001e")> _
  Public Overrides ReadOnly Property MessageClass() As String
    Get
      Return MyBase.Item.MessageClass
    End Get
  End Property
End Class

まとめ
この記事では、C# 開発者が Office 相互運用 API 拡張機能を使用して Office 開発をどのように効率化できるかを示しました。拡張機能では、柔軟に型指定された Office オブジェクト モデルの上に、厳密に型指定された薄い層が提供されます。これにより、コードのエラーが減り、堅牢性が高まり、テストとメンテナンスのコストを大幅に削減できます。また、カスタム シナリオで拡張機能を拡張する方法と、Visual Basic での使用方法についても説明しました。

Andrew Whitechapel は、Visual Studio Business Applications チームのプログラム マネージャであり、Visual Studio での新しい Office 開発機能の設計を担当しています。

Phillip Hoff は、Visual Studio Business Applications チームのソフトウェア開発エンジニアであり、最初の Office 相互運用 API 拡張機能ライブラリの作成者の 1 人です。現在は、Visual Studio との SharePoint ツール統合について作業しています。

Vladimir Morozov は、Visual Studio Business Applications チームの開発者です。Phillip Hoff と共に、Office 相互運用 API 拡張機能ライブラリの構想と作成に従事しました。現在は、Visual Studio 用の SharePoint ツールについて作業しています。


SOA データ アクセス
LINQ to SQL と Entity Framework による柔軟なデータ アクセス
Anthony Sneed
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事では、次の内容について説明します。
  • LINQ to SQL
  • ADO.NET Entity Framework、サービス指向アーキテクチャ
  • データ転送オブジェクト
  • 保存、更新、および変更の追跡
 この記事では、次のテクノロジを使用しています。
LINQ to SQL、ADO.NET Entity Framework、および SOA
目次
データ アクセス層を作成する
個々のオブジェクトを保存する
サービスの境界を越えて変更を追跡する
クライアントを構成する
まとめ
LINQ to SQL と ADO.NET Entity Framework のリリースにより、リレーショナル データの世界とオブジェクト指向プログラミングの世界との間に存在する "インピーダンス ミスマッチ" (不整合) を減らすためにデザインされた製品が、マイクロソフトから開発者に 2 つ提供されることになりました。これらの各製品は、オブジェクトの保存を実現するために必要となる多くのプラミング コードの記述を不要にします。ただし、これらのオブジェクト リレーショナル マッピング (ORM) テクノロジをいくつかのサービス指向アプリケーションに導入する場合、アプリケーション開発者はまったく新しい一連の課題に直面します。
たとえば、必要に応じて ORM プロバイダを別のプロバイダに切り替えられるように、オブジェクトの保存をアプリケーションの他の部分から抽象化するためのデータ アクセス層 (DAL) を作成するには、どのようにすればよいでしょうか。また、クライアント側で LINQ to SQL または Entity Framework を使用せずに、エンティティへの変更を追跡するには、どのような方法があるでしょうか。さらに、複数のエンティティに対する挿入、更新、および削除を、サービスへの 1 回のトリップだけを伴う 1 回のトランザクションで実行するには、どうしたらよいでしょうか。
この記事では、これらの問題に対処するためのロードマップを提示し、いくつかの提案を行います。最初に、Northwind サンプル データベースからの注文を処理する DAL を作成します。この DAL は、2 つの実装によるインターフェイスを利用します。1 つは LINQ to SQL、もう 1 つは Entity Framework を使用したインターフェイスです (図 1 を参照)。LINQ to SQL および Entity Framework はいずれも、データベース スキーマに基づいてエンティティを生成するツールを備えていますが、DAL ではこれらのエンティティを使用する代わりに、自身の保存について基本的に意識することのない、データ転送オブジェクト (DTO) だけを公開します。
図 1 注文処理アプリケーションのサービス指向アーキテクチャ (クリックすると拡大画像が表示されます)
LINQ to SQL と Entity Framework ではそれぞれ、新しいエンティティを作成するか既存のエンティティを更新するかに応じて、切り離されたエンティティを異なる方法で保存します。したがって、適切な API を呼び出すためには、エンティティの状態を事前に知っておく必要があります。たとえば、注文明細と共に注文を受け付ける UpdateOrder メソッドがある場合、作成、更新、および削除された注文明細を受け付けるための 3 つのパラメータを UpdateOrder メソッドに追加できます。ただし、それを行うと詳細なプラミング コードによってメソッドのシグネチャが複雑化する傾向があります。
その代わりに、変更状態の情報を帯域外で (たとえば、SOAP または HTTP ヘッダーなどで) 渡すこともできますが、その場合は通信プロトコルに変更追跡が付加される可能性があります。この記事では、別のアプローチとして、変更状態を各エンティティのデータ コントラクトの一部に含める方法を紹介します。
[DataContract]
public enum TrackingInfo
{
    [EnumMember]
    Unchanged,
    [EnumMember]
    Created,
    [EnumMember]
    Updated,
    [EnumMember]
    Deleted
}
各 DTO では、他のプロパティに加えて、TrackingInfo 列挙型を使用した TrackingState プロパティを使用します。必要なのは、このデータ コントラクトについて両者が合意することだけです。
各 DTO に TrackingState プロパティを追加した場合でも、どのオブジェクトを作成、更新、または削除したかを追跡して、TrackingState プロパティを対応する値に設定する作業については、LINQ to SQL や Entity Framework に関する知識を持たない、クライアントに頼る必要があります (図 1 を参照)。そこで、このタスクを実行するために、汎用の変更追跡コレクションを作成しました。このコレクションには 2 つの種類があります。1 つは ObservableCollection<T> を Windows Presentation Foundation (WPF) アプリケーション用に拡張するものであり、もう 1 つは BindingList<T> を Windows Forms アプリケーション用に拡張するものです。各キャッシュではコレクションから取り除く前に項目を削除しています。また、挿入、更新、削除された項目だけを返す GetChanges メソッドを備えています。

データ アクセス層を作成する
DAL は、オブジェクトの保存に関する詳細部分からアプリケーションの他の部分を分離するのに役立ちます。この理由により、DAL を通じて公開されるオブジェクトでは、特定のデータ アクセス テクノロジに関する要素を残さず除外することが重要です。LINQ to SQL と Entity Framework ではいずれも、コマンド ライン ツールと Visual Studio デザイナの両方を使用してエンティティを作成できますが、コードで生成されたそれらのエンティティには、本来は存在しないアーティファクトがいくつか含まれます。
たとえば、LINQ to SQL では、関連するオブジェクトの遅延読み込みをサポートするために EntityRef<T> および EntitySet<T> 型のコレクションを使用してデータの関係を表します。一方、Entity Framework では、EntityReference<T> および EntityCollection<T> を使用してナビゲーション プロパティを表します。同様に、これらのエンティティには、検証 (部分メソッドを使用) やデータ バインド (INotifyPropertyChanged を使用) など、クライアント/サーバー シナリオをサポートするためのコード要素が含まれており、これらは、Windows Communication Foundation (WCF) サービスとの通信にだけ使用する場合には不要なものです。
LINQ to SQL と Entity Framework はいずれも DataContractSerializer を使用した (DataContract および DataMember 属性でマークすることにより) エンティティのシリアル化をサポートしているので、それをサービス操作で使用したい誘惑にかられる場合があります (結果として、余計な荷物が持ち込まれますが)。結局のところ、LINQ to SQL や Entity Framework で使用されているようなコレクションの種類は、WCF サービス参照を設定する際、クライアント側では単純な配列として扱われます。
ただし、LINQ to SQL の DataContext の SerializationMode を None から Unidirectional に設定するときには、1 対多関係の "多" の側だけが DataMember 属性でマークされることを考慮する必要があります。つまり、Customer および Order_Details プロパティを持つ Order エンティティがある場合、Order_Details は DataMember でマークされてデータ コントラクトに含まれますが、Customer はマークされません。一方、Entity Framework では、必要とするよりも多くのフィールドがデータ コントラクトに含まれ、EntityKey、EntityKeyMember、EntityObject、EntityReference など、Entity Framework 固有の機能に対してクライアント側でクラスが生成されます。
これらの理由により、LINQ to SQL または Entity Framework のツールで生成されたエンティティを DAL を通じて公開することは避け、その代わりに、サービスの境界を越えてデータを伝達することだけを目的とした単純な DTO を返す必要があります。たとえば、Order DTO (図 2 を参照) には、プロパティだけが含まれ、メソッドは含まれません。すべての DTO は DataTransferObjects 名前空間で定義されることにより、ツールで生成される同名のクラスから区別されています。
図 2 Northwind からの注文を表す DTO
namespace DataTransferObjects
{
    [DataContract]
    public class Order
    {
        [DataMember]
        public int OrderID { get; set; }

        [DataMember]
        public string CustomerID { get; set; }

        [DataMember]
        public string CustomerName { get; set; }

        [DataMember]
        public DateTime? OrderDate { get; set; }

        [DataMember]
        public List<OrderDetail> OrderDetails { get; set; }

        [DataMember]
        public byte[] Updated { get; set; }

        [DataMember]
        public TrackingInfo TrackingState { get; set; }
    }
}
柔軟性のある DAL では、基になる保存テクノロジへの変更から注文サービスをバッファリングする必要があります。たとえば、データ ストア用に SQL Server を選択し、また、オブジェクト モデルがデータベース スキーマとよく似ているので LINQ to SQL を使用してオブジェクトを保存することに決めたとしましょう。しかし後になって、LINQ to SQL の保存ロジックを記述してから、Oracle など別のデータベース システムを使用することを決めたり、または、概念と論理的なスキーマを分離するために Entity Framework のマッピング機能の利用が必要になったりする場合もあります。また、新たに利用したいデータ アクセス テクノロジが出現するかもしれません。プラグイン アーキテクチャに基づいて柔軟な DAL をデザインしておけば、app.config 内のエントリに基づいてプロバイダをその場で切り替えることが可能になります。
この柔軟性を実現するために、IDataProvider というインターフェイスを作成しました。このインターフェイスには、顧客の注文を取得して更新するためのメソッドに加え、顧客および製品に関する補助的な情報を取得するメソッドがあります (図 3 を参照)。サンプル アプリケーションには、共に IDataProvider を実装する 2 つのクラスが含まれています。LINQ to SQL を使用する SqlDataProvider と、Entity Framework および LINQ to Entities を使用する EntityDataProvider です (図 1 を参照)。DAL の app.config ファイルの "DataProvider" 設定には単に完全修飾クラス名を入力します。また、Assembly クラスの CreateInstance メソッドを使用してクラスのインスタンスを作成し、それを IDataProvider にキャストします。注文サービスは、どのクラスが IDataProvider を実装するかについてはまったく意識しないため、ユーザーは選択したどのデータ プロバイダでも使用できます。
IDataProvider provider =
    Assembly.GetExecutingAssembly()
    .CreateInstance(Settings.Default.DataProvider, true)
    as IDataProvider;
図 3 インターフェイスで DAL を分離する
namespace DataAccessLayer
{
    // Alias the namespace containing Data Transfer Objects
    using DTO = DataTransferObjects;

    public interface IDataProvider
    {
        // Retrieve Customers, Products, Orders
        DTO.Customer[] GetCustomers();
        DTO.Product[] GetProducts();
        DTO.Order[] GetCustomerOrders(string customerID);

        // CRUD operations for Order
        DTO.Order GetOrder(int orderID);
        DTO.Order CreateOrder(DTO.Order order);
        DTO.Order UpdateOrder(DTO.Order order);
        void DeleteOrder(DTO.Order order);
    }
}
既定では、LINQ to SQL および Entity Framework はいずれも、プロジェクトと同じ名前空間内でエンティティを生成します。ただし、新しい "LINQ to SQL Classes" 項目を L2S という名前のプロジェクト フォルダに配置することで、このフォルダ名が入れ子になった名前空間として使用され、LINQ to SQL のエンティティを DTO エンティティから区別できます。同様に、新しい "ADO.NET Entity Data Model" を L2E プロジェクト フォルダに配置することで、Entity Framework エンティティは入れ子になった L2E 名前空間に配置されます。または、LINQ to SQL のプロパティ ウィンドウおよび Entity Framework のビジュアル コンポーネント デザイナで名前空間を指定することもできます。
ここでは、LINQ to SQL と Entity Framework の両方のエンティティを、クエリの結果を DTO に変換することで生成することにします。また、ツールで生成されたエンティティをいっさい使用せずに、XML ファイルを使用して DTO をマッピングできる可能性もあります。Entity Framework では既に XML マッピング ファイルが使用されていますが、Entity Framework の最初のバージョンでは、POCO (Plain Old C# Object: 単純な古い C# オブジェクト) を使用して純粋な PI (Persistence Ignorance: 保存を意識しないこと) を実現するのはまだ不可能です。バージョン 1 ではこの目標に近づくことができますが、エンティティは IEntityWithRelationships および IEntityWithChangeTracking の 2 つのインターフェイスを実装する必要があるため、IPOCO (POCO + インターフェイス) が残されます。Entity Framework の後続のバージョンでは、POCO および PI に対するサポートの向上が期待されます。
LINQ to SQL では、XML マッピング ファイルを使用した POCO からデータベース スキーマへのマッピングについて、優れたサポートが用意されています。ただし、L2S.Order または L2E.Order から DTO.Order への投影を行うと、さらに高いレベルの柔軟性が得られます。たとえば、図 4 は、SqlDataProvider の GetOrder メソッドを示しています。このメソッドは、DTO.Order の CustomerName プロパティを L2S.Order の Customer プロパティの ContactName プロパティから設定し、結果として Customer と Order の間の関係をフラット化します。同じアプローチを使って、DTO.OrderDetail の ProductName プロパティを L2S.Order_Detail の Product プロパティから設定します。いずれの場合も、LINQ to SQL のエンティティを一括読み込みするように DataContext の LoadOptions プロパティを構成する必要があります。
図 4 GetOrder メソッドで LINQ to SQL フレームワークを使用して DTO.Order を返す
namespace DataAccessLayer
{
    using DataAccessLayer.L2S;
    using DTO = DataTransferObjects;

    public class SqlDataProvider : IDataProvider
    {
        public DTO.Order GetOrder(int orderID)
        {
            using (NorthwindDataContext db = new NorthwindDataContext
                (Settings.Default.NorthwindSqlConnection))
            {
                // Set load options
                DataLoadOptions opts = new DataLoadOptions();
                opts.LoadWith<Order>(o => o.Customer);
                opts.LoadWith<Order>(o => o.Order_Details);
                opts.LoadWith<Order_Detail>(od => od.Product);
                db.LoadOptions = opts;

                // Create a DTO.Order from the L2S.Order
                DTO.Order order =
                    (from o in db.Orders
                     where o.OrderID == orderID
                     select new DTO.Order
                     {
                         OrderID = o.OrderID,
                         CustomerID = o.CustomerID,
                         CustomerName = o.Customer.ContactName,
                         OrderDate = o.OrderDate,
                         Updated = o.Updated.ToArray(),
                         OrderDetails =
                            (from od in o.Order_Details
                             select new DTO.OrderDetail
                             {
                                 OrderID = od.OrderID,
                                 ProductID = od.ProductID,
                                 ProductName = od.Product.ProductName,
                                 Quantity = od.Quantity,
                                 UnitPrice = od.UnitPrice,
                                 Updated = od.Updated.ToArray()
                             }).ToList()
                     }).Single();
                return order;
            }
        }
    }
}
ここで、EntityDataProvider の GetOrder メソッドを見てみましょう。このメソッドは、LINQ to Entities を使用し、L2E.Order に基づいて DTO.Order を返します (図 5 を参照)。Order_Details および Order_Details.Product プロパティの一括読み込みに使用されている Include 演算子に注意してください (LINQ の演算子の詳細については、「データ ポイント」コラムの「LINQ の標準クエリ演算子」を参照してください)。Entity Framework には、LINQ to SQL のようなすぐに使用できる遅延読み込み機能は用意されていないため、クエリの結果に含める関連エンティティを明示的に示す必要があります。指定された注文を取得した後で、単純にそれを使用して新しい DTO.Order を作成できます。
図 5 GetOrder メソッドで LINQ to Entities を使用して DTO.Order を返す
namespace DataAccessLayer
{
    using DataAccessLayer.L2E;
    using DTO = DataTransferObjects;

    public class EntityDataProvider : IDataProvider
    {
        public DTO.Order GetOrder(int orderID)
        {
            using (NorthwindEntities db = new NorthwindEntities
                (Settings.Default.NorthwindEntityConnection))
            {
                // Get the specified L2E.Order
                Order nwOrder =
                    (from o in db.OrderSet
                        .Include("Order_Details")
                        .Include("Order_Details.Product")
                     where o.OrderID == orderID
                     select o).First();

                // Create a DTO.Order from the L2E.Order
                DTO.Order order = new DTO.Order
                {
                    CustomerID = nwOrder.CustomerID,
                    CustomerName = (from c in db.CustomerSet
                                 where c.CustomerID == nwOrder.CustomerID
                                    select c).First().ContactName,
                    OrderID = nwOrder.OrderID,
                    OrderDate = nwOrder.OrderDate,
                    Updated = nwOrder.Updated,
                    OrderDetails =
                        (from od in nwOrder.Order_Details
                         select new DTO.OrderDetail
                         {
                             OrderID = od.OrderID,
                             ProductID = od.ProductID,
                             ProductName = od.Product.ProductName,
                             Quantity = od.Quantity,
                             UnitPrice = od.UnitPrice,
                             Updated = od.Updated
                         }).ToList()
                };
                return order;
            }
        }
    }
}

個々のオブジェクトを保存する
LINQ to SQL と Entity Framework では、切り離されたエンティティの保存に対して異なるアプローチを採用しています。たとえば、1 つまたは複数の注文明細を持つ新しい注文を作成するとします。図 6 に示されるように、LINQ to SQL では少なくとも 2 行のコードが必要となります。InsertOnSubmit を呼び出して注文を挿入するコードと、InsertAllOnSubmit を使用して注文の明細項目を挿入するコードです。一方、Entity Framework で必要なのは、ObjectContext の OrderSet に注文を追加することだけです。これにより、オブジェクト グラフ全体が自動的に追加されます (図 7 を参照)。LINQ to SQL と Entity Framework はいずれも、最初に親注文を挿入し、OrderID (データベース内の ID 列) の値を取得してから、子注文明細を挿入することで、適切な処理を実現しています。
図 6 LINQ to SQL を使用して注文を作成する
public DTO.Order CreateOrder(DTO.Order order)
{
    // Insert new order
    using (NorthwindDataContext db = new NorthwindDataContext
        (Settings.Default.NorthwindSqlConnection))
    {
        // Create an L2S.Order
        Order nwOrder = new Order
        {
            OrderID = order.OrderID,
            CustomerID = order.CustomerID,
            OrderDate = order.OrderDate,
            Updated = order.Updated
        };

        // Add order details
        nwOrder.Order_Details.AddRange(
            from od in order.OrderDetails
            select new Order_Detail
            {
                OrderID = od.OrderID,
                ProductID = od.ProductID,
                Quantity = od.Quantity,
                UnitPrice = od.UnitPrice,
                Updated = od.Updated
            });

        // Insert order and order details separately
        db.Orders.InsertOnSubmit(nwOrder);
        db.Order_Details.InsertAllOnSubmit(nwOrder.Order_Details);
        db.SubmitChanges();

        // Return the updated order
        return GetOrder(nwOrder.OrderID);
    }
}
図 7 LINQ to Entities を使用して注文を作成する
public DTO.Order CreateOrder(DTO.Order order)
{
    using (NorthwindEntities db = new NorthwindEntities
        (Settings.Default.NorthwindEntityConnection))
    {
        // Create an L2S.Order
        Order nwOrder = new Order
        {
            OrderID = order.OrderID,
            CustomerID = order.CustomerID,
            OrderDate = order.OrderDate,
            Updated = order.Updated
        };

        // Add order details
        foreach (DTO.OrderDetail od in order.OrderDetails)
        {
            Order_Detail detail = new Order_Detail
            {
                Order = nwOrder,
                Product = (from p in db.ProductSet
                            where p.ProductID == od.ProductID
                            select p).First(),
                Quantity = od.Quantity,
                UnitPrice = od.UnitPrice,
                Updated = od.Updated
            };
            nwOrder.Order_Details.Add(detail);
        }

        // Add order and order details
        db.AddToOrderSet(nwOrder);
        db.SaveChanges();

        // Return the updated order
        return GetOrder(nwOrder.OrderID);
    }
}
エンティティの削除または更新について見る前に、同時実行の問題にどのように対処するかについて説明しておきます。LINQ to SQL では、DeleteOnSubmit を呼び出す前に、切り離されたエンティティをアタッチする必要があります。それにより、LINQ to SQL はオプティミスティックな同時実行のチェックを実行するため、ユーザーが項目に変更を加えた後で他のユーザーがその項目を削除しようとすると、削除は ChangeConflictException により失敗します。実際、この動作を無効にするための簡単な方法はありません。なぜなら、最初に Attach を呼び出さずに DeleteOnSubmit を呼び出そうとすると LINQ to SQL では InvalidOperationException がスローされるためです。一方、Entity Framework では、エンティティを削除するときに変更の競合処理を要求されることはありません (この記事に付属するコード サンプルを参照)。
図 8 に、LINQ to SQL を使用して注文を更新するためのコードを示します。Attach メソッドがブール型の asModified パラメータを受け取ることに注意してください。このパラメータを true に設定することにより、同時実行管理のためにタイムスタンプ列を使用することを LINQ to SQL に示します。それにより、LINQ to SQL では SQL Update ステートメントの WHERE 句に現在のタイムスタンプ値が含められます。レコードが別のユーザーによって更新された場合、その更新はどの行にも影響を与えず、LINQ to SQL は ChangeConflictException をスローします。
図 8 LINQ to SQL を使用して注文を更新する
public DTO.Order UpdateOrder(DTO.Order order)
{
    using (NorthwindDataContext db = new NorthwindDataContext
        (Settings.Default.NorthwindSqlConnection))
    {
        // Create an L2S.Order
        Order nwOrder = new Order
        {
            OrderID = order.OrderID,
            CustomerID = order.CustomerID,
            OrderDate = order.OrderDate,
            Updated = order.Updated
        };

        // Attach and save order
        db.Orders.Attach(nwOrder, true);
        db.SubmitChanges();

        // Return the updated order
        return GetOrder(order.OrderID);
    }
}
この戦略により、クライアントは元の値を保持してそれを更新時に送信する必要がなくなります。これは、Attach メソッドの別のいずれかのオーバーロードの場合には、渡す必要のある値です。列はタイムスタンプ データ型であれば、名前は何でもかまいません。Visual Studio の LINQ to SQL デザイナにタイムスタンプ列を含むテーブルを追加すると、すべてのエンティティ フィールドに対して UpdateCheck 属性が Never に設定され、LINQ to SQL ではこのタイムスタンプを使用して変更の競合をチェックします。
Entity Framework でのエンティティの更新と同時実行の管理は、現在のところ、LINQ to SQL の場合よりも多少複雑です。これは、同時実行に対してタイムスタンプを使用した場合でも、エンティティの元の値が必要とされるためです (図 9 を参照) (これについては Entity Framework の次のバージョンで改善される可能性があります)。クライアントから元の注文が提供されていないため、データベースから注文を取得して、その Updated プロパティをクライアントから提供された値に設定する必要があります。これが、実際に意識する唯一の "元の" 値です。注文をいったんデタッチしてオブジェクト コンテキストに再アタッチするか、または AcceptAllChanges を呼び出すことにより、必ず変更を Updated にコミットしてください。その後で、ApplyPropertyChanges を呼び出し、更新された注文からの値で元の注文を更新します。Entity Framework で同時実行チェック用にタイムスタンプ列を使用するためには、もう 1 つの手順が必要となります。それは、Order エンティティの Updated プロパティの ConcurrencyMode を None から Fixed に設定することです (図 10 を参照)。
図 9 LINQ to Entities を使用して注文を更新する
public DTO.Order UpdateOrder(DTO.Order order)
{
    using (NorthwindEntities db = new NorthwindEntities
        (Settings.Default.NorthwindEntityConnection))
    {
        // Get original order using key from updated order
        EntityKey key = db.CreateEntityKey("OrderSet",
            new Order { OrderID = order.OrderID });
        Order origOrder = db.GetObjectByKey(key) as Order;

        // Set Updated to match DTO.Order - (for concurrency)
        origOrder.Updated = order.Updated;

        // Commit change to the Updated property
        db.Detach(origOrder);
        db.Attach(origOrder);

        // Create a NW order for updating the original order
        Order updatedOrder = new Order
        {
            OrderID = order.OrderID,
            OrderDate = order.OrderDate,
            CustomerID = order.CustomerID,
            Updated = order.Updated
        };

        // Apply changes to the original order
        db.ApplyPropertyChanges("OrderSet", updatedOrder);

        // Persist changes
        db.SaveChanges();

        // Return the updated order
        return GetOrder(order.OrderID);
    }
}
図 10 Updated の同時実行モードを Fixed に設定する

サービスの境界を越えて変更を追跡する
サービスに対する 1 回の呼び出しによって、同じトランザクション内の複数のエンティティの変更を保存することが目標である場合には、各エンティティの変更状態を決定するための方法が必要となります。サンプル アプリケーションで各 DTO に TrackingState プロパティが含まれているのはそのためです。このプロパティがクライアントによって設定されていると仮定すると、DAL では、切り離されたエンティティに対する挿入、更新、および削除を保存するために LINQ to SQL または Entity Framework の適切な API を適用できます。それにより、バッチ更新用に DTO の配列をサービス操作に渡し、挿入、更新、または削除された注文明細を単純な LINQ to Objects のクエリで取り出すことができます。たとえば、注文に追加された注文明細だけを取得するには、TrackingState が Created である項目のコレクションについてクエリを実行できます。
List<Order_Detail> insertedDetails =
    (from od in order.OrderDetails
     where od.TrackingState == TrackingInfo.Created
     select new Order_Detail
     {
         OrderID = od.OrderID,
         ProductID = od.ProductID,
         Quantity = od.Quantity,
         UnitPrice = od.UnitPrice,
         Updated = od.Updated == null ? new byte[0] : od.Updated
     }).ToList();
データベース テーブルのタイムスタンプ列では通常は Null が許可されないため、Updated に対する Null 値は空のバイト配列に変換する必要があります。挿入された注文明細のリストが取得されたら、従来の LINQ to SQL API を使用して、注文明細のコレクションを渡すことにより新しいエンティティを追加できます。
db.Order_Details.InsertAllOnSubmit(insertedDetails);
ご推察のとおり、対応する AttachAllOnSubmit および DeleteAllOnSubmit メソッドも存在します。DataContext に対して SubmitChanges を呼び出すと、LINQ to SQL では、これらの変更を正しい順序 (挿入の場合は親、子の順、削除の場合は子、親の順) でコミットし、すべてを 1 つのトランザクションにラップします。LINQ to Entities を使用して注文明細を保存する場合にも同じアプローチを採用できます。LINQ to SQL と Entity Framework の両方に対する UpdateOrder の完全なコード リストについては、サンプルを参照してください。
ここまでの説明で、DAL がクライアントを利用して各 DTO の変更状態を追跡していることは明らかです。クライアント上でオブジェクトに対する変更を追跡するための最も便利なメカニズムは、汎用コレクションです。コレクションで TrackingState プロパティを更新するには、型引数に制約を加えてインターフェイスを実装する必要があります (TrackingState プロパティの取得と設定に Reflection を使用することもできますが、汎用の型制約を使用すればタイプ セーフと良好なパフォーマンスの両方が得られます)。TrackingState プロパティを唯一のメンバとして持つ ITrackable インターフェイスを入力します。
public interface ITrackable
   {
       TrackingInfo TrackingState { get; set; }
   }
最初に述べたとおり、変更追跡コレクションには 2 つの種類があります。1 つは Windows Forms アプリケーション用に BindingList<T> を拡張するものであり、もう 1 つは WPF アプリケーション用に ObservableCollection<T> を拡張するものです。ただし、この両方のコレクションで変更追跡ロジックを複製する代わりに、これを ChangeTrackingHelper<T> というクラスに集約しました。このクラスは、Collection<T> から派生し、ITrackable を実装するよう T に制約を加えます (図 11 を参照)。また、INotifyPropertyChanged を実装するようにも T に制約を加えるので、各項目の PropertyChanged イベントを処理し、TrackingState プロパティを Updated に設定できます。
図 11 変更追跡ヘルパ クラス
internal class ChangeTrackingHelper<T> : Collection<T>
    where T : ITrackable, INotifyPropertyChanged
{
    Collection<T> deletedItems = new Collection<T>();

    // Listen for changes to each item
    private bool tracking;
    public bool Tracking
    {
        get {  return tracking; }
        set
        {
            foreach (T item in this)
            {
                if (value) item.PropertyChanged += OnItemChanged;
                else item.PropertyChanged -= OnItemChanged;
            }
            tracking = value;
        }
    }

    void OnItemChanged(object sender, PropertyChangedEventArgs e)
    {
        if (!Tracking) return;
        ITrackable item = (ITrackable)sender;
        if (e.PropertyName != "TrackingState")
        {
            // Mark item as updated
            if (item.TrackingState == TrackingInfo.Unchanged)
                item.TrackingState = TrackingInfo.Updated;
        }
    }

    // Mark item as created and listen for property changes
    protected override void InsertItem(int index, T item)
    {
        if (!Tracking) return;
        item.TrackingState = TrackingInfo.Created;
        item.PropertyChanged += OnItemChanged;
        base.InsertItem(index, item);
    }

    // Mark item as deleted and cache it
    protected override void RemoveItem(int index)
    {
        if (!Tracking) return;
        T item = this.Items[index];
        if (item.TrackingState != TrackingInfo.Created)
        {
            item.TrackingState = TrackingInfo.Deleted;
            item.PropertyChanged -= OnItemChanged;
            deletedItems.Add(item);
        }
        base.RemoveItem(index);
    }

    public ChangeTrackingHelper<T> GetChanges()
    {
        // Create a collection with changed items
        ChangeTrackingHelper<T> changes = new ChangeTrackingHelper<T>();
        foreach (T existing in this)
        {
            if (existing.TrackingState != TrackingInfo.Unchanged)
                changes.Add(existing);
        }

        // Append deleted items
        foreach (T deleted in deletedItems)
            changes.Add(deleted);
        return changes;
    }
}
コレクションに項目が追加されると、その TrackingState は Created に設定されます。項目が削除されると、TrackingState は Deleted に設定され、削除された項目のコレクション内にキャッシュされます。ユーザーは、Tracking プロパティを true に設定することによりコレクションに対する追跡の開始を指定します。この時点で、コレクションは各項目の PropertyChanged イベントをサブスクライブします。GetChanges メソッドは、Created、Updated、または Deleted とマークされた項目だけで構成される新しい ChangeTrackingHelper コレクションを作成します。

クライアントを構成する
これで、汎用の変更追跡コレクションのセットが用意されたので、それらを使用してみましょう。これらのコレクションは別個のクラス ライブラリに配置したため、クライアント アプリケーションからそれを参照するだけで済みます。ただし、クライアント プロジェクトにサービス参照を追加するときに、svcutil.exe によって生成されるクラスをコレクションで使用できるようにするために、いくつかの追加手順が必要となります。Customer、Product、Order、OrderDetails などのクラスは既に TrackingState プロパティを持っていますが、ChangeTrackingHelper<T> で必要とされる ITrackable インターフェイスは実装しません。
さらに、TrackingInfo 列挙型は、サービス参照の追加時に svcutil.exe によって配置される入れ子になった名前空間内ではなく、変更追跡コレクションが見つけることのできる名前空間に配置する必要があります。最後に、サービス操作によって返されたオブジェクトの配列がクライアント アプリケーションで変更追跡コレクションとして表示されれば便利です。それにより、Order.OrderDetails のデータ型が OrderDetail の配列の代わりに ChangeTrackingCollection<T> または ChangeTrackingList<T> に設定されるため、変更された注文明細をサービスに送信するときに余分な作業が不要となります。
次に、変更追跡コレクションを使用してエンティティの変更を追跡し、それをサービスに送信できるように、Windows Forms または WPF のクライアント アプリケーションを設定する手順を示します。この手順は、必ず以下の順序で実行してください。
  1. Windows Forms または WPF のクライアント アプリケーションから、ITrackable インターフェイスおよび変更追跡コレクションが格納されている ClientChangeTracker クラス ライブラリ プロジェクトまたはアセンブリへの参照を設定します。
  2. WCF サービス アプリケーションが実行されている状態で、メタデータ エンドポイントまたは Web サービス記述言語 (WSDL) の URL を指し示すサービス参照を追加します。サービス参照を追加する前に ClientChangeTracker アセンブリを参照することが重要です。既定では、サービス参照は参照されるすべてのアセンブリ内の型を再利用します。また、ClientChangeTracker には DataContract および EnumMember シリアル化属性を持つ TrackingInfo 列挙型が含まれるため、svcutil.exe では、サービス参照によって作成される Reference.cs コード ファイルにコピーする代わりに、それを再利用します。
  3. サービス参照を設定した後、サービス参照によって作成された各 DTO クラスに対して部分クラスを追加します。たとえば、サービス操作で Customer、Product、Order、および OrderDetail オブジェクトが公開される場合、これらはサービス参照に含まれます。プロジェクトのすべてのファイルを表示し、サービス参照および Reference.svcmap ノードを展開すると、これらのクラスが Reference.cs に含まれていることがわかります。それらのクラスを含む名前空間をメモしておいてください (これは、サービス参照の追加時に指定したものと一致するはずです)。ここで、プロジェクトにクラス ファイルを追加し、Reference.cs 内の名前空間に合わせて名前空間を変更します。この名前空間内で、各 DTO クラスに対してパブリック部分クラスを追加し、ITrackable インターフェイスを実装します。
    namespace WpfClient.OrderService
{
    public partial class Customer : ITrackable { }

    public partial class Product : ITrackable { }

    public partial class Order : ITrackable { }

    public partial class OrderDetail : ITrackable { }
}
    TrackingState プロパティがないことに注意してください。コードによって生成されたクラスには既にこのプロパティが含まれているため、ここで挿入する必要はありません。これは、各 DTO に対するデータ コントラクトの一部です。また、アプリケーションで使いやすくなるように、これらのクラスに追加のコードを挿入することもできます。たとえば、クラスを初期化するコンストラクタなどです (これは、new を使用してクラスを作成したときに呼び出され、WCF によって作成されたときには呼び出されません)。例については、コード サンプルを参照してください。
  4. 最後に、おそらく、System.Array ではなくコレクション型として ChangeTrackingCollection<T> または ChangeTrackingList<T> を使用するようにサービス参照を構成する必要があります。これは、サービス操作で Order 配列が返されたときに、クライアント側では ChangeTrackingCollection<Order> または ChangeTrackingList<Order> として実体化されることを意味します。各 Order の OrderDetails プロパティも同様に、変更追跡コレクション型として表されます。これは実にすばらしいことです。このマジックを実現するためには、Reference.svcmap ファイルを開き (このファイルを見るにはすべてのプロジェクト ファイルを表示してサービス参照を開く必要があります)、XML に注目します。WPF クライアントの場合、CollectionMappings 要素を次のように構成します。
<CollectionMappings>
    <CollectionMapping TypeName="ChangeTracker.ChangeTrackingCollection'1" Category="List" />
</CollectionMappings>
Windows Forms クライアントの場合は、ChangeTracker.ChangeTrackingCollection`1 を ChangeTracker.ChangeTrackingList`1 で置き換えます。
  1. ここで、サービス アプリケーションがまだ実行されている状態で、サービス参照を右クリックして [サービス参照の更新] をクリックし、クライアント アプリケーションを再コンパイルします。すべてがうまくいけば、プロジェクトは正常にビルドされ、期待どおりの結果が得られます。
これらの手順を実行した後は、クライアント アプリケーションのコードで、いくつかの DTO を取得し、それらをユーザーが更新できるよう UI にバインドしてから、サービスへと送り返すように記述すれば、DTO はサービスによってデータベースに保存されます。サービスからのオブジェクトの取得は、単にサービス プロキシのインスタンスを作成して、Customer、Product、および Order を取得する操作を呼び出すことで行います。たとえば、orderId に対して int を受け取る GetOrder メソッドがサービスにある場合、クライアントのコードは次のようになります。
using (OrderServiceClient proxy = new OrderServiceClient())
{
   CurrentOrder = proxy.GetOrder(orderId);
}
データ バインドを適切に設定していれば、注文が注文明細と共にユーザー インターフェイスに表示されます。サンプル アプリケーションで WPF クライアントがどのようになるかを図 12 に示します。[Get Order] (注文の取得) ボタンを使用すると、顧客の注文のリストから選択することで既存の注文を取得できます。[New Order] (新規注文) ボタンは単に新しい注文を作成し、[Delete Order] (注文の削除) ボタンはサービス プロキシに対して現在の注文を渡して DeleteOrder を呼び出します。
図 12 サンプル WPF クライアント アプリケーション
[Save Order] (注文の保存) ボタンは、サービスの CreateOrder 操作を呼び出して新しく作成された注文を保存するか、UpdateOrder 操作を呼び出して既存の注文への変更を保存します。どちらの操作でも、挿入された注文に対する新しい OrderId プロパティ (Northwind の Order テーブルでは ID 列を使用) および現在の Updated プロパティ (同時実行管理用) を含む注文オブジェクトが返されます。更新された注文を取得した後は、変更追跡を開始し、注文を使用するためのデータ バインドを設定します。
ただし、既存の注文を保存する前には、まだ少しやることがあります。ここで必要なのは作成、更新、または削除された注文明細を渡すことであり、変更のない明細を送信することで帯域幅を浪費するのは好ましくありません。ChangeTrackingCollection<T> および ChangeTrackingList<T> を使用すれば、挿入、変更、または削除された注文明細だけを返す GetChanges メソッドを公開することによって、この問題に比較的簡単に対処できます。UpdateOrder を呼び出すコードは図 13 のようになります。
図 13 UpdateOrder を呼び出す
Order changedOrder = new Order
{
    OrderID = CurrentOrder.OrderID,
    CustomerID = CurrentOrder.CustomerID,
    OrderDate = CurrentOrder.OrderDate,
    Updated = CurrentOrder.Updated,
    TrackingState = CurrentOrder.TrackingState,
    OrderDetails = CurrentOrder.OrderDetails.GetChanges()
};

using (OrderServiceClient proxy = new OrderServiceClient())
{
    CurrentOrder = proxy.UpdateOrder(changedOrder);
}
ChangeTrackingCollection<T> をコレクション型として使用するようにサービス参照を構成してあるため、Order クラスの OrderDetails プロパティは ChangeTrackingCollection<OrderDetail> 型となり、それに対して直接 GetChanges を呼び出すことで、追加、変更、または削除された注文明細だけを取得でき、それらを UpdateOrder 操作に渡すことができます。また、ChangeTrackingCollection<T> は ObservableCollection<T> を拡張しているため、WPF アプリケーションに対して役立つデータ バインド機能をサポートします (たとえば、項目がコレクションに追加されたり削除されたりしたときに UI で項目のコントロールを更新できるように、INotifyCollectionChanged を実装しています)。同様に、ChangeTrackingList<T> は BindingList<T> を拡張しているため、BindingList<T> によって実装されるインターフェイスを利用してユーザー エクスペリエンスを向上させる、Windows フォームの DataGridView に対して、優れたデータ ソースとして機能します。

まとめ
これで、オブジェクトがどのように保存されるかをクライアントがまったく意識することのない、柔軟なサービス指向アプリケーション アーキテクチャが実現されました。また、ツールで生成されたエンティティをサービスの境界を越えて渡すのと同じくらい魅力的なこととして、DTO によってオブジェクト モデルの構造や形態をよりきめ細かく制御できることを見てきました。さらに、データ アクセス層を構築することで、使用するどのような保存テクノロジからもアプリケーションが分離されるため、アプリケーションの他の部分に影響を与えることなくデータ アクセス実装を置き換えることができます。
LINQ to SQL および ADO.NET Entity Framework は、ORM 空間における画期的な進歩を体現し、オブジェクトとリレーショナル データの間のインピーダンス ミスマッチ低減に向けて大きく前進しています。これらのツールは、従来のクライアント/サーバーおよびサービス指向アーキテクチャを含めたさまざまなシナリオをサポートします。ただし、DTO を使用したデータベースの更新には、開発者側で追加の作業が必要となります。特に、同時実行ユーザー間の競合を検出するためのタイムスタンプ値を使用する場合などです。そのような必要性はありますが、変更状態をクライアントとサービスの間のデータ コントラクトに含めることで、元のコンテキストから切り離されたエンティティをアタッチできます。最後に、汎用の変更追跡コレクションがクライアントでの変更状態管理という重労働を担うことにより、サービスへの 1 回のラウンド トリップでバッチ更新を処理できるようになります。
Entity Framework の詳細については、Elisa Flasko による記事「ADO.NET: Entity Framework で柔軟なデータ モデリングを実現する」を参照してください。

Anthony Sneed は、開発者教育企業 DevelopMentor でインストラクタを務めており、.NET Framework 3.5、LINQ、および Entity Framework に関するコースを作成し、講師を担当しています。余暇には、料理の腕をふるって評判のチリをこしらえたり、家族のビデオ撮影に励んだりしています。連絡先は、tony@tonysneed.com です。また、blog.tonysneed.com にブログを公開しています。


Geneva Framework
クレームベースの WCF サービスを構築するためのより優れたアプローチ
Michele Leroux Bustamante
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事は、"Geneva" Framework のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • Geneva Framework でセキュリティを実装する
  • WCF セキュリティ
  • クレームベースのセキュリティ
  • セキュリティ トークン サービス
 この記事では、次のテクノロジを使用しています。
Windows Communication Foundation
目次
なぜクレームベースのセキュリティなのか
セキュリティ トークン サービス
クレームベースのセキュリティのサポート
Geneva Framework 以前の WCF サービス
発行されたトークンと WSFederationHttpBinding
WCF サービスに対して Geneva Framework を有効化する
クレームを要求する
<microsoft.identityModel> を構成する
Geneva Framework を IIS ホスティングでフックする
WCF から Geneva Framework へ
アプリケーションを Geneva Framework に移行する
"Geneva" Framework (以前は "Zermatt" と呼ばれていました) は、クレームベースのアプリケーションおよびサービスの構築と、フェデレーション セキュリティ シナリオの実装を行うための新しいフレームワークのコード名です。その機能には、カスタムのセキュリティ トークン サービス (STS) を構築するプラミング、ASP.NET アプリケーションからフェデレーション認証を要求するメカニズム、ASP.NET アプリケーションおよび Windows Communication Foundation (WCF) サービスのクレームベースの承認を容易にするオブジェクト モデルなどがあります。
また、Geneva Framework には Windows CardSpace をサポートする機能 (管理された情報カードの発行や、Windows CardSpace ログイン操作を簡略化する ASP.NET コントロールなど) もあります (Windows CardSpace の詳細については、「ID: Windows CardSpace を使用して ASP.NET アプリケーションと WCF サービスのセキュリティを保護する」を参照してください)。明らかに、Geneva Framework は広範なセキュリティ機能を提供しますが、その中核となるのはクレームベースのセキュリティです。
WCF では、常に、クレームベースのセキュリティ モデルをネイティブでサポートしてきましたが、Geneva Framework によってその能力が強化されます。Geneva Framework は、実行時のクレームへのアクセスを簡略化し、Microsoft .NET Framework で既に利用可能であるロールベースの承認プリンシパルと一貫性のある方法でクレームベースの承認をサポートするメカニズムを提供します。ASP.NET アプリケーションでは、Geneva Framework を利用することにより、ロールベースのセキュリティを適用する既存の ASP.NET ログイン コントロールと互換性のある、クレームベースの承認機能が提供されます。この記事では、クレームベースのセキュリティ モデルを実装することの意義について重点的に解説し、Geneva Framework を使用してクレームベースの WCF サービスに取り組む方法について説明し、Geneva Framework なしで WCF がクレームベースのセキュリティを適用する方法との比較を論じます。
この記事をお読みになる前に、Keith Brown および Sesha Mani が開発者に向けて執筆した「Geneva Framework ホワイト ペーパー」を参照することをお勧めします。このホワイト ペーパーでは、Geneva Framework の機能の概要およびクレームベースのセキュリティの概念にかかわる背景と、ASP.NET アプリケーションおよび WCF サービスで Geneva Framework の機能を有効化する方法 (特に前者に重点が置かれています) について説明されています。さらに詳しく理解するには、2007 年 9 月号で Keith Brown が執筆した「セキュリティに関するブリーフィング」コラムの「要求に基づく ID の詳細」を参照してください。

なぜクレームベースのセキュリティなのか
クレームベースのセキュリティ モデルに移行する理由は何でしょうか。Geneva Framework を使用したソリューションの実装を検討する前に、この質問の答を知る必要があります。ロールベースのセキュリティで十分対応できる場合があることは確かです。アプリケーション ロールの定義が不変であり、なおかつセキュリティ プリンシパルとロールのマッピングが単一の認証メカニズムで行われるという前提があれば、たいていは大丈夫でしょう。しかし、クレームベースのセキュリティでは、アプリケーションおよびサービスが特定の種類の資格情報や特定のロールのセットに関連付けられるように設計できます。これは、クレームベースのセキュリティ モデルの価値提案の 1 つです。
アプリケーションおよびサービスをロールから分離することにより、システムに影響を与えることなく、ロールの名前と意味を変更できます。承認用のより細かいアーティファクトであるクレームを認証済みユーザーに割り当てることができます。クレームは、認証済みユーザーに基づいて割り当てるか (図 1)、または認証済みユーザーのロールに基づいて割り当てる (図 2) ことができます。
図 1 認証済みユーザーに基づくクレームの割り当て
図 2 認証済みユーザーのロールに基づくクレームの割り当て
両方ともクレームに基づいて承認が実行されるというモデルをサポートしていますが、後者の場合は、背後でロールを使用することで、より簡単にクレームをグループ化することができます。この観点から見ると、クレームベースのセキュリティにはアクセス許可ベースのセキュリティに似たプロパティがあります。ただし、クレームは信頼された発行者によって保証されており、これはセキュリティ上の利点です。なお、図 1 および 2 で使用されているのは、認証済みユーザーの名前クレームの割り当てと、いくつかのカスタム アクセス許可クレーム (Create、Read、Update、および Delete) の割り当てです。
クレームに基づくアクセスの承認は、クレームと、そのクレームが権限を与える機能またはリソースの関連付けが変わらない (少なくとも頻繁に変更されない) 場合に最も有用です。一方、ユーザーとロールにクレームを割り当てるルールは、承認ロジックに影響を与えることなく自由に変更できます。たとえば、Delete クレームは、システム内のすべての削除操作によって要求される可能性がありますが、新しい SuperUsers ロールが導入された場合に Administrators ロールが常にその Delete クレームを許可するとは限りません。
クレームベースのセキュリティ モデルにより、アプリケーションおよびサービスは複数の資格情報の種類をサポートできます。単純な (一般的な) 例は、同じドメイン上の内部ユーザーに対しては Windows 資格情報に依存し、ドメインの外側に存在する外部ユーザーに対してはカスタムのユーザー名とパスワード アカウントに依存するアプリケーションです。複数の資格情報の種類をサポートすると、認証プロセスおよび関連する承認コードが複雑化する可能性があります。複数の資格情報の種類をサポートする ASP.NET アプリケーションでは、それぞれの資格情報の種類に対するログイン サポート、構成、およびロールの初期化で複雑化が進みます。
WCF サービスでは、各種の資格情報の動作の構成や、AuthorizationContext の適切な初期化において、さまざまな資格情報の種類をサポートするための追加作業も必要です。ASP.NET アプリケーションおよび WCF サービスが、必要なクレーム (ロールなど) を保持する資格情報を受け取ることができると、ユーザーが認証された方法にかかわらず、承認の作業を一般的なルールのセットに正規化することができます。
理想では、STS がさまざまな資格情報の種類の認証を処理し、クレームを保持するセキュリティ トークンの生成を処理します。このトークンは (たいていは SAML (Security Assertion Markup Language) トークンです)、デジタル署名に基づいたサービスの認証に使用することができ、トークンに保持されているクレームはアクセスの承認に使用できます。
フェデレーション セキュリティ シナリオには、当然のことですがクレームベースのセキュリティが適しています。フェデレーション セキュリティ シナリオでは、ユーザーは自分のセキュリティ ドメインにより認証され、そのドメインにより発行されたトークンは別のドメインの認証に使用されます。

セキュリティ トークン サービス
STS は、フェデレーション シナリオであるかどうかにかかわらず、クレームベースのセキュリティ シナリオで重要な役割を果たします。クレームに基づいてアクセスを承認するには、信頼されたクレームのセットをアプリケーションで利用できるようにする必要があります。STS では、ユーザーを認証した後で、要求されたクレームを含むセキュリティ トークンを発行できます。アプリケーションが信頼する必要があるのはセキュリティ トークンのみです。つまり、そのデジタル署名を信頼するということです。図 3 では、STS により発行された SAML トークンを信頼する 2 つのアプリケーション (証明書利用者) として WCF サービスと ASP.NET アプリケーションが示されています。通信のフローの概要は次のようになります。
図 3 STS により発行されたトークンを信頼する証明書利用者
  1. ユーザーが STS で認証を行います。
  2. STS は、認証済みユーザーにクレームを割り当て、それらのクレームを (SAML 属性として) 保持するための SAML トークンを構築します。STS は秘密キー (IPKey) でトークンに署名し、要求と共に提供される公開キー (RPKey) を使用するアプリケーション用に暗号化します。
  3. クライアント アプリケーションまたはブラウザが、アプリケーション (証明書利用者) に対してそのトークンを示します。トークンは WCF シナリオではメッセージと共に渡され、ASP.NET シナリオでは Cookie として渡されます。トークンの署名が信頼できる場合 (IPKey)、保持されているクレームは信頼され承認できるものと見なされます。
アプリケーションまたはサービスで複数の資格情報の種類がサポートされている場合、STS では、それぞれの資格情報の種類に固有の認証手順をサポートし、認証済みの各ユーザーに対して適切なクレームを保持するセキュリティ トークンを発行することができます。この場合も、アプリケーションが信頼する必要があるのはセキュリティ トークンのみです。さいわいにも、STS が各ユーザーの認証に使用した資格情報はアプリケーションで認識されません。図 4 では、STS が認証用の Windows アカウント、ユーザー名およびパスワード、証明書、または SAML トークンを受け付けて、認証済みユーザーに対して適切なクレームを生成しています。
図 4 複数の資格情報の種類を認証する STS
図 3 と 4 は、同じセキュリティ ドメイン内で STS が使用されるシナリオを示しています。フェデレーション シナリオにおいては 2 つ以上のセキュリティ ドメイン間で信頼関係が確立されるため、ユーザーは自身の資格情報が管理されているドメインで認証を行うことができ、なおかつ、別のドメイン内のリソースに対するアクセス許可を受けることができます。
フェデレーションにより、ID 管理に関連したリスクの多くを避けることができます。複数のアプリケーションやドメインをまたいでユーザーの資格情報を管理する必要がなくなるため、複数のドメインにわたるプロビジョニングとプロビジョニング解除に関連するリスク (複数の場所にあるアカウントを削除し忘れるような事態) が低減されます。アカウントの複数のコピーが管理されることがないため、パスワードの同期化も問題ではなくなります。これらの利点に加えて、フェデレーションでは、ユーザーは 1 つのアプリケーションにログインした後、認証手続きを繰り返すことなく別のアプリケーションに (場合によってはセキュリティ ドメインをまたいで) アクセスできることから、シングル サインオン (SSO) シナリオが容易になります。図 5 は、Web アプリケーションのフェデレーション シナリオを示しています。フローは次のとおりです。
  1. ユーザーがドメイン B の Web アプリケーションにアクセスし、ドメイン B の STS で認証を行います。
  2. STS は、そのユーザー用に SAML トークンを発行し、このトークンは Cookie としてブラウザに返されます。
  3. ユーザーはドメイン A の Web アプリケーションにアクセスし、SAML トークンを Cookie として渡します。
  4. ドメイン B の STS との信頼関係があるため、そのトークンの署名はドメイン A の STS により信頼されます。
  5. ドメイン A の STS は、ドメイン A に関連するクレームを含めて、そのユーザー用に新しい SAML トークンを発行します。このトークンは、Cookie としてブラウザに返されます。
図 5 ドメイン間の信頼を示すフェデレーション シナリオ (クリックすると拡大画像が表示されます)
ドメイン A の STS とドメイン B の STS との信頼関係があるため、ドメイン B により発行されたトークンをドメイン A が認証の証拠として受け付けることができますドメイン B により発行されたクレームには、ユーザー情報 (ユーザー名、電子メール アドレスなど) や、ドメイン A により認識されている権限 (DomainAReadOnly) を含めることができます。ドメイン A は、これらのパートナー クレームを、ドメイン A によりホストされるアプリケーションにとって意味のあるクレームに変換します (たとえば、Read クレームを付与します)。
STS により発行されたクレームは拡張可能です。つまり、これらのシナリオに対して、認証の委任やフェデレーションなど潜在的な構成が数多く存在します。ただし、構成するには、クレームベースのセキュリティがなぜ有用であるか、クレームをどのように発行するか、ということに関して基礎的な理解が必要です。次に、WCF サービスおよび ASP.NET アプリケーションのいくつかの実装について詳しく見ていきましょう。

クレームベースのセキュリティのサポート
先にご紹介したホワイト ペーパーで説明されているように、Geneva Framework には多数の機能があります。Geneva Framework を使用すると、すべてのプラミングを記述することなくカスタム STS を構築して、WS-Trust エンドポイントを公開したり、クレームを保持する SAML トークンを構築したりできます。このフレームワークにより、管理されたインフォメーション カードを発行して、Windows プラットフォームの Windows CardSpace のような ID セレクタをサポートすることもできます。ASP.NET アプリケーションで Windows CardSpace ログインのサポートをより簡単に行うことができ、WCF サービスおよび ASP.NET アプリケーションに対する統合されたクレームベースのサポートを活用できます。
Geneva Framework の主要な目的は、クレームベースのアプリケーションをより簡単に構築できるようにすることです。そのためには、すべての .NET ベースのアプリケーションに共通する既存のロールベースのセキュリティ モデルに対するクレームベースの拡張機能、各ランタイムの対話相手となるクレームを抽出するセキュリティ トークンを処理するフック、および、ASP.NET アプリケーションで必要なクレームを保持するトークンを発行するための STS の呼び出しをトリガするメカニズムを使用することができます。

Geneva Framework 以前の WCF サービス
WCF サービスは、当初から、クレームベースのセキュリティをサポートできるように設計されています。サービス操作にメッセージが送信されると、メッセージと共に供給された各セキュリティ トークンは、既存の操作から ServiceSecurityContext を通じてアクセス可能なクレームのセットに変換されます。セキュリティ トークンが Windows トークンまたは UserName トークンである場合、開発者は通常は要求スレッドにアタッチされたセキュリティ プリンシパルに基づき、従来のロールベースのセキュリティを使用して呼び出しを承認します。セキュリティ プリンシパルが証明書または SAML トークンである場合、クレームの有用性が大きくなります。開発者は、通常は AuthorizationContext にアクセスしてクレームを評価します。
1 つの例について考えてみましょう。WCF サービスが Create、Read、Update、および Delete (CRUD) の各操作を公開し、ユーザーのアプリケーションに対する CRUD 権限に基づいて呼び出しを承認することを想定しているとします。DeleteSomething 操作では、図 1 および 2 で示されているように、認証済み呼び出し元がカスタムの削除の権限 (アクセス許可クレームにより指定) を持っていることが必要です。AuthorizationContext で Delete クレームを検索するコードを 図 6 に示します。このコードは、AuthorizationContext でクレーム セットをトラバースして目的のクレームを探します。信頼されたクレーム セットで目的のクレームが見つからない場合は、SecurityException がスローされます。控えめに言っても、AuthorizationContext をこの方法で検索するコードは煩雑です。Geneva Framework は、WCF でのクレームベースの承認を大幅に簡略化できる、クレーム評価の別の手法を提供します。
図 6 WCF の AuthorizationContext でのアクセス許可クレームの検索
public string DeleteSomething() {
    AuthorizationContext authContext =
      ServiceSecurityContext.Current.AuthorizationContext;

    bool foundClaim = false;
    foreach (ClaimSet cs in authContext.ClaimSets) {
        Claim claim = new Claim("http://schemas.contoso.com/samples/
          2008/09/claims/permission","Delete", Rights.PossessProperty);

        if (cs.ContainsClaim(claim)) {
            foundClaim = true;
            break;
        }

    }

    if (!foundClaim)
        throw new SecurityException(
          "Access is denied. Required claims not satisfied.");

    return String.Format("DeleteSomething() called by user {0}",
      System.Threading.Thread.CurrentPrincipal.Identity.Name);
}

発行されたトークンと WSFederationHttpBinding
WCF では、フェデレーション バインディングを提供することで、WCF サービスにおいて STS によるトークンを想定したシナリオをサポートします。通常、これは SAML トークンを意味します (ただし、必須要件ではありません)。発行されたトークンをサポートする元の標準バインディングが WSFederationHttpBinding であり、このバインディングの基になる WS-* 製品の最新リリースをサポートする更新版が WS2007FederationHttpBinding です。ここではこの記事の目的を考慮し、WSFederationHttpBinding を使用して WCF サービス エンドポイントを構成します。この構成では、Geneva Framework で構築されたカスタム STS で発行された SAML 1.1 トークンが必要です。
図 7 に示すように、この例ではサービス コントラクトの ICrudService に 4 つのサービス操作があります。これらの操作は CRUD 操作を示しており、サービスの実装では、図 1 および 2 に示されているような CRUD アクセス許可クレームに基づいて各操作へのアクセスを承認します。
図 7 CRUD 操作を公開する WCF サービス コントラクト
[ServiceContract(Namespace="http://www.contoso.com/samples/2008/09")]
public interface ICrudService
{
  [OperationContract]
  string CreateSomething();

  [OperationContract]
  string ReadSomething();

  [OperationContract]
  string UpdateSomething();

  [OperationContract]
  string DeleteSomething();
}
図 8 は、WSFederationHttpBinding エンドポイントを公開する 1 つのサービスおよびその関連付けられた動作構成を含む WCF 構成の例です。このバインディング構成には、必要なトークン形式 (SAML 1.1)、必要なクレームの種類 (名前およびアクセス許可クレーム)、および STS WS-Trust エンドポイント アドレスとメタデータ交換アドレスを指定する設定が含まれています。この構成では、クライアントが STS で認証を行って SAML トークンを取得し、そのトークンを使用して WCF サービスで認証を行うことができるプロキシを生成するのに十分な情報が提供されています。
図 8 WSFederationHttpBinding エンドポイントを含む WCF 構成
<system.serviceModel>
  <services>
   <service name="ClaimsBasedServices.CrudService"
    behaviorConfiguration ="serviceBehavior">
    <endpoint contract="ClaimsBasedServices.ICrudService" binding=
     "wsFederationHttpBinding" bindingConfiguration="wsFed"/>
      <endpoint contract="IMetadataExchange" binding="mexHttpBinding"
          address="mex"/>
   </service>
  </services>
  <bindings>
   <wsFederationHttpBinding>
    <binding name="wsFed" >
      <security mode="Message">
        <message issuedTokenType="http://docs.oasis-open.org/wss/
          oasis-wss-saml-token-profile-1.1#SAMLV1.1" >
          <claimTypeRequirements>
            <add claimType="http://schemas.xmlsoap.org/ws/2005/05/
              identity/claims/name" isOptional="false"/>
            <add claimType="http://schemas.contoso.com/samples/2008/09/
              claims/permission" isOptional="false"/>
          </claimTypeRequirements>
           <issuer address="http://localhost:51213/TokenIssuer/Service.svc" />
           <issuerMetadata address="http://localhost:51213/TokenIssuer/
              Service.svc/mex" />
        </message>
      </security>
    </binding>
   </wsFederationHttpBinding>
  </bindings>
  <behaviors>
   <serviceBehaviors>
    <behavior name="serviceBehavior">
      <serviceMetadata httpGetEnabled="true"/>
      <serviceCredentials>
        <issuedTokenAuthentication allowUntrustedRsaIssuers="false">
          <knownCertificates>
            <add findValue="IPKey" storeLocation ="LocalMachine"
             storeName="TrustedPeople" x509FindType "FindBySubjectName"/>
          </knownCertificates>
        </issuedTokenAuthentication>
        <serviceCertificate findValue="RPKey" storeLocation=
         "LocalMachine" storeName="My" x509FindType="FindBySubjectName"/>
      </serviceCredentials>
      <serviceAuthorization principalPermissionMode="None" />
    </behavior>
   </serviceBehaviors>
  </behaviors>
</system.serviceModel>
サービス動作構成には重要な設定がいくつかあります。<serviceCredentials> 内の <issuedTokenAuthentication> セクションは、SAML トークンをどのように認証するかを指定します。この構成は、信頼された発行者のみが許可されることを示しています。つまり、SAML トークンは認識されている証明書によって署名される必要があります。このシナリオでは、認識されている証明書は IPKey というサブジェクト名を持っています。STS は IPKey を使用して SAML トークンに署名するため、SAML トークンに含まれるすべてのクレームが信頼されます。Geneva Framework においても信頼された発行者を指定するための構成はありますが、Geneva Framework を使用した場合でも、このセクションの有用性は変わりません。
<serviceAuthorization> セクションは、認証が行われた後で要求スレッドにアタッチされるセキュリティ プリンシパルの種類に影響を与えます。一般的に、発行済みトークンのシナリオではこの設定に "Custom" を指定します。その場合は、カスタム承認ポリシー (IAuthorizationPolicy を実装する型) を指定する必要があります。承認ポリシーの役割は、要求スレッドに対してセキュリティ プリンシパル (IPrincipal 型) を作成し、関連するクレームをすべて AuthorizationContext にアタッチすることです。WCF サービスで Geneva Framework を有効にした後は、この機能は有用ではなくなるため、この設定を "None" にします。

WCF サービスに対して Geneva Framework を有効化する
Geneva Framework の機能に基づいたクレームベースのセキュリティ モデルは、いくつかの簡単な手順で実現できます。最初の手順として、Microsoft.Identity アセンブリ (Geneva Framework に属するコア アセンブリ) への参照を追加します。次に、Geneva Framework ランタイムの機能に対して各 WCF サービスの ServiceHost を初期化する必要があります。これには、信頼されたトークン発行者の一覧を ServiceHost に登録することなどが含まれます。このフレームワークの最終的な構成では、セキュリティ プリンシパルが各呼び出しの要求スレッドにアタッチされ、承認に必要な信頼された発行者からのクレームのコレクションが保持されます。
FederatedServiceCredentials は、静的メソッドを公開し、Geneva Framework に対して ServiceHost インスタンスを初期化する、ServiceCredentials 型の派生です。図 9 は、ServiceHost を初期化するコードが初期化のコンテキストのどこに組み込まれるかを示しています。
図 9 Geneva Framework に対する ServiceHost の初期化
ServiceHost host = new ServiceHost(typeof(Services.RelyingParty));
try
{
  FederatedServiceCredentials.ConfigureServiceHost(host, new
                                      TrustedIssuerNameRegistry());
  host.Open();
  Console.ReadLine();
}
finally
{
  if (host.State != CommunicationState.Faulted)
    host.Close();
  else
    host.Abort();
}

public class TrustedIssuerNameRegistry : IssuerNameRegistry
{
    public override string GetIssuerName(SecurityToken securityToken)
    {
        X509SecurityToken x509Token = securityToken as X509SecurityToken;
        if (x509Token != null)
        {
            if (x509Token.Certificate.SubjectName.Name == "CN=IPKey")
            {
                return x509Token.Certificate.SubjectName.Name;
            }
        }
        throw new SecurityTokenException("Token signature is not trusted
                                         by the issuer name registry. ");
    }
}
ConfigureServiceHost メソッドは、ServiceHost インスタンスが開かれる前に呼び出され、主に次のようなアクションを実行します。まず、<microsoft.identityModel> 構成セクションを読み取り、トークンの検証のための最大クロック スキュー、トークン ハンドラ、信頼された発行者などのランタイムの領域を初期化します。次に、principalPermissionMode に "Custom" を設定し (図 8 を参照)、ServiceAuthorizationPolicy というカスタム承認ポリシー (IAuthorizationPolicy 型) を追加します。ServiceAuthorizationPolicy により、ClaimsPrincipal 型の作成と Thread.CurrentPrincipal の初期化が行われます。また、他の承認ポリシーの削除と、要求のために AuthorizationContext に追加されたクレーム セットのクリアも行われます。Geneva Framework では、AuthorizationContext を使用する代わりに、ClaimsPrincipal に基づいてクレームを評価することが想定されているからです。
図 9 のコードでは、IssuerNameRegistry 型の実装が ConfigureServiceHost に渡されています。これにより、<microsoft.identityModel> 構成セクションでこの型を指定する必要がなくなります。GetIssuerName のオーバーライドにより、トークンの署名が信頼された発行者の一覧に対してチェックされます。この例では、信頼された発行者のハードコードされた名前として IPKey を使用しています。<microsoft.identityModel> セクションの要素は WCF サービスに対する Geneva Framework の構成に関連しているため、後で説明します。

クレームを要求する
サービスで Geneva Framework を有効にすると、ClaimsPrincipal インスタンスが各要求スレッドにアタッチされ、認証済みユーザーに対するクレームが保持されます。アプリケーションのコードおよびリソースへのアクセスを承認するために、クレームのコレクションをトラバースして特定のクレームの型と値を探します。次のコードは、ClaimsIdentity 型により公開された Claims コレクションにアクセスし、Administrators ロールの URI 値を含むロール クレームを検索します。
ClaimsIdentity identity =     Thread.CurrentPrincipal.Identity as ClaimsIdentity;
if (!identity.Claims.Exists(c=>
    c.ClaimType=="http://schemas.microsoft.com/ws/2006/04/identity/
    claims/role" && c.Value=="http://schemas.contoso.com/
    samples/2008/09/roles/administrators"))
throw new SecurityException("Access is denied.");
Claims コレクションでは、さまざまな発行者からのクレームが保持されており、特定の発行者からのクレームの有無を確認することができます。通常、これは不要なステップです。なぜなら、発行者が信頼済みとなった後は、アクセスを承認するときにだれがクレームを発行したかどうかは関係ないからです。
ClaimsIdentity インスタンスによる Claims コレクションへの直接的なアクセスは便利ですが、ClaimsPrincipal では、次のようなロール クレームの要求に利用できる IsInRole 実装が提供されます。
if !Thread.CurrentPrincipal.IsInRole
    ("http://schemas.contoso.com/samples/2008/09/roles/administrators"))
  throw new SecurityException("Access is denied.");
この手法では、STS でアプリケーションに適した値を持つロール クレームを発行することが前提です。Geneva Framework のロール クレームは、既定により、"http://schemas.microsoft.com/ws/2006/04/identity/claims/role" という URI で指定されています。実際のロールは任意の文字列にすることもできますが、この場合は、アプリケーション ロールを指定するカスタム URI の "http://schemas.contoso.com/samples/2008/09/roles/administrators" です。
IsInRole はクレームベースのセキュリティ チェックを実行する便利な方法ですが、PrincipalPermission および PrincipalPermissionAttribute を使用して従来のアクセス許可の要求を実行することもできます。これらは両方とも、要求スレッドにアタッチされた IPrincipal 型に基づきますが、この場合は ClaimsPrincipal が同様の役割を果たします。
プログラムによる要求は動的に実行できます。
PrincipalPermission p = new PrincipalPermission(null,
    "http://schemas.contoso.com/samples/2008/09/roles/administrators",
    true);
    p.Demand();
Demand はスレッドの ClaimsPrincipal に対して IsInRole チェックを実行し、要求が満たされなかった場合は例外をスローします。また、指定のクレームが見つからなかった場合は別の方法を実行します。PrincipalPermission インスタンスのコレクションを PermissionSet にグループ化して、複数のロールの中で要求が満たされる 1 つのロールがあるかどうかを確認することができます。
もちろん、より洗練されたソリューションは、次に示すように PrincipalPermissionAttribute をサービス操作に適用する方法です。
[PrincipalPermission(SecurityAction.Demand, Role =
    "http://schemas.contoso.com/samples/2008/09/roles/administrators")]
    public string CreateSomething()
この属性は、PrincipalPermission 型を構築し、同じ要求を実行します。これは、属性の適用対象の操作が実行される前に行われます。このモデルの優れた点は、宣言型の性質です。開発者は、サービス操作によって要求されるロールを確認できます。この属性はスタックが可能であるため、複数のロールでアクセス許可要求を満たすことができます。

<microsoft.identityModel> を構成する
Geneva Framework は、新しい構成セクションの <microsoft.identityModel> に基づいて環境を初期化します。先にご紹介したホワイト ペーパーで説明されているとおり、この構成セクションは、ASP.NET アプリケーションおよび WCF アプリケーションに対する Geneva Framework 機能によって使用されます。ここでは、Geneva Framework を有効にした WCF サービスに関連する主要な要素について説明します。
この新しいセクションのサポートを有効化するには、app.config または web.config (WCF サービスのホスティング モデルによって異なる) に次のように入力する必要があります。
<configSections>
    <section name="microsoft.identityModel"
      type="Microsoft.IdentityModel.Configuration.
      MicrosoftIdentityModelSection,
      Microsoft.IdentityModel,
      Version=0.4.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<microsoft.identityModel> 構成セクションには必須の要素はありませんが、任意の主要な設定を配置して、図 10 に示されている要素の宣言型の初期化を提供することができます。
図 10 Geneva Framework 用の ServiceHost の初期化
名前 | 説明
audienceUris | SAML トークン処理に対して許可された対象 URI を示します。特定の URI を提供することによって、トークンの使用可能な場所を制限できる、SAML トークンの機能を参照します。これが SAML トークン内に存在する場合、サービスは許可された URI の一覧をチェックして、そのトークンが信頼された URI の 1 つを対象としているかどうかを確認できます。トークンを悪用から保護します。
issuerNameRegistry | トークンの署名の検証に使用される issuerNameRegistry 型を宣言的に指定するために使用します。
maxClockSkew | トークンで反射攻撃を防ぐために有効な時刻ウィンドウの制御を提供します。
securityTokenHandlers | 発行されたトークンのトークン ハンドラの種類をカスタマイズする場合に使用します。たとえば、SAML 1.1 と SAML 2.0 のハンドラがあるとします。このセクションで、トークン ハンドラの設定をカスタマイズしたり、トークン ハンドラを置き換えたりできます。カスタマイズにより、別のロール クレームの種類を提供することもあります。
図 11 に、これらの値を初期化する構成セクションの例を示します。構成で issuerNameRegistry 型が指定されている場合、図 9 でのサービス ホストの初期化は次のように変わります。
FederatedServiceCredentials.ConfigureServiceHost(host);
図 11 WCF 用の主要な Geneva Framework 機能
<microsoft.identityModel>
  <issuerNameRegistry type="Services.TrustedIssuerNameRegistry,
    Services"/>
  <SecurityTokenHandlers>
    <remove type="Microsoft.IdentityModel.Tokens.Saml11.
      Saml11SecurityTokenHandler, Microsoft.IdentityModel,Version=0.4.1.0,
      Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <add type="Microsoft.IdentityModel.Tokens.Saml11.
      Saml11SecurityTokenHandler, Microsoft.IdentityModel,
      Version=0.4.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
      <samlSecurityTokenRequirement audienceUriMode="Never">
        <nameClaimType value="http://schemas.contoso.com/ws/2005/05/
          claims/name"/>
        <roleClaimTypes>
          <add value="http://schemas.contoso.com/samples/2008/09/claims/
          permission"/>
        </roleClaimTypes>
      </samlSecurityTokenRequirement>
    </add>
  </SecurityTokenHandlers>
</microsoft.identityModel>
トークン ハンドラの場合同様、カスタムの securityTokenHandler を作成するか、このセクションを使用して既存のトークン ハンドラの機能を初期化します。図 11 に後者を示しています。この例では、SAML 1.1 トークン ハンドラがカスタムの名前とロール クレームの種類で初期化されています (ご紹介したホワイト ペーパーにあるように、Geneva Framework の将来のリリースでは、ロール クレームの種類のコレクションは、コレクションではなく単一の値となる可能性が高くなっています)。ここで指定したロール クレームの種類は "schemas.contoso.com/samples/2008/09/claims/permission" です。つまり、図 1 および 2 で示されているようなアクセス許可クレームを STS が発行できるということです。また、既に述べたように、WCF サービスが従来のロールベースのセキュリティ メソッドを使用して、指定のアクセス許可に対するアクセス許可要求を実行することもできます。

Geneva Framework を IIS ホスティングでフックする
WCF の使用経験によって異なると思いますが、サービスが IIS でホストされる場合のカスタム ServiceHost の初期化メカニズムについて、詳しい方もそうでない方もいらっしゃるでしょう。ServiceHost に対して Geneva Framework を有効化するには、このメカニズムを理解する必要があります。ServiceHost 初期化をフックするには、次に示すように @ServiceHost ディレクティブでカスタムの ServiceHostFactory 型を提供する必要があります。
<%@ ServiceHost Factory="ClaimsBasedServices.
    ClaimsBasedServiceHostFactory"
    Service="ClaimsBasedServices.CrudService" %>
この例では、ClaimsBasedServiceHostFactory 型によって、指定したサービスの種類の ServiceHost インスタンスを構築しています。ファクトリ内で直接的に ServiceHost 型を構築し、インスタンスをランタイムに渡す前に Geneva Framework サービスに対して初期化するか、または、カスタムの ServiceHost 型を作成して InitializeRuntime メソッドをオーバーライドし、サービス チャネルを開く前にカスタム コードを提供することができます。後者を 図 12 に示します。
図 12 IIS でホストされた WCF サービスでの Geneva の有効化
public class ClaimsBasedServiceHostFactory: ServiceHostFactory
{
        public ClaimsBasedServiceHostFactory()
        {
        }

        public override System.ServiceModel.ServiceHostBase CreateServiceHost(
                                   string constructorString, Uri[] baseAddresses)
        {
            Type t = Type.GetType(string.Format("{0}, {1}", constructorString,
                constructorString.Substring(0, constructorString.IndexOf("."))));
            return new ClaimsBasedServiceHost(t, baseAddresses);
        }

        protected override System.ServiceModel.ServiceHost CreateServiceHost(
                                           Type serviceType, Uri[] baseAddresses)
        {
            return new ClaimsBasedServiceHost(serviceType, baseAddresses);
        }
}

public class ClaimsBasedServiceHost: ServiceHost
{
        public ClaimsBasedServiceHost(object singletonInstance, params Uri[]
                           baseAddresses): base(singletonInstance, baseAddresses)
        {
        }

        public ClaimsBasedServiceHost(Type serviceType, params
                          Uri[] baseAddresses) : base(serviceType, baseAddresses)
        {
        }

        protected override void InitializeRuntime()
        {
            FederatedServiceCredentials.ConfigureServiceHost(this, new
                                                    TrustedIssuerNameRegistry());
            base.InitializeRuntime();
        }
}

WCF から Geneva Framework へ
Geneva Framework を WCF サービスに対して有効化するときに、特に、Geneva Framework 以前のテクノロジで既にクレームベースのソリューションを実装している経験豊かな WCF 開発者から寄せられる、いくつかの一般的な質問があります。1 つには、なぜ Geneva Framework では System.IdentityModel.Claims 名前空間の ClaimSet 型や Claim 型を使用しないのかという疑問です。その答は、単純化ということです。従来の WCF では、呼び出し元をそのクレームに基づいて承認するために、(前述したとおり) AuthorizationContext で ClaimSet インスタンスのコレクションをトラバースする必要がありました。これは煩雑なプログラミング モデルです。Geneva Framework では、ClaimSet に関連する複雑さをいくつか解消し、必要な機能を Claim 型に追加することにより、このモデルを改良しました。
ClaimSet 型はクレームを発行者ごとにグループ化します。このモデルには、いくつかの制限があります。たとえば、発行者が異なる場合に 2 つの ClaimSet インスタンスをマージできません。すべてのクレームを含む単一のコレクションを調べることはできません。一方、Geneva Framework では、承認中に調べることが可能な ClaimsIdentity を通してクレームの単一のコレクションを公開します。特定の発行者からのクレームを信頼できることが (その発行者が信頼できるかどうかに基づいて) ランタイムで確認された後は、承認コードで問題になるのは発行者ではなく Claim 型と値です。
Geneva Framework で生成されたクレームのコレクションは、Microsoft.IdentityModel.Claims 名前空間の新しい Claim 型を使用します。この Claim 型には Issuer プロパティがあるため、必要に応じて、クレーム コレクションに対して LINQ クエリを実行することで特定の発行者からのクレームをすべて収集することができます。
次の質問は、クレームの Issuer プロパティが ClaimSet ではなく単純な String 型であるのはなぜかということです。これは、多くの開発者が疑問に思われるのではないでしょうか。WCF の Issuer プロパティは再帰的なプロパティです。各発行者は ClaimSet により定義されます。その ClaimSet が保有する発行者が別の ClaimSet により定義されている可能性もあり、そのようにしてルート発行者 (自己署名) までさかのぼることも考えられます。これは作業が困難なプログラミング モデルであるだけでなく、トークン認証の間に直接的な発行者が信頼できるということが確定した後は、このチェーンは実行時にはあまり重要ではありません。こういった理由で、Geneva Framework では Issuer プロパティで発行者の文字列表現を使用するようになりました。ただし、文字列は単に発行者をわかりやすい形式で表すための手段であり、信頼を確立するときに使用されるのはトークンの署名です。
また、Geneva Framework が有効になったときに AuthorizationContext がクリアされることに驚かれたかもしれません。WCF のクレームベースのセキュリティ機能は、特に AuthorizationContext に依存しています。これにより、信頼されたセキュリティ トークンから抽出される、利用可能な要求のクレーム セットへのアクセスが提供されます。残念ながら、WCF サービス以外では、クレーム プログラミング モデルを分割する AuthorizationContext に意味はありません。
Geneva Framework の目的は、.NET セキュリティの既存の機能を活用しながら、すべての .NET ベースのアプリケーションでクレームベースのセキュリティをより簡単にサポートできるようにすることです。ClaimsPrincipal を要求スレッドに割り当てることにより、既存のロールベースのセキュリティ実装からクレームベースのセキュリティに簡単に移行できます。さらに、WCF サービスと ASP.NET アプリケーションにおける一貫性のある操作環境が提供されます。セキュリティ プリンシパルを使用する別の利点は、新しいスレッドが作成されたときに、元のスレッドと同じセキュリティ プリンシパルで初期化されることです。一方、AuthorizationContext は新しいスレッドに自動的に渡されません。
つまり、Geneva Framework が WCF サービスに対して有効化された場合に、AuthorizationContext のクレーム セットに基づいたクレームベースのセキュリティ チェックは実行されなくなるという前提のうえで、Geneva Framework は AuthorizationContext をクリアします。したがって、AuthorizationContext への値の格納および要求スレッドのセキュリティ プリンシパルの作成においてカスタム承認ポリシーを使用している WCF サービスは、Geneva Framework が有効化されたときに移行する必要があります。

アプリケーションを Geneva Framework に移行する
先にご紹介した開発者向けのホワイト ペーパーとこの記事をお読みになったことで、Geneva Framework を使用してクレームベースの WCF サービスおよび ASP.NET アプリケーションを構築する要件に対して理解を深めていただけたと思います。次に、Geneva Framework が WCF と ASP.NET に与える影響についてまとめます。
WCF サービスに対して Geneva Framework を有効化するには、少なくとも次の手順を実行する必要があります。フェデレーション セキュリティ バインディングの 1 つを使用してサービスのエンドポイントを公開し、Geneva Framework に対して ServiceHost を初期化し、信頼された発行者の一覧を提供し、クレームベースのセキュリティに基づいてロール クレームの種類を構成し、アクセス許可要求を操作に適用してロール クレームに対する呼び出しを承認します。要求スレッドにアタッチされたセキュリティ プリンシパルを通じてクレームに直接アクセスできるようになり、AuthorizationContext は使用されなくなるので、既にクレームベースになっている WCF サービスでは変更が必要です。
ASP.NET アプリケーションでは、認証を STS に委任するフェデレーション セキュリティ シナリオを実装するために Geneva Framework を利用できます。また、ClaimsPrincipal インスタンスおよび ClaimsIdentity インスタンスでアクセス可能な、信頼された発行者からのクレームで要求スレッドを初期化するためにも利用できます。既にロールベースであるアプリケーションでは、クレームベース スキームへの移行時にロールと一致するクレームが必要ですが、新しいアプリケーションでは、この記事で説明したアクセス許可のようにロールよりも細かいクレームを使用できます。いずれの場合でも、ASP.NET ログイン コントロールやその他のロールベースのセキュリティ アクセス許可要求は、要求スレッドにアタッチされている ClaimsPrincipal を参照して呼び出しを承認します。今後の記事において、クレームベースおよびフェデレーション セキュリティ シナリオのためのカスタム STS の構築など、Geneva Framework についてさらに詳しく解説する予定です。

Michele Leroux Bustamante は、IDesign Inc. の Chief Architect、サンディエゴ地域の Microsoft Regional Director、および Connected Systems の Microsoft MVP を兼任しています。彼女の最新の著書は『Learning WCF』です。連絡を取るには、mlb@idesign.net 宛てに電子メールを送るか、idesign.net にアクセスしてください。Michele のブログのアドレスは、dasblonde.net です。


編集者のメモ
自分の中のビジネス性
Howard Dierking

今月号の編集者のメモは、テキサス州オースティンで開催中のオープン スペース カンファレンスからお送りしています。ブログ社会の論争を引き起こした去年のカンファレンスを覚えている方もいるかもしれません。あの ALT .NET と呼ばれたカンファレンスに参加した多くの人々が、絶えず進化するソフトウェア開発について討論するため、今年はオースティンに戻って来ました。
デザインとコーディングに関連する手法の共有に重点が置かれていた昨年とは異なり、今年のミーティングでは開発プロジェクトの評価と管理の背後にある方法や理念が重視されています。製造やエンジニアリングなどの他分野から理念や方法を受け入れることについて非常に高い関心があり、ビジネスの観点から開発作業を理解しようとする関心も高まってきています。ここで ROI のような言葉が飛び出すのを頻繁に耳にしたため、自分が目的のカンファレンスに出席しているかどうか外に出て掲示を再確認しなくてはなりませんでした。
これらはすべてコミュニティとしての私たちに有益な議論ですが、筆者にとって最も有益だったのは、自分自身の展望がどれほど劇的に変化したかを実感できたことです。1 年前は、日々の業務がテクノロジから遠ざかって行き、経験と自信を持っていたすべてのことから、そしてたくさんの共通の思い出や苦労を共有していた仲間から遠ざかって行くように思えて悲嘆していました。
ところが、昨夜部屋の後ろに座って開発者とビジネスの間に生じる摩擦に関する手法、成功、挫折に関する議論を聞いていると、自分の中のビジネス性に初めて気が付いたのです。そして、そのことを嬉しく感じています。
このような単純な結論に辿り着くまでに 1 年もかかったのかと驚かれるかもしれませんが、アイデンティティの大部分を占める要素から決別するのは簡単ではありません。MSDN Magazine の仕事では、(予算と人員の両方の) 戦略および管理という 2 つの主な優先事項があります。戦略には、内部的および外部的に使用される、ソフトウェア開発プロジェクトが含まれます。これらのプロジェクトは通常、予算から資金が供給されています。筆者の観点では、保守性などの特性は、資金準備に値する最終目標にはなりません。これは、保守性にまったく資金準備の価値がないというのではなく、むしろ、より大きく重要なビジネス目標の中で促進される必要があるという意味です。
保守性に関して言えば、これらの優れた手法について考慮する場合、私たちは手法によって解決される問題を認識しているけれども、ビジネス全体のコンテキストについては馴染みがないため、これらの手法を最終目標レベルとして登用しがちであると感じます。そのため、ソフトウェアで支援されるビジネスの性質を理解していない場合、ビジネスを一般化し、システムの機能しない "優れた点" を重視してしまう傾向があります。これにより、開発グループとソフトウェアによって支援されるはずのビジネスとの間に直接の亀裂が生じ、最終的に多くのプロジェクトの失敗につながります。
開発者が集まって ROI などに関する議論が行われているという単純な事実は、こうしたパターンに対処するための大きな第一歩です。また、他の業界からソフトウェア プロジェクトに適用できるいくつかの創造的な方法があると考えます。たとえば、従来の "要件のインタビュー" の代わりに、開発者に実際のユーザー業務を行わせることを検討しています。これは、要件収集のプロセスで仮定が生じるのを防ぐのに役立つ場合があります。
最後に、成功したプロジェクトと失敗の両方が積み重なって専門的な成熟があると考えます。そのため、失敗よりも多くの成功のために率先して行動する開発者が存在することをありがたく思います。

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

今月号の記事の執筆に携わったマイクロソフトの技術部門の次の専門家に感謝します。Mukundan Agaram、Spotty Bowles、Paul Despe、Elisa Flasko、Buck Hodges、Bryce Jonasson、Irena Kennedy、Ravi Krishnamurthy、Sesha Mani、Beth Massi、Dwayne Need、Michael Puleio、Karl Reinsch、Gerhard Schneider、Cliff Simpkins、および Don Smith。


ツールボックス
.NET 用の静的分析ツール、Matt Berseth のブログ
Scott Mitchell

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

目次
静的コード分析ツールによるソフトウェア品質の向上
注目のブログ

静的コード分析ツールによるソフトウェア品質の向上
ほとんどのソフトウェア チームはコード レビューを使用して、開発者が会社のデザイン ガイドラインに従った正しい、安全なコードを作成していることを確認します。これらのガイドラインには、名前付け規則や、データまたは他の外部リソースへのアクセスに使用するパターンなどの概略が示されています。コード レビュー プロセスのほとんどの側面はかなり機械的であり、自動化できます。静的コード分析ツールは、ソース コードまたは中間コードをスキャンし、定義されたデザイン ガイドライン ルールに対する違反を検出します。
マイクロソフトが開発したフリー ツール FxCop (Version 1.36) は、Microsoft .NET Framework 内のアプリケーションに対応する静的分析ツールの 1 つです。FxCop はコンパイルされた .NET アセンブリの中間コードを分析し、デザイン、セキュリティ、およびパフォーマンス向上のための推奨事項を提供します。既定では、FxCop はクラス ライブラリ開発のデザイン ガイドラインに定められているルールに基づいてアセンブリを分析します。デザイン ガイドライン ルールは、デザイン、グローバリゼーション、パフォーマンス、およびセキュリティなどの 9 つのカテゴリに分類されます。名前付け規則の一例には、"イベントに 'before' または 'after' プレフィックスを使用してはならない" などがあります。FxCop は、BeforeUpdate という名前のイベントを識別した場合、BeforeUpdate を現在時制のイベント名 (つまり、Update) で置き換えるよう推奨します。自社の内部的なデザイン ガイドラインを反映したカスタム ルール クラスを組み込むことも可能です。
アセンブリを分析するには、FxCop を起動し、新しいプロジェクトを作成し、プロジェクトにアセンブリを追加します。FxCop では、アセンブリの分析時に使用される 200 を超えるルールが表示されます。既存のルールをオフにしたり、独自のルールを追加したりできます。[Analyze] (分析) ボタンをクリックして、分析を開始します。型、クラス、メソッド、およびアセンブリのメンバを列挙した後、FxCop は適切でないコードおよび違反のあるルールの一覧を示す分析結果を表示します。結果を選択して、より詳細な説明と解決策を参照できます。
FxCop はスタンドアロン アプリケーションとして使用できます。また、自動化されたビルド プロセスに簡単に組み込み可能なコマンドライン実装も含まれています (FxCop とほぼ同様のツールである Code Analysis は、Visual Studio Team System に付属し、Visual Studio シェルに統合されています)。FxCop の使用方法の詳細については、John Robbins による Bugslayer のコラム「Bad Code? FxCop to the Rescue (FxCop による適切でないコードの救出)」および「Three Vital FXCop Rules (3 つの重要な FXCop ルール)」を参照してください。
.NET デザイン ガイドラインが設定するルールに基づいた FxCop によるアセンブリの分析 (クリックすると拡大画像が表示されます)
マイクロソフトのもう 1 つの静的コード分析ツールは、StyleCop (Version 4.3) です。FxCop では中間コードに対してデザイン ガイドラインが評価されましたが、StyleCop では C# ソース コードのスタイルが評価されます。スタイル ガイドラインは、ソース コードの書式設定を指定するルールです。インデントにスペースやタブを使用するかどうかや、for ループまたは if ステートメントなどの構成要素の形式を指定します。StyleCop のルールの例としては、for ステートメントの本体は中かっこで囲む必要がある、= および != 演算子の両端にはホワイト スペースを書く必要がある、クラス内でメンバ変数を呼び出すときは "this" で始める必要がある、などがあります。
StyleCop は Visual Studio Team System にインストールされていないため、自身でインストールする必要があります。Visual Studio 内部から StyleCop を実行すると、現在開いているソリューションのソース コードが分析され、エラー一覧のウィンドウに結果が警告として表示されます。StyleCop を MSBuild に統合することも可能です。
FxCop と StyleCop ではルール違反が特定されますが、これらのツールの推奨事項を反映するのは開発者の役目です。SubMain の CodeIt.Right (Version 1.1) は、静的コード分析を次のレベルまで引き上げ、ルール違反が自動的に準拠コードにリファクタリングされることを実現しました。FxCop と同様、CodeIt.Right は前述のデザイン ガイドライン ドキュメントに基づいた豊富な事前定義ルールを含み、カスタム ルールを追加する機能も備えています。CodeIt.Right ではさらに簡単にカスタム ルールを作成、使用できます。
FxCop でカスタム ルールを使用するには、ルール クラスを作成およびコンパイルし、FxCop に組み込む必要があります。CodeIt.Right では、グラフィカル ユーザー インターフェイスからカスタム ルールが生成されます。新しいルールを定義する際に、基本の動作パターンを選択し、いくつかのプロパティをカスタマイズします。CodeIt.Right は Visual Studio .NET 2003、Visual Studio 2005、および Visual Studio 2008 のシェルの内部に統合され、また、コマンドライン実装も提供されます。
CodeIt.Right で選択した違反がルールに準拠するよう更新される (クリックすると拡大画像が表示されます)
Visual Studio の内部から CodeIt.Right を使用するには、CodeIt.Right メニューの [Start Analysis] (分析の開始) オプションを選択します。ソリューションの分析が終了すると、CodeIt.Right により Visual Studio IDE 内のウィンドウに結果が表示され、ルール違反の一覧が示されます。このレポートを XML または Microsoft Office Excel ファイルにエクスポートできます。
CodeIt.Right の最大の利点は自動的なコード リファクタリングです。結果の画面から、修正する違反を選択し、[Correct Checked] (選択項目の修正) をクリックします。CodeIt.Right で加えられたすべての変更は Visual Studio 内部で強調表示され、ボタンをクリックして自動変更をロールバックできます。
静的コード分析ツールは、ソース コードが事前定義されたデザインおよびスタイルのガイドラインに準拠していることを迅速で自動的に確認する手段を提供します。これらのガイドラインに準拠することは、より一貫性のあるコードを作成するのに役立ちます。また、セキュリティ、パフォーマンス、相互運用性、およびグローバリゼーションが潜在的に不足している箇所が指摘される場合もあります。静的コード分析ツールは、手動によるコード レビューに置き換わるものではありません。むしろ、コード ベースの最初の確認を行って、上級開発者が慎重に確認する必要のある領域を強調します。
価格: FxCop (無料)、StyleCop (無料)、CodeIt.Right (ユーザー ライセンスごとに 250 ドル)。
FxCop
StyleCop
CodeIt.Right

注目のブログ
多くの Web サイトでは、応答性の高いユーザー インターフェイスを構築するための一連のテクノロジである AJAX を取り入れてきました。AJAX 対応の Web アプリケーションの構築を助けるため、ASP.NET には、AJAX シナリオの実装の中心的な機能を提供する ASP.NET AJAX Framework が付属しています。また、マイクロソフトは AJAX コミュニティと組んで、オープン ソースの AJAX 対応 Web コントロールのセット AJAX Control Toolkit を無償でリリースしました。
AJAX 対応のサイトを構築するすべての Web 開発者は、Matt Berseth のブログをチェックする必要があります。ほとんどのブログ エントリでは、特定の Web ベースの UI を作成する方法が、ダウンロード コード、ライブ デモ、大量のコード スニペットおよびスクリーンショットを使って非常に詳しく説明されています。たとえば、あるブログ エントリでは、AJAX Control Toolkit の ListView コントロールと Accordion コントロールを使用して、Microsoft Office Outlook のナビゲーション バーをミラーリングする UI を作成する方法について説明しています。別のエントリでは、ModalPopup コントロールを GridView および DetailsView コントロールと共に使用して、編集インターフェイスがモーダル ウィンドウに表示される編集可能なグリッドを作成する方法について説明しています。Matt のブログにはデモ ギャラリーもあり、ブログ エントリ全体に分散しているライブ デモのすべてがこのページに集められています。
また、Silverlight、JavaScript、LINQ、および ASP.NET データ Web コントロールなどのテクノロジを扱った投稿もあります。数日ごとに新しい投稿が掲載される Matt のブログは、ASP.NET を使用する最先端の Web 開発者のための情報が満載です。
www.mattberseth.com

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

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


CLR 徹底解剖
CLR でのスレッド管理
Erika Fuentes および Eric Eilebrecht

このコラムは、CLR Threading System および Task Parallel Library のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。

目次
同時実行での一般的な問題
ロックの競合
同期
スケジュール
ワークロードの種類とサイズ
メモリ階層の認識
危険なチューニング
ノイズ
同時実行制御
並列処理の抽出に関する最後の注意点
シングルコアからメニーコア アーキテクチャへの継続的な技術的進化は大きな利点をもたらします。たとえば、複数のコアと並列処理を使用して複数のスレッドを効率的に使用することでパフォーマンスを改善できるスレッド環境について考えてみてください。たとえば、複数のスレッドを使用して、データベースに対して多くの独立したクエリを実行する ASP.NET アプリケーションなどです。
ただし、複数のコアを使用すると、新たな問題が発生します。おそらく、プログラミング モデルと同期モデルの複雑さが増し、同時実行の制御が必要になり、パフォーマンスのチューニングと最適化がより困難になります。また、パフォーマンスと同時実行アプリケーションの動作に影響する新しい要因の多くは、よく理解されていません。したがって、目標とする最適化がアプリケーション固有ではなく汎用的な場合には特に、パフォーマンスのチューニングが難しくなります。
CLR のスレッドは、多くの要因 (複数コア アーキテクチャで導入されたものなど) が同時実行の動作とパフォーマンスに影響する可能性のある同時実行環境の例です。ロックおよびキャッシュの競合と過剰なコンテキスト切り替えは、その中のほんの一部です。CLR ThreadPool に対して継続している作業の目的は、並列処理フレームワークをランタイムに統合し、最も一般的な問題のいくつかを内部的に処理することにより、開発者が同時実行と並列処理を簡単に利用して、より高い同時実行レベルと優れたパフォーマンスを実現できるようにすることです。
このコラムでは、開発者が直面する一般的な問題と、特にマルチプロセッサ ハードウェアでマルチスレッド マネージ コードをチューニングするときに考慮する必要のある重要ないくつかの要因について説明します。ThreadPool の今後の変更によって、これらの問題の一部をどのように処理する必要があるか、また、プログラマの作業がどのように軽減されるかについても説明します。さらに、同時実行制御の重要な側面についてもいくつか説明します。これは、高度なマルチスレッド環境でアプリケーションの動作を理解するのに役立つ可能性があります。読者は、CLR での同時実行、同期、およびスレッドの基本概念に精通していることを前提とします。

同時実行での一般的な問題
ほとんどのソフトウェアは、シングルスレッド実行用にデザインされていました。その理由の一部は、シングルスレッド実行のプログラミング モデルによって複雑さが低減され、コーディングが簡単になることです。既存のシングルスレッド アプリケーションは、複数コアで使用するようにうまくスケーリングできません。さらに悪いことに、シングルスレッド モデルを複数コアに適応させることは簡単ではありません。原因の多くは予測可能です。それは、ロックとリソースの競合、競合状態、停止状態などの同期の問題です。その他の原因は、まだよく理解されていません。たとえば、特定の種類 (およびサイズ) のワークロードについて、特定の時点で最善の同時実行レベル (スレッド数) はどのようにして判断できるでしょうか。
シングルコア マシンで動作するようにデザインされた、CLR のスレッド システムについて考えます。タスク (WorkItem) は、1 つのロックで保護された単一リストにキュー登録されます。各スレッドは、キューのアイテムを順番に処理します。これがマルチコア アーキテクチャに移行されると、より高い同時実行レベルでロックの競合が多数発生する場合にパフォーマンスが低下します。興味深いことに、2008 年 10 月号の「Windows と C++」のコラム「高性能アルゴリズムについて調べる」で、Kenny Kerr は「シングル プロセッサ上の適切にデザインされたアルゴリズムは、複数プロセッサ上の不十分な実装よりもパフォーマンスが高い場合がよくあります」と述べています。これは、プロセッサを単純に追加しても、パフォーマンスが向上するとは限らないということを暗に意味しています。
ここからは、大まかに既存のスレッド システムを ThreadPool と呼び、WorkItem の概念をタスク キューのアイテム (たとえば、QueueUserWorkItem) と呼びます。シングルコア マシンでは、同時実行の管理は、プロセッサの時間を各スレッドに分割することで、タスクの慎重なスケジュールによって処理できます。複数のコアがあるので、作業項目をコアに公平に配分する方法、および正確性とパフォーマンスの基礎となるメモリ階層について検討し、より高い同時実行レベルを制御および活用する方法を決定する必要もあります。
公平性とパフォーマンス
同時実行環境では、タスクの公平性とパフォーマンスの矛盾が深刻です。公平なタスク スケジューリングと全体的に高いパフォーマンスを同時に実現することはほぼ不可能です。たとえば、キュー内で実行を待機している多数の WorkItem について考えます。公平にするには、各 WorkItem を到着した順に実行 (処理) する必要があります。しかし、これでは最適なパフォーマンスが得られません。
オペレーティング システムの観点からは、優れたパフォーマンスを実現するのに適した方法は、"ホット" なアイテム (最近使用したアイテム) をキャッシュ内に保持して、メイン メモリへの長い道のりを避けることです。しかし、このアプローチはアイテムを順番に処理するという前に述べた概念と矛盾します。最新の情報をキャッシュに維持する最善の方法は、キュー内の最後のアイテムを最初にデキューすることです。
ThreadPool 環境では、主に公平性に焦点を当ててきましたが、次のバージョンの目標はパフォーマンスを改善することです。この目的で、work-stealing アルゴリズムなどの戦略が組み込まれています。work-stealing アプローチでは、複数のキュー (プロセッサあたり 1 つなど) があり、キュー内の "コールド" アイテムを、たとえば別のプロセッサで "盗用" して実行できます。これにより、ある程度の公平性を維持したままパフォーマンスを改善できるため、多くの同時実行アプリケーションに利益があります。

ロックの競合
同時実行環境では、さまざまな理由でロックの競合が頻繁に発生することがあります。たとえば、ThreadPool では、WorkItem がタスク キューに追加されるたび、またデキューが試行されるたびにロックが取得されます。多くの小さい WorkItem があり、ロックを頻繁に取得する必要がある場合に競合が発生します。作業項目が到着し、大きなバーストとしてキューに登録された場合 (これらのバースト中にロックが頻繁に取得されるため)、また同時実行制御に欠陥がある場合などです。ThreadPool は、作業項目が使用可能になるたびにすべてのスレッドをアクティブにするようにデザインされているため、多くのスレッドがあり、キュー内に作業項目がない場合に、単一の WorkItem が到着すると、すべてのスレッドが同時にアクティブになり、ロックの取得を試行することがあります。
スレッドはロックを待機している間にスピンし、これらがビジー スレッドであるという確信につながるため、この問題はさらに複雑になる可能性があります。このときに、WorkItem がさらにエンキューされた場合、新しいスレッドがさらに追加され、それらのロックも取得を試行するため、サイクルが継続します。一部の ASP.NET アプリケーションではこの問題が頻繁に発生します。
この最後の状態の対処方法は、一度に解放するスレッド数をさらに制限することでした。あいにく、この方法では解放されるスレッド数が少なすぎて、ThreadPool 全体が行き詰まることがありました。競合を軽減するために ThreadPool に対して継続中の作業は、特定のロックの削除から構成されていました。

同期
同期のデザインを最適化する必要がありますが、同期プリミティブの使用は簡単ではありません。同期プリミティブを選択し、(各操作の頻度に応じて) 読み取りロックと書き込みロックのどちらを使用するかを決定する必要があります。また、マルチコア システムでは、シングルプロセッサ マシンの場合よりもプリミティブの使用のコストが高くなることがあります。
その例は、かなりうまく実行されているシングル プロセッサのミューテックスであり、ワークロード分散の公平性を促進できますが、アプリケーションがメニーコア環境に移行されると公平性が悪化します。その場合、ミューテックスによって実現されている公平性が、パフォーマンスを害し始めます。なぜなら、作業項目が本来待機する必要のない場所で待機する必要があるからです。場合によっては、Microsoft .NET Framework でモニタを使用する場合のように、ロックの待機よりもスピンの方がよい状態になることがあります (パフォーマンスとスレッド同期の詳細については、「同時実行の問題」のコラム「パフォーマンスを意識したスレッド同期」を参照してください)。

マルチコア環境でのスケジューリング タスクは、非常に大きなトピックであり、詳細な説明はこのコラムの範囲外です。ただし、すべての実装に共通した問題がいくつかあります。たとえば、作業項目の処理戦略はどのようにして選択するのでしょうか。答えはアプリケーションによって異なります。最後にキューに登録された WorkItem が最新のアクティビティを表していると判断できるため、キューの深い場所にある WorkItem よりもこれらの WorkItem を先に処理すると、パフォーマンスが向上することがあります。

ワークロードの種類とサイズ
パフォーマンスは、アプリケーションのワークロードの種類 (またはサイズ) によって異なる場合があります。CPU バウンドのワークロードではスケジューリングと同期の問題が多く、CPU バウンドでないワークロードは同時実行レベルの制御に影響することが多いと気付く場合があります。また、前に述べたように、複数のコアで使用するようにアプリケーションをスケーリングすることは難しい場合があります。
次の例は、この問題を示しています。ThreadPool を使用して、単純な実験アプリケーションを異なる数のコアで実行しています (コアは同じマシン上にありますが、異なるプロセス アフィニティ マスクを使用しています)。図 1 に、典型的な結果をいくつか示します。複数のコアを使用して問題がうまくスケーリングされるかどうかを調べるために、ワークロードが変更されています。
図 1 コア数を増やした場合の異なるワークロードに対するスループット結果
0 という "実験的" なワークロードは、より大きい (また、より現実的な) ワークロードよりもパフォーマンスが優れています。この意味では、アプリケーションは、より多くのコアで、より大きいワークロードに対するパフォーマンスがよくなるように適切にスケーリングされていません。一方、より大きいワークロードに対応する下部の線 (水色と青) は、コアが追加されるとスループットが実際に増加することを示していますが、実験的な空のワークロードのスループットはわずかな低下を示しています。さらに、ワークロードが大きいほど、ロック競合が増える可能性があります。ご想像どおり、これらすべての落とし穴に単一の汎用的な最適化手法で対処することは困難です。

メモリ階層の認識
メモリ階層を考慮することは、パフォーマンス チューニングで最も重要な問題の 1 つです。コアの数が多い場合には特に、メモリの局所性を利用することが不可欠です。Non-Uniform Memory Access (NUMA) アーキテクチャでは、"遠い" ノードにあるメモリ位置へのアクセスはコストが高く、パフォーマンスに大きな悪影響を与えることがあります。基礎となるメモリ階層を理解することは、スケジューリングの目的で重要です。たとえば、マルチプロセッサ環境でメモリ アクセスをよりローカルに維持する方法は、同じプロセッサの特定のタスクで動作するようにスレッドを維持することです。ThreadPool は NUMA 対応ではありませんが、work stealing などの戦略を追加すると、メモリの局所性が必然的に利用されます。同様のタスクは、ローカル キュー登録とデキューにより同じノードに残る傾向があります。
一般に、キャッシュの効率的な利用によって、パフォーマンスが大幅に向上することがあります (メモリ階層の詳細については、「パラダイム シフト: 並列プログラミングの設計上の考慮事項」および「CLR 徹底解剖: スケーラビリティのための同時実行の使用」を参照してください。

危険なチューニング
難しいことに、なんらかのパフォーマンス改善をもたらすためにデザインされたロジックの導入が逆の効果をもたらすケースがあります。典型的な例は、このような改善が汎用的な最適化を提供するようにデザインされている場合です。一部のアプリケーションでは、大量のオーバーヘッドが追加されるだけで、パフォーマンスが低下する結果になります。
複数のコアで機能するようにさまざまな同期メカニズムがアプリケーションに追加されるケースを考えてみます (たとえば作業項目を公平に配分するため)。同期によって複雑さが増し、より簡単なソリューションの方がうまく動作する可能性のある単純なアプリケーションに不必要なオーバーヘッドが生じる結果になることがあります。同時実行性の高いアプリケーションでは、有意義なチューニング パラメータを識別するのが困難です。たとえば、CPU 使用率を最大化しても、必ずしもスループットが向上するとは限りません。シングルプロセッサ マシンでは向上しても、メニーコア マシンでは、CPU 使用率を最大化しないでスレッドが時間を "浪費する" 方がよいことがあります。

ノイズ
ノイズも、パフォーマンス チューニングを難しくする別の要因です。この説明で示したすべての実験は、専用マシンで実行しました (非常にコストの高い他のアプリケーションは同時に実行しませんでした)。しかし、大きなノイズはありませんでしたが、理解するのがかなり難しい予期しないバリエーションがありました。
現実のシステムでは、複数のアプリケーション (したがって、より多くのスレッド) がリソースを競合することがよくあります。ここで使用している ThreadPool の場合、同時に実行しているさまざまなアプリケーション ドメインによってノイズが生じることもあります。

同時実行制御
環境のスレッド数 (同時実行レベル) は、環境とそのアプリケーションの動作およびパフォーマンスに影響するため非常に重要です。たとえば、コアあたり 1 スレッドの低い同時実行レベルは、CPU を集中的に使用するアプリケーションに適しています (さらに、システムのスレッドの最小数は、通常はコアの数と同じに設定されます)。一方、高い同時実行レベルでは、コンテキストの切り替えやリソース競合が過剰になることがあります。
図 2 に、同時実行レベルがアプリケーションのパフォーマンスにどのように影響するかについての典型的な例を示します (青いドットは各同時実行レベルでキャプチャされた観察です。黒い線は、これらの実験の近似曲線を示しており、動作を説明する機能の種類を視覚化するのに役立ちます)。このコンテキストでパフォーマンスを測定する方法は、1 秒間に完了した WorkItem の数として測定されるスループットを使用することです。
図 2 同時実行レベルの作用としてのスループット
このプロットは、WorkItem を一定期間実行する実験的なアプリケーションを使用して生成しました (各 WorkItem の実行時間は 100 ミリ秒で、CPU 使用率が 10 ミリ秒、待機時間が 90 ミリ秒でした)。デュアルコア 2 GHz マシンで実験を複数回実行して、データを収集し、潜在的なノイズとバリエーションを評価しました。このマシンでの理論的な最大スループットは、20 の同時実行レベルで 1 秒あたり 200 の完了です。ただし、実験では、20 の同時実行レベルに達する少し前にスループットが 150 でピークに達し、その後は低下しています。低下の主な原因はコンテキストの切り替えです。
この実験中に、CPU が最大化されました (ただし、特定の時点後は、これによって最適なスループットが得られました)。100% の CPU 使用率は、実際には使用されていないスレッドのメモリからデータが取得されることを意味している場合があります。これにより、現実には CPU 時間を浪費しているときに、アプリケーションの生産性が高いように見えることがあります。この状況は、シングルプロセッサ環境でも発生しますが、メニーコアでより明らかになります。
前に見たように、欠陥のある同時実行制御によって、特定の問題の発生が増加することがあります。並列処理が使用されている場合は特にその傾向が顕著です。また、複数のタスクの同時実行管理は、複数コアが存在する場合にさらに難しくなりました。
一例は、メモリ階層の問題に関連します。図 2 の例に基づいて単純な原理に従うと、同時実行レベルをあるポイントまで上げることに利点がありますが、マルチコア環境では、スレッドが必要な場所を簡単には判断できません。たとえば、他のプロセッサが利用されているときに、プロセッサがなんらかの理由 (たとえば、不適切なスケジューリングなど) で過負荷になっている場合、全体的なスループットが非常に低くなることがありますが、新しいスレッドを追加すると、それらが不適切な処理単位に間違って割り当てられることがあります。
反対に、同時実行レベルが非常に高くても、全体的なスループットが低下し始める場合に、スレッドの削除を開始することに決める場合がありますが、削除されるスレッドは、実際にスレッドを必要とし、スレッドを追加することでスループットを改善できるプロセッサから取り去られることがあります。
図 3 に、同時実行レベルの変更を単純に試行すると予期しない動作が引き起こされる様子を示します。実験は、実行するさまざまな数の作業項目 (WorkItem) と各 WorkItem の実行時間 (ワークロード) を使用した、テスト アプリケーションの一連の実行から構成されます。
図 3 同時実行レベルを変更すると予期しない動作が引き起こされる
図 3 の上のセクションは、ThreadPool でこのテストを実行した結果です。スループットはよくありませんが、すべての試行で一貫した結果を示しています。図 3 の下半分は、同時実行制御ロジックに対して単純な調整を行い、スレッドを追加および削除する方法を改善しようとした結果を示しています。スループットには多くのバリエーションがあり、作業量に関係なく、調整によって制御が向上するというよりノイズが増えているように見えます。
一部のポイントは、値の特定の組み合わせで改善されるように見えます。これは、アプリケーションの種類に応じて異なる種類の同時実行制御の調整が必要であることを示唆しています。
この問題への対処として考えられる 1 つのアプローチは、なんらかの種類の動的チューニング ロジックを組み込んで、(たとえば、統計的な方法を使用して) アプリケーションに基づいて同時実行レベルを調整することです。同時実行制御とチューニングのアプローチの詳細な検討も、説明の対象外です。
適切なチューニングによって、パフォーマンスが向上することがあります。次の例は、このテスト アプリケーションのために ThreadPool の多くの要因とパラメータが手動でチューニングされた類似の実験を示しています。
パフォーマンス結果を図 4 に示します。改善 (赤い線で示されています) は、主にスレッドを追加した結果であり、これによって同時実行レベルが急速に増加します。ThreadPool (青い線で示されています) では、同時実行レベルの増加が必要と思われるときに 1 つのスレッドのみ追加されています。この実験では、チェックは 250 ミリ秒ごとの間隔で行われます。さらに、特定のポイント後に、それ以上の改善はないため、スレッドの追加が終了しています。
図 4 ThreadPool の手動チューニングによる改善
この実験では、WorkItem がキューに登録され、それぞれが CPU 作業を行ってから、一定時間ブロックされます。30 秒ごとに、ブロック時間が変更されます。最初の 30 秒では 1 ミリ秒で終わり、次には 2 ミリ秒で終わり、再び 1 ミリ秒に切り替わります。90 秒のマークで、作業は 10 秒間停止します。その後、前のブロック サイクルが継続します。青い線は、既存の ThreadPool を使用したパフォーマンス結果を示しています。赤い線は、この ThreadPool を手動でチューニングした場合に得られる改善を示しています。

並列処理の抽出に関する最後の注意点
並列処理の導入により、パフォーマンスが向上する可能性がありますが、並列の概念とアルゴリズムを既存のアプリケーションに組み込むのは簡単ではありません (たとえば、「同時実行の障害: マルチスレッド コードでよく見られる 11 の問題を解決する」で概説されているような多くの落とし穴があります)。
Task Parallel Library (TPL) は、.NET Framework に対する拡張機能であり、Parallel Computing Platform チームが開発しました。TPL は、同時実行を公開する方法と、並列処理を利用するためのサポートを提供します。TPL は、MSDN で、この開発に関する詳細情報と共に現在使用可能になっています。
もちろん、公開された並列処理の利用は、平均的なプログラマにとってシームレスではありません。考慮が必要な要因がまだ多数あるからです。また、図 1 で示したように、アプリケーションは必ずしも適切にスケーリングされません。(再帰的な性質を持つ) 分割統治手法を使用して問題を並列処理する場合にも同じことが言えます。これらのアルゴリズムは、ThreadPool (再入可能なコードを有効にしません) のデッドロックの一般的な原因であるため、これは厄介な場合があります。最後に、結果を特定の順序で計算する逐次アプリケーションの通常のケースについて考えます。このアプリケーションが並列処理される場合、結果は、同時実行タスクによって異なる順序で計算されることがあります。さらに、必要に応じて結果を並べ替えて正確性と機能性を確保するのはプログラマの責任です。
同時実行および並列環境で実行するようにプログラムを調整することは、簡単ではありません。汎用的なチューニングはさらに難しく、すべてのアプリケーションで最適に動作することは保証できません。同時実行環境の基礎となる構造を理解すると、チューニングするパラメータや採用する最適なプログラミング戦略の決定に役立つことがあります。将来の ThreadPool バージョンに向けて継続中の作業には、手動チューニングの必要性の軽減が含まれます。

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

Erika Fuentes 博士は、CLR チームのプログラム マネージャであり、同時実行のコア オペレーティング システムに携わっています。統計、適応システム、および科学計算の分野に、いくつかの学術的な出版物があります。

Eric Eilebrecht は、CLR チームのシニア ソフトウェア開発エンジニアであり、コア オペレーティング システムに携わっています。マイクロソフトには 11 年間勤務しており、CLR チームに参加する前は、Hotmail のバックエンド ストレージ システムに携わっていました。


Advanced Basics
ObservableCollection クラス
Ken Getz
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照
目次
ObservableCollection クラスを導入する
サンプルを調べる
コードを調べる
Windows フォーム アプリケーションを作成中であり、既に DataGridView コントロールを標準的な List(Of Customer) データ構造にバインドしているとしましょう。このとき、グリッド内の項目と基になるデータ ソースの値の同期を維持する (つまり、他のコードやフォームで List 内の顧客データが変更された場合に、変更後のデータがグリッドで更新および表示されるようにする) 必要があるとします。
Windows フォームを使用した場合、こうした処理は可能ですが、ごく一般的なものにとどまります。更新作業は行えるものの、厳しい制限が伴うのです。たとえば、条件さえ揃っていれば、更新内容をグリッドに瞬時に反映できますが、新しい行がデータ ソースに追加されたときにグリッドに新しい行を簡単に追加することはできません。そこで、Windows Presentation Foundation (WPF) が Microsoft .NET Framework にもたらす機能を活用してください。バインドされたコントロールとそのデータ ソースの同期を確実に維持できるようになります。ここでは、WPF に用意されている ObservableCollection クラスの使用方法について説明します。
ObservableCollection クラスを使用すると、バインドされたコントロールと基になるデータ ソースの同期を WPF アプリケーションで維持できるだけでなく、さらに有用な情報を得ることもできます。ObservableCollection クラスは、コレクションの項目の追加、削除、移動、更新、または置換が行われた際に、CollectionChanged イベントも発生させるためです。この機能により、ウィンドウ外のコードによって基になるデータが変更されたときに対処することができます。今月のサンプル アプリケーションでは、この情報の使用方法を確認できます。この点については次のセクションで説明します。

ObservableCollection クラスを導入する
System.Collections.ObjectModel.ObservableCollection(T) クラスは Collection(T) を継承します。これはジェネリック コレクションの基本クラスであり、INotifyCollectionChanged インターフェイスと INotifyPropertyChanged インターフェイスの両方を実装します。コレクションを興味深いものにするのは、INotifyCollectionChanged インターフェイスです。これは、コレクションが変更されているかどうかをバインドされたオブジェクト (およびコード) で判断できるようにするインターフェイスです。
注意が必要なのは、ObservableCollection クラスはその要素の変更についての情報をブロードキャストするが、その要素のプロパティの変更には関知しないという点です。つまり、このクラスは、そのコレクション内の項目のプロパティ変更通知は監視しません。
コレクション内のいずれかの項目のプロパティが変更されているかどうかを把握する必要がある場合は、コレクション内の項目に INotifyPropertyChanged インターフェイスを実装し、それらのオブジェクトに対してプロパティ変更イベント ハンドラを手動でアタッチする必要があります。どのような方法でコレクション内のオブジェクトのプロパティを変更しても、コレクションの PropertyChanged イベントは発生しません。実際のところ、ObservableCollection の PropertyChanged イベント ハンドラは保護されています。イベントをクラスから継承して自分で公開しない限り、そのイベントに応答することすら不可能です。もちろん、継承したコレクションからコレクション内の各項目の PropertyChanged イベントを処理することはできます。ただし、サンプル アプリケーションでは、筆者はもっと単純なアプローチを採用して、クライアント アプリケーションで個別の項目の変更イベントを処理できるようにしています。
継承および保護されたメンバを無視した場合に残る興味深いメンバは、メンバをコレクション内の新しい場所に移動できる Move メソッドと、コレクションのコンテンツの変更に関する情報をブロードキャストする CollectionChanged イベントだけです (ただし、ObservableCollection クラスのすべてのメンバの派生元である Collection 基本クラスを熟知していることを前提とします)。このコラムを読み進める前に、これらの機能がよくわかるサンプル WPF アプリケーションをダウンロードしてインストールしてください。
サンプルを調べる
サンプル ソリューション ObservableCollectionTest には、ObservableCollection を継承する CustomerList というクラスが含まれています (図 1 を参照)。ご想像どおり、CustomerList クラスは Customer オブジェクトを含む ObservableCollection インスタンスを公開します。ただし、コードを調べてみると、クラスが公開しているリストは 1 つだけなので、クラスの複数のコンシューマがそれぞれ同じコレクションへの参照を取得することがわかります (これがこのデモの重要なポイントです。他のアプリケーションには該当しません)。このクラスはプライベート コンストラクタを提供するため、クラスのインスタンスを取得するには、既存のコレクション インスタンスを分配する GetList 共有メソッドを呼び出す必要があります。
Private Shared list As New CustomerList

Public Shared Function GetList() As CustomerList
  Return list
End Function
図 1 CustomerList
System.Collections.ObjectModel
Imports System.ComponentModel

Public Class CustomerList
  Inherits ObservableCollection(Of Customer)

  Private Shared list As New CustomerList

  Public Shared Function GetList() As CustomerList
    Return list
  End Function

  Private Sub New()
    ' Make the constructor private, enforcing the "factory" concept
    ' the only way to create an instance of this class is by calling
    ' the GetList method.
    AddItems()
  End Sub

  Public Shared Sub Reset()
    list.ClearItems()
    list.AddItems()
  End Sub

  Private Sub AddItems()
    Add(New Customer("Maria Anders"))
    Add(New Customer("Ana Trujillo"))
    Add(New Customer("Antonio Moreno"))
  End Sub
End Class
プライベート コンストラクタは AddItems メソッドを呼び出します。一方、Reset パブリック共有メソッドはリストをクリアしてから AddItems メソッドを呼び出します。どちらを使用しても、コレクションに 3 人の顧客を追加できます。
Private Sub AddItems()
  Add(New Customer("Maria Anders"))
  Add(New Customer("Ana Trujillo"))
  Add(New Customer("Antonio Moreno"))
End Sub
この例では、Customer クラスは非常に単純です (つまり、必要な機能を説明するにあたって複雑すぎないということです)。図 2 に示すクラスに含まれているのは Name プロパティだけなので、プロパティ値が変更された際にクラスのインスタンスのコンシューマ (データ バインド コントロールを含む) に通知が行われるようにする INotifyPropertyChanged インターフェイスをクラスが実装しているという事実がなければ、説明すら必要ないでしょう。
図 2 PropertyChanged イベントを含む Customer クラス
Imports System.ComponentModel

Public Class Customer
  Implements INotifyPropertyChanged

  Public Event PropertyChanged( _
    ByVal sender As Object, _
    ByVal e As PropertyChangedEventArgs) _
    Implements INotifyPropertyChanged.PropertyChanged

  Protected Overridable Sub OnPropertyChanged( _
    ByVal PropertyName As String)

    ' Raise the event, and make this procedure
    ' overridable, should someone want to inherit from
    ' this class and override this behavior:
    RaiseEvent PropertyChanged( _
      Me, New PropertyChangedEventArgs(PropertyName))
  End Sub

  Public Sub New(ByVal Name As String)
    ' Set the backing field so that you don't raise the
    ' PropertyChanged event when you first create the Customer.
    _name = Name
  End Sub

  Private _name As String
  Public Property Name() As String
    Get
      Return _name
    End Get
    Set(ByVal value As String)
      If _name <> value Then
        _name = value
        OnPropertyChanged("Name")
      End If
    End Set
  End Property
End Class
クラスが INotifyPropertyChanged インターフェイスを実装する場合は、PropertyChanged イベントを指定する必要があります。
Public Event PropertyChanged( _
  ByVal sender As Object, _
  ByVal e As PropertyChangedEventArgs) _
  Implements INotifyPropertyChanged.PropertyChanged
標準的な .NET の設計パターンを使用してイベントを発生させるために、Customer クラスには、保護されたオーバーライド可能な OnPropertyChanged プロシージャが含まれています。このプロシージャがイベントを発生させます。
Protected Overridable Sub OnPropertyChanged( _
  ByVal PropertyName As String)

  ' Raise the event, and make this procedure
  ' overridable, should someone want to inherit from
  ' this class and override this behavior:
  RaiseEvent PropertyChanged( _
    Me, New PropertyChangedEventArgs(PropertyName))
End Sub
次に、新しい値がプロパティの現在の値と異なる場合は、Name プロパティの定義内でプロパティ セッターが OnPropertyChanged メソッドを呼び出します。
Private _name As String
   Public Property Name() As String
     Get
       Return _name
     End Get
     Set(ByVal value As String)
       If _name <> value Then
         _name = value
         OnPropertyChanged("Name")
       End If
     End Set
   End Property
クラスが PropertyChanged イベントを発生させるからこそ、そのクラスやクラスのインスタンスのコレクションを使用するコードで PropertyChanged イベントに応答し、プロパティが変更されたという事実に基づいて処理を実行できるようになります (PropertyChangedEventArgs クラスは、PropertyName プロパティだけを標準的なイベント引数に追加し、プロパティの新旧の値に関する情報は提供しないという点に注意してください。後で説明するように、サンプル アプリケーションではこの制限を回避して、変更されたプロパティの新しい値だけは特定します)。
サンプルには、MainWindow という名前の単一の WPF ウィンドウも含まれています (図 3 を参照)。ウィンドウのマークアップで唯一重要な要素は、ListBox コントロールの定義です。この定義には、コントロールの ItemsSource プロパティの宣言型データ バインドが含まれています。このバインドは、コントロールがそのデータを MainWindow クラスの Data という名前のプロパティから取得することと、コントロールが Data プロパティ内の各項目の Name プロパティを表示することを示しています。
<ListBox
  DisplayMemberPath="Name"
  ItemsSource=
    "{Binding ElementName=MainWindow, Path=Data}"
  Grid.Column="3" Grid.RowSpan="3" Name="ItemListBox"
  Margin="5" />
図 3 サンプル WPF ウィンドウ (画像をクリックすると拡大表示されます)
MainWindow の分離コード クラスには次の宣言が含まれています。
Public WithEvents Data As CustomerList = CustomerList.GetList()
図 3 に示すように、このコードは CustomerList インスタンスのコンテンツをウィンドウに公開します。
分離コード クラスの残りのコードを調べる前に、ひとまずアプリケーションを試してみてください。ウィンドウの ListBox は ObservableCollection を継承するクラスにバインドされているため、リスト ボックスには常に最新のコレクションのコンテンツが表示されると思われるのではないでしょうか。デモのウィンドウを見ると、それが正しいことがわかります。
また、サンプルには 2 つの独立したメイン ウィンドウ インスタンスが表示されます。両方のウィンドウの ListBox コントロールは同じ ObservableCollection インスタンスにバインドされているため、一方のウィンドウで行った変更は両方のウィンドウに反映されます。2 つのウィンドウ インスタンスを開くために、Application.xaml ファイルには次のマークアップが含まれています。このマークアップでは、アプリケーションの起動時に Application_Startup プロシージャのコードを実行するように指定しています。
<Application x:Class="Application"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Startup="Application_Startup">

  <Application.Resources>

  </Application.Resources>
</Application>
Application.xaml.vb 分離コード ファイルには、MainWindow.xaml の 2 つのインスタンスを作成する次のスタートアップ コードが含まれています。各インスタンスには独自のタイトルがあります。
Private Sub Application_Startup( _
  ByVal sender As System.Object, _
  ByVal e As System.Windows.StartupEventArgs)

  Dim window As New MainWindow
  window.Title = "Observable Collection 1"
  window.Show()

  window = New MainWindow
  window.Title = "Observable Collection 2"
  window.Show()
End Sub
次の手順に従って、サンプル アプリケーションを試してみてください。
  1. Visual Studio 2008 からサンプル アプリケーションを読み込んで実行します。同じウィンドウの 2 つのインスタンスが表示されます。
  2. ウィンドウの左側のコンボ ボックスをクリックして開きます。コントロールには、3 人の現在の顧客に対応する 0 ～ 2 の数字が含まれています。1 を選択すると Ana Trujillo が選ばれ、その名前がテキスト ボックスにコピーされます。
  3. 一方のウィンドウで [Delete] (削除) をクリックします。両方のウィンドウから Ana Trujillo の表示が消えます。2 つの ListBox コントロールが同じ ObservableCollection インスタンスにバインドされており、そのバインドによって更新内容が瞬時に反映されるためです。コンボ ボックスをもう一度開きます。顧客が 2 人だけになっていることに注目してください。両方のウィンドウで、両インスタンスが最新の状態であることを確認します。
  4. さらに 2 回 [Delete] (削除) をクリックし、すべての顧客を削除します。[Reset Data] (データのリセット) をクリックし、両方のウィンドウのリストに顧客を再追加します。
  5. [Add New Item] (新しい項目の追加) ボタンの隣のテキスト ボックスに自分の名前を入力し、[Add New Item] (新しい項目の追加) をクリックします。新しい名前が両方の ListBox コントロールに表示されます。コンボ ボックスをクリックして開き、0 ～ 3 の数字が含まれていることを確認します (各数字は 4 人の顧客それぞれに対応しています)。両方のウィンドウのコンボ ボックスが変更されていることを確認します。両方のウィンドウのクラスがコレクションが変更されたことを示すイベントを受け取ったことは明らかです。
  6. 一方のウィンドウの ListBox で名前を選択します。左下のテキスト ボックスで名前を修正し、[Change] (変更) をクリックします。まず、プロパティを変更したことを示す通知が表示されます。通知を無視すると、両方のウィンドウで名前が変更されます (図 4 を参照)。
図 4 コレクションのデータの変更イベントをキャプチャする (画像をクリックすると拡大表示されます)
ウィンドウのユーザー インターフェイスは、サンプル ウィンドウのすべてのコードによって処理されます (顧客名を変更したときに表示された通知と、コレクション内の顧客の数に合わせて項目が変更される ComboBox コントロールは除く)。つまり、ListBox コントロールと ObservableCollection インスタンスの同期を維持する処理を行うえで、特別な作業は必要ありません。こうした処理は WPF によって管理されます。
新しい項目を追加すると、ListBox には完全なリストが自動的に表示されます。項目を変更すると、ListBox には変更されたリストが自動的に表示されます。また、項目を削除した場合も、ListBox には基になるコレクションがそのまま正確に表示されます。したがって、WPF のコントロールへの ObservableCollection クラスのバインドに関しては、「適切に動作している」と言っても差し支えありません。
実際に、内部では "魔法のような処理" が行われています。ListBox を ObservableCollection にバインドする場合、WPF では実際には CollectionView インスタンスを作成します。このインスタンスは、グループ化、並べ替え、フィルタリングなどを処理するデータのビューを提供します。ListBox コントロールには、コレクションの既定のビューが表示されます。同じコレクションに基づく複数の CollectionView インスタンスを作成し、ListBox コントロールごとに異なる方法でデータを表示することもできます (たとえば、データを並べ替えて表示するなど)。今回のコラムのテーマから外れるのでここでは説明しませんが、同じコレクションの複数のビューを表示する必要がある場合は、CollectionView クラスを調べてみてください。詳細については、MSDN の「CollectionView クラス」を参照してください。

コードを調べる
MainWindow クラスは WithEvents キーワードを使用して CustomerList インスタンスを定義するため、ハンドラをコードで手動で追加することなく、ObservableCollection リストのイベントをコードで処理できます。
Public WithEvents Data As CustomerList = CustomerList.GetList()
コードの変更イベント ハンドラの部分には、CollectionChanged イベント ハンドラがあります。このハンドラは、項目がコレクションに追加されたか、または項目がコレクションから削除されたかを確認します。このような変更が行われていた場合、コードはコンボ ボックスのデータ ソースを設定し、必要に応じてウィンドウのボタンを有効にします (図 5 を参照)。
図 5 変更されたコレクションの確認
Private Sub Data_CollectionChanged( _
  ByVal sender As Object, _
  ByVal e As NotifyCollectionChangedEventArgs) _
  Handles Data.CollectionChanged

  ' Because the collection raises this event, you can modify your user
  ' interface on any window that displays controls bound to the data. On
  ' both windows, if you add or remove an item, all the controls update
  ' to indicate the new collection!

  ' Did you add or remove an item in the collection?
  If e.Action = NotifyCollectionChangedAction.Add Or _
    e.Action = NotifyCollectionChangedAction.Remove Then

    ' Set the list of integers in the combo box:
    SetComboDataSource()

    ' Enable buttons as necessary:
    EnableButtons()
  End If
End Sub
この単純なコードで重要なのは、NotifyCollectionChangedEventArgs パラメータです。このオブジェクトは、コレクション内で変更された要素に関する情報に加えて、5 つの興味深いプロパティも提供します。図 6 にこれらのプロパティを示します。
図 6 NotifyCollectionChangedEventArgs パラメータ
パラメータ | 説明
Action | イベントを発生させたアクションに関する情報を取得します。このプロパティには NotifyCollectionChangedAction 値が含まれます。この値は、Add、Remove、Replace、Move、Reset のいずれかです。
NewItems | コレクションの変更に関連する一連の新しい項目を取得します。
NewStartingIndex | 変更の行われたコレクションのインデックスを取得します。
OldItems | Replace、Remove、または Move アクションの影響を受けた一連の古い項目を取得します。
OldStartingIndex | Replace、Remove、または Move アクションが実行されたコレクションのインデックスを取得します。
イベント ハンドラは、これらすべての情報を基に、コレクション内で実行された、イベントをトリガしたアクションを正確に特定できます。サンプル コードでは単に Action プロパティを使用しており、コレクションのサイズが変更された場合は、ComboBox コントロールの整数値のリストを更新します。
If e.Action = NotifyCollectionChangedAction.Add Or _
  e.Action = NotifyCollectionChangedAction.Remove Then
ObservableCollection クラスの説明とは関係ありませんが、コードで ComboBox コントロールのインデックスのリストを設定する方法を確認してみてもおもしろいでしょう。
Private Sub SetComboDataSource()
  ' Set the list of integers shown in the
  ' combo box:
  ItemComboBox.ItemsSource = _
    Enumerable.Range(0, Data.Count)
End Sub
ループを実行して範囲内 (0 からコレクション内で最大のインデックス値まで) の整数値を含むリストを生成する代わりに、このコードでは、単純に Enumerable.Range メソッドを呼び出して、Data.Count 値を含む 0 以上の整数値のコレクションを取得します。このコードでは、返されるコレクションに ComboBox コントロールの ItemsSource プロパティを設定します。必要な処理はこれだけです (Enumerable クラスの詳細については、筆者が以前執筆した 2 つの Advanced Basics コラム「LINQ Enumerable クラス (第 1 部)」および「LINQ Enumerable クラス (第 2 部)」を参照してください)。
コレクション内の項目のプロパティを変更した場合にサンプル アプリケーションに通知が行われるようにするには、コードをもう少し記述する必要があります。クラス内の各プロパティは、コードによってプロパティの値が変更されるたびに PropertyChanged イベントを発生させます (当然のことながら、プロパティの変更と同時に PropertyChanged イベントを発生させるかどうかは、そのクラスの作成者が決定します。このイベントは自動的には発生しないためです。ただし、前に説明した Customer クラスの Name プロパティの場合は、イベントが発生します)。
MainWindow クラスには HookupChangeEventHandler プロシージャが含まれています。このプロシージャは、個別の Customer オブジェクトの PropertyChanged イベントをフックします。
Private Sub HookupChangeEventHandler(ByVal cust As Customer)
  ' Add a PropertyChanged event handler for
  ' the specified Customer instance:
  AddHandler cust.PropertyChanged, _
    AddressOf HandlePropertyChanged
End Sub
HookupChangeEventHandlers プロシージャは、次のように顧客の ObservableCollection 内の各 Customer のイベント ハンドラをフックします。
Private Sub HookupChangeEventHandlers()
  For Each cust As Customer In Data
    HookupChangeEventHandler(cust)
  Next
End Sub
ウィンドウが読み込まれたとき、または [Reset Data] (データのリセット) ボタンがクリックされたとき、このコードは HookupChangeEventHandlers プロシージャを呼び出します。[Delete] (削除) をクリックすると、ウィンドウはコレクションから項目を削除すると共に、イベント ハンドラを削除します。
' From DeleteItemButton_Click

Dim index As Integer = ItemComboBox.SelectedIndex
If index >= 0 Then
  RemoveHandler Data.Item(index).PropertyChanged, _
    AddressOf HandlePropertyChanged

Data.RemoveAt(index)
[Add New Item] (新しい項目の追加) をクリックすると、コードは新しい顧客を作成し、その PropertyChanged イベントをフックします。
' From NewItemButton_Click

cust = New Customer(NewItemTextBox.Text)
HookupChangeEventHandler(cust)
Data.Add(cust)
もちろん、ウィンドウでは ListBox コントロールを ObservableCollection インスタンスにバインドしているため、これらすべての変更はウィンドウの両方のインスタンスに自動的に反映されます。コードによるサポートは必要ありません。事実、コードによるサポートが必要なのは、リストに対する顧客の追加と削除をプログラムで行う場合と、個別の顧客の変更をフックして変更に対処する場合に限られます。
Customer クラス内のプロパティの値を変更すると、クライアント アプリケーションは PropertyChanged イベント ハンドラを通じて通知を受け取ります。HandlePropertyChanged プロシージャ (図 7 を参照) には、アプリケーションの最も複雑なコードが含まれています。変更の通知で提供されるのは変更されたプロパティの名前だけなので、必要に応じてプロパティの現在の値を取得するにはコードを操作しなければならないという点に注意してください。
図 7 リフレクションを使用する HandlePropertyChanged
Private Sub HandlePropertyChanged( _
ByVal sender As Object, _
ByVal e As PropertyChangedEventArgs)

  ' In this particular application, you only want to bother with this
  ' code for the first window, although both will run the code. In this
  ' case, if the event was raised by the window whose title is
  ' "Observable Collection 1" then process the event:
  If Me.Title.EndsWith("1") Then
    Dim propName As String = e.PropertyName
    Dim myCustomer As Customer = CType(sender, Customer)

    ' Unfortunately, no one hands you the old property value, or the new
    ' property value. You can use Reflection to retrieve the new property
    ' value, given the object that raised the event and the name of the
    ' property:
    Dim propInfo As System.Reflection.PropertyInfo = _
      GetType(Customer).GetProperty(propName)
    Dim value As Object = _
      propInfo.GetValue(myCustomer, Nothing)

    MessageBox.Show(String.Format( _
      "You changed the property '{0}' to '{1}'", _
      propName, value))
  End If
End Sub
このプロシージャは、まずコードが 1 回だけ実行されるようにします。ウィンドウのインスタンスは 2 つ開きますが、コードをインスタンスごとに実行して警告を 2 度表示しても意味がないためです。次のコードは、単純にタイトルの最後の文字をチェックし (ウィンドウの Title プロパティを変更していないことを前提としています)、動作を単一のウィンドウに制限します。
If Me.Title.EndsWith("1") Then
  'Code removed here…
End If
次のコードは、変更されたプロパティの名前とイベントを発生させたオブジェクト (つまり現在の顧客) への参照を取得し、格納します。
Dim propName As String = e.PropertyName
Dim myCustomer As Customer = CType(sender, Customer)
次に、リフレクションを使用して System.Reflection.PropertyInfo インスタンスを取得し、プロパティの名前と型を得ます。
Dim propInfo As System.Reflection.PropertyInfo = _
  GetType(Customer).GetProperty(propName)
PropertyInfo オブジェクトと特定の Customer インスタンスの取得後、コードでプロパティの現在の値を取得できます。
Dim value As Object = _
  propInfo.GetValue(myCustomer, Nothing)
アプリケーションの残りのコードでは、ボタンの有効化と無効化、コンボ ボックスとリスト ボックスの同期の維持など、ユーザー インターフェイスの管理を行います。
このアプリケーションでは、ユーザー インターフェイスを更新するために、ObservableCollection クラスによって提供されるバインドのサポートを活用し、その CollectionChanged イベントに応答していますが、このクラスをこの方法で使用する必要はありません。このアプリケーションでは、コンテンツの変更がリスナに通知されるため、使用する List インスタンスまたは Collection インスタンスを ObservableCollection インスタンスに置き換え (作成しているのが WPF アプリケーションではない場合も可能)、イベント ハンドラをフックすることで、コレクションのコンテンツが変更されたことをクライアントに通知できます。
サンプル ウィンドウにおいて、コレクションのサイズが変更されたときにコレクション インデックスに対応する整数値のリストを更新したのと同様に、コレクションにおける変更には、クライアント クラスから必要に応じた方法で対処できます。ただし、コレクション自体が、その子要素のプロパティが変更されたかどうかを通知することはありません。コレクション内の子要素のプロパティが変更されたときにクライアントに通知を行うには、クライアント内でイベント ハンドラをフックする必要があります。
また、サンプル アプリケーションにおけるリッチなデータ バインドのサポートは、WPF アプリケーションでのみ機能するという点にも注意が必要です。Windows フォーム アプリケーションを作成している場合は、コレクションが変更された際に、ObservableCollection インスタンスにバインドされているコントロールのバインドを手動で更新しなければなりません。とはいえ、コレクションによってその変更が通知されるので、少なくとも必要な更新を適時に行うことは可能になりました。

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

Ken Getz は、MCW Technologies のシニア コンサルタントであり、AppDev (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 です。


Cutting Edge
ASP.NET のプレゼンテーション パターン
Dino Esposito

このコラムは、ASP.NET MVC Framework のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。
目次
ASP.NET イベント ハンドラの内部
元の MVC パターン
Model2: MVC の Web バリエーション
ASP.NET MVC Framework と手動の MVC
MVP パターン
Page Controller パターン
階層型 Web アプリケーションでは、プレゼンテーション層は、ナビゲーション ロジック、ビジネス ロジック、およびデータ アクセスと同様に重要です。開発者は、プレゼンテーション層 (主にプレゼンテーション ロジック) が UI テクノロジおよびプラットフォームから独立した状態を維持する必要がありますが、これは必ずしも容易ではありません。そこで、設計パターンが役に立ちます。
今月のコラムでは、ASP.NET のプレゼンテーション層を構築するための設計パターンをいくつか紹介します。最初に取り上げるのは、UI の構築に使用されるすべてのパターンのベースである Model-View-Controller (MVC) パターンです。ASP.NET MVC Framework 以外の ASP.NET に対する MVC の適用性についても説明します。

ASP.NET イベント ハンドラの内部
ASP.NET のプレゼンテーション層は、主に、HTTP ランタイム環境で調整される .aspx ページで構成されます。一般的な .aspx ページでは、特定のユーザー操作 (ボタン クリック、リストの選択など) 後のある時点で HTTP 要求が行われることがあります。ASP.NET Web フォームのプログラミングでは、これらのイベントは、通常はページの分離コード クラスに記述されたメソッド (イベント ハンドラ) によって処理されます。一見、ユーザー操作とシステムの応答の間には直接的なつながりがあるように思えます。デスクトップ アプリケーションにはこうしたつながりが存在する可能性もありますが、ASP.NET には存在しません。
ASP.NET では、AJAX を使用しているかどうかに関係なく、ユーザーによるクリックから更新されたページの表示までの間に数多くの処理が行われます。ただし、ASP.NET MVC Framework を使用して構築した ASP.NET アプリケーションの方が情報のフローは短くなります。
ボタン クリック イベントについて考えてみましょう。開発者は、ボタン クリック イベントを処理するために、Button コントロールの Click イベント ハンドラにコードを記述します。そのコードは、次のようにページの分離コードに格納されます。
void Button1_Click(object sender, EventArgs e)
{
    // Perform any required action
}
ここに、必要な操作のすべてのコードを配置できます。また、オブジェクトで公開される静的メソッドまたはインスタンス メソッドでこれらすべてのコードをグループ化することもできます。
void Button1_Click(object sender, EventArgs e)
{
    // Static method bound to the user action
    ActionManager.Button1Clicked();
}
ユーザーのアクティビティに応答するコードをコントローラと操作にまとめることも可能です。
void Button1_Click(object sender, EventArgs e)
   {
       // Static method bound to the user action
       ThisPageController controller = new
       ThisPageController();
       controller.Button1Clicked();
   }
ここでは、各ページに独自のコントローラ コンポーネントがあること、そして各コントローラにはユーザーが行う可能性のある操作に論理的にバインドされた一連のパブリック メソッドが含まれていることを想定しています。
コントローラにページの状態と新しいビューの生成方法を認識させるには、どうすればよいでしょうか。ユーザーがボタンをクリックし、ASP.NET ランタイムがそれに対応する HTTP 要求を受信および処理した後、サーバーで Button1_Click が呼び出されます。Button1_Click メソッドは、System.Web.UI.Page から派生したクラスによって公開されます。このクラスは UI コントロールにアクセス可能です。
ここで、コントローラのシグネチャに少し手を加え、コントローラがページ内のパブリックな要素にアクセスできるようにしてみます。
void Button1_Click(object sender, EventArgs e)
{
    // Static method bound to the user action
    ThisPageController controller = new ThisPageController(this);
    controller.Button1Clicked();
}
これで、コントローラのシグネチャは現在の ASP.NET ページ (ビュー) への参照を取得できるようになりました。コントローラのメソッドは、所定の処理を行ってから、単純にサーバー コントロールを更新します。そして ASP.NET の標準的なページ ライフサイクルで残りの処理を行い、ブラウザに対して HTML を生成します。
ページ ライフサイクル
図 1 に、ページ ライフサイクルの主要なステップを示します。ASP.NET 要求が Web サーバーに到達し、それを処理する HTTP ハンドラが見つかると、図に示したステップが実行されます。ご覧のように、単純なボタン クリックのコードは、ASP.NET インフラストラクチャ全体のごく一部にすぎません。ページ ライフサイクルは、ASP.NET ランタイムが各要求を処理するために実行する作業の一部です。これについては、この後に説明します。
図 1 ASP.NET のページ ライフサイクル
Button1_Click のコードを実行できるように、ASP.NET ランタイムは、要求された URL に対する所定の動作を保持する Page クラスのインスタンスを作成します。Page クラスは、図 1 のステップに従って要求を処理し、必要に応じてイベント ハンドラを使用します。
コントローラ クラスの Button1Clicked メソッドでは、プレーンな分離コード イベント ハンドラで使用するのと同じ構文を使用して、ページのあらゆるコントロールにアクセスできる必要があります。ただし、UI コントロール (TextBox1 など) を参照するメンバは、Page クラスのパブリック メンバではありません。それらのメンバがコントローラにアクセスできるようにするには、どうすればよいでしょうか。それには、コントローラから操作する UI コントロールのパブリック プロパティを含むカスタム インターフェイスを、分離コード クラスで実装してください。その例を次に示します。
public interface IThisPageView
{
    TextBox TextBox1Property;
    ...
}

// In the codebehind class
public TextBox TextBox1Property
{
    get {return TextBox1;}
}
図 2 は、サンプル コントローラの例です。Button1Clicked については、ASP.NET Web フォームの通常のプログラミングを行い、サーバー コントロールの状態を単純に更新することで次のビューを特定できるようにします。ページのロジックは、ページとは別に開発できる独立したクラスにカプセル化されています。
図 2 ページのコントローラとして機能するサンプル クラス
public class ThisPageController
{
   protected IThisPageView thePage;

   public ThisPageController(IThisPageView page)
   {
      thePage = page;
   }

   public void Button1Clicked()
   {
       // Get the text currently stored in control TextBox1
       string text = thePage.TextBox1Property.Text;

       // Do something: i.e., turn to upper case
       string newText = text.ToUpper();

       // Update the text in control TextBox1
       _thePage.TextBox1Property.Text = newText;
   }
}
ただし、このソリューションは依然としてページのコントロールに緊密に結合されています。そこで、より洗練された Model-View-Presenter (MVP) パターンというソリューションについて考えてみましょう。MVC パターンと MVP パターンの基本について、簡単におさらいします。

元の MVC パターン
MVC パターンは、あらゆる要素を備えた自立型のフロント エンドから脱却するためのアプローチとして、1979 年に考案されました。自立型のビューはクラスです。このクラスは、コンテンツの表示やビューの状態情報の管理を担い、ユーザー操作を最初から最後まで処理する完全なロジックを実装します。
次のコードは、自立型のビューの土台となります。
void Button1_Click(object sender, EventArgs e)
   {
       // Perform any required action. All the code you
       // need to handle the event goes here. A page
       // built in this way is an autonomous view.
       // MVC fights autonomous views.
   }
このようなモノリシック ビューを使用して、基になる層から切り離されたテスト可能なプレゼンテーション層を構築するのは困難です。関心の分離 (SoC) の概念を適用すると、さまざまな機能を混在させるのではなく各層に分離できるため、適度にコンポーネントの結合度を抑え、凝集度を高めることができます。また、SoC を適用すると、次に生成されるページを決定するナビゲーション ワークフローの実装が容易になります。
MVC のアプローチについて説明しているオリジナルの論文「How to Use Model-View-Controller (Model-View-Controller の使用方法)」が、オンラインで提供されています。この論文をまだお読みでない場合は、MVC の原理に関する知識の程度を問わず、目を通しておくことをお勧めします。今日、MVC はプレゼンテーション層を構築するためのパターンとして認識されていますが、もともとは完全なアプリケーションを構築するための手法として考案されました。この点から、一読しておくと役に立ちます。
MVC は大まかに定義されたパターンであり、実装の詳細はアーキテクトの裁量に委ねられています。MVC にこれほど多くのバリエーションがあるのは、おそらくこの特徴のためだと思われます。
MVC では、アプリケーションをモデル、ビュー、コントローラという 3 つの部分に切り分けます。モデルは、アプリケーション データを参照し、アプリケーションの機能を管理し、ビューに状態の変化を通知します。ビューは、ユーザーに表示するコンテンツに関係する要素です。さらに、コントローラはユーザー ジェスチャをモデルのアクションにマッピングし、現在のビューを更新するか、次のビューを選択します。これら 3 つのアクターは、しばしば MVC の 3 要素 (MVC triad) と呼ばれます。図 3 に、自立型のビューと MVC アプリケーションを比較したイメージを示します。
図 3 自立型のビューと MVC の比較
MVC では、ユーザーはビューを操作し、操作 (ボタン クリックなど) はビューによってキャプチャされ、コントローラに転送されます。コントローラは実行すべき処理を決定し、それをモデルとのやり取りを通じて実行します。モデルは、基本的にはビジネス層といくつかの追加機能を組み合わせたものです。具体的には、モデルは UI の更新が必要な変更をビューに通知します。
モデルはビューの詳細は認識しませんが、モデルとビューの間には "オブザーバ" の関係があります。つまり、ビューはモデルへの参照を保持し、自身をモデルに登録して変更の通知を受け取ります。通知を受け取ると、ビューはモデルから新しいデータを取得して UI を更新します。図 4 は、MVC 対話のシーケンス図です。ただし、MVC の実装によっては、コントローラがビューに変更を通知することもあります。
図 4 標準的な MVC 対話 (クリックすると拡大画像が表示されます)

Model2: MVC の Web バリエーション
MVC はデスクトップ アプリケーション向けに考案されましたが、比較的緩やかに定義されているため、Web に応用することができます。よく使用される MVC パターンの Web バリエーションが、Model2 です。Model2 は、今日の ASP.NET MVC Framework に大きな刺激を与えた、歴史のあるパターンの名前です。
Model2 パターンでは、ブラウザを通じてビューをユーザーに公開します。ユーザー操作はブラウザによってキャプチャされ、新しい HTTP 要求に変換されます。要求は、ブラウザから Web サーバーに配置された特殊なコンポーネント (フロント コントローラと呼ばれる) にこの方法で渡されます。フロント コントローラは、Web アプリケーションに対して行われるすべての要求を調整します。
ASP.NET では、フロント コントローラは URL ルーティング HTTP モジュールの形をとります。HTTP モジュールは、要求された操作を実行するために、要求をキャプチャして適切なコントローラにルーティングします。アクション メソッドが返ると、コントローラはビューに更新を指示し、新しい UI 用の新しいデータを渡します。ビューの出力はフロント コントローラ HTTP モジュールによってキャプチャされ、ブラウザに返されます。
図 5 は、標準的な Model2 対話のシーケンス図です。図に含まれているのは一般的なステップだけであることに注意してください。たとえば、ASP.NET MVC パターンの実装では、URL ルーティング コンポーネントはコントローラを使用するだけでなく、何らかの処理も行います。
図 5 ASP.NET MVC Framework における Model2 対話 (クリックすると拡大画像が表示されます)
元の MVC と Model2 には、いくつかの違いがあります。おわかりのように、ビューとモデルの間には、前に述べた "オブザーバ" の関係に基づく元の MVC の構成で見られるようなやり取りはありません。ビューはユーザー操作のキャプチャと処理は行いません。コントローラがビューをレンダリングし、表示データをビューに明示的に渡します。
このコラムの冒頭で簡単に説明したように、ASP.NET で MVC と言うときは、Model2 と MVC の手動実装のどちらを指しているのかを明確にする必要があります (詳細については、MSDN Magazine の記事「Web フォームを使用しないで Web アプリケーションを作成する」を参照してください)。

ASP.NET MVC Framework と手動の MVC
各ページ (またはページのグループ) のコントローラ クラスを手に入れるために、ASP.NET MVC Framework を使用した場合と、MVC パターンの独自の実装を展開した場合とでは、何らかの違いはあるのでしょうか。SoC の観点からすると、大きな違いはありません。どちらの場合も、ビューやモデル (ビジネス層やサービス層へのゲートウェイとなる) から適切に切り離されたコントローラ クラスを手に入れることができます。
テストの容易性という点では、MVC パターンの元の構成よりも Model2 パターンの方が優れています。ビューとモデルが整然と分けられていることと、ビューが非常に小さいことがその理由です。このような特徴があるだけでも、ASP.NET MVC Framework 用に記述されたコードをきわめて効率よくテストできるようになります。そのうえ、Model2 にはビューとコントローラの間で確立できる一種の緩やかなコントラクトが存在します。ASP.NET MVC Framework では、このコントラクトは ViewData コンテナ オブジェクトで表されます。また、ビュー クラスの ViewPage<T> 基本クラスで表されることもあります (こちらの方が望ましい方法です)。
ASP.NET Web フォームと比較したとき、Model2 の実装には、要求された操作の実行が速いという特徴もあります。Model2 の実装で必要なのは、HTTP 要求を処理してコントローラを呼び出すコンポーネントだけです。ページ クラスを動的に作成する必要はありません。コントローラの検索は、Web フォームのシナリオで HTTP ハンドラを検索する場合よりもはるかに単純なアルゴリズムに基づいて行われます。同様に、ページ ライフサイクル、viewstate、サーバー コントロールも必要ありません。ASP.NET MVC Framework などの Model2 の実装では、はるかにアジャイルなランタイム環境を実現できます。この点について、さらに詳しく見ていきましょう。
Web フォーム プログラミング モデルに基づいて実装される MVC シナリオでは、ユーザー操作から HTTP ポストが生成されます。Web サーバーがこれらの要求をキャプチャし、ページ クラスにマッピングします。Page クラスは、図 1 に示すような独自のライフサイクルを通じて処理されます。次に、Page クラスは適切なコントローラを特定し、メソッドを呼び出します。メソッドが実行されると、モデルが変更されます。ビューの更新はページ ライフサイクルの残りのステップで行われます。
ASP.NET MVC Framework を使用して実装される Model2 のシナリオでは、次のビューの生成ははるかに簡単なプロセスとなります。URL ルーティング モジュールが URL を解析し、使用するコントローラをその URL から特定します。そしてコントローラでアクションを呼び出します。次に、コントローラが新しいビュー用の新しいデータを収集し、その情報を渡します。ビューでは、キャプチャされた要求に対する応答としてブラウザに返される HTML をアセンブルします。ビューは、実質的にテストが不要な受動的な要素です。そのため、開発者はコントローラのテストに注力できます。
Model2 は、アプリケーションのテストの容易性を高めることのできる優れた代替パターンです。しかし、MVC パターンで得られるセッション、キャッシュ、サーバー コントロール、さらには viewstate をサポートする Web フォームのランタイム環境が必要な場合は、どうすればよいでしょうか。このような環境を手に入れるためには、SoC やテストの容易性をあきらめなければならないのでしょうか。決してそんなことはありません。最初に紹介した MVC の別のバリエーション、つまり MVP パターンを採用してください。このパターンは、特別なランタイムなしに Web アプリケーションに応用できます。

MVP パターン
MVC の元の構成には、ある重要な弱点があります。それは、ビューの更新メカニズムです。図 4 の情報のフローを見ると、ビューはユーザー ジェスチャをコントローラに伝達していますが、変更の通知はモデルから受信しています。その後で、ビューは更新されたデータを取得して自身を更新するために、モデルに関する詳細な情報を利用する必要があります。MVP は、3 つの要素をより明確に分離する MVC のバリエーションです。図 6 に、MVP モデルにおけるアクター間の一般的な対話を示します。
図 6 MVP パターンのフロー (画像をクリックすると拡大表示されます)
MVP では、ビューはユーザー入力をプレゼンタに転送し、プレゼンタから更新用の新しいデータを受け取ります。一方、プレゼンタは、モデルとやり取りして要求を処理します。
元の MVC には、ビューが必要とするデータを指定する明示的なコントラクトは存在しません。MVC では、ビューはモデルへの参照を保持し、独自のロジックを実行して必要なデータを選択し、それを UI 要素に反映させます。このロジックのためにビューの受動性が抑えられ、テストが難しくなるのです。 また、ビューは元になる UI プラットフォームにある程度依存します。
MVP では、プレゼンタは実質的にエンド ユーザーとアプリケーション間の仲介役を務めます。プレゼンタには、ビューをレンダリングする機能と、モデルとやり取りする機能があります。結果的に、プレゼンテーション ロジックの大部分はプレゼンタに存在することになります。プレゼンタは UI の含まれないプレーンなクラスなので、本質的にテストのしやすいクラスであると言えます。MSDN Magazine の 2006 年 8 月号の Design Patterns コラム「Model View Presenter (モデル ビュー プレゼンタ)」に、ASP.NET における MVP パターンの実用的な実装が紹介されています。MVP は、Microsoft Web Client Software Factory (ASP.NET 関連のアプリケーション ブロックのコレクション) で最初からサポートされています。詳細については、MSDN の「Model-View-Presenter クイック スタート」を参照してください。
MVP は一般的な UI パターンですが、Model2 とは違って、Web 向けに設計されたわけではありません。Model2 (ASP.NET MVC Framework) と MVP は非常によく似ています。興味深いことに、どちらのパターンも、もともとは汎用パターンとして設計されたわけではありません。
厳密に言うと、Model2 と MVP には 1 つだけ違いがあります。それは、コントローラ (MVP ではプレゼンタ) とビューの間のコントラクトです。コントラクトは、Model2 では大まかに定義されていますが、MVP では厳密に定義されています。
MVP では、ビューがインターフェイスを実装し、プレゼンタはインターフェイス上のメンバだけを使用してビューとやり取りします。たとえば、プレゼンタはインターフェイス内のメソッドと getter を使用してビューの入力データを読み取り、インターフェイス内のメソッドと setter の両方またはどちらか一方を使用してビューの新しい値を設定します。Model2 では、厳密に型指定されたコンテナを使用します。
MVP を使用すると、プレゼンテーション ロジックを UI プラットフォームから切り離すことができるため、異なるプラットフォーム間で同じプレゼンタを再利用しやすくなります。また、同じプレゼンタを同じアプリケーションの別のビューと共に使用できるため、SaaS (Software as a Service) シナリオに対応可能です。さらに、MVP では、ページ間のナビゲーション用のロジックを格納できるコンポーネント (プレゼンタ) を使用できます。内部でロジックを実装するか、Windows Workflow Foundation (WF) コンポーネントを使用するかは、開発者の裁量で決定してください。

Page Controller パターン
MVC、Model2、および MVP は、ASP.NET ランタイムの設計とは無関係なパターンです。ASP.NET の Web フォーム プログラミング モデルは、Page Controller パターンというさらに別の設計パターンに基づいています。Page Controller パターンでは、特定の Web ページ宛てのすべての HTTP 要求を処理する中心的なオブジェクト (分離コード クラスから継承する、動的に生成されるクラス) を作成する必要があります (図 7 を参照)。
図 7 Page Controller 構造
このパターンをカスタマイズすると、ページ間でのロジック (ナビゲーション ロジックを含む) の再利用に関していくつかの利点が得られます。具体的に説明しましょう。単純に Page クラスの階層を作成し、ナビゲーション ロジックとプレゼンテーション ロジックを階層の上位にあるクラスに配置してください。これにより、派生クラスがそれらのロジックを使用できるようになります。ページ コントローラをカスタマイズすると、新しいパターンに移行しなくても、プレゼンテーション ロジックとナビゲーション ロジックの再利用性を高めることができます。
MVP を適用した場合は、実質的にページの分離コード クラスを別の方法で設計することになります (つまり、Page クラスでインターフェイスを実装し、ユーザー ジェスチャを処理するために独立したコンポーネント (プレゼンタ) を使用する)。要求は通常の方法で分離コード クラスにルーティングされるため、ランタイム環境を変更する必要はありません。
ASP.NET MVC Framework プロジェクトを作成する場合は、フロント コントローラ (URL ルーティング モジュール) を有効にしてアクションに関係する要求をコントローラで解決するために、ランタイム環境に変更を加えることになります。どちらの方法も、アプリケーションの開発に影響を及ぼします。
最初に検討する必要があるのは、viewstate とサーバー コントロールが必要かどうかという問題です。これらの要素を組み込みたくない場合もあるでしょう。たとえば、サーバー コントロールがなければ、CSS を使用した UI のスタイル設定ははるかに簡単になります。今のところ、ASP.NET MVC Framework ソリューションで AJAX を使用するよりも、サーバー コントロールで AJAX を使用する方が簡単です。多数のデータ エントリや複雑なテーブル ベースのビューがあるアプリケーションをリッチなサーバー コントロールなしで構築するのは、それほど簡単ではありません。そのため、最終的には妥協が必要です。重要なのは、サーバー コントロールと viewstate が必要かどうかという問題です。はっきりした答えはありません。
Web フォームで進めたほうがよいと判断したが、ある程度 SoC も重要な場合は、MVP が最適です。MVP は重要な UI パターンですが、比較的シンプルなアプリケーションで実装するには、コストが高くつく可能性があります。一方、複数のプラットフォーム間や SaaS シナリオでできるだけ多くのプレゼンテーション ロジックを再利用する必要のあるエンタープライズ クラスのアプリケーションでは、MVP はその真価を発揮できます。

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

Dino Esposito は、IDesign 社のアーキテクトであり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。


実践的なパターン
テストの容易性を高める設計
Jeremy Miller
目次
価値提案
テストの容易性とは
厄介なものを切り離す
フェイクを使って境界条件を確立する
ゲートウェイ パターン
決定と実行とを分ける
大きなテストの前に小さなテスト
全体像
ダイヤルは回さないでください。ソフトウェア設計の基本に関するコラムはまだ続いています。今回は、テストの容易性について述べたいと思います。これは設計の重要な品質の 1 つであると同時に、設計を作成して内容を検討する際に利用できるツールの 1 つでもあります。自動化されたテストの実行には否定できない価値がありますが、問題は、自動化されたテストの利益がそのために費やされる時間や労力の投資に見合うものであるかということです。自動テストの利益とコストは、設計およびアーキテクチャの選択によって大きく影響を受けます。このコラムでは、テストの容易性の向上につながる設計の問題、パターン、および原則について検討します。
このコラムは、読者がテスト駆動型開発 (TDD) の設計手法を使用している、または使用を考えていることを前提としてはいません。より多くの自動テストをプロセスに追加することに関心を持つ方々や、単にソフトウェア設計全般に興味のあるすべての方々にとって、このコラムが役立つことを願っています。
過剰なテスト セットアップのにおい

ProcessWorkflowEvent サンプルで筆者は、インフラストラクチャの問題を結合すると単体テストの労力が増えるという点を強調しています。このサンプルには、より一般的な問題が明らかに存在します。単体テストのセットアップのための過剰な作業は、"code smell (コードのにおい)" の 1 つです。単体テストの過剰なセットアップは、不適切な結合や低い凝集度が原因であることがしばしばです。多くの場合は、手のかかるセットアップを伴う単体テストを記述する前に、コードのリファクタリングを行う方が有益です。

価値提案
開発者はコードを書きますが、その実際の使命はソフトウェアを出荷することです。ソフトウェアを出荷するための労力は、ソフトウェアの設計、コーディング、デバッグ、およびテストに費やす時間の合計です。リーン プログラミングは、局所的な最適化が望ましくないことを教えてくれました。コーディング作業だけに頭を悩ます代わりに、作業全体に目を向けてください。自動テストの価値提案は、コーディング + デバッグ + テストの合計時間を短縮することです。自動テストによって、追加でかかるコーディング時間よりもデバッグとテストの時間短縮の方が大きいのであれば、試す価値があります。
米国には、人生で確実なことは死と税金だけである、という格言があります。では、もう 1 つの格言を付け加えましょう。あなたとあなたのチームが書き上げたコードには、確実にバグがあります (もちろん私の書いたコードにも)。これには、要件の誤解、純粋なプログラミング上のミス、予想外の特殊なケース、期待どおりに動作しないサードパーティ API など、さまざまな理由があります。コードにバグが侵入した原因が何であったとしても、それらの問題は発見して取り除く必要があります。
結局、テストの容易性とは、コードの問題を見つけるために、開発プロセス内に迅速で効果的なフィードバック サイクルを設けることに他なりません。発見が早ければ早いほど問題の修正にかかるコストは低い、というのはソフトウェア開発の原則です。エラーの検出と修正はそのように開発作業の主要な部分の 1 つであるため、開発者として、エラーをより迅速に見つけるためのフィードバック メカニズムに投資することには、時間に見合った価値があります。あるコードベースでの開発が他のコードベースと比較して容易である場合、最も重要な 1 つの要素は、少しのコードを記述してから、そのコードが正しく動作するかどうかに関する何らかのフィードバックを受け取るまでのフィードバック サイクルの短さであると言えます。
モック作成のベスト プラクティス

フェイク オブジェクトには、さまざまなものがあります。このコラムで取り上げているほとんどの例では、スタブを使用しています。スタブは、あらかじめ準備された答えと戻り値を提供する単純なオブジェクトです。別の種類のフェイクとして、クラス間の相互作用を記録または検証するために使用される "モック" オブジェクトがあります。モックは、自動テストで最も重要な (ただし混乱を招き誤用されやすい) 概念の 1 つです。次に、テストでモックを最大限に利用するためのベスト プラクティスをいくつか示します。
  • 多数の呼び出しを必要とする、ADO.NET のような粒度の細かい、またはやり取りの多いインターフェイスに対しては、呼び出しにモックやフェイクを使用しません。かかる手間がそのメリットをはるかに上回り、そのようなテストは一般に読み取り困難なものとなります。IEmailGateway の例のような粒度の粗いインターフェイスにだけモックを使用してください。
  • 完全に理解していないインターフェイスやクラスにはモックを使用しません。自分のコードベースの外部からのインターフェイスに対しては、モックの使用に際して特に慎重になる必要があります。多くの場合、外部 API の周りに、その API の機能を自分のアーキテクチャの用語で表現する独自のアダプタ クラスまたはゲートウェイを記述することをお勧めします。
  • 複数のテストにわたって同じモック セットアップを繰り返している場合は、そのような繰り返しのモック セットアップを必要としているコードを分離するよう設計を変更する必要があります。
  • モック オブジェクトは、まだ存在しないクラスに対するプレースホルダとして使用します。特に、パブリック インターフェイスが何を行うかを決定するための手段として利用します。モック オブジェクト (またはスタブ) を使用すると、最初に実際の実装を構築しなくても、API の形やシグニチャを調べることができます。
モックおよびテスト代替の詳細については、Mark Seemann による MSDN Magazine の記事「単体テスト: テスト代替の連続性について検討する」を参照してください。

テストの容易性とは
テストの容易性とは、コスト効率の高い自動テストを可能にするソフトウェア設計の品質として定義します。テストの容易性の最終的な目標は、コードの欠陥を見つけて取り除くために、開発プロセス内に迅速なフィードバック サイクルを設けることです。テストの容易性のいくつかの側面を次に示します。
繰り返し可能 自動テストは、繰り返しが可能である必要があります。既知の入力に対して予想された結果を取得できる必要があります。既知の状態にセットアップできないシステムでは自動テストを実行できません。
記述が容易 テストは機械的に簡単に記述できる必要があります。これはごく単純な方程式に基づきます。あるコードの単位に対して、自動テストへの入力をセットアップするのに多大な作業を要する場合、そのテストの記述に費やす投資は回収できません。
理解しやすい テストは読みやすく、意図が明確にわかる必要があります。記述した自動テストが、設計や要件に関する便利なドキュメントとしても利用できるのが理想です。実世界では、既存のコードベースを変更すると、ある時点で既存の自動テストが壊れてしまいます。その場合、意図が明確にわかる形で自動テストが記述されていれば、開発者がテストが壊れた理由を理解してそれを修正する際の助けとなります。自動テストの適用範囲は、チームがコードの変更に対して自信を持つことができ、不可解なテストを壊してしまうことを恐れなくて済むようなものでなければなりません。
高速 自動テストはすばやく動作する必要があります。繰り返しますが、目標は、問題を発見するための迅速なフィードバック サイクルを確立して、開発プロセス全体を短縮することです。実行の遅いテストは、生産性を低下させます。
以上 4 つのポイントは、設計上の意思決定に直接関連しています。具体的には、関心の分離、凝集度、および結合度という古典的な設計品質、要するに一般的に必要とされるようなことに関して、留意するということです。ただし、制御の反転やさまざまな形の分離されたプレゼンテーションなど、テストの容易性を大きく向上できる特定の設計パターンが存在します。
すべての物事と同様に、トレードオフもいくつか存在します。ここで示すパターンの多くは、さまざまな論議を呼んでいます。その理由は、現在のマイクロソフトのツールの方向性と一致しないから、比較的新しいから、または、いくつかの従来の .NET アプローチの有用性を失わせるように見えるから、などです。このコラムの残りの部分では、いくつかのサンプル シナリオと設計パターンを示し、TDD や自動テストに重きを置くチームがこれらのパターンを重要と考える理由を説明しようと思います。

厄介なものを切り離す
5 年前、筆者は自分がオブジェクト指向設計に強いと考えていました。その後、TDD を使用したいくつかのプロジェクトに従事し、自分が実際にはほとんどわかっていなかったということを身をもって知りました。それらのプロジェクトにかかわる前には、NUnit を使用した単体テストの記述についてかなり多くの情報を読み込んでいました。それには次のような例が示されていました。
[TestFixture]
public class HolyHandGrenadeTester {
  [Test]
  public void users_should_count_to_three_after_pulling_the_pin() {
    new HolyHandGrenade().TheNumberToCountTo().ShouldEqual(3);
  }
}
これらの例を頭に入れて最初の TDD プロジェクトに臨んだ筆者は、すぐに問題に直面しました。記述した C# コードはデータベース、Web サービス、ASP.NET ランタイム、および外部システムとやり取りします。これらは、完全に 1 つの CLR AppDomain 内で動作するコードと比べると、テストするのがずっと困難でした。
特に覚えているのは、小さなカスタム ワークフロー サブシステムに対して単体テストを記述するのに苦労したことです。ワークフロー データはかなり複雑で階層的であったため、ワークフローの状態を XML データとして Oracle の CLOB フィールドに格納することにしました。ワークフロー コード自体は、データベースから XML データを取り出して操作を行い、XML を更新してから、別の ADO.NET コードを呼び出して変更をデータベースに保存するという手順を実行可能でした。筆者の記述したコードは、図 1 に示す例のようなものでした。
図 1 ワークフロー コード
public void ProcessWorkflowEvent(
  string newStatus, long workflowId) {

  // First, go get the XML out of the database
  OracleConnection connection =
    new OracleConnection(ProjectConfiguration.ConnectionString);
  OracleCommand command = connection.CreateCommand();
  command.CommandType = CommandType.StoredProcedure;
  command.CommandText = "sp_that_returns_the_workflow_information";
  // and set the necessary parameters

  command.ExecuteNonQuery();

  string xml = (string) command.Parameters["@xml"].Value;
  XmlDocument document = new XmlDocument();
  document.LoadXml(xml);

  // depending upon what the new status is, make changes
  // to the Xml document, decide whether or not to send an email,
  // then save the changed Xml document with another set
  // of ADO.NET calls
}
SpecUnit

単体テスト サンプルの多くでは、SpecUnit ライブラリからの拡張メソッドを使用しています。SpecUnit は、単体テストを古典的な xUnit Assert.AreEqual(expected, actual) 構文よりもずっと読みやすくする一連の拡張メソッドを提供します。
SpecUnit からのサンプルを次に示します。
public static object ShouldEqual(
    this object actual, object expected) {
  Assert.AreEqual(expected, actual);
  return expected;
}
このメソッドは、単体テスト内で次のように使用されます。
item.Name.ShouldEqual("Smith");
item.Age.ShouldEqual(9);
SpecUnit は、.NET の代表的な各種 xUnit ツールから使用できます。筆者のチームでは、SpecUnit をアプリケーション固有の多くの拡張メソッドと共に使用することにより、単体テストを多少記述しやすく、そしてずっと読み取りやすくしています。
このコードに対して自動化された単体テストを記述するためには何を行う必要があるかを考えてみましょう。このデータにテスト入力を与える唯一の方法は、適切な形式の XML ドキュメントを作成してから、その XML をデータベースの行に配置することです。XML ファイルの状態を変更するようなビジネス ロジックが多数存在したため、以前の状態とユーザー入力との順列を多数テストする必要がありました。
既に述べたとおり、このコードに対して単体テストを記述したときには非常に苦労しました。実際に問題となったのは、ワークフローの状態変更に関するビジネス ルールを正しく設定することでした。データベースに XML データをセットアップする必要性により、作業の速度は低下しました。このとき筆者は、テストの容易性に関する最初の重要な教訓を学んだのです。それは、テストで扱うには明確に厄介なものがあるということです。
必要なことは、厄介なものを切り離すことでした。筆者が "厄介なもの" として実際に意味しているのは、複雑だったり手間がかかったり、単にテストに含めるのに不便であったり、またはテストの実行を非常に遅くするような種類のコードやインフラストラクチャです。次に、厄介なものの例をいくつか挙げます。
  • データベース アクセス。TDD の専門家のほとんどが言っているように、データベースが関係するテストは、完全に 1 つの CLR AppDomain 内で実行できるテストに比べて、実行が 1 桁低速になります。データベースのデータのセットアップは、参照整合性とデータの制約のために、単純にメモリ内にオブジェクトを構築する場合と比べて、ずっと時間がかかります。データベースに対してテストを行うときには、多くの場合、参照整合性と値の制約を満足するためだけに、実際のテストとは関係のないセットアップ データをデータベースに追加していることに気付きます。
  • GUI テクノロジ。これは、Windows Presentation Foundation (WPF) を使用する場合や、Web アプリケーションを直接ブラウザでテストする場合などです。ユーザー インターフェイス自体をテストすることは可能ですが、時間的に大きな投資となり、テストの実行はデータベース テストよりもさらに低速になります。
  • Active Directory アクセス。
  • Web サービス。独自のコードをテストするために既知の状態をセットアップすることは十分に困難です。2 つ以上のシステムにわたってテストをセットアップするためにまったく別のチームと協力し合うことは、さらに困難です。お勧めするのは、Web サービスの機能を実際の Web サービス プロトコルとは独立にテストできるようにすることです。コードの外部にある Web サービスに対しては、Web サービス プロキシ クラスを直接使用する代わりに、コードが外部 Web サービスに対する抽象化されたインターフェイスに依存するようにすることを強くお勧めします。これにより、内部のテスト中に、外部 Web サービスを独自のスタブ実装で置き換えることができます。
  • 構成ファイル。
以上に挙げたものはすべて重要な要素であり、そのすべてをどこかの時点でテストする必要があります。しかし、鍵となるのは、少なくともコア アプリケーションのコードがテストしやすくなるように、コア アプリケーションのできるだけ多くの部分をこのインフラストラクチャから切り離すことです。切り離された状態でデータ アクセスをいったんテストした後で、データベースのことを何も気にせずにビジネス ロジックに対する単純なテストを記述できる必要があります。
サンプルのワークフロー サブシステムに戻りましょう。当時筆者は、ビジネス ロジックを正しく設定しようと頑張っていましたが、XML データのセットアップやデータ アクセスのおかげで作業のペースは落ちていました。筆者が本当に必要としたのは、ビジネス ロジックのコードにまっすぐ取り組み、高速に実行できるテストによって完全にメモリ内でコードを扱えるようになることでした。最初に必要だったのは、インフラストラクチャおよび XML からビジネス ロジックを切り離すことだったのです。
public class WorkflowState {
  public DateTime LastChanged { get; set; }
  public string CurrentStatus { get; set; }
  public string Priority { get; set; }
  // Other properties

  public void ChangeStatus(string newStatus) {
    // WorkflowState will update itself based on
    // the current state of the WorkflowState object
    // and the newStatus
  }
}
ビジネス ルールで、顧客が混乱している場合には WorkflowState の優先順位を Urgent に昇格させる必要があると指示されていると考えましょう。このとき、次のように、ワークフロー ビジネス ロジックをデータベース インフラストラクチャおよび XML から切り離してテストできます。
[TestFixture]
public class WorkflowStateTester {
  [Test]
  public void
    the_priority_should_be_escalated_to_urgent_when_the_customer_is_angry() {
    WorkflowState state = new WorkflowState(){Priority =
      "Low", CurrentStatus = "NotStarted"};
    state.ChangeStatus("CustomerIsVeryAngry");

    state.Priority.ShouldEqual("Urgent!");
  }
}
ワークフロー ビジネス ロジックはテストしやすくなりましたが、この変更によって他に得られたものは何でしょうか。ビジネス ロジックはデータ アクセス インフラストラクチャおよび XML 形式から独立しているため、記述も理解もしやすくなっています。これにより、データ アクセスとビジネス ロジックを独立して変更することがずっと容易になりました。
多くの可変要素を持つコードの領域、適切なテストのために入力の順列を多数必要とするコードの領域、または要件の変更に関して影響を受けやすいコードの領域では、テストの容易性はきわめて重要です。
図 1 に示されている元の ProcessWorkflowEvent メソッドでは、データベースとの間で XML ドキュメントを読み込んだり保存したりする操作は、ワークフロー ビジネス ロジックで何が行われている場合でも基本的には同じです。筆者はそのような保存コードに対していくつかのテストを記述し、それらの機能に自信を持つことができます。ビジネス ロジックを経由するすべてのコード パスを適切にテストするためには、さらに多くのテスト シナリオが必要でした。ビジネス ロジックが完全に切り離された状態で単体テストを実行できたとしたら、元のプロジェクトでかなりの労力を節約できたでしょう。

フェイクを使用して境界条件を確立する
ワークフロー ロジックの 2 番目のバージョンがまだ完全でないことにお気付きかもしれません。まだデータベースとやり取りする必要があり、おそらくは電子メール サーバーともやり取りしなくてはなりません。元の ProcessWorkflowEvent メソッドを書き換えて、図 2 に示される新しい WorkflowService クラスのメソッドのようにします。WorkflowService で直接データ アクセスを実行して電子メールを送信する代わりに、後述の IRepository および IEmailGateway という新しいサービスに委任することにします。
図 2 ワークフロー イベントを処理する
public class ChangeWorkflowStatusMessage {
  public string NewStatus { get; set; }
  public long WorkflowId { get; set; }
}

public class WorkflowService {
  private readonly IEmailGateway _emailGateway;
  private readonly IRepository _repository;

  public WorkflowService(IEmailGateway emailGateway, IRepository repository) {
    _emailGateway = emailGateway;
    _repository = repository;
  }

  public void ChangeStatus(ChangeWorkflowStatusMessage message) {
    // Fetch the correct WorkflowState object from the database
    WorkflowState workflow = _repository.Find<WorkflowState>(message.WorkflowId);
    workflow.ChangeStatus(message.NewStatus);

    // email the owner of this workflow and tell them that something happened
    // if this is an Urgent item
    if (workflow.Priority == "Urgent") {
      // workflow.Owner is the user who is responsible for
      // this workflow
      _emailGateway.SendChangeStatusEmailTo(workflow, workflow.Owner);
    }

    // Save any changes to the WorkflowState object
    _repository.Save(workflow);
  }
}
自動テストの前提条件の 1 つは、テストへの既知の入力を確立することです。WorkflowService.ChangeStatus の機能を完全にテストするためには、Urgent である WorkflowState オブジェクトおよび Urgent ではない WorkflowState オブジェクトを対象としたテスト入力をセットアップする必要があります。テストの実行ごとにそれらのテスト入力に対してデータベースをセットアップすることもできますが、もっと簡単な方法があります。フェイク データベースを使用しましょう。Repository パターンを使用してアプリケーションのドメイン モデルとデータ アクセス サービスの間を仲介することにより、テストのシナリオでのフェイク データベースの制御がより簡単になります。
筆者のチームでは、常に Repository パターンを IRepository インターフェイスと共に使用することを選択しています。
public interface Irepository {
  T Find<T>(long id) where T : Entity;
  void Delete<T>(T target);
  T[] Query<T>(Expression<System.Func<T, bool>> where);
  void Save<T>(T target);
}
Repository の実際の実装では、NHibernate および NHibernate 用の LINQ を使用して、Entity オブジェクトとデータベース テーブルの間で情報を転送します。単体テストのシナリオでは、実際に意識する必要があるのは、この IRepository インターフェイスで Entity が保存または削除されたことや使用可能であることだけです。なぜなら、アプリケーションの他の部分が知っていることはそれで全部だからです。テストに対しては、図 3 に示す IRepository の実装を使用して既知のデータベース状態を作成できます。この実装は、オブジェクトを単純にメモリに格納し、NHibernate 用の LINQ の代わりに LINQ-to-Objects を使用してクエリを実行します。
図 3 IRepository からのフェイク データベース
public class InMemoryRepository : IRepository {
  private readonly Cache<Type, object> _types;
  private MockUnitOfWork _lastUnitOfWork;

  public InMemoryRepository() {
    _types = new Cache<Type, object>(type => {
      Type listType = typeof(List<>).MakeGenericType(type);
      return Activator.CreateInstance(listType);
    });
  }

  private IList<T> listFor<T>() {
    return (IList<T>)_types.Get(typeof(T));
  }

  public T Find<T>(long id) where T : Entity {
    return listFor<T>().FirstOrDefault(t => t.Id == id);
  }

  public void Delete<T>(T target) {
    listFor<T>().Remove(target);
  }

  public T[] Query<T>(Expression<Func<T, bool>> where) {
    var query = from item in listFor<T>() select item;
    return query.Where(where.Compile()).ToArray();
  }

  public void Save<T>(T target) {
    listFor<T>().Add(target);
  }
}
単体テストでは、目的のデータによってセットアップされた InMemoryRepository のインスタンスと共に IRepository に依存するオブジェクトを構築します。InMemoryRepository を使用してデータベース状態をシミュレートする単体テストのサンプルを図 4 に示します。
図 4 データベース状態をシミュレートするテスト
[Test]
public void find_the_user_by_user_name_and_password() {
  // Set up an InMemoryRepository with a User
  InMemoryRepository repository = new InMemoryRepository();
  User user = new User(){UserId = "jeremy"};
  user.ResetPassword("thePassword"); // the password is hashed
  repository.Save(user);

  // Construct a new instance of SecurityDataService using the
  // InMemoryRepository from above
  SecurityDataService service = new SecurityDataService(repository);
  service.Authenticate("jeremy", "wrong").ShouldBeFalse();
  service.Authenticate("jeremy", "thePassword").ShouldBeTrue();
  service.Authenticate("wrong", "thePassword").ShouldBeFalse();
  service.Authenticate("wrong", "and wrong").ShouldBeFalse();
}
このサンプルは、SecurityDataService クラスの機能をテストします (図 5 を参照)。内部でこのテストは、指定したユーザー名とパスワードを持つユーザーが存在するかどうかのクエリを IRepository に対して実行します。
図 5 テストされるクラス
public class SecurityDataService : ISecurityDataService {
  private readonly IRepository _repository;

  // The IRepository dependency is set up via Constructor Injection
  // In our real application, Both the real NHibernateRepository
  // and the SecurityDataService would be constructed by
  // an Inversion of Control container
  public SecurityDataService(IRepository repository) {
    _repository = repository;
  }

  public bool Authenticate(string username, string password) {
    return _repository.FindBy<User>(u => u.UserId ==
      username && u.Password == User.HashPassword(password)) != null;
  }
}
実際のアプリケーションでは、SecurityDataService はデータベースをアクセスする NHibernate Repository オブジェクトを使用します。しかし、テストでは、より簡単に制御でき、より速く複数のテスト ケースをシミュレートできる、フェイク Repository で問題なく代用できます。このようなフェイク オブジェクトによる代用を行えるのは、IRepository を使用するクラスが、IRepository の宣言されたインターフェイスにのみ依存する場合に限られます。IRepository を使用するクラスが、通常の IRepository が NHibernate 固有の処理を実行するという事実にたとえ間接的にでも依存している場合は、フェイク オブジェクトで完全に代用することができません。
スタブやモックなどのフェイク オブジェクトは、緊急または継続的な設計を行う場合には多大な利益を提供します。まだ作成していないサービスの代用となることで、システムを段階的に構築 (または設計) することを可能にするからです。

ゲートウェイ パターン
筆者がかかわるほとんどすべてのプロジェクトには、システム内の何らかの動作や状態に応じてユーザーに自動的な電子メール通知を送信することを必要とするような機能が、少なくとも 1 つは含まれています。しかし、アプリケーション コードが電子メール機能によってハングアップすることは、明らかに望ましくありません。
筆者は、ゲートウェイ パターンを使用して電子メール機能をラップすることにしています。ゲートウェイ パターンでは、外部のシステムまたはリソースへのアクセスをオブジェクトにカプセル化します。SMTP タイプの構成などを含め、電子メールの送信に必要なすべてのコードを次のような IEmailGateway オブジェクトの内部に収めて、コードの他の部分から隠蔽します。
public class EmailMessage {
  public string Body { get; set; }
  public string[] To { get; set; }
}

public interface IEmailGateway {
  void SendChangeStatusEmailTo(WorkflowState workflow, User owner);
}
.NET コードから電子メールを送信する既存のソリューションは多数存在するため、実際に電子メールを送信する操作についてはそれほど心配していませんが、アプリケーション コードが電子メールの送信を決定するかどうか、だれに送信するか、およびメッセージの本文がどのようになるかについては、非常に気にかかります。WorkflowService でテストする必要があることは、WorkflowState オブジェクトの最終的な状態に基づいて電子メールの送信が決定されたのかという単純な事実です。

決定と実行を分ける
コードで行える最善のことの 1 つが、操作を実行することと、操作の実行を決定することとを 2 つの別々な作業として扱うことです。WorkflowService の場合は、電子メールの送信の決定だけを行います。
// email the owner of this workflow and tell them
// that something happened if this is an Urgent item
if (workflow.Priority == "Urgent") {
  // workflow.Owner is the user who is responsible for
  // this workflow
  _emailGateway.SendChangeStatusEmailTo(workflow, workflow.Owner);
}
単にコードで電子メールを適切な宛先に送信することもできますが、フェイクの電子メール アドレスをセットアップしたり、システムから電子メールを受け取ったかどうかを人々に聞いて回ったりするのは一般にかなり面倒です。IEmailGateway の代わりに、WorkflowService が電子メール通知の送信を決定したかどうかをチェックするだけのモック オブジェクトを使用できます (図 6 を参照)。
図 6 電子メールが送信されたかどうかをテストする
[Test]
public void send_an_email_if_the_WorkflowState_is_urgent() {
  // setting up the WorkflowState

  var theOwner = new User();
  var theWorkflowState = new WorkflowState{Priority =
    "Urgent", Id = 5, Owner = theOwner};
  repository.Save(theWorkflowState);

  // Exercise the ChangeStatus() method
  service.ChangeStatus(new ChangeWorkflowStatusMessage(){
    NewStatus = "Urgent", WorkflowId = 5});

  // Verify that the change status email was sent
  emailGateway.AssertWasCalled(x =>
    x.SendChangeStatusEmailTo(theWorkflowState, theOwner));
}
電子メール機能を IEmailGateway に分離することでビジネス ロジックの単体テストは簡単になりましたが、他にもメリットがあるでしょうか。実際の電子メール送信コードへのアクセスを 1 つのクラスに収めることには、次のような利点があります。
  • コード内での重複を防ぐことができます。.NET コードで電子メールを送信する System.Mail.SmtpClient オブジェクトをセットアップするための構成データを検索するには、繰り返しのコーディングが必要となります。
  • 電子メールの送信に必要なコードが多少単純になります。WorkflowService は単に、"この電子メールをこのユーザーに送信せよ" と指示するだけで済みます。
  • 電子メールのインフラストラクチャを変更できます。System.Mail.SmtpClient の代わりにサードパーティのツールを使用して電子メールを送信する必要があるとしたらどうなるでしょうか。電子メールごとに追加のログが必要であったり、障害時のリトライ機能を実装する必要があったり、バッチ送信を行うために電子メールをキューに入れたりする場合も考えられます。電子メール送信へのアクセスに一貫して同じゲートウェイ パターン インターフェイスを使用する場合は、IEmailGateway に依存するクラスを変更することなく、より堅牢なインフラストラクチャを後で追加することもできます。
アプリケーションのテストの容易性向上に関連したリソース

モックを使用した単体テスト
モックを使用してテストの質を向上する
ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める
Visual Studio Team Test を使用した単体テストのチュートリアル

大きなテストの前に小さなテスト
問題を迅速に除去するためには、コードに対してすばやく正確なフィードバック ループが必要です。どのような自動テストでも、何かが期待どおりに動作しないときには問題があることが示されますが、的を絞った小さなテストを行えば、問題のある箇所を正確に特定することができます。大規模なエンド ツー エンドのテストが失敗したときには、多数のさまざまな要因を考慮する必要があり、デバッグはしばしば困難となります。それでも大規模なエンド ツー エンドのテストは必要ですが、筆者が強くお勧めするのは、最初にコードの細かい単位を記述してテストすることで、エンド ツー エンドのテストの実行前にすばやく問題を除去できるように、アプリケーションを構築することです。
WorkflowService の例では、実際の WorkflowService をエンド ツー エンドで実行する前に、次のテストを実行します。
  1. WorkflowState オブジェクトを記述し、そのすべてのビジネス ロジックをさまざまな状態の順列でテストします。
  2. データベースとの間で WorkflowState オブジェクトの保存と読み込みを正しく行えることを証明するためのテストをいくつか記述します。
  3. WorkflowService とやり取りするいくつかのテストを記述して、WorkflowService が IEmailGateway、IRepository、および WorkflowState オブジェクトと正しく協調していることを確認します。
  4. 具象 EmailGateway クラス内に単体テストを記述し、クラスが電子メールの送信とは独立に、特定の WorkflowState および User オブジェクトに対して正しい電子メールの内容を作成していることを確認します。
WorkflowService のすべての部分がそれぞれ独立して機能していることに十分な確信を得られた場合にのみ、実際のデータベースを使用して電子メールを送信する WorkflowService の完全なテストを実行します。
小さなテストは、より低いコストで作成でき、理解しやすく、実行が速く、デバッグもずっと簡単です。多くの単体テストを記述することは 1 つの統合されたテストを記述するよりも手間がかかりそうに思えますが、筆者の経験では、多くの場合、小さな単位のコードをテストした後に大きなテストでそれらを統合する方が、より効率的でした。

全体像
過去 5 年間にわたってテストの容易性に重点を置いてきたことは、他の何にもまして、ソフトウェア設計に対する筆者のアプローチ方法に大きな影響を与えてきました。とは言え、コードのテストの容易性を高める目的のためだけに設計で特別な努力を必要すると場合もあります。しかし、最終的には、テストの容易性は古典的な良い設計の定義と協調するものだと考えます。
どのようにしてこれを切り離してテストできるか、という問いを注意深く検討することは、コードベースでの凝集度、結合度、関心の分離といった古典的な設計品質の実現を助けるツールの 1 つとなります。

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

Jeremy Miller は、C# の Microsoft MVP であり、.NET での依存関係のインジェクション用に公開されているオープン ソースの StructureMap (structuremap.sourceforge.net) ツール、および .NET での FIT テスト用に近日公開予定の意欲的な StoryTeller (storyteller.tigris.org) ツールの作成者でもあります。彼のブログ「The Shade Tree Developer (日陰の開発者)」は、CodeBetter サイト上で公開されています。


テストの実行
Virtual Server を使用した構成テスト (第 2 部)
Dr. James McCaffrey および Paul Despe
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照
目次
Windows PowerShell を使用した Virtual Server の自動化
バーチャル マシンを作成する
バーチャル マシン上での構成テスト
まとめ
ソフトウェアの構成をテストする方法はいくつかあります。場合によっては、Microsoft Virtual Server を使用してバーチャル マシンのライブラリを作成するのも有効な方法です。Virtual Server は一連の COM モジュールで構築されているため、バーチャル マシンの作成および実行のプロセスを完全に自動化できます。
個々の Virtual Server の自動化に関するドキュメントは整備されていますが、テスト担当者との話し合いの中で、ソフトウェア構成をテストするための Virtual Server の自動化の各部分を統合する総合的なエンド ツー エンドのサンプルが必要だということがわかりました。また、現在のところ、ほとんどの Virtual Server の自動化で、より高機能な Windows PowerShell ではなく、古い VBScript 言語を参照しています。
今月のコラムでは、Virtual Server および Windows PowerShell を使用したソフトウェア構成テストの自動化のプロセスについて説明します。これらのテクノロジに対する基礎知識があることを前提としていますが、これらのテクノロジを初めて使用する場合でも、記事内のサンプルを理解できる必要があります。
(詳細については、Windows PowerShell について説明している 2007 年 5 月号の「テストの実行」のコラム「Windows PowerShell を使用した軽量テスト」および Jim Truher によるコラム「コマンドレット: カスタム コマンドで Windows PowerShell を拡張する」を参照してください。また、2008 年 9 月号の「テストの実行」の筆者のコラム「Virtual Server を使用した構成テスト (第 1 部)」では、Virtual Server を使用した構成テストについて説明しています)。
図 1 のスクリーンショットをご覧になれば、このコラムで説明しようとしていることの概要がつかめるでしょう。物理的なホスト マシンでは Windows Server 2008 を実行していますが、この記事で説明しているすべての方法は Windows Server 2003 でも動作します。ホスト マシンには Virtual Server 2005 R2 SP1 がインストールされています。図 1 のスクリーンショットは、Windows XP SP2 を実行中のバーチャル ゲスト マシン VitualMachine-Test を示しています。このスクリーンショットからはわかりませんが、バーチャル マシンの作成には Windows PowerShell 1.0 スクリプトを使用しています。スクリーンショットのバックグラウンドでは、別の Windows PowerShell スクリプトでバーチャル マシン上に [タスク] が設定されていることがわかります。このタスクは、バーチャル マシンの cmd.exe シェル (バーチャル マシンの前面の画面) で実行中のテスト自動化です。
図 1 構成テストの例 (クリックすると拡大画像が表示されます)
このコラムの以降のセクションでは、図 1 のバーチャル マシンを作成するスクリプトおよびテスト自動化を設定し、実行するスクリプトについて詳しく説明します。この記事で説明する方法がソフトウェアのテスト、開発、管理のスキル向上に役立つことがおわかりいただけると思います。

Windows PowerShell を使用した Virtual Server の自動化
Virtual Server は従来の DCOM テクノロジを使用した一連のオブジェクトで構築されています。VBScript、JavaScript、C#、Visual Basic .NET、Windows PowerShell など、COM 対応のあらゆる言語で Virtual Server を自動化できます。お勧めは Windows PowerShell を使用する方法です。この方法では、Microsoft .NET Framework およびそのコマンド ライン機能を直接呼び出すことができるため、スクリプトの開発を対話的に行うことができるなどの長所があります。VBScript および JavaScript はネイティブな Win32 スクリプト言語であるため、Virtual Server オートメーション オブジェクトを直接作成し、使用することができます。ただし、Windows PowerShell は .NET 準拠の言語であるため、Virtual Server を自動化するリモート オブジェクトを Windows PowerShell で使用するには、各オブジェクトの偽装を有効にする必要があります。
方法はいくつかあります。Visual Studio および C# を使用して、ネイティブ Win32 オブジェクトの偽装レベルを設定するメソッドを含むカスタム DLL を作成し、そのカスタム メソッドを Windows PowerShell から呼び出す方法が最も簡単ですが、ここでは、Windows PowerShell を使用して実行時にカスタム DLL を作成し、コンパイルするという少々奇抜な方法を用います。
図 2 のスクリプトでは、SetImpersonation という名前の Windows PowerShell 関数を作成しています。この関数を使用して、Virtual Server オートメーション オブジェクトの偽装レベルを設定できます。このコードを Windows PowerShell の特殊な $profile スタートアップ スクリプト内に配置します。スクリプトの最初に、2 つの write-host メッセージを使用して、カスタム スタートアップ スクリプトで Windows PowerShell に機能が追加されることをユーザーに通知します。次に、C# コンパイラと一時ファイルへの参照を設定します。
$csc = (join-path ($env:windir) Microsoft.NET\Framework\v2.0.50727\csc.exe)
$tempFile = [IO.Path]::GetTempFileName() + ".cs"
図 2 カスタム SetImpersonation 関数
write-host "Executing custom startup script"
write-host "Creating custom SetImpersonation function'n"
set-location D:\

$csc = (join-path ($env:windir) Microsoft.NET\Framework\v2.0.50727\csc.exe)

$tempFile = [IO.Path]::GetTempFileName() + ".cs"

$source = @"
  using System;
  using System.Runtime.InteropServices;

  public class MySecurity
  {
    [DllImport("Ole32.dll", CharSet=CharSet.Auto)]
    public static extern int CoSetProxyBlanket(IntPtr pProxy,
      uint dwAuthnSvc,
      uint dwAuthzSvcp2,
      IntPtr pServerPrincName,
      uint dwAuthnLevel,
      uint dwImpLevel,
      IntPtr pAuthInfo,
      uint dwCapabilities);

    public static int EnableImpersonation(object objDCOM) {
      return CoSetProxyBlanket(Marshal.GetIDispatchForObject(objDCOM),
      10, 0, IntPtr.Zero, 0, 3, IntPtr.Zero, 0);
    }
  }
"@

set-content $tempFile $source
$targetDLL = [IO.Path]::GetTempFileName() + ".dll"

invoke-expression "$csc /nologo /target:library /out:$targetDLL $tempFile"
[System.Reflection.Assembly]::LoadFrom($targetDLL) | out-null

function SetImpersonation($obj)
{
  [MySecurity]::EnableImpersonation($obj) | out-null
}
join-path コマンドレットを使用して .NET Framework 2.0 C# コンパイラの完全パスを作成します。ここでは、このコンパイラがホスト マシン上にあることを前提にしています。次に、System.IO 名前空間を直接呼び出し、Path クラスの静的な GetTempFileName メソッドを使用して、ホスト マシンのシステム Temp フォルダに拡張子 .cs が付いた一意のファイル名を生成します。
次に、カスタム偽装関数を作成するために C# のソース コードを次のように設定します。
$source = @"
  using System;
  using System.Runtime.InteropServices;

  public class MySecurity
  {
    . . .
  }
"@
Windows PowerShell の here-string 機能を使用して、C# プログラムを 1 つの文字列として作成します。目的は、C# P/Invoke メカニズムを使用して、Win32 の CoSetProxyBlanket 関数の外側にマネージ ラッパー メソッド EnablePersonation を作成することです。CoSetProxyBlanket 関数の詳細についてはこのコラムでは説明を省きますが、肝心なことは、オブジェクトがリモート システム上のクライアントのセキュリティ コンテキストを偽装するためのメソッドを作成することです。
次に、作成した C# のソース コードを使用して、実行時にマネージ DLL を作成します。
set-content $tempFile $source
$targetDLL = [IO.Path]::GetTempFileName() + ".dll"
invoke-expression "$csc /nologo /target:library /out:$targetDLL $tempFile"
[System.Reflection.Assembly]::LoadFrom($targetDLL) | out-null
set-content コマンドレットを使用して、C# のソース コードを一時ファイルに配置します。次に、GetTempFileName を使用して DLL ファイルに一意の名前を付けます。C# コンパイラは既定でソース コード ファイル名に基づいて DLL 名を作成するため、この手順は省略します。次に、invoke-expression コマンドレットを使用して C# コンパイラを実行します。
invoke-expression の引数は二重引用符で囲まれた文字列であるため、文字列内のオブジェクトが評価されます。EnableImpersonation メソッドで C# コンパイラがカスタム DLL のビルドを完了したら、LoadFrom メソッドを使用して DLL を現在の Windows PowerShell 実行環境に読み込みます。
最後に、C# の EnableImpersonation メソッドの外側に Windows PowerShell ラッパー関数 SetImpersonation を作成してスクリプトは終了します。
function SetImpersonation($obj)
{
  [MySecurity]::EnableImpersonation($obj) | out-null
}
EnableImpersonation の呼び出しを out-null コマンドレットにパイプします。これは、関数の呼び出し時に診断情報が出力されないようにするためだけの処理です。このスクリプトを Windows PowerShell のスタートアップ プログラムに配置すると、Windows PowerShell の新しいインスタンスを起動するたびにスクリプトが実行されます。そこで、SetImpersonation 関数を使用して新しいカスタム DLL を作成します。
この方法には、Windows PowerShell のインスタンスを起動するたびに同じ DLL が再作成されるという短所があります。代わりに、このコードを Virtual Server COM オブジェクトにアクセスする必要があるすべての Windows PowerShell スクリプトの先頭に直接配置する方法もあります。しかし、C# または実行中の Windows PowerShell でカスタム DLL を作成し、これをホスト マシン上のあちこちに配置することはお勧めできません。
多くの場合、自動化スクリプトは想像以上に長期間使用され続けるものです。数か月または数年経ってから万一 DLL を紛失したときに再作成する方法がわからないという破目に陥りたくはないものです。

バーチャル マシンを作成する
図 3 のスクリーンショットは、自動化を使用して Virtual Server バーチャル マシンを作成する手順を示しています。まず、カスタム SetImpersonation 関数が使用可能であることを確認します。次に、前回の自動化の実行によってバーチャル マシンの構成ファイルが既に存在するかどうかを確認します。構成ファイルが存在する場合、バックグラウンドの Virtual Server サービスを停止し、構成ファイルを削除してからサービスを再開します。
図 3 バーチャル マシンを作成する (クリックすると拡大画像が表示されます)
すべての自動化機能を含むプライマリ Virtual Server オートメーション オブジェクトのインスタンスを作成します。次に、バーチャル マシンを作成して RAM、オペレーティング システムがインストールされたハード ディスク、CD/DVD ドライブ、ネットワーク アダプタを準備します。最後に、バーチャル マシンを起動し、バーチャル マシンのオペレーティング システムが稼動していることを確認します。
図 3 で実行している Windows PowerShell の createVM.ps1 スクリプトの内容は次のとおりです。まず、前述の SetImpersonation 関数を検索します。
write-host "'nChecking to see if SetImpersonation function exists"
$funcs = get-childitem function:\*
$found = $false
foreach ($f in $funcs) {
  if ($f.name -eq "SetImpersonation") {
    $found = $true
  }
}
get-childitem コマンドレットを使用して、現在の Windows PowerShell 環境に認識されているすべての関数のコレクションを取得します。get-command コマンドレットに "-commandtype function" 引数を使用することもできます。SetImpersonation の name プロパティで関数を検索するコレクション内のすべてのアイテムに対して同じ処理を繰り返します。または、Windows PowerShell の break ステートメントを使用して、SetImpersonation が見つかった時点ですぐに繰り返しループを終了することも可能です。
function:\ コレクションの検索が終了したら、ブール型の $found 変数を使用して SetImpersonation 関数が現在の環境に存在するかどうかを確認します。
if ($found) {
 write-host "Found custom SetImpersonation function"
}
else {
 throw "No SetImpersonation function found"
}
SetImpersonation 関数が見つからなかった場合は、その後の自動化は必ず失敗するので、例外をスローします。自動化の次のフェーズでは、バーチャル マシンの構成ファイルが既に存在する場合、その構成ファイルの削除を準備します。Windows PowerShell を使用してバーチャル マシンの構成ファイルを削除するには、まずバックグラウンドの Virtual Server サービスを停止する必要があるため、StopService ユーティリティを作成してその処理を実行します。
このヘルパ関数では、まず Virtual Server サービスに関する情報を取得します。
function StopService
{
  write-host "'nStopping Virtual Server service"
  $s = get-service "Virtual Server"

  if ($s.status -ne "Stopped") {
    $s.stop()
  }
  . . .
}
便利な get-service コマンドレットを使用して Status プロパティを持つオブジェクトをフェッチし、そのオブジェクトの Stop メソッドを呼び出します。このとき、自動化を一時停止して Virtual Server サービスが停止するまで待機することも考えられますが、停止時間をどのくらいの長さにしたらよいかわからないという問題があります。
この場合、遅延ループに入る方がはるかに賢明です。
$ct = 0
while ( ((get-service "Virtual Server").status -ne "Stopped")
          -and ($ct -lt 100) ) {
  $ct++
  start-sleep -m 200
  write-host "Waiting for Virtual Server service to stop . . ."
}
まず、自動化が一時停止した回数を追跡するカウンタを初期化します。次に、遅延ループに入り、サービスの status が Stopped の場合、または遅延が 100 を超えた場合にループを終了します。遅延ループ内では、start-sleep コマンドレットを使用して 200 ミリ秒の間停止します。つまり、遅延時間は最大で 20,000 ミリ秒 (20 秒) になります。遅延ループが終了したら、終了状態を確認します。
if ( (get-service "Virtual Server").status -ne "Stopped" ) {
  throw "Failed to stop Virtual Server service in allowed time"
}
Virtual Server の status が Stopped 以外の場合、ループ回数が 100 回を超えたために終了したことになるので、例外をスローします。ヘルパ関数 StartService を定義します。そのロジックは StopService 関数と同じです。
. . .
if ($s.status -ne "Running") {
  $s.start()
}
. . .
while ( ((get-service "Virtual Server").status -ne "Running")
        -and ($ct -lt 100) ) {
. . .
}
if ( (get-service "Virtual Server").status -ne "Running" ) {
  throw "Failed to start Virtual Server service in allowed time"
ヘルパ関数 StopService および StartService をバーチャル マシン作成スクリプト内に直接配置する代わりに、Windows PowerShell の $profile スタートアップ スクリプト内に配置することもできます。定義済みのこの 2 つのヘルパ関数を使用して、以前の自動化で残ったバーチャル マシンの構成ファイルを削除する必要があるかどうかを確認できます。
$vmcFile = test-path "D:\VMs\VirtualMachine-Test.vmc"
$lnkFile = test-path "D:\Users\All Users\Microsoft\Virtual Server\Virtual Machines\VirtualMachine-Test.lnk"
ここでは、test-path コマンドレットを使用して、コマンドレットのファイル引数が存在するかどうかに応じて true または false の値を返します。
バーチャル マシンを作成する場合、.vmc 構成ファイルをホスト マシン上の任意の場所に配置できます。Virtual Server で、ホストのオペレーティング システムに応じた標準の場所 (Windows Server 2008 の場合には %SystemRoot%\Users\All Users\Microsoft\Virtual Server\Virtual Machines\) に .vmc ファイルのショートカット ファイル (.lnk) を作成することもできます。
特定の名前のバーチャル マシンを作成するには、.vmc ファイルと対応する .lnk ファイルを削除する必要があります。削除しておかないと、対象バーチャル マシンが既に存在することを示すエラーが発生します。サンプルでは、わかりやすくするために、バーチャル マシン名をスクリプトにハードコーディングしていますが、実稼動環境では、この値をパラメータ化してスクリプトに渡すのが一般的です。古いバーチャル マシンが存在するかどうかを確認したら、そのバーチャル マシンを図 4 のような方法で削除できます。
図 4 古いバーチャル マシンの削除
if ($vmcFile -or $lnkFile) {
  write-host "'nFound existing .vmc and/or .lnk file"
  StopService
  if ($vmcFile) {
    write-host "Deleting .vmc file"
    remove-item "D:\VMs\VirtualMachine-Test.vmc"
  }
  if ($lnkFile) {
    write-host "Deleting .lnk file"
    remove-item "D:\Users\All Users\Microsoft\Virtual Server\Virtual
      Machines\VirtualMachine-Test.lnk"
  }
  StartService
}
else {
 write-host "'nDid not find existing .vmc or .lnk files"
}
対象の .vmc ファイルまたは .lnk ファイルが存在する場合、StopService ヘルパ関数を呼び出してバックグラウンドの Virtual Server サービスを停止し、remove-item コマンドレットを使用して該当するファイルを削除し、サービスを再開します。
これまでのスクリプトで、プライマリ Virtual Server COM オートメーション オブジェクトの作成準備ができました。
if ((get-service "Virtual Server").status -ne "Running") {
  StartService
}
$vs = new-object -com "VirtualServer.Application"
if ($vs -eq $null) {
  throw "Failed to create main COM automation object"
}
SetImpersonation $vs
まず、Virtual Server サービスが動作していることを確認します。次に、new-object コマンドレットを使用して Virtual Server 自動化ライブラリのインスタンスを作成します。このインスタンスには VirtualServer.Application の ProgID があります。作成が失敗した場合は自動化が失敗するため、例外をスローします。成功した場合は、オートメーション オブジェクトのカスタム SetImpersonation 関数を呼び出して、オブジェクトがクライアントのセキュリティ コンテキストを Virtual Server の基本となる DCOM トランスポート機構で偽装できるようにします。
バーチャル マシンの作成はきわめて簡単です。
write-host "'nCreating virtual machine"
$vm = $vs.CreateVirtualMachine("VirtualMachine-Test", "D:\VMs")
if ($vs -eq $null) {
  throw "Failed to create virtual machine object"
}
SetImpersonation $vm
ここでの処理は、CreateVirtualMachine メソッドを呼び出して、文字列のバーチャル マシン名と結果となる .vmc ファイルを配置するホストの場所 (この場合はハードコーディングされた D:\VMs\ ディレクトリ) を渡すだけです。バーチャル マシン オブジェクトを操作する必要があるため、偽装レベルを設定します。
これで、新規に作成したバーチャル マシンのプロビジョニングを開始できます。
write-host "Assigning 256 MB memory"
$vm.Memory = 256

write-host "'nAdding existing virtual hard disk drive"
write-host "Copying .vhd file from library to working directory"
copy-item "D:\VirtualMachinesLibrary\VirtualMachine-WinXP-SP2.vhd"
  "D:\VirtualMachines\"
まず、ゲスト マシンの RAM を割り当てます。その際、実行中のすべてのバーチャル マシンとホスト マシンに使用可能な RAM の総容量がホスト マシンの RAM 容量によって制限される事実を考慮してください。次に、ハード ディスク ドライブ ファイル (.vhd) をバーチャル マシンにアタッチする準備をします。OS などの主要ソフトウェアを含む .vhd ファイルをバックアップの場所から作業ディレクトリにコピーします。その目的は、構成テスト中に .vhd ファイルが壊れた場合のために将来使用可能な元のバージョンの .vhd ファイルを確保しておくことにあります。
.vhd ファイルのサイズは一般に 2.0 GB 程度なので、多くの場合、ファイルのコピーには数分かかります。Virtual Server で .vhd ファイルを作成すると、.vhd ファイルは関連付けられた .vmc ファイルと同じディレクトリに格納されます。ただし、前述のように既存の .vhd ファイルを新しいバーチャル マシンにアタッチすると、.vhd ファイルと .vmc ファイルを別のディレクトリに格納することが可能です。有効な .vhd ファイルがあれば、そのファイルをバーチャル マシンにアタッチできます。
$loc = "D:\VirtualMachines\VirtualMachine-WinXP-SP2.vhd"
if (test-path $loc) {
  write-host "Found the .vhd file"
  $hd = $vm.AddHardDiskConnection("D:\VirtualMachines\VirtualMachine-WinXP-SP2.vhd", 0, 0, 0)
}
else {
  throw "Failed to find .vhd file"
}
まず、test-path コマンドレットを使用して、対象 .vhd ファイルの有効なコピーが存在することを確認します。次に、AddHardDiskConnection メソッドを使用してバーチャル ハード ディスクをアタッチします。AddHardDiskConnection メソッドの第 1 パラメータはハード ドライブのバスの種類 (この場合は vmDriveBusType_IDE = 0 および vmDriveBusType_SCSI = 1 の列挙値) です。
ホスト マシンに IDE ドライブしか存在しない場合でも、バーチャル マシンに SCSI ドライブを指定することでパフォーマンスを向上させることができます。第 2 パラメータはバス番号です。この値は、IDE ドライブの場合は 0 または 1、SCSI ドライブの場合は 0 ～ SCSI コントローラの数です。第 3 パラメータはデバイス番号です。この値は、IDE ドライブの場合は 0 または 1、SCSI ドライブの場合は 0 ～ 6 です。
.vhd バーチャル ハード ドライブをアタッチしたら、ドライブを Undoable (復元可能) にすることができます。
write-host "Making .vhd disks undoable"
$vm.undoable = $true
既定では、Virtual Server はバーチャル マシンの実行中に加えられたすべての変更を自動保存しますが、復元ディスクを構成すると、バーチャル マシンの実行中に加えられたすべての変更はバーチャル復元ディスク ファイル (.vud) に保存されます。この場合、バーチャル マシンのシャットダウン時に復元ディスクを破棄して元の .vhd ディスクに戻すか、復元ディスクを適用して .vhd ファイルに変更を保存するかを選択できます。
サンプルでは、基本構成の .vhd ファイルを作業ディレクトリにコピーしているのでディスクを復元可能にするかどうかは問題になりませんが、必要なときのためにその方法を説明しておきます。
まず、CD/DVD ドライブを準備します。
write-host "'nAdding CD/DVD drive"
$dvd = $vm.AddDVDROMDrive(0,1,0)
if ($dvd -eq $null) {
  write-host "Failed to add CD/DVD drive"
}
SetImpersonation $dvd

write-host "Attaching CD/DVD drive to F:"
$dvd.AttachHostDrive("F")
Virtual Server の GUI インターフェイスを使用してバーチャル マシンを作成すると、ホスト マシンに CD/DVD ドライブが存在すれば、ゲスト マシンに CD/DVD ドライブが自動的に追加およびアタッチされます。自動化を使用してバーチャル マシンを作成した場合は CD/DVD ドライブを明示的に構成する必要があります。
CD/DVD ドライブの場合、AddDVDROMDrive メソッドの第 1 パラメータ (バスの種類) に 0、第 2 パラメータ (バス番号) に 0 または 1、第 3 パラメータ (デバイス番号) に 0 または 1 を指定します。次に、AttachHostDrive メソッドを使用して、ゲスト マシン上のバーチャル CD/DVD ドライブをホスト マシン上の物理ドライブに関連付けます。サンプル スクリプトでは、わかりやすくするために "F:" をハードコーディングしていますが、実稼動環境では、この値をパラメータ化してスクリプトに渡すのが一般的です。次に、ネットワーク アダプタの構成を準備します。
$vn = $vs.FindVirtualNetwork('External Network (Broadcom 802.11g Network
   Adapter)')
if ($vn -eq $null) {
  write-host "Failed to find virtual network adapter"
}
SetImpersonation $vn
FindVirtualNetwork メソッドを使用して既存のネットワーク アダプタを探します。Virtual Server では、ネットワークという用語とネットワーク アダプタという用語は多くの場合に同じ意味で扱われます。Virtual Server をインストールすると、セットアップ プログラムがホスト マシンをスキャンして、すべてのアクティブなアダプタを検出し、検出したアダプタに名前を付けます。アダプタ名はかっこで囲まれ、先頭に "External Network" が付加されます。Virtual Server のインストール後にネットワーク アダプタを追加した場合は、ユーザーが任意の名前を付けることができます。
アダプタ オブジェクトに偽装レベルを設定したら、バーチャル アダプタをバーチャル ネットワークにアタッチします。
$adapters = $vm.NetworkAdapters
SetImpersonation $adapters
SetImpersonation $adapters.Item(1)
$adapters.Item(1).AttachToVirtualNetwork($vn)
NetworkAdapters プロパティを使用して、すべてのアダプタのコレクションをフェッチします。Windows PowerShell でアダプタにアクセスするには、コレクション オブジェクトおよびコレクション内の各アダプタに偽装を設定する必要があります。最初のアダプタ Item(0) は組み込みの "Internal Network" アダプタなので、Item(1) が最初の外部アダプタです。AttachToVirtualNetwork メソッドを使用してアダプタをネットワークに接続します。これで構成は完了し、バーチャル マシンを起動できます。
write-host "'nStarting Virtual Machine"
$startTask = $vm.StartUp()
SetImpersonation $startTask
$startTask.WaitForCompletion(60000) # wait up to 1 minute
Startup メソッドは、メソッドの進行状況を監視する機能を持つ VMTask オブジェクトを返します。このタスク オブジェクトには、タスクの完了を待機する時間の最大値 (単位はミリ秒) を受け取る便利な WaitForCompletion メソッドがあります。
次に、ゲスト バーチャル マシン上のオペレーティング システムが起動するまで待機する必要があります。
write-host "'nWaiting for guest virtu al machine to boot                up'n"
   $ct = 0
   while ( ((test-path "\\VM-017\Public") -eq $false) -and $ct
          -lt 100 )
   {
     $ct++
     write-host "Waiting . . . $ct"
     start-sleep -s 10
   }
ここでは、遅延ループを使用します。遅延の繰り返し処理内で 10 秒停止します。while ループ条件では test-path を使用してバーチャル マシンを検索し、ゲスト上に存在することがわかっているディレクトリが見つかった場合、または検索の試行回数が最大値を超えた場合に終了します。スクリプトの最後に遅延ループの終了方法を指定します。
if ((test-path "\\VM-017\Public") -eq $false) {
  throw "'nFailed to discover guest machine"
}
else {
  write-host "'nVirtual machine is up and running"
}
バーチャル ゲスト マシンが見つからなかった場合は例外をスローします。見つかった場合は成功のメッセージを表示します。

バーチャル マシン上での構成テスト
ここまで、スクリプトを使用したバーチャル マシンの作成およびプロビジョニングの方法について説明しました。この時点で手動テストを実行できますが、ここでは構成テストのプロセスを自動化する主要な方法を説明します。図 5 のスクリーンショットを見ると、exerciseVM.ps1 という名前の Windows PowerShell スクリプトが存在することがわかります。サンプル スクリプトでは、対象のバーチャル マシンが動作していることを確認した後、テスト関連ファイルをホスト マシンからゲスト マシンにコピーします。次に、指定の時間に自動化テストを起動するゲスト マシン上に Windows タスクを作成します。バーチャル マシンをオフにし、復元ディスクを破棄してスクリプトを終了します。
図 5 バーチャル マシンへのテスト自動化の配置 (クリックすると拡大画像が表示されます)
exerciseVM.ps1 では、まず、test-path コマンドレットを使用して対象のバーチャル マシンを探します。
if ((test-path "\\VM-017\C$") -eq $false) {
  throw "'nFailed to discover guest machine"
}
else {
  write-host "Found virtual guest machine"
}
バーチャル マシンのシステム ルートが見つからない場合、例外をスローしてスクリプトを終了します。次に、自動化ファイルがホストに存在することを確認します。
if (test-path "D:\Public\testHarness.js") {
  write-host "Found testHarness.js file to copy"
}
else {
  throw "Did NOT find testHarness.js file to copy"
}
ゲストにテスト自動化を受け取るパブリック共有が存在することを確認します。この方法は、バーチャル マシンの .vhd ファイルに共有が構成され、その共有にユーザーがファイルをコピーできるようになっていることを前提にしています。ホスト マシンで実行中のスクリプトには、このようなセキュリティ構成をゲスト マシンで実行する簡単な方法がありません。
if (test-path "\\VM-017\Public") {
  write-host "Found \\VM-017\Public directory to copy to"
}
else {
  throw "Did NOT find \\VM-017\Public directory to copy to"
}
copy-item "D:\Public\testHarness.js" "\\VM-017\Public"
ここでは、copy-item コマンドレットを使用して 1 つの自動化ファイルをホストからゲストにコピーしています。次に、バーチャル ゲスト マシン上のテスト自動化を実行する Windows タスクを設定します。
$dt = new-object -com WbemScripting.SWbemDateTime
$now = [DateTime]::now
$later = $now.addSeconds(90)
write-host "'nTest automation testHarness.js schedeuled to run at " $later
$later.ToUniversalTime() | out-null
$dt.SetVarDate($later)
ここでは、Windows Management Instrumentation (WMI) および Common Information Model (CIM) テクノロジを使用しているため、.NET の DateTime オブジェクトではなく、SWbemDateTime オブジェクトを作成します。ホストの現在の日付と時刻をフェッチするには .NET の静的 Now プロパティを使用します。次に、AddSeconds メソッドを使用して、90 秒後の .NET DateTime オブジェクトを作成します。また、.NET の実行時間を協定世界時 (UTC) オブジェクトに変換します。SetVarDate を使用して SWbemDateTime 日付時刻オブジェクトを構成します。
次に、テスト自動化タスクをスケジューリングします。
[System.Management.ManagementClass] $mc =
  new-object System.Management.ManagementClass("\\VM-017\root\cimv2",
    "Win32_ScheduledJob", $null)
$mc.Create("cscript.exe //nologo C:\Public\testHarness.js",
           $dt.value, $false, 0, 0, $true) | out-null
ここでは、Windows PowerShell の get-wmiobject コマンドレットを使用して直接 WMI テクノロジを操作するのではなく、Windows PowerShell で .NET Framework を直接呼び出す機能と System.Management 名前空間の ManagementClass を使用します。このクラスは WMI 関数のフレンドリーな .NET ラッパーと考えることができます。
このオブジェクトのコンストラクタの第 1 引数はオブジェクトのスコープです。このパラメータはターゲット マシンを識別します。第 2 引数は WMI クラス名です。第 3 引数は WMI クラスを取得する際に使用するオプション情報を保持するオブジェクトです。
Create メソッドを使用して、スケジューリングしたテスト自動化を設定します。第 1 パラメータは、実行するコマンドを表す文字列です。第 2 パラメータは、タスクをいつ実行するかを指定するオブジェクトです。第 3 パラメータは、タスクを複数回実行するかどうかを指定するブール値です。第 4 および第 5 パラメータは、複数のジョブを実行するときに指定します。第 6 パラメータは、ジョブを対話モード (可視状態) で実行するかどうかを指定するブール値です。
スクリプトでリモート マシンまたはゲスト マシンのタスクのスケジューリングを設定するには、ゲスト マシンの調停が必要です。有効にするリモート ジョブ スケジューリングの詳細は、使用するオペレーティング システムによって異なります。図 1 の例の場合、ゲスト マシンに 4 点の変更が必要です。まず、リモート WMI アクセスを使用するために、Windows XP ゲスト上の Windows ファイアウォールを無効にします。また、[セキュリティの設定]、[ローカル ポリシー]、[セキュリティ オプション] の順に選択して、ゲストのローカル セキュリティ ポリシーに 3 つの変更を加える必要があります。まず、[DCOM: セキュリティ記述子定義言語 (SDDL) 構文でのコンピュータ アクセス制限] の設定を [Allow for Anonymous Login and Everyone users] (匿名ログインと Everyone ユーザーを許可する) に変更する必要があります。また、[ネットワーク アクセス: Everyone のアクセス許可を匿名ユーザーに適用する] を有効にします。さらに、[ネットワーク アクセス: ローカル アカウントの共有とセキュリティ モデル] の設定を [クラシック - ローカル ユーザーがローカル ユーザーとして認証する] に変更します。
つまり、Virtual Server ホスト マシンを使用してゲスト マシンのテスト自動化をスケジューリングする場合、基本となる .vhd ファイルのいくつかのセキュリティ設定を変更する必要があります。必ずしもホスト マシンからタスクをスケジューリングしなければならないわけではありません。代わりに、ゲストの .vhd ファイルに定期的なテスト自動化の組み込みを構成する方法もあります。
テスト自動化をスケジューリングしておけば、テストの実行を待機し、ゲスト マシンをシャットダウンすることができます。
$dummy = read-host -prompt "'nPress <Enter> to continue"
write-host "'nPreparing to turn off guest machine"
write-host "Creating main COM automation object"
$vs = new-object -com "VirtualServer.Application"
if ($vs -eq $null) {
  throw "Failed to create main COM automation object"
}
SetImpersonation $vs
Virtual Server Web ベースの管理ページでテスト自動化を監視し、すべてのテストが完了したときを認識できるようにするため、read-host コマンドレットを使用してスクリプトの実行を停止します。この方法は自動化の開発時には便利ですが、実稼働環境では、他のメカニズムを使用してすべてのテストが完了したことを確認することが考えられます。たとえば、遅延ループを使用し、test-path コマンドレットでテスト結果ファイルを探すという方法があります。
このテスト自動化スクリプトは、バーチャル マシンに接続できるようになりました。
write-host "Looking for virtual machine"
$vm = $vs.FindVirtualMachine("VirtualMachine-Test")
if ($vm -eq $null) {
  throw "Failed to find virtual machine"
}
else {
  write-host "Found virtual machine"
}
SetImpersonation $vm
FindVirtualMachine メソッドを使用して、createVM.ps1 スクリプトで作成したバーチャル マシンを探します。検索するバーチャル マシンはゲストとして実行されていることを前提にしています。対象バーチャル マシンの名前には .vmc 拡張子を使用していません。
バーチャル マシン オブジェクトの偽装レベルを設定した後は、バーチャル マシン オブジェクトをオフにすることができます。
write-host "'nTurning virtual machine off"
$offTask = $vm.TurnOff()
SetImpersonation $offTask
$offTask.WaitForCompletion(5000)
write-host "'nDiscarding undo disk"
$vm.DiscardUndoDisks()
TurnOff メソッドは VMTask オブジェクトを返します。このオブジェクトを使用して、バーチャル マシンがシャットダウンするか、タイムアウト値を超えるまで待機することができます。ここでは、バーチャル ハード ディスクは復元可能ではないため、バーチャル マシンの状態の変更を保存する復元ディスクを破棄します。したがって、次回にバーチャル マシンの .vhd ファイルを使用するときは、クリーンな状態から始まります。

まとめ
この記事では、Virtual Server の自動化手法を適用および拡張するためのさまざまな方法を説明しました。重要な原則をなるべく多く示すために、サンプルを 2 つの比較的大きいパラメータ化しないスクリプトで構成しました。これまでの説明に従って、この 2 つのスクリプトを固有のニーズに応じて変更または再構成し、スクリプト内でハードコーディングされている値の一部または全部をパラメータ化することも可能です。
サンプル スクリプトは例を示すことが目的なので、実稼働環境のコードに通常取り入れるエラー チェック コードを一部省略しています。それでも、いくつかのエラー トラップは用意しているので、実稼働での使用に耐える堅牢な自動化にするにもさほどの苦労はありません。
今後、Virtual Server に代わって新しい Microsoft Hyper-V 仮想化テクノロジが採用されるようになることが予想されますが、まだ数年は Virtual Server が役立つでしょう。また、この記事で説明した Virtual Server の自動化の原則および方法、特に Windows PowerShell を使用する方法は Hyper-V への移行に有効です。

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

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

Paul Despe は、Hyper-V チームのプログラム マネージャです。ソフトウェア設計エンジニアとして、Virtual PC および Virtual Server 製品のテストに携わっています。マイクロソフトに在籍する前は、日本で勤務した経験を持ち、2003 年にマイクロソフトが買収した仮想化ソフトウェア会社、Connectix にも勤務していました。連絡先は paulde@microsoft.com です。


基礎
WPF でパス上にテキストをレンダリングする
Charles Petzold
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照
目次
パスとテキストを統合する
端点と中点
UserControl を使用する
サイズについて
レベルを下げる
測定の難点
ビジュアルな子の世界
テキストを歪める
テキストは、単にその意味が伝わればよいわけではありません。印刷される場合でも、コンピュータ画面に表示される場合でも、フォントの選択によってテキストのインパクトは強調されもすれば弱められもします。手招きするようなテキスト、拒絶するようなテキスト、なごやかなテキスト、荒々しいテキストなど、さまざまな印象を与えることができます。また、コンピュータ グラフィックスを使用すれば、テキストをお決まりの行単位の並びから解放することもできます。テキストの文字をグラフィカルなオブジェクトとして扱うことで、プログラマは画面上でテキストを躍らせたり飛ばしたりできます。
求められる技法の 1 つとして、図 1 に示すようにテキスト文字を曲線に沿って配置する処理があります。グラフィックス プログラミングでは、直線や曲線の 1 つの集合体を "パス" と呼ぶので、このタスクは "パス上のテキスト" と呼ばれることがあり、それが今回のコラムのテーマです。
図 1 曲線パス上のテキスト
Windows Presentation Foundation (WPF) に対してプログラミングを行う場合、パス上のテキストのプログラミングに関して大きな利点があります。パスを定義する個々の点をアニメーションで動かし、文字がそれに応じて跳ね回るのを見ることができるのです。
WPF では一般的なことですが、この作業を行う方法は複数あり、ここではいくつかのアプローチを紹介します。また、これも WPF の常として、作業の中で難しい部分は、最初に想定されるようなこととは異なります。パス上にテキストを配置する上で実際に大きな問題は、テキスト文字を移動および回転する方法を考え出すことではありません。それは比較的単純です。難しいのは、WPF のレイアウト システムに対して、結果のグラフィックの正しいサイズを適切に伝えることです。

パスとテキストを統合する
グラフィック パスは、いくつかの直線と曲線の集まりです。これらの直線および曲線のいくつかは互いに接続されています。囲まれた領域を形成している場合もあります。WPF では、グラフィック パスは PathGeometry クラスと StreamGeometry クラスの両方にカプセル化されます。StreamGeometry の方がパフォーマンスが優れていますが、パスを定義する個々の点が固定され、アニメーション化できません。
パスにテキストを配置する場合、パスを構成する直線および曲線が互いにエンドツーエンドで接続されていることが非常に重要です。パス内で切れている箇所をジャンプするようなテキスト文字列を扱うのはお望みではないでしょう。その理由により、ここで実際に関心のある WPF クラスは PathFigure であり、これは接続された直線および曲線の単一のコレクションです。PathGeometry は、1 つまたは複数の PathFigure オブジェクトのコレクションです。
PathFigure はそれ自体が StartPoint を定義し、セグメントのコレクションを含みます。最初のセグメントは StartPoint から始まり、以降の各セグメントは前のセグメントの終点から始まります。これらのセグメントはすべて、抽象 PathSegment クラスの派生クラスであり、LineSegment、PolyLineSegment、BezierSegment、PolyBezierSegment、QuadraticBezierSegment、PolyQuadraticBezierSegment、および ArcSegment があります。言い換えれば、PathFigure は、直線、ベジエ曲線、二次ベジエ曲線、および弧 (楕円の円周上の曲線) が接続された連なりです。
PathFigure には、幾何学的な長さがあります。このコラムに付属のソース コードでは、この長さを変数 pathLength で参照します。PathFigure が LineSegment および PolyLineSegment オブジェクトだけから構成されている場合、長さの計算は簡単です。単にピタゴラスの定理を使用し、各直線の長さを計算して合計します。しかし、ArcSegment の場合はより複雑な数式が必要となりますし、ベジエ曲線の長さの計算は明らかに気の遠くなる作業です。
PathFigure の長さの計算を単純化するには、曲線を近似するポリラインのコレクションにパスを変換すると便利です。これをパスのフラット化と呼びます。そうすれば、ピタゴラスの定理を繰り返し適用することで長さを簡単に計算できます。WPF では、ピタゴラスの定理の知識さえも不要になります。ある点を他の点から減算すると結果は Vector オブジェクトとなり、Vector オブジェクトには、2 つの点間の長さを返す Length プロパティがあります。
PathFigure クラスには GetFlattenedPathFigure というメソッドがあり、このメソッドは、LineSegment および PolyLineSegment オブジェクトだけを含む別の PathFigure を返します。そしてこれは、PathFigure 全体の長さを計算するのに理想的です。
特定のフォントおよびフォント サイズでレンダリングされるテキスト文字列にも、幾何学的なサイズがあります。よく知られた TextBlock 要素を使用してテキストを表示する場合、表示されるテキストの幅は、ActualWidth プロパティから得られます。実際に表示を行う前のレイアウト時には、ActualWidth はまだ設定されない場合もありますが、TextBlock はその DesiredSize プロパティをテキストのサイズに設定します。
TextBlock は FormattedText オブジェクトを使用してテキストのサイズを計算しますが、アプリケーション プログラマもこのクラスを利用できます。FormattedText のコンストラクタには、テキスト文字列自体と、フォント サイズ、および Typeface 型のオブジェクトが必要となります。このオブジェクトは、目的の FontFamily、FontStyle、FontWeight、および FontStretch の各オブジェクトから構成されます。
PathFigure とテキスト文字列を統合する方法としては、いくつかの異なるアプローチが頭に浮かびます。テキストに対して特定のフォント サイズを指定し、パスの先頭からテキストを表示することもその 1 つです。しかし、テキストはパス全体に収まらない場合もありますし、(悪くすれば) パスの長さを超えてしまう場合もあります。そのときはどうすればよいのでしょうか。
これらの問題を避けるために、ここでは別のアプローチを採用しました。パスの始点から終点までの長さと厳密に一致するようにテキストのサイズを調整する方法です。テキストは最初に任意のフォント サイズで作成され、サンプル プログラムでは 100 です。テキストの幅は textLength という名前の変数に格納されます。
pathLength と textLength との比は、scalingFactor という数値です。テキストがパス全体にぴったり収まるように、個々のテキスト文字にこの倍率を変換として適用する必要があります。
テキスト文字列内の各文字には、この倍率変換だけでなく、他にも 2 つの変換が適用されます。移動変換は文字をパス上の特定の位置に移動し、回転変換は文字のベースラインがパスに接するよう文字を回転します。
PathGeometry は、GetPointAtFractionLength というメソッドを定義しています。このメソッドは、テキストをパス上に配置する場合に大変便利です。このメソッドは、パスの部分的な長さを示す 0 ～ 1 の値を引数として受け取ります。そして、その長さに対応するパス上の点を返します。この部分的な長さは、簡単に指定できます。各文字がパス上で占める部分的な長さは、各文字の幅に倍率を掛けて pathLength で割ったものに等しくなります。メソッドで返されるパス上の点は、文字をパスに変換する際に有用です。さらに、GetPointAtFractionLength メソッドは、その点でのパスの接線を表す第 2 の点を返します。この第 2 の点の X および Y プロパティを Math.Atan2 メソッドに渡すと、テキスト文字の回転に必要な角度が得られます。
GetPointAtFractionLength は、PathFigure ではなく PathGeometry によって定義されますが、GetPointAtFractionLength を使用するために 1 つの PathFigure から PathGeometry を作成するのは簡単です。

端点と中点
一般的な技法は確立されましたが、いくつかの疑問が残っています。各文字のどの部分をパスに揃えればよいのでしょうか。文字の上部、下部 (ディセンダより下)、またはベースラインでしょうか。ここでは最も自然なやり方としてベースラインを選択し、その後で上部または下部に切り替えるのが簡単と考えました。FontFamily には、この目的に適した Baseline プロパティがあります。これは 1 単位のフォント サイズに基づいているため、文字の上部からそのベースラインまでの距離は単純に、Baseline プロパティ、フォント サイズ (このコードでは 100)、および scalingFactor の積です。
図 2 に、テキスト文字を単純な円のパス上に配置する 2 つの異なる方法を示します。左側の図で文字 h、x、および n を見てください。各文字のベースラインの左右の端点がパスに接していることがわかります。右側の図で文字 e および o を見ると、各文字のベースラインの中点がパスに接しています。
図 2 パス上にテキストを揃える 2 つの方法
一見、これはささいな違いのように見えます。これらの円自体が表示されていなければ、2 つの間に違いがあることにさえ気付かないでしょう。ただし、左側のアプローチの方が、文字間の視覚的な連続性がわずかに優れているように感じました。
これが右側のアプローチと比較して、(アルゴリズム的に) どれだけ難しいかを理解したとき、筆者は失望させられました。文字のベースラインの左端をパス上に配置することは難しくありませんが、右端については、最初の点から倍率適用後の文字幅に等しい直線距離を持つパス上の点を見つける必要があります。さらに、すべての文字が配置された後、最後の文字がパスの端点を超えていることに気付きます。これは、文字が曲線パスに基づいて倍率調整されていながら、パス上で直線のショートカットに基づいて配置されているためです。このアプローチでは、複数の手順を重ねて倍率を逐次再調整することが必要となります。
図 2 ではこのアルゴリズムの単純なバージョンを使用しましたが、ダウンロード可能なコードではすべて、ずっと単純な中点のアプローチを使用しています。各文字が占めるパスの部分的な長さは、合計テキスト幅に対する文字幅の割合に等しい必要があります。

UserControl を使用する
UserControl からの派生は WPF プログラミングではきわめて一般的な手法であり、頑強なコントロールをすばやく構築できる建築機材セットのようなものです。UserControl は ContentControl から派生するので、コントロールを定義するビジュアル ツリーは一般に、XAML でそのコントロールの Content プロパティとして定義されます。分離コード ファイルでは、さらにいくつかのカスタム プロパティが定義される場合があり、イベントおよび相互作用が処理されます。
日常的な WPF の構成要素である UserControl、Canvas、および TextBlock を使用して、パス上のテキストを実装することが可能です。UserControl は、テキストを定義するために必要なプロパティの多くを既に備えているため、この作業に対しては二重に便利です。それらのプロパティは、FontFamily、FontStyle、FontWeight、FontStretch、および Foreground です (テキスト サイズはパス長に基づいて調整されるため、FontSize プロパティは無視されます)。TextBlock 要素が UserControl の子孫である場合、UserControl でのこれらのプロパティの設定は、TextBlock によって継承されます。追加のプロパティ変更イベント ハンドラを提供するために、これらのプロパティのメタデータのオーバーライドが必要になりますが、それはたいしたことではありません。必要な新しいプロパティは、Text (文字列型) と PathFigure (PathFigure 型) だけです。
結果のクラスは TextOnPathControl という名前で、図 3 に抜粋を示します。もちろん、ファイルの残りの部分は、コード サンプルに含まれています。このコラムで使用するソース コードは、TextOnPath という名前の 1 つのソリューションから構成され、これには Petzold.TextOnPath という DLL プロジェクトが含まれています。このプロジェクトには、パス上にテキストを配置するためのさまざまな手法を実装するすべてのクラスが含まれています。TextOnPath ソリューションに含まれる他のプロジェクトは、デモンストレーション プログラムです。
図 3 TextOnPathControl.cs からの抜粋
static void OnTextPropertyChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args)
{

  TextOnPathControl ctrl = obj as TextOnPathControl;
  ctrl.mainPanel.Children.Clear();

  if (String.IsNullOrEmpty(ctrl.Text))
    return;

  foreach (Char ch in ctrl.Text)
{
    TextBlock textBlock = new TextBlock();
    textBlock.Text = ch.ToString();
    textBlock.FontSize = FONTSIZE;
    ctrl.mainPanel.Children.Add(textBlock);
  }

  ctrl.OrientTextOnPath();
}

void OrientTextOnPath()
{
  double pathLength =
    TextOnPathBase.GetPathFigureLength(PathFigure);
  double textLength = 0;

  foreach (UIElement child in mainPanel.Children)
{
    child.Measure(new Size(Double.PositiveInfinity,
      Double.PositiveInfinity));
    textLength += child.DesiredSize.Width;
  }

  if (pathLength == 0 || textLength == 0)
    return;

  double scalingFactor = pathLength / textLength;
  PathGeometry pathGeometry =
    new PathGeometry(new PathFigure[] { PathFigure });
  double baseline =
    scalingFactor * FONTSIZE * FontFamily.Baseline;
  double progress = 0;

  foreach (UIElement child in mainPanel.Children)
{
    double width = scalingFactor * child.DesiredSize.Width;
    progress += width / 2 / pathLength;
    Point point, tangent;

    pathGeometry.GetPointAtFractionLength(progress,
      out point, out tangent);

    TransformGroup transformGroup = new TransformGroup();

    transformGroup.Children.Add(
      new ScaleTransform(scalingFactor, scalingFactor));
    transformGroup.Children.Add(
      new RotateTransform(Math.Atan2(tangent.Y, tangent.X)
      * 180 / Math.PI, width / 2, baseline));
    transformGroup.Children.Add(
      new TranslateTransform(point.X - width / 2,
        point.Y - baseline));

    child.RenderTransform = transformGroup;
    progress += width / 2 / pathLength;
  }
}
OnTextPropertyChanged メソッドは、Text プロパティに対するプロパティ変更ハンドラであり、すべての TextBlock 子要素を作成する役割を担います。mainPanel 変数は、UserControl の Content プロパティに設定された Canvas オブジェクトです。フォント関連プロパティおよび PathFigure プロパティに対する他のプロパティ変更ハンドラと同様に、OnTextPropertyChanged は最後に OrientTextOnPath を呼び出します。
OrientTextOnPath は基本的に、すべての TextBlock 要素に対して変換を適用します。パスの合計長 (TextOnPathBase は DLL 内の別のクラス) と TextBlock オブジェクトの合計幅を取得して、非常に重要な scalingFactor を計算します。このメソッドは最後に、Canvas のすべての TextBlock 子要素を対象とした大きなループを実行します。GetPointAtFractionLength メソッドに渡される変数 "progress" は、倍率適用後の文字幅の半分に基づいているため、メソッドから返される点は文字の中点に対応します。
ループの最後には、3 つの変換の定義があります。最初の変換は、scalingFactor に基づいて TextBlock のサイズを調整します。次に、TextBlock は、GetPointAtFractionLength から返された接線値に基づいて回転されます。回転の中心は文字のベースラインの中点であることに注意してください。最後に、TranslateTransform により、サイズ調整と回転を行った文字をパス上の目的の点まで移動します。この 3 つの変換のグループが、TextBlock の RenderTransform プロパティとなります。
図 1 の画像の作成には、図 4 に示される TextOnPathControlDemo1.xaml ファイルを使用しました。TextOnPathControl クラス自体は、テキストの配置に使用される PathFigure を描画しないことに注意してください。描画するには、その処理を別途準備する必要があります。ただし、TextOnPathControlDemo1.xaml に示されるように、両方のタスクに同じ PathGeometry を使用できます。
図 4 TextOnPathControl
<!-- TextOnPathControlDemo1.xaml by Charles Petzold, September 2008 -->
<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:textonpath=
    "clr-namespace:Petzold.TextOnPath;assembly=Petzold.TextOnPath"
  Title="TextOnPathControl Demo #1"
  WindowTitle="TextOnPathControl Demo #1"
  FontFamily="Times New Roman"
  Foreground="Blue">

  <Page.Resources>
    <PathGeometry x:Key="path"
      Figures="M 100 100 C 200 150 300 0 400 100" />
  </Page.Resources>

  <Grid>
    <Path
      Data="{StaticResource path}"
      Stroke="Red" />

    <textonpath:TextOnPathControl
      Text="Hello, Path!"
      PathFigure="{Binding Source={StaticResource path},
      Path=Figures[0]}" />
  </Grid>
</Page>

サイズについて
TextOnPathControl クラスは、UserControl の多用性の一端を示していますが、同時にその制限も示しています。コントロールはすべての TextBlock オブジェクトの親として Canvas を使用しますが、明示的な高さや幅を与えられていない Canvas は、レイアウトのためのサイズを持っていません。たとえば、このコントロールを StackPanel に配置した場合、何も表示されません。
TextOnPathControl 内の Canvas は、単一セルの Grid で置き換えることもできます。その場合、コントロールに対してレイアウト用にゼロでないサイズが与えられますが、そのサイズは正しくありません。コントロールのサイズは、すべてのサイズ調整前の TextBlock 子要素が重なって表示されるとして計算されます。これはまさに、コントロールが RenderTransform の適用前に行うことです。ご存知のとおり、RenderTransform はレイアウトに何も影響を与えません。
変換後の TextBlock 要素が WPF レイアウト システムで認識されるように、RenderTransform を LayoutTransform で置き換えることも考えられます。しかし、LayoutTransform は移動変換を無視するため、これはうまくいきません。
通常、UserControl に設定されるビジュアル ツリーには、WPF レイアウト システムに各自のサイズを報告する、他のコントロールおよび要素が含まれています。したがって、UserControl は、そのすべての子要素の合計サイズを持つことになります。しかし、これらの子要素の RenderTransform プロパティを設定する場合、それらが要素の境界内に収まる保証はありません。
事実を認めましょう。TextOnPathControl クラスは UserControl の多用性を示しますが、それは決して最適なアプローチではありません。個々の文字それぞれに対して TextBlock を作成することは、過剰な作業であり、各文字をそれぞれ独自のマウス処理やツールチップなどに関連付ける場合にだけ意味を持ちます。

レベルを下げる
そこで、レベルを少し下げて、FrameworkElement から派生することにしましょう。これは、TextBlock それ自体、および Control を含む他の多くの要素に対して、共通の基本クラスです。一般に、FrameworkElement から派生するクラスは、OnRender メソッドのオーバーライドで要素の視覚的内容を描画します。OnRender メソッドは、DrawingContext 型のオブジェクトを使用して呼び出されます。これは、DrawText などの描画メソッドの完全なセットを定義するクラスです。これは、本格的な WPF アプリケーションで実行できる最も低いレベルのグラフィック出力です。
残念ながら、FrameworkElement は、Text や PathFigure はもとより、FontFamily、FontStyle、FontWeight、FontStretch、Foreground といった必要なプロパティを備えていません。このコラムで使用する他のクラスでもこれらの同じプロパティを必要とするため、この 7 つのプロパティすべてを TextOnPathBase というクラスに配置しました。また、このクラスは、OnFontPropertyChanged、OnForegroundPropertyChanged、OnTextPropertyChanged、および OnPathPropertyChanged という 4 つの抽象プロパティ変更メソッドを定義します。子孫クラスではこれらをオーバーライドして、測定および描画コードを実装します。TextOnPathElement クラスは、TextOnPathBase から派生しています。図 5 に、このクラスの抜粋を示します。
図 5 TextOnPathElement.cs からの抜粋
protected override void OnTextPropertyChanged(
  DependencyPropertyChangedEventArgs args)
{

  formattedChars.Clear();
  textLength = 0;

  foreach (char ch in Text)
{
    FormattedText formattedText =
      new FormattedText(ch.ToString(),
      CultureInfo.CurrentCulture,
      FlowDirection.LeftToRight, typeface, 100,
      Foreground);

    formattedChars.Add(formattedText);
    textLength +=
      formattedText.WidthIncludingTrailingWhitespace;
  }

  InvalidateMeasure();
  InvalidateVisual();
}

protected override void OnPathPropertyChanged(
  DependencyPropertyChangedEventArgs args)
{

  pathLength = GetPathFigureLength(PathFigure);

  InvalidateMeasure();
  InvalidateVisual();
}

protected override Size MeasureOverride(Size availableSize)
{
  if (PathFigure == null)
    return MeasureOverride(availableSize);

  Rect rect = new PathGeometry(
    new PathFigure[] { PathFigure }).Bounds;
  return (Size)rect.BottomRight;
}

protected override void OnRender(DrawingContext dc)
{
  if (pathLength == 0 || textLength == 0)
    return;

  double scalingFactor = pathLength / textLength;
  double progress = 0;
  PathGeometry pathGeometry =
    new PathGeometry(new PathFigure[] { PathFigure });

  foreach (FormattedText formText in formattedChars)
{
    double width = scalingFactor *
      formText.WidthIncludingTrailingWhitespace;
    double baseline = scalingFactor * formText.Baseline;
    progress += width / 2 / pathLength;
    Point point, tangent;

    pathGeometry.GetPointAtFractionLength(progress,
      out point, out tangent);
    dc.PushTransform(
      new TranslateTransform(point.X - width / 2,
      point.Y - baseline));
    dc.PushTransform(
      new RotateTransform(Math.Atan2(tangent.Y, tangent.X)
      * 180 / Math.PI, width / 2, baseline));
    dc.PushTransform(
      new ScaleTransform(scalingFactor, scalingFactor));

    dc.DrawText(formText, new Point(0, 0));
    dc.Pop();
    dc.Pop();
    dc.Pop();

    progress += width / 2 / pathLength;
  }
}
Text プロパティまたはいずれかの Font プロパティが変更されると、このクラスは文字列内の各文字に対して FormattedText オブジェクトを作成します。これらの文字の幅は、textLength フィールド内で加算されます。PathFigure プロパティが変更されると、パスの長さが pathLength フィールドに格納されます。実際の作業のほとんどは、OnRender オーバーライドで実行されます。
OnRender は、テキスト文字列内の各文字に対して、DrawingContext の DrawText メソッドを呼び出します。ただし、3 つの PushTransform 呼び出しに注意してください。これらは、TextOnPathControl クラスで見たのと同じ変換ですが、適用されたときと逆の順序で DrawingContext スタックにプッシュされる必要があります。DrawText の呼び出し後、Pop の呼び出しによって DrawingContext スタックから 3 つの変換が取り出されます。

測定の難点
FrameworkElement のレベルで作業する際には、MeasureOverride メソッドのオーバーライドによって、WPF に要素の大きさを伝えることもできます。必要なのは、要素で必要とされる Size オブジェクトを返すことです。MeasureOverride には availableSize 引数があり、これはグラフィックのサイズ調整に使用することも、完全に無視することもできます。
ただし、難点があります。WPF レイアウト システムは、要素が OnRender メソッドで左上隅を点 (0, 0) としてレンダリングされていると仮定するため、それ以外の場合には、多少問題が生じます。たとえば、点 (-50, 100) から点 (100, 50) まで直線を描くことだけを目的とした FrameworkElement の派生クラスを記述するとします。OnRender メソッドには、おそらく次のような呼び出しが含まれます。
dc.DrawLine(pen, new Point(-50, 100), new Point(100, 50));
しかし、MeasureOverride は何を返すでしょうか。標準的なアプローチは、レンダリングされるオブジェクトの最大の X、Y 座標に基づいて Size オブジェクトを返すことです。これは、MeasureOverride の本体が単純に次のようになることを意味します。
return new Size(100, 100);
実際には、MeasureOverride では直線の描画に使用されるペンの幅を考慮する必要がありますが、ここではそのような詳細は無視します。
この要素を、背景色をシアン、列および行の幅を共に GridLength.Auto に設定して Grid セルに配置すると、結果は図 6 の最初の図のようになります。
図 6 直線を描画してそのサイズを指定する各種の方法
縦方向については、グリッド セルの座標がゼロで終わっていますが、直線がサイズ内に収まらず、セルの左側の境界からはみ出しています。これを修正するために、実際にレンダリングされたグラフィックの幅と高さに等しい値を MeasureOverride が返すようにしてみましょう。
return new Size(150, 50);
しかし、それではさらにひどくなってしまいます。図 6 の 2 番目の図に示されるように、直線が依然としてセルに収まらない上に、セルが大きすぎます。
そこで、ずっと賢明な方法として、MeasureOverride から (150, 150) のサイズを返す一方で、OnRender メソッドで原点が (0, 0) となるように座標を調整します。単純に、X 座標に 50 を加算し、Y 座標から 50 を減算します。
dc.DrawLine(pen, new Point(0, 50), new Point(150, 0));
これにより、図 6 の 3 番目の図に示されるように、グラフィックが Grid セルにぴったり収まりました。ただし、このグラフィックを同じ Grid セル内または Canvas 上で他の直線と混在させる場合、DrawLine 呼び出しによって座標点が調整されているため、適切にレンダリングされません。パス上のテキストに関するクラスの 1 つを使用してテキストをパス上に表示すると、テキストとパスが揃わなくなります。
不本意ながら、図 6 の最初の図が正しいアプローチを示していると結論せざるを得ません。これは、たとえば通常の Line 要素が行っている動作です。
ここで、図 6 の 4 番目の図に示されるように、パス上にテキストを配置してみます。テキストを収めるために要素のサイズはどのように変更すればよいでしょうか。この場合について言えば、まったく変更する必要はありません。テキストは、直線の最大水平座標も最大垂直座標も超えていません。テキストと差が生じるような他のパスもありますが、通常は大きく異なることはありません。これが、図 5 に示されるように、TextOnPathElement メソッドが PathFigure のサイズだけに基づいて MeasureOverride メソッドを実装している理由です。次に説明するクラスでは、もう少し高度なことを行います。

ビジュアルな子の世界
FrameworkElement から派生するクラスは、Visual からも派生するため、ビジュアルな子のコレクションを保持できます。実用的な観点で見ると、これらの子は通常は DrawingVisual 型です。そして、ある程度、これらの DrawingVisual オブジェクトには要素に似た面があります。つまり、ビジュアルな外観を持ち、ヒット テストに使用できます (ただし、ずっと軽量です)。キーボード、マウス、またはスタイラスのイベントは受け取らず、レイアウトには直線関与しません。
最初に DrawingVisual 型のオブジェクトを作成した後は、一般に RenderOpen メソッドを呼び出して DrawingContext オブジェクトを取得します。この DrawingContext オブジェクトと共に、OnRender オーバーライドで使用したのと同じ描画メソッドを使用します。最後に、Close メソッドを呼び出します。描画コマンドによってレンダリングされたグラフィックは、DrawingVisual に格納されます。
DrawingVisual オブジェクトを画面上に直接レンダリングすることはありません。その代わりに、DrawingVisual オブジェクトを作成して格納するクラスは、VisualChildrenCount プロパティをオーバーライドしてビジュアルな子の数を示し、また、GetVisualChild メソッドをオーバーライドして、インデックスに基づきビジュアルな子を返す必要があります。
ビジュアルな子を使用するクラスは、ビジュアルな子がイベント ルーティングに関与するように、AddVisualChild および RemoveVisualChild も呼び出す必要があります。ビジュアルな子を VisualCollection オブジェクトに格納する場合、このオーバーヘッドはシステムによって処理されます。
パス上にテキストを表示するためには、テキスト文字列の各文字に対して DrawingVisual オブジェクトを作成するのが適切です。OnRender メソッドで描画する場合と同様に、PushTransform メソッドを呼び出してグラフィックに変換を適用できます。ただし、パス上にテキストを配置するには、もっと良いアプローチがあります。DrawingVisual はそれ自体が Transform プロパティを持っています。つまり、前に TextOnPathControl クラスで行ったように、DrawingVisual オブジェクトの作成と Transform の適用を分離することができます。この分離により、DrawingVisual オブジェクトの再作成なしで PathFigure をアニメーション化できます。
図 7 のコードは、TextOnPathVisuals クラスの 2 つのメソッドを示しています。GenerateVisualChildren メソッドは、Text プロパティまたはいずれかのフォント プロパティが変更されたときに呼び出され、最後に TransformVisualChildren を呼び出します。TransformVisualChildren メソッドは、PathFigure が変更されたときに呼び出されます。GenerateVisualChildren メソッドでは、3 つの変換を含む TransformGroup を作成し、それを DrawingVisual の Transform プロパティに設定することで、DrawingVisual を準備しています。これらの変換には、TransformVisualChildren メソッドで適切な値が設定されます。
図 7 TextOnPathVisuals からの抜粋
protected virtual void GenerateVisualChildren() {
  visualChildren.Clear();

  foreach (FormattedText formText in formattedChars)
{
    DrawingVisual drawingVisual = new DrawingVisual();

    TransformGroup transformGroup = new TransformGroup();
    transformGroup.Children.Add(new ScaleTransform());
    transformGroup.Children.Add(new RotateTransform());
    transformGroup.Children.Add(new TranslateTransform());
    drawingVisual.Transform = transformGroup;

    DrawingContext dc = drawingVisual.RenderOpen();
    dc.DrawText(formText, new Point(0, 0));
    dc.Close();

    visualChildren.Add(drawingVisual);
  }

  TransformVisualChildren();
}

protected virtual void TransformVisualChildren()
{
  boundingRect = new Rect();

  if (pathLength == 0 || textLength == 0)
    return;

  if (formattedChars.Count != visualChildren.Count)
    return;

  double scalingFactor = pathLength / textLength;
  PathGeometry pathGeometry =
    new PathGeometry(new PathFigure[] { PathFigure });
  double progress = 0;
  boundingRect = new Rect();

  for (int index = 0; index < visualChildren.Count; index++)
{
    FormattedText formText = formattedChars[index];
    double width = scalingFactor *
      formText.WidthIncludingTrailingWhitespace;
    double baseline = scalingFactor * formText.Baseline;
    progress += width / 2 / pathLength;
    Point point, tangent;
    pathGeometry.GetPointAtFractionLength(progress,
      out point, out tangent);

    DrawingVisual drawingVisual =
      visualChildren[index] as DrawingVisual;
    TransformGroup transformGroup =
      drawingVisual.Transform as TransformGroup;
    ScaleTransform scaleTransform =
      transformGroup.Children[0] as ScaleTransform;
    RotateTransform rotateTransform =
      transformGroup.Children[1] as RotateTransform;
    TranslateTransform translateTransform =
      transformGroup.Children[2] as TranslateTransform;

    scaleTransform.ScaleX = scalingFactor;
    scaleTransform.ScaleY = scalingFactor;
    rotateTransform.Angle =
      Math.Atan2(tangent.Y, tangent.X) * 180 / Math.PI;
    rotateTransform.CenterX = width / 2;
    rotateTransform.CenterY = baseline;
    translateTransform.X = point.X - width / 2;
    translateTransform.Y = point.Y - baseline;

    Rect rect = drawingVisual.ContentBounds;
    rect.Transform(transformGroup.Value);
    boundingRect.Union(rect);

    progress += width / 2 / pathLength;
  }
  InvalidateMeasure();
}
結果として、TextOnPathElement クラスよりも多少優れたパフォーマンスと、文字を考慮したレイアウト サイズを示す機能が得られます。TransformVisualChildren メソッドは、最初に boundingRect という名前の Rect オブジェクトを作成します。変換はビジュアルな子のそれぞれに対して適用されるため、(変換されていないビジュアルを表す) ContentBounds プロパティが複合変換によって変更され、その長方形と boundingRect との和が求められます。
Rect rect = drawingVisual.ContentBounds;
rect.Transform(transformGroup.Value);
boundingRect.Union(rect);
その結果は、すべての変換された文字を収める長方形です。MeasureOverride の本体は実に単純です。
return (Size)boundingRect.BottomRight;
図 8 に、アニメーション化された正弦曲線の山と谷に沿ってテキストを表示する TextOnPathVisualsDemo プログラムの実行結果を示します。ピンク色の背景はレイアウト サイズを示し、これはアニメーションで異なる文字が右や下に移動すると変化します。右側の y および一番下の g のディセンダが収まるように長方形が拡張されていることに注目してください。
図 8 TextOnPathVisualsDemo プログラム
過去の記事
テキスト、アニメーション、および Microsoft .NET Framework に関する筆者の研究についてさらに知りたい方は、次に示す「基礎」コラムの過去の記事をご覧ください。

WPF アニメーション クラスを拡張する

WPF の 3D テキスト

ベクタ グラフィックスと WPF の Shape クラス

テキストを歪める
テキストをパス上に表示するためのまったく異なるアプローチが、TextOnPathWarped クラスに実装されています。このクラスでは、各文字のベースラインがパスに沿うように実際に曲線化されます。文字内の縦の線はそれぞれ直線のままですが、各点でパスに対して垂直になります。パスが曲げられると、文字は凸側では広がり、凹側では狭まります。
このアプローチでは、テキストを個々の文字に分割する必要がありません。準備段階として、テキスト文字列全体に対して FormattedText の BuildGeometry メソッドを呼び出してから、形状をフラット化します。変換は適用されませんが、少しの三角法が必要となります。一般的な問題として、テキストをパスに変換すると、テキストのレンダリング ヒントが失われ、小さなサイズでは読み取るのが難しくなります。パスはアニメーション化できますが、長いテキスト文字列の場合、パフォーマンスに大きく影響する可能性があります。
TextOnPathWarped は、ShiftToOrigin プロパティも定義します。このクラスを true に設定すると、曲げられた形状内のすべての点が、左上隅が (0, 0) となるようにオフセットされます。これにより、このクラスは MeasureOverride メソッドで実際のサイズを示すことができますが、その結果は他のグラフィックとは正しく組み合わせることができません。図 9 のスクリーンショットのように、いくつかの場合には、文字が歪んでいるのを実際に見ることができます。
図 9 TextOn-PathWarpedDemo3 プログラム
同じ問題に対するいくつかの異なるソリューションを見るのは常に興味深いものですが、どれが最良でしょうか。全体的には、(特に、パスのアニメーション化も行う場合には) TextOnPathVisuals クラスでビジュアルな子を使用するのが最良のソリューションと筆者は考えます。

ご意見やご質問は 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』(Wiley、2008 年) の著者でもあります。彼の Web サイトは、charlespetzold.com です。


Windows と C++
擬似変数と書式指定子を使用した x64 でのデバッグ
Kenny Kerr
目次
プロセッサ アーキテクチャ
擬似変数
書式指定子
呼び出し規約を視覚化する
エラー コード
セキュリティ コンテキストをデバッグする
何年も前から、Visual C++ にはデバッグに使用するための一連の擬似変数と書式指定子が用意されています。擬似変数とは、デバッガのウォッチ ウィンドウに入力することにより、そこに C++ 変数とは必ずしも関連のないなんらかの値を表示させることができるものです。残念ながら、これまで擬似変数について詳しく解説しているドキュメントはありませんでした。筆者は自分でドキュメントを作成できるほどの内部情報は持ち合わせていませんが、最も有用だと思った擬似変数と書式指定子の一部をご紹介したいと思います。このコラムをきっかけにして、このトピックに対する皆さんの学習意欲が高まることを願っています。
擬似変数 (擬似レジスタと呼ぶこともあります) について説明する前に、プロセッサ アーキテクチャとレジスタについて少し説明しておきます。ほんの少しの背景情報で、擬似変数をさらに役立てることができるからです。また、64 ビット版のアプリケーションと従来の 32 ビット版のアプリケーションとの違いの一面を理解するのにも役立ちます。今回のコラムの主役はプロセッサ アーキテクチャではなく Visual C++ ですので、プロセッサ アーキテクチャについては x86 と x64 に絞って話を進めていきます。Itanium プロセッサによってもたらされた相違点については、MSDN ライブラリのドキュメントを参照してください。

プロセッサ アーキテクチャ
プロセッサ アーキテクチャについて完全に理解することは、ごく平凡な人間には簡単なことではありません。そのため、C 言語とその後続言語はソフトウェア業界に大きな影響を与えました。x86 および x64 プロセッサ アーキテクチャはほとんどのユーザーが所有しているプロセッサですから、少なくともそれらが何を表しているのかはご存じでしょう。Windows および Visual C++ では Itanium プロセッサもサポートされていますが、Itanium プロセッサを対象にする余裕のある開発者はほとんどいません。
良くも悪くも、8 ビット Intel 8080 プロセッサが起源の x86 は、この数十年間主流を占めています。AMD と Intel は、x86 との下位互換性を維持するために x64 では実践的な選択を行いました。一方、Itanium は従来の x86 に縛られない強力な新しいアーキテクチャをもたらしました。もちろん、その特性ゆえに、対応アプリケーションの数ははるかに少ないですが、Windows が何年にもわたってそれほど困難なく新しいアーキテクチャを取り入れることができているのは、Windows NT の開発を指揮した David Cutler と、移植可能なオペレーティング システムという彼のビジョンの功績の証です。
プロセッサ アーキテクチャ間の違いの多くは C++ コンパイラによって隠蔽されますが、開発者にとって明らかな利点となるのが 1 つの呼び出し規約への移行です。x86 コンパイラでは複数の呼び出し規約がサポートされますが、x64 版の C++ コンパイラでは 1 つの呼び出し規約のみがサポートされます。これは歓迎すべき変化です。なぜなら、呼び出し規約の不一致が原因の潜在的なバグの多くを解消できるからです。特に、マネージ コード相互運用機能では、C# や Visual Basic .NET などの Microsoft .NET Framework コンパイラにおいて C++ ヘッダー ファイル内の元の呼び出し規約を確認できません。そのため、手動で作成した属性を使用する必要があります。属性の定義は間違いやすく、正しく定義しなかった場合はさまざまなスタック破損バグが発生します。
たとえば、thiscall と呼ばれるネイティブ C++ の呼び出し規約を使用すると、ecx レジスタには、メンバ関数が呼び出される前に "this" ポインタが格納されます。この知識はデバッグする際に役に立ちますが、呼び出し規約に依存している点が問題です。呼び出し規約は、アセンブラ コードやレジストリ値を見ただけではわからないからです。x64 では、"this" ポインタは最初のパラメータとして常に挿入され、rcx レジスタに格納されるため、はるかに単純になります。
通常、デバッグ時のレジスタの使用方法はプロセッサによってかなり異なりますが、さいわいにも、x86 と x64 の関係を利用できます。x86 アーキテクチャによって、サブレジスタという概念が導入されました。サブレジスタは、そのスーパーレジスタの下位半分で構成されます。たとえば、ax は eax レジスタの下位半分で構成された 16 ビット サブレジスタです。x64 アーキテクチャでは、x86 の 32 ビット レジスタを引き継ぐ 64 ビット レジスタを提供することでレジスタを拡張しています。たとえば、x64 の eax は 64 ビット rax レジスタの下位半分で構成された 32 ビット サブレジスタです。eax と rax は、関数によって返されるポインタや整数に対して、それぞれ x86 と x64 で使用されるため、どちらも興味深いものがあります。通常、x86 で関数を実行して 64 ビット値を返すには、レジスタのペアを使用する必要があります。

疑似変数
最も一般的な疑似変数は、おそらく $err だと思います。この疑似変数は、SetLastError 関数によって設定された最新のエラー値を表示します。表示される値は、GetLastError 関数によって返される値を示しています。
疑似変数を使用して、プロセッサ レジスタの値を表示することもできます。たとえば、$ecx を使用すると、x86 アーキテクチャの ecx レジスタの値が表示されます。プロセッサ レジスタの値を確認することは、.NET を使用する新世代の開発者の目には少し不可解に映るかもしれませんが、わかりにくいバグの診断や実行時のプログラムの動作の理解に本当に役立ちます。x86 でネイティブ C++ の呼び出し規約を使用している場合は、$ecx を使用して "this" ポインタのアドレスを表示できます。また、$eax を使用すると、両方のアーキテクチャで、32 ビットの戻り値を表示することもできます。デバッグ ニーズに応じて、役に立つレジストリは他にもたくさんあります。
他に役に立つ擬似変数には、プロセスのカーネル オブジェクトに対して開いているハンドルの数を表示する $handles や、現在のプロセス トークンとスレッド トークンに関する詳細情報を表示する $user があります。後者は、偽装関連のコードのデバッグに役立ちます。図 1 に、一般的な擬似変数の一部を示します。
図 1 疑似変数
疑似変数 | 説明
$handles | カーネル オブジェクトへのハンドルの数
$vframe | 現在のスタック フレームのアドレス
$TID | 現在のスレッド ID
$registername | 指定されたレジスタの内容
$clk | クロック周期の時間
$user | プロセス トークンとスレッド トークンの情報

書式指定子
書式指定子は、変数、擬似変数、または式の値をウォッチ ウィンドウで表示する方法を変更する場合に便利です。ほとんどの場合、ウォッチ ウィンドウでは値の型に基づいて最適な書式が選択されますが、場合によっては、その書式をオーバーライドする必要があります。デバッガが変数の型を推測できない場合や、型情報を通常持たないメモリ アドレスを表示する場合です。たとえば、hr 書式指定子を使用すると、値に対応する Win32 または HRESULT が表示されます。他には、値を Null で終わる Unicode 文字列として表示する su があります。
もちろん、なんらかの書式が設定されているテキストの実際の値を表示できます。その場合は、! を使用します。最後に、ポインタの場合は、コンマの後に表示する要素数を指定することで、ウォッチ ウィンドウでポインタを n 要素の配列として扱うことができます。図 2 に、書式指定子を示します。
図 2 書式指定子
指定子 | 説明
D | 10 進数
U | 符号なし 10 進数
O | 8 進数
X | 16 進数
F | 浮動小数点
E | 指数表記
C | 文字
S | 文字列
Su | Unicode 文字列
s8 | UTF-8 文字列
Hr | HRESULT または Win32 エラー コード
wc | Windows クラス
wm | Windows メッセージ
! | 未処理の書式

呼び出し規約を視覚化する
背景情報はこれぐらいにして、いくつか例を見てみましょう。次のコードについて考えます。x86 コンパイラでは、メンバ関数に対して既定で __thiscall 呼び出し規約を使用します。これは、パラメータがスタックにプッシュされ、"this" ポインタが ecx レジスタに格納されることを示しています。
struct Sample
{
    size_t m;
    Sample() : m(0x11223344) {}

    size_t Method(size_t p1, size_t p2)
    {
        return 0x44556677;
    }
};
図 3 をご覧ください。ウォッチ ウィンドウに、興味深い変数が表示されています。$vframe によって、現在のスタック フレームのアドレスが表示されています。この疑似関数を使用すると、プロセッサに依存しない方法で、スタック フレームのアドレスを取得できます。次に、2 つのパラメータのアドレスが表示されています。それぞれの値は、0x22334455 と 0x33445566 です。
図 3 32 ビット スタックとレジスタ (クリックすると拡大画像が表示されます)
1 つ目のメモリ ウィンドウにも、現在のスタック フレームが表示されています。また、スタック上にある 2 つのパラメータの値が確認できます。次は ecx レジスタですが、オブジェクトのメンバを表示するために、このレジスタをポインタとして Sample にキャストしています。2 つ目のメモリ ウィンドウにも、メモリ内のオブジェクトが表示されています。また、スタックのかなり下の方にあるメンバ変数が確認できます。次に、$eax 変数によって、関数呼び出しの結果を格納しているレジスタの値が表示されています。
同じ C++ コードを x64 コンパイラでコンパイルすると、結果はかなり異なったものになります。このコンパイラでは、x64 で利用可能なレジスタが増えたことを活かして、不必要に値がスタックにプッシュされないようにレジスタを積極的に使用します。特に、最初の 4 つのパラメータ (ポインタまたは整数) は、rcx、rdx、r8、および r9 の各レジスタにそれぞれ配置されます。メンバ関数の場合は、"this" ポインタが最初のパラメータとして扱われます。関数がパラメータをスタックに一時的に保存するときのために、領域が確保されています。最後に、ポインタや整数の結果はすべて、rax レジスタに格納されて返されます。
図 4 をご覧ください。ここでも、$vframe によって、現在のスタック フレームのアドレスが表示されています。今回は、値がすべて 64 ビットであることがわかります。次に、2 つのパラメータのアドレスが表示されています。パラメータはレジスタと共に呼び出された関数に渡されましたが、これらのパラメータは現在のスタック フレームに存在しています。これが、デバッグ ビルドです。この関数によって、パラメータがスタックに保存されました。次の 3 つの変数、$rcx、$r8、および $rdx によって、先頭に追加された "this" ポインタを含む 3 つのパラメータの値が表示されています。最後に、$rax によって、関数呼び出しの結果を格納しているレジスタの値が表示されています。
図 4 64 ビット スタックとレジスタ (クリックすると拡大画像が表示されます)

エラー コード
擬似変数と書式指定子は、さまざまな場所で発生する、さまざまな書式のエラー コードを確認する場合にも非常に役に立ちます。図 5 に、最新のエラー コードの値と説明を返す $err,hr など、いくつか例を示します。これは、ボリューム シャドウ コピー サービスの ERROR_VOLMGR_DISK_NOT_EMPTY エラー コードと同等です。$err 擬似変数を使用して値を取得し、hr 書式指定子を使用してその値をエラー コードとして書式設定するようにウォッチ ウィンドウに指示しています。hresult は C++ 変数なので、ウォッチ ウィンドウにはその値を表すよく知られた定数が自動的に表示されていることにも注意してください。E_INVALIDARG,x は、自動的に設定された書式をオーバーライドする方法を示しています。この例では、x 書式指定子を使用して、値を 16 進値として表示するようにウォッチ ウィンドウに指示しています。最後の例には、ほとんどのアドレスを配列に変換できる方法を示しています。
図 5 エラー コード (画像をクリックすると拡大表示されます)

セキュリティ コンテキストをデバックする
図 6 に、すばらしい $user 擬似変数を示します。なんと、Bob が Alice を装っているのがわかります! この擬似変数を使用すると、プロセス トークンとスレッド トークンの両方の詳細を取得できるため、クライアント/サーバー アプリケーションで偽装問題をデバッグするときに役に立ちます。
図 6 $user 擬似変数 (クリックすると拡大画像が表示されます)
Visual Studio のウォッチ ウィンドウで使用できる手法は他にもあります。C# コードのデバッグ用の技法もいくつかあります。さらに、Windows 用デバッグ ツールには、さまざまな擬似変数が用意されています。デバッグに関するヒントについては、『Advanced Windows Debugging』を参照してください。

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

Kenny Kerr は、Windows 向けのソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr です。


いろいろな場所へ
接続マネージャの接続方法
Marcus Perryman
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照
目次
PDP コンテキスト
ネットワーク トポロジのモデリング
接続する
接続要求と調停
SDK サンプル コードを使用する
あれはカンファレンスの 3 日目のことでした。椅子に座って凍るようなエアコンの冷気を浴びながら、新しい接続マネージャ テクノロジのすばらしさを語る主任プログラム マネージャの説明を聞いていると、前の 2 日間の濃密な情報のせいで (前夜に遅くまで飲みすぎたせいもあるかもしれません)、頭がぼやけてきました。
そのぼやけた頭で、このテクノロジの重要性に疑問を抱きました。デバイスが接続されていることを通知するテクノロジが一体何の役に立つのかという疑問です。当時、接続にはアンビリカル ケーブルや途方もない付属物が必要で、ネットワーク プロトコルの分析に博士級の知識を要し、操作にも多大な忍耐が要求されました。デバイスが接続されているかどうかもわからないユーザーにデバイスを所有する資格はありませんでした。
ところが、テクノロジが進化して、最近の平均的な携帯電話にはお弁当のおかずの揚げ具合を 40 段階に調節できる機能など、奇抜な無線テクノロジが溢れています。Windows Mobile デバイスはさらに豊富な機能を持ち、Windows CE のプリエンプティブなマルチタスク カーネルと統合されています。これで接続マネージャ チームの先見性が証明されました。
接続マネージャがなければ、アプリケーションで接続を選択するには、どんなに単純なネットワーク環境でもユーザーに接続のリストを提示して選択させる以外に方法はありません。この方法を実装するにしても、博士号級の知識を要するネットワーク プロトコルの分析が必要になります。接続マネージャでは、アプリケーションが動作しさえすればよいと思っているユーザーに接続を選択させるのではなく、この問題を解決しなければユーザーにはなすすべがないという理由から、携帯電話会社および OEM に問題を委ねます。
主に 2 つの理由から、Windows Mobile アプリケーションでネットワーク データを使用する際には接続マネージャを適切に使用することが重要です。1 つ目の理由は、接続を必要としているのは自分が作成するコードだけではないということです。最近のアプリケーションの多くは、更新プログラムや追加機能をダウンロードするために接続または少なくとも何らかのオンライン機能を使用します。そのため、デバイス上のどこかで 2 つのアプリケーションの接続が競合する可能性が高くなっています。電子メール ソリューションにプッシュまたはポーリングを使用している企業ユーザーの場合、必ず競合が起こるといっても過言ではありません。2 つ目の理由は、Windows Mobile デバイスが世界中で販売され、使用されているということです。グローバル市場を惹きつけることに成功したアプリケーションは、携帯電話会社が夢見るあらゆるネットワーク構成を巧みに操作する必要があります。筆者の経験上、携帯電話会社はネットワーク トポロジのことになると想像が逞しくなります。
何かをうまく使いこなすには、理解を深めることが重要です。このコラムでは、接続マネージャの内部処理に光を当て、広域携帯電話ネットワーク上で接続する場合の値の追加方法や、複雑さを解消する方法を説明します。これは、接続マネージャ API を直接呼び出す機会が多い C++ 開発者にとって特に重要な問題だと思われます。マネージ コードの開発者には、接続マネージャを間接的に使用する HttpWebRequest などのさまざまなクラスが用意されています。とはいえ、.NET 開発者にとっても、接続マネージャがマネージ コードの動作を背後でどのように処理しているかを理解することは、複雑な接続の問題を解決するうえで役立ちます。

PDP コンテキスト
ネットワークが複雑になる原因の多くは、携帯電話会社が 3 つの競合する要件の狭間を進まなければならないことにあります。まず、携帯電話会社には、無線ネットワーク テクノロジの目まぐるしい変化についていくという顧客要件があります。2 つ目に、膨大なインフラストラクチャを使用して広域の無線エリアを確保する必要があります。3 つ目に、株主に損をさせないという要件があります。
この課題が特に重要になり、ソフトウェア開発者に直接影響するのは、パケット データ プロトコル (PDP) コンテキストが関連する場合です。PDP コンテキストは電話のハンドセットと GSM (Global System for Mobile Communication) ネットワークとの合意を記述し、APN (アクセス ポイント名) や IP アドレスなどの重要な情報を含んでいます。この合意は不変であり、作成後に変更することはできません (詳細については、Wikipedia の「GPRS コア ネットワーク」の記事を参照してください)。
アプリケーションでデータを送受信できるようにするには、まず、基地局に要求し、対象 APN 名を送信することによって PDP コンテキストを確立する必要があります。通常、要求は携帯電話会社の課金ネットワークに転送されて承認の対象になり、ユーザーに有効な契約が存在するか、クレジット残高は不足していないかなどの確認が行われます。この処理が完了すると、APN によって IP アドレスが提供され、基地局とハンドセットに渡されます。これで、割り当てられた IP アドレスを使用してデバイスはデータを送受信できます。
本当に PDP コンテキストがあれば十分なのでしょうか。常に十分であるとはいえませんが、APN にはさまざまな方法で課金やトラフィック制限を行う便利な手段があります。たとえば、特定の APN で企業のプライベート ネットワークに直接接続し、携帯電話会社の課金システムを使用して APN へのアクセスを制限できます。または、ある APN をメガバイトごとの課金システムで使用し、別の APN を "使い放題" サービスで使用することもできます。この方法は広く採用されています。実際の例として、携帯電話会社が特定の APN を介してすべてのプッシュ電子メール データを送信し、別の APN を介して他のインターネット トラフィックを送信する場合があります。
前述のように、一般に PDP 要求は課金インフラストラクチャを使用してルーティングされ、これには時間がかかる場合があります。事業者やネットワークによって異なりますが、コンテキストの作成には最大 20 秒かかります。PDP コンテキストは不変なので、APN を動的に変更することはできません。その場合は、コンテキスト全体を破棄して、新しいコンテキストを作成する必要があります。
パフォーマンスのためだけに複数の PDP コンテキストの同時実行をサポートすることは優れた方法ですが、ご想像どおり、それにはハンドセットと基地局の追加機能が必要です。さいわいなことに、最近のハンドセットの多くは 3 つ以上のコンテキストをサポートしていますが、基地局に関してはもう少し特異な事情があります。多くの新しい装置はこの機能をサポートしていますが、1 つのハンドセットで 1 つの PDP コンテキストのみをサポートする第 1 世代の 3G 基地局もいまだに相当数利用されています。
そのため、各アプリケーションを適切に接続するために相当に複雑なルールが設けられています。しかも、接続する基地局と電波のサービス レベルに応じて、適切な方法は頻繁に変わります。

ネットワーク トポロジのモデリング
接続マネージャには、主に次の 3 つの目的があります。
  • 携帯電話会社や OEM が多様なネットワーク トポロジおよびデバイス機能を指定するのに十分な柔軟性と構成可能性を備える。
  • 開発者が任意のネットワーク上の任意のハンドセットでネットワーク リソースを選択し、接続するために必要な手間を減らし、手順を簡素化する。
  • 限られたリソースを求めて競合しているアプリケーション間を調停する。
前述したような PDP コンテキストの複雑さと、接続マネージャのサポートが GPRS (汎用パケット無線サービス) 以外に WiFi、ダイヤルアップ、有線、さらにデスクトップ パススルー接続にまで拡張されていることを考えると、これらの目的を達成するのは容易ではありません。
その手順としては、まず、システムの有効期間中に主流になる新しいテクノロジも含めた十分に抽象化されたモデルを作成し、使用可能な接続の全体像を接続マネージャに伝えます。接続マネージャは、携帯電話会社や OEM がハードウェアおよびネットワーク トポロジをモデル化するために使用できる単純で一貫性のある構成プリミティブを集めたものです。各プリミティブは Open Mobile Alliance (OMA) Device Management、OMA Client Provisioning、または場合によってはユーザー インターフェイスの設定によって構成可能です。
次に、参考のために単純な OMA Client Provisioning の XML のサンプルを示します。
<wap-provisioningdoc>
  <characteristic type="CM_GPRSEntries">
    <characteristic type="Internet GPRS">
      <parm name="DestId" value="{436EF144-B4FB-4863-A041-8F905A62C572}" />
      <characteristic type="DevSpecificCellular">
        <parm name="GPRSInfoValid" value="1" />
        <parm name="GPRSInfoAccessPointName" value="MyInternetAPN" />
      </characteristic>
    </characteristic>
</wap-provisioningdoc>
基本的なコンポーネントはメタネットワークで始まります。これは接続マネージャで使用される最も抽象的な概念であり、ネットワーク カードやコンピュータなどの物理的なコンポーネントにマップすることはできませんが、これらのエントリはネットワーク モデルのエンドポイントまたはノードとして認識されます。多くの場合は Internet、DMZ、Walled Garden などのわかりやすい名前が付けられ、GUID によって識別されます。MSDN ライブラリで公開され、あらゆるデバイスに展開されている "Internet" や "work" など、一般的に使われる接続先の定義済みリストがありますが、このリストは高度に拡張可能です。CM_Networks は、プロビジョニング時の OMA Client Provisioning 構成名です。
GPRS などのパケット データ接続は、APN 名、ユーザーおよびパスワード情報などが設定されている固有の構成セクションに指定されます。これらの設定には、接続が確立されたときに使用可能になるメタネットワーク (宛先メタネットワーク) の ID も含める必要があります。パケット データのエントリは常にデバイスであるため、ソース ネットワークは存在しません。CM_GPRSEntries は OMA Client Provisioning に使用します。
WiFi やダイヤルアップなど、接続マネージャによって管理可能なハードウェアには別のエントリがあります。VPN およびプロキシのエントリの場合はメタネットワーク間 (ソース メタネットワークと宛先メタネットワークとの間) の接続を提供するため、若干異なる固有の構成があります。
これにもう 1 つ重要な構成設定を追加する必要があります。それはマッピング テーブルです (CM_Mappings は OMA Client Provisioning に使用します)。厳密に言うと、このテーブルはネットワーク トポロジに関するモデルではなく、アプリケーションが宛先メタネットワークを検出するためのものです。メタネットワークは事業者間、デバイス間で変わるため、アプリケーションには、URL がわかっていても、その URL に接続できるメタネットワークを知る手掛かりがありません。
マッピング テーブルは、指定された URL をメタネットワークの GUID に変換する API によって、メタネットワークの選択を携帯電話会社または OEM に任せます。マッピング テーブルが存在しない場合、宛先メタネットワークは固定しているか、ユーザーによって指定されています。この場合は従来のユーザー任せの問題が再浮上します。
図 1 は、モデル化可能なネットワーク トポロジと対応するマッピング テーブルの例を示しています。マッピング テーブルは URL パターンの順序を指定するリストです。各 URL パターンはメタネットワークの GUID と対になっており、その問い合わせには ConnMgrMapURL API を使用します。アプリケーションはターゲット URL を ConnMgrMapURL に渡し、最低の ID からマッピング テーブルの検索を開始して、URL に一致する最初のパターンを探します。一致するパターンが見つかると、GUID が返されます。その GUID を ConnMgrEstablishConnection に渡すと、接続を確立できます。
図 1 ネットワーク トポロジの例 (クリックすると拡大画像が表示されます)
図 2 は、マッピング テーブルを XML 形式で表した場合の例を示しています。ここでは、6 つのマッピング エントリが示され、501 が先頭になっています。また、パターン マッチの候補の多くを示しています。このパターンは正規表現としては不十分ですが、柔軟性が高くなっています。* はワイルド文字として使用します。たとえば、*://*.operator.*/* はすべてのプロトコル (テキスト パターン .operator. の後に / および末尾のページ名を付加したすべてのアドレス) にマップされます。次の URL はこの宛先ネットワークに一致します。
http://www.operator.com/
rtsp:// rtsp.operator.media.com/20070707_ABHDD3227DD/today_news1.sd
https://my.long.name.operator.da/index.aspx
図 2 マッピング テーブルの XML の例
<wap-provisioningdoc>
  <characteristic type="CM_Mappings">
    <characteristic type="501">
      <parm name="Pattern" value="*://*/*.3gp"/>
      <parm name="Network" value="{D3B2D798-9E69-4B65-A75B-6DDFBECEAAAA}"/>
    </characteristic>
    <characteristic type="610">
      <parm name="Pattern" value="*://*.operator.*"/>
      <parm name="Network" value="{7022E968-5A97-4051-BC1C-C578E2FBA5D9}"/>
    </characteristic>
    <characteristic type="536870912">
      <parm name="Pattern" value="wsp://*/*"/>
      <parm name="Network" value="{7022E968-5A97-4051-BC1C-C578E2FBA5D9}"/>
    </characteristic>
    <characteristic type="553648128">
      <parm name="Pattern" value="wsps://*/*"/>
      <parm name="Network" value="{F28D1F74-72BE-4394-A4A7-4E296219390C}"/>
    </characteristic>
    <characteristic type="570425344">
      <parm name="Pattern" value="*://*.*/*"/>
      <parm name="Network" value="{436EF144-B4FB-4863-A041-8F905A62C572}"/>
    </characteristic>
    <characteristic type="587202560">
      <parm name="Pattern" value="*://*/*"/>
      <parm name="Network" value="{A1182988-0D73-439E-87AD-2A5B369F808B}"/>
    </characteristic>
  </characteristic>
</wap-provisioningdoc>
最後の 4 つのエントリは Windows Mobile Standard の既定のエントリで、最後の 2 つのエントリは、それぞれインターネット (アドレスに . が付いている) および処理トラフィックを表します。
これは、ユーザーが多様な URL を入力するブラウザやメディア プレイヤーなどの汎用的なアプリケーションには確かに最適ですが、1 つの URL のみを使用するアプリケーションには過剰な処理です。使用する URL が 1 つだけの場合は、接続のたびにマッピング テーブルを使用することを強くお勧めします。これは、世界中のあらゆるネットワーク構成で適切なメタネットワークを取得する唯一の方法です。

接続する
ネットワークのモデルでは、接続マネージャがこの困難な処理を引き受けます。ここで API のドキュメントに書かれていることを繰り返し説明してもあまり意味はありません。MSDN ライブラリに掲載されている接続マネージャ API のドキュメントを参照した方が便利でしょう。それよりも、API の通常のシーケンスと主な設定の呼び出しに重点を置いて説明する方が有益だと思います。
接続の一般的なシーケンスは次のとおりです。
  1. 宛先メタネットワークの ConnMgrMapUrl を呼び出します。
  2. 接続マネージャの処理を指定する CONNMGR_CONNECTIONINFO 構造体を準備します。
  3. ConnMgrEstablishConnection (この API の同期バージョンもあります) を呼び出して要求を作成します。
  4. 接続マネージャのメッセージを監視し、状態の変更を追跡します。
  5. 接続が完了したら、ConnMgrReleaseConnection を呼び出して要求を終了し、リソースを再割り当てできるようにします。
図 3 は、接続の API シーケンスの簡潔な例を示しています。CONNMGR_CONNECTIONINFO 構造体には、設定を間違えやすい重要箇所がいくつかあります (この構造体の詳細については、MSDN ライブラリの「CONNMGR_CONNECTIONINFO」の記事を参照してください)。ここでは、特に重要な点を説明します。
DWORD dwFlags 開発者が意識する必要があるフラグがいくつかあります。たとえば、Proxy Aware オプションは、アプリケーション コードが通信に使用するプロキシ サーバーの種類を接続マネージャに通知します。これらのフラグによって、指定された種類のプロキシ サーバーで使用可能な個々のメタネットワークを対象に含めるか、除外するかを指定します。可能な限り、プロキシ対応のコード (通常は HTTP プロキシで十分です) を作成してください。何も難しいことはありません。ネイティブ C++ コードの場合は InternetOpen または InternetOpenURL WinInet API を使用します。これらの API が多くの処理を自動的に行います。マネージ コードの場合、.NET Compact Framework のマネージ クラス (HttpWebRequest など) の多くが既にプロキシ サーバーをサポートしているため、さらに処理が簡単です。
DWORD dwPriority 優先度は接続マネージャが限られた接続リソースの共有方法を決定する主要な方法であるため、優先度の設定はきわめて重要です。詳細については後述します。
BOOL bExclusive この設定は、接続マネージャで、宛先メタネットワークが同じである場合でも、他のアプリケーションとのネットワーク接続の共有を停止する場合に使用します。この設定はセキュリティの影響緩和に役立ちますが、使用する場合には注意が必要です。
図 3 接続マネージャの使い方を示すコード例
#include <windows.h>
#include <Connmgr.h>
#include <Wininet.h>

HRESULT ConnectionManager_Connect() {
  HRESULT hr = S_OK;
  GUID dest;
  CONNMGR_CONNECTIONINFO connInfo;

  // look up the URL and connect to the specified destination
  hr = ConnMgrMapURL(g_urlString, &dest, NULL);
  if (SUCCEEDED(hr)) {
    memset(&connInfo, 0, sizeof( connInfo));
    connInfo.cbSize = sizeof( connInfo );
    connInfo.bDisabled = FALSE;
    connInfo.bExclusive = FALSE;
    connInfo.hWnd = g_hWnd;
    connInfo.guidDestNet = dest;
    connInfo.dwParams = CONNMGR_PARAM_GUIDDESTNET;
    connInfo.dwFlags = CONNMGR_FLAG_PROXY_HTTP;
    connInfo.dwPriority = CONNMGR_PRIORITY_USERINTERACTIVE;
    connInfo.uMsg = WM_USER+101;    // Message callback.
    hr = ConnMgrEstablishConnection(&connInfo, &g_ConnHandle);

    if (!SUCCEEDED(hr)) {
      LogMessage( TEXT("Failed to Establish connection") );
    }
  }

  return hr;
}

HRESULT ProcessConnectionManagerMessage() {
  HRESULT hr = S_OK;
  DWORD status = 0;

  // Connection Manager status change
  hr = ConnMgrConnectionStatus( g_ConnHandle, &status);

  if (SUCCEEDED(hr)) {
    switch (status) {
      case CONNMGR_STATUS_CONNECTED:
        // Do something useful here…
        // Done with the connection—tear it down.
        ConnMgrReleaseConnection(g_ConnHandle, 1);
        g_ConnHandle = NULL;
        break;

      case CONNMGR_STATUS_NOPATHTODESTINATION:
      case CONNMGR_STATUS_CONNECTIONFAILED:
      case CONNMGR_STATUS_CONNECTIONCANCELED:
      case CONNMGR_STATUS_CONNECTIONLINKFAILED:
      case CONNMGR_STATUS_CONNECTIONDISABLED:
      case CONNMGR_STATUS_AUTHENTICATIONFAILED:
        // Non transient error state.
        ConnMgrReleaseConnection(g_ConnHandle, 1);
        g_ConnHandle = NULL;
        break;

      default:
        // These states will eventually end up at as connected
        // or not. Just ignore for now.
        break;
    }
  }
  return hr;
}

LRESULT CALLBACK WndProc(
  HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {

  switch (message) {
    case WM_USER+101:
      ProcessConnectionManagerMessage();
      break;

    case WM_COMMAND:
      if (IDM_GET == LOWORD(wParam)) {
        ConnectionManager_Connect();
      }
      break;

    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}
ConnMgrEstablishConnection を呼び出すと、接続が有効かどうかを追跡する内部接続要求 (CR) が作成されます。ConnMgrReleaseConnection を呼び出すと、CR は解放されます。
次に、接続マネージャが CR を処理する際に実行する手順を簡単に説明します。まず、接続パスが宛先メタネットワークに存在することを確認します。接続マネージャは CR を確認し、ネットワーク モデル (GPRS、WiFi、VPN など使用可能なあらゆる接続方法とプロキシの詳細) を使用して、宛先メタネットワークに接続する最適なコネクタを選択します。
接続マネージャのコネクタは 1 つとは限りません。たとえば、Work メタネットワークに接続するには、GPRS 接続と VPN 接続が必要な場合があります。また、接続マネージャは、デバイスの現在の接続状態に応じて異なるパスを選択する場合もあります。たとえば、デバイスが現在接続されていない場合は直接 GPRS コネクタが最適と考えられます。ところが、デバイスが Work ネットワークに接続している場合は、プロキシ サーバーを使用すれば十分と考えられます。適切な接続先のパスが見つからない場合はアプリケーションに通知され、処理は終了します。
次に、CR の状態遷移が開始します。各 CR はいくつかの遷移状態に遷移し、最終的に 5 つ前後の終了状態のいずれかに到達します (接続状態も遷移状態です)。状態が変化すると、要求側アプリケーションは Windows メッセージを受信します。CR が終了状態に達すると、以後、状態は変化しません。したがって、アプリケーションをまだ接続する必要がある場合は、その CR を終了して新しい CR を作成する必要があります。
長々説明するよりも、図で示した方がわかりやすい場合があります。筆者がこれまで見た中で最も優れた接続マネージャの状態遷移図が Adam Dyba のブログに掲載されています。

接続要求と調停
新しい CR を作成しただけで、ただちに接続できるわけではありません。調停が行われます。要求時間の順に CR の優先順位が付けられます。次に、優先度リストの上から順にリソースが割り当てられます。優先度が最も高く、要求された時期が最新の CR がリストの先頭になり、優先度が最も低く、要求された時期が最も古い CR がリストの最後になります。
調停は新しい CR によってトリガされるばかりでなく、CR の優先度の変更、CR の解放、ネットワーク リソースからのイベント (基地局の変更など) によってもトリガされます。
音声通話にも接続マネージャを使用し、優先度が最高であることを示す予約フラグ、CONNMGR_PRIORITY_VOICE を使用します。このフラグを設定すると、他のすべての CR より優先されます。音声接続が存在する場合、音声リソースを共有する CR の接続を音声通話中は中断状態にし、CR が解放された時点で自動的に再接続することもできます。
PDP コンテキストなどによってネットワークが複雑になると、調停に影響する可能性があります。使用可能なコンテキストが 1 つのみの場合、リストの先頭の CR が接続され、他の CR は WAITINGFORRESOURCE 状態になります。各 CR の排他設定によっては、同じメタネットワークの優先度の低い CR も CONNECTED 状態になる場合があるため、実際の処理はもう少し複雑です。
複数の PDP コンテキストが存在する場合、接続マネージャはリストの先頭から始めて複数の CR を接続します。前述したように、新しい CR は常に同じ優先度の既存の CR より優先されるということを覚えておいてください。
この点をよく考えてみてください。ここに不注意な開発者が陥りがちな落とし穴があります。たとえば、常時接続アプリケーションをプライベート APN に接続し、インターネット接続を使用しない場合を考えてみましょう。このアプリケーションをバックグラウンドで実行し、データを快適にサーバーとやり取りするために CR を接続します。
追加リソース

MSDN の接続マネージャに関するドキュメント

接続マネージャ API

.NET Compact Framework を使用して IM アプリケーションを作成する

モバイル アプリケーションからデータにアクセスする
やがて、ユーザーが Web ブラウザを起動して株式ポートフォリオの実績を確認します。一般的なインターネット コンテンツにアクセスするには、別の無制限な APN 接続が必要になるため、ブラウザは新しい宛先メタネットワークを要求する CR を発行します。複数の PDP コンテキストが存在する場合、2 つ目の CR が接続され、問題なく平行して実行されます。ただし、PDP コンテキストが 1 つしか存在しない場合、2 つ目の CR がアプリケーションの要求より優先され、接続マネージャはアプリケーションのブラウザ接続の CR を終了します。
たとえば、ユーザーが頻繁にサイトを移動することを考慮して、接続エラーから回復可能なアプリケーションを作成するとします。元の CR が終了すると、アプリケーションはすぐに新しい CR を要求します。優先度が同じで、ブラウザの CR より新しいため、すぐにリソースが再取得され、アプリケーションは再度接続されます。この処理はきわめて短時間に行われるため、ブラウザが接続状態になるのが間に合わず、ユーザーにエラーが報告される場合があります。ユーザーにはブラウザでエラーが発生した原因がわからないため、再度接続を試行し、ループに入りますが、いつまでたっても接続されません。
次に、ブラウザの代わりに別のバックグラウンド アプリケーションを使用する場合を考えてみましょう。アプリケーションが接続をめぐって競合した結果、どのアプリケーションも接続できないため、サービス拒否の状況に陥っている可能性がきわめて高くなります。
アプリケーションのどこに問題があるのでしょうか。まず、接続の優先度が適切に設定されていない可能性が考えられます。USERINTERACTIVE の使い過ぎです。ブラウザ ページの要求など、ユーザーが直接接続を確立した場合にのみ設定するべきです。その後、新しいアプリケーション ウィンドウが表示されたことなどによって、アプリケーションがユーザーとの対話を行わなくなった場合には、ConnMgrSetConnectionPriority 優先度 API を使用して、CR の優先度を USERBACKGROUND などに下げます。これによって競合状態になる可能性が低くなります。
また、接続が破棄されたときに新しい CR がすぐには生成されないように、バックオフ ロジックを実装することが適切な場合があります。または、連続したエラーの回数に関連する遅延を設定してから再試行する方法もあります。
接続を設定する最後の手順として、接続マネージャで、優先度が下がった CR を切断し、新しい CR を接続します。接続マネージャは、解放されたリソースを破棄してから新しい要求を接続します。CR を破棄するときに新しい要求が不要な場合、接続マネージャはすぐに切断するか、接続を構成可能な期間だけキャッシュすることができます。この機能は、再接続のコストを避けるうえで便利です。
読者は、複数の PDP をサポートする場合の特異な課題にお気付きかもしれません。複数の PDP を接続した場合、デバイスは同じアダプタに対して複数の IP アドレス (コンテキストごとに 1 つの IP アドレス) を保持します。この場合、アプリケーションが適切なネットワークの適切な IP アドレスを使用するにはどうしたらよいでしょう。
ここで接続マネージャが真価を発揮します。CR が接続されると、ソケット ライブラリ コードは、そのプロセスの以後のすべての自動ソケット バインドに PDP コンテキストからの IP アドレスを使用します。この処理はプロセス境界の粒度で実行されます。マニュアルの記載どおり、接続マネージャはプロセスごとに 1 つの CR のみをサポートします。API は 1 つのプロセスから複数の CR を作成することを禁止しませんが、複数の CR を作成すると、すべての CR が同一の PDP コンテキストおよび後続の IP アドレスに解決されます。それ以外の場合、接続マネージャの動作は不定です。

SDK サンプル コードを使用する
接続マネージャの SDK サンプル コードは、機能を的確に示すことを目的としています。サンプルから実稼動環境のコードに移行する場合は、必ず優先度の処理を適切に更新し、状態変更の処理を更新して遷移状態が正しく処理されるようにしてください。WAITINGFORRESOURCE や WAITINGFORNETWORK などの状態は、視覚的な進行状況や状態をユーザーに適切に示すために役立ちます。
サンプル コードでは、ほとんどの遷移状態をエラーとして扱い、CR を破棄しています。実稼動環境のコードでは、これらの中間状態を回復可能な状態として処理し、一時的でない状態に達した場合のみ接続をエラーとして扱う必要があります。
接続マネージャは高機能であるにもかかわらず、過小評価されることが多いテクノロジです。接続マネージャの使い方を見直し、ネットワーク構成がいかに不明瞭であってもすべてのアプリケーションが適切に連携する方法を確保することをお勧めします。

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

Marcus Perryman は、マイクロソフトに 11 年在籍し、デベロッパー エバンジェリストやデベロッパー コンサルタントなどの技術部門の職務に携わってきました。現在は、Windows Mobile 製品グループのソフトウェア設計エンジニアとして、次世代モバイル オペレーティング システムの設計および開発を担当しています。


実践的なユーザビリティ
ソフトウェアの人間的な顔
Dr. Charles B. Kreitzberg および Ambrose Little
目次
ユーザー エクスペリエンスとはなにか
単なる描画ではない
UX の各側面
アジャイルとの相乗効果
「実践的なユーザビリティ」にようこそ。今回は、ユーザー エクスペリエンス (UX) のデザインに焦点を当てたコラム シリーズの第一回です。過去のほとんどの開発プロジェクトでユーザー エクスペリエンスの優先順位は高くありませんでしたが、この状況は変化しました。今日、エンド ユーザーは Web やソフトウェアを操作する多くの機会があります。わかりやすく、使いやすく、ワークフローに適したデザインが求められています。このコラムでは、これらのデザインを提供するしくみについて説明します。
UX デザインは十分に開発された分野ですが、開発プロセスとの統合は遅れています。UX が本質的に高度に反復的で、アジャイルなプロセスであることを考えると、これはやや意外です。しかし、開発コミュニティの関与が増えていることから、開発フレームワークが UX のデザインと重要なタッチポイントについてより深く配慮するようになると期待しています。
筆者は 2 人ともユーザー エクスペリエンス デザイナですが、経歴は少し異なります。一方の Charlie は認知心理学者として訓練を受け、片方の Ambrose は開発者の出身です。両者とも UX に対する情熱がありますが、違うレンズを通して見ています。私たちの共通の目標は、開発プロセスと適切に調和し、優れた UX が成功するインタラクティブな製品の構築に不可欠であるとするデザイン方法論を確立することです。

ユーザー エクスペリエンスとはなにか
"ユーザー エクスペリエンス" という用語は、製品と対話する個人の主観的な体験を表します。私たちは毎日レストランでのすばらしい体験や映画館での悲惨な体験を語るときに、ユーザビリティについて日常的に議論します。しかし、インタラクティブな製品のユーザー エクスペリエンスをデザインまたは評価する場合には、製品について調査および議論するためのより構造的な手段が必要になります。
UX デザイナにとって成功とは、便利で、使いやすく、魅力的な製品を構築することを意味します。便利であるためには、ユーザーにとって製品が価値を持つ必要があります。開発用語で言えば、製品が適切な機能を実装している必要があります。使いやすい製品とは、簡単に学び、使うことのできる製品です。魅力的な製品とは、ユーザーが熱望する製品です。製品が魅力的であるほど、ユーザーはより多くの時間と労力を製品に振り向けてくれます。熱心なユーザーは製品の最善の擁護者です。

単なる描画ではない
UX は、色、フォント、美意識と同等と考えられ、ビジュアルなデザインと誤解されることがしばしばあります。もちろん、グラフィック デザインはユーザー エクスペリエンスの 1 つの要素ですが、UX デザイン プロセスはより奥深いものです。本質的なところで、UX は、ユーザーとシステムの対話の点から機能が実装されているしくみに関連しています。
機能仕様は、常に開発の一部とされてきました。従来のデザインをやや誇張して単純化した場合、ビジネス分析者によって作成された一連の要件が開発者に渡されます。開発者は、要件の実装方法を決定し、製品を構築します。しかし、従来の開発方法では、多くの場合、タスク フローの分析が無視されてきました。
ビジネス分析者が製品の機能に注目しがちであるのに対し、UX デザイナは、UI を通じて機能を表現する方法に焦点を当てます。ユーザーのスキルやニーズがさまざまであるため、UX デザイナはサポートする必要のあるユーザー セグメントを決定します。デザイナはユーザー セグメントに関するデータを収集し、デザイン プロセス時の議論に役立つペルソナ (実際のユーザーのデータに基づいた想像上のバイオグラフィ) を作成する場合もあります。面談や観察を通して、UX デザイナは、ユーザーが実行するタスク、タスクの処理方法、タスクを表すために使用する用語について理解します。これらのデータは、シナリオ、ユーザー ストーリー、またはストーリーボードとしてまとめることができます。
ユーザー セグメントとタスク フローをドキュメント化したら、UX デザイナはデザインの作成を開始できます。これらは通常、高度に反復的な方法で作成され、大まかな概念化から開始して、デザインが進行するにつれて徐々に詳細化されます。
ユーザビリティ テストは、デザイン概念の問題の識別に使用されるデザイン品質保証 (QA) の 1 つの形式です。通常は、特定のユーザー セグメントから参加者が選ばれ、特定のタスクを実行するよう求められます。初期の段階では、UI は単に紙の上に書かれ、参加者がコントロールを指し示し、タスクが実行される方法を示す場合があります。後の段階では、信頼性の高いプロトタイプが多用されます。

UX の各側面
ユーザー エクスペリエンスのデザインは、いくつかのサブフィールドから構成されています。ほとんどの UX デザイナはすべての中心的な領域でのスキルを持っていますが、一部のより特化した領域があります。主な領域は、対話デザイン (IxD)、情報アーキテクチャ (IA)、ユーザー リサーチ、ビジュアル デザイン、およびユーザビリティ テストです。
対話デザイン ユーザーが製品と対話するしくみを定義します。ユーザーの操作に応答する製品の動作を指定し、製品のナビゲーションと、使用する特定のコントロールに焦点を当てます。
情報アーキテクチャ 情報がどのように構成され、表示されるかを定義します。見つけやすさと使いやすさを手助けすることが目的です。
ユーザー リサーチ ユーザーのニーズ、能力、および好みに合わせたデザインを開発するためにユーザーを調査するプロセスです。方法はさまざまで、通常はいくつかの面談手法と、観察を使用します。また、ユーザー ログと副次的なソースも使用されます。多くの UX デザイナが避けているアンケートやフォーカス グループを使用することもできます。
ビジュアル デザイン UI のグラフィック表現です。ビジュアル デザイナは、UX デザイナというよりもグラフィック アーティストである場合があります。ビジュアルが美しいことは重要ですが、ビジュアル デザインのせいで読みやすさや使いやすさが損なわれないことが不可欠です。
ユーザビリティ テスト プロトタイプまたは UI のモックアップ上で特定のタスクを実行するユーザーを観察するプロセスです。従来のユーザビリティ テストは、ワンウェイ ミラーの後ろに観察者がいるラボで実行されました。しかし、ユーザビリティ テストで Web キャスト テクノロジが使用されることが増えてきています。

アジャイルとの相乗効果
概念的に言えば、優れた UX をデザインする理由とは、内部の操作または外部の顧客のいずれに対するものであれ、効果と効率性を向上し、開発のオーバーヘッドと修正を削減し、可能な限り大きな価値を付加することです。
よく聞く話だと思われるかもしれません。そのとおりです。これらは、ソフトウェア開発に対するアジャイルな手法の背後にある目標と本質的に同じです。実際に、これらの目標は、すべてのビジネス ソフトウェア構築のほぼ中心にあります。優れた UX をデザインすることは、基本的なビジネス戦略とソフトウェア開発の中心的な価値に呼応しています。大まかに言えば、ほぼすべてのビジネス ソフトウェアで必要な場所 (つまり、ソフトウェアが対象としている人々) に焦点を当てることになります。
システムで必要な動作や方法をはっきり指摘するようにユーザーに求めるのは効率的ではありません。このようなやり方は、固有のニーズをソフトウェアの機能に言い換える負担をユーザーに強いることになり、デザイナや開発者が提案できるかもしれない、より適切な代わりの手段を見つけることが難しくなります。ユーザー中心のデザイン (UCD) は、ユーザー (彼らの目標、タスク、およびアクティビティ) を理解することに焦点を当て、その知識を構築作業の基として使用することで、価値を高めます。
ペルソナ、シナリオ、ストーリーボード、モックアップ、プロトタイプ、およびユーザビリティ テストなどの手法は、人間による要因に関する適用された知識と組み合わされて、最終製品が対象ユーザーにとって最大限に役立つものとなる可能性を高めるために役立ちます。これは、他の懸念事項 (主にビジネスまたはマーケットおよびテクニカル) を無視しているわけではなく、それらの懸念事項がソフトウェアを使用するユーザーを中心に据えるものであることを示しています。
さらに、ユーザビリティ テストでは、作成中のソフトウェアのユーザビリティ バグを可能な限り少なくするために、実際に対象となるユーザーが早期から頻繁に (反復的に) 検証を行うのが理想的です。これは、ユーザー受け入れテスト (UAT) よりも優れた方法です。UAT は一般に開発サイクルの終盤に行われるため、ほとんどの場合、この時点で問題を修正するには遅すぎるか、コストがかかりすぎるためです。さらに異なる点として、理想的なユーザビリティ テストは、人間による要因に関する訓練を受け、システムと対話するユーザーを観察する担当者によって適切に実行されるため、ユーザーでさえ気づいていないユーザビリティの問題が明らかにされることがしばしばあります。
事実上は、アジャイルとの多くの相乗効果があります。両方とも本質的に反復的です。両方ともソフトウェアが対象としている人々 (理想的には実際のユーザー) との定期的な共同作業が必要です。
アジャイルとの特定の相乗効果があるにしても、UCD 手法によって任意の開発プロセスを拡張できる点にも触れておきましょう。すべてのケースにおいて、実際のユーザーと彼らのニーズ、目標、タスク、およびアクティビティに対する認識を深めたり、実際のアプリケーションの要件を検出する優れたツールを提供したり、すべてのソフトウェアの基になる目標 (人間的な関心のある側面を拡張すること) が満たされる可能性を高めたりすることで、プロジェクトが成功する機会を高める価値が提供されます。
今後のコラムでは、ソフトウェア開発手法の中で UCD の知識、手法、およびツールを使用する、より具体的な例について見ていきます。

Dr. Charles Kreitzberg はユーザビリティのコンサルティングとユーザー エクスペリエンスのデザイン サービスを提供する Cognetics Corporation の CEO です。彼は、製品のビジネス目標をサポートしつつも、ユーザーを魅了し、楽しませる直感的なインターフェイスを作成することに情熱を燃やしています。セントラル ニュージャージー在住で、パフォーミング ミュージシャンとして夜間のアルバイトをしています。
Ambrose Little は、夫人と 4 人の子供とセントラル ニュージャージーで暮らしています。ソフトウェアのデザインおよび開発に 10 年以上携わっており、INETA 講演者であり、Microsoft MVP でもあります。最近は、テクニカル デザインから人々のためのデザインにシフトし、現在は Infragistics 社のユーザー エクスペリエンス デザイナです。


{ End Bracket }
優れたソフトウェア テスト担当者の条件
Dr. James McCaffrey

最近、マイクロソフトなどの企業のエンジニアや管理者との会話の中で、優れたソフトウェア テスト担当者はただでさえ得難いものなのに、近年ますます探すのが難しくなっている気がするという話を何度も耳にしました。
そこで、今回は優れたソフトウェア テスト担当者の条件について考えてみたいと思います。特に重要な 8 つの資質に絞って話を進めていきます。
  1. 分析とテストに対する情熱 あらゆる仕事において、成功の鍵を握るのは、その仕事に対する心からの情熱です。一般に、優れたソフトウェア テスト担当者は、ソフトウェア アプリケーションを作成することよりも、そのアプリケーションを実行するテストの自動化プログラムを作成することに満足感を覚えます。これは、コンピュータ サイエンス、数学、経済学などの計量的分野の学位取得者に通じる特性です。多くの場合、分析に対するこのような情熱は後天的に得られるものではなく、先天的な特性のように思われます。
  2. 技術的スキル 優れたソフトウェア テスト担当者には、テスト対象システムを理解し、開発者とコミュニケーションをとり、テスト自動化プログラムを作成するための高度なコーディング スキルが必要です。多くの技術的スキルは教育や経験によって習得可能です。優れたテスト担当者に必要なコーディング スキルのレベルについては意見が分かれるところですが、1 ～ 10 の評価尺度で 7.5 以上のアプリケーション開発スキルが必要だというのが筆者の考えです。
  3. 知性 優れたソフトウェア テスト開発者には、高度な分析力と論理力を備えた知性が必要です。ソフトウェア開発は本質的にロジックの実行です。そのロジックを環境内で高度に運用するには、ソフトウェア開発者の知性が必要です。
  4. 優先順位を付けて整理する能力 あらゆる仕事に必要とされる能力ですが、ソフトウェア テスト担当者にとっては特に重要な資質です。ソフトウェアの開発およびテストはきわめて流動性が高く、主要な変数が週単位、場合によっては日単位に変化する可能性があります。このように頻繁に変化する業務環境を認識し、解釈し、整理する能力が不可欠です。
  5. 適応力と学習能力 ソフトウェア エンジニアリングの世界では、新しいテクノロジが目まぐるしい速度で登場します。たとえば、2003 年初期に登場した C# および Microsoft .NET Framework はソフトウェア アプリケーション開発に革新を起こしました。優れたソフトウェア テスト担当者は、継続的に学習し、常にスキルを磨く習慣を身に付けている必要があります。数年前に実施した調査の結果、ソフトウェア エンジニアの "スキルの半減期" は約 18 か月でした。スキルの進歩がないソフトウェア エンジニアは即座に就職市場から取り残されるということです。
  6. 監督者なしで仕事を遂行する能力 これも一般的な職業能力ですが、ソフトウェア テスト担当者にとって特に重要な特性です。自立した行動をとる能力が必要です。ソフトウェアのテストに関連する技術的ニーズまたは業務上のニーズを敏感に察知し、管理者の承認がなくてもとるべき行動を認識できる能力は欠かせません。これは同時に、管理者の承認なく事を進めてはいけない場合を認識する能力をも意味します。
  7. コミュニケーション能力 優れたソフトウェア テスト担当者には、文書および口頭による高いコミュニケーション スキルが必要です。また、製品ドキュメントの読解および分析能力、テスト プラン、わかりやすいバグ報告書、および管理者に提出する一貫性のある業務報告書 (正式な報告書および電子メールによる随時の報告) を作成する能力、担当者どうしの会議で冷静に話を聞き、合理的に話をする能力が求められます。
  8. ビジネス戦略を理解する能力 優れたソフトウェア テスト担当者について、ここまでに挙げた特性は主に戦術的な性質のものでした。最後に挙げるスキルは、会社全体のビジネス戦略という、より大きな概念を理解する能力です。この能力が備わっていれば、優先度 2 または重要度 2 のバグを見つけて報告するだけの一開発者にとどまらず、一段上の積極参加型の優れたソフトウェア テスト担当者として、ソフトウェア システムの戦略的な強みと弱みを把握し、最終的にビジネスの競争力向上に貢献することが可能になります。
ソフトウェア システムの複雑化に伴い、優れたソフトウェア テスト担当者の価値がますます高まっています。優れたテスト担当者に必要な条件を備えていると自任する方は、「マイクロソフトのテスト担当者の採用情報」をご覧ください。

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