: MSDN Magazine February 2007

February 2007
FEBRUARY 2007 ボリューム 22 番号 2

• RibbonX API: 2007 Office system を独自のリボン タブとコントロールで拡張する
Eric Faller

• 進化したアート: 2007 Office System でカスタムの SmartArt グラフィックを作成する
Janet Schorr

• Office アドイン: VSTO を使用して PowerPoint および Visio 用のアドインを作成する
Paul Stubbs

• チームに追加されたデータベース開発機能: Visual Studio 2005 Team Edition for Database Professionals の概要
Brian A. Randell

• SQL Server 2005: 正規表現によるパターン マッチングとデータ抽出の簡略化
David Banister

Code Download (922 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: .NET アプリケーションの拡張性
Jack Gudenkauf および Jesse Kaplan

• データ ポイント: ADO.NET とカスタム オブジェクトを使用したデータ バインド アプリケーション
John Papa

• Office Space: Office オープン XML ファイルを構築する
Ted Pattison

• テストの実行: AJAX テストの自動化
Dr. James McCaffrey

• Cutting Edge: ASP.NET AJAX の展望
Dino Esposito

• サービス ステーション: WCF の Service Factory
Aaron Skonnard

• 基礎: キューに登録する WCF 応答サービスを構築する
Juval Lowy

• Netting C++: ネイティブ C++ から共通型システム (CTS) へのマッピング
Stanley B. Lippman

• C++ アット ワーク: "The Rationale"、ハイライト、そして 1 つの別れ
Paul DiLascia

• {End Bracket}: コーディングの勧め
Charles Petzold

各出版物

• 編集者のメモ: 新しい Office について
Joshua Trupin

• ツールボックス: リッチ Web アプリケーション UI、正規表現用ツールなど
Scott Mitchell


RibbonX API: 2007 Office system を独自のリボン タブとコントロールで拡張する

RibbonX API

2007 Office system を独自のリボン タブとコントロールで拡張する

Eric Faller
この記事の内容 : :
  • リボンの概要
  • RibbonX コントロールとその機能
  • アドインをアップグレードして RibbonX を使用する
  • Word および Excel 用のアドインを構築する
この記事は次のテクノロジを使用しています:
2007 Microsoft Office system
コードのダウンロード : : RibbonX2007_02.exe (201KB)
コンテンツ
既存のアドインのサポート
RibbonX の新機能
StartFromScratch モード
コマンドの無効化と再利用
必要に応じてアドインを読み込む
Word テンプレートを RibbonX にアップグレードする
XML とイメージを追加する
Excel 用の COM アドイン
コールバック関数を実装する
getVisible コールバックの値を無効にする
その他のコールバック
まとめ

2007 Microsoft Office system をこれまでにご覧になったことがある場合は、新しい Microsoft® Office のユーザー インターフェイスが従来の Office とは大きく異なっていることにお気付きになったでしょう。

その場合は、まったく新しい拡張モデルがこの UI の背後に用意されていることにも驚かれないはずです。

従来のツール バーとメニューの拡張で経験したように、Office 97 以降使用可能な CommandBars オブジェクト モデルは効率的に使用するのが困難でした。この新しい拡張モデルは RibbonX と呼ばれ、使いやすさ、一貫性、現代性、エンド ユーザーの操作に対する予測可能性など、新しい UI と同じ特長を多く備えています。

RibbonX では、UI の要素と構造を指定する XML と共に、動的なコールバックのメカニズムを使用して、UI の設計とコードを分離しています。UI の詳細についてはこの記事の後半で説明しますが、まずは、経験豊富な Office 開発者が新しい UI に関して理解しておくと便利な点をいくつか紹介します。

マイクロソフトでは、多くの使い慣れた用語が使用されている従来の UI から離れ、代わりにこれまで使用されていなかった新しい概念を導入しました。まず、念のため、新しい UI の基礎を確認しましょう。図 1 では、Microsoft Office Word 2007 の主な UI コンポーネントをラベルで示しています。

図 1 Word 2007 の新しいインターフェイスの要素
図 1 Word 2007 の新しいインターフェイスの要素(画像を縮小するには、ここをクリックします)
図 1 Word 2007 の新しいインターフェイスの要素
図 1 Word 2007 の新しいインターフェイスの要素 (画像を拡大するには、ここをクリックします)

1. リボン ドキュメントの上部にある大きな四角形の領域はリボンと呼ばれます。リボンには、タイトル バー、Office ボタン、クイック アクセス ツール バー、およびタブが含まれます。RibbonX はリボンとそれに含まれる要素で主に使用できます。

2. Office ボタン このボタンをクリックすると、Office メニューが表示されます。従来の Office では、これは [ファイル] メニューにほぼ該当します。Office メニューには、ドキュメントの内容ではなく、ドキュメント全体に対して操作するコマンドが含まれます。RibbonX アドインを使用すると、Office メニューの構成要素を自由に変更できます。ただし、Office ボタン自体はカスタマイズできません。

3. クイック アクセス ツール バー このツール バーには、一般的に使用されるコマンドが含まれます。エンド ユーザーが主にカスタマイズを行う場所は、このツール バーです。ユーザーはリボン コントロールのいずれかを右クリックし、そのコントロールをクイック アクセス ツール バーに追加できます。RibbonX を使用するカスタム コントロールも同様です。ただし、RibbonX アドインでは StartFromScratch モードが有効になっていない限り、通常、クイック アクセス ツール バーの変更はできません。これは、エンド ユーザーがこの領域を "所有" していると考えられているためです。

4. タブ リボンの中心的な要素となるのがタブです。タブには、作業中のドキュメントの内容を処理するための UI コントロールが含まれます。RibbonX アドインを使用すると、独自のカスタム タブの作成や組み込みタブの表示/非表示およびラベルの変更ができます。

5. 一連のコンテキスト タブ ドキュメント内で図や表などのオブジェクトを選択すると、そのオブジェクトを処理するための UI 要素を含む一連のコンテキスト タブが表示されます。RibbonX アドインを使用すると、一連の組み込みタブの表示/非表示を変更したり、カスタム タブを組み込みタブに追加したりできます。2007 Office system でサポートされていない機能の 1 つは、一連のカスタム コンテキスト タブの作成です。コンテキスト タブはタブ内に含まれており、それ以外については、通常のタブと同様に動作します。

6. グループ タブには一連のグループが含まれ、グループには個々の UI コントロールが含まれています。RibbonX アドインを使用すると、組み込みグループの表示/非表示を変更したり、独自のカスタム グループを作成したりできます。ただし、RibbonX アドインでは、組み込みグループの構成要素の変更はできません。この制限により、UI レイアウトが維持され、アドインどうしの競合や今後のバージョンの Office との競合を防げるためです。グループの角には、ダイアログ ボックス起動ツールが表示されている場合があります。これをクリックすると、[フォント] ダイアログ ボックスや [段落] ダイアログ ボックスなど、グループに関連したダイアログ ボックスが表示されます。

7. 作業ウィンドウ 2007 Office system でも作業ウィンドウがいくつか含まれていますが、同時に複数の作業ウィンドウを開けるようになりました。また、COM アドインを使用すると、ActiveX® コントロールや Windows® フォーム コントロールなどの要素を含むカスタム作業ウィンドウを作成できます (カスタム作業ウィンドウの機能は RibbonX とは異なるため、この記事では説明しません)。

8. ミニ ツール バー ミニ ツール バーは、一般的な書式設定コマンドの集まりで、選択したテキストの上部および右クリックすると表示されるショートカット メニューの上部に表示されます。RibbonX アドインでは、ミニ ツール バーの構成要素は変更できませんが、ミニ ツール バー上の組み込みコマンドを無効にしたり、再利用したりできます。

9. ショートカット メニュー これは、右クリックすると表示されるショートカット メニューで、従来の Office でおなじみのメニューです。2007 Office system では、ショートカット メニューで RibbonX を使用することはできませんが、従来と同様に CommandBars オブジェクト モデルを使用すると、拡張してカスタマイズできます。

10. ステータス バー ステータス バーには、文字カウントやズーム スライダなどの新しい便利なコントロールがいくつか含まれています。2007 Office system では、ステータス バーをアドインでカスタマイズすることはできませんが、非表示にすることは可能です。

既存のアドインのサポート

Office 2003 用のアドインを使用中の場合は、2007 Office system で動作するか疑問に思われるかもしれません。幸いなことに、このアドインが Office のバージョンを確認したり、新しいバージョンでの実行を拒否したりしない限り、これらは問題なく動作します。従来のメニューやツール バーはすべて、実際には存在しているのです。これらを操作するための従来のアドインも、2007 Office system で実行できなければならないという単純な理由のためです。では、従来のメニューやツール バーが新しい UI ではどのように表示されるか、例を見てみましょう。

図 2 に、カスタム ツール バーを作成し、従来のメニューやツール バーにボタンをいくつか追加する Word 2003 用のアドインの例を示します。また、Word 2007 でこのアドインを読み込むと、どのように表示されるかも示します。

図 2a 同じアドインを読み込んだ Word 2003 と Word 2007
図 2a 同じアドインを読み込んだ Word 2003 と Word 2007

図 2b

ご覧のように、従来の CommandBar アドインが読み込まれると、[アドイン] タブがリボンに表示されます。[アドイン] タブには、[メニュー コマンド]、[ツール バー コマンド]、および [ユーザー設定のツール バー] という 3 つのグループが表示されます。[メニュー コマンド] グループには、従来は組み込みのメニューに追加されたカスタム コントロールが表示され、[ツール バー コマンド] グループには、従来は組み込みのツール バーに追加されたカスタム コントロールが表示されます。また、カスタム操作を実行するために、従来の組み込みコントロールがアドインで Word 2007 用に再利用された場合 (たとえば、OnAction や Hyperlink プロパティの設定など) は、これらのグループに表示されます。[ユーザー設定のツール バー] グループには、従来のカスタム ツール バーがすべて上下に並べて表示されます。また、コントロールのヒントには、元のツール バーの名前が表示されます。

アドインをアンインストールしてもボタンが残る場合は、[アドイン] タブのコントロールで右クリックし、従来の CommandBar 構造からこれらを削除できます。

RibbonX の新機能

Office の RibbonX 機能では、XML ベースで UI を宣言する新しい時代のプログラミングが導入されています。これにより、UI を構築するためにさまざまなオブジェクト モデル呼び出しを行う複雑なコードを記述するのではなく、XML ファイルを作成して、構造化されたマークアップ形式で UI の外観を指定できます。これはアドイン開発者に多くの利点をもたらします。

1 つ目の利点は、UI とそのコードを分離できることです。これによって、UI 設計者は新しい設計を試すために、コードを手動で更新する方法を理解しておく必要がありません。つまり、アドインがその XML を外部ファイルから読み込む場合は、アドインを再コンパイルせずに UI を変更できます。これによって、UI をハードコードする場合に必要な "編集、コンパイル、実行" のサイクルの繰り返しでは、この中間の手順を行わずに済みます。

2 つ目の利点は、アドインのすべての UI が Office によって認識されることです。従来の Office では、CommandBars を使用したツール バーやメニューがどのアドインに付属するか認識していなかったと述べれば、この利点が重要であることがおわかりになるでしょう。UI の変更はすべて COM 呼び出しによって行われていたため、個々のアドインにはトレースバックされませんでした。これによって面倒な問題が発生していましたが、現在ではこのような問題の発生は完全に防がれています。

3 つ目の利点は、Office によってアドインの UI が自動クリーンアップされることです。これまで、アドインでは、終了時やアンインストール時にアドインのメニューやツール バーをすべて確認して削除するための特別なコードを用意しておく必要がありました。アドインがクラッシュした場合や、適切にクリーンアップを実行しなかった場合 (これは多くのアドインで発生した問題です) は、UI には不用になったボタンが残りました。2007 Office system ではアドインの UI を自動クリーンアップするため、クリーンアップ用のコードを記述する必要はなく、不用になった UI が残ることもありません。

また、Office の異なるバージョン間におけるバージョン管理に関する問題の発生も防げます。それは、有効な XML ファイルには xmlns 名前空間の宣言が含まれる必要があるためです。この宣言では、XML ファイルがどのスキーマのバージョンに基づくかを指定します。これによって、バージョン間での相違点がわかり、特定のアドインが対象とする Office のバージョンに関する情報が RibbonX に渡されて、UI が適切にマップされます。新しいバージョンの Office にアップグレードすると従来のアドインがよく壊れたり、COM 呼び出しで追加されたさまざまなコントロールが [アドイン] タブで区別されなかったりする問題の主な原因は、CommandBars オブジェクト モデルではバージョン管理が行われず、所有権も存在していなかったことです。

最後の利点は、XML ではツールのサポートが優れていることです。RibbonX スキーマ定義ファイル (XSD) については、さまざまな XML エディタ (Word 2007 も含む) の簡単なドラッグアンドドロップ機能を使用して、有効な RibbonX UI ファイルを作成できます。

CommandBars オブジェクト モデルは "プッシュ" モデルと呼ばれ、アドインはこのオブジェクト モデルのさまざまなプロパティを設定することで、すべての UI データを Office にプッシュします。UI コントロールの各プロパティは、起動時にアドインによって明示的に設定される必要があります。コントロールが属するメニューやツール バーが表示されない場合も同様です。大量のイメージ ファイルが読み込まれる場合は、負荷が非常に高くなり、パフォーマンスに影響を与える可能性があります。

一方、RibbonX は "プル" モデルを使用し、Office では必要な場合にのみアドインのデータを取得します。アドインでは、CommandBar.Name や CommandBarButton.Picture などのプロパティを設定するのではなく、getLabel や getImage などの get コールバックを提供します。Office では、コントロールのラベルやイメージを認識する必要があるときに、これらのコールバックを呼び出します。

プル モデルの主な利点は、パフォーマンスが優れていることです。2007 Office system では、ほとんどのアドインの UI はアプリケーションの起動時に表示されない場合があります。特に、アドインによって最上位レベルのタブや [アドイン] タブのグループが追加される場合は、UI がアプリケーションの起動時に表示されない場合があります。これらのコントロール用のイメージをすべて読み込むことで、パフォーマンスを低下させる必要はありません。ユーザーがタブをクリックした場合にのみ、イメージの読み込みが処理されればよいのです。RibbonX では、リソース読み込みの際のパフォーマンス低下をセッション全体で補うために、アドインが複数インストールされていても、Office アプリケーションがすばやく起動するように、コールバックの呼び出しをできるだけ遅延させます。

Office によってこれらのコールバックを呼び出すタイミングが判断されるとすれば、コントロールのプロパティがその都度どのように変更されるかについて疑問に思われるかもしれません。RibbonX ではコールバックの戻り値をキャッシュするため、アドインでこれらの値が無効にされるまで、コールバックが再度呼び出されることはありません。アドインでコールバックの戻り値を無効にするには、IRibbonUI インターフェイスの Invalidate メソッドと InvalidateControl メソッドを使用します。コントロールのプロパティが無効になると、RibbonX ではこれらが次に必要なときに再度呼び出す必要があることを認識します。このコントロールが現在表示されている場合は、即座にコールバックが呼び出されて、値が更新されます。このコントロールが表示されていない場合は、RibbonX ではコントロールが表示されるまで、コールバックを呼び出しません。この記事の後半では、サンプル アドインでコールバック関数を実装する方法と戻り値を無効にする方法の詳細について説明します。

図 3 に示すように、RibbonX には機能拡張に使用できる多くのコントロールの種類があります。CommandBars で使用できるのは、この一覧の最初の 6 つのアイテムだけでした。RibbonX アドインでは、多くの新しい UI オプションが使用できるようになっています。新しいコントロールの種類で最も興味深いのは、おそらく splitButton でしょう。このコントロールには、ボタンとドロップダウン メニューの両方が含まれます (図 4 を参照)。また、gallery コントロールを展開すると、一連のイメージが表示されます。これらはいずれも、UI を整理し、操作が容易に視覚化できるように、2007 Office system では多用されています。アドイン開発者は、開発する UI に同様の効果を与えられるように、これらの新しい種類のコントロールを使用することをお勧めします。

Figure 3 RibbonX コントロール (Close Figure 3)

RibbonX コントロール
1. button
2. toggleButton
3. editBox
4. menu
5. comboBox
6. dropDown
7. dialogBoxLauncher
8. gallery
9. splitButton
10. label
11. checkBox
12. group
13. tab
14. supertip
図 4 splitButton
図 4 splitButton

StartFromScratch モード

多くの開発者は独自の自己完結型アプリケーションを構築するために、Office をプラットフォームとして使用します。これらのアプリケーションでは、実行すると、Office のすべての組み込み UI が削除され、独自の UI に置き換えられることがよくあります。これまでは CommandBars を使用してこれを行うための標準メカニズムがありませんでした。そのため、このタスクを行うのは困難で、多くのエラーが発生しました。

RibbonX では StartFromScratch モードが導入されたため、Office の UI を非表示にするには、XML で 1 行記述するだけで済みます。ルート の <ribbon> タグで startFromScratch="true" に設定すると、以下のことができます。

  • 最上位レベルのメインの組み込みタブをすべて非表示にする (ただし、一連のコンテキスト タブの非表示は不可)
  • クイック アクセス ツール バーの要素を非表示にする
  • アドインで、クイック アクセス ツール バーに独自のボタンを追加できるようにする (通常は無効になっている機能)
  • Office メニューで、[新規作成]、[開く]、および [上書き保存] 以外のコマンドをすべて非表示にする

これらの変更が適用されたら、アドインでは UI にさらに変更を加えることができます。たとえば、一部の組み込みタブを表示したり、タブおよび Office メニューの項目をさらに非表示にしたりできます。

StartFromScratch モード機能のほとんどは、前述の UI 要素をすべて手動で非表示にしても同じ結果が得られます。ただし、StartFromScratch モードを使用すると、手動で行う場合には得られない利点がいくつかあります。1 つ目の利点は、手動では 50 行のコードを記述する必要がある場合に、XML を使用すると 1 行だけのコードで済むように、作業が非常に簡単なことです。2 つ目の利点は、StartFromScratch モードは今後のバージョンの Office との上位互換性を備えるように設計されていることです。Word の次のバージョンで新しい最上位レベルのタブや Office メニューの項目が追加されても、StartFromScratch モードではこれらを自動的に非表示にできます。手動で UI を非表示にするアドインでは、これらの新しい要素を非表示にするための変更を加える必要があります。

StartFromScratch モードがドキュメント単位で動作することも、優れた機能の 1 つです。ユーザーがマルチ ドキュメント インターフェイス (MDI) モードで複数のドキュメントを編集する場合は、ドキュメントの 1 つで StartFromScratch モードを有効にできます。これにより、このドキュメントと他のドキュメントを切り替えると、すべての UI の表示/非表示が自動的に切り替わります。つまり、コードを記述する必要がないのです。

コマンドの無効化と再利用

Office アドインを使用して一般的に行われる操作の 1 つには、組み込みコマンドの無効化があります。RibbonX を使用すると、この操作に必要なコードはわずか 1 行だけで済みます。開発者は、次に示すコードを XML の <commands> セクションに記述すると、組み込みコマンドを無効にできます。

<command idMso="Bold" enabled="false"/>

このコードでは、UI として表示される [太字] ボタンを無効にします。既定の構成では、このボタンはリボンの [フォント] グループおよびミニ ツール バーに含まれています。

また、このコードでは両方の場所で [太字] ボタンが無効になることにも注意してください。CommandBars を使用した場合は、このような操作ができませんでした。ユーザーがツール バーをカスタマイズして、別の場所に配置している可能性があるため、CommandBars の場合は、アドインでは UI をすべて列挙し、この種類のボタンをすべて手動で無効にしなければなりませんでした。また、CommandBars の場合は、"Bold" のような読みやすい名前ではなく、43 のような ID 番号でコマンドを検索しなければなりませんでした。RibbonX では、アドインでコマンドを特定の条件に基づいて無効にする場合に、getEnabled コールバックを使用できます。

RibbonX のコマンド再利用の機能でも、XML の <commands> 要素を使用します。アドインは、通常、Office の組み込み機能の強化や拡張のために使用されますが、UI の既存のボタンの再利用にも使用されます。また、IT 管理者は再利用機能を使用して、機能を制限する場合があります。たとえば、ユーザーが [印刷] ボタンをクリックすると、パスワード ダイアログ ボックスを表示されるように設定する場合があります。

RibbonX を使用すると、クリックで操作を行えるボタンはすべて再利用できます。ただし、ギャラリーやコンボ ボックスなどの複雑なコントロールは再利用できません。[上書き保存] ボタンの再利用に必要な XML の例を次に示します。

<command idMso="Save" onAction="MySaveFunction"/>

これによって、[上書き保存] ボタンをクリックすると、アドインの MySaveFunction が実行されます。この再利用機能では、[上書き保存] ボタンで組み込みの機能を実行するかどうかを選択し、独自の操作を行うことができます。

また、XML のこの 1 行で、Office メニューとクイック アクセス ツール バーの両方にある [上書き保存] ボタンをすべて再利用できます。個別のドキュメントで再利用機能が使用されている場合は、このドキュメントが全ドキュメント対象のテンプレートまたはアドインとして読み込まれていない限り、この機能は開いている他のドキュメントには適用されません。

必要に応じてアドインを読み込む

Office アドインにおける問題の 1 つは、DLL の起動に時間がかかる場合が多いことです。特に、DLL がマネージ コードで記述され、共通言語ランタイム (CLR) が読み込まれていない場合に、この問題が発生します。複数のアドインがインストールされている場合は、Office アプリケーションを起動するたびに、パフォーマンスが大幅に低下します。特定のセッションでは、アドインのほとんどの機能はおそらく使用されないにもかかわらず、その代価は払わなくてはならないのは皮肉なことです。

パフォーマンスの低下は Office とアドインの両方に悪影響を及ぼすため、Office では COM アドインを必要に応じて読み込む機能を備えています。この機能は、2007 Office system でアップグレードされ、RibbonX もサポートするように拡張されています。次に、このプロセスの概要を説明します。アドイン開発者は、アドインの LoadBehavior レジストリ キーを ConnectFirstTime (16) に設定します。アドインのインストール後、アプリケーションを初めて起動する際に、このアドインが起動します。Office ではアドインで RibbonX XML をクエリし、ディスクにこれをキャッシュします。次に、RibbonX ではアドインの get コールバックがすべて呼び出されて、イメージが読み込まれ、アドインの初期状態がキャッシュされます。

シャットダウン時に、アドインのこのレジストリ キーが DemandLoad に変更されます。アプリケーションのこれ以降の起動時には、アドインは起動しませんが、この RibbonX の UI はあたかも読み込まれているかのように表示されます。アドインのボタンをクリックすると直ちに、アドインが読み込まれ、このコマンドが実行されます。

必要に応じてアドインが読み込まれる機能は、エンド ユーザーには透過的に処理されます。また、開発者もレジストリ キーの設定以外には、何も行うことはありません (CommandBars で同様の機能を使用するためには、!<ProgID> 文字列の設定という困難なタスクが必要だったことと比較してください)。この機能はあらゆるアドインに有効とは言えませんが、単にボタンやメニュー項目を 1 つだけ UI に追加し、クリックされたらその機能をアクティブ化するようなアドインでは、多くの場合に有効です。ただし、ユーザーが UI を使用する前に複雑なコードを実行する必要があるアドインでは、この機能は有効ではありません。コードを実行するには、アドインが読み込まれている必要があるためです。

Word テンプレートを RibbonX にアップグレードする

RibbonX の主な機能についてはすべて説明したので、ここではアドインとコードの例をいくつか紹介しながら、その詳細について説明します。まず、従来のアドインで CommandBars を RibbonX にアップグレードするという例を紹介します。これは、2007 Office system にアップグレードする際に多くの開発者が行う作業です。

ここでアップグレードするアドインは、比較的簡単な Word テンプレート (.dot) です。このテンプレートでは、空白のページを法的文書に挿入して、"This page intentionally left blank" (このページは意図的に空白にされています) という標準のテキストを挿入します (結果的に、空白ではなくなるので矛盾したページになります)。

このアドインでは、文書に付属した CommandBars の [挿入] メニューのボタンを使用して実装されています。このボタンの OnAction プロパティは InsertBlankPage に設定されています。これは、次のコードを含む Visual Basic® for Applications (VBA) マクロです。

Sub InsertBlankPage()
    ' Insert a blank page
    Selection.InsertBreak Type:=wdPageBreak
    Selection.ParagraphFormat.Alignment = wdAlignParagraphCenter
    Selection.Font.Size = 14
    Selection.Font.Color = wdColorGray50
    Selection.Font.Name = "Arial Black"
    Selection.TypeText Text:="This Page Intentionally Left Blank"
    Selection.InsertBreak Type:=wdPageBreak
End Sub

このコードは簡単ですが、ここでは良い例となります。このファイルは MSDN® Magazine Web サイトからダウンロードできます。[テンプレートとアドイン] ダイアログ ボックスを使用すると、このファイルを全文書対象のテンプレートとしてインストールできます。

従来の CommandBar アドインを 2007 Office system の新しい UI にアップグレードする際の最初の手順は、従来の UI を新しいモデルにマップすることです。ただし、従来のツール バーとメニューがすべてリボンで置き換えられているため、アドインの UI をどこに配置すべきかわかりにくいという問題があります。

この最初の手順を行う前に、本当にアドインをアップグレードする必要があるかどうかを確認してください。予算が限られていれば、多くの場合は、アップグレードは不要という結論になるでしょう。このアドインを Word 2007 で読み込むと、[アドイン] タブで適切に動作することがわかります (図 5 を参照)。

図 5 従来のアドイン
図 5 従来のアドイン

ここでの動作には特に問題はありませんが、他の従来のアドインと共に [メニュー コマンド] グループに表示されることが問題になる場合があります。また、ここでは RibbonX の新しい機能はまったく使用されていません。ここで説明を終わりにしてもこの例を示した意味がないので、引き続き説明します。

このアドインでは文書にページを挿入するため、この UI は [挿入] タブに表示されるのが最適と言えます。アドインで文書全体を操作する場合は、Office メニューにこの UI を配置することもできます。既存の UI で適切に配置できる場所がない場合は、[アドイン] タブのカスタム グループに配置することをお勧めします。これで、新しいタブを作成して、ボタンを 1 つだけ配置する必要はありません。

Word の組み込みの [挿入] タブには、ページを挿入するためのグループがあります。このグループには、既に空白のページを挿入するためのボタンが含まれています。これは 2007 Office system の新機能です。この従来のアドインでは、意図的に空白にされていることを示すプレースホルダ テキストも挿入するため、これを組み込み機能とは区別します。この組み込みのグループの横に、"Legal Pages" というカスタム グループを追加し、わかりやすいアイコンを含む大きなボタンを配置します。この手順が完了すると、アドインの外観は図 6 のようになります。

図 6 ページを挿入するためのアドイン
図 6 ページを挿入するためのアドイン

次の手順は、このような UI を表示するための XML ファイルを作成することです。例を紹介する際は、最終結果を示してから、その詳細を説明する方が早い場合がよくあります。そのため、ここではまず図 7 に最終的な XML を示します。

Figure 7 空白のページを挿入するための XML (Close Figure 7)
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui">
  <ribbon>
    <tabs>
      <tab idMso="TabInsert">
        <group id="LegalGroup" label="Legal Pages"
               insertAfterMso="GroupInsertPages">
          <button id="BlankPageButton" label="Intentionally Blank Page"
                  size="large" image="BlankIcon"/>
    </group>
   </tab>
  </tabs>
 </ribbon>
</customUI>

ご覧のとおり、非常に単純です。ここではまず、ルート タグの <customUI> で 2007 Office system の RibbonX XML の名前空間を指定しています。次に、ribbon、tabs、tab、group、button の順にタグが記述され、UI の構造を表しています。

まず興味をひかれるのは、<tab> タグの idMso="TabInsert" プロパティです。XML のすべてのコントロールには、それらを識別するための ID を表すプロパティが必要です。組み込みコントロールでは、idMso プロパティを使用して、Office コントロールであることを表します。これにより、id プロパティが使用されるカスタム コントロールとは区別されます。つまり、XML ファイルでは、このプロパティによって、"組み込みの [挿入] タブを見つけて、そのタブにカスタム グループを作成する" ことを指示しています。また、insertAfterMso="GroupInsertPages" プロパティでは、"このグループを組み込みの [ページ] グループの後に挿入する" ことを指示しています。最後に、button id="BlankPageButton" プロパティで、"このグループ内にカスタム ボタンを作成する" ことを指示しています。

この例では、このグループとボタンのラベルは XML で静的に指定されているため、ラベルの文字列は変化しません。これはこの例では問題ありませんが、ローカライズなどの目的でラベルを動的に変更する場合は、代わりに getLabel プロパティを使用して、ラベルを返す関数を指定できます。

image="BlankIcon" も興味深いプロパティです。このプロパティでは、RibbonX に対し、指定した ID を持つ Word テンプレート ファイルのイメージを検索するように指示します。XML を挿入するときに、このイメージもファイルに挿入する必要があります。

XML とイメージを追加する

RibbonX XML の追加は 2007 Office system の新しいファイル形式でのみサポートされます。そのため、まず .dot ファイルを開き、.dotm ファイル形式で保存します。.dotm の "m" はこのファイルにマクロが含まれることを示します。次に、このファイル名に .zip 拡張子を追加して変更し、ファイルを開いて、その内容を確認します。これを図 8 に示します。

図 8 文書テンプレートの内容
図 8 文書テンプレートの内容(画像を縮小するには、ここをクリックします)
図 8 文書テンプレートの内容
図 8 文書テンプレートの内容 (画像を拡大するには、ここをクリックします)

オープン XML ファイル形式の詳細については別の記事で説明しますが、ここでは、これらのファイルはさまざまな相互関係を持つ複数のファイルを含んだ圧縮パッケージであるとだけ述べておきます。この例では、document.xml がメインの Word 文書であり、スタイル情報、VBA コード、文書に付属する CommandBars などのアイテムに関連付けられています。ここでは文書に付属する CommandBars はこのファイルには必要ないため、attachedToolbars.bin ファイルを .zip ファイルから削除し、再び圧縮します。

RibbonX XML およびアイコン イメージを挿入するには、.zip ファイルを手動で編集することもできますが、ツールを使用すると非常に簡単に編集できるため、ここでもツールを使用します。openxmldeveloper.org (英語) では、オープン XML ファイル形式に使用できるツールが数多く紹介されています。ここでは、Custom UI Editor (カスタム UI エディタ) というツールを使用します。

Custom UI Editor は、RibbonX XML および関連するイメージをオープン XML ファイル形式のファイルに挿入するための便利なツールです。このツールを使用して、.dotm ファイルを開き、XML を貼り付けて、イメージを挿入します (図 9 を参照)。次に、ファイルを保存します。ちなみに、このファイルを .zip ファイルとして開くと、このツールで作成した XML ファイルが \customUI\customUI.xml に含まれ、イメージ ファイルが \customUI\images\BlankIcon.png に含まれていることがわかります。また、これらの新しいファイルを参照できるように rel リレーションシップ ファイルがいくつか変更されたことがわかります。

図 9 テンプレートを編集する
図 9 テンプレートを編集する(画像を縮小するには、ここをクリックします)
図 9 テンプレートを編集する
図 9 テンプレートを編集する (画像を拡大するには、ここをクリックします)

このファイルを Word で開くと、新しいボタンが追加されていますが、クリックしても何も変化がないことがわかります。それはどのマクロを呼び出すかまだ指定していないためであることは明らかです。どのマクロを呼び出すか指定するには、次に赤で表示されているプロパティを XML に追加します。

<button id="BlankPageButton" label="Intentionally Blank Page"
        size="large" image="BlankIcon"
        onAction="RibbonXOnAction" tag="InsertBlankPage" />

ここで、InsertBlankPage マクロを再利用せずに、RibbonXOnAction という新しいマクロを作成するのは意外かもしれません。このようにする理由は、RibbonXOnAction マクロは、同様の機能を持つ CommandBar マクロとは異なるシグネチャを持つためです。マクロの数が多い場合は、これらをすべて手動で変更すると、作業に時間がかかり、間違いが起こりやすくなります。また、コードで間違いが起こると、従来の Office では実行できなくなります。

これらの問題の発生を防ぐには、新しい RibbonX マクロから従来の CommandBar マクロを呼び出します。ここで tag プロパティが役に立ちます。tag プロパティとは文字列のプロパティで、UI には表示されませんが、アドインのコードに渡されます。RibbonXOnAction マクロでこのプロパティをどのように使用するかを次に示します。このコードを新しい VBA モジュールに貼り付けることもできます。

Sub RibbonXOnAction(button As IRibbonControl)
    Application.Run button.Tag
End Sub

RibbonXOnAction マクロは IRibbonControl オブジェクトをパラメータとして受け取ります。これは先ほどクリックしたボタンを表します。CommandBars では、OnAction マクロはパラメータを受け取らなかったため、どのボタンがクリックされたかを判断するのが困難でした。RibbonX の IRibbonControl オブジェクトには Id プロパティと Tag プロパティがあるため、RibbonX ではこの問題が解決されています。

このマクロを新しいボタンに設定すると、ボタンが動作することがわかります。RibbonX を使用して、従来のアドインのコードをアップグレードして実行するには、新しい 1 行のマクロを追加するだけで済みます。既存のコードは何も変更する必要はありません。なんと簡単なのでしょう。

Excel 用の COM アドイン

2 つ目の例では、少し複雑な Excel® 用の COM アドインを記述してみましょう。このアドインはタイムカードを入力して提出するためのアプリケーションで、基幹業務 (LOB) アプリケーションを模しています。ここではコード スニペットを示しますが、完全なソース コードをダウンロードすることもできます。

まず、どのような種類の UI が必要かを判断します。アドインでは、ユーザーがタイムカードを作成して編集し、提出できるようにします。タイムカードで最初に表示する必要がある UI は [New Timecard] ボタンだけなので、これを Office メニューに表示します (図 10 を参照)。

図 10 新しいボタン
図 10 新しいボタン

[New Timecard] ボタンをクリックするか、既存のタイムカードを開くと、タイムカードの残りの UI がカスタム タブに表示されます。Excel では、[ページ レイアウト]、[数式]、[校閲]、[データ] などの多くのタブはタイムカードの入力には適用されないため、これらを同時に非表示にします (図 11 を参照)。

図 11 タイムカードのリボンに表示されるカスタム タブ
図 11 タイムカードのリボンに表示されるカスタム タブ(画像を縮小するには、ここをクリックします)
図 11 タイムカードのリボンに表示されるカスタム タブ
図 11 タイムカードのリボンに表示されるカスタム タブ (画像を拡大するには、ここをクリックします)

タイムカードの UI は前の例のアドインよりもかなり複雑です。そのため、この UI を作成するための XML も、図 12 に示すように複雑になります。COM アドインを RibbonX で使用するためにまず必要なことは、IRibbonExtensibility インターフェイスの実装です。幸いなことに、これは Visual Studio® の IntelliSense® 機能を使用すると簡単です。アドインを作成するには、Visual Studio の [新しいプロジェクト] ダイアログ ボックスで [その他のプロジェクトの種類] カテゴリを展開し、[機能拡張] をクリックして、[共有アドイン] をダブルクリックします。

Figure 12 タイムカードの UI の XML (Close Figure 12)
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"
          onLoad="OnLoad" loadImage="LoadImage">
  <ribbon>
    <officeMenu>
      <button id="NewTimecard" insertAfterMso="FileNew"
              label="New Timecard" imageMso="StartAfterPrevious"
              onAction="NewTimecard" />
    </officeMenu>
    <tabs>
      <tab id="TimecardTab" label="Timecard"
           getVisible="IsCustomTabVisible">
        <group id="EditingTools" label="Editing Tools">
          <button id="ClearTimecard" label="Clear Timecard" size="large"
                  imageMso="TableDeleteRowsAndColumnsMenuWord"
                  onAction="ClearTimecard"/>
          <button id="InsertDay" label="Insert New Day" size="large"
                  imageMso="CellsInsertDialog" onAction="InsertNewDay"/>
          <button id="CalculateTotal" label="Calculate Hours Worked"
                  size="large" imageMso="SlideShowRehearseTimings"
                  onAction="CalculateHours"/>
        </group>
        <group id="WageTools" label="Wage Tools">
          <editBox id="Wage" label="Hourly wage:"
                   image="dollar1.png" onChange="OnWageChanged"
                   getText="GetWage"/>
          <editBox id="Overtime" label="Overtime bonus:"
                   image="dollar2.png" onChange="OnOvertimeChanged"
                   getText="GetOvertime"/>
          <button id="CalculatePay" label="Calculate Pay"
                  size="large" imageMso="AcceptInvitation"
                  onAction="CalculatePay"/>
        </group>
        <group id="SubmissionTools" label="Submission Tools">
          <button id="SignTimecard" label="Sign Timecard"
                  size="large" image="signature.png"
                  onAction="SignTimecard"/>
          <button id="SubmitTimecard" label="Submit Timecard"
                  size="large" image="submit.png"
                  onAction="SubmitTimecard" getEnabled="IsTimecardSigned"
                  screentip="Make sure to sign your timecard before
                            submitting" />
        </group>
      </tab>
      <tab idMso="TabPageLayoutExcel" getVisible="IsBuiltinTabVisible"/>
      <tab idMso="TabData" getVisible="IsBuiltinTabVisible" />
      <tab idMso="TabReview" getVisible="IsBuiltinTabVisible" />
      <tab idMso="TabFormulas" getVisible="IsBuiltinTabVisible" />
    </tabs>
  </ribbon>
</customUI>

ダイアログ ボックスにはウィザードの手順が表示され、それに従ってクリックしていきます。ウィザードでは、アドインをインストールする Office アプリケーションや使用するプログラミング言語などのさまざまなオプションを選択する必要があります。この例では C# を使用します。

アドインが作成されると、COM アドインの IDTExtensibility2 インターフェイスを実装する Connect クラスが作成されたことがわかります。これは IRibbonExtensibility インターフェイスを実装するオブジェクトです。このインターフェイスが追加されると、Visual Studio ではそのメソッドを自動的に実装します。コードは次のようになります。

using Office = Microsoft.Office.Core;
[GuidAttribute("E8407539-C642-43BB-8FCB-2E27A46179DE"),
 ProgId("TimecardAddin.Connect")]
public class Connect : Object, Extensibility.IDTExtensibility2,
    Office.IRibbonExtensibility
{
    public string GetCustomUI(string RibbonID)
    {
        throw new Exception("Not implemented.");
    }

    // IDTExtensibility methods
}

IRibbonExtensibility のメソッドのうちここで唯一使用されているのは GetCustomUI です。このメソッドは RibbonX XML を文字列として返します。このメソッドは RibbonID パラメータを受け取ります。これは "Microsoft.Word.Document" や "Microsoft.Excel.Workbook" のような文字列であり、主に、複数のアプリケーションで実行するアドインや Microsoft Outlook のように複数の種類のリボンを持つアプリケーションで実行するアドインを記述する際に便利です。

customui.xml という XML ファイルを埋め込みリソースとしてプロジェクトに追加した場合は、次のようなコードを使用し、GetCustomUI メソッドでこのファイルを返します。

public string GetCustomUI(string RibbonID)
{
    Assembly assembly = Assembly.GetExecutingAssembly();
    using(Stream stream = assembly.GetManifestResourceStream(
        "TimecardAddin.customui.xml"))
    {
        return new StreamReader(stream).ReadToEnd();
    }
}

これで、アドインを構築し、インストールできます。Excel を起動すると、カスタマイズした UI が表示されます。ただし、カスタム イメージが読み込まれていないこと、および追加されているボタンはクリックしても何も変化がないことがわかります。

XML で参照されているコールバック関数はすべて、まだ実装されていないため呼び出せません。[Excel のオプション] ダイアログ ボックスの [詳細設定] セクションで、[アドイン ユーザー インターフェイスに関するエラーを表示する] チェック ボックスがオンになっている場合は、呼び出しに失敗したコールバックの一連のエラー メッセージが表示されます。RibbonX アドインの開発中は、このチェック ボックスをオンにしてください。アドインで問題が発生した場合に、さまざまなエラー メッセージを表示することができます。表示された最初のエラー メッセージでは、onLoad コールバックが実行できなかったことを表しています。では、まずこのコールバックを実装してみましょう。

コールバック関数を実装する

リボンのルート タグの <customUI> には、アドインで IRibbonUI オブジェクトへの参照を取得するために実装できる onLoad コールバックがあります。ただし、これは省略可能です。アドインの UI の状態を変更する必要があるときは、IRibbonUI オブジェクトを使用して、コールバックのキャッシュされた戻り値を無効にします。この方法の詳細については後ほど説明しますが、ここではメンバ変数にこのオブジェクトを格納します。次のコードを Connect クラスを追加します。

Office.IRibbonUI m_Ribbon;

public void OnLoad(Office.IRibbonUI ribbon)
{
    m_Ribbon = ribbon;
}

loadImage も <customUI> タグで使用するコールバックの 1 つで、これも省略可能です。このコールバックで、COM アドインのコントロールのアイコンを読み込むことができます。これが XML の image="dollar1.png" の行で行う操作です。RibbonX では、イメージのプロパティで指定した文字列ごとに、loadImage が呼び出され、該当のイメージが読み込まれます。

イメージの格納方法はアドインによって異なりますが、この例では XML の場合と同様に、イメージを埋め込みリソースとして追加します。TimecardAddin アセンブリに dollar1.png がリソースとして追加されていると仮定して、loadImage コールバックを次のように実装します。

public Bitmap LoadImage(string imageName)
{
    Assembly assembly = Assembly.GetExecutingAssembly();
    Stream stream = assembly.GetManifestResourceStream(
        "TimecardAddin." + imageName);
    return new Bitmap(stream);
}

この関数には興味深い点がいくつかあります。まず、イメージとして System.Drawing.Bitmap オブジェクトを返していることです。System.Drawing.Bitmap は、RibbonX の機能を持つ 2007 Office system で初めてサポートされました。これを使用するのは非常に簡単であり、マネージ アドインのイメージを返す方法としてお勧めします。C++ および Visual Basic 6.0 のアンマネージ アドインでは、イメージとして IPictureDisp オブジェクトを返すことができます。CommandBars ではこの形式が使用されていました。

次に興味深いのは、返すイメージが 1 つであっても、ビットマップ データと透明度データの両方を含んでいることです。つまり、Office のアドイン用に Picture アイコンと Mask アイコンの 2 つを個別に作成する必要がなくなりました。RibbonX を備えた Office では、アルファ チャネル付きのフル 32 ビット アイコンをサポートできるようになったためです。現在のツールでは、アルファ チャネルに対するサポートは PNG 形式が最も優れているため、RibbonX アイコンではこの形式を使用することをお勧めします。

次に実装する必要があるのは、組み込みタブとカスタム タブでの getVisible コールバックです。すべての組み込みタブで IsBuiltinTabVisible を使用し、カスタム タブでは IsCustomTabVisible を使用します。この 2 つの関数では逆の値を返します。つまり、タイムカードが編集されるときは組み込みタブが非表示になり、カスタム タブが表示される必要があります。

ファイルの Category プロパティを Timecard に設定することで、特定の Excel ブックがタイムシートかどうかをアドインで判断できます。このプロパティを持っていないファイルは無視されます。

IsBuiltinTabVisible と IsCustomTabVisible 関数の両方で、IsTimecard ヘルパ関数を使用します。

public bool IsCustomTabVisible(Office.IRibbonControl tab) {
    return IsTimecard(tab.Context as Excel.Window);
}

public bool IsBuiltinTabVisible(Office.IRibbonControl tab) {
    return !IsTimecard(tab.Context as Excel.Window);
}
private bool IsTimecard(Excel.Window window)
{
    if (null == window) return false;

    Excel.Workbook workbook = (Excel.Workbook)window.Parent;
    Office.DocumentProperties properties =
        (Office.DocumentProperties)workbook.BuiltinDocumentProperties;
    Office.DocumentProperty category = properties["Category"];

    return (null != category && null != category.Value &&
            category.Value.Equals("Timecard"));
}

このコードでは、IRibbonControl の Context プロパティを使用して、RibbonX コールバックに渡します。Context プロパティは、ほとんどの Office アプリケーションでは Window オブジェクトに等しくなります。ただし、Access には Window オブジェクトがないため、このプロパティは NULL になります。Outlook では、このプロパティは Inspector オブジェクトと等しくなります。アドインでは、この RibbonX コールバックでどのドキュメントが参照されているかを判断するために、Window オブジェクトを使用する必要があります。それは、このドキュメントが Application.ActiveDocument プロパティとは必ずしも等しくないためです。Window オブジェクトは、同じドキュメントを表示する 2 つの異なるウィンドウを区別するためにも使用できます。

Window オブジェクトから Workbook オブジェクトを取得し、このオブジェクトから Category プロパティを取得して、Timecard と等しいかどうかを確認できます。

getVisible コールバックの値を無効にする

Excel を起動すると、カスタム タブが非表示で、組み込みタブが表示されていることがわかります。既定のブックがタイムカードではないため、これが適切な状態です。ただし Timecard ブックを開いても、タイムカード用のタブが表示されません。これはなぜでしょうか。

パフォーマンス上の理由により、RibbonX では get コールバックからの戻り値をキャッシュします。キャッシュしないと、UI で変更がある場合に備えて、アドインでコールバックを毎回呼び出すことになるためです。そのため、UI で変更がある場合は、アドインから Office に伝える必要があります。

この場合に、前述の IRibbonUI オブジェクトが役立ちます。このオブジェクトには、Invalidate メソッドと InvalidateControl メソッドがあります。Invalidate メソッドでは、コールバックのすべてのキャッシュ値を無効にします。また、InvalidateControl メソッドでは、controlID パラメータを受け取り、特定のカスタム コントロール用のコールバックのキャッシュ値を無効にします。

ここでは、Excel でブックが切り替わるときに、タブの getVisible コールバックの戻り値を必ず無効にする必要があります。これを行うには、Excel の WorkbookActivate イベントを使用できます。OnStartupComplete メソッドを使用して、このイベント ハンドラを追加します。

public void OnStartupComplete(ref System.Array custom)
{
    excelApplication.WorkbookActivate +=
       new Excel.AppEvents_WorkbookActivateEventHandler(
           OnWorkbookActivate);
}

void OnWorkbookActivate(Excel.Workbook Wb)
{
    // the custom tab might need to become visible on workbook
    // switch, so invalidate the cached getVisible values
    m_Ribbon.Invalidate();
}

幸いなことに、Visual Studio の IntelliSense 機能を使用すると、「+=」と入力するだけで、このイベント ハンドラを自動的に実装できます。

これで、Excel を起動し、Timecard ブックと別のブックを切り替えると、タイムカード用のカスタム タブが表示され、組み込みタブが非表示になることがわかります。完璧でしょう。後は、アドインを使用して、タイムカードを実際に作成できるようにするだけです。

[New Timecard] ボタンのコールバックは非常に単純です。前述の OnAction と同じシグネチャを使用します。これで、Category プロパティが Timecard に設定されたブックが新規作成され、既定のワークシートが表示されます。

public void NewTimecard(Office.IRibbonControl button)
{
    Excel.Workbook timecard =
        excelApplication.Workbooks.Add(Missing.Value);
    Office.DocumentProperties properties =
        (Office.DocumentProperties)timecard.BuiltinDocumentProperties;
    properties["Category"].Value = "Timecard";
    Excel.Worksheet timesheet =
        (Excel.Worksheet)timecard.Worksheets[1];
    NewTimesheet(timesheet);
}

このコールバックでは、ブックの最初のワークシートでタイムカードを初期化するためのヘルパ関数が使用されています。これは [Clear Timecard] ボタンでも呼び出される関数です。

その他のコールバック

残りのコールバックは、[New Timecard] ボタンのコールバックに類似しています。そのため、ここではコードは紹介しませんが、興味深い点をいくつか述べておきます。時給値と超過作業時間の値を保持する editBox コントロールのコードは、他の簡単なボタンとは異なり、多少複雑になります。この時給値のボックス用のコールバックを次に示します。

double m_Wage = 5.25;

public void OnWageChanged(Office.IRibbonControl c, string wage)
{
    m_Wage = double.Parse(wage);
}

public string GetWage(Office.IRibbonControl c)
{
    return m_Wage.ToString();
}

OnWageChanged メソッドは、ユーザーが時給値のボックスに新しい値を入力すると呼び出されます。この例では新しい値を変換して記録するだけですが、より現実的なアドインでは、この値を受け入れる前に境界チェックや妥当性検査をいくつか行うことをお勧めします。

GetWage メソッドでは、editBox コントロールの初期値を返します。IRibbonUI オブジェクトによって UI が無効になった場合にもこのメソッドが呼び出されることに注意してください。アドインを使用する場合は、editBox コントロール内に保持されるユーザーの入力値に依存すべきではありません。そのため、ここで示した OnWageChanged および GetWage メソッドのように、onChanged と getText メソッドの組み合わせを使用して、現在値を格納することをお勧めします。

このアドインでは、アドインのメンバ変数に時給値を格納します。これが可能なのは、この値はアドインに対してグローバルであり、このブックに固有ではないためです。この値が個別のタイムカードに関連付けられる場合は、ユーザーがタイムカードを切り替えた際に、値が適切に更新されるように、タイムカードに関連付けられている値をメモリに格納する必要があります。

もう 1 つの興味深いコントロールは、[Sign Timecard] ボタンです。このボタンをクリックすると、ユーザーの名前を入力するダイアログ ボックスが表示され、その名前がタイムカードに書き込まれます。

public void SignTimecard(Office.IRibbonControl button)
{
    SignatureDialog sd = new SignatureDialog();
    if (DialogResult.OK == sd.ShowDialog())
    {
        string signature = sd.GetSignature();

        timesheet.get_Range("E" + (daycount + 5),
        Missing.Value).Formula = signature;

        m_Ribbon.InvalidateControl("SubmitTimecard");
    }
}

このコードでは、[Submit Timecard] ボタンで IRibbonUI.InvalidateControl が呼び出されています。[Submit Timecard] ボタンは、署名を確認する getEnabled 関数を使用して、タイムカードに署名されるまで無効になっています。署名が入力されると、このボタンを再度有効にするために、getEnabled 関数の戻り値を無効にする必要があります。

[Sign Timecard] ボタンがクリックされた場合にのみ、アドインは InvalidateControl を呼び出すため、ユーザーがタイムカードのセルに手動で署名を入力しても、[Submit Timecard] ボタンは有効になりません。また、ユーザーが署名を削除した場合でも、このボタンは無効になりません。これを行うには、署名のセルが変更されると、SheetChanged イベントをフックして、InvalidateControl を呼び出すように設定できます。

まとめ

タイムカード アドインについては以上です。このアドインには全体でも数百行のコードしか含まれていません。これには、Visual Studio で自動生成されたコードもすべて含まれています。これは非常に簡単なアドインですが、実際に使用するには、さらに多くのエラー チェック用のコードや入力値の検査用のコードを追加する必要があります。ただし、このアドインを紹介することで、RibbonX を使用すると、独自のアプリケーションの作成する際に、Office をプラットフォームとして簡単に活用できることがわかりました。このアドインを試してみたい場合は、ソース コードをダウンロードしてください。

この記事では、RibbonX 機能をごく簡単に紹介したにすぎません。詳細およびサンプル コードについては、MSDN の公式ページ (英語) を参照してください。このページには、RibbonX に関するすべてのドキュメント、コントロール ID の一覧、サンプル アドイン、RibbonX に関するブログなどへのリンクが含まれています。

Eric Faller は、マイクロソフトの Office ユーザー エクスペリエンス チームのソフトウェア開発者です。開発者が Office の新しい UI を拡張できるように日々取り組んでいます。連絡先は eric.faller@microsoft.com (英語) です。

© 2007 Microsoft Corporation and CMP Media, LLC. All rights reserved; 許可なしに一部または全体を複製することは禁止されています.


進化したアート: 2007 Office System でカスタムの SmartArt グラフィックを作成する

進化したアート

2007 Office System でカスタムの SmartArt グラフィックを作成する

Janet Schorr
この記事の内容 : :
  • SmartArt グラフィック データ モデル
  • カスタムの SmartArt グラフィック用 XML を作成する
  • アルゴリズム、制約、規則
  • カスタムの SmartArt グラフィックをテストおよびデバッグする
この記事は次のテクノロジを使用しています:
2007 Microsoft Office System
コードのダウンロード : : SmartArt2007_02.exe (152KB)
コンテンツ
グラフィック レイアウトを計画する
レイアウト ノードとアルゴリズム
レイアウト ツリーとデータ モデル マッピング
図形のプロパティ
制約、規則、テキスト プロパティ
レイアウト定義ファイルのパッケージ化
グラフィック レイアウトをテストする
エラーの種類とエラー ログ
有効ファイルの設計エラー
既存のグラフィック レイアウト定義を変更する
次のステップ

2007 Microsoft Office システムでは、Word ドキュメント、Excel® ワークシート、PowerPoint® プレゼンテーション、および Outlook® 電子メール メッセージなどの Office ファイルに、洗練されたグラフィックおよびダイアグラムをすばやく追加するための新しい方法が用意されています。SmartArt™ グラフィックと呼ばれるこの新しい機能には、すばやく挿入および構成できるテンプレートと定義済み図形のギャラリー (ライブラリ) が備えられています。たとえば、サイズと配置の自動調整や、オブジェクトおよびプロパティの編集が可能です (図 1 の例を参照)。どころが、これらのグラフィックを使っていろいろな可能性を試しているうちに、もっと多くのグラフィックをギャラリーに追加したいと考えるようになるかもしれません。そんなときでも、SmartArt グラフィックには全面的な拡張性がサポートされているので、ユーザーが独自のレイアウトを作成することが可能です。

SmartArt レイアウト ギャラリーに含まれるそれぞれのグラフィック レイアウトは、XML ファイルが土台になっています。この XML ファイルでは、ユーザーによって入力されたデータセットに基づいて (図 1 の場合は、このデータセットが "設計"、"作成"、"テスト" という 3 つの文字列で構成されています)、SmartArt グラフィックがオブジェクトをどのように構築すればよいかが定義されています。SmartArt グラフィック レイアウトでは、各種のレイアウト オプションを提供する一連のアルゴリズムが使用されています。これには、線形フロー アルゴリズム、循環アルゴリズム、連携して階層ダイアグラムを形成する 2 つのアルゴリズムなどがあります。また、複合アルゴリズムもサポートされているので、各種の図形のサイズ変更と配置をどこでどのように行うのかを明確に指定することができ、多様なグラフィックを柔軟に作成できます。この記事では、ユーザーが独自の SmartArt グラフィック レイアウトを作成するための基本事項を紹介します。

図 1a SmartArt グラフィックで作成したサンプル ダイアグラム
図 1a SmartArt グラフィックで作成したサンプル ダイアグラム

図 1b

図 1c

図 1d

グラフィック レイアウトを計画する

SmartArt グラフィック レイアウトを作成するための最初のステップは、グラフィックの外観を決めることです。外観が決まったら、その SmartArt グラフィックを分析して、どのように作成すればよいかを決めます。

図 2 のようなグラフィックを作成すると仮定します。この SmartArt グラフィックを作成するには複数のステップが必要です。この記事では各ステップについて詳しく説明します。まず、グラフィックを作成するにはどんな図形が必要になるかを考えます。この場合、青い図形用には角丸四角形、白い直線用には四角形、テキスト領域用には塗りつぶしや線のない四角形が必要です。

図 2 目的のグラフィック レイアウト
図 2 目的のグラフィック レイアウト(画像を縮小するには、ここをクリックします)
図 2 目的のグラフィック レイアウト
図 2 目的のグラフィック レイアウト (画像を拡大するには、ここをクリックします)

次に、それぞれの図形をどのように配置するかを考えます。この場合、青い四角形は描画領域の左端から水平に並んでいます。白いボックスと青いボックスの組み合せが単一の複合図形を形成しています。さらに、複合図形どうしの間には、空白があります。線形フロー、屈折した線形フロー、循環、階層、複合 (または固定位置) のレイアウトについては、SmartArt のいずれかのアルゴリズムによって図形の配置が決定されます。

ここで、テキストが図形内でどのように表示されるのかを見てみましょう。SmartArt グラフィックはすべて、元のデータ モデルにマップされます。データ モデルは階層リストとして可視化することができます。たとえば、図 2 のグラフィックに示されているテキストは、図 3 のリストに基づいています。

Figure 3 モデル (Close Figure 3)

設計
ステップ 1
ステップ 2
ステップ 3
作成
ステップ 1
ステップ 2
ステップ 3
テスト
ステップ 1
ステップ 2
ステップ 3

1 つの SmartArt グラフィックに含まれる各図形は、複数レベルのテキストをサポートすることができます。テキストはこの構造にマップされます。図 2 の例では、各複合図形に 2 レベルのテキストが含まれています。第 1 レベルは親テキストまたはレベル 1 アイテムと呼ばれることもあります。第 2 レベルは子テキストまたはレベル 2 アイテムと呼ばれることもあります。一般的に、子テキストの先頭には、親テキストの下位アイテムであることを示す行頭文字が付いています。

最後のステップは、特定のデータセットから離れて抽象化する場合の表示方法を決めることです。SmartArt グラフィック レイアウトでは、次に示す数種類のオプションから選択できます。

スタティック グラフィック レイアウト : 常に同数の図形を表示する場合は、スタティック グラフィック レイアウトを作成できます。先ほどの例では、常に 3 つの図形が含まれるレイアウトを作成できます。最初に設定した 3 つのレベル 1 アイテムについては、ユーザーが入力したテキストが図形内に表示されますが、後続のレベル 1 アイテムに追加したテキストは表示されません。

セミダイナミック グラフィック レイアウト : セミダイナミック グラフィック レイアウトを使用するとレベル 1 アイテムの数だけ複合図形を表示できます。図形の最大数は指定できます。先ほどの例では、0 ～ 3 個の複合図形が含まれるグラフィックを作成できます。ユーザーがレベル 1 テキストに行を追加すると、新しい図形が追加されます。3 行を超えるレベル 1 テキストを追加した場合は、新しい図形が追加されません。

ダイナミック グラフィック レイアウト : ダイナミック グラフィック レイアウトでは、図形が特定の数に限定されません。レベル 1 テキストの行数だけ複合図形を追加できます。図形が追加されると、描画領域内に収まるように、必要に応じて図形のサイズが縮小されます。サンプル グラフィックの場合は、ダイナミック グラフィック レイアウトを選ぶとよいでしょう。

レイアウト ノードとアルゴリズム

SmartArt グラフィック レイアウトは、どんな図形を作成するのか、これらをどのようにデータ モデルにマッピングするのか、図形を並べるためにどのアルゴリズムを使用するのかを記述した XML ファイルで作成されます (これらの XML ファイルは OpenDocument パッケージの一部であり、Word および PowerPoint に使用されている新しいファイル形式に似ています)。このようなファイルの 1 つである layout1.xml では、メインのレイアウト定義が提供されています。

レイアウト定義の基本要素はレイアウト ノードです。レイアウトの各ノードには、そのレイアウトの子ノードをサイズ調整または配置する方法や、含まれる図形内でテキストのサイズを調整する方法を指定した、アルゴリズムが関連付けられています。最終的には、レイアウト内のすべてのノードが、単一のレイアウト ノード以下にネストされます (図 4 を参照)。

図 4 グラフィック レイアウトの定義
図 4 グラフィック レイアウトの定義(画像を縮小するには、ここをクリックします)
図 4 グラフィック レイアウトの定義
図 4 グラフィック レイアウトの定義 (画像を拡大するには、ここをクリックします)

レイアウト定義内のレイアウト ノードは、レイアウト ツリーの形をした階層構造になっています。実際には、このツリーには、ユーザーのデータ モデル内の各親テキスト アイテムごとに、複合レイアウト ノードおよびスペース レイアウト ノードが含められます。ただし、このレイアウト ノードおよびその子孫の定義は、どのアイテムでも同じなので、レイアウト定義ファイルにこれらのレイアウト ノードを定義する必要があるのは 1 度だけです。これと一緒に、グラフィックに対するレイアウト ノードのこのパターンを何回作成するかを制御する追加情報も定義されます。図 5 は、この例に対応する XML の基本構造を示しています。

Figure 5 サンプル レイアウト ツリーを定義している XML 構造 (Close Figure 5)
<?xml version="1.0" encoding="utf-8"?>
<layoutDef xmlns=
        "http://schemas.openxmlformats.org/drawingml/2006/diagram">
    <layoutNode name="diagram">
        <alg type="lin" />
        <presOf />
        <forEach axis="ch" ptType="node">
            <layoutNode name="composite">
                <alg type="composite" />
                <presOf />
                <layoutNode name="roundRect">
                    <alg type="sp" />
                    <presOf axis="self" />
                </layoutNode>
                <layoutNode name="parentText">
                    <alg type="tx" />
                    <presOf axis="self" />
                </layoutNode>
                <layoutNode name="whiteRect">
                    <alg type="sp" />
                    <presOf />
                </layoutNode>
                <layoutNode name="childText">
                    <alg type="tx" />
                    <presOf axis="des" ptType="node" />
                </layoutNode>
            </layoutNode>
            <forEach axis="followSib" ptType="sibTrans" cnt="1">
                <layoutNode name="space">
                    <alg type="sp" />
                    <presOf axis="self" />
                </layoutNode>
            </forEach>
        </forEach>
    </layoutNode>
</layoutDef>

XML のレイアウト ノードにはすべて、名前が割り当てられています。この例では名前の指定は任意ですが、定義の他の部分では、参照目的で名前の指定が要求される場合があります。また、名前を付けておくと、XML を構造化し、各セクションを識別するのに役立ちます。

レイアウト ツリーとデータ モデル マッピング

XML サンプルには、レイアウト ノードとアルゴリズム タグに加えて、forEach タグと presOf タグが含まれています。これらは、レイアウト ノードをユーザー データ モデルにどのようにマッピングするかを示しています。

SmartArt グラフィックの基礎となるデータ モデルは、ノードとリレーションシップのコレクションです。SmartArt グラフィックのリレーションシップには、親子遷移と兄弟遷移の 2 種類があります。親子遷移では、ノード間に階層関係が作成されます。兄弟遷移は、同じ親に対して親子リレーションシップを持つ隣接したノード間のリレーションシップです。図 6 はこれらのリレーションシップを説明しています。

Figure 6 SmartArt グラフィックのリレーションシップ (Close Figure 6)

サンプルの箇条書き

o Parent One
    o Child One
    o Child Two

o Parent Two
    o Child One
対応するデータ モデル
o Root Element
    o Parent/child transition: Root to "Parent One"
    o Node: "Parent One"
        o Parent/child transition: "Parent One"
                  to "Child One"
        o Node: "Child One"
        o Sibling transition: "Child One"
                  to "Child Two"
        o Parent/child transition: "Parent One"
                  to "Child Two"
        o Node: "Child Two"
        o Sibling transition: "Child Two"
                  to "Child One"
    o Sibling transition: "Parent One" to "Parent Two"
    o Parent/child transition: Root to "Parent Two"
    o Node: "Parent Two"
        o Parent/child transition: "Parent Two" to "Child One"
        o Node: "Child One"
    o Sibling transition: "Parent Two" to "Parent One"

レイアウト定義 XML 内の forEach 要素は、データ モデル内を移動してノードのセットを選択し、presOf (presentation of: 表示方法) 要素は、レイアウト ノードをデータ モデル内の特定のアイテムにマッピングします。これらの属性要素については、図 7 で説明しています。

Figure 7 forEach 属性と presOf 属性 (Close Figure 7)

属性 | 説明
axis | データ モデル内の移動方法を決定し、移動に応じてコンテキスト ノードを設定します。コンテキスト ノードの開始点は常に、データ モデルのルート要素です。SmartArt グラフィック レイアウトで一般的に使用される axis 値には、self (現在のコンテキスト ノードを返す)、child (現在のコンテキスト ノードの直下の子を返す)、des (現在のコンテキスト ノードの子孫をすべて返す)、desOrSelf (現在のコンテキスト ノードとそのノードのすべての子孫を返す)、および followingSib (ノードであるか遷移であるかに関係なく、データ モデル内の次のアイテムを返す) があります。
ptType | 選択するデータ モデル アイテムの種類を決定し、axis 値から返されたアイテムのセットに対してフィルタの役割を果たします。ptType の有効値は、node (テキスト アイテム: SmartArt グラフィック テキスト領域に箇条書きとして表示される)、parChTrans (parent-child transitions: 親子遷移)、および sibTrans (sibling transitions: 兄弟遷移) です。
st | "start" の略です。返されたセット内のどのアイテムを最初に選択するかを示します。この属性の既定値は 1 なので、多くの場合は省略されます。
cnt | "count" の略です。返されたセット内のアイテムをいくつ選択するかを示します。この属性の既定値は、0 (すべてのアイテムを選択する) です。
hideLastTrans | このフラグは、返されたセット内の最後の兄弟遷移をセットから除去するかどうかを決定します。この属性の既定値は true ですが、循環ダイアグラムなど、一部のケースでは、false に設定した方がよい場合もあります。

サンプル グラフィック レイアウトの XML には、次に示すように、2 つの forEach 要素が含まれています。

<forEach axis="ch" ptType="node">
<forEach axis="followSib" ptType="sibTrans" cnt="1">

1 つ目は、ルート要素の直接の子であるすべてのノードを選択しています。基本的に、箇条書きリストに含まれる各レベル 1 テキストが選択されます。次に、選択した個々のデータ モデル アイテムについて、forEach 要素内にネストしてレイアウト ノードが作成されます。1 つのアイテムが処理されると、コンテキスト アイテムは、選択されているセット内の次のノードに再設定されます。

2 つ目の forEach 要素は、現在のコンテキスト アイテムに続く兄弟遷移アイテムを選択しています。この forEach 要素は 1 つ前の forEach 要素内にネストされているため、基本的に箇条書きリスト内の各アイテム間の兄弟遷移が選択されます。

サンプルの XML では、各 layoutNode に 1 つの presOf ステートメントがあります。次に例を示します。

<presOf />
<presOf axis="self" />
<presOf axis="des" ptType="node" />

空の presOf 要素 (<presOf />) は、その layoutNode がデータ モデル内のどのアイテムにもマッピングされていないことを示します。このようなタグは、テキストのない図形に使用されます。また、アルゴリズムが実際の図形を並べるために使用する、図形のないレイアウト ノードにも使用されます。

axis="self" が設定されている presOf は、生成されたレイアウト ノードにコンテキスト アイテムを関連付けています。サンプルの XML で、この presOf は、親テキストを親テキスト領域図形に配置しています。

axis="des" と ptType="node" が設定されている presOf は、生成されたレイアウト ノードにコンテキスト アイテムの子孫をすべて関連付けています。サンプルで、この presOf は、特定の親に対応するすべての子テキストを同じテキスト領域図形に配置しています。

図形のプロパティ

グラフィックの構造を設定し、レイアウト ノードをデータ モデルにマッピングしたので、作成する図形に関する情報を追加できます。SmartArt グラフィック レイアウトには、任意の Office 標準図形を使用できます。

すべてのレイアウト ノードが図形を表示する必要はありません。レイアウト ノードの中には、グラフィックを構造化したり、最終的なグラフィック内のスペースをレイアウト時に確保するという目的で使用されるものもあります。サンプルのグラフィックで、線形フローと複合アルゴリズムに使用されているレイアウト ノードや、複合図形間のスペースに使用されているノードは、図形を表示しません。図 8 の XML に示されているように、これらのノードはそれぞれ、空の図形要素に割り当てられます。

Figure 8 空の図形要素の割り当て (Close Figure 8)
<layoutNode name="diagram">
    <alg type="lin" />
    <presOf />
    <shape />
    <forEach axis="ch" ptType="node">
        ...
    </forEach>
</layoutNode>

<layoutNode name="composite">
    <alg type="composite" />
    <presOf />
    <shape />
    ...
</layoutNode>

<layoutNode name="space">
    <alg type="sp" />
    <presOf axis="self" />
    <shape />
</layoutNode>

空の図形要素は必須ではありませんが、通常は既定値です (実際の既定値は、レイアウト ノードに関連付けられているアルゴリズムによって決定されます)。ただし、空のタグを明示的に含めると、そのレイアウト ノードに図形を割り当てないことを示すことができます。

これで、実際のグラフィックに使用する図形の追加を開始することができます。説明したとおり、それぞれの複合図形は、目に見える 2 つの四角形 (青い角丸四角形と白い四角形) でできています。図形は、shape タグ内の type として、図形 ID を割り当てることで識別されます。角丸四角形の場合、この値は roundRect です。

角丸四角形は、Office 図形の 1 つであり、外観を調整するためのハンドルが付いています。このグラフィックでは、既定より丸くない角の図形にしたいと思います。そこで、XML を使用して、図形の種類とポイントを調整します。

最後に、四角形に適切な外観 (たとえば、PowerPoint で既定のテーマが使用された場合は青) が割り当てられるように、XML を使用して、レイアウト ノードにスタイル ラベルを割り当てます。データ モデルに関連付けられている図形の既定のスタイル ラベルは node1 です。ところが、このスタイルでは普通、別の色付き線が使用されており、白い四角形と揃いません。そのため、この例ではスタイル ラベル alignNode1 を使用することにしました。

<layoutNode name="roundRect" styleLbl="alignNode1" >
    <alg type="sp" />
    <presOf axis="self" />
    <shape type="roundRect">
        <adjLst>
            <adj idx="1" val="0.1" />
        </adjLst>
    </shape>
</layoutNode>

ところで、調整ハンドル用に使用すべき値はどのように知るのか、参照資料で値を調べることもなくどのように図形の種類を決めるのか、不思議に思われるかもしれません。これにはちょっとしたコツがあるのです。新しい Word や Excel のファイル形式のように、新しい PowerPoint ファイル形式も、XML に基づいています。ブランクのスライドに希望の図形を追加し、調整ハンドルを設定し、ファイルを保存します。その結果生成された XML を調べれば (あらかじめ、編集した PowerPoint ファイルに .zip 拡張子が付く名前を付け、ZIP パッケージを開くことで、XML データにアクセスします)、適切な図形名と調整ハンドル値を推定できます。ただし、PowerPoint の図形と SmartArt のグラフィック レイアウトでは、異なる測定単位が使用されているので、値の調整が必要になることを覚えておいてください。たとえば、PowerPoint の XML で、サンプル グラフィックに使用されている角丸四角形は次のようになります。

<a:prstGeom prst="roundRect">
    <a:avLst>
        <a:gd name="adj" fmla="val 10000" />
    </a:avLst>
</a:prstGeom>

白い四角形はもっと簡単です。この図形の場合、必要なのは図形の種類 (この場合は rect) のみです。スタイル ラベルについては、3D プロパティが設定されてもこのボックスを角丸四角形と区別でき、前面に配置された状態で維持されるようにする必要があります。スタイル ラベル fgAcc1 (foreground accent 1: 前面アクセント 1) で正しい外観が定義されています。

<layoutNode name="whiteRect" styleLbl="fgAcc1" >
    <alg type="sp" />
    <presOf />
    <shape type="rect" />
</layoutNode>

最後に、テキスト領域図形を 2 つグラフィックに含めます。これらは単純な四角形です。これらの図形はユーザーに見せたくないので、線と塗りつぶしの値が表示されないように、hideGeom (hide geometry : ジオメトリを隠す) 属性を使用します。テキストは、青い四角形の一部であるかのように見せたいので、これらの図形にも、角丸四角形に使用したのと同じスタイル ラベルを使用します。

<layoutNode name="parentText" styleLbl="alignNode1" >
    <alg type="tx" />
    <presOf axis="self" />
    <shape type="rect" hideGeom="true" />
</layoutNode>

<layoutNode name="childText" styleLbl="alignNode1" >
    <alg type="tx"/>
    <presOf axis="des" ptType="node" />
    <shape type="rect" hideGeom="true" />
</layoutNode>

制約、規則、テキスト プロパティ

レイアウト ノードに割り当てられたアルゴリズムは、後続のレイアウト ノードおよび対応する図形をキャンバス上でどのように配置するかを決定します。アルゴリズムでは、既定の図形サイズとフォールバック動作がサポートされていますが、ほとんどの場合は、レイアウト定義の全体を通して、図形のサイズ調整およびテキスト フォントの動作をコントロールしたいと思うことでしょう。

制約を使用すると、各図形に対する理想的な (または、開始点とする) サイズと、各図形のフォント サイズおよび余白値を指定できます。規則を使用すると、図形やテキストを追加するためにもっとスペースが必要になった場合、制約値の変更を許可する一定の範囲を指定できます。

たとえば、四角形の線形フローの場合、希望する図形のサイズが幅 2 インチ×高さ 1 インチで、テキスト フォント サイズが 65 ポイントであるとします。ところが、それぞれに 1 段落のテキストを持つ 10 個の図形を標準ページに追加すると、ページ内に収めることができません。比率を維持した形ですべてを縮小することもできますが、希望どおりの外観にはならない場合もあります。これに代わる方法の 1 つとして、図形の高さが最大 5 インチまで、フォント サイズが最小 10 ポイントまで調整されることを許可する規則を追加できます。このような制約と規則には、次の XML を使用します。

<constrLst>
    <constr type="w" val="50" />
    <constr type="h" val="25" />
    <constr type="primFontSz" val="65" />
</constrLst>
<ruleLst>
    <rule type="h" val="125" />
    <rule type="primFontSz" val="10" />
</ruleLst>

規則と制約を作成する際に、値は mm 単位、フォント サイズはポイント単位で指定します。規則は順番に適用されるので、上の XML では、図形がまず 125 mm まで拡大され、フォント サイズが縮小されます。規則の値は絶対的ではなく、テキストが 14 ポイントで収まれば、フォント サイズはそれ以上縮小されません。

制約と規則には、参照値を指定することもできます。たとえば、四角形の高さを幅の半分から開始して、幅の 2 倍になるまで調整を許可する場合、XML は次のようになります。

<constrLst>
    <constr type="w" val="50" />
    <constr type="h" refType="w" fact="0.5" />
    <constr type="primFontSz" val="65" />
</constrLst>
<ruleLst>
    <rule type="h" fact="2" />
    <rule type="primFontSz" val="10" />
</ruleLst>

制約と規則を追加したら、レイアウト定義の XML ファイルが完成します。では、サンプル レイアウトについて、制約と規則を見てみましょう。

ダイアグラム レイアウト ノードの制約 : まず、各種の制約と規則をどこに配置するかを決める必要があります。制約と規則は、親レイアウト ノードに指定することも、レイアウト ノードそのものに指定することもできます。配置する場所はいくつかの要素によって異なります。

  • 利便性 : すべてのレイアウト ノードに関するフォント サイズ情報を 1 か所に保存しておけば、後で変更する場合に便利です。
  • 参照 : 制約や規則で別の制約や規則を参照している場合、一般的には両方を同じ場所に定義する必要があります。
  • アルゴリズム : アルゴリズムによっては、制約を特定の場所に配置することが要求される場合があります。たとえば、複合アルゴリズムでは、子レイアウト ノードのサイズと位置を複合レイアウト ノードのレベルで指定する必要があります。

サンプル レイアウトで、複合レイアウト ノードの幅と高さはレイアウト領域と同じ大きさにする必要があります。ルート レイアウト ノードは常に、既定でキャンバス寸法を継承するので、リストの最初の制約では、複合ノードの幅と高さがルート レイアウト領域の幅と高さと等しくなるように設定します。

次に、複合ノード間のスペースは、複合モードが追加された場合にグラフィックがスペースで占領されないよう、グラフィックと共に縮小する必要があります。これを実現するには、スペースの幅を複合ノードの幅の 10% に設定します。

最後に、すべてのレイアウト ノードに対するフォント サイズを 65 ポイントに設定し、グラフィック全体で同じサイズになるように、等値制約を指定します。図 9 のコードは、これらの新しい要素を加えて完成した XML を示します。

Figure 9 ダイアグラム レイアウト ノードの制約 (Close Figure 9)
<layoutNode name="diagram">
    <alg type="lin" />
    <presOf />
    <shape />
    <constrLst>
        <constr type="w" for="ch" forName="composite"
                refType="w" fact="1" />
        <constr type="h" for="ch" forName="composite"
                refType="h" fact="1" />
        <constr type="w" for="ch" forName="space"
                refType="w" refFor="ch" refForName="composite"
                fact=".1" />
        <constr op="equ" type="primFontSz" for="des"
                ptType="node" val="65" />
    </constrLst>
    <ruleLst />
    <forEach axis="ch" ptType="node">
        ...
    </forEach>
</layoutNode>

複合レイアウト ノードの制約 : 複合アルゴリズムは、図形のサイズと位置を実際には制御しないという点で、SmartArt の他のアルゴリズムと一線を画しています。複合アルゴリズムは、制約として指定されたサイズと位置の値を使用して、図形を配置します。このため、複合アルゴリズムで配置するレイアウト ノードはどれも、複合レイアウト ノードの制約ブロックで制約が定義されていなければなりません。

図形のサイズと位置を指定する際は、水平軸と垂直軸の両方について、値を決定する必要があります。これら両方の開始位置は、レイアウト領域の左上角であり、値は 0 です。これらの制約値は、レイアウト ノードの幅と高さに対する率で表現することがよくあります。

水平軸では、全体的な幅と共に、左、中央、および右の位置が複合アルゴリズムで判別されます。同様に、垂直軸では、全体的な高さと共に、上、中央、下の位置が複合アルゴリズムで判別されます。いずれの軸についても、任意の 2 つの値を指定する必要があります。残りの値は算定されます。

複合制約を指定する際にもう 1 つ注意が必要なのは、レイアウト ノードのネスト構造と同じ順序で制約を指定する必要があるという点です。既定では図形の重なり順もレイアウト ノードの順序によって決定されますが、正しい外観にするために必要であれば、重なり順が優先されるように指定することもできます。サンプル グラフィック レイアウトの制約については、図 10 を参照してください。

Figure 10 複合レイアウト ノードの制約 (Close Figure 10)
<layoutNode name="composite">
    <alg type="composite" />
    <presOf />
    <shape />
    <constrLst>
        <!--constraints for roundRect -->
        <constr type="w" for="ch" forName="roundRect"
                refType="w" fact="1" />
        <constr type="h" for="ch" forName="roundRect"
                refType="h" fact="1" />
        <constr type="l" for="ch" forName="roundRect" val="0" />
        <constr type="t" for="ch" forName="roundRect" val="0" />

        <!--constraints for parentText -->
        <constr type="w" for="ch" forName="parentText"
                refType="w" fact="1" />
        <constr type="h" for="ch" forName="parentText"
                refType="h" fact=".175" />
        <constr type="l" for="ch" forName="parentText" val="0" />
        <constr type="t" for="ch" forName="parentText" val="0" />

        <!--constraints for whiteRect -->
        <constr type="w" for="ch" forName="whiteRect"
                refType="w" fact="1" />
        <constr type="h" for="ch" forName="whiteRect"
                refType="h" fact=".025" />
        <constr type="l" for="ch" forName="whiteRect" val="0" />
        <constr type="t" for="ch" forName="whiteRect"
                refType="h" fact=".175" />

        <!--constraints for childText -->
        <constr type="w" for="ch" forName="childText"
                refType="w" fact="1" />
        <constr type="h" for="ch" forName="childText"
                refType="h" fact=".8" />
        <constr type="l" for="ch" forName="childText" val="0" />
        <constr type="t" for="ch" forName="childText"
                refType="h" fact=".2" />
    </constrLst>
    <ruleLst />
    <layoutNode name="roundRect" styleLbl="node1" >
        ...
    </layoutNode>
    <layoutNode name="parentText" styleLbl="node1" >
        ...
    </layoutNode>
    <layoutNode name="whiteRect" styleLbl="fgAcc1" >
        ...
    </layoutNode>
    <layoutNode name="childText" styleLbl="node1" >
        ...
    </layoutNode>
</layoutNode>

roundRect および whiteRect レイアウト ノードの制約: roundRect 図形と whiteRect 図形にはテキストが含まれておらず、特定のサイズ調整動作もありません。これらの幅、高さ、および位置は、複合ノードの制約ブロックで指定されるので、制約や規則を追加する必要はありません。ここでは、完全なコードを示すために制約および規則のリスト要素タグを空にしましたが、これらの指定は任意です (図 11 を参照)。

parentText および childText レイアウト ノードの制約: parentText 図形の高さ、幅、および位置は、複合ノードの制約で指定されるので、このレイアウト ノードに追加する制約と規則は、テキスト プロパティに関するもののみです。SmartArt 図形のほとんどのテキストは、フォント サイズがプライマリ フォント サイズ (primFontSz) 制約で制御されます。サンプルでのこの制約は、65 ポイントから開始するようにルート レイアウト ノードで指定されています。

たいていのグラフィックで、65 ポイントは大きすぎます。このため、フォント サイズを最小 5 ポイントまで縮小することを許可するための規則を次のように追加できます。

<layoutNode name="parentText" styleLbl="node1" >
    <alg type="tx" />
    <presOf axis="self" />
    <shape type="rect" hideGeom="true" />
    <constrLst />
    <ruleLst>
        <rule type="primFontSz" val="5" />
    </ruleLst>
</layoutNode>

テキストが 5 ポイントでも収まらないときは、図形からはみ出して表示されます。

childText レイアウト ノードにも、フォント サイズ調整に関する制約と規則があります。これに加えて、テキストが正しく揃うように、アルゴリズム要素内にネストされた要素としてテキスト パラメータを設定する必要があります (図 12 を参照)。

Figure 12 アルゴリズム要素内のテキスト パラメータ (Close Figure 12)
<layoutNode name="childText" styleLbl="node1" >
    <alg type="tx">
        <param type="stBulletLvl" val="1" />
        <param type="parTxLTRAlign" val="l" />
        <param type="parTxRTLAlign" val="r" />
        <param type="txAnchorVert" val="t" />
    </alg>
    <presOf axis="des" ptType="node" />
    <shape type="rect" hideGeom="true" />
    <constrLst>
        <constr type="secFontSz" refType="primFontSz" />
    </constrLst>
    <ruleLst>
        <rule type="primFontSz" val="5" />
    </ruleLst>
</layoutNode>

このレイアウト ノードでは、単一のテキスト アイテムではなく箇条書きのテキストが表示されます。ここで表示される第 1 レベルのテキストから箇条書きを開始したいのですが、既定の動作では、図形内の第 2 レベルから箇条書きが始まります。この動作は、stBulletLvl (start bullet level: 箇条書きの開始レベル) パラメータで制御されます。サンプルのグラフィックでは、この値を既定値の 2 ではなく 1 に設定する必要があります。このパラメータでサポートされている値はこの 2 つだけです。

このテキストは箇条書きなので、左揃えかつ上揃えにする必要があります。図形内の第 1 レベルのテキストについては、既定ではテキストが垂直方向にも水平方向にも中央揃えされるので、この動作を変更する必要があります。これを実現するには、パラメータ parTxLTRAlign (parent text LTR align: 親テキスト左から右揃え) を l (left: 左)、txAnchorVert (text anchor vertical: 垂直方向のテキスト アンカー) を t (top: 上) に設定します。

当然ながら、テキストが右から左に表示される言語設定と共にこのグラフィックを使用する場合は、テキストを右に揃える必要があります。システム設定に従って使用する値を決定するための親テキスト配置パラメータは、2 種類あります。このグラフィックの場合、parTxRTLAlign (parent text right-to-left align: 親テキスト右から左揃え) を指定し、値を r (right: 右) に設定する必要があります。

すべての図形で、セカンダリ フォント サイズ (secFontSz) もサポートされています。この値は、図形内のどこから箇条書きを開始するかを示します。行頭文字のある行では、セカンダリ フォント サイズが使用されます。行頭文字のない行では、プライマリ フォント サイズが使用されます。既定では、セカンダリ フォント サイズはプライマリ フォント サイズの 78% です。

各図形には余白値もあり、既定ではプライマリ フォント サイズに比例します。セカンダリ テキストしかない図形であれば、余白にセカンダリ フォント サイズを参照させる方が理にかなっています。サンプル グラフィックの場合、セカンダリ テキストをプライマリ テキストより小さくする必要はないので、両者の値を等しく設定します。これにより、適切な値が余白に使用されます。

スペース レイアウト ノードの制約: スペース アルゴリズムは、サイズ調整が不要であることを示すためのプレースホルダとして、あるいは別のレイアウト ノードどうしの間に最小限のスペースを確保するための手段として使用されます。このレイアウト ノードでは、複合図形どうしの間に水平スペースが必要なので、レイアウト ノードに幅制約を設定します。ただし、この値はダイアグラム レイアウト ノードで定義されているので、実際のスペース レイアウト ノードについては制約リストおよび規則リストが空になっています (図 11 を参照)。では次に、レイアウト ファイルのパッケージ化について考えてみましょう。

Figure 11 空に設定した制約および規則のリスト要素タグ (Close Figure 11)
<layoutNode name="roundRect" styleLbl="node1" >
    <alg type="sp" />
    <presOf axis="self" />
    <shape type="roundRect">
        <adjLst>
            <adj idx="1" val="0.1" />
        </adjLst>
    </shape>
    <constrLst />
    <ruleLst />
</layoutNode>

<layoutNode name="whiteRect" styleLbl="fgAcc1" >
    <alg type="sp" />
    <presOf />
    <shape type="rect" />
    <constrLst />
    <ruleLst />
</layoutNode>

レイアウト定義ファイルのパッケージ化

ここで説明した XML は、包括的な Open Document パッケージの一部です。基本的には次のフォルダ/ファイル構造で、.glox 拡張子の付く ZIP ファイルに圧縮されています。

  • _rels
  • .rels
  • diagrams
  • layout1.xml
  • layout1header.xml
  • [Content_Types].xml

これらのファイルとフォルダについて、詳しく見てみましょう。

rels フォルダとファイル: .rels ファイルは、.glox ファイル形式に含まれる各パーツ間のリレーションシップを定義します。.rels ファイルはテキスト ファイルであり、次に示す XML が記述されています。

<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns=
        "http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship
        Type="http://schemas.openxmlformats.org/officeDocument/2006/
             relationships/diagramLayoutHeader"
        Target="/diagrams/layoutHeader1.xml" Id="rId1" />
    <Relationship
        Type="http://schemas.openxmlformats.org/officeDocument/2006/
             relationships/diagramLayout"
        Target="/diagrams/layout1.xml" Id="rId2" />
</Relationships>

diagrams フォルダとレイアウト定義パーツ: diagrams フォルダには、SmartArt グラフィック レイアウトを構成する XML レイアウト定義ファイルが 2 つ格納されています。Layout1.xml は、グラフィックのレイアウトとマッピングを記録するメインの定義ファイルであり、この記事で説明した XML が記述されています。

Layout1header.xml には、一意の ID、タイトル、説明などを含めた、レイアウト定義のヘッダー情報が格納されています。

<?xml version="1.0" encoding="utf-8"?>
<layoutDefHdr uniqueId="msdn/sampleGraphicLayout"
      xmlns="http://schemas.openxmlformats.org/drawingml/2006/diagram">
    <title val="MSDN Sample Graphic Layout" />
    <desc val=" " />
    <catLst>
        <cat type="list" pri="500" />
    </catLst>
</layoutDefHdr>

各グラフィック レイアウトには uniqueID が必要です。別のレイアウト定義に同じ uniqueID が設定されていると、ファイルが読み込まれません。

タイトルと説明はユーザー インターフェイスへの表示に使用されます。この領域を使用して、そのグラフィック レイアウトをいつどのように使用するのかという具体的な情報を提供することができます。

カテゴリと優先度の情報は、そのグラフィック レイアウトが SmartArt レイアウト ギャラリーのどこに配置されるかを決定します。SmartArt グラフィックでサポートされているカテゴリの種類は、リスト、手順、循環、集合関係、ピラミッド、マトリックスなどがあります。組み込みのレイアウト ファイルの優先度は、各カテゴリ内で 1000 から始まり、1000 ずつ増分されています。優先度には、任意の正の整数を指定できます。

[Content_Types].xml ファイル: コンテンツ タイプ ファイルは、パッケージ全体の構造と名前空間領域を設定します。このファイルには次の内容の XML を記述し、ルート レベルに配置しておく必要があります。

<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/
              content-types">
    <Default Extension="xml" ContentType="application/
        vnd.openxmlformats-officedocument.drawingml.
        diagramLayoutHeader+xml" />
    <Default Extension="rels" ContentType="application/
        vnd.openxmlformats-package.relationships+xml" />
    <Override PartName="/diagrams/layout1.xml"
        ContentType="application/vnd.openxmlformats-
            officedocument.drawingml.diagramLayout+xml" />
</Types>

グラフィック レイアウトをテストする

レイアウト定義をパッケージ化した .glox ファイルは、SmartArt レイアウト ギャラリーに含められるように正しいディレクトリに配置する必要があります。既定では、SmartArt グラフィック レイアウト ファイルはローカル設定のテンプレートディレクトリ (%APPDATA%\Microsoft\templates\SmartArt Graphics) に格納されます。

レジストリ キーを設定して、テンプレート ディレクトリを別の場所に変更することもできます。ただし、レジストリ キーは Word の normal.dotx ファイルの場所も含めてすべての Office テンプレートに適用されるので、注意が必要です。このレジストリ キーは HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Common\General にあり、文字列は UserTemplates です。値には、全般テンプレート フォルダの新しいパスを指定します。これにより、SmartArt グラフィック機能は、指定されたパス内で SmartArt グラフィック フォルダを探すようになります。

グラフィック レイアウト ファイルをテンプレート ディレクトリに配置したら、SmartArt グラフィックをサポートするアプリケーションを起動し、[SmartArt グラフィックの挿入] ボタンをクリックします。直接的なエラーがなければ、作成したグラフィック レイアウトが、ギャラリー内の指定したカテゴリに表示されます。

いずれかの XML ファイルに関してなんらかのエラーを修正する必要が生じた場合、修正した XML ファイルがギャラリーに再度読み込まれるようにするには、Office アプリケーション セッションを再起動する必要があります。

エラーの種類とエラー ログ

SmartArt グラフィックには、XML 形式のログ ファイルが提供されます。このファイルには、レイアウト定義ファイルに関連したエラーおよび警告が記録されます。ログ ファイルは、グラフィック レイアウトのどの部分を修正する必要があるのかを特定するのに役立ちます。レイアウト ファイルに関するエラーは通常、uniqueID の値が一意でないことによるエラー、XML またはスキーマのエラー、レイアウト検証エラーのいずれかに該当します。

uniqueID が実際には一意でなかった場合、重複した uniqueID を持つレイアウト ファイルのいずれかが読み込まれず、SmartArt レイアウト ギャラリーを開くとエラー メッセージが表示されます。このエラーが発生すると、次のようなエントリがログ ファイルに追加されます。

<entry>
    <time>2006-10-20T15:51:14.650</time>
    <sev>err</sev>
    <host>POWERPNT.EXE</host>
    <file>example.glox</file>
    <type>nonUniqueId</type>
    <desc>A user-defined or built-in definition with this
          uniqueId was already loaded.</desc>
    <context>example</context>
</entry>

XML の書式設定またはスキーマに関する検証エラーが生じた場合も、レイアウト定義が正しく読み込まれません。layoutHeader1.xml ファイルにエラーがあると、このファイルのどの部分も読み込まれません。layout1.xml ファイルにエラーがある場合は、ファイルが読み込まれますが、ギャラリーに赤い X が表示されます。このレイアウトをギャラリーから選択しようとすると、エラー メッセージが表示されます。次のログ エントリは、典型的な XML 書式設定エラーを示しています。

<entry>
    <time>2006-10-20T15:53:39.619</time>
    <sev>err</sev>
    <host>POWERPNT.EXE</host>
    <file>example.glox</file>
    <type>xmlError</type>
    <desc>No error detail available</desc>
    <context></context>
    <line>58</line>
    <col>35</col>
</entry>

このエラーの最も有益な部分は、エラーが発生した行とカラムを示しているところです。

レイアウト検証エラーの動作は XML エラーに似ていますが、発生するのは SmartArt グラフィックがレイアウトの実行を試行した場合のみです。次のログ エントリは、2 つのレイアウト ノードに同じ名前属性が設定されている場合に発生するエラーを示しています。

<entry>
    <time>2006-10-20T15:59:25.650</time>
    <sev>err</sev>
    <host>POWERPNT.EXE</host>
    <file>example.glox</file>
    <type>invalidName</type>
    <desc>The name attribute must be unique.</desc>
    <context>&lt;layoutNode name='sibTrans'/&gt;</context>
    <line>131</line>
    <col>17</col>
</entry>

SmartArt グラフィックでは、ログ ファイルを制御するために、LogFileSeverityLevel、LogFileMaxSize、および LogFileNumEntriesToRemove の 3 つがレジストリ キーとして参照されます。これらは、HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Common\SmartArt Graphics に格納されています。

LogFileSeverityLevel は、報告するエラーまたは警告の重要度レベルを設定する DWORD です。0 の場合はエラーのみが報告され、レベル 1 ～ 4 の場合は、1 を最も深刻なレベルとして各レベルの警告が報告されます。LogFileMaxSize は、ログ ファイルに格納できる最大バイト数を設定する DWORD です。この値に達すると、領域を確保するために、ログから 1 セットのエントリが削除されます。LogFileNumEntriesToRemove は、最大ログ ファイル サイズを超えた場合に削除するエントリ数を指定する DWORD です。

有効ファイルの設計エラー

グラフィック レイアウト ファイルが正しく読み込まれた後も、設計エラーがないかどうかを検証する必要があります。典型的なエラーを次に挙げます。

  • 意図していない図形にテキストが表示される。
  • テキストがまったく表示されない。
  • 図形サイズやフォント サイズが図形内で一貫していない。
  • 図形が正しく配置されない。
  • 図形を追加した際に、期待どおりにグラフィックが動作しない。

これらの問題を修正するには、実践による学習が必要です。定義ファイル作成の経験を重ねると、エラーの識別も修正も容易になります。

グラフィック レイアウトをテストする際には、ほとんどのシナリオで美しい外観になるように、多様な SmartArt グラフィックのスタイルおよび色について、さまざまなテーマにわたり、どのように表示されているか検証するようにします。ただし、すべてのグラフィックで、あらゆるスタイルや色について美しい外観になるとは限りません。

既存のグラフィック レイアウト定義を変更する

グラフィック レイアウトを白紙状態から作成することは、SmartArt グラフィック レイアウト ファイルの構造およ構文を理解するのには良い方法です。それでも、希望の外観を得るために、既存のグラフィック レイアウトを少しだけ変更したい場合もあるはずです。

SmartArt グラフィックをドキュメントに追加して保存すると、ドキュメントの他のパーツと一緒に、グラフィック レイアウト ファイルが保存されます。パッケージ内の diagrams ディレクトリを調べると、ドキュメントに含まれる個々の SmartArt グラフィック レイアウトに対応して、レイアウトとレイアウト ヘッダーの XML ファイルが見つかるはずです。このファイルをパッケージから取り出し、名前を layout1.xml に変更し、適切なセクションに変更を加え、このファイルを新しい .glox に格納します。

既存のファイルを変更すると、この記事で説明していないタグや属性があることや、指定されていないタグや属性もあることがわかるはずです。ここでは、指定が任意であったり、既定値の使用を想定した属性については説明していません。既存ファイルに指定されていない属性については、既定値が使用されています。

次のステップ

SmartArt グラフィック レイアウトの拡張性について、基本事項を説明してきましたが、SmartArt レイアウト ファイルを作成するうえでの複雑さには、ほとんど触れていません。もっと詳しく知りたい場合は、スキーマから開始し、既存のグラフィック レイアウト定義を調べてどんな可能性があるか考えてみることをお勧めします。

Janet Schorrは、Microsoft で 6 年間プログラム マネージャを務め、現在は Office Publisher チームのリード プログラム マネージャです。さまざまな製品チームでの経験が豊富で、最近では Office Graphics チームで SmartArt レイアウト設計を担当しました。


Office アドイン: VSTO を使用して PowerPoint および Visio 用のアドインを作成する

Office アドイン

VSTO を使用して PowerPoint および Visio 用のアドインを作成する

Paul Stubbs
この記事の内容 : :
  • Microsoft Office アプリケーションの機能拡張
  • PowerPoint オブジェクト モデル
  • Visio オブジェクト モデル
この記事は次のテクノロジを使用しています:
Microsoft Office
コンテンツ
PowerPoint 2007
PowerPoint 2007 オブジェクト モデル
プレゼンテーションを作成する
タイトル スライドを追加する
箇条書きのスライドを追加する
プレゼンテーションのテーマを設定する
リボンを作成する
Visio 2007
Visio 2007 オブジェクト モデル
新しい図面を作成する
図形を追加する
図形を接続する
背景ページを追加する
図面のテーマを設定する
コマンド バーを作成する
まとめ

Microsoft Visual Studio 2005 Tools for the Microsoft Office system (VSTO) は開発者用の強力なツールで、Microsoft® Office アプリケーションを拡張してカスタマイズするための機能を備えています。使用言語は、Visual Basic® および Visual C#® です。

新しいバージョンの VSTO の提供は 2007 Microsoft Office system に対応して開始されました。この新しいバージョンである Microsoft Visual Studio® 2005 Tools for the 2007 Microsoft Office system (別名は Visual Studio 2005 Tools for Office Second Edition または VSTO 2005 SE) を使用すると、Word、Excel®、Outlook®、PowerPoint®、Visio®、および InfoPath® 用の新しい機能を追加できます。このツールは、無償でダウンロードして、Visual Studio 2005 Professional Edition 以上および VSTO 2005 上にインストールできます。

VSTO 2005 SE を使用すると、リボン、カスタム作業ウィンドウ、Outlook フォーム領域などの 2007 Microsoft Office system の機能をアドインでカスタマイズできます。また、VSTO 2005 SE ランタイムを使用すると、既存の VSTO 2005 アプリケーションが 2007 Office system でサポートされます。このツールを使用すると、Office 2003 用のアドインも作成できます。

この新しいバージョンでは、PowerPoint および Visio が初めてサポートされました。この 2 つのアプリケーションのサポートについて説明しながら、優れた新しいアプリケーションを簡単に作成する方法を紹介します。

PowerPoint 2007

VSTO 2005 SE に対する要望で最も多かったものの 1 つが PowerPoint のサポートでした。VSTO アドインによって、プレゼンテーションの作成や更新などのさまざまなタスクを簡単に行うことができるようになりました。プレゼンテーション作成の一般的な例として、サーバー上に保存されているスライド ライブラリの既存のスライドを組み合わせて、新しいプレゼンテーションを作成する場合があります。他の一般的な例には、プログラムによって新しいスライドを作成する場合や現在のデータで既存のスライドを更新する場合があります。ここではスライドの作成や更新に関連する一般的なタスクのいくつかを紹介しますが、主に 2007 Office system についての説明であることに注意してください。

VSTO 2005 SE をインストールすると、Visual Studio では [新しいプロジェクト] ダイアログ ボックスの [Office] ノード下にフォルダが 2 つ追加されます (図 1 を参照)。1 つは Office 2003 アプリケーション用のアドインを作成するためのフォルダで、もう 1 つは 2007 Office system アプリケーション用のアドインを作成するためのフォルダです。では、[2007 アドイン] フォルダ内の [PowerPoint アドイン] テンプレートをクリックして、PowerPoint 用の新しいアドインを作成してみましょう。

図 1 VSTO テンプレート
図 1 VSTO テンプレート(画像を縮小するには、ここをクリックします)
図 1 VSTO テンプレート
図 1 VSTO テンプレート (画像を拡大するには、ここをクリックします)

PowerPoint プロジェクト テンプレートでは、ThisAddIn というクラスが作成されます。ThisAddIn_Startup と ThisAddIn_Shutdown という 2 つのイベントが追加されていることがわかります。ThisAddIn_Startup はこのアドインが読み込まれるときに呼び出されます (ここに初期化コードを追加することをお勧めします)。ThisAddIn_Shutdown はこのアドインがアンロードされるときに呼び出されます (ここにクリーンアップ コードを追加することをお勧めします)。

PowerPoint 2007 オブジェクト モデル

PowerPoint はアドイン作成に使用できる優れたオブジェクト モデルを備えています。使い始めるときに、オブジェクト モデルを完全に理解しておく必要はありません。

PowerPoint の最上位レベル オブジェクトは、他のすべての Office アプリケーションと同様に Application オブジェクトであり、これは PowerPoint アプリケーション全体を表します。VSTO プログラミング モデルでは、この Application オブジェクトに簡単にアクセスできます。「Me.Application」と入力すると、PowerPoint.Application 型のオブジェクトが返されます。共有アドインとは異なり、このオブジェクトを適切な Office アプリケーションの型にキャストする必要はありません。VSTO プロジェクト テンプレートによって、作成するアドインに適切な型が設定されるためです (たとえば、Word 用のアドインで Me.Application を呼び出すと、Microsoft.Office.Interop.Word.Application オブジェクトが返されます)。

この Application オブジェクトには Presentations コレクションが 1 つ含まれ、このコレクションには Presentation オブジェクトが 1 つ含まれます。実際の PowerPoint ドキュメントを表す Presentation オブジェクトには Slides コレクションが 1 つ含まれ、このコレクションには Slide オブジェクトが複数含まれます (プレゼンテーションのスライドごとに 1 つの Slide オブジェクトがあります)。また、各スライドには Shapes コレクションが 1 つ含まれ、このコレクションには Shape オブジェクトが複数含まれます。Shape オブジェクトは、スライドで表示するタイトル、レイアウト枠、図、グラフなどのアイテムです (図 2 を参照)。

図 2 PowerPoint スライドの Shape オブジェクト
図 2 PowerPoint スライドの Shape オブジェクト(画像を縮小するには、ここをクリックします)
図 2 PowerPoint スライドの Shape オブジェクト
図 2 PowerPoint スライドの Shape オブジェクト (画像を拡大するには、ここをクリックします)

プレゼンテーションを作成する

新しい Presentation オブジェクトを作成するには、Application オブジェクトへの参照を使用し、Presentations コレクションにこの参照を追加します。まず、新しく作成する Presentation オブジェクトへの参照を保持する変数を作成します。次に必要なことは、Presentations コレクションの Add メソッドの呼び出しです。

   Dim presentation As PowerPoint.Presentation = _
       Me.Application.Presentations.Add()

PowerPoint オブジェクトはどこから取得されるのか疑問に思われるでしょう。VSTO プロジェクト テンプレートは名前空間 Microsoft.Office.Interop.PowerPoint にインポート エイリアスを自動的に追加します。プロジェクトのプロパティ ページの [参照] タブで [インポートされた名前空間] セクションを参照すると、作成された他の参照とエイリアスが表示されています。

タイトル スライドを追加する

通常、プレゼンテーションの最初のスライドは、プレゼンテーションのタイトルおよびサブタイトルを表示するタイトル スライドになります。新しいスライドの作成は、新しいプレゼンテーションの作成に類似しています。プレゼンテーションのスライドのコレクションにスライドを追加するには、AddSlide メソッドを呼び出します。このメソッドでは、スライドのインデックス値 (プレゼンテーションにスライドを挿入する位置を指定) および customLayout オブジェクトの 2 つのパラメータを受け取ります。

customLayout オブジェクトは SlideMaster から作成できます。SlideMaster にはスライドで表示するオブジェクトのすべてが含まれています。以下のコードに示すように、PpSlideLayout 列挙からレイアウトの型を選択できます。

Dim customLayout As PowerPoint.CustomLayout = _
    presentation.SlideMaster.CustomLayouts.Item( _
        PowerPoint.PpSlideLayout.ppLayoutTitle)

これで AddSlide メソッドを使用して新しいスライドを作成できます。このスライドをプレゼンテーションの最初のスライドとして追加するには、スライドのインデックス値に 1 を使用し、SlideMaster から作成した customLayout オブジェクトを渡します。

Dim slide as PowerPoint.Slide = _
    presentation.Slides.AddSlide(1, customLayout)

これでタイトル スライドが作成できたので、タイトルとサブタイトルを 1 行のコードで設定できます。作成したオブジェクトを使用して、Shapes コレクションの Title プロパティを取得し、スライドのタイトル オブジェクトとなる Shape オブジェクトを返すことができます。Shape オブジェクトには TextFrame オブジェクトを返す TextFrame プロパティが含まれ、これによって TextRange オブジェクトが返されます。TextRange オブジェクトの Text プロパティを以下のように設定します。

slide.Shapes.Title.TextFrame.TextRange.Text = "VSTO 2005 SE Rocks"

サブタイトルを設定する手順も類似していますが、サブタイトルの Shape プロパティはありません。この場合、サブタイトルはスライドの 2 番目の Shape となるため、インデックス値で参照できます。

slide.Shapes(2).TextFrame.TextRange.Text = _
    "AN OFFICE DEVELOPMENT STORY"

箇条書きのスライドを追加する

次に、通常はタイトルが最上部にあり、アイテムの箇条書きがメイン コンテンツとなるスライドを追加します。これは最も一般的に使用されるスライドの種類です。これを行うには、前のセクションの手順に従って新しいスライドを作成しますが、ここでは ppLayoutText という別の customLayout 型を選択します。ここでは、タイトル スライドを最初のスライドとして表示するため、このタイトル スライドのインデックス値に 1 を使用します。既存の最後のスライドの後にスライドを順に挿入するには、既存のスライド数をカウントする必要があります。

Dim customlayout As PowerPoint.CustomLayout = _
    presentation.SlideMaster.CustomLayouts.Item( _
        PowerPoint.PpSlideLayout.ppLayoutText)
Dim slide as PowerPoint.Slide = _
    presentation.Slides.AddSlide( _
        presentation.Slides.Count + 1, customlayout)

プレゼンテーションのテーマを設定する

2007 Office system の新しい機能である Office テーマを使用すると、ドキュメントに定義済みスタイルを適用できます。テーマでは、プレゼンテーションのフォント、グラフィックス、色、効果などの外観を全体的に変更できます。テーマは Word、Excel、PowerPoint、および Outlook でサポートされています。これらは .thmx という拡張子のテーマ ファイルで定義されており、テーマがサポートされているアプリケーション間で共有されます。2007 Office system では、Office のインストール パスの Document Themes 12 ディレクトリに既定のテーマが付属しています。

テーマを設定するには、Presentation オブジェクトの ApplyTheme という新しいメソッドを呼び出します。このメソッドでは .thmx テーマ ファイルのパスをパラメータとして受け取ります。

presentation.ApplyTemplate( _
    "C:\Program Files\Microsoft Office\Document Themes 12\Civic.thmx")

リボンを作成する

Office の新しいユーザー インターフェイスで重要となるのがリボンです。従来の Office のメニューやツール バーがリボンに置き換えられました。リボンでは、コンテキスト タブに機能が整理されます。このタブでは、ボタン、ギャラリー、ドロップダウン リストなどのコントロールがグループ化されます。このプロジェクトでは、クリックすると新しいプレゼンテーションを作成できるボタンを 1 つ持つ簡単なリボンを作成します。VSTO 2005 SE ではリボンの機能拡張がサポートされているため、アプリケーションに新しいカスタム リボン タブやコントロールを簡単に追加できます。

まず、[プロジェクト] メニューの [新しい項目の追加] をクリックし、[Ribbon サポート] 項目をクリックして、該当のプロジェクトに追加します。これにより、プロジェクトにはリボン定義ファイルとリボンの分離コード ファイルの 2 つのファイルが作成されます。リボン定義ファイルはリボンを定義する XML ファイルです。リボンの分離コードではリボンの読み込みとイベントを処理します。図 3 にこの簡単なリボンの定義 XML ファイルを示します。このファイルでは、"My Tab" というリボン タブを追加します。このタブには、"マイ グループ" というグループが含まれます。このグループには "マイ ボタン" というボタンが 1 つ含まれます。このボタンには OnClick コールバック メソッドが onAction に指定されています。

Figure 3 簡単なリボンの定義 XML ファイル (Close Figure 3)
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"
     onLoad="OnLoad">
  <ribbon>
    <tabs>
      <tab id="MyTab" label="My Tab">
        <group id="MyGroup" label="マイ グループ">
          <button id="Button1" size="large" label="マイ ボタン"
                  screentip="My Button Screentip" onAction="OnClick"
                  imageMso="HappyFace" />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

自動作成されるリボン定義では、リボンの onLoad コールバックとボタンの onAction コールバックが定義されます。これらのコールバックを処理するには、Ribbon1 クラスで生成されるメソッドを編集する必要があります。Ribbon1 クラスは [Ribbon サポート] 項目をクリックして追加すると作成されます。Ribbon1 クラスは、要求に応じて Office に渡される IRibbonExtensibility インターフェイスを実装します。

Office のリボンはサービス要求のモデルを使用します。つまり、Office が RequestService メソッドを呼び出し、IRibbonExtensibility インターフェイスを実装するクラスがあるかどうかを確認します。Ribbon1 クラスのインスタンスが返されると、Office ではこのインスタンスをコールバックに使用します。

RequestService メソッドを処理するコードは、[Ribbon サポート] 項目をクリックして追加したときに既に記述されています。そのため、ここでは Ribbon1 クラスのファイルの上部にあるパーシャル クラス コードのコメントを解除します。次に、コールバックを処理するメソッドを記述します。リボンの全コールバックのシグネチャに関するドキュメントは MSDN® で参照できます。OnLoad コールバックでは、Office はリボンのインスタンスへの参照を渡します。この参照を保存すると、リボンを無効にするときに使用できます。

リボンを更新する唯一の方法は、リボンを無効にすることです。リボンを無効にすると、Office ではリボンの表示に必要なすべてのコールバックが再度実行されます。より細かい処理を行うために、単一のコントロールを無効にすることもできます。

Private ribbon As Office.IRibbonUI
Public Sub OnLoad(ByVal ribbonUI As Office.IRibbonUI)
    Me.ribbon = ribbonUI
End Sub

ボタンの OnClick コールバックでは、ThisAddIn クラスの参照を取得するために、Globals クラスを使用します。また、新しいプレゼンテーションを作成するには、CreateNewPresentation サブルーチンを呼び出し、すべての手順を実行します。

Public Sub OnClick(ByVal control As Office.IRibbonControl)
    Globals.ThisAddIn.CreateNewPresentation()
End Sub

F5 キーを押してソリューションを実行すると、このアドインが登録され、PowerPoint が起動します。"My Tab" という新しいリボン タブが右端に表示されています。

リボンを使用した開発の詳細については、今月号の MSDN Magazine で Eric Faller の記事を参照してください。

Visio 2007

Visio では Visual Basic for Applications のサポートや独自のソフトウェア開発キットの提供などにより、長年にわたってカスタム開発がサポートされてきました。VSTO 2005 SE では VSTO マネージ アドインを作成するための機能が拡張されています。

Visio ソリューションを使用するとデータを視覚化できます。たとえば、データベースのデータを使用してスケジュールのタイムラインを図にしたり、Active Directory からのライブ データを使用した組織図に各従業員の売上データを追加したりできます。ここでは、大きいアプリケーションの基盤となる Visio での基本タスクについて説明します。

他のすべての VSTO アドインと同様に、まず、新しい VSTO アドイン プロジェクトを作成します。Visio 2003 または Visio 2007 のいずれかを対象にできますが、ここでは Visio 2007 用のテンプレートを選択します。VSTO では PowerPoint 用のアドインを作成する場合と同様に、ThisAddIn_Startup と ThisAddIn_Shutdown というイベント ハンドラを含むアドインが作成されます。

Visio 2007 オブジェクト モデル

Visio オブジェクト モデルは他の Office アプリケーションとは大きく異なり、理解するのが難しい場合があります。幸いなことに、このプロジェクトのオブジェクト モデルについては完全に理解する必要はありません。基本的な概念を少し理解するだけで十分です。

Visio オブジェクト モデルのルート オブジェクトは、他のすべての Office アプリケーションと同様に Application オブジェクトであり、これには Document オブジェクトのコレクションが含まれます。Document オブジェクトは Visio 図面を表すオブジェクトであり、Page オブジェクトのコレクションが含まれます。Page オブジェクトにはそれぞれ Shape オブジェクトのコレクションが含まれます。これらの Shape オブジェクトが、Visio アプリケーションを作成する際に重要となります。

Shape オブジェクトはステンシルからページにドロップするオブジェクトです。ステンシルとは、図面に追加できる Visio 図形のコレクションです。ステンシルは .vss ファイルに含まれ、Visio 図面の左側に表示されます。

新しい図面を作成する

新しい図面を作成するには、Documents コレクションの Add メソッドを呼び出します。

Dim newDocument As Visio.Document = Application.Documents.Add("")

これで Visio.Document オブジェクトのインスタンスが返されます。

図面の作成時には、他のいくつかのプロパティも設定できます。たとえば、ページの向きや図面の幅と高さなどを設定できます。Visio 図面は非常に大きくなる場合があり、プロッタで印刷されたり、レター サイズで多くのページに印刷されたりするため、このような設定が一般的に行われます。

ページには PageSheet という特別な図形が含まれます。これはページ プロパティのコレクションです。Visio のプロパティはすべてこれを使用して設定されるため、この概念を理解しておくことが重要です。プロパティは Cell オブジェクトに格納されます。Cell オブジェクトへの参照を取得したら、Formula を設定できます。これは Visio によって評価され、最終値が取得されます。

Dim page As Visio.Shape = newDocument.Pages(1).PageSheet
' Set the layout.
page.Cells("PrintPageOrientation").Formula = _
    Visio.VisCellVals.visPPOPortrait
' Set the paper size.
page.Cells("PageWidth").Formula = "8.5 in"
page.Cells("PageHeight").Formula = "11.0 in"

図形を追加する

図面に図形を追加する前に、ステンシルを開く必要があります。ここでは、基本ステンシルを開きます。これを行うには、ステンシル ファイルの名前とフラグを渡します。たとえば、ファイルを読み取り専用で開き、アプリケーション ウィンドウに固定して表示するように指定できます。

Stencil = Application.Documents.OpenEx( "BASIC_M.VSS", _
    Visio.VisOpenSaveArgs.visOpenRO + _
    Visio.VisOpenSaveArgs.visOpenDocked)

ページに図形を追加するには、Page オブジェクトの Drop メソッドを呼び出し、ステンシルの Masters コレクションの Master オブジェクト、および図形を配置する X 座標値と Y 座標値を渡します。

Dim AddedShape As Visio.Shape = _
    Application.ActiveWindow.Page.Drop( _
        Stencil.Masters.ItemU("Square"), 2, 9)

次に、図形をカスタマイズできます。たとえば、図形に表示するテキストを追加します。図面のプロパティと同様に、図形のプロパティを設定するには、プロパティが含まれる Cell オブジェクトへの参照を取得します。ただし、Text プロパティはよく使用されるため、Shape オブジェクトに直接追加します。したがって、ここでは、テキストを設定するために Cell オブジェクトを使用する必要はありません。

AddedShape.Text = "Shape 1"

図形の追加は頻繁に行われるため、このコードを個別のメソッドに分離することを強くお勧めします。

Public Function AddShape(ByVal ShapeName As String, _
    ByVal X As Double, ByVal Y As Double) As Visio.Shape

    ' Add a new shape to the document from the stencil.
    Dim AddedShape As Visio.Shape = _
        Application.ActiveWindow.Page.Drop( _
            Stencil.Masters.ItemU(ShapeName), X, Y)

    Return AddedShape
End Function

これで複数の図形を簡単に追加できます。次に、正方形と三角形を追加して、各図形にテキストを設定するためのコード サンプルを示します。

' Add a square.
shape1 = AddShape("Square", 2, 9)
shape1.Text = "Shape 1"
' Add a Triangle.
shape2 = AddShape("Triangle", 2, 6)
shape2.Text = "Shape 2"

Visio 図面では、0,0 はページの左下を表します。そのため、このサンプルでは、正方形は左から 2 インチ、下から 9 インチの位置に配置されます。

図形を接続する

Visio では、図形を接続することによって、相互関係やデータ フローを視覚化できます。図形を接続する線も実際には図形であるため、図形を追加するコードと同じパターンを使用してコネクタを追加します。

Dim connector As Visio.Shape = AddShape("Dynamic Connector", 4, 4)
connector.Text = "Shape1 to Shape2"

三角形とその上部に位置する正方形を接続するには、まず、接続ポイントのプロパティへの参照を保持するために、2 つの Cell オブジェクトを定義します。図形の接続ポイントには ShapeConnectionPoint を使用し、コネクタの接続ポイントには ConnectorEndPoint を使用します。

Dim ShapeConnectionPoint As Visio.Cell
Dim ConnectorEndPoint As Visio.Cell

Shape2 は既に作成済みの三角形であり、一番上の頂点のプロパティは Connections.X4 で表されます。また、コネクタは動的コネクタ図形であり、EndX はコネクタの終点を表すプロパティ名です。

ShapeConnectionPoint = shape2.Cells("Connections.X4")
ConnectorEndPoint = connector.Cells("EndX")

次に、コネクタと図形を接続するには、Cell オブジェクトを接着します。

ConnectorEndPoint.GlueTo(ShapeConnectionPoint)

三角形には 3 つの接続ポイントしかないため、接続するのは簡単です。正方形にはそれぞれの角に接続ポイントがあり、全部で 4 つになります。これらのポイントは図形をページにドロップすると、小さい青色の X で表示されます。接着できるのは接続ポイントのみです。そのため、正方形の底辺などの異なる部分に接続する場合は、新しい接続ポイントを追加する必要があります。図 4 にこれを行うためのヘルパ メソッドを示します。

Figure 4 AddConnectionPoint ヘルパ メソッド (Close Figure 4)
Public Function AddConnectionPoint( _
        ByVal shape As Visio.Shape) As Visio.Cell

    Dim newConnectionPointIndex As Integer
    Dim newConnectionPoint As Visio.Row
    newConnectionPointIndex = shape.AddRow( _
        Visio.VisSectionIndices.visSectionConnectionPts, _
        Visio.VisRowIndices.visRowLast, _
        Visio.VisRowTags.visTagCnnctPt)
    'Get a reference to the new connection point.
    newConnectionPoint = shape.Section( _
        Visio.VisSectionIndices.visSectionConnectionPts) _
        .Row(newConnectionPointIndex)
    'Create the connection point in the middle on the bottom of the shape.
    With newConnectionPoint
        .Cell(Visio.tagVisCellIndices.visCnnctX).FormulaU = "Width*0.5"
        .Cell(Visio.tagVisCellIndices.visCnnctY).FormulaU = "Height*0"
        .Cell(Visio.tagVisCellIndices.visCnnctDirX).FormulaU = 0.0#
        .Cell(Visio.tagVisCellIndices.visCnnctDirY).FormulaU = 1.0#
        .Cell(Visio.tagVisCellIndices.visCnnctType).FormulaU = _
            Visio.tagVisCellVals.visCnnctTypeInward
    End With

    Return shape1.Cells("Connections.X" & newConnectionPointIndex + 1)
End Function

正方形と三角形の一番上の頂点を直線で接続するには (図 5 を参照)、次のように正方形の底辺の中点に新しい接続ポイントを追加します。

ShapeConnectionPoint = AddConnectionPoint(shape1)
ConnectorEndPoint = connector.Cells("BeginX")
ConnectorEndPoint.GlueTo(ShapeConnectionPoint)

図 5 Visio 2007 で図面を作成する
図 5 Visio 2007 で図面を作成する(画像を縮小するには、ここをクリックします)
図 5 Visio 2007 で図面を作成する
図 5 Visio 2007 で図面を作成する (画像を拡大するには、ここをクリックします)

AddConnectionPoint ヘルパ メソッドでは、新しい接続ポイントを追加し、Cell オブジェクトを返します。新しいプロパティとなる行を追加して新しい接続ポイントを追加すると、Visio では内部でプロパティをリストとして格納します。行を追加したら、数式の値を設定できます。ここまでは、ほとんどの数式は単純な値で構成されていました。ここでは、数式は底辺の中点を Width*0.5 で動的に計算します。最後に、メソッドは新しく作成された接続ポイントの Cell インスタンスを返します。

背景ページを追加する

Visio では、複数ページで表示するテキストや図形 (タイトルや凡例など) を背景ページに追加できます。背景ページとは、Background プロパティの値を 1、つまり True に設定した通常のページです。ここでは、BackPage プロパティを設定して、背景ページを別のページに関連付けます。

Dim backgroundPage As Visio.Page = _
    Application.ActiveDocument.Pages.Add()
backgroundPage.Name = "TheBackground"

backgroundPage.Background = 1 'True

Application.ActiveDocument.Pages(1).BackPage = backgroundPage.Name

背景ページにテキストを追加するには、長方形の Shape オブジェクトを作成し、フォントのインデックス値を渡して、図形のフォントのプロパティを設定します。次に、図形内に表示するテキストを設定するには、Characters オブジェクトのテキストを設定します。これを図 6 に示します。

Figure 6 背景ページにテキストを追加する (Close Figure 6)
' Create a textbox shape.
Dim textBox As Visio.Shape
textBox = Application.ActiveWindow.Page.DrawRectangle( _
    1.0, 10.0, 3.0, 10.5)
textBox.TextStyle = "Normal"
textBox.LineStyle = "Text Only"
textBox.FillStyle = "Text Only"
' Set the font.
fontIndex = Application.ActiveDocument.Fonts("Arial Black").ID
textBox.Cells("Char.Font").Formula = fontIndex
' Set the text.
Dim textBoxText As Visio.Characters
textBoxText = textBox.Characters
textBoxText.Begin = 0
textBoxText.End = 0
textBoxText.Text = "VSTO 2005 SE Rocks"

図面のテーマを設定する

Visio では、PowerPoint と同様に、定義済みテーマを設定できます。また、ほとんどの Office アプリケーションで同じテーマを設定できます。Visio での相違点の 1 つは、VisThemeColors 列挙を使用してテーマを指定できることです。この値を指定する方法は複数あります。たとえば、文字列や整数でこの値を指定できます。また、テーマの色と効果を個別に指定できることも Visio での相違点の 1 つです。これを行うには、ActivePage の ThemeColors プロパティと ThemeEffects プロパティをそれぞれ指定します。

Application.ActivePage.ThemeColors = _
    Visio.VisThemeColors.visThemeColorsCivic

Application.ActivePage.ThemeEffects = _
    Visio.VisThemeEffects.visThemeEffectsPillow

コマンド バーを作成する

当然ながら、図面を作成するサブルーチンを実行する方法が必要になります。Visio ではリボンがサポートされないため、このアドインではコマンド バーを使用します (このコードは、Visio 2003 や他の Office 2003 アプリケーションで使用する場合と同じです)。図 7 では、コマンド バーを作成して、スマイルのアイコン付きのボタン コントロールを追加するコードを示します。図 5 ではこのボタンを示しています。図 5 に示すように、最終的には、テーマが適用され、正方形が三角形に接続された図面になります。

Figure 7 コマンド バーを作成する (Close Figure 7)
Dim commandBars As Office.CommandBars
Dim commandBar As Office.CommandBar

commandBars = Application.CommandBars
commandBar = commandBars.Add("Demo Toolbar", _
    Office.MsoBarPosition.msoBarTop, , True)
' Set the context when the toolbar is visible.
commandBar.Context = Visio.VisUIObjSets.visUIObjSetDrawing & "*"

' Add a button with a smiley face icon.
DemoButton = _
    commandBar.Controls.Add(Office.MsoControlType.msoControlButton)
DemoButton.Tag = "Demo Button"
DemoButton.FaceId = 2950
DemoButton.TooltipText = "Run Demo"

まとめ

Visual Studio Professional Edition 以上で使用可能な無償のアドオンである VSTO 2005 SE を使用すると、最も一般的な 6 つの Office アプリケーション用のアドインを構築できます。また、2007 Office system に移行しても、既存の VSTO 2005 アプリケーションをそのまま使用できます。Office 2003 または 2007 Office system のどちらを使用しても、VSTO 2005 SE では優れた Office アドインを簡単に作成できます。

Paul Stubbs は Visual Studio Tools for Office および Visual Studio Tools for Applications 担当チームのプログラム マネージャです。『VSTO for Mere Mortals』(英語)(Addison-Wesley、2006 年) を Kathleen McGrath と共同で執筆しました。また、TechEd や TechReady では講演を行い、マイクロソフトのフォーラムでは開発者コミュニティに参加しています。彼のブログは、blogs.msdn.com/pstubbs (英語) で公開されています。


チームに追加されたデータベース開発機能: Visual Studio 2005 Team Edition for Database Professionals の概要

チームに追加されたデータベース開発機能

Visual Studio 2005 Team Edition for Database Professionals の概要

Brian A. Randell
この記事の内容 : :
  • データベースのソース管理と変更管理
  • 新規および既存データベースの操作
  • ストアド プロシージャ、関数、およびトリガの単体テスト
  • データベースの比較
この記事は次のテクノロジを使用しています:
Visual Studio 2005 Team System
コンテンツ
データベースのバージョンを管理する
はじめに
インポート プロセス
スキーマとオブジェクトを編集する
データ生成
単体テスト
データベースを比較する
ビルドと配置
まとめ

データベース開発は、Visual Studio® 2005 Team Edition for Database Professionals (以下、DB Pro エディション) のおかげで簡単になりました。

SQL Server™ に接続するソリューションを作成、管理、またはビルドする必要がある場合は、このツールが最適です。

DB Pro エディションには、データベースをより簡単に管理するための多数の機能が用意されています。Visual Studio 2005 Team Suite のメンバとして、DB Pro エディションは Team Foundation Server のソース コード管理機能と共に動作し、データベースの変更をソース コード管理と同様の方法で管理します。DB Pro エディションでは、データベースのサンドボックス開発がサポートされ、データベース比較、データベース生成、単体テスト、およびデータベース配置用の各ツールを利用できます。しかし、DB Pro エディションの最も魅力的な点は、データベース開発ライフ サイクル全体の一環として、そのすべての機能を併せて使用できることです。

記事に進む前に、いくつか確認しておくことがあります。第 1 に、DB Pro エディションの初期リリースでは、SQL Server 2000 と SQL Server 2005 の各リレーショナル データベースはサポートされていますが、Analysis Services はサポートされていません。その他のマイクロソフト データベースとマイクロソフト以外のデータベースは、今後のバージョンでプロバイダベース モデルによってサポートされる予定ですが、現時点では使用できません。第 2 に、Team Foundation Server と共に DB Pro エディションを使用すると、DB Pro エディションの多くの機能を強化できますが、Team Foundation Server は、DB Pro エディションの使用に関する前提条件ではありません。

データベースのバージョンを管理する

適切なデータベース駆動型アプリケーションをビルドするには、データベースのどのコピーが "実際の" バージョンなのかを知ることが重要です。ここで問題となるのは、ライブ システムに対する変更やテストは危険を伴うということです。実際に、このような作業の実行に対する権限を持っているのは、通常はデータベース管理者 (DBA) だけです。DB Pro エディションは 1 つのソリューションを提供しています。運用データベース システムは、データの実際のライブ バージョンを保持します。その一方で、DB Pro エディションは、プロジェクトベース システムを通じてスキーマの実際のバージョンを保持します。このスキーマは、完全にバージョン管理下に置かれます。DB Pro エディションは、各データベース オブジェクトを .sql ファイル セットとして管理し、それらのオブジェクト間の依存関係と関係を把握します。

このようにデータとスキーマに分離することは、変更管理の観点から見ると、非常に重要です。この結果、データベース支援のプロジェクトについて正しい反復開発プロセスを実行できるようになりました。

さらに DB Pro エディションは、Team System ファミリ メンバとして、Team Foundation Server、および Team System ファミリのその他の製品と統合されています。これにより、データベース スキーマの実際のチーム開発と管理が可能になりました。最も魅力的な機能は、おそらくバージョン管理でしょう。作業項目の追跡、コード チェックイン ポリシー、夜間ビルドの統合などの追加機能によって、データベースは、追跡および管理される通常の成果物となりました。データベースはもはや、手間のかかる独自の管理手順や開発手順を必要とする、特殊なエンティティではありません。

はじめに

DB Pro エディションは、根本的に Visual Studio 2005 内で使用するプロジェクト テンプレートとツールのセットです。一部のツールはプロジェクトを生成しなくても使用できますが、本当の力は DB Pro エディションのプロジェクトで発揮されます。

SQL Server は少なくとも 2 つのインストールが必要になります。1 つは運用システムとして、もう 1 つは開発システムとして機能します。データベースの複雑さとそのデータベース上のソフトウェアに応じて、インストールをさらに追加することもできます。チーム環境では、データベース開発者ごとにローカル プライベート インスタンスを用意する必要があります。さらに、統合とリグレッション テスト用に少なくとも 1 つの共有開発インスタンスが必要となります。(DB Pro エディションのライセンスの一部として、SQL Server 2005 Enterprise Edition の機能をすべて含む SQL Server 2005 Developer Edition のライセンスも付与されます。)

ローカル開発者マシンで作業を開始するには、SQL Server 2005 Developer Edition、Enterprise Edition、または Express Edition のローカル コピーと共に、DB Pro エディションをインストールする必要があります。DB Pro エディションは、SQL Server 2000 および SQL Server 2005 の両プロジェクトを設計時に検証する際に、SQL Server 2005 のインスタンスを使用します。検証で使用する SQL Server のインスタンスを制御するには、Visual Studio 2005 で [ツール]、[オプション]、[データベース ツール] の順にクリックし、[デザイン時検証データベース] オプションを選択します。Team Foundation Server を使用する場合は、Team Explorer もインストールしてください。

DB Pro エディションでは、データベースを最初から作成できますが、ユーザーはたいていの場合、管理する必要のある既存のデータベースを所有しています。データベースのスキーマをプロジェクトに読み込む方法は 2 つあります。1 つ目の方法では、SQL スクリプト ファイルをインポートします。この方法は、バージョン管理にスキーマのコピーを保存してスキーマの変更を管理している場合や、スキーマの現在のバージョンに直接接続できず、セキュリティやその他のビジネス要件のためライブでインポートする場合などに便利です。

スキーマを読み込む 2 つ目の方法では、実行中の SQL Server インスタンスに接続し、スキーマをライブでインポートします。この方法は、新しいプロジェクトを既に作成した場合や、新しいプロジェクト ウィザードを使用したプロジェクト作成の一貫として使用します。DBA がいる環境で作業する場合、DBA を開発プロセスに組み入れるには、2 つ目の方法が適しています。実際に、既存データベースに対して DB Pro エディションを使用するときに理想とされるのは、DBA がプロジェクトを作成し、ソース コード管理に読み込み、開発に渡すまでの間ずっと、その開発ライフ サイクル中に存在していることです。

スクリプトを通じたインポートとライブ データベースを通じたインポートには、1 つの重要な違いがあります。ライブ データベースからは 1 回しかインポートできませんが、スクリプトからは必要な回数のインポートを行うことができます。

インポート プロセス

インポート プロセスを開始するには、DB Pro エディションの [新しいプロジェクト] ダイアログを開きます。[データベース プロジェクト] という最上位のノードから (少しノードを展開した後で)、SQL Server のテンプレートを利用できます。空のプロジェクトを選択するか、ウィザードを使用できます。

ウィザードを実行すると、簡単に始められます。ウィザードの質問は、[プロジェクトのプロパティ]、[データベース オプションの設定]、[データベース スキーマのインポート]、および [ビルド/配置の構成] の各ページに分けられています。

[プロジェクトのプロパティ] ページでは、ディスク上のプロジェクトをオブジェクト タイプ別に編成するか、スキーマ別に編成するかを指定できます。いったんプロジェクトを作成し、スキーマをインポートしてしまうと、このオプションを変更することが難しいため、前もって考えておくことが重要です。スキーマベースの編成は、たとえばセキュリティ上の理由でデータベースの一部へのアクセス制限が必要な場合などに便利です。

[データベース オプションの設定] ページは、[ANSI NULL]、[引用符で囲まれた識別子] などのデータベース設定を定義するために使用します。[データベース スキーマのインポート] ページでは、データベース スキーマをライブ サーバーからインポートするかどうかを選択します。ここでインポートを選択した場合は、次に、データベースのオプションを使用するか、プロジェクトのオプションを使用するかを指定します。

最後に、[ビルド/配置の構成] ページで、ビルド スクリプトの配置、既定のターゲット データベースへの接続などに影響するオプションを調整します。ウィザードによって定義されるほとんどのオプションと同様に、これらのオプションは、[プロジェクトのプロパティ] ページで調整可能です。

ウィザードを終了して、運用データベース スキーマをインポートすると、オフラインのワークスペースが作成されます。これは、ユーザーがデータベース スキーマをオフラインで安全に使用できる場所です。

DB Pro エディションには 2 つのプロジェクト ビューがあります。1 つは、ソリューション エクスプローラで表示される文字どおりのファイル ビュー (図 1 を参照)、もう 1 つは、[スキーマ ビュー] ウィンドウで表示される、階層化されたスキーマ ビュー (オブジェクト タイプ別に編成、またはスキーマおよびオブジェクト タイプ別に編成) です。これらの 2 つのビューでは、プロジェクトの編成と管理を柔軟に行うことができます。ディスク上のファイル構造は、ソリューション エクスプローラのドラッグ アンド ドロップ機能を使用して全体を再編成できますが、オブジェクト タイプおよびスキーマ タイプ (またはそのいずれか) 別にオブジェクトに簡単にアクセスできる機能はそのまま維持されます。

図 1 ソリューション エクスプローラのデータベース
図 1 ソリューション エクスプローラのデータベース(画像を縮小するには、ここをクリックします)
図 1 ソリューション エクスプローラのデータベース
図 1 ソリューション エクスプローラのデータベース (画像を拡大するには、ここをクリックします)

DB Pro エディションは、ソリューション エクスプローラおよびファイル システムで、データベース スキーマを .sql ファイルとして表されるアトミック オブジェクトに細分化します (テーブル、ビュー、ストアド プロシージャなどの最上位オブジェクトだけでなく、インデックス、制約なども)。さらに、プロジェクトでは、スクリプトのセットが [配置前] および [配置後] フォルダに分割されます。ユーザーはこれらのスクリプトを使用して、スキーマ オブジェクトに相当しない T-SQL 操作を処理できます。また、管理およびユーティリティの独自のスクリプトを作成し、ニーズに合わせて編成できます。

Team Foundation Server を使用している場合は、この時点で Source Code Control にプロジェクトを追加し、作業項目をチェックオフします。マイクロソフトは、MSF と MSF CMMI の両方のテンプレート向けに、データベース開発固有の役割と作業の流れを追加するため、Team Foundation Server と共に提供するガイダンスを更新しています。

スキーマとオブジェクトを編集する

Source Code Control 下にあるデータベースでは、既存オブジェクトの変更はそれほど困難ではありません。オブジェクトを開き、編集を開始 (既定ではチェックアウト) して、変更を保存するだけです。そこから変更をチェックインし、先に進みます。一方、シェルビング機能を使用してサーバーに変更を投稿した場合は、チェックインする前に別のチーム メンバ (DBA など) による見直しを行うことができます。

オフライン ワークスペースはチームの共同作業を促進します。また、ロールバック機能によって、データベース スキーマの変更に関する固有のリスクが軽減されます。さらに、Team Foundation Server チェックイン ポリシーによって、チームはワークフローを制御できるようになり、すべての変更を 1 つの作業項目に関連付けたり、すべてのチェックインにコメントを付加したりできます。

オフライン ワークスペースでは、データベース スキーマを管理する方法が提供されています。ただし、実際にオブジェクトを作成し、クエリを実行し、データを変更するためのランタイム環境は提供されていません。この作業は、SQL Server のライブ インスタンス (サンドボックス) に対して行います。運用スキーマをインポートしたら、次に SQL Server のローカル開発者インスタンスにスキーマを発行します。これは、2 つの簡単なステップが必要になります。まず、プロジェクト デザイナの [ビルド] タブで、ターゲット接続と新しいデータベース名を指定します。このオプションは、インポート ウィザードを使用してデータベース プロジェクトを作成した場合に、ウィザードによって設定されます。次に、ソリューション エクスプローラでデータベース プロジェクトを右クリックし、[配置] オプションを選択します。これが完了すれば、データベースの使用を開始できます。

データベース オブジェクトを設計するには、3 つの場所のいずれかから新しい項目を追加します。その場所とは、[プロジェクト] メニュー、ソリューション エクスプローラのコンテキスト メニュー、またはスキーマ ビューのコンテキスト メニューです。(対話式のデザイン ツールは、この最初のリリースには含まれていません。)すべてのデータベース オブジェクトは、適切な T-SQL ステートメントを入力することによって作成します。たとえば、新しいテーブルを作成するには、[プロジェクト] メニューから [項目の追加] コマンド選択して、[新しい項目の追加] ダイアログを開きます。次に、[カテゴリ] セクションの [テーブルとビュー] オプションを選択し、[テンプレート] セクションの [テーブル] 項目を選択します。最後に、新しいテーブルの名前を指定します。ライブ サーバーからデータベースをインポートすると、ディスク上の .sql ファイルで使用される名前は、objectname.objecttype.sql の形式に従います。データベース プロジェクトでファイル名の基準を考える場合は、このことに注意してください。ただし、明確にするため、ユーザーがファイルに選択する名前は、データベース内で使用されるオブジェクト名と直交関係にあります。名前を指定して [新しい項目の追加] ダイアログを閉じると、[T-SQL エディタ] ウィンドウが開き、基本テンプレートが提供されます。ここから作業を始めます。

DB Pro エディションの T-SQL エディタは、完全な機能を備えたエディタおよび実行ウィンドウです。色分け編集機能、構文検証、クライアントの実行統計、および複数の結果フォーマットがサポートされています。ただし、この最初のリリースでは、IntelliSense® はサポートされていません。テーブルの作成時に、Ctrl キーを押しながら F5 キーを押すと構文を検証できます。これを最初に行うと、接続を確認するメッセージが表示されます。

T-SQL エディタでデータ定義言語 (DDL) ステートメントを使用するときは注意してください。使い慣れた F5 キーストロークは、T-SQL を実行し、構文検証用に選択したどのデータベースに対しても、記載されているコマンドを実行します。テーブルの設計が完了したら、Source Code Control にテーブルをコミットするか、見直しのためにシェルビングできます。

T-SQL を使用すると、DB Pro エディション内で定義されたオブジェクトが非常に詳細化されます。インデックス、制約などは個別に作成します。これは、ユーザーがデータベース オブジェクトの作成にどの程度慣れているかによって大きく変わる可能性があります。たとえば、Northwind サンプル データベースにオークション サポートを追加すると仮定します。主キーの ID 列を含むオークション テーブルを作成するには、まずオークション テーブルを作成し、次に主キーの制約を作成する必要があります。また、外部キーの制約を追加する場合は、独自のステップを実行し、独自の .sql ファイルが必要になります。

データベースに新しいオブジェクトを追加することもできますが、既存のオブジェクトに変更を加えることもできます。ストアド プロシージャにエラー処理を追加するなどの変更は特に問題ありません。しかし、リファクタリングのように、より大幅な再設計となる変更もあります。オブジェクト名の変更は、データベースのリファクタリングの中でも一番頭の痛い作業ですが、これは DB Pro エディションのリファクタリング ツールによって対処されます。

オブジェクト名を変更するには、まず [スキーマ ビュー] ウィンドウを使用してオブジェクトを探す必要があります。次に、そのオブジェクトを右クリックして、[リファクタ]、[名前の変更] の順に選択します。最初のダイアログが表示され (図 2 を参照)、ここで新しい名前を指定します。また、変更をプレビューするかどうか、ログを生成するかどうか、完全修飾名が指定されていないスクリプトおよび単体テストの参照をどう取り扱うかを指定します。[OK] をクリックして続行すると、[変更のプレビュー] ダイアログ (図 3 を参照) が表示されます (既定のオプションを受け入れた場合)。

図 2 [名前の変更] ダイアログ
図 2 [名前の変更] ダイアログ(画像を縮小するには、ここをクリックします)
図 2 [名前の変更] ダイアログ
図 2 [名前の変更] ダイアログ (画像を拡大するには、ここをクリックします)
図 3 [変更のプレビュー] ダイアログ
図 3 [変更のプレビュー] ダイアログ(画像を縮小するには、ここをクリックします)
図 3 [変更のプレビュー] ダイアログ
図 3 [変更のプレビュー] ダイアログ (画像を拡大するには、ここをクリックします)

更新が必要なすべての項目がツリー ビューに表示されます。変更は、データベース プロジェクト内が検索されるだけでなく、データベース単体テスト、データ生成ファイル、および追加サポート .sql ファイルも調査されます。

項目を展開すると各変更が表示され、項目をクリックすると新しい T-SQL が表示され、特定の変更をクリックすると新しいオブジェクト名が強調表示されます。変更に問題がなければ、[適用] ボタンをクリックします。この操作では、スキーマのローカル ワークスペース コピーにのみ変更が適用されます。Source Code Control を使用している場合は、この変更をコミットする必要があります。また、この変更を既存データベース (可能ならサンドボックス コピー) に表示するには、更新スクリプトを配置する必要があります。

データ生成

データベース アプリケーションに関しては、鶏が先か卵が先かという問題があります。データは必要ですが、セキュリティと法的な制約によって運用データのコピーを使用できないことがよくあります。しかし、新しいテーブルはデータがなければ少々魅力に欠けます。このテーブルで動作するオブジェクト (ストアド プロシージャなど) についても同様のことが言えます。

DB Pro エディションには、開発時の苦労を軽減する豊富なデータ生成ツールが用意されています。データベース オブジェクトには、データ生成プランをいくつでも追加できます。各プランは、スキーマの 1 つまたは複数のテーブルに対するデータ生成をサポートします。テーブルに対してデータ生成を有効にした場合は、列ごとにデータ生成プランを指定します。組み込みジェネレータを使用できますが、独自のカスタム データ ジェネレータを作成することもできます。

提供されているデータ生成ツールでは、データ ジェネレータが 1 つに決められている場合に、反復可能なテスト データの同一セットが長期にわたって生成されます。これにより、実行するすべてのテストに対して一定のベースラインを提供できます。

難しい設定を必要としない、さまざまな標準ジェネレータが多数用意されています。データ生成プランを定義すると、DB PRo エディションは、その列のデータ型に基づいて、自動的に既定の値ジェネレータを割り当てます。さらに DB Pro エディションでは、外部キー列など、制約を適用する列が認識されます。

ほとんどの組み込みデータ型 (文字列、整数、GUID など) に対応している値ジェネレータを使用すれば、列に対して任意のデータを生成できます。各値ジェネレータは、生成を制御する多数のプロパティをサポートしています。たとえば、NVARCHAR などの文字列型では、最小長、最大長、パーセンテージなどのプロパティを設定できます。列要件がもっと複雑な場合は、正規表現ジェネレータを使用して、電話番号、郵便番号などを生成できます。データ バインド ジェネレータを使用すると、データベース内のテーブルやその他のサポート データベースから既存データを読み込むことができます。マネージ コードを使用してカスタム ジェネレータを作成すれば、実際に有用なデータ生成プランを簡単に作成できます。

単体テスト

ユーザーの間ではおそらく、Visual Basic® または Visual C#® コードの単体テストは使用されています。しかし、ストアド プロシージャ、関数、およびトリガのテストに、この強力な技術が使用されることはないようです。DB Pro エディションには、自動化された簡単な方法で前述のオブジェクトをテストするための、反復可能なメカニズムの概念が導入されています。これには、たとえば Team System 標準テスト プロジェクトの一部として使用されるデータベース単体テストなどがあります。単体テスト自体は、Visual Basic または Visual C# クラス ファイルの上にマップされた T-SQL です。

DB Pro エディションで、データベース スキーマと、そのスキーマを使用するアプリケーションの間のコントラクトを検証するには、単体テストを使用します。単体テストの使用を開始するのは簡単です。Northwind サンプル データベースの多数のストアド プロシージャを使用すれば、練習を開始できます。

既にインポートされている Northwind データベースで、[スキーマ ビュー] ウィンドウを使用して CustOrderHist ストアド プロシージャを探します。そのオブジェクトを右クリックすると、[単体テストの作成] オプションが表示されます。これを選択すると、図 4 に示すダイアログが表示されます。

このダイアログは 3 つのグループに分割されています。[現在の選択] グループでは、DB Pro エディションで単体テストを生成する項目を指定します。ストアド プロシージャ、関数、またはトリガをいくつでも選択できます。(Northwind データベースにはストアド プロシージャしか含まれていないため、図 4 のダイアログ ボックスには、関数ノードとトリガは表示されていません。)

図 4 [単体テストの作成] ダイアログ
図 4 [単体テストの作成] ダイアログ(画像を縮小するには、ここをクリックします)
図 4 [単体テストの作成] ダイアログ
図 4 [単体テストの作成] ダイアログ (画像を拡大するには、ここをクリックします)

[出力プロジェクト] グループでは、新しいテスト プロジェクトを作成するか、既存プロジェクトに新しい単体テストを追加するかを定義します。新しいプロジェクトを作成する場合は、既定のプログラミング言語 (Visual Basic または Visual C#) を選択します。最後に、新しいクラス名を指定するか、既存のクラスを選択します (既存のテスト プロジェクトを選択した場合)。

初めてデータベースに対して単体テストの作成を開始し、[単体テストの作成] ダイアログが表示されたら、すべて選択して [OK] をクリックしたくなるかもしれません。これはやめてください。テスト可能なオブジェクトはすべて同じクラスでよいですか?それらはすべて同じテスト プロジェクトでよいですか?すべてのコードを単一クラス ライブラリ内の単一クラスに配置しないのと同様に、すべてのテストを限定した構成に配置することもおそらくないでしょう。データベースがスキーマを使用するという事実によって、優れたファクタリング メカニズムが提供されています。[OK] をクリックする前によく考えてください。

このサンプルでは、CustOrdersOrders および CustOrdersDetails ストアド プロシージャを選択します。CustOrderHist の横のチェック ボックスは自動的にオンなっています。選択したプログラミング オブジェクトごとにテスト スクリプトが自動生成されます (これはカスタマイズが必要になる場合があります)。プログラム言語を選択し、プロジェクト名を NorthwindTests に変更し、クラス名 を Customers に変更し、[OK] をクリックして、テスト プロジェクトと単体テストを生成します。(プログラミング言語を選択しても、その言語を使用しなければならないわけではありません。必要なことはすべて、T-SQL で行うことができます。)[OK] をクリックすると、[テスト プロジェクト構成] ダイアログ ボックスが表示されます (図 5 を参照)。上から順に、最初のセクションで接続を設定します。

図 5 [テスト プロジェクト構成] ダイアログ
図 5 [テスト プロジェクト構成] ダイアログ(画像を縮小するには、ここをクリックします)
図 5 [テスト プロジェクト構成] ダイアログ
図 5 [テスト プロジェクト構成] ダイアログ (画像を拡大するには、ここをクリックします)

最初の項目では、単体テストの実行時に使用するライブ データベース サーバーへの接続を指定します。2 つ目の項目では、単体テストの検証時に使用する接続を指定します。

ここでは、2 つの異なる接続を使用する場合を考えます。最初の実行用接続は、テスト対象のオブジェクトを実行するユーザーが、一般的なユーザー アカウントの資格情報を使用して実行するために設定します。2 つ目の検証用接続は、昇格した特権を持つユーザーの資格情報を使用して実行するために設定します。2 つの接続を指定すると、テスト ランタイムは、検証用接続を使用して事前テスト、事後テスト、初期化、およびクリーンアップの各プロシージャを実行し、それ以外の場合に実行用接続を使用します。この設定は、テーブルのクリーンアップなど多数の検証操作で昇格した特権が必要となるため、特に便利です。

接続を定義したら、ダイアログ ボックスの次のセクションで配置オプションを設定します。DB Pro エディションの既存プロジェクトを指定することによって、単体テストの実行前に新しいデータベース スキーマまたは更新されたデータベース スキーマを配置できます。また、使用する配置構成も指定できます。最後に、実行するデータ生成プランを選択できます。テスト ランタイムは、データベースを配置してから (選択した場合)、データ生成プランを実行し、次にテストを実行します。

データの生成を選択した場合は、新しいデータを読み込む前にテスト ランタイムによってテーブルをクリアできます。特別な設定が必要ない場合は、実行用接続に、データベース プロジェクトの作成に使用した同じ Northwind 接続を選択し、その他の設定の既定値を受け入れて、[OK] をクリックします。テスト プロジェクトの生成が完了すると、データベース単体テストのデザイナが開き、使用開始に役立つ、生成済み T-SQL コードが表示されます (図 6 を参照)。

図 6 テスト デザイナ
図 6 テスト デザイナ(画像を縮小するには、ここをクリックします)
図 6 テスト デザイナ
図 6 テスト デザイナ (画像を拡大するには、ここをクリックします)

このデザイナは、Visual Basic と Visual C# の T-SQL スクリプト エディタおよびクラス エディタをサポートしています。ウィンドウは 2 つのセクションに分割されています。1 つはエディタ、もう 1 つは条件マネージャです。

データベース単体テスト クラスには、1 つまたは複数のデータベース単体テストが含まれます。各単体テストは最大 3 つのスクリプトで構成されます。これは、事前テスト、テスト、および事後テスト スクリプトです。ただし、必須なのはテスト スクリプトだけです。また、各データベース単体テスト クラスは、共通の事前テスト スクリプトと事後テスト スクリプトをサポートしています。デザイナの上部にあるナビゲーション バーで、アクティブな単体テストとテスト スクリプトを選択できます。

生成されたテストでは、既定のテスト条件が未決の状態となります。テスト条件は、デザイナの下部で管理します。各テストには、複数のテスト条件を追加できます。テスト条件を指定せずにテストを実行すると、テストは自動的にパスします。したがって、未決のテスト条件の既定値を使用するときは必ず、自動生成スクリプトを編集し、意味のあるテスト条件を定義してください。

サンプル dbo_CustOrderHistTest の場合は、自動生成 T-SQL を修正して、ALFKI などの顧客 ID を指定します。次に既定のテスト条件を選択し、[削除] ボタン ([テスト条件] ボックスの右側) をクリックして削除します。[テスト条件] ボックスを展開すると、[実行時間]、[ResultSet が空ではありません]、[行数] など、組み込みテスト条件の一覧が表示されます。また、マネージ コードを使用して独自のカスタム テスト条件を作成することもできます。

dbo_CustOrderHistTest 単体テストでは、[ResultSet が空ではありません] テスト条件を追加します。また、[行数] テスト条件を追加し、[行数] プロパティを 11 に変更します。各テスト条件は名前を変更できます。また、必要に応じてテスト条件を有効または無効にすることもできます。

デザイナ ウィンドウをアクティブにし、F5 キーを押すとテストを実行できます。この操作によって、プロジェクトは保存され、コンパイルされます。次にテスト エンジンがすべての単体テストを実行します。図 7 は、データベース単体テスト クラスの実行の流れを示しています。

図 7 データベース単体テストの実行
図 7 データベース単体テストの実行(画像を縮小するには、ここをクリックします)
図 7 データベース単体テストの実行
図 7 データベース単体テストの実行 (画像を拡大するには、ここをクリックします)

テストを個別に実行するには、[テスト ビュー] ウィンドウを表示し、特定の単体テストを選択して右クリックし、[選択範囲の実行] コマンドを選択します。また、テストはデバッグできます。Team Foundation Server と共に DB Pro エディションを使用している場合は、Team Foundation Server にテスト結果を発行したり、チーム ビルドの一環としてテストを実行したりできます。また、Team Test 機能を含む Visual Studio 2005 のバージョンを実行している場合は、テスト一覧をいくつも作成して、有効なテストのタイプを含むさまざまなテスト スイート別に、テストを論理的にグループ化できます。

データベースを比較する

データベースに関するすべての作業が終了したら、変更をテスト システムに移動します。DB Pro エディションでは、プロジェクトのスキーマを実行中のデータベースと比較できます。また、データ比較ツールを使用して、2 つのデータベース内のテーブルおよびビューのデータを比較できます。スキーマ比較ツールとデータ比較ツールは、DB Pro エディションのプロジェクトが開いているかどうかに関係なく、いずれかを実行できます。

スキーマ比較ツールを使用すると、2 つのスキーマを比較し、必要に応じて DDL 変更スクリプトを生成できます。このスクリプトは、DB Pro エディション内からすぐに適用することも、後で使用するためにエクスポートすることもできます。スキーマ比較ツールにアクセスするには、[データ]、[スキーマ比較]、[新しいスキーマ比較] の順にクリックします。新しい [スキーマ比較] ダイアログ ボックスが開いたら、ソース スキーマとターゲット スキーマを指定します。DB Pro エディションのプロジェクトをデータベースと比較することや、データベースどうしを比較することが可能です。

ソースとターゲットを定義すると、比較が開始します。3 つのセクションに分かれたスキーマ比較ウィンドウが開き、結果が表示されます。最上部のセクションには、タイプ別にグループ化された、すべてのスキーマ オブジェクトが一覧表示されます。中央の読み取り専用 [オブジェクトの定義] セクションには、現在選択されているオブジェクトの T-SQL を示す、2 つのテキスト比較ウィンドウが表示されます。下部の読み取り専用 [スキーマ更新スクリプト] ウィンドウには、更新スクリプトが提案されます。

表示されているオブジェクトは、相違、不足、新規、または同等の比較に基づいてフィルタリングできます。オブジェクトを調べる際に、項目の [更新アクション] プロパティを変更することによって、スキーマ更新スクリプトにオブジェクトを含めたり除外したりできます。DB Pro エディションでは、同等オブジェクトの [更新アクション] プロパティが [スキップ] に設定されています。その他のすべてのオブジェクトは、[作成]、[更新]、または [ドロップ] のいずれかに設定され、[更新アクション] の既定値から [スキップ] に変更できます。スキップするオブジェトの中から、修正するオブジェクトを簡単にフィルタリングできます。DB Pro エディションは、その間もずっと、提案スクリプトをリアルタイムで更新します。

更新スクリプトに問題がないことがわかったら、いくつかの選択肢があります。もしあなたに勇気があれば、変更をターゲット スキーマに書き込むことができます。この選択は、ラボではうまくいくかもしれませんが、おそらく DBA とはうまくいかなくなるでしょう。代わりの方法として、更新スクリプトをファイルに、または DB Pro エディションの組み込み T-SQL エディタにエクスポートできます。よく見直してから、ターゲット データベースを更新してください。スキーマの比較の実行をさまざまな方法で制御するには、[ツール]、[オプション]、[データベース ツール]、[スキーマ比較] の順にクリックします。

データの比較を実行するには、まずソース データベースとターゲット データベースを指定します。テーブルおよびビューは、同じ名前、同じ所有者、および同じスキーマでなければなりません。テーブルは、同じキーとインデックス レイアウトを保持している必要があります。ウィザードを開始するには、[データ]、[データ比較]、[新しいデータ比較] メニュー項目の順にを選択します。データ比較プロセスに関連するさまざまなオプションを調整するには、[ツール]、[オプション]、[データベース ツール]、[データ比較] の順にクリックします。データ比較が完了したら、生成されたスクリプトを使用して、ソース データベースとターゲット データベースの間でデータ動作を実行できます。

ビルドと配置

ここまでで、スキーマ比較ツールを使用して手動で T-SQL 更新スクリプトを生成できることがわかりました。しかし、DB Pro エディションでは、ビルド プロセスの一環として、データベース プロジェクトを配置するための完全なスクリプトを生成できます。この配置スクリプトは、完全なビルドと増分更新のいずれかを行うことができます。ビルド プロセスは、すべての配置前スクリプトと配置後スクリプトを 1 つの完全な配置スクリプトに統合することもできます。このスクリプトを配置するには、Visual Studio 2005 で [ビルド]、[選択範囲の整列] コマンドの順に選択します。

プロジェクト プロパティの下に、ビルド プロセスを制御および調整するための多数のオプションが表示されます。[ビルド] タブには、[ターゲット接続]、[ターゲット データベース名]、[データ損失が発生する場合に増分配置をブロック] など、主要な設定が含まれています。また、[ビルド イベント] タブでは、ビルド前またはビルド後のイベント コマンドを入力できます。DB Pro エディションは、ビルド プロセスに MSBuild を使用し、Team Foundation Server が使用されている場合は Team Build との統合をサポートします。

まとめ

あなたが SQL Server 2000 または SQL Server 2005 を使用するデータベース開発者なら、Visual Studio 2005 Team Edition for Database Professionals がデータベース支援のソリューションを構築するための重要なツールだということがおわかりいただけると思います。この製品では、特に最初のリリースによって、データベースの管理方法を変更する豊富な機能セットが開発環境に提供されます。

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


SQL Server 2005: 正規表現によるパターン マッチングとデータ抽出の簡略化

SQL Server 2005

正規表現によるパターン マッチングとデータ抽出の簡略化

David Banister
この記事の内容 : :
  • 正規表現を使用した効率的な SQL クエリ
  • SQL Server 2005 における正規表現のサポート
  • SQL Server からの .NET Regex クラスの使用
  • データベース内での正規表現の効果的な使用
この記事は次のテクノロジを使用しています:
SQL Server 2005、.NET Framework
コードのダウンロード : : Regex2007_02.exe (154KB)
コンテンツ
CLR ユーザー定義関数
パターン マッチング
データの抽出
パターン保存
一致項目
一致項目の中でのデータ抽出
まとめ

T
-SQL は、ほとんどのデータ分析を行う場合に非常に強力なツールとして機能しますが、テキストの分析と操作に関するサポートがほとんどありません。組み込みの文字列関数を使用して複雑なテキスト操作関数を実行しようとすると、巨大な関数とストアド プロシージャを作成する結果になり、デバッグとメンテナンスが困難です。もっと良い方法はないでしょうか?

実際は、正規表現を使用すると、より効率的でスマートなソリューションが得られます。レコードを識別するためにテキストを比較する場合、正規表現がどれほど役に立つのか確認するのは簡単なことです。しかし、正規表現はそれ以外の用途でも役立ちます。ここでは、さまざまなタスクを実行する方法を紹介します。いくつかは単純で、いくつかは驚くほど複雑です。ここで紹介する方法は SQL Server™ 2000 では実用性を欠くか、もしくは不可能な方法ですが、SQL Server 2005 では実現が可能です。共通言語ランタイム (CLR) のホスティングがサポートされているからです。

正規表現は、SQL の分野では目新しいものではありません。Oracle は、10g で組み込みの正規表現を採用しました。また、多くのオープン ソース データベース ソリューションでは、何らかの正規表現ライブラリを使用しています。実際には、SQL Server の以前のバージョンでも正規表現を使用することは可能でしたが、そのプロセスは非効率でした。

ストアド プロシージャ sp_OACreate を使用する場合は、正規表現を実装した任意の OLE オートメーション オブジェクトが活用できますが、開発者は最初に COM オブジェクトを作成し、次に少なくとも 1 つの IDispatch 呼び出しを行い、さらに、そのオブジェクトを破棄する必要があります。ほとんどの目的では、これは非常に効率が悪く、パフォーマンスに関する多くの問題を引き起こしました。唯一の代替手段は、拡張ストアド プロシージャを作成することでした。しかし、現在は SQLCLR が用意されています。これは、CLR ユーザー定義関数 (UDF) であり、Microsoft® .NET Framework を使用して、効率的かつエラーの発生しにくい一連の関数を作成するのに役立ちます。

CLR ユーザー定義関数

CLR ユーザー定義関数は、.NET アセンブリの中で定義されている、簡潔な static メソッド (Visual Basic では Shared 関数) です。SQLCLR オブジェクトを使用するには、新しい CREATE ASSEMBLY を使用して該当のアセンブリを SQL Server に登録し、次にアセンブリ内にあるこの実装をポイントする各オブジェクトを作成する必要があります。関数に関して、CREATE FUNCTION ステートメントは、CLR ユーザー定義関数の作成をサポートするように拡張されました。手順を簡単にするために、Visual Studio® 2005 では SQL Server プロジェクトを使用する際に、すべての登録プロセスが自動的に実行されます。この種類のプロジェクトは、他のほとんどの Visual Studio プロジェクトとは異なっています。デバッグを実行 (またはデバッグなしで開始) しようとすると、プロジェクトは再コンパイルされ、その結果生成されるアセンブリとその中で定義されているすべての SQLCLR オブジェクトが配置され、SQL Server に登録されるからです。その後、IDE は、このプロジェクトに対して指定されたテスト スクリプトを実行します。ブレークポイントは、SQL スクリプト内と、開発者が作成した .NET コード内の両方に設定できるので、デバッグの実行は簡単なプロセスになります。

関数を追加するのは、他の種類のプロジェクトに対して新しいクラスを追加することに似ています。単純に新しい項目をプロジェクトに追加し、ダイアログ ボックスの中から [ユーザー定義関数] を選択するだけです。開発者が作成したすべての関数を格納する部分クラスに対して、新しいメソッドが追加されます。新しいメソッドには、そのメソッドに対して適用される SqlFunction 属性もあります。Visual Studio はこの属性を使用して、この関数を登録するのに必要な SQL ステートメントを作成します。SqlFunction 属性にあるIsDeterministic、IsPrecise、DataAccess、および SystemDataAccess の各フィールドは、SQL Server でさまざまな目的に使われます。

パターン マッチング

文字列がパターンに一致するかどうかを判断するのは、正規表現の最も簡単な使用方法です。図 1 からわかるように、実行するのは簡単です。

Figure 1 文字列の比較 (Close Figure 1)
public static partial class UserDefinedFunctions
{
    public static readonly RegexOptions Options =
        RegexOptions.IgnorePatternWhitespace |
        RegexOptions.Singleline;

    [SqlFunction]
    public static SqlBoolean RegexMatch(
        SqlChars input, SqlString pattern)
    {
        Regex regex = new Regex( pattern.Value, Options );
        return regex.IsMatch( new string( input.Value ) );
    }
}

最初に、関数の正規表現オプションを格納するために、Options フィールドを使用します。この例では、RegexOptions.SingleLine と RegexOptions.IgnorePatternWhitespace を選択しました。前者はシングルライン モードを指定し、後者はエスケープされていない半角スペースを正規表現から削除して、ポンド記号 (#) によるマークの付いたコメントを入力できるようにします。注意深い考察と分析の結果、考えられるもう 1 つのオプションは、RegexOption.Compiled です。使用頻度が非常に高い表現が極端に多い場合を除き、そのような表現に対して Compiled を使用すると、パフォーマンスが大幅に向上します。繰り返し使用される表現は、明らかにコンパイルするのが妥当です。ただし、使用頻度の低い正規表現に対しては、Compiled を使用しないでください。起動時のコストとメモリのオーバーヘッドが増加し、リソースの負担が大きくなります。そのような理由で、筆者が作成した汎用の RegexMatch 関数に対して、表現をコンパイルするかどうかを指定するパラメータを追加することもできます。増加するオーバーヘッドに対し、結果として得られるパフォーマンスの向上の方が価値が高いかどうかを状況ごとに判断し、この方法で指定することができます。

使用する RegexOption を指定した後で、SqlString ではなく SqlChars データ型を使用して RegexMatch 関数を定義します。SqlString データ型は nvarchar(4,000) への変換を行うのに対し、SqlChars は nvarchar(max) への変換を行います。新しい最大サイズを持つ関数を使用すると、SQL Server 2000 の 8,000 バイトという上限を上回る文字列を扱うことができます。この記事では、最大の柔軟性を達成できるように、可能な場合はいつでも nvarchar(max) を使用します。ただし、対象となるすべての文字列の長さが 4,000 文字未満である場合は、nvarchar(4,000) を使用する方がパフォーマンスは明らかに向上します。実際のニーズを検討し、それに合わせて適切にコーディングする必要があります。

このメソッドの中にある残りのコードは簡単です。定義したオプションと用意したパターンを使用して Regex インスタンスを作成し、次に IsMatch メソッドを使用して、指定した入力がそのパターンに一致するかどうかを判断します。ここで、テスト スクリプトに対して次の簡単なクエリを追加する必要があります。

select dbo.RegexMatch( N'123-45-6789', N'^\d{3}-\d{2}-\d{4}$' )

このステートメントの中にあるパターンは、米国の Social Security Number (社会保障番号) を簡単にテストするものです。この新しいクエリに対してブレークポイントを設定し、次にこの関数に対するデバッグのステップ実行を開始します。この関数を使用すると、さまざまなテストを実行できます。ただし、ここでは、ほとんどの人が思いつきそうにない内容を紹介します。たとえば、データベースの中で整合性のある名前付け規則を維持することは重要です。一方、自分の作成したすべてのストアド プロシージャが組織のガイドラインを満たしていることを確認する単一のクエリを作成することは困難です。RegexMatch 関数を使用すると、このタスクがかなり簡単になります。たとえば、次のクエリによるテストでは、このタスクを実行します。

select ROUTINE_NAME
from INFORMATION_SCHEMA.ROUTINES
where ROUTINE_TYPE = N'PROCEDURE'
    and dbo.RegexMatch( ROUTINE_NAME,
        N'^usp_(Insert|Update|Delete|Select)([A-Z][a-z]+)+$' ) = 0

このクエリは、すべてのストアド プロシージャに "usp_" というプレフィックスが付き、その後に "Insert"、"Update"、"Delete"、または "Select" が続き、さらに少なくとも 1 つのエンティティ名が続くかどうかをテストします。また、エンティティ内にある各単語が大文字で始まっていることも確認します。上記の 4 行を、組み込み関数のみを使用して簡略化した次のバージョンと比較してください。

select ROUTINE_NAME
from INFORMATION_SCHEMA.ROUTINES
where ROUTINE_TYPE = N'PROCEDURE'
    and ( LEN( ROUTINE_NAME ) < 11
    or LEFT( ROUTINE_NAME, 4 ) <> N'usp_'
    or SUBSTRING( ROUTINE_NAME, 5, 6 ) not in
        ( N'Insert', N'Update', N'Delete', N'Select' ) )

コードの量は多いにもかかわらず、このクエリには、正規表現バージョンに存在しているいくつかの機能が実際に欠落しています。第 1 に、これは大文字と小文字を区別せず、クエリの中で照合を使用していないので、テストを実行しても扱いにくい結果になります。第 2 に、プロシージャ名の中に含まれている実際のエンティティ名に関して、何もテストを実行しません。3 番目の問題は、クエリの中でテストされている 4 つの文字列がいずれも長さ 6 文字であるという偶然に頼っていることです。この例ではこのことを生かして、長さ 6 文字の部分文字列を取り出し、受け入れ可能な各操作と比較するという方法で、コードを簡略化することができました。この例に限っては、すべての操作名が長さ 6 文字なので、このことは問題になりません。一方、"Get"、"List"、または "Find" のように長さが異なる動詞を規定する標準について想像してください。RegexMatch 関数を使用する場合は、これらの動詞も簡単に処理できます。リストの中に選択肢を追加するだけで済むからです。

確認は、正規表現を活用する非常に一般的な分野です。電話番号や郵便番号から、各社独自のアカウント番号の書式まで適用できます。CHECK による制約は、次のテーブル定義が示すように、この点で完璧です。

CREATE TABLE [Account]
(
    [AccountNumber] nvarchar(20) CHECK (dbo.RegexMatch(
        [AccountNumber], '^[A-Z]{3,5}\d{5}-\d{3}$' ) = 1),
    [PhoneNumber] nchar(13) CHECK (dbo.RegexMatch(
        [PhoneNumber], '^\(\d{3}\)\d{3}-\d{4}$' ) = 1),
    [ZipCode] nvarchar(10) CHECK (dbo.RegexMatch(
        [ZipCode], '^\d{5}(\-\d{4})?$' ) = 1)
)

AccountNumber 列は、任意設定の規則に対して確認されます。これは、長さ 3 文字または 5 文字の文字で始まり、その後に 5 桁の数字、1 個のハイフン、最後に 3 桁の数字が続きます。電話番号と郵便番号 (ZIPCode) は両方とも、米国標準の郵便番号の形式と ZIP コードの形式に対して確認されています。RegexMatch 関数は SQL Server に対して多くの機能を提供しますが、.NET における正規表現の実装は、次に検討するようにさらに多くの機能を実現します。

データの抽出

正規表現の持つグループ化機能を使用して、文字列からデータを抽出することもできます。筆者が作成した RegexGroup 関数を使用して、T-SQL で次の機能を実現できます。

[SqlFunction]
public static SqlChars RegexGroup(
    SqlChars input, SqlString pattern, SqlString name )
{
    Regex regex = new Regex( pattern.Value, Options );
    Match match = regex.Match( new string( input.Value ) );
    return match.Success ?
        new SqlChars( match.Groups[name.Value].Value ) : SqlChars.Null;
}

この関数は、RegexMatch 関数と同様に Regex オブジェクトを作成します。ただし、単一の一致項目をテストする代わりに、入力文字列の中で最初に見つかった一致項目を処理するための Match オブジェクトを作成します。この Match オブジェクトを使用して、指定されたグループを取得します。入力の中に、一致するものが見つからない場合は、NULL 値が返されます。名前付きのグループではなく番号付きのグループを使用する場合も、同じくこの関数が役立ちます。SQL コードの中で整数値をこの関数に渡すだけで、nvarchar へのキャストが暗黙的に実行され、適切なグループが返されます。

SELECT のリストの中でこの RegexGroup 関数を使用して、何らかのデータ ピース (部分) から特定の情報ピースを抽出することもできます。たとえば、URL を格納している列が存在する場合は、その URL を簡単に解析して個別のピースを取得することができます。次のクエリはグループ化機能を使用して、UrlTable テーブルの Url 列に格納されている個別のサーバーを見つけ出します。

select distinct dbo.RegexGroup( [Url],
    N'https?://(?<server>([\w-]+\.)*[\w-]+)', N'server' )
from [UrlTable]

計算によって得られる列の中でこの関数を使用することもできます。次のテーブル定義は、電子メール アドレスを、メールボックス部分とドメイン部分に分割します。

CREATE TABLE [Email]
(
    [Address] nvarchar(max),
    [Mailbox] as dbo.RegexGroup( [Address],
        N'(?<mailbox>[^@]*)@', N'mailbox' ),
    [Domain] as dbo.RegexGroup( [Address], N'@(?<domain>.*)', N'domain' )

mailbox 列は、電子メール アドレスのうちメールボックス部分、言い換えるとユーザー名を返します。domain 列は、電子メール アドレスのうちドメイン部分を返します。

パターン保存

これらの関数が使用するパターンのすべては単純な文字列です。これは、どのパターンも、データベース内にある単一のテーブルに保存できることを意味します。国際的なデータを格納するデータベースのほとんどは、国を表す単一のテーブルを持っています。そのテーブルにいくつかの付加的な列を追加すると、国特有の確認パターンを保存できます。この方法で、行が所属する国に基づいて、address (住所) 列に対して適用される制約を変更することができます。

顧客に関連するデータを格納するデータベースには通常、顧客を表すテーブルがあります。そのテーブルを使用してグループ化のパターンを格納し、データベースの中に顧客の生データがどのように格納されているのか記述することもできます。その結果、計算によって得られる列を作成し、本当に必要なデータを顧客データから取り出すことができます。たとえば、各顧客がアカウント番号に関して独自の方式を使用していて、開発者が必要とするのはそのアカウント番号のうち特定のピースのみである場合は、各クライアントに対応する正しい情報ピースを取り出す表現を簡単に作成できます。

一致項目

文字列がパターンに一致するかどうか判断するのではなく、一致した項目すべてを取得する必要が生じることもあります。以前は、この種類の抽出を行うには、文字列の各セクションに対してカーソルの繰り返し処理を実行する必要がありました。このプロセスは遅いうえに、コードが読みにくく保守が困難でした。この操作を実行するには、正規表現の方が良い手段です。とりあえずの問題は、SQL 構造の中で必要なデータすべてを返す方法です。テーブル値関数がその答えです。

テーブル値関数は、従来の関数にある程度似ていますが、2 つの点で異なっています。第 1 に、メソッドに対して適用される属性は、返されるテーブルの構造を完全に宣言する必要があります。第 2 に、2 つのメソッドが関係しています。最初のメソッドは、関数の実際の結果ではなく、列挙可能なオブジェクトを返します。2 番目のメソッドは、列挙されたオブジェクトを渡され、各行のフィールドを挿入します。列挙子をとおして取得された各値は、結果セットの中にある 1 つの行に対応します。.NET Framework の ICollection インターフェイスは、IEnumerable を実装します。これは、最初のメソッドを使用してあらゆるコレクションを返せることを意味します。Regex クラスには、MatchCollection を返す Matches メソッドが含まれていますが、開発者はこのコレクションを使用できます。MatchCollection に伴う問題は、Matches メソッドが制御を返す前に文字列全体を処理する必要がある、ということです。SQL Server には、必要に応じて発生する処理に対応する最適化機能があるので、筆者はコレクション全体をまとめて返す代わりに、要求に応じて個別の一致項目を返す独自の列挙子を作成することを好みます。この決定は、関数の使用方法に大きく依存するので、列挙子を最適化する前に、重点的にテストする必要があります。

図 2 のコードに、列挙子を示します。MatchNode クラスは、返された一連の一致項目の中で自らの位置を追跡する間、文字列の中にある個別の一致項目をラップします。MatchIterator クラスは列挙可能であり、正規表現の処理を取り扱います。このクラスは、新しい yield キーワードを使用して、.NET Framework の以前のバージョンよりはるかに簡単に列挙子を作成します。この列挙子は、要求に応じて、入力文字列の中で見つかった個別の一致項目を返します。

Figure 2 比較に使用するカスタムの列挙可能なオブジェクト (Close Figure 2)
internal class MatchNode
{
    private int _index;
    public int Index { get{ return _index; } }

    private string _value;
    public string Value { get { return _value; } }

    public MatchNode( int index, string value )
    {
        _index = index;
        _value = value;
    }
}

internal class MatchIterator : IEnumerable
{
    private Regex _regex;
    private string _input;

    public MatchIterator( string input, string pattern )
    {
        _regex = new Regex( pattern, UserDefinedFunctions.Options );
        _input = input;
    }

    public IEnumerator GetEnumerator()
    {
        int index = 0;
        Match current = null;
        do
        {
            current = (current == null) ?
                _regex.Match( _input ) : current.NextMatch( );
            if (current.Success)
            {
                yield return new MatchNode( ++index, current.Value );
            }
        }
        while (current.Success);
    }
}

図 3 に示すコードは、テーブル値 CLR UDF を定義します。RegexMatches メソッドは、新しい MatchIterator を返します。RegexMatches メソッドの SqlFunctionAttribute には、いくつかの付加的なプロパティも含まれています。TableDefinition プロパティは、関数のテーブル定義と同じ値に設定されます。FillRowMethodName は、メソッドの名前と同じ値に設定されます。このメソッドは、返された列挙可能なオブジェクトの繰り返しごとに呼び出されるものです。この例では、このメソッドは FillMatchRow です。

Figure 3 比較用のテーブル値 CLR UDF (Close Figure 3)
[SqlFunction( FillRowMethodName = "FillMatchRow",
    TableDefinition = "[Index] int,[Text] nvarchar(max)" )]
public static IEnumerable RegexMatches(SqlChars input, SqlString pattern)
{
    return new MatchIterator( new string( input.Value ), pattern.Value );
}

[SuppressMessage( "Microsoft.Design", "CA1021:AvoidOutParameters" )]
public static void FillMatchRow( object data,
    out SqlInt32 index, out SqlChars text )
{
    MatchNode node = (MatchNode)data;
    index = new SqlInt32( node.Index );
    text = new SqlChars( node.Value.ToCharArray( ) );
}

MatchIterator の繰り返しごとに、MatchNode は FillMatchRow メソッドの最初の引数として渡されます。FillMatchRow メソッドの残りのパラメータは、out (出力) パラメータとして宣言する必要があり、最初の関数の中で定義したテーブル宣言に一致していることも必要です。FillMatchRow 関数は単純に、MatchNode のプロパティを使用してフィールド データを挿入します。

この関数を使用すると、最終的に単一の文字列から複数のデータ ピースを簡単に抽出できます。RegexMatches 関数の使い方を示すために、次のクエリを使用して文字列を処理し、個別の単語がその中に何個含まれているか判断してみましょう。

declare @text nvarchar(max), @pattern nvarchar(max)
select
    @text = N'Here are four words.',
    @pattern = '\w+'
select count(distinct [Text])
    from dbo.RegexMatches( @text, @pattern )

この例は、非常に明快です。この関数を使用した場合の潜在能力のいくつかを示していますが、個別のキーワードを削除することにより、文字列の全ワード数を返します。多くの Web サイトには、任意の長さを受け付けるように見えて、実際には長さを制限したテキスト入力ボックスがあります。この種類のテストを、新しい nvarchar(max) 表記と組み合わせると、長さ (バイト数) の代わりにワード数を基準として入力を制限できます。この種類のクエリは分析処理のさまざまなニーズで活用できますが、RegexMatches 関数はより一般的なタスクでも使用できます。残念ながら、この種類のクエリは、正規表現の過度の多用を示す例でもあります。この例で "\w+" 表現を使用して実行した分割操作は、String.Split メソッドを使用して容易に実行でき、しかも明らかに高速です。正規表現は非常に強力なツールです。しかし、それを使用するときには十分な理由があることを確認してください。特定の状況では、より簡単に使用でき、より高いパフォーマンスを達成する他のツールが存在するからです。

筆者はよく MSDN® フォーラムで、値リストをストアド プロシージャに渡す方法を尋ねる質問を見かけます。また、関連のあるレコードを判断するために、そのようなリストを解析して実際のリストに変換するために、さまざまな複雑なメソッドを組み合わせた例も目にしたことがあります。RegexMatches 関数は、より明快なアプローチを実現します。

declare @pattern nvarchar(max), @list nvarchar(max)
select @pattern = N'[^,]+', @list = N'2,4,6'

select d.* from [Data] d
inner join dbo.RegexMatches( @list, @pattern ) re
    on d.[ID] = re.[Text]

このパターンは、カンマ (,) を含まない任意の文字グループに一致します。Data というテーブルがあり、その中には ID という名前を持つ整数型の列があります。このクエリは、リストの中で識別された各レコードを返します。SQL Server の中で利用できる暗黙的なキャスト機能を考慮すると、これはさらに便利な機能になります。整数、日付と時刻、GUID、または浮動小数点の各データ型に対して同じクエリを使用できます。値リストを処理する他の方法では、この柔軟性を達成するために、複数の関数またはストアド プロシージャが必要です。カンマで区切られていないリストに関しても、この関数が使用できます。半角スペース、セミコロン (;)、タブ、改行、または他の識別可能な文字で区切られたリストを処理できます。

一致項目の中でのデータ抽出

一致項目を返すのと同様に、各一致項目からデータを抽出することもできます。SQL を使用してこの作業を試みるのは非常に困難です。通常、この種類のタスクは、データベースではなくアプリケーションの中で実装します。ただし、この方法は、データベースを使用する各アプリケーションに、必要な処理を実装する必要が生じるという問題が伴います。このような状況で妥当なアプローチとして、ストアド プロシージャの中でこの機能を実装することが考えられます。

RegexMatches の実装と同様、筆者はカスタムの列挙可能なオブジェクトを使用してグループ情報を返すことを好みます。グループ化は少しだけ複雑です。各一致項目の中で、グループを繰り返し処理する必要があるからです。図 4 に示す GroupNode クラスは MatchNode クラスに似ていますが、そのクラスが表すグループの名前を含んでいる点が異なります。GroupIterator クラスは MatchIterator クラスに似ていますが、各グループを返す追加のループがその中に含まれています。列挙可能なオブジェクトが既に存在しているので、RegexMatches 関数と同様に、テーブル値関数を定義します。

Figure 4 グループを対象としたカスタムの列挙可能なオブジェクト (Close Figure 4)
internal class GroupNode
{
    private int _index;
    public int Index { get { return _index; } }

    private string _name;
    public string Name { get { return _name; } }

    private string _value;
    public string Value { get { return _value; } }

    public GroupNode( int index, string group, string value )
    {
        _index = index;
        _name = group;
        _value = value;
    }
}

internal class GroupIterator : IEnumerable
{
    private Regex _regex;
    private string _input;

    public GroupIterator( string input, string pattern )
    {
        _regex = new Regex( pattern, UserDefinedFunctions.Options );
        _input = input;
    }

    public IEnumerator GetEnumerator()
    {
        int index = 0;
        Match current = null;
        string[] names = _regex.GetGroupNames();
        do
        {
            index++;
            current = (current == null) ?
                _regex.Match( _input ) : current.NextMatch( );
            if (current.Success)
            {
                foreach(string name in names)
                {
                    Group group = current.Groups[name];
                    if (group.Success)
                    {
                        yield return new GroupNode(
                            index, name, group.Value );
                    }
                }
            }
        }
        while(current.Success);
    }
}

図 5 では、RegexMatches 関数と同様に RegexGroups 関数を定義しています。ただし、後者の関数は一致項目の中にあるグループの名前を格納している付加的なデータ列を返します。この関数を使用すると、単一の文字列の中にある複数の一致項目を見つけ、各一致項目から特定の情報ピースを抽出することができます。

Figure 5 グループを対象としたテーブル値 CLR UDF (Close Figure 5)
[SqlFunction( FillRowMethodName = "FillGroupRow", TableDefinition =
    "[Index] int,[Group] nvarchar(max),[Text] nvarchar(max)" )]
public static IEnumerable
    RegexGroups( SqlChars input, SqlString pattern )
{
    return new GroupIterator( new string( input.Value ), pattern.Value );
}

[SuppressMessage( "Microsoft.Design", "CA1021:AvoidOutParameters" )]
public static void FillGroupRow( object data,
    out SqlInt32 index, out SqlChars group, out SqlChars text )
{
    GroupNode node = (GroupNode)data;
    index = new SqlInt32( node.Index );
    group = new SqlChars( node.Name.ToCharArray( ) );
    text = new SqlChars( node.Value.ToCharArray( ) );
}

さまざまな書式を持つデータをインポートするのは、データベースを扱う際の一般的なタスクです。カンマ区切り形式のファイルをインポートするのは、思ったより面倒な仕事です。ほとんどの開発者は、各行を処理し、データを抽出し、行ごとにストアド プロシージャを実行するアプリケーションを作成します。このプロセスは正常に機能しますが、筆者としては他のソリューションをお勧めします。ファイル全体をストアド プロシージャに渡し、ストアド プロシージャがプロセス全体を処理するとしたらどうでしょうか?この概念は通常、実装が複雑すぎると考えがちですが、RegexGroups 関数を使用すると、単一のクエリを使用するだけでこの挿入を実行できます。たとえば、次の顧客データについて考えましょう。

2309478,Janet Leverling,J
2039748,Nancy Davolio,N
0798124,Andrew Fuller,M
4027392,Robert King,L

各行には、取得する必要のある 3 つの情報ピースがあります。7 桁の顧客番号、顧客の氏名、および 1 文字から成る顧客の種類です。次の表現を使用すると、これら 3 つの情報ピースすべてを抽出できます。

(?<CustomerNumber>\d{7}),(?<CustomerName>[^,]*),(?<CustomerType>[A-Z])\r?\n

次に発生する問題は、RegexGroups 関数が返した結果が、直接使用できるものではないことです。カーソルを使用して結果を繰り返し処理する代わりに、SQL Server 2005 のピボット機能を使用できます。これらすべてを単一のストアド プロシージャに統合すると、必要なものすべてが得られます。図 6 に示すストアド プロシージャは、Unicode データから成る、最大 2GB のカンマ区切りファイル全体のテキストを受け入れます。これは、ファイル全体を処理し、ファイルの各行を Customer テーブルの各行として挿入します。任意の区切り形式テキスト ファイルを同じ方法で処理できます。パターンに小規模な変更を加えるだけで、文字列の中にあるカンマに対応するためのエスケープ シーケンスを追加することもできます。

Figure 6 カンマ区切りファイルの処理 (Close Figure 6)
create proc ImportCustomers
(
    @file nvarchar(max)
)
as

declare @pattern nvarchar(max)

set @pattern = N'(?<CustomerNumber>\d{7}),
    (?<CustomerName>[^,]*),(?<CustomerType>[A-Z])\r?\n'

insert [Customer]
(
    [CustomerNumber],
    [CustomerName],
    [CustomerType]
)
select
    f.[CustomerNumber],
    f.[CustomerName],
    f.[CustomerType]
from dbo.RegExGroups( @file, @pattern ) regex
pivot
(
    max([Text])
    for [Group]
    in ( [CustomerNumber], [CustomerName], [CustomerType] )
) as f

ただし、このプロシージャも、同じタスクを実行するために複数の方法があることを示します。また、正規表現が常に最善のオプションであるとも限りません。この例では、ピボットを使用するだけで、特殊なグループ化書式を返すために RegexGroups が実行したすべての作業を不要なものにできます。より簡単、かつより高速に各行を読み取る TVF (テーブル値関数) を使用し、カンマに対して String.Split を適用し、各行を返す方法で、データをテーブルに直接挿入できます。

まとめ

これらのマッチング (比較) 関数は非常に強力ですが、完璧ではありません。比較を実行する正確な方法を決定するうえで、多くのオプションが利用可能です。データベース照合が大文字と小文字を区別しない場合は、同様に、大文字と小文字を区別せずに比較を行う関数が必要です。いくつかの結果セットを縮小するために、明示的なキャプチャ オプションが必要になることもあります。複数行オプションを使用すると、特定のタスクではより正確なパターンを作成できます。さらに、必要とされる正確なオプションを各関数に渡すために、ユーザー定義型を作成することもできます。この結果、関数の実行ごとに、異なる一連のオプションを使用できます。

テキストを処理するときは、ローカライズの問題が発生することにも注意してください。たとえば、.NET Framework の Regex クラスは、筆者の例で登場したラテン文字だけではなく、他の多くの文字にも対応しています。したがって、国際データを使用するデータベースに関連してパターンを開発するときは、注意が必要です。

もちろん、この記事で何回か説明したように、正規表現は非常に強力ですが、本当にその力が必要なのか確認してください。一部のタスクは、より基本的なツール セットを使用して、より高速、そしてより簡単に実行できます。

簡略化のため、この記事で提供したサンプルには、確認とエラー処理が欠落しています。どのような運用システムにも、それらの機能を含める必要があります。関数に対する各入力の妥当性の検査や、要件定義の中で、NULL 値または空文字列が入力として与えられた場合の応答を決定する必要もあります。Regex クラスは、パターンが解析不能な場合や、オプションが無効な場合に、例外をスローすることもあります。これらの例外は、適切に処理する必要があります。

正規表現を SQL と組み合わせると、データを処理するために多くの代替手段が得られます。これらの関数を使用すると、データベースに機能を追加するために必要な時間を短縮し、システムの保守性を向上させることができます。どのようなデータベースでも正規表現は使用可能です。これらの関数を試し、新しく、より生産的な用途を見いだすことをお勧めします。

David Banister は、米国ジョージア州アトランタに住み、上位 4 社に入る会計事務所のために働く上級ソフトウェア開発者です。彼は長年にわたって、さまざまなソフトウェアの開発に取り組んできました。余暇の時間には、 ECMA 言語仕様の精読、テニス、ローカル バンドの支援を行っています。


編集者のメモ: 新しい Office について

編集者のメモ
新しい Office について
Joshua Trupin

本当にがっかりさせられること 魅力的なデザインにもかかわらず、機能面での実装が正しく行われていない製品ほどがっかりさせられるものはありません。たとえば、Bluetooth ヘッドセット。小型軽量で、ワイヤレスで、しゃれてはいるものの、機能面での実装がうまく行われていません。使いたくないときに電源が入り、使いたいときには電源が入らない。思い通りに機能するのかどうか、まったくわかりません。

優れたデザインには、それに見合った確実な機能の実装が必要です。数か月にわたる Windows Vista と 2007 Microsoft Office system のテストの結果、これらの製品では、デザインの優れた多くの機能が確実に動くことを私たちは目にしてきました。

今月の MSDN Magazine は、新しい 2007 Microsoft Office system に焦点を当てています。この製品には、ユーザーにとってもプログラマにとっても、タスクをより簡単に、また一貫性を持って実行できる数多くの斬新な機能が盛り込まれています (プログラムの開発者について語るとき、たまにマクロを作る人も含めて、Microsoft Office を使ってある程度のプログラムを作成している人たちが何百人もいることを忘れがちです)。

一目で分かる 2007 Microsoft Office system の変更点は、まったく新しい "リボン" インターフェイスです。過去 20 年間、Windows ベースのほとんどのアプリケーションで使用され、慣れ親しんできたメニュー方式に代わり、新しい Microsoft Office では、より多くの情報と機能をユーザーに提供し、より多くの可能性を開発者に提供するユーザー インターフェイスが採用されています。膨大なインストール 数を誇る製品の 2007 年のリリースに対して、思い付きで機能を変更できるはずがないにもかかわらず、今になってなぜ基本的なデザインを変更したのでしょうか。今月の注目記事として、これまでになかった新しい概念、コンポーネント、および用語に加え、リボン インターフェイスが採用された理由を Eric Faller が説明しています。また、開発者向けに、2007 Microsoft Office system の独自のリボンを作成するための関連 API の活用方法を紹介しています。

2007 Microsoft Office system のもうひとつの際立った機能として、SmartArt グラフィックと呼ばれるものがあります。この知的なグラフィック機能によって、Office ドキュメントに、より視覚的な効果を持たせることができます。Visio を強化したようなグラフィック機能ですが、Visio でできなかったことがたくさんできます。グラフィック アートのトレーニング経験がないユーザーでも、視覚効果の高い、魅力的なグラフィックを作成することができます。ここにお見せする愛犬ビーグルの成長記録は、適当な定義済みの図形をクリックし、テキストを追加し、絵を挿入するためにもう1 度クリックするだけの簡単な操作でできあがりました。

2007 Office system には、さまざまなテンプレートや定義済みの図形が用意されており、それらをドキュメントにすばやく挿入できます。さらに良い点は、拡張性があることです。今月の Janet Schorr による記事では、SmartArt グラフィックについて、またそれがどのように機能するのか、さらには独自のグラフィックを作成する方法について説明されています。

Microsoft Office の機能を拡張する 1 つの方法として、Microsoft Visual Studio Tools for Office (VSTO) を使用した 2007 Office system アプリケーション用のカスタム アドインの作成があります。今回新たにリリースされた VSTO は、PowerPoint と Visio を初めてサポートするようになりました。Paul Stubbs は彼の記事の中で、このサポートについて紹介し、Office system アプリケーションの拡張方法を説明しています。

最後に、コラムに関する変更点として、Ted Pattison が「Basic Instincts」に終止符を打ち、彼の新たなコラムとして 2007 Microsoft Office system を使った開発や、2007 Office system 用プログラムの開発について語る「Office Space」を始めました。今回の記事では、プログラムで構築する Office Open XML ファイルについて説明しています。

ユーザーのみならず、開発者にとっても、Microsoft Office から見いだすことのできる優れた機能はたくさんあります。そして冒頭で述べた Bluetooth ヘッドセットとは異なり、2007 Office system と Windows Vista は常に連携して信頼のおける機能を提供してくれます。

筆を置く前に、個人的なお知らせがあります。165 本のコラムを書いてきた Paul DiLascia は、今月のコラム執筆の後、しばらく休暇を取ることに決めました。Paul さん、長い間お疲れ様でした。MSDN Magazine への投稿を再開されるのを楽しみにしています。£J.T.

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Shawn Burke、Claudio Caldato、Pablo Castro、Jonathan Caves、Shy Cohen、Matt Gibbs、Mark Hall、Steven Hemingray、Doug Mahugh、Matt Nunn、Doug Purdy、Stefan Schackow、Ayman Shoukry、Don Smith、Herb Sutter、Damien Watkins、および Matthew Yip です。

Access、Active Directory、ActiveX、Excel、Expression、IntelliSense、Internet Explorer、InfoPath、Microsoft、MSDN、Outlook、PowerPoint、SmartArt、Virtual Earth、Visio、Visual Basic、Visual C++、Visual C#、Visual Studio、Windows、Win32、Windows CardSpace、Windows Live、Windows Media、および Windows Vista は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。ここに記載されている商標または会社名は各社の所有物です。 MSDN Magazine に記載されているコードまたはその他の情報に関し、明示または黙示にかかわらず一切の表明または保証を行うものではありません。MSDN Magazine は、コードまたはその他の情報の使用に対していかなる責任も負わないものとします。MSDN Magazine に記載されている推薦事項および技術ガイドラインは、特定の環境および構成に基づいたものです。これらの推薦事項やガイドラインは、異なった環境や構成には適用できない場合もあります。MSDN および Microsoft は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。MSDN Magazine、MSDN、および Microsoft のロゴは、CMP が許可を得て使用しています。MSDN Magazine は、CMP Technology LLC によって発行されています。CMP Technology LLC は、Microsoft Corporation の系列企業ではなく独立した企業です。Microsoft Corporation は、MSDN Magazine の編集内容のみに責任を負うものとします。

MSDN Magazine does not make any representation or warranty, express or implied with respect to any code or other information herein. MSDN Magazine disclaims any liability whatsoever for any use of such code or other information.


ツールボックス: リッチ Web アプリケーション UI、正規表現用ツールなど

ツールボックス
リッチ Web アプリケーション UI、正規表現用ツールなど
Scott Mitchell

コンテンツ
リッチ Web アプリケーション UI を作成する
正規表現用ツール
画面モックアップを作成する
推薦図書

リッチ Web アプリケーション UI を作成する

ASP.NET には、コア ユーザー インターフェイス要素を提供するさまざまな組み込み Web コントロールが付属しています。しかし、より優れたユーザー インターフェイスの導入が必要な場合や、より高度な視覚効果が必要な場合は、サードパーティのベンダに頼ることが最善の選択です。

現在は、さまざまな企業が、拡張グリッド、ツールバー、タブ ストリップ、チャート、メニューなどの豊富な ASP.NET ユーザー インターフェイスを、すべて 1 つのコントロール スイート パッケージにまとめて提供しています。そのような製品の 1 つに、Infragistics 社の NetAdvantage for ASP.NET Version 2006 Volume 2 があります。NetAdvantage for ASP.NET のライブラリには、データの処理、ユーザー入力の収集、ナビゲーション、チャートの作成、およびコントロールのグループ化に使用できる、ASP.NET Web コントロール一式が含まれています。

Infragistics 社の主力製品である UltraWebGrid には、ASP.NET の組み込み Web コントロールの DataGrid や GridView では利用できない、完全に機能するカスタマイズ可能なグリッド機能が用意されています。既定で DataGrid や GridView にない機能とは、たとえば、グループ化、列のサイズ変更、固定列ヘッダー、フィルタリング、列と行の選択などです。これらの機能は、デザイナでいくつかのプロパティを設定するだけで、UltraWebGrid に追加できます。また、AJAX 技術を利用できるように UltraWebGrid を構成すれば、オンデマンドでデータを読み込み、グリッドのデータに対する更新を実行できます。

NetAdvantage for ASP.NET
NetAdvantage for ASP.NET (画像を縮小するには、ここをクリックします)
NetAdvantage for ASP.NET
NetAdvantage for ASP.NET (画像を拡大するには、ここをクリックします)

NetAdvantage for ASP.NET では、他にも多数のコントロールを使用できます。たとえば、ツリー コントロール、ナビゲーション コントロール (ツールバー、タブストリップ、メニューなど)、リッチ テキスト エディタとマスク入力コントロール、チャート コントロール、カレンダーと追加の日付/時間関連コントロール、拡張ボタンとパネル コントロール、スペル チェック コントロールなどがあります。このすべてのコントロールでは、デザイン時に豊富な機能を使用できるため、コーディングの必要はほとんどありません。また、たいていのコントロールには、オンデマンドでの読み込み、先行入力機能、行フィルタリング、ページングなどの組み込み AJAX 機能が用意されています。

NetAdvantage for ASP.NET コントロールは、ユーザー インターフェイス設計に新しい世界を開くだけでなく、華やかな外観も提供します。すべてのコントロールには、外観をカスタマイズするための複数のプロパティが用意されています。また、ボタンをクリックして適用できる多数の定義済みテーマもあります。ASP.NET UI のレベルを上げる必要がある場合、または外観の優れた Web サイトを最低限の労力で作成したい場合は、Infragistics 社の NetAdvantage for ASP.NET を試してみてください。

価格 : 1 ユーザー ライセンス - 795 ドル

www.infragistics.com (英語)

正規表現用ツール

正規表現とは、特別な構文を使用した文字列であり、指定したパターンと一致する値を別の文字列内で探す場合に使用します。一般には、入力の検証と文字列の処理に使用されます。たとえば、ユーザーが YYYY-MM-DD 形式で日付を入力したかどうかを確認するには、正規表現パターン ^\d{4}-\d{2}-\d{2}$ を使用します (ただし、この単純なパターンは、桁数とハイフンのみをチェックするため、1111-99-44 などの無効な日付も有効と見なされます)。

正規表現を作成してテストするのは、骨の折れる作業です。正規表現の使用経験がない開発者にとって、テキストと特殊文字の寄せ集めは、理解不能な訳のわからない言葉にすぎません。Visual Studio® では、コード内での正規表現パターンの作成をサポートするツールはありません。構文の強調表示や IntelliSense® は使用できず、Visual Studio で正規表現パターンを簡単にデバッグまたはテストする方法もありません。

Regulator (画像を縮小するには、ここをクリックします)

Regulator (画像を拡大するには、ここをクリックします)

ありがたいことに、Roy Osherove (weblogs.ASP.NET/rosherove) (英語) が、Visual Studio に不足している正規表現サポートを埋め合わせる、いくつかの無料ツールを作成しました。これらのツールのうち最もよく知られているのは、スタンドアロン正規表現エディタの Regulator です。このエディタには、構文の色強調表示、IntelliSense に似たヒント、コード スニペット ウィンドウ、RegExLib.com (共通正規表現パターンのオンライン ライブラリ) への検索アクセスなどの機能が装備されています。また、入力用と出力用の各ウィンドウも提供されています。ユーザーは、テストする入力文字列を指定し、正規表現を使用して一致が見つかるかどうかを確認します。また、Regulator でボタンをクリックすると、パターン マッチング、文字列置換、または文字列分割の正規表現を実装するための、Visual Basic® または C# コードが自動的に作成されます。

Roy は、正規表現用ツールとして Regex Kit Visualizer for Visual Studio 2005 も作成しました。これをインストールすると、Visual Studio 2005 IDE でのデバッグ時に、Regulator と同様のインターフェイスを使用して正規表現を分析し、調査できます。

価格 : 無料

tools.osherove.com (英語)

画面モックアップを作成する

ソフトウェア システムのエンジニアリング時に、すべての主要な関係者がシステムの目的と機能を明確に理解できるようにすることは重要です。そのための情報は、一般にプロトタイプを使用して伝達されます。Visual Studio Designer を使用すると、機能的なプロトタイプをすばやく作成できますが、通常では必要な情報は図やシンプルな画面モックアップを使用して示されるため、このようなプロトタイプでは大げさすぎる場合があります。また、Visual Studio はプロトタイプ ツールとして設計されていないため、ユース ケース シナリオへのフォームの組み込み、ユーザー操作に基づく UI 変更の図示などの機能がありません。

よりシンプルで効果的な方法は、画面モックアップの作成用に設計されたプログラムを使用することです。Igor Ješe 社の MockupScreens Version 3.09 では、マウスをポイントしてクリックするだけで、ワイヤ フレーム画面を作成し、それらをシナリオに組み込むことができます。MockupScreens プロジェクトはシナリオのセットで構成され、各シナリオは画面のセットで構成されます。各画面は、ボタン、ラベル、ドロップダウン リスト、テキスト ボックスなどの UI 要素で構成されます。画面の作成は、Visual Studio デザイナによく似た機能を持つ Screen Editor を使用すると簡単です。ユーザー インターフェイス要素を画面に追加するには、ツールボックスから UI 要素を画面にドラッグするだけです。その後で、要素のプロパティを使用して正確に配置し、構成できます。

モックアップを作成したら、さまざまな方法で主要な関係者と共有できます。オンサイトの場合は、Mockup Screens プロジェクトのスライド ショー機能を使用して、プレゼンテーション設定でプロトタイプを説明できます。オフサイトの場合は、Mockup Screens プロジェクトを Web ページにエクスポートできます。また、個別の画面を JPG または GIF イメージ ファイルとしてエクスポートすることもできます。

MockupScreens (画像を縮小するには、ここをクリックします)

MockupScreens (画像を拡大するには、ここをクリックします)

アプリケーションのプロトタイプを作成する方法は、単に 1 枚の紙に概要を描き出すことから、Visual Studio を使用して機能的なプロトタイプを作成することまでさまざまです。MockupScreens には、ユース ケース シナリオに組み込まれたワイヤ フレーム モックアップを作成して共有するための、便利で簡単な方法が用意されています。UI のアイデアやデザインの決定事項を、明確かつ効果的な方法で主要な関係者に伝達する必要がある場合は、MockupScreens を試してみてください。

価格 : 1 ユーザー ライセンス - 79 ドル

www.mockupscreens.com (英語)

推薦図書

サイズ、適用範囲、または複雑さにかかわらず、すべてのプログラムは完全にテストして、正常、および異常系の入力に対して正確な結果と動作が得られることを確認する必要があります。多くの開発者は、商用またはオープン ソースのテスト フレームワークを導入して、テスト プロセスをサポートおよび自動化しています。テスト フレームワークが適している場合もありますが、『.NET Test Automation Recipes:A Problem-Solution Approach』 (2006 年、Apress 発行) の著者である、MSDN® Magazine のコラムニスト Dr. James D. McCaffrey は、その著書の中で、これらのテスト フレームワークは軽量テスト ハーネスによって補完されるべきだと提案しています。軽量テスト ハーネスとは、ターゲット アプリケーションに対して 1 つまたは複数のテストを実行するために作成された、短い (4 "ページ" 以下のソース コード) シンプルなスタンドアロン プログラムです。すべてのテスト フレームワークには学習曲線があります。また、多くのテスト フレームワークでは、開発者がテスト ケースを作成するときに特定のスタイルを導入せざるを得なくなっています。しかし、ソフトウェアの軽量テスト自動化によって、テスト ハーネスを作成する開発者は、現在の知識ベースとプログラミング スタイルを活用できるようになります。

(画像を縮小するには、ここをクリックします)

軽量テスト ハーネスの作成には、正式なテスト フレームワークではフレームワーク自体が処理するような、多数のステップが必要となります。たとえば、入力テスト データの読み取り、テストの実行、実際の出力値と期待値との比較、結果の保存と要約などです。.NET など、最新のプログラミング フレームワークを使用すれば、これらのタスクを実行するコードを簡単に作成できます。彼の本には、軽量テスト ハーネスの作成に関する 100 以上の標準タスクについて、コードと説明が記載されています。

この本は、「Windows Application Testing」、「Web Application Testing」、および「Data Testing」の 3 つのセクションに分かれています。「Windows Application Testing」の章では、プログラムでリフレクションを使用して、クラスのプロパティにアクセスしたり、メソッドを呼び出したりする方法や、特定のコントロールのクリック、メニュー項目の選択などのユーザー インターフェイス操作を実行するためのコード スニペットが紹介されています。「Data Testing」の章では、ADO.NET コードと SQL ストアド プロシージャのテスト方法や、XML データの解析、検証、および比較方法が紹介されています。

軽量テスト ハーネスは、その名のとおりシンプルであり、作成に時間をかける必要はありません。Dr. McCaffrey の本で紹介されている方法を実行すれば、すぐに作成時間を短縮できます。

価格 : $49.99.

www.apress.com (英語)

Scott の連絡先は toolsmm@microsoft.com.

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


CLR 徹底解剖: .NET アプリケーションの拡張性

CLR 徹底解剖
.NET アプリケーションの拡張性
Jack Gudenkauf and Jesse Kaplan

コードのダウンロード : : CLRInsideOut2007_02.exe (197KB)
コンテンツ
拡張可能ホストを構築する
探索
アクティブ化、分離、セキュリティ、およびサンドボックス化
有効期間管理
アドイン開発
まとめ

Microsoft .NET Framework により、さまざまな開発者とベンダによって構築されたコンポーネントを選んで自分自身のアプリケーションに統合することが、非常に簡単になりました。とは言え、簡単になるのは、構築対象のコンポーネントを知っている場合に限られます。構築時にコンポーネントのことを把握していないなら (これは実は、アドインではよくあることです)、状況ははるかに難しくなります。開発者がよく疑問に思うのは、いつアプリケーションを拡張したらよいのだろう、ということです。例えば、アドインはデータベースに格納した方がよいのでしょうか、それともディスク上の方がよいのでしょうか。アクティブ化の型を取得するために、既知のインターフェイスにアドインを追加することについて検討する必要があるでしょうか。AppDomain、AppDomainManager、および AppDomainSetup を使用するための最善の方法は何でしょうか。

私たちは、新しい System.AddIn 名前空間を Base Class Library (BCL) に導入するにあたり、「CLR 徹底解剖」の 2 回の連載記事でこれらの問題やその他の質問を扱います。このライブラリは、Visual Studio® の次のリリース (コードネーム "Orcas") で利用可能になります。このライブラリは、Community Technology Preview を通じて、完全リリース版の Visual Studio "Orcas" より先にダウンロードできます。また、共通言語ランタイム (CLR) アドイン チームのブログ (英語) でも、情報を見つけることができます。

このシリーズのコラムでは、アプリケーションに拡張性を追加した後、長期にわたってバージョン管理を行う、という開発ライフサイクルを詳しく説明します。この第 1 回目のコラムでは、初めてアプリケーションに拡張性を追加するときに開発者が直面するすべての課題、および私たちの API を使用することにより開発者の作業がどのように簡単になるのか、について説明します。来月は、ホストの環境が変化してもアドインの機能を維持する、という比較的長期にわたる問題に焦点を合わせます。このコラムで作成したサンプル アプリケーションを取り上げて、次のバージョンに向けて大幅に手を加えます。そして、元のアドインの機能を維持するのに必要な手順を詳しく説明します。

それでは、最初は拡張性の課題と API の利点に関する話から始めましょう。System.AddIn 名前空間が追加され、マネージ アドイン モデルを構築するためのガイダンスがあるので、CLR では、アプリケーションの最初のバージョンに起こりがちな一般的問題のソリューション、およびアプリケーションの将来の互換性を確保するためのガイダンスを提供します。コラムで使用する用語をより深く理解するため、[拡張性関連用語] サイドバーを参照してください。

拡張性関連用語

ホスト 拡張性をサポートするアプリケーション。ホストは、クライアントのときもあればサーバーのときもあります (Excel、Word、Paint.NET、SQL Server、Exchange Server など)。

拡張性 ホスト アプリケーション オブジェクトを操作したり、ホストの機能を拡張したりするためのしくみ。”自動化” と呼ばれることもあります。一般に、ホストの SDK の一部として公開されたオブジェクト モデルで利用可能になります。

Software Development Kit (SDK) パブリック インターフェイス (API) と関連ドキュメンテーションのセット。ホスト アプリケーションが、サービスをアドインに提供したり、反対にアドインからサービスを受けたりできるようにします。SDK がどういう状況でどこから提供されるのかは、さまざまです。たとえば、ホスト特有のアドインを可能にするために、ホストが SDK を提供する場合があります。また、多くのさまざまなホスト (たとえば、Windows Media® Player) が使用できるように、アドインが SDKを提供することもあります。多くのさまざまなホストとアドインが互いに通信できるようにするために、サードパーティが SDKを提供する場合もあります。

アドイン (アドオン、拡張機能、プラグイン、スナップインとも呼ばれます) カスタマイズの一種。ホストに読み込まれるアセンブリ。ホスト アプリケーション (クライアントなど) を自動化するもの。ホスト (サービス) に追加機能を提供するもの。アドインは、カスタム コードであり、通常はサードパーティによって記述されます。コンテキスト (ホストのスタートアップやドキュメントの読み込みなど) に基づいて、ホスト アプリケーションによって動的に読み込まれます。アドインは、ホストのパブリック API (たとえば、オブジェクト モデルやマネージ クラス ライブラリ) を介して、ホスト アプリケーションを拡張します。パブリック API は、SDK を介してアドインに対して利用可能にされたものです。

パイプライン ホストとアドインが、バージョンの変化に柔軟に対応できる、セキュアなプロトコルで通信できるようにするしくみ。

拡張可能ホストを構築する

ホストのオブジェクト モデルの定義が公開された後には、ホストとアドインの対話に 3 つの主要なフェーズがあります。それは、探索、アクティブ化、および有効期間管理です。探索には、利用可能なアドインを見つけて、どのアドインを使用したらよいのかを選択するための情報をホストまたはユーザーに十分に提供することが関係します。アクティブ化には、適切に分離されているセキュリティ サンドボックスに選択したアドインを読み込んで、起動することが関係します。有効期間管理とは、アプリケーションがアドインへの参照を保持している限り、アドインおよびアドインがホストと交換するオブジェクトを有効に保つ一方で、必要なときにアドインをすぐにシャットダウンする能力をホストに与えることを意味します。特に記載していない限り、ここのコードのすべては、Program.cs ファイルの一部です。これは、MSDN®Magazine の Web サイトからダウンロードできる SampleExtensibleCalculator.csproj に含まれています。

実行時のアドイン使用に関する詳細を説明する前に、SDK について、およびホストがアドインに提供するオブジェクト モデルを定義することについて説明しましょう。私たちのアドイン モデルは、さまざまなシナリオをサポートしますが、それらは通常、3 つのカテゴリのいずれかに該当します。

第 1 のカテゴリでは、アドインはホスト アプリケーションに対するサービスを提供します。たとえば、メール サーバーは、ウイルス スキャン、スパム フィルタリング、または IP 保護のためにアドインをホストすることがあります。ワード プロセッサには、スペルチェック用アドインが必要とされる場合があります。Web ブラウザは、ある種のファイルを処理するためにアドインをホストすることがあります。計算機の例は、このシナリオを示したものです。SampleCalculatorAddIn.cs のアドイン コードを見ると、アドインが Add、Subtract、Multiply、および Divide のサービス実装をホストに提供していることがわかります。

第 2 のカテゴリでは、ホストは動作をアドインに提供して、ホストを自動化する方法をアドインに定義させます。このシナリオでは、ホストはアドインに対するサービスを実際に提供します。Microsoft® Office のアドインの大半は、このカテゴリに属します。つまり、アドインを初期化するとすぐに、Office アプリケーションはルート オブジェクトをアドインに渡し、ホストを自動化できるようにします。

このモデルを使用して、ホストはアプリケーションが考えもしていなかった方法で、サードパーティにアプリケーションを拡張させることができます。これまでに、テキスト シンボルをリアルタイムの株価情報に自動的に置換する Microsoft Excel® のアドインや、Voice over IP (VoIP) プログラムをスタートアップして電話番号に電話をするドキュメントに、電話番号への仮想リンクを追加する Microsoft Word のアドインが公開されています。各種アプリケーション全体が、既存の 1 アプリケーションに対する 1 つの巨大なアドインとして構築されていることもあります。クライアント全体のフロントエンドが、Microsoft Outlook® に取り込まれているような、CRM (カスタマ リレーションシップ マネジメント) アプリケーションもいくつかあります。

最後のカテゴリは、ホスト特有の機能を使用するのではなく、ホストを主にスクリーン上での言わば定住先として使用するアドインです。一例としては、検索ツール バーがあります。つまり、このツール バーは、自己をブラウザ、Ｗindows タスクバー、および電子メール クライアントに追加しながらも、ホストにかかわらず同じ機能を提供します。

サンプル アプリケーションでは、サービスとしてアドインを使用するために、ホストは SDK をアドインに提供します。このケースでは、ホストは簡単な計算機機能を実装するためにアドインを使用します。私たちのモデルでは、オブジェクト モデルには 3 とおりの非常に似通った定義があります。私たちは、これらのコンポーネントをいわゆる “パイプライン” で定義しています。つまり、ホストのビュー、アドインのビュー、および分離境界を越えるコントラクトです。パイプラインについては後でさらに詳細に説明することにして、今はホストのビューを見てみましょう。

次のコードは、ホストのコアなコードではなくホストのビューの一部なので、hostCalculatorView.csproj の CalculatorView.cs で見つけることができます。

public abstract class Calculator
{
    public abstract double Add(double a, double b);
    public abstract double Subtract(double a, double b);
    public abstract double Multiply(double a, double b);
    public abstract double Divide(double a, double b);
}

見ればおわかりのように、ホストのビューは、アドインに期待される機能を定義する抽象的な基底クラスの一種にすぎません。ホストは、このビューに対してプログラムを直接作ります。そして、ホストとそのアドインとの間に分離境界があること、およびアドインにホストの非常に異なったビューがあるかもしれないことには、決して気付かないのです。

探索

アプリケーションでアドインを使用する際の最初の手順は、アドインの探索です。アプリケーションには、利用可能なアドインを見つけて、どれを読み込んでアクティブ化したらよいのかを決めるための情報を十分に取得する方法が必要です。一般に、アプリケーションに応じて、ホストはアプリケーションのスタートアップ時、ドキュメントが読み込まれるとき、またはユーザーの要求に従い、この手順を実行します。

ハイレベルな検出要件の一部としては、指定された場所で特定の種類のアドインを見つける能力、ホストまたはユーザーが、最初にアドインをアクティブ化しなくても、どのアドインを使用するのかを自己決定できるほど十分な情報を提供すること、利用可能なアドインのリストを再計算しなくても済むように、賢くキャッシュすること、およびアドインがインストール時にキャッシュを更新できるようにすること、などがあります。これらの目標を達成するために、私たちがとった方法を紹介しましょう。最初に、アドインを探す場所を定義します。このサンプルでは、AddIns とコンポーネントはカレント ディレクトリにインストールされることを想定していますが、アプリケーションがいくつかの別の場所 (たとえば、ユーザーのアプリケーション データ ディレクトリ) を選ぶこともよくあります。

String addInRoot = Environment.CurrentDirectory;

次に、アドイン ストアが新たにインストールされたアドインとパイプライン コンポーネントを認識できるように、アドイン ストアを更新します。AddInStore クラスは、何が何に接続できるのかを決定する際に使用されます。この情報が、いわゆる “パイプライン” を作ります。

ストアには、ホストとそのアドインとの間のパイプラインを作るすべてのコンポーネントが含まれます。AddInStore は、AddInStore.Update API を使用して実行時に更新されることもあれば、コマンドライン ツールを使用してアドインのインストール時に更新されることもあります。その目的は、探索のスタートアップ パフォーマンスを強化することです。次のコードは、新しい AddIns がインストールされたかどうかをチェックします。これが最後に呼び出された後に、新しいアドインやパイプライン コンポーネントがインストールされていない場合は、すぐに呼び出し元に戻されます。

AddInStore.Update(addInRoot);

いったんキャッシュを更新すると、ホストは自己が構築したビュー (抽象的な基底クラス) に接続できるすべての利用可能なアドインを見つけようとします。このコードは、ルート ディレクトリに Calculator AddIns がないかを探し、結果を格納します。

Collection<AddInToken> tokens =
    AddInStore.FindAddIns(typeof(Calculator), addInRoot);

AddInStore.FindAddIns メソッドは、探しているアドインの型、およびアドインが保存されている場所のパスを受け取って、AddInTokens のコレクションを返します。この時点では、アドイン アセンブリはまったく読み込まれておらず、アドイン コードは 1 つも実行されていないことに注意してください。各トークンは、一意のアドインと 4 つのプロパティ (名前、バージョン番号、発行元、および説明) について説明しています。これらは、読み込むアドインをアプリケーションまたはユーザーが決定できるように、アドインによって提供されます。今取り組んでいる計算機のシナリオで、ユーザーにトークンを表示する ChooseCalculator メソッドを構築しました。これにより、ユーザーは希望するものを選択できます。

AddInToken calcToken = ChooseCalculator(tokens);

アクティブ化、分離、セキュリティ、およびサンドボックス化

アプリケーションは、使用するアドインを決めたら、次はそのアドインをアクティブ化する必要があります。ホストは、ホストとしての観点からすると、要求された抽象的な基底クラス (すなわちビュー) を実装するインスタンスを必要とします。しかし、実際には、ホストが必要とするのはこれだけではないのが一般的です。つまり、ホストや他のアドインから分離されたアドインが必要ですし、そのアドインを特定のアクセス許可セットでサンドボックス化できる機能も必要です。

分離されたアドイン アセンブリは、別のアセンブリ内のコードとリソースに直接アクセスできません。.NET Framework では、分離のインプロセス ユニットとして AppDomains を使用します。AppDomains に慣れていない場合、AppDomains を軽量のプロセスとして捉えることができます。一般に、ホストは 1 つの AppDomainでコードを実行します。これは、ランタイムのスタートアップ時に作成される既定の AppDomain であるのが普通です。そして、ホストは各アドインが独自の AppDomain を持つことを望みます。アドインをホストおよびお互いから分離すると、いくつかの重要な利点が得られます。

  • 各アドインは、独自のフォルダに入っています。このフォルダは、独自のアプリケーション ベースを持っており、ホストおよび他のアドインと競合することなく独自のアセンブリを読み込みます。そのため、アドインは、アドイン固有の機能に使用されることがある独自の構成ファイルを持つことができるようにもなります。
  • ホストは、ホスト プロセスで自己や他のアドインに影響を与えることなく、1 つのアドインに発生したエラーをキャッチして、そのアドインをシャットダウン (そして、おそらく再起動も) できます。
  • このアドインとその依存関係は、アンロードできます (詳細については、このコラムの「有効期間管理」で説明します)。
  • アドインを特定のセキュリティのアクセス許可セットに対してサンドボックス化することができます (詳細はすぐ後で説明します)。

しかし、ホストとアドイン自体を開発するときには、ホストとアドインのビューを作る抽象的な基底クラスに対して直接プログラムを作成しているのですから、分離境界の制約について心配する必要はないことに注意してください。このバージョンでは、AppDomain レベルの分離をサポートしており、将来のバージョンではプロセス レベルの分離を追加することも検討しています。アドインと AppDomains の詳細については、「AppDomain's and Addin's」(英語) を参照してください。

ホストは、アドインを読み込んでアクティブ化するセキュリティ コンテキストについて考える必要があります。ホスト アプリケーション自体は、完全な信頼で実行されているかもしれませんが、アドインの方はアクセス許可を下げて実行することを望む場合があります。既に存在するセキュリティと AppDomain クラスの API は、非常に強力なので、AppDomain のサンドボックス化に関してさまざまな設定を自由に調整できます。大半のケースでは、ホストは、FullTrust、イントラネット、またはインターネットのセキュリティ設定のいずれかをアドインに与えて、それが意味する内容をフレームワークに判断してもらうことを望みます。私たちの API を使用すると、これら 3 つの設定のいずれか 1 つを選ぶのは非常に簡単になりますが、必要に応じてセットアップ パラメータを微調整する機能も用意してあります。これらすべてを 1 行のコードにまとめると、次のようになります。このコードは、新しい AppDomain 内で選択された AddInToken をアクティブ化し、アセンブリをインターネット ゾーンにサンドボックス化します。

Calculator calculator =
    calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);

この 1 行のコードは、選択されたアドインのアクティブ化されたインスタンスをホストに返します。それでは、このとき、背後では何が起きているのでしょうか。

AddIn フレームワークは、AppDomain を作成し、適切なアセンブリを AppDomain に読み込みます。次に、その AppDomain で実行されているコードに、指定されたセキュリティ レベル (このケースでは、インターネット アクセス許可が該当します) にふさわしいセキュリティ アクセス許容を与えます。そして、ApplicationBase を実際のアドイン アセンブリのための場所として設定し、依存関係とリソースを見つけられるようにします。次に、AppDomain の構成ファイルをアドインの DLL の隣に位置する構成ファイルとして設定します。最後に、ホストをアドインに接続するのに必要なパイプライン コンポーネントを取り付けます。

カスタムのアクセス許可セットをアドインに指定できるオーバーロードがある一方で、独自の AppDomain を提供できるようにするオーバーロードもあります。その結果、AppDomain とアドインのセキュリティ設定を微調整したり、有効期間とセキュリティ コンテキストが同じアドインをグループ化したり、AppDomain を再利用する際のパフォーマンス上の利点を有効にしたりできます。

図 1 は、これまでカバーした内容を使用するエンドツーエンドのホストを説明したものです。

Figure 1 アドインを探索からアクティブ化までホストする (Close Figure 1)
static void Main()
{
    // In this sample we expect the AddIns and components to
    // be installed in the current directory
    String addInRoot = Environment.CurrentDirectory;

    // Check to see if new AddIns have been installed
    AddInStore.Rebuild(addInRoot);

    // Look for Calculator AddIns in our root directory and
    // store the results
    Collection<AddInToken> tokens =
        AddInStore.FindAddIns(typeof(Calculator), addInRoot);

    // Ask the user which AddIn they would like to use
    AddInToken calcToken = ChooseCalculator(tokens);

    // Activate the selected AddInToken in a new AppDomain set sandboxed
    // in the internet zone
    Calculator calculator =
        calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);

    // Run the read-eval-print loop
    RunCalculator(calculator);
}

有効期間管理

どのアドイン モデルにも不可欠の部分は、オブジェクトの有効期間の内部管理に使用されるメソッドです。COM 有効期間管理は、参照カウントに完全に依存していましたが、CLR 有効期間管理は、ガベージ コレクタに依存しています。CLR リモートでは、スポンサ/タイムアウト アプローチを採用しています。システムは、AppDomains やプロセス境界を越える必要があります。そして、これらのシナリオでは、CLR のガベージ コレクタはもはや適切なサポートを提供できません。したがって、有効期間管理をホストとアドインの開発者に対して完全に透過的にするだけでなく、パイプライン (オブジェクト モデル) 開発者にとってできるだけ簡単にする有効期間管理のシステムを提供する必要があります。来月には、有効期間管理のソリューションを提供し、コラムでさらに詳細に説明します。

ホスト アプリケーションは、長期実行サービスであったり、メモリに制約があったり、または何かの理由で実行時にアドインをアンロードできる必要があったりする場合があります。ランタイム自体は、アセンブリが読み込まれた AppDomain をアンロードするときに、アセンブリのアンロードしか許容できません。つまり、従来の DllUnload は備えていません。これは、AppDomain 境界が値を追加するさらに別の方法です。AppDomain 境界は、プロセスを繰り返さなくても、システム リソースを再利用できるようにします。

ホストがアドインをすぐにシャットダウンする場合、どのアドインがどの AppDomain に属しているのかを追跡することを心配する必要はありません。その役割を果たす、AddInController と呼ばれるクラスを用意してあります。シャットダウンするアドインへの参照を持っている限り、わずか 1 行のコードで追跡することができます。

// Retreive the AddInController for my add-in and then shut it down
AddInController.GetAddInController(calculator).Shutdown();

私たちのモデルでは、アドインをシャットダウンするホストの能力とは別に、ホストとアドインは、自己の有効期間管理があたかもガベージ コレクタによって完全に制御されるかのように振舞うことができます。何かの開放を希望する場合には、参照を空にするか破棄するだけで済みます。実際には、背後では、参照カウント システムとランタイムのリモート サービスを組み合わせて、有効期間を管理しているのです。

アドイン開発

開発者の観点からは、アドイン システムのしくみは隠されています。ホストのオブジェクト モデルの対話、およびアドインが拡張性をホストに提供する手段は、できるだけ透過的である必要があります。私たちのモデルを使用してアドインを書くのは、非常に簡単です。アセンブリへのアドイン アセンブリ エントリを定義して、アドインについて説明的な情報をホストに提供するカスタム属性が採用されています。属性がいったん適用された後は、アドインがしなければならないのは、抽象的な基底クラスで定義されたメソッドを実装することだけです。

アドインのサンプル コード (コード ダウンロードに含まれる SampleCalculatorAddIn.csproj の一部として、SampleCalculatorAddIn.cs にあります) では、アドイン開発者向けのパイプラインのコードは含まれていないことに注意してください。たとえば、リモートと有効期間管理を処理するしくみは、省略されています (図 2)。

Figure 2 簡単な計算機のアドイン (Close Figure 2)
namespace SampleCalculatorAddIn
{
    [AddIn("Sample Calculator AddIn", Version="1.0.0.0")]
    public class SampleCalculatorAddIn : Calculator
    {
        public override double Add(double a, double b)
        {
            return a + b;
        }
        public override double Subtract(double a, double b)
        {
            return a-b;
        }
        public override double Multiply(double a, double b)
        {
            return a * b;
        }
        public override double Divide(double a, double b)
        {
            return a / b;
        }
    }
}

見ればおわかりのように、アドイン モデルには、アドイン開発者へのオーバーヘッドがほとんどまったくありません。ホストに対するアドインについて記述するために開発者が必要とするのは、カスタム属性を使用してから、ホストがアドインに必要とする機能を実装することだけです。このケースでは、計算機の簡単な四則演算だけです。

まとめ

このモデルを設計するにあたって私たちが目標としたことは、バージョンの変化に柔軟に対応できる、分離可能なコンポーネントを動的に構成できるようにすることでした。アドイン開発者の作業が、この目標の達成に関係するしくみによって煩雑になることを確実に回避し、ホストの仕事をできるだけ簡単にすることに、主に注意を払いました。

来月は、パイプライン、およびホストとアドインのバージョンとしてモデルを独立的に使用して互換性を可能にする方法について、さらに詳細に説明します。それまでの間は、私たちのチーム ブログにアクセスして、その他のサンプルをご覧ください。皆様からのフィードバックもお待ちしています。

ご意見やご感想は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Jack Gudenkaufは、Microsoft の CLR チームに所属するソフトウェア アーキテクトです。現在、.NET アーキテクチャの採用に関する問題を確認するため、ISV/VAR/SI のお客様の応対に精力的に当たっています。Jack は Developer Management、MIS Director、ソフトウェア エンジニアリング、およびコンサルティング サービスなどの役職、業務に携わっています。以前は、自分で起業したコンサルティング会社 The Logical Choice を経営していました。

Jesse Kaplanは、Microsoft で CLR チームの Program Manager for Extensibility and Add-Ins を務めています。これまでの経験としては、互換性とマネージ/ネイティブ相互運用性などがあります。


データ ポイント: ADO.NET とカスタム オブジェクトを使用したデータ バインド アプリケーション

データ ポイント
ADO.NET とカスタム オブジェクトを使用したデータ バインド アプリケーション
John Papa

コードのダウンロード : : DataPoints2007_02.exe (174KB)
コンテンツ
テスト - アプリケーションの実行
カスタム エンティティとインターフェイスの実装
ジェネリックと ADO.NET
データ ソース
バインディング コントロール
バインディング コード
データの保存
Predicates を使用したエンティティの検索
まとめ

Windows フォーム バインディング コントロールは、過去に存在していたデータ バインディングに大幅な改善を加えた後継コンポーネントです。これらのコントロールを使用すると、作業を迅速に進め、フォームの作成に関連する冗長なタスクに対処することができるので、フォームの動作を大幅にカスタマイズおよび拡張できます。Windows® フォーム バインディング ツールを使用すると、データは DataSet とカスタム クラス エンティティのようなさまざまなコンテナを経由できるので、これらの種類のオブジェクトすべてに対するバインディングを実行できます。DataSet を使用したくない場合は、アプリケーションで使用するデータを格納するためのカスタム エンティティを作成し、List<T> と他のコレクション型を使用して一連のカスタム エンティティを格納できます。これらの種類のカスタム エンティティは、BindingSource と BindingNavigator を使用して簡単にバインディングできます。このコラムでは、Microsoft® .NET Framework 2.0 の中に存在するバインディング ツールを使用して、ビジネス エンティティから成るカスタム リストをバインディングする方法を説明します。完全に機能するデータ駆動型の Windows フォームを作成して、理解を深めましょう。

最初は、アプリケーションについて説明します。特に、DataGridView、および BindingSource と BindingNavigator の各バインディング コントロールの使用方法です。次に、下位層について順に説明し、それらの構成方法と、データの取得、保持、アクセス、およびデータベースに戻す方法を示します。このサンプル アプリケーションを構成するすべてのコードは、この号のダウンロード ファイルの中に含まれています。

テスト - アプリケーションの実行

このアプリケーションを使用すると、ユーザーはレコードの表示、追加、削除、更新、検索、および移動を実行できます。このアプリケーションは、図 1 に示すように、Northwind の注文データを DataGridView の中に読み込みます。特定の注文を選択した時点で、TextBox、ComboBox、およびフォームの右側にある他のコントロールに対して、選択した注文に関する情報が書き込まれます。これらすべてのコントロールは、BindingSource コントロールを介して同じデータ ソースにバインドされます。

図 1 DataGridView の中で Northwind の Orders (注文) を表示
図 1 DataGridView の中で Northwind の Orders (注文) を表示(画像を縮小するには、ここをクリックします)
図 1 DataGridView の中で Northwind の Orders (注文) を表示
図 1 DataGridView の中で Northwind の Orders (注文) を表示 (画像を拡大するには、ここをクリックします)

BindingNavigator コントロールは、図 1 に示すフォームの上端にある、横長のツールバーです。この中には、画面に表示されている注文レコードを変更するための標準的な移動ボタンがあります。これらの移動ボタンは、左側に表示されているグリッド (表) と連動するので、現在のレコードとの同期が維持されます。このツールバーには、注文情報を追加、削除、および更新するためのイベント ハンドラを実行するボタンもあります。最後に、このアプリケーションを使用すると、特定の注文を検索できます (双眼鏡アイコンに注目してください)。

注文レコードのうち外部キー参照として使用されるフィールドは、ComboBox コントロールを使用して表示できます。たとえば、ComboBox を使用して、営業担当者 (つまり、従業員) から成るリストを表示することもできます。特定の注文に対応する営業担当者は、ComboBox の中で選択されます。これは、営業担当者の Employee ID (社員コード) を表示するより適切なオプションです。アプリケーションのユーザーにとって、コードはほとんど意味を持たない可能性が高いからです。図 1 の ComboBox で、Employee ID の代わりに従業員の名前 ([Employee] ボックス) が表示されていることに注意してください。顧客名 ([Customer] ボックス) も ComboBox の中に表示されています。

カスタム エンティティとインターフェイスの実装

DataSet はデータ アクセス用コンポーネントに属する強力なツールですが、カスタム クラスを使用して、アプリケーション内にあるデータ モデルを管理および表現する手法も強力です。両者の利点と欠点は広く文書化されていますが、多くの開発者は DataSet 派とカスタム クラス派のどちらかにかたくなに従っています。実際は、どちらの手法もエンタープライズ アーキテクチャにとって不可欠です。さらに、ADO.NET ツールは、DataSets とカスタム クラスの両方に連携して、データ オブジェクトを表すエンティティを作成します。結論として、データを格納するために、何らかの種類のデータ ストアが必要です。このアプリケーションでは、カスタム エンティティを使用します。

このサンプル アプリケーションの中には、2 つのプロジェクトがあります。1 つはデータの表示用、もう 1 つはビジネス ロジックとデータ アクセス用です。下位層でカスタム エンティティを作成する場合は、そのエンティティに対応するプロパティを作成する必要があります。たとえば、Customer クラスには CustomerID プロパティと CompanyName プロパティがあります。図 2 に、CustomerID と CompanyName を表すプライベート フィールドとパブリック プロパティを示します。このコードは、入力が面倒に思える可能性があります。特に、DataSet を使用し、プロパティを即座に生成する多数のリファクタリング ツールのうちいくつかを活用する場合は、非常に簡単にクラスを作成できるので、その方法と比較すると、差は顕著です。

Figure 2 Customer クラスのプロパティ (Close Figure 2)
public event PropertyChangedEventHandler PropertyChanged;

private string _CustomerID;
public string CustomerID
{
    get { return _CustomerID; } set { _CustomerID = value; }
}

private string _CompanyName;
public string CompanyName
{
    get { return _CompanyName; }
    set
    {
        if (!value.Equals(_CompanyName))
        {
            _CompanyName = value;
            if (PropertyChanged != null)
            {
                PropertyChanged(this,
                    new PropertyChangedEventArgs("CompanyName"));
            }
        }
    }
}

図 2 のコードを観察してください。この例の Customer クラスは、CompanyName プロパティが変化した場合に PropertyChanged を呼び出すイベントを発生させる、INotifyPropertyChanged インターフェイスを実装します。CompanyName プロパティの set アクセサが、新しい値を設定する前に、値が実際に変化したかどうか確認することに注意してください。変化した場合は、PropertyChanged イベントが発生し、このクラスは、待機しているコンポーネントに対してその変化を通知できます。筆者が作成したアプリケーションでは、BindingSource は、変化が通知されたときに、新しい値を使用して、フォーム内にあるコントロールを自動的に更新します。

このアプリケーションには、3 つのエンティティ クラスも含まれています。Order、Customer、および Employee です。これらすべては INotifyPropertyChanged インターフェイスを実装し、Property アクセサを用意して、プロパティの値の取得と設定を処理します。

ジェネリックと ADO.NET

エンティティを構築した後、これらを取得および保存するためのメソッドを作成する必要があります。このアプリケーションの各エンティティは、次のうち一部またはすべての static メソッドを実装します。これは、標準的なリストと言えます。GetEntity、GetEntityList、SaveEntity、および DeleteEntity です。

これらすべてのエンティティに対応する 1 つのクラスを作成し、データ アクセス メソッドを格納するためにもう 1 つ個別のクラスを作成することもできます。設計上、エンティティの保存と取得を行うメソッドがそれらのエンティティと密接に結合していない場合は、筆者は通常これらのクラスを分離します。このサンプル アプリケーションでは、メソッドは緊密に結合されているので、これらすべてを単一のクラスに収容し、メソッドを static にする方針を選択しました。

図 3 に、GetEntityList<Order> メソッドを示します。これは、Northwind データベースの中にあるすべての注文を表す Order エンティティを返します (もちろん、特定の顧客またはデータ範囲を表すパラメータを追加した場合は、これをフィルタ処理できます)。ジェネリックを使用し、ArrayList の代わりに List<T> を返すことにより、このコードは List の中に含まれているすべてのオブジェクトの型が "T" であることを保証します。これは、オブジェクトをエンティティと同じ型にキャストすることなく、List<T> の中にあるエンティティのプロパティにアクセスできることを意味します。仮に、エンティティがジェネリックではないリストの中に格納されている場合は、そのようなキャストを実行する必要があります。たとえば、次のコードを使用するだけで、リストの中に存在する最初の注文に相当する OrderID を取得できます。

List<Order> orderList = GetMyListOfOrders();
int orderID = orderList[0].OrderID;

Figure 3 List <Order> の取得 (Close Figure 3)
public static List<Order> GetEntityList()
{
    List<Order> OrderList = new List<Order>();

    using (SqlConnection cn = new SqlConnection(Common.ConnectionString))
    {
        string proc = "pr_Order_GetList";
        using (SqlCommand cmd = new SqlCommand(proc, cn))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            cn.Open();
            SqlDataReader rdr = cmd.ExecuteReader(
                CommandBehavior.CloseConnection);
            while (rdr.Read())
            {
                Order order = FillOrder(rdr);
                OrderList.Add(order);
            }
            if (!rdr.IsClosed) rdr.Close();
        }
    }
    return OrderList;
}

仮に、エンティティがジェネリックでないリストの中に格納されている場合は、次のようにオブジェクトを適切な型にキャストする必要があります。

ArrayList orderList = GetMyListOfOrders();
int orderID = ((Order)orderList[0])).OrderID;

DataSet または DataTable を作成し、その中に注文を書き込むことも可能です。ただし、ここでは SqlDataReader を使用する方針を選択しました。この方法でデータにすばやくアクセスする方が好みだからです。図 3 に示すように、SqlDataReader を介してデータを取得し、各行に対してインスタンス化と単一の Order エンティティの書き込みを行い、次にそれらのエンティティを List<Order> に追加し、さらにSqlDataReader の各行に対してこのプロセスを繰り返します。代わりに、DataTable を使用し、DataRows に関して同様に繰り返し処理を行うことも可能です。パフォーマンスの違いは無視できるものですが、この状況では DataTable に伴う余分なオーバーヘッドを発生させることに何の利点もありません。ここでは単純に行を反復処理し、カスタム エンティティ リストの書き込みを行うだけだからです。FillOrder メソッドは次のコードを実行します。これは、Order に対応するインスタンスを作成し、SqlDataReader から取得した値を使用してプロパティを設定します。

Order order = new Order();
order.OrderID = Convert.ToInt32(rdr["OrderID"]);
order.CustomerID = rdr["CustomerID"].ToString();
order.EmployeeID = Convert.ToInt32(rdr["EmployeeID"]);
order.OrderDate = Convert.ToDateTime(rdr["OrderDate"]);
order.ShipVia = rdr["ShipVia"].ToString();
order.ShipName = rdr["ShipName"].ToString();
order.ShipAddress = rdr["ShipAddress"].ToString();
order.ShipCity = rdr["ShipCity"].ToString();
order.ShipCountry = rdr["ShipCountry"].ToString();
return order;

図 3 では、CommandBehavior.CloseConnection を ExecuteReader メソッドに渡しました。この結果、SqlDataReader が閉じた直後に、SqlConnection オブジェクトも閉じます。

エンティティの中には、データの挿入、更新、および削除を行うための static メソッドもあります。SaveEntity という public static メソッドを作成しました。これは単一のエンティティを受け入れ、それが新規か既存か判断し、アクション クエリを実行する適切なストアド プロシージャを呼び出します。Customer クラスに対応する static AddEntity (図 4 に表示) はその Order エンティティを受け入れ、そのエンティティから取得した値をストアド プロシージャの対応するパラメータにマップします。

Figure 4 エンティティの追加 (Close Figure 4)
private static Order AddEntity(Order order)
{
    int orderID = 0;
    using (SqlConnection cn = new SqlConnection(Common.ConnectionString))
    {
        string proc = "pr_Order_Add";
        using (SqlCommand cmd = new SqlCommand(proc, cn))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@customerID", order.CustomerID);
            cmd.Parameters.AddWithValue("@employeeID", order.EmployeeID);
            cmd.Parameters.AddWithValue("@orderDate", order.OrderDate);
            cmd.Parameters.AddWithValue("@shipVia", order.ShipVia);
            cmd.Parameters.AddWithValue("@shipName",
                GetValue(order.ShipName));
            cmd.Parameters.AddWithValue("@shipAddress",
                GetValue(order.ShipAddress));
            cmd.Parameters.AddWithValue("@shipCity",
                GetValue(order.ShipCity));
            cmd.Parameters.AddWithValue("@shipCountry",
                GetValue(order.ShipCountry));
            cmd.Parameters.Add(new SqlParameter("@orderID",
                SqlDbType.Int));
            cmd.Parameters["@orderID"].Direction =
                ParameterDirection.Output;
            cn.Open();
            cmd.ExecuteNonQuery();
            orderID = Convert.ToInt32(cmd.Parameters["@orderID"].Value);
        }
        order = GetEntity(orderID);
    }
    return order;
}

ADO.NET とカスタム エンティティは両方とも、このアーキテクチャの重要な部分です。取得用メソッドは ADO.NET を使用してデータベースからデータを取得します。次にカスタム エンティティへの書き込みが行われ、プレゼンテーション層に返されます。SaveEntity と DeleteEntity の各メソッドはカスタム エンティティを受け入れ、それらから値を取得し、変更結果をデータベースに適用します。

データ ソース

ここまでで、カスタム エンティティを作成し、書き込み、返す方法を説明しました。次に、プレゼンテーション層について順に説明します。クラス ライブラリ プロジェクトの中には、Customer、Order、Employee の各クラスがあるので、これらを使用してフォームの中でバインディング コントロールを作成できます。[Data Sources] ウィンドウでは、このプロジェクトから利用可能なすべてのデータ ソースが表示されます。図 5 からは、クラス ライブラリ プロジェクトの中で作成した 3 つのエンティティを、筆者が UI プロジェクトの [Data Sources] ウィンドウに追加したことがわかります。

図 5 [Data Source] ウィンドウ
図 5 [Data Source] ウィンドウ

データ ソースは、Web サービス、オブジェクト、またはデータベースから取得できます。この例では、オブジェクト データ ソースを追加しました。現在はクラス エンティティを使用しているからです。ウィザードを使用してデータ ソースを追加しましたが、このとき、データ ソースとして追加する名前空間とクラスを選択する必要があります。既にクラス ライブラリ プロジェクトを参照している場合は、Customer、Employee、Order の各クラスが表示されます。

データ ソースを追加した後、そのデータ ソースは [Data Sources] ウィンドウに表示されます。図 5 の Order データ ソースは、Order クラスのパブリック プロパティを示しています。各プロパティ名の隣には、そのプロパティの値を表示するために使用されるコントロールの種類を表すアイコンがあります。OrderDate プロパティに対して DateTimePicker コントロール、ShipAddress に対して TextBox コントロールが表示されています。[Data Sources] ウィンドウの中で、プロパティ名を表すテキストをクリックし、ドロップダウン リストから他のコントロールを選択すると、これらのコントロールを変更できます。たとえば、OrderID プロパティに対応するコントロールを Label コントロールに変更して、読み取り専用にできます。

筆者は、Order.CustomerID プロパティなど、他のエンティティに対する参照フィールドであるプロパティを直接表示したいとは思いません。代わりに、それらのプロパティに関して、よりわかりやすい値を表示することを希望します。たとえば、CustomerID プロパティに対応するコントロールの種類を ComboBox に変更しました。これは、顧客の全データをリストに書き込み、該当する顧客の CompanyName プロパティを表示することを意図しています。また、CustomerID と EmployeeID の各プロパティも ComboBox に変更しました。フォームの中で特定のプロパティを表示したくない場合は、リストからコントロールの種類の代わりに [None] オプションを選択します。

プロパティとコントロールを設定した後で、それらを表示してみましょう。[Data Sources] ウィンドウの [Order] エントリをクリックし、リストから [Details] (詳細) を選択します。この結果、[Order] データ ソースをフォームにドラッグすることができます。フォームに対して、表示コントロールとバインディング コントロール (BindingSource および BindingNavigator) の両方が自動的に生成されます。この方法で、図 1 のフォームのうち右側にあるコントロールを作成しました。

バインディング コントロール

BindingSource は、非表示のコントロールであり、Order エンティティと、フォーム上のコントロールにあるリンクです。BindingSource の中にある現在の Order エンティティは、画面の右側にあるコントロールの中で表示され、それらのコントロールは表示または編集可能です。このアプリケーションでは、Order エンティティを List<Order> エンティティにバインドします。そして後者を、Order クラスの static メソッドである GetEntityList に渡します。

BindingNavigator コントロールも、Order データ ソースをフォームにドラッグする方法で作成したものです。このコントロールは、フォームの上端にあるツールバーとして表示されます。既定では、その中にレコードの追加、保存、および削除を行うためのボタンと、移動ボタンがあります。BindingNavigator は BindingSource コントロールと同期しているので、ユーザーがどちらかのコントロールを使用してレコードの位置を変更した場合は、もう一方のコントロールはその位置変更を自動的に反映します。たとえば、ユーザーが BindingNavigator コントロールの [Next] (次へ) ボタンをクリックした場合は、BindingSource コントロールは現在のエンティティから次のエンティティへの切り替えを行い、続いて、右側にあるコントロールは、変更後の現在のエンティティに相当するプロパティの値を表示します。

BindingNavigator のボタンもコントロールなので、その中にはプロパティとイベントがあり、開発者はそれらを設定することができます。このツールバーに対して、[Save] と [Delete] の各ボタンに対応するイベント ハンドラを追加しました。さらに、検証や他の作業を行うロジックを追加することも可能です。既に作成した、エンティティの static データ アクセス メソッドを使用して変更後のデータをデータベースに書き込む前に、これらのロジックを実行することを想定しています。

BindingNavigator コントロールはレコードの移動を処理し、データの変更結果を保存するための簡単な方法を提供しますが、必ずしもこれを使用しなくてもかまいません。データ ソースをフォームにドラッグしてこのコントロールが自動的に作成された後、開発者は問題なくこのコントロールをフォームから削除することもできます。もちろん、その場合は注文リストの中で実行される移動や変更結果の保存を実装する独自のコードを作成する必要があります。いずれにせよ、UI の要件に基づいて決定を下すことになります。

CustomerID に対応する ComboBox は現在、(BindingSource コントロールを介して) Order データ ソースの CustomerID プロパティにバインドされています。現在、顧客リストをこの ComboBox に書き込み、顧客の CompanyName を表示する必要があります。クラス ライブラリ プロジェクトを作成したときに、Customer エンティティを作成し、そこから GetEntityList<Customer> メソッドを公開しました。その後、Customer エンティティに対応するデータ ソースを追加しました。実行する必要があるのは、Customer データ ソースを CustomerID ComboBox までドラッグし、そこにドロップすることです。この結果、2 番目の BindingSource コントロールが作成されます。この ComboBox は、この新しい BindingSource を使用して、表示を行う目的で顧客リストを読み込みます。したがって、ComboBox のうち選択された値は、Order に対応する BindingSource コントロールと顧客リストにバインドされたままの状態で、顧客の CompanyName プロパティがコントロールの中で表示されます。Employee ComboBox に対応するリストへの書き込みを行う目的で、このプロセスが繰り返されます。データ ソースを ComboBox にドラッグ アンド ドロップしたので、もう 1 つの BindingNavigator コントロールが必要ないことが理解できます。

ただし、BindingNavigator に対応するツールバー以外に、注文レコードを移動する 2 番目の方法をユーザーに提供したいと考えました。そのため、[Data Sources] ウィンドウに戻り、[Order] データ ソースをクリックし、DataGridView の選択肢を変更し、[Order] データ ソースをフォームにドラッグしました。この結果、フォームの中に DataGridView コントロールが作成されました。その中にある各列では、Order エンティティに対応する各プロパティが表示されています。さらに、それらの列のほとんどを削除しました。グリッドの中には、キー情報だけが表示されています。追加のバインド コントロールは作成されません。このフォームの中には、Order データ ソースへのバインドを行う BindingSource コントロールが既に存在しているからです。この時点で、各コントロールに対応している現在の注文は、いずれも同じ注文になっています。これらすべてのコントロールは、Order の BindingSource コントロールにリンクされているからです。

バインディング コード

ここまでで一連のコントロールを設計しましたが、フォームに関して何もコードを作成していません。顧客、従業員、および注文に対応する BindingSource コントロールはいずれも作成済みで準備が完了していますが、まだそれぞれにデータを渡していません。この作業は簡単に実行できます。最初に、各エントリの static メソッドである GetEntityList を呼び出し、次に各エンティティの List<T> を取得します。その後、List<T> を BindingList<T> に変換し、各 BindingSource コントロールに対応するデータ ソースとして設定します。

図 6 に、3 つの BindingSource コントロールのそれぞれに対してリストを設定する方法を示します。UI をとおしてユーザーがデータの移動と表示をできるようにするには、このコードだけで十分です。フォームのコンストラクタの中で SetupBindings メソッドを呼び出すので、フォームを最初に読み込むときに、データの取得、バインド、および表示が実行されます。その後、ユーザーは BindingNavigator ツールバーの移動ボタンを使用するか、DataGridView コントロールの中にある行を選択する方法により、レコード間で移動できます。ただし、ユーザーが変更を加えられるように、イベント ハンドラを作成する作業が残っています。

Figure 6 BindingSource コントロールに対するデータの設定 (Close Figure 6)
private void SetupBindings()
{
    BindingList<Order> orderList =
        new BindingList<Order>(Order.GetEntityList());
    orderBindingSource.DataSource = orderList;

    BindingList<Customer> customerList =
        new BindingList<Customer>(Customer.GetEntityList());
    customerBindingSource.DataSource = customerList;

    BindingList<Employee> empList =
        new BindingList<Employee>(Employee.GetEntityList());
    employeeBindingSource.DataSource = empList;
}

データの保存

ユーザーがツールバーの [Delete] ボタンをクリックしたときに、現在選択および表示されている注文を削除できるように、このアプリケーションを変更しましょう。最初に、[Delete] ボタンの DeleteItem プロパティを [None] に設定し、カスタム コードを使用して削除を行うように設定します。次に、削除を実行するイベント ハンドラを追加します。これは、図 7 に示す Delete というプライベート メソッドを呼び出します。

Figure 7 現在の注文の削除 (Close Figure 7)
private void Delete()
{
    Order order = orderBindingSource.Current as Order;
    int orderID = order.OrderID;
    DialogResult dlg = MessageBox.Show(
        string.Format("Are you sure you want to delete Order # {0}?",
        orderID.ToString()));
    if (dlg == System.Windows.Forms.DialogResult.OK)
    {
        Order.DeleteEntity(order);
        orderBindingSource.RemoveCurrent();
        MessageBox.Show(string.Format(
            "Order # {0} was deleted.", orderID.ToString()));
    }
}

Delete メソッドは、BindingSource の Current プロパティから現在の注文を取得し、その値をエンティティと同じ Order 型にキャストします。次に、その注文を削除してよいかどうかユーザーにたずねます。ユーザーが [OK] をクリックした場合は、クラス ライブラリ プロジェクトの中にある static メソッド DeleteEntity に対して Order エンティティを渡します。最後に、BindingSource コントロールの RemoveCurrent メソッドを実行することにより、その注文を BindingSource から削除します。

BindingNavigator ツールバーには、[Add] ボタンもあります。これは DataGridView に新しい行を追加し、フォームの右側にある各コントロールをクリアします。その後、ユーザーは新しい注文に対応する値を入力および選択し、BindingNavigator ツールバーの [Save] ボタンをクリックします。この [Save] ボタンに、イベント ハンドラを追加しました。これは、Order エンティティの static メソッド SaveEntity に、ユーザーが入力した注文エンティティを渡します。他に、ユーザーは現在の注文レコードを編集し、上記と同じ [Save] ボタンをクリックして編集後の注文エンティティを Order エンティティの static メソッド SaveEntity に渡すこともできます。この SaveEntity メソッドは、Order エンティティの OrderID プロパティの値を確認することにより、挿入と更新の両方に対処します。エンティティが挿入または更新された後、注文リストをもう一度取得し、BindingSource のデータ ソースを再設定してデータを最新の状態に保つことにより、DataGridView を再読み込みします。

Predicates を使用したエンティティの検索

わかりやすいユーザー インターフェイスを作成することは重要です。ユーザーは Order ID がわかっている場合は、それに該当する特定の注文に移動したいと考えることがあります。BindingNavigator に ToolStripTextBox コントロールを追加する方法で、この機能を実現します。ユーザーはこの中で、検索する OrderID の全体または一部を入力できます。次に、ツールバーに新しいボタンを追加し、注文レコードの検索を実行するイベント ハンドラを追加します。図 1 に、これらのコントロール、およびユーザーが Order ID に基づいて注文を見つけるのに役立つツールチップのテキストを示します。

Select メソッドまたは Find メソッドを使用して、DataTable の中で DataRow を見つけるのは簡単です。ただし、この例ではカスタム エンティティを使用しました。これは、上記のような機能を使うことをあきらめたことを意味しているわけではありません。この例では、List<Order> の中から、ユーザーが検索コントロールの中に入力した値で始まる Order エンティティを検索する必要があります。List<T> は、リストの中にある 1 つ以上の項目を検索するのに役立ついくつかのメソッドを公開しています。Find、FindAll、FindIndex、FindLast、および FindLastIndex メソッドです。ここでは Find メソッドを使用します。これは、単一の引数である Predicate<T> を受け入れます。

Predicate<T> は、Predicate<Order> であることが必要です。この例では List<Order> を使用するからです。Predicate はデリゲートであり、List<Order> の中で、定義した条件 (検索フィールドに入力されたのと同じ値で始まる) を満たす Order を検索します。この Predicate を作成する前に、最初に検索を支援するクラスを作成しました (図 8 に示します)。OrderFilter クラスはコンストラクタをとおして、検索する値を受け入れます。このクラスには、特定のエンティティを検索するための 2 つのメソッドもあります。これらのメソッドはどちらも、一致するものが見つかったかどうかを示すブール値を返します。これらのメソッドは、Predicate に渡すデリゲートの基本をなします。

Figure 8 OrderFilter クラス (Close Figure 8)
private class OrderFilter
{
    private int orderID = 0;

    public OrderFilter(int orderID)
    {
        this.orderID = orderID;
    }

    public bool MatchesOrderID(Order order)
    {
        return order.OrderID == orderID;
    }

    public bool BeginsWithOrderID(Order order)
    {
        return order.OrderID.ToString().StartsWith(orderID.ToString());
    }
}

図 9 に、注文を検索し、BindingSource の位置変更を行うコードを示します。最初に、Order エンティティから成るリストの参照を BindingSource から取得し、コードを読みやすくします。次に、OrderFilter クラスのインスタンスを作成し、検索対象の order ID を使用してそのインスタンスを初期化します。さらに、Predicate を作成し、この Predicate を OrderFilter クラスの BeginsWithOrderID メソッドに渡します。最後に、List<Order> の Find メソッドを実行し、このメソッドに対し、作成したばかりの Predicate を渡します。続いて、Order エンティティから成るリストに対して繰り返し処理を行い、それらすべてを OrderFilter.BeginsWithOrderID メソッドに渡します。最初に true を返すエンティティがこのメソッドによって返され、BindingSource は、そのエンティティに対応するインデックス位置に移動されます。

Figure 9 注文の検索 (Close Figure 9)
private void toolBtnFindOrderNumber_Click(object sender, EventArgs e)
{
    List<Order> orderList = new List<Order>(
        orderBindingSource.DataSource as BindingList<Order>);
    OrderFilter orderFilter = new OrderFilter(
        Convert.ToInt32(toolTxtFindOrderNumber.Text));
    Predicate<Order> filterByOrderID =
        new Predicate<Order>(orderFilter.BeginsWithOrderID);
    Order order = orderList.Find(filterByOrderID);
    if (order == null)
        MessageBox.Show("No matching Order found",
                         "Not Found", MessageBoxButtons.OK);
    else
    {
        int index = orderBindingSource.IndexOf(order);
        orderBindingSource.Position = index;
    }
}

まとめ

このコラムでは、.NET に含まれる Windows フォーム バインディング コントロールの能力を紹介しました。アーキテクチャの中にある既存のエンティティまたは DataSet に対してこれらのバインディング コントロールを使用し、バインドされたフォームをより迅速に作成することもできます。これらのコントロールに特定の機能が欠けていると感じた場合は、フォームをカスタマイズして必要な機能を得ることができます。カスタム エンティティと DataSet のどちらを好む場合でも、データ バインディングを使用して、.NET エンタープライズ アプリケーションの中に両方のツールを実装することができます。

ご意見やご感想は、

John mmdata@microsoft.com.

まで英語でお送りください。
John Papa は、ASPSOFT (aspsoft.com) で働く上級 .NET コンサルタントです。野球を熱狂的に愛し、夏の夜のほとんどを家族および忠犬 Kadi (カーディ) と共にヤンキースのために費やします。John は C# MVP の 1 人であり、ADO、XML、および SQL Server に関する複数の著作があります。また、VSLive のような業界のカンファレンスで講演をすることが多く、codebetter.com/blogs/john.papa (英語) でブログを公開しています。


Office Space: Office オープン XML ファイルを構築する

Office Space
Office オープン XML ファイルを構築する
Ted Pattison

コードのダウンロード : : OfficeSpace2007_02.exe (338KB)
コンテンツ
.docx ファイルを生成する
リレーションシップの詳細
Package Viewer サンプル アプリケーション
Word 2007 の新しい "開発" 機能
Word 2007 でデータとその表現を分離する
XML データ ストアをプログラムで更新する
まとめ

Office Space へようこそ。Office Space のコラムでは、Microsoft® Office system を使用した開発について詳しく説明します。このコラムでは、Microsoft Office system アプリケーションとそのファイル形式の拡張およびカスタマイズの方法について説明します。マイクロソフトは、2007 Microsoft Office system の Word、Excel®、および PowerPoint® で、Office オープン XML ファイル形式の仕様に基づく新しいファイル形式を導入しました。この新しい形式で保存された Office ドキュメントは、パッケージと呼ばれる ZIP アーカイブ内で構造化されます。パッケージ内では、実際のコンテンツがパーツと呼ばれるコンポーネントに保存されます。パーツは通常、内部 XML ドキュメントとして保存され、そのコンテンツは公開されている XML スキーマに基づいて構造化されます。

2006 年 11 月号のコラム「Basic Instincts」(英語) では、Office オープン XML ファイル形式の基本概念を紹介しました。また、Microsoft .NET Framework 3.0 に付属の新しいパッケージ API を使用する方法についても説明しました。今月号のコラムでは、これらのトピックを発展させます。したがって、Office オープン XML ファイル形式を初めて使用する場合は、まず、11 月号の私のコラムを参照してください。

.docx ファイルを生成する

11 月号のコラム「Basic Instincts」では、.docx ファイルを初めて生成するために必要なコードの記述について説明しました。基本的な手順を確認するために、必要な基本コードの骨組みを示します (図 1 を参照)。まず、.NET Framework 3.0 に付属の WindowsBase アセンブリによって公開される Package クラスを使用して、新しいパッケージ ファイルを作成します。次に、パッケージ内にパーツを作成し、必要なコンテンツをパーツ内に書き込みます。簡単な .docx ファイルの場合は、/word/document.xml の URI (Uniform Resource Identifier) パスを使用してパーツを作成し、WordprocessingML コンテンツをこのパーツに書き込むだけです。ここでは、古典的な "Hello World" という Word 文書を作成します。

Figure 1 基本的な .docx ファイルを生成する (Close Figure 1)
Dim pack As Package = Nothing
pack = Package.Open("basic.docx", FileMode.Create, FileAccess.ReadWrite)

'*** create main document part (document.xml) ...
Dim uri As Uri = New Uri("/word/document.xml", UriKind.Relative)
Dim partContentType As String
partContentType = "application/vnd.openxmlformats" & _
                  "-officedocument.wordprocessingml.document.main+xml"
Dim part As PackagePart = pack.CreatePart(uri, partContentType)

'*** get stream for document.xml
Dim streamPart As New StreamWriter( _
    part.GetStream(FileMode.Create, FileAccess.Write))

Dim xmlPart As XmlDocument = New XmlDocument()
'*** code to generate 'Hello World' WordProcessingML omitted for clarity
xmlPart.Save(streamPart)

streamPart.Close()
pack.Flush()

'*** create the relationship part
Dim relationshipType As String
relationshipType = "http://schemas.openxmlformats.org" & _
                   "/officeDocument/2006/relationships/officeDocument"
pack.CreateRelationship(uri, TargetMode.Internal, _
                        relationshipType, "rId1")
pack.Flush()

'*** close package
pack.Close()

パッケージ内に新しいパーツを作成するときは、必ずコンテンツ タイプを指定する必要があります。図 1 のコードには、CreatePart メソッド呼び出しの 2 番目のパラメータとして、/word/document.xml パーツに必要なコンテンツ タイプが含まれています。このメソッドを呼び出してコンテンツ タイプを渡すと、パッケージ API では必要なエントリをパッケージ ファイルのルートにある [Content_Types].xml のコンテンツ タイプ アイテムにすべて追加します。

図 1 で最後に注目すべき点は、CreateRelationship メソッドの呼び出しです。これはパッケージと /word/document.xml パーツ間の親子関係を設定する重要なコードです。次のセクションで説明するように、リレーションシップの作成は重要です。リレーションシップを適切に設定しないと、Word では文書の読み込みと表示ができません。

リレーションシップの詳細

Office オープン XML ファイル形式のパッケージの構造は、リレーションシップに大きく依存します。既に説明したように、パーツの作成後、リレーションシップでパーツをパッケージに関連付けなかった場合は、Word などのコンシューマ アプリケーションではパーツが認識されません。すべてのパーツには、それらを含むパッケージとの関連付けを表すリレーションシップが必要なためです。

リレーションシップには 2 つの種類があります。1 つは、パッケージとその最上位レベルのパーツ間の関連付けを定義するためのパッケージのリレーションシップです。図 2 では、Microsoft Office Word 2007 で作成された .docx ファイルの最上位レベルの一般的なパーツは /docProps/app.xml、/docProps/core.xml、および /word/document.xml であることを示します。もう 1 つは、同じパッケージ内の 2 つのパーツ間の親子関係を定義するためのパーツのリレーションシップです。/word/document.xml パーツは通常、/word/settings.xml や /word/styles.xml などのいくつかの異なる子パーツに関連付けられています。

図 2 パッケージのコンテンツ
図 2 パッケージのコンテンツ

Office オープン XML ファイル形式の仕様では、パッケージ内のすべてのパーツは直接または間接的にそのパッケージと関連付けられている必要があります。/word/document.xml などのパーツは、パッケージのリレーションシップによってそのパッケージに直接関連付けられます。/word/styles.xml などのパーツは、パッケージに関連付けられている最上位レベルの /word/document.xml パーツに関連付けられることで、間接的にそのパッケージと関連付けられます。

コンシューマ アプリケーションでは、リレーションシップを列挙して、パッケージ内のパーツをすべて検出しなければならないことを理解することが非常に重要です。Word や Excel などの他のアプリケーションで作成されたパッケージを読み取る独自のアプリケーションを記述する場合は、リレーションシップを列挙して既存のパーツを検出するように設定することをお勧めします。

それでは例を見てみましょう。既存の .docx ファイルを開くコードを記述して、/word/document.xml パーツの XML コンテンツをコンソール ウィンドウに出力するとします。これを行うには、ハードコードされた URI を使用すると、簡単にこのパーツにアクセスできます。

Dim pack As Package = Package.Open("c:\Data\Hello.docx", _
                                   FileMode.Open, FileAccess.Read)
Dim partUri As Uri = New Uri("/word/document.xml", UriKind.Relative)
Dim part As PackagePart = pack.GetPart(partUri)

'** now add code to program against part

このコードでは、/word/document.xml パーツへのアクセスを取得し、ストリームを開いて、そのコンテンツを読み取ります。では、このパーツの取得を別の方法で行ってみましょう。今度はパッケージのリレーションシップを列挙し、特定のリレーションシップ タイプを検索します。ここでは、パッケージに関連付けられたパーツの中で、Word 文書のメイン パーツに使用されるリレーションシップ タイプを持つパーツを検索します。

Dim pack As Package = Package.Open("c:\Data\Hello.docx", _
    FileMode.Open, FileAccess.Read)

Dim relationshipType As String = _
    "http://schemas.openxmlformats.org" & _
    "/officeDocument/2006/relationships/officeDocument"

Dim rel As PackageRelationship, partUri As Uri

For Each rel In pack.GetRelationshipsByType(relationshipType)
    partUri = PackUriHelper.ResolvePartUri( _
        rel.SourceUri, rel.TargetUri)
    Dim part As PackagePart = pack.GetPart(partUri)
    '** now add code to program against part
Next

ご覧のように、このコードでは Package オブジェクトの GetRelationshipsByType メソッドを呼び出し、ターゲット リレーションシップ タイプを表す文字列を渡します。この方法を使用すると、特定の種類に基づいてパッケージのリレーションシップを列挙できます。コードで示すように、officeDocument というリレーションシップ タイプのパーツを検出する場合は、パッケージ内にこの種類のパーツが 1 つだけ存在している必要があります。

適切な PackageRelationship オブジェクトが見つかったら、コードでは .NET Framework パッケージ API に用意されている PackUriHelper クラスを使用して、ターゲット パーツの URI を動的に構築します。次に、ResolvePartUri メソッドを呼び出し、現在の PackageRelationship オブジェクトの SourceUri と TargetUri を渡して、必要な URI オブジェクトを取得できます。ターゲット パーツの URI を動的に構築したら、GetPart メソッドを呼び出します。

Package オブジェクトは破棄可能なオブジェクトであるため、それに応じた管理を行います。したがって、パッケージのアイテムにアクセスするコードは、次のように Using ステートメント内で構築します。

    Using pack As Package = _
    Package.Open("c:\Data\Hello.docx", _
    FileMode.Open, FileAccess.Read)
    '*** code to access package goes here
End Using

個々のコードについて説明したので、これらのコードをどのように組み合わせるかを見てみましょう。図 3 では、今月号のコラムに付属の DocumentReader というサンプル コンソール アプリケーションからのコードを示します。このコードでは、パッケージを開き、ターゲット リレーションシップ タイプを使用して、ターゲット パーツを検出します。次に、このコードでは URI を構築し、ターゲット パーツを開いて、コンテンツへのストリームベースのアクセスを取得します。このコンソール アプリケーションを実行すると、図 4 に示された結果が出力されます。

Figure 3 ターゲット パーツを検出する (Close Figure 3)
Dim relType As String
Dim rel As PackageRelationship
Dim partUri As Uri

'*** define target relationship type
relType = "http://schemas.openxmlformats.org" & _
    "/officeDocument/2006/relationships/officeDocument"

Using pack As Package = Package.Open("minimal.docx", _
    FileMode.Open, FileAccess.Read)

    For Each rel In pack.GetRelationshipsByType(relType)
        partUri = PackUriHelper.ResolvePartUri( _
            rel.SourceUri, rel.TargetUri)
        Dim part As PackagePart = pack.GetPart(partUri)
        Dim partStream As Stream, partReader As StreamReader
        partStream = part.GetStream(FileMode.Open, FileAccess.Read)
        partReader = New StreamReader(partStream)
        '*** print contents of part to console window
        Console.WriteLine(partReader.ReadToEnd)
        '*** close stream
        partStream.Close()
    Next

End Using
図 4 コンソール アプリケーションの出力
図 4 コンソール アプリケーションの出力(画像を縮小するには、ここをクリックします)
図 4 コンソール アプリケーションの出力
図 4 コンソール アプリケーションの出力 (画像を拡大するには、ここをクリックします)

Package Viewer サンプル アプリケーション

ここまでは、リレーションシップを使用してパッケージ内のパーツを検出し、そのパーツにアクセスする方法について説明しました。ここでは、この概念について少し深く考察してみましょう。既に説明したように、Office オープン XML ファイル形式の仕様では、パッケージ内のすべてのパーツはリレーションシップによって検出できなければなりません。そのため、パッケージを調べ、含まれるパーツをすべて表示するアプリケーションも記述できます。

今月号のコラムには、2 番目のサンプル アプリケーションである Package Viewer が付属しています。これは Windows® フォームベースのアプリケーションで、リレーションシップ階層を使用して、パッケージとそのパーツを示すノードを持つツリー ビュー コントロールを表示します。図 5 に、実行中の Package Viewer アプリケーションを示します。このアプリケーションでは、ツリー ビューのノードをクリックすると、パッケージと特定のパーツに関する詳細情報が表示されることがわかります。各パーツには、コンテンツ タイプ、親、および親に関連付けられているリレーションシップ タイプが表示されます。

図 5 パッケージのパーツを調べるための Package Viewer
図 5 パッケージのパーツを調べるための Package Viewer(画像を縮小するには、ここをクリックします)
図 5 パッケージのパーツを調べるための Package Viewer
図 5 パッケージのパーツを調べるための Package Viewer (画像を拡大するには、ここをクリックします)

Package Viewer では、標準の [ファイルを開く] ダイアログ ボックスを使用して、パッケージ ファイルを選択できます。パッケージ ファイルを選択したら、アプリケーションではこのパッケージのリレーションシップがすべて列挙され、最上位レベルのパーツが検出されます。図 6 にこのコードの基本構造を示します。ただし、TreeNode オブジェクトを持つ Windows ツリー ビュー コントロールを表示する処理の一部は省略されています。

Figure 6 ツリー ビューを表示する (Close Figure 6)
Dim rootNode As TreeNode
rootNode = New TreeNode(PackageName, 0, 0)

For Each rel As PackageRelationship _
    In CurrentPackage.GetRelationships()

    Dim PartUri As Uri = PackUriHelper.ResolvePartUri( _
        rel.SourceUri, rel.TargetUri)
    Dim part As PackagePart = CurrentPackage.GetPart(PartUri)
    Dim topLevelPartNode As TreeNode = New TreeNode( _
        part.Uri.OriginalString, 1, 1)

    '*** call helper method to discover child controls
    PopulateChildPartNode(part, topLevelPartNode)

    '*** add node to
    rootNode.Nodes.Add(topLevelPartNode)
Next

treePackageContents.Nodes.Add(rootNode)

Package クラスの GetRelationships メソッドを呼び出し、アプリケーションでパッケージ内の最上位レベルのパーツをすべて列挙します。For Each ループ内の繰り返し処理では、最上位レベルの特定のパーツごとに TreeNode オブジェクトが作成され、PopulateChildPartNode というヘルパ メソッドが呼び出されます。このヘルパ メソッドでは、関連付けられているすべての子パーツを検出する処理を行い、その子パーツに TreeNode オブジェクトを作成します。次に、PopulateChildPartNode メソッド内のコードを調べてみましょう (図 7を参照)。

Figure 7 PopulateChildPartNode メソッド (Close Figure 7)
Private Sub PopulateChildPartNode( _
    ByVal part As PackagePart, ByVal partNode As TreeNode)

    For Each rel As PackageRelationship In part.GetRelationships()

        Dim ChildPartUri As Uri = _
            PackUriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri)
        Dim ChildPart As PackagePart = part.Package.GetPart(ChildPartUri)

        Dim ChildPartNode As TreeNode = _
            New TreeNode(rel.TargetUri.OriginalString, 1, 1)

    '** use recursion
        PopulateChildPartNode(ChildPart, childPartNode)

        partNode.Nodes.Add(ChildPartNode)

    Next

End Sub

各 PackagePart オブジェクトによって GetRelationships メソッドが公開されることがわかります。これで最上位レベルの各パーツの子パーツが列挙されます。ただし、子パーツに子パーツが含まれ、さらにその子パーツに子パーツが含まれるように、パーツのリレーションシップ階層ではレベル数に制限はありません。したがって、このアプリケーションの設計では、PopulateChildPartNode が自分自身を呼び出す再帰が使用されています。これによって、現在のパッケージ内で検出されるすべてのパーツをツリー ビュー コントロールで表示するには、必要なレベルをすべてクロールするメソッドを 1 つ記述するだけで済みます。

Package Viewer アプリケーションのコードを調べると、各ノードが表すエンティティのさまざまな属性を追跡するために、ユーザー定義構造体および各 TreeNode オブジェクトの Tag プロパティが使用されていることがわかります。たとえば、各ノードがパッケージ、パーツ、外部リレーションシップのいずれを表すかを追跡するための情報が含まれています。つまり、パーツを表すノードには、パーツのコンテンツ タイプ、親、および親に関連付けられているリレーションシップ タイプを追跡するための情報が含まれています。ツリーのノードをクリックすると、アプリケーションのメイン フォームの右側のコントロールにこれらの情報が表示されます。

また、Package Viewer は、パッケージに含まれる XML ベースのパーツのコンテンツを表示する機能も備えています。これを行うには、パッケージを再度開き、ターゲット パーツへのストリームベースのアクセスを取得します。次に、パーツの XML ベースのコンテンツが一時ファイルに書き込まれます。最後に、Windows フォームの WebBrowser コントロールにこのファイルを読み込むと、XML コンテンツが色分けコーディングされて、展開可能なセクションに表示されます。このサンプル アプリケーションが、Office オープン XML 形式のドキュメントで作業する際に理解しておく必要があるパーツおよび XML の説明に役立つことを願っています。

ここまでは、Office オープン XML ファイル形式を使用した Word、Excel、および PowerPoint のドキュメントの生成に必要なタスクの概念を少し説明しました。理論的には、これらのタスクは単純です。つまり、必要なタスクは、新しいパッケージ ファイルの作成、必要なパーツの追加、および適切な XML スキーマに基づいて構造化された XML コンテンツの書き込みです。理論的には単純でしたが、その詳細について理解するには少し時間がかかります。また、ファイル形式の仕様に関連するドキュメントをすべて参照する必要もあります。これらは OpenXmlDeveloper.org (英語) からダウンロードできます。

Word 文書で作業する場合は、パッケージ内に含まれるパーツの種類、およびパーツがコンテンツ タイプおよびリレーションシップに基づいて構造化される方法について理解しておく必要があります。また、これらの各パーツに含まれる WordprocessingML を生成する方法も理解しておく必要があります。Excel スプレッドシートで作業する場合は、コンテンツ タイプとリレーションシップは Word 文書の場合と異なります。この場合、WordprocessingML ではなく、SpreadsheetML について理解しておく必要があります。表、グラフィックス、書式などを含むドキュメントを一から作成するために必要な XML を生成するには、詳細な知識を習得しておく必要があります。

Word 2007 の新しい "開発" 機能

最後に、WordprocessingML を生成するコードを記述しなくても、洗練された外観の Word 2007 文書を作成できる方法を説明して、今月号のコラムを締めくくります。まず、新しい Office オープン XML ファイル形式で保存された文書で作業する際に使用できる Word 2007 の新機能を 2 つ紹介します。1 つは XML データ ストアです。これを使用すると、.docx ファイル内にユーザー定義の XML ドキュメントをパーツとして埋め込むことができます。もう 1 つはコンテンツ コントロールです。これはデータ入力とデータ バインドをサポートするために、/word/document.xml パーツ内で定義されるユーザー インターフェイス コンポーネントです。

コンテンツ コントロールを使用するには、まず、Word 2007 の [Word のオプション] ダイアログ ボックスで [基本設定] をクリックし、[[開発] タブをリボンに表示する] チェック ボックスがオンになっていることを確認します。図 8 に示すように、[開発] タブには一連のコントロールが表示されます。これらのコントロールはいずれも Word 文書に追加できます。

図 8 [開発] タブのコントロール
図 8 [開発] タブのコントロール

コンテンツ コントロールは、Word の新しい形式である .docx で保存されたファイルにのみ追加できることに注意してください。コンテンツ コントロールは従来のバイナリ形式では定義できないため、.doc ファイルではサポートされません。ただし、新しい形式でコンテンツ コントロールを追加すると、Word 文書にユーザー入力用の要素を挿入できます。たとえば、カレンダーから日付を選択できるビジネス文書など、特定の情報をユーザーに要求する Word 文書を作成できます。

コンテンツ コントロールには、編集および表示の 2 つの異なるモードがあることに注意してください。編集モードでは、テキスト入力、日付の選択、ドロップダウン リストでのアイテムの選択などを行うことができます。表示モードは表示と印刷に最適化されています。つまり、コンテンツ コントロールでの編集が完了し、編集モードではなくなると、編集に関連するコンテンツ コントロールの要素は非表示になります。

Word 2007 でデータとその表現を分離する

XML データ ストアとコンテンツ コントロールは Word 2007 の異なる機能で、個別に使用できますが、併用するとさらに強力な機能になります。たとえば、Word 文書内に顧客や請求書のデータが含まれた XML ファイルを埋め込むことができます。また、XPath 式を使用して、コンテンツ コントロールを XML ファイル内のデータにバインドできます。これにより、Word でデータを表示する方法を含む書式設定の情報とデータ自体を効率的に分離できます。

では、簡単な例から始めましょう。今月号のコラムに付属の CustomerEntry.docx という Word 文書をダウンロードし、このサンプル文書を調べます。このファイルを Package Viewer で調べることをお勧めします。これにより、各パーツがどのように組み合わせられているかが簡単にわかります。ただし、このような文書を自身で構築する場合は、.docx ファイルの拡張子を .zip に変更する必要があります。これにより、2006 年 11 月号のコラム「Basic Instincts」で説明したように、このパッケージを直接開いて、そのコンテンツを操作できます。

CustomerEntry.docx には、顧客データを含むユーザー定義の簡単な XML ドキュメントが含まれます。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Customer xmlns="http://litware.com/2006/customer">
  <Name>Brian Cox</Name>
  <Address>2732 Baker Blvd</Address>
  <City>Eugene</City>
  <State>OR</State>
  <Zip>97403</Zip>
</Customer>

この XML ドキュメントは、/customXml/item1.xml の URI を使用して、.docx ファイル内にパーツとして埋め込まれています。このパーツは item1.xml 以外の名前にもできます。ただし、ここでは Word 2007 でパーツが XML データ ストア内に作成され、名前が付けられるときに使用される名前付けスキーマとの整合性を保ちます。

次に、顧客 XML ドキュメント内のコンテンツを特定する方法を作成する必要があります。これを行うには、特定の GUID で dataStoreItem を定義します。ここでは /customXml/itemProps1.xml というパーツを作成し、このパーツと親パーツである/customXml/item1.xml の間にリレーションシップを設定します。/customXml/itemProps1.xml のコンテンツの例を次に示します。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ds:dataStoreItem ds:itemID="{D5CB27EE-AE18-48C7-B53F-E921F4653E70}"
      xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/
      customXml">
  <ds:schemaRefs>
    <ds:schemaRef ds:uri="http://litware.com/2006/customer"/>
  </ds:schemaRefs>
</ds:dataStoreItem>

次に、/word/document.xml と /customXml/item1.xml の間にパーツのリレーションシップを設定します。このリレーションシップでは、/word/document.xml を親パーツに定義し、/customXml/item1.xml を子パーツに定義します。リレーションシップ タイプは、次の文字列を使用して指定する必要があります。

http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml

ユーザー定義の XML ドキュメントを特定の dataStoreItem として設定するための手順をすべて行うと、/word/document.xml からユーザー定義の XML ドキュメントのデータにアクセスできます。たとえば、マクロを有効にした Word 2007 文書用に Visual Basic® for Applications (VBA) コードを記述し、ActiveDocument オブジェクトから CustomXmlPart オブジェクトを取得して、埋め込んだ XML ドキュメントから必要な顧客データを取得できます。この例では、/word/document.xml でコンテンツ コントロールを作成し、それらを顧客データが含まれる XML ドキュメント内の特定の要素にバインドします。

残念ながら、Word 2007 には、ユーザー インターフェイスでコンテンツ コントロールを XML データ ストア内の要素にバインドする方法がありません。したがって、Visual Studio® 2005 などの XML エディタを使用して、/word/document.xml を直接編集する必要があります。以下の例に示すように、ユーザー定義の XML ドキュメントに含まれる Customer 要素内の Name 要素のバインドを作成する WordprocessingML 要素を /word/document.xml パーツに追加できます。

<w:sdtPr>
  <w:id w:val="256125470"/>
  <w:dataBinding
    w:prefixMappings="xmlns:ns0='http://litware.com/2006/customer'"
    w:xpath="/ns0:Customer[1]/ns0:Name"
    w:storeItemID="{D5CB27EE-AE18-48C7-B53F-E921F4653E70}"/>
  <w:text/>
</w:sdtPr>

dataBinding 要素には、顧客データが含まれている dataStoreItem の GUID を参照する storeItemID 属性が含まれています。また、dataBinding 要素には、コンテンツ コントロールをユーザー定義の XML ファイル内の特定の要素にバインドするための XPath 式を定義する xpath 属性も含まれています。ユーザー定義の XML ファイルから関連データを表示するために必要な dataBinding 要素をすべて追加して /word/document.xml を更新すると、Word 文書を作成できます。

ユーザー定義の XML ファイルおよびバインドされたコンテンツ コントロールを含む Word 文書を手動で作成する方法に慣れるには時間がかかる場合がありますが、時間をかけるだけの値はあります。それは、この洗練されたソリューションを使用すると、データとその表現を分離できるためです。最低限必要なコンテンツ コントロールをバインドした Word 文書を作成したら、残りは Word 内で直接編集して、洗練された外観を持つ文書にできます。たとえば、Word ユーザーならだれでも行うように、テキストやロゴを追加できます。また、新しい表やセクションを作成したり、コンテンツ コントロールをドラッグ アンド ドロップして表示したりできます。

バインドされたコンテンツ コントロールではユーザー定義の XML ファイルのデータと双方向の同期を行えることも、注意すべき重要な点です。ユーザー定義の XML ファイルでデータが追加されると、バインドされたコンテンツ コントロールでもこのデータが更新されます。また、コンテンツ コントロールでデータが更新され、ドキュメントが保存されると、ユーザー定義の XML ファイルにも同じ変更が書き込まれます。これにより、更新されたデータを使いやすい XML 形式で簡単に抽出できます。

XML データ ストアをプログラムで更新する

これまでは、コンテンツ コントロールを XML データ ストアのユーザー定義の XML ドキュメントにバインドする方法を説明してきました。最後に、特定の顧客データを含む XML ドキュメントの新しいインスタンスを生成するコードを記述し、適切にバインドされたコンテンツ コントロールが設定されている既存の Word 文書内の XML ドキュメントを上書きします。

今月号のコラムには、Letter Generator というサンプル アプリケーションが付属しており、ダウンロードが可能です。これは ASP.NET 2.0 アプリケーションで、LetterTemplate.docx という Word 文書を使用します。この Word 文書には、図 9 に示すような XML ドキュメントが埋め込まれています。これには Litware Corporation の従業員が顧客宛てのレターを作成するために必要なコンテンツがすべて含まれています。

Figure 9 ユーザー定義の XML ドキュメント (Close Figure 9)
<?xml version="1.0" encoding="utf-8"?>
<LitwareLetter xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema"
               xmlns="http://litware.com/2006/letters">
  <Customer>
    <ContactFirstName>Yoshi</ContactFirstName>
    <ContactLastName>Latimer</ContactLastName>
    <Company>Hungry Coyote Import Store</Company>
    <Address> 516 Main St.</Address>
    <City>Elgin</City>
    <State>OR</State>
    <Zip>97827</Zip>
  </Customer>
  <Date>October 16, 2006</Date>
  <Body>Thanks for your recent purchase at Litware </Body>
  <Employee>
    <Name>Rob Verhoff</Name>
    <Title>Director of Accounting</Title>
  </Employee>
</LitwareLetter>

Letter Generator アプリケーションでは、Access™ MDB ファイルの 3 つの異なるテーブルからデータを取得し、この一貫した形式で XML ドキュメントを生成します。このサンプルを拡張して、SQL Server™ やカスタム Web サービスなどの他のデータ ソースからデータを抽出できますが、その方法については皆さん自身で考えてください。

GenerateLetter.aspx というページが取得されると、顧客、レターの種類、およびレターに署名する従業員を選択するための WebControls が表示されます。作成するレター用に適切なパラメータをすべて選択し、[Generate Letter] ボタンをクリックすると、この要求の処理がサーバー側で開始されます。

[Generate Letter] ボタンのサーバー側コードでは、LetterTemplate.docx という既存の Word 2007 文書を MemoryStream オブジェクトに読み込み、.NET パッケージ API を使用してこの文書を開きます。次に、このコードでは、要求されたパラメータに基づいて新しい XML ドキュメントを作成します。次に、/customXML/item1.xml パーツを開き、新しく作成した XML ドキュメントでこのコンテンツを上書きします。最後に、このコードでは、2006 年 11 月号のコラム「Basic Instincts」で説明した方法を使用して、パッケージ ファイルを閉じ、作成した Word 文書を表示します。これで、顧客宛てのレターを作成するプロセスを自動化できます。

まとめ

今月号のコラムでは、Office オープン XML ファイル形式での作業をさらに詳しく説明しました。Word 2007 文書の構造を定義する際には、リレーションシップとリレーションシップ タイプが重要であることが理解できたはずです。コンテンツ コントロールをユーザー定義の XML ドキュメントの要素にバインドすることによって、データとその表現を分離した Word 2007 文書を作成するための強力な新しい方法についても説明しました。これで皆さんの創作意欲が刺激され、Word、Excel、および PowerPoint のユーザー向けに標準のビジネス ドキュメントを作成するプロセスを自動化するためのアプリケーションとコンポーネントの構築のための一歩を踏み出せることでしょう。

ご意見やご感想は、

Ted mmoffice@microsoft.com.

まで英語でお送りください。
Ted Pattison は、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。Ted は『Inside Windows SharePoint Services 3.0』(英語) という Microsoft Press の書籍を執筆しています。彼の会社 Ted Pattison Group では、企業の開発者を対象に SharePoint の上級者向けトレーニングを提供しています。


テストの実行: AJAX テストの自動化

テストの実行
AJAX テストの自動化
Dr. James McCaffrey

コードのダウンロード : : TestRun2007_02.exe (574KB)
コンテンツ
テスト用 Web アプリケーション
テストの自動化
asyncCall の内部
Delay の内部
テスト ページを作成する
テスト ハーネスを拡張する
まとめ

AJAX (Asynchronous JavaScript And XML) 技術を使用する Web アプリケーションの人気は、ここ数年確実に上昇しています。正確に言えば、AJAX は、非 AJAX Web アプリケーションと比べると、パフォーマンスとユーザー エクスペリエンスの点でたいへん優れています。ただし、AJAX Web アプリケーションは非同期で動作するため、一般に、従来の同期テストの自動化手法は使用できません。今月のコラムでは、軽量テストの自動化を記述して、AJAX Web アプリケーションの機能を検証する手法をご紹介します。

このコラムの最終目標をわかりやすく示すため、複数のスクリーンショットを用意しました。図 1 は、AJAX 技術を使用する、単純化した典型的な ASP.NET Web アプリケーションです。このアプリケーションは、Microsoft® Windows Live™ Local のような地図ツールをシミュレートしています。ユーザーが方角ボタン コントロールのいずれかをクリックすると、アプリケーションは Web サーバーから適切な地図イメージをフェッチし、中央の領域に新しい地図を表示します。このスクリーンショットからはよくわかりませんが、この Web アプリケーションは、Microsoft ASP.NET AJAX を使用して、地図イメージを非同期に送信し、取得します。もちろん、皆さんが Web アプリケーションを作成する場合はもっと複雑になりますが、ここで紹介する手法は、任意の複雑なアプリケーションにまで広く一般化され、実装テクノロジを問わず、どの AJAX 対応アプリケーションにも使用できます。

図 1 のアプリケーションを手動でテストするのは、時間がかかり、効率的でなく、退屈な作業です。もっと適切なテスト方法は、図 2 に示すように、軽量テストの自動化を記述することです。テスト ハーネスは、2 つのフレームで構成されるシンプルな HTML ページです。右側のフレームでは、変更やインストルメンテーションのない Web アプリケーションがホストされています。左側のフレームでは、Internet Explorer® Document Object Model (DOM) を使用して、右側のフレームの Web アプリケーションを操作する JavaScript コードの HTML ページがホストされています。

このテスト手法は、非同期操作を使用するアプリケーションを対象としていますが、従来の同期型 HTTP 要求/応答メカニズムを使用する Web アプリケーションにも応用できます。このコラムでは、従来のテスト自動化手法が AJAX アプリケーションには役に立たない理由がおわかりいただけるよう、テスト用 Web アプリケーションについて簡単に説明します。次に、図 2 のイメージを生成したテスト ハーネスについて詳しく説明し、ここで示した手法を独自のニーズに合わせて修正および拡張する方法についても説明します。最後には、この手法が開発者やテスターのスキル向上に役立つことがおわかりいただけると思います。

図 1 テスト用 AJAX Web アプリケーション
図 1 テスト用 AJAX Web アプリケーション(画像を縮小するには、ここをクリックします)
図 1 テスト用 AJAX Web アプリケーション
図 1 テスト用 AJAX Web アプリケーション (画像を拡大するには、ここをクリックします)

テスト用 Web アプリケーション

私は、AJAX アプリケーションの作成を大幅に簡易化する ASP.NET AJAX コード ライブラリを使用して、図 1 に示される AJAX 地図アプリケーションを作成しました。実際の地図イメージを使用する代わりに、1 から 9 の数字を含む 9 つのダミー イメージ (1.jpg から 9.jpg) を使用しました(Microsoft TerraServer を使用して実際の地図イメージを取得する例については、今月号の Jeff Prosise の「Wicked Code コラム」を参照してください) 。私が作成した小さな世界は、縦横 3 つのグリッドで構成され、1 行目に 1 から 3、2 行目に 4 から 6、3 行目に 7 から 9 の番号が割り当てられています。5 はこの世界の中心であり、5 が表示されているときに [North] コントロール ボタンをクリックすると、アプリケーションには 2 が表示されます。[North] ボタンのロジックを 図 3 に示します。

Figure 3 [North] ボタンのコード (Close Figure 3)
protected void Button1_Click(object sender, EventArgs e)
{
  string imageUrl = Image1.ImageUrl.ToString();
  if (!(imageUrl == "~/1.JPG" || imageUrl == "~/2.JPG" ||
        imageUrl == "~/3.JPG"))
 {
    string oldNum = imageUrl.Substring(2, 1);
    int oldNumAsInt = int.Parse(oldNum);
    int newNumAsInt = oldNumAsInt - 3;
    string newNum = newNumAsInt.ToString();
    string newImageURL = imageUrl.Replace(oldNum, newNum);
    Image1.ImageUrl = newImageURL;
  }
}
図 2 サンプル テストの実行
図 2 サンプル テストの実行(画像を縮小するには、ここをクリックします)
図 2 サンプル テストの実行
図 2 サンプル テストの実行 (画像を拡大するには、ここをクリックします)

現在の地図として、この世界の 1 番上の行 (1、2、3) が表示されている場合は、[North] をクリックしても効果はありません。それ以外の場合は、現在の地図位置の数字を取得し、3 を差し引き、対応する新しい JPEG イメージを決定して、Image コントロールの ImageUrl プロパティを更新します。[South]、[East]、および [West] ボタン コントロールのハンドラも同様です。

強調しておきますが、アプリケーション コードを短く、読みやすくするため、ここでは優れたコーディング方法を意図的に使用していません。実際の Web アプリケーションでは、コード ロジックはバックエンドの SQL または XML ストアのデータにアクセスしてフェッチし、そのデータを使用してアプリケーションの状態を更新することになるでしょう。このコラムの手法は Internet Explorer DOM を通じて Web アプリケーションをテストするため、アプリケーションの状態がどのように決定されるかは重要ではありません。どのようなユーザー入力でもアプリケーションの状態は変更され、アプリケーションの UI に反映されます。これには、Internet Explorer DOM を通じてアクセスできます。

AJAX を使用しない従来の方法では、HTTP 要求が Web サーバーに投稿され、要求情報はフォーム オブジェクトまたはクエリ文字列で渡されます。AJAX テクノロジを使用しなくても地図アプリケーションは動作しますが、欠点が 2 つあります。第 1 に、HTTP 要求/応答メカニズムは同期処理であるため、要求が Web サーバーで処理され、応答がクライアントに返されるまでは、Web アプリケーションとの対話処理がほぼできなくなります。第 2 に、ほとんどの場合、HTTP 要求によって応答ページ全体が作成されるため、Internet Explorer で応答が受信されるとページ全体が再描画されます。このため、要求/応答が比較的速い場合はページの不要なちらつきが発生し、さらに悪いことに、アクションがもっと遅い場合はページのブラックアウトが発生します。

AJAX は、この両方の問題を解決します。AJAX は、HTTP 要求の代わりに XMLHTTP 要求を送信することによって動作します。XMLHTTP 要求は非同期です。そのため、XMLHTTP 要求が裏側で処理されている間も、Web アプリケーションとの対話処理は継続できます。XMLHTTP 応答が受信されると、Internet Explorer DOM が使用され、新しいデータを含む Web ページ領域のみを再描画できます。ページ全体の再描画は必要ありません。

私の ASP.NET 地図アプリケーションでは、AJAX を有効にするために "未加工の" JavaScript を記述する代わりに、Microsoft ASP.NET AJAX フレームワークを使用しました。このフレームワークのインストールには、Visual Studio® AJAX Web サイト テンプレートが用意されているため、このフレームワークはとても簡単に使用できます。テンプレートを選択すると、Web アプリケーション プロジェクトに必要なアセンブリ参照が追加されます。地図アプリケーションで AJAX 機能を有効にするために、次のタグをソース ファイルに追加しました。

<asp:ScriptManager ID="sm" runat="server" />

次に、ASP.NET AJAX UpdatePanel コントロールに Image コントロール (非同期要求/応答の後に更新するコントロール) をラップしました。

<asp:UpdatePanel ID="up1" runat="server">
  <ContentTemplate>
    <asp:Image ID="Image1" runat="server" ImageUrl="~/5.JPG"
               (other attributes omitted) />
  </ContentTemplate>
  <Triggers>
    <asp:AsyncPostbackTrigger ControlID="Button1" EventName="Click" />
    <asp:AsyncPostbackTrigger ControlID="Button2" EventName="Click" />
  </Triggers>
</asp:UpdatePanel>

必要な作業はこれだけです。簡単ですね。XMLHTTP オブジェクトの作成、非同期応答のリッスン、エラー処理、ブラウザ間の相違など、面倒な作業はすべてフレームワークが担当します。

AJAX 非同期要求/応答は、4 つの方角すべてのコントロールについて有効にするのではなく、Button1 (North) および Button2 (South) コントロールにクリック イベントがあるときのみ有効にしています。これは、今月のコラムのテスト手法が非同期要求と同期要求の両方に役立つことを示すために、意図的に行いました。この記事に付属しているコード ダウンロードを使用して、ご自分でアプリケーションを試してみると、North および South に対する要求 (AJAX) と、East および West に対する要求 (非 AJAX) とでパフォーマンスが大きく違うことがおわかりいただけると思います。

従来のほとんどのテスト自動化手法は、AJAX Web アプリケーションには使用できません。Web アプリケーションの機能をテストする最も基本的な方法は、ユーザー入力によって生成される要求に相当する HTTP 要求をプログラムで Web サーバーに送信し、HTTP 応答をフェッチして、結果が合格/不合格かどうかを判断するために応答を調べます。この方法は、特殊な XMLHTTP 要求を使用する AJAX アプリケーションには使用できません。

もう 1 つの従来の方法では、JavaScript によって Internet Explorer DOM を操作して、Web サーバーに要求を送信し、onload イベント (応答がクライアントによって受信され、読み込まれたという通知) が起動するまで待機してから、JavaScript と Internet Explorer DOM を使用して、結果を判断するために Web ページの新しい状態を調べます。この方法で問題となるのは、AJAX が非同期で動作するため、応答がクライアントによって受信されたことを通知する onload イベントを使用できないことです。しかし、この方法を修正すれば、AJAX アプリケーションの軽量テストの自動化を作成できます。応答が受信されたことを通知する onload イベントを使用する代わりに、アプリケーションを監視して状態が予想どおりに変更されたら callback 関数に制御を移すコードを記述できます。

テストの自動化

図 2 のスクリーンショットに示された AJAX アプリケーション テスト ハーネス システムは、3 つのファイルで構成されています。図 4 は、ハーネス構造全体の構成図です。ハーネス (TestHarness.aspx) 自体は、2 つのフレームを持つシンプルな Web ページです。右側のフレームには、テスト用 AJAX Web アプリケーションが保持されます。左側のフレームには、最小限の UI と、テスト用 Web アプリケーションを操作および調査するためのすべての JavaScript コードを含むテスト シナリオ ページが保持されます。テスト ハーネスとテスト シナリオ ページには基本 HTML を使用することもできますが、一貫性を保つために 3 つのページをすべて .aspx ファイルとして作成しました。TestHarness.aspx ページのすべてのソースを以下に示します。

<html>
<head>
  <title>Test Harness for AJAX Web Apps</title>
</head>
  <frameset cols="45%,*">
    <frame src="http://localhost/AjaxTest/TestScenario001.aspx"
      name="leftFrame">
    <frame src="http://localhost/AjaxApplication/Default.aspx"
      name="rightFrame">
  </frameset>
</html>

図 4 テスト ハーネス構造
図 4 テスト ハーネス構造(画像を縮小するには、ここをクリックします)
図 4 テスト ハーネス構造
図 4 テスト ハーネス構造 (画像を拡大するには、ここをクリックします)

これを見てもわかるように、メインのテスト ハーネス ページは、実際は単なるコンテナ ファイルです。テスト シナリオの実行は、プログラム定義関数 asyncCall を呼び出すことによって開始します。これにより、テスト用アプリケーションは非同期 XMLHTTP 要求を Web サーバーに送信し、プログラム定義の delay 関数も呼び出されます。delay 関数は、Web サーバーが要求を処理して非同期応答を返す間ループして、Web アプリケーションの状態を頻繁に確認します。応答がクライアントによって受信され、アプリケーションの状態が更新されると、delay 関数は Web ページ状態の変更を検出し、新しい非同期呼び出しが発生します。

asyncCall の内部

AJAX テスト自動化手法の核心となるのは、プログラム定義関数の asyncCall および delay のペアです。これらの関数は組み合わせて使用します。asyncCall メソッドを次に示します。

function asyncCall(action, checkFunc, arg, callback, pollTime)
{
  numTries = 0;
  action();
  window.setTimeout("delay(" + checkFunc + ", " + "'" + arg +
    "'" + ", " + callback + ", " + pollTime + ")", pollTime);
}

5 つの引数が渡されます。最初のパラメータ action は、Internet Explorer DOM を使用して、非同期 XMLHTTP 要求を開始するアクションを起動するルーチンへの関数ポインタです。2 つ目のパラメータ checkFunc は、Web アプリケーションの状態が非同期応答の完了を示したときに true を返すルーチンへの関数ポインタです。3 つ目のパラメータ arg は、checkFunc 関数に渡される引数です。4 つ目のパラメータ callback は、非同期応答の完了時に呼び出すルーチンへの関数ポインタです。最後のパラメータ pollTime は、パートナーの delay 関数で呼び出し間に待機する時間 (ミリ秒) を指定します。

asyncCall 関数の内部では、最初にグローバル カウンタ numTries がゼロに設定されます。この変数は、非同期応答が発生したことを示す状態の検出が、指定した試行回数後に終了できるように、delay 関数に入った回数を記録します。次に、action 引数を呼び出して、非同期要求を起動します。関数名に追加されたかっこは、関数を呼び出すための構文メカニズムであることに注意してください。

ここからが実際のトリックです。組み込みの window.setTimeout 関数を呼び出します。ご覧のとおり、setTimeout は 2 つの引数を受け取ります。最初の引数は、1 度だけ実行する JavaScript ステートメントです。2 つ目の引数は、最初の引数の実行前に待機する時間 (ミリ秒) です。asyncCall のコードをよく見ると、setTimeout の最初の引数は次のように解決されることがわかります。

delay(checkFunc, 'arg', callback, pollTime)

2 つ目の引数は pollTime です。つまり、pollTime ミリ秒の待機後に、プログラム定義の delay 関数が呼び出されます。

asyncCall 関数の呼び出しは、次のようになります。

asyncCall(clickNorth, imgIsTwo, "2", clickSouth, 200);

この呼び出しは、"関数 clickNorth を呼び出して delay ループに入り、引数 2 で関数 imgIsTwo を 200 ミリ秒ごとに呼び出し、imgIsTwo が最終的に true を返したら関数 clickSouth に制御を移す" と解釈できます。

asyncCall メソッドの定義内で、arg を囲む単一引用符は重要です。asyncCall 定義内で単一引用符を使用していない場合に、パラメータは次のように参照渡しされることがあります。

asyncCall(doThis, findX, "X", doThat, 200);

このような呼び出しは、変数 X が定義されていないというエラーを生成します。単一引用符があれば、この状況で意図したように、パラメータは値渡しされます。

asyncCall 内での window.setTimeout 関数の呼び出しには、別の興味深い方法があります。通常は次のように記述します。

window.setTimeout("delay(" + checkFunc + ", " + "'" + arg +
    "'" + ", " + callback + ", " + pollTime + ")", pollTime);

上記の代わりに、JavaScript の無名関数を使用して、次のように記述できます。

window.setTimeout(
    function(){delay(checkFunc, arg, callback, pollTime);},
    pollTime);

引用符と文字列連結が除外されたため、このコードは無名関数を使用しない方法よりも多少見やすくなります。また、無名関数を使用しない方法は、スクリプトを処理するための新しいスクリプト環境をブラウザが作成しなければならないため、こちらの方法のほうがわずかに効率的です。ただし、checkFunc への arg 引数を区切る単一引用符がないため、arg が値渡しされるかどうかが不明確です (簡単なコメントで混乱を解消できる場合があります)。

Delay の内部

ここでは、asyncCall とペアで使用している delay 関数について説明します。

function delay(checkFunc, arg, callback, pollTime)
{
  ++numTries;

  if (numTries > maxTries) finish();
  else if (checkFunc(arg)) callback();
  else window.setTimeout("delay(" + checkFunc + ", " + "'" + arg +
      "'" + ", " + callback + ")", pollTime);
}

delay 関数は、呼び出し元の asyncCall 関数に渡された最後の 4 つの引数と一致する 4 つの引数を受け取ります。

delay 内部では、最初にグローバル カウンタ numTries の値をインクリメントします。このカウンタは、delay 関数に入った回数、つまり非同期応答が完了したかどうかを確認するため Web アプリケーションの状態をチェックした回数を記録します。このグローバル カウンタがグローバル定数 maxTries より大きい場合は、Web アプリケーションがサーバー側でタイム アウトになっているか、XMLHTTP 応答が正しくないかのどちらかであるため、finish 関数に制御を移して、何が起きたを判断します。

delay がタイム アウトになっていない場合は、checkFunc 関数を呼び出して、正しい応答を示す状態が true かどうかをテストします。checkFunc が true を返した場合は、callback 関数を呼び出して、テスト シナリオの実行を続行します。checkFunc 関数が false を返した場合は、引き続き待機する必要があるため、組み込みの setTimeout 関数をもう一度呼び出します。これにより、pollTime ミリ秒の待機後に delay がもう一度呼び出されます。

この delay 関数は、関数自体を直接呼び出さないため再帰的ではありませんが、setTimeout 関数を通じて間接的に呼び出すため、自己参照型です。ここで効果的なのは、finish 関数 (delay ループの最大回数を超えた場合) または callback 関数 (Web アプリケーションの状態が true になった場合) のいずれかに制御を移す待機ループです。この方法は、後から考えれば明白に思えることの 1 つなのですが、ソリューションを確認するまではそれほど明白ではありません。もちろん他の方法もいくつかありますが、実際にはこれがシンプルで効果的だということがわかっています。

asyncCall 関数内と同様に、名前付き関数の引数を使用して window.setTimeout 関数を呼び出す代わりに、次のように無名関数機能を使用できます。

window.setTimeout(
    function(){delay(checkFunc, arg, callback);},
    pollTime);

テスト ページを作成する

これまでに説明した asyncCall および delay 関数を使用して、テスト シナリオ ページを作成できます。テスト シナリオの構造全体は、図 5 に示されています。

Figure 5 テスト シナリオ構造 (Close Figure 5)
<html>
<head>
  <script type="text/javascript">
    var maxTries = 10;
    var numTries;
    var polling = 200; // milliseconds

    function asyncCall(action, checkFunc, arg, callback, pollTime)
    {
      // code here
    }

    function delay(checkFunc, arg, callback, pollTime)
    {
      // code here
    }

    // other functions go here
  </script>
</head>
<body bgcolor="#22EE66">
  <h3>This is the test scenario script page</h3>
  <p>Actions:</p><p><textarea id="comments" rows="16"
                      cols="34"></textarea></p>
  <input type="button" value="Run Test" onclick="runTest();" />
</body>
</html>

テスト シナリオ ページの <body> セクションは、タイトル、コメント表示用の ID "comments" 付き textarea、および自動化を起動するボタン コントロールで構成されています。<head> セクションには、すべての JavaScript コードが含まれています。グローバル定数 maxTries を宣言して初期化し、delay 関数に入る最大回数、つまり非同期応答が完了したことを通知する状態をチェックする回数を指定します。グローバル変数 numTries は、delay 関数に入った回数を記録します。グローバル定数 polling は、delay 関数への連続呼び出し間の遅延時間を設定します。

テスト用 AJAX Web アプリケーションの複雑さに応じて、maxTries と polling の値の修正が必要になる場合があります。ここでは、200 ミリ秒ごとの呼び出しを 10 回繰り返すと 2 秒にしかならないため、Web サーバーが XMLHTTP 要求を処理し、クライアントに応答を返すには十分ではない可能性があります。

テスト シナリオは、次のように runTest 関数を呼び出すことによって開始します。

function runTest()
{
  try
  {
    logRemark("Test Scenario #001");
    logRemark("Starting test run\n");
    step1(); // starting at 5, go N to 2
  }
  catch(ex) { logRemark("Fatal error: " + ex); }
  }
}

ここでは、プログラム定義の logRemark 関数を使用して、ハーネス ボディ上の <textarea> にいくつかのメッセージを表示し、step1 関数に制御を移しています。コードはシンプルな try/catch ブロック内にラップされているため、テスト中にスローされた例外はすべてキャッチされます。この logRemark 関数は、とてもシンプルです。

function logRemark(comment)
{
  var currComment = document.all["comments"].value;
  var newComment = currComment + "\n" + comment;
  document.all["comments"].value = newComment;
}

ID "comments" を含む <textarea> 要素の現在のコンテンツを取得し、改行文字と新しいコメント テキストを現在のコンテンツに追加し、更新されたコメント セットで <textarea> コンテンツを置き換えます。これは、効率的な方法ではありませんが、軽量テストの自動化では、シンプルな考え方が効率性よりも重要になることがあります。

step1 関数は、テスト用 AJAX Web アプリケーションの操作を開始します。

function step1()
{
  logRemark("Clicking North, waiting for '2'");
  asyncCall(clickNorth, checkImageSrc, "2", step2, polling);
}

コメントを表示した後で、asyncCall 関数を呼び出します。これは、このテストの自動化の重要な部分です。この呼び出しは、"関数 clickNorth を呼び出し、checkImgSrc("2") が true を返すまで 200 ミリ秒のチェック間隔で delay をループし、関数 step2 に制御を移す" ことを意味しています。delay ループが 10 (maxTries) 回より多く呼び出された場合は、制御が finish 関数に移されます。

clickNorth 関数はとてもシンプルです。

function clickNorth()
{
  var btnNorth = parent.rightFrame.document.all["Button1"];
  if (!btnNorth) throw "Did not find btnNorth";
  btnNorth.click();
}

Internet Explorer DOM を使用して、Button1 コントロールへの参照を取得し、そのクリック メソッドを呼び出します。この自動化は、AJAX アプリケーションとは別のフレーム内にあるため、テスト スクリプトからアプリケーションのコントロールにアクセスするには、親キーワードを使用して 1 つ上のレベルに移動し、アプリケーション コンテナのフレーム ID を使用する必要があります。Web ページ コントロールへの参照を取得する際に、私は document.all コレクションをよく使用しますが、私の同僚は次のように getElementById メソッドを好んで使用します。

var btnNorth = parent.rightFrame.document.getElementById("Button1");

checkImageSrc 関数は、テスト用 AJAX アプリケーションの Image1 コントロールが非同期応答によって更新されたことを自動化に通知します。

function checkImageSrc(target)
{
  try
  {
    var s = parent.rightFrame.document.all["Image1"].src;
    return s.indexOf(target) >= 0;
  }
    // traps case where Image1 is not yet loaded
    // logRemark("Error in checkImageSrc(): " + ex);
  catch(ex) { return false; }
}

基本的には、Image1 コントロールの src プロパティをチェックして、プロパティ内に対象の文字列があるかどうかを確認するだけです。たとえば、checkImageSrc("2") の呼び出しは、イメージの src プロパティが "~/2.jpg" (地図 2 のファイル名) のように "2" を含む場合に true を返します。独自の AJAX 自動化を記述するときは、適切なチェック関数を作成する必要があります。たとえば、Web アプリケーションが ID "TextBox1" の TextBox コントロールを更新すると仮定します。その場合は、次のようなチェック関数が考えられます。

function checkTextBoxValue(target)
{
  try
  {
    var s = parent.rightFrame.document.all["TextBox1"].value;
    return s.indexOf(target) >= 0;
  }
    // traps case where TextBox1 is not yet loaded
    // logRemark("Error in checkTextBoxValue(): " + ex);
  catch(ex) { return false; }
}

checkImageSrc 関数の try/catch ブロックに注目してください。ほとんどの場合、try/catch は予想外のエラー状態をトラップするために使用されますが、ここでは通常の関数ロジックの一部として使用されています。これは、Web ページが再描画されている間は Image1 コントロールへの参照を取得できないため、取得の試行は例外をスローするというアイデアに基づいています。これは、Web アプリケーションが不適切な状態にあるわけではなく、単に不完全な状態であることを意味しているため、例外をキャッチして false を返します。この動作は、logRemark の呼び出しのコメント化を解除し、ポーリング間隔を短くする (10 ミリ秒など) ことによって監視できます。

ところで、通常は、try/catch を普通の関数ロジックの一部として使用することは、悪いコーディング スタイルだと考えられています。しかし今回はシンプルさを優先するため、個人的な判断で通常のロジック フローに try/catch を使用しました。

テスト シナリオ スクリプトでは、step1 関数と同様に step2、step3、step4、および step5 関数が定義されています。step2 関数は clickEast 関数を呼び出し、地図表示領域に "3" が表示されるまで待機して、step3 を呼び出します。step3 関数は、clickSouth 関数を呼び出し、"6" が表示されるまで待機して、以下同様に続きます。step5 関数は、中に finish 関数を含む短い関数 step6 を呼び出します。

function step6()
{
  finish();
}

ここでは単に、実際の finish 関数に制御を移しています (図 6 を参照)。finish 関数は、delay 関数の呼び出し回数の上限を超えたときの処理をする関数でもあります。

Figure 6 finish 関数 (Close Figure 6)
function finish()
{
  if (numTries > maxTries)
  {
    logRemark("\nnumTries has exceeded maxTries");
    logRemark("\n*FAIL*");
  }
  else
  {
    logRemark("Checking final state");
    var is = parent.rightFrame.document.all["Image1"].src;
    if (is.indexOf("8") >= 0) logRemark("\n*Pass*");
    else logRemark("\n*FAIL*");
  }
  logRemark("\nAll done");
}

最初に、グローバル カウンタ numTries がグローバル定数 maxTries を超えたときの状況を処理します。これは、次の 2 つのケースのいずれかで発生します。AJAX アプリケーションのロジックが正確で、アプリケーションによるページ状態の更新に時間がかかりすぎているとき、またはアプリケーションのロジックが間違っていて、チェック関数が正しい状態を検出できなかったときです。ここでは、任意に、テスト シナリオを不合格にするというシンプルなソリューションを選択しています。

finish の 2 つ目のロジック分岐は、アプリケーションの最終状態をチェックして、結果が合格/不合格かどうかを判断します。今回の場合は、"5" から開始し、[North]、[East]、[South]、[South]、[West] コントロールの順にクリックした場合に、最後の地図イメージが予定どおりに "8" であるかをチェックします。

テスト ハーネスを拡張する

ここで紹介したテスト自動化システムは、非常に軽量であり、ユーザーが修正できるようデザインされています。雑誌の記事では常に、主要なアイデアを明確にするため、ほとんどのエラー チェックを除外しています。しかし、エラー トラップは自由に追加することができます。テストの自動化は本質的に複雑であり、エラーは例外よりもむしろ規則なのです。

このテスト ハーネスは完全に自動化されているわけではありません。自動化を起動するには、[Run Test] ボタンを手動でクリックする必要があります。このハーネスを完全に自動化するには、いくつかの方法があります。簡単な方法としては、テスト ハーネス フレームに onload ハンドラを追加します。

<frame src="http://localhost/AjaxApplication/Default.aspx"
  name="rightFrame"
  onload="leftFrame.launch();" >

この場合は、テスト シナリオ コードの launch 関数は次のように定義されます。

var started = false;
function launch()
{
  if (!started) runTest();
}

さらに、このコラムで紹介した runTest 関数は、次のステートメントで開始するよう編集されます。

started = true;

テスト用 AJAX Web アプリケーションを含む右側のフレームが読み込まれると、launch 関数が呼び出されます。最初に launch が実行されたときは、グローバル変数 started は false であり、制御は runTest 関数に移されます。この関数が変数 started を true に設定し、自動化を実行します。Web アプリケーションのその後のページ読み込みでは、通常の HTTP 要求/応答アクションが発生し、launch 関数が呼び出されますが、グローバル変数 started が true であるため、制御がもう一度 runTest に移されることはありません。テスト ハーネスの完全自動化バージョンでは、複数のハーネス ページとシナリオ ページを作成し、BAT ファイル内に次のようにステートメントを配置し、Windows タスク スケジューラを使用して自動化を起動することによって、複数のシナリオを実行できます。

iexplore http://localhost/AjaxTest/TestHarness01.aspx
iexplore http://localhost/AjaxTest/TestHarness02.aspx

この軽量システムの弱点は、システムを都合良くパラメータ化できないことです。テスト シナリオ ページから共通 JavaScript コードを単一の分離 .js ファイル内に簡単に配置することはできますが、単一のテスト ハーネス ページから複数のテスト シナリオを実行するよう簡単に調整する方法はありません。不可能だとは言いませんが、すぐに実現可能なタスクではありません。

AJAX テスト自動化に対するもう 1 つの拡張は、テスト シナリオの結果を保存するプロセスの自動化です。ここで紹介したシステムは結果を記録しませんが、結果を記録する方法はいくつかあります。1 つの方法としては、テスト シナリオ ページ内にフォーム要素を配置し、テスト シナリオの結果を保持するテキスト フィールドをフォームに追加し、次のように結果をサーバーに投稿します。

<form name="resultForm" method="Post" action="saveResults.aspx">
  <p>Result: <input type="text" name="result"></p>
  <p><input type="submit" name="saver" value="Save Results"></p>
</form>
次のコード行によって、finish 関数内で結果フィールドに合格/不合格の値を指定できます。
if (is.indexOf("8") >= 0)
{
  logRemark("\n*Pass*");
  theForm.result.value = "Pass";
}
else
{
  logRemark("\n*FAIL*");
  theForm.result.value = "Fail";
}

これで、[Save Results] ボタンをクリックしてシナリオの結果を手動で保存できます。また、シナリオ コードに次のコードを配置して保存を自動化することもできます。

document.all["theForm"].submit();

まとめ

ここで紹介したテスト ハーネスは、AJAX Web アプリケーションの多数のテスト方法の 1 つにすぎません。このコラムを読めば、この軽量アプローチやさらに高度なハーネスを使用して、AJAX Web アプリケーションを問題なくテストできるようになります。多くの場合、ASP.NET AJAX フレームワークのリリースを使用すると、AJAX Web アプリケーションをすばやく作成できます。軽量テストの自動化を記述する能力は、今後ますます重要となり、個人のスキル向上にも役立ちます。

ご意見やご感想は、

James testrun@microsoft.com.

まで英語でお送りください。
Dr. James McCaffrey は、Volt Information Sciences, Inc. で、マイクロソフトのソフトウェア エンジニア向けの技術トレーニングを担当しています。これまでに、Internet Explorer、MSN Search など複数のマイクロソフト製品を扱ってきました。また、『NET Test Automation Recipes』 (2006 年、Apress 発行) の著者でもあります。James の連絡先は、jmccaffrey@volt.com (英語) または v-jammc@microsoft.com (英語) です。マイクロソフトの Doug Walter、Shawn Burke、および Scott Guthrie のコメントを受けて、このコラムの内容は大幅に充実しました。


Cutting Edge: ASP.NET AJAX の展望

Cutting Edge
ASP.NET AJAX の展望
Dino Esposito

コンテンツ
ASP.NET を使用した AJAX への道
ASP.NET AJAX を使用する明確な手法
UpdatePanel と Interceptor パターン
アウトオブバンド呼び出しの制御
ページ メソッドと Web サービス メソッド
私が ASP.NET AJAX を使用する方法

最近 12 か月間はネットワークに接続していなかった - おそらく、遠く離れた南の島で休暇を過ごしていたか、オンラインではない現実のゲーム ショーに参加していた - 場合を除き、皆さんは AJAX についてある程度のことを知っていると思います。ですが、ここではいくつかの事項を手早く紹介します。

最初に、AJAX は、Asynchronous JavaScript and XML (非同期 JavaScript と XML) の略語です。次に、特定の目的があります。AJAX は真のブレイクスルーとして、他の方法では達成不可能、または実用的に達成できないソリューションを実現します。最後に、AJAX の構成要素について理解する必要があります。これは、本質的には単一のテクノロジではありません。むしろ、JavaScript、カスケード スタイル シート (CSS)、DHTML、XMLHttpRequest のように、数年にわたって登場してきたクライアント側 Web テクノロジから成る強力な組み合わせを活用して構成されたリッチなブラウザ アプリケーションを表す包括的な用語です。

大半のシェアを占める、現在利用可能なブラウザの多くは、共通の機能セットをサポートしています。Web 開発者はこれらの機能を使用して、高度に対話的、迅速な更新が可能、およびクライアント側で適切な対処ができるアプリケーションを構築できます。AJAX アプリケーションはこれらの能力を活用して、デスクトップ アプリケーションを使用する場合に似た、リッチなユーザー エクスペリエンスを実現できます。さらに、AJAX を使用すると、"加工型" アプリケーションをこれまでより容易に作成できます。加工型とは、複数のソースから得た情報を組み合わせる (たとえば、Microsoft® Virtual Earth™ を資産販売情報と連携させ、役立つ不動産ツールを作成) Web アプリケーションを意味します。

私は実際のところ、AJAX は非常に大きな存在であり、非常に重要なので、来年あたりには意識されなくなるのではないか、と考えています。どういう意味でしょうか?私は、業界のだれもが AJAX を受け入れ、毎日の生活で標準的な一部になり、特に意識することはなくなると考えています。現在、"XML や HTTP の使用を強調する必要がある" とは感じないのと同じことです。

2005 年 9 月の Microsoft Professional Developers Conference で公開された Microsoft ASP.NET AJAX (従来の開発コード名は "Atlas") により、ASP.NET 2.0 に対して多くの新しい機能が追加されました (ajax.asp.net (英語) を参照)。これらすべては、AJAX ベースの機能を Web サイトに追加するのに役立ちます。今月のコラムでは、ASP.NET AJAX を検討し、主要な機能のいくつかを詳細に観察します。このコラムでは、読者が少なくとも基本的な概念とツールに親しんできたことを想定します。良好な入門編として、次の記事を参照してください。「Atlas at Last:ASP.NET Atlas Powers the AJAX-Style Sites You've Been Waiting For」(ついに登場した Atlas: ASP.NET Atlas は待望の AJAX スタイル サイトを強化)、Matt Gibbs 著、MSDN®Magazine、2006 年 7 月号。また、いくつかの詳細情報について、囲み記事「Still Synchronous for Now」(現在も同期処理中) も参照してください。

ASP.NET を使用した AJAX への道

AJAX を使用すると、どのような新しいアプリケーション機能を構築できるでしょうか?より具体的に言うと、非同期の JavaScript 呼び出しとブラウザのオブジェクト モデルを使用して、どのような機能を構築できますか?AJAX の機能を実現する 3 つの主要なテクノロジがあります。リモート メソッド呼び出し、クライアント側データ バインド、およびビジュアル効果です。"リモート メソッド呼び出し" という表現は非常に漠然としたものに聞こえるので、部分的なページのポストバック、またはアウトオブバンド (帯域節約) 要求という概念に狭めることにします。

新規または既存の ASP.NET アプリケーションを AJAX アプリケーションに狭めるにはどうすれば良いでしょうか?現在では、AJAX 開発用に 100 を上回るフレームワークが存在することが報告されています。それらは、さまざまなプラットフォームをサポートし、多数のプログラミング アプローチを採用しています。全般的に、私は AJAX フレームワークを、その能力に基づいて 3 つのカテゴリに分類する傾向があります。コールバック フレームワーク、UI フレームワーク、および完全フレームワークです。

コールバック フレームワークは、単純なクライアント ライブラリとサーバー ライブラリによって構成されています。これを使用する場合に実行するのは、クライアントからサーバー側コードの一部を呼び出し、入出力パラメータをシリアル化形式で移動することだけです。標準的な UI フレームワークは、専門家が開発した既存の制御ライブラリを進化させたものであり、高度なグリッド コントロール、グラフ コントロール、およびツリー コントロールを提供します。これらは非同期ポストバックと、クライアント側での JavaScript コードの挿入をサポートし、ページ更新の自動化を実現します。最後に、完全フレームワークは、コントロールとアプリケーション サービスから成る強力なプログラミング モデルを提供します。より望ましいのは、クライアントとサーバーの両方で提供されることです。Microsoft ASP.NET AJAX は、この 3 番目のカテゴリに分類されます。

既に商用のコントロール スイートを使用している場合は、AJAX 対応の次期バージョンにアップグレードすると、シームレスな開発環境を利用できる可能性が高くなります。アプリケーション全体を社内で開発し、プレゼンテーション層とバックエンド層の両方を担当している場合は、完全フレームワークが最も有効です。最後に、単純なフレームワークが役に立つのは、バージョン 1.1 と 2.0 の両方を含む既存の ASP.NET アプリケーションに対して、簡略的なコールバック機能を手早く追加する場合だけです。

ASP.NET 2.0 は、既に組み込みのスクリプト コールバック API を提供しています。この点については、過去にこのコラムで何回か説明しました。たとえば、「Cutting Edge:Script Callbacks in ASP.NET」(Cutting Edge: ASP.NET におけるコールバック) (英語) と「Cutting Edge:Custom Script Callbacks in ASP.NET」(Cutting Edge: ASP.NET におけるカスタム スクリプト コールバック) (英語) です。また、このコラムに掲載されているコードの中にも、リモート メソッド呼び出し用の非常に簡単な AJAX フレームワークのデモを記述しました。これは、わずか 50 行のコードであり、一部は JavaScript、一部はマネージド コードによるものです。もちろん、これは ASP.NET AJAX に匹敵するものではありませんが、ASP.NET 1.1 アプリケーションに AJAX の基本的な機能を追加するための中核のコードとして使用できます。

ASP.NET AJAX を使用する明確な手法

ASP.NET AJAX は、2 つの異なる API で構成されていますが、これらは互いに排他的なものではありません。クライアントとサーバーです。直接的なクライアント側プログラミング、従来のサーバー側プログラミング、またはこれら 2 つの組み合わせを使用して AJAX の機能を構築できます。AJAX ベースで作成されたページは、ブラウザのドキュメント オブジェクト モデル (DOM) を解決するためのクライアント側 JavaScript コードが必要であり、また何らかのアプリケーション固有の拡張も必要です。ただし、そのようなスクリプト コードの作成は ASP.NET プログラマが担当する必要はありません。実際、フレームワーク側で、サーバー側コントロールの出力として、カスタム スクリプト コードを生成することも可能です。この形式による間接的なページ更新は、新規または既存の ASP.NET 2.0 ページに対して AJAX の機能を追加する非常に簡単な方法です。ASP.NET AJAX では、サーバー コントロール、つまり UpdatePanel コントロールによって挿入される小規模なコードを使用してページ更新を制御できます。

UpdatePanel コントロールは、ASP.NET AJAX のうちサーバー中心のプログラミング モデルで中枢の機能を果たします。これを使用してサーバー側コードを実行し、更新後のマークアップをクライアントのブラウザに返すことができます。これは従来のポストバックとどう違うのか、と疑問を感じるかもしれません。違いは、ポストバックの実装方法にあります。UpdatePanel コントロールを使用する場合は、ページ全体を更新する代わりに、更新対象のマークアップに関してアウトオブバンド (帯域節約) 要求を送信し、応答の準備ができた段階で DOM ツリーを更新するよう管理が行われます。

ASP.NET のうちクライアント中心のプログラミング モデルは、リモート エンドポイント (ほとんどの場合は ASP.NET Web サービスと Windows® Communication Foundation サービスですが、これに限定されるわけではありません) を呼び出す機能を中心としています。クライアントのブラウザから直接実行する場合は、リモート エンドポイントの呼び出しをするために、JavaScript プロキシとわずかな JavaScript コードが必要です。最後に、クライアント側のデータ バインドは、従来の JavaScript ランタイムと DOM に対する拡張と見なすことができます。純粋なクライアント側プログラミング スタイルでは、リモート エンドポイントへの接続を行い、データをダウンロードし、そのデータを DOM サブツリーにバインドします。テンプレートの構造およびいくつかの状態情報はクライアント側に残り、未加工のデータだけがサーバーからクライアントに移動されます。

ASP.NET AJAX 環境でのプログラミングには、主に 3 つのアプローチがあります。図 1 では、これらのツールの概要を示し、どれが最適なのか判断する方法を説明します。

Figure 1 ASP.NET AJAX 環境でのプログラミングに対するアプローチ (Close Figure 1)

アプローチ | 説明 | 用途
UpdatePanel | サーバー コントロールから成るグループを格納し、それらのポストバックとレンダリング プロセスを監視します。格納されているコントロールが送信したあらゆるポストバック要求はインターセプトされ、スクリプトをとおしてサービスされます。ページに対する変更は、DHTML を使用して適用されます。 | チームで JavaScript のスキルが大幅に不足している場合や、クライアント側プログラミングに関する収録を最小限に抑えることを希望する場合に使用します。アプリケーション内にある機密性の高いビジネス ロジックを保護する必要がある場合も、UpdatePanel を使用することをお勧めします。
リモート メソッド呼び出し | ページ メソッド、ローカル Web サービス、および外部 Web サービスに対する非同期呼び出しを表す包括的な用語です。呼び出しにより、入力引数が移動され、型に依存しない方法で、JSON ストリームをとおして戻り値を受け取ることになります。この呼び出しをトリガして変更結果をクライアントに返すには、厳密に型指定された JavaScript コードが必要です。 | 非同期の手法でリモート操作のトリガと制御を行う、よりスマートなクライアントに関して作業する必要があるときに使用します。このアプローチを使用するには、JavaScript のコーディングを実施して戻り値を処理し、現在の DOM のうち、結果の影響を受けるあらゆる部分を更新する必要があります。
クライアント側のデータ バインド* | テンプレートベースのバインドを実装するために、AJAX JavaScript を使用して全体を記述された 2 つのクライアント側コントロールがあります。これらは、複数レコード ビュー用の ListView と、単一レコード ビュー用の ItemView です。これら 2 つのコントロールを、クライアント データ ソースおよびフィルタ コンポーネントと組み合わせます。 | 最新、そして複数のブラウザに対応するバージョンの DHTML アプリケーションが必要な場合に使用します。クライアント側データ バインドを行う場合は、最終的なページには JavaScript と XML Script の一方または複数から成る厚い層が存在し、Script Manager コントロールを除くと、マネージド コードは事実上存在しません。

UpdatePanel と Interceptor パターン

UpdatePanel コントロールは、ページ内容を更新するときに、ページ全体の更新なし、およびクライアント側プログラミングの必要なしというプログラミング スタイルを表します。ASP.NET AJAX の用語では、これを "部分的なレンダリング" と呼びます。UpdatePanel コントロールは、この機能の実現に関与する、最も目立つコンポーネントです。ただし、部分的なレンダリングの背後にある本当の頭脳は、ScriptManager コントロールです。UpdatePanel コントロールは引き続き、部分的に更新可能なページを作成するときに開発者が使用する主要なツールになります。

Web ページの更新は、手動で [送信] ボタンをクリックするか、プログラムから DOM フォーム オブジェクトの submit メソッドを呼び出すか、2 つの方法のどちらかで実行できます。どちらの場合でも、ブラウザは現在のページを固定し、新しい要求をバックエンド Web サーバーに送信します。その後、到着した応答により、既存の内容が上書きされます。

UpdatePanel コントロールの目的は、単純にこのプロセスを組み立てることです。このコントロールは、フォームの送信をインターセプトし、送信されようとしているすべての情報をキャプチャし、XMLHttpRequest に基づくアウトオブバンド呼び出しをとおして同じ情報を送信します。また、UpdatePanel コントロールは、いくつかの付加的な情報を追加し、複数のコントロールのうちどの選択肢が処理され、サーバー側の部分的なページ レンダリング機能によってレンダリングされたのか示します。

UpdatePanel コントロールに所属しない動作は、interceptor パターンという形で完全に記述します。このパターンは、オブジェクト呼び出しの監視、および呼び出し側と受信側の間で独自コードの挿入を行うあらゆる外部コンポーネントを参照します。この方法で、外部コンポーネントは動作の制御を完全に取得し、タスクの実行方法を制御し、有効な応答を呼び出し側に返します。図 2 では、ASP.NET AJAX 環境におけるこのパターンの実装を示します。

図 2 UpdatePanel コントロールと Interceptor パターン
図 2 UpdatePanel コントロールと Interceptor パターン(画像を縮小するには、ここをクリックします)
図 2 UpdatePanel コントロールと Interceptor パターン
図 2 UpdatePanel コントロールと Interceptor パターン (画像を拡大するには、ここをクリックします)

クライアント ページの中に挿入されるコードは、フォームの onsubmit DOM イベントに対応するハンドラを登録します。この新しいハンドラは単純に、従来の要求を、XMLHttpRequest を使用して付加的な情報を渡すものに置き換えます。特に、入力フィールドから成る既存のリストに対するポストバックに対応する UpdatePanel コントロールの名前を追加します。ページング可能なグリッドを持つページに対応する、変更後の内容は次のとおりです。

ScriptManager1=UpdatePanel1|GridView1&
__EVENTTARGET=GridView1&
__EVENTARGUMENT=Page%241&
__VIEWSTATE=...&
__VIEWSTATEENCRYPTED=&
__EVENTVALIDATION=...

ScriptManager の擬似パラメータは、上記のポストバックに対応する UpdatePanel コントロールの ID を参照します。指定した UpdatePanel コントロールに関連付けられている一連のコントロールだけが更新され、それらに対応する変更後のマークアップが、更新後の viewstate 情報と共にブラウザに対して送信されます。

部分的なレンダリングは、さまざまな AJAX フレームワークに共通する機能ですが、内部の実装は異なっています。違いのほとんどは、更新可能なコントロールを要求に関連付けるために使用されているアプローチに関連しています。ASP.NET AJAX では、更新可能なコントロールは最も外側にあるコンテナ コントロール (UpdatePanel) の中にグループ化されています。このコンテナの中にあるすべてのものは、マークアップに対してレンダリングされます。つまり、これは、現在表示されているページに対するデルタ (差分) を表します。もう 1 つの代替アプローチでは、AJAX スタイルのポストバックをとおしてレンダリングを行うために、個別のコントロールがプログラムからフレームワークに対して登録を行う必要があります。これは、単一のコントロールの状態をサポートする複数のコントロールに関して、ASP.NET 2.0 環境で発生していることとほぼ同じです。

単一のコンテナの中に複数のコントロールをグループ化する方法より詳細、かつより柔軟性のあるアプローチは、AJAX スタイルのポストバックをレンダリングする方法を、個別のコントロールに決定させることです。ただし、付加的な codebehind 命令、またはアドホック コントロールを使用する必要があります。このアプローチは、コントロール ライブラリや他の類似製品にとっては適切ですが、ASP.NET のような Web フレームワークにとっては煩雑すぎます。

UpdatePanel コントロールは、System.Web.UI.Panel から派生したクラスです。UpdatePanel は、ASP.NET コントロールの子であり、UI のないコンテナとして、任意の ASP.NET サーバー コントロールを格納することができ、開発者は AJAX 対応の特殊なコントロールを使用する必要がありません。UpdatePanel を使用することにより、AJAX プログラミングは、従来の ASP.NET と同じアプリケーション モデルおよび同じプログラミング スタイルを活用して、サーバー上ですべてを実行することができます。開発者は、JavaScript やクライアント側の Microsoft AJAX Library について学ぶ必要がありません。同様に、XML Script やブラウザの DOM を習得する必要もありません。

従来の ASP.NET ページを AJAX ページに変換するのは、2 つの手順からなる操作です。最初に、ScriptManager コントロールをページに追加します。次に、部分的な更新、および UpdatePanel を使用した非同期更新の対象であるコントロールからなるグループをラップします。UpdatePanel コントロールは、シナリオのネストをサポートし、動的に作成することもできます。必要に応じて UpdatePanel コントロールを ASP.NET ページにいくつでも格納できます。

実際の更新は、次の 2 つの方法でトリガできます。子コントロールがポスト バックを実行するとき、および特定の外部イベントが発生するときです。トリガと他のプロパティをとおして、各 UpdatePanel コントロールが条件に基づいてその内容を更新するように設定できます。

UpdatePanel コントロールを使用すると、ASP.NET ページを段階的に AJAX に移行し、しかも非常に迅速に作業を進めることができます。実際、同じ ASP.NET ページが、AJAX スタイルのポストバックと従来のポストバックの両方を起動することもできます。この理由で、UpdatePanel は要求ごと、しかもすべての要求に対して、ページのビュー状態 (viewstate) を移動する必要があります。特に、ビュー状態はサーバーに対して送信され、選択されたコントロールに関する変更を反映して更新され、その後、クライアントに向かってダウンロードされます。同じことが、イベント検証データに関して発生します。ビュー状態とイベント検証データは、クライアント側で更新することはできません。また、この理由で、サーバーに対して送信され、改ざんされていないことを確認され、正しく更新される必要があります。ビュー状態とイベント検証データに関する最新のコピーがクライアント側で利用できることは、AJAX スタイルと従来のスタイルの両方で、ページを整合性のある状態に保ち、ポストバックを成功させるための鍵になります。

最終的には、UpdatePanel は ASP.NET AJAX を学ぶための最も簡単なアプローチです。アップグレードと移行の作業は可能な限り最小限に抑えられていて、従来のページと AJAX ページの間で完全な相互運用性があります。更新可能なパネルを使用する場合は、ページは非常に迅速に更新されます。実際、時にはこれらの更新は非常に高速なので、ユーザーが変化に気がつきません。この理由で、何が発生しているのか知らせる高速動作パネルは、十分ではない可能性があります。UpdateProgress コントロールのような高速動作パネルは、長時間のタスクを想定して設計されたものです。表示内容の変化についてユーザーにフィードバックを提供するものではありません。このような変化に関してユーザーにフィードバックを提供するためのより良好なツールは、UpdatePanelAnimation エクステンダです。これは、AJAX Control Toolkit の最新 CTP (Community Technology Preview) の中で定義されています。これのサンプルを、実際の動作 という形で表示できます。このエクステンダは、ポストバックの実行中に、UpdatePanel で囲まれた領域をアニメーション表示します。特に、エクステンダに対する更新中および更新後のイベントに対して、アニメーション表示を指定できます。ただし、AJAX Control Toolkit を個別にインストールする必要があることに注意してください。これは、ASP.NET AJAX Extensions ダウンロードの中に含まれていないからです。

アウトオブバンド呼び出しの制御

JavaScript に満足していて、クライアントからサーバー直接呼び出しを試したい場合は、主に 2 つのオプションがあります。Web サービスを使用する方法、および静的ページを呼び出す方法です。

ASMX Web サービスをスクリプト マネージャに登録することにより、JavaScript プロキシ クラスを生成し、それをクライアント ページに挿入するよう ASP.NET AJAX インフラストラクチャに対して指示することになります。アドレス バー (ローカル コンピュータ) に Web サービスの URL を入力し、その後に /js サフィックスを付けることにより、プロキシ クラスを観察できます。ここでは、2 つのパブリック メソッドを持つ Web サービスに対応する JavaScript プロキシの例を示します。このコードは、読みやすさを重視して簡略化したものです。

Type.registerNamespace('Samples.MyWebService');
Samples.MyWebService = new function()
{
    this.appPath = "http://YourServer/AjaxDemo/";
    var cm = Sys.Net.ServiceMethod.createProxyMethod;
    cm(this, "LookupCustomer", "id");
    cm(this, "LookupAllCustomers");
}

このコードを使用すると、スクリプト タグから Web サービスを呼び出すことができます。これは従来の Proxy パターンを使用するもので、全体のメカニズムは、従来の ASP.NET アプリケーションまたは Windows アプリケーションから、サーバー上にある Web サービスを使用する場合とまったく同じです。

ただし、いくつかの注意点があります。クライアントから、どのような種類のサーバー側コードを呼び出しますか?または、他の方法について考えると、クライアントから Web サービスに対してどのような操作を起動することを想定していますか?AJAX ページから起動される Web サービスは、何らかのビジネス ロジックをクライアント ページに対して公開します。パブリック エンドポイントである Web サービスは、インターネットから起動することが可能であり、アプリケーション ロジックを保護するためのセキュリティ層は組み込まれていません。この脆弱性は、新しいものではありません。

開発者は ASP.NET AJAX を使用すると、Web サービスをとおしてアプリケーション ロジックを公開する機会が得られます。インターネットに対して公開した後は、(どのようなセキュリティ層によっても保護されていない) ロジックが、任意の呼び出し側から利用できるようになります。一般的に、インターネットでの使用に対して安全であると考えられるデータとロジックだけを公開する必要があります。製品カタログを返す Web サービスを作成および起動する場合は、これは現実的な問題を招かない可能性もあります。一方、ビジネス ロジック層 (BLL) の公開は、たとえばその中にはクレジット カード確認プロセスも含まれますが、開発者が実行するべきことではありません。

ASP.NET AJAX を使用すると、Web サービスを活用して BLL をクライアント ページに対して公開し、直接の使用を実現することもできます。AJAX Web サービスは、重要ではないユーザー インターフェイス層 BLL の一種であると考えてください。BLL のうち機密性の高い部分は、従来のポストバックまたは UpdatePanel コントロールをとおして起動する必要があります。

Web サービスを ASP.NET AJAX ページに直接リンクするには、それらの Web サービスを、呼び出し側のページと同じ Web アプリケーションの中でホストされているローカル Web サービスにする必要があります。この要件には、2 つの主な理由があります。最初に、この Web サービスは、各要求に対して追加された /js サフィックスの処理方法を把握している ASP.NET AJAX アプリケーションによって支援されている必要があります。ただし、他の ASP.NET AJAX アプリケーションを使用してこの Web サービスをホストすることは可能です。この場合、クロスサイト呼び出しを防止している最新のブラウザで、より厳格なセキュリティ設定がなされている状況では、クライアント呼び出しがクラッシュする可能性があります。

ページ メソッドと Web サービス メソッド

サーバー コードを ASP.NET AJAX ページにバインドするために利用可能なもう 1 つのアプローチは、ページ メソッドを起動することです。呼び出し可能なページ メソッドは、codebehind クラスの中で定義され、Web サービス メソッドに関連して使用するために同じ WebMethod 属性で修飾された public static (または Visual Basic® .NET の Shared) メソッドです。現時点では、これは ASPX ページに限定されています。インライン コードと codebehind コードの両方です。ただし、将来は、ユーザー コントロールとカスタム コントロールに対して拡張される可能性があります。

ASMX Web サービスとページ メソッドでは、異なる設定が必要であることに注意してください。特に、スクリプト マネージャに対して Web サービスを登録し、ページ メソッドを実行するためにスクリプト HTTP モジュールをインストールする必要があります。図 3 に、web.config ファイルで必要なものを示します。

Figure 3 ASP.NET AJAX に対応するために Web.config に加える変更 (Close Figure 3)
<microsoft.web>
   <scripting>
      <webServices>
          ...
      </webServices>
   </scripting>
</microsoft.web>

<system.web>
    <httpHandlers>
    <remove verb="*" path="*.asmx"/>
        <add verb="*" path="*.asmx"
             type="Microsoft.Web.Script.Services.ScriptHandlerFactory"
             validate="false"/>
    </httpHandlers>
    <httpModules>
        <add type="Microsoft.Web.UI.ScriptModule" name="ScriptModule"/>
    </httpModules>
</system.web>

Web サービスを登録し、プロキシを強制的に生成するには、次のものが必要です。

<asp:ScriptManager ID="scriptManager1" runat="server">
  <Services>
     <asp:ServiceReference Path="~/WebServices/MyDataService.asmx" />
  </Services>
</asp:ScriptManager>

Web サービス クラスを修飾するために、付加的な属性も必要です。特に、ジェネリック タイプを除き、使用することを計画しているカスタム タイプごとに、JavaScript プロキシ クラスと GenerateScriptType の生成を有効にするための ScriptService 属性が必要です。図 4 に例を示します。

Figure 4 ASP.NET AJAX の Web サービス メソッド (Close Figure 4)
namespace Samples
{
    [WebService(Namespace = "http://samples.ajax/")]
    [ScriptService]
    [GenerateScriptType(typeof(Customer))]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class MyDataService : System.Web.Services.WebService
    {
        public MyDataService() {}

        [WebMethod]
        public Customer LookupCustomer(string id)
        {
            return CustomerManager.Load(id);
        }

        [WebMethod]
        public CustomerCollection LookupAllCustomers()
        {
            return CustomerManager.LoadAll();
        }
}

GenerateScriptType は、Customer 型に対して適用する必要があります。一方、CustomerCollection に関しては、この型がジェネリック Collection<T> クラス (このクラスは、ASP.NET AJAX に組み込みサポートされています) から派生するよう定義されている場合は、CustomerCollection に適用する必要はありません。

public class CustomerCollection : Collection<Customer> {}

ページ メソッドに関して必要とされている HTTP モジュールは、ASPX 要求に対応する既定の HTTP ハンドラを組み立て、レンダリング段階をコードのカスタム部分にリダイレクトします。このカスタム部分は、指定されたメソッドを実行し、戻り値をシリアル化します。

ページ メソッドを使用するアプローチと、Web サービス メソッドを使用するアプローチの両方とも、バックエンド BLL にアクセスするのに役立ちます。パブリックに公開された Web サービス メソッドと同様、ページ メソッドはブラウザのアドレス バーから簡単に自動化できます。これを実現するには、JavaScript Object Notation (JSON) に関するいくつかの基本的なスキルだけで十分です。したがって、これらのページを使用する場合は、どの BLL ロジックを公開するかに関して、WebMethods を使用する場合と同程度の注意を払う必要があります。

最後に、ページ メソッドと Web サービス メソッドのどちらを使用するかは、純粋に好みの問題です。どちらの場合でも、ファサード層を呼び出す必要があります。図 5 に示すように、その層は正しい BLL 関数へのルーティングを行います。Web サービス メソッドはどのページからも呼び出せます。ただし、サービスに対するエンドポイントを各ページに登録することが条件です。ページ メソッドは、それらのメソッドを定義したページのみから呼び出せます。現時点で、私の個人的な好みは、ページ メソッドを使用することです。

図 5 ページ メソッドと Web サービス メソッドの比較
図 5 ページ メソッドと Web サービス メソッドの比較(画像を縮小するには、ここをクリックします)
図 5 ページ メソッドと Web サービス メソッドの比較
図 5 ページ メソッドと Web サービス メソッドの比較 (画像を拡大するには、ここをクリックします)

ASP.NET AJAX の Web サービス メソッドとページ メソッドの両方とも、JSON を広範囲に使用して、データを広範囲に渡します。簡易型のデータ交換形式である JSON は、オブジェクトをシリアル化して文字列にする目的で使用されます。固有の簡潔さにより、JSON は人間とコンピュータの両方にとって適切です。SOAP パケットを読み書きできる人より、JSON 文字列を読み書きできる人が多くいます。また、コンピュータは JSON の解析と処理を非常に簡単に実行できます。

ページ メソッドは Web サービスより高いパフォーマンスを達成すると予期される可能性があります。結局、Web サービスの呼び出しを解決するには、ASP.NET ランタイムは SOAP パケットを解析する必要があります。ただし、これは厳密に正しい説明ではありません。ASP.NET AJAX は、すべての ASMX 要求をインターセプトする、カスタムメイドの HTTP ハンドラ (図 3 を参照) をインストールするからです。/js サフィックスの付いた要求は異なる方法で処理され、JSON のペイロードおよび Web サービス メソッドと共に直接的に作業されます。その結果、SOAP はまったく関与せず、要求の本体は単純に、入力引数に相当する JSON 文字列を格納しています。AJAX 以外の要求に関しては、新しい HTTP ハンドラは、SOAP を理解する元の ASP.NET ハンドラにコール バックを委任します。

私が ASP.NET AJAX を使用する方法

すべてのことを考慮すると、私としては開発チームの多数派にとって UpdatePanel が最善のアプローチであると考えます。これは従来の ASP.NET を排除するものではなく、都合に合わせて既存のページを変更することを可能にします。また、これは控えめであり、作業を開始する前に多くのことを学ぶ必要がありません。さらに、UpdatePanel を使用すると、従来の Web ページと同じレベルで BLL が保護され、長時間の作業を実行する非同期 ASP.NET ページも完全にサポートされます。

私の最後のアドバイスは、次のとおりです。さまざまな AJAX プラットフォームを組み合わせることを避けてください。JavaScript の組み込みオブジェクトの拡張に関しては、ASP.NET AJAX と他のフレームワークの間で競合が存在する可能性があります。より重要なこととして、複数の製品の組み合わせが現時点で動作しているとしても、将来も引き続き動作することは保証されません。いずれかのフレームワークのいずれかの新しいバージョンが、新しい競合を引き起こす可能性があります。

現在も同期処理中

AJAX の機能 は、多くの場合、非同期の手法に関連付けられるものです。その結果、多くの場合、ASP.NET AJAX は既定でプログラミングを非同期で実行すると想定されています。クライアントの観点では、これは確かに正解です£タスクはサーバー上で開始され、他の操作をするために、現在のページは引き続き使用可能だからです。

ただし、ASP.NET AJAX はASP.NET 2.0 の非同期ページをサポートしません。また、リモート呼び出しを非同期で実装する (非同期 HTTP ハンドラをとおして、という意味です) わけでもありません。AJAX Web サービス要求をホストする HTTP ハンドラは、同期で動作します。同じことは、ASP.NET ページが "非同期処理" というマークを付けられているかどうかにかかわりなく、ページ メソッドをサービスする HTTP モジュールにも当てはまります。これらアーキテクチャ上の問題のほとんどは、ASP.NET の将来のバージョン (現時点での開発コードネームは "Orcas"です) で解決される可能性が高いものです。ただし、現在のところは、UpdatePanel コントロールを使用して AJAX を実装するのが最善の方法です。

ご意見やご感想は、

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Esposito は、Solid Quality Learning の指導者であり、『Programming Microsoft ASP.NET 2.0』 (Microsoft Press 発行、2005 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで講演しています。cutting@microsoft.com (英語) で連絡を取るか、weblogs.asp.net/despos (英語) のブログに参加してください。


サービス ステーション: WCF の Service Factory

サービス ステーション
WCF の Service Factory
Aaron Skonnard

コンテンツ
Service Factory の開発プロセス
はじめに
データ コントラクト
メッセージ コントラクト
フォールト コントラクト
サービス コントラクトと実装
サービス エンドポイントを公開する
WCF セキュリティ ガイダンス パッケージ
セキュリティの選択肢をカスタマイズする
高度なカスタマイズ
まとめ

前回のコラムで、ソフトウェア ファクトリの概念を紹介し、Microsoft の Patterns & Practices (P&P) チームが開発した新しい Web Service Software Factory (Service Factory とも呼ばれます) のツアーを行いました。前回説明したとおり、ソフトウェア ファクトリは、パターンとベスト プラクティスを使用することで、ソフトウェアの構築プロセスをスピードアップするのに役立つガイダンス資産の集まりです。前回のコラムをお読みでない方は、まず、私の 2006 年 12 月のコラム をお読みください。そうすれば、今回の資料をより理解できます。

今月も Windows® Communication Foundation の Web Service Software Factory に目を向けながら、ソフトウェア ファクトリの説明を続けます。話を進めていく中で、ガイダンス パッケージをカスタマイズする方法を示します。このパッケージは、Visual Studio® 2005 と連携し一般的な開発作業を自動化する一揃いのコード レシピを提供します。

使用できる Service Factory のガイダンス パッケージは、大きく分けて 2 種類あります。1 つは、ASP.NET Web サービス (ASMX) コードを生成し、もう 1 つは、Windows Communication Foundation コードを生成します。ASMX バージョンは既に MSDN® で公開されていますが、Windows Communication Foundation バージョンの方は、この記事を執筆している時点ではまだ開発中です。それでも、Windows Communication Foundation ガイダンス パッケージのコミュニティ ドロップは現在、GotDotNet で入手可能です (practices.gotdotnet.com/svcfactory (英語) を参照してください)。

前回は、ASMX ガイダンス パッケージについて説明しましたが、Windows Communication Foundation バージョンを調べる時間はありませんでした。そこで、今回は Windows Communication Foundation ガイダンス パッケージについて詳しく説明します。ただし、Windows Communication Foundation ガイダンス パッケージの細部については、最終リリースまでに変更される可能性があることに注意してください。

Service Factory の開発プロセス

最初に、Service Factory の開発プロセスを簡潔に説明します。P&P チームは、透過的でアジャイルな開発方法を採用しました。同チームは、オンライン コミュニティに定期的にドロップを公開して、フィードバックを求めています。しかし、ここで注目したいのは、チームがコミュニティ フィードバックを実際にどう使用して、製品の次のバージョンの改良に役立てているのか、ということです。開発者コミュニティが、P&P の成果物のでき具合にどれほど大きい影響を及ぼし得るのかを、私は直接見てきました。これは、参加を希望する開発者にとって大きな価値となり、最終的には、成果物の品質が向上します。これは、関係者すべてにとっての利益にもつながります。

開発プロセスのもう一方の興味深い面は、開発者たちが自分の仕事を推進するために、参照実装にどれほど依存しているのか、ということです。図 1 に示したように、現実的な参照実装を開発することは、検討中の推奨プラクティスの適用性を検証できるだけではなく、そうしていなければ気づかなかったような新しいプラクティスを発見する助けにもなります。推奨プラクティスの最終セットは結局、記述されたパターン、ハウツー物のトピック、そしてもちろん、ガイダンス パッケージなど、Service Factory に付属して公開されるあらゆるものに影響を及ぼします。

図 1 Software Factory の開発プロセス
図 1 Software Factory の開発プロセス(画像を縮小するには、ここをクリックします)
図 1 Software Factory の開発プロセス
図 1 Software Factory の開発プロセス (画像を拡大するには、ここをクリックします)

はじめに

Windows Communication Foundation ガイダンス パッケージをダウンロードして、インストールすると、Visual Studio 2005 New Project ダイアログ ボックスに表示されます。Windows Communication Foundation ガイダンス パッケージを使用するには、まず始めに Windows Communication Foundation Service テンプレートを選択し、新しいソリューションの名前を入力します。次に、[OK] をクリックして、最初のソリューション構造を生成するレシピを起動します。最初のソリューション構造が、ASMX ガイダンス パッケージで使用された構造と非常に似ていることに気付くはずです。

最も大きな相違点は、サービス層にあります。サービス層には、DataContracts、FaultContracts、ServiceContracts、および ServiceImplementation の 4 つのプロジェクトが含まれます。これは、構築することになる、対応する Windows Communication Foundation の成果物を編成するための論理構造を提供します。主な変更点は、フォールト コントラクトのプロジェクトです。このプロジェクトは、Windows Communication Foundation が導入した新しい概念です。別の変更点は、サービス ホストが Tests フォルダに置かれるようになった、ということです。

この最初のソリューション構造が自分のニーズを満たさない場合は、ソリューションをアンロックし再編成することによって、変更を加えることができます。ソリューションを自分の好みに合わせて編成した後は、プロジェクトの担当責任を再度割り当てることができます。つまり、各プロジェクトでどのレシピを利用できるようにしたらよいのかを Service Factory に伝えます。その方法については、前回のコラムで説明しました。

それでは、サービス層の変更点を詳しく掘り下げましょう。Windows Communication Foundation サービス層の実装は、複数の手順から成ります。最初に、メッセージの中で使用するデータ コントラクトを定義します。次に、操作を作り上げる、メッセージ、フォールト、およびサービス コントラクトをモデル化します。そして最後に、サービス コントラクトを実装して、サービス エンドポイントを公開します。

Windows Communication Foundation ガイダンス パッケージは、これらの作業それぞれを実行するためのレシピを数多く提供します。Visual Studio 内で、[Guidance Navigator Window] ([表示]、[その他のウィンドウ] の順にクリックします) を開くと、さまざまな Windows Communication Foundation 特有のレシピが表示されます。これらのレシピは、Windows Communication Foundation ガイダンス パッケージ内で提供されます (図 2)。

図 2 Guidance Navigator
図 2 Guidance Navigator

データ コントラクト

Windows Communication Foundation ガイダンス パッケージは、データ コントラクトを生成するいくつかの方法を提供します。XML スキーマ定義が既に用意されている場合には、XML スキーマ定義 (XSD) 自体からデータ コントラクトの型を生成することができます。レシピは、Schema からの Create XML Serializable 型と呼ばれる ASMX レシピに似ており、可能な場合に [DataContract] 型だけを生成します。Windows Communication Foundation における DataContractSerializer は、XML スキーマ言語のサブセットだけをサポートするので、スキーマがそのサブセット外のもの (XML 属性など) を使用すると、レシピはフォールバックし、XmlSerializer 互換の型を生成します。

新しいデータ コントラクトを最初から設計するときは、Create Data Contract レシピを実行できます。すると、ウィザードが起動します。クラス名、およびデータ コントラクト名 (XML 名) と名前空間を指定するよう要求されます。次に、データ コントラクトのメンバを指定します。Order や IsRequired などのさまざまな [DataContract] 詳細を指定できる点に注意してください (図 3)。[完了] をクリックすると、図 4 に示したようなクラスが生成されます。

Figure 4 WinePreference クラス (Close Figure 4)
[DataContractAttribute(
 Namespace = "http://CohoWinery.DataContracts/2006/09",
 Name = "WinePreference")]
public class WinePreference
{
    private string country;
    private string region;
    private string type;

    [DataMemberAttribute(Name = "Country", Order = 1)]
    public string Country
    {
        get { return this.country; }
        set { this.country = value; }
    }
    ... // remaining properties omitted
}
図 3 Create Data Contract レシピ
図 3 Create Data Contract レシピ(画像を縮小するには、ここをクリックします)
図 3 Create Data Contract レシピ
図 3 Create Data Contract レシピ (画像を拡大するには、ここをクリックします)

また、Decorate 型を DataContract レシピとして使用することによって、既存のクラス定義をデータ コントラクトに変えることができます。このレシピは、さまざまな [DataContract] 属性を既存のクラスに適用するプロセスを自動化します。また、Validate DataContract という名前のレシピもあります。このレシピは、DataContractSerializer によるシリアル化が可能かどうかを確認するために選択されたクラスを分析します。

メッセージ コントラクト

サービス コントラクトの定義を開始する前に、各操作で使用するメッセージ コントラクトも定義する必要があります。ASMX ガイダンス パッケージでは、この作業を “メッセージ タイプの作成” と呼んでいました。ASMX では、メッセージ タイプは、要求または応答のメッセージ全体を表す XmlSerializer の型にすぎません。このような型を ASMX のラップされないパラメータ スタイルで使用した場合、型自体が SOAP 本文で使用される XML の全体構造を定義します。

Windows Communication Foundation では、メッセージ タイプは何の制約も受けません。メッセージ タイプの定義は、さまざまな属性のセット ([MessageContract]、[MessageHeader]、および [MessageBodyHeader]) で行います。メッセージ コントラクトを使用すると、SOAP エンベロープ全体の構造をモデル化できます。どのフィールドが SOAP ヘッダーに入り、どれが本文に入るのかを指定できます。また、メッセージ コントラクトで、メッセージをラップするかどうか (ASMX のパラメータ スタイルと同様です) を指定することもできます。メッセージ コントラクトで指定しない場合には、Windows Communication Foundation が、操作名を表す要素内ですべてのメッセージをラップします。

Windows Communication Foundation では、データ コントラクトまたはメッセージ コントラクトの型のどちらかを使用して、操作を定義できます。そこで疑問なのは、どういう場合にどちらのアプローチを採用したらよいのか、ということです。P&P による一般的なガイダンスとしては、再利用できる型 (たとえば、5 つの異なったメッセージで使用する必要がある型) を処理しているときはいつでも、データ コントラクトを使用する方をお勧めします。そして、再利用されない要求/応答メッセージを表す型を定義しているときは、メッセージ コントラクトを使用するとよいでしょう。また、メッセージ コントラクトを使用すると、将来カスタム ヘッダー処理を柔軟に導入して、カスタム ヘッダー処理を設計時の決定要因にすることができるようになる場合もあります。

データ コントラクトに関してメッセージ タイプを定義する場合には、データ コントラクト レシピを引き続き使用できます。Windows Communication Foundation メッセージ コントラクトを定義する場合には、専用のレシピを使用します。メッセージ コントラクト レシピは、既存のデータ コントラクトからメッセージ コントラクトを設計するプロセスを詳しく示します。そして、[MessageContract] 属性を持つクラス定義を生成します。

フォールト コントラクト

カスタム SOAP フォールトをクライアントに返すことを計画している場合、フォールト要素内で返す予定の型を定義する必要があります。Windows Communication Foundation では、データ コントラクト定義を使用することでこれらのフォールトの型を定義します。データ コントラクトは、フォールトの詳細要素にシリアル化される XML の構造をモデル化します。次に、詳細な型を指定する [FaultContract] で、操作コントラクトに注釈を付けます。[FaultContract] が複数のフォールトの型を返す場合は、何度も操作に適用できます。

[FaultContract] 注釈により、適切なフォールト情報をサービス メタデータに含めるのに十分な情報、つまり、XSD および Web Services Description Language (WSDL) 定義が、Windows Communication Foundation に与えられます。その結果、クライアントは、カスタム フォールトを処理するために必要なクライアント側コードを生成することができます。これにより、Windows Communication Foundation のクライアントが、FaultException<T> を使用してユーザー定義の例外をキャッチすることができます。

Windows Communication Foundation ガイダンス パッケージは、カスタム フォールトを生成するためのレシピを提供します。このレシピを起動すると、新しいフォールトの型に名前を付けて、DefaultFaultContract 基底クラスを使用するかどうかを指定するように要求されます (図 5)。

Figure 5 DefaultFaultContract 基底クラス (Close Figure 5)
[DataContractAttribute(
 Namespace="http://FaultContracts/Default/DefaultFaultContract",
 Name="DefaultFaultContract")]
public class DefaultFaultContract
{
    int errorId;
    string errorMessage;
    Guid correlationId;

    public DefaultFaultContract() : this(-1, String.Empty, Guid.Empty)
    {
    }

    public DefaultFaultContract(
        int errorId, string errorMessage, Guid correlationId)
    {
        this.errorId = errorId;
        this.errorMessage = errorMessage;
        this.correlationId = correlationId;
    }

    [DataMemberAttribute(IsRequired = true, Name = "ErrorId", Order = 1)]
    public int ErrorId
    {
        get { return errorId; }
        set { errorId = value; }
    }

    [DataMemberAttribute(IsRequired = true, Name = "ErrorMessage",
        Order = 2)]
    public string ErrorMessage
    {
        get { return errorMessage; }
        set { errorMessage = value; }
    }

    [DataMemberAttribute(IsRequired = true,
        Name = "CorrelationId", Order = 3)]
    public Guid CorrelationId
    {
        get { return correlationId; }
        set { correlationId = value; }
    }
}

既定の基底クラスは、例外処理スキーマで利用できる一般的なエラー関連のフィールドを供給します。また、カスタム フォールトの型に追加メンバを定義することもできます。レシピは、図 6 に示したような新しいデータ コントラクトの型を生成します。

Figure 6 新しいデータ コントラクト (Close Figure 6)
[DataContractAttribute(
   Namespace = "http://CohoWinery.FaultContracts/2006/09",
   Name = "RegistrationError")]
public class RegistrationError : DefaultFaultContract
{

    private string registrationDetails;

    [DataMemberAttribute(Name = "RegistrationDetails", Order = 1)]
    public string RegistrationDetails
    {
        get { return this.registrationDetails; }
        set { this.registrationDetails = value; }
    }
}

データ、メッセージ、およびフォールト コントラクトの準備がすべて整ったら、サービス コントラクトの定義を始めることができます。

サービス コントラクトと実装

データ コントラクトと同様に、Windows Communication Foundation ガイダンス パッケージを使用してサービス コントラクトを定義する方法は、いくつかあります。実装する必要がある WSDL 定義が既にあるときは、WSDL からサービス コントラクトを生成するレシピを実行できます。これにより、XSD と WSDL が関係するコラボレーションが絶対に必要となる WSDL 駆動のシナリオが簡単になります。

また、Windows Communication Foundation ガイダンス パッケージに収録されている Create Service Contract レシピを実行することによって、新しいサービス コントラクトを最初から定義できます。このレシピは、.NET インターフェイス名、サービス コントラクト名 (WSDL 名)、および名前空間を指定するよう要求します。また、サービス コントラクトのセッションを必要とするかどうかをたずねます。

図 7 サービス コントラクトの作成
図 7 サービス コントラクトの作成(画像を縮小するには、ここをクリックします)
図 7 サービス コントラクトの作成
図 7 サービス コントラクトの作成 (画像を拡大するには、ここをクリックします)

これで、操作を定義する準備が整いました (図 7)。各操作に対して、動作、要求と応答の型 (データ型とメッセージ コントラクト型のどちらか)、フォールトの型、および Windows Communication Foundation セッション使用中のセッション コントラクトに関連する追加の詳細を指定できます。操作の定義が完了すると、レシピは、次のような Windows Communication Foundation サービス コントラクトの定義を生成します。

[ServiceContractAttribute(
    Namespace = "http://CohoWinery.ServiceContracts/2006/09",
    Name = "ClubMembershipService")]
[ServiceKnownTypeAttribute(typeof(RegistrationError))]
public interface IClubMembershipService
{
    [OperationContractAttribute(Action = "RegisterNewMember")]
    [FaultContractAttribute(typeof(RegistrationError))]
    void RegisterNewMember(RegisterNewMember request);
    ...
}

このサービス コントラクトを実装するには、Implement Service Interface レシピを実行します。このレシピは、次に示すように、サービス コントラクトを表す .NET インターフェイスを実装し、各メソッドを消すクラスを生成します。

public class ClubMembershipService : IClubMembershipService
{
    #region IClubMembershipService Members

    public void RegisterNewMember(RegisterNewMember request)
    {
        throw new Exception(
          "The method or operation is not implemented.");
    }

    #endregion
}

要求と応答の型に XmlSerialzer 互換型を選択すると、レシピは賢いことに、[XmlSerializerFormat] 属性をその特定の操作コントラクトに適用します。すると、このコントラクトが、実行時に XmlSerializer を使用するよう Windows Communication Foundation ディスパッチャに指示します。

そして、各サービス操作の実装を終了するのは、開発者であるあなたの仕事です。その際には、Create Service Contract Translator レシピを使用して、メッセージ タイプとビジネス エントリ タイプを相互に翻訳するコードを生成できます。これは、実のところ、Service Factory アーキテクチャにおけるサービス実装の主要な役割なのです。

サービス エンドポイントを公開する

最後に、サービス実装をテストするため、ホストして少なくとも 1 つのサービス エンドポイントを公開する必要があります。これは、Expose Service レシピで実行できます。このレシピは、Host プロジェクトで利用できるようになります。このレシピを実行するには、サービス実装クラスの名前を指定し、バインドの型を選択して、[完了] をクリックします。Windows Communication Foundation ガイダンス パッケージが現在提供しているガイダンスは、主な HTTP バインド (basicHttpBinding と WSHttpBinding) だけです。レシピは .svc ファイルを生成します。このファイルは、次に示されているように、サービス実装クラスを参照します。

<%@ServiceHost language="c#" Debug="true"
 Service="CohoWinery.ServiceImplementation.ClubMembershipService" %>

また、図 8 に示した <system.serviceModel> 構成をホストの web.config ファイル内に生成します。レシピにより、WSDL メタデータ検索と WS-Metadata Exchange (MEX) エンドポイントが、既定で有効になっているのに気付くはずです。したがって、この時点で .svc エンドポイントを参照し、生成されたメタデータを表示できます。そして、クライアント テスト アプリケーション内で Add Service Reference レシピを実行することもできます。このレシピは、MEX を使用してサービスについて照会し、エンドポイントを選択するよう要求します。次に、選択したエンドポイントを呼び出すのに必要なクライアント プロキシ コードと構成を生成します。

Figure 8 生成された ServiceModel 構成 (Web.config) (Close Figure 8)
<system.serviceModel>
  <behaviors>
   <serviceBehaviors>
    <behavior name=
      "CohoWinery.ServiceImplementation.ClubMembershipService_Behavior">
     <serviceDebug includeExceptionDetailInFaults="false" />
     <serviceMetadata httpGetEnabled="true" />
    </behavior>
   </serviceBehaviors>
  </behaviors>
  <bindings>
   <basicHttpBinding>
    <binding name="basicHttpBinding_
       CohoWinery.ServiceContracts.IClubMembershipService">
     <security mode="None" />
    </binding>
   </basicHttpBinding>
  </bindings>
  <services>
   <service behaviorConfiguration=
    "CohoWinery.ServiceImplementation.ClubMembershipService_Behavior"
    name="CohoWinery.ServiceImplementation.ClubMembershipService">
    <endpoint
       binding="basicHttpBinding" bindingConfiguration=
        "basicHttpBinding_
         CohoWinery.ServiceContracts.IClubMembershipService"
       contract="CohoWinery.ServiceContracts.IClubMembershipService" />
    <endpoint address="mex" binding="mexHttpBinding"
       contract="IMetadataExchange" />
   </service>
  </services>
 </system.serviceModel>

Windows Communication Foundation には、バインド構成、動作、およびホスト環境のためのオプションが、非常に多くあります。バインド構成に関して特別な注意を必要とするのが、セキュリティです。

WCF セキュリティ ガイダンス パッケージ

Web サービス セキュリティの分野は、広大です。WS-Security と関連する仕様は、さまざまな種類のセキュリティ トークンでサービスをセキュリティ保護するための可能性を数多く提供します。その結果、柔軟性は向上しますが、セキュリティ関連のガイダンスと自動化もさらに要求されることになります。

そのため、Windows Communication Foundation Service Factory には、Windows Communication Foundation セキュリティ向けの別のガイダンス パッケージが付属します。このガイダンス パッケージを有効にするには、Visual Studio 内で Guidance Package Manager を開き、[Enable / Disable Packages] をクリックします。いったん有効にすると、Guidance Navigator を使用してセキュリティ関連のガイダンスを参照できます (図 9)。また、Host プロジェクトを右クリックすると、[Service Factory - Security] という新しいコンテキスト メニューが表示されます。

図 9 WCF セキュリティのレシピ
図 9 WCF セキュリティのレシピ(画像を縮小するには、ここをクリックします)
図 9 WCF セキュリティのレシピ
図 9 WCF セキュリティのレシピ (画像を拡大するには、ここをクリックします)

最も一般的なセキュリティ パターンとプラクティスを自動化するレシピが、いくつか利用可能になっていることに気付くはずです。これには、相互証明書認証、Kerberos の他に、ID ストアとして Windows アカウント、SQL Server™、または Active Directory® Application Mode (ADAM) のいずれかを使用する直接認証などがあります。また、これらのレシピにより、メッセージがサーバー証明書で保護される一方でクライアント認証はまったく不要な匿名の認証も提供されます。

それでは、一般的なシナリオを見てみましょう。SQL Server による直接認証です。レシピを実行すると、最初にウィザードが表示されます。ウィザードの最初の画面には、この特定のレシピが実装するセキュリティ パターンについての説明、および P&P の Web サイトへのリンクが記載されています。このサイトには、パターンに関するさらに詳細な説明があります。ウィザードの続きでは、Windows Communication Foundation エンドポイント、バインド、動作の詳細に加えて、使用するサーバー証明書、必要なメッセージ保護のレベル、および SQL Server のロール プロバイダの詳細を指定するプロセスが自動化されます。ウィザードが完了すると、Host プロジェクトの web.config ファイルに、該当する Windows Communication Foundation 構成が生成され、いつでも使用できるようになります。他のセキュリティ レシピも同様に使いやすいものです。

サービスのセキュリティ保護が完了すると、クライアント アプリケーション内で次回 Add Service Reference レシピを実行するときに、セキュリティ設定がクライアント構成にインポートされます。その際、資格情報などの追加の詳細事項が、レシピにより要求されることがあります。

この種のセキュリティの自動化は、非常に有益です。アーキテクトと開発者は、自分たちが実績のあるセキュリティ上のパターンとプラクティスに従っていることを確信できます。Windows Communication Foundation ガイダンス パッケージは進化し続けているので、セキュリティに関するさらに高度な技術を期待できます。

セキュリティの選択肢をカスタマイズする

セキュリティ レシピにより有益な自動化を図れますが、正しいセキュリティ レシピを選択することは、依然として開発者に残された責任です。正しく選択した後でも、レシピによっては扱う際にいくつもの決定を下さなければなりません。アーキテクトとして、プロセスをさらに簡素化し、アーキテクト独自のセキュリティ基準を徹底させるため、公開相手の開発者が利用できる選択肢のセットを制限する必要があるかもしれません。

Windows Communication Foundation セキュリティガイダンス パッケージでは、利用できるセキュリティ設定を変更するためのレシピを提供することによって、セットの制限を可能にしています。ソリューションを右クリックして、[Service Factory - Security]、[Modify Security Settings] の順にクリックします。ユーザーが利用できるセキュリティ シナリオのセットを減らすことができるウィザードが、表示されます。たとえば、開発者が Kerberos か X.509 資格情報だけを使用できるようにするには、その他のオプションの選択を解除するだけです。また、メッセージ保護レベルのオプションを制限することもできます。たとえば、開発者が Sign and Encrypt オプションだけを使用できるようにするには、その他のオプションをすべて無効にします。

ウィザードが完了すると、ソリューション構成が更新されます。それ以降は、ガイダンス パッケージにより、指定したセキュリティ オプションだけが利用できるようになります。たとえば、Guidance Navigator または Service Factory のコンテキスト メニューに戻ると、Kerberos と X.509 レシピだけがリストに表示されます。さらに、利用できるレシピのいずれかを操作するときも、Sign and Encrypt オプションだけがメッセージ保護オプションに表示されます。

ツールキット間での相互運用性を保証する必要があるときは、セキュリティの選択肢が非常に重要になります。したがって、一般的な相互運用性のシナリオをさらに簡素化するために、Windows Communication Foundation セキュリティ ガイダンス パッケージには、特定の相互運用性シナリオを想定して、セキュリティの選択肢を制限する別のウィザードも用意されています。このウィザードを起動するには、ソリューションのコンテキスト メニューの [Modify Interop Settings] をクリックします。

使用しているシステムで、Web Services Enhancements (WSE) 3.0 クライアントから Windows Communication Foundation サービスをすべて呼び出せるようにする必要があるなら、それを可能にする Windows Communication Foundation の各種セキュリティ オプションを開発者に表示するだけで済みます。そのためには、WSE 3.0 か Windows Communication Foundation のオプションを選択します。そして、両方のツールキットがサポートする個別の仕様に関するセキュリティの選択肢を、さらに制限することができます。すると、次回セキュリティ レシピを実行するときに、指定された選択肢だけが利用できるようになります。最終的には、より多くの相互運用性のシナリオがサポートされる予定です ("BEA WebLogic Portal 9.2 クライアントが消費する Windows Communication Foundation サービス" のようなシナリオも対象です)。

この両方のウィザードが、ソリューションに関連する SecurityConfiguration.xml というファイルを生成します。このファイルは、どのオプションを利用できるようにしたらよいのかを Windows Communication Foundation セキュリティ ガイダンス パッケージに伝えます。全員に同じオプションを確実に表示するため、カスタム ガイダンス パッケージの一部としてこのファイルを再配布することができます。

高度なカスタマイズ

私の個人的意見としては、Microsoft ソフトウェア ファクトリ イニシアチブの最も貴重な特長は、各組織専用のカスタム ガイダンス パッケージを作成できる機能です。P&P が作成した一般的なレシピの他に、ドメイン固有の独自のレシピを活用できることから、興味をそそられるようなレシピの組み合わせが可能となります。共通の作業をできるだけ多く自動化すると、最終的に生産性の向上、品質の向上、一貫性、および予見可能性が組織全体で達成されます。

このコラムで見てきたように、ASMX と Windows Communication Foundation ガイダンス パッケージは、簡単にカスタマイズできます。たとえば、ソリューション構造を変更したり、プロジェクト責任の割り当てをやり直したり、パッケージによって利用可能にされたセキュリティまたは相互運用性の設定を変更したりできます。ただし、これより高度なものとなると、Guidance Automation Toolkit (GAT) に精通し、独自のガイダンス パッケージを構築して展開する方法を学ぶ必要があります。

Web 上には、GAT についての詳しい記事やその他の資料があります。また、Service Factory for ASMX をインストールした場合は、新しいガイダンス パッケージを作成するためのプロセスについて手順を追って説明する「How to: Create a New Guidance Package that Includes your Configuration Settings」(英語) というドキュメントが用意されています。このレベルのカスタマイズに関心がある場合は、このドキュメントを必ず最初にお読みください。

まとめ

Windows Communication Foundation のための Service Factory は、ガイダンスが渇望されている .NET Framework 3.0 の世界に貴重なガイダンス資産をもたらします。Windows Communication Foundation ガイダンスは、広く認められたセキュリティ パターンに従う Windows Communication Foundation サービスをさらに迅速に開発するのに役立ちます。.NET Framework 3.0 の公開時期については、公式の発表をお待ちください。

最終バージョンには、このコラムを書いている時点ではまだ利用できない新機能がいくつか搭載される見込みです。たとえば、コントラクトの設計 (データ、メッセージ、およびサービス)、コントラクトのバージョン管理、メッセージ検証、展開、大きいメッセージの処理に関するガイダンスに加えて、ワークフローを Windows Communication Foundation のサービス実装に組み入れるパターンに関するガイダンスもあります。また、特定のサービス構成 (WSE Policy Advisor と同様のもの) のセキュリティ設定を検証する機能の開発も進んでいます。このコラムが公開されるときまでには、これらの機能の多くが既に完成しているかもしれません。

GotDotNet の Service Factory Community (英語) に参加して、皆様のこれまでの経験に基づくフィードバックや、将来の機能に向けて役立つ情報をお寄せください。詳細については、「その他のリソース」のリンクを参照してください。

その他のリソース
  • Microsoft patterns & practices Developer Center (英語)
  • Web Service Software Factory on MSDN (英語)
  • Web Service Software Factory Community (英語)
  • Introduction to the Guidance Automation Toolkit (英語)

Aaron の連絡先は sstation@microsoft.com.

(英語) です。
Aaron Skonnardは、Microsoft .NET のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Aaron は、Pluralsight が提供する Applied Web Services 2.0、Applied BizTalk Server 2006、および Introducing Windows Communication Foundation の各コースの著者です。Aaron はこれまで何年もの間、コースの開発、会議での講演、およびプロ開発者の指導に従事してきました。連絡先は pluralsight.com/aaron (英語) です。


基礎: キューに登録する WCF 応答サービスを構築する

基礎
キューに登録する WCF 応答サービスを構築する
Juval Lowy

コードのダウンロード : : Foundations2007_02.exe (188KB)
コンテンツ
キューに登録された呼び出し
応答サービス
応答サービス コントラクトを設計する
メッセージ ヘッダーを使用する
ResponseContext クラス
クライアント側のプログラミング
サービス側のプログラミング
応答サービスのプログラミング
まとめ

Windows Communication Foundation (WCF) では、クライアントとサービスが接続されていなくても作業を行うことができます。クライアントがメッセージをキューに投稿すると、サービスが後で処理します。このような対話では、既定の要求/応答パターンとは異なるプログラミング モデルが可能となります。たとえば、いくつか挙げるだけでも負荷の均一化、可用性の向上、補正作業などが可能となります。このコラムでは、最初に Windows® Communication Foundation のキューに登録された呼び出しを短く紹介します。次に、キューに登録された呼び出しから結果を得る方法という興味深い問題、Windows Communication Foundation のいくつかの賢いプログラミング技法を駆使したソリューション、およびこのために書き起こしたヘルパー クラスについて説明します。

キューに登録された呼び出し

Windows Communication Foundation では、NetMsmqBinding を使用して、キューに登録された呼び出しをサポートします。Windows Communication Foundation は、メッセージの転送に TCP でも HTTP でもなく、Microsoft® Message Queuing (MSMQ) を使用します。クライアントは、Windows Communication Foundation メッセージをライブ サービスに送信するのではなく、MSMQ キューに投稿します。どのクライアントも、サービスのエンドポイントではなくキューを認識して、対話を行います。その結果、呼び出しは必然的に非同期となり、接続されない状態となります。呼び出しが実行されるのは、サービスがメッセージを後で処理するときです。

Windows Communication Foundation メッセージから MSMQ メッセージへのダイレクト マッピングは存在しないことに注意してください。1 つの MSMQ メッセージに、Windows Communication Foundation メッセージを複数含めることも、コントラクト セッション モードに従って 1 つだけ含めることもできます。必須のセッション モードでは、複数の Windows Communication Foundation 呼び出しを 1 つの MSMQ メッセージに含めることができます。また、許可済み/未許可のセッション モード (呼び出しごとのサービスと単独サービスで使用) では、Windows Communication Foundation の各呼び出しは、別々の MSMQ メッセージに入れられます。

他の Windows Communication Foundation サービスと同様に、クライアントはプロキシと対話します (図 1)。プロキシは MSMQ バインディングを使用するように構成されているので、Windows Communication Foundation メッセージをどのサービスにも送信しません。むしろ、呼び出しを MSMQ メッセージに変換し、エンドポイントのアドレスに指定されたキューに投稿します。

図 1 WCF のキューに登録された呼び出しのアーキテクチャ
図 1 WCF のキューに登録された呼び出しのアーキテクチャ(画像を縮小するには、ここをクリックします)
図 1 WCF のキューに登録された呼び出しのアーキテクチャ
図 1 WCF のキューに登録された呼び出しのアーキテクチャ (画像を拡大するには、ここをクリックします)

サービスの側では、キューに登録されたエンドポイントを持つサービス ホストが起動されると、ホストはキュー リスナをインストールします。キュー リスナは、キューにメッセージがあることを検知し、メッセージをキューから取り出して、インターセプタをつなげたチェーンをホスト側に作成します。このチェーンの終端には、ディスパッチャがあります。ディスパッチャは、通常どおりサービス インスタンスを呼び出します。複数のメッセージをキューに投稿する場合、メッセージがキューから出て行くと同時に、リスナは新しいインスタンスを作成することができます。こうして、非同期、非接続の同時呼び出しが誕生します。

ホストがオフラインの場合、メッセージはキューで保留状態として扱われます。そしてホストが次回オンラインになったときに、メッセージがサービスに転送されます。

キューに対して潜在的に非接続にされた呼び出しが、値を返すことはありません。サービスがキューにディスパッチされるときに起動されるサービス ロジックは、1 つもないからです。さらに、呼び出しがサービスにディスパッチされ処理されるのは、クライアント アプリケーションがシャットダウンした後です。この時点では、戻り値の処理を担当できるクライアントはありません。同様に、呼び出しはクライアントにサービス側の例外を何も返すことができません。しかも、例外をキャッチして処理するクライアントが存在しないこともあります。操作を起動してクライアントをブロックすることはできない (より正確に言えば、クライアントは極めて短時間だけブロックされるにすぎない) ので、クライアントはメッセージをキューに登録します。キューに登録された呼び出しは、クライアントからすれば必然的に非同期になります。これは、一方向呼び出しの古典的な特性です。その結果、NetMsmqBinding を使用するエンドポイントによって公開されるコントラクトが行える操作は、一方向だけになります。Windows Communication Foundation では、これをサービスとプロキシの読み込み時に検証します。

//Only one-way calls on queued contracts
[ServiceContract]
interface IMyContract
{
   [OperationContract(IsOneWay = true)]
   void MyMethod();
}

MSMQ との対話はバインディングにカプセル化されているので、呼び出しがキューに登録されているという事実に関係するサービスとクライアントの呼び出しコードには、何もありません。サービスとクライアントの呼び出しコードは、Windows Communication Foundation の他のクライアントとサービスのコードと同じです (図 2)。

Figure 2 キューに登録されたサービスの実装と破棄 (Close Figure 2)
Service Side
[ServiceContract]
interface IMyContract
{
   [OperationContract(IsOneWay = true)]
   void MyMethod();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod() {...}
}
Client Side
MyContractClient proxy = new MyContractClient();
proxy.MyMethod();
proxy.Close();

キューに登録されたサービスのエンドポイントを定義するときには、エンドポイント アドレスにキューの名前と種類 (パブリックまたはプライベート) を含める必要があります。

<endpoint
     address = "net.msmq://localhost/private/
         MyServiceQueue"
   binding = "netMsmqBinding"
   ...
/>

最後になりますが、MSMQ は Windows Communication Foundation のトランザクション リソース マネージャです。キューがトランザクション対応の場合、クライアントが投稿したメッセージは、クライアントのトランザクションが中断するとロールバックされます。サービス側では、キューからメッセージを取り出して読むことにより、新しいトランザクションが開始されます。サービスがそのトランザクションに関わっていて、(おそらく例外の結果として) トランザクションを中断すると、メッセージはキューにロールバックし、次のリトライを待ちます。Windows Communication Foundation では、高度な障害検知と有害メッセージ処理をサポートしていますが、このコラムの範囲を超えているので、ここでは取り扱いません。

応答サービス

これまで説明してきたキューに登録された呼び出しのプログラミング モデルは、一方的なものです。クライアントが一方向メッセージをキューに投稿すると、サービスが後でそのメッセージを処理します。キューに登録された操作が、設計上一方向の呼び出しの場合には、このモデルで十分です。ただし、キューに登録されたサービスは、呼び出しの結果、返された結果、およびエラーについてもクライアントにレポートする必要がある場合があります。しかし、既定ではこれは不可能です。Windows Communication Foundation では、キューに登録された呼び出しを一方向呼び出しと同じものとして扱います。そのため、このような応答は必然的に不可とされます。さらに、キューに登録されたサービス (とそのクライアント) は、潜在的に接続されていません。クライアントが、接続していないサービスにキュー登録された呼び出しを投稿する場合、サービスがメッセージを受け取って処理した時点で、値を返すクライアントが存在しないことがあります。そのクライアントはかなり前からいなくなっているからです。この問題を解決するには、そのサービスに、クライアント提供のキューに登録されたサービスに対してレポートさせます。このようなサービスを “応答サービス” と呼びます。図 3 は、このようなソリューションのアーキテクチャです。

図 3 応答サービス
図 3 応答サービス(画像を縮小するには、ここをクリックします)
図 3 応答サービス
図 3 応答サービス (画像を拡大するには、ここをクリックします)

応答サービス自体も、システム内ではキューに登録されたサービスの 1 つです。クライアントから接続を絶たれたり、別のプロセスやマシンにホストされたりことがあります。また、クライアントのプロセスを共有することができます。応答サービスがクライアントのプロセスを共有する場合、クライアントが起動されると、応答サービスはキューに登録された応答の処理を開始します。クライアントとは別のプロセス (場合によっては、別のマシン) で応答サービスを実行すると、このサービスを使用するクライアントから、応答サービスのライフタイムを切り離すのに役立ちます。

応答サービス コントラクトを設計する

他の Windows Communication Foundation サービスの使い方と同様に、クライアントとサービスは、応答コントラクトおよび用途 (“戻り値とエラー情報の両方” や “戻り値だけ” など) に関して事前に同意している必要があります。応答サービスを 2 つのサービスに分割して、1 つを結果用に、もう 1 つをエラー用にすることもできることに留意してください。たとえば、キューに登録された MyCalculator サービスが実装する、次に示したような ICalculator コントラクトを検討してみてください。

   [ServiceContract]
   interface ICalculator
   {
      [OperationContract(IsOneWay = true)]
      void Add(int number1,int number2);
      ... //More operations
   }
   [ServiceBehavior(InstanceContextMode =
      InstanceContextMode.PerCall)]
   class MyCalculator : ICalculator {...}

MyCalculator サービスは、クライアントに計算結果を応答し、エラーがあれば知らせる必要があります。計算結果は整数であり、エラーは Windows Communication Foundation の ExceptionDetail データ コントラクトの形式を取ります。応答サービスに対して、ICalculatorResponse コントラクトを次のように定義できます。

[ServiceContract]
interface ICalculatorResponse
{
   [OperationContract(IsOneWay = true)]
   void OnAddCompleted(int result,ExceptionDetail error);
}

ICalculatorResponse をサポートする応答サービスは、返されたエラー情報を調べ、クライアント アプリケーション、ユーザー、またはアプリケーション管理者にメソッドの完了を通知し、関係者が結果を使用できるようにする必要があります。ICalculatorResponse をサポートする簡単な応答サービスを次に示します。

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculatorResponse : ICalculatorResponse
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void OnAddCompleted(int result,ExceptionDetail error)
   {
      MessageBox.Show("result =  " + result,"MyCalculatorResponse");
      if(error != null)
      {
         //Handle error
      }
   }
}

MyCalculator と MyCalculatorResponse の両方を実装しようとすると、2 つの問題がすぐに浮かび上がってきます。最初の問題は、同じ応答サービスを使用して、キューに登録された複数のサービスに対する複数の呼び出しの応答 (完了) を処理できますが、MyCalculatorResponse (そして、より重要な点として、サービスの提供先クライアント) には、複数の応答を区別する方法がないということです。この問題の解決策として、キューに登録された呼び出しを発行したクライアントに、呼び出しのタグを付けさせます。具体的には、呼び出しに一意の ID を関連付けます。キューに登録されたサービス MyCalculator は、その ID を MyCalculatorResponse に渡して、MyCalculatorResponse がその ID に関してカスタム ロジックを適用できるようにする必要があります。

2 番目の問題は、応答サービスのアドレスを発見するキューに登録されたサービスです。二重通信のコールバックとは異なり、Windows Communication Foundation には、応答サービスの参照をサービスに渡すための組み込みサポートがありません。そのアドレスをサービスのホスト構成ファイル (クライアント セクションにあります) に配置するのは、賢明ではありません。キューに登録された同じサービスが、独自の専用応答サービスとアドレスを持つ複数のクライアントに呼び出されることがあるからです。

この問題の解決策としては、クライアント管理 ID と目的の応答サービスの両方をパラメータとして、キューに登録されたサービスのコントラクトの各操作に明示的に渡すことが考えられます。

[ServiceContract]
interface ICalculator
{
   [OperationContract(IsOneWay = true)]
   void Add(int number1,int number2,
            string responseAddress,string methodID);
}

これとほぼ同様の方法ですが、キューに登録されたサービスは、応答サービスに対するメソッド ID をキューに登録された応答のコントラクトの各操作にパラメータとして明示的に渡すこともできます。

[ServiceContract]
interface ICalculatorResponse
{
   [OperationContract(IsOneWay = true)]
   void OnAddCompleted(int result,ExceptionDetail error,
                       string methodID);
}

メッセージ ヘッダーを使用する

アドレスと ID を明示的なパラメータとして渡す方法は、うまくいくと思われますが、この方法により元のコントラクトは、ゆがめられてしまいます。さらに、プラミング レベルのパラメータに加えてビジネス レベルのパラメータも、同じ操作に導入することになります。この問題に対するより優れた解決策は、クライアントに応答アドレスと操作 ID を呼び出しの発信メッセージ ヘッダーに格納させることです。メッセージ ヘッダーをこのように使用することは、帯域外情報をサービスに渡すための汎用テクニックです。帯域外情報とは、渡されない限りサービス コントラクトには存在しない情報を指します。

操作コンテキストにより、着信/発信ヘッダーのコレクションが提供されます。これらは、IncomingMessageHeaders と OutgoingMessageHeaders の各プロパティから取得できます。

public sealed class OperationContext : ...
{
   public MessageHeaders IncomingMessageHeaders {get;}
   public MessageHeaders OutgoingMessageHeaders {get;}
   ... //More members
}

各コレクションは MessageHeaders タイプであり、これは MessageHeader オブジェクトのコレクションとなっています。

public sealed class MessageHeaders : IEnumerable<...>, ...
{
   public void Add(MessageHeader header);
   public T GetHeader<T>(int index);
   public T GetHeader<T>(string name,string ns);
   ... //More members
}

MessageHeader クラスは、アプリケーション開発者が直接対話することを意図したものではありません。タイプ セーフを提供し、共通言語ランタイム (CLR) タイプをメッセージ ヘッダーに簡単に変換できるようにする MessageHeader<T> クラスを使用してください。

public abstract class MessageHeader : ...
{...}

public class MessageHeader<T>
{
   public MessageHeader();
   public MessageHeader(T content);
   public T Content {get;set;}
   public MessageHeader GetUntypedHeader(string name,string ns);
   ... //More members
}

MessageHeader<T> のタイプ パラメータとしては、任意のシリアル化可能な型またはデータ コントラクトの型を使用できます。CLR タイプに関して MessageHeader<T> を構築してから、GetUntypedHeader メソッドを使用して MessageHeader に変換し、発信ヘッダーに格納することができます。GetUntypedHeader では、汎用タイプ パラメータの名前と名前空間を提供する必要があります。名前と名前空間は、ヘッダー コレクションからヘッダーを検索するために使用されます。検索は、MessageHeaders の GetHeader<T> メソッドを介して行います。GetHeader<T> を呼び出すと、使用した MessageHeader<T> のタイプ パラメータ値を取得できます。

ResponseContext クラス

クライアントは、メッセージ ヘッダーにアドレスとメソッド ID の両方を格納して渡す必要があるので、1 つのプリミティブなタイプ パラメータでは不十分です。むしろ、私が作成した ResponseContext クラスを使用してください (図 4)。なお、例外処理コードは一部省略してあります。

Figure 4 ResponseContext クラス (Close Figure 4)
Service Side
[ServiceContract]
interface IMyContract
{
   [OperationContract(IsOneWay = true)]
   void MyMethod();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod() {...}
}
Client Side
MyContractClient proxy = new MyContractClient();
proxy.MyMethod();
proxy.Close();

ResponseContext により、応答のアドレスと ID の両方を格納する場所を確保できます。さらに、クライアントがエラー専用の応答サービスの使用を望む場合には、ResponseContext でエラー応答サービス用アドレスのフィールドも得られます (これは、図 4 には示していません。すべてを記述したコードについては、この問題に関するダウンロードを参照してください)。

クライアントは、一意の ID を備えた ResponseContext のインスタンスの構築を担当します。クライアントはこの ID を構築パラメータとして提供できますが、ResponseContext のコンストラクタを使用することも可能です。その場合、必要なのは応答アドレスだけであり、コンストラクタにその ID の GUID を生成させます。ResponseContext の重要なプロパティは、静的 Current プロパティです。このプロパティは、メッセージ ヘッダーとの対話を完全にカプセル化します。そのため、Current プロパティにアクセスすると、優れたプログラミング モデルが得られます。つまり、get アクセサが着信メッセージ ヘッダーから ResponseContext のインスタンスを読み取り、set アクセサが発信メッセージ ヘッダーに ResponseContext のインスタンスを格納します。

クライアント側のプログラミング

クライアントは、各呼び出しに対して別の ResponseContext のインスタンスを使用して、メソッドの呼び出しごとに ID を用意します。クライアントは ResponseContext のインスタンスを発信メッセージ ヘッダーに格納する必要があります。しかも、この処理を新しい操作コンテキストで行う必要があるのです。既存の操作コンテキストは使用できません。この要件は、サービス クライアントと非サービス クライアントの両方に同じように適用されます。Windows Communication Foundation では、クライアントが次のように定義された OperationContextScope クラスで、現在のスレッドに対して新しい操作コンテキストを採用できるようにします。

public sealed class OperationContextScope : IDisposable
{
   public OperationContextScope(IContextChannel channel);
   public OperationContextScope(OperationContext context);
   public void Dispose();
}

OperationContextScope は、今あるコンテキストがふさわしくない場合に新しいコンテキストを作るための一般的なテクニックです。OperationContextScope のコンストラクタは、現在のスレッドの操作コンテキストを新しいコンテキストに置き換えます。OperationContextScope インスタンスで Dispose を呼び出すと、古いコンテキストがリストアされます (null の場合でも)。Dispose を呼び出さないと、前のコンテキストが当然リストアされるものと思っていた同じスレッドの他のオブジェクトが、破損することがあります。そのため、OperationContextScope は using ステートメント内で使用し、例外が発生した場合でも、新しい操作コンテキストにコードのスコープだけを提供するよう設計されています。

新しい OperationContextScope インスタンスの構築時には、呼び出しに使用されるプロキシの内部チャネルをコンストラクタに提供します。クライアントは新しい OperationContextScope を作成し、スコープ内で ResponseContext.Current に割り当てる必要があります。手順を図 5 に示します。

Figure 5 応答サービスに関するクライアント側のプログラミング (Close Figure 5)
string methodId = GenerateMethodId(); // use helper method
string responseQueue =
   "net.msmq://localhost/private/MyCalculatorResponseQueue";

CalculatorClient proxy = new CalculatorClient();

using(OperationContextScope contextScope =
   new OperationContextScope(proxy.InnerChannel))
{
   ResponseContext.Current = new ResponseContext(
      responseQueue,methodId);
   proxy.Add(2,3);
}

proxy.Close();

クライアント側の作業を合理化して自動化するためには、図 5 の応答サービスのセットアップ手順をカプセル化するプロキシ ベースのクラスが必要です。二重通信のコールバックとは異なり、Windows Communication Foundation ではこのようなプロキシ クラスは提供されないので、自作する必要があります。読者の作業を簡単にするために、私が ResponseClientBase<T> を書いておきました (図 6)。

Figure 6 ResponseClientBase<T> (Close Figure 6)
public class ResponseClientBase<T> : ClientBase<T> where T : class
{
   public readonly string ResponseAddress;

   public ResponseClientBase(string responseAddress)
   {
      ResponseAddress = responseAddress;
   }

   public ResponseClientBase(string responseAddress,string endpointName)
      : base(endpointName)
   {
      ResponseAddress = responseAddress;
   }

   public ResponseClientBase(string responseAddress,
      NetMsmqBinding binding,EndpointAddress remoteAddress) :
         base(binding,remoteAddress)
   {
      ResponseAddress = responseAddress;
   }

   protected string Enqueue(string operation,params object[] args)
   {
      using(OperationContextScope contextScope =
         new OperationContextScope(InnerChannel))
      {
         string methodId = GenerateMethodId();

         ResponseContext.Current =
            new ResponseContext(ResponseAddress,methodId);

         Type contract = typeof(T);
         MethodInfo methodInfo = contract.GetMethod(operation);
         methodInfo.Invoke(Channel,args);

         return responseContext.MethodId;
      }
   }

   protected virtual string GenerateMethodId()
   {
      return Guid.NewGuid().ToString();
   }
}

ResponseClientBase<T> を使用するには、具象クラスを派生させ、タイプ パラメータのためにキューに登録されたコントラクト タイプを提供します。通常のプロキシでの手法とは異なり、サブクラスにはコントラクトから派生させないでください。むしろ、メソッド ID の文字列 “not void” を返すメソッドだけで構成される同じようなセットを用意します (それで、コントラクトからは派生できません。コントラクト上の操作は何も返さないからです。これらの操作はすべて一方向です)。たとえば、キューに登録されたこのサービス コントラクトを使用する、対応する応答サービス認識プロキシを図 7 に示します。

[ServiceContract]
interface ICalculator
{
   [OperationContract(IsOneWay = true)]
   void Add(int number1,int number2);
   ... //More operations
}

Figure 7 ResponseClientBase<T> からの派生 (Close Figure 7)
class CalculatorClient : ResponseClientBase<ICalculator>
{
   public CalculatorClient(string responseAddress) :
      base(responseAddress) {}

   public CalculatorClient(string responseAddress,string endpointName) :
      base(responseAddress,endpointName) {}

   public CalculatorClient(string responseAddress,
      NetMsmqBinding binding,EndpointAddress remoteAddress) :
      base(responseAddress,binding,remoteAddress) {}

   ... //More constructors

   public string Add(int number1,int number2)
   {
      return Enqueue("Add",number1,number2);
   }
}

ResponseClientBase<T> を使用することにより、図 5 に示したコードは次のように短縮されます。

string responseAddress =
   "net.msmq://localhost/private/MyCalculatorResponseQueue";
CalculatorClient proxy = new CalculatorClient(responseAddress);
string methodId = proxy.Add(2,3);
proxy.Close();

ResponseClientBase<T> の仮想メソッド GenerateMethodId では、メソッド ID に GUID を使用します。ResponseClientBase<T> のサブクラスは、この GUID をオーバーライドして、インクリメントされた整数など、他の一意の文字列を提供できます。

ResponseClientBase<T> のコンストラクタは、応答アドレス、およびエンドポイントの名前、アドレス、バインディングなどの通常のプロキシ パラメータを受け取ります。コンストラクタは、応答アドレスを読み取り専用のパブリック フィールドに格納します。ResponseClientBase<T> は、通常の ClientBase<T> から派生するため、すべてのコンストラクタは、それぞれのベース コンストラクタに委譲します。

ResponseClientBase<T> の心臓部は、Enqueue メソッドです。Enqueue メソッドは、呼び出す操作 (実際には、メッセージをキューに登録します) の名前、および操作パラメータを受け取ります。新しい操作コンテキスト スコープの作成、新しいメソッド ID の生成、および ResponseContext への ID と応答アドレスの格納を行います。ResponseContext.Current を設定して、ResponseContext を発信メッセージのヘッダーに割り当てた後、リフレクションを使用して提供された操作名を呼び出します。ResponseClientBase<T> では、リフレクションと遅延バインディングを使用するので、コントラクト階層とオーバーロードされた操作はサポートされません。これらの操作のために、図 5 に示したようなコードを開発者が手作業で書く必要があります。

サービス側のプログラミング

キューに登録されたサービスは、ResponseContext.Current を介して着信メッセージのヘッダー コレクションにアクセスして、応答アドレスとメソッド ID を読み取ります。サービスは、応答サービスのプロキシを構築するためのアドレス、および応答サービスに提供するための ID を必要とします。サービスは通常は、ID を直接使用できません。

サービスはクライアントと同じテクニックを使用して、メソッド ID を応答サービスに渡します。つまり、明示的パラメータではなく発信メッセージのヘッダーを使用して、ID を帯域外で応答サービスに渡します。発信メッセージのヘッダー コレクションを変更できるようにするには、サービスはクライアントと同様に、OperationContextScope を介して新しい操作コンテキストを使用する必要があります。サービスは、応答サービスのプロキシをプログラム的に構築し、そのプロキシに応答アドレスと NetMsmqBinding のインスタンスを提供できます。さらに、構成ファイルからバインディング設定を読み取ることもできます。手順を図 8 に示します。

Figure 8 応答サービスに関するサーバー側のプログラミング (Close Figure 8)
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void Add(int number1,int number2)
   {
      int result = 0;
      ExceptionDetail error = null;
      try
      {
         result = number1 + number2;
      }
      catch(Exception exception) //Don't re-throw
      {
         error = new ExceptionDetail(exception);
      }
      finally
      {
         ResponseContext responseContext = ResponseContext.Current;

         EndpointAddress responseAddress =
            new EndpointAddress(responseContext.ResponseAddress);

         NetMsmqBinding binding = new NetMsmqBinding();
         CalculatorResponseClient proxy =
            new CalculatorResponseClient(binding,responseAddress);

         using(OperationContextScope scope =
            new OperationContextScope(proxy.InnerChannel))
         {
            ResponseContext.Current = responseContext;
            proxy.OnAddCompleted(result,error);
         }

         proxy.Close();
      }
   }
}

サービスは、ビジネス ロジック操作がスローした例外をすべてキャッチし、各例外を ExceptionDetail オブジェクトでラップします。サービスが例外を再スローすることはありません。再スローすると、応答メッセージを応答サービス キューに登録するトランザクションを中断する際にも、応答を取り消すことになるからです。

サービスは、例外とは無関係に、最後のステートメントで応答します。応答コンテキストのアドレスおよび NetMsmqBinding のインスタンスを使用する応答サービスのプロキシを構築します。プロキシの内部チャネルを使用して、新しい OperationContextScope を配布します。新しいスコープ内部で、Response.Current を設定して受け取った同じ応答コンテキストを、新しいコンテキストの発信メッセージ ヘッダーに追加します。また、応答サービス プロキシを呼び出します。これは実質的には、応答をキューに登録することです。次に、コンテキスト スコープを廃棄して、プロキシを閉じます。

サービスがヘッダーから応答パラメータを抽出して応答するのに必要な作業を合理化し自動化するために、次のような ResponseScope<T> クラスを作成しました。

public class ResponseScope<T> : IDisposable where T : class
{
   public readonly T Response;

   public ResponseScope();
   public ResponseScope(string bindingConfiguration);
   public ResponseScope(NetMsmqBinding binding);

   public void Dispose();
}

ResponseScope<T> は、破棄可能なオブジェクトであり、新しい操作コンテキストをインストールします。新しい操作コンテキストを破棄するときは、古い操作コンテキストをリストアします。例外発生時のイベントを自動化するには、using ステートメントで ResponseScope<T> を使用する必要があります。

ResponseScope<T> は、応答コントラクトを表す型パラメータを取り、Response に同じ型の読み取り専用パブリック フィールドを提供します。Response は応答サービスのプロキシの一種であり、ResponseScope<T> のクライアントは、Response を使用して応答サービスの操作を呼び出します。ResponseScope<T> が Dispose で破棄を行うので、Response を破棄する必要はありません。ResponseScope<T> は、構成ファイルにある既定のエンドポイントを基礎とするか、またはコンストラクタに提供されたバインディング情報 (構成セクション名または実際のバインディング インスタンス) を使用して、Response をインスタンス化します。図 9 に ResponseScope<T> の実装の様子を示します。

Figure 9 ResponseScope<T> の実装 (Close Figure 9)
public class ResponseScope<T> : IDisposable where T : class
{
   private OperationContextScope m_Scope;
   public  readonly T Response;

   public ResponseScope() : this(new NetMsmqBinding()) {}

   public ResponseScope(string bindingConfiguration) :
      this(new NetMsmqBinding(bindingConfiguration) {}

   public ResponseScope(NetMsmqBinding binding)
   {
      ResponseContext responseContext = ResponseContext.Current;
      EndpointAddress address =
         new EndpointAddress(responseContext.ResponseAddress);

      ChannelFactory<T> factory = new ChannelFactory<T>(binding, address);
      Response = factory.CreateChannel();

      //Switching context now
      m_Scope = new OperationContextScope(Response as IContextChannel);
      ResponseContext.Current = responseContext;
   }

   public void Dispose()
   {
      IDisposable disposable = Response as IDisposable;
      disposable.Dispose();
      m_Scope.Dispose();
   }
}

ResponseScope<T> を使用すると、図 8 の最終ステートメントは、次のように短縮されます。

finally
{
   using(ResponseScope<ICalculatorResponse> scope =
      new ResponseScope<ICalculatorResponse>())
   {
      scope.Response.OnAddCompleted(result,error);
   }
}

ResponseScope<T> のコンストラクタは、ResponseContext.Current を使用して着信応答コンテキストを抽出します。次に、チャネル ファクトリを使用して、応答サービスのプロキシに関して Response を初期化します。ResponseScope<T> 実装の秘訣は、using ステートメント内で OperationContextScope を使用しないことです。新しい OperationContextScope を構築することで、ResponseScope<T> は新しい操作コンテキストを確立します。ResponseScope<T> は、OperationContextScope をカプセル化し、新たに作成した OperationContextScope を保存します。古いコンテキストは、using ステートメント内で ResponseScope<T> が破棄されるとリストアされます。ResponseScope<T> は次に、ResponseContext.Current を使用して、新しい操作コンテキストの発信メッセージ ヘッダーに応答コンテキストを追加し、元に戻ります。

応答サービスのプログラミング

応答サービスは、着信メッセージのヘッダー コレクションにアクセスして、メソッド ID を読み取り、その内容に応じて応答します。図 10 にこのような応答サービスの実装例を示します。

Figure 10 応答サービスの実装 (Close Figure 10)
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculatorResponse : ICalculatorResponse
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void OnAddCompleted(int result,ExceptionDetail error)
   {
      string methodID = ResponseContext.Current.MethodId;

      if(error == null)
      {
         //Notify client, fire event, and so on
      }
      else
      {
         //Handle error
      }
   }
}

応答サービスは、ResponseContext.Current を介して操作コンテキストの着信メッセージ ヘッダーにアクセスし、キューに登録されたサービスと同様にメソッド ID を抽出して、完了またはエラーを処理します。

まとめ

キューに登録されたサービスは、多くのアプリケーションで役立ちます。完了、エラー、および結果をクライアントにレポートできることが、自分が使用しているアプリケーションにとって重要であれば、このコラムで説明した helper クラスを使用してください。Windows Communication Foundation から得られる基本的な機能を拡張し、欠けているサポートを 1、2 行のコードで実装して補うことができます。

メッセージ ヘッダーとの対話、操作コンテキストの置換、カスタムなプロキシ ベースのクラスとカスタム コンテキストの記述など、このコラムで示した Windows Communication Foundation のさまざまなテクニックは、他の多くのケース、特にカスタム フレームワークで役立ちます。

ご意見やご感想は、

Juval mmnet30@microsoft.com.

まで英語でお送りください。
Juval Lowyは、WCF トレーニングと WCF アーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最近の著作としては、『Programming WCF Services』 (O'Reilly、2007) があります。彼はまた、シリコン バレー地域の Microsoft Regional Director も務めています。 Juval の連絡先は、www.idesign.net (英語) です。


Netting C++: ネイティブ C++ から共通型システム (CTS) へのマッピング

Netting C++
ネイティブ C++ から共通型システム (CTS) へのマッピング
Stanley B. Lippman

コンテンツ
キーワードに関する意外な事実
継承
コピー セマンティクス
新しい言語のレッスン
次回予告

このコラムでは、December 2006 installment (2006 年 12 月のインストール) に掲載しなかった点を取り上げ、TQL (Text Query Language) の Query クラス階層を C++ から .NET 共通型システム (CTS: Common Type System) に変換する作業を開始します。この例では C++/CLI を使用しますが、代わりに C# を使用することもできます。C++/CLI の主な利点は、アプリケーションを徐々に変換し、ソースレベルで混在したネイティブ C++ とマネージド C++ を維持できることにあります。最初に、図 1 に示すネイティブの Query 基本クラスの実装を参照してください。

Figure 1 ネイティブの抽象 Query 基本クラス (Close Figure 1)
#include <vector>
#include <iostream>
#include <set>
#include <string>
#include <utility>

using namespace std;

typedef pair< short, short > location;

class Query {
public:
   virtual ~Query(){ delete _solution; }
   Query& operator=( const Query& );

   // the virtual set of operations ...
   virtual void eval() = 0;
   virtual Query *clone() const = 0;
   virtual ostream& print( ostream &os ) const = 0;
   virtual bool add_op( Query* ) { return true; }
   virtual void display_solution( ostream &os = cout, int tabcnt = 0 );

   // factored query-wide operations
   void  handle_tab( ostream &os, int tabcnt ) const;
   void display_partial_solution();

   const set< short > *solution();
   const vector< location > *locations() const { return &_loc; }

   // support for recognizing embedded parentheses within the query
   // as in alice && ( fiery || magical )
   void lparen( short lp ) { _lparen = lp; }
   void rparen( short rp ) { _rparen = rp; }

   short lparen() { return _lparen; }
   short rparen() { return _rparen; }

   void print_lparen( short cnt, ostream& os ) const;
   void print_rparen( short cnt, ostream& os ) const;

protected:

   Query() : _lparen( 0 ), _rparen( 0 ), _solution( 0 ) {}
   Query( const Query &rhs )
      : _loc( rhs._loc ), _solution( 0 ),
        _lparen( rhs._lparen ), _rparen( rhs._rparen )
      {}

   Query( const vector<location> &loc )
      : _loc( loc ), _solution( 0 ),
        _lparen( 0 ), _rparen( 0 )
      {}

    set<short>* _vec2set( const vector<location>* );
    short  _lparen;
    short  _rparen;

    vector<location> _loc;
    set<short> *_solution;
};

Query クラスの実装は、ハイブリッドの抽象クラス設計を示しています。抽象というのは、このクラスは純粋仮想インターフェイスを提供し、各派生クラスのインターフェイスがそれをオーバーライドする必要があるからです。ハイブリッドというのは、すべての派生型によって共有されるインフラストラクチャの具体的な実装をこのクラスが提供しているからです。ハイブリッド設計の利点は 2 つあります。第 1 に、ハイブリッド設計は特殊な派生クラスの型を簡単に供給できるので、設計者は特殊なアルゴリズムに集中できます。設計者の専門分野には、ドメインを使用しないインフラストラクチャを繰り返し実装することは含まれていませんし、それらに関してほとんど関心を抱いていないか、理解が不十分です。第 2 に、ハイブリッド設計は仮想ではない実装もサポートするので、共有機能に関する潜在的なインライン実装もサポートされています。効率に関するこの利点は、ネイティブ モードでは顕著に実証できますが、.NET 環境ではプラットフォームのセキュリティ関連の事項が原因でそこまで明確ではありません。

CTS は、ネイティブ C++ が実現できない方法で、この設計に伴うハイブリッド特有の性質を適切に対処します。一連の純粋仮想関数は IQuery インターフェイス クラスの中でキャプチャされ、具体的な共有インフラストラクチャは抽象 Query 参照基本クラスという形で構成されています。図 2 に、その概要を示します。

Figure 2 Query 参照基本クラス (Close Figure 2)
// pure virtual interface goes into IQuery
interface class IQuery {
    void eval();
    void display_solution();
    // ...
};

// shared implementation goes into Query
public ref class Query abstract : IQuery {

    short lparen;
    short rparen;

    list<location>^ _loc;
    set<short>^_     solution;

public:
    // ...
};

// AndQuery inherits Query and implements IQuery
public ref class AndQuery : Query { ... };

アセンブリの外部から参照クラスにアクセスできるようにするには、その参照クラスをパブリックとして指定する必要があることに注意してください。インターフェイスは既定でパブリックになるので、キーワード public を指定することは可能ですが、冗長です。(C++/CLI には、このようにオプションの冗長性が多数あります。)

キーワードに関する意外な事実

図 2 の中には、ネイティブ C++ のプログラマにとって意外な事実が 2 つあります。Query クラスの宣言の中にキーワード abstract がありますが、これは ISO の C++ では存在しないものです。もう 1 つは、AndQuery の中で、Query 基本クラスより前にキーワード public が欠落していることです。それぞれについて簡単に調べてみましょう。

Bjarne Stroustrup は、1988 年前後に cfront Release 2.0 と呼ばれるようになった製品の後期ベータ版で、言語レベルでの抽象クラスのサポートを導入しました。この製品は、ベル研究所の中で、管理優先順位の異なる複数のプロジェクト グループにまたがっていました。抽象クラスをインスタンス化した場合はコンパイル時にエラーが発生したので、致命的なランタイム エラーが発生する可能性は防止されていました。全員がこれは良いことだと合意しましたが、リリース サイクルの終盤になって新しいキーワードを追加することに関しては、全員の合意が得られたわけではありません。私の記憶では、Bjarne には言語の考案者として抽象基本クラスのサポートを追加する自由があるように思えましたが、実際には製品にキーワードを追加する自由を行使しませんでした。仮に追加した場合は、製品のリリース成功に対して悪影響を及ぼす可能性がありました。Bjarne は事態を進展させるために、関連付けられた純粋仮想関数を持つクラスを抽象クラスとして扱う、というルールを考案しました。非常に興味深いことに、C++ のこの設計理念は、非常に長い間にわたって、批判の対象にならなかった歴史があります。

C++/CLI に関しては、キーワード abstract を追加することを決定しました。筆者の個人的な意見では、図 2 の基本クラス Query のように、クラス名の後にこのキーワードを記述するのは設計上の適切な決定でした。このキーワード abstract は拡張され、純粋仮想関数を宣言するための代替構文としても使用されるようになりました (もちろん、これは C# における abstract の使用方法に似ています)。純粋仮想関数に関する両方の宣言形式は、C++/CLI の中では同じものと見なされますが、どちらを選択するかによって、プログラマがネイティブ C++ とマネージド C++ のどちらを土台にしているかわかるような気がします。クラスの継承または仮想関数のオーバーライド能力に対して明示的に制限を加えるためのもう 1 つのキーワード、sealed があります。次世代の ISO C++ で、委員会がこれらの拡張機能を採用して過去の不足を補うとしたら、非常にすばらしいことです。

継承

CTS では、パブリック継承だけがサポートされています。単一継承のみにこの制限を適用することは意味のあることです。設計時に想定されていたプライベート継承の主な用途は、多重継承の Mix-in モデルです。この設計モデルでは、実装はプライベート継承され、一方でパブリック継承は抽象のインターフェイスを表します。たとえば、私の記憶が正しいとすると、1990 年代にさかのぼる独創的な Booch Components では、固定長のキューは次のように実装されていました。

class FixedLengthQueue :
      public Queue, private Vector { ... };

一方、動的可変長キューの場合は、同じパブリック インターフェイスを維持しますが、ベクタではなく、リストに対応するプライベートのバックアップ ストアを使用します。スペースと時間は余分に必要になりますが、柔軟性が高まるのが利点です。標準テンプレート ライブラリ (STL) が成功した結果、この種類の設計は、継承を扱うコミュニティと C++ のどちらでも使用されなくなりました。

CTS では、インターフェイスにより多重継承の必要性がなくなったので、それに伴ってプライベート継承も不要になりました。2001 年にリリースされた Visual Studio® .NET に含まれていた、C++ に対する従来のマネージ拡張 (Visual C++® 7.0) では、基本クラスと派生クラスの間で使用可能な唯一の関係がパブリック継承である場合でも、キーワード public が必須でした。これを省略した場合は、コンパイラ エラーになりました。下位互換性を維持するために、このような処理が必要だったと感じます。この処理を変更すると、ネイティブ C++ プログラマが .NET に移行した場合に混乱する可能性があったからです。(もちろん、ネイティブ C++ でキーワード public が欠落しているのは、プライベート継承という関係を示唆しています。)C++/CLI 環境では、キーワード public はオプションです。現時点における考え方は基本的に、ネイティブ C++ プログラマが混乱する可能性のある、より大きな問題が他にも多数存在するということです。したがって、話題を注意深く選択することにしましょう。実際、これは次のトピックに登場する話題として分類できるものです。つまり、ネイティブ C++ と .NET の間でのコピー セマンティックスの違い、およびその違いが設計に及ぼす影響です。特に、図 1 をもう一度参照すると、コピー構築に関する課題と、ネイティブの Query 実装の中での仮想クローン パターンの使用方法を解決する必要があることがわかります。

コピー セマンティクス

コピー構築と仮想クローン パターンを解決する方法を考慮する前に、ネイティブ実装の中でなぜそれらを実行する必要があるのか確認してみましょう。

ネイティブ C++ では、頻度の高い設計の落とし穴が、次の方法で発生します。最初に、ヒープの中に割り当てられた何かのメモリに対してアドレス指定を行うクラスの中で、クラス コンストラクタまたはファクトリ パターンを使用してポインタ メンバを宣言します。次に、そのポインタ メンバに関連付けられたメモリを削除するデストラクタを定義しますが、コピー コンストラクタを提供する必要があることを忘れるか、その存在を見落としてしまいます。この結果、ポインタ メンバとデストラクタの組み合わせによって、プログラムの動作は、技術的には未定義になります。統計的には、これは望ましくないことです。

コピー コンストラクタの作業 (もちろん、コピー代入演算子の作業でもあります) は、ポインタ メンバによってアドレス指定されたオブジェクトのディープ コピー (階層コピー) を実行 (もちろん、これは任意の数、および任意の深さを持つポインタ メンバに対応してスケール化することができます) し、ネイティブ ヒープ内に新しいオブジェクトを作成し、オリジナルからすべての値を個別にコピーする方法で新しいオブジェクトを初期化することです。ネイティブ C++ では、ネイティブ ポインタの既定の動作はシャロウ コピーなので、この作業が (より複雑な参照カウント スキームの代わりに) 必要です。つまり、ソース ポインタによってアドレス指定された、メモリ内にある同じオブジェクトをアドレス指定するよう、ターゲット ポインタが割り当てられます。ディープ コピー (または、特定の形式の参照カウント) を使用しない場合、複数のクラス オブジェクトにまたがって単一のオブジェクトをエイリアス指定するので、致命的な結果を招きます。

ポインタが Query のように継承階層の基本クラスをアドレス指定する場合は、この問題は複雑になります。この場合は、ポインタがポリモーフィックであり、ランタイムに解決される仮想メソッドが必要なので、コピー コンストラクタを正しく呼び出せません。ただし、どのコンストラクタも仮想にすることはできません。選択しようとする型を作成する前に、物理メモリの中でコンストラクタを呼び出すからです。

これらは、言語の考案者が巧みな手法で対処したか、さもなければ最大の難関であると見なす類の問題です。C++ の歴史の中では、この点に関するソリューションは広く親しまれているものです。受け入れられている設計パターンは、Query の中にあるような仮想クローン関数を用意し、各派生クラスの中でオーバーライドすることです。クローン メソッドの実装は、簡単です。これは、強力なパターンです。

//native code clone pattern ...
class AndQuery : public Query {
public:
    virtual Query *clone() const
          { return new AndQuery( *this ); }

    // ...
};

新しい言語のレッスン

既存のソリューションを新しい言語に変換することを最初に試みるときは、下の実装を新しい言語に機械的にマップするよう誘惑されることがよくあります。そして、明確なマッピングが利用不可能である場合、筆者は多くの場合、本能的に新しい言語を批判します。この場合は、このディープ コピー メカニズムを .NET にマップするよう誘惑された可能性があります。結局のところ、C++/CLI にはコピー コンストラクタ (およびコピー代入演算子) があり、.NET Framework には IClone インターフェイスがあります。ただし、この方法を実行するのはおそらく誤りでしょう。

最初の問題は、C++/CLI のコピー コンストラクタは、C++/CLI のデストラクタと同様、呼び出せる可能性も、呼び出せない可能性もあることです。この点は、参照クラス オブジェクトを宣言する方法に大きく依存しますが、クラスの設計者はこのことをプログラマに強制できません。たとえば、筆者が AndQuery クラス用のコピー コンストラクタを提供した場合、他の開発者は次のコードを作成する可能性があります。

AndQuery ^aq1 = gcnew AndQuery;
AndQuery ^aq2 = aq1;

AndQuery 用のこのコピー コンストラクタは、2 番目の宣言ステートメントによって呼び出されません。代わりに、aq2 と aq1 の両方は、最初の宣言ステートメントによって割り当てられた、同じ AndQuery オブジェクトを参照します。多くのプログラマにとって、これは直観的に理解できることではありません。仮に value クラスの中で既定のコンストラクタを呼び出せないとすると、マネージド拡張の中でサポートされている value クラスに対応する既定のコンストラクタを削除するのが安全であると判断するはずです (筆者個人は、潜在的なトラブルという観点では、この状況に違いを見いだせません)。

IClone インターフェイスに関しては、Microsoft 自体が "ああ、考え方は合っているが、成功していないだけだ" という類の状況に陥りました。CLR (共通言語ランタイム) の .NET Framework 2.0 バージョンでは、IClone の使用は不適切なものになりました。ジェネリックなコレクション型を参照すると、それらの型が IClone の実装を提供していないことがわかります。ただし、ジェネリックの複製元であるジェネリック以外のコレクション型は実装を提供しています。この状況での問題は、IClone が保証するディープ コピーの方法のあいまいさにあります。

どちらの方法でも、一貫性のあるセマンティクスに頼ることができない場合は、このメカニズムの使用を完全に避けることが安全であるように感じられます。では、どの方向に向かいますか。実際は、立ち止まって考えるのは良いことです。これは、新しい言語を学ぶ際に有益な効果をもたらす事柄の 1 つです。これまで使い慣れた言語に基づいて考えるのをやめたことに、頻繁に気づくはずです。

コピー コンストラクタを virtual と宣言することはできないので、clone を導入する必要がありました。クラス デストラクタがヒープ オブジェクトを割り当て解除するのでコピー コンストラクタを導入する必要がありました。ネイティブ ヒープでは開発者がヒープ メモリを明示的に管理する必要があったので、デストラクタを導入する必要がありました。

幸いなことに、漫画でよくあるように、1 つの考えがひらめきました。.NET 環境では、マネージド ヒープはガベージ コレクションの対象になります。これは、デストラクタが必要ないことを意味します。しかもそれは、ディープ コピーを管理する必要がないことも意味します。さらに、クローン パターンすら必要ないことを意味します。複雑なメカニズム全体が、実際は破棄可能です。これは、プログラムではなく、プログラミング言語による成果です。

次回予告

ネイティブ C++ から C++/CLI へのリエンジニアリングに関する次回の複雑な懸案は、テンプレートに関する全般的な事項と、STL のうち特定の 1 点に関するものです。図 1 をもう一度参照してください。Query クラスの実装が STL と 1 組のテンプレートを多用していることがわかります。これらはどちらも、(今のところ) マネージ フォームでは使用できません。次回のコラムでは、これらが欠如していることに対処する方法を探索します。

ご質問やご意見は、

Stanley purecpp@microsoft.com.

まで英語でお送りください。
Stanley B. Lippman は現在、多人数参加型オンライン (MMO) ゲームの開発、および開発とサポートを支援するためのインフラストラクチャ テクノロジに特化した会社、Perpetual Entertainment で働いています。Stan は、オリジナルの C++ cfront コンパイラに関して、その創案者である Bjarne Stroustrup と共に働いたことで知られています。Stan がこれまでに携わったフルタイムの仕事の中で気に入っているのは、Fantasia 2000 の Firebird セグメントに関するソフトウェア技術ディレクター (Software Technical Director) です。C++/CLI を考案するために、Microsoft の Visual C++ チームと共に働いたこともあります。


C++ アット ワーク: &quot;The Rationale&quot;、ハイライト、そして 1 つの別れ

C++ アット ワーク
"The Rationale"、ハイライト、そして 1 つの別れ
Paul DiLascia

コンテンツ
"The Rationale"
マネージ拡張との決別
将来の楽しみ
プログラミングの進化
最終目標
ここで一応、さよなら

今月は、いつもの Q&A 形式から外れて、オンラインで見つけたすばらしいドキュメントについてお話ししたいと思います。何週間か前のことですが、"C++/CLI で const 関数を宣言できないのはなぜですか" という質問が私に送られてきました。

// reference class
ref class A {
   void f() const; // NOT!
};

これに対して、私はこう答えました。"それがルールだからです。共通言語基盤 (CLI: Common Language Infrastructure) は、Visual Basic® や Java、それに COBOL なども含めた、多数の言語をサポートできるように設計されています。これらの言語では、const の意味すら認識できません。CLI は const メンバ関数が何であるかを知らないのです。const を使用できないのはそのためです。"

このように返信してしまってから、記憶の奥底に埋もれていた何かをぼんやりと思い出したのです。なんだか、他の言語では無視できるコンパイラ ヒントと関係する事柄のようでした。そこで、以前に書いた記事を検索したところ、2004 年 7 月に、const に関する質問に答えていたことがわかりました。実は、C++/CLI では const メンバ関数を宣言できませんが、const データ メンバおよび const パラメータの宣言は許可されるのです。図 1 は、static const のデータ メンバを持つ参照クラスを使用した簡単なプログラムを示しています。このプログラムをコンパイルし、ILDASM を使用して逆アセンブルすると、次のようになります。

field public static int32
    modopt([mscorlib]System.Runtime.CompilerServices.IsConst)
        g_private = int32(0x00000001)

Figure 1 const.cpp (Close Figure 1)
////////////////////////////////////////////////////////////////
// To compile type:
//    cl /clr const.cpp
//
#include <stdio.h>

ref class A {
   int m_val;
   // const data member allowed, will generate modopt
   static const int g_private = 1;
public:
   // public const member could be modified by Visual Basic or other
   // programs that don't know const -- so use literal instead.
   literal int g_public = 1;
   A(int i) { m_val = i; }
   void print();  // const;             // NO!--const fn not allowed
};

void A::print()
{
   printf("Val is %d\n",m_val);
}

int main()
{
   A a(17);
   a.print();
}

modopt (随意修飾子) は MSIL 宣言子であり、CLI コンシューマに対し、「これが何かわかれば結構、わからなければ、安全に無視できますよ」と言っています。これとは反対に、modreq (必須修飾子) は、「これが何だかわからなければ、この関数は使えませんよ」ということを言っているのです。volatile は modreq の 1 つです。volatile 参照は OS やハードウェアによって (時として別のスレッドによっても) いつでも変更される可能性があるので、CLI コンシューマは volatile オブジェクトを使用するなら volatile の意味を知っておく必要があるというわけです。ところが const は随意修飾子なのです。マネージ拡張では C++ の const オブジェクトが CLI の literal に変換されていましたが、C++/CLI ではこの処理が行われなくなりました。ですから、注意が必要です。public const データ メンバを宣言しても、Visual Basic のような言語で記述されたクライアントであれば、値が変更される可能性があります。CLI クライアントによって値が変更されないようにするには、図 1 に示したように、このオブジェクトを literal として宣言する必要があります。では、メンバ関数の場合はどうでしょう。const メンバ関数はなぜ許可されないのでしょうか。

"The Rationale"

その答を探すうちに、非常に優れたドキュメントを見つけました。マイクロソフトの Herb Sutter 氏による『A Design Rationale for C++/CLI』(C++/CLI のための設計原理) というものです。彼は C++/CLI の設計者の 1 人です。以降、このドキュメントのことを "The Rationale" と呼ぶことにします (彼のブログから入手できますが、URL は長すぎるのでここには記載しません。Web で "C++/CLI Rationale" を検索すると、見つかります)。タイトルでわかるように、"The Rationale" では、C++/CLI に含まれているすべてが、なぜそのようになっているのかを説明しています。"そもそも、なぜ C++ を拡張するのか" という質問に始まり、今回の const 関数に関する疑問に至るまで、すべての答が示されています。C++/CLI の根拠を理解したい方は、"The Rationale" を読むべきだと思います。"The Rationale" には、多くの反復部分があるので、ここでは一部のハイライトを紹介します。

では、"そもそも、なぜ C++ を拡張するのか" という最も重要な疑問から始めましょう。その答は単純明快で、"C++ を CLI の優良市民にするため" です。Windows® 開発の未来は Microsoft® .NET Framework にかかっています。COBOL だって CLI をサポートしているのです。ですから、C++ の成功がこれからも確実になるように、C++/CLI が設計されたというわけです。

でも、なぜ C++ をいじる必要があるのでしょう。わざわざ ^ や % などの新しい概念や、ref、value、property などの新しいキーワードを使ってまでこの言語を拡張するのはなぜでしょう。まあ、他に方法がないのは事実です。これについて "The Rationale" では、他ならぬ Bjarne Stroustrup 氏の言葉を引用しています。"クラスを使うと、必要とする概念のほとんどを表現できる。ライブラリの道が真にふさがれた場合に限って、言語拡張の道を選ぶべきである。"

単純な C++ コードの場合、CLI をターゲットとすることは、別のプロセッサを対象としたコンパイラを記述するようなものなので、問題はありません。ところが CLI には、C++ では表現できず特殊なコードの生成を必要とするような新しい概念が導入されています。たとえば、プロパティには特殊なメタデータが必要になります。プロパティはライブラリやテンプレートでは実装できません。"The Rationale" では、プロパティおよびそれ以外の CLI の機能に代わるものとして別の構文を仮定的にいくつか示し、これらがなぜ拒否されるかを説明しています。

マネージ拡張との決別

さらに "The Rationale" では、マイクロソフトがなぜマネージ拡張を見限ることにしたかについても説明しています。マネージ拡張において、マネージ オブジェクトおよびネイティブ オブジェクトの両方に対して * を使用する点は、言語を変更せずに C++ と CLI を統合するうえではまことに巧妙かつ勇敢な試みでしたが、このために参照オブジェクトとネイティブ オブジェクトの間にある重要な相違がわかりにくくなっていました。これら 2 種類のポインタは、外見が同じでも、動作が異なるのです。たとえば、デストラクションのセマンティクス、コピー コンストラクタ、コンストラクタ内/デストラクタ内での仮想呼び出しなどはすべて、ポインタが実際に指しているオブジェクトの種類によって動作が異なります。これはよくありません。"The Rationale" で述べられているように、"不要な相違点を隠すだけではなく、必要な相違点を見せることが重要" であり、Bjarne 氏も "重要な動作は明確に見せるようにしている" と述べています。ネイティブ クラスとマネージ クラスは根本的に異なる生き物なので、相違は隠すより明らかにすべきです。さまざまなメカニズムについて検討と拒否が繰り返され、ついに C++/CLI チームは gcnew、^ (ハンドル)、および % (追跡参照) に落ち着いたのです。マネージ クラスとネイティブ クラスをこのように分離することには、予期しなかった効用があります。たとえば、分離された gcnew 演算子を使用してマネージ オブジェクトを割り当てられるということは、そのうちネイティブ クラスをマネージ ヒープから割り当てられるようになる可能性があり、その逆もあり得ます。

ところで、マネージ拡張には、アンダーバーの付いた不恰好なキーワード (__gc や __value など) がなぜ存在するのか、疑問に思ったことはありませんか? マネージ拡張は、C++ の "新しいキーワードの導入が真に必要ならば、名前の先頭にダブル アンダースコアを付けよ" という規範に忠実に従ったのです。でも、知っていましたか? マイクロソフトが __gc や __value などを導入したとき、Redmond には "予期しなかったほど強い" 不満がプログラマたちから寄せられたのです (そうだそうだ! 世界のプログラマたち、団結しよう! 失うのはアンダーバーだけさ!)。アンダーバーを使うと、まるである種のアセンブリ言語で記述したプログラムのように、コードが読みにくくなります。だからこそ、C++/CLI には、アンダースコアなしの ref と value があるのです。これは C++ に新しいキーワードを追加することを意味しますが、それがどれほどの問題でしょうか。Bjarne は "私の経験では、人々は概念を導入するためにキーワードを使うことに熱心になりすぎて、独自のキーワードを持たない概念を教えるのは驚くほど難しいというレベルに達している。この影響は、人々が新しいキーワードに対する嫌悪を口に出して表現するよりもっと重要で根深いものだ。" と言っています (そうそう、Bjarne がプログラミングの心理学を説明しているところがとてもいい)。とにかく、C++/CLI でアンダーバーを使用していないのはこのためです。これらを予約語ではなく位置指定キーワードにすると、これらの語を変数名や関数名として既に使用しているプログラムと競合する状況を防ぐことができます。

好奇心の強いあなたなら、"gc がアンダーバーを失う過程で ref に変わったのはなぜだろう?" と疑問に思うかも知れません。"The Rationale" にも説明されているとおり、マネージ クラスに関する重要なことは、これらがガベージ コレクション済みのマネージ ヒープ上に存在することではありません。重要なのはマネージ クラスの参照セマンティクスです。ハンドル (^) は、ポインタではなく参照のような働きをします。どうですか? "The Rationale" を読めば、理解できるようになってきたでしょう?

本質的な相違点を見せることが重要なら、表面的な相違点を隠すことも重要です。たとえば、オブジェクトがネイティブ、ref、value のいずれかであれば、オーバーロードする演算子はすべて、通常の C++ プログラマが期待するような形で動作します。C++/CLI で相違点が隠されているもう 1 つの例として、次のスニペットについて考えてみましょう。

// ref class R as local variable
void f()
{
   R r; // ref class on stack???
   r.DoSomething();
   ...
}

ここでの r はスタック オブジェクトのように見えますが、マネージ クラスを物理的にスタックに割り当てられないことは周知の事実です。マネージ クラスはマネージ ヒープから割り当てなければなりません。つまり、次のような同じ処理を行うコードを生成することで、コンパイラはこのスニペットを期待通りに動作させることができるのです。

// how the compiler sees it.
void f()
{
   R^ r = gcnew R; // allocate on gc heap
   try
   {
      r->DoSomething();
      ...
   }
   finally
   {
      delete r;
   }
}

オブジェクトは物理スタック上に存在してはいませんが、気にするほどのことはないでしょう? 大切なのは、ローカル変数の構文が、C++ プログラマの期待通りに動作することです。具体的には、f を終える前に r のデストラクタが呼び出されることです。そしてデストラクタといえば、C++/CLI ではなぜ確定的デストラクションが復活しているのかについても、同じ理論で説明できます。つまりデストラクションは、すべての C++ プログラマが慣れ親しんだ同じセマンティクスに従って動作します (やった!)。非確定的デストラクションは、マネージ拡張の最悪な悪夢の 1 つでした。C++/CLI ではデストラクタが Dispose にマップされ、特別な ! 構文がファイナライザとして導入されています。参照オブジェクトが使用したメモリは、ガベージ コレクタに余裕ができるまで回収されませんが、それは大したことではありません。C++ プログラマは、オブジェクトが破棄されたときにメモリが回収されるかどうかについてはあまり気にしません。本当に重要なのは、必要なときにデストラクタが実行されることです。C++ プログラマは、ファイル ハンドルやデータベース ロックなどの非メモリ リソースを初期化または解放するために、コンストラクション/デストラクション パターンを頻繁に使用します。C++/CLI では、参照クラスでもネイティブ クラスでも、慣れ親しんだコンストラクション/デストラクション パターンが期待通りに動作してくれます。マネージ拡張に関しては、問題点もその対応も、Redmond の功績です。

将来の楽しみ

"The Rationale" には他にも興味をひかれるセクションが多くありますが、その中に、"Future Unifications" (将来の統一化) という箇所があります。そこには、将来的に C++/CLI が取り得る方向性に関して、非常に興味深い手がかりがいくつか示されています。たとえば、今のところは、ネイティブ クラスをマネージ クラスから派生させたり、マネージ クラスからネイティブ クラスを派生させたりすることができません。ところが、ある方法を取り入れることで、これと同じ結果を得ることができるのです。その方法とは、"基本" クラスをデータ メンバとしてインクルードし、含まれているインスタンスの起動以外に何も行わないパススルー ラッパーを記述することです。しかしこれでは、ちょっと定型的すぎるかもしれません。コンパイラにやらせてみるのはどうでしょうか? コンパイラで、オブジェクトの混合物を、2 つの部分から成る 1 つのオブジェクトとして表現するのです。つまり、すべての CLI パーツが含まれた 1 つの CLI オブジェクトと、すべての C++ パーツが含まれた 1 つの C++ オブジェクトで、1 つのオブジェクトを表現するというわけです。

これについて Sutter 氏は、逸話を語っています。混合クラスに関するこのアイデアを初めて Bjarne Stroustrup 氏に語ったとき、Bjarne は本棚まで行って "本を開いて見せてくれた。そこには、C++ でオブジェクトを単一のメモリ ブロックに連続して並べるように要求するのは間違いだと、批判をよそに彼がいつも主張してきたことが書かれていた" というのです。当時はだれも、非連続オブジェクトのメリットをわかっていませんでしたが、.NET と CLI についても予測できなかったのです。Bjarne の主張どおり、非連続性への扉を開放しておけば、混合オブジェクトも不可能ではなくなります。ですから、C++/CLI の今後のバージョンでは、混合オブジェクトが登場するかもしれませんよ。ここで覚えておきたい金言 : 新しい言語や複雑なプログラムを設計する場合は、それがずっと残っていき、予測しない形で成長していくようなものなら、単にその方が楽だからというだけの理由で余計な推測はしないことです。

"The Rationale" には、歴史的な逸話がもう 1 つ紹介されています。Redmond で元々使っていた C++/CLI の名前は、アルバート アインシュタインの業績にちなんで、M(anaged)C(++) の略にハット (^) を合わせた、MC^2 だったというのです。でも、Sutter 氏いわく、これは "かわいすぎ" だと思ったそうです。私もそう思います。だって、もし C++/CLI が MC^2 と呼ばれていたら、受け入れられると思いますか? C++/CLI に決める際にも、設計者たちは Bjarne の足跡に従ったのです。"C++ を選んだのは、短くて、素晴らしい解釈が何とおりもあり、形容詞 + C という形ではなかったからだ" と Bjarne は述べています。C++/CLI は、あえて "形容詞 + C++" という形を避け、C++ が最優先という意味を暗示しています。

"The Rationale" は、property、gcnew、generic、const など、他の C++/CLI 拡張の正当性を示した後、有益な FAQ セクションで締めくくられています。詳細は、"The Rationale" をダウンロードして自分で読んで確認してください。const といえば、"C++/CLI で const データが許可され、const 関数が許可されないのはなぜか" という、そもそもの疑問点はどうなるでしょう。簡潔に答えるなら、"CLI では関数に対して直接的に modopt/modreq を付けることができない" ということになります。ただし、CLI にはこの情報をメタデータ内でエンコードする方法がなくはありません。まだテストされていないだけです。"The Rationale" で慎重に述べられているように、少なくとも "まだ" なのです。これは、この機能がそのうち追加される可能性があることを示唆しています。

プログラミングの進化

C++/CLI を使用すると、C++ を CLI の優良市民にすることができます。それだけでなく、"The Rationale" を読むとわかるように、この変革による C++ への混乱は最小限です。しかも、C++ がシステム プログラミングに最適な言語であることには変わりません。これは、C++ では他のどの言語よりも直接的に CLI にアクセスできるからであり、もうしばらくは存在し続けるであろう Win32® API を呼び出したければ、C に戻ることも不可能ではないからです。

C++/CLI の重要性とその意味を理解するには、プログラミングが進化する歴史において現在がどういう時点であるかを考慮する必要があります。では、超特急でプログラミングの歴史を振り返ってみましょう。昔は、スイッチを切り換えることでプログラムが記述されていました。それを改善したのが紙テープでしたが、コンピュータごとに独自の "マシン語" が存在していました。コンピュータの増殖に従って、プログラマは新しいマシンごとにプログラムを何度も書き直さなければなりませんでした。これにはかなりの労力を要したので、プログラマたちは FORTRAN、BASIC、C などの高級言語を発明したのです。これらの高級言語を各マシンに対する命令に翻訳するためには、"コンパイラ" と呼ばれるものが使用されました。図 2 でこのことを示しています。今では、プログラムを 1 度記述すれば、各種のマシン用にコンパイルするだけで済みます。すばらしいでしょう! C は、システム プログラミング用として好まれる言語になりました。それは C が "最低レベルの高級言語"、つまりマシンとの間で解決すべき問題が最小であるからです。現在使用されているほとんどのオペレーティング システムは C で記述されています。もしかすると、パフォーマンスが重要であったり、ハードウェアとのインターフェイスになったりするような部分については、アセンブリ言語で記述されているかもしれません。

これより何年も後に、C をオブジェクト指向にし、もっとおもしろくなるよう改良して、C++ が生まれました。Bjarne の言葉をここでも引用します。"C++ は、著者とその友人が、アセンブラ、C、または各種の近代的な高級言語でプログラミングしなくて済むように設計された。主要な目的は、優れたプログラムを個々のプログラマがより簡単に、より快適に記述できるようにすることである。" ところが、C++ はすばらしかったのですが、高級言語どうしのやりとりは容易ではありませんでした。C++ でコードを記述してそれを BASIC で使用したり、その逆を行うことは不可能であったり、少なくとも相当な困難を伴いました。それぞれの言語は、それぞれの独自の世界で動作していたのです。スタンドアロン アプリケーションの場合はそれでもよかったのですが、アプリケーションが複雑さを増し、環境の分散度が高くなるにつれ、コード共有へのニーズが深刻になり始めました。最初のサブルーチンが生み出されて以来ずっと、プログラマたちは完全にカプセル化された再利用可能なコンポーネント (プログラマが組み合わせることでアプリケーションの作成に使用できるような、小さな構成要素) への道を模索してきました。しかしなぜ、すべてのパーツを同じ言語で記述しなければならないのでしょうか。

共同利用可能なコンポーネントを実現しようと、何年にもわたってさまざまなソリューションが展開されました。当初から、各言語にはライブラリが付属していました (C のランタイムや printf を思い浮かべてください)。Windows の世界では、メモリ節約のために、DLL によって遅延読み込み (ダイナミック リンク ライブラリの "ダイナミック" とはこのことを指しています) が提供されました。さらに DLL では、相互運用性も実現されました。Visual Basic や COBOL のような言語で、DLL を呼び出すことが可能だからです。これには、適切な C リンケージ呼び出しが DLL に発行されるようにコンパイラに指示するための import 文を使用します。しかし、アプリケーションと DLL との間のリンケージはあまりにも堅く、もろく、壊れやすいので、各アプリケーションは DLL 内の各エントリについて、名前とシグネチャを知る必要があります。また、これとは反対方向に、DLL からアプリケーションを呼び出すには、コールバックとして関数ポインタを渡さなければならないので、面倒です。このためプログラマは VBX を発明したのです。それが OCX になり、さらには COM が生まれました。幸いにも、COM は各言語に対して中立の立場にあります。"タイプ ライブラリ" というもののおかげで、各言語がリンク時に関数を知る必要はなく、実行時に typelib を探せばよいのです。COM もよくできているのですが、プログラミングの困難さで有名になってしまいました (皮肉にも、COM は COM の着想および実装に使用された言語である C++ でプログラミングするのが最も難しいのです)。COM にはこれ以外の問題もあります。低級すぎて、セキュリティやメモリ管理などの対応が施されていません。

最終目標

ここでは、話が 2007 年に戻っているので、.NET Framework およびその標準化されたサブセットである CLI を私たちは手にしています。CLI は、プログラミング言語とマシンの間に新しい抽象レイヤを挿入するというまったく別の方法で再利用性の問題を解決します。機械語の命令を生成する代わりに、コンパイラが MSIL コードを生成し、それを CLI 仮想マシン/ジャスト イン タイム (JIT) コンパイラがその場で機械語にコンパイルします。この仮想マシン (頭字語好きのために言えば、VES: Virtual Execution System) は、マシンの上層で一定レベルの抽象化を提供します。仮想マシン自体は新しい概念ではなく、実際に以前から存在していました。Pascal や ZIL (Zork Implementation Language: 私も働いていた Infocom で、ゲーム制作用に内部的に使用されていた) などの言語は、高級言語によるプログラムを P コード (または Z コード) と呼ばれるものにコンパイルしたうえで、それを仮想マシンで翻訳することによって機能します。一方、CLI ではすべての言語で使用できる、共通 (つまり、Common Language Infrastructure の Common の意味) の仮想マシンが提供されています。CLI は、クラス、プロパティ、継承、リフレクションなどの基本概念をサポートします。VES/VM では、メモリ管理やセキュリティなどが提供されるので、プログラマは、バッファ オーバーランなど、悪意のあるウイルスによる不正アクセスの原因となりかねないバグに悩まされる必要がなくなります。.NET Framework と CLI が普及するにつれ、Visual Basic、Fortran、COBOL、C# などの高級言語は、どれも互いに似通った、C++ に近い形に姿を変えてきました。クラス、継承、メンバ関数など、基礎を成す CLI の概念をサポートしなければならないからです。言語ごとに異なる特色は残っているので、プログラマは .NET Framework を使用するために一から勉強しなおす必要はなく、新しい概念をいくつか学ぶだけで済みます。

つまり、プログラマがどれでも好きな言語でクラスを記述し、それを他のプログラマが好きな言語でコンシュームすることが可能になったのです。だれでも任意の言語で任意のコンポーネントをプログラムでき、プログラマが最小限の努力を払えば、あらゆるコンポーネントどうしのやりとりをシームレスに行えるというわけです。プログラマたちは、セキュリティ、ガベージ コレクション、その他のインフラストラクチャ (つまり、Common Language Infrastructure の Infrastructure の意味) などの機能のメリットを享受できます。もし Redmond の面々が新しい CLI 機能を明日追加したら、その機能のメリットをすべての言語で享受できることになります。Windows Vista™ と .NET Framework 3.0 (Windows Presentation Foundation、Windows Communication Foundation、Windows Workflow Foundation、Windows CardSpace™ などの新しいテクノロジに関連した 10,000 もの新しいクラスで構成されている) の組み合わせにおいて、Windows 自体が CLI クラスとして書き直されています。再利用可能な、言語に依存しない、共同利用可能なコンポーネントというゴールに、遂に到達したようです。これは、驚くべき進化を段階的にたどり、大規模なパラダイム シフトが行われた結果であるといえます。祝福しましょう。これからは、プログラミングが本当の意味で容易になっていくのです。

これらを冷静に考えてみると、C++ がこの "すばらしい新世界" に参加せずにいられるわけはないのです。そこで C++/CLI の出番です! C++/CLI がなければ、C++ は、最近のプログラミング言語で唯一、Windows プログラミングに使用できない言語という寂しい位置付けになっていたことでしょう。C++ はゆっくり消滅するか、そうでなくてもすっかり取り残されることになったかもしれないのです。C++/CLI が登場したおかげで、このようにならなくて済みました。私のように、C++ を愛するプログラマが、今後何世代にもわたり C++ を使い続けることができるのです。図 2 は、物理マシンではなく仮想マシンへのパラダイム シフトと、"C++" に "/CLI" が付く前と後の位置付けを示しています。

図 2 プログラミングの進化
図 2 プログラミングの進化(画像を縮小するには、ここをクリックします)
図 2 プログラミングの進化
図 2 プログラミングの進化 (画像を拡大するには、ここをクリックします)

ここで一応、さよなら

ここで、お知らせしたいことがあります。実は今回で、小生が "C++ アット ワーク" に寄稿するのは最後になりました。記事は現役引退ですが、長い在任期間をほめてもらえると信じたいです。ほら、すばらしい選手が引退するときみたいに。新しく出発することを決めたのは、マイクロソフトや MSDN® Magazine 側で、C++ への取り組み方が欠如しているなどという理由ではなく、14 年近くにもわたり 164 もの記事を書いてきたので (これで 165 個目)、ちょっと疲れてしまったというのが本当のところです。ここでも逸話があります。編集責任者の Josh Trupin は最近、私のことを "MSDN Magazine の Cal Ripken" と呼んでいたのです。まあ、私は野球ファンではないのですが (フットボールの方が好きなので)、Cal Ripken が誰であるかは知っています。でもここで、実際のところを知りたくなりました。Cal はいったい、何試合に連続出場したんだろう? Web で情報を探してみたところ、2,632 試合でした。また、略歴には、"多くの専門家は、Cal がちょっとひと休みしたなら、さらによい選手になっていたであろう" とも記されていました。なるほどね。ではこれをメッセージとして受け取ることにします。でも心配はご無用。別に MSDN Magazine に二度と投稿しないというわけではありませんので。

お別れする前に、私に質問を送り、間違いや抜けのご指摘によって私が正直でいられるように気遣ってくださり、思いやり深い賞賛の言葉 (ずっと大事にします) を贈ることで私のエゴを増長させてくださった読者のみなさんに、感謝の意を表したいと思います。何人かの読者などは、私が記事を書けるように、出荷前のソリューションをテストしてくださいました。これまで一緒に仕事をしてきた、MSDN Magazine のすばらしい仲間たちにも感謝を捧げたいと思います。Steve、Josh、Joanne、Eric、Etya、Terry、Laura、Joan (2 人とも)、Nancy、Val、Milo (順不同)。誰か抜けてる? 彼らは実に広い心で何年にもわたって私をサポートしてくれ、私が書きたいことをほとんど何でも書くという自由をくれました。特に、Gretchen Bilson に感謝したいと思います。彼女は数年前に退職しましたが、1993 年、『MSDN Magazine』がまだ『Microsoft Systems Journal』だったころに、私を雇ってくれました。ありがとう、Gretchen!

では、ここまでにしましょう。ひとまずはさようなら。でも永遠の別れという意味ではありません。不死身の Arnold Schwarzenegger 氏のように、オーストリア人風のモノトーンで、"I'll be back" と言いましょう。そしていつも言うことですが、プログラミングを楽しんでくださいね!

Paul DiLascia は、フリーランスのソフトウェア コンサルタントであり、Web/UI 全般のデザイナーでもあります。著書に、『Windows++: Writing Reusable Windows Code in C++』(1992 年 Addison-Wesley 発行) があります。Paul は空き時間を利用して MFC クラス ライブラリ "PixieLib" の開発を行い、自分の Web サイト www.dilascia.com で公開しています。

"ぎゃっ!" の山 (失礼、今何と?)

今回は形式ばらなくてよいということなので、 これまでに私に送られてきた、えーと、特に "珍しい" 質問をご紹介して楽しく終わろうと思います。私はこれらの質問を Gak というフォルダにまとめています ("Gak" は "ぎゃっ!" というような意味)。これらをカテゴリ別に分類してみます。たとえば、言語に関しては常に一定量の質問が送られてきます。

C++ はどこでダウンロードできるのですか?

みんなが C++ はすごいと言うけれど、どこがすごいのですか? C++ にできて、Visual Basic や Delphi にできないことは何ですか? 私は今までにこれらを両方使用しました。Visual C++® も使ってみましたが、必要な機能のほとんどは、これらすべての言語で見つかりました。単に、年配の上級プログラマの方々が使い慣れているので、よさそうに聞こえるだけなんでしょうか?

おっと。"年配の上級プログラマ" が私のことを指しているのでなければいいのですが。次にご紹介する質問でわかるように、ポインタも混乱の元となることがよくあります。

今だに、ポインタ (変数、オブジェクト、機能) が理解できません。ご説明いただけないでしょうか。あるいは、学習用の資料をお送りいただけると助かります。

C++ の初心者です。ポインタとは何であり、何に使うのか、疑問に思っています。いろいろな本を読みましたが、説明が非常に複雑です。ポインタは、特定のメモリ位置を指すということはわかっているのですが、それがなぜ必要なのかがわからないのです。コンピュータは、それぞれの場所を自動的に記録していると思っていたので。お返事をお待ちしています。

(この質問はライターである友人の Matt Pietrek と John Robbins に送り、アドバイスを求めました。Matt の提案はこうでした。"君は正しいと言ってあげなさい。ポインタは、古くなった概念だよ。もうポインタについて勉強する必要はないよ。" John の答は "ポインタというのは犬の種類だと思ってたよ。何か他にもあるのかい?")

次に行きます。コンパイラ関係の質問も 1 つか 2 つはいつも送られてきます。

[原文のまま] Dir Paul、コンパイルの間。コンパイルはいかに MFC ライブラリをリンクしますか? DLL がときどきコンパイルします。接続しているからです。これまで、私はこのソースをコンパイルしません。お願いします。コンパイルの方法か、ソースを送りなさい。よい 1 日を。

でももっと多く送られてくるのが、Windows 関連の質問です。

あなたのキーボード/マウス検知プログラムを見つけました。それは私にとって驚くべき経験でした。でもおたずねしたいことがあります。現時点ではあなたのプログラムは、"タイム カウント パネル" にフォーカスがあれば動作しますが、これにフォーカスがなくてもプログラムがきちんと動作するようにする方法はありますか? 解決を試みたのですが、行き詰ってしまったのでメールさせていただきました。お時間をとらせてすみません。

でも本当をいうと、この人が言っている "キーボード/マウス検知プログラム" とは何のことかわからないのです。これまでに作成したこともありません。驚くべきものなら覚えているはずですが。まあ仕方ありません。

数学に関する質問も、ときどき送られてきます。

文字を "コード化" し、その後その文字を "解読" するための一連の演算を使用するプログラムを作成しています。解読の部分では、% の逆操作が必要なので、非常に苦労しています。残余も % もわかるのですが、元の数値を得ることができないようなのです。たとえば、演算の残余が 22 であるとすると、演算は (数値)%63 ですが、この数値は何でしょうか? お教えいただけると嬉しいです。

うーん。この人がなぜ、文字の解読に "非常に苦労する" のか、わかる気がします。n%a=b (a と b は指定値、n は未知) には無数の解があるからです。悲しいかな、この人は授業で A はもらっていないでしょう。授業といえば、明らかに "宿題を私の代わりにやって" というカテゴリに分類される質問も、一定の割合で送られてきます。

[原文のまま] 20 個の数字を入力して昇順に並べる、短いプログラムを作りたいのですが。

以下の 2 つのプログラムに関して、手を貸してもらえませんか? 明日の正午までに必要なんです。助けてください!

ユーザーに対して期首残高を要求するプログラムを記述し、ユーザーが「-1」を入力するかユーザーの口座残高がゼロを下回るまで、引き出し額を口座に借方記入しなさい。ユーザーの期首残高、期末残高、小切手の合計発行数、小切手の合計発行金額、小切手の平均額を表示しなさい。

給与プログラムを記述して、ユーザーの隔週の純支払を表示しなさい。必ず労働時間数と時間給がユーザーから入力されるようにしなさい。時間外手当にも対応が必要です。給与総額に対する差し引き額として合計 18% を全員が支払うものとします。ユーザーの給与額、純支払、年収を表示しなさい。

(実際には 3 つ目もあるのですが、読者のために割愛します。) 何ページもの長いソース コードの入った質問をよく受け取るのですが、これらは普通、読まずに削除します。私が書く記事に関係のないトピック (たとえば Visual Basic や Microsoft Access™ に関するもの、あるいは Excel® のプログラミング方法など) についての質問が送られてくることもあります。一般ユーザーから、"マシンがクラッシュしたのですが、どうすればいいですか" のような質問をもらったこともあります。あるいは、"コンピュータをシャットダウンするのに、[スタート] ボタンを押さなければならないのはなぜですか?" というのもありました。でもよく考えてみれば、非常にまともな質問ですね! 本当に、なぜなんだろう? (Raymond Chen がこの質問に答えています。) また、次のように、カテゴリ分類が不可能な質問もあるのです。

Paul さん、平均的な開発者が 1 日で記述できる Visual C++ コードの平均行数を示している統計サイトがあれば、教えてください。よろしくお願いします。

DiLascia さん、どうすればあなたのようになれますか?

拝啓、私は MSDN をよく拝読しておりまして、MSDN の MFC のセクションであなたのことを知り、嬉しくなったので、おたずねします。"マイクロソフトで働こうと思った理由は何ですか?" Visual C++ のプログラミングが非常に好きです。スキルと、価値と、夢のために、マイクロソフトで働きたいと思っています。現在、私は MFC でマルチメディア プログラムを作成できます。学校では、器用な方だと思います。しかし私の英語は "まあまあ" のレベルです。プログラミングのせいで、5 年間彼女がいません。プログラミングがとても楽しいからです。このようなメールは何の役にも立たないと思うかもしれませんし、それも理解できます。でも、返信をいただけると嬉しいです。マイクロソフトで働くには、どうすればよいか教えてください。よろしくお願いいたします。

私が気に入っているのは、"まあまあ" レベルの英語 (私の中国語に比べると間違いなく数段上) にかかわらず、この学生さんの熱心さが明確に伝わってくることです。私は普通、"すみませんが、わかりかねます。" としか言えなくても、ほとんどの質問に答えるようにしています。多くの読者は、私が実際に返信すると、驚かれるようです。

Paul、あなたのように有名な著者が、赤の他人の質問に対してこんなにもすばやく答えてくれるとは思っていませんでした。あなたは博識なプロフェッショナルであるばかりか、とても良い人だと思います。ありがとうございました。

うーん困った、こちらこそ、ありがとう、Mike! 最後に、この秀作を紹介します。

Windows プログラミングで、"サイキック ウィンドウ" とは何ですか? .実をいうとスペルも合っているかどうか確信がありません。友人に聞かれたので、どうか教えてください。

残念ながらこの質問の答は私にもわかりません。Uri Geller 氏に尋ねた方がいいかもしれませんね。これを読んでいる皆さんの中に、サイキック ウィンドウについて知っている人がいたら、お知らせください (宇宙テレパシーで)。

Paul DiLasciaは、フリーランスのソフトウェア コンサルタントであり、Web/UI 全般のデザイナーでもあります。著書に、『Windows++: Writing Reusable Windows Code in C++』(1992 年 Addison-Wesley 発行) があります。Paul は空き時間を利用して MFC クラス ライブラリ "PixieLib" の開発を行い、自らの Web サイト www.dilascia.com で公開しています。


{End Bracket}: コーディングの勧め

{End Bracket}
コーディングの勧め
Charles Petzold

1920 年代、 ハリウッドはトラブルに陥っていました。スキャンダルに見舞われ、"映画は性と暴力の描写を描いて堕落した" という一般の見方にさらされ、政府からの干渉という明確な危険も存在していました。映画スタジオはヘイズ オフィス (検閲機関) を設立して自らを監視し、銀幕の浄化を開始しました。ヘイズ オフィスは、正しい言葉遣いから適切な愛情表現まであらゆるものに関するルールを制定し、映画に関して私的な検閲を行う主要な組織になりました。

映画脚本家と監督は 1930 年代半ばに、それらの制限を回避する方法を見つける形で対処しました。そのような回避策の 1 つは、スクリューボール コメディ (どたばた喜劇) と呼ばれる新しい分野で、ロマンス、スラップスティック (どたばた)、警句などを特徴としていました。スクリューボール コメディには、みだらなキスというわずかな危険がありました。ロマンチックな複数の主役は、多くの場合、大半の映画で互いに争っていました。なお良いことに、対話は非常に早口で流行語を採用していたので、検閲者は聞き取ることさえできませんでした。その結果、『It Happened One Night』(或る夜の出来事)、『Twentieth Century』(特急二十世紀)、および『Bringing Up Baby』(赤ちゃん教育) などの古典映画が制作されました。仮に脚本家と監督が堕落しきった欲望のままに何でも実行する自由があったとしたら、これらの映画は決して制作されなかったはずです。

創造性は多くの場合、完全な自由から得られる産物ではなく、製作者に対して厳しい制約が課せられている場合に得られるものです。技術者と芸術家は、この概念を理解しています。アポロ 13 号が月へ向かっている途中で酸素タンクが爆発したとき、修復をするには、多大な想像力と工夫の才能が要求されました。この想像力は、手近にある限られた資材から直接導き出されたものです。

筆者は最近、これまで経験した中で、最も挑戦的でしかも興味深いプログラミングを楽しんだときに、この矛盾を思い出しました。しかも、正規のプログラミング言語さえ使用しなかったのです。最近、XAML (Extensible Application Markup Language: 拡張可能アプリケーション マークアップ言語) を使用したプログラミングに熱中していました。これは、Microsoft® Windows® Presentation Foundation の主要な部分を構成しています。

XAML を使用すると、Windows Presentation Foundation の強力なクラスにアクセスし、レイアウトの実行、およびグラフィックとアニメーションの表示をすることができます。XAML は、宣言型のプログラミング言語に分類することさえできます。ただし、より親しまれているプログラミング言語に比較すると、XAML には多くの基本的なプログラミング機能が欠けています。XAML には、ループがなく、条件分岐がなく、数値の加算や乗算をする方法すらありません。

そのうえ、筆者は XAML のみを使用して問題を解決するという制約を自ら追加しました。まるで、"living on Planet XAML" (惑星を模した、XAML という最小限の閉鎖空間で生活) を体験した気分でしたが、創造性は高まりました。XAML を使用して、複合的なグラフィック変換を定義できることを確認しました。これは、本質的には行列同士の乗算なので、必要とされる乗算と加算の機能を用意する必要がありました。さらに、XAML の中で配列をシミュレートできることも確認しました。単一の ListBox を使用して複数の要素を格納し、データ バインディングによってそれらの要素にインデックス番号を割り当てる手法です。

一番の自慢は、XAML を使用した時計アプリケーションです。時計の外周部に目盛りを付けたいと考えました。慣習に従うと、時計には 12 個の大きな目盛りと、48 個の小さな目盛りがあります。ただし、ループを使用しない場合は、通常はこれらの目盛りを表現するために、個別の XAML 要素を 60 個使用する必要があります。残念ながら、そのように多くの繰り返しマークアップを作成することはできませんでした。解決策が思いつくまで、筆者は数日にわたってうなっていました。最終的には、破線を使用して 2 つの円を描く方法で、希望していた目盛りを正確に作成することができました。この方法は正常に機能したうえに、他のほとんどの時計アプリケーションで必要とされている 60 個のグラフィック オブジェクトの代わりに、わずか 2 個のオブジェクトを使用するだけでこの作業を実行できたことを確認しました。

XAML を記述するのは楽しみです。XAML を記述するのは、挑戦です。XAML を記述すると、思いが膨らみます。ただし、一部の人の脳裏では、XAML を記述するのは常軌を逸したことかもしれません。XAML は、人間が記述することを想定したものではありません。Microsoft のあるブログ作成者が、"ツールから使用するための XAML" と表現したとおりです。また、リリースの数か月前には、XAML のいくつかの機能は、"ツールではなく間違いなく人間に利便性をもたらす" という理由で削除されました。もちろん、Visual Studio® や Microsoft Expression® Interactive Designer のような XAML 生成用のツールは、審美的な創造性を刺激する可能性はあります。ただし、コーディングに関する我々の創造性に働きかけるわけではありません。

将来のプログラマは、XAML の構文を習得することにうんざりするでしょうか?それとも、Visual Studio がボタンとコンボボックスのレイアウトを保存するために生成する、ただの "何か古風な XML 系ツール" として扱うでしょうか?

対話型のデザイナとコード ジェネレータは、間違いなく現代のプログラマのツールボックスの中で、一定の地位を占めています。それらが多くの時間を節約することは確実です。ただし、自分の立場を忘れないようにしましょう。我々はプログラマです。我々には、強力なコードを作成する技術に関して、高いスキルがあります。また、最小限のコードを使用して最大限の効果を得ることに熱中します。XAML のような言語を活用して、決して意図されていなかったことを実行する力ができます。歌わせることさえできるでしょう。

Charles Petzold は、MSDN Magazine の寄稿編集者で、『Applications = Code + Markup:A Guide to the Microsoft Windows Presentation Foundation (Microsoft Press, 2006)』(アプリケーション = コード + マークアップ : Microsoft Windows Presentation Foundation) の著者です。