6 月 2008 6 月 2008
SAAS: ホストされた BizTalk サービスを使用してエンタープライズ アプリケーションを接続する
この記事では、ホストされたサービスとして BizTalk Server の Enterprise Service Bus 機能を提供する新しいテクノロジ、BizTalk サービスについて紹介します。 Jon Flanders および Aaron Skonnard
同時実行: 同時実行の問題を特定するためのツールと手法
従来のアプリケーションを並列プロセッサ コンピュータで実行するだけでは、効率的な並列アプリケーションは生まれません。そのメリットを最大限に享受するには、調整が必要です。 Rahul V. Patil および Boby George
ロボティクス: Microsoft Robotics Studio で世界をシミュレーションする
Microsoft Robotics Studio は単なるロボット操作の手段ではありません。多様なハードウェア デバイス向けのサービスベース アプリケーションを構築する機能も備えています。 Sara Morgan
フォーム フィラー: データのキャプチャとドキュメント作成のためのワークフローを構築する
InfoPath フォームおよび他の Office ドキュメントを使用して、ターゲット アクティビティにデータを渡したり、Office ドキュメントで使用するためのデータを渡すワークフローの作成方法を学習します。 Rick Spiewak
GUI ライブラリ: Windows フォーム アプリケーションの簡潔さをネイティブ アプリケーションで実現する
この記事では、著者 John Torjo が自作の C++ GUI ライブラリ、eGUI++ のガイドを提供し、ユーザー インターフェイス プログラミングを簡単にする方法を説明しています。 John Torjo
Columns
編集者のメモ: お気付きですか?: 印刷バージョンのデザイン変更や、Web サイトへのバーチャル ラボ (コードを試すことができます) の導入など、MSDN Magazine の新しい特徴や機能をご紹介します。Howard Dierking | ツールボックス: クロスブラウザ テスト、モック オブジェクト、そして Raymond Chen: 今月は、テスト環境を設定せずにさまざまなプラットフォームおよびブラウザ上で Web サイトをテストする方法、単体テストにモック オブジェクトを使用する方法、および Raymond Chen のブログを紹介します。Scott Mitchell | CLR 徹底解剖: 大きなオブジェクト ヒープの秘密: 今月の CLR 徹底解剖では、大きなオブジェクト ヒープの背後にある秘密を解き明かします。そこに何があり、それがいつ解放され、そのオブジェクトがガベージ コレクタでどのように処理されるのかを説明します。Maoni Stephens
Cutting Edge: ASP.NET AJAX とクライアント側テンプレート: AJAX は、単なる部分的なページ レンダリングを超えた機能の提供を目指しています。ASP.NET AJAX と共に、動的なページが今後どのような方向に進むのかについて、Dino Esposito の考えをお伝えします。Dino Esposito | 実践的なパターン: 開放/閉鎖原則: 開放/閉鎖原則に従うと、既存のコードベースを拡張する場合に、まったく新規にコードを作成する場合と同様に生産性が高く、ストレスなく処理することができます。ここではそのしくみについて説明します。Jeremy Miller | サービス ステーション: WCF ルーターを構築する (第 2 部): 今回の記事では、Windows Communication Foundation ルーターの動作を詳しく解剖し、パススルー ルーターの実装の詳細を説明します。Michele Leroux Bustamante
基礎: ビットマップとピクセル ビット: Charles Petzold は、Windows Presentation Foundation の保持モード グラフィックス機能によって提供される柔軟なビットマップ ピクセル形式の内側を調べています。Charles Petzold | Windows と C++: WIC を使用した Windows Vista アイコンのデコード: Kenny Kerr は、Visual C++ を現代的かつ便利に使用できるようになる Visual C++ 2008 Feature Pack を絶賛しています。Kenny Kerr | 同時実行の問題: AsyncEnumerator で簡素化された APM: Jeffrey Richter が、異なるスレッド プールのスレッドが異なる時点で同じコードを実行できるように反復子を駆動する AsyncEnumerator クラスを紹介します。Jeffrey Richter
いろいろな場所へ: Windows Mobile 用のアダプタブル アプリケーション: Windows Mobile デバイスのさまざまな画面と機能を最大限に利用できるアダプタブル アプリケーションを作成するための手法を示します。Michael Saffitz | { End Bracket }: 投票者も信用できる選挙結果: 意外にも暗号化は選挙過程に応用できます。投票者各人が投票集計の信頼性を確認できるようになります。ここでは、その方法について説明しています。Josh Benaloh


SAAS
ホストされた BizTalk サービスを使用してエンタープライズ アプリケーションを接続する
Jon Flanders および Aaron Skonnard

この記事は、ASP.NET 3.5 および Microsoft AJAX Library のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。
この記事では、次の内容について説明します。
  • ESB サービスとしての BizTalk サービス
  • BizTalk サービスに基づく WCF アプリケーション
  • 中継される接続のオプション
  • Identity サービスとトークン プロバイダ
 この記事では、次のテクノロジを使用しています。
BizTalk サービス、.NET Framework 3.0
目次
Software as a Service
Internet Service Bus
BizTalk サービス
BizTalk サービス SDK
一般的な WCF アプリケーション
中継される接続のオプション
RelayedHttp を使用してファイアウォールを通過する
Identity サービスを構成する
カスタム トークン プロバイダ
使用を開始する前に
今日の企業には、新しいアプリケーションの開発、導入、および既存環境への統合を迅速に行う能力がこれまで以上に必要です。数多くの企業がそのアプリケーションの基盤をサービス指向アーキテクチャ (SOA) に移しましたが (または移しつつありますが)、その主な理由の 1 つは、動的な疎結合アプリケーションに対するニーズの高まりにあります。
企業が SOA に向かうにつれて、会社全体のさまざまなアプリケーションからシステムを構成することが重要視されるようになりました。この新たな世界では、他のチームや組織によりネットワーク上で提供される各種アプリケーション サービスのビジネス プロセスを統制する役目は、開発者が担います。チームまたは組織ごとに異なる実装テクノロジや基幹業務アプリケーションが使用されている場合があり、システム全体の複雑さは増大します。SOA によって各ポイントツーポイント接続は簡素化されますが、アプリケーションに必要なサービス接続の合計数が徐々に増加するにつれて、複合アプリケーションの管理が難しくなったり、不具合が発生しやすくなったりするおそれがあります (図 1 を参照)。
図 1 複合アプリケーションのポイントツーポイント接続の管理 (クリックすると拡大画像が表示されます)
こうした現実に即して、多くの企業がより緩やかに結合された持続性のあるサービス パターンを採用するようになってきています。このサービス パターンは、一般に Enterprise Service Bus (ESB) と呼ばれています。基となるメッセージング システム上に抽象層を提供するセントラル バスを通じて複数のサービス接続を管理できるようになるため、ESB モデルの普及は進んでいます。たとえば、ESB では、サービス間でブローカーの名前付け規則、ID 管理、メッセージ形式、および通信プロトコルが異なっていてもかまいません。サービスがバス上にホストされると、そのバス上のあらゆるものが (通常はサービスと直接通信できないものであっても) サービスに接続できるようになります (図 2 を参照)。
図 2 ESB を利用した複合アプリケーションの構築 (クリックすると拡大画像が表示されます)
このモデルでは、バス上の各種サービス間、および複合アプリケーションとそれらが使用するサービス間に、ある程度の間接性が設けられます。ESB は、システム全体での緩やかな結合を実現するパブリッシュ/サブスクライブ機能など、さまざまなメッセージング機能も提供します。パブリッシュ/サブスクライブ機能があれば、コンシューマがサービスと直接接続されている必要がなくなります (あらゆるノードがバスと接続できさえすればよいため)。また、ノードの追加や削除を自在に行うことができるようになります。通常、ESB は、ビジネス プロセス (この層は、一般にオーケストレーションまたはワークフローと呼ばれる) を構成するメッセージングを統制するためのプロセス フロー エンジンも提供します。
マイクロソフト プラットフォーム上で ESB パターンを採用したシステムを構築する方法の 1 つに、BizTalk® Server を使用する方法があります。BizTalk Server には、これまでに説明したすべての ESB 機能が標準で用意されています。

Software as a Service
ESB が企業に浸透しつつある間に、Web に別のビジネス トレンドも見えてきました。これは、今後数年のうちにエンタープライズ システムに必ず影響を及ぼすに違いありません。SaaS (Software as a Service: サービスとしてのソフトウェア) と呼ばれるこのトレンドの中心となっているのは、コンシューマが料金を支払って使用できる完全なアプリケーションやサービスを Web 上 (インターネット クラウドとも呼ばれる) で公開している企業です。SaaS を利用することにより、同様のソフトウェアの開発や管理に伴う面倒な作業を行わなくても済むうえ、そのコスト (設計、開発、運用、および保守のコスト) を節約することができます。こうしたソフトウェアの開発や管理には、本来ばく大な初期投資が必要です。
SaaS モデルでは、基本的に、特定の分野 (CRM、会計、給与支払、データ ストレージ、電子メールなど) の専門技術を持つ企業がホストしているソフトウェアに、コンシューマがリモートでアクセスできます (図 3 を参照)。SaaS のサービス プロバイダは、通常、特定のソフトウェア ニーズを満たす完全なソリューションを提供します。コンシューマは、システムのその部分を外部調達できることになります。サービス プロバイダはソフトウェアを継続的に管理し、コンシューマに使用料金を請求します。つまり、コンシューマはそれらのソフトウェア サービスをローカルで管理する必要がありません。
図 3 SaaS を利用して必要なソフトウェアを外部調達する (クリックすると拡大画像が表示されます)
使い勝手を良くし、対象コンシューマの幅を広げるため、SaaS プロバイダが提供するアプリケーションやサービスには、通常、標準的な Web プロトコルとデータ形式が採用されています。現在は、広く普及している HTTP や一般的な Web データ形式 (XML、RSS、JSON など) の採用が主流になっています。SaaS では必ずしもこれらを使用する必要はありませんが、プロバイダにおいては、対象顧客の幅を広げるために採用せざるを得ない状況にあります。
SaaS システム アーキテクチャに固有の外部依存関係を利用することに、どれほどのメリットがあるのか、との疑問を抱くアーキテクトの方もいらっしゃることでしょう。それは、接続性やサービス レベル契約 (SLA) への懸念から生じるものと言えますが、どこにいてもインターネットに接続できるようになった今日、Web 上でのソフトウェアの公開はほぼ市民権を得たように思われます。また、新しい可能性が開けるという点を考えると、メリットも非常に大きいように思います。

Internet Service Bus
マイクロソフトでは、ESB および SaaS という話題の概念を取り入れ、その最も魅力的な特性を Internet Service Bus (ISB) という新しい概念と統合しました。ISB モデルは、機能の面では ESB とほぼ同じです。ただし、ISB はより広範なインターネット スコープで利用できるので、標準、対象顧客、およびスケーラビリティの面では SaaS モデルに似ています。Microsoft® ISB は、簡単に言うと、企業による ESB インフラストラクチャの導入を容易にするものです。
ESB の機能はさまざまな企業にとって魅力的なものですが、今のところそのメリットを十分に享受しているのは規模の大きな企業だけのようです。ほとんどの場合、その理由は、ESB のような重要なインフラストラクチャの導入に伴う先行投資と財務上のリスクにあります。そのため、多くの企業が、最終的には独自の (よりコストがかかることが多い) カスタム システムの統合インフラストラクチャを構築しています。現在、これが BizTalk Server の採用を妨げる最も大きな障害の 1 つとなっています。
さらに、ESB を導入する力のある企業でも、自社の ESB とパートナーの ESB との通信を実現するのには苦労しています。同じ ESB インスタンスはありません。このことが、緩やかに結合された ESB パターンを採用する場合でさえも、企業間の統合を困難にしているのです。
マイクロソフトの ISB プラットフォームでは、主要なサービス バス コンポーネントをインターネット クラウドに配置して公開することで、そのコンポーネントを任意のプラットフォームで使用できるようにしています。マイクロソフトは、基本的に SaaS モデルを通じてサービス バスを提供しているため、どのような規模の企業でもバスを利用できます。ISB は、パブリッシュ/サブスクライブのアーキテクチャと、企業ファイアウォールやネットワーク アドレス変換 (NAT) を通過するために必要なメッセージング インフラストラクチャを提供します。さらに、図 4 に示すように、フェデレーション ID 管理機能と、Web 上にカスタム ビジネス プロセスをホストするためのワークフロー サービス層も、ISB で提供されます。
図 4 新しい Internet Service Bus での構築 (クリックすると拡大画像が表示されます)
マイクロソフトは ISB を実現するために積極的に取り組んでいます。その取り組みについては、BizTalk サービスの Web サイトでご覧いただけます (labs.biztalk.net を参照)。現時点の Community Technology Preview (CTP) には、すぐにお試しいただけるいくつかの基本的なサービスが用意されています。マイクロソフトでは、このイニシアチブを今後数年の間に発展させて、最新のさまざまな接続型アプリケーションの優れたビルディング ブロックを提供できる、リッチな通信機構を作り上げる予定です。
ただし、次の点にご注意ください。CTP 版として現時点で使用できるサービスは、評価とフィードバックの収集のためのものです。実稼働環境での使用は想定されていません。現時点では、マイクロソフトは品質を保証していません。また、実稼働環境での使用が可能になった場合の ISB の使用料金については、未発表です。

BizTalk サービス
BizTalk サービスは、Web 上の ISB コンポーネントの最初の実装です。現在、BizTalk サービスはいくつかのコア サービス (BizTalk Connectivity サービスや BizTalk Identity サービスなど) で構成されています。もう 1 つの主要サービスである BizTalk Workflow サービスは、近日中にリリースされます。マイクロソフトでは、その他のサービスを順次追加する予定であり、サードパーティからも追加のサービスが ISB にホストされる見込みです。
現時点で提供されているコア サービスの詳細について見ていきましょう。疎結合複合アプリケーションの作成の際に最も頭を悩ませる課題の 1 つが、ネットワーク レベルでのサービスの接続方法を管理することです。たとえば、ネットワーク境界に位置するサービスと、ファイアウォール内部に位置するサービスがあり、それらが相互に通信できなければならないようなケースもあります。また、サービスをビジネス パートナーに公開する必要がある場合に、パートナー側のサービスがパートナー独自のファイアウォールやその他のネットワーク デバイス (プロキシや NAT など) によるファイアウォールで保護されているケースも考えられます。
BizTalk Connectivity サービスは、ネットワークのトポロジや構成に関係なく 2 つのノード間で直接的または間接的に通信を行うためのメカニズムや、サービスの汎用名前付け規則を提供することで、こうした通信にかかわる課題の多くを単純化します。そのために、ネットワーク デバイスを通じて接続を安全に中継するための制御されたメカニズムを提供します。ただし、ノード間で直接接続を確立できる場合には、BizTalk Connectivity サービスは道を譲り、トラフィックの中継なしで接続できるようにします。
もちろん、この種類のコードを開発者が記述することもできますが、難しく煩雑な作業になる可能性があります。この機能を BizTalk サービスを通じて入手することの主なメリットは、こうしたコード (エンドポイントごとにカスタマイズされることも多い) を記述または管理する必要がなくなることです。開発者は、インフラストラクチャの問題にかかわるコーディングから解放され、ビジネス上の問題を解決するためのコードの記述に専念できるようになります。BizTalk Connectivity サービスは、その他にも URI (Uniform Resource Identifier) に基づくグローバル名前付け規則、シンプルなパブリッシュ/サブスクライブ メカニズム、マルチキャスト機能などの主要な機能も提供します。
BizTalk サービスのもう 1 つのコア パーツが BizTalk Identity サービスです。これは、WS-Trust 標準に準拠した、パブリックにアクセス可能なセキュリティ トークン サービス (STS) です。WS-Trust 標準は、当事者間の信頼関係に基づいてセキュリティをネゴシエートするための相互運用性を備えています。たとえば、一方の当事者はクライアントを認証し、クライアントに関する要求セットを含むトークンを発行できます。そのトークンは、トークンを発行した元の当事者を信頼するもう一方の当事者に安全に送信されます。このアプローチにより、組織の境界を越えた重要な ID 情報のフェデレーションが可能になります。
BizTalk Identity サービスは、他の機能 (BizTalk Connectivity サービスなど) を一切利用していない場合でも、サードパーティの ID および認証管理サービスとして利用可能です。ただし、他の BizTalk サービスを使用している場合は、ユーザーの認証と承認に BizTalk Identity サービスが必ず使用されます。
BizTalk Connectivity サービスを使用している場合、クライアントとサービスは両方とも、ユーザー名/パスワード、クライアント証明書、マネージ カードのいずれかを使用して BizTalk Identity サービスで認証されます。認証後、一連の承認規則が処理されて、BizTalk Connectivity サービスに提供される新しいトークンが発行されます。BizTalk サービスの Web サイトでは、承認規則の管理用のユーザー インターフェイスと、いくつかのプログラミング API (RESTful インターフェイスなど) が提供されています。
BizTalk Workflow サービスは、まだ一般向けにはリリースされていません。ただし、BizTalk サービスの今後のリリースで、Windows® Workflow Foundation (WF) に基づいたワークフロー サービスを ISB でホストする機能が提供される予定です。ワークフローは、特定の URI の ISB で受信されたメッセージに基づいてアクティブ化されます。現時点では、これらの情報しか公開されていません。

BizTalk サービス SDK
BizTalk サービス SDK は、biztalk.net から無料でダウンロードできます。このサイトでは、ISB に関するプログラミングを開始するために必要な情報が提供されています。プログラミングを始めるにあたって、BizTalk サービス SDK をインストールし、BizTalk サービスのサイトでユーザー アカウントを作成する必要があります。BizTalk サービスは Windows Communication Foundation (WCF) を基に構築されています。この SDK は、Microsoft .NET Framework 3.0 に基づくアセンブリ セットです。Microsoft .NET Framework 3.0 により、WCF プログラミング モデルを通じて ISB を利用できるようになります。
BizTalk サービス SDK の大部分は System.ServiceBus アセンブリに含まれます。System.ServiceBus では、RelayBinding という名前の新しいバインドを定義するために標準の WCF 拡張ポイントを使用します。WCF をご存じない方のために一応説明しておくと、バインドとは、サービス エンドポイントによって公開または使用される通信の種類を定めるものです。バインドは、メッセージを送受信するための通信層 (チャネル スタックと呼ばれる) を作成するために、WCF ランタイムによって使用されます。チャネル スタックのオブジェクトは、ネットワーク通信を実行し、特定のエンドポイントに対するセキュリティやその他のプロトコルの実装方法を指定します。
リスナの側では、RelayBinding を使用してエンドポイントを公開できます。このエンドポイントには、BizTalk Connectivity サービスでサポートされているグローバル URI を使用してアクセス可能です。RelayBinding を使用してエンドポイントが開かれると、その URI は BizTalk Connectivity サービスに登録されます。それ以降、承認されたクライアントは、同様に構成されたクライアントを使用してそのグローバル URI にメッセージを送信できるようになります。そして BizTalk Connectivity サービスは、それら 2 つのエンドポイント間での通信に使用されるコンジットとなります。

一般的な WCF アプリケーション
それでは、簡単な例を挙げて詳しく見ていきましょう。ある会社で、リッチ クライアント アプリケーションから呼び出される WCF サービスを内部ネットワーク上で公開しているとします。クライアント アプリケーションは、注文を処理するために営業担当者によって使用されます。アプリケーションがサービスを呼び出すと、サービスは注文の処理に必要なバックエンド プロセスを開始します。
サービスには、TCP/IP を介した高速接続を可能にする NetTcpBinding を使用して、単一のエンドポイントが構成されています。ファイアウォール内部で処理されるサービスの場合、通常は NetTcpBinding が最適です。
標準の ServiceHost を使用してエンドポイントを構成し、サービスを開始するコードを次に示します。
ServiceHost sh = new ServiceHost(typeof(SalesService));
sh.AddServiceEndpoint(typeof(ISalesService),
  new NetTcpBinding(),
  "net.tcp://salesservicehost/SalesService");

sh.Open();
...
これが標準的な WCF コードです。サービス実装は SalesService 型であり、コントラクトは ISalesService です。AddServiceEndpoint が呼び出されたときに作成される ServiceEndpoint は、NetTcpBinding を使用し、そのアドレスは net.tcp://salesservicehost/SalesService になります。
この方法でエンドポイントをハードコーディングする代わりに、アプリケーションの構成ファイルでエンドポイントを構成することもできます (この方法が一般的です)。ただし、このサンプルではすべての要素がコードに含まれているので、こちらの方が理解しやすいと言えます。次に、注文を処理するために SalesService を呼び出すクライアント アプリケーションのサンプルを示します。
static void MakeSale(SaleData sd) {
  EndpointAddress ea = new EndpointAddress(
    "net.tcp://salesservicehost/SalesService");
  ChannelFactory<ISalesService> cf =
    new ChannelFactory<ISalesService>(new NetTcpBinding(), ea);
  ISalesService service = cf.CreateChannel();
  service.EnterSale(sd);
}
やがて、営業部の移動性が高まってきたとしましょう。営業担当者からは、外出先で任意のネットワーク (その一部は仮想プライベート ネットワーク (VPN) 接続が不可) に接続しているときに、社内のサービスに接続して注文を処理することができないという報告が入るようになりました。
BizTalk サービスを利用すると、この問題を解決できます。BizTalk Connectivity サービスを介して SalesService を公開する方法を見てみましょう。
ServiceHost sh = new ServiceHost(typeof(SalesService));
sh.AddServiceEndpoint(typeof(ISalesService),
  new RelayBinding(),
  "sb://connect.biztalk.net/services/contososervices/SalesService");

sh.Open();
...
ご覧のように、BizTalk サービスを通じてサービスを公開するには、1 行のコード (ServiceHost.AddServiceEndpoint の呼び出し) を変更するだけで済みます。NetTcpBinding の代わりに RelayBinding を使用してください。RelayBinding により、このエンドポイントは BizTalk Connectivity サービスを通じて指定の URI で外部に公開されます。そのアドレスに送信されるメッセージは、ローカルの SalesService インスタンスに中継されます。
URI の形式に注目してください。URI は "sb" スキームから始まり、その後に BizTalk サービスのホスト名と "services" が続いています。その後ろには BizTalk サービスの ID (設定済みのアカウント名) を追加します。この場合は contososervices です。さらに、URI を一意にするために任意の文字列を追加します。ここではシンプルに SalesService を追加しました。
ServiceHost.Open を呼び出すと、サービス ID を認証して特定の URI でのリッスンを承認するために、RelayBinding によって WCF と BizTalk Identity サービスの通信が開始されます。このときの既定の動作では、Windows CardSpace™ セレクタが表示され、ユーザーはサービスの ID を表すカードを選択するよう求められます。
ほとんどのサービスでは、このように認証時に UI が表示されるプロセスは許容されません。サービスは一般にヘッドレス環境で実行され、コンピュータにログインできないからです。ただし、この問題は数行のコードを記述することで回避できます。この点については後ほど説明することにして、先に進みましょう。ここではまず、SalesService がリッスンを開始できるようにするカードを選択します。BizTalk Identity サービスを使用し、特定の URI でのリッスンを許可する ID を管理できます。
次に、クライアントを更新して、クライアントが BizTalk サービス エンドポイントを使用できるようにする方法について見ていきましょう。
static void MakeSaleISB(SaleData sd) {
  string uri =
    "sb://connect.biztalk.net/services/contososervices/SalesService";
  ChannelFactory<ISalesService> cf = new ChannelFactory<ISalesService>(
    new RelayBinding(), new EndpointAddress(uri));
  ISalesService service = cf.CreateChannel();
  service.EnterSale(sd);
}
RelayBinding を使用していることとサービス バス (sb://...) の URI を指定していることを除き、クライアントとサービスはまったく同じであることに注目してください。既定では、クライアントにも Windows CardSpace セレクタが表示されます。ここでクライアントは ID を選択でき、その ID は認証と承認のために BizTalk Identity サービスに送信されます。クライアントによる指定の URI へのメッセージの送信が承認されると、BizTalk Identity サービスはその事実を示すトークンを返します。そのトークンは、証拠として BizTalk Connectivity サービスに渡されます。
このシナリオでは対話型のクライアント アプリケーションを使用しているため、Windows CardSpace セレクタを表示しても問題ありません。ここでも、BizTalk Identity サービスを使用することで、特定の URI への送信を許可するクライアント ID を管理できます。
BizTalk Connectivity サービスは、承認された送信元から着信メッセージを受信すると、そのメッセージをローカルで実行されている SalesService のインスタンスに単純に中継します。そのしくみについては、今後変更される可能性の高い実装の細部なので特に注目する必要はありません。ただし、バックグラウンドでは、RelayBinding は WCF の TcpTransport を使用して BizTalk Connectivity サービスに接続しています。メッセージを中継するとき、BizTalk Connectivity サービスでは既存の TCP 接続をトンネルしてローカル サービス インスタンスに送信できます。
なぜこれほどシンプルなのでしょうか。それは、BizTalk Connectivity サービスでは標準の WCF 拡張ポイントを活用して、新しい RelayBinding 内部にクライアントとサービスの両方の接続および中継ロジックをカプセル化するためです。バインドを変更するだけで基本的な通信方法を変更できるという点は (完全に構成で行うことが可能)、WCF プログラミング モデルの採用により得られる真のメリットの 1 つです。

中継される接続のオプション
RelayBinding オブジェクトの重要なプロパティの 1 つが、ConnectionMode です。プロパティの型は RelayedConnectionMode という列挙型です。これを設定することで、BizTalk Connectivity サービスとの接続をどのように行うかを指定します。BizTalk Connectivity サービスでは、送信側と受信側の間で多数の通信モードを使用できます。そのため、個別の通信シナリオに大きな柔軟性がもたらされます (図 5 を参照)。
図 5 RelayBinding の ConnectionMode オプション
ConnectionMode の値 | 説明
RelayedOneWay | 一方向の操作だけを受信または送信するように接続を設定します。この設定を使用するには、IsOneWay プロパティが true に設定されている OperationContractAttribute が、サービス コントラクトのすべてのメソッドに含まれている必要があります。
RelayedDuplex | 要求/応答コントラクト (双方向コントラクトを含む) をサポートするように接続を設定します。
RelayedDuplexSession | RelayedDuplex と同じですが、WS-ReliableMessaging を使用するように構成します。
DirectDuplexSession | RelayedDuplexSession と同じですが、それを最適化したモードと言えます。送信側と受信側の最初の接続では BizTalk Connectivity サービスを使用しますが、その後、2 つのノード間の直接接続のブリッジを試みます (可能な場合)。直接接続を確立できる場合、送信側と受信側は BizTalk Connectivity サービスを間に挟まずに通信を行うようになります。直接接続を確立できない場合は、BizTalk Connectivity サービスが引き続き通信を中継します。
RelayedMulticast | パブリッシュ/サブスクライブ機能を提供するように接続を設定します。このモードを使用する場合は、同じアドレスを使用して複数のリスナを登録できます。この設定を使用するには、IsOneWay プロパティが true に設定されている OperationContractAttribute が、サービス コントラクトのすべてのメソッドに含まれている必要があります。
RelayedHttp | ローカル ネットワーク上のコンピュータで実行されているサービス (通常はインターネットを通じてアクセスできないサービスも含む) にメッセージを中継する HTTP エンドポイントを、BizTalk サービスを介して公開するように接続を設定します。
RelayedOneWay は一方向のメッセージング シナリオ向けのモードです。したがって、IsOneWay=true とマークされた操作でのみ使用できます。一方、RelayedDuplex は、双方向コントラクトの含まれる要求/応答シナリオ向けのモードです。RelayedDuplexSession は RelayedDuplex とよく似ていますが、チャネル スタック内の信頼性の高いメッセージングも有効になります。これは既定の接続モードなので、前の例で実際に使用しています。また、DirectDuplexSession モードは、可能であれば送信側と受信側の直接接続のブリッジを試みるという点で、RelayedDuplexSession を最適化したモードと言えます。直接接続を確立できない場合は、トラフィックを中継します。
ここからは、最後の 2 つの接続モードについて詳細に見ていきましょう。まず、RelayedMulticast を設定すると、WCF 上でのパブリッシュ/サブスクライブが有効になります。これは、どの組み込み WCF バインドでも実現されない通信パターンです。また、RelayedHttp を設定すると、企業のファイアウォール内部から、または NAT の背後から、HTTP ベースのエンドポイントを公開できるようになります。Web サービスの登場以来、開発者たちはこの状態を実現するために懸命に取り組んできました。
BizTalk Connectivity サービスでは、最初のリスナによって RelayedMulticast が指定されると、複数のリスナが自身を URI に登録できます。その同じ URI でリッスンしているエンドポイントを含む各 ServiceHost は、暗黙的にサブスクライバとなります。クライアントがその URI 宛てにメッセージを送信すると、そのメッセージは、現在サブスクライブしているすべてのサービス エンドポイントに配信されます。RelayedMulticast を使用する場合の唯一の制約は、RelayedOneWay と同様に、すべての操作が IsOneWay=true とマークされている必要があることです。
前の販売アプリケーションに戻りましょう。この会社では、さまざまな販売品目の価格を日中定期的に更新すると仮定します。全営業担当者がすぐに把握できるように、新しい価格をブロードキャストします。そのためには、各クライアント アプリケーションで、ServiceHost を開始し、RelayBinding を使用して (ConnectionMode は RelayMulticast に設定)、同じ URI をリッスンできる必要があります。
ServiceHost sh = new ServiceHost(typeof(PriceUpdateService));
sh.AddServiceEndpoint(typeof(IPriceUpdateService),
  new RelayBinding(RelayConnectionMode.RelayedMulticast),
  "sb://connect.biztalk.net/services/contososervices/priceupdate");
sh.Open();
...
アプリケーションが起動すると、各クライアントではこのコードが実行されます。そのため、すべてのクライアント アプリケーションが sb://connect.biztalk.net/services/contososervices/clientupdate に送信されるメッセージのサブスクライバとなります。これで、各クライアントはこのコントラクトと URI のためのサブスクライブ サービスとなりました。このシナリオでは、次に示すように、バックエンド システムが WCF クライアントの役割を果たし、すべてのサブスクライバにメッセージをブロードキャスト (パブリッシュ) します。
static void DoPriceUpdate(PriceUpdate pu) {
  EndpointAddress ea = new EndpointAddress(
    "sb://connect.biztalk.net/services/contososervices/priceupdate");
  ChannelFactory<IPriceUpdateService> cf =
    new ChannelFactory<IPriceUpdateService>(
      new RelayBinding(RelayConnectionMode.RelayedMulticast), ea);
  IPriceUpdateService service = cf.CreateChannel();
  service.UpdatePrice(pu);
}
UpdatePrice が呼び出されると、PriceUpdate メッセージが BizTalk Connectivity サービスに送信されます。BizTalk Connectivity サービスは、同じ URI に現在サブスクライブしているすべてのサービスにそのメッセージを中継します。

RelayedHttp を使用してファイアウォールを通過する
RelayedHttp モードでは、ローカル ネットワーク上で実行されている HTTP サービスを選び、それを BizTalk Connectivity サービスを通じてパブリックな Web 上に HTTP 経由で公開できます。通常はネットワーク外部から直接アクセスできないサービスであっても公開可能です。この機能は、.NET Framework 3.5 で新たに導入された WCF Web プログラミング モデルと共に使用する場合に、特に役立ちます。
この新しい Web プログラミング モデルでは、WCF を使用して RESTful Web サービスをより簡単に実装できます。また、このプログラミング モデルには、WCF サービスと、今日の Web でよく使用されている既存の RSS/Atom および AJAX インフラストラクチャを簡単に統合する機能が用意されています。BizTalk サービスには .NET Framework 3.5 は必要ありませんが、こうした新しい WCF 機能が導入されている .NET Framework 3.5 をインストールすると、RelayedHttp モードをいっそう活用できるようになります。
販売アプリケーションに戻りましょう。販売担当者は、リッチ クライアント アプリケーションを実行していない場合でも、最新の価格情報を受け取る必要がある場合があります。頻繁に更新されるデータを公開するための興味深い手段の 1 つに、Web フィードがあります。これは、よく知られた XML 形式 (RSS や Atom など) のデータを、フィードの使用方法を把握しているアプリケーションに返す HTTP エンドポイントです。フィード リーダー (ユーザーが使用する UI に Web フィードを集めるプログラムの一般的な名称) を使用して、設定したスケジュールで Web フィードをポーリングし、新しいコンテンツを取得できます。これにより、エンド ユーザーは、サイトにアクセスしなくてもフィード リーダーを通じて更新情報を確認できるようになります。
WCF Web プログラミング モデルを利用すると、価格の更新データを公開する Web フィードを簡単に作成できます。まず必要なのは、HTTP の呼び出しを有効にするための新しい WCF 属性を使用するコントラクトです。これらの属性は、SOAP Action ヘッダーではなく URI に基づいて、サービスのメソッドに着信 HTTP 要求をルーティングする方法を、WCF メッセージング層に伝えます。次に例を示します。
[ServiceContract]
public interface IPriceUpdateFeed {
  [OperationContract()]
  [WebGet(UriTemplate = "*")]
  Atom10FeedFormatter GetPriceUpdate();
}
この場合、WebGetAttribute は、HTTP GET 要求がサービスに到着するたびにこのメソッドを呼び出すよう WCF に指示します (常に HTTP POST を使用する SOAP 要求とは対照的です)。
WebGetAttribute の UriTemplate プロパティにも注意してください。このプロパティは、着信要求の URI に基づいて別のメソッドに着信メッセージをルーティングする方法を WCF に正確に指示します。この例では、すべての GET 要求をこの特定のメソッドにルーティングするよう指示する、ワイルドカード テンプレート ("*") を使用しています。サービスには、それぞれ異なる UriTemplate を処理する複数のメソッドを簡単に構成できます。また、GET 以外の HTTP 動詞 (POST、PUT、DELETE、HEAD など) に応答するようにする場合は、WebInvokeAttribute を使用できます。
このコントラクトでもう 1 つ注意しなければならないのは、メソッドの戻り値の型です。WCF Web プログラミング モデルには、今日の標準的なフィード形式のデータを生成できる、特別に作成された新しい型が用意されています。現在サポートされている 2 つの主要な形式は、Atom と RSS です。この記事の例では、Atom 形式を使用します。
図 6 に、Atom シンジケーション フィードを生成する GetPriceUpdate 操作のシンプルな実装を示します。この実装では、論理フィードを生成するための新しい SyndicationFeed API を使用しています。これは、適切な XML 形式のデータを生成するために Atom10FeedFormatter オブジェクトでラップされて返されます。
図 6 Web フィードの生成
public Atom10FeedFormatter GetPriceUpdate() {
  SyndicationFeed feed = new SyndicationFeed("Product Update Feed",
    "", null);
  List<SyndicationItem> items = new List<SyndicationItem>();
  feed.Items = items;
  IEnumerable<Product> products = GetUpdatedProducts();
  string iText = "Product update for {0}. Old price={1}. New Price={2}";
  foreach (var product in products) {
    items.Add(new SyndicationItem("Product update for " + product.Name,
      String.Format(iText, product.Name, product.OldUnitPrice,
      product.UnitPrice),
      null,
      product.Name,
      product.UpdatedTime));
  }
  return new Atom10FeedFormatter(feed);
}
ここで、これらの要求をリッスンするようにエンドポイントを構成する必要があります。WCF 3.5 には、エンドポイントを非常に簡単に構成できる新しいバインド クラスが用意されています。WebHttpBinding クラスは、SOAP メッセージをサポートしない RESTful チャネル スタックの作成に関する詳細情報をカプセル化します。このクラスでは、単純に HTTP トランスポートと WCF のテキストベース メッセージ エンコーダ (エンコーダのメッセージ バージョンは None に設定) を使用します。次に、新しいエンドポイントをフックして実行するために必要なコードを示します。
ServiceEndpoint se =
  sh.AddServiceEndpoint(typeof(IPriceUpdateFeed),
  new WebHttpBinding(), "http://localhost:8099/ProductFeed");
se.Behaviors.Add(new WebHttpBehavior());
...
WebHttpBehavior のインスタンスを使用して WebHttpBinding エンドポイントが構成されていることに注目してください。このインスタンスは、必要な HTTP ベースのディスパッチ ロジックをランタイムに挿入します。この新しいエンドポイントを構成したうえで ServiceHost を実行すると、サービスが localhost:8099/ProductFeed でリッスンしている HTTP URI を参照して Atom フィードを確認できるようになります。
ここで、内部ネットワークに接続していない営業担当者にこのフィードを公開する必要があるとしましょう。このとき、このコンピュータをファイアウォール越しに公開する代わりに、単純に BizTalk Connectivity サービスを使用して、HTTP トラフィックをローカル サービスに中継することができます。そのためには、次に示すように RelayBinding を使用して ConnectionMode を RelayedHttp に指定し、別のエンドポイントを追加します。
ServiceEndpoint se =
  sh.AddServiceEndpoint(typeof(IPriceUpdateFeed),
  new RelayBinding(RelayConnectionMode.RelayedHTTP),
  "http://connect.biztalk.net/services/contososervices/ProductFeed");
se.Behaviors.Add(new WebHttpBehavior());
...
これで、サービスはパブリックな Web に安全に公開されました。ただし、ロジックは内部サーバーにホストされています。図 7 は、インターネットでアクセスできるパブリック URI を通じてこのフィードが公開されていることを表します。もちろん、表示されるフィードはまったく同じです。外出中の営業担当者はこのフィードをサブスクライブし、VPN、ファイアウォール、およびネットワークの問題に煩わされることなく、更新情報を入手できます。
図 7 BizTalk Connectivity サービスを通じた HTTP サービスの公開 (クリックすると拡大画像が表示されます)

Identity サービスを構成する
RelayBinding で使用している接続モードに関係なく、リスナ (サービス) と送信側 (クライアント) はどちらも BizTalk Identity サービスで認証および承認される必要があります。認証とは、最初の要求セットが提供されたときにクライアントまたは要求側を認証するプロセスのことです。承認とは、その最初の要求セットにユーザー定義の承認規則のセットを適用するプロセスのことです。
承認規則によって、一連の要求変換を通じて単純に新しい要求セットが生成されます (各規則により入力要求セットが出力要求セットにマッピングされます)。入力要求の初期セットは、要求側によって BizTalk Identity サービスに送信される要求です。現時点の BizTalk Identity サービスは、ユーザー名/パスワード、クライアント証明書、およびマネージ カードの 3 種類の初期要求セットをサポートしています。ユーザー名/パスワードの場合は、ユーザー名が初期要求で、パスワードがその要求の証拠です。
BizTalk Identity サービスでは、出力要求を生成するために、まずユーザー名の要求を使用して承認規則の処理を開始します。生成された新しい要求は他の規則の入力要求の役割を果たすため、規則のチェーンを形成できます。出力要求の最後のセットは、BizTalk Identity サービスから返されるトークンに含まれるセットです。BizTalk Connectivity サービスへのアクセスを試みる際には、特定の出力要求を送信する必要があります。
証明書を使用して BizTalk Identity サービスを構成し、サービスによって生成されるトークンを保護することもできます。証明書を構成すると、要求側に提供されるトークンは証明書の公開キーで暗号化されます。したがって、トークンをより安全に送信できるうえ、転送中にクライアントの改ざんを受ける可能性を抑えることができます。
これらの承認規則は、いくつかの方法で構成できます。その 1 つが、BizTalk サービスの Web サイトに表示される UI を使用する方法です (図 8 を参照)。また、SDK に含まれている ID 管理 API を使用してプログラミングする方法もあります。最近、そのインターフェイスの RESTful バージョンもリリースされました。
図 8 BizTalk サービス Web サイトの要求マッピング用 UI (クリックすると拡大画像が表示されます)
承認規則を構成する際には、BizTalk Identity サービスの使用方法について考える必要があります。繰り返して言いますが、BizTalk Identity サービスは、一般的な STS としても、BizTalk Connectivity サービスのアクセス制御メカニズムとしても使用できます。
BizTalk Identity サービスを一般的な STS として使用する場合は、着信ユーザー名入力要求を任意の出力要求にマッピングできます。出力要求は、他の当事者に送信できる結果のトークンに含められます。サービスをこのように使用する場合は、入力要求がユーザー名である規則が少なくとも 1 つ必要です。新しい出力要求に基づいてカスケードする規則を追加できます。たとえば、ユーザー名の規則によって生成された出力要求を、別の出力要求に対する入力要求として利用することができます。そのため、任意の出力要求セットを構成可能です。詳細な例については、BizTalk サービスの SDK に含まれる AccessControl のサンプルを参照してください。
BizTalk Connectivity サービスを使用して認証を行うには、BizTalk Identity サービスにいくつかの特定の規則を構成していることを確認する必要があります。前に述べたとおり、入力要求がユーザー名である規則が少なくとも 1 つ必要です。また、カスケードする規則を追加することもできます。
ただし、BizTalk Connectivity サービスが一意である場合は、Resource#Operation 出力要求 (Resource はサービスのリッスン URI、Operation は Listen と Send のどちらか) が最後のトークンに含まれている必要があります。{URI}#Listen の場合、ユーザーは指定の URI でサービス エンドポイントのリッスンを開始できます。一方、{URI}#Send の場合は、ユーザーは指定の URI にメッセージを送信できます。

カスタム トークン プロバイダ
前の部分で説明したように、サーバーによるリッスンの開始時にサーバー アプリケーションで対話型のログイン画面 (Windows CardSpace セレクタなど) を表示するのは、通常は適切ではありません。こうした画面が表示されるのは、ITokenProvider インターフェイスを実装するオブジェクトを含む RelayBinding が各エンドポイントに使用されているためです。このインターフェイスの既定の実装は、CardspaceTokenProvider です。これを設定すると、BizTalk Connectivity サービスとの接続が確立された際に Windows CardSpace セレクタが表示されます。
SDK には、CardspaceTokenProvider のほかにも ITokenProvider のいくつかの実装が含まれています。UsernameTokenProvider を設定すると、登録されているユーザー名とパスワードを使用して BizTalk Identity サービスからトークンが取得されます。AutomaticRenewalTokenProvider を設定すると Windows CardSpace セレクタが表示されるようになりますが、現在のトークンの期限が切れる前に新しいトークンが自動的に取得されるため、エンドポイントの構成の手間が省けます。これらの両実装も、既定の CardspaceTokenProvider の代わりにクライアントまたはサービス エンドポイントに追加できるエンドポイント動作です。
この記事のサービスでは、次に示すように、ServiceHost.Open の呼び出しの前に UsernameTokenProvider をエンドポイントに構成することで、対話型のログイン画面が表示されないようにすることができます。
ServiceHost sh = new ServiceHost(typeof(SalesService));
ServiceEndpoint se = sh.AddServiceEndpoint(typeof(ISalesService),
  new RelayBinding(),
  "sb://connect.biztalk.net/services/contososervices/SalesService");
UsernameToken token = new UsernameTokenProvider("someusername",
  "somepassword");
se.Behaviors.Add(token);
sh.Open();
...
これで、いつサービス処理が開始されても、BizTalk Identity サービスを使用して、人の介在なしに自動的に認証が行われるようになりました。ITokenProvider のその他の実装も、エンドポイント動作として追加するだけで RelayBinding エンドポイントに構成することができます (SDK に含まれる TokenProvider 基本クラスは、ITokenProvider と IEndpointBehavior の両方を実装します)。

使用を開始する前に
BizTalk サービスの使用を開始するには、まず labs.biztalk.net にアクセスしてアカウントを作成してください。.NET Framework 3.0 がインストールされていることを確認する必要があります。確認後、BizTalk サービス SDK をダウンロードしてインストールできます。アカウントを作成し、SDK をお使いのコンピュータにインストールしたら、この記事で紹介したサンプルを再作成できます。当然のことながら、URI 中のアカウント名を "contoso" からご自身のアカウント名に変更する必要があります。また、SDK には参考用にその他のサンプルもいくつか用意されており、ここで説明した概念を理解するうえで役立ちます。使い始めて何らかの疑問が生じた場合は、BizTalk サービス サイトの FAQ を確認してください。健闘をお祈りします。

Jon Flanders は、フリーのコンサルタント、講演者、Pluralsight のトレーナーという 3 つの肩書きを持っています。彼は、BizTalk Server、Windows Workflow Foundation、および Windows Communication Foundation のスペシャリストです。Jon のブログのアドレスは masteringbiztalk.com/blogs/jon です。

Aaron Skonnard は、Microsoft .NET の第一のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Aaron は、Pluralsight が提供する Applied Windows Communication Foundation、Applied BizTalk Server 2006、および Applied Web Services の各コースのほか、多数の書籍、ホワイト ペーパー、および記事を執筆しています。彼のブログのアドレスは pluralsight.com/aaron です。


同時実行
同時実行の問題を特定するためのツールと手法
Rahul V. Patil および Boby George

この記事では、次の内容について説明します。
  • 同時実行のテストにおける問題点
  • 見つけにくいバグとその依存関係
  • 障害を検出するための新しいツール
  • テスト設計のベスト プラクティス
 この記事では、次のテクノロジを使用しています。
動的分析と静的分析、モデル チェック
目次
競合状態、デッドロック、その他
メモリ オーダリングに起因する問題
テスト戦略
同時実行テスト ツール
パフォーマンス テスト
ストレス テスト
まとめ
コンピュータの処理能力の向上は、永遠の課題です。この課題に対処するため、ハードウェア業界は、マルチコアおよびメニーコア プロセッサ システムに着実にシフトしつつあります。アプリケーションのパフォーマンスはクロック速度の速い高速のプロセッサを搭載することで改善できますが、メニーコア システムのパフォーマンスを高めるには、効率の良い並列プログラムを記述するしかありません。
ソフトウェア業界には、随分以前から並列処理という考え方がありました。ただし、並列ハードウェアの能力を最大限に活用するメインストリームのソフトウェア アプリケーションを開発するには、逐次アプリケーション向けの従来の慣習を大きく変える必要があります。
並列アプリケーションのテストは容易ではありません。たとえば、並列アプリケーションには非確定的な動作があるため、同時実行に関するバグの検出は難しくなります。これらのバグを検出できたとしても、同じバグを一貫して再現するのは困難です。また、問題を修正した後で、問題が本当に修正されたのか、それとも単に見えなくなっただけなのかを見極めるのも簡単ではありません。そのうえ、並列化によって、特定すべきパフォーマンス ボトルネックが新たに生まれることもあります。
この記事では、並列プログラム向けのテスト手法について説明し、危険性を秘めた欠陥の特定に使用できる 6 つの有用なツールを紹介します。最初に、3 つのカテゴリ (競合状態、不適切な相互排除、およびメモリの並べ替え) の同時実行に関するバグについて説明します。

競合状態、デッドロック、その他
競合は、マルチスレッド プログラムの複数の実行スレッドが同じ共有データにアクセスしたときに、その中の少なくとも 1 つのアクセスが書き込みアクセスである場合に発生します。不適切な競合状態は予期しない動作につながりますが、多くの場合、その状態を検出することは容易ではありません。競合状態の影響は、大分時間がたってから明らかになることもあれば、プログラムのまったく別の箇所に現れることもあります。そのうえ、再現がきわめて困難です。スレッド間で処理を適切に順序付けする同期の手法を使用すると、競合を避けることができます。
テスト データとレポート メトリックの特定
並列アプリケーションのテスト データを定義するには、シナリオのスケーラビリティの側面を明らかにする必要があります。アプリケーションのスケーラビリティは、使用されるアルゴリズムの並列化可能性に大きく左右されます。特定の問題領域 (分布が不均一なコンテンツの検索など) において超線形の速度向上を実現するアルゴリズムもあれば、並列化可能性に限界があるために並列的な速度向上がそれほどでもなく、全体としての実行時速度が CPU の数に比例しないようなアルゴリズムもあります。さらに、処理速度がデータセットのサイズに比例しないアルゴリズム (ソート アルゴリズムなど) もあります。おわかりのように、次のカテゴリの要素によってアプリケーションのパフォーマンスがどのように変化するかを理解することが重要です。
スレッドや CPU コアの数 ユーザーは、通常、CPU やスレッドの数が増えると、それに比例して並列アプリケーションのパフォーマンスも向上するものと考えています。
データセット サイズ ユーザーは、通常、入力データセットのサイズが大きくなっても、並列アプリケーションのパフォーマンスは低下しないものと考えています。
処理ワークロード 処理が進むと、スレッドによる作業の量が一定に保たれなくなることがあります。ワークロードは、増える場合も減る場合もあります。また、ランダムに変化することも、正規分布の形で増減することもあります。処理の進行に伴うワークロードの変化は、アプリケーションのパフォーマンス特性に影響を及ぼす可能性があります。こうしたワークロードの変化のタイプには、増加、減少、ランダムな変化、正規分布の形での増減などがあります。
レポートに使用される最も一般的なメトリックは、実行時間です。それ以外のレポート メトリックとしては、逐次アプリケーションとの比較により明らかになる速度の差、スケーラビリティのさまざまな要素を適用したときの速度向上などがあります。重要なのは、レポート メトリックとその測定方法を定義することです。まず、どのような値をレポートする必要があるのか、その値はどのような方法でレポートするのか、測定に使用する機器はパフォーマンスに影響を及ぼす可能性があるか、などを検討することから始めてください。また、可能であれば、パーツごとに測定を行うことをお勧めします。システムのサブパーツのパフォーマンスを把握できるようにするためです。
競合は、安全で意図的なものである場合もあります。たとえば、"done" というグローバル フラグがあり、それに対してライタが 1 つ、リーダーが多数存在するケースを考えてみましょう。ライタ スレッドはフラグを設定することで、すべてのスレッドに対して安全に終了するよう伝えます。このとき、すべてのリーダー スレッドは while (!done) によってループ状態で実行され、繰り返しフラグを読み続けている可能性があります。done フラグが設定されていることを認識したスレッドは、while ループから抜けます。ほとんどの場合、これは問題のない競合と言えます。この記事の後半で説明するように、競合状態の検出に使用できるツールがいくつか存在します。ただし、そのようなツールは、実際には問題のない競合を検出し、誤検知と呼ばれるエラーを報告することがあります。
デッドロックは、複数のスレッドが互いの処理を待ち、循環を形成して、すべてのスレッドの処理が進行しなくなる場合に発生します。プログラマは、競合状態を避けようとして、かえってデッドロックを生み出すことがあります。たとえば、同期プリミティブを適切に使用しないと (ロックが適切な順序で取得されない場合など)、複数のスレッドが互いの処理を待機することがあります。デッドロックは、ロック構造を使用していない場合にも発生することがあります。つまり、どのような循環待機もデッドロックにつながる可能性があると言えます。
次の例に、デッドロックが発生する可能性のある状態を示します。スレッド 1 は次の行を実行します。
Acquire Lock A;
Acquire Lock B;
Release Lock B;
Release Lock A;
Thread 2 proceeds like so:
Acquire Lock B;
Acquire Lock A; // if thread 1 has already acquired lock
                // A and waiting to acquire lock B, then
                // this is a deadlock
Release Lock A;
Release Lock B;
停止状態とは、マルチスレッド アプリケーションの 1 つまたは複数の実行可能なスレッドが無期限に遅延している状態か、永続的にブロックされている状態です。ブロックされておらず、何らかの処理を待機している状態でなくても、そのスレッドに実行される見込みがない場合は、停止状態にあると言います。通常、停止状態は、ルールやポリシーのスケジュールの結果です。たとえば、優先順位が高く、非ブロックで、継続的に実行されるスレッドが、優先順位の低いスレッドと共にスケジュールされている場合、シングルコア CPU では優先順位の低いスレッドは実行されなくなります。こうした状況を避けるために、Windows® スケジューラが頻繁に介入し、停止状態にあるスレッドの優先順位を必要に応じて引き上げることで、停止状態を抑制しています。
ライブロックが発生するのは、スレッドが互いの状態の変化にその都度反応するために、スケジュールされているにもかかわらず処理が進まなくなる場合です。この状態を最もよく表すのが、狭い廊下で対面した 2 人の人物が同じ方向によけることで、お互いの行く手をふさぎ続けているような場面です。こうした動作によって処理の進行が妨げられ、結果的にライブロック状態に陥ります。実際に処理が行われているようすがないのに CPU の使用率が高い状態は、ライブロックの典型的な危険信号です。ライブロックの検出や診断はきわめて困難です。

メモリ オーダリングに起因する問題
コンパイラがコードを最適化しているときに、パフォーマンスを高めるためにコードの一部を並べ替えることがあります。ところが、この並べ替えにより、グローバル状態の変化を監視しているスレッドに思わぬ動作が発生するおそれがあります。たとえば、スレッド 1 とスレッド 2 という 2 つの実行スレッドがあるとします。また、ゼロに初期化されている、x および y というグローバル変数もあると仮定します。スレッド 1 は次の行を実行します。
x=10;
y=5;
スレッド 2 は次の行を実行します。
if (y == 5)
{
  Assert(x != 10);
}
このとき、最適化コンパイラは、y には最終的に 5 が再代入されることになると判断し、コードの順序を変更して、x に 10 を代入する前に y に 5 を代入する場合があります (こうした 2 回の代入を行う単一のスレッドという観点からは、オーダリングは問題になりません)。したがって、スレッド 2 のアサートが実行される可能性があります。x が 10 に等しくない場合があるためです。
必要に応じて、volatile 変数とコンパイラ組み込みを使用し、コンパイラによるメモリの並べ替えが行われないようにすることができます。また、これも重要な情報ですが、共通の最適化フラグを有効にした場合、スレッド 2 は変数の y または x の更新を確認しなくなります。その場合は、volatile 変数を使用する必要があります。
コンパイラによる並べ替え以外にも、厄介な問題があります。メモリ アクセスの最適化 (投機実行によるキャッシュや高度なフェッチなど) により、ある CPU のあるスレッドによって実行されたメモリ操作が、別の CPU の別のスレッドによって異なる順序で行われた操作として判断されることがあります。このようにメモリ操作が認識されるのは、複数の CPU または複数の CPU コアを搭載したシステムに限られます。
具体的に考えてみましょう。スレッド 1 およびスレッド 2 という 2 つの実行スレッドがあると仮定します。さらに、m と n はゼロに初期化されたグローバル変数であり、a、b、c、および d はローカル変数であるとします。(プロセッサ 1 上の) スレッド 1 には次の命令があります。
m = 5;     // A1
int a = m; // A2
int b = n; // A3
(プロセッサ 2 上の) スレッド 2 には次の命令があります。
n = 5;     //A4
int c = n; //A5
int d = m; //A6
論理的には (処理が一貫して順番に行われるメモリでは)、これらの命令がすべて実行された後に b == d == 0 になることはあり得ません。ただし、ハードウェア メモリの並べ替えが行われると、共有メモリ (A1 および A4) への書き込みがストアバッファに送られ、各プロセッサによる書き込みの認識に違いが生じることがあります。

テスト戦略
並列アプリケーションのテストを行う場合は、正確性、信頼性、パフォーマンス、およびスケーラビリティをテストする必要があります。アプリケーションの正確性は、静的分析、動的分析、モデル チェックなどの手法を使用して見極めます。パフォーマンスと信頼性をテストするには、並列処理によって生じるオーバーヘッドを調査し、ストレス テストを実施します。スケーラビリティをテストするには、さまざまなサイズのシステムでアプリケーションがどの程度のパフォーマンスを発揮するかを分析します。
静的分析の手法では、コードを分析する際、実際にはプログラムは実行されません。静的分析では、コンパイルされたアプリケーションや注釈付きソース コードのメタデータを調べるのが普通です。一般に、静的分析では、プログラマの意図と想定によって不適切な動作が抑えられていることを確認するために、形式的な検査も行われます。PREfix と PREfast の 2 つのツールは、ネイティブ アプリケーションの分析に最も広く使用されている静的分析ツールです。また、FxCop はマネージ コードの分析に広く使用されています。
静的分析には長所と短所があります。長所としては、カバレッジの広さと細かさ、製品設計の信頼性 (形式的な分析がビルドに役立ちます)、バグの検出と修正を容易にする正確なエラー レポートなどがあります。その一方で、静的分析には、同時実行に関するバグを適切に処理するために、意図を示す注釈が大量に必要になります。しかも、その注釈自体に正確性が求められます。また、静的分析を使用するツールには誤検知が多い傾向があるため、誤検知を最小限に抑える作業に多大な労力を費やさなければなりません。
動的分析では、実行のフットプリントを調べてバグの検出を行います。動的分析には、オンライン分析とオフライン分析の 2 種類があります。オンライン動的分析を使用するツールでは、実行中のプログラムを分析します。一方、オフライン動的分析を使用するツールでは、トレースを記録し、後からそれを分析してバグを検出します。開発者がプログラミングの時点で不要な作業を行う必要がほとんどないうえ、誤検知も少なく、明確な問題に対処しやすくなるので、動的分析は利用しやすい手法です。バグの検出は実行パスだけで行われるので、パスを頻繁にスキャンすることで一連の重大なバグを見つけることができます。そのため、信頼性を簡単に高めることができます。
ただし、短所もあります。バグの検出は実行パスだけで行われるので、コード カバレッジを広げるために、テスト ケースを使用する必要があるのです。事実、コードを実行したときに発生した競合しか検出できないようなツールもあります。したがって、ツールによるバグの報告がなくても、バグがないという確証を得ることはできません。ほとんどの動的分析ツールは、実行時の動作を変更できる何らかのインストルメンテーションに依存していますが、これも動的分析ツールの短所です。これらのツールは複雑なので、パフォーマンスが非常に低い傾向にあります。
最後の手法は、有限状態並列システムの正確性を検証するためのモデル チェックです。この手法では、形式的な演繹法を利用できます。モデル チェッカーは競合とデッドロック状態のシミュレーションを試みます。モデル チェックを行うと、競合とデッドロックがないことを形式的に証明することができます。この手法には、設計とアーキテクチャの信頼性を大幅に高めることができ、カバレッジが非常に広く、外部のドライバが最小限で済むという長所があります。
他のテスト手法と同様に、モデル チェックにもいくつかの短所があります。ほとんど場合、コードから自動的にモデルを抽出することが非常に困難です (限定的な利用の場合はまれに可能です)。モデルを手動で抽出するのもきわめて面倒です。状態空間の肥大化 (起こり得る状態の数が分析しきれないほど多い状態) のため、検証にも長時間を要します。状態空間の肥大化は、複雑なヒューリスティックを利用する抑制手法を適用することである程度は制御可能ですが、そのようなヒューリスティックには、長時間実行アプリケーションのバグを見逃すというマイナス面もあります。
さらに、モデル チェックを行うには、設計と実装に関する綿密な計画が必要になります。モデル チェックによって設計にエラーがないことが証明されても、実装には依然として問題が残されているようなケースもあります。実際のところ、モデル チェックが役立つのは、製品の重要な一部分を調べる場合に限られます。その他、動的分析とモデル チェックを組み合わせるハイブリッドな手法もあります。その一例が CHESS ですが、これについては後ほど説明します。
競合検出アルゴリズム
静的分析ツールと動的分析ツールの両方で使用されるロックセット アルゴリズムは、複数のスレッドが共通のロックを持たない状態で共有サービスにアクセスした場合に発生する可能性のある競合を報告します。基本的に、このアルゴリズムでは、各共有メモリ変数 v の空でないロック セット C(v) は、その変数にアクセスしているスレッドによって保持されます。当初は、C(v) はすべてのロックの一覧です。各スレッドは、locks(t) (保持されているすべてのロック) と writeLocks(t) (保持されているすべての書き込みロック) の 2 つのロック セットも保持します。次に、アルゴリズムによる処理方法を示します。
  1. 各共有メモリ変数 v に対し、ロック セット C(v) を保持します。当初、C(v) はすべてのロックの一覧です。
  2. 各スレッドは、locks(t) (保持されているすべてのロック) と writeLocks(t) (保持されているすべての書き込みロック) の 2 つのロック セットも保持します。
  3. スレッド t による v の各読み取り :
    1. C(v) = C(v) ∩ locks(t) と設定。
    2. C(v) == NULL セットの場合、エラーを生成。
  4. スレッド t による v の各書き込み :
    1. C(v) = C(v) ∩ writeLocks(t) と設定。
    2. C(v) == NULL セットの場合、エラーを生成。
基本的に、アプリケーションの処理が進むと各変数の C(v) は小さくなります。また、C(v) が null になるとエラーが発生します (たとえば、共有メモリへのアクセス時にスレッドのロックセットの積集合が NULL の場合)。
残念ながら、ロックセット アルゴリズムによって報告される競合は、必ずしも実際の競合であるとは限りません。巧みなプログラミング トリックを利用するか、他の同期プリミティブ (シグナルなど) を使用すると、ロックを使用せずに競合の起こらないコードを記述することもできます。ただし、このような手法を使用すると本物のバグの検出が非常に困難になります。この問題を軽減するうえで役立つのが、注釈や特定の抑制です。
競合を検出するためのアルゴリズムとしては、分散システムでのイベントの半順序に基づく "happens-before (事前発生)" アルゴリズムもあります。ここでは、あるイベントの前にどのイベントが発生したかを特定するために半順序を計算するアルゴリズムについて説明します (この文脈でのイベントとは、読み取り、書き込み、およびロックを含めたすべての命令のことです)。
  • 単一のスレッド内では、イベントは発生した順番で配列されます。
  • スレッド間では、イベントは同期プリミティブのプロパティに従って配列されます。たとえば、2 つのスレッドが lock(a) を取得しようとしている場合、一方のスレッドによるロックの解除は、もう一方のスレッドによるロックの "前に発生した (happened-before)" と言います。
  • 複数のスレッドが共有変数にアクセスした場合に、それらのアクセスが "happens-before (事前発生)" の関係によって確定的に順序付けされないとき、競合が発生したと言います。
図 A にこのアルゴリズムを示します。
スレッド 1 のロック解除はスレッド 2 のロックの前に発生しています。したがって、共有変数へのアクセスは同時には起こり得ず、競合はありません。このアルゴリズムの大きな短所は、こうした関係の監視による負荷が非常に大きくなる場合があるという点です。
さらに問題なのは、このアルゴリズムの競合検出能力がスケジュールの順序に完全に依存している点です。半順序は特定のスケジュール インスタンスに対して構成されるため、同じテストを別の日に行うと、バグが見過ごされる可能性があります。図 B の場合は競合は報告されませんが、図 C の実行順序の場合は競合が報告されます。製品がリリースされてから何年もたって初めて発生する競合が数多くあることが知られています。そのため、この検出アルゴリズムを使用しても、ありとあらゆるバグを検出できた、という安心感を得ることはできません。
その一方で、"happens-before (事前発生)" アルゴリズムには誤検知が非常に少なく、検出されるバグのほとんどが実際に問題のあるバグです。ただし、"happens-before (事前発生)" アルゴリズムは多数のエラーを見逃すため (false negative)、効率的に実装するのは非常に困難です。
それと同じように、ロックセット アルゴリズムは非常に効率が良く、多数のエラーを検出できますが、誤検知 (false positive) が非常に多い傾向にあります。そのため、これらのアルゴリズムを組み合わせることで両アプローチの弱点を克服しようという取り組みが続けられています。注 : これらの競合検出アルゴリズムには、ここ数年の間に改良が加えられています。その詳細については、ACM/IEEE のポータルを参照してください。
図 A "Happens-Before (事前発生)" アルゴリズム (クリックすると拡大画像が表示されます)
図 B 競合は報告されない (クリックすると拡大画像が表示されます)
図 C 競合が検出される (クリックすると拡大画像が表示されます)

同時実行テスト ツール
デッドロック、ライブロック、ハングなど、並列処理の際に起こり得るあらゆる問題への対処に役立つ同時実行テスト ツールは、多数市販されています。ここで紹介するツールは、いずれも特定の領域で力を発揮するものです。
CHESS: Microsoft Research が作成した CHESS は、モデル チェックと動的分析を組み合わせた斬新なツールです (go.microsoft.com/fwlink/?LinkId=116523 を参照してください)。このツールは、体系的にスレッド スケジュールを調べ、インタリーブすることで、同時実行のエラーを検出します。競合状態、デッドロック、ハング、ライブロック、およびデータ破損の問題を検出する機能を備えています。また、デバッグを支援するために、実行は完全に反復可能になっています。ほとんどのモデル チェックの場合と同じく、体系的な検証が行われるため、完全なカバレッジが実現されます。
動的分析ツールとしての CHESS は、専用のスケジューラを使用して、定期的な単体テストを繰り返します。実行のたびに異なるスケジュール順序が選択されます。モデル チェッカーとしては、特定のスレッド インタリーブを作成する機能を備えた専用のスケジューラを制御します。状態空間の肥大化を抑制するために、CHESS は半順序法と新しい反復コンテキスト範囲を利用します。
CHESS の反復コンテキスト範囲では、深度で状態空間の肥大化を制限する代わりに、特定の実行におけるスレッドの切り替え数を制限します。切り替えまでの間にスレッド自体で実行できるステップの数に制限はなく、実行深度にも制限は加えられません (これは、従来のモデル チェックから大きく改良された点です)。このメカニズムは、同時実行に関するほとんどのバグは、少数の体系的なスレッド切り替えだけで明らかにできるという経験的証拠に基づいています。
CHESS ではデッドロックと競合を検出できますが (go.microsoft.com/fwlink/?LinkId=116877 で説明されている Goldilocks ロックセット アルゴリズムを使用)、他の状態を検証する場合は、プログラマによるアサーションが必要になります。また、すべてのプログラムを終了する必要があると同時に、すべてのスレッドの公平性が保証されている (処理が進行する) 必要もあります。そのため、プログラムが連続したループに入ると、ライブロックが報告されます。
テストという観点からすると、反復コンテキスト範囲を 2 に設定して CHESS を実行することをお勧めします。バグが検出されなくなったら範囲を 3 にし、その後同様に 1 ずつ大きくしていきます。経験から言って、2 と 3 の範囲でほとんどのバグを検出できます。したがって、バグを短時間で除去するうえで非常に効率的な手法と言えます。
インストルメンテーションのために、CHESS は Win32® API 同期呼び出しを迂回させて、非確定性を抑制したり、意図的に組み込んだりします。また、CHESS を使用する場合は、状態の一貫性を確保するために、開発者コードに多数のアサートを含める必要があります (適切なコードは常にそうあるべきですが)。ただし、ほとんどの動的分析ツールと同様に、広範なカバレッジを実現するには適切なテスト スイートが必要になります。インタリーブ テストの質を高めるため、これまでは負荷を利用するしかなかった開発者やテスト担当者に、CHESS は非常に人気があります。CHESS では、定期的に単体テストを行い、注目すべきインタリーブを体系的にシミュレートすることができます。
Intel Thread Checker: このツールは、デッドロック (発生する可能性のあるデッドロックも含む)、ストール、データ競合、およびネイティブ Windows 同期 API の不適切な使用を検出するための動的分析ツールです (go.microsoft.com/fwlink/?LinkId=115727 を参照してください)。Thread Checker では、すべてのメモリ参照と標準の Win32 同期プリミティブを識別できるようにするために、ソース コードまたはコンパイルされたバイナリをインストルメント化する必要があります。実行時に、そのインストルメント化されたバイナリから、アナライザが実行の半順序を構成するために必要な情報が提供されます。その後、半順序に基づいて "happens-before (事前発生)" 分析が行われます。"happens-before (事前発生)" 分析の詳細については、「競合検出アルゴリズム」を参照してください。
このツールは、パフォーマンスとスケーラビリティの理由から、共有変数に対するアクセスをすべて記憶することはせず、最近のアクセスだけを記憶します。そのため、長時間実行アプリケーションを分析する際のツールの効率が高まります。ただし、一部のバグを見逃すという副作用もあります。妥協が必要になりますが、実行時間が非常に短いアプリケーションのバグをすべて見つけることよりも、長時間実行アプリケーションのバグを多数見つけることの方が重要と言えます。
その他に、このツール唯一の大きな弱点は、カスタム スピン ロックで使用されるようなインタロック操作を通じた同期が考慮されていないという点です。ただし、このツールは、標準の同期プリミティブだけが使用されているアプリケーション用としては、ネイティブ アプリケーションの同時実行テストに利用できるテスト ツールの中でも最も支持されているツールの 1 つであると言えます。
RacerX このツールは、競合とデッドロックの検出に使用される、フローを識別する静的分析ツールです。このツールでは、ソース コード全体に大量の注釈を付けなければならないという問題が解消されています。実際に、注釈については、ロックの取得と解除に使用する API を指定したテーブルを提供するだけでかまいません。このテーブルには、スピン、ブロック、再入などのロック プリミティブの属性も指定できます。通常、このテーブルのサイズはごく小さく、含まれるエントリは多くても 30 程度です。そのため、大規模なシステムのソースに注釈を付ける負担が大幅に軽減されます。
最初の段階で、RacerX は各ソース コード ファイルを反復処理し、制御フロー グラフ (CFG) を作成します。CFG には、関数呼び出し、共有メモリ、ポインタの使用などのデータに関連する情報が含まれます。CFG を作成する際に、RacerX は同期プリミティブ テーブルの逆参照も行い、これらの API の呼び出しをマークします。
CFG が完成すると、分析段階に入ります。この段階では、競合チェッカーとデッドロック チェッカーが実行されます。CFG のスキャンには時間がかかることがありますが、適切な時間短縮とキャッシュの手法が使用されるため、影響は最小限に抑えられます。コンテキスト フローをスキャンする際にはロックセット アルゴリズムが使用され、発生する可能性のある競合が検出されます (このアルゴリズムの詳細については、「競合検出アルゴリズム」を参照してください)。デッドロック分析では、ロックが取得されるたびにロック サイクルが計算されます。
最後の段階では、報告された全エラーの後処理が行われ、エラーの重大性と危険性を基に優先順位が付けられます。静的分析には誤検知の少なさやバグ検出の信頼性の高さが求められますが、このツールの作者は、こうした要望に応えるために多くの時間を費やしてきました。このツールの検出結果は、すばらしいものです。テスト エンジニアリングの点からすると、非常に実用的であると言えます。
Chord: このツールは、フローではなくコンテキストを識別する、Java 向けの静的分析ツールです。フローを識別しないため、他の静的ツールよりもはるかにスケーラブルですが、その分、正確性が損なわれています。このツールでは、Java で使用できる特殊な同期プリミティブも考慮されています。使用されているアルゴリズムは非常に複雑で、多くの概念が取り入れられています (Chord の詳細については、go.microsoft.com/fwlink/?LinkId=116526 を参照してください)。
KISS: これは、Microsoft Research によって開発された、C 並列プログラム向けのモデル チェッカーです。並列システムでは状態空間がすぐに肥大化するため、KISS では、C 並列プログラムをインタリーブの実行をシミュレートする逐次プログラムに変換します。その後で、逐次モデル チェッカーを使用して分析を行います。
アプリケーションは、並列プログラムを逐次プログラムに変換するステートメントによってインストルメント化されますが、KISS には非確定性を制御する機能があります。非確定的なコンテキストの切り替えは、上の CHESS で説明したのと同様の原理で制限されます。プログラマは、同時実行に関する想定を有効にするために、アサートを含める必要があります。このツールでは誤検知は報告されません。このツールは研究用のプロトタイプであり、主に C コードを使用する Windows ドライバ チームによって使用されてきました (go.microsoft.com/fwlink/?LinkId=115723 を参照してください)。
Zing: このツールは、並列プログラムの設計検証用の純粋なモデル チェッカーです。Zing には複雑な状態と遷移を記述するための独自のカスタム言語があります。また、並列ステート マシンをモデル化する機能を完全に備えています。他のモデル チェッカーと同様に、Zing には設計を検証するための包括的な方法が用意されています。このツールを使用すると、想定の有効性を検証し、特定の状態の有無を形式的に証明できるので、設計品質の信頼性を高めることもできます。また、このツールでは、革新的な抑制手法を利用して同時実行状態空間の肥大化を抑えています。
(プログラムの正確性を確認するために) Zing で使用するモデルは、手動で、またはトランスレータを使用して作成する必要があります。特定の用途に使用するドメイン トランスレータは記述可能ですが、ネイティブ アプリケーションや CLR アプリケーション向けの完全かつ実用的なトランスレータとなると、まだ目にしたことがありません。トランスレータがなければ、プロジェクトの重要なサブセクションの正確性を検証する場合を除き、大規模なソフトウェア プロジェクトで Zing を使用するのは不可能でしょう (go.microsoft.com/fwlink/?LinkId=115725 を参照してください)。

パフォーマンス テスト
パフォーマンス テストは、同時実行のテスト プロセスに欠かせない重要な要素です。結局のところ、並列アプリケーションの主要な開発目的の 1 つは、逐次アプリケーションよりも優れたパフォーマンスを実現することです。アムダールの法則とグスタフソンの法則で仮定されているように、並列アプリケーションで実現されるパフォーマンスの向上は、そのアルゴリズムの並列化可能性の側面、プログラム中の逐次処理が必要な部分の量、並列処理のオーバーヘッド、データ/ワークロードの特性などに大きく左右されます。開発関係者は、パフォーマンス テストを行うことで、並列アプリケーションのパフォーマンス特性を把握し、分析することができます。
並列アプリケーションのパフォーマンス テストを行う際の一般的な手法は、逐次アプリケーションのパフォーマンス テストの手法と同じです。以下のセクションでは、パフォーマンス テストの主要なステップを並列アプリケーションに合わせて実行する方法について説明します。最も有用な測定要素については、「テスト データとレポート メトリックの特定」を参照してください。
パフォーマンス テストにおいて最も重要なステップは、テストの目的と目標を明確にすることです。並列アプリケーションの場合は、使用されているアルゴリズムの並列化可能性とスケーラビリティの理解、設計手段によって異なるパフォーマンス特性の把握、同期と通信のオーバーヘッドの検出、パフォーマンス要件を満たしていることの確認などが、適切なテスト目的と言えます。
パフォーマンス テストの目的と範囲は、テストの実施者によっても変わるという点に注意してください。たとえば、並列アプリケーションをこれから導入する顧客の場合は、ビジネス ニーズを満たしていることを確認するためにパフォーマンス テストを行うことがあります。一方、開発チームの場合は、ボトルネックを特定し、プログラムの並列化可能性を改善するために、徹底的なパフォーマンス テストを行うことに注目している可能性があります。そのうえ、テストの目的はソフトウェアの開発サイクルの間に変化することもあります。テストの目的は、設計フェーズや実装フェーズでは、アプリケーションのパフォーマンス特性を把握し、向上させるためであり、テスト フェーズやリリース フェーズ (安定化フェーズ) では、ビルドによってパフォーマンスが低下しないことを確認するためという具合に変わる可能性があります。
パフォーマンスのテスト プロセスでは、テスト シナリオの作成とその目標の設定も重要になります。並列アプリケーションのパフォーマンス特性を把握したいと考えている開発チームの場合は、顧客シナリオ テスト、主要業績評価指標 (KPI) テスト、ミクロ ベンチマーク (MBM) テストの 3 種類のテスト シナリオを作成する必要があります。
これら 3 種類のパフォーマンス テストの関係は、システム テスト、統合テスト、および単体テストの関係と同等のものと考えることができます。つまり、顧客シナリオ テストは一連の KPI テストにつながり、KPI テストは一連の MBM テストにつながる、と言えます。これらのテスト タイプの関係を理解しておくと、開発者は、並列アプリケーションのさまざまなサブパーツ間のパフォーマンスの関係も理解できます。また、こうした理解があれば、パフォーマンス バグの適切な優先順位付けが可能になります。そして、さらに重要なことですが、結果指向の提案を行うことにより、開発チームはパフォーマンスに関する顧客の苦情に対処できるようになります。
並列アプリケーションの場合は言うまでもありませんが、複雑なアプリケーションのパフォーマンス目標を設定するのは非常に難しい作業です。目標を設定する際の 1 つのアプローチとして、アプリケーションの各コンポーネントとサブコンポーネントに期待するアルゴリズムのパフォーマンス メトリック/理論的パフォーマンス メトリック、比較分析 (比較の候補には競合他社の類似アプリケーションを含む)、および既存の実装/アプリケーション (存在する場合) のプロファイリングから得られる 3 つのメトリックを評価することにより、目標を導き出す方法があります。
テスト目標の場合と同様に、どのテスト シナリオにも一連の判定基準 (テスト結果を検証するための一連の値) を設ける必要があります。並列性のテスト シナリオでは、判定基準として次の要素を使用できます。
テスト結果の変動性 : これは、アプリケーションの不安定性を示します。テスト結果の変動性を明らかにするには、テスト結果の標準偏差を計算します。
CPU 使用率 : CPU 使用率が低い状態は、同時実行に関するバグ (デッドロックや同期オーバーヘッドなど) の兆候である可能性があります。逆に CPU 使用率が高い場合も、必ずしも良好な状態であるとは言えません。ライブロックが発生すると、CPU 使用率が高まります。
ガベージ コレクション : マネージ アプリケーションでは、過剰な数のメモリ管理処理によって実際の処理の実行が妨げられることがあります。その場合は、設計や実装に問題があることがわかります。
スレッドの合計実行時間 : これを基に、プログラムを逐次実行した場合の合計実行時間を見積もることができます。スレッドの合計実行時間を、同じアルゴリズム プログラムを逐次実行した場合の経過時間と比較することで、並列処理のオーバーヘッドを把握できます (また、テストが有効であるかどうかも検証できます)。
メモリの合計使用量 : メモリの合計使用量の測定は、アプリケーションのメモリ プロファイルの把握に役立ちます。マネージ アプリケーションでは、アプリケーションの合計メモリ使用量の変化にガベージ コレクタが大きく関係しています。メモリ使用量を評価する際には、ガベージ コレクタを必ず考慮に入れてください。
パフォーマンス テストを開始する前に 3 つの重要な手順を実行しておくと、より適切な結果を得ることができます。まず、テスト環境の可変性を最小限に抑えます (可変性が生じる原因は、アプリケーションとテスト環境の両方にあります)。テスト環境の可変性を抑えるには、不要なサービスとアプリケーションを停止し、ネットワークの干渉を抑制してください。アプリケーションが原因の可変性については、ウォームアップを繰り返し行い、同じテストを複数回行って測定値を収集することで、その影響を抑えることができます。
次に、マネージ アプリケーションでは、ガベージ コレクタが不適切なタイミングで実行されると、パフォーマンスの結果の有効性が失われることがあります。そこで、パフォーマンスの測定中にガベージ コレクタが呼び出される可能性を最小限に抑える必要があります。そのためには、測定値を収集する前または後にガベージ コレクション (GC) を強制的に実行するか、GCLatencyMode を LowLatency に変更します (Microsoft® .NET Framework 3.5 の場合のみ)。
最後に、メトリックを収集する前に必ずウォームアップを行うようにします。測定値を収集する前にウォームアップとしてテストを実行することで、変数の初期化、(マネージ アプリケーションにおける) JIT 処理のコスト、初期のキャッシュ ミスによる遅延など、1 回限りのコストを抑えることができます。ただし、パフォーマンス シナリオによっては、コールド起動パフォーマンスの測定も重要になる場合があります (アプリケーションの起動パフォーマンス改善の詳細については、msdn2.microsoft.com/magazine/cc337892.aspx を参照してください)。
同時実行テストに関する参考資料

同期を使用する
go.microsoft.com/fwlink/?LinkId=115730

Bugslayer: Wait Chain Traversal
msdn2.microsoft.com/magazine/cc163395

ハングを解消する : .NET アプリケーションのデッドロックを回避および検出するための高度な手法
msdn2.microsoft.com/magazine/cc163618

ReadWriteBarrier (C++)
go.microsoft.com/fwlink/?LinkId=115731

Intel 64 アーキテクチャ メモリ オーダリング
go.microsoft.com/fwlink/?LinkId=115732

同期とマルチプロセッサの問題点
go.microsoft.com/fwlink/?LinkId=115734

PREfast ステップ バイ ステップ
go.microsoft.com/fwlink/?LinkId=115736

AppVerifier を使用してアプリケーションをテストする
go.microsoft.com/fwlink/?LinkId=115739

Web アプリケーション向けパフォーマンス テスト ガイダンス
go.microsoft.com/fwlink/?LinkId=115741

ストレス テスト
多くの場合、ソフトウェア ストレス テストでは、アプリケーションの堅牢性、可用性、および正常なエラー処理についてテストします。通常、堅牢性をテストするには、同じ機能または同じ機能の組み合わせを繰り返し呼び出して、実行の正確性を確認します。可用性をテストするには、リソースが大量に使用されている状態 (メモリが少ない状態、CPU の負荷が高い状態など) でアプリケーションを実行し、アプリケーションがクラッシュしないことやエラーが適切に発生することを確認します。正常なエラー処理については、多くの場合、エラーを挿入する (フォールト挿入) か、複数のエラーを発生させることでテストします。
並列アプリケーションのストレス テストを行う際には、アプリケーションの堅牢性と可用性を特に重点的にテストする必要があります。並列アプリケーションには非確定的な動作があるため、同じ機能または同じ機能の組み合わせを繰り返し呼び出した際に、異なるコード パスが使用されることがあります。これは、数多くのバグの検出につながる可能性があります。また、リソースが大量に使用されている状態 (非常に多くのスレッドが作成されている状態や、複数のスレッドがデッドロック/ライブロックに陥っている状態) での可用性のテストは特に重要です。並列アプリケーションの場合、ユーザーがそのような場面に遭遇する可能性が比較的高いためです。
ストレスに関係する同時実行のバグの根本原因を分析することや、ストレスに関係するバグを一貫した方法で再現することは、きわめて困難です。とはいえ、同時実行テスト ツールがいまだ発展途上にある現状では、ストレス テストは機能テストを補完する重要な手段です。

まとめ
並列コンピューティングの処理能力を真に活用するためには、逐次アプリケーションを開発、テストするために築かれた現状のソフトウェア開発慣習を進化させる必要があります。並列化によって生じる新種のバグが原因で、並列アプリケーションの開発とテストは難しい作業になっています。この記事では、同時実行に関するさまざまな種類のバグと、そのようなバグを見つけるための戦略の概要を示しました。また、パフォーマンス テストを同時実行のシナリオに利用する方法についても説明しました。

Rahul V. Patil は、マイクロソフトの並列コンピューティング プラットフォーム チームで SDET リードを務め、ネイティブな並列フレームワークを使用したテスト作業を指揮しています。

Boby George もマイクロソフトの並列コンピューティング プラットフォーム チームの SDET であり、マネージ並列コンピューティング フレームワークを使用したパフォーマンス テスト作業を担当しています。


ロボティクス
Microsoft Robotics Studio で世界をシミュレーションする
Sara Morgan

この記事では、次の内容について説明します。
  • ロボット アニメーションを作成する
  • 環境を構築する
  • ロボットまたはシミュレーションを駆動する
  • エンティティを作成する
 この記事では、次のテクノロジを使用しています。
C#、Robotics Studio
コードのダウンロード : RoboticsStudio2008_06.exe (646 KB)
オンラインでのコードの参照

目次
シミュレーションとは
準備
シミュレーションを実行する
エンティティおよびエンティティの種類
プログラムで新規エンティティを追加する
VSE で新規エントリを定義する
エンティティ クラスを作成する
メッシュを作成する
メッシュをバイナリ形式に変換する
シミュレーション サービスを作成する
シミュレーション シーンを設定する
マニフェストを作成する
シミュレーションを実行する
差動駆動なしのエンティティを作成する
名前に惑わされないでください。Microsoft® Robotics Studio (MSRS) は単なるロボット操作の手段ではありません。約 2 年前に Microsoft Research がリリースした MSRS は平凡なビジネス アプリケーション開発者からはまったく注目されない存在でしたが、多様なハードウェア デバイス向けのサービスベース アプリケーションを構築する機能を備えています。ツールキットには、Windows® Communication Framework (WCF) 開発者ならすぐに使いこなせるランタイムが含まれています。また、ビジュアル プログラミング言語 (VPL) ツールおよびビジュアル シミュレーション環境 (VSE) も含まれています。
MSRS は、ロボット工学 (ロボティクス) に基づくアプリケーションの設計および展開に必要なツールとサービス指向ランタイムを提供します。ロボティクス アプリケーション開発が初めての開発者がスムーズに開発を進めるためのビジュアル オーサリング ツール、チュートリアル、ドキュメントも用意されています。市販ソフトウェアを開発する場合は低価格のツールキットを購入する必要がありますが、趣味や学術研究が目的の場合はこのツールキットを無償でダウンロードして使用することができます。
MSRS ランタイムは CLR 2.0 上の 2 つの低レベル ランタイムで構成されます。その 2 つのランタイムとは、分散ソフトウェア サービス (DSS : Decentralized Software Services) と CCR (Coordination and Concurrency Runtime) です。DSS は、Web の機能に使用されている Representation State Transfer (REST) 原則に基づく軽量のサービス指向ランタイムです。CCR は、非同期処理をサポートする Microsoft .NET Framework ライブラリです。データは常に多数のセンサおよびアクチュエータから送受信されているため、これはきわめて重要なロボティクス アプリケーションです。
MSRS ツールキットには、ランタイムのほか、デザイン画面上に要素をドラッグ アンド ドロップするだけでロボティクス アプリケーションを作成できるようになる VPL ツールも含まれています。また、複数のロボットや障害物が関連する複雑なシミュレーションを実験するための VSE も含まれています。この記事では、MSRS のこの機能に重点を置きます。ここでは、ビジュアル シミュレーション環境について説明し、新しいロボティック エンティティを使用してシミュレーションを作成する手順を示します。この記事のコードを操作するためにロボットは不要なので安心してください。これがシミュレーションの魅力です。高価なハードウェアに投資することなく学習することができます。﻿

シミュレーションとは
シミュレーションになじみのない読者はパラパラ漫画を想像してください。各ページに絵が描かれていて、すばやくパラパラとページをめくると絵が動く小型の本です。シミュレーションのしくみはこれと同様です。シミュレーションには 1 つ以上のエンティティが存在し、各エンティティはフレーム内でレンダリングされます。フレームはパラパラ漫画のページに相当します。フレームのレンダリング速度は、使用するコンピュータのグラフィックス カードによって異なります。
パラパラ漫画とシミュレーションの根本的な相違は、パラパラ漫画は静的で予測可能であるのに対し、シミュレーションはそうではないという点です。シミュレーションの各フレームは動的にレンダリングされるため、各フレーム内のエンティティの内容は必ずしも事前にはわかりません。ここがシミュレーションの便利な点です。ロボットの動作をデモンストレーションし、ロボットと実世界とのやり取りを確認することができます。

準備
MSRS ツールキットをまだダウンロードしていない場合は、go.microsoft.com/fwlink/?LinkId=113902 からダウンロードしてインストールすることができます。MSRS Web サイトでは、現在、バージョン 1.5 の更新インストールを提供しています。このインストールには、MSRS 1.5 の完全バージョンと使用可能なすべての更新プログラムが含まれます。
MSRS には Visual Studio® 2005 のテンプレートが用意されていますが、Visual Studio 2008 を実行しても、この記事の付属サンプルを実行できます。開発用コンピュータには DirectX® 9.0 互換のグラフィックス カードが必要です。グラフィックス カードは、頂点シェーダ VS_2_0 以降またはピクセル シェーダ PS_2_0 以降をサポートしている必要があります。最近のグラフィックス ゲームが動作する多くのハイエンド デスクトップおよびラップトップは、この要件を満たすカードを既に装備しています。
使用しているグラフィックス カードのサポート内容が不明な場合は、go.microsoft.com/fwlink/?LinkId=113904 で、グラフィックス カードとそのカードがサポートするシェーダの一覧を確認してください。また、MSRS をインストールして VSE でヘルプ メニューを使用して、サポートされるピクセル シェーダおよび頂点シェーダを確認できます。MSRS またはこの記事に付属しているシミュレーションのいずれかを実行しても画面に何も表示されない場合、グラフィックス カードに互換性がありません。
MSRS では、AGEIA PhysX エンジンを使用してシミュレーション環境の物理的特性を指定します。物理特性なしではシミュレーションする世界を表現する手段がないため、シミュレーションは役に立ちません。たとえば、重力がなければエンティティは浮遊することができます。MSRS の付属ドキュメントに AGEIA から提供されている別売アクセラレータ カードの使い方が記載されています (ageia.com を参照)。ただし、この記事のシミュレーションを操作するためにこの物理カードを購入してインストールする必要はありません。シミュレーションでは AGEIA PhysX ソフトウェア エンジンによって物理特性を使用できます。

シミュレーションを実行する
そろそろシミュレーションを見たくてたまらなくなってきたことと思います。自作のシミュレーションを作成する前に、MSRS に付属しているシミュレーションのいずれかを実行してみてください。MSRS をインストールすると、7 つのシミュレーションを格納したメニュー フォルダが作成されます。このフォルダの最初のシミュレーション、Basic Simulation Environment では、図 1 のようなシーンがレンダリングされます。
図 1 Basic Simulation Environment (クリックすると拡大画像が表示されます)
この基本シミュレーションにはエンティティが 2 つ (地球儀と箱) しかありません。実際には、シミュレーション シーンには、メイン カメラ、地面、空、太陽を表すエンティティも含まれます。シーンに含まれるエンティティを確認するには、VSE メニューの [Mode] (モード) および [Edit ] (編集) をクリックします。
図 2 のように、編集モードでは左側のペインが表示され、ここで各エンティティに関連付けられたプロパティを変更できます。これらのプロパティは、エンティティの名前やシミュレーション環境内での位置などのあらゆる情報を制御します。また、これらのプロパティを使用して、ユーザーはエンティティのレンダリング方法を細かく制御したり、シミュレーションでの表示方法を変更することができます。
図 2 エンティティのプロパティを変更する編集モード (クリックすると拡大画像が表示されます)
実行モードに戻ると、マウスまたは方向キーでシミュレーション シーン内を移動できます。これにより、メイン カメラの視点が変わり、それがユーザーのシミュレーションでのビューになります。もう 1 つ重要なことは、VSE ではシーンをさまざまなモードでレンダリングできるということです。既定はビジュアル モードです。このモードはシミュレーション シーンを写実的に表示します。[Render] (レンダ) メニューで [Wireframe] (ワイヤフレーム)、[Physics] (物理特性)、[Combined] (結合)、[No Rendering] (レンダリングなし) のいずれかのモードを選択することもできます。
レンダリングはシミュレーションの一面にすぎないため、[No Rendering] (レンダリングなし) というオプションが用意されています。シミュレーションの実行で最も大切なことは、各種エンティティ間のやり取りです。シミュレーション シーン内でエンティティをレンダリングするとリソースが大量に使用されるため、エンティティの数が多い場合は [No Rendering] (レンダリングなし) が便利です。

エンティティおよびエンティティの種類
エンティティの種類では、特定のエンティティの種類の新しいインスタンスを定義できます。たとえば、基本シミュレーション環境に表示された地球儀のエンティティの種類は単一図形です。エンティティの種類は、特定のエンティティの種類に関連付けられたプロパティを指定するという点で新しいエンティティのテンプレートとしての役割を果たします。これらのプロパティの値はエンティティを作成した後で変更できますが、どのプロパティが含まれるかはエンティティの種類によって定義されます。
シミュレーションにエンティティを追加するには、VSE にエンティティの種類が必要です。たとえば、iRobot の Create などのロボットを追加するには、編集モードで [Entity] (エンティティ) および [New] (新規作成) をクリックして、VSE 内から新しいエンティティを追加します。[New Entity] (新しいエンティティ) ダイアログ ボックスが表示されます (図 3 を参照)。
図 3 [New Entity] (新しいエンティティ) ダイアログ ボックスでの新しいエンティティの挿入
iRobot の Create は MSRS にエンティティ型が用意されているロボットの 1 つです。そのため、[New Entity] (新しいエンティティ) ダイアログ ボックスを使用するだけで、新しい Create ロボットをシミュレーションに追加できます。[New Entity] (新しいエンティティ) ダイアログ ボックスの [Type] (種類) で [iRobotCreate] を選択し、「Create Entity」などの一意の名前を入力します。[OK] をクリックすると、Create ロボットがシミュレーション シーンに表示されます。

プログラムで新規エンティティを追加する
MSRS にはシミュレーションを作成および使用する複数の方法があります。DSS サービス プロジェクトを作成することにより、VSE だけでなく、プログラムによってエンティティも追加できます。MSRS には新しい DSS サービスの作成に使用できる Visual Studio テンプレートがあります。MSRS をインストールしたら、新規 Visual Studio の作成時にテンプレートとして [Simple Dss Service (1.5)] を選択するだけです (図 4 を参照)。
図 4 Visual Studio 2005 テンプレートでの新規 DSS サービスの作成 (クリックすると拡大画像が表示されます)
テンプレートを使用して新規 DSS サービスを作成すると、2 つのクラス ファイルが作成されます。実装クラス (既定ではプロジェクトと同じ名前) にコードを追加して新しいエンティティを作成します。シミュレーションをプログラムで作成した場合、Simple DSS Service テンプレートに含まれていないアセンブリにアクセスする必要があります。そのため、図 5 に示すようにアセンブリへの参照を追加する必要があります。
図 5 シミュレーション プロジェクトに含まれるアセンブリ
アセンブリ名 | 説明
PhysicsEngine | 基になる AGEIA ソフトウェア物理エンジンへのアクセスを提供します。
RoboticsCommon | ロボットの物理属性を定義する PhysicalModel 名前空間へのアクセスを提供します。
SimulationCommon | シミュレーションおよび物理エンジンの操作に使用する型定義へのアクセスを提供します。
SimulationEngine | シミュレーション エンジンへのアクセスを提供します。
SimulationEngine.proxy | シミュレーション エンジンをパートナーとして読み込む場合に使用するシミュレーション エンジンのプロキシを表します。
参照を追加した後、次の名前空間宣言を実装クラス ファイルに追加する必要があります。
using Microsoft.Robotics.Simulation;
using Microsoft.Robotics.Simulation.Engine;
using engineproxy = Microsoft.Robotics.Simulation.Engine.Proxy;
using Microsoft.Robotics.Simulation.Physics;
using Microsoft.Robotics.PhysicalModel;
シミュレーションに必要なコードを理解するには、まず MSRS に付属しているシミュレーション チュートリアルを確認する必要があります。MSRS インストールの \samples\simulation tutorials ディレクトリに格納されているサンプルは、MSRS インストール後のメニューから選択可能なシミュレーションに相当します。たとえば、Basic Simulation Environment は SimulationTutorial1 プロジェクトと同じです。
SimulationTutorial1 プロジェクトを Visual Studio 2005 で開くと、Basic Simulation Environment の作成に使用されたコードを表示できます。まず、Start メソッドに注目してください。このメソッドはサービスの起動時に自動的に呼び出されます。
protected override void Start()
{
      base.Start();
      // Orient sim camera view point
      SetupCamera();
      // Add objects (entities) in our simulated world
      PopulateWorld();
}
この Start メソッドにコードを追加して、シミュレーション環境を定義します。SimulationTutorial1 プロジェクトの場合、このメソッドでカメラを設定し、シミュレーション シーンにエンティティ オブジェクトを配置します。
基本シミュレーション環境には、メイン カメラのほかに空、大地、箱、地球儀を表すエンティティが含まれます。地球儀、つまりテクスチャを設定した球体を挿入するコードは図 6 のようになります。
図 6 地球儀の挿入
void AddTexturedSphere(Vector3 position)
{
    SingleShapeEntity entity = new SingleShapeEntity(
       new SphereShape(
          new SphereShapeProperties(10, // mass in kg
          new Pose(), // pose of shape within entity
          1)), //default radius
       position);

    entity.State.Assets.Mesh = "earth.obj";
    entity.SphereShape.SphereState.Material =
       new MaterialProperties("sphereMaterial", 0.5f, 0.4f, 0.5f);

    // Name the entity
    entity.State.Name = "detailed sphere";

    // Insert entity in simulation.
    SimulationEngine.GlobalInstancePort.Insert(entity);
}
AddTexturedSphere メソッドのコードの 1 行目では、SingleShapeEntity のインスタンスを作成します。この型は球などの単一幾何図形のエンティティを表します。単純な物理ジオメトリのエンティティを追加する必要がある場合に便利な型です。この場合、質量 10 kg (約 4.5 ポンド) のエンティティを作成しています。
このエンティティに割り当てられたメッシュは、ファイル拡張子が .obj のオブジェクト ファイルです。このオブジェクト ファイルは 3D グラフィカル編集ツールで作成し、エイリアス オブジェクト形式にエクスポートしたものです。MSRS のメッシュ オブジェクト ファイルはこの形式にする必要があります。AddTexturedSphere メソッドの最後の処理として、球体のエンティティをシミュレーション環境に挿入します。

VSE で新規エントリを定義する
次に、Parallax の Boe-Bot を表す新しいロボット エンティティを作成します。Boe-Bot は、2 輪差動駆動システムを採用した車輪付き小型ロボットです (図 7 の写真を参照)。Boe-Bot の詳細については、Parallax Web サイト (parallax.com) を参照してください。
図 7 Parallax の Boe-Bot (クリックすると拡大画像が表示されます)
Boe-Bot は、MSRS で標準サポートされるロボット プラットフォームです。そのため、MSRS のインストールには、Boe-Bot の駆動システムと組み込みの接触センサーを操作するための基本サービスが含まれています。MSRS には Boe-Bot 用のプラットフォーム サービスが含まれていますが、Boe-Bot のエンティティ型は含まれていません。

エンティティ クラスを作成する
Boe-Bot の新しいエンティティ型を作成するには、DifferentialDriveEntity から派生したクラスを追加する必要があります。DifferentialDriveEntity クラスを継承することによって、シミュレーションでの Boe-Bot の動作を定義するコードを再利用できます。BoeBot エンティティ型を作成するコードは図 8 のようになります。
図 8 BoeBot エンティティ型の作成
[DataContract]
public class BoeBot : DifferentialDriveEntity
{
    Port<EntityContactNotification> _notifications =
           new Port<EntityContactNotification>();

    // Default constructor, used for creating the entity from XML
    public BoeBot() { }

    // Custom constructor for building model from hardcoded values.
    // Used to create entity programmatically
    public BoeBot(Vector3 initialPos)
    {
        MASS = 0.454f; //in kilograms  (around 1 pound)
        // the default settings approximate the BoeBot chassis
        CHASSIS_DIMENSIONS = new Vector3(0.09f, //meters wide
                                         0.09f,  //meters high
                                         0.13f); //meters long
        FRONT_WHEEL_MASS = 0.01f;
        CHASSIS_CLEARANCE = 0.015f;
        FRONT_WHEEL_RADIUS = 0.025f;
        CASTER_WHEEL_RADIUS = 0.0125f;
        FRONT_WHEEL_WIDTH = 0.01f;
        CASTER_WHEEL_WIDTH = 0.008f;
        FRONT_AXLE_DEPTH_OFFSET = 0.01f; // distance from center of robot

        base.State.Name = "BoeBot";
        base.State.MassDensity.Mass = MASS;
        base.State.Pose.Position = initialPos;

        // chassis position
        BoxShapeProperties motorBaseDesc =
            new BoxShapeProperties("BoeBot Body", MASS,
                new Pose(new Vector3(
                0, // Use 0 for X axis offset
                CHASSIS_CLEARANCE + CHASSIS_DIMENSIONS.Y / 2,
                0.03f)), // minor offset in the z/depth axis
                CHASSIS_DIMENSIONS);

        motorBaseDesc.Material =
             new MaterialProperties("high friction", 0.0f, 1.0f, 20.0f);
        motorBaseDesc.Name = "Chassis";
        ChassisShape = new BoxShape(motorBaseDesc);

        // rear wheel is also called the castor
        CASTER_WHEEL_POSITION = new Vector3(0, // center of chassis
             CASTER_WHEEL_RADIUS,              // distance from ground
             CHASSIS_DIMENSIONS.Z / 2);        // at the rear of the robot

        RIGHT_FRONT_WHEEL_POSITION = new Vector3(
             +CHASSIS_DIMENSIONS.X / 2,// left of center
             FRONT_WHEEL_RADIUS,       // distance from ground of axle
             FRONT_AXLE_DEPTH_OFFSET); // distance from center, on z-axis

        LEFT_FRONT_WHEEL_POSITION = new Vector3(
             -CHASSIS_DIMENSIONS.X / 2,// right of center
             FRONT_WHEEL_RADIUS,       // distance from ground of axle
             FRONT_AXLE_DEPTH_OFFSET); // distance from center, on z-axis

        MotorTorqueScaling = 30;

        // specify a default mesh
        State.Assets.Mesh = "boe-bot.bos";
}
BoeBot クラスのコンストラクタは、DifferentialDriveEntity クラスに定義した複数の変数の値の設定に使用します。たとえば、Mass には 0.454 という値が設定されています。これは、質量を kg で表した値です。また、Boe-Bot のシャーシの幅、長さ、高さを定義しています。各測定値は、実物のロボットを計量し、メートル巻尺を使って測定した値です。
Boe-Bot の位置は、エンティティの作成時に渡された一連の座標を使用して定義します。これらの座標は X、Y、Z の各座標点を表します。MSRS シミュレーション エンジンでは右手座標系を使用します。そのため、Z 座標が向かう方向に影響があります。
BoeBot コンストラクタはシャーシの位置とエンティティ内の車輪の位置も定義します。DifferentialDriveSystem クラスは、ロボットに 2 つの主輪と、主にバランスをとるために使用する 1 つの小さい後輪があることを前提にしています。主輪を制御する左右のモーターに動力を割り当てます。各車輪に割り当てられた動力レベルの相違によって、前後左右のどの方向に移動するかが決まります。これは物理ロボットの駆動と同じ方法です。
シミュレーションの魅力は、理論上、仮想ロボットも物理ロボットも違いはないということです。ロボットを動かすコードとセンサーからデータを受信するコードは同じです。シミュレーション プロジェクトのコードの一部は実物のロボットの操作に再利用できます。"理論上" という言葉が気になったかもしれません。この言葉を言い添えたのは、シミュレーションで現実世界の環境を完全にシミュレーションすることはできないからです。シミュレーションでは間違った位置に存在する障害物などのノイズ (予期しない物体) を考慮できません。
シミュレーションで可能なことは、新しいロボットの設計を実験したり、複数のロボット間のやり取りをシミュレーションするための現実に近似した機会を提供することです。この機能はリソースが制限され、学生数が多い学術環境では非常に便利です。

メッシュを作成する
各エンティティをメッシュに関連付けて、エンティティをリアルに表現することができます。たとえば、球体の場合、メッシュによって球体エンティティが地球のように見えます。厳密に言えば、エンティティをメッシュに関連付ける必要はありませんが、ロボットなどの複雑なエンティティの場合、メッシュ オブジェクトの方が適しています。
たいていの 3D グラフィカル編集ツールでメッシュを作成できます。この記事では、同僚の Steve Grand が SoftImage という 3D パッケージを使用して作成してくれた Boe-Bot メッシュを使用しています (SoftImage の詳細については、softimage.com を参照してください)。このメッシュに関連付けられたファイルは、この記事に付属のダウンロード可能な .zip ファイルに含まれています。この記事の手順に従うには、.zip ファイルをダウンロードし、このファイルを展開して、Boe-Bot フォルダの内容をローカル MSRS インストールに関連付けられた /store/media ディレクトリにコピーします。
SoftImage では、イメージを .obj 形式にエクスポートできます。この機能はすべてのパッケージに存在するわけではありません。たとえば、SolidWorks (solidworks.com を参照) は Channel9 Wiki (go.microsoft.com/fwlink/?LinkId=114031) で MSRS によって推奨されている 3D パッケージです。ただし、残念なことに SolidWorks には .obj 形式へのエクスポート機能がありません。この場合は Blender (blender.org) というツールを使用して変換を行うことをお勧めします。メッシュ ファイルを作成するグラフィックス パッケージを選択する際は、このことを念頭に置いてください。
Boe-Bot メッシュの作成に関する詳細はこの記事では説明しませんが、複数の多角形メッシュ図形を結合して作成されているものであるということは知っておいてください。たとえば、メタル シャーシの場合、立方体図形を変更して Boe-Bot の寸法と形状を表します。これに円柱図形を追加して車輪を表します。階層を設定して車輪をシャーシにバインドすることにより、オブジェクト全体を単一の図形として機能させることが可能になります。これで、図 9 のような物理 Boe-Bot を表すイメージができあがります。
図 9 SoftImage を使用した Boe-Bot メッシュ ファイルの作成 (クリックすると拡大画像が表示されます)

メッシュをバイナリ形式に変換する
MSRS には、.obj ファイルを最適化されたバイナリ ファイル (拡張子 .bos) に変換するコマンドライン ツールがあります。このファイル タイプの利点は、.obj ファイルよりも読み込みが高速だということです。Obj2bos.exe という名前のこのコマンドライン ツールには、MSRS インストールに付属しているコマンド プロンプトのメニュー項目からアクセスできます。
メッシュを操作するときに覚えておく必要がある重要点は、素材を使用して物理オブジェクトに色を付け、各エンティティを 1 つ以上の素材に関連付けできるということです。そのため、.obj ファイルを作成すると、拡張子 .mtl が付いたマテリアル ファイルが追加される場合があります。テクスチャを作成するイメージ ファイルが含まれる場合もあります。これらのファイルは、メッシュ変換ツールを使用する前に .obj ファイルと同じ場所にコピーしておく必要があります。
obj ファイルおよび関連付けられたすべてのファイルは、ローカル MSRS インストールに関連付けられた /store/media ディレクトリに配置する必要があります。ダウンロード コードに含まれている Boe-Bot メッシュに関連付けられたファイルには、Aluminum.png、Boe-bot.mtl、Boe-bot.obj、BoebotWheel.png、Pcb.png があります。これらのファイルを media ディレクトリに配置したら、MSRS コマンド プロンプトを開いて次のように入力します。
Obj2bos.exe /i:"store\media\Boe-bot.obj"
変換ツールが media ディレクトリに Boe-bot.bos という名前のファイルを作成します。このファイルはそのままにしておいてください。後で参照します。

シミュレーション サービスを作成する
これで、Boe-Bot シミュレーション サービスの作成準備ができました。この記事に付属のダウンロード コードに従う場合は、MSRS がインストール済みであること、およびダウンロードしたコードがローカル MSRS インストールの \samples フォルダに保存されていることを確認してください。
プロジェクトを一から作成する場合、Visual Studio 2005 テンプレートで新規 DSS サービスを作成する必要があります。プロジェクトに SimulatedBoeBot という名前を付けます。また、前述のアセンブリの参照以外に、Microsoft.Xna.Framework への参照を追加し、SimulatedBoeBot.cs クラス ファイルの先頭に次の名前空間の参照を追加する必要があります。
using xna = Microsoft.Xna.Framework;
using xnagrfx = Microsoft.Xna.Framework.Graphics;
using xnaprof = Microsoft.Robotics.Simulation.MeshLoader;
Xna フレームワークは、MSRS でエンティティのレンダリングに使用されます。Xna の参照は Boe-Bot エンティティ型を定義するコードで使用されます。MSRS のすべてのエンティティ型は Microsoft.Robotics.Simulation.Engine 名前空間にあります。

シミュレーション シーンを設定する
あらゆるシミュレーション サービスの主なタスクの 1 つは、エンティティをシミュレーション シーンに挿入することです。この処理はサービスの起動時に行う必要があるため、通常は PopulateWorld という名前のメソッドの呼び出しを Start メソッドに追加します。Boe-Bot シミュレーションの場合、PopulateWorld メソッドは次のようになります。
private void PopulateWorld()
{
     AddSky();
     AddGround();
     AddBoeBot(new Vector3(1, 0, 1));
     AddBox(new Vector3(2, 1, 1));
}
AddSky メソッドおよび AddGround メソッドは BoeBot が存在する環境を設定します。AddSky メソッドでは、新しい SkyDomeEntity 型を作成してシミュレーションに挿入します。太陽を表すディレクショナル ライトもシミュレーションに挿入します (このメソッドのコードは図 10 を参照)。
図 10 空の追加
void AddSky()  {
    // Add a sky using a static texture. We will use the sky texture
    // to do per pixel lighting on each simulation visual entity
    SkyDomeEntity sky = new SkyDomeEntity("skydome.dds",
                                          "sky_diff.dds");
    SimulationEngine.GlobalInstancePort.Insert(sky);

    // Add a directional light to simulate the sun.
    LightSourceEntity sun = new LightSourceEntity();
    sun.State.Name = "Sun";
    sun.Type = LightSourceEntityType.Directional;
    sun.Color = new Vector4(0.8f, 0.8f, 0.8f, 1);
    sun.Direction = new Vector3(0.5f, -.75f, 0.5f);
    SimulationEngine.GlobalInstancePort.Insert(sun);
}
AddGround メソッドは HeightFieldEntity 型を使用して標高 0 の広大な水平面を作成します。MSRS インストールで提供されるテクスチャ イメージを使用して地面を表します。このメソッドのコードは次のようになります。
void AddGround()
{
    // create a large horizontal plane, at zero elevation.
    HeightFieldEntity ground = new HeightFieldEntity(
        "simple ground", // name
        "03RamieSc.dds", // texture image
        new MaterialProperties("ground",
            0.2f, // restitution
            0.5f, // dynamic friction
            0.5f) // static friction
        );
    SimulationEngine.GlobalInstancePort.Insert(ground);
}
次に、Boe-Bot エンティティを追加します。AddBoeBot メソッドは、ロボットの位置を表す受信 Vector3 パラメータを受け取ります。この位置は、先に作成した BoeBot エンティティのコンストラクタに渡されます。また、AddBoeBot メソッドは、シミュレーションされた Boe-Bot 駆動サービスをエンティティ パートナーとして起動します。このメソッドのコードは次のようになります。
void AddBoeBot(Vector3 pos)
{
     BoeBot boeBot = new BoeBot(pos);
     boeBot.State.Name = "SimulatedBoeBot";

     // Start simulated Boe-Bot Drive service
     CreateService(
        drive.Contract.Identifier,
          Microsoft.Robotics.Simulation.Partners.CreateEntityPartner(
           "http://localhost/" + boeBot.State.Name));

     SimulationEngine.GlobalInstancePort.Insert(boeBot);
}
追加する最後のエンティティは、障害物を表す中空でないボックスです。AddBox メソッドは、SingleShapeEntity 型から箱型のエンティティを作成します。このメソッドのコードを図 11 に示します。
図 11 箱型エンティティの作成
void AddBox(Vector3 position)
{
    Vector3 dimensions =
        new Vector3(0.2f, 0.2f, 0.2f); // meters

    // create simple movable entity, with a single shape
    SingleShapeEntity box = new SingleShapeEntity(
        new BoxShape(
            new BoxShapeProperties(
              100, // mass in kilograms.
              new Pose(), // relative pose
              dimensions)), // dimensions
              position);

    box.State.MassDensity.Mass = 0;
    box.State.MassDensity.Density = 0;

    // Name the entity. All entities must have unique names
    box.State.Name = "box";

    // Insert entity in simulation.
    SimulationEngine.GlobalInstancePort.Insert(box);
}

マニフェストを作成する
多くのサービスは、他のサービスと連携して必要なデータや機能にアクセスします。これらのパートナー サービスは、Partner 属性を使用して実装クラスの最初に参照されます。たとえば、Boe-Bot シミュレーション サービスはシミュレーション エンジン サービスと連携します。このパートナーの宣言を追加するコードは次のようになります。
//Simulation Engine Port
[Partner("Engine",
     Contract = engineproxy.Contract.Identifier,
     CreationPolicy = PartnerCreationPolicy.UseExistingOrCreate)]
private engineproxy.SimulationEnginePort _engineStub =
     new engineproxy.SimulationEnginePort();
DSS サービスは、マニフェストと呼ばれる XML ベースのファイルを使用して、基本サービスに関連付けられたパートナー サービスの一覧を作成します。これにより、基本サービスの読み込み方法や、これらのパートナー サービスとのデータの受け渡し方法を MSRS ランタイムに通知します。マニフェストは XML ベースのファイルであり、テキスト ベース エディタでも編集できますが、MSRS に付属の DSS マニフェスト エディタを使用するのが最適です。DSS マニフェスト エディタは、パートナー サービスをデザイン画面にドラッグ アンド ドロップする機能を持つビジュアル ツールです。DSS マニフェスト エディタを使用する前に、基本サービスをコンパイルする必要があります。コンパイルすると、DSS マニフェスト エディタで使用するアセンブリ ファイルが作成されます。
DSS マニフェスト エディタを読み込むと、使用可能なすべてのサービスの一覧が左側のペインに表示されます。まず、使用可能なサービスの一覧から基本サービスを見つけて、そのインスタンスをデザイン画面上にドラッグする必要があります。Boe-Bot シミュレーション サービスでこの操作を行うと、デザイン画面に SimulatedBoeBot サービスのノードとシミュレーション エンジン サービスのサブ ノードが追加されます。
MSRS インストールの \samples フォルダには複数のサービスが保存されています。その 1 つ、SimpleDashboard は、シミュレーションしたロボットの制御パネルとして使用できます。SimpleDashboard を使用して、作成するロボットに固有の機能を制御する他のサービスを起動できます。たとえば、SimulatedBoeBot サービスのコードはシミュレーション シーンにエンティティ (そのうちの 1 つが Boe-Bot) を設定するだけです。実際にロボットを動かすコードはサービスに含まれていません。このコードは Drive サービスから借用します。
単純なダッシュボードを読み込んで使用するには、サービス一覧から SimpleDashboard のインスタンスを探し、デザイン画面の下部にドラッグしてマニフェストを追加する必要があります。最終結果は図 12 のようになります。最後に、マニフェストを SimulatedBoeBot サービスのプロジェクト ディレクトリに保存し、SimulatedBoeBot.mainfest.xml という名前のマニフェストを上書きします。
図 12 DSS マニフェスト エディタのマニフェストの最終バージョン (クリックすると拡大画像が表示されます)

シミュレーションを実行する
SimulatedBoeBot サービスを実行する準備ができました。サービスを実行するのに必要な操作は、Visual Studio 2005 メニューで [デバッグ]、[実行] の順にクリックするだけです。最初にコマンド プロンプト ウィンドウが表示されます。
サービスが読み込まれると、2 つの追加ウィンドウが表示されます。Simple Dashboard のインターフェイスは、複数のグループ ボックスを表示する Windows フォームです。フォームの右側 (図 13 を参照) に [Remote Node] (リモート ノード) というタイトルのグループ ボックスが表示されます。まず、[Machine] (コンピュータ) テキスト ボックスにローカル コンピュータの名前 (localhost) を入力し、[Connect] (接続) をクリックします。これで SimulatedBoeBot サービスの接続が開始し、ディレクトリ リストボックスに表示された駆動サービスなどのエンティティ パートナー サービスが読み込まれます。
図 13 仮想世界の Boe-Bot の駆動 (クリックすると拡大画像が表示されます)
駆動サービスは、Boe-Bot を制御する車輪に動力を送ります。これにより、コンピュータに接続された仮想ジョイスティック コントロールまたは本物のジョイスティックを使用して、Boe-Bot をシミュレーション上で動かすことができます。まず、リストボックスに表示された SimulatedBoeBot をダブルクリックします。次に、Simple Dashboard フォームの左上にある丸い仮想ジョイスティック コントロールをマウスでドラッグします。
仮想ジョイスティック コントロールに慣れるまでに多少時間がかかるかもしれません (シミュレーションの外観は図 14 を参照)。Boe-Bot を動かして、障害物である箱にぶつかったときの反応を確認します。次に、編集モードでいずれかのエンティティを選択し、プロパティを調整して、エンティティの変化を確認します。しばらくシミュレーション環境をいろいろ試してみて、操作の感覚をつかみます。
図 14 仮想世界でシミュレーションした Boe-Bot の操作 (クリックすると拡大画像が表示されます)

差動駆動なしのエンティティを作成する
この記事では、Boe-Bot という名前の新しいロボット エンティティの作成方法を説明しました。Boe-Bot は差動駆動システムでロボットの車輪を制御するので、DifferentialDriveEntity クラスから派生したエンティティ型を作成することができました。これにより、MSRS のコードを再利用して、この種類の駆動システムを使用するロボットを制御することができるようになりました。
別の駆動システムを使用したロボットをシミュレーションする場合は、最初に、その駆動システムを表すクラスを追加する必要があります。たとえば、ロボットによっては、三輪駆動システムと呼ばれる駆動システムを使用しています。この場合、1 つの前輪を使用してロボットを動かし、2 つの側輪を個別のモーターに接続して、ロボットをどちらかの方向に操縦します。
この種類のシステムをシミュレーションまたは物理世界で使用するには、この種類の駆動システムを受け持つクラスを作成する必要があります。この処理の手順についてはこの記事では説明しませんが、MSRS ではこのようなシナリオも実現可能です。このような拡張性の高さは、MSRS がロボット研究者にとって有益とされる理由の 1 つです。MSRS を拡張して、あらゆる種類のハードウェア プラットフォームをサポートできます。

Sara Morgan は、Office Communications Server の 2007 年度 Microsoft MVP です。最初の著書『Building Intelligent .NET Applications』(Addison-Wesley Publishing、2005 年) は 2005 年に出版されました。複数の Microsoft トレーニング キットを共著し、最近『Programming Microsoft Robotics Studio』(Microsoft press、2008 年) を出版しました。現在は、CoroWare.com のロボティック ソフトウェア エンジニアとしての職務に従事しています。


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

この記事では、次の内容について説明します。
  • カスタム アクティビティを構築する
  • Microsoft Office とのインターフェイス
  • ワークフロー アクティビティにデータを渡す
  • ワークフロー データを Office ドキュメントに抽出する
 この記事では、次のテクノロジを使用しています。
Windows Workflow Foundation、2007 Office System、Visual Basic
目次
全般的なデザイン
ワークフローにデータを取り込む
データを処理する
サンプル ワークフロー
分岐先を決める
支援機能
最後のしあげ
ワークフローは、ビジネス プロセスを自動化するための方法を記述します。たとえば、顧客からの要求に対応したり、保険の請求について処理したり、投資信託で株式を換金したりする際に利用されます。ワークフローが開始されるのは、メールで文書が届いたとき、Web サイトから要求を受け取ったとき、顧客からサポート センターに電話があったときなどです。ワークフローが開始されると、定義済みのタスクが、自動または手動によって実行されるよう割り当てられます。
従来、このような種類の自動化を実現する場合には、一般にモノリシックなワークフロー システムを利用し、それに基づいてビジネス プロセスを構築していました。Windows® Workflow Foundation (WF) を使用すると、コンポーネント ベースのアプローチにより、(ワークフローに合わせてビジネス プロセスを構築するのではなく) ビジネス プロセスをサポートするためのワークフローを構築できます。
ワークフローを必要とするビジネス プロセスでは、多くの場合、プロセス関連ドキュメントの使用や作成が必要になります。たとえば、ワークフロー プロセス中で (ローンや株式の換金などの) 要求が承認または拒否された場合です。プログラム (自動) や証券会社 (手動) によって調査が行われた後で、ドキュメントが必要になる場合もあります。手紙を書いたり、残高を示すスプレッドシートを作成するケースが考えられます。
この記事では、Microsoft® Office アプリケーションを WF と統合する手法について紹介します。InfoPath® フォームや他の Office ドキュメントを使用してデータをキャプチャする方法や、対象となるアクティビティにデータを渡す方法、およびこれらのデータ フィールドを意思決定や新しい Office ドキュメントの作成または記述に使用する方法を説明します。
このような WF と Office の統合に関するいくつかのシナリオを、Visual Studio® 2008 を使用してテストしました。テストしたシナリオには、名前付きフィールドの列挙、それらの内容の抽出、テンプレートに基づくドキュメント作成などが含まれます。このテストにおいて、データの入力、各種ドキュメントの記述、完成したドキュメントの表示などをサポートするカスタム ワークフロー アクティビティを記述できました。また、これらのアクティビティに対して、その見かけを他の WF アクティビティから明確に区別できるようにするデザイナ サポートも追加しました。

全般的なデザイン
ワークフローと Office を統合するためのデザインには、3 つの基本的な要素があります。ワークフローへのデータの取り込み、取り込んだデータによる Office ドキュメントの作成または更新、および出力ドキュメントの格納または返送です。これらのニーズに対応するため、基盤となる Office のドキュメントおよびアプリケーション上でいくつかの個別のワークフロー タスクをサポートできるように、一連のインターフェイスを作成しました。
最初のタスクは、Office ドキュメントのデータ フィールドの名前を取得することです。ここでは "フィールド" という一般的な用語を使用していますが、Microsoft Word の場合、これはブックマークとなります。Microsoft Excel® では名前付き範囲を使用し、InfoPath のフィールドは XML ノードとして表示され、PowerPoint® では図形に名前が付いています (2007 Office system より前には、UI 内で図形の名前を変更する方法はありませんでした。PowerPoint 2007 では、[オブジェクトの選択と表示] ウィンドウを開いて変更できます)。
次のタスクは、これらのフィールドにデータを格納することです。このタスクは、入力辞書として Dictionary(Of String, String) を受け取り、辞書の内容をドキュメントのフィールドと対応付けることで実現しました。また、COM オブジェクトをクリーンアップできるように IDisposable インターフェイスを実装しました。
Office ドキュメントにデータを書き込むアクティビティを実装するために、OfficeFormFill クラスという名前の基本クラスを使用しています。このクラスは、破棄可能なパターンをサポートし、アクティビティに共通の外観を与える共有デザイナを備えています。この基本クラスから、カスタム アクティビティ クラスを派生しました。
ワークフローは、WordFormFill、ExcelFormFill、PowerPointFormFill、および DataEntryActivity という名前のカスタム アクティビティに基づいて構築しました。DataEntryActivity の機能は、単に、さまざまな OfficeFormFill アクティビティで使用される汎用辞書構造に、ドキュメントに固有ではないワークフロー関連変数を導入できるようにすることです。DataEntryActivity は、そのためのテンプレートとして Office ドキュメントを使用し、名前付きフィールドを列挙してユーザーに表示し、内容を入力できるようにします。このテンプレートは、ワークフローで作成および記述される最終的な対象ドキュメントの種類にかかわらず、サポートされている任意のドキュメント クラスから作成できます。

ワークフローにデータを取り込む
Office ドキュメントをワークフローに統合する際に最初に直面した課題の 1 つは、入力ドキュメントに含まれるデータをどのようにしてワークフローから取り出すかということでした。標準的なワークフローのパラダイムは、アクティビティに関連付けられたプロパティの名前があらかじめわかっていることを前提としています。依存関係プロパティは、ワークフローおよび他のアクティビティから見えるように昇格させることができます。ただし、ワークフロー全体のデザインを入力ドキュメント内の特定のフィールドに結び付けることになるため、方法として柔軟性に欠けると考えました。Office の統合の場合、ワークフロー アクティビティは、任意の Office ドキュメントに対する汎用プロキシとして機能します。ドキュメント内のフィールドの名前を事前に決定しておくことは、ドキュメントの種類ごとにカスタム ワークフロー アクティビティを用意する必要があるため、現実的ではありません。
ワークフローに引数が渡される方法、および拡張アクティビティにより渡されるということから判断して、引数は汎用の Dictionary(Of String, Object) として渡されていることがわかります。そして、WF ランタイムは、辞書の要素をワークフロー自体のプロパティにバインドします。Office ドキュメントのフィールドにデータを格納するには、2 つの情報が必要です。1 つはフィールドの名前、もう 1 つは挿入する値です。筆者が過去に採用した一般的な戦略は、ドキュメント内の名前付きフィールドを列挙してから、それらを WF 入力パラメータの辞書に名前で対応付けることでした。辞書内のキーに一致するドキュメント フィールドがある場合は、それを同じ名前の入力パラメータに渡します。ただし、これは筆者が適切と考える処理方法ではありませんでした。なぜなら、使用される可能性のあるドキュメント フィールドのそれぞれに対してワークフローにプロパティを作成する必要があったためです。
ドキュメント内のフィールドに対応するプロパティにアクティビティ内で名前を付けて昇格させる代わりに、汎用の Dictionary(Of String, String) を使用してこれらを渡すことにしました。このパラメータに Fields という名前を付け、各ワークフロー アクティビティで使用します。このキーをフィールド名の対応付けに使用し、値をフィールド内容の格納に使用します。次に、この Fields 辞書を、ワークフローに渡されるパラメータのエントリの 1 つとして渡します。これで、少なくとも Office の統合に関する範囲では、Dictionary 内の辞書に実パラメータが含まれることになります (図 1 を参照)。
図 1 ワークフローにフィールドを渡す
Private WithEvents workflowRuntime As WorkflowRuntime = Nothing
Private workflowInstance As WorkflowInstance = Nothing
Private WithEvents waitHandle As New AutoResetEvent(False)
Public Sub main()
   Dim WorkflowType As Type = GetType(Workflow4)
   Dim Params As New Dictionary(Of String, Object)
   Dim Fields As New Dictionary(Of String, String)
   Params.Add("Fields", Fields)
   ' Start the workflow
   workflowRuntime = New WorkflowRuntime
   workflowInstance = _
      workflowRuntime.CreateWorkflow(WorkflowType, Params)
   workflowInstance.Start()
   waitHandle.WaitOne()
筆者は、すべてのワークフロー アクティビティを幅広く利用でき、入力ドキュメントまたはテンプレートを指定する方法を複数から選べるのが望ましいと考えました。それを実現するために、すべてのアクティビティが共通して InputDocument パラメータを備えています。これは依存関係プロパティであるため、ワークフローでの必要性に応じて昇格させることができます。InputDocument パラメータは、入力ドキュメントまたはテンプレートへのパスを保持します。ただし、コードでは、アクティビティの名前と一致する名前を持つ Field パラメータを使用することもできます。Field パラメータには、対象となる Office アプリケーションに適したドキュメントまたはテンプレートへのパスが含まれている必要があります。
実際のワークフローには必ず、入力データのソースが存在します。ここでは説明のために、DataEntry アクティビティを使用しました。このアクティビティは、サポートされている任意の種類の Office ドキュメントからテンプレート (フィールドおよび既定値) を抽出できます。そして、ユーザーが対象アクティビティを指定するための入力フォームを表示します。これには、特定の OfficeFormFill アクティビティ、または複合アクティビティを使用できます。例では、IfElseActivity を使用しています。コードで事前にわかっている必要があるのは、すべての OfficeFormFill アクティビティで使用される汎用の Fields プロパティだけです。

データを処理する
前述のとおり、各種の Office ドキュメントには独自の名前付きフィールドがあります。OfficeFormFill から派生したアクティビティは、それぞれ特定のドキュメントの種類をサポートするように記述されています。複数のドキュメントの種類を参照する関数を結合することは確かに可能ですが (DataEntryActivity もその一例)、これには 1 つの注意点があります。コード内で Office プライマリ相互運用機能アセンブリ (PIA) の 1 つを参照していて、コードの最終的な展開先であるシステムにその PIA が存在しなかった場合、実行パスでその特定の PIA を使用していなくても、例外が発生する可能性があります。たとえば、存在しないコンポーネントをバイパスするように見える Select Case または If ステートメントでも、例外が発生します。それが、Office コンポーネントへの呼び出しを分離する理由です。結合されたアクティビティを作成する場合は、必ずその分離を保持してください。もちろん、考えられるすべての使用環境でアプリケーションをテストする必要があります。
各種の Office ドキュメントをサポートするアクティビティは、すべて同じパターンに従います。InputDocument プロパティがある場合は、これをドキュメントまたはテンプレートのパスとして使用します。InputDocument プロパティが null である場合、アクティビティは Fields プロパティを調べて、アクティビティの名前に一致するキーを探します。一致するキーが見つかった場合は、アクティビティで処理するドキュメントの種類と一致するサフィックスを持つパスがキーに含まれているかどうかを確認します。これらの条件が一致した場合には、InputDocument プロパティをこの値に設定します。
Fields コレクションの一致するエントリをそれぞれ使用して、ドキュメント内の対応するフィールドに値を格納します。その結果が出力ドキュメントに格納されます。これは、対応する依存関係プロパティ (OutputDocument) として渡されるか、または Output KeyValuePair エントリとして Fields コレクション内にあります。いずれの場合も、出力ドキュメントにサフィックスがなければ、適切な既定のサフィックスが付加されます。これにより、同じ値を使用して異なるドキュメントの種類を作成したり、異なる種類の複数のドキュメントを作成できるようになります。
出力ドキュメントは、指定されたパスに格納されます。ほとんどのワークフロー環境では、ネットワーク共有または SharePoint® ドキュメント ライブラリが格納先です。ここでは単純にするために、ローカル パスを使用しています。
また、各アクティビティには Visible という名前のフィールドがあり、データを格納するときにドキュメントが表示されるようオプションで選択できます。Word ドキュメントにデータを格納するコードを図 2 に示します。特に次の行に注意してください。
WordFiller = New WordInterface(InputDocument, Fields)
図 2 Word ドキュメントにデータを格納する
<STAThread()> Protected Overrides Function Execute _
  (ByVal executionContext As ActivityExecutionContext) _
  As ActivityExecutionStatus

  Dim Status As ActivityExecutionStatus

  ' Open the target document or template
  If String.IsNullOrEmpty(Me.InputDocument) Then
    If Me.Fields _
      IsNot Nothing AndAlso Me.Fields.ContainsKey(Me.Name) Then
      ' Use a field named for *this* activity if it is a document
      Dim NameValue As String = Fields(Me.Name)
      Select Case System.IO.Path.GetExtension(NameValue).ToLowerInvariant
        Case ".doc", ".docx", ".dot", ".dotx"
          InputDocument = NameValue
        Case Else
          Throw New ArgumentException( _
   "Input Document Invalid or Missing")
      End Select
    End If
  End If

  ' Create or open the required document from an input
  ' document or template
  WordFiller = New WordInterface(InputDocument, Fields)

  ' Production workflow may not want to show the document
  If Me.Fields.ContainsKey("Visible") Then
    Boolean.TryParse(Me.Fields("Visible"), WordFiller.Visible)
  End If

  ' Get success or failure from the Word Interface class
  Dim Success As Boolean = WordFiller.FillInDocument()
  WordApp = WordFiller.WordApp

  If Success Then
    ' Find the target output document
    If String.IsNullOrEmpty(Me.OutputDocument) Then
      If Me.Fields.ContainsKey("Output") Then
        Dim NameValue As String = Fields("Output").ToString

        If NameValue.EndsWith(".doc", _
          StringComparison.CurrentCultureIgnoreCase) _
          OrElse NameValue.EndsWith(".docx", _
          StringComparison.CurrentCultureIgnoreCase) Then

          ' Set the document property to the provided name
          Me.OutputDocument = NameValue
        Else 'Force .doc suffix
          Me.OutputDocument = NameValue & ".doc"
        End If
      End If
    End If

    ' Save the output
    WordApp.ActiveDocument.SaveAs(Me.OutputDocument)

    Cleanup()

    Status = ActivityExecutionStatus.Closed
    executionContext.CloseActivity()
  Else
    Cleanup()

    Status = ActivityExecutionStatus.Closed
    executionContext.CancelActivity(Me)
  End If

  Return Status
End Function
この行で、WordInterface クラス インスタンスを構築し、テンプレートとして使用するドキュメントへのパスをフィールド データと共に渡しています。これらは単に、クラスのメソッドで使用する、対応するプロパティに格納されています。
WordInterface クラスは、ターゲット ドキュメントにデータを格納する機能を提供します (図 3 を参照)。入力ドキュメントがテンプレートである場合は、ドキュメントの新しいインスタンスが作成されます。通常のドキュメントである場合は、誤って上書きされるのを防ぐために、読み取り専用として開かれます。このクラスは、基本クラスから Visible プロパティも継承しています。ここに示したコードでは、簡潔にするため、いくつかのエラー チェックを省略しています。
図 3 FillInDocument
Public Overrides Function FillInDocument() As Boolean
  Dim Status As Boolean = False
  _WordApp = New Word.Application
  WordApp.Visible = Visible

  ' Check for template
  Select Case Path.GetExtension(Document).ToLowerInvariant
    Case ".dot", ".dotx"
      _WordDocument = WordApp.Documents.Add(Document)
    Case ".doc", ".docx"
      _WordDocument = WordApp.Documents.Open(FileName:=Document, _
        ReadOnly:=True)
  End Select

  ' Determine dictionary variables to use
  ' based on bookmarks in the document matching Fields entries
  If WordDocument IsNot Nothing _
    AndAlso WordDocument.Bookmarks.Count > 0 Then

    Dim BookMark As Word.Bookmark = Nothing
    For i As Integer = 1 To WordDocument.Bookmarks.Count
      BookMark = WordDocument.Bookmarks(i)
      Dim BookMarkName As String = BookMark.Name
      Dim rng As Word.Range = BookMark.Range
      If Me.Fields.ContainsKey(BookMarkName) Then
        rng.Text = Fields(BookMarkName).ToString
        'This results in the bookmark being lost, it needs to be replaced
        WordApp.ActiveDocument.Bookmarks.Add(BookMarkName, rng)
      Else
        ' Handle special case(s)
        Select Case BookMark.Name
          Case "FullName"
            rng.Text = GetFullName(Fields)
        End Select
      End If
    Next
      Status = True
  Else
    Status = False
  End If

  Return Status
End Function
ここで、FullName という特別な場合のフィールド名を追加しています。ドキュメントにこの名前のフィールドが含まれる場合は、Title、FirstName、および LastName という入力フィールドを連結してデータを格納します。すべての Office ドキュメントの種類で同様なニーズがあるため、この関数は、他のいくつかの共通プロパティと共に、基本の OfficeInterface クラスに移動しました。また、OfficeInterface クラスは、Office ドキュメントの操作用に作成された COM オブジェクトが適切に破棄されるように、必要なクリーンアップ ロジックも処理します。
各 OfficeFormFill クラスには、OutputDocument プロパティがあります。このプロパティは、いくつかの方法によって直接設定できます。デザイナ内では、プロパティをワークフロー レベルのパラメータ (他のアクティビティから昇格されたプロパティも含む) または定数値にバインドできます。実行時には、各種の OfficeFormFill がその OutputDocument プロパティを参照して、ドキュメントを保存するパスを確認します。このプロパティが設定されていない場合は、Fields コレクション内から Output という名前のキーを探します。値に適切なサフィックスが付いていれば、それをそのまま使用します。適切なサフィックスが付いていない場合は、サフィックスを付加します。そして、Activity により出力ドキュメントが保存されます。これにより、出力ドキュメントへのパスをどこに配置するかを非常に柔軟に決定できます。サフィックスが省略されるため、任意の種類の OfficeFormFill が同じ値を使用して、対応するそれぞれの形式でドキュメントを保存できます (Word および PowerPoint については、互換モードでのサフィックスを付加しています。Excel には、ドキュメントの種類を区別する Excel8CompatibilityMode というプロパティがあるため、サフィックスを直接決定できます)。

サンプル ワークフロー
図 4 に、統合のしくみを説明するためのサンプル ワークフローを示します。次に、これらの各アクティビティを順番に取り上げ、その内容について説明します。
図 4 ドキュメント作成ワークフロー (クリックすると拡大画像が表示されます)
一番上にある EnterCustomerData は、DataEntryActivity クラスのインスタンスです。これは、DataEntryDocument プロパティに依存します。ここでは、このプロパティは単純にデザイン時に設定されています (図 5 を参照)。このプロパティは、ワークフローを起動したプログラムで設定することもできます。DataEntryActivity は、DataGridView コントロールを持つ Windows フォームを表示します。このコントロールには、DataEntryDocument から抽出した名前付きフィールドによってデータが格納されます。また、このドキュメントへのパスも表示されます。ユーザーはフィールド内の値を追加または変更できます。
図 5 EnterCustomerData アクティビティに対するバインド (クリックすると拡大画像が表示されます)
DataEntryDocument には、サポートされている任意の種類の Office ドキュメントを使用できます。この例では、InfoPath ドキュメント テンプレートを使用しています。ドキュメントからフィールドを抽出するために、適切な OfficeInterface クラスを呼び出します。このクラスは、ターゲット ドキュメントを適切なアプリケーションに読み込み、フィールド (および存在する場合はその内容) を列挙します。ここでは、既定で設定されているチェック ボックスを使用して、PowerPoint の図形の既定の名前を除外します。これらは TextBox 1 のような名前です。DataEntryActivityForm のコードには、正規表現に基づくフィールド名を除外するルーチンが含まれています。これを使用して、DataGridView にこれらの値および名前を含める操作をスキップしています。
DataEntryActivity によって提供されるフィールドの 1 つに、TargetActivity があります。これは、入力ドキュメントから収集されたフィールドによって Fields プロパティにデータを格納する、対象アクティビティの名前です。TargetActivity により、DataEntryActivity は、OfficeFormFill から派生された任意のアクティビティまたは任意の CompositeActivity をターゲットとすることができます。この記事では、TargetActivity フィールドに CustomerScenario のデータが事前に格納されています。
ターゲット アクティビティには、DataEntryActivity からデータが渡されます。ターゲットが複合アクティビティのとき、このプロセスでは場合により、アクティビティ ツリーをたどって適切な Fields プロパティを見つける必要があります。この例では、名前または必要なプロパティに基づいてアクティビティを検索するルーチンを含む WorkflowUtilities クラスを作成しました。

分岐先を決める
CustomerScenario アクティビティは、IfElseActivity (CompositeActivity から派生) の 1 つです。IfElseActivity 型のアクティビティは、True の戻り値が見つかるまで、各分岐を順番に実行します。いずれかの分岐が True を返した場合、他の分岐は実行されません。
CompositeActivity の構造は、Workflow4.xoml という名前の XAML (Extensible Application Markup Language) ファイルに保持され、そのロジックは Workflow4.xoml.vb という名前のコード ファイルに保持されます。CustomerScenario アクティビティの最初の部分を図 6 に示します。
図 6 CustomerScenario からの抜粋
<IfElseActivity x:Name="CustomerScenario">
  <IfElseBranchActivity x:Name="ProcessApproval">
    <IfElseBranchActivity.Condition>
    <CodeCondition Condition="ifElseBranchActivity1Condition" />
    </IfElseBranchActivity.Condition>
    <CodeActivity x:Name="codeActivity1" ExecuteCode=
      "{ActivityBind Workflow4,Path=codeActivity1_ExecuteCode1}" />
    <ns1:WordFormFill Description="Fill in fields in a Word document"
      x:Name="WriteCustomerLetter" Fields="{x:Null}"
      OutputDocument="{x:Null}"
      InputDocument=" C:\MSDN Workflow\Templates\Customer Letter.doc" />
</IfElseBranchActivity>
分岐のコードで決定を処理するために、各分岐に渡されるイベント引数の Result プロパティに戻り値が設定されます。アクティビティに渡される Status フィールドの値に基づいて、特定の分岐を続行するかどうかがコード内で決定されます。ProcessApproval というラベルの付いた分岐は、値 Approved に対して動作します。ProcessDisapproval 分岐は、値 Disapproved に対して動作します。他の値はすべて、ProcessUndecided 分岐によって処理されます。図 7 に、ProcessApproval 分岐で実行されるコードを示します。
図 7 ProcessApproval
Public Sub ifElseBranchActivity1Condition(ByVal sender As Object, _
  ByVal e As ConditionalEventArgs)
  ' Look for status matching desired value for *this* branch
  If Me.Fields.ContainsKey("Status") AndAlso _
    Me.Fields("Status") = "Approved" Then

    ' Look for a WordFormFill activity as a child to *this* branch
    Dim target As Activity = _
      WorkflowUtilities.FindActivityByType( _
      GetType(WordFormActivity.WordFormFill), _
      (DirectCast(sender, Activity)))

    If target IsNot Nothing Then
      Dim TargetActivity As WordFormActivity.WordFormFill = _
        DirectCast(target, WordFormActivity.WordFormFill)
      TargetActivity.Fields = Me.Fields
      ' Set input document by using field named for target activity
      TargetActivity.Fields.Add(TargetActivity.Name, Me.InputDocument)
      e.Result = True

    Else
      e.Result = False
    End If

  Else
    e.Result = False
  End If

End Sub
他の分岐も同様ですが、それらのデータは PowerPointFormFill 型または ExcelFormFill 型のアクティビティに対して渡します。実際のビジネス プロセスでは、別の種類の Office ドキュメントに対してインターフェイスを行う代わりに、単に異なる Word テンプレートを使用して顧客への手紙などを作成する場合があります。
この分岐で実行される WordFormFill インスタンスでは、図 8 に示すように、入力 (テンプレート) ドキュメントがデザイン時に設定されます。最初は Fields プロパティが null であることに注意してください。これは、このプロパティがプログラムで設定されるためです。
図 8 WriteCustomerLetter プロパティ (クリックすると拡大画像が表示されます)

支援機能
このワークフロー例で使用されているアプローチの一部は、アクティビティが他のアクティビティを見つける能力に依存しています。したがって、アクティビティは、主要なワークフローの種類 (シーケンシャル、ステート マシン、およびルール ドリブン) を含む、CompositeActivity の子アクティビティである必要があります。つまり、重要なワークフローはすべて、アクティビティ間にこのような種類の関係を持っています。
WorkFlowUtilities クラスは、アクティビティのツリーから名前または種類によって子孫アクティビティを検索する方法を提供します。また、ツリーを上にたどって、特定のプロパティを持つ先祖アクティビティを探す機能も提供します。
これらのユーティリティ メソッドは、パラメータを渡すターゲット アクティビティを見つけたり、入力パラメータのソースを見つけたりするのに役立ちます。したがって、たとえば、DataEntryActivity に対して Fields コレクションで TargetActivity パラメータが渡された場合、自身のコンテナから開始してアクティビティ ツリー内の任意の場所で目的のアクティビティを見つけることができます。
Dim EntryActivity As Activity = Nothing
EntryActivity = WorkflowUtilities.FindActivityByName( _
  TargetActivityName, Me.Parent)

最後のしあげ
最後に、アクティビティの完了を宣言します。Status フィールドで選択されたアクティビティが、指定された場所に出力ドキュメントを作成します。ここから、他のアクティビティがそのドキュメントを使用して他の処理を行うことができます。
ここでは、ワークフローと Office クライアント アプリケーションとのインターフェイスに関する基本的なデザイン アプローチの概要を述べました。オブジェクト指向デザインの原則に従うことで、さまざまな種類の同様なニーズを満足する、再利用可能なワークフロー アクティビティおよびサポート クラスを作成できます。

Rick Spiewak は、The MITRE Corporation 社でリード ソフトウェア システム エンジニアを務めています。現在、米空軍電子システム センターでミッション プランニング業務に従事しています。1993 年から Visual Basic の開発作業、2002 年から Microsoft .NET Framework の開発作業に携わり、Visual Studio .NET 2003 のベータ テスタも務めました。


GUI ライブラリ
Windows フォーム アプリケーションの簡潔さをネイティブ アプリケーションで実現する
John Torjo

この記事では、次の内容について説明します。
  • GUI プログラミングの問題
  • ウィンドウ オブジェクトの作成
  • イベントおよび通知の処理
  • フォームとコントロール
 この記事では、次のテクノロジを使用しています。
Win32 API、C++
目次
ネイティブで移植可能
windows.h がない
各ウィンドウの処理
簡単なコード
コントロールとフォーム
フォームのプログラミング
フォームの処理
古い ID が不要
イベントおよび通知
メニューやショートカットなど
タブ コントロールとフォーム
サイズの変更
Visual Studio 2005 との統合
動作の実装
C++ の GUI プログラミングの問題は、低レベルのライブラリが多く、プログラマの負担が大きいということです。C に類似した構造体に依存するか、複雑さを残したままのラッパー クラスを使用します。イベントのプログラミングも単純とはいえず、基本となる WM_ メッセージに関する知識が必要です。
この記事では、クライアント プログラマが GUI アプリケーションを処理するための高レベル インターフェイスとして筆者が作成した C++ ライブラリ、eGUI++ について説明します。このライブラリは、WM_ メッセージの知識がなくても使えるようにしただけのものですが、それだけで複雑さが隠蔽され、イベント プログラミングがきわめて単純になります。C に類似した元の構造体を処理する必要はなく、常にクラスを処理します。概して、eGUI++ のクライアント コードは解読しやすく、記述も単純です。
eGUI++ は Windows® 専用の機能です。クロスプラットフォーム GUI アプリケーションは、サイズが小さいか、単純なテスト フレームワーク、プロトタイプ、または教育目的の場合以外にはあまり信頼性がありません。さらに言えば、基本 OS の機能を利用する方が信頼性が高いのは確実です。Windows XP や Windows Vista® には、たくさんの機能があります。

ネイティブで移植可能
CLR コードをお探しなら、既にマネージ C++ があります。マネージ C++ は優れたプラットフォームなので、改善する必要はありません。Windows 2000 以降のオペレーティング システムに対応するネイティブ Windows コードを生成するための優れたライブラリをお探しなら、このコラムを最後まで読んでください。これから、使い方が簡単で、ターゲット OS の機能を活用したライブラリを紹介します。Microsoft® .NET Framework も不要です。作成するコードは C++ に類似しています。また、作成するコードは Visual C++ コンパイラ固有ではありません。g++ (GNU C++ コンパイラ) 4.1 でもコンパイルできます。基本的に、Win32® API をラップする場合、移植可能なコードの作成を妨げる要因はありません。
とはいえ、複雑な GUI の場合、Visual Studio® 2005 または Visual Studio 2008 Express エディションなどの優れた IDE が必要になります。筆者は、自作のライブラリを Visual Studio 2005 Express 以降と統合し、GUI がより使いやすくなるように調整しました。特に注目したのは、新しい GUI クラスを作成したり、既存の GUI クラスを拡張する場合に、IDE を最大限に活用するコード補完機能です。
筆者は GUI アプリケーションの作成を楽しくしたいと考え、eGUI++ 作成の目標を、解読しやすく、作成が簡単な GUI コードを作成することに定めました。たとえば、可能な限り、コード補完機能を実装しました。GUI プログラミングは安全になり (エラーが発生した場合は可能な限りコンパイル時に捕捉し、捕捉できなかった場合はランタイム例外をスローします)、eGUI++ はリソース エディタとの組み合わせが容易です (リソース エディタの Visual Studio 2005 以降のバージョンと連携しています)。

windows.h がない
windows.h をインクルードする場合の主な問題は、エラーが発生しやすいということです。発生しないイベントの監視をやめる理由は何でしょうか。たとえば、ボタン クラスがキーボード イベントを待機しても、このイベントは発生しません。
そもそも windows.h が必要な理由があるでしょうか。この場合、通常の C++ クラスを使用して C++ で Windows アプリケーションを作成できるようにする必要があります。そうすれば windows.h の内部を意識する必要がありません。WM_LBUTTONDBLCLK や WM_LBUTTONUP などの長いイベント名や、LPNMITEMACTIVATE や NMHDR などのあやふやな C 構造体を知らなくても済みます。C のようなキャストも不要になります。優れた C++ GUI ライブラリの主な機能は、Win32 API を抽象化し、クラスで処理できるようにすることです。
図 1 のような元の C 構造体を処理するのはもうたくさんだと思いませんか。そう思った筆者は、次のようなコードを作成しました。
wnd<rebar> w = new_(parent);
rebar::item i(rebar::item::color | rebar::item::text);
w->add(i);
図 1 古いウィンドウ コードの改良
// The old way
hwndRB = CreateWindowEx(WS_EX_TOOLWINDOW,
  REBARCLASSNAME, NULL,
  WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|
  WS_CLIPCHILDREN|RBS_VARHEIGHT|
  CCS_NODIVIDER,
  0,0,0,0, hwndOwner, NULL, g_hinst, NULL);
...
rbi.cbSize = sizeof(REBARINFO);
rbBand.cbSize = sizeof(REBARBANDINFO);
rbBand.fMask  = RBBIM_COLORS | RBBIM_TEXT |
  RBBIM_BACKGROUND;
rbBand.fStyle = RBBS_CHILDEDGE;
ご覧のように、eGUI++ は複雑な処理を隠蔽しています。開発者は C++ クラスのみを使用し、複雑な API 関数 (CreateWindowEx など)、定数名 (WS_* 定数)、複雑な C 構造体 (REBARPARAMINFO など) を頭に入れる必要がありません。
eGUI++ は Windows 2000 以降を対象にしています。既定では、Windows XP SP2 を対象にしていますが、より機能の少ない、またはより機能の多い別の OS を対象に選択することもできます。別の OS を対象にする場合は、eGUI++ ヘッダーをインクルードする前に、#define EGUI_OS で対象 OS の定数を指定します (図 2 を参照)。
図 2 OS の指定
// code from eGUI++
struct os {
 typedef enum type {
 win_2k,
 win_2k_sp4,
 win_xp,
 win_xp_sp2,
 win_vista
 };
};

#ifndef EGUI_OS
#define EGUI_OS os::win_xp_sp2
#endif
コード内の #ifdefs の数が少ないため、はるかに読みやすくなります。代わりに、OS 固有のプロパティが指定されているのがおわかりでしょう。
property<int,os::win_xp> some_prop;
古い OS をターゲットにして上記のプロパティを使用すると、コンパイル時エラーなどが発生します。

各ウィンドウの処理
通常、オブジェクトの生存期間を決めるのはプログラマです。ところが、GUI プログラミングの重大な問題は、ビジュアル ウィンドウとウィンドウ オブジェクトとの間に差異があり、ウィンドウを閉じるタイミングを決めるのはユーザーだということです。コード内では、ユーザーによって破棄されたウィンドウのウィンドウ オブジェクトへの有効なポインタまたは参照を持つことができます。そのため、オブジェクト インスタンスを表す画面上のウィンドウと画面上のウィンドウを表すオブジェクト インスタンスの 1 対 1 対応を維持することは困難です。このようなシナリオの 1 つの明確な制限は、スコープの終了時に破棄されるローカル ウィンドウ インスタンスを保持できないということです。
{
form f(...);
f.show();
...
}
画面にフォーム f を表示するとします。f のスコープが終了するとどうなるでしょうか。この場合の選択肢は、画面上のフォームを破棄するか、対応する C++ インスタンスを破棄して画面上のフォームをそのままにしておくかのどちらかですが、どちらの方法も適切ではありません。前者の場合、フォームが見えなくなったユーザーが混乱します。後者の場合、画面上にフォームは残っていますが、対応する C++ インスタンスが破棄されているため、イベントに応答しません。また、f がスコープ内にあるときにユーザーが画面上のフォームを既に閉じている可能性もあります。この場合、画面上に存在しないオブジェクトを操作することになります。
この問題の解決策は、常に (間接的な) ポインタを使用してウィンドウにアクセスすることです。ユーザーが画面上のウィンドウを閉じると、対応する C++ インスタンスに無効のマークが付けられ、そのウィンドウにアクセスしようとすると例外がスローされます。次のように、ウィンドウの有効/無効はいつでも確認可能であり、プログラマがウィンドウを破棄することもできます。
wnd<> w = ...;
// is window valid?
if ( is_valid(w) ) w->do_something();
// is window valid?
if ( w) w->do_something();

// destroy the window
delete_(w);
画面上の各ウィンドウに対応する C++ インスタンスが存在するため、wnd<> テンプレート クラスを使用してウィンドウを処理します。このテンプレート クラスはウィンドウへの共有 (参照カウント対象) ポインタを表します。wnd<> クラスにはウィンドウの種類を指定するオプション引数があります。これは基本的に予期されるとおりです。既定では window_base ですが、テキスト、ラベル、Rebar、エディットなどのウィンドウ クラスを指定することもできます。図 3 に、ウィンドウ オブジェクトの使い方とウィンドウ オブジェクト間のキャストの例を示します。
図 3 ウィンドウ オブジェクトの作成とキャスト
// when constructing a window, you can specify its type
wnd<> w = new_<form>(parent);
w->bg_color( rgb(0,0,0));

// when constructing a window, if you don't specify its type,
// it will guess it, based on who you assign it to
wnd<button> b = new_(w, rect(10,10,200,20) );
b->events.click += &my_func;

// destroying a window
delete_(b);

// casting - if it fails, it throws
wnd<form> f = wnd_cast(w);

// casting - if it fails, returns null
if ( wnd<edit> e = try_wnd_cast(e) )
  e->text = "not nullio";
ウィンドウを作成する場合は new_ 関数を使用し、削除する場合は delete_ 関数を使用します。ここでも、ウィンドウを閉じるのは通常はユーザーです。また、X 型 (既定は window_base) のウィンドウが Y 型であるかどうかも確認するには、キャストを使用します。キャストは常に明示的です。キャストには、失敗時に例外をスローする wnd_cast と、失敗時に null ウィンドウを返す try_wnd_cast の 2 種類があります。
eGUI++ クラスの開発時には、基本クラスから派生すると共に、サイズ変更やスキン化などの追加動作も継承すると便利な場合があります。このような場合、複数の再利用可能な動作クラスを作成し、追加で派生することができます。

簡単なコード
作成および解読が容易な GUI コードを目にすると気分爽快になります。筆者は、あらゆる裏技を駆使して可能な限りコード補完機能を活用できるようにしました。大規模な GUI ライブラリを使用するときによく忘れがちなものにプロパティ名、イベント名、フラグなどがあります。サンプル コードではこれらにも対処しています。ドキュメントには Doxygen を使用しました。これは優れたシステムです。使えば使うほど愛着が湧きます。
ドキュメントの参照は容易です。プロパティ名の場合、図 4 のように「w->」だけを入力すれば、メソッド名とプロパティ名が表示されます。プロパティはメンバ変数として表示されるため、区別が容易です。イベント名の場合、クラスで処理可能なイベントを簡単に憶えられます。スコープ演算子の後に「クラス名::ev::」と入力するだけで、コード補完機能によって該当するイベントにジャンプして表示します。とはいえ、eGUI++ の真価が発揮されるのは、フラグの処理です。フラグの組み合わせになり得る各プロパティについて選択可能なフラグ オプションを検索する場合は、フラグ プロパティに "." を付加すれば、コード補完機能が補助してくれます。おまけとして、プロパティに演算子オーバーロードを追加しました。その結果、次のコードは有効です。
w->text = "hello";
w->text += " world";
w->style |= w->style.tiled;
図 4 コード補完機能
また、プログラマが使用できるコントロール一覧の内容を忘れたときのために、egui::ctrl という名前の名前空間を追加しました。

コントロールとフォーム
Win32 GUI プログラミングの経験があるプログラマにとっては、ダイアログはお馴染みです。API でのダイアログの作成 (::CreateDialog) は、ウィンドウの作成 (::CreateWindow[Ex]) とは方法が異なることもご存知でしょう。プログラマは、シグネチャがまったく異なる 2 つの複雑な関数の使い方を覚える必要はありません。eGUI++ ではどちらもウィンドウ型として扱い、ウィンドウの作成方法は 1 つです (new_ 関数を使用)。
種類の異なるウィンドウを扱う場合は、名前ダイアログよりも名前フォームを使用した方が表現力が高まります。名前フォームは他のコントロールを表示するウィンドウを記述したものであり、表示される各コントロールがデータを保持しています。ダイアログとフォームのどちらを使用してもかまいませんが、筆者はフォームの方が好みです。実際に表示されるコードは次のとおりです。
typedef form dialog;
概念的には、ウィンドウの種類はコントロールとフォームの 2 種類のみです。コントロールは、ユーザーに変更を許可することが可能なデータを表示するウィンドウです。すべてのコントロール クラスは control クラスから派生します。フォームは、1 つ以上のコントロールとその固有のロジックの一部 (一部のデータの操作を許可するロジックなど) をホストするウィンドウです。
各ウィンドウ型で使用可能な機能は、ウィンドウの動作によって異なります。たとえば、フォームでは子コントロールを列挙できますが、コントロールでは子コントロールを列挙できません。このように、コード エラーが発生しにくくなっています。また、通常はリソース エディタを使用してフォームにコントロールを配置済みなため、コントロールを作成する必要はほとんどありません。
フォームにもモーダル ダイアログとメッセージ ボックスの 2 種類があります。モーダル ダイアログを作成するには、フォームの作成時に form::style::modal を追加するだけです。メッセージ ボックスを作成するには、次のように msg_box<> 関数を使用し、ボタンをテンプレート引数として指定します。
if ( msg_box<mb::ok | mb::cancel>("q") == mb::ok)
    std::cout << "ok pressed";
また、msg_box<> はボタンの組み合わせが有効かどうかをコンパイル時に判断します。
// ok
   msg_box<mb::yes | mb::no>("q");
   // compile-time error
   msg_box<mb::ok | mb::yes>("q");

フォームのプログラミング
繰り返しになりますが、フォームのことを Win32 API ではダイアログと呼びます。Windows フォームの場合、フォームのプログラミングが優れていることは実証済みです。各フォームになんらかのコントロールがあり、フォームごとに 1 つのタスクを解決します。旧来の複雑なシングル ドキュメント インターフェイス (SDI) やマルチ ドキュメント インターフェイス (MDI) の代わりに、タブを使用してコントロールや他のフォームをホストできます。そのため、CFrameWnd や CMDIChildWnd に類するものを目にすることはありません。必要ないからです。1 つのフォーム上で複数のフォームをホストする必要がある場合は tab_form クラスを使用します。このクラスでは、それぞれ固有のタブ上に子フォームを追加できます。

フォームの処理
ウィザードは好みではないのですが、いくつかのウィザードを使ってプログラミング作業が楽になることも確かにときどきあります。そこで、フォームを作成するための New Class Wizard を作成しました。クラス ビューで [Add Class] を選択し、[Categories] で [eGUI] を選択します。左側で eGUI フォームを選択し、[Add] をクリックして、クラス名を指定します。それで終わりです (図 5)。ウィザードは、eGUI++ の内部で保持するヘッダー ファイル <ダイアログ名>.h、ソース ファイル <ダイアログ名>.cpp、追加ヘッダー ファイル <ダイアログ名>_form_resource.h を作成します。
図 5 Add Class (画像をクリックすると拡大表示されます)
最後のヘッダー ファイルにはフォームで使用するすべてのコントロールの名前が格納されます。そのため、MFC のようにデータのやり取りに使用するコントロール変数を追加作成する必要がありません。コントロールを直接使用します。2 つのエディット ボックス ([User Name] (ユーザー名) と [Password] (パスワード)) と 2 つのボタン ([OK] と [Cancel] (キャンセル)) を持つ図 6 のようなログイン ダイアログがあるとしましょう。
図 6 エディット ボックスとボタン
次のファイルが自動生成されます。
// login.h
#pragma once
#include "login_form_resource.h"
struct login : form,
 private form_resource::login {};

// login.cpp
#include "stdafx.h"
#include "login.h"
コードはごく単純です。"enum {IDD = ... }" のようなウィザード スタイルのコードやメッセージ マップはありません。不要であれば、カスタム コンストラクタを指定する必要もありません。既定の設定を使用すれば十分です。
ログイン クラスは form_resource::login からプライベートに派生し、login_form_resource.h (このファイルは eGUI++ ライブラリで保持されます) に実装されます。form_resource::login クラスにはフォームのコントロールに関する情報 (コントロールの名前、種類、コントロールからの通知を捕捉できるかどうか) が格納されます。クラスのメンバ データは通常プライベートですが、フォームでも同様で、フォーム上のコントロールは通常プライベートにする必要があります。
今度は、次のような form_resource::login が生成されます。
// login_form_resource.h
#pragma once
struct form_resource::login {
 // ... (code to allow
 // handling of notifications)
 wnd<edit> username;
 wnd<edit> passw;
 wnd<button> ok, cancel;
};
これでフォームのコントロールが操作しやすくなります。パスワードが "secretword" であることを確認するとします。
void login::on_button_click(ev::button_click &, ok_) {
 if ( passw->text == "secretword")
 { pass_ok = true; visible = false; }
}
ご覧のように、Visual Basic® の場合と同様、フォームを非表示にするには、visible プロパティを false に設定するだけです。

古い ID が不要
リソース エディタを使ったことがある読者は、ID_、IDD_、IDC_、IDR_、IDS_ など多種のリソース プレフィックスを目にしたことがあるでしょう。プレフィックスはリソース エディタには有効です。ただし、コード内では、記憶する必要も意識する必要もない余分な情報です。eGUI++ アプリケーションではこれらのプレフィックスは完全に無視されるため、まったく意識する必要がありません。
たとえば、前述の名前 (username、passw、ok、cancel) はリソース エディタからのショートカットです。eGUI++ ライブラリはこれらの ID* のプレフィックスを自動的に削除します。元の名前は IDC_username、IDC_passw、IDOK、IDCANCEL だった可能性があります。

イベントおよび通知
前述したように、WM_ メッセージを記憶する必要はありません。とはいえ、イベントを使いこなすのは容易ではありません。イベントは数が多いため、応答可能なイベントを探し出してすぐに応答できるようにする簡単な方法が必要です。フォームのコントロール上で発生したイベントの通知を受けて、コントロールを拡張したり、固有のイベントを追加したりできるようにするための簡単な方法も必要です。
すべてのウィンドウ クラス (コントロールまたはフォーム) はイベントを生成できます。各ウィンドウ クラスにすべてのイベントを捕捉するイベント ハンドラがあります。イベントごとにイベントを処理する関数が定義されます。この関数は仮想関数であり、実装しても何も実行しません。各イベント ハンドラ関数には 1 つの引数 (イベント データ) があります。
既存のコントロールでは、対応するイベント クラスの名前は handle_events::control_name です。既存の各 eGUI++ ウィンドウ クラスの wnd_name は既に handle_events::wnd_name から派生しています。既存のウィンドウ クラスを拡張する場合、常にそのイベントを処理できます (わかりやすくするため、すべてのイベント ハンドラ関数を "on_" で始めます)。たとえば、次のようになります。
struct my_btn : button {
 void on_char(ev::char& e) {
 cout << "typed " << e.ch;
 }
};
他の GUI ライブラリを使用した経験があれば、一見簡単そうでも実はそうでないことをご存知でしょう。既存のコントロールは、イベントではなく通知を送信します。通知は WM_COMMAND/WM_NOTIFY メッセージの形式で送信され、送信先はコントロールではなく、コントロールの親です。一見、これは合理的なように思われます。通知を必要としているのはコントロールの親 (フォーム) です。ところが、これではコントロール クラスの拡張はきわめて困難です。現在のファイル システムを視覚化するツリーが必要な場合はどうでしょう。この場合、アイテムの展開 (TVN_ITEMEXPANDING) などのイベントを捕捉する必要がありますが、これらのイベントはコントロールの親に送信されます。したがって、プログラマが通知をコントロールに渡す手段が必要です。
eGUI++ では、通知はイベントです。通知は常にコントロールに送信され、その後でコントロールの親に送信されます。コントロール クラスを継承によって拡張すると、各通知はそれぞれ異なるイベントに変換されます。たとえば、ユーザーが最初の列を編集しているときに、エディット コントロールではなくコンボボックスを表示するリスト コントロールを作成する場合のコードは次のようになります。
struct list_with_combo : list {
 ...
 void on_begin_label_edit(
  ev::begin_label_edit & e) {
 e.allow_default = false;
 combo->rect(...);
 combo->visible = true;
 }
 wnd<combo_box> combo;
};
イベントを処理するには、次のようにイベント ハンドラ関数をオーバーロードします。
struct my_btn : button {
 void on_char(ev::char& e);
};
ここでは、文字キーが押されたときのイベント (Win32 API を使用する場合は WM_CHAR メッセージ) に応答します。
on_my_event イベント ハンドラ関数のイベント引数は常に ev::my_event 型であることを思い出してください。クラスが処理できるイベントはすべて ev:: 構造体です。「ev::」と入力するだけで、コード補完機能により、クラスで処理可能なすべてのイベントが表示されます (図 7 を参照)。イベント情報を探す最も簡単な方法は、「e.」と入力し、コード補完機能でイベントに関連するすべてのデータを表示することです (図 8)。
図 7 コード補完機能によるイベントの表示
図 8 イベント情報の取得
コントロールのイベントに関するドキュメントを参照する場合は、コントロールを選択してから ev クラスを選択すると、すべてのイベントが表示されます。ライブラリでは、同じイベントを複数のイベント ハンドラに送信できます。たとえば、通知はコントロールに送信された後、コントロールの親にも送信されます。
すべてのイベントに .sender プロパティがあります。これはイベントを送信したコントロールであり、通知の処理、特に通知の送信元を確認する場合に役立ちます。すべてのイベントに .handled プロパティがあります。このプロパティに設定可能な値は handled_partially (既定) と handled_fully の 2 つです。このプロパティを handled_fully に設定することにより、イベントの処理を停止できます。他のイベント ハンドラが残っていても、それらのハンドラは呼び出されません。たとえば、edit クラスを拡張する際にテキストの変更を親に通知しないようにするには、次のようなコードを作成します。
struct independent_edit : edit {
                      void on_change(ev::change &e) {
                      e.handled = handled_fully;
                      }
                     };
上記のように、コントロールの拡張は単純です。フォーム上の通知の処理もこのように単純にする必要があります。通知を処理する場合、送信元 (e.sender) を知っている必要があります。それ以上に、特定のコントロールからの通知を操作する機能が欲しいところです。そこで、イベント ハンドラ関数に、コントロール名の後にアンダースコア (_) を付加した引数を追加します。たとえば、ユーザーが username エディット ボックスに入力した内容を確認するには、次のようなコードを実行します。
void login::on_change(
 edit::ev::change &e, username_) {
 cout << "name=" << e.sender->text;
}
たとえば、米国ドルとユーロとの間で通貨を換算するとします。[EUR] ボックスに値を入力し、何か入力すると、[USD] ボックスの値が更新されます。また、[USD] ボックスに値を入力し、何か入力すると、[EUR] ボックスの値が更新されます (図 9 を参照)。この処理を行うコードは次のようになります。
struct convert : form, form_resource::convert {
 double rate;
 convert() : rate(1.5) {}
 int mul_str(const string& a, double b) { ... }
 void on_change(edit::ev::change&, eur_) {
 usd->text = mul_str ( eur->text, rate); }
 void on_change(edit::ev::change&, usd_) {
 eur->text = mul_str ( usd->text, 1/rate); }
};
図 9 通貨コンバータ
コードの内容は一目瞭然です。mul_str は、string を double に変換して double 型と string 型の乗算を行い、その値と換算レートを乗算します。
前述したとおりにイベントを処理するためには相当の作業が必要です。フォーム上に 3 つのエディット ボックスがあるとします。各エディット ボックスは一連のイベントを生成します。次のように、on_change などの各イベントに対してコントロールごとに 1 つずつオーバーライド可能な関数を生成することができます。
void on_change(edit::ev::change& e, ctrlname_);
または、次のようなオーバーライド関数を 1 つ生成する方法もあります。
void on_change(edit::ev::change& e);
筆者なら前者の方法を選びます。こちらの方がクライアント コードがはるかに単純で、Visual Basic の方法により類似しています。処理内容も簡単にわかります (後者の方法では、イベントの実装でプログラマが e.sender を使用して、イベントを生成したコントロールを手動で問い合わせる必要があります)。
そこで最初の方法を実装することにしました。ただし、この処理の裏には eGUI++ によるリソース エディタの監視という多大な作業が伴っています。新しいコントロールを追加したり、コントロールの名前を変更したりすると、すべての <ダイアログ名>_form_resource.h ファイルが更新されます。form_resource::<ダイアログ名> クラスの各 <ダイアログ名>_form_resource.h ファイルについて、既存のコントロールからのすべての通知を上書きし、上書きされた各通知を送信できるコントロールを探す必要があります。次に、各コントロールについて、上書き可能な別の関数に転送する実装を生成します。例として、2 つのエディット ボックスと 2 つのボタンを持つログイン フォームのコードを図 10 に示します。
図 10 ログイン フォームのコード
struct form_resource::login {
  wnd<edit> name;
  wnd<edit> passw;
  wnd<button> ok, cancel;

  typedef ... ok_;
  typedef ... cancel_;
  typedef ... name_;
  typedef ... passw_;

  virtual void on_change(edit::ev::change& e, name__) {}
  virtual void on_change(edit::ev::change& e, passw__) {}

  virtual void on_change(edit::ev::change& e) {
    if ( e.sender == name) on_change(e, name__());
    else if ( e.sender == passw) on_change(e, passw__());
  }
  // ... same for other edit notifications

  virtual void on_click(button::ev::click & e, ok__) {}
  virtual void on_click(button::ev::click & e, cancel__) {}
  virtual void on_click(button::ev::click & e) {
    if ( e.sender == ok) on_click(e, ok__());
    else if ( e.sender == cancel) on_click(e, cancel__() );
  }
  // ... same for other button notifications
};
new_event<> からの派生によって固有のイベントを作成することもできます。既存のイベントを送信する場合も、自作の固有イベントを送信する場合も、手順は同じです。次のように、send_event 関数を使用します。
struct hover : new_event<hover> {
 int x,y; // position
 hover(int x,int y) : x(x),y(y) {}
};

w->send_event( hover(x,y) );
ライブラリはスレッド セーフです。参考までに付記しておきますが、筆者は、各ウィンドウが持つ m_cs mutex 変数 (これは基本的に CRITICAL_SECTION です) を使用して各メソッドのアクセスがスレッド セーフであるかどうかを確認しています。ウィンドウ クラスを拡張する際は、m_cs 変数を再利用しても、固有の変数を作成してもかまいません。

メニューやショートカットなど
GUI プログラミングの経験があれば、メニュー コマンドを押した場合にも、キーを押した場合にも WM_COMMAND が送信されることをご存知でしょう。そのため、WM_COMMAND を受け取った場合にイベントの送信元がコントロールであるか、メニューであるか、(またはこの場合はキーボード ショートカットであるか) を判別することが困難です。eGUI++ では、メニューをフォーム (ダイアログ) 上に直接配置することでこの問題の一部を解決できます。フォームに送信されたコマンドの送信元がボタンでなければ、送信元はメニューです。
これでメニュー コマンドを処理できます。次はショートカットを見てみましょう。ショートカットの問題は、キー入力はいつでも (たとえば、エディット ボックス内でも) 可能だという点にあります。キーボード ショートカット (アクセラレータ) は最初に直接ウィンドウに送信され、次にウィンドウをホストするフォーム、フォームの親という順に、最上位ウィンドウに達するまで階層の上位に向かってルーティングされます。ショートカットのイベント ハンドラが最初に見つかったところで処理は停止します (特定のショートカットは複数のウィンドウでは処理されません)。
これでツール バーが残ります。ツール バーはメニューおよびショートカットと連携しています。ツール バー ボタンを押すと、イベントはメニュー コマンドに変換され、コマンドの送信元がメニュー、ショートカット、ツール バー ボタンのいずれであるかに関係なく、そのイベントをホストするフォームに直接ルーティングされます。
次のように、メニュー コマンドを処理するフォームを実装するとします。
void on_menu_command( ev::menu&,
 menu::some_menu_id) { ... }
2 つのメニュー コマンド (new_file および open_file) を処理するには、次のようなハンドラを作成します。
void on_menu_command( ev::menu&,
    menu::new_file) { ... }
   void on_menu_command( ev::menu&,
    menu::open_file) { ... }

タブ コントロールとフォーム
タブはよく使われる GUI パラダイムです。タブ コントロールを拡張して、tab_type プロパティに normal または one_dialog_per_tab (この場合、タブ コントロールが他のフォームをホストします) の値を指定できるようにします。後者の場合、次のようにして新しいフォームを追加できます。
tab->add_form<form_type>( new_([args]) );
前述のようなログイン フォームを作成するには、次のようなコードを記述します。
tab->add_form<login>( new_() );
1 つ以上のフォームを追加したら、このタブ フォームに保持可能なタブの数を指定できます。
tab->count = 5;
ここでは、タブの数を 5 つにします。このためには、最後に追加したフォームを必要な数だけ複製します。以前にタブが 1 つしか存在しなかった場合、最初のタブ上のフォームがあと 4 回複製されます。そうです、既存のどのウィンドウでも複製できます。
ここまでは、イベントの侵入型の処理を示してきました。つまり、ウィンドウ クラスを拡張し、最終的にイベントに応答します (または、フォームを実装している場合は通知に応答します)。ところが、場合によっては、相互に関連のない複数のウィンドウに適用する動作を実装する必要があります。
サイズ変更とスキン化を例にとってみましょう。これらの機能は非侵入型の方法 (動作を実装するクラスを作成し、そのクラスから GUI クラスを派生する方法) で実装できます。ただし、この方法ではコードが複雑になり、スキン化の場合などは常に実現可能であるとは限りません。動作を非侵入型の方法で実装すれば、その動作を他のアプリケーションで再利用できるうえ、動作を無効にすることも簡単です。
たとえば、非侵入型のイベント ハンドラ クラスを作成し、このクラスのインスタンスを作成して、登録します。新しいウィンドウを作成すると、ハンドラ インスタンスが通知され、このインスタンスを監視することができるようになります。この場合、次のように、監視の対象にするイベントを手動で指定する必要があります。
// monitor button clicks
struct btn_handler : non_intrusive_handler {
 void on_new_window_create(wnd<> w) {
 if ( wnd<button> b = try_cast(w)) {
  b->events.on_click += mem_fn(&on_click,this);
 }
 }
 void on_click(button::ev::click&) { ... }
};
イベント ハンドラの登録は簡単です。次のコードを実行するだけです。
btn_handler bh;
window_base::add_non_intrusive_handler(bh);

サイズ変更機能を実装する場合は、アプリケーションに応じてさまざまな方法があります。たとえば、新しいフォームのサイズに基づいて各フォームの on_size イベントを上書きし、コントロールの位置を更新することも可能です。ただし、これは作業量が多く、良い方法とは言えません。または、各フォーム上に "a.x = b.x + b.width + 4;" のようなコントロール間の関係を作成することもできます。柔軟性の高い方法ですが、この場合も作業量は多くなります。
あるいは、各フォーム上の各座標に関して、コントロールにサイズ変更可能または移動可能のマークを付ける方法もあります。コントロールに特定の座標上でのサイズ変更可能のマークを付けると、フォームのサイズを変更したときにそのコントロールのサイズが更新されます。コントロールに特定の座標上での移動可能のマークを付けると、フォームのサイズを変更したときにそのコントロールが移動します。多くのアプリケーションにはこれで十分です。この発想は WTL の CResizeWindow から拝借したもので、非侵入型のハンドラを使用して実装しました。図 11 のようなダイアログを想定してみましょう。このダイアログのサイズを変更すると、図 12 のようになります。この処理のコードは次のようになります。
resize(name, axis::x, sizeable);
resize(desc, axis::x | axis::y, sizeable);
resize(ok, axis::x | axis::y, moveable);
resize(cancel, axis::x | axis::y, moveable);
図 11 ダイアログ
図 12 サイズ変更後のダイアログ
GUI の操作が失敗すると必ず例外がトリガされます。このしくみにより、問題が発生したことがわかります。デバッグ モードではアサーション エラーが生成され、プログラムはデバッグ モードに入ります。これは、エラーを警告なしで無視するよりはるかに好ましい動作です。問題が発生していることが視覚的にわかり、バグを探すことができます。

Visual Studio 2005 との統合
Visual Studio は優れた IDE であり、その主なメリットは拡張可能であるという点にあります。eGUI++ はその Visual Studio を利用しています。Visual Studio は New Form Class Wizard を提供するアドインに付属しています。また、Visual Studio には Visual Basic に似たバーがあり、それを使ってフォーム上のコントロールの通知を処理できます。これを行うには、コントロールを選択します。すると、そのコントロールで生成可能な通知の一覧が表示されます。処理済みの通知は太字で表示されます。イベントをクリックすると、まだ一覧に存在しない場合はハンドラが追加されます。たとえば、図 13 のようになります。
図 13 フォーム上のコントロールの通知の処理 (クリックすると拡大画像が表示されます)
内部では、eGUI++ はリソース エディタを監視し、変更が発生するたびに、必要に応じて _form_resource.h ファイルを更新します。これまでに詳しく説明したように、コード補完機能も使用できます。

動作の実装
GUI を構築したら、次に、動作を実装してデータをバインドできるようにします。多くのフォームはデータ収集専用に使われます。初歩的な方法としては、汎用フォーム クラスを実装し、操作するデータを構築時に取得して、フォームのコントロールにバインドします。これで、データを検証するための一連のルールを指定できるようになります。破棄時に、検証ルールが成功した場合、元のデータはコントロールからの値に更新されます。検証ルールが失敗した場合、元のデータは不変です。このように、各新規フォームについて必要な作業は、リソース エディタでフォームを作成し、データの検証に使用する一連のルールを指定することだけです (新規フォーム クラスを作成してロジックを複製する必要はありません)。
今後のことを考えて、標準テンプレート ライブラリ (STL) 配列とコレクションとの差異や、リスト コントロールとツリー コントロールとの差異を埋めることができます。たとえば、employees 配列とリスト コントロールがあるとします。employees 配列とコントロールをバインドするには、次のようにします。
list_ctrl->bind(employees);
この場合、予想されるとおり、リスト コントロールが更新されます。リスト コントロールのセルを変更すると、変更が employees 配列と自動的に同期されます。
eGUI++ の作成目的は、GUI プログラミングが楽しくなるような優れたライブラリを作成することでした。C++ プログラマの読者にはこの目的に共感していただけるものと期待しています。ソースおよびバイナリは torjo.com からダウンロードできます。

John Torjo は、10 年以上 C++ でのプログラミングを経験した今でもこの言語を愛好する C++ プログラマです。ログ記録と GUI を得意とし、難題を課されることを楽しんでいます。難しい課題があれば、彼に電子メールを送ってください。詳細については、John のサイトである torjo.com を参照してください。


編集者のメモ
お気付きですか?
Howard Dierking

今月号をご覧になって、「何かが変わった」と思われるかもしれません。確かにそのとおりです。私たちは、印刷バージョンとオンライン バージョンの両方の MSDN Magazine にいくつかの新しい要素を取り入れるため、過去数か月にわたって懸命に取り組んできました。今月号の「編集者のメモ」では、いつものようにテクノロジに関する話題を取り上げる代わりに、これらの特徴や機能について説明し、読者の皆様に活用していただけるようにしたいと思います。
真っ先に読者の皆様の目に留まるとよいのですが、まず、デザインを新しくしました。目次は従来どおり表紙に掲載されていますが、見出し部分のスペースが若干広くなりました。これにより、全体の内容を把握しやすくなっています。各ページでは、色、フォント、およびその他のグラフィカルな要素を明るくしています。デザインを変更するにあたって目指したのは、印刷バージョンの見た目をシンプルに、そして新しくすると同時に、同じく変更を加えたオンライン バージョンの見た目と視覚的なつながりを持たせることです。
今月号の記事では、インサイトと呼ばれる新しい特別なリンクが 2 つ追加されていることにお気付きでしょう。このタイプのリンクは、機能やテクノロジが "内部で" どのように動作しているかを詳細に示したり、そのテクノロジに関する設計上の決定事項を説明したりするために、マイクロソフトの製品チームが使用します。インサイト リンクは、Dino Esposito の「Cutting Edge」コラムと Jeffrey Richter の「同時実行の問題」コラムでご覧いただけます。
さらに、オンラインで体験できるバーチャル ラボという新しい機能を導入したことをお伝えします。バーチャル ラボを起動すると、分割ペイン ビューが表示されます。一方のペインにはラボ環境が、もう一方のペインには MSDN Magazine の記事が含まれています。バーチャル ラボでは、コードを操作しながら記事を読むことができます。また、その逆も可能です。
初めのうちは、現在お使いのツールセットに含まれていない可能性の高いテクノロジや、構成が難しいテクノロジについての記事に対して、バーチャル ラボをホストします (たとえば、バーチャル ラボは SharePoint の開発を扱った記事に最適です)。今月号では、Charles Petzold の「基礎」コラムに対応するバーチャル ラボを提供しています。このバーチャル ラボでは、ビットマップをアプリケーション内の変更に応答させる RenderTargetBitmap クラスと WriteableBitmap クラスを試すことができます。バーチャル ラボの詳細については、今月号の Charles Petzold のコラムを参照するか、ラボ (go.microsoft.com/fwlink/?LinkId=117105) に直接アクセスしてください。
MSDN Magazine 2008 年 5 月号の SharePoint 展開の自動化に関する Ethan Wilansky の記事に対応するバーチャル ラボも提供しています。お使いのブラウザで go.microsoft.com/fwlink/?LinkId=117106 にアクセスすると、このバーチャル ラボを開始できます。
新しい追加コンテンツが皆様のお役に立てばさいわいです。いつものように、新しい特徴/機能についてのご意見やご感想は、mmeditor@microsoft.com まで電子メールでお送りください。

今月号の記事の執筆に携わったマイクロソフトの技術部門の次の専門家に感謝します。Tim Cahill、Patrick Dussud、Muhammed Abd El Aziz、Joseph Fernando、Steve Fox、Matt Gibbs、Bertrand Le Roy、Ade Miller、Dwayne Need、Dave Reed、Gerhard Schneider、Don Smith、Stephen Toub、Clemens Vasters、および Robert Wlodarczyk


ツールボックス
クロスブラウザ テスト、モック オブジェクト、そして Raymond Chen
Scott Mitchell

目次
ブラウザ互換性テストの向上
注目のブログ
モックを使用した単体テスト
推薦図書

ブラウザ互換性テストの向上
Web 開発者は夜眠るとき、すべての Web ブラウザが HTML 標準に完全準拠する夢を見ます。彼らは、すべての Web サーファーが同じモニタ解像度、色深度、フォント サイズを使用する世界を想像します。ところが、現実はそれとはかなりかけ離れています。Web 開発者は、起きている間、膨大な数の構成で適切に機能する Web アプリケーションを設計しなければなりません。
完全な Web テスト環境の構築には、手間暇がかかり、複数のブラウザ、バージョン、オペレーティング システムをインストールする必要があります。リモート Web ベースのテスト環境は、開発者のこのような負担を取り除いてくれます。
そのためのサービスの 1 つに "Browsershots" があります。URL を指定すると、Browsershots は選択されたブラウザ、バージョン、オペレーティング システムの組み合わせでスクリーンショットを取得し、表示します。組み合わせは 53 とおりあり、さらに画面サイズ、色深度、JavaScript のサポートなどのクライアント設定を指定できます。Browsershots は無償サービスですが、処理する要求の数に応じて 1 分弱～ 30 分以上の待ち時間が生じる場合があります。1 か月あたり 15 ドルで優先処理にサインアップできます。この優先処理を利用すると、あなたの要求がキューの先頭に移動されます。
スクリーンショットは特定のブラウザでの Web サイトの表示を確認するには便利ですが、サイト上でユーザーが操作したときの動作はわかりません。この種のテストを行うには、BrowserCam を使用します。BrowserCam は Browsershots と同様にスクリーンショットを提供しますが、その他に各種オペレーティング システムやブラウザをリモートでアクセスすることもできます。この機能により、コンピュータにログインし、設定を行い、Web サイトを操作できます。
価格 : Browsershots (無償)、BrowserCam (プレミアム サービス、89.95 ドル/月)
browsershots.org
browsercam.com
Browsershots で撮った Microsoft.com (クリックすると拡大画像が表示されます)

注目のブログ
コンピュータをシャットダウンするために [スタート] ボタンをクリックする必要があるのはなぜだろうという疑問を抱いたことはありますか。あるいは、なぜ Windows® オペレーティング システムのデバッガを "ワトソン博士" と呼ぶのかを不思議に思ったことはありますか。コンピュータ ゲームが動作しないという問題が発生して、Windows XP SP2 のベータ版リリースが遅れる危機にさらされたことがありますが、それがどのような問題だったのかをご存知ですか。レジストリを "ハイブ" と呼ぶ理由を知っていますか。Raymond Chen は知っています。
Raymond は 1992 年に Windows 95 のソフトウェア エンジニアとして Microsoft に入社して以来、Windows の歴史的な情報に関する仮想リポジトリの役割を果たしています。ブログ「The Old New Thing」の中で、彼は前述の質問に回答し、その他の多くの Windows 従来バージョンにも取り組んでいます。Raymond は多くの Windows のデザイン決定の現場にいたので、Windows の舞台裏に関する生の情報を持っています。内部情報とドライな機知が満載の Raymond のブログは楽しくて役に立つ読み物です。
ところで、Windows レジストリを "ハイブ" (蜂の巣) と呼ぶのは、Windows NT® の開発者の中に蜂嫌いがいたからだということですが、Raymond は後でこのジョークの報いを受けることになりました。ともかく、ここでは 1 つの目標を念頭に置いた重要な決断が下されています。それは下位互換性を確保するということです。Raymond は、古い 16 ビットの Visual Basic® アプリケーションや人気のある MS-DOS® ゲームが新しいバージョンの Windows でも問題なく動作するように、下位互換性を確保するための多大な努力が費やされた場面を見事に浮き彫りにしています。
Windows の歴史に関するブログ以外に、TechNet Magazine (.com) で評判のコラム「Windows 秘話」も執筆しています。また、Windows プログラマ向けのさまざまなヒントや裏技を定期的に投稿しています。
go.microsoft.com/fwlink/?LinkId=115142
Raymond Chen のブログ : 「The Old New Thing (古くて新しいもの)」(クリックすると拡大画像が表示されます)

モックを使用した単体テスト
優れた単体テストは、簡潔かつ実行が迅速であり、他のテストから独立しています。データベース、Web サービス、構成ファイルなどの外部依存関係のある機能をテストする場合は、これらの目標を達成するための追加手順が必要になります。1 つの選択肢として、各単体テストを実行する前に外部依存関係の状態を記録し、各テストの完了後に状態を元に戻す方法があります。ただし、このような過剰な設定や後処理を行うと単体テストの実行速度が大幅に低下する可能性があるので、重点が単体テストの作成から外部依存関係の状態管理に移ります。
もう 1 つの選択肢として、モック オブジェクトを使用して外部依存関係の呼び出しをシミュレーションする方法があります。単体テストの実行時に、モック オブジェクトは外部依存関係にアクセスするロジックに置き換えられます。テスト対象のコードが操作するのは実際の外部依存関係ではなくモック オブジェクトなので、複雑な設定や後処理のロジックは不要です。また、モック オブジェクトを使用すると、コードと外部依存関係とのやり取りを明示的にテストできます。
複数のオープンソースおよび市販のモック作成用フレームワークが存在し、オブジェクトのモックを作成するための構文要件もさまざまです。一部のフレームワークでは、モックを作成するオブジェクトを指定する文字列をハードコーディングして機能をシミュレーションします。その他に、厳密に型指定されたオブジェクトを使用するフレームワークもあります。このタイプのフレームワークは、実行時よりコンパイル時のエラーの特定に役立ちます。
Typemock Isolator (version 4.2) は、どちらの方法も使用できるモック作成フレームワークです。どちらの方法を使用しても、単体テストでモック オブジェクトを使用する場合のセマンティクスは同じです。モック オブジェクトを定義し、テスト対象コードとのやり取りの方法を指定し、コードを実行して、想定どおりの結果になることを確認します。
Typemock が他の多くのモック作成フレームワークと異なる点は、ツールと統合のサポートです。モック オブジェクトを使用した単体テストでは、どこで実際の外部依存関係の代わりにモック オブジェクトを使用しているかが必ずしも明確ではないため、デバッグが困難になる可能性があります。
このトレース ユーティリティを使用すると、モック オブジェクトがどのように使われているかを確認できるため、この困難が解消されます。このユーティリティは、モックの型と単体テストの実行中に行われたすべての呼び出しを記録して表示します。単体テストのデバッグ時に、Typemock はモックされたメンバの概要を IDE に表示して、モックされているオブジェクトの内容を明示します。また、Typemock を Visual Studio® シェルに統合してシステムの機能を強化し、NAnt および MSBuild を使用した単体テストの自動化をサポートすることもできます。
Typemock は Microsoft® .NET Framework バージョン 1.1 以降をサポートし、無償の Community Edition や、より機能が豊富な Professional Edition および Enterprise Edition を提供しています。
価格 : Community Edition (無償)、Enterprise Edition (449 ドル)
typemock.com
Typemock Trace Utility (クリックすると拡大画像が表示されます)

推薦図書
.NET Framework 2.0 以降のバージョンでは、コア機能を維持したままで新機能が追加されています。このことが、.NET Framework のバージョン 2.0 以降に関する書籍を執筆する著者を悩ませています。それは、新機能だけに重点を置くべきか、それとも新機能とバージョン 2.0 以降のその他の内容も盛り込むべきかという悩みです。
約 1,500 ページに及ぶ『Pro ASP.NET 3.5 in C# 2008』(Apress、2007 年) の共著者 Matthew MacDonald と Mario Szpuszta は明確に後者を選択しています。本書は 33 の章で構成され、単純な Web フォーラムの作成方法、AJAX や Silverlight™ の最新機能の使い方など、幅広いトピックを取り上げています。
Web フォーラム、Web コントロール、状態管理などの ASP.NET の基礎に関する概要から始まり、ADO.NET、LINQ、ASP.NET AJAX、Silverlight などの話題に移行していきます。ASP.NET のほとんどすべての機能が取り上げられていますが、説明はよく使われる機能に重点を置いています。
これまでに Web 開発の経験があり、ASP.NET 3.5 の習得に関心がある中級または上級の開発者には、『Pro ASP.NET 3.5 in C# 2008』をお勧めします。
価格 : 59.99 ドル
apress.com
(クリックすると拡大画像が表示されます)

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

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


CLR 徹底解剖
大きなオブジェクト ヒープの秘密
Maoni Stephens

目次
大きなオブジェクト ヒープと GC
大きなオブジェクトが収集されるタイミング
LOH のパフォーマンスへの影響
LOH のパフォーマンス データを収集する
デバッガを使用する
CLR のガベージ コレクタ (GC) は、オブジェクトを小さなオブジェクトと大きなオブジェクトに分けて扱います。オブジェクトが大きい場合、オブジェクトに関連付けられた属性のいくつかは、オブジェクトが小さい場合に比べて、より重要な意味を持つようになります。たとえば、最適化 (メモリをヒープ上の他の場所にコピーする処理) のコストが高くなります。今月のコラムでは、大きなオブジェクト ヒープを詳細に見ていきます。大きなオブジェクトの定義、その収集方法、大きなオブジェクトがパフォーマンスに与える影響などについて説明します。

大きなオブジェクト ヒープと GC
Microsoft® .NET Framework 1.1 および 2.0 では、オブジェクトのサイズが 85,000 バイト以上の場合に、大きなオブジェクトと見なしています。この数値は、パフォーマンス チューニングの結果として決定されたものです。オブジェクトの割り当てが要求されたときに、オブジェクトのサイズがこのしきい値条件を満たしている場合は、大きなオブジェクト ヒープ上に割り当てられます。これは厳密には何を意味するのでしょうか。それを理解するために、.NET ガベージ コレクタに関する基本的な事項をいくつか説明します。
多くの方は既にご存知と思われますが、.NET ガベージ コレクタは、世代に基づくコレクタです。オブジェクトは、世代 0、世代 1、世代 2 の 3 つの世代に分かれています。この区分は、適切にチューニングされたアプリケーションにおいて、ほとんどのオブジェクトが世代 0 で終了することが期待できるよう設けられています。たとえば、サーバー アプリケーションでは、各要求に関連付けられた割り当ては、その要求の完了後に終了する必要があります。処理中の割り当て要求は、世代 1 に進み、その世代で終了します。本質的に、世代 1 は、短期間存在するオブジェクトの領域と長期間存在するオブジェクトの領域との間でバッファとして機能します。
世代の観点で見ると、大きなオブジェクトは世代 2 に属します。これは、大きなオブジェクトが世代 2 の収集時にだけ収集されるためです。ある世代の収集時には、それより存在期間の短い世代もすべて収集されます。したがって、たとえば、世代 1 のガベージ コレクションが行われるときには、世代 1 と世代 0 の両方が収集されます。また、世代 2 のガベージ コレクションが行われるときには、ヒープ全体が収集されます。この理由により、世代 2 のガベージ コレクションは、フル ガベージ コレクションとも呼ばれます。このコラムでは、フル ガベージ コレクションの代わりに世代 2 ガベージ コレクションという用語を使用しますが、これらは同じ意味です。
世代は、ガベージ コレクタ ヒープの論理的なビューです。物理的には、オブジェクトはマネージ ヒープ セグメント上に存在します。マネージ ヒープ セグメントは、ガベージ コレクタがマネージ コードに代わって OS から (VirtualAlloc の呼び出しによって) 予約するメモリのチャンクです。CLR が読み込まれると、2 つの初期ヒープ セグメントが割り当てられます。1 つは小さなオブジェクト用、もう 1 つは大きなオブジェクト用で、それぞれ小さなオブジェクト ヒープ (SOH)、大きなオブジェクト ヒープ (LOH) と呼ばれます。
割り当て要求は、これらのマネージ ヒープ セグメントのいずれかにマネージ オブジェクトを配置することで満足されます。オブジェクトが 85,000 バイト未満の場合は SOH セグメントに配置され、それ以上の場合は LOH セグメントに配置されます。セグメントに割り当てるオブジェクトが増加していくと、セグメントは (小さいチャンクへと) コミットされます。
SOH の場合、ガベージ コレクションで終了しなかったオブジェクトは次の世代に昇格し、たとえば、世代 0 で残ったオブジェクトは世代 1 のオブジェクトと見なされます。ただし、最も古い世代で終了しなかったオブジェクトは、引き続き最も古い世代と見なされます。つまり、世代 2 で終了しなかったオブジェクトは世代 2 のオブジェクトのままであり、LOH で終了しなかったオブジェクトは LOH オブジェクト (世代 2 で収集) のままです。ユーザー コードでは、世代 0 (小さなオブジェクト) または LOH (大きなオブジェクト) のオブジェクトだけを割り当てることができます。ガベージ コレクタだけが、世代 1 (世代 0 で残ったオブジェクトを昇格) または世代 2 (世代 1 および世代 2 で残ったオブジェクトを昇格) のオブジェクトを "割り当てる" ことができます。
ガベージ コレクションがトリガされると、ガベージ コレクタは、現在存在しているオブジェクトをひととおりトレースし、それらを最適化します。ただし、LOH の場合は最適化のコストが高いため、CLR チームでは、それらを一掃し、終了したオブジェクトの空きリストを作成することに決めました。これらのオブジェクトは、新しい大きなオブジェクトの割り当て要求を満たすために、後で再利用できます。隣接する複数の終了したオブジェクトは、1 つの空きオブジェクトに結合されます。
ここで 1 つ重要な注意点として、LOH は現在は最適化されませんが、将来は最適化されるようになる可能性があります。したがって、大きなオブジェクトを割り当てたときに、そのオブジェクトが移動されないようにするには、オブジェクトを固定する必要があります。
ここに示した図は、説明のために用意したものです。ヒープで何が起こっているかを単純に示すことが目的なので、ここではごくわずかなオブジェクトしか使用していません。実際には、他にもずっと多くのオブジェクトが存在しています。
図 1 に示したシナリオでは、世代 0 の GC で Obj1 と Obj3 が終了した後に世代 1 を形成し、世代 1 の GC で Obj2 と Obj5 が終了した後に世代 2 を形成しています。
図 1 SOH の割り当てとガベージ コレクション (クリックすると拡大画像が表示されます)
図 2 では、Obj1 と Obj2 を終了させた世代 2 のガベージ コレクションの後、Obj1 と Obj2 が占有していたメモリから 1 つの空き領域を形成し、Obj4 に対する割り当て要求を満足させるためにその空き領域を使用しています。最後のオブジェクト Obj3 の後、セグメントの末尾までの領域は、以降の割り当て要求を満足させるために引き続き使用できます。
図 2 LOH の割り当てとガベージ コレクション (クリックすると拡大画像が表示されます)
大きなオブジェクトの割り当て要求を満足させるだけの十分な空き領域がない場合は、まず追加のセグメントを OS から取得することを試みます。それが失敗した場合は、多少の領域を解放できることを期待して、世代 2 のガベージ コレクションをトリガします。
世代 2 のガベージ コレクション中に、有効なオブジェクトが存在していないセグメントを (VirtualFree を呼び出すことで) 解放して OS に返す処理を行っています。最後の有効なオブジェクトからセグメントの末尾までのメモリは、デコミットされます。また、空き領域は引き続きコミットされていますが、リセットされます。つまり、OS はその領域のデータをディスクに書き戻す必要がありません。図 3 では、セグメントを解放して OS に返し (セグメント 2)、残りのセグメント上で、より多くの領域をデコミットしています。新しい大きなオブジェクトの割り当て要求を満足させるために、セグメント末尾のデコミットされた領域を使用する必要がある場合は、メモリを再度コミットします。
図 3 ガベージ コレクション中に LOH で解放される終了セグメント (クリックすると拡大画像が表示されます)
コミット/デコミットの詳細については、VirtualAlloc に関する MSDN® ドキュメント (go.microsoft.com/fwlink/?LinkId=116041) を参照してください。

大きなオブジェクトが収集されるタイミング
大きなオブジェクトがいつ収集されるかを知るために、まず、一般的にガベージ コレクションが行われるタイミングについて説明します。ガベージ コレクションは、次のいずれかの条件が満たされたときに行われます。
割り当てが世代 0 または大きなオブジェクトのしきい値を超えた場合 ほとんどの GC は、マネージ ヒープ上の割り当てによって発生します (これが最も一般的なケースです)。
System.GC.Collect が呼び出された場合 世代 2 に対して GC.Collect が呼び出されると (GC.Collect に引数を渡さないか、または GC.MaxGeneration を引数として渡した場合)、LOH はマネージ ヒープの残りの部分と共に、直ちに収集されます。
システムのメモリが不足している場合 これは、OS からメモリ使用率が高いという通知を受け取ったときに起こります。世代 2 の GC を行うことが有効であると判断した場合は、それをトリガします。
しきい値は、各世代のプロパティの 1 つです。オブジェクトを世代に割り当てると、その世代のメモリ量が増加し、世代のしきい値に近づきます。そして、ある世代のしきい値を超えたときには、その世代に対してガベージ コレクションがトリガされます。したがって、小さなオブジェクトまたは大きなオブジェクトを割り当てると、世代 0 および LOH のそれぞれのしきい値が消費されます。また、ガベージ コレクタがオブジェクトを世代 1 および世代 2 に割り当てると、世代 1 のしきい値が消費されます。これらのしきい値は、プログラムの実行時に動的に調整できます。

LOH のパフォーマンスへの影響
次に、割り当てのコストを見てみましょう。CLR では、すべての新しいオブジェクトに対して解放時にメモリがクリアされることが保証されています。これは、大きなオブジェクトの割り当てコストが完全に、メモリのクリアによって占められることを意味します (ガベージ コレクションをトリガしない限り)。1 バイトをクリアするのに 2 サイクルかかるとすると、大きなオブジェクトの中で最小のものであっても、クリアするのに 170,000 サイクルかかることになります。ユーザーが割り当てる大きなオブジェクトのサイズは、多少大きすぎる場合も珍しくありません。2 GHz のマシンで 16 MB のオブジェクトをクリアする場合、約 16 ミリ秒かかります。これはかなり大きなコストです。
ここで、コレクションのコストについて見ていきます。前に述べたとおり、LOH と世代 2 は一緒に収集されます。そのどちらかのしきい値を超えたときに、世代 2 のコレクションがトリガされます。LOH のしきい値によって世代 2 がトリガされた場合、世代 2 自体は、ガベージ コレクション後に非常に小さくなるとは限りません。ここで、世代 2 にあまり多くのデータがない場合には、問題はありません。しかし、世代 2 のサイズが大きい場合は、世代 2 のガベージ コレクションが多くの回数トリガされると、パフォーマンス上の問題が生じる可能性があります。多くの大きなオブジェクトがごく一時的に割り当てられ、さらに、大きな SOH がある場合には、ガベージ コレクションの実行に過度な時間を費やす可能性があります。非常に大きなオブジェクトの割り当てと解放を引き続き繰り返すと、割り当てコストが相当に増加することは言うまでもありません。
LOH 上の非常に大きなオブジェクトは、通常は配列です (極端に大きなインスタンス オブジェクトを持つということは、ごくまれです)。配列の要素に参照が多く含まれる場合は、コストが高くなります。要素に参照が含まれない場合は、配列をまったく確認する必要がありません。たとえば、配列を使用してバイナリ ツリーのノードを格納する場合、これを実装する 1 つの方法は、ノードの右および左のノードを実際のノードによって参照することです。
class Node
{
    Data d;
    Node left;
    Node right;
};

Node[] binary_tr = new Node [num_nodes];
num_nodes が大きな数値である場合、要素ごとに少なくとも 2 回の参照を行う必要があることを意味します。他に、左右のノードのインデックスを格納するという方法もあります。
class Node
{
    Data d;
    uint left_index;
    uint right_index;
};
これにより、左のノードのデータを left.d として参照する代わりに、それを binary_tr[left_index].d として参照します。ガベージ コレクタは、左右のノードに対して参照を確認する必要がありません。
コレクションを行う 3 つの理由のうち、通常は最初の 2 つが 3 番目よりも大きな比重を占めます。したがって、大きなオブジェクトのプールを割り当てて、一時的なオブジェクトを割り当てる代わりにそれを再利用できれば最善です。そのようなバッファ プールの良い例を Yun Jin 氏がブログで紹介しています (go.microsoft.com/fwlink/?LinkId=115870)。もちろん、バッファのサイズをより大きくすることをお考えでしょう。

LOH のパフォーマンス データの収集
LOH に関連したパフォーマンス データを収集するにはいくつかの方法があります。ここでは、それらを説明する前に、なぜそれが必要かについてお話しましょう。
特定の領域に対するパフォーマンス データの収集を開始する前の状況としては、その領域に着目する理由が既にわかっているか、または既知の他の領域を調べ尽くしても、観測されているパフォーマンス上の問題を説明できる原因が見つからなかったことが考えられます。
詳細については、著者の書いたブログ記事をお読みになることをお勧めします (go.microsoft.com/fwlink/?LinkId=116467 を参照)。ここでは、メモリと CPU の基礎について述べています。さらに、2006 年 11 月号に掲載されたメモリ問題の調査に関する CLR 徹底解剖の記事では、マネージ ヒープに関連する可能性のあるマネージ プロセス内のパフォーマンス問題の診断手順を説明しています (msdn2.microsoft.com/magazine/cc163528 を参照)。
.NET CLR のメモリ パフォーマンス カウンタは、通常、パフォーマンスの問題について調べる最初の手順として有効です。LOH に関連するカウンタは、世代 2 のコレクション回数と、大きなオブジェクト ヒープのサイズです。世代 2 のコレクション回数は、プロセスが開始されてから世代 2 のガベージ コレクションが何回実行されたかを示します。このカウンタは、世代 2 のガベージ コレクション (フル ガベージ コレクションとも呼ばれます) の終了時にインクリメントされます。カウンタには、最後に測定された値が表示されます。
大きなオブジェクト ヒープのサイズは、空き領域を含めた大きなオブジェクト ヒープの現在のサイズ (バイト数) です。このカウンタは、各割り当て時にではなく、ガベージ コレクションの終了時に更新されます。
パフォーマンス カウンタを見る一般的な方法は、パフォーマンス モニタ (PerfMon.exe) で表示することです。図 4 に示すように、対象のプロセスに対して表示するカウンタを追加するには、[カウンタの追加] を使用します。
図 4 パフォーマンス モニタにカウンタを追加する (クリックすると拡大画像が表示されます)
パフォーマンス カウンタのデータは、PerfMon のログ ファイルに保存できます。また、パフォーマンス カウンタに対してプログラムでクエリを実行することもできます。多くのユーザーは、日常的なテスト プロセスの一部として、そのような方法でカウンタのデータを収集しています。カウンタに通常範囲外の値を見つけたときは、他の手段を使用して、調査に役立つ詳細な情報を取得できます。

デバッガを使用する
始める前に、ここで説明するデバッグ コマンドは Windows® デバッガに適用されることに注意してください。実際にどのようなオブジェクトが LOH に存在するかを確認する必要がある場合は、CLR で提供される SoS デバッガ拡張機能を使用できます。これについては、前にも述べた 2006 年 11 月号の記事で説明されています。LOH の分析出力の例を図 5 に示します。
図 5 で太字になっている部分は、LOH のヒープ サイズが (16,754,224 + 16,699,288 + 16,284,504 =) 49,738,016 バイトであることを示しています。そして、アドレス 023e1000 と 033db630 の間で、8,008,736 バイトが System.Object[] オブジェクトによって占有され、6,663,696 バイトが System.Byte[] オブジェクトによって占有され、2,081,792 バイトが空き領域となっています。
図 5 LOH 出力
0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
generation 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
 segment    begin allocated     size
0018f2d0 790d5588  790f4b38 0x0001f5b0(128432)
013e0000 013e1000  013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
 segment    begin allocated     size
023e0000 023e1000  033db630 0x00ffa630(16754224)
033e0000 033e1000  043cdf98 0x00fecf98(16699288)
043e0000 043e1000  05368b58 0x00f87b58(16284504)
Total Size  0x2f90cc8(49876168)
------------------------------
GC Heap Size  0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000  033db630
total 133 objects
Statistics:
      MT    Count    TotalSize Class Name
001521d0       66      2081792      Free
7912273c       63      6663696 System.Byte[]
7912254c        4      8008736 System.Object[]
LOH の合計サイズが 85,000 バイトより少なくなる場合もあります。それはなぜでしょうか。その理由は、ランタイム自体が実際に LOH を使用して、大きなオブジェクトよりも小さいいくつかのオブジェクトを割り当てるためです。
LOH は最適化されないため、LOH が断片化の元なのではないかと疑う人もいます。実際のところ、まず断片化とは何を意味するのかを明確にする必要があります。マネージ オブジェクト間の空き領域の量 (SoS で !dumpheap -type Free を実行したときの表示内容) からわかるように、マネージ ヒープには断片化が生じています。また、仮想メモリ (VM) アドレス領域にも断片化が生じています。これは、MEM_FREE として示されるメモリであり、windbg で各種のデバッガ コマンドを使用して確認できます (go.microsoft.com/fwlink/?LinkId=116470 を参照)。図 6 に、仮想メモリ領域の断片化 (太字の部分) を示します。
図 6 VM 領域の断片化
0:000> !address
    00000000 : 00000000 - 00010000
                    Type     00000000
                    Protect  00000001 PAGE_NOACCESS
                    State    00010000 MEM_FREE
                    Usage    RegionUsageFree
    00010000 : 00010000 - 00002000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageEnvironmentBlock
    00012000 : 00012000 - 0000e000
                    Type     00000000
                    Protect  00000001 PAGE_NOACCESS
                    State    00010000 MEM_FREE
                    Usage    RegionUsageFree
... [omitted]
-------------------- Usage SUMMARY --------------------------
    TotSize (      KB)   Pct(Tots) Pct(Busy)   Usage
     701000 (    7172) : 00.34%    20.69%    : RegionUsageIsVAD
   7de15000 ( 2062420) : 98.35%    00.00%    : RegionUsageFree
    1452000 (   20808) : 00.99%    60.02%    : RegionUsageImage
     300000 (    3072) : 00.15%    08.86%    : RegionUsageStack
       3000 (      12) : 00.00%    00.03%    : RegionUsageTeb
     381000 (    3588) : 00.17%    10.35%    : RegionUsageHeap
          0 (       0) : 00.00%    00.00%    : RegionUsagePageHeap
       1000 (       4) : 00.00%    00.01%    : RegionUsagePeb
       1000 (       4) : 00.00%    00.01%    : RegionUsageProcessParametrs
       2000 (       8) : 00.00%    00.02%    : RegionUsageEnvironmentBlock
       Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)

-------------------- Type SUMMARY --------------------------
    TotSize (      KB)   Pct(Tots)  Usage
   7de15000 ( 2062420) : 98.35%   : <free>
    1452000 (   20808) : 00.99%   : MEM_IMAGE
     69f000 (    6780) : 00.32%   : MEM_MAPPED
     6ea000 (    7080) : 00.34%   : MEM_PRIVATE

-------------------- State SUMMARY --------------------------
    TotSize (      KB)   Pct(Tots)  Usage
    1a58000 (   26976) : 01.29%   : MEM_COMMIT
   7de15000 ( 2062420) : 98.35%   : MEM_FREE
     783000 (    7692) : 00.37%   : MEM_RESERVE
前述したように、マネージ ヒープの断片化は割り込み要求に対して発生します。より一般的に仮想メモリの断片化が見られるケースは、一時的な大きなオブジェクトが、OS から新しいマネージ ヒープ セグメントを取得して空のセグメントを OS に解放するために、頻繁にガベージ コレクションを必要とするような場合です。
LOH が VM の断片化を生じさせているかどうかを確認するには、VirtualAlloc と VirtualFree にブレークポイントを設定して、その呼び出し元を確認します。たとえば、OS から 8 MB を超える VM チャンクを割り当てようとしているオブジェクトを調べる場合、次のようにブレークポイントを設定できます。
bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
これにより、VirtualAlloc が 8 MB (0x800000) を超える割り当てサイズで呼び出された場合にはデバッガの実行がブレークされてコールスタックが表示され、それ以外の場合にはブレークされません。
CLR 2.0 では、セグメント (大きなオブジェクト ヒープと小さなオブジェクト ヒープの両方のセグメントを含む) が頻繁に取得および解放されるような状況で使用できる VM Hoarding という機能を追加しました。VM Hoarding を指定するには、ホスティング API で STARTUP_HOARD_GC_VM というスタートアップ フラグを指定します (go.microsoft.com/fwlink/?LinkId=116471 を参照)。これを指定すると、空のセグメントを解放して OS に返す代わりに、これらのセグメント上のメモリを単にデコミットして、スタンバイ リストに含めます。スタンバイ リスト上のセグメントは、後で新しいセグメント要求を満たすために使用されます。したがって、次に新しいセグメントが必要になったときには、スタンバイ リスト上に十分に大きなセグメントがあれば、それを使用します。
これは、非常に大きなセグメントに対しては行われないことに注意してください。この機能は、既に取得済みのセグメントを保持する必要のあるアプリケーションに対しても有用です。たとえば、メモリ不足エラーを防ぐために VM 領域の断片化をできるだけ回避する必要があるサーバー アプリケーションなどです。通常、そのようなアプリケーションはマシン上で占有率の高いアプリケーションであるため、この機能を実行できます。この機能を使用する際には、アプリケーションを慎重にテストして、メモリ使用量が十分に安定しているのを確認することを強くお勧めします。
大きなオブジェクトは高コストです。CLR では、すべての新しく割り当てられるオブジェクトに対してメモリのクリアを保証するため、新しく割り当てる大きなオブジェクトのメモリをクリアする必要があり、割り当てコストは高くなります。LOH はヒープの残り部分と共に収集されるため、アプリケーションのパフォーマンスにどのような影響があるかを注意深く分析してください。マネージ ヒープおよび VM 領域の断片化を避けるために、大きなオブジェクトはできる限り再利用することをお勧めします。
最後に、現在の時点で、LOH はコレクション時に最適化されませんが、これは実装の詳細事項であり、そのことに依存した処理は避けてください。したがって、何かが GC によって移動されないよう保証するには、常にそれを固定する必要があります。ここで学んだ LOH の新しい知識を活用して、ヒープを適切に制御してください。

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

Maoni Stephens は、マイクロソフトの CLR チームでガベージ コレクタに取り組んでいる上級開発者です。CLR チームに参加する前は、マイクロソフトのオペレーティング システム グループに数年間在籍していました。


Cutting Edge
ASP.NET AJAX とクライアント側テンプレート
Dino Esposito

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

目次
AJAX の一般的な機能
部分的なレンダリングの制限
リアルタイム株価ページを再検討する
ASP.NET AJAX でのクライアント側テンプレート
ブラウザ側テンプレート パターン
その他の考慮事項
HTML Message パターン
ASP.NET AJAX アプリケーションの開発者に対して部分的なレンダリングが特に魅力的である理由の 1 つは、その本質的な単純さです。既存のページに与える影響が小さく、開発者のスキルに対する要求もそれほど高くありません。しかし、部分的なレンダリングは、ポストバック要求の実行でブラウザをバイパスするための単なるトリックに過ぎません。部分的なレンダリングを使用するページは、引き続き古いポストバック手法に依存しているため、真の AJAX ページとは言えません。では、真の AJAX アプリケーションとは、何を意味するのでしょうか。
本質的に、真の AJAX アプリケーションは、XMLHttpRequest オブジェクトを使用してブラウザをバイパスし、Web サーバーおよびホストされた HTTP エンドポイントとの間に直接通信を確立します。アプリケーションは、必要なデータを非同期で取得でき、ユーザー インターフェイスの各ブロックを独立して更新できます。この主要な動作に基づいて、いくつかの AJAX フレームワークがデザインされています。これらのフレームワークは、それぞれ異なる量の糖衣構文を使用し、機能セットの大きさもさまざまで、提供するユーザー インターフェイス ウィジェットのファミリも豊富なものから簡素なものまであります。ASP.NET AJAX は、そのようなフレームワークの 1 つです。
真の AJAX アプリケーションのデザインや構築は簡単な作業ではなく、適切なプログラミング ツールがなければさらに難しくなります。他の AJAX フレームワークと比較して、ASP.NET AJAX は、サーバー側コードにアクセスするためのサポートが優れています。ASP.NET AJAX を使用した場合、スクリプト対応の Web または Windows® Communication Foundation (WCF) サービスへの接続は、開発者の観点からは非常に簡単です。URL を参照するだけで、フレームワークによって自動的に JavaScript プロキシが生成されるためです。これはコストがかからず、かつ効果的です。XMLHttpRequest プラミング、シリアル化および逆シリアル化、データ形式、HTTP パッケージ化などの詳細は、すべてプロキシ クラスによって隠蔽されます。単にメソッドを非同期で呼び出し、固有のプロパティのセットと共に返されるプレーンなデータ転送オブジェクト (DTO) を受け取るだけです。
ただし、ダウンロードされたデータをブラウザで処理する段階になると、ASP.NET AJAX にはそれと同じように効率的なメカニズムがありません。これは間違いなくいずれ改良されますが、現時点で ASP.NET AJAX は、サーバー側サービス モデルに見合うほどリッチなクライアント側 UI モデルを提供できません。
それでは現在、ASP.NET AJAX の純粋な Web アプリケーションの UI モデルを改良するにはどうすればよいでしょうか。先月号では、シングル ページ インターフェイス (SPI) の概念を紹介し、SPI ページの構築に利用できる単純なパターンをいくつか示しました。今月は、クライアント側テンプレートとデータ バインドについて説明します。

AJAX の一般的な機能
AJAX アプローチの能力を示す一般的な例の 1 つとして、リアルタイムで株価を取得するアプリケーションがあります。現在、この機能を提供する非 AJAX の Web サイトでは、メタリフレッシュ タグを使用するか、または Flash、Silverlight™、ActiveX® コントロールなどのプラグイン ベースのアプローチを使用しています。一般に、表示される株価の一覧はサーバー側のグリッド コントロールにバインドされ、このグリッドは、次のポストバックまたはページ要求時にページ内の他の部分と一緒に更新されます。このようなページは、少しの部分的なレンダリングによってすばやく拡張できます。その方法を見てみましょう。図 1 に示すマークアップでは、Timer コントロールによって定期的に更新される UpdatePanel コントロール内にグリッド コントロールをラップしています。
図 1 リアルタイム株価ページの UI
<asp:UpdatePanel runat="server" ID="UpdatePanel1" UpdateMode="Conditional">
  <ContentTemplate>
    <div style="margin:10px;">
      <asp:Label runat="server" ID="Label2" />
      <asp:GridView ID="GridView1" runat="server" SkinID="ClassicGrid"
        AutoGenerateColumns="false"
        OnRowDataBound="GridView1_RowDataBound">
        <Columns>
          <asp:BoundField DataField="Symbol" HeaderText="SYMBOL" />
          <asp:BoundField DataField="Quote" HeaderText="LAST" >
            <ItemStyle HorizontalAlign="Right" />
          </asp:BoundField>
          <asp:BoundField DataField="Change" HeaderText="CHANGE" >
            <ItemStyle HorizontalAlign="Right" />
          </asp:BoundField>
        </Columns>
      </asp:GridView>
      <small><asp:Label runat="server" ID="Label1" /></small>
    </div>
  </ContentTemplate>
  <Triggers>
    <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
  </Triggers>
</asp:UpdatePanel>

<asp:Timer runat="server" id="Timer1" Enabled="true"
  Interval="600000" OnTick="Timer1_Tick" />
このグリッド コントロールは、Symbol、Quote、Change という少なくとも 3 つのプロパティを持つオブジェクトのコレクションにバインドされます。グリッドの内容は、10 分ごとに更新されます。ASP.NET AJAX の Timer コントロールは、クライアント タイマをアクティブにし、更新間隔に達するとポストバックを実行します。これは UpdatePanel にラップされているため、株価を表示するグリッドはページとは別に更新されます。サーバーでは、タイマの周期ごとに次のコードが実行されます。
protected void Timer1_Tick(object sender, EventArgs e)
{
    Label2.Text = "<b>Last update: </b>" + DateTime.Now.ToLocalTime();
    GridView1.DataSource = picker.Update();
    GridView1.DataBind();
}
picker オブジェクトは、カスタム ヘルパ クラス StockPicker のインスタンスです。このクラスは、株価の取得を行うサービス クラスのインスタンスを含んでいます。StockPicker クラスのコンストラクタを次に示します。
public StockPicker(string stocks)
{
   string provider = ConfigurationManager.AppSettings["StockProvider"];
   _service = GetFactory(serviceProvider, stocks);

    if (_service == null)
      return;

    _desc = _service.GetServiceDescription();
}
picker は構成ファイルからサービス プロバイダ クラスの名前を読み取り、それをインスタンス化します。picker は、IQuoteService インターフェイスによって表されるコントラクトを通じて、サービス クラスとやり取りします。
interface IQuoteService
{
    StockInfoCollection GetQuotes();
    string GetServiceDescription();
}
このコラムに付属するサンプル コードでは、株価の代わりにランダムな数字を返すだけの仮の株価サービスを使用しています。ただし、このページ コードは、実際の株価サービスを使用するように簡単に構成できます。必要なのは、IQuoteService インターフェイスを実装するクラスを作成して、実際の株価データ サービスに接続することだけです。サンプル コードでは、バインド可能なデータを StockInfo オブジェクトのコレクションとして表しています。
public class StockInfo
{
    public string Symbol { get; set; }
    public string Quote { get; set; }
    public string Change { get; set; }
    public string Day { get; set; }
    public string Time { get; set; }
}
図 2 に、この API の動作に基づくサンプル ASP.NET AJAX ページを示します。
図 2 部分的なレンダリングを使用したリアルタイム株価ページ (クリックすると拡大画像が表示されます)

部分的なレンダリングの制限
機能的に見れば、図 2 に示したページは、期待したとおりに動作します。そのパフォーマンスと全体的な動作に満足できれば、それで問題はありません。ただし、そのようなページには、皆さんの安眠を妨げるほどではないにしても、注意を要する欠点がいくつかあります。
まず、このページは更新されるたびに大量のデータをやり取ります。このデータ量は、10 分ごとに更新されるこのページのような比較的更新頻度の緩いページでは、それほど問題とならないかもしれません。Fiddler または Web Development Helper を使用して監視すると、ペイロードのサイズがダウンロード時には 3 KB、アップロード時はそれより多少小さいことがわかります。この数値に問題がなければ、部分的なレンダリングをぜひ導入してください。
しかし、ページの内部動作をよく見ると、この数値が決して安心できるものではないことがわかります。ページのビュー ステートは、更新のたびに送受信されます。応答には、データとレイアウト情報の両方が含まれます。さらに、データは周辺のマークアップと結合され、それらを分離することはできません。最後に、ページ上に同様に機能する更新可能なパネルが他にもある場合、別の部分的なレンダリングの操作が開始されるとタイマが一時的に停止する可能性があります。または、タイマがトリガされたときに他の保留中の操作が停止する場合もあります。
結局、部分的なレンダリングはすぐに習得して適用できる反面、非同期の同時呼び出しをサポートする必要がある場合には、適切的なメカニズムとは言えません。次に、図 2 のページを純粋な AJAX アプローチを用いて書き直すには何が必要かを見ていきます。

リアルタイム株価ページを再検討する
インサイト : ASP.NET AJAX

テンプレート駆動のレンダリングは、長い間にわたり、ASP.NET サーバー コントロールの主要な能力の 1 つとなっています。<% %> および <%= %> ブロックを使用してマークアップの周囲でループする古い技法に比べて、テンプレートにはいくつかの利点があります。たとえば、双方向のバインドや、モデルの全般的なクリーンさが挙げられます。クライアントでは、フレームワークがないと単純な <% %> ブロックさえも使用できないため、良質なテンプレート エンジンは性能向上に欠かせません。また、リスト全体を再レンダリングする代わりにリスト内で変更された項目だけを再レンダリングするなど、クライアント側では簡単に実現できて、サーバー側ではそれよりずっと困難であるような最適化の多くが可能になります。その一方で、クライアントでのレンダリングのパフォーマンスは不十分であるため、テンプレート エンジンはパフォーマンスについて特に注意深く最適化される必要があります。
Dino 氏はこの記事で、セットアップが簡単な、現時点で適切に動作するクライアント側テンプレート レンダリングを実現するための、単純なアプローチを紹介しています。もちろん、ASP.NET チームでもテンプレートの威力は認識しています。実際、ASP.NET AJAX の最初のプレビューのリリース時には、テンプレート駆動レンダリングを実現する機能の 1 つとして、XML-Script と呼ばれる非常に精巧な宣言構文を導入しました。残念ながら、これはあまりに複雑で冗長であると判断され、また、パフォーマンスも私たちやユーザーが満足するレベルではありませんでした。さらに、ツールのサポートも欠けていました。
テンプレートには、単に HTML 文字列内のプレースホルダにデータを挿入する以外に、ずっと多くの要件があることに注意してください。まず、テンプレート エンジンは、単純なフィールド挿入を超えた動作を実現するために、ある種の式言語を備える必要があります。次に、条件レンダリングのシナリオを有効にする必要があります。そして最も重要なこととして、レンダリングされたマークアップに対して、リッチな動作を単純に追加できる必要があります。つまり、エンジンは、作成したマークアップに付加するコンポーネントを単純な方法でインスタンス化できる必要があるのです。
ASP.NET AJAX の次のバージョンでは、テンプレートを再度取り上げ、それらを単純な宣言コンポーネント構文と共に AJAX フレームワークに再導入する予定です。単純なリスト ビューは、次のようなものになります。
<body xmlns:sys="javascript:Sys" xmlns:dv=  "javascript:Sys.UI.DataView">
...
<div id="dataview1" sys:type="dv" dv:data="{{someArray}}">
  <div class="sys-template">
    <h2><a href="{{ 'products/' + id }}">{{name}}</a></h2>
    <!--% if (description) { %-->
      <p>{{description}}</p>
    <!--% } %-->
  </div>
</div>
ここではいくつか注目すべき点があります。式は区切り記号 {{ }} を使用してマークアップに埋め込まれます。これらの式は、現在のデータ項目フィールド (名前、説明など) への直接参照であるか、または複雑な JavaScript 式 ('products/' + id) を使用できます。body タグでは、DataView JavaScript コンポーネントにマッピングされる "dv" 名前空間を定義しています。その後、"dataview1" div の sys:type を "dv" に設定することで、DataView コントロールをインスタンス化してその要素にアタッチすることを指定します。
同じタグで、dv:data="{{someArray}}" は DataView コントロールの data プロパティを someArray グローバル変数に設定します。テンプレートの内部で同じ宣言構文を使用することにより、任意のコントロールおよび動作をインスタンス化してアタッチできます。ここでテンプレート自体は dataview1 div 内部のマークアップですが、これを外部コンテンツから構築することもできます。最後に、エンジンでは <!--% %--> ブロックを使用してテンプレート マークアップに任意のコードを埋め込むことができます。ここではそれを使用して、description データ列を含む段落を条件レンダリングしています。
—Bertrand Le Roy
プログラム マネージャ、ASP.NET
純粋な AJAX アプローチは、リアルタイム株価ページのデザインにどのような影響を与えるのでしょうか。ページはタイマを設定し、XMLHttpRequest を使用してリモート サービスを呼び出します。このサービスは、アプリケーションのバック エンドの一部であり、標準の Microsoft® .NET Framework API を使用して財務 Web サービスを呼び出し、データを取得します。データは JavaScript オブジェクトとしてブラウザに返されます。最後に、それをユーザーにレンダリングする部分は、デザイナの仕事です。
違いは何でしょうか。まず、呼び出される URL がページ自体ではありません。ページは、スクリプト対応の Web または WCF サービスでバックアップされる HTTP エンドポイントを呼び出します。ページのライフサイクルや、ポストバック イベント、ビュー ステートの復元などはありません。その結果、ネットワークのトラフィックが大きく削減されます。このサンプル ページの場合、ペイロードは部分的なレンダリングの場合と比べて 10 倍小さくなります。アーキテクチャ的な観点から見ると、クライアント側のフロントエンドとサーバー側のバックエンドという 2 つの整然と分離されたコード ブロックが動作しているのがわかります。前者は JavaScript によって駆動され、後者はマネージ コードに基づいています。
ASP.NET AJAX は、プログラミング インターフェイスやデータ型の統一を行うため、このような状況で威力を発揮します。それにより、JavaScript クライアント開発者は、サーバー上で定義されているのとまったく同じプログラミング インターフェイスおよびコントラクトを使用できます。JavaScript Object Notation (JSON) 層により、クライアントに到着した DTO がサーバーから送信されたデータをそのまま反映していることが保証されます。
それでは、いくつかのコードで実験してみましょう。新しいページには、ユーザーがクリックして新しいデータをダウンロードできるクライアント ボタンがあります。ここでタイマの代わりにボタンを使用しているのは、単に操作性を良くするためです。
<input type="button" id="btnRefresh"
       value="Live Quotes"
       onclick="_getLiveQuotes()" />
これにより JavaScript ハンドラが、ラッパであるリアルタイム株価 Web サービスのメソッドを呼び出し、StockInfo オブジェクトのコレクションを取得します。
<%@ WebService Class="Samples.WebServices.LiveQuoteService" %>
図 3 に、サービスのソース コード全体を示します。
図 3 リアルタイム株価 Web サービス
namespace Samples.WebServices
{
    [WebService(Namespace = "http://samples.ajax/")]
    [ScriptService]
    public class LiveQuoteService : System.Web.Services.WebService
    {
        private static string stocks =
            ConfigurationManager.AppSettings["StockSymbols"];
        private static StockPicker picker = new StockPicker(stocks);

        public LiveQuoteService()
        {
        }

        [WebMethod]
        public StockInfoCollection Update()
        {
            return picker.Update();
        }

        [WebMethod]
        public string GetServiceDescription()
        {
            return picker.GetServiceDescription();
        }
    }
}
部分的なレンダリングでは、ユーザー インターフェイス要素 (ビュー) とコア アプリケーション ロジック (モデル) の間に本質的な区別はありません。すべてがサーバー上でアセンブルされ、すぐに使用できる状態でクライアントに提供されます。純粋な AJAX アーキテクチャでは、プレゼンテーション層がより洗練されて機能が追加され、ビジネス層から物理的に分離されています。そのような高度に分離されたアーキテクチャは、本質的に柔軟性が高くテストもしやすいことは言うまでもありません。さらに、図 4 に示すように、データをキャッシュできる場所が他にもいくつかあります。
図 4 AJAX アーキテクチャ内の可能なキャッシュ レベル (クリックすると拡大画像が表示されます)
データは、(Web) サービスのフロント エンドおよび実装上にあるサーバーにキャッシュできます。また、途中で HTTP プロキシによってキャッシュするか、ブラウザで実行中の JavaScript コードでキャッシュすることもできます。さらに重要なこととして、キャッシュされるのはマークアップではなく、使用可能なデータ (オブジェクトまたは JSON 文字列) であり、実行時の条件に対していつでもチェックすることができます。
レンダリングのロジックをクライアントに移すことは、負荷の高いサーバーの負担を軽減するという利点もあります。サーバーが小さなデータだけを返し、マークアップ生成とビュー ステート処理を含むページのライフサイクル全体を実行する必要がなければ、CPU サイクルおよびメモリを節約できます。
ASP.NET AJAX をクライアントから使用し、リモート サービスとやり取りしてリアルタイムのデータをブラウザに取り込むことも、同じように単純明快です。たとえば、次に示すのはリモート メソッドを呼び出すコードです。
function _getLiveQuotes()
{
    Samples.WebServices.LiveQuoteService.Update(onDataAvailable);
}
function onDataAvailable(results)
{
    // Update the UI
}
ASP.NET AJAX では、サーバー側サービスと同じ名前を持つ JavaScript プロキシ オブジェクトと、いくつかの静的メソッドが提供されます。このコード スニペットでは、Update メソッドが最終的に JavaScript ドキュメント型定義 (DTD) の配列をフェッチし、ユーザー インターフェイスを最終的に更新するコールバック関数にそれを渡します。コード内の results という変数には、サービスからの応答が含まれます。これは、グリッドにバインドする必要のあるデータそのものです。
残念ながら、サーバー ベースの ASP.NET アプローチで使用できるものと同様な、クライアント側の GridView コントロールはありません。何らかの形のクライアント側データ バインド (できればテンプレートをサポートするもの) が必要となります。現在のところ、ASP.NET AJAX はこの目的にはあまり役立ちません。この問題に関するマイクロソフト ASP.NET チームからの直接の説明については、「インサイト : ASP.NET AJAX」のリンクを参照してください。
いくつかの主要な ASP.NET コントロール ベンダは最近、等価なクライアント側オブジェクト モデルを備えたサーバー コントロールの提供を開始しました。これらのライブラリのいずれかを使用すると、クライアントのデータ ソース プロパティと DataBind メソッドを備えた、サーバー側コントロールを模倣する JavaScript オブジェクトが得られる可能性があります。ASP.NET AJAX は、クライアントから Web サービスを呼び出すために必要な基盤を提供しますが、AJAX を考慮してデザインされた完全な UI フレームワークは提供せず、クライアント データの構築やテンプレート作成もサポートしません。UI の観点から見た各種の AJAX アプローチについての興味深い記事が、Miljan Braticevic 氏によって投稿されています (go.microsoft.com/fwlink/?LinkID=116054)。

ASP.NET AJAX でのクライアント側テンプレート
"Atlas" を覚えているでしょうか。後に ASP.NET AJAX Extensions となる、その初期のビルドでは、クライアント側のデータ バインドおよびテンプレートを提供しようという試みがなされました。そして、それは実際にかなり良いものでした。しかし、何かの理由で、ASP.NET の最終リリースには組み込まれず、ASP.NET Futures ライブラリに押し込まれています。
基本的に、古い Atlas バインド モデルは、HTML テンプレートと、マークアップ要素とデータ フィールドの間のバインドを表す宣言構文に基づいていました。そしてテンプレートとバインドは、リスト ビュー コントロールというクライアント コントロールで結合されます。XML を使用して構成された Atlas リスト ビュー コントロールは、受信したすべての情報を使用して多くの解析作業を行い、動作、コントロール、およびバインドをマークアップのさまざまな部分にアタッチします。
この最後の段階から始めて、データ バインドおよびテンプレートに対する小さく効果的な独自のフレームワークを構築する方法を見ていきましょう。最終的な出力は HTML マークアップの文字列でなければならないことに注意してください。最初の入力は、いくつかのパブリック プロパティを持つオブジェクトのコレクションです。必要な作業は、ユーザーの期待を満足させる HTML コードを構築することです。一般には、バインドされたコレクションに対してループを実行し、テンプレートのテキストをオブジェクトのプロパティの値と連結することから開始します。
このアプローチの悪いところはどこでしょうか。実際には、何も悪いところはありません。同じ段階でこれとまったく同じことを行わないような、まったく別のアプローチは思いつきません。これは、この基本的なルーチンを抽象化するフレームワークが確実に作成できることを意味します。AJAX パターンのカタログ (www.ajaxpatterns.org を参照) には、クライアント側のテンプレート モデルを定義する主要な手順が、ブラウザ側テンプレート (BST) パターンという名前で紹介されています。

ブラウザ側テンプレート パターン
ブラウザ側テンプレート (BST) パターンの目的は、データのビューを生成するコードをデータ自体から分離することです。ソフトウェア システムで従来からの問題の 1 つである、ビューとデータ間の分離には、Model View Controller (MVC) パターンのいずれかのバリエーションを用いれば、標準的な解決策が見つかります。
MVC や BST のような表示パターンは、決して相互に排他的なものではなく、BST はそれ自体が MVC のようなものと考えることができます。ただし、コントローラという固有の概念はなく、ブラウザとサーバーが実際に分離されていることでビューとモデルが別々に保持されます。図 5 に、BST に関連した手順を示しています。ユーザーは、データをクライアントにダウンロードするリモート呼び出しをトリガします。このデータは、新しい種類のコンポーネント (マークアップ ビルダ) をインスタンス化する JavaScript コールバックによって管理されます。
図 5 ブラウザ側テンプレートの動作 (クリックすると拡大画像が表示されます)
マークアップ ビルダは、ページのドキュメント オブジェクト モデル (DOM) 内の HTML テンプレートへの参照とダウンロードされたデータに基づく、HTML 文字列を返します。最後に、コールバックはページの DOM に文字列を挿入します。
ここで、いくつかのコードを見てみましょう。この実装では、BST の中核は JavaScript MarkupBuilder クラス内にあり、これは最大 3 つの HTML テンプレート (ヘッダー、フッター、および項目) を受け取ります。これらのテンプレートは、直接 DOM から参照するか、またはプレーンな文字列リテラルとして指定できます。
function pageLoad()
{
    if (builder === null)
    {
        builder = new Samples.MarkupBuilder();
        builder.loadHeader($get("header"));
        builder.loadFooter($get("footer"));
        builder.loadItemTemplate($get("item"));
   }
}
HTML テンプレートは、非表示の DIV を使用して直接ページに埋め込むこともできます。ただし、これはブロック内のマークアップが十分に整っている場合にだけ機能します。より適切なオプションは、次に示すように、テンプレートを XML データ アイランドとして埋め込むことです。
<xml id="item">
    <tr style="background-color:#F0FAFF;">
        <td align="left">#Symbol</td>
        <td align="right">#Quote</td>
        <td align="right">#Change</td>
    </tr>
</xml>
このコードからわかるように、テンプレートは、カスタム表記を使用してバインド フィールドを参照する HTML のチャンクです。ここでは、#PropertyName 式を使用して、バインドされた値のプレースホルダを示しています。データが使用可能であれば、単にマークアップ ビルダで bind メソッドを呼び出します。
function _getLiveQuotes()
{
    Samples.WebServices.LiveQuoteService.Update(onDataAvailable);
}
function onDataAvailable(results)
{
    var temp = builder.bind(results);
    $get("grid").innerHTML = temp;
}
言うまでもありませんが、grid という名前の要素は、この例の最終的な出力に対するプレースホルダです。MarkupBuilder クラスは、Microsoft AJAX クライアント ライブラリに基づいています。ソース コードの全体を図 6 に示します。
図 6 MarkupBuilder クラス
Type.registerNamespace('Samples');

// Class ctor
Samples.MarkupBuilder = function Samples$MarkupBuilder()
{
    Samples.MarkupBuilder.initializeBase(this);

    // Initializes private members
    this._header = "";
    this._footer = "";
    this._itemTemplate = "";
}
Samples.MarkupBuilder = function Samples$MarkupBuilder(header, footer)
{
   Samples.MarkupBuilder.initializeBase(this);

    // Initializes the private members
    this._header = header;
    this._footer = footer;
    this._itemTemplate = "";
}

// PROPERTY:: header (String)
function Samples$MarkupBuilder$get_header() {
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._header;
}
function Samples$MarkupBuilder$set_header(value) {
    var e = Function._validateParams(arguments, [{name: 'value',
        type: String}]);
    if (e) throw e;

    this._header = value;
}

// PROPERTY:: footer (String)
function Samples$MarkupBuilder$get_footer() {
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._footer;
}
function Samples$MarkupBuilder$set_footer(value) {
    var e = Function._validateParams(arguments, [{name: 'value',
        type: String}]);
    if (e) throw e;

    this._footer = value;
}

// PROPERTY:: itemTemplate (String)
function Samples$MarkupBuilder$get_itemTemplate() {
    if (arguments.length !== 0) throw Error.parameterCount();
    return this._itemTemplate;
}
function Samples$MarkupBuilder$set_itemTemplate(value) {
    var e = Function._validateParams(arguments, [{name: 'value',
        type: String}]);
    if (e) throw e;

    this._itemTemplate = value;
}

// METHOD:: bind()
function Samples$MarkupBuilder$bind(data) {
   var temp = this._generate(data);
   return temp;
}
// METHOD:: loadHeader()
function Samples$MarkupBuilder$loadHeader(domElement) {
   var temp = domElement.innerHTML;
   this._header = temp;
}

// METHOD:: loadFooter()
function Samples$MarkupBuilder$loadFooter(domElement) {
   var temp = domElement.innerHTML;
   this._footer = temp;
}

// METHOD:: loadItemTemplate()
function Samples$MarkupBuilder$loadItemTemplate(domElement) {
   var temp = domElement.innerHTML;
   this._itemTemplate = temp;
}

///////                     ///////
///////  PRIVATE members    ///////
///////                     ///////

function Samples$MarkupBuilder$_generate(data) {
    var _builder = new Sys.StringBuilder(this._header);

    for(i=0; i<data.length; i++)
    {
        var dataItem = data[i];
        var template = this._itemTemplate;

        var pattern = /#\w+/g;  // Finds all #word occurrences
        var matches = template.match(pattern);
        for (j=0; j<matches.length; j++)
        {
            var name = matches[j];
            name = name.slice(1);
            template = template.replace(matches[j], dataItem[name]);
        }

        _builder.append(template);
    }

    _builder.append(this._footer);
    return _builder.toString();
}

// PROTOTYPE
Samples.MarkupBuilder.prototype =
{
    get_header:         Samples$MarkupBuilder$get_header,
    set_header:         Samples$MarkupBuilder$set_header,
    get_footer:         Samples$MarkupBuilder$get_footer,
    set_footer:         Samples$MarkupBuilder$set_footer,
    get_itemTemplate:   Samples$MarkupBuilder$get_itemTemplate,
    set_itemTemplate:   Samples$MarkupBuilder$set_itemTemplate,
    bind:               Samples$MarkupBuilder$bind,
    _generate:          Samples$MarkupBuilder$_generate,
    loadHeader:         Samples$MarkupBuilder$loadHeader,
    loadFooter:         Samples$MarkupBuilder$loadFooter,
    loadItemTemplate:   Samples$MarkupBuilder$loadItemTemplate
}

Samples.MarkupBuilder.registerClass('Samples.MarkupBuilder');
内部では、マークアップ ビルダは Sys.StringBuilder オブジェクトを使用して HTML 文字列を作成します。最初に、ヘッダー テンプレート (ある場合) を追加してから、バインドされたデータのループに進みます。最後に、フッター テンプレートを追加します。この実装では、ヘッダー テンプレートとフッター テンプレートはデータ バインドされていません (図 7 に、マークアップ ビルダを使用したページを示します)。
図 7 マークアップ ビルダを使用した新しいページ (クリックすると拡大画像が表示されます)

その他の考慮事項
テンプレートをプレーンな文字列として指定するのと、埋め込まれた DOM サブツリーまたは XML データ アイランドとして指定するのとでは、何か違いがあるのでしょうか。マークアップ ビルダの観点から言えば、その違いはあまり問題となりません。必要なのは、StringBuilder のバッファに追加する文字列だけです。開発者の観点からは、DOM テンプレートを使用する方が優れた選択肢かもしれません。なぜなら、テンプレートの検証が容易であり、テンプレートを URL からダウンロードできる可能性もあるためです。
ここに示したソリューションは、データのコレクションだけを扱うようにデザインされているため、クライアント側のテンプレートに関しては他のフリー フレームワークのガイドラインに従っています。これらのフレームワークのうち、具体的に挙げておきたいものの 1 つが、PrototypeJS です。PrototypeJS の詳細、サンプル、およびソース コードについては、prototypejs.org/api/template を参照してください。
さらに何か改善できる点はあるでしょうか。もちろん、あります。このコラムで示したコードは、ブラウザ側テンプレートおよびバインド エンジンから期待される最小限の機能セットだけを実装していることに注意してください。
図 7 を見るとわかるように、現在の実装には、図 2 のような上昇した株価と下降した株価を区別するための色分けがありません。図 2 のコードでは、GridView コントロールの RowDataBound イベントを処理して、関連したセルのスタイルを直接変更しています。
void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.Cells[2].Text.StartsWith("+"))
        e.Row.Cells[2].ForeColor = Color.Green;
    if (e.Row.Cells[2].Text.StartsWith("-"))
        e.Row.Cells[2].ForeColor = Color.Red;
}
部分的なレンダリングのソリューションでは、このスタイル変更はサーバー上で行われるため、サーバーのデータ バインド コントロールの機能豊富なプログラミング モデルを利用できます。クライアント側のソリューションでは、関数デリゲートに対応した、より高度なマークアップ ビルダが必要となります。この関数デリゲートは、任意のバインドされたデータに対して、バッファに付加する前にスタイルを設定します。
図 2 と図 7 を生成したコードの間には、印刷された図には表れていない、より重要な違いが他にもあります。この違いを見つけるために、両方のページを実行して、表示される他のボタン ([Weather Forecasts] (天気予報) と [Start Some Task] (タスクの開始)) をクリックしてみてください。図 7 に示されるページでは、株価の取得中に他のタスクは停止せず、ユーザーが何か他のタスクを開始するためにボタンをクリックしても、サービス呼び出しは中断されません。
ポイントは何でしょうか。部分的なレンダリングでは、通常の非 AJAX ページの場合と同様に、セッションごとに一度に 1 つの要求しか保留できなかったことを思い出してください。もちろん、直接 XMLHttpRequest で制御すると、同時にいくつでも呼び出しを行うことができます。さらに、直接のサービス呼び出しを行った場合は、ビュー ステートが存在しないため、複数の部分的なレンダリング呼び出し間でその一貫性を保つことを気にしなくて済みます (これが、部分的なレンダリング実装で同時操作を実行できない真の理由です)。

HTML Message パターン
このコラムの締めくくりとして、将来のコラムで取り上げる価値のある別のパターンについて簡単に述べさせてください。これは、HTML Message パターンと呼ばれるものです。HTML Message パターンの目的は、ブラウザで表示される HTML マークアップのブロックをサーバーで生成させることです。お気付きのように、これは、部分的なレンダリングと、真の AJAX アプリケーションを構築する他のモデルとの中間に位置するものです。
このパターンはどのように使用するのでしょうか。可能な実装の 1 つは、リモート URL (サービスまたはアドホック HTTP ハンドラ) への呼び出しを行い、すぐに表示できる HTML スニペットを受信することです。これは、プレーンなサービス呼び出しよりも多くのトラフィックを生成しますが、部分的なレンダリングによるトラフィックよりは少なくなります。
サーバー上では、サーバー コントロールの新しいインスタンス (ビュー ステートなし) を使用して、必要な任意の出力を好みのスタイルで出力できます。このパターンについては、今後のコラムで取り上げるつもりです。
ここで、ASP.NET AJAX を使用するときには、AJAX サービス層を定義してそれをクライアント ブラウザから呼び出すためのプログラミング ツールが必要であることを思い出してください。また、JavaScript ベースのデータ バインドやテンプレートなど、クライアント上のデータを効果的に操作するための強力なツールも必要です。
読者の皆さんが、このコラムで紹介した BST AJAX パターンのサンプル実装をご覧になり、それを部分的なレンダリングに基づいたソリューションと比較することで、必然的に著者と同じ結論に到達されることを期待しています。

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

Dino Esposito は、『Programming ASP.NET 3.5 Core References』の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。


実践的なパターン
開放/閉鎖原則
Jeremy Miller

目次
単一責任の原則
Chain of Responsibility (責任の連鎖) パターン
二重ディスパッチ
リスコフの置換原則
クロージャの発見
このコラムは、ソフトウェア設計の基礎に関する新しい MSDN® Magazine コラムの第 1 回です。このコラムでは、特定のツールやライフサイクルの方法に依存しない設計のパターンおよび原則について説明します。ここでは、あらゆるテクノロジやプロジェクトでより優れた設計を行うための基本的な知識をお伝えします。
まずは、Robert C. Martin 氏の著書『アジャイルソフトウェア開発の奥義』(ソフトバンククリエイティブ、2004 年) で広まった開放/閉鎖原則とこれに関連する概念について説明します。タイトルの "アジャイル" という語句を見て、背を向けないでください。この本には優れたソフトウェア設計のための有益な情報が満載されています。
これまでの経験を振り返ってみてください。新しいアプリケーションを一から作成した回数と、既存のコードベースに新機能を追加した回数のどちらが多いですか。既存のコードベースに新機能を追加している場合の方がはるかに多いという読者が少なくないのではないでしょうか。
では、もう 1 つ質問です。新規にコードを作成するのと、既存のコードを変更するのとではどちらが簡単ですか。筆者は、多くの場合に、古いコードを解読して変更の必要な箇所を探すより、新しいメソッドやクラスを作成した方がはるかに楽だと感じます。古いコードを変更する場合は、既存の機能を損なうリスクがあります。新しいコードを作成する場合は、一般に新機能のみをテストすれば済みます。ところが、古いコードを変更する場合は、変更した箇所をテストするだけでなく、既存のコードの回帰テストを行って、既存コードが損なわれていないことを確認する必要があります。
普段、既存のコードベースを使用している読者にとっても、新規にコードを作成する方が古いコードを変更するよりも容易なはずです。既存のコードベースを拡張する場合に、まったく新規にコードを作成する場合と同様に生産性が高く、ストレスのない方法があればよいと思いませんか。そこで登場するのが開放/閉鎖原則です。開放/閉鎖原則とは、ソフトウェア エンティティは拡張に対して開いていなければならず、変更に対しては閉じていなければならないという考え方です。
一見、矛盾する考え方のようですが、矛盾ではありません。これは、アプリケーションを構築する場合、新機能を追加する際に既存コードの変更を最小限に抑える必要があるという意味です。筆者は以前、開放/閉鎖原則とはプラグインを使用することだと解釈していましたが、実際はそれだけではない奥深い概念です。
特に避けたいのは、1 か所の単純な変更がアプリケーションのさまざまなクラスに影響することです。このようなシステムは脆弱で、回帰の問題が生じやすく、拡張のコストが高くつきます。変更を切り離すには、一度作成したら変更する必要のない方法でクラスやメソッドを記述する必要があります。
では、変更を切り離すようなコードを作成するにはどうすればよいでしょうか。それには、まず単一責任の原則に従うことです。

単一責任の原則
開放/閉鎖原則に従って、クラスやメソッドを一度作成したら、その後は一切手を加えず、安心して処理を任せ、後戻りして変更を加えないで済めばいいと思います。開放/閉鎖原則を完璧に実現することは不可能だとしても、関連する単一責任の原則を積極的に守ることによって開放/閉鎖原則に近づくことができます。その単一責任の原則とは、クラスを変更する理由は 1 つしか存在してはならないというものです。
変更が一切不要なクラスを作成する簡単な方法の 1 つは、単一機能のみのクラスを作成することです。そうすれば、その唯一の機能を変更する必要が生じた場合以外にクラスを変更する必要はありません。図 1 は、単一責任の原則に従わなかった場合のサンプルを示しています。このようなシステムを設計することはまずないだろうと思いますが、このようなコードを作成してはいけない理由を確認しておくのは有益です。
図 1 機能が多すぎるクラス
public class OrderProcessingModule {
  public void Process(OrderStatusMessage orderStatusMessage) {
    // Get the connection string from configuration
    string connectionString =
      ConfigurationManager.ConnectionStrings["Main"]
      .ConnectionString;

    Order order = null;

    using (SqlConnection connection =
      new SqlConnection(connectionString)) {
      // go get some data from the database
      order = fetchData(orderStatusMessage, connection);
    }

    // Apply the changes to the Order from the OrderStatusMessage
    updateTheOrder(order);

    // International orders have a unique set of business rules
    if (order.IsInternational) {
      processInternationalOrder(order);
    }

    // We need to treat larger orders in a special manner
    else if (order.LineItems.Count > 10) {
      processLargeDomesticOrder(order);
    }

    // Smaller domestic orders
    else {
      processRegularDomesticOrder(order);
    }

    // Ship the order if it's ready
    if (order.IsReadyToShip()) {
      ShippingGateway gateway = new ShippingGateway();

      // Transform the Order object into a Shipment
      ShipmentMessage message =
        createShipmentMessageForOrder(order);
      gateway.SendShipment(message);
  }
}
OrderProcessingModule は処理が多すぎます。このモジュールでは、データ アクセス、構成ファイル情報の取得、注文処理に関するビジネス ルールの実行 (おそらく、この処理だけでも複雑です)、完了した注文の出荷への変換を行っています。このような OrderProcessingModule を作成した場合、絶えずこのコードに手を加えて変更を行う必要が生じる可能性があります。システム要件の変更が多い場合は、OrderProcessingModule のコードに対する変更はさらに増え、システム変更のリスクとコストが高まります。
1 本の膨大なコードを作成するのではなく、単一責任の原則に従って OrderProcessingModule クラス全体を関連する複数のクラスから成るサブシステムに分割し、各クラスがそれぞれ専用の機能を果たすようにする必要があります。たとえば、データ アクセス機能全体を OrderDataService という名前の新しいクラスに記述し、注文のビジネス ロジックを別のクラスに記述する方法があります (詳細については次のセクションで説明します)。
開放/閉鎖原則の観点から見た場合、ビジネス ロジックとデータ アクセスの役割を別々のクラスに分割すると、他方に影響を与えずにどちらか一方を独立して変更できます。物理的なデータベースの配置が変更され、データ アクセスの処理を完全に異なるものに置き換えることになっても (拡張に対して開いている)、注文ロジック クラスは不変です (変更に対して閉じている)。
単一責任の原則の要点は、単にクラスやメソッドを小さくすることではありません。重要なのは、各クラスが一貫性のある関連機能を実装することです。単一責任の原則に従う簡単な方法は、クラスのすべてのメソッドおよび処理がそのクラスの名前に直接関連しているかどうかを常に確認することです。クラス名に適さないメソッドが見つかった場合は、そのメソッドを別のクラスに移動することを検討する必要があります。

Chain of Responsibility パターン
ビジネス ルールは、システムの他のどの部分よりもコードベースのライフサイクルでの変更が多いと考えられます。OrderProcessingModule クラスには、受け取った注文の種類に応じて分岐する次のようなやや長い注文処理ロジックがありました。
if (order.IsInternational) {
  processInternationalOrder(order);
}

else if (order.LineItems.Count > 10) {
  processLargeDomesticOrder(order);
}

else {
  processRegularDomesticOrder(order);
}
実際の注文処理システムは、ビジネスの成長に伴って対応する注文の種類を増やせるように拡大する可能性が高く、政府機関や得意客、週替わりサービスなどを対象とする特例ケースが増えていくことが予想されます。既存のビジネス ルールを損なうリスクを負わずに新しい注文処理ロジックを作成し、テストできることは大きなメリットです。
そこで、図 2 のような形式の Chain of Responsibility パターンを使用した注文処理の例で開放/閉鎖原則について詳しく検討してみましょう。まず、元の OrderProcessingModule の各条件分岐を分割し、IOrderHandler インターフェイスを実装する次のような個別のクラスにします。
public interface IOrderHandler {
  void ProcessOrder(Order order);
  bool CanProcess(Order order);
}
図 2 Chain of Responsibility の例
public class OrderProcessingModule {
  private IOrderHandler[] _handlers;

  public OrderProcessingModule() {
    _handlers = new IOrderHandler[] {
                new InternationalOrderHandler(),
                new SmallDomesticOrderHandler(),
                new LargeDomesticOrderHandler(),
    };
  }

  public void Process (OrderStatusMessage orderStatusMessage,
    Order order) {
    // Apply the changes to the Order from the OrderStatusMessage
    updateTheOrder(order);

    // Find the first IOrderHandler that "knows" how
    // to process this Order
    IOrderHandler handler =
      Array.Find(_handlers, h => h.CanProcess(order));

    handler.ProcessOrder(order);
  }

  private void updateTheOrder(Order order) {
  }
}
次に、Order の種類ごとに IOrderHandler の実装を個別に作成し、その Order の処理方法が指定されているモジュールに Order を処理させるロジックを追加します。
このようにビジネス ロジックを Order の種類ごとに別々のハンドラ クラスに分離しておけば、他の種類の Order のルールを破壊する心配なく特定の種類の Order のビジネス ルールを変更できます。また、新しい注文処理の種類を追加する場合にも、既存コードの変更を最小限に抑えることができます。
たとえば、後からシステムに政府からの注文のサポートを追加する必要が生じたとします。この場合、Chain of Responsibility パターンを使用して、IOrderHandler インターフェイスを実装する新しいクラス GovernmentOrderHandler を作成できます。GovernmentOrderHandler が目的どおりに機能することを確認したら、この新しい政府注文処理ルールを追加するには、次のように OrderProcessingModule のコンストラクタ関数の 1 行を変更するだけで済みます。
public OrderProcessingModule() {
  _handlers = new IOrderHandler[] {
              new InternationalOrderHandler(),
              new SmallDomesticOrderHandler(),
              new LargeDomesticOrderHandler(),
              new GovernmentOrderHandler(),
  };
}
開放/閉鎖原則に従って注文処理ルールを処理することによって、今までよりもずっと簡単に新しい種類の注文処理ロジックをシステムに追加できるようになりました。政府の注文ルールを追加する際に、全種類の注文処理を 1 つのクラスに実装した場合と比べて、他の注文の種類が不安定になるリスクがはるかに低くなりました。

二重ディスパッチ
では、将来、手順が複雑化した場合はどうなるでしょうか。純粋なポリモーフィズムでは将来起こり得るあらゆる変更に対応しきれない場合はどうしたらよいでしょう。このような場合は、二重ディスパッチと呼ばれるパターンを使用して、既存のインターフェイス コントラクトを破壊しない方法で変更をサブクラスに抽出できます。
たとえば、なんらかの種類のメイン パネルで一度に 1 つの画面を表示する複合デスクトップ アプリケーションを作成するとします。この場合、アプリケーションで新しい画面を開くたびに多くの処理を実行する必要があります。使用可能なメニューの変更、既に開いている画面の状態確認、画面全体の表示のカスタマイズ、なんらかの方法での新しい画面の表示が必要になる可能性があります。
通常、筆者は自分のデスクトップ クライアント アーキテクチャになんらかの種類の Model View Presenter (MVP) パターンを使用し、通常は Application Controller パターンを使用してアプリケーションのさまざまな MVP トライアドを調整しています。Application Controller での MVP の使用を考えると (MVP の詳細については、Jean-Paul Boodhoo による MSDN Magazine のコラム「Design Patterns」(msdn2.microsoft.com/magazine/cc188690) を参照)、画面のアクティブ化には次の 3 つの基本要素が関連します。
  1. 単一画面用のプレゼンタ。各プレゼンタは、単一の個別画面に関するすべての情報を得ています。
  2. アプリケーションのメイン フォームの ApplicationShell。ApplicationShell は、その内部に Panel または TabControl 内の個々のビューを表示する役割を果たします。ApplicationShell にはすべてのメニューも含まれます。
  3. アプリケーションの交通整理を行う ApplicationController。ApplicationController は、ApplicationShell およびアプリケーションを通じて渡されるすべてのプレゼンタに関する情報を得ています。ApplicationController は画面のアクティブ化および非アクティブ化のライフサイクルを制御します。
アクティブ化したときに ApplicationShell にビューを表示するだけであれば、コードは図 3 のようになります。単純なアプリケーションの場合はこれで十分に機能しますが、アプリケーションが複雑になった場合はどうでしょうか。2 回目のリリースで、一部の画面がアクティブな状態でメニュー項目をメイン シェルに追加するという新しい要件が生じた場合はどうしますか。すべてではなく一部のビューのメイン画面の左端に揃えて新しいペインに追加コントロールを表示する場合はどうしたらよいでしょう。
図 3 単純なビューベースのアプリケーション
public interface IApplicationShell {
  void DisplayMainView(object view);
}

public interface IPresenter {
  // Just exposes a getter for the inner WinForms UserControl or Form
  object View { get; }
}

public class ApplicationController {
  private IApplicationShell _shell;

  public ApplicationController(IApplicationShell shell) {
    _shell = shell;
  }

  public void ActivateScreen(IPresenter presenter) {
    teardownCurrentScreen();

    // Setup the new screen
    _shell.DisplayMainView(presenter.View);
  }

  private void teardownCurrentScreen() {
    // teardown the existing screen
  }
}
さらに、アーキテクチャをプラグ可能にすることによって、新しいプレゼンタにプラグインするだけでアプリケーションに新しい画面を追加できるようにして、新しいメニューおよび左ペインの構造体の情報を既存のプレゼンタの抽象化に取り込みます。次に、新しいメニュー項目と左ペインに追加されたコントロールに対応するように ApplicationShell または ApplicationController を変更する必要があります。
考えられる解決策を図 4 に示します。IPresenter インターフェイスに新しいプロパティを追加して、新しいメニュー項目、および新しい左ペインに追加可能なコントロールをモデル化します。また、これらの新しい概念用の IApplicationShell に新しいメンバを追加します。次に、ApplicationController.ActivateScreen(IPresenter) メソッドに新しいコードを追加します。
図 4 IPresenter を拡張する試み
public class MenuCommand
{
    // ...
}
public interface IApplicationShell
{
    void DisplayMainView(object view);

    // New behavior
    void AddMenuCommands(MenuCommand[] commands);
    void DisplayInExplorerPane(object paneView);
}
public interface IPresenter
{
    object View { get; }

    // New properties
    MenuCommand[] Commands{ get; }
    object[] ExplorerViews { get; }
}
public class ApplicationController
    {
    private IApplicationShell _shell;

    public ApplicationController(IApplicationShell shell)
        {
        _shell = shell;
        }

    public void ActivateScreen(IPresenter presenter)
    {
        teardownCurrentScreen();

        // Setup the new screen
        _shell.DisplayMainView(presenter.View);

        // New code
        _shell.AddMenuCommands(presenter.Commands);
        foreach (var explorerView in presenter.ExplorerViews)
        {
        _shell.DisplayInExplorerPane(explorerView);
        }
    }

    private void teardownCurrentScreen()
    {
        // teardown the existing screen
    }
}
ところで、このソリューションは開放/閉鎖原則に従っているでしょうか。いいえ、少しも従っていません。まず、IPresenter インターフェイスを変更する必要がありました。IPresenter はインターフェイスなので、コードを再度コンパイルできるようにするためだけに、コードベース内の IPresenter インターフェイスのすべての実装を探し出し、これらの新しいメソッドの空の実装を追加する必要がありました。多くの場合、このような変更は耐えがたいものです。IPresenter を実装するコードのいずれかが自分の直接管理下にない場合は特にそうです。これについては後述します。
また、ApplicationController クラスを変更して、メイン ApplicationShell に必要になる可能性がある各画面の新しい種類のカスタマイズのすべてに関する情報を取得できるようにする必要がありました。最後に、新しいシェルのカスタマイズをサポートするために ApplicationShell を変更する必要がありました。たいした変更ではありませんでしたが、今後さらに画面のカスタマイズを追加するのはお断りです。
実際のアプリケーションでは、ApplicationShell を構成するための機能を追加しなければ、ApplicationController クラスが複雑化する可能性があります。この機能を各プレゼンタに持たせることができれば、問題はありません。
インターフェイスではなく Presenter という名前のクラスを使用することによって、各 IPresenter の実装の変更を軽減できます。この場合、図 5 のような抽象クラスに既定の実装を追加するだけで済みます。新しい動作を追加するために既存のプレゼンタの実装を変更する必要はありません。
図 5 抽象プレゼンタの使用
public abstract class BasePresenter
{
    public abstract object View { get;}

    // Default implementation of Commands
    public virtual MenuCommand[] Commands
    {
         get
         {
             return new MenuCommand[0];
         }
    }

    // Default ExplorerViews
    public virtual object[] ExplorerViews
    {
         get
         {
             return new object[0];
         }
    }
}
開放/閉鎖原則の理念に近づくもう 1 つの方法があります。こちらの方法の方がわかりやすいかもしれません。それは、IPresenter または BasePresenter 抽象の getter を設定する代わりに二重ディスパッチ パターンを使用する方法です。
現実の環境では、二重ディスパッチ パターンの予期しない例が見られます。筆者が所属するチームは新オフィスに移転したばかりで、ネットワークの問題に取り組んでいました。先週、社内のネットワークの達人から電話があり、VPN に接続するために担当者がどのような作業を行う必要があるかを筆者に伝え始めました。ところが、筆者には理解できない難解なネットワーク用語をまくしたてるので、私は受話器を担当者に渡して直接聞いてくれと言いました。
ApplicationController でこれと同じことをやってみましょう。ApplicationController を使用して ApplicationShell に表示する内容を各プレゼンタに問い合わせるのではなく、プレゼンタが仲介役をスキップして各画面に必要な処理を直接 ApplicationShell に通知します (図 6 を参照)。
図 6 二重ディスパッチ プレゼンタ
public interface IPresenter {
  void SetupView(IApplicationShell shell);
}

public class ApplicationController {
  private IApplicationShell _shell;

  public ApplicationController(IApplicationShell shell) {
    _shell = shell;
  }

  public void ActivateScreen(IPresenter presenter) {
    teardownCurrentScreen();

    // Set up the new screen using Double Dispatch
    presenter.SetupView(_shell);
  }

  private void teardownCurrentScreen() {
    // tear down the existing screen
  }
}
最初にどのような処理を行ったとしても、新しいメニューのカスタマイズと左ペインのコントロールについて ApplicationShell を変更する必要がありますが、二重ディスパッチ方式から始めれば、ApplicationController および各 Presenter に必要な変更は少なくなります。この場合、画面の概念を追加するために ApplicationController クラスまたは Presenter クラスに手を加える必要がありません。このアーキテクチャは、新しいシェルの概念の拡張に対して開いていますが、ApplicationController クラスおよび個々の Presenter クラスは変更に対して閉じています。

リスコフの置換原則
前述したように、開放/閉鎖原則の最も一般的な形は、ポリモーフィズムを使用してアプリケーションの既存部分を新しいクラスで置換する方法です。たとえば、1 日が始まる時点で、ビジネス プロセスを実行する BusinessProcess というクラスを使用していたとします。そのうちに、このクラスでデータ ソースのデータにアクセスすることが必要になりました。
public class BusinessProcess {
  private IDataSource _source;

  public BusinessProcess(IDataSource source) {
    _source = source;
  }
}
public interface IDataSource {
  Entity FindEntity(long key);
}
BusinessProcess クラスを変更しなくても、IDataSource の実装を入れ替えるだけでシステムを拡張できれば、この設計は開放/閉鎖原則に従っています。単純な XML ファイルベースのメカニズムから始めて、データベースを使用したストレージに移行し、最終的になんらかの種類のキャッシュを使用することになった場合に、BusinessProcess クラスは変更しないで済ませたいものです。それはすべて実現可能です。ただし、そのためには関連したある原則に従う必要があります。リスコフの置換原則です。
大まかに言って、抽象を受け入れる箇所で抽象の実装を使用できる場合はリスコフの置換原則に従います。BusinessProcess は、IDataSource の実装を変更しないで使用できる必要があります。BusinessProcess には、パブリック インターフェイスを使用して伝えられる情報以外には、IDataSource の内部情報を一切通知しないことも必要です。
全体像を示すために、リスコフの置換原則の失敗例を図 7 に示します。BusinessProcess クラスのこのバージョンは、FileSource のブートストラップ用に固有のロジックを使用します。また、DatabaseSource クラスに固有のエラー処理ロジックに関する情報に依存しています。IDataSource の実装は、個別のインフラストラクチャ要件のすべてを処理できるように作成します。この場合、図 8 のような BusinessProcess クラスを作成できます。
図 7 IDataSource を抽象化できない BusinessProcess
public class BusinessProcess {
  private IDataSource _source;

  public BusinessProcess(IDataSource source) {
    _source = source;
  }

  public void Process() {
    long theKey = 112;

    // Special code if we're using a FileSource
    if (_source is FileSource)  {
      ((FileSource)_source).LoadFile();
    }

    try {
      Entity entity = _source.FindEntity(theKey);
    }
    catch (System.Data.DataException) {
      // Special exception handling for the DatabaseSource,
      // This is an example of "Downcasting"
      ((DatabaseSource)_source).CleanUpTheConnection();
    }
  }
}
図 8 改善後の BusinessProcess
public class BusinessProcess {
  private readonly IDataSource _source;

  public BusinessProcess(IDataSource source) {
    _source = source;
  }

  public void Process(Message message) {
    // the first part of the Process() method

    // There is NO code specific to any implementation of     // IDataSource here
    Entity entity = _source.FindEntity(message.Key);

    // the last part of the Process() method
  }
}

クロージャの発見
あるクラスが、情報をやり取りする他のクラスのパブリック コントラクトのみに依存する場合、開放/閉鎖原則はポリモーフィズムによってのみ実現します。抽象化を使用するクラスを 1 つのセクションで特定のサブクラスにダウンキャストする必要がある場合は、開放/閉鎖原則に従っていません。
別のクラスを使用するクラスにその依存関係の内部処理に関する情報が埋め込まれている場合 (メソッドの結果が常に降順に並べ替えられることを前提にする場合など)、実際にはその依存関係を別の実装に置き換えることはできません。このような特定の実装との暗黙の結び付きは、コードの可読性を損なうので特に有害です。抽象化のコンシューマがその抽象化のパブリック コントラクト以外に依存しないようにする必要があります。
開放/閉鎖原則は直接の目標とするよりも、設計の 1 つの方向性として扱うことをお勧めします。可能と思われるすべてのコードを完全にプラグ可能な機能に変更しようとすると、処理が困難な過剰設計のシステムができあがる可能性があります。常にあらゆる面で開放/閉鎖原則を満たすコードを作成することは無理だとしても、この原則に部分的に従うだけでも有益です。

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

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


サービス ステーション
WCF ルーターを構築する (第 2 部)
Michele Leroux Bustamante

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

目次
パススルー ルーターのシナリオ
Action ヘッダーによる転送
カスタム ヘッダーを使用した転送
サービスを登録する
メッセージを検査する
ルーターとトランスポート セッション
二重ルーター
混在トランスポート セッション
2008 年 4 月号のサービス ステーションでは、呼び出し元のクライアントとターゲット サービスとの間でメッセージが透過的に流れるようにする単純なルーターの作成方法を示しました。そこでは、Windows® Communication Foundation (WCF) のアドレス指定とメッセージ フィルタに関する重要なセマンティクスについて確認し、型指定のないメッセージを処理するルーター コントラクトの設計方法を学習し、メッセージがルーターで変更されずに通過できるようにするバインドおよび動作の構成方法を学習しました。今回の記事では、議論の続きとして、より実際的なシナリオをルーターに採用する場合に問題となる、実装の詳細についてより詳しく見ていきます。

パススルー ルーターのシナリオ
第 1 部で述べたように、クライアントとサービスの間にパススルー ルーターを挿入した場合、クライアントはルーターとの間にではなくターゲット サービスとの間に関係を結ぶことになります。メッセージは確かに、ルーターが理解できるトランスポート プロトコルとメッセージ エンコーダを使用して送信する必要がありますが、メッセージの内容全体 (たとえば、セキュリティ ヘッダーおよび信頼できるセッションを含む) をルーターでは処理しません。パススルー ルーターが適用する可能性のあるわずかな処理としては、負荷分散、コンテンツベースのルーティング、メッセージ変換などがあります。
サーバー リソース間での負荷分散および作業の分配には、ネットワーク負荷分散 (NLB) が有効であり、またはハードウェア負荷分散デバイスを用いるとさらに効果的です。それでも、そのような機能を持たない環境でサーバーをホストしている場合や、直接制御できない物理インターフェイスにサービスがインストールされている場合、またはドメイン固有のロジックに基づいたルーティングが必要な場合や、アプリケーションが単純に、構成の簡単な軽量のルーティング ソリューションを必要としている場合などには、WCF ルーターを負荷分散に利用できます。そのような WCF ルーターを使用することで、同じマシン上の複数のプロセスや、マシン間で分散された複数のプロセスでホストされているサービスに対して、メッセージを配信できます。
使用する分散モデルに関係なく、負荷分散ルーターには必ずいくつかの中核機能が必要となります。サービスを負荷分散に含めるには、そのサービスを何らかの方法でルーターに登録する必要があります。ルーターは、正しくメッセージを転送するために、サービスの種類および関連付けられたエンドポイントを決定できる必要があります。また、ルーターには、古典的なラウンドロビン アプローチや、何らかの形式の優先度ベースのルーティングなど、負荷を分散するためのアルゴリズムが必要です。
負荷分散とは対照的に、サービス間のメッセージ配信がメッセージの内容に基づいて処理される場合もあります。一般に、コンテンツ ベースのルーターは、メッセージ ヘッダーまたはメッセージ本文を検査してルーティング情報を取得します。たとえば、有効なライセンス キーを持つクライアントからのメッセージは、高い処理能力を持つサーバー マシンの大きなプールに、高い優先度で転送されますが、試用ライセンスによるメッセージは、能力の比較的低いサーバーの小さなプールに転送されます。このシナリオでは、ルーターはメッセージの転送先を知る必要があるだけでなく、転送先を決定する前に各メッセージとそのヘッダーまたは本文の内容を検査する能力を持つ必要があります。以降のセクションでは、これらのシナリオをサポートする関連ルーティング機能を説明します。

Action ヘッダーによる転送
ルーターで受信されたメッセージには、メッセージを正しいサービスに転送するために利用できる 2 つのアドレス指定ヘッダーがあります。
To: このヘッダーは、エンドポイントの名前を示します。このヘッダーがターゲット サービスと一致し、ルーターと一致しない場合は、メッセージの宛先であるサービス エンドポイントの URL を示しています。
Action: このヘッダーは、メッセージの目的であるサービス操作を示しますが、必ずしも有効な URL 自体を表すものではありません。
ただし、多くの場合、To ヘッダーはルーター アドレスと一致してサービスとは一致せず、Action ヘッダーがメッセージの正しい宛先に関するより信頼できる情報源となります。Action ヘッダーは、サービス コントラクトの名前空間、サービス コントラクト名、および操作名から派生されることを思い出してください。これは、コントラクトが異なる種類のサービス間で共有されていないと仮定した場合、ルーターがターゲット サービスを一意に識別するために十分な情報となります。次に示すサービス コントラクトが、それぞれ異なる種類のサービスに実装されていると考えてください。
[ServiceContract(Namespace =
"http://www.thatindigogirl.com/samples/2008/01")]
public interface IServiceA {
  [OperationContract]
  string SendMessage(string msg);
}
[ServiceContract(Namespace =
  "http://www.thatindigogirl.com/samples/2008/01")]
public interface IServiceB {
  [OperationContract]
  string SendMessage(string msg);
}
public class ServiceA : IServiceA {...}
public class ServiceB : IServiceB{...}
図 1 に示されるように、ルーターは、各サービス コントラクトのコントラクト名前空間とメッセージの宛先であるサービス エンドポイントとの間のマッピングを利用できます。
図 1 コントラクト名前空間をサービス エンドポイントにマッピングする (クリックすると拡大画像が表示されます)
次のコードに示す辞書は、各コントラクト名前空間エントリと、使用する正しいチャネル構成設定を示す構成要素との間で、マッピングを行うよう初期化されています。
static public IDictionary<string, string> RegistrationList =
  new Dictionary<string, string>();

RegistrationList.Add(
  "http://www.thatindigogirl.com/samples/2008/01/IServiceA",
  "ServiceA");
RegistrationList.Add(
  "http://www.thatindigogirl.com/samples/2008/01/IServiceB",
  "ServiceB");
チャネルを初期化するコードは、次のようになります。
string contractNamespace =
  requestMessage.Headers.Action.Substring(0,
  requestMessage.Headers.Action.LastIndexOf("/"));

string configurationName =
  RouterService.RegistrationList[contractNamespace];

using (ChannelFactory<IRouterService> factory =
  new ChannelFactory<IRouterService>(configurationName))
{...}
このシナリオには、注意を要する重要な設計上の依存事項がいくつかあります。
  • 構成を単純にし、複数のルーター インスタンスをサポートするために、ほとんどの場合、コントラクトからサービスへのマッピングはデータベース内に保持されます。
  • コントラクトを実装するすべてのサービスでメッセージを処理できる場合を除き、サービス コントラクトを複数の種類のサービスに実装することはできません。
  • サーバー ファーム内にサービスのインスタンスが複数ある場合、各エンドポイントに対する構成は、物理的なロード バランサが後で適切に分散できるような仮想アドレスにマッピングされる必要があります。
  • アプリケーション サービス用以外のアクション ヘッダーを含むメッセージはサポートされません。
この最後の点は重要です。なぜなら、アプリケーション サービスに対して安全なセッションや信頼できるセッションが有効になっている場合、実際のアプリケーション サービス メッセージの前に、これらのセッションを確立するために追加のメッセージが送信されるためです。これらのメッセージは、それぞれのプロトコルに対して Action ヘッダーを使用し、アプリケーション サービスとは完全に独立しています。これは、メッセージの転送に、Action ヘッダーの代替手段を使用する必要があることを意味します。

カスタム ヘッダーを使用した転送
クライアントが通信しようとするアプリケーション サービスを適切に示すルーティング ヘッダーがすべてのメッセージに確実に含まれるようにするため、次に示すように、アプリケーション サービスのエンドポイント構成セクションにカスタム ヘッダーを指定できます。
<service behaviorConfiguration="serviceBehavior"
  name="MessageManager.ServiceA">
  <endpoint address="http://localhost:8010/RouterService"
    binding="wsHttpBinding" bindingConfiguration="wsHttp"
    contract="IServiceA" listenUri="ServiceA">
    <headers>
      <Route
        xmlns="http://www.thatindigogirl.com/samples/2008/01">
        http://www.thatindigogirl.com/samples/2008/01/IServiceA
      </Route>
    </headers>
  </endpoint>
</service>
カスタム ヘッダーには、名前、名前空間、および値があります。ヘッダーがこれより動的な場合もありますが、ここでは、ヘッダーはサービス コントラクトの名前空間を表すように固定されています。Route 要素がヘッダー名を示し、名前空間は xmlns 属性によって示されます。このヘッダーはエンドポイント構成の一部として指定されるため、サービスのメタデータに含まれます。そのため、クライアントがプロキシを生成するときには、次に示すように、ヘッダーを含むクライアント構成も生成します。
<client>
  <endpoint address="http://localhost:8010/RouterService"
    binding="wsHttpBinding" bindingConfiguration="wsHttp"
    contract="localhost.IServiceA" >
    <headers>
      <Route xmlns="http://www.thatindigogirl.com/samples/2008/01">
        http://www.thatindigogirl.com/samples/2008/01/IServiceA
      </Route>
    </headers>
  </endpoint>
</client>
これにより、ヘッダーの存在がクライアントのコーディング作業に対して透過となり、安全なセッションや信頼できるセッションを確立するためのメッセージも含めて、すべてのメッセージにヘッダーが含まれるようになります。ルーターは、次に示すように、どのメッセージからも名前と名前空間によってヘッダー値を取得できます。
string contractNamespace =
  requestMessage.Headers.GetHeader<string>(
  "Route",
  "http://www.thatindigogirl.com/samples/2008/01");
この実装が前の例から変更されている唯一の点は、ルーターがコントラクトの名前空間を確認する方法であり、Action ヘッダーの代わりにカスタム Route ヘッダーを使用しています。これにより、ルーターは、安全なセッションまたは信頼できるセッションに関連したメッセージを適切なサービス エンドポイントに転送できます。

サービスを登録する
アプリケーション サービスのエンドポイントをハードコーディングする代わりに、ルーターは、サービスがオンラインまたはオフラインになったときにサービスを登録または登録解除できるように、サービスのエンドポイントを公開することができます。これにより、ソフトウェアまたはハードウェアのロード バランサが存在しない場合には、アプリケーション サービスのスケール アウトが必要なときや、各エンドポイント アドレスのポートまたはマシン名が変更されたときに、ルーターの構成オーバーヘッドが低減されます。このモデルをサポートするには、次の手順を実行します。
  • ルーターに対してサービス登録コントラクトを実装し、ファイアウォールの背後のアプリケーション サービスにエンドポイントを公開します。
  • ルーターに対して登録リストを保持します。
  • 各 ServiceHost の初期化後に、サービス エンドポイントをルーターに登録します。
  • 各 ServiceHost が障害状態となったとき、または終了したときに、サービス エンドポイントをルーターから登録解除します。
図 2 に示す登録プロセスでは、物理エンドポイント アドレスにマッピングされたコントラクト名前空間を保持するエントリが追加されています。
図 2 サービスをルーターに登録する (クリックすると拡大画像が表示されます)
このアプローチの場合、登録に必要なのは、各サービス エンドポイントのコントラクト名前空間と物理アドレスだけです。図 3 に、登録および登録解除時にルーターに渡される、IRegistrationService サービス コントラクトおよび関連付けられた RegistrationInfo の詳細を示します。
図 3 IRegistrationService コントラクトとデータ コントラクト
[ServiceContract(Namespace =
  "http://www.thatindigogirl.com/samples/2008/01")]
public interface IRegistrationService {
  [OperationContract]
  void Register(RegistrationInfo regInfo);

  [OperationContract]
  void Unregister(RegistrationInfo regInfo);
}

[DataContract(Namespace =
  "http://schemas.thatindigogirl.com/samples/2008/01")]
public class RegistrationInfo {
  [DataMember(IsRequired = true, Order = 1)]
  public string Address { get; set; }

  [DataMember(IsRequired = true, Order = 2)]
  public string ContractName { get; set; }

  [DataMember(IsRequired = true, Order = 3)]
  public string ContractNamespace { get; set; }

  public override int GetHashCode()   {
    return this.Address.GetHashCode() +
    this.ContractName.GetHashCode() +
    this.ContractNamespace.GetHashCode();
  }
}
ルーターはコントラクトごとに 1 つのエントリを格納することもできますが、その場合、各コントラクトに対して複数のサービスは使用できません。複数のエントリにまたがる分散をサポートするには、ルーターは登録ごとに一意のキーを使用する必要があります。このコードでは、各エントリに対して RegistrationInfo インスタンスのハッシュ コードを一意に関連付けるための辞書を使用しています。
// registration list
static public IDictionary<int, RegistrationInfo>
  RegistrationList =
  new Dictionary<int, RegistrationInfo>();

// to register
if (!RouterService.RegistrationList.ContainsKey(
  regInfo.GetHashCode())) {
  RouterService.RegistrationList.Add(regInfo.GetHashCode(),
    regInfo);
  }

  // to unregister
  if (RouterService.RegistrationList.ContainsKey(
    regInfo.GetHashCode())) {
    RouterService.RegistrationList.Remove(
      regInfo.GetHashCode());
  }
ルーターは、メッセージを受信すると、コントラクト名前空間を収集し、辞書内で一致する適切な項目を探します。一致する項目が複数ある場合は、選択条件を使用して、適切なサービス エンドポイントにメッセージを送信します (図 4 を参照)。
図 4 メッセージと適切なエンドポイントのマッチング
string contractNamespace =
  requestMessage.Headers.Action.Substring(0,
  requestMessage.Headers.Action.LastIndexOf("/"));

// get a list of all registered service entries for
// the specified contract
var results = from item in RouterService.RegistrationList
  where item.Value.ContractNamespace.Contains(contractNamespace)
  select item;

int index = 0;
// find the next address used ...

// create the channel
RegistrationInfo regInfo = results.ElementAt<KeyValuePair<int,
  RegistrationInfo>>(index).Value;

Uri addressUri = new Uri(regInfo.Address);
Binding binding = ConfigurationUtility.GetRouterBinding    (addressUri.Scheme);
EndpointAddress endpointAddress = new EndpointAddress(regInfo.Address);

ChannelFactory<IRouterService> factory = new
  ChannelFactory<IRouterService>(binding, endpointAddress)
// forward message to the service ...
動的登録は、サービスに対する負荷分散のニーズをマシン間で分散する以外にも、同じマシンでサービスの複数のインスタンスがホストされる可能性があるような状況でも有用です。そのようなインスタンスが Windows サービスでホストされる場合は、複数のポート割り当てが必要となります。
これをサポートするには、サービスがマシンに対して動的ポート割り当てを選択する必要があります。TCP サービスの場合、これはエンドポイント構成でリッスン URI モードを Unique に設定することで実現できます。
<endpoint address="net.tcp://localhost:9000/ServiceA"
  contract=" IServiceA" binding="netTcpBinding"
  listenUriMode="Unique"/>
ただし、名前付きパイプおよび HTTP に対しては、この設定で一意のポートは選択されません。その代わりに、アドレスに GUID が付加されます。
net.tcp://localhost:64544/ServiceA
http://localhost:8000/ServiceA/66e9c367-b681-4e4f-8d12-80a631b7bc9b
net.pipe://localhost/ServiceA/6660c07e-c9f5-450b-8d40-693ad1a71c6e
TCP および HTTP サービス エンドポイントに対して一意のポートを割り当てるには、コードでベース アドレスまたは明示的なエンドポイントを初期化します。
Uri httpBase = new Uri(string.Format(
  "http://localhost:{0}",
  FindFreePort()));
Uri tcpBase = new Uri(string.Format(
  "net.tcp://localhost:{0}",
  FindFreePort()));
Uri netPipeBase = new Uri(string.Format(
  "net.pipe://localhost/{0}",
  Guid.NewGuid().ToString()));

ServiceHost host = new ServiceHost(typeof(ServiceA),
  httpBase, tcpBase, netPipeBase);
図 5 では、同じマシンでホストされた複数のサービスをルーターに登録しています。また、この図では、ルーターの単一点障害を取り除くために、インスタンス間で登録呼び出しを分散させるソフトウェアまたは物理ロード バランサが必要な場合があることが示されています。もちろん、登録リストが共有データベースに格納されることも示されています。
図 5 負荷分散ルーターをとおしてサービスを動的ポートに登録する (クリックすると拡大画像が表示されます)

メッセージを検査する
一般にルーターは元のメッセージをアプリケーション サービスに転送しますが、メッセージの内容に基づいた操作を実行する場合もあります。たとえば、コンテンツ ベースのルーティングでヘッダーまたは本文要素を検査したり、ヘッダーまたは本文要素の有効性に基づいてメッセージを拒否します。
Message 型では、アドレス指定ヘッダーを直接取得したり、カスタム ヘッダーを名前と名前空間で取得するための Headers プロパティを公開しているため、ヘッダーの検査は簡単です。次のサービス操作を考えてください。ここでは、メッセージ コントラクトを使用して、受信操作に対するカスタム LicenseKey ヘッダーを追加しています。
// operation
[OperationContract]
SendMessageResponse SendMessage(SendMessageRequest message);

// message contract
[MessageContract]
public class SendMessageRequest {
  [MessageHeader]
  public string LicenseKey { get; set; }

  [MessageBodyMember]
  public string Message { get; set; }
}
クライアントは、LicenseKey ヘッダーを含むメッセージを送信します。ライセンス キーを持っていない場合、このヘッダーは空の可能性があります。ルーターは、このヘッダーを次のようにして取得できます。
string licenseKey =
  requestMessage.Headers.GetHeader<string>(
  "LicenseKey",
  "http://www.thatindigogirl.com/samples/2008/01");
同じ LicenseKey 値がメッセージ本文内で渡される場合には、ルーターはメッセージ本文を読み取って値にアクセスする必要があります (この情報が Message 型から直接得られないため)。GetReaderAtBodyContents メソッドは、メッセージ本文を読み取るのに使用できる XmlDictionaryReader を次のように返します。
XmlDictionaryReader bodyReader =
  requestMessage.GetReaderAtBodyContents();
Message の State プロパティは、MessageType の列挙値 Created、Copied、Read、Written、または Closed になります。メッセージは Created 状態から始まり、操作に対して Message パラメータを受信したルーターはメッセージを処理しないため、状態は Created のままです。
メッセージ本文を読み取ると、要求メッセージの状態が Created から Read に遷移します。メッセージの読み取り、書き込み、またはコピーは 1 回しか行えないため、いったん読み取られたメッセージはアプリケーション サービスに転送できません。
コンテンツ ベースのルーター実装では、メッセージを読み取る前に、メッセージをバッファにコピーする必要があります。このメッセージのバッファ コピーを使用して、元のメッセージの新しいコピーを次のように作成して処理に使用できます。
MessageBuffer messageBuffer =
  requestMessage.CreateBufferedCopy(int.MaxValue);
Message messageCopy = messageBuffer.CreateMessage();
XmlDictionaryReader bodyReader =
  messageCopy.GetReaderAtBodyContents();

XmlDocument doc = new XmlDocument();
doc.Load(bodyReader);
XmlNodeList elements = doc.GetElementsByTagName("LicenseKey");
string licenseKey = elements[0].InnerText;
同じバッファを再度使用して、アプリケーション サービスに転送するメッセージを作成できます。この CreateMessage の呼び出しは、元のメッセージに基づいた新しい Message インスタンスを返します。

ルーターとトランスポート セッション
パススルー ルーターの場合、クライアントはルーターで想定しているトランスポート プロトコルおよびエンコード形式でメッセージを送信する必要があり、ルーターは、アプリケーション サービスで想定しているトランスポート プロトコルおよびエンコード形式でメッセージをアプリケーション サービスに転送する必要があります。ここまでに説明したすべてのルーティング機能は、転送の両方の側が共に HTTP (セッションあり、またはセッションなし) の場合には問題なく動作します。ただし、TCP などのトランスポート セッションを導入すると、いくつかの興味深い課題が発生します。セキュリティが無効で信頼できるセッションがないという最も単純なケースではすべてがうまくいきますが、これらの機能を追加したときに問題が生じます。
アプリケーション サービスに対してセキュリティを有効にした場合、ルーターは署名付きの To ヘッダーを提供する必要があります。これは通常、To ヘッダーをクライアントが送信した状態のまま変更しないことを意味しますが、既定では、手動アドレス指定が有効でない限り、ルーターはメッセージが送信されたときにサービスのアドレスと一致するよう To ヘッダーを変更します。たとえば、ルーターが TCP プロトコルを使用してメッセージをサービスに転送する場合、送信チャネルが要求/応答コントラクトに基づいているときには手動アドレス指定が許可されません。
信頼できるセッションが有効で、ルーターが TCP プロトコルを使用してサービスを呼び出す場合には、また別の問題が生じます。この場合は、ルーターをとおして非同期の受信確認が返されます。したがって、ルーターは非同期の受信確認を受け取るためにサービスとのセッションを維持する必要があります。その結果、クライアントは同じ非同期の受信確認を受け取るために、ルーターとの二重セッションを維持する必要があります。
どちらの問題も、セッションをサポートして受信/送信の二重チャネルを使用するルーターを実装することで、部分的に解決できます。呼び出し元のクライアントもアプリケーション サービスもこれを直接認識する必要はありません。これはルーター内の実装詳細です。ただし、これにはセッション対応のバインディングが必要であり、また、信頼できるセッションで非同期の受信確認を使用する場合には二重通信が必要となります。

二重ルーター
図 6 のコードは、クライアント、ルーター、およびアプリケーション サービスの間でメッセージが TCP を介して送信される状況をサポートするための、二重ルーター コントラクトの例を示しています。これは、従来の要求/応答ルーター コントラクトとは次のように異なります。
  • ProcessMessage が一方向操作です。
  • サービス コントラクトにセッションが必要であり、コールバック コントラクトが関連付けられています。この場合、クライアントはコールバックを実装する必要がないことに注意してください。これはルーター内部で処理されます。
  • コールバック コントラクトには、ルーター呼び出しからアプリケーション サービスへの応答を受信する 1 つの一方向メソッドがあります。また、サービスは応答がコールバック チャネルに送信されていることを認識しません。これらの応答には要求/応答メッセージを使用できます。
図 6 二重ルーター コントラクト
[ServiceContract(Namespace =
  "http://www.thatindigogirl.com/samples/2008/01",
  SessionMode = SessionMode.Required,
  CallbackContract = typeof(IDuplexRouterCallback))]

public interface IDuplexRouterService {
  [OperationContract(IsOneWay=true, Action = "*")]
  void ProcessMessage(Message requestMessage);
}

[ServiceContract(Namespace =
  "http://www.thatindigogirl.com/samples/2008/01",
  SessionMode = SessionMode.Allowed)]
public interface IDuplexRouterCallback {
  [OperationContract(IsOneWay=true, Action = "*")]
  void ProcessMessage(Message requestMessage);
}
二重ルーターのアーキテクチャを図 7 に示します。クライアントの観点から見ると、要求が送信され、同期応答を期待します。ルーターは一方向操作への要求を受信し、応答を送信するためにクライアントのコールバック チャネルを保存します。同時に、ルーターは二重チャネルを使用してメッセージを転送し、サービスから応答を受信するためのコールバック チャネルを提供します。
図 7 二重ルーターのアーキテクチャ (クリックすると拡大画像が表示されます)
サービスは要求を受信し、同期応答を送信します。この応答はルーターのコールバック チャネルで受信されます。このコールバック チャネルは、次にクライアントのコールバック チャネルを使用して、クライアントに応答を返します。最初から最後まで、操作は同期して動作しますが、ルーターはアクティビティを分離し、基になる受信および送信チャネル内で二重通信を利用してメッセージを相関付けます。
これを行うルーターの実装を図 8 に示します。要求/応答ルーターの実装に対していくつかの関連した変更があります。まず、このルーターはセッションをサポートし、二重コントラクトを実装します。ルーターがサービスにメッセージを転送するときには、DuplexChannelFactory<T> を使用して二重チャネルが作成されます。これは、サービスから応答を受信するコールバック オブジェクトを提供することを意味します。
図 8 二重ルーターの実装
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
  ConcurrencyMode = ConcurrencyMode.Multiple,
  AddressFilterMode=AddressFilterMode.Any,
  ValidateMustUnderstand=false)]

public class DuplexRouterService : IDuplexRouterService, IDisposable {
  object m_duplexSessionLock = new object();
  IDuplexRouterService m_duplexSession;

  public void ProcessMessage(Message requestMessage)  {
    lock (this.m_duplexSessionLock)  {
      if (this.m_duplexSession == null) {
        IDuplexRouterCallback callback =
          OperationContext.Current.GetCallbackChannel
          <IDuplexRouterCallback>();

        DuplexChannelFactory<IDuplexRouterService> factory =
          new DuplexChannelFactory<IDuplexRouterService>
          (new InstanceContext(null,
          new DuplexRouterCallback(callback)), "serviceEndpoint");
        factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
        this.m_duplexSession = factory.CreateChannel();
      }
    }

    this.m_duplexSession.ProcessMessage(requestMessage);
  }
  public void Dispose() {
    if (this.m_duplexSession != null) {
      try {
        ICommunicationObject obj = this.m_duplexSession as
          ICommunicationObject;
        if (obj.State == CommunicationState.Faulted)
          obj.Abort();
        else
          obj.Close();
      }
      catch {}
    }
  }
}

public class DuplexRouterCallback : IDuplexRouterCallback {

  private IDuplexRouterCallback m_clientCallback;

  public DuplexRouterCallback(IDuplexRouterCallback clientCallback) {
    m_clientCallback = clientCallback;
  }

  public void ProcessMessage(Message requestMessage) {
    this.m_clientCallback.ProcessMessage(requestMessage);
  }
}
コールバック オブジェクトはコールバック コントラクトを実装し、サービスから応答を受信します。このコールバック オブジェクトは、クライアントのコールバック チャネルを使用して、クライアントに応答を返します。
ルーターのサービス インスタンス、クライアントのコールバック チャネル参照、およびルーターのコールバック チャネルはすべて、クライアントとのセッションの期間中ずっと存在しています。そのため、これがうまく機能するには、ルーターがセッションをサポートするエンドポイントを公開し、ダウンストリームのサービスがセッションをサポートする必要があります。

混在トランスポート セッション
状況によっては、ルーターが TCP 経由でアプリケーション サービスにメッセージを転送する一方で、クライアントは HTTP 経由でルーターにメッセージを送信する方が望ましい場合もあります。セキュリティ機能または信頼できるセッションが有効になっている場合は、二重ルーター構成でもこの状況をサポートするには不十分です。
前に述べたように、手動アドレス指定は要求/応答チャネルでのみサポートされています。それ以外の場合、サービス モデルはアドレス指定機能を使用してメッセージを相関付けます。TCP は要求/応答をネイティブではサポートしないため、コントラクトが一方向でない限り、手動アドレス指定はオプションとして選択できません。したがって、図 7 の送信チャネルは、IDuplexRouterService のような一方向コントラクトから作成する必要があります。コールバック チャネルは、応答を受信するために提供されます。
また、ルーターのコールバック チャネルは応答が送信されるまで保持し、クライアントのコールバック チャネルも同様に保持される必要があります。これをサポートするには、クライアントがルーターとのセッションを確立し、ルーターがサービスとのセッションを確立する必要があります。
ルーターによって呼び出されるアプリケーション サービスが安全であると仮定すると、ルーターで変更を加えずにメッセージを転送するには手動アドレス指定が必要になると考えられます。ルーターが TCP 経由でアプリケーション サービスを呼び出す場合には、前に述べたように二重ルーター実装が必要であるため、送信呼び出しは一方向チャネルとすることができます。これにより、クライアントは強制的にセッション対応のバインディングをとおしてメッセージを送信するようになり、これは安全なセッションまたは信頼できるセッションが HTTP 経由で有効になることを意味します。
ルーターがパススルー ルーターである場合、必要なことは、セキュリティおよび信頼できるセッションのヘッダーをアプリケーション サービスで処理させることです。HTTP 経由のセッションをサポートするために、ルーターがクライアント エンドポイントに対して安全なセッションまたは信頼できるセッションを必要とする場合は、ルーターがこれらのヘッダーを処理し、アプリケーション サービスとの間にセッションは確立されません。
したがって、チャネル層の内部にアクセスして既定の動作を上書きしない限り、プロトコルの混在は限定された状況でのみ機能します。セキュリティおよび信頼できるセッションが無効の場合、ルーターが TCP 経由でアプリケーション サービスにメッセージを転送する一方で、クライアントは HTTP 経由でルーターにメッセージを送信します。セキュリティまたは信頼できるセッションが有効である場合は、ルーター チャネルに対して信頼できるセッションや安全なセッションを有効にしないでセッションを確立できるようにするため、クライアントは TCP 経由でルーターにメッセージを送信する必要があります。

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

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


基礎
ビットマップとピクセル ビット
Charles Petzold

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

目次
RenderTargetBitmap を使用する
WriteableBitmap を使用する
ピクセルの配列
ビットマップ ピクセル形式
PixelFormats の誤記
プリマルチプライ済みアルファ
WriteableBitmap アプリケーション
Windows® Presentation Foundation (WPF) の保持モード グラフィックス システムによって、Windows グラフィックスのプログラミングに変革がもたらされました。システムで要求されたときに、プログラムは画面にビジュアルな外観を再作成する必要がなくなりました。コンポジション システムは、すべてのグラフィック図形を保持し、それらを総合的なビジュアル プレゼンテーションに配置します。
保持モード グラフィックスによって便利になったのは確かですが、Windows プログラマにとって、便利さは優先事項ではありませんでした。これは実際には、保持モード グラフィックス システムと、WPF の柔軟性と能力を解放する依存関係プロパティなどの通知メカニズムの組み合わせです。パスやブラシなどのグラフィカル オブジェクトは、コンポジション システム内でまだ "動作" し、プロパティの変更とグラフィックスの変換に応答し続けているように見えるため、これらのオブジェクトをデータ バインドやアニメーションのターゲットにすることができます。
筆者は最近、WPF ビットマップが同様の動的な品質を持つことを発見しました。レンダリングされたビットマップは、依然として変更に (だれもが知っていたグラフィックス変換だけでなく、ビットマップ内の実際のピクセル ビットの変更にも) 応答します。
この動的な応答性を明らかにする 2 つのビットマップ クラスは、WPF のすべてのビットマップ サポートの基礎となる抽象クラス BitmapSource から派生する 9 つのクラスのうちの、RenderTargetBitmap と WriteableBitmap です。プログラムがこれらのビットマップ オブジェクトのどれかをどのように使用するか、つまり Image 要素で表示するか、ImageBrush クラスで並べて表示されたブラシにするか、ImageDrawing クラスでより大きい描画 (場合によってはベクタ グラフィックスとの混合) の一部にするかに関係なく、ビットマップがただレンダリングされて忘れ去られることはまずありません。ビットマップはビジュアル コンポジション システム内で引き続き動作し、アプリケーションの変更に応答し続けます。
バーチャル ラボ ビットマップ、ピクセル、および WPF
WPF ビットマップは、グラフィックス変換と、ビットマップ内のピクセルに対する変更の両方を通じて、変更に応答します。このバーチャル ラボでは、ビットマップをアプリケーション内の変更に応答させる RenderTargetBitmap クラスと WriteableBitmap クラスを実験できます。Charles Petzold のサンプル コードを含め、環境はすべて設定されているため、この初回の基礎的なコラムを読んだらすぐにコーディングを開始できます。

バーチャル ラボでの実験 :
go.microsoft.com/fwlink/?LinkId=117105
RenderTargetBitmap を使用する
RenderTargetBitmap は、Visual 型のオブジェクトをそのサーフェイスに転送することで効果的に描画できるビットマップです。RenderTargetBitmap 型の新しいオブジェクトを作成する唯一の方法は、ビットマップのピクセル寸法、インチあたりのドット数 (dpi) 単位での水平および垂直解像度、および PixelFormat 型のオブジェクトを必要とするコンストラクタを使用することです。
PixelFormat 構造と、関連する静的 PixelFormats クラスについて、簡単に追加説明します。RenderTargetBitmap 型のオブジェクトを作成するには、PixelFormats.Default または PixelFormats.Pbgra32 を RenderTargetBitmap コンストラクタの最後の引数として使用する必要があります。どちらの場合も、ピクセルあたり 32 ビットで透明度のあるビットマップを作成します。
最初は、RenderTargetBitmap オブジェクトは完全に透明です。Visual 型のオブジェクト (FrameworkElement や Control など、Visual から派生するクラスを含む) で Render メソッドを呼び出して、ビットマップ上で描画できます。Clear を呼び出すと、すべて透明なイメージを復元できます。ビットマップが現在表示されている場合、これらの呼び出しは、表示されるビットマップに反映されます。面倒なことは何もありません。
図 1 に、RenderTargetBitmap を示す小さい完全なプログラムを示します。このプログラムは、幅が 1,200 ピクセルで高さが 900 ピクセルのビットマップを作成します。これらは、ビットマップ オブジェクトの PixelWidth および PixelHeight プロパティになります。各ピクセルの幅は 4 バイトなので、ビットマップは 4 MB を超えるメモリを占有します。
図 1 RenderTargetBitmapDemo
class RenderTargetBitmapDemo : Window
{
    RenderTargetBitmap bitmap;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new RenderTargetBitmapDemo());
    }
    public RenderTargetBitmapDemo()
    {
        Title = "RenderTargetBitmap Demo";
        SizeToContent = SizeToContent.WidthAndHeight;
        ResizeMode = ResizeMode.CanMinimize;

        // Create RenderTargetBitmap
        bitmap = new RenderTargetBitmap(1200, 900, 300, 300,
                                        PixelFormats.Default);

        // Create Image for bitmap
        Image img = new Image();
        img.Stretch = Stretch.None;
        img.Source = bitmap;
        Content = img;
    }
    protected override void  OnMouseDown(MouseButtonEventArgs args)
    {
        Point ptMouse = args.GetPosition(this);
        Random rand = new Random();
        Brush brush = new SolidColorBrush(
            Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256),
                                                (byte)rand.Next(256)));

        DrawingVisual vis = new DrawingVisual();
        DrawingContext dc = vis.RenderOpen();
        dc.DrawEllipse(brush, null, ptMouse, 12, 12);
        dc.Close();

        bitmap.Render(vis);
    }
    protected override void OnClosed(EventArgs args)
    {
        PngBitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bitmap));
        FileStream stream = new FileStream("RenderTargetBitmapDemo.png",
                                      FileMode.Create, FileAccess.Write);
        enc.Save(stream);
        stream.Close();
    }
}
RenderTargetBitmap コンストラクタの呼び出しも、300 dpi の解像度を指定します。ピクセル寸法と解像度の組み合わせによって、4 インチの幅と 3 インチの高さのビットマップが作成されます。デバイスに依存しない WPF 座標系は 1 インチが 96 単位なので、ビットマップのデバイスに依存しない幅は 384 単位になり、高さは 288 単位になります。これらは、BitmapSource で定義されている Width プロパティと Height プロパティを調べた場合に見られる数値です。
RenderTargetBitmapDemo プログラムは、Image 要素を使用して、ビットマップを拡大しないで表示します。MouseDown イベントもトラップします。マウスをクリックするたびに、プログラムでは、直径 1/4 インチの小さい塗りつぶされた円の DrawingVisual オブジェクトが作成され、Render メソッドを呼び出してこれをビットマップ イメージに追加します。表示されるビットマップに円が表示されます。このプログラムは、単一のビットマップにレンダリングと格納の機能を結合する基本的なペイント アプリケーションと考えることができます。
Image 要素では、OnRender メソッド中に渡される DrawingContext オブジェクトの DrawImage メソッドを呼び出すことで、ビットマップが表示されることがわかります (または、推測できます)。RenderTargetBitmapDemo プログラムがビットマップを変更したときに、Image クラスは OnRender メソッドの呼び出しを繰り返しません。ビットマップに対するこれらの変更は、ビジュアル コンポジション システムのより深いところで発生します。
RenderTargetBitmapDemo プログラムは、プログラムの終了時に結果のビットマップを PNG ファイル形式で保存します。RenderTargetBitmapDemo から保存されたビットマップを調べると、サイズが 1,200 × 900 ピクセルであり、各円の直径が 75 ピクセル (300 dpi の単位で 1/4 インチ) であることがわかります。
WriteableBitmap を使用する
WriteableBitmap クラスには、2 つのコンストラクタがあります。その 1 つは、RenderTargetBitmap コンストラクタに非常によく似ています。最初の 4 つの引数は、ビットマップのピクセル寸法と dpi 単位での解像度です。5 番目の引数は PixelFormat オブジェクトですが、RenderTargetBitmap よりもはるかに柔軟性があります。WriteableBitmap コンストラクタには、カラー パレットを必要とするビットマップ形式に対して、カラー パレット用の追加パラメータがあります。
WriteableBitmap のピクセルは、すべてゼロに初期化されます。この意味は、ピクセル形式によって異なります。多くの場合、ビットマップは完全に黒です。ビットマップが透明度をサポートしている場合、ビットマップは透明になります。ビットマップにカラー パレットがある場合は、ビットマップ全体がパレットの最初の色になります。
WriteableBitmap の変更は、RenderTargetBitmap の変更と大きく異なります。WriteableBitmap の場合は、WritePixels という名前のメソッドを呼び出す必要があります。このメソッドは、実際のピクセル ビットをローカル配列からビットマップにコピーします。明らかに、配列内のデータの形式とサイズがビットマップの寸法およびピクセル形式と一致することが重要です。
比較的簡単な例から始めましょう。図 2 に示す AnimatedBitmapBrush.cs プログラムは WriteableBitmap を作成し、作成した WriteableBitmap を、ウィンドウの Background プロパティに設定する並べて表示された ImageBrush の基礎として使用します。プログラムは、100 ミリ秒のタイマを設定し、WritePixels を繰り返し呼び出してビットマップを変更します。
図 2 AnimatedBitmapBrush
class AnimatedBitmapBrush : Window
{
    const int COLS = 48;
    const int ROWS = 48;

    WriteableBitmap bitmap;
    byte[] pixels = new byte[COLS * ROWS];
    byte pixelLevel = 0x00;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new AnimatedBitmapBrush());
    }
    public AnimatedBitmapBrush()
    {
        Title = "Animated Bitmap Brush";
        Width = Height = 300;

        bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
                                     PixelFormats.Gray8, null);
        ImageBrush brush = new ImageBrush(bitmap);
        brush.TileMode = TileMode.Tile;
        brush.Viewport = new Rect(0, 0, COLS, ROWS);
        brush.ViewportUnits = BrushMappingMode.Absolute;
        Background = brush;

        DispatcherTimer tmr = new DispatcherTimer();
        tmr.Interval = TimeSpan.FromMilliseconds(100);
        tmr.Tick += TimerOnTick;
        tmr.Start();
    }

    void TimerOnTick(object sender, EventArgs args)
    {
        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++)
            {
                int index = row * COLS + col;

                double distanceFromCenter =
                    2 * Math.Max(Math.Abs(row - ROWS / 2.0) / ROWS,
                                 Math.Abs(col - COLS / 2.0) / COLS);
                pixels[index] =
                    (byte)(0x80 * (1 + distanceFromCenter * pixelLevel));
            }

        bitmap.WritePixels(new Int32Rect(0, 0, COLS,ROWS),pixels,COLS,0);
        pixelLevel++;
    }
}
定数 COLS および ROWS の値は、このビットマップのピクセル寸法を定義します。これらの同じ値は、並べて表示されたブラシ Viewport の四角形およびタイマの Tick イベント ハンドラでも使用されます。ピクセル形式は PixelFormats.Gray8 に設定されます。この形式は、ビットマップの各ピクセルが、グレー シェードを示す 8 ビット値 (ピクセル値 0x00 は黒で、0xFF は白) で表現されることを意味します。Gray8 形式にパレットは不要なので、WriteableBitmap コンストラクタの最後の引数は null に設定されます。
各ピクセルは 1 バイトなので、ピクセルと言い表したバイト配列の寸法は、単純に COLS × ROWS として計算されます。このピクセル配列はフィールドとして定義されるので、Tick イベント ハンドラを呼び出すたびに再作成する必要はありません。配列内のデータは、一番上の行の左から開始して右に進み、次に 2 行目の左から右に進みます。以下も同様です。Tick イベント ハンドラには、ビットマップの行と列のループが 2 つありますが、この 2 つの値は 1 次元配列インデックスに結合されます。
int index = row * COLS + col;
WritePixels は多次元配列を受け付けません。WritePixels の最初の引数は、更新するビットマップの四角形のサブセットを示す、ピクセル座標の単位の Int32Rect 構造です。Int32Rect オブジェクトの X および Y プロパティは、ビットマップの左上隅との相対で四角形の左上隅の座標を示します。Width および Height プロパティは、この四角形のピクセル寸法を示します。ビットマップ全体を更新する場合は、X と Y を 0 に設定し、Width と Height をビットマップの PixelWidth および PixelHeight プロパティに設定します。WritePixels の最後の 2 つの引数について簡単に説明します。
正直に言うと、このビットマップには少々異なるアニメーション パターンをコーディングするつもりでしたが、どちらかと言えばここで偶然に発見したパターンの方が (少なくとも少しは) おもしろそうです。イメージの 1 つを図 3 に示します。
図 3 AnimatedBitmapBrush の表示
ピクセルの配列
WritePixels を呼び出すしくみは、ビットマップに 1 バイト/ピクセル形式がない場合、およびビットマップの四角形のサブセットのみが更新される場合には複雑になる可能性があります。同じ配列形式のピクセル ビットは、新しいビットマップを作成するために静的 BitmapSource.Create メソッドで、また、ビットマップから配列にピクセル ビットをコピーするために BitmapSource の CopyPixels メソッドで必要になるので、学習する必要がある重要な手法です。3 つのすべてのメソッドで、代わりに IntPtr を使用してローカル バッファを指すことができますが、ここでは配列アプローチに注目します。
ビットマップの合計ピクセル数は、BitmapSource クラスで定義されている PixelWidth プロパティと PixelHeight プロパティの積です。BitmapSource は、PixelFormat 型の取得専用 Format プロパティも定義します。これは、1 ～ 128 の範囲の BitsPerPixel という名前の取得専用プロパティを定義します。極端な例として、単一バイトが 8 つの連続ピクセルのデータを格納する場合や、各ピクセルが 16 バイトのデータを必要とする場合があります。多くの場合、ピクセル ビットは byte、ushort、uint、または float の配列に制限します。
WritePixels メソッドに提供する Int32Rect オブジェクトは、ビットマップ内の四角形のサブセットを定義します。ピクセル配列内のバイト数には、Int32Rect オブジェクトによって示される行と列の数に対して十分なデータが含まれている必要があります。いくつかのピクセル形式では 1 バイトに複数のピクセルを格納するので、これは少々複雑になります。そのようなピクセル形式では、データの各行をバイト境界で開始する必要があります。
たとえば、4 ビット/ピクセルの形式のビットマップを処理していると想定します。アクセスまたは更新するビットマップの四角形領域は、幅が 5 ピクセル、高さが 12 ピクセルです。これらは、指定する Int32Rect オブジェクトの Width プロパティと Height プロパティです。配列内の最初のバイトには、最初の 2 ピクセルのデータが含まれ、2 番目のバイトには次の 2 ピクセルのデータが含まれますが、3 番目のバイトには、最初の行の 5 番目のピクセルのデータのみが含まれます。次のバイトは、2 行目の最初の 2 ピクセルに対応します。
データの各行には 3 バイトが必要であり、四角形領域全体に 36 バイトが必要です。この計算に役立てるために、WritePixels メソッドは stride という引数を必要とします。これは、ピクセル データの各行のバイト数です。stride の一般的な計算は次のとおりです。
int stride = (width * bitsPerPixel + 7) / 8;
幅は Int32Rect 構造の Width プロパティと同じです。ushort、uint、または float 値の配列を使用する場合でも、stride 値は常にバイト単位です。さらに、このような配列の合計バイト数を計算できます。
int dimension = height * stride;
ushort、uint、または float の配列を使用している場合は、それぞれ 2、4、または 8 で除算します。
Windows API では、ビットマップ データの各行を 32 ビット メモリ境界で開始する必要があり、そのために stride を 4 の倍数にする必要があることを思い出してください。これは WPF では不要です。ただし、必要に応じて、式で計算された値よりも大きい stride を設定できます。たとえば、ピクセルあたり 1 バイトのビットマップを処理している場合に、配列が byte ではなく uint 型であるとします。この場合、配列内の各要素には 4 ピクセルが格納されます。現在の多くのハードウェア プラットフォームでは通常、整列されたコピーの方が整列されていないコピーよりも高速なので、厳密には必要がない場合でも配列の各行を単位境界で開始します。
ビットマップ ピクセル形式
ビットマップ内の各ピクセルは、そのビットマップの色を定義する 1 つ以上のビットによって表現されます。WPF では、特定のピクセル形式が構造型 PixelFormat のオブジェクトによって表現されます。静的 PixelFormats クラスは、ビットマップの作成時に使用できる PixelFormat 型の 26 個の静的プロパティを定義します。これらのプロパティを書き込み可能な形式と書き込み不可の形式という 2 つのグループに分け、図 4 に示します。3 つの例外 (Bgr555、Bgr565、および Bgr101010) がありますが、プロパティ名の数字はピクセルあたりのビット数と同じです。
図 4 PixelFormat クラスの静的プロパティ
書き込み可能な形式
Indexed1
Indexed2
Indexed4
Indexed8
BlackWhite
Gray2
Gray4
Gray8
Bgr555
Bgr565
Bgr32
Bgra32
Pbgra32
書き込み不可の形式
Default
Bgr24
Rgb24
Bgr101010
Cmyk32
Gray16
Rgb48
Rgba64
Prgba64
Gray32Float
Rgb128Float
Rgba128Float
Prgba128Float
BitmapSource.Create でビットマップを作成する場合は、PixelFormats.Default を除き、PixelFormats クラスの任意の静的プロパティを使用できます。WriteableBitmap 型のビットマップを作成する場合は、書き込み可能な形式のみを使用できます。
Indexed という単語で始まる形式は、静的 BitmapSource.Create メソッドまたは WriteableBitmap コンストラクタの ColorPalette オブジェクトを必要とする形式です。各ピクセルは ColorPalette オブジェクトのインデックスであり、この 4 つの形式は、それぞれ 2、4、16、および 256 色に関連付けられています。実際のピクセル ビットが可能な最大の大きさになることがない場合は、ColorPalette を色の最大数と同じ大きさにする必要はありません。
ピクセルあたり 8 ビットよりも少ない形式では、バイト内の最上位桁のビットが一番左のピクセルに対応します。たとえば、Indexed2 形式では、0xC9 というバイトは 2 進 11001001 と等価であり、11、00、10、および 01 の 4 つの 2 ビット値に対応します。これらは、ColorPalette コレクションの 4、1、3、および 2 番目の色に対応します。
BlackWhite、Gray2、Gray4、および Gray8 形式は、ピクセルあたり 1、2、4、または 8 ビットのグレーシェード ビットマップです。すべてゼロのピクセルは黒で、すべて 1 のピクセルは白です。
残りの 5 つの書き込み可能な形式は、色の形式です。文字 B、G、および R は、青、緑、および赤の原色を意味します。文字 A はアルファ チャネルを意味し、ビットマップが透明度をサポートすることを示します。文字 P は、後で簡単に説明するプリマルチプライ済みアルファを意味します。
Bgr555 形式と Bgr565 形式は、どちらもピクセルあたり 16 ビット (2 バイト) を必要とします。Bgr555 形式は、原色ごとに 1 ビットを残して 5 ビットを使用します (したがって、32 とおりのグラデーションが可能です)。青の原色のビットが B0 (最下位ビット) から B4 (最上位ビット) で表現され、緑と赤も同様に表現される場合は、図 5 に示すように、データの連続する 2 バイトに 3 つの原色が格納されます。
図 5 2 バイト ピクセル形式 (クリックすると拡大画像が表示されます)
緑のビットが 2 バイト全体に分散していることに注意してください。この配置は、ピクセルが実際に 16 ビットの符号なし整数で、最下位バイトが最初に格納されていることがわかっている場合に大きな意味を持ちます。図 6 に、単一の short 型整数で原色がどのようにエンコードされるかを示します。
図 6 short 型整数ピクセル形式 (クリックすると拡大画像が表示されます)
これにより、この形式に該当することが保証されるため、図 7 に示す Gradient555Demo プログラムは、この形式のビットマップを作成し、左側の青から右側の緑へのグラデーションを表示するようにピクセルを書き込みます。ushort 配列の寸法は、行と列の合計の積であることに注目してください。
図 7 Gradient555Demo
class Indexed2Demo : Window
{
    const int COLS = 50;
    const int ROWS = 20;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new Indexed2Demo());
    }
    public Indexed2Demo()
    {
        Title = "Bgr555 Bitmap Demo";

        WriteableBitmap bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
                                        PixelFormats.Bgr555, null);

        ushort[] pixels = new ushort[ROWS * COLS];

        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++)
            {
                int index = row * COLS + col;
                int blue = (COLS - col) * 0x1F / COLS;
                int green = col * 0x1F / COLS;
                ushort pixel = (ushort)(green << 5 | blue);
                pixels[index] = pixel;
            }

        int stride = (COLS * bitmap.Format.BitsPerPixel + 7) / 8;
        bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels,
                           stride, 0);

        Image img = new Image();
        img.Source = bitmap;
        Content = img;
    }
}
このコードは、ピクセルあたりのバイト数に対応する、バイト以外の型の配列を使用する利点を示しています。任意の行および列ピクセル アドレスについて、配列インデックスは、行数に列あたりのピクセル数を乗算した積を単純に列に加えた値です。
Bgr565 形式は、Bgr555 と非常によく似ていますが、緑に 6 ビットを使用する点が異なります (目は緑に最も敏感です)。残りの 3 つの書き込み可能な形式は、操作がはるかに簡単です。すべての形式が青から開始してピクセルあたり 4 バイトを使用します。Bgr32 形式では、最後の 4 バイトがゼロです。透明度はありません。他の 2 つの形式では、第 4 バイトがアルファ チャネルです。アルファ値の範囲は、透明の 0x00 から不透明の 0xFF までです。ピクセルが符号なし 32 ビット整数として扱われる場合、最下位の 8 ビットは青をエンコードし、最上位の 8 ビットはゼロまたはアルファ チャネルです。
一般に、ビットマップ内のバイトの順序は、プロパティ名の文字 B、G、R、および A の順序に対応します。書き込み不可の形式の一覧は、ピクセルあたり 16 または 24 ビットのみを使用するいくつかの色形式から始まります。Bgr101010 形式は、ピクセルあたり 32 ビットを使用しますが、各原色には 10 ビットを使用します。ピクセルが 32 ビットの符号なし整数として表現される場合、最下位の 10 ビットは青のビットです。Cmyk32 形式は、印刷に使用されるシアン、マゼンタ、黄色、および黒のレベルをエンコードします。
Gray16、Rgb48、Rgba64、および Prgba64 形式は、16 ビットのグレー シェードと 16 ビットの原色をエンコードします。医療用の画像など、この高い色精度を必要とするハードウェアとアプリケーションを処理する場合は、このような高解像度ビットマップ データを格納および表示できるようになったので便利です。ただし、それ以外で理由でこれらの形式を使用することは実際にはありません。追加の色精度は、原色あたり 8 ビットで表示する場合、または原色あたり 8 ビットのファイル形式で保存する場合には無視されます。
ピクセル形式の一覧は、色レベルと透明度を表現するために単精度浮動小数点値を使用する 4 つの形式で終わっています。これらの形式は、従来の sRGB 色空間とガンマ値 2.2 ではなく、scRGB の色空間とガンマ値 1 に基づきます (説明については、筆者の『Applications = Code + Markup』(Microsoft Press、2006 年) という著書の 24 ～ 25 ページを参照してください)。0.0 の浮動小数点色値は 0x00 (黒) のバイト値に対応し、1.0 の浮動小数点色値は 0xFF のバイト値に対応しますが、ビデオ ディスプレイよりも広い色範囲を持つ表示デバイスでは、浮動小数点色値が 1 を超える場合があります。
PixelFormats の誤記
PixelFormats のドキュメントでは、いくつかの形式に混同が見られます。Gray16、Rgb48、Rgba64、および Prgba64 形式は、すべてガンマ値 1 に基づくと記載されていますが、Gray16 を除き、逆説的に sRGB 形式であるとも記載されています。単純にそうとは言えません。Float ピクセル形式だけが、scRGB 色形式とガンマ値 1 を使用します。
ピクセルを色成分に分解するため、または色成分からピクセルを構成するために、一般化された手法が必要です。PixelFormat 構造には、Mask という名前のプロパティが含まれます。このプロパティは、PixelFormatChannelMask 型のオブジェクトのコレクションです。青、緑、赤、およびアルファの順の色チャネルごとに、1 つの PixelFormatChannelMask オブジェクトがあります。
PixelFormatChannelMask 構造は、Mask プロパティを定義します。このプロパティはバイトのコレクションであり、その数はピクセルあたりのバイト数と等しく、ピクセルのバイト順序に対応します。たとえば、Bgr555 形式の場合、それぞれ 2 バイトを持つ 3 つの PixelFormatChannelMask オブジェクトがあります。青の場合、2 バイトは 0x1F と 0x00 です。緑の場合は 0xE0 と 0x03 で、赤の場合は 0x00 と 0x7C です。このデータを使用する場合は、自身のビットシフト係数を派生させる必要があります。
BitmapSource.Create メソッドには、PixelFormats.Default を除く任意の PixelFormats メンバを使用できると述べましたが、WriteableBitmap コンストラクタには、図 4 の書き込み可能な形式のみを使用できます。WriteableBitmap のドキュメントを調べると、BitmapSource オブジェクトから WriteableBitmap オブジェクトを作成する代替コンストラクタがあることがわかります。
実際には、まず図 4 の書き込み不可の形式から BitmapSource オブジェクトを作成し、BitmapSource に基づいて WriteableBitmap を作成できます。ただし、次の制限を回避できるとは想定しないでください。書き込み不可の形式を持つビットマップは、アルファ チャネルが存在するかどうかによって Bgr32 形式または Pbgra32 形式に変換されます。
作成するビットマップは、サポートされているファイル形式 (具体的には BMP、GIF、PNG、JPEG、TIFF、および Microsoft Windows Media Photo のいずれかの形式) でファイルに保存できます。ただし、ビットマップ データはプロセスにおいて異なる形式に変換される場合があります。たとえば、GIF ファイルとして保存する場合、ビットマップは必ず最初に Indexed8 形式に変換されます。JPEG ファイルとして保存する場合、ビットマップは必ず Gray8 または Bgr32 に変換されます。現時点では、原色あたり 8 ビットを超えるデータを含むファイルを保存する PixelFormat と BitmapEncoder の組み合わせはありません。
プリマルチプライ済みアルファ
PixelFormats クラスの静的プロパティのうちの 3 つは、プリマルチプライ済みアルファ (Premultiplied Alpha) を意味する文字 P から開始します。プリマルチプライ済みアルファは、部分的に透明なピクセルのビットマップ レンダリングの効率の改善に使用される手法です。アルファ チャネルを持つビットマップにのみ適用されます。
次のように計算される色で SolidColorBrush を作成したと想定します。
Color.FromArgb(128, 0, 0, 255)
これは、50% の透明度の青いブラシです。このブラシをレンダリングする場合、色を表示画面の既存の色と結合する必要があります。黒の背景に描画すると、描画結果の RGB 色は (0, 0, 128) になります。白い背景に対する描画結果の色は (127, 127, 255) です。これは単純な加重平均計算です。
次の式のサブスクリプトは、既存のサーフェイスに部分的に透明なピクセルをレンダリングした結果を示します。
Rresult = [(255 – Apixel) * Rsurface + Apixel * Rpixel] / 255;
Gresult = [(255 – Apixel) * Gsurface + Apixel * Gpixel] / 255;
Bresult = [(255 – Apixel) * Bsurface + Apixel * Bpixel] / 255;
この計算は、ピクセルの R、G、および B の値が既に A 値で乗算され、255 で除算されている場合に高速化できます。その場合、各式の 2 番目の乗算は除外できます。たとえば、Bgra32 ビットマップ内のピクセルが ARGB 値 (192, 40, 60, 255) であるとします。Pbgra32 ビットマップでは、同じピクセルが (192, 30, 45, 192) になります。RGB 値は、既に 192 のアルファ値で乗算され、255 で除算されています。
Pbgra32 ビットマップ内の任意のピクセルについて、R、G、または B 値はいずれも A 値より大きくできません。大きくすると何も表示されません。値は最大 255 にバインドされますが、目的の透明度レベルは得られません。
WriteableBitmap アプリケーション
いくつかの単純な動的グラフィックス (たとえば棒グラフ) を表示する必要があるアプリケーションで WriteableBitmap を使用すると便利です。対応するベクタ グラフィックスが WPF で描画されるよりも高速にビットマップを更新できます。
おそらく、WriteableBitmap で最も一般的なアプリケーションはリアルタイムのイメージ処理と非線形変換の実行です。TwistedBitmap プロジェクトでは、図 8 に示すように、任意の 8 ビット/ピクセルまたは 32 ビット/ピクセルのビットマップを読み込み、Slider コントロールを使用して、その中心でイメージをねじります。最適な結果を得るためには、より小さいイメージを使用します。
図 8 ねじりを加えたビットマップ (クリックすると拡大画像が表示されます)
プログラムでは、BitmapFrame.Create を使用してファイルからビットマップを読み込み、CopyPixels を呼び出して、pixelsSrc という名前の配列内のすべてのピクセル ビットをコピーします。図 9 に示す SliderOnValueChanged イベント ハンドラは、pixelsSrc から、WritePixels の呼び出しに使用する pixelsNew という名前の配列への変換を行います。
図 9 TwistedBitmap での変換
void SliderOnValueChanged(object sender,
                          RoutedPropertyChangedEventArgs<double> args)
{
    if (pixelsSrc == null)
        return;

    Slider slider = sender as Slider;
    int width = bitmap.PixelWidth;
    int height = bitmap.PixelHeight;
    int xCenter = width / 2;
    int yCenter = height / 2;
    int bytesPerPixel = bitmap.Format.BitsPerPixel / 8;

    for (int row = 0; row < bitmap.PixelHeight; row += 1)
    {
        for (int col = 0; col < bitmap.PixelWidth; col += 1)
        {
            // Calculate length of point to center and angle
            int xDelta = col - xCenter;
            int yDelta = row - yCenter;
            double distanceToCenter = Math.Sqrt(xDelta * xDelta +
                                                yDelta * yDelta);
            double angleClockwise = Math.Atan2(yDelta, xDelta);

            // Calculate angle of rotation for twisting effect
            double xEllipse = xCenter * Math.Cos(angleClockwise);
            double yEllipse = yCenter * Math.Sin(angleClockwise);
            double radius = Math.Sqrt(xEllipse * xEllipse +
                                      yEllipse * yEllipse);
            double fraction = Math.Max(0, 1 - distanceToCenter / radius);
            double twist = fraction * Math.PI * slider.Value / 180;

            // Calculate the source pixel for each destination pixel
            int colSrc = (int) (xCenter + (col - xCenter) *
                                Math.Cos(twist)
                (row - yCenter) * Math.Sin(twist));
            int rowSrc = (int) (yCenter + (col - xCenter) *
                                Math.Sin(twist)
                + (row - yCenter) * Math.Cos(twist));
            colSrc = Math.Max(0, Math.Min(width - 1, colSrc));
            rowSrc = Math.Max(0, Math.Min(height - 1, rowSrc));

            // Calculate the indices
            int index = stride * row + bytesPerPixel * col;
            int indexSrc = stride * rowSrc + bytesPerPixel * colSrc;

            // Transfer the pixels
            for (int i = 0; i < bytesPerPixel; i++)
                pixelsNew[index + i] = pixelsSrc[indexSrc + i];
        }
    }
    // Write out the array
    bitmap.WritePixels(rect, pixelsNew, stride, 0);
}
ビットマップ変換を処理する場合は、変換を逆に実行することが不可欠です。元のビットマップのすべてのピクセルを調べ、新しいビットマップでの位置を決定する場合には、元のビットマップにある異なる複数のピクセルが、新しいビットマップの同一のピクセルにマッピングされる可能性が高くなります。つまり、新しいビットマップの一部のピクセルには値が設定されず、イメージに "穴" が開きます。
代わりに、新しいビットマップのすべてのピクセルについて、元のビットマップのどのピクセルがその行と列にマッピングされるかを調べるようにしてください。この方法を使用すると、新しいビットマップのすべてのピクセルが計算済みの値を持つことが保証されます。

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

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


Windows と C++
WIC を使用した Windows Vista アイコンのデコード
Kenny Kerr

目次
Windows アイコンの歴史
Windows アイコン形式
基本的なデコーダ
探索
次回の予定
私は MSDN® Magazine の 4 月号で、Windows® Imaging Component (WIC) の基礎について説明し、各種イメージ形式をエンコードまたはデコードする方法を示しました。今月は、独自のコーデックを記述して WIC を拡張する方法を紹介します。特に、Windows アイコン形式の代替デコーダを開発する最初の各ステップについて詳しく説明します。まず、コーデックを記述するプロセスについて解説します。
コーデックを記述するプロセスの大半はシンプルです。特定のイメージ形式のエンコードをサポートするコーデックは、IWICBitmapEncoder インターフェイスを実装する COM クラスを提供する必要があります。同様に、特定のイメージ形式のデコードをサポートするコーデックは、IWICBitmapDecoder インターフェイスを実装する COM クラスを提供する必要があります。これらは、イメージ処理に WIC を使用する開発者が使用するインターフェイスと同じです。実際には、コーデックの COM クラスは処理中に読み込まれ、開発者のアプリケーション コードにより直接アクセスされます。これにより、非常に効率的な抽象化が提供され、コンシューマは高パフォーマンスなイメージ処理を達成できます。
エンコーダは、イメージの個別のフレームをエンコードする方法を提供する IWICBitmapFrameEncode インターフェイスを実装することも必要です。複数のフレームをサポートしていないイメージ形式であっても、IWICBitmapFrameEncode インターフェイスを使用してイメージ ピクセルを表します。同様に、デコーダも、イメージの個別のフレームをデコードする方法を提供する IWICBitmapFrameDecode インターフェイスを実装する必要があります。
コーデックが既に記述されていると、コンシューマは CoCreateInstance 関数を使用してコーデックを直接作成できますが、これはあまり興味深い機能とは言えません。コーデックと WIC の真の統合を行うには、さらにいくつかの手順が必要です。特に、エンコーダおよびデコーダは、それぞれの機能を表す追加のメタデータをレジストリに含める必要があります。エンコーダまたはデコーダを含む DLL はすべて署名済みでなければなりません。メタデータを使用することにより、WIC では、コーデックから必要な機能が提供されるかどうかを識別して、特定のイメージ形式に適したエンコーダまたは特定のイメージファイル用のデコーダを見つけることができます。コンシューマがどの実装を受け取るかは実行時に確定される場合があるため、WIC はある程度の保証を実現するために、検索および抽象化のプロセスに署名済みコーデックのみを使用します。
一般的な概念がわかったところで、具体的な例を見ていきましょう。Windows アイコン形式の代替デコーダの実装を中心に説明します。アイコン形式の選択理由は 2 つあります。その 1 つは、Windows Vista® でアイコン形式に対する更新が導入されたことです。変更点については後ほど詳しく解説しますが、これは注目に値します。もう 1 つの理由は、Windows Vista に付属する組み込みのデコードが、更新されたアイコン形式を実際にサポートしていないことです。

Windows アイコンの歴史
Windows の他の多くの要素と同じように、アイコン形式にはさまざまな歴史があります。16 ビットの Windows オペレーティング システムの初期においては、デバイス依存ビットマップ (DDB) 形式のみがサポートされていました。このビットマップの場合は、幅と高さをピクセルで指定するだけでなく、ピクセルを特定のデバイスのカラー パレット内のエントリにマップするテーブルも指定します。デバイスごとに解像度や色の機能が異なるため、これらのビットマップは 1 つのデバイスから別のデバイスへと簡単には移動できません。しかし、DDB は非常に効率的でした。今でも特定の操作には使用されています。この DDB に固有の問題をすべて修正するために登場したのが、デバイス独立ビットマップ (DIB) 形式です。このビットマップは、デバイスに依存しない固有のカラー テーブルを含んでいました。ビットマップのピクセルはビットマップのカラー テーブルにマップされ、赤、緑、および青 (RGB) の色の値を使用してピクセルの色を明確に定義します。
この DIB 形式をアイコン形式に利用しようと考えるのは当然の流れですが、それには問題がありました。透明度を表すためのアルファ チャネルが、まだ使用されていなかったのです。また、当時のハードウェアで許容されるピクセルあたりのバイト数では、アルファ チャネルの要件には対応できませんでした。そこで、別のソリューションが必要になりました。DIB では、カラー ビットマップの後に追加のビットマップを保持していました。この追加のビットマックは、カラー ビットマップ内の透明なピクセルと不透明なピクセルを識別するビットマスクです。ブール型の演算子を使用してビットマスク内のビットをターゲット デバイスのサーフェイスと組み合わせる場合、BitBlt 関数でビットブロック転送を行うと、簡単かつ (さらに重要なことですが) 効率的に実行できました。
アイコン形式自体に複数の DIB 構造が含まれ、各構造に異なるサイズの関連ビットマスクを保持できるため、異なるシナリオのイメージを 1 つのアイコンで提供できました。たとえば、アイコンがデスクトップに表示される場合は 32×32 ピクセルの DIB を使用し、アイコンがウィンドウのタイトル バーに表示される場合は 16×16 ピクセルの DIB を使用します。アイコン形式で各種の表示機能を提供する必要もあったため、さまざまなサイズのカラー テーブルを含む追加の DIB もサポートされました。
最終的に、アイコンはカラー テーブルを使用せずに、赤、緑、青のそれぞれの値としてピクセルあたり 8 ビット (bpp)、合計で 24 ビットを直接格納できるようになりました。これにより、鮮明なアイコンの表示が可能になりましたが、透明度の定義には依然としてビットマスクが使用され、境界部分が非常に粗くなるという状態がしばしば発生しました。さいわい、一般的にアイコン ビットマップは小さかったため、これは大きな問題になりませんでした。
マイクロソフトでは Windows XP の開発中に、より大きく、より質の高いアイコンの導入を計画しましたが、透明度を指定するビットマスクの手法ではそれを実現できませんでした。各ピクセルが任意のレベルの透明度を持ち、シャドウのような効果を実現するには、アルファ チャネルが必要でした。Windows XP のアイコンでは 32 bpp の使用が開始され、各ピクセルに必要な透明度のレベルを指定するアルファ チャネルに適応できるようになりました。これらのアイコンでは互換性のためにビットマスクが引き続き維持されましたが、従来のアプリケーションでアルファ チャネルではなくビットマスクが誤って使用されたときは、すぐにわかりました。なぜなら、アイコンのアウトラインがぎざぎざになる箇所が数多く出現し、アイコンの設計者が指定したシャドウなどのビジュアル効果が適切に表示されないからです。ただし、Windows XP 用に適切に記述されたアプリケーションには、魅力的な 32 bpp アイコンを含めることができました。また、より大きなアイコン (最大サイズが 48×48 ピクセル) をビジュアル効果に追加することが促進され始めたのも、Windows XP のときです。
Windows Vista の開発中に、マイクロソフトは、Windows XP で取り入れたトレンドを継続できないという認識を持ちました。DIB 形式は実行時に非常に効率的ですが、ビットマップが大きくなるにつれ、ディスク上またはメモリ内で消費する領域があまりにも大きくなります。コンピュータの処理能力が向上することで、さらに高度な機能を持つイメージ形式の使用が現実的になったため、マイクロソフトでは PNG を採用しました。Windows Vista のアイコンには DIB の代わりに PNG を埋め込むことができ、これらは今では Windows 全体で使用されています。サイズが 256×256 ピクセルのビットマップを魅力的な 32 bpp の PNG アイコンで提供できるだけでなく、領域要件は DIB の数分の 1 で済みます。図 1 に、Windows Vista のアイコンをいくつか示しています。
図 1 Windows Vista の PNG ベースのアイコン (クリックすると拡大画像が表示されます)
Windows Vista では、このような高品質 PNG イメージの機能をアイコン内部に組み込むことで、アイコンをベクタ イメージのように表示することができます。この結果、アイコンを、16×16 ピクセルから 256×256 ピクセルの範囲で任意の大きさに変えることができ、その拡大縮小のようすを画面で見ることができます。この動作を確認するには、Windows エクスプローラを開き、Ctrl キーを押しながらマウス ホイールをスクロールしてください。実に見ごたえのある効果です。もちろん、Windows Vista にはさらに優れた機能もあります。フォルダの内容をプレビューできるフォルダ アイコンです。ただし、これはアイコン形式の機能と言うよりは、Windows エクスプローラの機能です。

Windows アイコン形式
アイコン形式の構造を図 2 に示します。最初の WORD (2 バイト) は常に 0 で、2 番目の WORD は常に 1 です。後者はリソースの種類であると考えることができます。アイコンとカーソルの形式は非常によく似ています。アイコンとカーソルを見分ける方法として、2 番目の WORD を確認します。アイコンの場合は常に 1 であり、カーソルでは常に 2 です。3 番目の WORD は、アイコン内に含まれるイメージの数を示しています。
図 2 アイコン形式の構造 (クリックすると拡大画像が表示されます)
これらの 3 つの WORD 値の後に、イメージを表す一連の構造が続きます。このサブストラクチャ内の必要不可欠な値は、イメージ サイズおよびイメージ オフセットの値のみです。残りの値は、設定が不適切な場合もあるため、単なるヒントとしては使用できますが、信頼することはお勧めしません。ほとんどの場合は、単純に無視してください。DIB または PNG イメージ ビット自体を調べることにより、知る必要のあるイメージの情報をすべて得ることができます。イメージ サイズは、イメージによって占有されるバイト数を示しています。イメージ オフセットは、ファイルの冒頭から開始イメージ ビットまでのオフセットを示しています。

基本的なデコーダ
ここでは、アイコン用の基本的な WIC デコーダの開発について説明します。開発の大部分は、IWICBitmapDecoder インターフェイスを適切に実装する作業ですが、最も重要な 8 つのインターフェイス メソッドに関するある程度の知識が必要になります。そのメソッドとは、QueryCapability、Initialize、GetFrame、GetContainerFormat、GetDecoderInfo、CreateComponentInfo、GetFrameCount、および GetFrame です。
QueryCapability メソッドは、指定されたストリームを調べ、イメージをデコードできるかどうかを識別します。デコード可能な場合は、イメージ内のフレームをすべてデコードできるか (WICBitmapDecoderCapabilityCanDecodeAllImages)、またはフレームのサブセットのみをデコードできるか (WICBitmap-DecoderCapabilityCanDecodeSomeImages) を識別します。その他の機能フラグが指定される場合もあります。必ず、戻る前のストリームの位置を復元してください。QueryCapability では、前のセクションで説明したアイコン形式の基本的な構造を読み取ることができます。デコーダで必要な構造になっていることを確認します。イメージが読み込み可能かどうかを確認するために、アイコン内のイメージを列挙することが必要な場合もあります。
Initialize メソッドは、デコーダの読み取り先となるストリームを提供します。指定されたオプションに応じて、すべてのフレームを即座に読み込むか (WICDecodeMetadataCacheOnLoad)、または GetFrame メソッドで個別に要求された場合のみ読み込みます (WICDecodeMetadataCacheOnDemand)。後者の場合は、すべてのフレームが読み込まれるまでストリームを保持する必要があります。
ここで、私がお勧めする便利な操作を紹介しましょう。アイコン内の各イメージのイメージ サイズおよびオフセットに基づくサブストリームを作成し、これらのサブストリームを IWICBitmapFrameDecode インターフェイス実装に渡すと便利であることがわかりました。さいわい、前回の 4 月号のコラムで説明した WIC イメージング ファクトリのオブジェクトを使用すると簡単に行うことができます。図 3 に、WIC ストリーム オブジェクトを使用してサブストリームを作成する CreateSubStream ヘルパ関数を示します。
図 3 サブストリームの作成
HRESULT CreateSubStream(IWICImagingFactory* factory,
                               IStream* stream,
                               ULONGLONG offset,
                               ULONGLONG maxSize,
                               IStream** subStream)
{
    ASSERT(0 != factory);
    ASSERT(0 != stream);
    ASSERT(0 != subStream);

    *subStream = 0;

    CComPtr<IWICStream> wicStream;
    HR(factory->CreateStream(&wicStream));

    ULARGE_INTEGER quadOffset = { 0 };
    quadOffset.QuadPart = offset;
    ULARGE_INTEGER quadMaxSize = { 0 };
    quadMaxSize.QuadPart = maxSize;

    HR(wicStream->InitializeFromIStreamRegion(stream,
                                              quadOffset,
                                              quadMaxSize));

    return wicStream.QueryInterface(subStream);
}
GetContainerFormat メソッドは、デコーダが表すイメージ形式を指定します。この場合は、単純に組み込みのアイコン コンテナ形式の識別子 (GUID_ContainerFormatIco) を返すことができます。
GetDecoderInfo メソッドは、デコーダを表す IWICBitmapDecoderInfo インターフェイスを返します。特に複雑というわけではありませんが、このインターフェイスは非常に大きく、実装が煩雑であることは確かです。さいわい、レジストリに格納されたコーデックのメタデータの抽象化としてのみ動作します。WIC API では、有効に活用できるストック実装が提供されます。ここでも、イメージング ファクトリ オブジェクトにより提供されます。典型的な GetDecoderInfo の実装については、図 4 を参照してください。必要な操作は、デコーダの CLSID を CreateComponentInfo メソッドに渡すことのみです。その結果、レジストリに格納されたデコーダのメタデータを含むオブジェクトが作成されます。
図 4 GetDecoderInfo の実装
HRESULT Kerr::IconDecoder::GetDecoderInfo(__out IWICBitmapDecoderInfo** decoderInfo)
{
    if (0 == m_imagingFactory)
    {
        return E_UNEXPECTED;
    }

    if (0 == decoderInfo)
    {
        return E_POINTER;
    }

    *decoderInfo = 0;

    CComPtr<IWICComponentInfo> componentInfo;

    HR(m_imagingFactory->CreateComponentInfo(__uuidof(IconDecoder),
                                             &componentInfo));

    return componentInfo.QueryInterface(decoderInfo);
}
ご想像のとおり、GetFrameCount メソッドは、デコードするイメージ内のフレームの数を返します。
最後に、GetFrame メソッドにより、イメージ内の特定のフレームを表す IWICBitmapFrameDecode インターフェイスが返されます。
ここまでは、IWICBitmapDecoder インターフェイスの実装について説明してきました。このような実装により、CoCreateInstance を使用してデコーダのインスタンスを作成できますが、あと少し作業を追加すると、WIC で提供される検索および抽象化のフレームワークに独自のコーデックを完全に統合できます。その結果、アプリケーションでは、コーデックの機能を自動的に利用できるようになり、あらかじめコーデックを認識する必要はなくなります。
アプリケーションは、さまざまな方法で、特定のコンピュータにある利用可能なコーデックを活用できます。まずは、すべてのエンコーダおよびデコーダを文字どおり列挙し、使用するものを見つけたり、適切なコーデックを選択するオプションをユーザーに提供したりできます。エンコーダを列挙する方法の例を図 5 に示します。結果の IWICBitmapEncoderInfo インターフェイスにより、各エンコーダの機能に関する有用な情報が豊富に提供されていることに注目してください。
図 5 エンコーダの列挙
HRESULT EnumerateEncoders()
{
    CComPtr<IWICImagingFactory> imagingFactory;
    HR(imagingFactory.CoCreateInstance(CLSID_WICImagingFactory));

    CComPtr<IEnumUnknown> enumerator;

    HR(imagingFactory->CreateComponentEnumerator(WICEncoder,
                 WICComponentEnumerateDefault, &enumerator));

    CComPtr<IUnknown> unknown;

    while (S_OK == enumerator->Next(1,
                                    &unknown,
                                    0)) // ignored
    {
        CComQIPtr<IWICBitmapEncoderInfo> info(unknown);
        unknown.Release();

        if (info)
        {
            CLSID clsid = { 0 };
            HR(info->GetCLSID(&clsid));

            UINT count = 0;

            HR(info->GetFriendlyName(0, // no buffer length
                                     0, // no buffer
                                     &count));

            CString friendlyName;

            HR(info->GetFriendlyName(count,
                                     friendlyName.GetBufferSetLength(count),
                                     &count));

            friendlyName.ReleaseBuffer(count - 1);

            TRACE(L"%s\n", friendlyName);
        }
    }

    return S_OK;
}
また、アプリケーションが WIC に依存して、適切なコーデックが自動的に選択されるようにする場合もあります。これは前回のコラムでも書きましたが、イメージング ファクトリで CreateDecoder メソッドが提供されるため、実装を考慮せずに、特定のイメージ形式用のエンコーダを作成できます。さらに、CreateDecoderFromStream メソッドでは、ストリームを調べ、コンピュータ上の利用可能なデコーダの登録情報を比較して、適切なデコーダを自動的に選択します。

次回の予定
今月はこれで終了です。Windows Imaging Component では、Windows Vista の新しい高解像度のアイコン形式を完全にサポートしているわけではありませんが、独自のコーデックで WIC を拡張して対応することができます。解像度が異なる各種イメージ フレームを区別する簡単な方法がないため、1 日の作業を終えたときに、WIC がアイコン イメージをデコードするのに適した状態になっていない可能性もあります。ただし、デコーダの記述を実演することは、WIC がどのように設計されているかを理解するために役立ちます。ほとんどの場合、LoadImage 関数はアイコン イメージを読み込むための最良の方法です。

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

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


同時実行の問題
AsyncEnumerator で簡素化された APM
Jeffrey Richter

目次
AsyncEnumerator クラスを使用する
AsyncEnumerator のアーキテクチャ
Wait および Inbox カウンタを操作する
スレッドセーフ モーフィング
さらなる調整
前回のコラム (msdn2.microsoft.com/magazine/cc163323) では、新しい C# 言語機能 (匿名メソッド、ラムダ式、および反復子) を使用して非同期プログラミングを簡略化する方法についての概念を紹介しました。前回のコラムの終わりに、C# 反復子を利用した同期プログラミング モデルを使用して非同期プログラミングをどのように実現できるかを示しました。反復子の例を図 1 に示します。ただし、コードは常に foreach を呼び出すスレッドによって実行され、非同期 I/O 操作が完了するまで一時停止することなく実行が継続されるため、通常の C# foreach ステートメントを使用するだけでは反復子のコードを実行できません。
このコラムでは、筆者の AsyncEnumerator クラスを紹介します。このクラスは、反復子をインテリジェントに駆動するため、異なるスレッド プールのスレッドが異なる時点でコードを実行でき、非同期 I/O 操作の完了後にのみ反復子を進めることができます。AsyncEnumerator クラスのアーキテクチャと、その動作についても説明します。

AsyncEnumerator クラスを使用する
筆者の AsyncEnumerator クラスの定義を次に示します。
public class AsyncEnumerator {
   // Methods called by code outside of the iterator
   public AsyncEnumerator();
   public void Execute(IEnumerator<Int32> enumerator);

   // Methods called by code inside the iterator
   public AsyncCallback End();
   public IAsyncResult DequeueAsyncResult();
}
筆者の AsyncEnumerator クラスの使用は非常に簡単です。最初に、反復子メンバの実装方法について説明してから、その呼び出し方法について説明します。
必要なパラメータを受け入れ、AsyncEnumerator オブジェクトへの参照である追加パラメータを追加するように反復子メンバを定義します。反復子メンバは、Int32s のコレクションを返すものとしてプロトタイプ化する必要があります。つまり、IEnumerator<Int32> の戻り値の型が必要です。
次に、反復子メンバの内部で、適切な BeginXxx メソッドを呼び出すことにより、各非同期操作を開始します。ご存知のように、BeginXxx メソッドを呼び出す場合は、非同期操作の完了時に呼び出される必要のあるメソッドの名前を渡すことが重要です。
独自のメソッドを定義する代わりに、AsyncEnumerator オブジェクトの End メソッドを呼び出します。End メソッドは、筆者の AsyncEnumerator クラスの内部で定義されたプライベート メソッドを識別する AsyncCallback デリゲートを返します。このため、各非同期操作が完了すると、AsyncEnumerator オブジェクト内のコードが通知を受けます。このコードは、完了した操作の IAsyncResult オブジェクトを受信トレイと呼ぶ List<IAsyncResult> オブジェクトに配置します。
コードでは、BeginXxx メソッドの呼び出しの後に、キューに登録した非同期操作の数を返す yield return ステートメントを配置します。図 1 では、1 つの BeginXxx メソッド (BeginRead) のみ呼び出したため、yield return 1 を実行します。yield return ステートメントは、反復子メソッドを中断し、そのメソッド内のコードがそれ以上実行されないようにします。
図 1 C# 反復子
private static IEnumerator<Int32> ApmPatternWithIterator(
   AsyncEnumerator ae, String pathname) {
   using (FileStream fs = new FileStream(pathname, FileMode.Open,
      FileAccess.Read, FileShare.Read, 8192, FileOptions.Asynchronous)) {

      Byte[] data = new Byte[fs.Length];
      fs.BeginRead(data, 0, data.Length, ae.End(), null);
      yield return 1;

      Int32 bytesRead = fs.EndRead(ae.DequeueAsyncResult());
      ProcessData(data);
   }
}
1 以外の戻り値を生成する他の例を後で示しますが、多くのアプリケーションでは、1 を返すことが適切です。yield return ステートメントで指定する数値は、AsyncEnumerator オブジェクトに対して、反復子を再開する前に完了する必要のある非同期操作の数を指示します。このため、反復子が中断している間に、開始したすべての非同期操作が完了を開始します。それぞれが完了するたびに、AsyncEnumerator の受信トレイにエントリが追加されます。受信トレイ内のアイテムの数が、yield return ステートメントで指定した数値と等しくなると、AsyncEnumerator オブジェクトは反復子を再開し、そのコードが実行を継続できるようにします。
インサイト : コンテキストでの AsyncEnumeration
C# 反復子を使用した非同期処理の実行は、同時プログラミングに関連するはるかに大きな課題のセットの一部です。Microsoft には現在、これらの課題への対処を模索する開発イニシアチブがいくつかあります。
その 1 つは、Concurrency and Coordination Runtime (CCR) です。これは、非同期関連と同時実行関連のさまざまな問題に対処するために使用できる、より汎用的なフレームワークです。
CCR には、C# 反復子を使用して非同期列挙を実行するための実装が含まれます。たとえば、CCR で反復子を使用してファイルを非同期に処理する方法を次に示します。
IEnumerator<ITask> CcrIterator(string pathname) {
  var resultPort = new Port<IAsyncResult>();
  using (FileStream fs = new FileStream(pathname,
    FileMode.Open, FileAccess.Read, FileShare.Read,
    8192, FileOptions.Asynchronous)) {
      Byte[] data = new Byte[fs.Length];
      fs.BeginRead(data, 0, data.Length, resultPort.Post, null);
      yield return resultPort;
      IAsyncResult ar = resultPort;
      Int32 bytesRead = fs.EndRead(ar); }
    ProcessData(data);
  }
}
反復子のサポートに加えて、CCR は、例外処理とアービタなどのコーディネーション プリミティブの拡張サポートも提供します。CCR の詳細については、「CCR ユーザー ガイド」(msdn2.microsoft.com/library/bb905447)、2006 年 9 月の「同時実行の問題」のコラム (msdn2.microsoft.com/magazine/cc163556)、および Channel 9 の「CCR」(channel9.msdn.com/showpost.aspx?postid=219308) を参照してください。
—Howard Dierking (編集長)
yield return ステートメントの後、対応する EndXxx メソッドを呼び出します。EndXxx メソッドを呼び出すときは、IAsyncResult オブジェクトを渡す必要があります。さいわい、AsyncEnumerator オブジェクトがこれらを受信トレイのキューに登録しているので、AsyncEnumerator オブジェクトの DequeueAsyncResult メソッドを呼び出してキューから取り出すだけで済みます。アイテムは、先入れ先出し (FIFO) アルゴリズムを使用して受信トレイのキューから取り出されます。
EndXxx メソッドから戻った後、非同期操作の結果を反復子の内部で直接処理できます。これは非常に魅力です。非同期操作を実行している場合でも同期プログラミング モデルを使用できるからです。
多くのシナリオでは、追加の BeginXxx メソッドを呼び出して、別の非同期操作をキューに登録すると便利です。反復子で、別の BeginXxx メソッドを呼び出してから、別の yield return 1 ステートメントに続けて対応する EndXxx メソッドを呼び出します。これにより、スレッドをブロックすることなく、一連の非同期操作を次々に簡単に実行できます。さらに、同期プログラミング モデルを使用して、すべてのコードを記述できます。
たとえば、図 2 に示した HtmlToFile 反復子は、1 KB 単位で Web サーバーから HTML を読み取り、読み取った HTML をローカル ファイルに保存します。反復子の呼び出しは非常に簡単です。AsyncEnumerator オブジェクトを作成し、Execute メソッドを呼び出して、反復子メンバの名前を渡します。反復子メンバに引数を渡すことができます。この場合は、反復子が End および DequeueAsyncResult メソッドを呼び出せるように AsyncEnumerator オブジェクトへの参照を反復子に渡す必要もあります。HtmlToFile 反復子を呼び出すコードを次に示します。
AsyncEnumerator ae = new AsyncEnumerator();
ae.Execute(HtmlToFile(
  ae,
  "http://www.Wintellect.com/",
  "LocalFile.html"));
図 2 HtmlToFile 反復子
private static IEnumerator<Int32> HtmlToFile(AsyncEnumerator ae,
   String url, String file) {

   // Issue asynchronous web request operation
   WebRequest webRequest = WebRequest.Create(url);
   webRequest.BeginGetResponse(ae.End(), null);
   yield return 1;

   WebResponse webResponse;
   try {
      webResponse = webRequest.EndGetResponse(ae.DequeueAsyncResult());
   }
   catch (WebException e) {
      Console.WriteLine("Failed to contact server: {0}", e.Message);
      yield break;
   }

   using (webResponse) {
      Stream webResponseStream = webResponse.GetResponseStream();

      // Read the stream data and write it to a file in 1KB chunks
      Byte[] data = new Byte[1024];
      using (FileStream fs = new FileStream(file, FileMode.Create,
         FileAccess.Write, FileShare.None, data.Length,
         FileOptions.Asynchronous)) {

         // See support.microsoft.com/kb/156932
         fs.SetLength(webResponse.ContentLength);

         while (true) {
            // Issue asynchronous web response stream read operation
            webResponseStream.BeginRead(data, 0, data.Length,
               ae.End(), null);
            yield return 1;

            // Get result of web response stream read operation
            Int32 bytesRead = webResponseStream.EndRead(
               ae.DequeueAsyncResult());
            if (bytesRead == 0) break; // Stream end: close file & exit

            // Issue asynchronous file write operation
            fs.BeginWrite(data, 0, bytesRead, ae.End(), null);
            yield return 1;

            // Get result of file write operation
            fs.EndWrite(ae.DequeueAsyncResult());
         }
      }
   }
}
筆者の AsyncEnumerator の Execute メソッドは、反復子メンバが終了するか yield break ステートメントを実行するまで戻りません。メイン スレッドはブロックされていますが、スレッド プールのスレッドは、非同期操作が完了した時点で反復子の実行を継続します。つまり、理論的には反復子のコードの各部分を異なるスレッドで実行できるので、反復子コードは、スレッド ローカル ストレージ、カルチャ、UI カルチャ、プリンシパル、優先順位などのスレッド固有の状態に依存する必要がありません。
ところで、反復子が実行を完了するまで Execute を呼び出すスレッドをブロックするのは筆者の好むところではありません。しかし、そうすると筆者の AsyncEnumerator クラスの動作が説明しやすくなり、実験とデバッグを簡略化できます。ただし、反復子を実行している間に AsyncEnumerator をブロックしない方法があり、Windows® フォームまたは Windows Presentation Foundation (WPF) アプリケーションの GUI スレッドから反復子を呼び出す場合は拡張性と応答性を高めるためにこの方法が重要であることを指摘しておきます。 次回のこのコラムでは、反復子メンバを非同期に実行する方法を示します。

AsyncEnumerator のアーキテクチャ
筆者の AsyncEnumerator クラスは、図 3 に示すプライベート フィールドを定義します。m_enumerator フィールドは反復子メンバを参照し、コンストラクタはフィールドを null に初期化し、Execute はこのフィールドを渡された値に設定します。m_inbox フィールドは、IAsyncResult オブジェクトのコレクションを参照します。m_waitAndInboxCounts フィールドは、2 つの 16 ビット整数値を含む構造体です。これらの値を操作する方法については、このコラムで後ほど説明します。
図 3 AsyncEnumerator フィールド
public partial class AsyncEnumerator {
   // Refers to the iterator member's code
   private IEnumerator<Int32> m_enumerator;

   // Collection of completed async operations (the inbox)
   private List<IAsyncResult> m_inbox =
      new List<IAsyncResult>();

   // Structure with Wait & Inbox counters
   private WaitAndInboxCounts m_waitAndInboxCounts;
}
前回のコラムで説明したように、反復子は、IEnumerator<T> インターフェイスを実装するクラスをコンパイラで作成する実に簡単な方法です。通常、IEnumerator<T> オブジェクトの処理を進める方法として foreach ステートメントを使用します。ただし、IEnumerator<T> MoveNext メソッドと Current プロパティをコードで明示的に呼び出してもなんら問題ありません。それはまさに筆者の AsyncEnumerator クラスで行っていることです。
AsyncEnumerator の Execute メソッドは、ResumeIterator というプライベート メソッドを内部的に呼び出します (図 4 を参照)。このメソッドは、反復子の開始と、中断状態からの再開を行います。
図 4 ResumeIterator
private void ResumeIterator() {
   Boolean continueIterating;

   // While there are more operations to perform...
   while (continueIterating = m_enumerator.MoveNext()) {

      // Get the value returned from the enumerator
      UInt16 numberOpsToWaitFor = checked((UInt16) m_enumerator.Current);

      // If inbox has fewer items than requested, keep iterator suspended
      if (!m_waitAndInboxCounts.AtomicSetWait(numberOpsToWaitFor)) break;

      // Inbox has enough items, loop to resume the iterator
   }

   // The iterator is suspended, just return
   if (continueIterating) return;

   // The iterator has exited, execute the iterator's finally code
   m_enumerator.Dispose();
}
スレッドは、ResumeIterator を呼び出す場合に、反復子を起動して実行する IEnumerator<T> MoveNext メソッドを呼び出します。反復子が終了するか、yield break ステートメントを実行した場合、IEnumerator<T> MoveNext メソッドは、それ以上処理するものがないことを示す false を返します。筆者の ResumeIterator メソッドの continueIterating 変数が false に設定されている場合は、ループが終了し、IEnumerator<T> オブジェクトの Dispose メソッドを呼び出してクリーンアップを可能にします。反復子が作業をすべて完了したため、ResumeIterator メソッドが戻ります。
一方、列挙子オブジェクトの MoveNext メソッドが true を返した場合、このメソッドによっていくつかの非同期操作が開始された後、このメソッドの実行が yield return ステートメントの実行によって中断されたということです。ここで、筆者の ResumeIterator メソッドは、yield return ステートメントで指定された IEnumerator<T> オブジェクトの数を知る必要があります。この数は、反復子を再開する前に完了する必要がある非同期操作の数を示します。この数を取得するために、ResumeIterator は列挙子オブジェクトの Current プロパティをクエリで取得します。このプロパティは yield return ステートメントで最後に指定された値を返します。
ResumeIterator は、WaitAndInboxCounts AtomicSetWait メソッドを呼び出します。このメソッドは、反復子が待機する必要のあるアイテムの数を設定します。受信トレイ内のアイテム数がこの待機カウントより少ないことを検出した場合、AtomicSetWait は false を返します。それによって ResumeIterator が戻ります (このスレッドは他に何もすることがないので)。受信トレイ内のアイテム数が待機カウント以上であることを検出した場合、AtomicSetWait は true を返してループを処理します。このとき、列挙子オブジェクトの MoveNext メソッドを再び呼び出して反復子が実行を再開できるようにします。それによって反復子は完了した操作を処理できます。
反復子の内部では、すべての BeginXxx メソッドが AsyncCallback メソッドに対して同じメソッドを渡します。EnqueueAsyncResult というこのプライベート メソッドは、AsyncEnumerator オブジェクトの End メソッドを呼び出すことで取得され、次のように実装されます。
private void EnqueueAsyncResult(IAsyncResult result) {
   // Add this item to the inbox
   lock (m_inbox) { m_inbox.Add(result); }

   // Add 1 to inbox count. If inbox has enough items
   // in it; this thread calls ResumeIterator
   if (m_waitAndInboxCounts.AtomicIncrementInbox())
      ResumeIterator();
}
スレッドセーフな方法で、このメソッドは完了した操作の IAsyncResult オブジェクトを受信トレイに追加し、受信トレイ内のアイテム数に 1 を加算する WaitAndInboxCounts AtomicIncrementInbox メソッドを呼び出します。AtomicIncrementInbox は、受信トレイ内のアイテム数が待機カウントよりも少ないことを検出した場合に false を返します。これにより、スレッド プールのスレッドがプールに戻るため、そのスレッドを他の作業の実行に使用できます。
AtomicIncrementInbox は、受信トレイ内のアイテム数が待機カウントと等しいことを検出した場合に true を返し、AsyncEnumerator の ResumeIterator メソッドを呼び出します。このメソッドは、列挙子オブジェクトの MoveNext メソッドを呼び出して、反復子の実行を再開するため、反復子は完了した操作を処理できます。

Wait および Inbox カウンタを操作する
各 AsyncEnumerator オブジェクトは、16 ビットの Wait カウントと 16 ビットの Inbox カウントを管理します。どちらのカウントも 0 ～ 65535 の範囲です。Wait カウントは、列挙子オブジェクトが再開する前に完了する必要のある非同期操作の数を示します。反復子は、yield return ステートメントを使用してこの値を設定します。Inbox カウントは、完了した非同期操作の数を管理します。反復子が yield return ステートメントを実行している間も複数のスレッドで非同期操作完了コードを同時に実行できるため、Wait カウントと Inbox カウントは、スレッドセーフな手法を使用して変更およびチェックする必要があります。
これらのカウントをスレッドセーフ方式で最も高速に更新する方法は、単一の Int32 に両方を挿入してから、インターロック メソッドを使用して Int32 を更新することです。コードを読みやすく、保守しやすくするために、WaitAndInboxCounts 構造体を定義して、この 2 つのカウントを管理することにしました (図 5 を参照)。この構造体では、プライベート Int32 フィールドを定義し、スレッドセーフな方法で Int32 を操作するための内部メンバを提供します。AsyncEnumerator コードは、これらのメンバを呼び出します。
図 5 スレッドセーフな Wait および Inbox カウント
private struct WaitAndInboxCounts {
   private const UInt16 c_MaxWait = 0xFFFF;
   // Wait=High 16 bits, Inbox=low-16 bits
   private Int32 m_waitAndInboxCounts;

   private UInt16 Wait {
      get { return (UInt16) (m_waitAndInboxCounts >> 16); }
      set { m_waitAndInboxCounts = (Int32) ((value << 16) | Inbox); }
   }
   private UInt16 Inbox {
      get { return (UInt16) m_waitAndInboxCounts; }
      set { m_waitAndInboxCounts =
         (Int32)((m_waitAndInboxCounts &
            0xFFFF0000)|value); }
   }

   private WaitAndInboxCounts(Int32 waic) { m_waitAndInboxCounts = waic; }
   private Int32 ToInt32() { return m_waitAndInboxCounts; }

   internal void Initialize() { Wait = c_MaxWait; }

   internal Boolean AtomicSetWait(UInt16 numberOpsToWaitFor) {
      return InterlockedEx.Morph<Boolean, UInt16>(
         ref m_waitAndInboxCounts,
         numberOpsToWaitFor, SetWait);
   }

   private static Int32 SetWait(Int32 i, UInt16 numberOpsToWaitFor,
      out Boolean shouldMoveNext) {
      WaitAndInboxCounts waic = new WaitAndInboxCounts(i);
      // Set the number of items to wait for
      waic.Wait = numberOpsToWaitFor;
      shouldMoveNext = (waic.Inbox >= waic.Wait);

      // Does the inbox contain enough items to MoveNext?
      if (shouldMoveNext) {
         // Subtract the number of items from the inbox
         waic.Inbox -= waic.Wait;
         // The next wait is indefinite
         waic.Wait = c_MaxWait;
      }
      return waic.ToInt32();
   }

   internal Boolean AtomicIncrementInbox() {
      return InterlockedEx.Morph<Boolean, Object>(
         ref m_waitAndInboxCounts,
         null, IncrementInbox);
   }

   private static Int32 IncrementInbox(Int32 i, Object argument,
      out Boolean shouldMoveNext) {
      WaitAndInboxCounts waic = new WaitAndInboxCounts(i);
      // Add 1 to the inbox count
      waic.Inbox++;
      shouldMoveNext = (waic.Inbox == waic.Wait);

      // Does the inbox contain enough items to MoveNext?
      if (shouldMoveNext) {
         // Subtract the number of items from the inbox
         waic.Inbox -= waic.Wait;
         // The next wait is indefinite
         waic.Wait = c_MaxWait;
      }
      return waic.ToInt32();
   }
}
使用可能な 2 つの内部メソッドは、AtomicSetWait および AtomicIncrementInbox です。このコラムで、これらのメソッドが何を行うかを説明しました。ここでは、これらのメソッドがその処理をどのように行うかを説明します。これは、アトミック形式で複数の値を操作する必要のある多くのシナリオで使用できる便利な手法です。
読者の多くがご存知だと思いますが、Microsoft® .NET Framework クラス ライブラリの Interlocked クラスには、Int32 値をスレッドセーフな方法で操作するためのメソッドがいくつか用意されています。Int32 を 1 つずつ増やすメソッド、Int32 を 1 つずつ減らすメソッド、Int32 に正または負の値を加算するメソッド、Int32 を新しい値に変更するメソッド、Int32 が特定の値かどうかをチェックし、その値だった場合に別の値に変更するメソッドがあります。要するに、インターロック メソッドは優れていますが、Int32 を操作するための方法はわずかしか提供していません。

スレッドセーフ モーフィング
筆者の最初の「同時実行の問題」のコラム (msdn.microsoft.com/msdnmag/issues/05/10/ConcurrentAffairs) では、Interlocked クラスの CompareExchange メソッドを使用してより優れた方法で Int32 値を操作する方法について、よく知られたパターンを示しました。具体的には、アトミックなビット単位の AND、OR、または XOR 操作を Int32 に対して実行する方法を示しました。ただし、アトミックな multiply、divide、min、max など、さらに多くの操作を実行するために、このパターンを簡単に拡張できます。
実際に、筆者はこれをチューニングして次のような Morph<TResult, TArgument> という汎用メソッドにすることで、パターンを拡張することにしました。
public static TResult Morph<TResult, TArgument>(ref Int32 target,
   TArgument argument, Morpher<TResult, TArgument> morpher) {

   TResult morphResult;
   Int32 i, j = target;
   do {
      i = j;
      j = Interlocked.CompareExchange(ref target,
         morpher(i, argument, out morphResult), i);
   } while (i != j);
   return morphResult;
}
このメソッドを呼び出すときに、値をアトミックに操作する Int32 を (参照によって) 渡し、モーフィングに使用する追加の引数も渡すことができます。最後に、次のように定義される、シグネチャが Morpher デリゲートと一致するメソッドを渡します。
delegate Int32 Morpher<TResult, TArgument>(Int32 startValue,
   TArgument argument, out TResult morphResult);
Morpher メソッドには、ターゲットの Int32 内の初期値と、Morph に渡される追加引数が渡されます。Morpher メソッドは、任意のアルゴリズムを Int32 値に適用し、目的の新しい値を返します。アルゴリズムがコードを実行しているときに元の Int32 値が別のスレッドによって変更されなかった場合は、ターゲット内の値に、Morpher メソッドによって返される値が割り当てられます。アルゴリズムがコードを実行しているときに元の Int32 値が別のスレッドによって変更された場合、Morph メソッドは Morpher メソッドを再び呼び出し、(他のスレッドによって変更された) 新しい Int32 値を渡します。
Morpher メソッドは、out パラメータ morphResult を設定する必要もあることがわかります。Morph がターゲットを正常に変更すると、この out パラメータは、Morph メソッドが呼び出し元に返す値になります。これのすばらしいところは、Morpher メソッドの内部のコードがスレッドセーフである必要がまったくないことです。コードを何千行もの長さにすることができ、複数の操作を実行できます。Morph メソッドは、元の Int32 値がスレッドセーフな方法で操作されることを保証します。
これで、Morph メソッドと Morpher デリゲートが定義されたので、このインフラストラクチャを使用して WaitAndInboxCounts AtomicSetWait メソッドと AtomicIncrementInbox メソッドを実装できます。
内部的には、AtomicSetWait は Morph を呼び出し、新しい Wait カウントを反復子の yield return ステートメントで返された値に設定する Morpher メソッドに渡します。Inbox カウントが新しい Wait カウントよりも小さい場合は、新しい Wait カウントがアトミックに設定されます。また、false が Morph の呼び出し元に返されます。これにより、他のアイテムが受信トレイに現れるのを待機することを反復子が示すため、列挙子オブジェクトの MoveNext メソッドを呼び出す必要のないことが呼び出し元に通知されます。
Morph メソッドは、Inbox カウントが新しい Wait カウント以上であることを検出した場合に、Wait カウントを Inbox カウントから減算し (論理的にこれらのアイテムを受信トレイから削除するため)、Wait カウントが最大値 (65535) に設定されるので、受信トレイに (他のスレッドによって) 追加されるアイテムは、反復子が実行を再開する原因になりません。ただし、このすべてが正常に実行されると、Morph メソッドは呼び出し元に true を返し、このスレッドは列挙子オブジェクトの MoveNext メソッドを呼び出して、待機していた完了アイテムを処理できるように反復子を再開します。
内部的に、AtomicIncrementInbox は Morph を呼び出し、Inbox カウントに 1 を加算する Morpher メソッドに渡します。Inbox カウントが現在の Wait カウントと等しくない場合は、Wait カウントがアトミックに設定され、false が Morph の呼び出し元に返されます。これにより、列挙子オブジェクトの MoveNext メソッドを呼び出す必要のないことが呼び出し元に通知されます。呼び出しが必要ない理由は、反復子が受信トレイへの他のアイテムの到着を待機することを示しているからです。
Morph と同様に、Morpher メソッドは、Inbox カウントが現在の Wait カウントと等しいことを検出した場合、Wait カウントを Inbox カウントから減算し (論理的にこれらのアイテムを受信トレイから削除するため)、Wait カウントが最大値に設定されるため、受信トレイに (他のスレッドによって) 追加されるアイテムは、反復子が実行を再開する原因になりません。ただし、このすべてが正常に実行されると、Morph メソッドは呼び出し元に true を返し、このスレッドは列挙子オブジェクトの MoveNext メソッドを呼び出して、待機していた完了アイテムを処理できるように反復子を再開します。
WaitAndInboxCounts 構造体は、スレッドセーフでない方法で Int32 値を操作するために内部的に使用される追加のプライベート メンバをいくつか定義します。プライベート コンストラクタは、Int32 を WaitAndInboxCounts 構造体にキャストします。プライベート ToInt32 メソッドは、WaitAndInboxCounts 構造体の内部で Int32 値を返します。また、Int32 フィールドの下位および上位の 16 ビット値を取得および設定するプライベート Wait プロパティと Inbox プロパティがあります。

さらなる調整
多くの開発者は、パフォーマンスが高くスケーラブルなアプリケーションとコンポーネントを実装できるようにするために、非同期プログラミングがどれほど貴重であるかを理解しています。ただし実際には、多くのプログラマは非同期プログラミング モデルの使用が非常に難しいことを発見して使用するのをやめ、アプリケーションでのパフォーマンスの低さを受け入れています。筆者の AsyncEnumerator クラスを C# 反復子と共に使用すると、非同期プログラミングが非常に簡単になります。プログラマがこれを採用し、スレッド数とコンテキスト スイッチの削減によってアプリケーションのパフォーマンスとスケーラビリティが向上することを希望します。
次回のコラムでは、AsyncEnumerator クラスで提供する必要のある追加機能を示し、このクラスでサポートされるさまざまな使用パターンも示します。次回お会いできるのを楽しみにしています。

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

Jeffrey Richter は、アーキテクチャのレビュー、トレーニング、およびコンサルティング サービスを提供する Wintellect (www.Wintellect.com) 社の共同設立者です。彼の著書には『プログラミング Microsoft .NET Framework 第 2 版』(日経 BP ソフトプレス、2006 年) などがあります。Jeffrey は、MSDN Magazine の寄稿編集者で、1990 年からマイクロソフトのコンサルティングを担当しています。


いろいろな場所へ
Windows Mobile 用のアダプタブル アプリケーション
Michael Saffitz

目次
アダプタブル アプリケーション
デザインとアーキテクチャ
表示の違い
ネイティブ アプリケーションの解像度認識
マネージ アプリケーションの解像度認識
基本的なデバイスの違い
その他のデバイスの違い
テストとさらなる探求
Microsoft が Pocket PC 2000 を最初に出荷したとき、およびその後数年間は、開発者が、すべてのデバイスで正常に実行できる 1 つのアプリケーションを比較的簡単に作成できました。一般には、すべてのデバイスに、タッチ サポート、画面解像度、縦長の向き、API セットなど、同じ機能と特性がありました。したがって、ほぼアプリケーションの機能だけに集中できました。このような、デバイスのその他の要素を気にする必要はなく、これらの要素が変更される可能性を考慮する必要はまったくありませんでした。
しかし、変化は避けられません。市場とテクノロジが開発されるにつれて、より多くのデバイスが使用可能になり、これらのデバイスは多様なデザインを持ち始めました。使用可能な Windows Mobile® 電話は 2008 年までに急速に増え、今では 140 種類を超えました。これらの電話は、12 のキーと QWERTY キーボード、キャンディ バー スタイルと折りたたみ式スタイル、GPS の有無、正方形、縦長、横長の画面、Wi-Fi、Bluetooth、赤外線通信、デバイスごとに異なるその他の機能など、さまざまなオプションをサポートします。Pocket PC 2000 とは異なり、顧客はニーズ、好み、予算に合ったデバイスをはるかに簡単に見つけられるようになっています。

アダプタブル アプリケーション
一方、このような Windows Mobile デバイスの多様性の拡大によって、開発者はこれらの違いを認識し、アプリケーションでこれらの違いを計画することが必要になりました。開発者は、アダプタブル アプリケーション (すべての Windows Mobile デバイスで正常に動作し、優れた外観を持つアプリケーション) と呼ばれる適応性に優れたアプリケーションを構築する必要があります。
さいわい、オペレーティング システムとして、Windows Mobile は複数のデバイスで使用できる最も一貫性のあるモバイル プラットフォームのコア API セットの 1 つを提供します。Windows Mobile 開発者は、デバイスの携帯電話会社やハードウェア メーカーに関係なく、(デスクトップに見られるものと多くの点で一致するようにデザインされた) 同じコア API セットを見つけることができます。
その結果、アダプタブル アプリケーションを構築するほとんどの開発者は、異なる画面解像度、ドット/インチ (DPI)、および向きへの対処が主要なフォーカス分野となることに気付きます。より少数の開発者にとっては、デバイスの機能 (テレフォニーの存在や GPS など) の違い、Windows Mobile の主要リリース間での API の変更、およびタッチ デバイスと非タッチ デバイスの両方を対象としたデザインも、考慮が必要な領域になります。
開発者がアダプタブル アプリケーションの作成に使用できるツールと手法をいくつか示します。一般的なデザインとアーキテクチャの原理を説明してから、画面と機能の違いへの適応に関連する詳細を掘り下げます。
確かに、アダプタブル アプリケーションの作成を選択するときにいくつかの追加の注意と考慮が必要ですが、多くの場合、先行投資の利益は、別の方法 (現在および将来のすべてのデバイスに対してアプリケーションのバージョンを開発およびテストすること、またはデバイスをサポートしないことを選択して潜在的なユーザー ベースを制限すること) を選んだ場合の利益を大幅に上回ります。

設計とアーキテクチャ
アダプタブル アプリケーションを作成する場合は、実績のある慣行とアーキテクチャ パターンに従うことが重要です。実践の観点からは、要件を慎重かつ明示的に識別することで、アプリケーションで必要な最小のデバイス プロファイルを理解し始めることができます。また、存在する場合にはアプリケーションで動的に使用できるオプション機能も識別します。このアプローチをとる場合には、コードを最小の共通部分にする必要なく、アプリケーションを最大数のデバイスで実行できます。
簡単な例として、単純なパッケージ配信追跡アプリケーションの構築を要求されたとしましょう。要件を調べ、パッケージが正常に配信されたことを示す署名を収集する必要があると判断しました。あると便利な機能として、アプリケーションではオンライン追跡用にパッケージ ステータスをサーバーにアップロードする必要があります。署名を収集する必要があるので、アプリケーションはタッチ デバイス (Windows Mobile Professional および Windows Mobile Classic) に制限されます。これを特定したことで、タッチを必要とする追加機能 (ボタン コントロールなど) を自由に使用できることになります。
同時に、接続コンポーネントの設計も慎重に検討する必要があります。たとえば、アプリケーションが移動体通信データ接続にアクセスする必要があると想定した場合は、電話機能はないが Wi-Fi はある Classic デバイスを除外します。
最後に、クライアントの気が変わって、署名要件を取り除いた場合、その後のボタン コントロールの使用を考えると、必ずしも非タッチ デバイス (Window Mobile Standard) をサポートできる必要はありません。

表示の違い
前に述べたように、多くの場合、画面の解像度、DPI、および向きの違いは、アダプタブル アプリケーションの作成時に最も差し迫った問題になります。ここでも、これらの違いを計画し、アーキテクチャについてよく考えることは、ソリューションの実装時に大きな影響を与えます。
現在サポートされている Windows Mobile 6 解像度、DPI、向き、およびアイコンの寸法を、図 1 に示します。これらに加えて、マイクロソフトは、新しい構成、OEM (Original Equipment Manufacturer)、および携帯電話会社を追加することがあります。また、一部のデバイスでは、画面の向きの動的な回転をサポートします。たとえば、縦長の 240×320 で表示するデバイスは、キーボードがスライド アウトされたときに横長の 320×240 の表示に回転されることがあります。
図 1 Windows Mobile 6 の表示オプション
Windows Mobile Professional および Classic (タッチ スクリーンあり)
解像度 | DPI | 向き | 小さいアイコン | 大きいアイコン
240×240 | 96 | 正方形 | 16×16 | 32×32
240×320 | 96 | 縦長および横長 | 16×16 | 32×32
240×400 | 96 | 縦長および横長 | 16×16 | 32×32
320×320 | 128 | 正方形 | 21×21 | 43×43
480×480 | 192 | 正方形 | 32×32 | 64×64
240×240 | 192 | 縦長および横長 | 32×32 | 64×64
240×240 | 192 | 縦長および横長 | 32×32 | 64×64
Windows Mobile Standard (タッチ スクリーンなし)
解像度 | DPI | 向き | 小さいアイコン | 大きいアイコン
176×220 | 96 | 縦長 | 16×16 | 32×32
240×320 | 131 | 縦長および横長 | 22×22 | 44×44
320×320 | 131 | 正方形 | 22×22 | 44×44
240×400 | 131 | 縦長および横長 | 22×22 | 44×44
440×240 | 131 | 横長 | 22×22 | 44×44
これまで以上に、アプリケーションで 1 つ以上の画面特性を前提とせずに、すべての解像度、向き、および少なくとも 96 DPI と 131 DPI に動的に適応し、これらをサポートするアプリケーションを作成することが重要になっています (192 DPI も重要ですが、非常に少数のデバイスに限定される傾向にあります。128 DPI は 131 DPI に十分近いので、大きな問題にはなりません)。
このタスクを簡単にし、ネイティブ アプリケーションとマネージ アプリケーションの両方の開発に使用できるリソースとツールがいくつかあります。これらのうちのいくつかを説明し、解像度を認識するアプリケーションの作成プロセスで使用できる一般的なアプローチとベスト プラクティスを確認します。

ネイティブ アプリケーションの解像度認識
Windows Mobile 6 SDK は、ネイティブ コードで解像度認識アプリケーションを作成するための 2 つの主要リソースを提供しています。それは、UILayout サンプル内の再利用可能な ScreenLib クラスと、DeviceResolutionAware.h ヘッダーです。UILayout サンプルは、Windows Mobile SDK の \Samples\Common\CPP\Win32 ディレクトリにあります。DeviceResolutionAware.h は、Microsoft® Visual Studio® インストール ディレクトリの \VC\ce\atlmfc\include ディレクトリにインストールされます。
ScreenLib は、画面に要素を配置するためのヘルパ関数のセットを提供します。たとえば、DockControl 関数を使用して、特定のコントロールを画面の端にドッキングしたり、4 辺にドッキングしてクライアント領域を充填したりできます。OptimizeWidth および OptimizeHeight 関数は、1 つのコントロール (OptimizeWidth の場合は複数のコントロール) を画面で配置およびサイズ変更し、左右と上下にそれぞれ小さな余白を残します。コントロールを配置するため、およびコントロールのセットを同じサイズに変更するための追加関数があります。これらの関数によって、ScreenLib は、フォームベース アプリケーションを大量に処理するときに最も役立つ傾向にあります。
DeviceResolutionAware.h は、ScreenLib で省かれた場所を取り上げ、より複雑なアダプタブル アプリケーションの作成を支援する 20 を超える関数とマクロを提供します。これらは、GetDisplayMode (ディスプレイの特性と機能を提供します) などの適応性の高いユーザー インターフェイスの構成要素を提供する基本的な関数とマクロから開始します。
現在の解像度に合わせて値を拡大縮小する SCALEX、SCALEY、SCALERECT、および SCALEPT があります。次に、StretchIcon や ImageList_StretchBitmap など、現在のディスプレイ特性に合わせてイメージを拡大縮小するのに役立つ関数があります。また、RelayoutDialog などの関数を使用して、向きが変更されたときにダイアログのレイアウトを自動的に調整できます。
ScreenLib と DeviceResolutionAware.h は、ネイティブの解像度認識アプリケーションを作成するための強力な基盤を提供します。ただし同時に、表示バッファとの直接対話や複雑な UI の作成時には特に、状況に固有のデザインとソリューションの代わりにはならないことに注意することが重要です。

マネージ アプリケーションの解像度認識
マネージコード アプリケーションの場合、Microsoft .NET Compact Framework は、解像度を認識するアプリケーションの作成時に役立つ表示プロパティのセットを提供します。ScreenLib 内の機能と同様に、.NET Compact Framework は、指定されたコントロールを親の端にバインドする Control.Dock プロパティを提供します。Control.Anchor は、親の端からの固定距離にコントロールをバインドすることで、同様の機能を提供します。Control.AutoScroll プロパティを使用すると、フォーム内のコントロールが画面に治まらない場合にスクロールバーが自動的に追加されます。さらに、Control.AutoScale は、フォームがデザインされた解像度を追跡し、DPI または解像度が変更された場合にフォームを動的に拡大縮小します。想像どおり、Control.AutoScale プロパティは単純なフォームに対しては適切に動作しますが、フォームが複雑になると、実行できる機能は制限されます。
より複雑なフォームに対して、これらのプロパティは開始点を提供しますが、向きの変更に対する動的な調整の点では特に、必ずしも完全なソリューションにはなりません。1 つのアプローチは、Microsoft patterns & practices グループが Mobile Client Software Factory (msdn2.microsoft.com/library/aa480471) の一部としてリリースした Orientation Aware Control (OAC) を使用することです。OAC は、フォームベースのアプリケーションでは特に、複数の向きを対象とする簡単な方法を提供します。
OAC がインストールされると、Visual Studio のマネージ フォーム デザイナを使用して、縦長モードで UI をレイアウトし、OAC を使用して横長に回転できます。この新しい向きでは、横長に合わせて UI を調整できます。OAC は、エンド ユーザーが縦長または横長モードのときに、適切なレイアウトが実行時に使用されるように変更を追跡します。
ただし、OAC の使用には弱点があります。まず、OAC では基本的に特定の向きと解像度の組み合わせをデザインできますが、サポートされる向きと解像度は将来的に変更される可能性があるので、このアプローチはお勧めできません。さらに、さまざまな DPI、または Windows Mobile Standard ベースのスマートフォン デバイスで最近普及してきた正方形の画面に対して明示的にデザインできません。最後に、OAC が画面ごとに複数のレイアウトを作成および管理すると、フォームと複雑さが追加されるので、パフォーマンスが大幅に低下します (極端な例では、アプリケーションの起動時間が著しく長くなることがあります)。
これらの弱点により、Windows Mobile チームは開発者に OAC からの移行を推奨してきました。そのグループは、Windows Mobile Line of Business Accelerator (go.microsoft.com/fwlink/?LinkId=115317) の新しいバージョンをリリースしています。このバージョンには、単一の UI を持ち、ドッキング、アンカー、およびその他の手法を利用する解像度認識アプリケーションの作成に役立つコンポーネントが含まれています。

基本的なデバイスの違い
さまざまなツールがあり、新しいアクセラレータがあるにもかかわらず、アダプタブル アプリケーションの作成に対する万能薬はありません。各アプローチには、ニーズと状況に対して評価する必要のある利点と欠点があります。しかし、採用するアプローチに関係なく、役に立つ可能性のある一般的なガイドラインがいくつかあります。
正方形、縦長、横長の 3 つの基本的な向きから開始し、それぞれに UI をレイアウトする方法について考えます。これが完了したら、UI がさまざまな解像度と DPI に適切にスケーリングされることの確認に集中できます。複雑な UI の場合は、デザインを調整してそれを複数のフォームに広げる方が、単一フォームに保持して向きが変更されたときに再配置するよりも簡単な場合があります。
ビジュアル アダプテーション用のアプリケーションを実装するときには、UI とアプリケーション ロジックの間を明確に分離することがさらに重要になります。UI を対話のポイントと考え、フォームベースの考え方を避けることで、これらの要素が個々のフォームにマップされる方法の詳細を集中化および抽象化する管理クラスに UI 要素をカプセル化できます。
正方形画面専用にデザインすること、コントロールを上と左にドッキングすること、縦長および横長の向きを正方形に効果的に縮小して、ある種の適応性を提供することも考えられます。しかし、これを行う場合は、アプリケーションとユーザーから潜在的な画面スペースの 1/3 ～ 2/3 が奪われます。さらに、最初は開発時間を短縮するように見えたものが、結局より多くの時間を消費する場合があります。画面スペースが小さくなると、UI のデザインを制限したり、追加の画面を組み込んだりする必要が生じます。

その他のデバイスの違い
多くの場合、ディスプレイ特性の違いへの対処は、アダプタブル アプリケーションの作成の最大部分ですが、他の側面もあります。API は何年にもわたって導入、変更、および廃止されるので、Windows Mobile のバージョンが重要です。利用できるなら採用したいデバイス固有の機能もあるでしょう。
このような状況およびその他の状況でも、すべてのデバイスで実行できるようにアプリケーションを作成できます。基本機能を定義し、デバイス固有の実装を持つインターフェイスとの組み合わせでは特に、検討する 1 つの手法としてファクトリ パターンがあります。
前に説明した収集作業のデザインと要件の一部を含む単純な例を見てみましょう。レストランのレビューを含むシティ ガイドの作成を求められました。コア要件は、レストランとレビューのディレクトリを含めることと、特定のレストランを選択して表示し、そのレビューを読む機能です。アプリケーションは Windows Mobile 2003 以降のバージョンで実行する必要があります。最後に、テレフォニーを提供するデバイスを使用するときに、レビュー画面からレストランに直接電話をかけられることという要件があります。
この例では C# を使用しますが、提示されている概念は Visual Basic® と C++ にも適用できます。マネージ テレフォニー API は、最初に Windows Mobile 5.0 に導入されました。その結果、筆者のアプリケーションでは、アプリケーションが実行される Windows Mobile のバージョンを認識し、適切な実装を使用する必要があります。
まず、デバイスや特定の実装に関係なく、必要な共通機能を定義するテレフォニー インターフェイスを作成します。
interface ITelephony {
  bool HasPhone {
    get;
  }
  void MakeCall(string phoneNumber);
}
次に、図 2 に 2 つの実装を示します。Windows Mobile 5.0 より前のデバイス用と、Windows Mobile 5.0 以降を実行しているデバイス用です。簡潔にするため、Windows Mobile 5.0 より前のデバイスで必要な P/Invoke は省略しました。
図 2 ITelephony の実装
class PreWindowsMobile5 : ITelephony {
  public bool supportsTelephony {
    get { return File.Exists(@"\Windows\Phone.dll"); }
  }

  public void MakeCall(string phoneNumber) {
    // call Native Phone API
  }
}

class PostWindowsMobile5 : ITelephony {
  public bool supportsTelephony   {
    get { return SystemState.PhoneRadioPresent; }
  }

  public void MakeCall(string phoneNumber) {
    Phone thePhone = new Phone();
    thePhone.Talk(phoneNumber);
  }
}
最後に、アプリケーションのコア ロジックからさまざまな実装を集中化および分離する TelephonyFactory クラスを作成します (図 3 を参照)。この例では、デバイスがテレフォニーをサポートしているかどうかを認識し、サポートされていない場合には例外をスローする機能を TelephonyFactory クラスに含めることを選択しました。これで、アプリケーション内でテレフォニーを使用する場合は、単純に次のコードを実行できます。
try {
    ITelephony myTelephony =
      TelephonyFactory.GetTelephony();
    myTelephony.MakeCall(phoneNumber);
  }
  ...
図 3 TelephoneFactory
class TelephonyFactory {
  static ITelephony _myTelephony = null;

  public static ITelephony GetTelephony() {
    if (_myTelephony == null) {
      if (Environment.OSVersion.Version.Major >= 5)
        myTelephony = new PostWindowsMobile5();
      else
        myTelephony = new PreWindowsMobile5();
    }
    if ( ! myTelephony.supportsTelephony)
      throw(new NotImplementedException());

    return _myTelephony;
  }
}
このパターンを使用して、適応性を提供し、アプリケーションがすべての Windows Mobile デバイスで稼働することを保証する一方で、デバイス間に発生する差異を特定し、アプリケーションの更新と保守を簡単にしました。

テストとさらなる探求
優れたデザインと注意深いコーディングは、アダプタブル アプリケーション作成の 3 つの側面のうちの 2 つです。さいわい、最後の側面であるテストは、Windows Mobile SDK の一部として、またスタンドアロン パッケージで提供されているエミュレータ イメージの多様性によって簡単になっています。エミュレータ イメージによって、動的な回転を含め、ほとんどすべてのディスプレイ特性に対してアプリケーションをテストできます。
SDK にある追加のツールは、GPS 機能のシミュレーション、複数の種類の移動体通信ネットワーク、およびさまざまなセキュリティ構成にも対応しており、動的な機能検出と使用をテストできます。最後に、エミュレータ環境の多くは、Device Emulator Manager Automation API と Core Connectivity Framework を通じてプログラム可能なので、複数のデバイス構成のテストを自動化できます。
単一のアプリケーションが多様なデバイスをターゲットにできるプラットフォームの提供は、現在 Windows Mobile が最善のモバイル開発プラットフォームを提供している方法の 1 つにすぎません。経験豊かなベテランであるか、プラットフォームを初めて使用するかにかかわらず、Windows Mobile 開発者 Web サイト (msdn.microsoft.com/windowsmobile) に、ドキュメント、チュートリアル、およびツールが用意されています。これらは、幅広い API や高度なテクノロジと連動して、Windows Mobile 上で制限のない開発を可能にします。このコラムで説明した適応性の問題に関する追加資料と最新情報は、msdn.microsoft.com/windowsmobile/adaptyourapp で入手できます。
このコラムを着想させ、寄稿資料となった「Adapt Your App」の著者、Jim Wilson 氏 (pluralsight.com/blogs/jimw) に感謝します。

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

Michael Saffitz は、マイクロソフトの Windows Mobile Developer Experience チームのプログラム マネージャです。アプリケーションの互換性、プラットフォームの一貫性、および ISV エバンジェリズムに注目しています。


{ End Bracket }
投票者も信用できる選挙結果
Josh Benaloh

暗号化には、データの機密保持、識別情報の認証、メッセージの整合性維持など、重要でよく知られた用途が数多くあります。しかし、意外な用途として、選挙過程への応用があります。投票データを暗号化して投票者のプライバシーを守ることや、機密性の高いシステムには権限のある職員だけがアクセスできるようにすることだけを言っているのではありません。暗号化の採用によって選挙のあり方を完全に変えることができます。つまり、投票者各人が投票集計の正確性と信頼性を確認できるようになります。最も驚くべきことは、選挙の設備、ソフトウェア、および職員にまったく頼らずにそれを実現できることです。
しかし、どうしてこのようなことが可能なのでしょうか。プログラマ、ハードウェア ベンダ、システム マネージャ、あるいは他のだれか、または何かを頼みにする必要はないのでしょうか。実際のところ必要ありません。これを理解するには、Web ポスティングを利用した記名投票選挙を思い描いてください。投票者は、選挙の締め切り時に凍結される公開 Web サイトに名前と票をポストします。このサイトの最終的な内容は引き続き読むことができ、地元の新聞など広く流通しているメディアでも公開されます。その時点で、すべての投票者が、自分の票が正しく掲載されているか、すべての票が有権者のものであるか、および集計が実際の投票総数と一致するかを確認できます。だれにも、または何にも頼らずに集計の正確性を検証できます (もちろん、サービス拒否攻撃がこの選挙または他のいずれかの選挙の正常な終了を妨げる可能性はありますが、サービス拒否攻撃などの不正行為はよく知られており、簡単に検出できます。単に、選挙が公正で正確かどうか投票者がわかればいいのです)。
この単純で完全に検証可能な選挙で 1 つだけ欠けているのがプライバシーであり、この点こそ暗号化が真価を発揮するところです。投票者に名前と暗号化された票をポストしてもらうように前述の選挙過程を修正するとします。これでプライバシーは取り戻しましたが、検証可能性を失ったようです。ただし、少しの努力で、貴重な検証可能性の特質も取り戻すことができます。目標は、ポストされた暗号票群と集計された獲得票の一致を数学的に証明することです。この目標が実現されて、その証明をだれでも確認できるようになれば、エンド ツー エンドの完全な検証が可能な秘密投票選挙を実施できるようになります。すべての投票者だけでなく、選挙に無関係な人でさえ、集計の正確性を確認できるようになるでしょう (これは現在使用されている通常の文書記録などの方法では不可能なことです)。
単純な暗号化技術は、ここで必要なすべての特質を備えているわけではありません。暗号化機能の公開が必要であり、個人票を暗号化解除できる存在が皆無になるように堅牢な分散型の暗号化解除プロセスが必要であり、暗号化された値に対して、加算や乗算などの基本的な計算を暗号化解除と再暗号化のプロセスを間に挟むことなく直接実行できるようにする特殊な同形の特質がいくつか必要です。さいわい、ちょうどこれらの特質を備え、十分に研究された暗号化機能が、研究文献にいくつか含まれています。そのような機能を使って、個別に暗号化された票を統合または変換して、集計および集計が正しいという証明を生成できるシステムを構築できます。投票者は自分の無記名票が正しくカウントされているかどうかを自分で確認できるようになります。
ただし、問題が 1 つ残ります。投票者はどのようにして自分の票を暗号化するのでしょうか。公開キーを公開しさえすれば、投票者が自分の設備を使用して票を暗号化できるはずと思われるかもしれません。しかし、そうすると投票者が強要や票の買収にさらされることになります。票を改ざんするために家庭のコンピュータが不正に操作される可能性があることは言うまでもありません。
強要のない選挙を行う最適な方法は、監視されている投票所で本人が直接投票することです。監視されているので、投票者は自分 1 人で投票せざるを得ません。そのとき、不当な強要からは解放されています。しかし、同時に投票者は自分の自由にならない設備を使用することを余儀なくされます。信頼できない設備で票が正しく暗号化されるかどうかを投票者が確認できる方法がいくつか開発されています。ただし、投票者は自分がどのように投票したかを第三者に証明することはできません。難しいのは、現在の選挙で通常必要な手順以上に複雑な手順を投票者に強制することなく、投票者にとって簡単な手順を維持することです。かなり簡単な手順の 1 つは、投票者に代わって暗号化された票の正当性を疑うか、その票を投じるかのいずれかの選択肢 (決して両方選択できるようにはしない) を投票者に対して控えめに用意することです。これで、投じられた票も正しいというある程度の信頼が生まれます。投票者のインターフェイスは、この問題が最も活発に研究されている分野です。普通の人々が選挙の正確性を確認する方法が次々と改良され、新たに誕生しています。詳細については、research.microsoft.com/crypto/voting を参照してください。

Josh Benaloh は Microsoft Research の上級暗号技術者です。彼は国際暗号学会の理事を 8 年間務めました。彼の多数の出版物の中に 1987 年の博士論文『Verifiable Secret-Ballot Elections』があります。