 MSDN Magazine April 2008

MSDN Magazine April 2008

応答: Speech Server 2007 を使用した音声応答ワークフロー

Microsoft .NET Framework および Visual Studio ツールと統合された Speech Server 2007 を使用することで、高度な音声応答アプリケーションを開発できます。ここではその方法を説明します。

Michael Dunn パフォーマンス: ASP.NET アプリケーションのスケーリング戦略

Web アプリケーションのスケールアップに伴い、パフォーマンスの問題が生じてくる場合があります。そのような場合には、その原因に加え、問題に対処するための最良の戦略を見つける必要があります。

Richard Campbell and Kent Alstad Silverlight: Silverlight 2.0 で高度な 3D アニメーションを作成する

Silverlight を使用したアニメーションは、思っているよりも簡単です。ここでは、XAML と C# を使用し、DirectX の数学ライブラリをエミュレートすることにより、平面を折りたたんで多面体を組み立てる 3D アプリケーションを作成します。

Declan Brennan インタビュー ++: 言語の進化について語る Bjarne Stroustrup 氏

Howard Dierking が、C++ の考案者である Bjarne Stroustrup 氏に、言語の熱狂的な支持者、プログラミングの進化、およびプログラミングの未来についてインタビューしました。

Howard Dierking Office 開発: ドキュメント情報パネルを使用したメタデータの管理

この記事では、Microsoft 2007 Office system のドキュメント情報パネルを使用して、Office ドキュメントのメタデータを操作し、検索と管理を向上させます。

Ashish Ghoda いろいろな場所へ: モバイル デバイスのプロビジョニング

Windows Mobile SDK に含まれる Provisioning API のおかげで、数行のコードと XML を使って会社の各モバイル デバイスを設定できるようになりました。その設定方法を説明します。

Mike Calligaro 編集者のメモ: サービス指向の編集者のメモ

Howard Dierking が、サービスと分散アプリケーションの違いについての意見を述べ、自分のプロジェクトでのサービスへのアプローチ方法を紹介します。

Howard Dierking ツールボックス: Web アプリケーション エラーのログ記録、LINQ について、その他

より優れた状態監視のための Web アプリケーション エラーのログ記録方法、LINQ の理解に役立つ本、および Scott が勧める今月のブログを紹介します。

Scott Mitchell CLR 徹底解剖: パフォーマンスの早期および頻繁な測定 (第 1 部)

今月のコラムでは、アプリケーションの展開後に発生する結果に対処するのではなく、最初からパフォーマンスを考慮してアプリケーションを構築する方法の詳細について説明します。

Vance Morrison 基本的な本能: My Extensibility による My 名前空間の拡張

このコラムでは、My Extensibility 機能を使用して、My 名前空間の拡張を Visual Basic 開発環境に組み込む方法について説明します。

Matthew DeVore Cutting Edge: ListView のヒントと秘訣

今月は、入れ子になった ListView コントロールを使用して階層的なデータのビューを作成し、カスタム ListView クラスを派生させて ListView のイベント モデルを拡張します。

Dino Esposito 基礎: WCF でのコード アクセス セキュリティ (第 1 部)

ここでは、WCF (Windows Communication Foundation) におけるコードアクセス セキュリティと、WCF サービス向けの、部分的に信頼されたクライアントを可能にするためのソリューションについて説明します。

Juval Lowy テストの実行: LINQ を使用して SQL ストアド プロシージャをテストする

統合言語クエリ (LINQ: Language-Integrated Query) を利用することで、いろいろな操作を簡単に行うことができるようになります。ここでは、LINQ (具体的には LINQ to SQL プロバイダ) を使用して、SQL ストアド プロシージャのテストを行います。

Dr. James McCaffrey サービス ステーション: WCF ルーターを構築する (第 1 部)

ここでは、WCF クライアントおよびサービスについて見ていきます。また、アドレス指定とメッセージ フィルタの動作を調整してサービスを適切にルーティングする方法について説明します。

Michele Leroux Bustamante Windows と C++: Windows Imaging Component の基礎

Visual C++ 2008 の新しい Feature Pack を使用すると、現代的なユーザー インターフェイスを構築するための多数の新しい MFC クラスなど、Visual C++ の利便性が向上します。

Kenny Kerr { End Bracket }: ソフトウェアの魔法

映画『マイノリティ・リポート』の透明なコンピュータ画面やドラマ『CSI』で使われている派手な指紋分析ソフトウェア プログラムは思ったより早く現実になるのでしょうか。おそらくそうなります。

Julia Lerman


応答: Speech Server 2007 を使用した音声応答ワークフロー

応答

Speech Server 2007 を使用した音声応答ワークフロー

Michael Dunn
この記事の内容 : :
  • 音声応答アプリケーションの基礎
  • 音声応答ワークフローを作成する
  • 音声ガイダンス、キーワード、および文法
  • ユーザー応答を処理する
この記事は次のテクノロジを使用しています:
Speech Server 2007、.NET Framework
コンテンツ
音声アプリケーションの基礎
音声応答ワークフロー
音声ガイダンスを構築する
録音済みの音声ガイダンス
キーワードと話し言葉の文法
応答を処理する
デバッグとテストを行う

埋め込みのプレゼンス、インスタント メッセージング (IM)、電話会議とビデオ会議、およびテレフォニーは、Microsoft® Office Communications Server (OCS) 2007 の提供するユニファイド コミュニケーション機能の一部です。開発者は、さまざまな OCS API に基づいて開発を行い、各自のアプリケーションにこれらの機能やこれ以外の機能を組み込むことができます。ただし、OCS 2007 には (まだご存知でないかもしれませんが) 開発者向けのある新機能が追加されています。その機能とは、Microsoft Speech Server プラットフォームに基づく対話型音声応答 (IVR) ワークフローです。

IVR がどのようなものか、ご存知でしょうか。会社に電話をかけた際に、「商品については 1 を、カスタマー サービスについては 2 を押してください」などのメッセージをお聞きになったことはありませんか。これが、IVR システムです。

音声アプリケーションの基礎

Microsoft Speech Server 2004 では、Speech Application Language Tags (SALT) API による IVR の開発のみサポートされていました。新しいバージョンの OCS 2007 Speech Server (略して Speech Server 2007) では、SALT と VoiceXML の両方がサポートされているだけでなく、IVR アプリケーション開発用の Microsoft .NET Framework API も導入されています。また、Speech Server 2007 には、Voice Response Workflow Designer と呼ばれる、Windows® Workflow Foundation (Windows WF) に基づくビジュアル IVR アプリケーション デザイナも用意されています。

SALT や VoiceXML などの現在の IVR 開発標準は、XML タグや JavaScript に依存した Web ベースの開発モデルの使用に重点を置いています。またこれらの開発標準は、どちらも IVR アプリケーションのユーザー インターフェイスを開発することに焦点を合わせたものでもあります。ただし、複雑な IVR アプリケーションでは、データベースに含まれるデータへのアクセスなどのタスクを実行する必要があります。こうしたタスクのために、通常 .NET 準拠の言語または Java で開発されるサーバー側コードも必要です。そのため、プラットフォーム間での IVR アプリケーションの互換性という、SALT や VoiceXML に期待されている利点がいくぶん損なわれることになります。

開発者の観点から見ると、IVR アプリケーションの開発の土台として .NET Framework を使用することは、SALT や VoiceXML に比べて大きな利点があります。その主な利点は、XML タグの代わりにオブジェクト指向のプログラミング言語を使用できることです。一方、ビジネスの観点から見ると、.NET Framework ベースの IVR の場合は、既存のビジネス ロジックやデータ アクセス ロジックを再利用できるため、マイクロソフト テクノロジに対するこれまでの投資を活かすことができます。

また、重要な人材を簡単に見つけることができるという利点もあります。採用担当マネージャや採用担当者に、VoiceXML を使える開発者と .NET を使える開発者を探すよう依頼してみてください。どちらが先に見つかるでしょうか。.NET を使用している開発者の方がたくさんいます。

音声応答ワークフロー

ASP.NET ページと同様に、音声応答ワークフローのロジックは 2 つのファイルに収められた部分クラスで構成されます。一方のファイルには Visual Studio® で生成されたコードが含まれ、もう一方のファイルには独自のコード (分離コードと呼ばれる) が含まれます。音声応答ワークフローでは、標準的な Windows WF アクティビティに加えて、Speech Dialog アクティビティが使用されます。Visual Studio ツールボックスには、両方の種類のアクティビティが含まれます。

IVR アプリケーションの開発は、通話フローをレイアウトする作業から始めます。Visual Studio でレイアウトするには、図 1 に示すように、Speech Workflow アクティビティを [Speech Workflow] (音声ワークフロー) キャンバスにドラッグし、アクティビティを実行する順番に並べます。

Figure 1 Voice Response Workflow Designer
Figure 1 Voice Response Workflow Designer(画像を縮小するには、ここをクリックします)
Figure 1 Voice Response Workflow Designer
Figure 1 Voice Response Workflow Designer (画像を拡大するには、ここをクリックします)

着信に応答する IVR アプリケーションでは、通常、最初のアクティビティが AnswerCall で、最後のアクティビティが DisconnectCall です。これらのアクティビティは、新しい音声応答ワークフローの作成時に [Speech Workflow] (音声ワークフロー) に自動的に追加されます。既定では、音声アクティビティは上から下に順番に実行されます。音声応答ワークフロー アプリケーションは Windows WF をベースとしますが、シーケンシャル ワークフローのみをサポートし、ステート マシン ワークフローはサポートしません。

最も一般的に使用されるのは、Statement および QuestionAnswer の 2 つのアクティビティです。これらの 2 つのアクティビティだけを使用して、最小限の労力で非常にシンプルな IVR アプリケーションを開発できます。いずれのアクティビティも、音声合成 (TTS) または録音済みの音声ガイダンスのどちらかを使用して、ユーザーに音声を流します。Statement アクティビティの場合はユーザーに音声を流すだけですが、QuestionAnswer アクティビティの場合は、ユーザーに対し、音声認識またはタッチトーン (デュアル トーン多重周波数 (DTMF)) を使用したキー操作によって応答することを求めます。この点で、QuestionAnswer アクティビティと Statement アクティビティは異なります。

音声ガイダンスを構築する

TTS または録音済みの音声ガイダンスを通じてユーザーに情報を提供する各アクティビティには、MainPrompt というプロパティ (PromptBuilder 型) があります。MainPrompt は、アクティビティが最初に実行された際にユーザーに対して読み上げられる内容を定義します。PromptBuilder 型は、音声ガイダンスの聞こえ具合を微調整するための複数のメソッドを提供します。最も一般的に使用されるのが、SetText、ClearContent、および AppendText の 3 つのメソッドです。SetText を使用して "Thank you for calling. (お電話ありがとうございます。)" というメッセージを作成する場合の例を次に示します。

this.statementActivity.MainPrompt.SetText("Thank you for calling.");

次の 2 行では、ClearContent および AppendText を使用し、同じように "Thank you for calling. (お電話ありがとうございます。)" というメッセージを発信できます。

this.statementActivity.MainPrompt.ClearContent();
this.statementActivity.MainPrompt.AppendText("Thank you for calling.");

いずれのスニペットでも同じ目的を実現できるので、どちらを使用してもかまいません。私は、通常、ClearContent を含む行の後ろに AppendText メソッドが続く場合には SetText メソッドに置き換えています。これは、コードは 2 行よりも 1 行の方が扱いやすいという単純な理由からです。

Statement アクティビティ (図 2 を参照) には、音声ガイダンスの実行のたびに生成される標準的なイベント セットがあります。これらのイベントのいずれかに静的な音声ガイダンスを設定すると、音声ガイダンスが実行されるたびに、アプリケーションによって音声ガイダンスの同じ値が書き込まれるようになります。

Figure 2 Statement Activity Order of Event Execution
Figure 2 Statement Activity Order of Event Execution(画像を縮小するには、ここをクリックします)
Figure 2 Statement Activity Order of Event Execution
Figure 2 Statement Activity Order of Event Execution (画像を拡大するには、ここをクリックします)

アクティビティの初期化前に静的な音声ガイドを設定する (たとえば、アクティビティではなくワークフローの初期化イベントに設定する) 方が、より効率的です。また、音声ガイダンスが何らかの動的な値に依存している場合も、アクティビティの初期化前に値を設定する方がいっそう効率的になります。実際には、Visual Studio ツールを使用することにより、最も効率よく静的な音声ガイダンスを設定できます。Visual Studio ツールでは、ワークフローのコンストラクタが InitializeComponent メソッドを呼び出す際に 1 回だけ読み込まれるリソース ファイルに、音声ガイダンスのテキストが追加されるからです。

動的な音声ガイダンスの場合はどうでしょうか。ユーザーに音声を流すだけでなく、ユーザーによる入力を待つことも必要となるアクティビティの場合、話は変わってきます。その良い例が QuestionAnswer アクティビティです。QuestionAnswer は同じ実行順序に従いながら、ユーザーによる入力も受け付けます。したがって、ユーザーが話した内容をシステムが認識できない場合には、再度 TurnStarting イベントが発生します。この場合、データベース検索など動的なリソースの使用が繰り返されるだけでなく、同じ動的な音声ガイダンスを再度設定することになるため、非効率的です。動的な音声ガイダンスをアクティビティの実行時に設定する必要がある場合は、TurnStarting イベントではなく Executing イベントに設定してください。TurnStarting イベントは QuestionAnswer アクティビティが実行されるたびに生成されるのに対し、Executing イベントが生成されるのはセッションごとに 1 回だけだからです。

実際に読み上げられるテキストは、ユーザーに対してそのアクティビティが実行された回数、およびユーザーが返した応答に基づいて変わります。読み上げるテキストに、対応する音声ガイダンスの種類を割り当てることで、特定の時点に読み上げられる内容を定義できます。

QuestionAnswer アクティビティには、Statement アクティビティの音声ガイダンスに加えて、さらに 4 種類の音声ガイダンスがあります。Silence、Escalated Silence、No Recognition、および Escalated No Recognition です。MainPrompt の設定は必須ですが、これらの音声ガイダンスは省略可能です。Silence 音声ガイダンスは、InitialTimeout プロパティで指定されている時間内にユーザーが応答しなかった場合に流されます。それでもなおユーザーが応答しない場合には、Escalated Silence 音声ガイダンスが再生されます。No Recognition および Escalated No Recognition 音声ガイダンスは、Silence および Escalated Silence 音声ガイダンスと同じ規則に従って流されますが、使用されるのは、ユーザーからの応答をアプリケーションが認識できなかった場合です。

動的な音声ガイダンスの場合、テキスト リソースを音声に変換する際にいくつか問題が発生します。最も一般的なのが、日付と時刻という 2 つの問題です。一般的なデータ ソースには、"2008/01/01" のような読み取り可能な形式で日付が格納されています。これを音声ガイダンスとして流す際に、"2008 年 1 月 1 日" と読み上げられるようにするにはどうすればよいでしょうか。こうした一般的な日付形式を一般的な音声形式に変換するために使用できるのが、AppendTextWithHint メソッドです。このメソッドは、AppendText が受け取るのと同じ文字列パラメータだけでなく SayAs パラメータも受け取ります。SayAs パラメータは、読み取り可能なテキストを音声テキストに変換するためのさまざまなオプションを含むシンプルな列挙体です。例を挙げると、日付を変換する場合は次のようなコードを記述します。

this.statementActivity.MainPrompt.AppendTextWithHint(
    "01/01/2008", Microsoft.SpeechServer.Synthesis.SayAs.Date);

録音済みの音声ガイダンス

既定では、音声応答ワークフローでは TTS エンジンの音声を使用し、音声ガイダンスに定義されているテキストを読み上げます。ただし、TTS エンジンを使用する場合は高い処理能力が必要になります。また、TTS を使用する他のシステムと同じような音声になってしまう可能性もあります。

そこで、録音済みの音声ガイダンスを使用するという選択肢もあります。テキストを割り当てる代わりに、音声ガイダンスをコーディングして WAV ファイルを再生することもできますが、単に音声ガイダンス データベース プロジェクトをソリューションに追加する方が賢明です。音声ガイダンス データベースを使用すると、アプリケーションで使用する WAV ファイルを簡単に作成、編集、および管理できるほか、それらのファイルを圧縮形式で格納できます。この圧縮形式により、音声ガイダンスの読み込みが速くなります。音声ガイダンス データベースを使用する場合、アプリケーションの音声ガイダンスに対してコードの変更を加える必要は一切ありません。

WAV ファイルを音声ガイダンス データベースにインポートまたは作成する際には、WAV ファイルのトランスクリプションを割り当てることができます。アプリケーションではテキストを読み上げる際に、音声ガイダンスのデータをチェックし、トランスクリプション フィールドに一致する値があるかどうかを確認します。たとえば、"Thank you for calling. (お電話ありがとうございます。)" と読み上げる必要があり、音声ガイダンス データベースに "Thank you for calling. (お電話ありがとうございます。)" というトランスクリプションがある場合には、TTS を使用してテキストを合成する代わりに、関連付けられている WAV ファイルが再生されます。

アプリケーションに膨大な数の音声ガイダンスが含まれる場合、こうした WAV ファイルを作成、インポートするプロセスには時間がかかる可能性があります。次の音声ガイダンスについて検討してみましょう。

  • "I'll transfer you to a representative. (担当者におつなぎします。)"
  • "I'll transfer you to a loan officer. (融資担当者におつなぎします。)"
  • "A representative will be with your shortly. (担当者におつなぎしますので、しばらくお待ちください。)"
  • "A loan officer will be with you shortly. (融資担当者におつなぎしますので、しばらくお待ちください。)"

4 つの異なる録音済み音声ガイダンスを作成する代わりに、抽出の手法を利用できます。抽出を利用することで、ある WAV ファイルのフレーズの全体または一部を、別のファイルのフレーズの全体または一部に結合できます。自動的に抽出を実行することはできないので、トランスクリプション フィールドで、トランスプリクションのどの部分で抽出を行うかを指定する必要があります。これにより、音声ガイダンス データベースに 4 つの異なる WAV ファイルを格納する代わりに、抽出を示す角かっこを持つ 2 つの WAV ファイルを含めればよくなります (図 3 を参照)。

Figure 3 Prompts with Extraction Annotated
Figure 3 Prompts with Extraction Annotated(画像を縮小するには、ここをクリックします)
Figure 3 Prompts with Extraction Annotated
Figure 3 Prompts with Extraction Annotated (画像を拡大するには、ここをクリックします)

アプリケーションが "I'll transfer you to a (...におつなぎします)" というフレーズを見つけると、音声のその部分が他の部分と自動的に結合されます。検出された音声ガイダンスの一部が音声ガイダンス データベースに含まれていない場合には、録音済みの音声ガイダンスと TTS を組み合わせることもできます。したがって、"I'll transfer you to a mortgage specialist (住宅ローン担当者におつなぎします)" という別の音声ガイダンスを後で追加するのであれば、一時的に "I'll transfer you to a (...におつなぎします)" というフレーズには WAV ファイルを使用し、"mortgage specialist (住宅ローン担当者)" というフレーズは TTS エンジンを使用して合成することで、ガイダンスを流すことができます。

キーワードと話し言葉の文法

これで、アプリケーションで音声を流すことができるようになりましたが、さらに音声認識または DTMF を通じてユーザーからの応答を受け付けることが必要な場合もあります。Speech Server 2007 は、ユーザーが話すすべての内容を自動的に認識することはできません。そのため、受け付け可能なユーザーの応答を指定する必要があります。これらの応答は、アプリケーションの文法で定義します。音声認識アプリケーションで使用される文法には、キーワードと話し言葉の 2 種類があります。キーワードの文法は、応答で使用される特定の単語に基づきます。キーワードの文法は、ユーザーに " Do you want Account Information, Loan Inquiry, or to speak with a representative? (アカウント情報のご確認か、ローンに関するお問い合わせでしょうか。あるいは担当者との通話をご希望でしょうか)" などの非常に直接的な質問をする場合に役立ちます。

受け付け可能な回答の一覧に含まれていない内容をユーザーが話した場合には、たとえユーザーの応答の内容がアプリケーションで検索中の内容とよく似ていても、"I didn't understand what you said. Please say ... (お答えいただいた内容を認識できませんでした。... と言ってください。)" のようなメッセージが返されることになります。

一方、話し言葉の文法の場合は、ユーザー側の役割を減らし、ユーザーが求めている事柄をアプリケーション側で判断するようにすることで、この問題に対処します。この場合は、直接的な質問の代わりに、ユーザーに対して "How can I help you? (どのようなご用件でしょうか)" などの制限のない質問をします。

これは、より自然な質問の方法です。ユーザーからはより自然で複雑な回答が返ってくるでしょう。Conversational Grammar Builder を使用すると、話し言葉の文法を簡単に作成できます。そのため、統計的言語モデルを自身で処理する手間がほとんど不要になります。

図 4 に示すように、Conversational Grammar Builder は、[Keywords] (キーワード) と [Answers] (回答) の 2 つのセクションに分けられています。[Answers] (回答) セクションには、"概念の回答"、"キーワードの回答"、および "コマンド" の 3 つのオプションがあります。キーワードの回答の場合は、発信者が完全に一致するフレーズを言わないと認識されません。一方、概念の回答の場合は、事前に定義した回答のフレーズのいずれかに類似した応答をしても認識されます。この点で、概念の回答とキーワードの回答は異なります。

Figure 4 Conversational Grammar Builder
Figure 4 Conversational Grammar Builder(画像を縮小するには、ここをクリックします)
Figure 4 Conversational Grammar Builder
Figure 4 Conversational Grammar Builder (画像を拡大するには、ここをクリックします)

この例 (図 4 を参照) では、メイン メニューの音声ガイダンスに対し、概念の回答を 1 つ追加できます。一般に、概念の回答は、ユーザーに応答を求める 1 つの質問を示します。この場合は、メイン メニューの質問の "How can I help you? (どのようなご用件でしょうか)" です。

次に、この音声ガイダンスで受け付けられるようにする回答ごとに、概念の回答に概念を 1 つ追加する必要があります。先に作成したメイン メニューの QuestionAnswer アクティビティの場合、MainMenu という名前の概念の回答と、AccountInformation、LoanInquiry、および Representative という 3 つの概念が含まれます。

次に、"I'd like to pay my loan. (ローンを返済したい。)" というフレーズについて考えてみましょう。このフレーズが単にキーワードに基づいて評価された場合、ユーザーは実際には支払いを行うつもりであるのに、新しいローンに関する問い合わせとして認識されるおそれがあります。このフレーズは、メニューの回答として AccountInformation に含まれている可能性があるからです。

[Answer Examples] (回答の例) ペインには、ユーザーが話す可能性のあるフレーズを追加できます。定義した概念ごとに、回答例のフレーズを追加する必要があります。Representative ノードには、次のような一連のフレーズを追加できます。

  • "I need to talk to a person. (人と話す必要があります。)"
  • "Can I speak with someone? (だれかと話せますか。)"
  • "I'd like to talk with a representative. (担当者と話したい。)"

次に、キーワードの使用について考える必要があります。これは認識のためではなく、入力した [Answer Examples] (回答の例) のフレーズを拡張するためです。"person (人)"、"someone (だれか)"、および "representative (担当者)" は、いずれも同じもの (担当者) を示していることに注目してください。ユーザーが "I need to talk to a representative. (担当者と話す必要があります。)" と言う場合も考えられます。各回答の例で 3 つの異なる文章を作成する代わりに、キーワードを作成して、文章をより動的に変化させることができます。話し言葉の文法で概念のノードを使用する場合は、必ずしもキーワードを使用する必要はありませんが、キーワードを使用すると、回答のフレーズの作成に要する時間を短縮できます。図 4 は、キーワードを使用することで 3 つのフレーズを 1 つにまとめたようすを示しています。

Conversational Grammar Builder の [Keywords] (キーワード) の領域には、[Answers] (回答) の Representative ノードに対応するキーワード コンテナが必要です。この場合は、コンテナの名前は RepresentativeKeys です。このノードで "Person (人)" というキーワードを追加できます。[Keyword Phrase] (キーワードのフレーズ) 領域には、メイン キーワードに属す単語またはフレーズを追加します。たとえば、"Person (人)" というキーワードに、ユーザーが担当者のことを示す際に使用する可能性のある一連の単語 ("person (人)"、"representative (担当者)"、"someone (だれか)" など) を追加できます。

キーワードの目的は、これらの単語を区別することではなく、ユーザーが担当者のことを表す際に使用する可能性のある単語の一覧自体にあります。アプリケーションで "person (人)" と "representative (担当者)" という単語の違いを判断する必要がある場合は、[Keywords] (キーワード) にそれぞれのキーワード コンテナ ノードを追加します。

これで、Representative 概念ノードで、RepresentativeKeys キーワード ノードを参照できるようになりました。次に、[Parse] (解析) ボタンをクリックします。すると、概念のフレーズで見つかったキーワードが置き換えられます。また、置き換えられる際に、その単語が静的なものではなく、参照されるキーワードに属す変数であることを示すかっこが付けられます。緑色のチェックマークは、フレーズの中でキーワードが使用されていることを表し、赤色の "x" はキーワードが使用されていないことを表します。概念の種類の文法の場合、キーワードは必須ではありません。そのため、フレーズでキーワードが参照されなくてもかまいませんし、実際にそのような場合はよくあります。

QuestionAnswer アクティビティやユーザー入力を受け付けるその他のアクティビティの作成済みの文法は、参照できます。Visual Studio 環境を通じて参照することも、分離コード ファイルで参照することもできます。音声ガイダンスの場合と同様に、Visual Studio を使用せずにコードで文法を設定する場合は、イベントの実行順序に注意してください。文法を繰り返し設定する必要はありません。

応答を処理する

これで、アプリケーションでメッセージを読み上げたり、応答を受けたりできるようになりました。次に、ユーザーが話した内容の結果を取得する必要があります。コードを通じてこうした結果を取得するには、QuestionAnswer アクティビティの RecognitionResult プロパティを使用します。Text プロパティにより、ユーザーが話したフレーズを正確に取得できます。

this.questionAnswerActivity.RecognitionResults.Text;

ユーザーが "I need to talk to a person. (人と話す必要があります。)" と応答した場合、認識結果の Text プロパティには "I need to talk to a person. (人と話す必要があります。)" が含まれます。担当者との通話を希望していることを意味するフレーズであることを、アプリケーションはどのように認識するのでしょうか。ここで活躍するのが Semantic 値です。アプリケーションにとって、ユーザーが "I need to talk to a person. (人と話す必要があります。)" と話したのか、それとも "Can I speak with someone? (だれかと話せますか。)" と話したのかは重要ではありません。ユーザーが担当者との通話を希望しているということだけを認識する必要があります。

Text プロパティの代わりに SemanticResult プロパティを使用して、キー値を指定できます。この場合は、キー値は概念の回答の名前である MainMenu です。

this.questionAnswerActivity.RecognitionResults.Semantics[
  "MainMenu"].Value.ToString();

このコードは概念の名前を返します。ユーザーが "Can I speak with someone? (だれかと話せますか。)" と喋った場合は、"Representative (担当者)" を返します。

これで結果は取得できましたが、それらの結果を使用して何らかの作業を行う必要があります。通常、音声応答アプリケーションでは、各選択肢を表す分岐を作成します。Visual Studio を使用して文法を参照した場合、QuestionAnswer アクティビティを右クリックして、[Generate Branching] (分岐の生成) を選択できます。この操作により、各分岐とそれらの基準が自動的に作成されます。別の方法として、各分岐を自分で作成し、認識結果に基づいてユーザーを分岐に導く際の基準を設定することもできます。たとえば、MainMenu では、認識に成功した際にユーザーを担当者につなぐ Representative という分岐を作成できます。

デバッグとテストを行う

音声応答ワークフロー アプリケーションについても、使い慣れた方法でデバッグを行うことができます。ただし、他のほとんどのアプリケーションとは異なり、IVR アプリケーションの音声を聞いたり、アプリケーションに対して言葉を発したりできる必要があります。図 5 に示すとおり、Speech Server では [Voice Response Debugging Window] (音声応答デバッグ ウィンドウ) を Visual Studio に追加できます。

Figure 5 Debugging Window
Figure 5 Debugging Window

このデバッグ用電話を使用すると、アプリケーションを実際に展開したり、電話回線を接続したりせずにテストできます。マイクを使用して文法をテストすることも、アプリケーションの文法をテストするためのテキストを単純に入力することもできます。これは単純な機能のように思えますが、他のほとんどの IVR プラットフォームでは、展開せずにテストやデバッグを行う機能が見落とされています。Speech Server 2007 の詳細を確認し、ツールをダウンロードするには、microsoft.com/uc/products/speechserver.mspx の Microsoft 音声テクノロジ Web サイトを参照してください。

Michael Dunn は、マイクロソフトのシニア コンサルタントです。また、『Pro: Microsoft Speech Server 2007』の著者でもあります。また、blogs.msdn.com/midunn (英語) で彼のブログをご覧いただけます。

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


パフォーマンス: ASP.NET アプリケーションのスケーリング戦略

パフォーマンス

ASP.NET アプリケーションのスケーリング戦略

Richard Campbell and Kent Alstad
この記事の内容 : :
  • ASP.NET のアプリケーションとデータベースをスケーリングする
  • コードを最適化する
  • 効率的なキャッシュ
  • アフィニティと負荷分散
この記事は次のテクノロジを使用しています:
ASP.NET
コンテンツ
パフォーマンス方程式
スケーリングの問題
コードを最適化する
負荷分散
アフィニティ
ペイロードを最小化する
キャッシュ
データベースをスケーリングする
無限のスケーリング作業

ASP.NET のパフォーマンス アドバイザとして、私たちは普段、既にトラブルが起きている段階でプロジェクトにかかわります。多くの場合、声がかかるのは、アプリケーションの運用が実際に開始された後です。開発者にとってうまく機能したことが、ユーザーに対してはうまく機能しないのです。不満は、サイトが遅すぎるということです。経営陣は、これがテストで発見されなかった理由を知りたがりますが、開発陣は問題を再現できません。1 人以上の人間が、ASP.NET はスケーリングできないと言っています。よく聞く話だと思われるかもしれません。

世界で最もアクセス数の多い Web サイトのいくつかが、ASP.NET で動作しています。MySpace はその好例です。実際、このサイトはいくつかの異なるプラットフォームで運用された後、ASP.NET に移行されました。アプリケーションのスケールアップに伴い、パフォーマンスの問題が生じることがあります。そのような場合には、実際の問題が何であるのかを突き止め、その問題に対処するための最良の戦略を見つける必要があります。直面する最も大きな課題は、アプリケーションのパフォーマンスを隅から隅までカバーする測定方法のセットを作成することです。問題の全体を見なければ、どこにエネルギーを集中すればよいかがわかりません。

パフォーマンス方程式

2006 年 9 月、NetForecast の Peter Sevcik 氏と Rebecca Wetzel 氏が、『Field Guide to Application Delivery Systems』という論文を発表しました。この論文は、ワイド エリア ネットワーク (WAN) アプリケーションのパフォーマンスの向上に焦点を当て、図 1 に示すような方程式を含んでいました。この方程式は WAN のパフォーマンスに適用されるものですが、少し変更を加えることで、Web アプリケーションのパフォーマンス測定にも使用できます。変更した式を図 2 に示し、その各要素を図 3 で説明しています。

Figure 3 パフォーマンス方程式の要素 (Close Figure 3)

変数 | 定義
R | 応答時間。ユーザーが (リンクをクリックするなどして) ページを要求してから、ユーザーのコンピュータ上にページ全体がレンダリングされるまでの合計時間。通常は、秒単位です。
ペイロード | ブラウザに送信される合計バイト数であり、マークアップおよびすべてのリソース (CSS、JS、イメージ ファイルなど) を含みます。
帯域幅 | ブラウザとの間の転送速度。これは、非対称の場合があり、特定のページが複数のソースから生成されている場合には、複数の速度を表すこともあります。通常は、平均されて、単一の帯域幅がバイト/秒の単位で表されます。
AppTurns | 特定のページが必要とするリソース ファイルの数。これらのリソース ファイルには、CSS、JS、イメージ、およびページのレンダリング中にブラウザによって取得される他のファイルが含まれます。方程式では、HTML ページは、AppTurns の項の前にラウンドトリップ時間 (RTT) を追加することで個別に表されます。
RTT | 転送されるバイト数に関係なく、ラウンドトリップにかかる時間。すべての要求が、ページそれ自体に対して最低 1 RTT を費やします。通常は、ミリ秒単位です。
同時要求数 | ブラウザがリソース ファイルに対して同時に行う要求の数。既定では、Internet Explorer は 2 つの同時要求を実行します。この設定は調整できますが、調整することはまれです。
Cs | サーバー上の計算時間。これは、コードが実行され、データベースからデータを取得し、ブラウザに送信する応答を作成するのにかかる時間です。単位はミリ秒です。
Cc | クライアント上の計算時間。これは、ブラウザが画面上に実際に HTML をレンダリングし、JavaScript を実行し、CSS 規則を実現するなどの作業にかかる時間です。
Figure 1 The Original Performance Equation
Figure 1 The Original Performance Equation(画像を縮小するには、ここをクリックします)
Figure 1 The Original Performance Equation
Figure 1 The Original Performance Equation (画像を拡大するには、ここをクリックします)
Figure 2 The Web Version of the Performance Equation
Figure 2 The Web Version of the Performance Equation(画像を縮小するには、ここをクリックします)
Figure 2 The Web Version of the Performance Equation
Figure 2 The Web Version of the Performance Equation (画像を拡大するには、ここをクリックします)

方程式を手に入れたところで、問題となるのは各要素の測定です。最後の値である応答時間は、比較的測定が簡単です。プロセス全体の所要時間を厳密に測定するツールがいくつかあります。

ペイロードは各種のツール (websiteoptimization.com/services/analyze は優れた選択肢の 1 つ) を使用して測定でき、帯域幅 (speedtest.net を参照) とラウンドロビン時間 (Ping を使用) も同様です。websiteoptimization.com/services/analyze のようなツールでも、Web ページの HTML、CSS、JavaScript、イメージなどのサイズをレポートできます。同時要求数は、基本的に定数です (Internet Explorer® では既定で 2)。

残りは Cs と Cc ですが、これには追加の開発作業が必要になります。ページの実行が開始される厳密な秒数を記録して、実行の完了時にその時間を現在の時刻から減算するようなコードを、ASP.NET ページで記述するのは、比較的単純です。クライアント側でも同じことが言えます。HTML ページの先頭で小さな JavaScript を実行し、時刻を記録してから、ページの完了時に OnLoad イベントが発生した時点で、その時刻を減算することができます。

実際、パフォーマンス方程式を利用する Web サイトをデバッグ モードで構築する場合は、これらすべての要素に対してコーディングが可能です。そして、そうすることには適切な理由があります。ブラウザでパフォーマンス方程式の要素を定型的にレンダリングできれば、パフォーマンス上の問題がどこにあるのかを簡単に検出できます。

たとえば、ユーザーが別の大陸からアクセスするような ASP.NET アプリケーションで、帯域幅が低い場合を考えます。長い ping 時間 (> 200 ms) と低い帯域幅 (< 500 kbps) により、合計のペイロードとアプリケーション内のラウンドトリップ数がユーザーに対して大きな影響を与えます。そのようなユーザーのコンテキストでアプリケーションを捉えることは、開発者が経験する内容とは大きく異なるという点で、非常に重要です。

スケーリングの問題

私たちはコンサルタントとして、テスト環境では良好に動作するアプリケーションが実際の運用では性能が低いという場合、おそらくスケーリングの問題であるとわかっています。通常、テスト環境と実運用環境との違いは、同時実行ユーザーの数だけです。アプリケーションの性能が常に低いという場合は、スケーリングではなくパフォーマンスの問題です。

スケーリングを改善するために採用できる戦略は 3 つあります。特化、最適化、および分散です。それらの適用方法は場合によって異なりますが、実際の戦略は明快で一貫性があります。

特化の目的は、問題の特定のためにアプリケーションを小さな部分に分割することです。たとえば、イメージ、CSS、JS ファイルなどの静的リソースを、ASP.NET サーバーの外部に移すことを考えます。ASP.NET 用に調整されたサーバーは、そのようなファイルの提供に対して特に適しているわけではありません。したがって、リソース ファイルの提供用に調整された別個の IIS サーバーのグループを使用すると、実行するアプリケーションのスケーラビリティが大きく違ってきます。

圧縮や暗号化 (SSL 用) を頻繁に実行する場合は、SSL 専用のサーバーを設定すると役に立ちます。圧縮と SSL 終端用に特化されたハードウェア デバイスも考慮してください。

サーバー層を分割するためのより従来的な戦略では、データ アクセスや複雑な計算用に、Web ページを実際に生成するものとは別個のサーバーが求められるかもしれませんが、私はむしろ、3 台の Web サーバーと 2 台のビジネス オブジェクト サーバーよりは、すべてを実行する 5 台の Web サーバーを使用します。Web サーバーとビジネス オブジェクト サーバーの間で行われるプロセス外呼び出しは、すべて大きなオーバーヘッドを生じます。

特化は、既知の利益が期待される場合にのみ行う必要があります。また、最も速いソリューションが常に最良とは限りません。スケーラビリティの目標は、パフォーマンスの一貫性です。負荷の増加に伴ってパフォーマンスの範囲は狭める必要があります。ユーザーが 1 人であっても 1,000 人であっても、特定のページをすべてのユーザーに対して同じ時間で表示する必要があります。

最終的には、より効果的にスケーリングできるようサーバーのコードを最適化する必要があります。サーバー上の計算時間を除き、パフォーマンス方程式のほぼすべての要素は、線形にスケーリングされます。より多くの帯域幅をいつでも追加することができ (その時期はかなり簡単に判断できます)、クライアント上の計算時間はクライアントの数が増えても変化しません。パフォーマンス方程式の他の要素も、スケーリングについては一貫性があります。ただし、サーバー上の計算時間は、ユーザー数が増えるごとに調整が必要になります。

コードを最適化する

サーバー コードの最適化のコツは、テストを実行して、実際に違いが得られるかどうかを確認することです。プロファイリング ツールを使用して、アプリケーションを分析し、アプリケーションが最も多くの時間を費やしている箇所を突き止めます。プロセスの全体が、経験に基づいている必要があります。ツールを使用して、改良するコードを見つけ、そのコードを改良し、パフォーマンスが実際に向上したことをテストで確認し、それを何度も繰り返します。実際に大規模なサイトでは、このようなパフォーマンスのチューニングをゴールデン ゲート ブリッジの塗装作業にたとえることがよくあります。橋全体を塗り終わったころには、また最初の部分を塗り直す時期になっているというわけです。

私はいつも、スケーリングの開始点が分散であると信じている人が多いことに驚いています。「もっとハードウェアを追加しろ」と彼らは叫びます。誤解しないでください。ハードウェアの追加が有用であることは間違いありません。ただし、特化と最適化がなければ、その成果はごく小さなものになります。

特化では、アプリケーションの小さな部分を必要に応じて分散できます。たとえば、イメージ サーバーを分離した場合、イメージ サービスをアプリケーションの残りの部分から独立して簡単にスケーリングできます。

また、最適化によって特定の操作に必要な作業量を減らすと、分散にもメリットが得られます。これは、同じユーザー数にスケーリングするために必要なサーバー数の削減に、直接つながります。

負荷分散

サーバーを追加するために必要な分散を実現するには、それらのサーバー間でアプリケーションを複製し、負荷分散を行います。負荷分散には、Windows Server® 2003 のすべてのエディションに付属するサービスである、ネットワーク負荷分散 (NLB) を使用できます。NLB を使用すると、負荷分散の関係においてすべてのサーバーが同等なパートナーとなります。すべてのサーバーが負荷分散に対して同じアルゴリズムを使用し、すべてのトラフィックに対して 1 つの共有仮想 IP アドレス上で監視を行います。負荷分散アルゴリズムに基づき、各サーバーは、特定の要求に対してどのサーバーが動作するかを知っています。クラスタ内の各サーバーは、ハートビートを送信して、自分が動作中であることを他のサーバーに知らせます。サーバーに障害が発生すると、そのサーバーのハートビートが停止し、他のサーバーが自動的にそれを補償します。

NLB は、多数のユーザーがよく似た要求を行っている場合は良好に機能します。ただし、一部の要求が他の要求よりもずっと大きな負荷を生むような状況では、補償メカニズムはうまく機能しません。さいわい、そのような状況に対しては、ハードウェア負荷分散ソリューションがあります。

アフィニティ

最終的に、効果的な分散を実現するための課題は、アフィニティをなくすことにあります。たとえば、Web サーバーが 1 台しかない場合、そのサーバーにセッション データを格納することは完全に合理的です。しかし、Web サーバーが複数ある場合は、どこにセッション情報を保持すればよいでしょうか。

1 つのアプローチは、それを Web サーバー上に保持し、アフィニティを使用することです。これは本質的に、特定のユーザーからの最初の要求が負荷分散され、それ以降、そのユーザー/セッションからの要求はすべて、最初の要求と同じサーバーに送信されることを意味します。これは単純なアプローチであり、すべての負荷分散ソリューションでサポートされ、場合によっては、合理的でさえあります。

ただし、長い目で見ると、アフィニティは悩みの種となります。セッション データをプロセス内で保持すると高速かもしれませんが、ASP.NET ワーカー プロセスがリサイクルされると、それらのセッションはすべて無効になります。そして、ワーカー プロセスのリサイクルは多くの理由によって実行されます。高負荷の場合、IIS は ASP.NET のワーカー プロセスがハングしていると判断し、リサイクルすることがあります。実際、IIS 6.0 では既定で 23 時間ごとにワーカー プロセスがリサイクルされます。これは調整可能ですが、いずれにしても、ユーザーはプロセス中にセッション データを失う可能性があります。サイトが小さいうちは、これはそれほど大きな問題ではありませんが、サイトが大きくなりアクセス頻度が高まるにつれ、重要な問題となっていきます。それだけではありません。

IP アドレスによって負荷分散を行っている場合、あるサーバーが (AOL のような) メガプロキシによってヒットされると、負荷全体を 1 台では処理できなくなります。さらに、サーバーをアプリケーションの新しいバージョンで更新することがより困難になります。ユーザーがサイト上で作業を完了するまで何時間も待つか、またはユーザーをセッションから追い出して困惑させるかのいずれかになります。そして、信頼性が問題となってきます。サーバーを失い、多数のセッションを失うのです。

アフィニティをなくすことが、分散の重要な目標です。そのためには、セッションの状態データをプロセス外に移動する必要があります。それは、スケーラビリティを高めるためにパフォーマンスを低下させることを意味します。セッションをプロセス外に移すと、セッション データは、すべての Web サーバーがアクセスできる場所に記録されます。SQL Server® 上、または ASP.NET 状態サーバー上です。これは、web.config で設定されます。

また、プロセス外セッションをサポートするために必要なコーディング作業もあります。Session オブジェクトに格納されるすべてのクラスは、Serializable 属性でマークされる必要があります。これは、そのクラス内のすべてのデータが無視されるように、各データをシリアル化可能にするか、または NonSerialized とマークする必要があることを意味します。クラスをマークアップしない場合、シリアライザが動作してセッション データをプロセス外に格納するときに、エラーが発生します。

最後に、セッションをプロセス外に移動することは、セッション オブジェクト内のデータが多すぎることに気付く良い手段となります。なぜなら、ページ要求ごとに、そのように大きな BLOB データをネットワーク上で 2 回送受信するというコストがかかるためです (ページの先頭での取得時に 1 回と、ページの末尾で返すときに 1 回)。

Session オブジェクトに決着をつけたら、メンバシップやロール マネージャのような他のアフィニティ問題を追求します。それぞれの問題には、アフィニティをなくすための固有の困難があります。しかし、ASP.NET アプリケーションを真にスケールアップさせるためには、見つけられるあらゆる形のアフィニティをあぶり出し、それを解消する必要があります。

ここまでに述べたすべての戦略は、スケーリングを必要とする実際上すべての Web アプリケーションに適用できます。事実、これらの戦略は、任意のテクノロジを使用したほとんどどのようなアプリケーションのスケーリングにでも適用されます。この後は、いくつかの ASP.NET 固有の技法について見ていきましょう。

ペイロードを最小化する

パフォーマンス方程式を見ると、ペイロードが大きな影響を持つことがわかります。特に、限られた帯域幅を扱っている場合にはそうです。ペイロードのサイズを小さくすると、応答時間が向上し、送受信するバイト数が減ることでスケーリング上の利益も得られ、帯域幅のコストも節約できます。

ペイロードのサイズを減らすためにできる最も単純なことの 1 つは、圧縮を有効にすることです。IIS 6.0 では、静的ファイル、動的に生成される応答 (ASP.NET ページなど)、またはその両方を圧縮するかどうかを指定できます (図 4 を参照)。

Figure 4 Configuring Compression Server-Wide in IIS 6.0
Figure 4 Configuring Compression Server-Wide in IIS 6.0(画像を縮小するには、ここをクリックします)
Figure 4 Configuring Compression Server-Wide in IIS 6.0
Figure 4 Configuring Compression Server-Wide in IIS 6.0 (画像を拡大するには、ここをクリックします)

IIS 6.0 は要求に応じて静的ファイルを圧縮し、指定した圧縮ファイル キャッシュに格納します。動的に生成される応答に対しては、コピーは格納されません。これらは毎回圧縮されます。IIS 7.0 は、圧縮に関してはより洗練されていて、頻繁に使用されるファイルだけを圧縮します。

圧縮はプロセッサ サイクルを消費しますが、一般に、専用 Web サーバー上には余分なプロセッサ容量が多くあります。ただし、IIS 7.0 はさらに最適化され、プロセッサが本当にビジーになると、圧縮作業を中断します。また、Web サーバー自体とは独立に圧縮を行う専用デバイスもあります。

ペイロードの削減に関連したもう 1 つの領域として、ViewState があります。開発中には、ViewState の使用量が手に負えなくなることがよくあります。ほとんどの Web コントロールは何らかの ViewState を使用しており、コントロールが集中しているページでは、ViewState が何千バイトにも及ぶ場合があります。ViewState の使用を減らすには、それを必要としないコントロールでは ViewState を無効にすることです。場合により、開発者は ViewState を減らすためにコントロール自体を削除することがありますが、それは必ずしも必要ではありません。今日の Web コントロールのほとんどは、過度の ViewState の問題に敏感であるため、そのサイズをきめ細かく制御できるようになっています。また、コードやアプリケーションの実行方法を変えずに ViewState を削除したり置換したりできるハードウェア デバイスもあります。

ペイロードのサイズを減らすための最も効果的なテクノロジの 1 つが、AJAX です。AJAX は実際にはペイロードのサイズを小さくするわけではありません。単に、ブラウザに送信する合計バイト数を増やしつつ、ペイロードの見かけのサイズを減らすだけです。AJAX を使用すると、親ページが小さくなるため、最初のレンダリング時間が高速になります。その後、そのページ内の個々の要素が、サーバーにそれぞれ要求を発行して、データを格納します。

実質的に、AJAX はペイロードを長い時間に振り分けることで、他の部分を読み込んでいる間、ユーザーに何かが表示されるようにしています。そのため、AJAX を使用すると、ユーザーの全体的な操作感が向上しますが、作業の真のコストを測定するには、パフォーマンス方程式に戻る必要があります。一般に、AJAX は、クライアント上の計算時間を、ときには劇的に、パフォーマンスが許容できなくなるほど増加させます。

ページ全体を要求する代わりに、サーバーへの AJAX ラウンドトリップで個々の要素にデータを格納すると、正味のラウンドトリップ回数は減ります。ただし、多くの場合、特定のユーザーに対するラウンドトリップの合計数は増加します。AJAX によってパフォーマンスが向上したか低下したかを判断するには、入念なテストが必要です。

キャッシュ

ASP.NET アプリケーションのスケーリングの専門家は、キャッシュについて多くを語ります。基本的に、キャッシュとは、データをユーザーの近くに移動することです。標準的な ASP.NET アプリケーションでは、大規模な最適化作業が行われるまでは、ユーザーが必要とするデータはほとんどすべてデータベース内に存在し、要求のたびにデータベースから取得されます。キャッシュを使用すると、そのような状況が変わります。ASP.NET は、実際には 3 つのキャッシュ方式をサポートしています。ページ キャッシュ (出力キャッシュとも呼ばれる)、部分ページ キャッシュ、およびプログラム キャッシュ (データ キャッシュとも呼ばれる) です。

ページ キャッシュは、最も単純なキャッシュ方式です。これを使用するには、ASP.NET ページに @OutputCache ディレクティブを追加し、有効期限に関する規則を含めます。たとえば、ページを 60 秒間だけキャッシュするように指定できます。このディレクティブを配置すると、そのページの最初の要求は通常どおり処理され、データベースや、ページの生成に必要なその他のリソースがアクセスされます。その後、そのページは Web サーバー上のメモリに 60 秒間だけ保持され、その間の要求はすべてメモリから直接データが提供されます。

この例は単純明快ではありますが、残念ながら、ページ キャッシュの基本的な現実を無視しています。実質的にすべての ASP.NET ページは、その全体を特定の時間だけキャッシュできるほど静的ではありません。そこで、部分ページ キャッシュを使用することになります。部分ページ キャッシュでは、ASP.NET ページの特定の位置をキャッシュ可能とマークできるため、ページ内で定期的に変更される部分だけが計算されます。これはより複雑ですが、効果的です。

おそらく、最も強力な (そして最も複雑な) キャッシュ形式は、ページで使用されるオブジェクトに焦点を合わせた、プログラム キャッシュでしょう。プログラム キャッシュの最も一般的な用途は、データベースから取得されたデータの格納です。

データのキャッシュに関して最も明確な問題は、キャッシュした後で元のデータが変更されている可能性があることです。キャッシュの期限切れは、あらゆる形のキャッシュを実装する際に直面する、最大の問題です。ただし、メモリについても検討する必要があります。

ビジーな ASP.NET サーバーでは、さまざまな理由によって、メモリが大きな問題となります。ASP.NET ページを計算するときには必ず、ある程度のメモリが使用されます。そして、Microsoft® .NET Framework は、メモリを非常にすばやく割り当てながら、ガベージ コレクションによって、比較的ゆっくり解放するように設定されています。ガベージ コレクションと .NET のメモリ割り当てに関する議論は、それ自体が 1 つの記事であり、これまでに何回も書かれています。ここでは、ビジーな Web サーバーでは、ASP.NET アプリケーションに対して使用可能な 2 GB のメモリ空間に対する需要が大きいとだけ言っておけば十分でしょう。理想的には、そのメモリ利用のほとんどは、Web ページの計算に使用される変数や構造体に割り当てられるため、一時的なものです。

ただし、プロセス内セッションやキャッシュ オブジェクトなどの永続的なメモリ オブジェクトについては、メモリの使用量がずっと大きな問題になります。そしてもちろん、これらの問題は、アプリケーションが本当にビジーのときにしか表面化しません。

次のようなシナリオを考えてみましょう。あなたの Web サイトは、ある新しいマーケティング プロモーションをきっかけに、何千人ものユーザーからのアクセスを集め、多大な収益を上げています。このサイトでは、良好な応答時間を維持するために、ページの一部やデータ オブジェクトのグループを可能な限りキャッシュしています。ユーザーからの各ページ要求がそれぞれ少量のメモリを消費するため、メモリ消費量のバーは上昇し続けます。ユーザーの増加に従って、バーの上昇も速くなります。また、キャッシュ オブジェクトやセッション オブジェクトによって大きく跳ね上がることもあります。

メモリの合計使用量が ASP.NET の既定のキャッシュ メモリ制限値の 90 パーセントに近づいたため、ガベージ コレクションが呼び出されます。ガベージ コレクタは、メモリ空間を探索し、永続的なメモリ オブジェクト (キャッシュ オブジェクトやセッション オブジェクトなど) を選別しながら、既に使用されていないメモリ (Web ページの計算に使用されていたメモリ) を解放します。未使用メモリの解放は高速ですが、永続オブジェクトの選別は低速です。したがって、永続オブジェクトの数が多いほど、ガベージ コレクタの作業は困難を増します。このような問題の発生は、perform.exe 内で Gen 2 コレクションの数が多いことから識別できます。

ガベージ コレクションの実行中、その ASP.NET サーバーではページが提供されないことを思い出してください。すべてがキューに保持され、ガベージ コレクション プロセスの完了を待ちます。また、IIS もそれを監視しています。プロセスに長い時間がかかりすぎ、ハングしている可能性があると判断した場合、IIS はワーカー スレッドをリサイクルします。これにより、それらの永続メモリ オブジェクトがすべて破棄されるため、大量のメモリがすばやく解放されますが、何人かのユーザーを困惑させることになります。

現在は、空きメモリが少ない場合にプログラム キャッシュから自動的にオブジェクトを削除する ASP.NET 用のパッチが提供されていて、それは表面的には良いアイデアのように思えます。クラッシュよりはいいでしょう。ただし、キャッシュから削除したものはすべて、コードによって最終的に元に戻されることに注意してください。

何かをキャッシュした時点で、それが間違っているというリスクを負うことになります。たとえば、部品のデータベースおよび対応する注文ページを例として考えます。部品ページの最初の実装では、ページのレンダリングごとに、在庫に残っている部品数をデータベースから取得する要求が含まれます。これらの要求を分析すると、99 パーセントの時間は、同じ数値を何度も取得していることに気づきます。それは、キャッシュできるのではないでしょうか。

これをキャッシュする単純な方法の 1 つは、時間に基づいてキャッシュすることです。そこで、部品の在庫を 1 時間にわたってキャッシュすることにします。この手法の欠点は、だれかが部品を購入した後で、そのページに戻ったときに、表示されている在庫数が依然として同じであるということです。そのことにはクレームがあるでしょう。ただし、それよりずっと問題なのは、だれかが部品を購入しようとして、それが実際には売り切れているにもかかわらず、まだ在庫があるように表示されることです。入荷待ちシステムを構築することはできますが、いずれにしても、顧客の失望を招くことになります。

おそらく問題なのは、有効期限の処理方法でしょう。時間で区切るだけでは不十分です。部品をだれかが購入するまで在庫数をキャッシュしておき、その後でキャッシュ オブジェクトを期限切れにすることもできます。これはより論理的ですが、ASP.NET サーバーが複数ある場合にはどうなるでしょうか。どのサーバーにアクセスしたかによって、部品の在庫数が異なることになります。新しい在庫 (在庫数に加算される) の受け取りは Web アプリケーションを経由しないことを考えると、これは方法自体が誤っています。

期限切れを ASP.NET サーバー間で同期させることもできますが、これには注意が必要です。キャッシュ オブジェクトと Web サーバーの数が増えるほど、Web サーバー間で生成する対話の量は、幾何学的に増加します。

キャッシュの有効期限がパフォーマンスに与える影響も、慎重に検討する必要があります。高負荷条件下では、キャッシュ オブジェクトを期限切れにすると、多くの不満を生む可能性があります。たとえば、データベースから結果が返るのに 30 秒間かかるような高コストのクエリがあるとします。負荷が高いときには、このページが毎秒 1 回要求されるため、この高コストを節減するために、このクエリをキャッシュすることにします。

キャッシュ オブジェクトを処理するコードはかなり単純です。データベースから必要なときにデータを取得する代わりに、アプリケーションは最初にキャッシュ オブジェクトにデータが格納されているかどうかを確認します。格納されている場合は、キャッシュ オブジェクトからのデータを使用します。格納されていない場合は、データベースからデータを取得するコードを実行してから、キャッシュ オブジェクトにそのデータを格納します。その後コードは、通常どおりに実行を続けます。

ここで、次のような問題が生じます。30 秒間かかるクエリを受け取り、このページを毎秒実行している場合、キャッシュ項目にデータを格納している間に、他の要求が 29 個受信され、そのすべてがそれぞれ、データベースに独自のクエリを実行してキャッシュにデータを格納しようとします。この問題を解決するには、他のページ実行がデータベースからデータを要求しないようにスレッド ロックを追加できます。

しかし、もう一度シナリオをよく見てみましょう。最初の要求が受信され、キャッシュ項目にデータが格納されていないことを確認し、コードにロックを適用し、クエリを実行してキャッシュ オブジェクトにデータを格納します。最初の要求がまだ実行されている間、1 秒後に 2 番目の要求が到着し、キャッシュ オブジェクトにデータが格納されていないことを確認しますが、ロックがかかっているため、ブロックされます。以降 28 個の要求についても同様です。そして、最初の要求が処理を完了し、ロックを削除して、実行を継続します。他の 29 個の要求はどうなるでしょうか。既にブロックはされていませんので、同じように実行を継続します。ただし、それらの要求は、キャッシュ オブジェクトにデータが格納されているかどうかの確認を既に終えています (そして、その時点では格納されていませんでした)。したがって、各要求はロックを適用しようとし、そのうちの 1 つが成功して、再びクエリを実行します。

問題がわかりましたでしょうか。最初の要求がキャッシュ オブジェクトの処理を完了した後に到着した他の要求は正常に実行されますが、クエリの実行中に到着した要求は、困難な状況に陥ります。これに対処するコードを記述する必要があります。ある要求がロックに遭遇した場合、そのロックが解除されたときには、図 5 に示されるように、その要求はキャッシュ オブジェクトにデータが格納されているかどうかを再度確認する必要があります。おそらくその時点で、キャッシュ オブジェクトにはデータが格納されています。それが最初にロックが適用された理由であるためです。ただし、ロック中に他のコードがキャッシュ オブジェクトを再度期限切れにし、データが格納されていない可能性もあります。

Figure 5 キャッシュ オブジェクトを確認し、ロックし、再確認する (Close Figure 5)
// check for cached results
object cachedResults = ctx.Cache["PersonList"];
ArrayList results = new ArrayList();

if  (cachedResults == null)
{
  // lock this section of the code
  // while we populate the list
  lock(lockObject)
  {
    // only populate if list was not populated by
    // another thread while this thread was waiting
    if (cachedResults == null)
    {
      ...
    }
  }
}

適切に動作するキャッシュ コードを書くことは困難な作業ですが、それによって得られる利益は膨大です。ただし、キャッシュは複雑さを増加させるので、慎重に使用する必要があります。その複雑さから本当にメリットが得られることを確認してください。キャッシュ コードは常に、次のような複雑なシナリオに対してテストする必要があります。同時に複数の要求が発生した場合にはどうなるでしょうか。期限切れが短時間で起こる場合はどうでしょうか。これらの質問への答えを知っている必要があります。それにより、キャッシュ コードがスケーリングの問題をさらに悪化させるような事態を避けることができます。

データベースをスケーリングする

Web サイトをスケーリングするための通常のアプローチは、スケールアップではなく、スケールアウトです。これは主に、ASP.NET のスレッドおよびメモリの制限と、時間が短いという Web 要求の特質によるものです。

ただし、データベースのスケーリングに関しては、通常の手法はスケールアップです。データベースは、1 つの巨大な箱とするか、またはクラスタ構成の 2 つの箱とします (ただし、ある時点で実際に実行されているデータベースは 1 つだけです)。しかし、すべての大規模な Web アプリケーションでは、最終的に 1 つのデータベースでは負荷を処理しきれなくなります。そのため、スケールアウトが必要です。これは可能です。単に Web アプリケーション自体に対するのと同じ戦略を適用するだけです。最初の手順は常に特化です。データベースを論理パーティションに分割します。これらのパーティションは、データ中心で、おそらくは地域ごとに区切られます。したがって、それぞれがデータベース全体の一部分を含む、複数のデータベースが作成されます。たとえば、1 つのサーバーには東海岸のデータ、もう 1 つのサーバーには西海岸のデータが格納されます。

ただし、本当に大規模な Web アプリケーションでは、データベースを読み取り用と書き込み用に分割します (図 6 を参照)。読み取り用データベースは読み取り専用であり、書き込み用データベースからレプリケーションによってデータを受け取ります。すべてのデータ クエリは、データをできるだけ高速に読み取るよう最適化されている、読み取り用データベースに渡されます。読み取り用データベースは、本質的に、高いレベルの分散が可能です。

Figure 6 Distributed Database Architecture
Figure 6 Distributed Database Architecture(画像を縮小するには、ここをクリックします)
Figure 6 Distributed Database Architecture
Figure 6 Distributed Database Architecture (画像を拡大するには、ここをクリックします)

すべてのデータ書き込み要求は、書き込みを効率的に行えるよう分割され調整されている、書き込み用データベースに送られます。レプリケーションによって、書き込み用データベースから読み取り用データベースに新しいデータが移動します。

そのような特化したデータベースを作成した結果、待ち時間が生じます。書き込まれたデータが読み取り用データベースに送られるのには時間がかかります。ただし、この待ち時間に対処できれば、スケーリングの潜在能力は膨大になります。

無限のスケーリング作業

アプリケーションが成長を続ける限り、スケーリングの作業も同様に増大し続けます。10,000 人の同時ユーザーに対して効果的に動作する ASP.NET の手法も、100,000 人のユーザーに対しては同じように効果的ではなく、100 万人のユーザーに対してはまたルールが変わります。もちろん、パフォーマンスは、アプリケーションに完全に依存します。ここでは、1,000 人以下のユーザーでスケーリングの課題を抱えているアプリケーションを見てきました。

効果的なスケーリングの鍵は、切る前に測る、ということです。テストを使用して、必要なところに労力を費やしていることを確認してください。作業をテストして、単に変更しただけではなく、実際に改善を行ったことを確認してください。スケーラビリティに対する最適化に焦点を合わせた開発サイクルであっても、その終わりには、最も遅い部分がどこであるかを知っておく必要があります。ただし、それはおそらく今日のユーザーにとっては十分に高速なので、後は将来のユーザーのために必要となる作業に専念できるでしょう。

Richard Campbell は、Microsoft Regional Director を務めると共に、ASP.NET の MVP であり、「.NET ロック - .NET 開発者のためのインターネット オーディオ トークショー」(dotnetrocks.com) の共同ホストでもあります。何年かにわたって ASP.NET のパフォーマンスとスケーリングに関する企業のコンサルタントを務め、また、Strangeloop Networks の共同創設者の 1 人でもあります。

Kent Alstad は、Strangeloop Networks (strangeloopnetworks.com) の CTO を務め、Strangeloop の出願中の特許すべての主執筆者または共同執筆者です。Strangeloop の設立に協力する前は、多くの高性能、高スケーラビリティ ASP.NET アプリケーションの構築やコンサルティングに携わっていました。


Silverlight: Silverlight 2.0 で高度な 3D アニメーションを作成する

Silverlight

Silverlight 2.0 で高度な 3D アニメーションを作成する

Declan Brennan
この記事の内容 : :
  • XAML の基礎
  • XAML で要素を作成する
  • 多面体を組み立てる方法
  • DirectX 数学ライブラリをエミュレートする
この記事は次のテクノロジを使用しています:
Silverlight
コンテンツ
XAML を使用する
XAML のいくつかの例
XAML を使用するためのヒント
多面体を組み立てる方法
DirectX 数学ライブラリをエミュレートする
さらなる応用

何か信じられない不運によって、SilverlightTM に関するこの数か月間の発表をまったく耳にされていない方のために、ここで最新の情報をお伝えしておきます。Silverlight は、Microsoft から提供される新しいクロスブラウザ プラグインであり、以前は Flash や Java アプレットが独占していた領域に、Microsoft® .NET Framework の能力を取り入れるものです。Silverlight には、すぐに使える便利な機能が豊富に組み込まれています。.NET Framework 3.5 の簡易版がサポートされ、これには XML と XAML (Extensible Application Markup Language)、ジェネリック コレクション、Web サービス、LINQ などが含まれています。Silverlight はいくつかの .NET 互換言語もサポートしていますが、ここでは C# だけを取り上げます。

新しいテクノロジを習得するための最善の方法は、多少の楽しみを交えることだと思います。そこで私は、Silverlight 1.1 のアルファ版がリリースされ、ダブリンの IMT カンファレンスで Tim Sneath による興味深いプレゼンテーションを見たときに、教育用のちょっとしたアプリケーションを作成することを決めました。それは、平面のテンプレートを折りたたんでさまざまな 3D 図形 (多面体) を組み立てるようすを表示するというものです。Silverlight は既定では 3D をサポートしていないため、3D 処理を行う DirectX® 数学ライブラリのエミュレーションを構築する必要がありました。

多面体は、平らな面から構成される 3 次元オブジェクトです。この Silverlight サンプルで扱っているのは正多面体および半正多面体であり、それぞれプラトン多面体およびアルキメデス多面体とも呼ばれます。これらの多面体の面はすべて正多角形であり、たとえば正三角形や正方形のように、面の各辺がすべて同じ長さです。また、これらは凸形であり、突き出した部分を持ちません。古代ギリシャ人の名前が付いていることからもわかるとおり、これらの物体は長い間にわたって人類を魅了してきました。興味のある方は、George Hart の Web サイトでそれらについて詳しく知ることができます (georgehart.com/virtual-polyhedra/vp.html)。

完成したアプリケーションのデモを図 1 に示しています。また、picturespice.com/ps/Polyhedra/ClientBin/TestPage.html でも見ることができます。基本的に、このアプリケーションではマウスを使って 1 つの図形 (多面体) を選択できます。すると、ウィンドウの右上隅に選択した図形の情報が表示され、選択した多面体を形成するために平面のテンプレートを折りたたむようすがアニメーションで表示されます。最後に、[循環] ボタンをクリックすると、各図形に対して順番に同様なアニメーションが表示されます。

Figure 1 Silverlight Demonstration of Polyhedra
Figure 1 Silverlight Demonstration of Polyhedra(画像を縮小するには、ここをクリックします)
Figure 1 Silverlight Demonstration of Polyhedra
Figure 1 Silverlight Demonstration of Polyhedra (画像を拡大するには、ここをクリックします)

XAML を使用する

他の多くの Silverlight アプリケーションと同じように、Polyhedra は XAML コードを多数使用しています。XAML はコンテンツ定義言語の 1 つで、HTML と似ていますが、より高い柔軟性を持っています。ここで、HTML との類似点について考えます。HTML ページを作成する場合、HTML ドキュメント オブジェクト モデル (DOM) だけを使用して作成することも可能ですが、コンテンツを作成するのに賢明な方法であるとは言い難く、多くの場合は、コーディングに時間がかかるだけでなく、初期化の遅いページが生成されます。ほとんどの場合における最良の方法は、ページ内のできるだけ多くの部分を HTML マークアップとしたまま、柔軟性が必要な箇所では JavaScript や DOM を使用して機能を強化することです。

XAML の場合も、これと非常によく似たアプローチが採用されます。コンテンツを作成する最も早い方法は、できるだけ多くの箇所に XAML マークアップを使用しながら、必要に応じて C# や Silverlight Media API などの .NET 準拠言語を使用して機能を強化することです。XAML は、ハンド コーディングすることも、Expression BlendTM などのデザイン パッケージによって生成することもできます。さらには、開発プロセス中に実行するプログラムから生成したり、サーバー上で動的に生成したりもできます。これには、ある程度考え方を変える必要があるかもしれません。C# プログラマにとってはすべてが簡単すぎるため、XAML に任せるのが最良である機能も自分が慣れた環境でコーディングしてしまいがちです。

ここでは、XAML についてごく簡単に紹介するにとどめておきます。XAML については、Charles Petzold 氏の著書『Applications= Code+Markup』で詳しく説明されています。

次に、新しい言語を学習するときにはだれもが目にする、"Hello World" サンプルの XAML 版を示します。

<UserControl x:Class="Polyhedra.Page"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <TextBlock>Hello World</TextBlock>
  </Grid>
</UserControl>

ルート要素は、UserControl です。これには Grid が含まれ、さらに Grid には、"Hello World" テキストを含む TextBlock 要素が含まれています。

UserControl の属性を簡単に見てみましょう。あまり詳しくは説明しませんが、これは XAML の分離コード クラスに相当するものを定義しています。このクラスは、XAML の解析および読み込みと同時にインスタンス化されます。初期化のさまざまな部分をコンストラクタで実行できますが、より細かな動作を得るには、多くの場合、イベント ハンドラを使用する必要があります。これは、Silverlight の主要な機能の 1 つです。イベント ハンドラは、各種の XAML オブジェクトにアタッチでき、好みの .NET 準拠言語で実装できます。JavaScript とは異なり、コンパイル型言語であるため、そのような言語でしか実現できないさまざまな可能性が開かれています。

HTML との類似点に戻ると、要素は、ページ上での位置を調整するためにさまざまな DIV 内にグループ化されることがよくあります。同様に、XAML では、図形が Canvas 内に (または Grid など、Canvas の一種である他の要素内に) グループ化されます。HTML で DIV が入れ子になることが多いように、Canvas も XAML 内で入れ子にできます。HTML では、ほとんどの要素が長方形です。一方、XAML では、TextBlock、Rectangle、Polygon、Ellipse などあらゆる種類の図形と、ユーザー定義図形を使用するための非常に柔軟な Path をサポートしています。HTML の要素は、ID 属性で識別されます。それに相当する、XAML 内の要素を識別する属性は x:Name です。ここで x は、XAML 名前空間のエイリアスです。

XAML では、単に静的なページを生成するだけでなく、さらに多くのことが可能です。最も強力な機能の 1 つは、Storyboard を使用して、XAML で指定された初期 UI への変更をアニメーション化することです (Internet Explorer® 5.0 では、それに似た HTML+Time と呼ばれる機能が HTML に追加されています)。たとえば、オブジェクトの色、可視性、または透明度に対する変更をアニメーション化できます。変換と組み合わせることで、Storyboard ではオブジェクトの回転、拡大/縮小、および移動ができます。

Storyboard は、従来のコードをほとんどまたはまったく使用せずに、あらゆる種類のアニメーション効果を生成できます。トリガと組み合わせて使用すれば、理論的には、あるオブジェクトに対して MouseEnter などのイベントが発生したときに、各種のアニメーションを自動的に開始することが可能です。ただし、2008 年 3 月の Silverlight 2.0 Beta の時点では、トリガで処理されるイベントは Loaded だけです。他のイベントに対しては、イベント ハンドラの形で少量のプラミング コードが必要となります。この記事を読者の皆さんが読まれている時点ではどうなっているかわかりませんが、この状況は近いうちに変わるでしょう。

Silverlight の Storyboard が持つ優れた機能の 1 つは、動作がフレームベースではなく時間ベースであるということです。異なる時間やイベントに結び付けられた複数の独立した Storyboard を使用することで、複雑な動作を単純に実装できるようになります。結局のところ、実世界はフレーム単位で動いているわけではありません。フレームベースの処理は、動画をセルロイドで実現していた時代の遺物です。それぞれ独立したオブジェクトが独立した動作で実装されれば、人生はずっと単純になるでしょう。

XAML のいくつかの例

Polyhedra アプリケーションの核心である折りたたみアニメーションは、C# コードを使用して生成されます。ただし、残りの大部分は XAML で定義されています。これには、サンプル多面体が並んだ円の表示、現在選択されている多面体をさまざまな方法で強調する機能、および [循環] ボタンが有効になっていることを示す回転する矢印のアニメーションなどが含まれます。

Page.xaml からのいくつかの抜粋で、これらのアニメーションがどのように実現されているかを見てみましょう。最初に、図 2 に示される [循環] ボタンのコードを見ます。ここには、Cycle という名前の Path があります。データ属性は一連の動作を指定し、M は移動、A は円弧、L は直線です。これはかなり単純なグリフであるため、単に使用する図形を紙に描いて、必要な動作を手動で書き出しました。ほとんどの場合は、Expression Design などのツールを使用した方がよいでしょう。

Figure 2 [循環] ボタン (Close Figure 2)
<Canvas x:Name="CycleButton"
  Canvas.Top="250"
  MouseLeftButtonDown="CycleButtonLeftMouseDown" >
  <Path x:Name="Cycle" Stroke="#000033"
    Fill="#FFB47C0D" Canvas.Left="10" Canvas.Top="10"
    Data="M 25,35 A 10,10 180 1 0 25,15 L 25,20 L 12.5,10 L 25,0 L 25,
         5 A 10,10 180 1 1 25,45 Z"
    Width="50" Height="50">
    <Path.RenderTransform>
      <RotateTransform x:Name="CycleRotate" Angle="0"
        CenterX="25" CenterY="25"/>
    </Path.RenderTransform>
    <Path.Resources>
      <Storyboard x:Name="CycleLatched">
        <DoubleAnimation Storyboard.TargetName="CycleRotate"
          Storyboard.TargetProperty="Angle" From="360" To="0"
          Duration="00:00:02" RepeatBehavior="Forever"/>
      </Storyboard>
    </Path.Resources>
  </Path>
  <TextBlock x:Name="CycleCaption" Canvas.Left="65" Canvas.Top="10"
    Foreground="#FFB47C0D" FontSize="30" FontWeight="Bold"
        Text="Cycle" />
  <Rectangle Width="200" Height="70" RadiusX="30" RadiusY="30"
    Stroke="#FFB47C0D" StrokeThickness="4" Fill="Transparent"/>
</Canvas>

この Path には、CycleRotate という名前の RotateTransform もあります。最初、この変換は、角度が 0 に設定されているため、何もしません。ただし、CycleLatched という名前の Storyboard があり、これはアクティブになると、角度を小さな一定の値ずつ連続的に増加させながら、矢印を回転させます。

図 3 に、1 つの多面体サンプルがどのように定義されているかを示します。この XAML の終わりの部分を見ると、このサンプルが、四面体を定義する 4 つの多角形から構成されていることがわかります。これらは、Model0 という名前の専用の Canvas 内にあり、最初は不可視である Ring0 という名前のリング (Ellipse) で囲まれています。2 つの Storyboard アニメーションが定義され、1 つは MouseEnter 用、もう 1 つは MouseLeave 用です。MouseEnter アニメーションは、リングを直ちに可視状態にして、Model0 Canvas のサイズを拡大し、0.7 秒間だけ不透明度を高めます。MouseLeave アニメーションは、それらと逆の変化を行います。

Figure 3 Polyhedron サンプル (Close Figure 3)
<Canvas x:Name="Canvas0" Width="116.376" Height="116.376"
  Canvas.Left="671.812" Canvas.Top="341.812"
  MouseEnter="TriggerMouseEnter" MouseLeave="TriggerMouseLeave">
  <Canvas.Resources>
    <Storyboard x:Name="Canvas0MouseEnter">
      <DoubleAnimation Duration="00:00:00" Storyboard.TargetName="Ring0"
        Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" />
      <DoubleAnimation Duration="00:00:00.7"
        Storyboard.TargetName="Model0"
        Storyboard.TargetProperty="Opacity" From="0.7" To="1.0" />
      <DoubleAnimation Duration="00:00:00.7"
        Storyboard.TargetName="Model0Scale"
        Storyboard.TargetProperty="ScaleX" From="0.7" To="1.0" />
      <DoubleAnimation Duration="00:00:00.7"
        Storyboard.TargetName="Model0Scale"
        Storyboard.TargetProperty="ScaleY" From="0.7" To="1.0" />
    </Storyboard>
    <Storyboard x:Name="Canvas0MouseLeave">
      <DoubleAnimation Duration="00:00:00" Storyboard.TargetName="Ring0"
        Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" />
      <DoubleAnimation Duration="00:00:00.7"
        Storyboard.TargetName="Model0"
        Storyboard.TargetProperty="Opacity" From="1.0" To="0.7" />
      <DoubleAnimation Duration="00:00:00.7"
        Storyboard.TargetName="Model0Scale"
        Storyboard.TargetProperty="ScaleX" From="1.0" To="0.7" />
      <DoubleAnimation Duration="00:00:00.7"
        Storyboard.TargetName="Model0Scale"
        Storyboard.TargetProperty="ScaleY" From="1.0" To="0.7" />
    </Storyboard>
  </Canvas.Resources>
  <Canvas x:Name="Model0" Opacity="0.7"
    Width="116.376" Height="116.376" >
    <Canvas.RenderTransform>
      <ScaleTransform x:Name="Model0Scale" CenterX="58.188"
        CenterY="58.188" ScaleX="0.7" ScaleY="0.7"/>
    </Canvas.RenderTransform>

    <Polygon Canvas.ZIndex="-12545653" Fill="#FFCC0000"
      Stroke="#000000" StrokeThickness="1"
      Points="97.566,74.278 43.052,40.971 33.836,99.937 "/>
    <Polygon Canvas.ZIndex="-11948057" Fill="#FFCC0000"
      Stroke="#000000" StrokeThickness="1"
      Points="43.052,40.971 97.566,74.278 58.188,37.662 "/>
    <Polygon Canvas.ZIndex="-11309683" Fill="#FFCC0000"
      Stroke="#000000" StrokeThickness="1"
      Points="33.836,99.937 43.052,40.971 58.188,37.662 "/>
    <Polygon Canvas.ZIndex="-10481036" Fill="#FFCC0000"
      Stroke="#000000" StrokeThickness="1"
      Points="97.566,74.278 33.836,99.937 58.188,37.662 "/>
  </Canvas>
  <Ellipse x:Name="Ring0" Opacity="0" Stroke="#FFB47C0D"
    StrokeThickness="4" Width="116.376" Height="116.376" />
</Canvas>

これらの Storyboard はそれぞれ独立して動作できるため、ユーザーがどれだけ速くマウスを動かしても、すべてが期待どおりに動作します。一般に、1 つのサンプルに対する MouseLeave アニメーションは、新しく選択されたサンプルに対する MouseEnter アニメーションと同時に実行されます。ただし、Silverlight は複数のアクティブな Storyboard を自動的に並行して処理するため、その点について特に気にかける必要はありません。

余談ですが、四面体サンプルの 4 つの多角形のそれぞれの頂点をどのように計算したのか疑問に思われているかもしれません。アプリケーション内の他の各サンプルでは、これよりずっと多数の多角形が使用されていますから、なおさらです。私は、コンピュータの方が速くできることは自分ではやらない、という怠惰なアプローチを強く信奉しています。したがって、私がしたことは、実際のプロセスでは Polyhedra の改定版を使ったということだけです。このバージョンでは、各サンプルについて中心のアニメーションの 1 フレームを順番に実行しました。これは、完全に閉じた図形のフレームです。結果の Polygon グループをそれぞれ、円の周囲に等間隔で並んだ Canvas のセットに配置し、メイン プログラムで使用するためにその全体を 1 つのファイルに格納しました。

オブジェクトを円の周囲に配置するには、角度を 2*PI/NumSamples の等間隔で刻み、x = Radius*Cos(Angle) および y = Radius*Sin(Angle) の座標で各サンプルの中心を指定する必要があります。また、オブジェクトを配置する Canvas は Left および Top プロパティを使用するため、中心をそれぞれ幅の 1/2 と高さの 1/2 だけオフセットする必要がありました。

XAML を使用するためのヒント

既にお気づきのとおり、XAML ではコードをほとんど追加せずに非常にリッチな UI を実現することが可能です。Page.xaml のような大きなファイルに対しても、初期化は驚くほど短時間で済みます。話を先に進める前に、Silverlight 2.0 の現在の実装 (March 2008 Beta) での私の経験に基づいて、いくつかのヒントを示しておきましょう。

既に述べたとおり、現在のところ、トリガは Loaded イベントに対してだけ、Storyboard を自動的に (Begin メソッドを介して) 開始できます。他のイベントに対しては、次のようなイベント ハンドラの形で少量のプラミング コードが必要となります。

public void MouseEnterHandler(
  object o, EventArgs e) {
  this.MouseEnterStoryBoard.Begin();
}

Storyboard に対して、オブジェクト名の後にイベント名が続くなど、何らかの命名規則を適用すれば、コーディングが必要な個別のイベント ハンドラの数を大きく減らすことができます。たとえば、Polyhedra では、円内のすべてのサンプルに対する共有イベント ハンドラとして次のメソッドが使用されています。

public void MouseEnterHandler(
  object o, EventArgs e) {
  this.triggerStoryboard(o,"MouseEnter");
}
private bool triggerStoryboard(
  object o, string eventType) {
  Canvas el = o as Canvas;

  string name= el.GetValue(NameProperty) as String;
  Storyboard sb = el.FindName(name + eventType) as Storyboard;
  if (sb != null)
    sb.Begin();
    return (sb != null);
}

Silverlight 2.0 Beta では、メインの初期化 (InitComponents を呼び出す) が Loaded イベント ハンドラから分離コード オブジェクトのコンストラクタに移動されています。これはより洗練されていますが、コンストラクタですべてが可能なわけではないことに注意してください。たとえば、ここでは Storyboard 上で Begin または Pause を呼び出すことができないため、それはやはりイベント ハンドラで実行する必要があります。

Andy Beaulieu 氏が作成した、小惑星を爆破する Silverlight Rocks! というサンプル (www.andybeaulieu.com/Home/tabid/67/EntryID/73/default.aspx) から発見したことですが、コードベースのアニメーションを実現する優れた方法の 1 つは、短い時間に設定した Storyboard を使用しながら、Completed イベント ハンドラを使用して、アニメーションの 1 フレームの実行後に Storyboard を再起動することです。

public Page() { // Constructor for "code-behind"
  // Required to initialize variables
  InitializeComponent();
  this.animationTimer.Completed +=
    new EventHandler(animationTimer_Completed);
}

void animationTimer_Completed(object sender, EventArgs e) {
  [ Do a frame of animation ]
  this.animationTimer.Begin();
}

Silverlight 2.0 Alpha の September Refresh では、Storyboard の要件が変更されたため、アニメーションには、使用しない場合でもターゲットが必要となっています。

<Canvas.Resources>
  <Storyboard x:Name="animationTimer">
    <DoubleAnimation Duration="00:00:00.01"
      Storyboard.TargetName="bogusTimerTarget"
      Storyboard.TargetProperty="Width" />
  </Storyboard>
</Canvas.Resources>
<Canvas Name="bogusTimerTarget">
</Canvas>

同じ HTML ページ上に個別の Silverlight コントロールを多数配置することは避けてください。Polyhedra の最初の実装では、円内の各サンプルに対して別個のコントロールを使用していたため、メモリをかなり大量に消費しました。これは、場合に応じて、HTML から XAML にコンテンツを移動して、使用するコントロール数を減らす必要があることを意味します。

XAML の利点の 1 つは、UI 内の多くの定型処理のための作業負荷が減り、創造的な問題領域のコードに集中できるようになることです。この例での問題領域は、3D 図形を形成するためのテンプレートの折りたたみであり、これは次のセクションで説明します。

多面体を組み立てる方法

前述した Charles Petzold 氏の著書は、それよりもずっと古い、オブジェクト指向以前に Niklaus Wirth 氏によって書かれた『アルゴリズム + データ構造 = プログラム』という本の翻案ではないかと私は考えています。この本は、長い年月が過ぎても、私がこれまでに読んだ中で最も影響を受けた本の 1 つであり、その後言語やパラダイムにさまざまな変化があったにもかかわらず、今でも十分に役立つ内容を含んでいます。この本の基本的なテーマは、どのようなデータ構造が問題を最もよくモデル化できるかを特定し、それらのデータ構造をどのようなアルゴリズムで処理または変更できるかを特定するという、開発アプローチです。これは、標準的でない問題に取り組む際に、私がよく採用するアプローチです。

最終的に Polyhedra で使用したアプローチに落ち着くまでには、さまざまなアプローチを試してみました。折りたたみアニメーションを実現するために、どれだけ少ない情報から実際にスタートできるかを知りたかったからです。その結果わかったことは、すべての辺が同じ長さであるため、与えられた多面体でどの面がどの面と接続しているかを知るだけで、ほとんどすべてが可能になるということです。これは、グラフ データ構造を暗示しています。最終的な XAML 出力に至る前に、一連のアルゴリズムを使用して、2 つの個別のツリー データ構造でグラフを処理しています。それらについてはすべて、後で説明します。

Windows® Presentation Foundation (WPF) は XAML で 3D をサポートできますが、Silverlight は既定では 2D しかサポートしません。これは、マシン内にどのようなグラフィック プロセッサ ユニット (GPU) ハードウェアが含まれているかを気にしなくて済む方が、ブラウザ間の互換性をずっと簡単に実現できるからです。もちろん、突き詰めて考えれば、コンピュータ画面上の 3D は目の錯覚にすぎません。どのような操作が行われているにしろ、結局、モニタ画面に表示されているのは通常の 2D 多角形の集まりです。独自のコードで 3D 操作を実行してこれらの多角形の座標を計算する手間をかけるならば、ハードウェア アクセラレータを使用しない 3D レンダリングも可能です。数十個のポリゴンに限定すれば、パフォーマンスも許容範囲内です (主観的には、フレームのパフォーマンスは Alpha から Beta への移行により向上しています)。

図形が選択されると、Net という名前 (.NET とは無関係) の、展開された 2 次元テンプレートが作成されます。この作業は、図 4 に示すように、2 段階で行われます。最初に、図形の各面に 1 つの GraphNode が対応するように、メモリ内にグラフが作成されます (Graph.cs を参照)。このグラフは、アプリケーション アセンブリに埋め込まれた .shp リソースの 1 つから接続性情報 (どの面がどの面に接続されているかを示す) のセットを取り込むことで作成されます。たとえば、cube.shp の内容は次のようになります。

Figure 4 Building the Shape, from Graph to XAML
Figure 4 Building the Shape, from Graph to XAML(画像を縮小するには、ここをクリックします)
Figure 4 Building the Shape, from Graph to XAML
Figure 4 Building the Shape, from Graph to XAML (画像を拡大するには、ここをクリックします)

1:3,4,5,2
2:1,5,6,3
3:2,6,4,1
4:3,6,5,1
5:1,4,6,2
6:2,5,4,3

次に、グラフ内の任意の GraphNode を、Net の作成を開始する最初のノードとして選択します (詳細については、コード ダウンロードの Net.cs を参照してください)。そして、GraphNode の隣接数と同じ数の辺を持つ 2 次元 FlatFace を配置します。さらに、任意の隣接する GraphNode を選んで同じ処理を繰り返し、現在の FlatFace と辺を共有する次の FlatFace を配置します。各 GraphNode を 1 回だけ選択し、すべての GraphNode が選択されるまで処理を続けると、FlatFace のツリー構造が構築されます。

3D アニメーションの各フレームで、Net から 3 次元多面体 (おそらく部分的に閉じている) が作成されます (この操作の詳細については、この記事で後述する Polyhedron.cs を参照してください)。これには、FlatFace のツリーを Face のツリーにコピーして、2D 頂点を 3D の点で置き換える作業が含まれます (図 5 に、この 2 つの座標系を示しています。水平面から開始するために、Y はゼロから開始する必要があります。そのため、X = X、Y = 0、および Z = Y に設定することで、2D を 3D にマッピングしています)。

Figure 5 Transforming Points from 2D to 3D
Figure 5 Transforming Points from 2D to 3D(画像を縮小するには、ここをクリックします)
Figure 5 Transforming Points from 2D to 3D
Figure 5 Transforming Points from 2D to 3D (画像を拡大するには、ここをクリックします)

最後に、再帰処理により、Face のツリー内の各接合点を順に回って、折りたたみを実行します。折りたたみの量は、図形が完全に閉じているときに各面が互いに離れている角度 (二面角) を分割した量です。理論的には、.shp ファイル内の接続性情報だけを使用して、二面角を直接計算することが可能です。

これは、3 つの面が頂点で接していたり、任意の数の同じ種類の面が頂点で接していたりするような、特別なケースでは、かなり簡単です。ただし、一般的なケースではより難しくなるため、これらの角度を個別の .dihedrals リソースに格納することしました。たとえば、cube.dihedrals からの抜粋を次に示します。

1,3:1.5707965056551
1,4:1.57079661280793
1,5:1.57079614793469
1,2:1.57079604078186
2,1:1.57079604078186
2,5:1.57079628466395
2,6:1.57079661280793
2,3:1.57079636892585
3,2:1.57079636892585
3,6:1.57079614793469
3,4:1.57079628466395
...

これらの数の間の微小なばらつきは、単に計算方法に起因する誤差です (この場合は、本来すべてπ/2 に等しくなります)。

最終的なプロセスには、多面体のビューを多角形の集まりとしてモニタに投影するための、一連の変換が含まれます。お気づきかもしれませんが、折りたたみアニメーションの表示中、多面体は垂直軸の周りを回転しています。これを実現するために、回転中心を原点に変換し、回転を実行した後で、視点に変換しています。その後、透視変換を行って、(Z 軸上で) 遠くにある物体が小さく見えるようにしています (次のセクションで、変換の方法についてもう少し詳しく説明します)。

最後に、モニタ画面への 3D 図形の投影に対応して、XAML 多角形のセットが生成されます。Silverlight は、3D 多角形の中心の Z 座標 (float から int に変換) を使用して各多角形の XAML ZIndex プロパティを設定することで、これらの多角形を後ろから前への順序で描画します。これはかなり単純化された 3D の実現方法であり、たとえば各多角形が互いに交差していない (このサンプルではそれを回避できていますが) など、理想的な条件でのみ機能します。より洗練された形態の 3D では、奥行きバッファなどのメカニズムが必要となります。それには GPU アクセスが必要であるため、Silverlight .NET サンドボックスの範囲からは外れます。

最後の仕上げとして、XAML では (Opacity プロパティを使用して) 多角形の一部を透明にすることができ、それにより多面体が部分的に透けて見える芸術的効果が得られます。必要な作業は以上です。

DirectX 数学ライブラリをエミュレートする

ここで、アニメーションの実現に使用されている数学について軽く触れておきます。より詳しく知りたい場合は、wikipedia.com、euclideanspace.com、mathworld.wolfram.com、gamasutra.com、gamedev.net など、多くの役に立つ Web サイトがあります。

前述の XAML サンプルで、既に RotationTransform や ScaleTransform などいくつかの 2D 変換を紹介しています。これらのサンプルでは、Silverlight がすべての作業をやってくれます。XAML を WPF 環境で使用する場合は、3D 変換にもアクセスできますが、Silverlight では 3D 変換を使用できません。WPF では、そのための低レベル操作に DirectX を利用しています。DirectX には 3D 変換に必要な数学演算を行う優れたクラスのセットが用意されているので、それは理にかなっています。Silverlight で同じ変換のいくつかを行うために、私はこれらのクラスの多くに対して DirectX 数学ライブラリのエミュレーションを作成し、Silverlight サンドボックスで使用できるようにしました。

Direct3D® でのコーディング経験をお持ちの方であれば、Vector2、Vector3、Matrix などの Microsoft 実装についてはよくご存知かと思われます (図 6 を参照)。それ以外の方のために、厳密ではありませんが非常に簡単な概要と、いくつかの例を示します。

Figure 6 Emulated DirectX Math Classes
Figure 6 Emulated DirectX Math Classes(画像を縮小するには、ここをクリックします)
Figure 6 Emulated DirectX Math Classes
Figure 6 Emulated DirectX Math Classes (画像を拡大するには、ここをクリックします)

2D で点を特定するには、X と Y の 2 つの座標が必要です。3D では、X、Y、および Z の 3 つの座標が必要です。厳密に言えば点はベクトルではありませんが、2D の点は Vector2 オブジェクトに格納でき、3D の点は Vector3 オブジェクトに格納できます (ベクトルは実際には大きさと方向を持つ存在であり、原点から始まりその点で終わる矢印として表すことができます。ある意味、すべてのベクトルは原点から始まると考えることができます)。

3 次元の図形は、面の集合として識別できます。そして面は、頂点の集合として識別されます。実行する必要がある操作は、ほとんどすべて、回転、変換 (または移動)、拡大/縮小などのいくつかの操作によって、これらの点を別の点の集合に変換する操作を含んでいます。必要な操作のほとんどすべてを、行列と呼ばれる 4 × 4 個の数字のグリッドで表すことができます (一般に行列は、実際には任意の数の行および列を持つことができますが、ここでは、同次座標と呼ばれる座標による 3D 点の操作に使用する特別な行列だけを取り上げます)。これらの操作は、適用されるオブジェクトの形状を (過度には) ゆがめないため、線形変換と呼ばれます。

非常に便利なことに、そのような 2 つの行列を、同じ効果を持つ 1 つの行列に結合する方法があります。この演算は行列乗算と呼ばれ、何回か繰り返すことができます。これは、一連の変換全体をまとめて、同じ効果を持つ 1 つの行列に置き換えられることを意味します。この方法は、各変換を別々に適用するよりもずっと効率的です。行列乗算がどのように実装されるのか、または行列乗算がそのような変換マジックをどのように行うのかを気にする必要はありません。それは単にツールとして利用します。

元の点を目標の点に変換するために、行列とベクトル オブジェクトとの間で定義されている乗算もあります。(行列間の乗算との) 混乱を避けるために、これは TransformCoordinate メソッドを使用して Vector3 に実装されています。

いくつかの小さなコード スニペットで、これをよりわかりやすく示します。最初に、polyhedron.cs 内の次のコードを使用して、2 つの面をその共通の辺に沿って折りたたみます。

Vector3 axis = axisTo - axisFrom;
Matrix foldTransform =
  Matrix.Translation(-axisFrom) *
  Matrix.RotationAxis(axis, proportion * _dihedralAngle) *
  Matrix.Translation(axisFrom);
Vector3[] p= Vector3.TransformCoordinate(
  face.Points,foldTransform);

ここで、共通の辺は、axisFrom から axisTo までの直線として定義されています。回転は常に原点を通る直線の周りで定義されるため、回転を行う前に、辺の 1 つの点を原点に移動し、後でそれを元の位置に戻す必要があります。そのために、3 つの線形変換を結合して、面座標の変換に使用する行列を作成しています。3 つの線形変換とは、原点への移動、回転、および元の位置への移動です。

ここで、もう 1 つ別の小さな仕掛けがあります。回転軸を表すベクトルも原点から始まる必要があるため、axisFrom を減算して、(axisFrom, axisTo) を (origin, axisTo-AxisFrom) に移動しています。

まだ頭が数学で満杯になっていないようでしたら、もう 1 つの例 (projector.cs に基づく) を試してみましょう。これは、多面体を構成する多角形を、画面の視点から見たときの状態に変換するために使用します。

Matrix projection =
  Matrix.Translation(-pivot) *
  Matrix.RotationY(yaw) *
  Matrix.RotationX(angle) *
  Matrix.Translation(viewPoint) *
  Geometry.Perspective(5);
Vector3[] p= Vector3.TransformCoordinate(face.Points,foldTransform);

折りたたまれている間、図形は水平面内で回転されることを思い出してください。これを行うために、回転軸上の点を原点に移動してから、垂直 (Y) 軸の周りにヨー角度だけ回転します (実際に、またはシミュレーションで飛行機を飛ばした場合、ヨーに加えてピッチおよびロールと呼ばれる 2 つの独立した回転が加わる可能性があります)。

ここでは、開始位置に戻る代わりに、視点への移動を行っています。最後に、透視変換を行って、Z 軸上で遠くにある物体を小さくしています。ここでは多くのことが行われていますが、すべてを 1 つの 4 × 4 行列に結合することができるのが非常に印象的です。

さらなる応用

Polyhedra では使用していませんが、エミュレーション ライブラリでは四元数も使用できます。これは、複数の 3D 回転を 1 つの回転に結合するための優れた方法です。また、1 つの向きから別の向きへとスムーズに変更するのにも有用です。四元数は、Sir William Rowan Hamilton 氏 (私の故郷であるアイルランドのダブリン出身) が街を歩いている間に発見したもので、彼は忘れないようにすぐブルーム橋の側面に数式を書き刻んだそうです (www.maths.tcd.ie/pub/HistMath/People/Hamilton/Quaternions.html を参照)。私の落書きもすべて同じように学問的なものだと主張したいですが、おそらく信じてはもらえないでしょう。

また、四元数を使用すると、ジンバル ロックと呼ばれる問題を回避できます。これは、アポロ月計画でのジャイロスコープの操作で重要な役割を演じ、アポロ 13 号の任務を描いた映画でも取り上げられました。

ベクトルの内積や外積など、ここで取り上げていない事項はまだ他にもたくさんありますが、DirectX の数学ライブラリのエミュレーションで何が可能になるのか、感じだけでもつかんでいただけたかと思います。もともとこのエミュレーションは、ASP.NET が動作する、DirectX をインストールできないホストされたサーバー上で変換を実行するために開発したものです。このような種類の環境では、他の応用方法も見つかるかもしれません。

ユーザーの興味を引き付け、すばやく、印象的な方法で情報を伝えることができるような Web UI を提供するために、Silverlight がどのようにして便利で拡張可能なツールを提供するのか、Polyhedron によってイメージしていただけたらさいわいです。過去の .NET の経験に基づいて、さまざまな種類の創造的な方法でアプリケーションを構築できます。JavaScript とは異なり、クライアントで実行されるコードの行数にとらわれる必要はありません。

Declan Brennan は、世界最初のマイクロプロセッサについて覚えているほどのベテランで、どんな願いもかなえてくれる精霊がそばにいるという状況にはまだ慣れていません。彼は、自分の住む世界が、テクノロジによって制約されることのない、ただ想像力の豊かさだけが左右する世界であるという幸運を、信じることができません。詳細については、declan.brennan.name を参照してください。


インタビュー ++: 言語の進化について語る Bjarne Stroustrup 氏

インタビュー ++

言語の進化について語る Bjarne Stroustrup 氏

Howard Dierking
コンテンツ
言語に対する考え
言語の傾向
手法とベスト プラクティス
将来を見据える
書籍と電話

ときには、 飛躍的な進化により、エンジニアリングのあらゆる分野が急激に進歩したり、再構築されたりすることがあります。ソフトウェア開発においては、そうした飛躍は C++ プログラミング言語の登場と共に起こりました。この飛躍は、言語そのものに由来するものではありませんでした。C++ 以前から、Simula67 や Smalltalk などのオブジェクト指向言語は存在していましたが、C++ は C プログラミング言語を基に開発されたからこそ (また、既存の C プログラムをコンパイルできたため)、オブジェクト指向という概念をメインストリームに押し上げることができたのです。

C++ は、設計パターンからメタプログラミングまで、ソフトウェアの設計と開発を取り囲む非常に多くの考えに影響を及ぼしました。そのハードウェア プラットフォーム間での移植性と細かい表現力のため、C++ はより速くて小さいハードウェアの分野で不可欠なものとなるに違いありません。

先日、C++ の考案者である Bjarne Stroustrup 氏にお話を伺う機会に恵まれ、言語に対する考えから業界の全体的な傾向、個人的な読書リストに至るまで、多数のテーマについてお話しいただきました。質問の多くは、私のブログの読者からお寄せいただいたものです。質問をお送りくださったみなさんに感謝します。そしてもちろん、Bjarne 氏にお礼を申し上げます。

言語に対する考え

Howard Dierking プログラミング言語は、言語の熱狂的な支持者のコミュニティなどの形で、非常に深いレベルで人々に結び付いています。これはなぜでしょうか。

Bjarne Stroustrup その質問は、コンピュータ科学者ではなく、心理学者や社会学者、あるいは経済学者におたずねになった方がよいのではないでしょうか。私が思うに、私たちが自分のアイデアを表現するために使用している言語は、私たち自身の一部になっています。だからこそ、1 つの言語しか知らない場合、他の言語の支持者が個人的な脅威として映るのではないでしょうか。その場合の解決策は、より多くの言語をよく知ることだと思います。プログラミング言語を 1 つしか知らないのに、ソフトウェアの分野でプロフェッショナルになれるとは思えません。他方で、お金の絡んだ理由もあるでしょう。基本を理解するだけであればプログラミング言語の壁を越えることはできますが、さまざまな実用的なスキルを習得するとなるとそうはいきません。そのため、たとえば私が X という言語とそのツール セットだけを使用でき、あなたが Y という言語とそのツール セットを支持している場合、あなたは私の生活を脅かしていることになります。繰り返しますが、解決策は複数の言語とツール セットを知る (そして基本をしっかり学ぶ) ことだと思います。残念なことに、この解決策では対処できない問題があります。必要最低限の作業を済ませた後で学習に時間を費やすような余裕はない、というのが大方の現実でしょう。とはいえ、これは熱狂的な支持者であることの言い訳にはならないですね。

HD ソフトウェア開発において、IDE はどのような役割を果たすべきでしょうか。また、IDE はどのように言語をサポートすべきでしょうか。

BS 私は IDE を多用していません。私の言語を理解してくれる応答性の高い IDE エディタは高く評価していますが、IDE を使用せずに作業できることが好ましいと考えています。優れた IDE が広く使用され、実際に IDE が言語の一部 (あるいは言語が IDE の一部) になるのであれば、別の意見を持つかもしれません (

Figure 1 The IDE Designer as a Language(画像を縮小するには、ここをクリックします)
Figure 1 The IDE Designer as a Language
Figure 1 The IDE Designer as a Language (画像を拡大するには、ここをクリックします)

HD 不要な要素や可読性については、今日の汎用言語における問題として受け止めておられますか。そうである場合、どのような解決策が考えられますか。

BS シンプルな構文は好ましいと思います。ただし、多くの人々が可読性について不平を漏らしているようですが、表現されていることの複雑さに比べると、実際のテキストはそれほどでもないのではないでしょうか。各種の言語で記述されたさまざまなプログラムを把握することや、(オンラインのサポート窓口からはほんのわずかしか支援を受けられないのに) プログラムを表現するために使用されている構造やプログラム自体のロジックをすべて理解することを望んでいる人々が多すぎるのです。自然言語について考えたり、それらを使用したりする場合と比べてみてください。背景を知らないのにシェークスピアのソネットを理解できると思われますか。最初の古英語で書かれたベーオウルフの場合はどうでしょう。おそらく、私たちはプログラミング言語に期待しすぎているのです。多岐にわたるアプリケーションの領域に必要なものをすべて表現できる言語は、特定のアプリケーションを開発する際には必要以上に複雑であると感じられるものです。それでもこうした言語には、無限に増えるアプリケーションに対応できる必要があります。ドメイン固有言語は特定の状況では役に立ちますが、今はまず、多くの言語とその相互作用の複雑性に対処しなければなりません。

HD 汎用言語は、コンポーネント プログラミングやサービス プログラミングなどのアプリケーション設計のアイデアをどのようにサポートする必要がありますか。

BS 汎用言語は、全般的な概念とアプリケーション固有の概念を表現するライブラリの記述をサポートする必要があります。また、ツールの構築を支援し、アプリケーションの各パーツを結び付ける "接着剤" の役割を果たさなければなりません。そのために言語に求められるものは、柔軟性、表現力に富む型システム、良好な基本パフォーマンス、および長期にわたる安定性です。

HD 多重ディスパッチは好ましい機能でしょうか。

BS はい。従来の単一ディスパッチ オブジェクト指向プログラミング言語 (Simula、C++、Smalltalk、Java、C# など) では、実行時まで正確な型が判別されず、乗算や 2 つの図形の交点の検出などの単純な処理を簡潔に表現できません。作成したコード (二重ディスパッチ、ビジター パターンなどに依存する) は遅く、私たちが望むほど管理が容易ではありません。比較した場合、実行時の多重ディスパッチをサポートする言語 (Dylan や CLOS など) の方が優れています。また、コンパイル時の多重ディスパッチをサポートする言語 (C++ など) が有用な場合もあります。昨年、私が教える学生たちと共同で、多重ディスパッチを C++ にクリーンに追加する方法に関する研究論文を発表しました。多重ディスパッチを使用して作成したコードは、これまでのどの手段を利用した場合よりも短く簡潔になりました。また、使用するメモリも減り、実行速度が上がりました (

Figure 2 Multiple Dispatch via Multiple Inheritance(画像を縮小するには、ここをクリックします)
Figure 2 Multiple Dispatch via Multiple Inheritance
Figure 2 Multiple Dispatch via Multiple Inheritance (画像を拡大するには、ここをクリックします)

HD 今日の C++ における共変性や反変性についてはどのような考えをお持ちですか。現在の動作は今後の言語のバージョンでは変わると思われますか。

BS それほど変わらないと思います。この領域では、C++ は果たすべき役割を果たしていると思います。あなたは、vector<Apple> を vector<Fruit> に変換するような例を考えておられるのでしょうか。vector<Fruit> が不変でない限り (または、Orange を追加することができなければ)、これは非常に危険です。黙示的な実行時の型チェックがこうした問題に対する有効なアプローチであるとは思いません。

HD メソッド呼び出しではなく、メッセージ渡しを主要な言語の機能とすることについては、どのように考えておられますか。

BS 明示的なメッセージ渡しは好きですが、随分前に、分散システムのコンテキストで使用したきりです。現実的には、幅広い場面で使用できるように、言語やツールでのメッセージ渡しのサポートについて真剣に取り組まなければならないでしょう。これまでそれが行われてきたとは思いません。私が間違っているかもしれませんが。メッセージやメッセージ キューに頼れば、共有やロックにかかわる多くの問題が解決します。C++ 標準ライブラリで実現できればよいのですが、2 ～ 3 年のうちに可能かもしれません。

HD 汎用言語では、多種多様なユーザーを同時にサポートすることと、コードの表現力および設計の簡潔さを向上させることは、本質的に競合するのでしょうか。後者を実現するうえでの言語の役割は、どのようなものでしょうか。

BS 特化を通じて設計を簡潔にできるのは確かです。こうした意味では、競合はあると思います。また、対象ユーザーが少なければ (ユーザー コミュニティが小さければ) その好み (だけ) は満たせますし、そのユーザー コミュニティをコントロールして ("型の理論を研究して、これを使えるようになる必要があります" などと伝えて)、従来の表記や概念から引き離すこともできるでしょう。汎用言語は、多種多様な用途をサポートする必要性に加え、前提や学習背景が異なる人々の大集団に教えやすいものでなければならないという要件からも制約を受けます (基礎については、言語を熟知していない教師のもとであっても高校生が扱えるようなものが必要)。

だからこそ、簡潔なコードを汎用言語で表現できる限りは、汎用言語は簡潔さの向上に貢献できると思います。C++ の場合、非常に簡潔なコードを記述できます。広く利用されている汎用言語なので、そうした例はだれでも入手可能です。また、大勢の人々が目を通せる記事や教科書に取り上げることもできます。特化した言語には、簡潔という長所はあるものの、そうした選択肢はありません。

HD 私たちはアーキテクチャを重視しすぎているのではないでしょうか。

BS 少なくとも私が定義するアーキテクチャについては、そのようなことはありません。それどころか、アーキテクチャは軽視されすぎています。また、構造の原理をほとんど理解しないままに質の低いコーディングが広く行われています。アーキテクチャに関する大きな問題は、多くのプログラマがコードの良さを活かす方法を明確に理解していないことにあると思います。優れたコードを見た際に、そう認識できるだけでは十分ではありません。また、してはならないことを把握しているだけでは不十分です。私たちには明確な規範的ルールが必要なのです。

HD オープン ソース コミュニティは、ソフトウェアの品質、設計、および職業意識に貢献しているでしょうか。それとも悪影響を及ぼしているでしょうか。あるいは、何の影響も及ぼしていないでしょうか。

BS それは本当に難しい問題です。オープン ソース コミュニティが貢献している (コードの品質や関与している人々の職業意識を高めている) ケースや、悪影響を及ぼしている (実に酷い習慣や心構えを教えている) ケース、とても話せないような多くのケースも見てきました。

コミュニティへの全体的な影響がどういう状態にあるのか、どのようなことがオープン ソースの取り組みのレベルを高めているのか、あるいは下げているのか、私には知る由もありません。単純に、私にとってコミュニティは推測するには大きすぎるのです。

言語の傾向

HD 近い将来、動的言語への大きなシフトが起きると思われますか。

BS そうは思いません。りんごとオレンジを比較するようなことがあまりに頻繁に行われています。一般に、静的言語と動的言語のどちらか 1 つを選ぶことができるとは思いません。また、言語はその 2 つのカテゴリにきれいに分けられるとも思いません。すべてとは言いませんが、ほとんどの動的言語は静的に判断される側面を持ちますし、主要なあらゆる静的言語では、値の意味を実行時に判断する必要のあるコードを記述できます。もちろん流行はあり、それについては予測できませんが、多くの現実の言語は、アプリケーションの要件、アプリケーションの領域、または対応できる開発者のスキルに基づいて合理的に選択されるものと考えています。たとえば、Ruby で Java ランタイムを実装しようとは思いませんし、対話性の高いシミュレーション言語を C++ で表現しようとも思いません (その実装とは逆に)。

HD 最終的に void*、variant、object などをすべて取り除くのは適切だと思われますか (図 3 は良い例です)。

BS 原則として、あらゆるキャッチオール オプションを取り除くのは良い考えだと思いますが、実際には、表現するものすべてを完全に分類して必ず詳細に指定できるようにすることでしか、実現できないと思います。たとえば、どんなオブジェクトにも何らかの影響を及ぼすことができるような実関数は見たことがありません。何かを行うとき、自分が操作しているものについて必ず何らかの推測を立てるものです (反例として思い付く最も近いものが、純粋な転送関数です)。C++ の世界における概念に関する現在の取り組み (ジェネリック アルゴリズムの制約と要件) が、ここで役立つでしょう。とはいえ、予測できないニーズに対処するための "明確にはわからないが、何らかのもの" を表現する真に包括的なメカニズムなしには不可能だと思います。

HD 柔軟に型指定された言語に再びシフトする傾向にありますが、ハンガリアン記法をもう一度考慮に入れるべきでしょうか。

BS そうした傾向があるとは知りませんでしたが、柔軟に型指定された言語に向いている作業全体の各部分では増えてきているのでしょう。言い換えると、柔軟に型指定された言語の使用は急速に増えていますが、静的に型指定された言語の使用はまだこれから増える可能性があるのです (私はそう思います)。そして質問への回答は "いいえ" です。ハンガリアン記法は使用すべきではありません。ハンガリアン記法は危険な考え方です。ソース コードはプログラムの意味を反映するべきであり、型システムをまねるべきではありません。ハンガリアン記法の必要性を本当に感じている人がいるとすれば、その人はおそらく開発中のアプリケーションに適していない言語を使用しているのでしょう。

HD 2000 年に、あなたは「C++: A New Language for a New Millennium」というプレゼンテーションを行い、Wrap<T> という概念を紹介されました。これは本質的にはアスペクト指向プログラミング (AOP) です。AOP については概してどのように考えておられますか。また、Wrap<T> パターン (pointcut や advice など) の正規化についてはどのような考えをお持ちでしょうか。

BS 明確な答えを出せるほど AOP に時間を費やしていません。私は、言語でサポートされる ("サポートされない" でも "ツールによってサポートされる" でもありません) 構成 (特に、煩雑でない構成) を好みます。そのため、言語外のツールや非標準のツール チェーンに憂慮しています。C++ テンプレートは、煩雑でない構成に関してはすばらしい成功を収めてきました。標準テンプレート ライブラリ (STL) や組み込みシステム プログラミングでのいくつかの使用例を見てください。柔軟でない階層や設計済みの階層に無理に合わせることなくアイデアを組み合わせることができるという点は、実用的な長所です。ただし、一部の開発者や管理者にとって管理が困難だったのも事実です。また、記述が非常に多くなります。C++0x では、柔軟性やパフォーマンスに影響を及ぼすことなくこれに対処しようという試みがあります。

HD C++ 言語の一部の特性は、意図しない厄介な結果 (マクロなど) を生み出す可能性をはらんでいます。一般に C++ や新しい言語において、その他にどのような意図しない結果が解消されるとよいと考えておられますか。

BS 実際、私が C を使い始めたころ、マクロは厄介な存在だと考えていました。ところがほとんどの人々と同じように、その厄介さとさまざまな悪影響を過小評価していたのです。10 年前に優れた C++ 開発環境がなかった大きな理由は、C のマクロの濫用にあると思います。C++ では、オブジェクトを初期化する方法が多すぎます。また、わかりにくい方法も存在します。これには C++0x の統一メカニズムで対処できればと考えています。前の質問に対する回答で、テンプレートについて言及しました。これは後期の C++ (1985 年より後) で収めた大きな成功でしたが、言語を過度に使用していました。繰り返しますが、C++0x の一部はこうした問題に対処するためのものです。

この数年で私たちが発見した大小さまざまな問題は、互換性の理由から C++ では解決できません。たとえば、宣言構文は必要以上に複雑です。ほとんどの線形表現の方が適切でしょう。同様に、多くの既定の要素に問題があります。たとえば、コンストラクタは既定では変換であってはなりませんし、名前は既定では他のソース ファイルからアクセス不可能であるべきです。リンカの未制御により、たえず問題が発生していました。特に、実装者は互換性のない形式で同様の機能を提供することに喜びを感じているように思えます。

嬉しい驚きもありました。最も目を引いたのは、リソース管理や (例外を使用した) エラー処理に関係する手法でデストラクタが幅広く使用されたことです。私は、デストラクタは良いアイデアであると考えていました。結局のところ、コンストラクタの影響は元に戻す必要があるからです。ところが、C++ を活用するうえでデストラクタがどれほど重要であるかを完全には理解できていませんでした。

HD あなたはご自身の Web サイト上で、「簡潔さは言語自体に求めるのではなく、構築されるアプリケーションに求めるべきだと考えている」とコメントしておられます。こうした考えをまとめたものが、ドメイン固有言語 (DSL) に向かう動きなのではないでしょうか。

BS ほぼそのとおりだと思います。多くの場合、その方向での試みです。場合によっては、成功しさえするでしょう。

HD DSL には概してどのような考えをお持ちですか。また、DSL と汎用言語の関係をどのように捉えておられるのでしょうか。

BS 私は、鳴り物入りで設計、実装、導入されるのに、たいした影響もなく消えていく言語の数に憂慮しています。この間 (つまり通常は多年にわたる開発フェーズの間) に、実際にはリターンがないにもかかわらず、新しい言語によって大量のリソースが消費されています。この現象については、『A Rationale for Semantically Enhanced Library Languages』(research.att.com/~bs/SELLrationale.pdf) という題名の論文を書きました。私は、ツールや汎用言語によってサポートされ得るライブラリの使用には賛成しています。

私が思うに、DSL は最後の手段であるべきです。最初の手段であってはなりません。DSL は、可能な限り汎用言語や標準的なツール チェーンに根差したものでなければなりません。DSL を実装したり、そのランタイム プリミティブを実装したりするには、汎用言語 (または、最低でもシステム プログラミング言語) が必要です。私が最も望ましいと考えるのは、DSL を少なくとも 1 つの汎用言語に意識的に対応させることです。これにより、その汎用言語で記述されたライブラリを使用して新しい機能を簡単に追加できるようになります。プロフェッショナルは複数の言語を習得する必要があるとはいえ、さまざまな DSL を使用することで積み重なった複雑さは問題になるほどではなかったのでしょうか。また、多くの (ほとんどとは言わないまでも) DSL は、汎用言語に "なりたがっている" ように見えます。

HD C++ の多数の構成要素は、さまざまなハードウェアの多様な定義のために、意図的にあいまいなままにしてあると述べておられます。相互運用性には、これらの構成要素のあいまいさを排除できるほどの向上は見られましたか。

BS "あいまいさ" は適切な表現ではありません。あまりにも多くの要素が、未定義のまま残されているか、実装時に定義されている状況です。もし私が C++ をゼロから再定義できるなら、未定義の動作はなくなり、実装時に定義される動作もはるかに少なくなるでしょう。とはいえ、私はタイム マシンを持っていませんし、今日の分析力を持ってしても、何億ものコードの行を分けることなどできません。

手法とベスト プラクティス

HD 普段どのような処理手法を使用したり、教えたりしていますか。

BS アプリケーションの主要な概念を把握すること、役立つライブラリを特定すること、アプリケーションの概念をサポートするために新しいライブラリを構築すること、アイデアは早く試してみること、早期に統合すること、早期に、そして頻繁にテストすること、設計ツールとしてドキュメントやチュートリアル資料を使用すること、小さなプログラムから大きなプログラムに成長させること (途中で繰り返すこと)、などです。私が今比較的規模の小さいプロジェクトに重点に置いていることがよくわかるでしょう。

HD 言語と開発手法の間には、共通部分や相性があると思われますか。

BS ライブラリ設計を設計/開発手法として捉える限りは、そうだと思います。レベルが高まり続けている (アプリケーションに近づきつつある) 機能にライブラリの構築を通じて取り組むと、言語に対して要求が生じます。この点は誇張しているわけではありませんが、COBOL、C、Java、C++、Python 向けの単一の開発手法があるとも、各言語から最小限のサポート以上のものを得られるとも思っていません。

HD ソフトウェアを開発する際の個人的な経験則はありますか。

BS 主要な概念、ソフトウェアのインターフェイス、リソース (メモリ、ファイル、ロックなど) の管理、エラー処理を重視することです。クラスの適切な不変性の設計や、RAII (Resource Acquisition Is Initialization: リソースの確保は初期化時に行う) は、重要な手法です。

HD アジャイルであると面倒も多いものです。"アジャイル" はあなたにとって何を意味するのでしょうか。C++ はアジャイルをサポートしていますか。

BS 私はその表現を使っていません。あまりに漠然としすぎています。何を意味するにせよ、C++ はもちろんアジャイルをサポートしています。

将来を見据える

HD テンプレート、動的イベント、自己記述コードなどの高度な機能をサポートする一方で、初心者が使用しやすい状態を保つために、言語はどのように進化し得るでしょうか。

BS わかりません。一般的な答えはないと思います。新しい機能は、それらが言語のコンテキストでより効率的な手法をサポートするのであれば重要です。ただし、安定性も不可欠です。C や C++ が力を発揮し続けている理由の 1 つとして、標準委員会によるケアが挙げられます。これは、古い (ときには何十年も前の) コードの有効性を保ち、新しい機能の統合を円滑に進めるための作業です。控えめに言っても、簡単な作業ではありません。新しい機能の導入は、常に成功するとも限りません。標準委員会のメンバーは、初心者への配慮を忘れることがよくあります。統一された初期化、自動キーワード (変数型をその初期化子から推定するため)、テンプレート引数の要件のチェックなどの概念をはじめ、C++0x で計画されている主要な機能の一部は、専門家以外のユーザーが言語をより簡単に使用できるようにするうえで役立つでしょう。

HD 言語のメタデータは、将来のプログラミング言語の重要な土台であると考えておられますか。

BS いいえ。個人的には、メタデータの多用には抵抗があります。

HD CPU のコア数が増え始めた場合、同時実行には根本的な変化が生じると思いますか。C++0x では同時実行の課題にはどのような方法で対処できるのでしょうか。

BS C++0x は、マルチスレッドに適したマシン モデル、ライブラリ構築用の低レベル プリミティブ セット、スレッドおよびロックのライブラリ API などの基礎的な要素を提供します。それ以上のもの、特に、スレッド プール、フューチャ、メッセージ キューに基づくよりシンプルで高レベルな同時実行モデルも提供できればと考えています (おそらく 2 ～ 3 年の間には可能でしょう)。多数のプロセッサへの計算の分散と、それらのプロセッサでのアクティビティのローカライズを自動化 (またはほとんど自動化) する方法を見つける必要があります。この領域には C++ と同様に多くの仕事が残っています。まだ主流のモデルはありません。例として、Texas A&M University の STAPL や Intel の TBB が挙げられます。

HD グラフィックス プロセッシング ユニットでの汎用コンピューティング (GPGPU) についてはどのような考えをお持ちですか。

BS 非常に興味深いですが、私には実際の経験がないので、専用プロセッサを使用するには豊富なスキルが必要だという事実しかコメントできません。

HD ハイ パフォーマンス コンピューティング (HPC) はプログラマに対して最終的には透過的になると予想されますか。さらに、この目標は言語、またはライブラリやフレームワークにとっての目標なのでしょうか。

BS ある程度透過的にはなるでしょう。とはいえ、同時実行を完全に透過的にできるとも、そうすべきであるとも思いません。まず第一に、エラー処理は、プロセッサの可用性、共有メモリ (非共有メモリ)、分散 (非分散)、および待ち時間に応じて大きく異なる可能性があります。必要に応じて透過性を見積もることは、新しい言語、言語の新しい機能、および (私が好む) 新しいライブラリの目標であり続けることでしょう。後者については、基になっている言語でマシン モデルと非常に低いレベルのプリミティブ セットがサポートされている場合にのみ実現可能です。C++0x はこれをサポートしています。

HD C++0x の設計の進捗状況はいかがですか。

BS やっとのことで、完成に近づいています! 少なくとも私はそうであってほしいと思っているのですが、決議が下されるまでは何が起こるかわかりません。完成を前にして、切迫した状態にならないとも限りません。現在の予定では、新しい完全な標準を 6 月の公式発表に向けて決定することになっています。12 ～ 18 か月後に新しい正式な標準が完成するでしょう。間違いなく、私は "標準はどのようにして作り上げるのか、何を基本原則とすべきなのか、正確には何が含まれているのか" という 1 つのテーマで書籍を書き上げることができます。いや、既に書き上げているのかもしれません。research.att.com/~bs/hopl-almost-final.pdf から入手できる HOPL の論文『Evolving a language in and for the real world: C++ 1991-2006』と、私のホーム ページでタイトルに C++0x が含まれるページを参照してください。研究熱心な方には、Web 上で "WG21" を検索して、ISO C++ 標準委員会のすべての記事 (あらゆる提案が含まれています) を探すことをお勧めします。少なくとも、たいへんな取り組みであることをおわかりいただけるでしょう。広く使用されているプログラミング言語に改良を加える作業は、困難を極めます。特に、多数のツール、言語、およびアプリケーションの実装レイヤでは難しい作業です。私や他の開発者が作成したいくつかのビデオも、オンラインで、または私の C++ のページで入手できます。

書籍と電話

HD 今、何を読んでおられますか。

BS 技術資料では、マシン アーキテクチャについての考えを新たにするために、Hennessy と Patterson を読み直しています。また、人々が質の高いコードを記述するうえで役立つ論文や記事を集めているところです (私が教えているコースのため)。そのような記事を見つけるのは、予想よりはるかにたいへんです。学術論文は非常に専門的ですし、それ以外の論文は、役立ちそうだと思っても根拠に欠けていることがよくあります (ご提案は大歓迎です)。それからもちろん、C++ の標準化に関する膨大なドキュメントを読んでいます。Knuth についての理解を新たにしようとしていたのですが、第 3 巻を持っていってしまった人がいるので、お預けになりました。気晴らしに、O'Brian のオーブリー & マチュリン シリーズを少し読んでいます。科学についての理解を新たにしようと思っており、Richard Dawkins をちょうど読み終えたところです。その他、Rodger の『Command of the Ocean: A Naval History of Britain, 1649-1815』を最近読了しました (今 O'Brian を読んでいるのはそのためです)。

HD あなたは、「コンピュータが電話と同じくらい簡単に使えるようになればいいといつも思っていたが、私の願いはかなった。もはや電話自体の使い方がわからなくなってしまったからだ」と述べておられます。あなたはスマートフォンをお持ちですか。また、スマートフォンはいくらか使いやすくなっていますか。

BS 私は電話が大好きというわけではありません。面と向かい合ったコミュニケーションの方が好きですし、それが難しい場合は書かれた言葉を読みたいと思っています。どんなに高機能な電話も、うまい表現の電子メールにはかないません。私は、ポケットにすっきり収まるスリムな電話を使用しています。その全部の機能は使っていません。音質や信頼性に関する機能には、お手上げです。とは言うものの、私が先の発言をしたころに比べて、ユーザー インターフェイスははるかに使いやすくなってきています。

Howard Dierking は MSDN Magazine の編集長です。


Office 開発: ドキュメント情報パネルを使用したメタデータの管理

Office 開発

ドキュメント情報パネルを使用したメタデータの管理

Ashish Ghoda
この記事の内容 : :
  • ドキュメント情報パネル
  • メタデータを管理する
  • InfoPath フォームを使用する
この記事は次のテクノロジを使用しています:
2007Office System、.NET Framework 3.5
コンテンツ
ドキュメント情報パネルを使用したメタデータ管理
ドキュメント プロパティの管理
カスタム ドキュメント プロパティの管理
InfoPath フォーム テンプレート
ドキュメント情報パネルの表示または非表示
チェックリスト クイック パーツの挿入
バインド
ドキュメント情報パネルをドキュメントと統合する
バージョンに関する問題
まとめ

今日では、どのような組織でも困難な情報管理の課題に直面しています。重要な情報がばらばらな場所にさまざまな形式で格納されている状態では、データを簡単に検索したり、使用したりすることが困難になります。単にデータの検索および使用を可能にするだけでなく、企業はさらにデータ保持および行政コンプライアンスに対応するための適切なポリシーも保持していなくてはなりません。

このような新たなニーズを満たし、情報駆動型アプリケーションを実装するためには、ドキュメントを適切にメタデータで属性付けし、そのメタデータを企業のすべてのシステムで使用可能にする必要があります。この記事では、ユーザーがドキュメント レベルのメタデータを管理することができる Microsoft® Word 2007、Excel® 2007、および PowerPoint® 2007 のフォームベース コンポーネントであるドキュメント情報パネルを使用して、効果的なメタデータおよびコンテンツ管理を実現する方法を説明します。

ドキュメント情報パネルを使用したメタデータ管理

Microsoft Office プラットフォームでは、常に、ドキュメントの標準プロパティおよびカスタム プロパティの両方を管理することができました。ただし、ドキュメント情報パネルを使用すると、メタデータに基づく検索や情報駆動型ビジネス プロセスの自動化などの追加機能を実装することができます。ドキュメント情報パネルでは、次の 4 種類のドキュメント プロパティに関するドキュメント固有のメタデータとして実装できます。それらは、コア プロパティまたは標準プロパティ、カスタム ドキュメント プロパティ、Microsoft Office SharePoint® Server (MOSS) 2007 のコンテンツ タイプ、拡張および詳細カスタム プロパティです。それぞれについて説明します。

コア プロパティ ドキュメント情報パネルの標準のドキュメント プロパティ ビューを使用すると、すべての Office ドキュメントに共通のコア ドキュメント プロパティである Author、Title、Subject、Keywords、Category、Status、および Comments を管理できます。

カスタム ドキュメント プロパティ Office ドキュメントにビジネス コンテキストをさらに追加するために、2007 Microsoft Office system では、信頼されたカスタム InfoPath® フォーム テンプレートをカスタム ドキュメント情報パネルとして添付することができます。図 1 に、ChecklistMetadata という InfoPath フォーム テンプレートを示します。これについては、後ほど詳しく説明します。このフォーム テンプレートは、カスタム ドキュメント情報パネルとして Word 2007 ドキュメントに添付されます。

Figure 1 InfoPath Form Template Attached to a Word 2007 Document
Figure 1 InfoPath Form Template Attached to a Word 2007 Document(画像を縮小するには、ここをクリックします)
Figure 1 InfoPath Form Template Attached to a Word 2007 Document
Figure 1 InfoPath Form Template Attached to a Word 2007 Document (画像を拡大するには、ここをクリックします)

SharePoint Server 2007 のコンテンツ タイプ MOSS 2007 ドキュメント ライブラリに格納されているドキュメントにはカスタム ドキュメント プロパティが含まれており、これらのプロパティは関連付けられている SharePoint サイトに対して定義されているコンテンツ タイプにマップされています。これらのドキュメントはコンテンツ タイプのフィールドをカスタム ドキュメント情報パネルとして自動的に表示します。ユーザーはオフライン/オンライン モードでコンテンツ タイプ フィールドの値を更新できます。また、ユーザーがオンラインに接続したときに、これらの値はドキュメント ライブラリに自動的に保存されます。

MOSS 2007 ベース ドキュメントのカスタム ドキュメント情報パネルの詳細については、go.microsoft.com/fwlink/?LinkId=109825 を参照してください。

拡張および詳細カスタム プロパティ 図 2 のように、ドキュメント情報パネルの [詳細プロパティ] オプションを使用してドキュメント プロパティ ウィンドウにアクセスすることにより、追加のドキュメント固有のカスタム プロパティにアクセスしたり、これらのプロパティを管理することができます。このダイアログから、[要約] タブを使用して拡張プロパティを設定したり、[カスタム] タブを使用して新しいカスタム プロパティを作成したりすることができます。より豊富なメタデータ管理機能のために、ドキュメント情報パネルを構成し、InfoPath フォームを介してドキュメント メタデータの表示および管理を行うことができます。

Figure 2 Advanced Properties Menu
Figure 2 Advanced Properties Menu(画像を縮小するには、ここをクリックします)
Figure 2 Advanced Properties Menu
Figure 2 Advanced Properties Menu (画像を拡大するには、ここをクリックします)

2007 Office system の [開発] タブでは、コード、コントロール、カスタマイズされた XML、およびテンプレートを使用してドキュメントをカスタマイズすることができます。また、このタブでは、ドキュメント情報パネルを使用可能にすることもできます。図 3 では Word 2007 ドキュメントの強調表示されたドキュメント パネル オプションを示しています。Office 2007 ドキュメント内でドキュメント情報パネルを表示および構成するには、[Document Panel] (ドキュメント パネル) をクリックします。

Figure 3 Document Panel Option for a Word 2007 Document
Figure 3 Document Panel Option for a Word 2007 Document(画像を縮小するには、ここをクリックします)
Figure 3 Document Panel Option for a Word 2007 Document
Figure 3 Document Panel Option for a Word 2007 Document (画像を拡大するには、ここをクリックします)

カスタム ドキュメント情報パネルを添付するには、InfoPath フォーム テンプレートのネットワーク パスまたは URL を使用してカスタム テンプレートを指定します。なお、標準のドキュメント プロパティのみを表示する場合は、この時点でカスタム テンプレートを指定する必要はありません。

次に、ドキュメント情報パネルの [既定で表示] として使用する標準プロパティまたはカスタム プロパティ (カスタム InfoPath フォーム テンプレートが添付されている場合のみ) のオプションを選択します。InfoPath フォーム テンプレートで複数のビューが使用可能な場合は、既定のビューがカスタム ドキュメント情報パネルとして表示され、ここから他のビューにアクセスできます。

効率的なメタデータ管理における主要な要件の 1 つは、アプリケーション間で一貫したメタデータ スキーマを使用するということです。ドキュメント情報パネルを使用すると、この一貫性と再利用が促進されます。つまり、同一の InfoPath 2007 テンプレートをカスタム ドキュメント情報パネルとして、Word、Excel、および PowerPoint 2007 ドキュメントに添付することができます。

おそらくご存じのことと思いますが、2007 Office system のドキュメントは、オープン XML 形式に基づいています。そのため、これらのドキュメントは、ドキュメントに関連するコンテンツおよびメタデータを保持する XML ファイルを含む .zip ファイル パッケージで構成されます。そのため、ドキュメント プロパティはパッケージ内に XML として格納されています。この情報はすべて XML として格納されるため、プログラムからこのプロパティにアクセスし、メタデータ駆動型ビジネス プロセスを自動化するのはごく簡単です。以下のセクションで、そのしくみについて説明します。

ドキュメント プロパティの管理

標準のドキュメント プロパティは、ドキュメント情報パネルのドキュメント プロパティ ビューを使用して管理することができます。これらのプロパティが OpenXML パッケージ内で実際に格納されている場所を確認するには、解凍された Office ドキュメントの _rels フォルダの .rels ファイルを開きます。図 4 からわかるように、このファイルでは、標準ドキュメント プロパティ (コア プロパティ) が docProps フォルダ内の core.xml ファイルに格納されていることを示しています。core.xml ファイルにはドキュメント情報パネルのドキュメント プロパティ ビューから設定されるすべての標準のドキュメント プロパティが含まれています。

Figure 4 関係ファイル (Close Figure 4)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships
   xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3"
    Type="http://schemas.openxmlformats.org/officeDocument/2006/
    relationships/extended-properties" Target="docProps/app.xml"/>
  <Relationship Id="rId2"
    Type="http://schemas.openxmlformats.org/package/2006/
    relationships/metadata/core-properties" Target="docProps/core.xml"/>
  <Relationship Id="rId1"
    Type="http://schemas.openxmlformats.org/officeDocument/2006/
    relationships/officeDocument" Target="word/document.xml"/>
  <Relationship Id="rId4"
    Type="http://schemas.openxmlformats.org/officeDocument/2006/
    relationships/custom-properties" Target="docProps/custom.xml"/>
</Relationships>

図 5 のコードは、Word オブジェクト モデルの DocumentProperties オブジェクトを使用して、標準ドキュメント プロパティの値を取得し、その値を新しい値に設定する方法を示します。Word ドキュメント オブジェクトの BuiltInDocumentProperties プロパティは、標準の DocumentProperty オブジェクトのコレクションである DocumentProperties オブジェクトを返します。次に、Item プロパティを使用して、プロパティ名 (この場合は、カテゴリ プロパティ) を指定することにより、特定の DocumentProperty オブジェクトを取得します。DocumentProperty オブジェクトの Value プロパティを使用して、プロパティの値を設定または取得します。

Figure 5 標準ドキュメント プロパティの取得 (Close Figure 5)
Dim StandardProperties As Microsoft.Office.Core.DocumentProperties
Dim StandardProperty As Microsoft.Office.Core.DocumentProperty

Dim Category As String

'Assign Current Document Standard Properties
StandardProperties = _
  DirectCast(Globals.ThisDocument.BuiltInDocumentProperties, _
  Microsoft.Office.Core.DocumentProperties)

'Assign "category" property
StandardProperty = StandardProperties.Item("category")

'Get "category" property value
Category = StandardProperty.Value

'Set "category" property value to "Modified Checklist"
StandardProperty.Value = "Modified Checklist"

Word オブジェクト モデルでは、CustomXMLParts オブジェクトを使用して、XML ファイルとしてこれらのプロパティにアクセスすることもできます (図 6 を参照)。CustomXMLParts オブジェクトは、ドキュメントを記述する XML ファイルを含む CustomXMLPart オブジェクトのコレクションを返します。コレクションの最初の XML ファイルは core.xml ファイルであり、標準のドキュメント プロパティが含まれています。XMLDocument オブジェクトへの参照を取得したら、SelectSingleNode などの XPath クエリ メソッドを使用してカテゴリ プロパティ ノードなどのノードを操作できます。

Figure 6 XML としてのプロパティへのアクセス (Close Figure 6)
Dim OfficeCustomXMLParts As Microsoft.Office.Core.CustomXMLParts
Dim Category As String

'Assign CustomXMLParts containing standard/core
'document properties XML Part
OfficeCustomXMLParts = DirectCast(Globals.ThisDocument.CustomXMLParts, _
  Microsoft.Office.Core.CustomXMLParts)

'Find the "category" property node and get the value of "category"
 'standard property

Category = OfficeCustomXMLParts.Item(1).SelectSingleNode _
 ("//*[local-name(.)='category']").Text

'Find the "category" property node and set the value of "category"
' standard property
OfficeCustomXMLParts.Item(1).SelectSingleNode _
 ("//*[local-name(.)='category']").Text = "modified category"

カスタム ドキュメント プロパティの管理

カスタム ドキュメント情報パネルとして添付されている InfoPath フォーム テンプレートを使用して、カスタム ドキュメント プロパティを管理できます。InfoPath フォーム データはカスタム XML ドキュメント パーツとしてドキュメント内部に格納されます。なお、ドキュメントに含まれるすべてのカスタム XML パーツの関係は、Word/_rels フォルダ内の document.xml.rels ファイルに定義されています。カスタム XML ファイル自身は、customXml フォルダ内にあります。各カスタム XML ドキュメント パーツには、customXml/_rels サブフォルダに格納されている独自の関係定義ファイルがあります。次に示すように、item1.xml パーツには対応する item1.xml.rels ファイルがあります。このファイルは itemProps1.xml ファイルを指しており、この XML パーツが item1.xml を記述するカスタム プロパティを保持していることを指定します。

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Relationships
  xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1"
    Type="http://schemas.openxmlformats.org/officeDocument/2006/
    relationships/customXmlProps" Target="itemProps1.xml" />
</Relationships>

次に、itemProps1.xml ファイルは、スキーマ参照を使用して、カスタム XML スキーマが InfoPath スキーマであることを示します。これにより、Word では次のように item1.xml を InfoPath フォームとして開きます。

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<ds:datastoreItem ds:itemID="{A9851B97-75EE-4C97-ACC6-D9EB7DA1D3DC}"
  xmlns:ds="http://schemas.openxmlformats.org/officeDocument/
  2006/customXml">
  <ds:schemaRefs>
    <ds:schemaRef ds:uri="http://schemas.microsoft.com/office/
      infopath/2003/myXSD/2007-09-01T16:03:56" />
    <ds:schemaRef ds:uri="http://schemas.microsoft.com/
      office/infopath/2003" />
  </ds:schemaRefs>
</ds:datastoreItem>

カスタム XML プロパティをカスタム ドキュメント情報パネルとして添付する必要があることを指定するには、次のように、スキーマ参照を使用してこの関係を示すだけで済みます。

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<ds:datastoreItem ds:itemID="{B97FA79B-5941-4441-9A4B-3E001C0278D6}"
   xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/
  customXml">
  <ds:schemaRefs>
    <ds:schemaRef ds:uri="http://schemas.microsoft.com/office/2006/
    customDocumentInformationPanel" />
  </ds:schemaRefs>
</ds:datastoreItem>

これで、この項目を開くと、InfoPath がカスタム XML プロパティ エディタとして使用され、著者の ChecklistMetadata.xsn ファイルが InfoPath フォーム テンプレートとしてカスタム ドキュメント情報パネルに添付されることを示しています。添付された InfoPath フォームの XML ファイルにアクセスするには、標準のドキュメント プロパティへのアクセスに使用したように、CustomXMLParts オブジェクトを使用できます。

次のコードは、CustomXMLParts オブジェクトおよび SelectSingleNode メソッドを使用して、CL—Business—Information Technology カテゴリ チェックリストの InfoPath フォーム フィールドの ProjectName ノードを取得する方法を示します。

Dim InfoPathXML As XmlDocument = New XmlDocument()
Dim projectname As String

'Load InfoPath Form XML
InfoPathXML.LoadXml(Globals.ThisDocument.CustomXMLParts.Item(4).XML)

'Get the "Project Name" field value of the
'   CL - Business - Information Technology checklist metadata
projectname = InfoPathXML.SelectSingleNode _
 ("//*[local-name(.)='ProjectName']").InnerText

追加のカスタム プロパティは、ドキュメント情報パネルの [詳細プロパティ] ビューを使用して管理することができます。root _rels フォルダの .rels ファイルを確認すると、ドキュメント パッケージ内でこれらのプロパティが格納されている場所を確認できます。次に説明するように、関係ファイルは、カスタム ドキュメント プロパティが docProps フォルダ内の custom.xml ファイルに格納されていることを示します。

デモンストレーションのために、[詳細プロパティ] ダイアログを使用して、このドキュメントのカスタム プロパティとして Client を追加しました。次のコードに示すように、Client プロパティは docProps/custom.xml ファイルに格納されます。

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Properties xmlns="http://schemas.openxmlformats.org/
  officeDocument/2006/
  custom-properties"
  xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/
  docPropsVTypes">
  <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
    pid="2" name="Client">
    <vt:lpwstr>
      Client Value
    </vt:lpwstr>
  </property>
</Properties>

図 7 に、Word オブジェクト モデルを使用して、Client カスタム プロパティの値をプログラムで取得し、新しい値を設定する方法を示します。ここに示すように、Word ドキュメント オブジェクトの CustomDocumentProperties プロパティは、カスタム DocumentProperty オブジェクトのコレクションを返します。次に、Item プロパティを使用して、プロパティ名 (この場合は、Client) を指定することにより、特定の DocumentProperty オブジェクトを取得できます。DocumentProperty オブジェクトの Value プロパティを使用して、プロパティのカスタム ドキュメントの値を設定または取得します。

Figure 7 クライアント カスタム プロパティの取得 (Close Figure 7)
Dim CustomProperties As Microsoft.Office.Core.DocumentProperties
Dim CustomProperty As Microsoft.Office.Core.DocumentProperty
Dim client As String

'Assign Current Document Standard Properties
CustomProperties = DirectCast(Globals.ThisDocument.CustomDocumentProperties, Microsoft.Office.Core.DocumentProperties)

'Assign "client" property
CustomProperty = CustomProperties.Item("Client")

'Get "client" property value
client = CustomProperty.Value

'Set "client" property value to "Microsoft"
CustomProperty.Value = "Microsoft"

次のコードは、新しいカスタム プロパティ Checklistversion を追加し、値を 1 に設定する方法を示します。

Dim CustomProperties As Microsoft.Office.Core.DocumentProperties

'Assign Current Document Standard Properties
CustomProperties = _
  DirectCast(Globals.ThisDocument.CustomDocumentProperties, _
  Microsoft.Office.Core.DocumentProperties)

'Add "ChecklistVersion" as a custom property and set value to "1.1"
CustomProperties.Add("ChecklistVersion", False, _
  Microsoft.Office.Core.MsoDocProperties.msoPropertyTypeNumber, "1.1").

ご覧のように、CustomDocumentProperties オブジェクトの Add メソッドは、新しいカスタム ドキュメント プロパティを追加し、その値を 1.1 に設定することの両方に使用されます。また、値は文字列として追加されますが、msoPropertyTypeNumber は文字列を数値と見なす必要があることを指定します。

ここまでで明らかになったのは、ドキュメント情報パネルを使用すると、OpenXML ドキュメント パッケージ内にメタデータを格納することにより、ユーザーとシステム管理者がメタデータをより効果的に管理および維持することができるということです。ただし、メタデータはそのパッケージ内の個別のドキュメント パーツとして格納します。ただし、効率的なメタデータ駆動型ビジネス プロセスの実装は、メタデータの値を使用して、ビジネス プロセスの自動化、コンテンツ環境の制御、およびユーザーによる繰り返し可能なコンテンツ生成の自動化を行うことができる場合に限り実現できます。

次に、Word 2007 に基づくチェックリスト管理アプリケーションを開発し、標準ドキュメント プロパティおよび InfoPath フォーム ベースのカスタム フィールドを、コンテンツ コントロール、クイック パーツ/ビルド ブロック、およびカスタマイズされた Fluent リボン コントロールなどの Word 2007 の他のコンポーネントと関連付けることによって、ビジネス プロセスを自動化できる方法を示します。

Word 2007 を使用してチェックリスト管理を自動化するために、まず、Word 2007 ドキュメントにカスタム ドキュメント情報パネルとして添付する、チェックリストのメタデータを表す InfoPath フォーム テンプレート ChecklistMetadata.xsn を開発し、公開する必要があります。次に、Visual Studio® で Word 2007 ドキュメント プロジェクトを作成し、カスタム ドキュメント情報パネルとして ChecklistMetadata.xsn フォーム テンプレートを添付します。次に、ドキュメント情報パネルの表示と非表示の切り替えに使用するカスタム リボン コントロールを作成しました。

次に、分類されたカスタム クイック パーツとしてさまざまなチェックリストが含まれている Word 2007 テンプレート ChecklistsQuickPartsTemplate.dotm を開発し、Word 2007 ドキュメントと統合するためにユーザーのアプリケーション データ フォルダに配布します。これで、添付された ChecklistsQuickPartsTemplate.dotm テンプレートからチェックリスト クイック パーツをビルド ブロックとしてドキュメントにプログラムで挿入する、カスタム リボン コントロールを実装できます。カスタム ドキュメント情報パネルで選択されたチェックリスト カテゴリに基づいて、チェックリスト クイック パーツを挿入します。

これに続いて、Word 2007 コンテンツ コントロールを使用して、標準ドキュメント プロパティおよび InfoPath フォーム フィールド (カスタム ドキュメント情報パネルを使用して表示される) をプログラムでドキュメント自身にバインドしました。今度は、標準および拡張ドキュメント プロパティをクイック パーツとしてドキュメントに手動で追加する方法を示します。

InfoPath フォーム テンプレート

すべてのチェックリストは、共通のメタデータおよびチェックリスト固有のメタデータを持つことができます。著者が開発しようとしている InfoPath フォーム テンプレートでは、サンプルの汎用メタデータおよびチェックリスト固有メタデータを定義します。

まず、設計時に InfoPath フォーム テンプレートに次のチェックリスト カテゴリを定義します。図 8 に、これらのカテゴリを示します。説明のために、2 つのタイプの主なカテゴリ Business および Personal のみを定義しました。また、2 つのカテゴリのメタデータ セクションも定義および開発しました。これらは、Business—Information Technology カテゴリと Personal—Hospital カテゴリです。

Figure 8 Sample Checklist Categories
Figure 8 Sample Checklist Categories(画像を縮小するには、ここをクリックします)
Figure 8 Sample Checklist Categories
Figure 8 Sample Checklist Categories (画像を拡大するには、ここをクリックします)

Business—Information Technology カテゴリに追加されたメタデータには、Project Name、Purpose、および Team Members があります (図 9 に示されているとおり、すべて複数行のテキスト ボックス)。また、Project Number と Technology Used (1 行のテキスト ボックス)、および日付選択用の Start Date と End Date があります。同様に、図 10 に Personal—Hospital カテゴリのメタデータが表示されるようすを示します。

Figure 9 Business—Information Technology Metadata
Figure 9 Business—Information Technology Metadata(画像を縮小するには、ここをクリックします)
Figure 9 Business—Information Technology Metadata
Figure 9 Business—Information Technology Metadata (画像を拡大するには、ここをクリックします)
Figure 10 Personal—Hospital Metadata
Figure 10 Personal—Hospital Metadata(画像を縮小するには、ここをクリックします)
Figure 10 Personal—Hospital Metadata
Figure 10 Personal—Hospital Metadata (画像を拡大するには、ここをクリックします)

メタデータ カテゴリの選択に基づいて、関連するメタデータ セクションが表示されます。この機能は、各カテゴリ セクションの InfoPath の条件付き書式設定機能の利用によって可能になります。セキュリティ上の理由から、InfoPath フォーム テンプレートをカスタム ドキュメント情報パネルとしてドキュメントに添付する前に、このテンプレートを公開するか、またはローカルなイントラネットあるいは信頼できるサイトにインストールする必要があります。

InfoPath フォーム テンプレートを公開する前に、[ツール] メニューの [フォームのオプション] の [セキュリティと信頼] オプション (図 11) を使用して、適切なセキュリティ設定を行います。テンプレートを信頼できる場所に公開したら、カスタム ドキュメント情報パネルとして添付します。

Figure 11 Security and Trust Settings
Figure 11 Security and Trust Settings(画像を縮小するには、ここをクリックします)
Figure 11 Security and Trust Settings
Figure 11 Security and Trust Settings (画像を拡大するには、ここをクリックします)

高度なセキュリティを保つためには、正規のサードパーティの認証プロパイダが提供する証明書を使用して InfoPath フォーム テンプレートをデジタル署名し、MOSS ドキュメント ライブラリや Forms Server などの信頼できる場所にそのテンプレートを公開します。

開発段階にある場合は、自己署名入り証明書を使用して InfoPath フォーム テンプレートに署名し、ローカル マシンに公開することはそれほど困難ではありません。このプロセスの詳細については、go.microsoft.com/fwlink/?LinkId=109827 を参照してください。

InfoPath フォーム テンプレートを作成したら、Visual Studio 2008 で新しい Word 2007 ドキュメント プロジェクトを作成します。設計時に、カスタム ドキュメント情報パネルとして ChecklistMetadata.xsn ファイルをドキュメントに添付し、既定のビューとしてカスタム ビューを選択します。InfoPath フォーム ビュー Checklist Metadata をドキュメントでカスタム ドキュメント情報パネルとして表示する必要があります。

チェックリスト カテゴリを CL—Business—Information Technology および CL—Personal—Hospital に変更することによって、InfoPath フォーム テンプレートが機能していることを確認できます。ドキュメント情報パネルに適切なメタデータ セクションが表示されているはずです。

ドキュメント情報パネルの表示または非表示

ドキュメントで作業するときにモニタ上により多くのスペースを確保するため、ドキュメント情報パネルを非表示にする方が望ましい場合があります。Office オブジェクト モデルを使用すると、ドキュメント情報パネルの表示および非表示をプログラムで制御することができます。カスタマイズされたリボン コントロールを作成して、このパネルの表示と非表示を切り替える方法を説明します。

新しいアイテムのダイアログから、[リボン (ビジュアル デザイナ)] を選択して、プロジェクトにリボンを追加します。そのリボンに CustomizeChecklistRibbon という名前を付けます。ShowHideDIP というボタン コントロールを追加します。これは、ドキュメント情報パネルの表示と非表示を切り替えるために使用します (図 12 を参照)。

Figure 12 Show or Hide Document Information Panel
Figure 12 Show or Hide Document Information Panel(画像を縮小するには、ここをクリックします)
Figure 12 Show or Hide Document Information Panel
Figure 12 Show or Hide Document Information Panel (画像を拡大するには、ここをクリックします)

次のコードは、ドキュメント情報パネルの表示または非表示をプログラムで切り替える方法を示しています。ShowHideDIP ボタンのクリック イベント ハンドラ内に、次のコードを追加します。

'Check if Document Information Panel is visible
If Globals.ThisDocument.Application _
  .DisplayDocumentInformationPanel = True Then
  'Hide Document Information Panel
  Globals.ThisDocument.Application. _
  DisplayDocumentInformationPanel = False
Else
  'Show Document Information Panel
  Globals.ThisDocument.Application. _
  DisplayDocumentInformationPanel = True
End If

Word アプリケーション オブジェクトの DisplayDocumentInformationPanel プロパティは、ドキュメント情報パネルの表示の制御に使用するブール値を取得または設定します。

チェックリスト クイック パーツの挿入

メタデータ ベースのコンテンツの再利用性は、効果的なタスクの自動化にとって重要な要件の 1 つです。Word 2007 クイック パーツ機能を使用すると、ドキュメントの再利用可能なコンテンツ セクションを特定し、それらのコンテンツ セクションを分類して、既定の Microsoft ビルド ブロック テンプレートまたはカスタム クイック パーツ テンプレートのいずれかにクイック パーツとして追加することができます。コンテンツがクイック パーツとして追加され、ユーザーのマシンの AppData フォルダにコピーされたら、任意の Word 2007 ドキュメントの再利用可能なコンテンツにアクセスできます。

カスタム ドキュメント情報パネルで選択したカテゴリに固有のチェックリスト クイック パーツを挿入するには、次のメソッドを呼び出すことにより、"Insert Checklist" リボン コントロールのクリック イベントから ChCategory InfoPath フォームの XML フィールドを取得します (図 13)。

Function GetChecklistCategoryInfoPathField() As String
  Dim InfoPathXML As XmlDocument = New XmlDocument()
  Dim ChecklistCategory As String
    InfoPathXML.LoadXml(Globals.ThisDocument.CustomXMLParts.Item(4).XML)
    ChecklistCategory = InfoPathXML.SelectSingleNode( _
      "//*[local-name(.)='ChCategory']").InnerText
  Return ChecklistCategory
End Function

カスタム ドキュメント情報パネルとして添付された Checklist InfoPath XML は、CustomXMLParts コレクションの 4 番目のファイルであり、CustomXMLParts オブジェクトの Item プロパティを使用して取得されます。InfoPath XML を新しい XmlDocument オブジェクトに読み込み、選択した ChCategory ノードのドキュメントをクエリします。

図 13 に示すメソッドでは、選択したチェックリスト カテゴリに基づいて、チェックリスト クイック パーツ テンプレートからチェックリスト クイック パーツをフィルタします。残りのすべてのチェックリスト クイック パーツは、次に、新しいページからそれぞれのクイック パーツと共にドキュメントに挿入されます (チェックリストを分類されたクイック パーツとして識別し、カスタム チェックリスト クイック パーツ テンプレートに挿入する詳細については、go.microsoft.com/fwlink/?LinkId=109830 を参照してください)。

Figure 13 InsertChecklistQuickPartsAsBuildingBlock (Close Figure 13)
Sub InsertChecklistQuickPartsAsBuildingBlock(ByVal _
  ChecklistCategory As String)

  'Declaration
  Dim oAttachedTemplate As Microsoft.Office.Interop.Word.Template
  Dim oBuildingBlock As Microsoft.Office.Interop.Word.BuildingBlock
  Dim selection As Microsoft.Office.Interop.Word.Selection
  Dim TotalQuickParts As Integer
  Dim iIndex As Integer

  'Define Attached Template - ChecklistsQuickPartsTemplate.dotm
  oAttachedTemplate = Globals.ThisDocument.AttachedTemplate

  'Get Total Number of quick parts available in the selected checklist
  ' category in the InfoPath form
  TotalQuickParts = oAttachedTemplate.BuildingBlockTypes _
    .Item(Word.WdBuildingBlockTypes.wdTypeQuickParts) _
    .Categories.Item("CL - " + ChecklistCategory) _
    .BuildingBlocks.Count

  selection = Globals.ThisDocument.Application.Selection

  'Insert all quick parts available in the defined category
  'Each quick part starts with a new page
  For iIndex = 1 To TotalQuickParts

    'Add Building Block Content Control
    selection.Range.ContentControls.Add(Word.WdContentControlType _
      .wdContentControlBuildingBlockGallery)

    'Define Quick Part as Building Block
    oBuildingBlock = oAttachedTemplate.BuildingBlockTypes _
      .Item(Word.WdBuildingBlockTypes.wdTypeQuickParts) _
      .Categories.Item("CL - " + ChecklistCategory) _
      .BuildingBlocks.Item(iIndex)

    'Insert Quick Part
    oBuildingBlock.Insert(selection.Range)

    selection.EndKey(Word.WdUnits.wdStory)
    selection.TypeParagraph()

    'No new page required if it is a last Quick Part
    If Not iIndex = TotalQuickParts Then
      selection.InsertBreak(Type:=Word.WdBreakType.wdPageBreak)
    End If

  Next

End Sub

コードがドキュメントに関連付けられているテンプレート (この例では、ChecklistQuickPartsTemplate.dotm) を取得したら、ビルド ブロック タイプ (WdBuildingBlockTypes.wdTypeQuickParts) ごと、およびユーザーが選択したカテゴリごとに、テンプレートの BuildingBlock オブジェクトのフィルタ処理に進みます。最後に、フィルタ処理されたクイック パーツを繰り返し、各チェックリスト クイック パーツをコンテンツ コントロールとしてドキュメントに挿入します。アクティブな Word 2007 ドキュメントに挿入ポイントを定義するには、Selection オブジェクトを使用することに注意してください。

次に、次のチェックリスト クイック パーツを挿入する前に、コードでは、ドキュメントに改ページを挿入する必要があります。Selection オブジェクトの InsertBreak メソッドは、ページ、列、またはセクションの区切りを Word 2007 ドキュメントに挿入します。WdBreakType.wdPageBreak 列挙体により、区切りが改ページであることを指定します。

バインド

InfoPath フォーム フィールドおよびドキュメント プロパティはカスタム XML ファイルとして定義および格納されるため、ドキュメント情報パネルで使用できる情報をドキュメント内のコンテンツ コントロールにバインドできます。次のコードでは、カスタム ドキュメント情報パネル内に作成される ProjectName InfoPath フォーム フィールドにコンテンツ コントロールをバインドする方法を示します。

'Sample code to bind first content control to InfoPath
'Form Field ProjectName
Globals.ThisDocument.ContentControls(1).XMLMapping.SetMapping _
  ("//*[local-name(.)='ProjectName']", , _
  Globals.ThisDocument.CustomXMLParts.Item(4))

XMLMapping オブジェクトは、Word 2007 コンテンツ コントロールと XML ノード間のマッピングを表します。XMLMapping オブジェクトの SetMapping メソッドがこのマッピングを確立します。著者は、XPath 式を使用してカスタム XML ファイルのノード ProjectName を特定しました。Checklist InfoPath フォームの XML ファイルは、ドキュメント オブジェクトの CustomXMLParts コレクションを使用して取得されます。

次に、カテゴリの標準ドキュメント プロパティにコンテンツ コントロールをバインドする方法を示します。

'Sample code to bind first content control to a standard document
'property - category
Globals.ThisDocument.ContentControls(2).XMLMapping.SetMapping _
  ("//*[local-name(.)='category']", ,  _
  Globals.ThisDocument.CustomXMLParts.Item(1))

このコードと InfoPath フォーム フィールドを使用してコンテンツ コントロールをマップするコードとの唯一の違いは、CustomXMLParts コレクションからドキュメント パーツを取得するために使用する XPath 式の変更と索引の変更のみです。

図 14 は、ProjectName チェックリスト メタデータに関連する InfoPath フォーム フィールドにコンテンツ コントロールをバインドし、そのカテゴリのドキュメント標準プロパティにバインドする方法を示します。おわかりのように、マップされたプロパティの値は、自動的にコンテンツ コントロールに設定されます。

Figure 14 Content Control Bound to InfoPath Form Field
Figure 14 Content Control Bound to InfoPath Form Field(画像を縮小するには、ここをクリックします)
Figure 14 Content Control Bound to InfoPath Form Field
Figure 14 Content Control Bound to InfoPath Form Field (画像を拡大するには、ここをクリックします)

ドキュメント情報パネルをドキュメントと統合する

標準ドキュメント プロパティおよび拡張ドキュメント プロパティをドキュメント内にクイック パーツとして統合することができます。ご存知のとおり、ドキュメント情報パネルの標準プロパティ ビューでは、標準ドキュメント プロパティを管理できます。また、ドキュメント情報パネルを介して詳細ドキュメント プロパティ管理ウィンドウを使用して、拡張ドキュメント プロパティにアクセスすることができます。

図 15 に示すように、[挿入] タブに移動し、[クイック パーツ] オプションをクリックします。[Document Property] (ドキュメント プロパティ) オプションを使用すると、標準および拡張ドキュメント プロパティを挿入できます。この図では、[Author] 標準ドキュメント プロパティがドキュメントにクイック パーツとして追加されています。挿入されたクイック パーツは、ドキュメント プロパティと緊密に統合されます。著者の例では、ドキュメント情報パネルまたはドキュメント クイック パーツ内の Author プロパティの値を変更すると、変更された値は自動的に反映され、ドキュメント プロパティが変わります。

Figure 15 Author Standard Document Property Added as a Quick Part
Figure 15 Author Standard Document Property Added as a Quick Part(画像を縮小するには、ここをクリックします)
Figure 15 Author Standard Document Property Added as a Quick Part
Figure 15 Author Standard Document Property Added as a Quick Part (画像を拡大するには、ここをクリックします)

バージョンに関する問題

2007 Office system 製品スイートを使用すると、互換モードでドキュメントを開き、保存することができるため、以前の Office (Office 97 ～ Office 2003) を使用しているユーザーはドキュメントを開き、作業することができます。当然ながら、Office 製品の以前のバージョンでサポートされていない機能は、著者がここで説明したソリューションではサポートされていません。この意味について説明します。

ドキュメント情報パネルには、標準ドキュメント プロパティおよびカスタム ドキュメント プロパティが表示されます。標準およびカスタム ドキュメント情報パネル ビューに関連するすべての機能は、ドキュメントが互換モードで格納されており、ドキュメントの作業に 2007 Office system のアプリケーションを使用している場合に限り機能します。

ただし、Office の以前のバーションを使用してドキュメントを開いて保存すると、カスタム ドキュメント情報パネルは関連する機能と共にドキュメントから分離されます。後で Office 2007 を使用してドキュメントを再度開いても、ユーザーは対応する情報を使用することはできません。

良い点としては、互換モードで保存した 2007 Office system のドキュメントを、以前のバージョンの Office を使用して開いたり保存したりしても、標準ドキュメント プロパティはそのまま保持されます。Office 97 ～ Office 2003 を使用してドキュメントを開いた場合は、ドキュメント プロパティ管理ダイアログを使用して標準ドキュメント プロパティにアクセスできます。2007 Office system を使用してドキュメントを開いた場合、標準ドキュメント プロパティには、ドキュメント情報パネルを使用してアクセスできます。標準プロパティに加えられたすべての変更は、バージョンをまたがるシナリオで保持されます。

[ドキュメントを開くときと、初めて保存するときにドキュメント情報パネルを必ず表示する] をオンにして、ドキュメント情報パネルを常に表示する場合、ドキュメントを互換モードで保存し、以前のバージョンの Office を使用して開いて保存すると、その情報は保持されません。この場合、最初に以前のバージョンで保存した後に Office を使用してドキュメントを開くと、ドキュメント情報パネルは既定では表示されません。

互換モードで Word 2007 ドキュメントを保存するときに、任意のドキュメント プロパティをドキュメント内のコンテンツ コントロールにバインドすると、すべてのコンテンツ コントロールが静的テキストに変換されるため、ドキュメント プロパティにはマップされません。

まとめ

標準およびカスタムのドキュメント情報パネルの効果的な使用により、効率的なメタデータ管理が簡単になり、クイック パーツやコンテンツ コントロールと併用して、効率的なコンテンツ管理アプリケーションを構築することができます。これらのテクノロジを使用することの価値は、バージョン 2007 の Word、Excel、および PowerPoint の間でメタデータが標準化および構造化されることです。この機能を幅広く活用すればするほど、コンテンツの検索やビジネス プロセスの推進が容易になります。

このドキュメント情報パネル機能に関する質問は、AskAshish@technologyopinion.com までご遠慮なくお寄せください。また、このプロジェクトのコードはすべて MSDN® Magazine Web サイトからダウンロードできることも忘れないでください。

Ashish Ghoda は、大手 4 社に入る会計事務所の技術課長です。彼は、マイクロソフト認定プロフェッショナルおよびマイクロソフト認定の .NET のアプリケーション デベロッパーです。また、Web サイト technologyopinion.com を管理しており、開発に関する記事を書いています。連絡先は AskAshish@technologyopinion.com です。


いろいろな場所へ: モバイル デバイスのプロビジョニング

いろいろな場所へ

モバイル デバイスのプロビジョニング

Mike Calligaro
コンテンツ
なぜプロビジョニングなのか
構成サービス プロバイダ
XML をデバイスに送る
だれが XML を入力するか
すべてのプロビジョニングを終えて準備完了

モバイル デバイス開発専門の新コラム "いろいろな場所へ" へようこそ。トピックの中心は主に Windows Mobile® フォンと Tablet PC になりますが、モビリティに関するトピックなら何でも扱います。

私は Mike Calligaro といいます。14 年間マイクロソフトの開発者として働いています。Windows® デスクトップ グループでの 2 週間の任務は別として、私の経歴はすべて、現在の Windows Mobile 事業部で費やされました。初期の双方向 TV、SEGA ドリームキャスト、および Windows CE オペレーティング システムを含む広範な製品に取り組んできました。この 8 年は、Windows Mobile のさまざまなパーツへの取り組みに費やしてきました。

今日は Windows Mobile デバイスをプログラムで構成 (言葉を飾ると "プロビジョニング") する方法についてお話します。ユーザーのために Windows Mobile デバイスを手動で構成しようとしている IT マネージャやサポート技術者の方は、ちょっと手を止めてお読みください。これはそのような方のための記事です。Web ページをブラウザのお気に入りに簡単に追加する方法を探しているモバイル アプリケーション開発者の方にも役に立ちます。

私がしばしポリエステル製のスーツを着て、VPN の設定、デバイスの日時の設定、電子メール アカウントの構成、デバイスへのファイルの追加、着信音の設定、レジストリ キーの変更などなど、さまざまなことが数行のコードと XML でできると言ったらどうしますか。さて、いくら払いますか。100 万ドルですか。200 万ドルですか。この MSDN® Magazine スペシャル オファーでは、ごく低価格 (無料) でこれらの機能を使用できるようにします。そうです。無料です。ですから今すぐ、このデジタルの魚をミキサーにかけている間にお電話ください。

できればセールスマンというより優れた開発者でありたいと思います。まじめな話をすると、Provisioning API は標準の Windows Mobile SDK に含まれ、Windows Mobile デベロッパー センター (msdn.microsoft.com/windowsmobile) から無料でダウンロードできます。

なぜプロビジョニングなのか

Windows Mobile という名前は後から付けられましたが、マイクロソフトは携帯電話用の最初のプラットフォームを 2002 年にリリースしました。電話の領域に入り込むということは、携帯電話会社が求めているが使い慣れていない多くの機能 (デバイスをリモートで構成する機能など) を実装することでした。現在、2002 年頃の一般的な電話では構成することはほとんどありませんが、競争に勝つためにはその基本的な構成レベルをサポートする必要があります。インフラストラクチャを整備したら、多くのリモート構成を行うのに便利であることがわかりました。

たとえば私は、仕事で必要に迫られて、いくつかの電話とさまざまな OS バージョンを使用しています。開発ライフサイクルのより不安定な時期には、1 日に複数回さまざまな電話を設定することが必要な場合がありました。仕事がスムーズに進んでいるときでも、1 週間に 1 台以上の電話を構成することは珍しくありません。そのため、手動で Exchange Server アドレスを入力して着信音を設定するのではなく、そうした設定事項の多くをまとめて実行する XML BLOB を 1 つ作成します。その XML ファイルと、それを処理する小さいプログラムをメモリ カードに保存します。このようにすることで、いつ新しい電話を渡されても試すことができます。

携帯電話会社の要件にはペニー アーケードをブラウザのお気に入りに設定できることは決して含まれていなかったと思うのですが、このときは "人はハンマーを持つと、すべてが釘に見える" という心理をうまく利用できました。

この構成の魔法をかけるために必要なものは、単一の API (DMProcessConfigXML) と XML だけです。たとえば、Live Search をブラウザのお気に入りに設定するとします。まず、次の XML を記述します。

// WAP provisioning XML to add a new browser favorite.
LPCWSTR g_wszFavoriteXml =
    L"<wap-provisioningdoc> "
    L"   <characteristic type=\"BrowserFavorite\"> "
    L"      <characteristic type=\"Live Search\"> "
    L"         <parm name=\"URL\" value=\"http://m.live.com/\"/> "
    L"      </characteristic> "
    L"   </characteristic>"
    L"</wap-provisioningdoc>";

この場合、この XML を使用してお気に入りを設定するために必要なすべてのコードは次のとおりです。

HRESULT AddFavorite() {
   HRESULT hr         = E_FAIL;
   LPWSTR wszOutput   = NULL;

   // Process the XML.
   hr = DMProcessConfigXML(g_wszFavoriteXml,
     CFGFLAG_PROCESS, &wszOutput);

   // The caller must delete the XML returned from DMProcessConfigXML.
   delete [] wszOutput;
   return hr;
 }

スペースの関係で、ここでは少し簡単に説明します。作成したコードでセキュリティとエラーのチェックを行う必要があります。XML をコードにコンパイルしている時間よりも、ファイルの XML を読んでいる時間の方が長いことでしょう。一言で言えば、それが開発者の仕事です。

DMProcessConfigXML のマネージ ラッパーもあります。C# では、代わりに ConfigurationManager.ProcessConfiguration を使用できます。同じコードを C# で記述すると次のようになります。

XmlDocument configDoc = new XmlDocument();
configDoc.LoadXml(
    "<wap-provisioningdoc>"+
    "<characteristic type=\"BrowserFavorite\">"+
    "<characteristic type=\"Live Search\">"+
    "<parm name=\"URL\" value=\"http://m.live.com/\"/>"+
    "</characteristic>"+
    "</characteristic>"+
    "</wap-provisioningdoc>"
    );
ConfigurationManager.ProcessConfiguration(configDoc, false);

構成サービス プロバイダ

DMProcessConfigXML は受け取った XML をさまざまな構成サービス プロバイダ (CSP) に送信します。これらのプロバイダは XML を読み取り、XML に従って動作します。変更したいある種の構成がサポートされていない場合、その構成の CSP がまだ作成されていない可能性があります。追加すると便利だと思われる新しい CSP について、ご意見をお寄せください。

最も確実な CSP のリストは msdn2.microsoft.com/bb737536.aspx にあります。また、msdn2.microsoft.com/bb737572.aspx に XML のサンプルも数多くあります。

残念ながら、CSP の可用性については、Windows Mobile Professional (Pocket PC) の電話と Windows Mobile Standard (Smartphone) の電話ではまだ差があります。一部の CSP は Standard では使用可能ですが Professional では使用できません (私たちはこの皮肉な状況を認識しています)。将来プラットフォームを統合するために準備を進めています。また、OEM および携帯電話会社は、変更したくない機能をロックダウンするために、CSP を選択して無効にすることができます。安全のため、サポートを考えている電話に配置予定のすべての構成を必ずテストする必要があります。

いくつかの例を挙げてみます。1 つ目は、タイム ゾーンを設定し、アラームをオンにして設定する XML の BLOB です。

<wap-provisioningdoc>
   <characteristic type="clock">
      <parm name="TimeZone" value="4"/>
      <parm name="AlarmOn" value="1"/>
      <parm name="AlarmTime" value="06:30:00Z"/>
   </characteristic>
</wap-provisioningdoc>

characteristic type は、Clock CSP を使用することを示しています。MSDN に各 CSP 用のページがあり、変更可能なパラメータと、各パラメータに指定できる値を説明しています。たとえば、Clock CSP のドキュメントは msdn2.microsoft.com/bb737261.aspx にあり、TimeZone、AlarmOn、および AlarmTime と、このサンプルにない 2 つのパラメータ (日付と時間) に指定できる値を説明しています。

必ずドキュメントを参照して、パラメータに割り当てる正しい値を確認してください。それらの値は、だれかにとっては意味があるのでしょうが、私のようなごく平凡な人間には、多くの場合意味がわかりません。たとえば、GMT -8 の TimeZone の値は 4 ですが、GMT -5 の値は 35 です。図を見てください。

図 1 は、Sync CSP で実行できることの一部のサンプルです (Sync プロバイダのドキュメントは msdn2.microsoft.com/bb737700.aspx にあります)。ご覧のように、どのデータを同期するか、どこから同期するか、同期の頻度などを選択できます。このプロバイダで構成できる同期関連のパラメータは 50 個ほどあります。

Figure 1 Sync CSP の使用 (Close Figure 1)
<wap-provisioningdoc>
   <characteristic type="Sync">
      <characteristic type="Connection">
         <parm name="Server" value="testserver"/>
         <parm name="User" value="testuser"/>
         <parm name="Domain" value="thephone-company"/>
         <parm name="AllowSSLOption" value="1"/>
      </characteristic>
      <characteristic type="Settings">
         <parm name="PeakStartTime" value="0800"/>
         <parm name="PeakEndTime" value="1800"/>
         <parm name="PeakFrequency" value="0"/>
         <parm name="BodyTruncation" value="5120"/>
      </characteristic>
      <characteristic type="Calendar">
         <parm name="Enabled" value="1"/>
      </characteristic>
      <characteristic type="Contacts">
         <parm name="Enabled" value="1"/>
      </characteristic>
      <characteristic type="Mail">
         <parm name="Enabled" value="1"/>
      </characteristic>
   </characteristic>
</wap-provisioningdoc>

これらのサンプルはほんのわずかですが、CSP の動作が理解できると思います。

XML をデバイスに送る

さて、すばらしい XML をまとめあげて、XML ファイルを開いてデータを DMProcessConfigXML に送信する小さいアプリケーションを作成しました。しかし、まだプロビジョニングは行いません。その前に XML とアプリケーションをデバイスに送ります。

ここでいくつかのオプションがあります。最も簡単なのは、ファイルとアプリケーションをメモリ カードに保存し、そのカードをデバイスに挿入し、ファイル エクスプローラを使用してアプリケーションを実行することです。私はこの方法でデバイスを構成します。

しかし、何百台ものデバイスを構成する必要がある IT マネージャはどうすればよいでしょうか。ファイル エクスプローラを起動してアプリケーションを呼び出す追加の手順は時間がかかりすぎます。その場合は、メモリ カードを挿入すると自動的に構成アプリケーションが実行されるようにできます。

メモリ カードのルートに 2577 という名前のフォルダ (理由は聞かないでください) を作成し、そのフォルダに構成アプリケーションを入れます。アプリケーションの名前を autorun.exe とします。アプリケーションが標準の XML ファイル名 (たとえば config.xml) を自動的に開く場合、カードを挿入するだけでデバイスを構成できます。この AutoRun の仕掛けは、デバイスの起動時にも動作するので、メモリ カードを挿入するために電池を外す必要がある電話でも機能します。

もちろん、他のオプションもあります。初期設定の後でさらにデバイスを構成する予定がある場合は、構成アプリケーションをデバイスに置いて拡張子を関連付けることができます。その後、関連付けられた拡張子を持つ構成ファイルを電子メールでユーザーに送信できます。ユーザーは、添付の構成ファイルを開くだけでデバイスを構成できます。または、ユーザー用の構成ファイルをダウンロードする Web サイトを参照することもできます。

また、PushRouter を使用して、SMS を介してワイヤレス アプリケーション プロトコル (WAP) 構成ファイルをデバイスに送信することもできます。PushRouter についてはこの記事では説明しませんが、携帯電話会社はこの方法で、自社のネットワーク上の電話に構成ファイルを送信します。

独立系ソフトウェア ベンダ (ISV) の方であれば、セットアップ プログラム内から CSP を使用して、ファイルの追加からレジストリ キーの変更、ブラウザのお気に入りの追加、ホーム ページの変更まで、必要なデバイス構成を行うことができます。構成の形式が単純なので、インストーラが実行したことを元に戻すアンインストーラ用に、対応する XML ファイルを用意することも簡単です。

ActiveSync® または Windows Mobile デバイス センターを使用してデバイスに接続する場合、デバイスにファイルを置かなくてもデバイスを構成できます。Windows Mobile SDK の tools ディレクトリにデスクトップ ツール rapiconfig.exe があります。このツールは、XML ファイルを読み取り、RAPI を使用して、現在 ActiveSync に接続されているデバイスを構成します。デバイスを接続したら、SDK の tools ディレクトリに移動し、(XML ファイルの名前を config.xml と仮定して) 次のように入力します。

rapiconfig config.xml

RapiConfig の詳細については、msdn2.microsoft.com/bb737541.aspx を参照してください。

最後に、CAB ファイルまたは CAB Provisioning Format (CPF) ファイルを作成できます。CAB と CPF はどちらもデバイスにコピーでき、プログラムなしで直接実行できます。どちらのファイルの種類もデスクトップ ツール makecab.exe を使用して作成します。このツールは Windows Mobile SDK にも含まれます。

CAB ファイルと CPF ファイルの主な違いは、CPF は実行時にダイアログ ボックスが表示されない (暗黙的に実行される) ことです。これは、CPF の主な欠点にもなります。デバイスのセキュリティ設定と、ファイルが署名されているかどうかに応じて、CAB と CPF は、デバイスをプロビジョニングしてもいいかどうかをユーザーに確認するダイアログ ボックスを表示することが必要な場合があります。CPF は暗黙的に実行する必要があるので、このダイアログ ボックスを表示できません。そのため、失敗します。最後に、構成ファイルに署名する予定がある場合は、CPF を使用してください。署名しない場合は、CAB を使用してください。

いずれの場合も作成方法はとても簡単です。通常の XML ファイルを makecab に渡して、出力ファイルに任意の名前を指定するだけです。1 つの落とし穴は、XML ファイルの名前を _setup.xml とする必要があることです。他の名前の場合、makecab は成功して CAB または CPF ファイルが正常に生成され、それを使用したときに暗号に関するエラーで失敗します。ドキュメントではこの点についてあまり明確に説明していません。

CAB ファイル作成の構文は次のとおりです。

makecab _setup.xml config.cab

XML ファイルとは異なり、生成される CAB ファイルの名前にそのような制限はありません。たとえば、config.cab の代わりに、生成される CAB ファイルに任意の名前を指定できます。CPF ファイル作成の構文は同様に次のとおりです。

makecab _setup.xml config.cpf

この場合も、config.cpf を、CPF ファイルに付ける任意の名前に置き換えてください。はい、そのとおりです。この 2 つの違いは、出力ファイルの名前を .cab にするか、.cpf にするかということだけです。そしてまた、いずれの場合も XML ファイルの名前は _setup.xml であることが必要です。

CPF ファイルの詳細については、msdn2.microsoft.com/bb737689.aspx を参照してください。CPF ファイルの署名の詳細については、msdn2.microsoft.com/bb416436.aspx を参照してください。

だれが XML を入力するか

経験豊富な IT プロフェッショナルであれば、XML を手動で入力してもかまいませんが、ユーザーが一連のオプションから自分の構成を選択できるようにするにはどうしたらよいでしょうか。多数のチェックボックスを使った Web サイトなどがあったらすばらしいのではないでしょうか。そのサイトをユーザーが参照し、必要なものを選択することで、XML ファイルが生成されます。

このシナリオでは、Microsoft® .NET Framework が XmlDocument クラスを提供していて、XML をプログラムで生成するのが簡単です。たとえば、前述の時計の XML を覚えているでしょうか。図 2 のコードは、実行中にその XML を作成します。

Figure 2 時計の構成の作成 (Close Figure 2)
void CreateXML()
{
    XmlDocument xmlDoc = new XmlDocument();
    XmlElement root = CreateRoot(xmlDoc);

    XmlElement type = CreateCharacteristic(xmlDoc, "clock", root);
    CreateParm(xmlDoc, "TimeZone", "4", type);
    CreateParm(xmlDoc, "AlarmOn", "1", type);
    CreateParm(xmlDoc, "AlarmTime", "06:30:00Z", type);

    xmlDoc.Save("config.xml");
}

XmlElement CreateRoot(XmlDocument xmlDoc)
{
    XmlElement root = xmlDoc.CreateElement("wap-provisioningdoc");
    xmlDoc.AppendChild(root);
    return root;
}

XmlElement CreateCharacteristic(XmlDocument xmlDoc, string type, XmlElement parent)
{
    XmlElement element = xmlDoc.CreateElement("characteristic");
    element.SetAttribute("type", type);
    parent.AppendChild(element);
    return element;
}

XmlElement CreateParm(XmlDocument xmlDoc, string name, string value, XmlElement parent)
{
    XmlElement element = xmlDoc.CreateElement("parm");
    element.SetAttribute("name", name);
    element.SetAttribute("value", value);
    parent.AppendChild(element);
    return element;
}

CreateRoot、CreateCharacteristic、および CreateParm はヘルパ関数であり、まとめて使用できます。次に、これらの関数を呼び出す独自の CreateXML を作成するだけです。このサンプルではパラメータ情報がハードコーディングされていますが、Web ページではこの値はユーザーが UI で選択した内容から取得されます。XML が生成されると、ユーザーが実行できるようにデバイスにダウンロードされます。

あるいは、デバイスで実行する構成アプリケーションを作成することもできます。ユーザーが適切な構成を選択した後、図 2 のようなコードで XML を生成して直接 ConfigurationManager.ProcessConfiguration に送信します。したがって、XML をファイルに送信してそれを読み取る手順は必要はありません。

すべてのプロビジョニングを終えて準備完了

電話の数字キーを使用して Exchange Server アドレスを入力したことがあるのなら、快適なデスクトップ キーボードに移動して XML を作成するべきです。だれもその苦労を経験する必要はありません。この記事で自由な気持ちになっていただけることを期待しています。プロビジョニングを楽しんでください。

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

Mike Calligaro は、マイクロソフトの Windows Mobile チームのシニア開発担当者、および Windows Mobile チームのブログ (blogs.msdn.com/windowsmobile) の投稿者です。


編集者のメモ: サービス指向の編集者のメモ

編集者のメモ
サービス指向の編集者のメモ
Howard Dierking

"サービス" という用語は、 人によって受け取り方が異なってきています。これまでソフトウェアは、主製品に付加されるサービスまたはサービスを要求するものとして位置づけられてきました。アプリケーション内に、ドメイン サービスと、外部に向けられるサービスがあります。ユーザー インターフェイスは "プレゼンテーション サービス"、データ アクセス コードは "データ サービス" と呼ばれます。サービス指向アーキテクチャを計画して実装することが、最近のアーキテクチャの傾向の 1 つです。

サービスと分散アプリケーションは関連していますが、等価ではありません。ほとんどのサービス、特に ASP.NET や Windows Communication Foundation (WCF) などのフレームワークを基に構築されたサービスは本来、ネットワークへの分散に役立つものです。一方、分散アプリケーションは、サービスで構成されている場合に最も理解および維持しやすくなります。とは言うものの、分散アプリケーションはスケーラビリティなどの問題を解決する技術的なソリューションであるのに対し、サービスはより意味的な性質を持ち、システムの複雑さを分解および管理する方法として存在します。

サービスの問題は、ビジネス ドメインの概念をテクノロジの成果物に変換するようなパラダイム シフトが必要に思われて、私が開発者として取り組み続けているものです。オブジェクト指向のヒューリスティックやデザイン パターンのようなトピックでの自分のスキルについて考え、向上させるために、多大なエネルギーを費やしてきました。この考え方の人々の間で一般に受け入れられているのは、リッチ ドメイン モデルの概念です。これは、ビジネスの言語に非常に密接につながったクラス モデルです。このモデルは、ユーザー インターフェイスやデータベースは必要なく個別にテスト可能で、ビジネス ドメインの問題に対処するために必要なすべての操作をカプセル化します。今日まで私はすべての開発の仕事を、ドメイン モデル クラス ライブラリとドメイン モデル テスト クラス ライブラリの 2 つのプロジェクトから始めています。

まず、仕事の中でサービスを有効にするために、ドメイン クラスをシリアル化属性で修飾し、それを ASP.NET または WCF サービスを介した通信で送信しました。そこで、思わぬ副作用が生まれることに気付きました。ドメイン オブジェクトのパブリック データ インターフェイスがコントラクトの裏にあるロジックについて暗黙の想定をしているのです。そしてこのことがはっきりとわかるのは、生成されたサービス プロキシに基づいてテストしたときだけでした。その結果、私はアプローチを変更し、サービスの境界を越えてデータを渡すためにデータ コンテナ オブジェクトを使用しました。このオブジェクトはデータ転送オブジェクト (DTO) とも呼ばれます。この方法は、ドメイン層のセマンティクスとアプリケーションの作成方法をあまり変更する必要もなく、しばらくの間かなりうまくいきました。

そこへ Windows Workflow Foundation が登場しました。同じころ、私は Office Business Application (OBA) 開発に手を出し始めました。その平衡状態に、データ ウェアハウスとその他のいくつかのテクノロジを追加すると、今日の状況に至ります。これをフレームワークとして、私は開発業務の文脈でサービスを定義しようとしています。現在、ワークフロー インスタンスとドメイン クラスとの間でロジックがどのように考慮されているか、ワークフロー サービスをテスト駆動方式でどのように単体テストすることができるか、といった問題に取り組んでいます。

ここで私自身の経験を述べているのは、サービスがアプリケーションとプロセスにどのように適合するかを理解するために多くの人が同じような経験をしているだろうと思うからです。仕事でのサービス指向の課題への取り組み方について考えのある方はご意見をお送りください。

最後に、MSDN Magazine に参加してくれた Mike Calligaro と Windows Mobile チームを歓迎し、移動性のすべてのものに焦点を当てる彼らの新しいコラム "いろいろな場所へ" を紹介します。システムは次々とサービス対応になっているので、システムは従来のネットワーク境界をはるかに越えていくことができます。Mike と彼のチームは、あらゆるプラットフォームでその能力を最大限に利用する方法を教えてくれます。

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Tim Cahill、Barak Cohen、Alex DeJarnatt、Steve Fox、Matt Gibbs、Scott Hunter、Albert Kooiman、Steve Maine、Faisal Mohamood、Laurence Moroney、Mark Rideout、Stefan Schackow、Don Smith、Ed Triou、および Robert Wlodarczyk です。


ツールボックス: Web アプリケーション エラーのログ記録、LINQ について、その他

ツールボックス
Web アプリケーション エラーのログ記録、LINQ について、その他
Scott Mitchell

コンテンツ
Web アプリケーション エラーをログ記録する/確認する
注目のブログ
推薦図書

Web アプリケーション エラーをログ記録する/確認する

理想の世界では、運用環境でエラーが発生することはないでしょう。データベースや Web サーバーは決してダウンしません。コードは、エラーを引き起こすようなあらゆる状況または入力を正常に処理するように、完璧にテストおよび設計されていることでしょう。実際にはそうはいきません。プログラミングの現場は現実の世界です。常に納期が存在し、機能の要求に終わりはありません、さらに、データベースや Web サーバーがオフライン状態に陥ることもあります。運用環境でエラーが発生した場合は、その詳細をログに記録し、開発チームに迅速に通知することが絶対的に求められます。

ASP.NET アプリケーションのエラーをログ記録するための一般的なソリューションとして、ASP.NET の状態監視システムと Atif Aziz のエラー ログのモジュールとハンドラ (ELMAH: Error Logging Modules and Handlers) の 2 つがあります。どちらのシステムを使用する場合でも、ページ開発者は、指定したログ ソースにエラーの詳細を記録するようにプログラミングできます。加えて、どちらのシステムも、ハンドルされていない例外を自動的にログに記録するためのメカニズムを備えています。

マイクロソフトの状態監視システムは、ASP.NET 2.0 で採用された堅牢で低レベルのフレームワークです。エラーの監視に加えて、アプリケーション ライフサイクル イベント (アプリケーションの起動、シャットダウンなど)、要求レベル イベント、およびセキュリティ関連のイベント (承認違反、成功および失敗したログイン試行など) を含むさまざまなメトリックスを記録することができます。

監視対象のイベントが発生した場合、そのイベントを任意の数の指定されたログ ソースに記録することができます。状態監視システムのログ機能は、プロバイダ モデルの上に構築されているので、開発者は、カスタム ログ プロバイダをプラグインとして利用したり、任意の組み込みプロバイダを使用したりできます。組み込みプロバイダの中には、イベントの詳細を Windows® イベント ログに記録するもの、イベントを SQL Server データベースに書き込むもの、詳細を電子メール アドレス宛てに送信するものがあります。

状態監視システムの使用を開始するには、適切な構成マークアップをアプリケーションの web.config ファイルに追加します。状態監視構成情報には、監視対象のイベント、ログ プロバイダ、および各監視対象イベントの記録方法が含まれている必要があります。

この基本情報に加え、構成マークアップは、ログをバッファ処理するかどうか、ログを開始するまでに発生する必要があるイベントの回数、特定のイベントを記録する最大回数 (それ以降のインスタンスは無視されます) などのオプションを含むことができます。

もう 1 つの一般的なログ オプションである ELMAH は、オープン ソースの HTTP ハンドラとモジュールのセットです。ASP.NET 状態監視システムがさまざまなイベントを監視できるのに対し、ELMAH は、アプリケーション エラーのみを記録するように設計されています。エラーが発生すると、ELMAH は、指定されたログ プロバイダに詳細を記録し、オプションで電子メールを構成可能な受信者のセット宛てに送信します。

ELMAH には、組み込みのログ プロバイダのほか、コミュニティによって作成されたログ プロバイダが多数あります。このようなログ プロバイダには、XML ファイル、SQL Server® データベース、Oracle データベース、および SQLite データベース (軽量のオープン ソース データベース エンジン) を記録先とするものがあります。

ELMAH は、いくつかの興味深い表示機能を備え、ASP.NET のすべてのバージョンをサポートしています。ASP.NET 状態監視システムがログに記録されたイベントを表示するための機能を備えていないのに対し、ELMAH には、エラー ログを Web ページから表示したり RSS フィードとして表示したりできる組み込み HTTP ハンドラが含まれています。また、状態監視システムが ASP.NET 2.0 以降のみに対応するのに対し、ELMAH は ASP.NET 1.0 以降に対応します。

私は、2006 年 8 月のツールボックスで初めて ELMAH を記事にしました (msdn.microsoft.com/msdnmag/issues/06/08/Toolbox を参照)。その後、Atif は、ELMAH を更新して、いくつかの新機能の追加およびパフォーマンス強化 (たとえば例外フィルタ) を行いました。

既に説明したように、ELMAH は既定ですべてのエラーを指定されたログ プロバイダに記録します。ただし、現在では、例外フィルタを使用することで、エラーを破棄する (したがってログに記録しない) 状況を指定できます。さらに、ELMAH の最新バージョンでは、エラー ログ全体を .csv ファイルとしてダウンロードできるようになり、サイズが大きなエラー ログのパフォーマンスも向上しています。

状態監視システムと ELMAH は、根本的には、ASP.NET アプリケーションのエラーを記録するための同じ機能を提供します。異なる種類のイベントを監視できるという理由で状態監視システムを好む開発者もいます。また、組み込みのエラー ログ表示機能や ASP.NET バージョン 1.x アプリケーションのサポートがあるという理由で ELMAH を好む開発者もいます。長い目で見れば、どちらのログ記録プラットフォームを選ぶかは重要ではありません。大切なのは、精密なエラー ログ システムを適切に配置して、避けられないアプリケーション エラーの詳細をすべて記録することです。

価格 : 無料

状態監視システム : go.microsoft.com/fwlink/?LinkId=109807

ELMAH: go.microsoft.com/fwlink/?LinkId=109809

Error Log in ELMAH
Error Log in ELMAH (画像を縮小するには、ここをクリックします)
Error Log in ELMAH
Error Log in ELMAH (画像を拡大するには、ここをクリックします)

注目のブログ

最近の同僚との会話の中で、Jeff Atwood 氏のブログ「Coding Horror」にあった投稿について話題にしました。同僚はこのブログを知らず、どのような内容が書かれているブログなのか私にたずねてきました。私は、"コンピュータとプログラミング関連" について書かれているという答えを返すことしかできませんでした。

「Coding Horror」で扱われている内容は多岐にわたっているので、どのようなサイトであるかを説明するのは困難です。このブログには、非常に技術的な記事 (主に ASP.NET および Web 関連のテクノロジ) のほか、優秀なプログラマになるための提案や優れたプロジェクト管理上の決定を行うための提案が書かれています。さらに、各ブランドのマザーボード、マウス、モニタを比較して、コンピュータ ハードウェアを評価している記事もあります。消費者向けガジェットのユーザビリティやデザイン上の欠点、ダイエットやエクササイズのマニア向けの話題、ブログの将来、EULA の問題、DONKEY.BAS の歴史などの記事も登場します。

Jeff 氏は、間違いなく、業界のいくつかの重要な問題の核心に迫っています。ソフトウェアに関する高等教育の現状に対する彼の見解を読んでください。Jeff 氏が指摘しているように、Visual Studio® を使用しているときにどこをクリックするか、といったことから、チューリング マシンの理論まで、ソフトウェアの学習内容は多岐にわたります。では、ソフトウェアを作成する場合の最善のアプローチとは何でしょうか。その答えはここには書きません。ぜひ、Jeff 氏のブログを読んで、答えを見つけてください。

彼のブログは人気があります。75,000 人を超える読者が RSS フィードを購読しているので、山ほどのコメントが寄せられています。加えて、Jeff 氏は他の有名ブログ作成者へのリンクも提供しているので、意見をサンプリングする場所として非常に有用です。唯一のマイナス面は、大量のコンテンツ内を探索するためのツールがないことです。検索ボックスはありますが、カテゴリ別に記事が分類されてはいません。

www.CodingHorror.com

Jeff Atwood's Blog, Coding Horror
Jeff Atwood's Blog, Coding Horror (画像を縮小するには、ここをクリックします)
Jeff Atwood's Blog, Coding Horror
Jeff Atwood's Blog, Coding Horror (画像を拡大するには、ここをクリックします)

推薦図書

Microsoft® .NET Framework 3.5 における最も有用な技術革新の 1 つは LINQ です。簡単に説明すると、開発者は LINQ を使用することで、T-SQL の構文を忠実に反映しながら、任意のサポートされているデータ ソース (列挙可能なコレクション、XML ドキュメント、SQL Server データベースなど) を直接クエリするための C# コードおよび Visual Basic コードを記述できます。たとえば、次の C# コードは、ここに定義されている配列に偶数を出力します。

int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var evens = from p in numbers
            where p % 2 == 0
            select p;

foreach (var e in evens)
    Console.WriteLine(e);

新しい言語構文と LINQ の豊富な機能および複雑さゆえに、私は、オンラインの記事やチュートリアルを探索するのに時間を費やすよりも、本を読んで LINQ について学習する方法を選びました。私が最初に選んだ LINQ 関連の本は、Joseph C. Rattz Jr. 氏によって書かれた『Pro LINQ in C# 2008』でした。

この本は、わかりやすく好奇心をそそられる LINQ の紹介から始まります。この部分では、コレクション、XML ファイル、および SQL Server データベースをクエリするための LINQ の使用法が解説されています。次に、LINQ を可能にしたラムダ式、匿名型、オブジェクト初期化、拡張メソッド、自動プロパティなどの新しい言語機能が簡単に説明されています。

この本の大部分は、LINQ to Objects、LINQ to XML、LINQ to DataSets、および LINQ to SQL を説明する 4 つのセクションに分けられます。各セクションでは、LINQ 構文と、LINQ を使ってこれらの異なるデータ ストアをクエリできるようにする各種のクラスおよびメソッドがていねいに解説されています。LINQ to Objects に関するセクションは、標準クエリ演算子のテクニカル リファレンスとして使用できます。演算子が列挙され、簡単な説明とプログラミング例が示されています。後続のセクションには、有用なヒント、よくある落とし穴、現実により即した例など、LINQ についてさらに有益な情報が含まれています。

最後の LINQ to SQL に関するセクションは、大多数の開発者にとっておそらく最も興味深く有用な章です。当然ながら、内容の 1/3 は LINQ to SQL の説明に費やされ、オブジェクト リレーショナル デザイナと SQLMetal.exe コマンド ライン ツールを使用して基底の SQL エンティティ クラスを作成するための手順が解説されています。これらのクラスを DataContext クラスと共に使用して、データのクエリ、更新、挿入、および削除を行う例も紹介されています。加えて、同時実行制御やトランザクションなどの、より複雑なデータベースの問題を扱う章もあります。

価格 : 44.99 ドル

www.apress.com

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

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


CLR 徹底解剖: パフォーマンスの早期および頻繁な測定 (第 1 部)

CLR 徹底解剖
パフォーマンスの早期および頻繁な測定 (第 1 部)
Vance Morrison

コンテンツ
計画の作成
プロファイル データ処理の例
早期および頻繁な測定
測定の計画
パフォーマンスの結果の検証
デバッガによるミクロベンチマーク データの検証
まとめ

Microsoft® .NET Framework 共通言語ランタイム チームのパフォーマンス アーキテクトとしての著者の職務は、高パフォーマンスのアプリケーションを記述するためにランタイムを最大限に活用できるようユーザーを支援することです。実際、このことには何の不思議もありません。ユーザーが行う必要があるのは、.NET あるいはその他の方法で、最初からパフォーマンスを考慮してアプリケーションを設計することだけです。非常に多くのアプリケーションが、パフォーマンスをほぼ考慮せずに作成されています。ほとんどのプログラムは実行する計算が比較的少なく、プログラムを操作する人間より計算がはるかに高速であるため、このことは、多くの場合、問題になることはありません。残念ながら、高パフォーマンスが必要な場合に、適切な作業を行うための知識、スキルおよびツールがないというだけなのです。

ここでは、高パフォーマンス アプリケーションを記述するために必要であることについて説明します。これは汎用的な概念ですが、ここでは .NET 用に記述されたプログラムに重点を置いて説明します。.NET では通常の C++ コンパイラよりも基盤となるマシンを抽象化しており、リフレクション、カスタム属性、正規表現などの強力で負荷の大きい機能を提供するため、パフォーマンスにとって重要なコード パスにコストの高い操作を無意識のうちに挿入してしまうことが簡単に起こります。これらのコストを回避できるように、さまざまな .NET 機能のコストを定量化し、どのような場合にこれらの機能を適切に使用できるかについて説明します。

計画の作成

既に述べたように、ほとんどのプログラムはパフォーマンスを十分考慮せずに記述されていますが、すべてのプロジェクトにパフォーマンス計画を作成する必要があります。さまざまなユーザー シナリオを考慮し、優れたパフォーマンス、適切なパフォーマンス、および不適切なパフォーマンスを明確にする必要があります。次に、データ ボリューム、アルゴリズムの複雑性、および以前に同様のアプリケーションを作成した経験に基づいて、定義したパフォーマンスの目標を簡単に満たすことができるかどうかを決定する必要があります。多くの GUI アプリケーションでは、パフォーマンスの目標は控えめであるため、特別な設計がなくても、適切なパフォーマンスを容易に達成することができます。この場合、パフォーマンスの計画はこれで完了です。

パフォーマンスの目標を簡単に満たすことができるかどうかわからない場合、ボトルネックになる可能性のある領域の一覧を作成し、計画を書くことから始める必要があります。よく問題となる領域には、起動時間、一括データ操作、およびグラフィック アニメーションがあります。

プロファイル データ処理の例

例を使ってさらに具体的に示します。現在、著者はプロファイル データを処理するための .NET インフラストラクチャを設計しています。OS が生成するイベント (ページ フォールト、ディスク I/O、コンテキスト スイッチなど) の一覧を意味のある方法で示さなければなりません。関連するデータ ファイルは、大きい傾向があります。小さいプロファイルは 10 MB 程度であり、ファイル サイズが 1 GB をはるかに上回ることも珍しくありません。

パフォーマンス計画の作成に取り組んでいると、表示の描画に必要なデータセットの部分のみを計算する場合、つまり、表示が "手抜き" である場合、データの表示は問題にならないという結論が得られました。残念ながら、ツリー コントロール、リスト コントロール、テキスト ボックスなどの GUI オブジェクトで手抜きを行うには余分な作業が必要になります。これが、ファイル サイズが大き過ぎる場合 (たとえば、100 MB)、ほとんどのテキスト エディタのパフォーマンスが許容できないほど低下する理由です。パフォーマンスを考慮することなく GUI を設計した場合、ほとんど間違いなく使用に耐えないものとなるでしょう。

ただし、手抜きはファイル内のすべてのデータを使用する必要のある操作 (合計を計算する場合など) では通用しません。データセットのサイズを考慮すると、データのディスパッチ メソッドおよび処理メソッドが、慎重に設計される必要のある "ホット" なコード パスです。プログラムのその他のほとんどは、パフォーマンスにとって重要であることはほとんどなく、特に注目する必要がありません。

次は、一般的な経験です。高パフォーマンス シナリオでも、95% のアプリケーションにはパフォーマンス計画は必要ありませんが、パフォーマンス計画の必要がある最後の 5% のアプリケーションを慎重に特定する必要があります。また、著者のケースのように、その 5% に関連する可能性のあるプログラムの部分を特定することは、通常ごく簡単です。

早期および頻繁な測定

高パフォーマンス設計の次のステップは、測定です。コードを記述する前に、パフォーマンスの目標が達成可能であるかどうかを把握し、可能である場合、設計にどのような制約が課されるかを把握します。著者のケースでは、生ファイルの入出力およびデータベース アクセスなど、設計で考慮している基本操作のコストを把握する必要があります。前に進むには、いくつかの数値が必要です。これは、プロジェクトの設計において最も重要な段階です。

残念ながら、ほとんどのパフォーマンスは開発プロセスのごく初期の段階で失われてしまいます。プログラムの中核をなすデータ構造を選択するまでに、アプリケーションのパフォーマンス プロファイルは変更の余地がなくなります。アルゴリズムを選択すると、パフォーマンスはさらに制限されます。さまざまなサブコンポーネント間のインターフェイス コントラクトを選択すると、パフォーマンスがさらに制約を受けます。このような初期設計における決定のそれぞれのコストを理解し、賢明な決定を下すことが重要です。

設計は反復的なプロセスです。まずは、最もわかりやすく、最も単純で、最も明確な選択を行い、設計のスケッチを作り (実際には、ホットなコードのプロトタイプを推奨します)、そのパフォーマンスを評価することをお勧めします。また、パフォーマンスを唯一の要因とする場合、設計がどのようになるかを考え、次にアプリケーションの速度を見積もる必要もあります。ここからが、楽しいエンジニアリングの始まりです。設計を試行錯誤し、最良の結果を得られる設計を追求するため、2 つの両極端の間にある手段について検討します。

ここでも、プロファイル データ プロセッサに関する著者の経験が役立ちます。ほとんどのプロジェクトと同様に、データ表現に関する私の選択は重要でした。データをメモリ内に格納する必要があるでしょうか。データをファイル内でストリームする必要があるでしょうか。データをデータベースに格納する必要があるでしょうか。標準的なソリューションでは、任意の大きなデータセットをデータベースに格納する必要があります。しかし、データベースの最適化では、頻繁に変更される大容量のデータではなく、比較的変更頻度の低いデータが想定されています。著者のアプリケーションは、データベースに何ギガバイトものデータを日常的にダンプします。データベースはこれに対応できるでしょうか。データベース操作のちょっとした測定および分析により、必要とするパフォーマンス プロファイルがデータベースにないことが簡単に確認できました。

過度のページ フォールトを発生させる前にアプリケーションがどの程度のメモリを使用することができるかについて数回測定した後、メモリ内ソリューションを除外しました。その結果、基本のデータ表現にはファイルからのデータのストリーミングが残りました。

しかし、依然として多くの他の設計上の決定を行わなければなりませんでした。プロファイル データの基本形式は、異種のイベントのリストです。しかし、イベントはどのようになるのでしょうか。文字列 (適切な一貫性がある) でしょうか。C# の構造体またはオブジェクトでしょうか。

オブジェクトである場合、明確な解決策は、イベントごとに 1 つの割り当てを作成することです。この場合、割り当てが多くなります。これを受け入れることはできるでしょうか。イベントを何度も繰り返すときは、ディスパッチはどの程度厳密に動作するでしょうか。コールバック モデルまたは反復モデルでしょうか。ディスパッチはインターフェイス、デリゲート、またはリフレクションを介して動作するでしょうか。設計上の決定事項は数多くあり、これらはすべてプログラムの最終的なパフォーマンスに影響します。そのため、トレードオフを理解するために測定を行う必要がありました。

測定の計画

設計の際は、多くの測定を行うのは明らかです。では、正確にはどのように実行するのでしょうか。役に立つプロファイリング ツールは数多くありますが、最も単純でよく利用される汎用的な技法はミクロベンチマーキングです。この技法は単純です。特定の操作のコストを知りたい場合、使用例を設定し、その操作にかかる時間を直接測定するだけで済みます。

.NET Framework には、この目的専用に設計された System.Diagnostics.Stopwatch と呼ばれる高精度のタイマがあります。精度はハードウェアによって異なりますが、通常 1 マイクロセカンド未満という十分な精度があります。これは .NET Framework に付属しているため、必要な機能は既に揃っています。

Stopwatch がうまく開始したら、適切なベンチマーク ハーネスでさらに操作を行う必要があります。正確に測定を行うため、小さな操作をループ内に配置して、十分な時間間隔を作り出します。ジャスト イン タイム (JIT) コンパイルおよびその他の 1 回の初期化が完了するように、測定を行う前にベンチマークを一度実行する必要があります (もちろん、目標がその初期化の測定でない限りです)。測定にはぶれがあるため、ベンチマークを複数回実行し、測定の安定性を特定するために統計情報を収集する必要があります。また、多くのベンチマーク (設計のバリエーション) を一括で実行し、比較用にすべての結果を表示するレポートを取得することも簡単です。

著者は MeasureIt.exe というベンチマーク ハーネスを記述しました。これは、Stopwatch クラスを基に構成されており、これらの目標に対応します。これは、MSDN® Magazine Web サイトのコラムのコード ダウンロードで入手できます。展開後、実行するには次のように入力するだけで済みます。

MeasureIt

数秒で、50 を超える標準ベンチマークのセットが実行され、Web ページに結果が表示されます。データの抜粋を図 1 に示します。これらの結果では、測定ごとに操作を 10,000 回実行します (1,000 回実行されるループで操作が 10 回クローンされます)。次に、各測定が 10 回実行され、標準の統計情報 (最小値、最大値、中央値、平均値、標準偏差) が計算されます。

Figure 1 MeasureIt.exe を使用したさまざまな操作の測定 (Close Figure 1)

測定される操作 | 中央 | 平均 | 標準偏差 | 最小 | 最大 | サンプル
MethodCalls: EmptyStaticFunction() [count=1000 scale=10.0] | 1.000 | 1.005 | 0.084 | 0.922 | 1.136 | 10
MethodCalls: aClass.Interface() [count=1000 scale=10.0] | 1.699 | 1.769 | 0.090 | 1.696 | 1.943 | 10
ObjectOps: new Class() [count=1000 scale=10.0] | 6.248 | 8.040 | 3.556 | 5.087 | 16.296 | 10
Arrays: aIntArray[i] = 1 [count=1000 scale=10.0] | 0.616 | 0.638 | 0.071 | 0.612 | 0.850 | 10
Delegates: aInstanceDelegate() [count=1000 scale=10.0] | 1.233 | 1.244 | 0.088 | 1.160 | 1.398 | 10
PInvoke: FullTrustCall() [count=1000] | 7.452 | 6.946 | 0.804 | 5.878 | 7.913 | 10
Locks: Monitor lock [count=1000] | 11.487 | 12.129 | 0.901 | 11.322 | 13.843 | 10

時間測定をより意味あるものにするために、空の静的な関数からの呼び出し (および返り) の中央値の時間が 1 つの単位となるように正規化されます。ベンチマークでは、時間が大きく異なることは珍しいことではありません。このため、すべての統計情報が重要になります。FinalizableClass ベンチマークでは、最小時間 (71.299) と最大時間 (953.864) に大きな違いがあることに注目してください。ベンチマークのデータを信頼できるようにするために、このばらつきについて説明する必要があります。特定の場合、このばらつきは、ブックキーピングのデータ構造を一括で割り当てるためにより遅いコード パスを定期的に実行するランタイムの結果です。既に、これらの統計情報を有効にすることは、データの検証に役立つことが立証されつつあります。

次の表は、便利なパフォーマンス データの宝庫であり、.NET を対象とするコードで使用されるほとんどの基本操作のコストの詳細を示しています。次回のこのコラムで詳細を説明しますが、ここでは MeasureIt の重要な機能について説明します。MeasureIt は、独自のソース コードを備えています。MeasureIt のソース コードを展開し、Visual Studio® を起動するには (Visual Studio が使用可能な場合)、次のように入力します。

MeasureIt /edit

ソースがあるということは、ベンチマークが測定する内容を正確にすばやく理解できるということです。また、新しいベンチマークをスイートに簡単に追加できるということでもあります。

ここでも、プロファイル データ プロセッサに関する著者の経験が役立ちました。設計のある時点で、C# イベント、デリゲート、仮想メソッド、またはインタフェースを使用する特定の一般的な操作を実行できます。決定を行うには、これらの選択を行った場合それぞれどのようなパフォーマンスのトレードオフがあるかを理解する必要があります。著者はそれぞれの手段のパフォーマンスを測定するミクロベンチマークを数分で記述しました。図 2 に関連のある行が表示されているため、これらの手段間でたいした違いのないことがわかります。この情報から、著者は最も適切な方法を選択することができ、その方法を選択することでパフォーマンスを犠牲にしていないことを知ることができます。

Figure 2 .NET イベント、デリゲート、インターフェイス、および仮想メソッドの測定 (Close Figure 2)

測定される操作 | 中央 | 平均 | 標準偏差 | 最小 | 最大 | サンプル
MethodCalls: aClass.Interface() [count=1000 scale=10.0] | 1.651 | 1.660 | 0.084 | 1.579 | 1.814 | 10
MethodCalls: aClass.VirtualMethod() [count=1000 scale=10.0] | 1.228 | 1.175 | 0.077 | 1.083 | 1.277 | 10
Delegates: aInstanceDelegate() [count=1000 scale=10.0] | 1.151 | 1.159 | 0.085 | 1.075 | 1.314 | 10
Event: Fire Events [count=1000 scale=10.0] | 1.228 | 1.195 | 0.070 | 1.088 | 1.291 | 10

パフォーマンスの結果の検証

MeasureIt アプリケーションを使用すると、幅広いベンチマーク用のデータ収集が非常に容易になります。残念ながら、MeasureIt はベンチマーク データを利用するという重要な側面に対応していません。つまり、データを検証することができません。意図と異なるものを測定してしまうことは非常によくあることです。その結果は、誤りに過ぎず、無駄よりなお悪いデータです。"話がうま過ぎる (悪すぎる) と思ったときはおそらくそのとおりである" という古いことわざは、パフォーマンス データにまさに当てはまります。重要な設計の決定で使用するデータを検証することは、必要不可欠です。

デバッガによるミクロベンチマーク データの検証

パフォーマンスの結果の検証とは何でしょうか。これは、パフォーマンス結果を予測する他の情報を収集し、2 つの手法が一致するかどうかを確認するということです。ごく小さなミクロベンチマークの場合、機械語命令を調べ、実行された命令数に基づいて見積もりを作成することが、すばらしいチェックになります。Visual Studio のようなデバッガでは、ベンチマーク コードにブレークポイントを設定し、逆アセンブリ ウィンドウ ([デバッグ]、[ウィンドウ]、[逆アセンブル] の順にクリックします) に切り替えるのと同じように簡単です。残念ながら、Visual Studio の既定のオプションは、デバッグを簡易化するように設計されており、パフォーマンス調査を行うようには設計されていません。そのため、パフォーマンス調査を機能させるためには、2 つのオプションを変更する必要があります。

まず、[ツール] メニューの [オプション...]、[デバッグ]、[一般] で、[JIT 最適化を抑制する] チェックボックスをオフにします。このチェックボックスは、既定ではオンになっています。つまり、最適化する必要のあるコードをデバッグするときでさえ、デバッガはランタイムに最適化を行わないよう指示します。デバッガのこの動作は、最適化がローカル変数の調査に干渉しないようにするためのものですが、実際に実行されるコードを確認できないという意味でもあります。デバッガは調査のみに集中し、デバッグ対象のプログラムを変更すべきでないと著者は強く確信しているため、このオプションを必ずオフにします。なお、このオプションの設定を解除してもデバッグ用にコンパイルされたコードに影響はありません。これは、ランタイムがそのコードをどのような方法でも最適化しないためです。

次に、[ツール] メニューの [オプション...]、[デバッグ]、[一般] ダイアログにある ['マイ コードのみ' 設定を有効にする] チェックボックスをオフにします。マイ コードのみ機能は、ユーザーによって記述されていないコードを表示しないようデバッガに指示します。一般的に、この機能は、呼び出しフレームの乱雑さを解消しますが、多くの場合、アプリケーション開発者はこのことには関心を払いません。ただし、この機能は最適化されるコードがユーザーのコードではないことを想定しています (ユーザーのコードは、デバッグ構成または抑制された JIT 最適化がオンである機能を使用してコンパイルされることを想定しています)。JIT 最適化を有効にし、マイ コードのみをオンにした場合、ブレークポイントにヒットしないことがわかります。これは、デバッガにおいてコードがユーザーが記述したものでないと想定されているためです。

これらのオプションのチェックをオフにすると、すべてのプロジェクトに対してオフのままになります。通常、これは効果的ですが、マイ コードのみ機能を使用しないわけではありません。デバッグからパフォーマンス評価のプロセスを進んだり、戻ったりする間に、マイ コードのみのオンとオフを切り替える場合があります。

パフォーマンスの結果を検証するためにデバッガを使用できる例としては、図 3 の抜粋に示すように、データの異常を調査することができます。このデータでは、C# 構造体のインターフェイス メソッドへの呼び出しは、静的メソッドへの呼び出しより数倍迅速であることを示しています。静的メソッドの呼び出しが最も効率的なタイプの呼び出しであることを予期している場合、もちろんこれは奇妙です。この点を調査するには、このベンチマークにブレークポイントを設定し、アプリケーションを実行します。逆アセンブル ウィンドウ ([デバッグ]、[ウィンドウ]、[逆アセンブル]) に切り替え、次のコードだけでベンチマーク全体が構成されていることを確認します。

Figure 3 パフォーマンスの結果の検証のためのデバッガの使用 (Close Figure 3)

測定される操作 | 中央 | 平均 | 標準偏差 | 最小 | 最大 | サンプル
MethodCalls: EmptyStaticFunction() [count=1000 scale=10.0] | 1.000 | 0.964 | 0.102 | 0.857 | 1.196 | 10
MethodCalls: aStructWithInterface.Interface() [count=1000 scale=10.0] | 0.031 | 0.029 | 0.012 | 0.021 | 0.039 | 10

aStructWithInterface.InterfaceMethod();
00000000  ret

このコードが示すのは、ベンチマーク (インターフェイス メソッドへの 10 個の呼び出し) が何も行わないようインラインになっているということです。ret 命令は、実際はベンチマーク全体を定義するデリゲート本体の末尾にあります。もちろん、何も操作を行わないことがメソッド呼び出しの実行より高速であるのは驚くことではなく、これが異常の理由を示しています。

唯一の謎は、静的メソッドもインラインになっていない理由です。これは、静的メソッドに対して、著者が特別に MethodImplOptions.NoInlining 属性を使用してわざわざインラインを抑制したためです。筆者は、JIT コンパイラが特定のインターフェイス呼び出しを仮想ではない呼び出しと同じくらい効率的に作成できることを示すために、このインターフェイス呼び出しのベンチマークにこれを配置することをわざと "忘れ" ました (ベンチマークの上にこの説明のコメントがあります)。

まとめ

繰り返しになりますが、特に JIT コンパイラの最適化の対象である小さな内容を測定する場合、意図と異なるものを測定してしまうことは非常によくあることです。また、最適化されないコードをはからずも測定したり、メソッド自体でなくメソッドの JIT コンパイルのコストを測定してしまうことも非常に簡単に起こります。MeasureIt /usersGuide コマンドでは、ユーザーズ ガイドが表示されます。これは、ベンチマークの作成時に発生する可能性のある多くの落とし穴について説明しています。独自のベンチマークを作成する準備が整ったら、これらの詳細をお読みになることを強くお勧めします。

著者が強調する点は、検証の次のような概念です。自分のデータについて説明できない場合は、設計上の決定を行うためにそのデータを使用するべきではありません。一般的でないデータがある場合、理想的にはさらにデータを収集してベンチマークをデバッグするか、データを説明できるようになるまで、より専門的な知識を持つ他のユーザーの協力を得る必要があります。説明できないデータは非常に疑わしいため、重要な決定を行う上で使用すべきではありません。

このコラムは、高パフォーマンス アプリケーションを作成するための基本についての説明です。ソフトウェアの他の属性と同様に、最初からパフォーマンスを考慮して製品を設計する必要があります。これを実現するには、さまざまな設計上の決定を行った場合のトレードオフを定量化する測定が必要です。つまり、パフォーマンスの実験を行うということです。MeasureIt は高品質のミクロベンチマークを簡単にすばやく生成することができます。そのため、設計プロセスにとって欠かせないパーツになるでしょう。また、MeasureIt は、.NET Framework の基本操作のほとんどに対応するベンチマークのセットを備えているため、すぐに使用できて便利です。

また、.NET Framework の一部に対して独自のベンチマークを簡単に追加することもできます。これはたいへん興味深いことでしょう。このデータを使用すると、アプリケーション コードを記述する前に、アプリケーション コストのモデルを作成し、各設計手段のパフォーマンスに関して妥当な (大まかな) 推定を行うことができます。

.NET のアプリケーションのパフォーマンスについては、説明することがまだたくさんあります。ミクロベンチマークの作成に関連する潜在的な落とし穴があります。そのため、コードを記述する前に MeasureIt ユーザーズ ガイドをお読みください。また、ディスク I/O、メモリ、またはロックの競合が大きなボトルネックとなる状況については次回に説明します。さまざまなプロファイリング ツールを使用して、設計後のアプリケーションのパフォーマンスの状態を検証および監視する方法についても説明していません。

知るべきことがたくさんあり、また、多くの場合、情報の多さから、開発者はこのようなテストを行う意欲をなくしてしまいます。ただし、ほとんどのパフォーマンスはアプリケーションの設計段階で失われるため、他に何の対策も行わない場合は、初期段階でパフォーマンスを考慮する必要があります。このコラムにより、皆さんの次の .NET ソフトウェア プロジェクトの設計においてパフォーマンスがより明確になることを願っています。

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

Vance Morrison は、マイクロソフトの CLR チームのコンパイラ アーキテクトを務めています。.NET の設計に当初から従事しています。Vance は .NET 中間言語 (IL) の設計を推進したほか、ジャスト イン タイム (JIT) コンパイラ チームのリーダーでした。


基本的な本能: My Extensibility による My 名前空間の拡張

基本的な本能
My Extensibility による My 名前空間の拡張
Matthew DeVore

コンテンツ
My 名前空間を拡張する方法
シングルトンを使用して My を拡張する
My 拡張のその他のシナリオ
拡張をコーディングする
拡張をテンプレートにパッケージ化する
テンプレートとしての My 拡張
.customdata ファイル
試してみよう
Visual Studio インストーラ ファイルで拡張を発行する

My 名前空間の機能は、一般的なコーディング作業のためのショートカットの手段と API を提供するために、Visual Basic® 2005 で採用されました。それ以降、ユーザーは、My 名前空間の拡張を記述し、自身のコード ライブラリに簡単にアクセスできるようになりました。My Extensibility は Visual Basic 2008 の新機能であり、My 名前空間をさらに簡単に拡張することができます。

新しい My Extensibility 機能を使用すると、プロジェクト プロパティ デザイナを使用するか、関連付けられている参照をプロジェクトで追加または削除するときに、My 名前空間の拡張をアクティブ化または非アクティブ化することができます。この機能により、一般的なコーディング作業のための API を展開することで、Visual Basic 開発環境が簡単に拡張できるようになります。

今回は、自作した拡張を My Extensibility 機能に組み込む方法について説明します。My 名前空間に対する実際のコード拡張を作成する方法の詳細については、MSDN® Magazine 2005 年 7 月号の「My 名前空間のカスタマイズによる共通タスクの単純化」(msdn.microsoft.com/msdnmag/issues/05/07/My) を参照してください。

My 名前空間を拡張する方法

My 名前空間を拡張するために使用可能なコード モデルはいくつかあります。皆さんも既に使い方をご存じの、最も簡単な方法から始めましょう。実際に、My 名前空間に何かを追加するのは、その他の名前空間に何かを追加するのによく似ています。最も簡単な例を示します。

Namespace My.HandyStuffForMy
    <Global.Microsoft.VisualBasic.HideModuleName()> _
    Friend Module HandyStuffForMyModule
        Sub Foo()
            ...
        End Sub

        Property Bar()
            ...
        End Property
    End Module
End Namespace

このコードでは、My 名前空間内の HandyStuffForMy という名前空間の中で、Foo という名前のプロシージャと Bar という名前のプロパティを定義しています。この例には注意すべき点がいくつかあります。それは、HideModuleName 属性と、Module に対するアクセス修飾子 Friend です。HideModuleName 属性を指定すると、モジュール名が IntelliSense® に現れません。メンバを参照するためにモジュール名を入力する必要はないため、モジュールを表示しても IntelliSense が無用に複雑になるだけです (図 1 を参照)。

Figure 1 Sub Namespace in IntelliSense
Figure 1 Sub Namespace in IntelliSense

これらのメンバを、サブ名前空間に追加せずに直接 My に追加する場合は (図 2 を参照)、以下のように、名前空間 My.HandyStuffForMy を名前空間 My に変更するだけです。

Namespace My
    <Global.Microsoft.VisualBasic.HideModuleName()> _
    Friend Module HandyStuffForMyModule
        Sub Foo()
            ...
        End Sub

        Property Bar()
            ...
        End Property
    End Module
End Namespace

Figure 2 No Sub Namespace
Figure 2 No Sub Namespace

また、Resources のように、既に My の中に存在する名前空間を使用すると、これらのメンバはその名前空間に追加されます。これにより、拡張に柔軟にアクセスすることができます。注意すべき第 2 の点は、モジュールに対するアクセス修飾子です。アクセス修飾子 Friend を指定すると、他のアセンブリがこれらのメンバにアクセスできなくなります。Public アクセスを使用する場合は、要素がこれら他のプロジェクトの My 名前空間で宣言されたシンボルと衝突する可能性があります。

ここで説明した方法を使用する My 拡張のサンプルを後ほど作成します。ただし、My の中のすべてのものが名前空間というわけではありません。シングルトンと呼ばれる、特定のクラスのグローバル インスタンスを返すプロパティもあります。

シングルトンを使用して My を拡張する

My.Computer、My.Settings、My.Application は、それぞれクラス My.MyComputer、My.MySettings、My.MyApplication のシングルトン インスタンスです。部分クラス機能を使用して、これらのクラスにメンバを簡単に追加することができます。たとえば、以下のコードは、Shutdown というプロシージャを My.Computer オブジェクトに追加します。

Namespace My
    Partial Class MyComputer
        Sub Shutdown()
            ...
        End Sub
    End Class
End Namespace

このコードは、My.Application と My.Settings を拡張するために使用できます。部分クラスを使用すると、別の場所で定義されているクラスにメンバを追加できます。ただし、そのクラスのソース コードが現在のプロジェクト内に存在する場合に限ります。これには、Visual Basic プロジェクトの MyComputer が含まれます。これは、コードがコンパイルされる前に、コンパイラによって自動的にクラスが挿入されるためです。

それでは、My.Computer、My.Settings、My.Application のように、追加した要素にシングルトンからアクセスできるようにしつつ、独自の要素を作成するにはどうすればよいでしょうか。そのためには、このコラムの冒頭で紹介した方法を使用して、ReadOnly プロパティを My 名前空間に追加し、そのプロパティが、ThreadSafeObjectProvider(Of T) クラスで提供されるグローバル インスタンスを返すようにします。前述の新しい名前空間の方法と比べると、この方法ではクラスの新しいインスタンスがスレッドごとに生成されます。図 3 は、Foo および Bar というメンバを持つグローバル オブジェクト My.CustomMyObject を追加する方法を示します。図 4 は、その結果得られる IDE での IntelliSense の動作を示します。

Figure 3 My.CustomMyObject の追加 (Close Figure 3)
Namespace My
  <Global.System.ComponentModel.EditorBrowsable _
  (Global.System.ComponentModel.EditorBrowsableState.Never)> _
  Partial Friend Class MyCustomMyObject
    Public Sub Foo()
            ...
    End Sub

    Public Property Bar()
            ...
        End Property
  End Class

  <Global.Microsoft.VisualBasic.HideModuleName()> _
  Friend Module CustomMyObjectModule
    Private instance As ThreadSafeObjectProvider(Of MyCustomMyObject)

    ReadOnly Property CustomMyObject() As MyCustomMyObject
      Get
        Return instance.GetInstance()
      End Get
    End Property
  End Module
End Namespace
Figure 4 My.CustomMyObject in IntelliSense
Figure 4 My.CustomMyObject in IntelliSense

My 拡張のその他のシナリオ

My.User の拡張は、おそらく最も難しいシナリオです。My.User はプロジェクトの外部にある Visual Basic ランタイム DLL 内で定義されているため、部分クラスでは拡張できません。My.User を拡張するには、プロジェクトレベルのコンパイル定数を定義する必要があります。つまり、My.User を拡張するには、拡張を使用する開発者がこの手順を手動で実行する必要があります。

いずれにせよ、My.User を拡張するコードの例とガイダンスが必要な場合は、前述の MSDN Magazine の記事を参照してください。この記事には、My.Application.Log の拡張、My.Computer.Network の拡張、My.Settings にアプリケーション構成情報を格納する方法のカスタマイズなど、特殊なシナリオについての情報も含まれています。

では、My.WebServices や My.Forms などのオブジェクトはどうでしょうか。これらのオブジェクトのクラスは、モジュール内で定義されている、入れ子にされた型であるため、部分クラス機能では使用できません。また、NotInheritable であるため、My.User のように拡張することもできません。これらのクラスに簡単にメンバを追加する方法はありません。この場合、My 名前空間に拡張を適合させる良い方法を見つける必要があります。

拡張をコーディングする

ここまでは My 名前空間に対する拡張について概要を説明しましたが、次に、これらの拡張を各自のプロジェクトで簡単に実装し共有する方法について説明します。ここまでの説明では、My が他の名前空間と同じ名前空間であることと、モジュールを追加して My の名前空間を指定するだけで、それに上位プロパティを追加できることを示しました。ここでは、文字列が米国およびカナダの郵便番号コード、電話番号、電子メール アドレス、その他いくつかの重要な形式に一致するかどうかをチェックする、一連の検証関数を作成します。

まずは Visual Studio® を起動し、新しいコンソール アプリケーションを作成します。プロジェクトの名前は MyValidation にします。次に、ソリューション MyValidation.vb に新しいモジュールを追加し、MyValidation.vb ファイルのコードを、図 5 に示すコードで置き換えます (ここでは、スペースの関係上抜粋を記載しています)。

Figure 5 検証コードの抜粋 (Close Figure 5)
Imports System.Text.RegularExpressions
Imports System
Imports System.Linq
Imports Microsoft.VisualBasic

Namespace My.Validation
  <Global.Microsoft.VisualBasic.HideModuleName()> _
  Public Module MyValidationMod
    Public Function IsEmpty(ByVal value As Object) As Boolean
      If (value Is Nothing) OrElse _
         (value Is System.DBNull.Value) OrElse _
         (value.ToString = String.Empty) OrElse _
         (TypeOf value Is Date AndAlso CDate(value) = Date.MinValue) _
      Then
        Return True
      Else
        Return False
      End If
    End Function

    Public Function IsAlphaNumeric(ByVal value As String, _
     Optional ByVal emptyOK As Boolean = False, Optional ByVal _
     whitespaceOK As Boolean = False) As Boolean
      If IsEmpty(value) Then Return emptyOK

      Dim expr As String
      If whitespaceOK Then
        expr = "^[a-zA-Z0-9\s]+$"
      Else
        expr = "^[a-zA-Z0-9]+$"
      End If

      Return Regex.IsMatch(value, expr)
    End Function

...

    Public Function IsCanadianProvince(ByVal st As String) As Boolean
      Dim allProv = "|AB|BC|MB|NB|NL|NT|NS|NU|ON|PE|QC|SK|YT"
      Return st.Length = 2 AndAlso allProv.IndexOf("|" & st) <> -1
    End Function

    Public Function IsUSAState(ByRef st As String) As Boolean
      Dim allStates = "|AL|AK|AZ|AR|CA|CO|CT|DE|FL|GA|HI|ID|IL|IN|IA|" & _
        "KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|" & _
        "OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY"
      Return st.Length = 2 AndAlso allStates.IndexOf("|" & st) <> -1
    End Function

    Public Function IsUSAPostalCode(ByVal zip As String) As Boolean
      If String.IsNullOrEmpty(zip) Then Return False

      Return Regex.IsMatch(zip, "^\d{5}(-\d{4})?$")
    End Function

    Public Function IsCanadianPostalCode(ByVal zip As String) As Boolean
      If String.IsNullOrEmpty(zip) Then Return False

      Return Regex.IsMatch(zip, "^[A-Z]\d[A-Z] \d[A-Z]\d$")
    End Function

    Public Function IsNorthAmericanPhone(ByRef phone As String) As Boolean
      If String.IsNullOrEmpty(phone) Then Return False

      Dim expr As String = "^(?:\([2-9]\d{2}\)\ ?|[2-9]\d{2}" & _
                           "(?:\-?|\ ?))[2-9]\d{2}[- ]?\d{4}$"

      Return Regex.IsMatch(phone, expr)
    End Function

    Public Function IsEmail(ByRef email As String) As Boolean
      If String.IsNullOrEmpty(email) Then Return False

      Dim localPartCharset = "[0-9a-zA-Z!#$%*/?|\^{}`~&'+\-=_]"
      Dim domainPartCharset = "[0-9a-zA-Z\-]"
      Dim expr = String.Format("^{0}+(\.{0}+)*@{1}+(\.{1}+)*", _
        localPartCharset, domainPartCharset)

      Return Regex.IsMatch(email, expr)
    End Function
  End Module
End Namespace

My 拡張テンプレートを作成するときは、コードがどのプロジェクト構成でも動作するように注意してください。特に、Option Explicit、Option Strict、Option Compare、Option Infer (Visual Basic 2008 の新機能) の指定可能な値に注意してください。最も柔軟性が低い構成 (Option Explicit On、Option Strict On、Option Infer Off、Option Compare を未指定) で動作するようにコードを記述するか、拡張の各コード ファイルの上部で各オプションを明示的に指定します。ここでは後者のアプローチをお勧めします。

また、プロジェクト内で定義されているシンボルとの競合を避けるため、さまざまなプロジェクト インポートに対応するように注意する必要もあります。そのためには、テンプレート内で定義されていないシンボルを、Global または My キーワードで修飾します。たとえば、Text.Encoding の代わりに、Global.System.Text.Encoding を使用します。Text.Encoding だけを使用すると、テンプレートは、Text という名前の名前空間を含む 2 つの名前空間をインポートする Windows® フォーム アプリケーションで動作しません。Global を省略し System.Text.Encoding だけを使用すると、テンプレートは System という名前の名前空間を定義するプロジェクトで動作しない可能性があります。

コードを他の人に配布する前には必ず完全にテストすることをお勧めしますが、これは My 拡張にも当てはまります。自動的に作成されるモジュール (Module1.vb) を使用して、My 拡張のテストを作成しましょう。バグを見つけたときに拡張を再エクスポートするのは面倒なため、拡張をできるだけ動作可能にし、初めてエクスポートする前に必ず適切なテストを行ってください。

Module1.vb をダブルクリックしてソリューション エクスプローラで開き、図 6 に示す抜粋したテスト コードをファイルに記述し、以前のコードを置き換えます。完全なコードについては、MSDN Magazine の Web サイト上のサンプル コードを参照してください。これで My 拡張をテストできるようになったため、F5 キーを押してプロジェクトを実行し、デバッガをアタッチします。以下の内容が出力されます。

Figure 6 テスト コードの抜粋 (Close Figure 6)
Module Module1
  Private testNumber As Integer
  Private testGroup As String

  Sub StartTestGroup(ByVal name As String)
    testGroup = name
    testNumber = 0
  End Sub

  Sub Test(ByVal result As Boolean, ByVal expected As Boolean)
    testNumber += 1

    If result <> expected Then
      Console.WriteLine("Test #{0} of group {1} FAILED.",  _
        testNumber, testGroup)
    End If
  End Sub

  Sub Main()

    StartTestGroup("IsAlphaNumeric")
    Test(My.Validation.IsAlphaNumeric("123HH2", True, True), True)

    ...

    StartTestGroup("IsCanadianPostalCode")
    Test(My.Validation.IsCanadianPostalCode("H0H 0H0"), True)

    ...

    StartTestGroup("IsCanadianProvince")
    Test(My.Validation.IsCanadianProvince("ON"), True)

    ...

    StartTestGroup("IsEmail")
    Test(My.Validation.IsEmail("a@b.com"), True)

    ...

    Console.WriteLine _
      ("Tests are finished. No FAILED messages indicates success.")
    Console.WriteLine("Press any key to continue.")

    Console.ReadKey(True)
  End Sub
End Module

Tests are finished. No FAILED messages indicates success.
Press any key to continue.

拡張をテンプレートにパッケージ化する

拡張を記述し適切なテストを実施したら、My 拡張テンプレートにパッケージ化します。プロジェクトを開いた状態で、[ファイル] メニューの [テンプレートのエクスポート] を選択します。このオプションが [ファイル] メニューに表示されない場合は、ツールバーを右クリックして [ユーザー設定]、[コマンド] タブの順に選択します。次に [ファイル] カテゴリを選択し、[テンプレートのエクスポート] コマンドを [ファイル] メニューにドラッグします。テンプレートをエクスポートすると、[テンプレートのエクスポート ウィザード] が表示されます。[項目テンプレート] を選択します (図 7 を参照)。My 拡張は常に項目テンプレートであり、プロジェクト テンプレートにはなりません。

Figure 7 Choose Template Type
Figure 7 Choose Template Type(画像を縮小するには、ここをクリックします)
Figure 7 Choose Template Type
Figure 7 Choose Template Type (画像を拡大するには、ここをクリックします)

テンプレートの作成元のプロジェクトを選択します。このソリューションではプロジェクトが 1 つしかないため、単に [次へ] をクリックします。[エクスポートする項目] ボックスの一覧から [MyValidation.vb] ファイルだけを選択し、[次へ] をクリックします。次の手順で、この項目に含める参照を選択できます。My 拡張をプロジェクトに追加したときに動作するうえで必要なすべての参照を選択する必要があります。この例では [System.Core] を選択し、再度 [次へ] をクリックします。

最後の手順で、[テンプレートの説明] ボックスに内容を入力します。たとえば、「文字列検証メソッドを使用して My 名前空間を拡張します」と入力します。最後に、[テンプレートを自動的に Visual Studio にインポート] チェックボックスをオフにします。この設定により、テンプレートはすぐにはインポートされません。My 拡張として使用する準備が整う前に、テンプレート内の数か所を変更する必要があるため、[完了] をクリックします。

テンプレートとしての My 拡張

My 拡張テンプレートでは、他の Visual Studio テンプレートと同様に、すべてのテンプレート パラメータが使用できます。これらは、プロジェクトに追加したときに My 拡張コードに自動的に挿入可能な、特別な文字列です。たとえば、$clrversion$ は CLR の現在のバージョンで置き換えられます。My 拡張テンプレートでは、$safeitemname$ は .vstemplate ファイル中の <DefaultName> 要素の値から拡張子を除いたもので置き換えられます。これは、テンプレート コードが格納されている .vb ファイルの名前に等しくなります。

通常のテンプレートで行うことができることはすべて My 拡張テンプレートでも行うことができます。たとえば、.vstemplate ファイル内で、<Hidden> 要素の値を false に設定することで、項目をプロジェクトに追加するときに、テンプレートを [新しい項目の追加] ダイアログに含めることができます。また、<References> セクションでアセンブリ参照を必ず追加するよう設定したり、Windows フォーム テンプレートと同様にコード ファイルのファミリを追加して、フォームのユーザー コードだけでなくデザイン コードも含めたりできます。

.vstemplate ファイルによるテンプレートの構成の詳細については、MSDN ライブラリの「VSTemplate 要素」(go.microsoft.com/fwlink/?LinkId=110905) を参照してください。テンプレート パラメータの詳細については、Visual Studio のテンプレート パラメータに関するページ (go.microsoft.com/fwlink/?LinkId=110907) を参照してください。

一般的な項目テンプレートと My 拡張項目テンプレートには、大きな違いがいくつかあります。項目テンプレートを Visual Basic プロジェクト デザイナの [My 拡張] ページから管理できるようにするには、特別な手順を実行して適切に構成する必要があります。

まず、エクスポートされたテンプレートの .zip ファイル中にある MyTemplate.vstemplate ファイルに <CustomDataSignature> 要素を追加する必要があります。これは、その項目テンプレートが My 拡張として扱われることを示します。また、テンプレートをプロジェクトに追加したときに、テンプレート内の項目が自動的に開かないようにするため、<ProjectItem> 要素にいくつかの属性を追加する必要もあります。これは、My 拡張テンプレートのコードが利用者から見えないようにするためです。さらに、<Hidden> 要素を MyTemplate.vstemplate に追加し、ユーザーが [新しい項目の追加] ダイアログを通じて項目をプロジェクトに追加することにしたときに、インストール済みテンプレートの一覧にこのテンプレートを表示しないことを指示する必要があります。最後に、一般的なテンプレートには適用されない情報 (すなわち一意の ID、バージョン番号、トリガ アセンブリ) を提供するため、.customdata ファイルをパッケージに追加する必要があります。

そのためには、.zip ファイルをアンパックして変更を加え、必要なファイルを再度パックします。そのために、まずテンプレートの内容を空のディレクトリに展開して編集できるようにします。Visual Studio では、ファイルが %MyDocs%\Visual Studio 2008\My Exported Templates に格納されます。%MyDocs% は "マイ ドキュメント" フォルダです。

テキスト エディタまたは XML エディタで MyTemplate.vstemplate ファイルを開き、図 8 で赤く強調表示された変更を行います。XML では、Visual Basic と違い大文字と小文字が区別される点に注意してください。

Figure 8 My 拡張項目テンプレート (Close Figure 8)
<VSTemplate Version="2.0.0"
   xmlns=http://schemas.microsoft.com/developer/vstemplate/2005
   Type="Item">
  <TemplateData>
    <DefaultName>MyValidation.vb</DefaultName>
    <Name>MyValidation</Name>
    <Description>Extends the My namespace with a set of string
       validation methods.</Description>
    <ProjectType>VisualBasic</ProjectType>
    <SortOrder>10</SortOrder>
    <Icon>TemplateIcon.ico</Icon>

    <!-- Configures the template to not be shown as an option
      when the user selects to add an item template to his project -->
    <Hidden>true</Hidden>

    <!-- Indicates it is to be treated as a My Extension. -->
    <CustomDataSignature>Microsoft.VisualBasic.MyExtension
    </CustomDataSignature>

  </TemplateData>
  <TemplateContent>
    <References>
      <Reference> <Assembly>System.Core</Assembly> </Reference>
    </References>
    <ProjectItem SubType="Code" TargetFileName="$fileinputname$.vb"
      ReplaceParameters="true" OpenInEditor="false"
      OpenInWebBrowser="false" OpenInHelpBrowser="false">MyValidation.vb
    </ProjectItem>
  </TemplateContent>
</VSTemplate>

.zip ファイルにパックする、新しいファイル MyValidation.customdata を作成します。拡張子が .customdata のファイルであれば名前は何でも構いません。メモ帳か任意のテキスト エディタで、以下のマークアップを追加します。

<VBMyExtensionTemplate
    ID="MyValidationMyNamespaceExtension"
    Version="1.0.0.0" />

この例で省略されている AssemblyFullName 属性は、My 拡張をトリガするアセンブリを示します。言い換えれば、ユーザーがこのアセンブリをプロジェクトに追加するときに、ユーザーに対して My 拡張も追加するかどうか質問することをここで指定できます。ユーザーがこのアセンブリをプロジェクトから削除しようとすると、My 拡張が現在アクティブであれば、ユーザーに対して My 拡張を削除するかどうかを質問します。System.Windows.Forms などのトリガ アセンブリを指定するには、以下のテキストを .customdata ファイルに挿入します。

<VBMyExtensionTemplate
    ID="MyValidationMyNamespaceExtension"
    Version="1.0.0.0"
    AssemblyFullName="System.Windows.Forms" />

これでファイルをテンプレートの .zip ファイルに戻す準備ができました。テンプレートのアイコンを編集したくなるかもしれませんが、My 拡張テンプレートのアイコンはユーザーに表示されないため、変更しないことをお勧めします。Windows エクスプローラを使用して 4 つのファイルを .zip ファイルにコピーします。その後、.zip ファイルを %MyDocs%\Visual Studio 2008\Templates\ItemTemplates\ にコピーします。%MyDocs% は "マイ ドキュメント" フォルダです。

.customdata ファイル

プロジェクト内での My 拡張の扱いは、その大半が目に見えない自動的な処理です。Visual Studio が、最小限のユーザー入力でこれらのファイルをスムーズに扱えるように、プロジェクト ファイル (拡張子 .vbproj) に新しい属性がいくつか追加されています。MyValidation 拡張を追加したプロジェクトの .vbproj ファイルをテキスト エディタで開くことで確認できます。内容は以下のようになります。

<ItemGroup>
  ...
  <Compile Include="My Project\MyExtensions\MyValidation.vb">
    <VBMyExtensionTemplateID>
      MyValidationMyNamespaceExtension
    </VBMyExtensionTemplateID>
    <VBMyExtensionTemplateVersion>
      1.0.0.0
    </VBMyExtensionTemplateVersion>
  </Compile>
  ...
</ItemGroup>

ただし、Module1.vb などの一般的なコード ファイルのエントリを見ると、以下のようになっています。

<ItemGroup>
  <Compile Include="Module1.vb" />
</ItemGroup>

このように、いくつかの違いがあります。まず、MyValidation.vb ファイルは "マイ プロジェクト" フォルダに格納されるため、ユーザーが [ソリューション エクスプローラ] ウィンドウで [すべてのファイルを表示] ボタンを押さない限り表示されません。また、MyValidation.vb ファイルには追加の 2 つのメタデータがあり、MyValidation.customdata ファイルに追加した値に対応します。

この情報をここに保存しておくと、Visual Studio のユーザーは、ファイルを直接追加または削除するのではなく、プロジェクト デザイナの [My 拡張] タブを通じて拡張を追加および削除できます。また、トリガ アセンブリの参照が削除された場合、Visual Studio ではユーザーに対し、My 拡張が動作しなくなることを警告し、テンプレートを削除するかどうかを質問することができます。この情報があることで、拡張の 2 つのコピーが同じプロジェクトに存在するのを防ぐこともできます。

試してみよう

これで、My 拡張を My Extensibility 機能に組み込んで使用する準備ができました。使用するには、まず Visual Studio の新しいインスタンスを起動し、新しいプロジェクトを作成します。その後、作成したプロジェクトで右クリックして [プロパティ] を選択するか、ソリューション エクスプローラ内の [マイ プロジェクト] をダブルクリックして、プロジェクト デザイナを開きます。[My 拡張] タブで、[拡張の追加] をクリックし、作成したテンプレートを選択して、[OK] をクリックします。これで拡張がプロジェクトに読み込まれます。コード内で「My.」と入力すると (ドットに注意してください)、IntelliSense で表示される名前空間の 1 つとして [Validation] が表示されます。

Visual Studio インストーラ ファイルで拡張を発行する

インストーラ ファイルを作成すると、拡張を他の開発者に簡単に配布できます。Visual Studio のインストーラ ファイルは、拡張子が .vsi となるように名前が変更された .zip ファイルです。このファイルには、Visual Studio アドオンと、それを記述する .vscontent ファイルが格納されます。サンプルを展開し、My 拡張用の .vsi ファイルを作成しましょう。まず、テキスト ファイル MyValidation.vscontent を作成し、その内容を記述するために図 9 のテキストを入力します。

Figure 9 MyValidation インストーラ (Close Figure 9)
<VSContent xmlns="http://schemas.microsoft.com/developer/vscontent/2005">
    <Content>
        <FileName>MyValidation.zip</FileName>
        <DisplayName>My.Validation Extension</DisplayName>
        <Description>Extends the My namespace with a set of string
           validation methods.</Description>
        <FileContentType>VSTemplate</FileContentType>
        <ContentVersion>1.0</ContentVersion>
        <Attributes>
            <Attribute name="TemplateType" value="Item"/>
        </Attributes>
    </Content>
</VSContent>

別の My 拡張を作成するときに変更する可能性が高い要素は、FileName、DisplayName、Description、ContentVersion の値だけです。Visual Studio インストーラ ファイルの作成の詳細とこれらの要素の詳しい説明については、MSDN ライブラリのトピック (go.microsoft.com/fwlink/?LinkId=110908) を参照してください。

この MyValidation.vscontent ファイルと MyValidation.zip ファイルを別の .zip ファイル MyValidationInstall.zip に格納します。その後、MyValidationInstall.zip の名前を MyValidationInstall.vsi に変更します。これで、ダブルクリックするだけでテンプレートが自動的に Visual Studio にインストールされます。同じ名前のテンプレートが既に存在する場合は、上書きするかどうかが質問されます。状況を正確に理解している場合は、ファイルを上書きして構いません。この .vsi ファイルを配布することで、My 拡張を他の開発者のマシンに簡単にインストールすることができます。

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

Matthew DeVore は、Visual Basic 言語の QA チームで 1 年半の勤務経験があり、彼が初めてプログラミングを学んだ QBasic 言語の最新の革新に取り組んでいます。Visual Basic 2008 では、匿名型や My Extensibility 機能、および程度は少ないもののラムダ式のテストを行っています。


Cutting Edge: ListView のヒントと秘訣

Cutting Edge
ListView のヒントと秘訣
Dino Esposito

コンテンツ
階層型メニューを構築する
階層型ビューを構築する
エクステンダを使用してユーザー体験を向上させる
DataPager と ListView
複数の項目テンプレート
まとめ

先月号では、ASP.NET 3.5 のコントロール ツールボックスに新たに追加された ListView コントロールを紹介しました。おさらいしておくと、ListView は、DataList コントロールの拡張版です。これを使用すると、生成されたマークアップに対してより詳細な制御が可能で、ページングをサポートでき、データ ソース ベースのバインディング モデルと完全に統合できます。

このコラムでは、ListView テンプレートとデータ バインドの高度な使い方の紹介として、実際のページでかなり一般的に使用されるものの、追加のコーディングが必要とされる機能を実装します。具体的には、入れ子になった ListView コントロールを使用して階層的なデータのビューを作成し、カスタム ListView クラスを派生させて ListView のイベント モデルを拡張する方法について説明します。

特に、バインドされたデータ項目の各グループで異なるテンプレートを使用できるように、イベント モデルを洗練させます。たとえば、与えられたいくつかの条件に一致するデータ セット内のすべてのデータ項目に対して、異なるテンプレートを使用できます。これは、単に特定の項目に異なるスタイルを与えるだけではありません。ItemDataBound イベントを処理するだけで、任意のビュー コントロールで簡単に行うことができます。

通常、メニューは、CSS でスタイル設定された一連の <li> タグとして実装されます。フラットなメニューを表示するだけなら特にバインドの問題は発生しませんが、サブメニューが必要な場合はどうなるでしょうか。この場合、組み込みの Menu コントロールを使用するか、ListView を使用して、よりカスタマイズされた表示方法を作成します。なお、Menu コントロールの既定では、ListView で得られる CSS と親和性が高い出力ではなく、テーブルベースの出力を使用することにも注意してください (Menu コントロールで CSS との親和性が高い出力を得るには、CSS Control Adapter Toolkit (www.asp.net からダウンロードできます) をインストールして構成する必要があります。

階層型メニューを構築する

多くの Web アプリケーションでは、ページの左側または右側に垂直型のメニューがあります。このメニューを使用して、ユーザーは、2 つ以上の入れ子になったレベルのページ間を移動することができます。ASP.NET の Menu コントロールは、そのための選択肢として明らかに有望ですが、私は、メニューにデータを設定するための階層型データ ソース (一般には XML ファイル) があり、飛び出し型のサブメニューを作成する必要がある場合にだけ Menu コントロールを使用する傾向にあります。

複数レベルの静的な項目リストに対しては、UI デザイン チームによって作成されたマークアップを出力するために、繰り返し型のコントロールを使用します。ASP.NET 3.5 では、繰り返し型のコントロールとして ListView コントロールを選びます。

図 1 に示すようなメニューを考えます。これは、oswd.org からダウンロード可能な、CoffeeNCream の無料の HTML テンプレートを使って表示されています。サンプル ページでは、ASP.NET のマスタ ページに HTML マークアップを取り込んだだけです。

Figure 1 A Standard Menu
Figure 1 A Standard Menu(画像を縮小するには、ここをクリックします)
Figure 1 A Standard Menu
Figure 1 A Standard Menu (画像を拡大するには、ここをクリックします)

右側のメニューの HTML ソース コードは以下のようになります。

<h1>Something</h1>
<ul>
    <li><a href="#">pellentesque</a></li>
    <li><a href="#">sociis natoque</a></li>
    <li><a href="#">semper</a></li>
    <li><a href="#">convallis</a></li>
</ul>

ご覧のように、最上位の文字列の後にリンクのリストが続いています。最初の ListView を使用して H1 要素を作成し、次に入れ子になった ListView (または同様のデータバインド コントロール) を使用してリンクのリストを作成します。まず、メニューに表示するデータを取得する必要があります。以下の疑似タイプ オブジェクトのコレクションを使用して各項目を生成するのが理想です。

class MenuItem {
  public string Title;
  public Collection<Link> Links;
}

class Link  {
  public string Url;
  public string Text;
}

MenuItem コレクションにデータを設定する妥当な方法は、XML ファイルから情報を表示することです。ドキュメントのスキーマは以下のようになります。

<Data>
  <RightMenuItems>
     <MenuItem>
       <Title>Something</Title>
       <Link url="..." text="pellentesque" />
         :
     </MenuItem>
  </RightMenuItems>
</Data>

以下のコードは、LINQ to XML を使用して内容を読み込んで処理する方法を示します。

var doc = XDocument.Load(Server.MapPath("dataMap.xml"));
var menu = (from e in doc.Descendants("RightMenuItems")
            select e).First();
var menuLinks = from mi in menu.Descendants("MenuItem")
                select new
                {
                   Title = mi.Value,
                   Links = (...)
                };

ドキュメントを読み込んだ後、最初のノード RightMenuItems を選択して、その下位の MenuItem すべてを取得します。各 MenuItem ノードの内容は、2 つのプロパティ Title と Links を持つ新しい匿名タイプに読み込まれます。それでは、Links コレクションにはどのようにしてデータを設定するのでしょうか。以下にコードを示します。

Links = (from l in mi.Descendants("Link")
         select new {Url=l.Attribute("url").Value,
                     Text=l.Attribute("text").Value})

次の手順は、この複合データをユーザー インターフェイスにバインドすることです。前述のように、外側の ListView を使用してタイトルを表示し、入れ子になった ListView を使用して下位のリンクの一覧を表示します (図 2 を参照)。最も内側の ListView は、Eval メソッドを使用してデータにバインドする必要があります。それ以外の方法はうまく機能しません。

Figure 2 入れ子になった ListView (Close Figure 2)
<asp:ListView runat="server" ID="RightMenuItems"
  ItemPlaceholderID="PlaceHolder2">
  <LayoutTemplate>
      <asp:PlaceHolder runat="server" ID="PlaceHolder2" />
  </LayoutTemplate>

  <ItemTemplate>
    <h1><%# Eval("Title") %></h1>

      <asp:ListView runat="server" ID="subMenu"
        ItemPlaceholderID="PlaceHolder3"
          DataSource='<%# Eval("Links") %>'>
          <LayoutTemplate>
            <ul>
              <asp:PlaceHolder runat="server" ID="PlaceHolder3" />
            </ul>
          </LayoutTemplate>
          <ItemTemplate>
            <li>
              <a href='<%# Eval("Url") %>'><%# Eval("Text") %></a>
            </li>
          </ItemTemplate>
      </asp:ListView>
  </ItemTemplate>
</asp:ListView>

<asp:ListView runat="server" ID="subMenu"
    ItemPlaceholderID="PlaceHolder3"
    DataSource='<%# Eval("Links") %>'>
    ...
</asp:ListView>

データ バインド処理を開始するには、データを最上位の ListView にアタッチします。その際、入れ子になった ListView を含め、ListView の本体全体が表示されます。理論的には、親の ListView の ItemDataBound イベントを取得し、コントロール ツリーをたどって、下位の ListView の参照を取得し、それをプログラム的にデータにバインドすることができます。その場合、例外はスローしませんが、内側の ListView に対するバインド コマンドは失われます。表示に影響を与えるにはバインド コマンドの発生が遅すぎるためです。これに対しデータ バインド式は、データ バインド イベントの中で、コントロールのライフサイクルの正しい時点で、自動的に評価されます。これにより、正しいデータがユーザー インターフェイスに適切にバインドされます。

階層型ビューを構築する

階層型メニューにデータを設定するための同じモデルを、階層型のデータ ビューを構築するためにも使用できます。この場合、TreeView コントロールを使用して、複数レベルのデータ表現を準備することもできますが、TreeView コントロールに対するデータ バインドでは、階層型のデータ ソースが必要になります。入れ子になった ListView コントロールを使用することで、データ ソースの構造と、得られるユーザー インターフェイスの両方の点で柔軟性が高まります。この概念について詳しく説明します。

顧客、注文、および注文の詳細を、既存のテーブルのリレーションシップに応じて表示するために、階層型のデータ グリッドを作成する必要があるとします。データを取得してコントロールにバインドするにはどうすればよいでしょうか。図 3 のコードを参照してください。LINQ to SQL を使用して、データ階層を格納するのに適しているオブジェクト モデルにデータを簡単に読み込むことができます。LINQ to SQL でクエリを実行するとき、実際には明示的に要求したデータだけが取得されます。言い換えれば、グラフの最初のレベルだけが取得されます。関連するオブジェクトが同時に自動的に読み込まれることはありません。

Figure 3 適切なデータの読み込み (Close Figure 3)
Public Class DataCache
{
    public IEnumerable GetCustomers()
    {
        NorthwindDataContext db = new NorthwindDataContext();
        DataLoadOptions opt = new DataLoadOptions();
        opt.LoadWith<Customer>(c => c.Orders);
        opt.LoadWith<Order>(o => o.Order_Details);
        db.LoadOptions = opt;

        var data = from c in db.Customers
                   select new { c.CompanyName, c.Orders };

        return data.ToList();
    }

    public int GetCustomersCount()
    {
        // Return the number of customers
        NorthwindDataContext db = new NorthwindDataContext();
        return db.Customers.Count();
    }

    public IEnumerable GetCustomers(int maxRows, int startRowIndex)
    {
        if (maxRows < 0)
            return GetCustomers();

        NorthwindDataContext db = new NorthwindDataContext();
        DataLoadOptions opt = new DataLoadOptions();
        opt.LoadWith<Customer>(c => c.Orders);
        opt.LoadWith<Order>(o => o.Order_Details);
        db.LoadOptions = opt;

        var data = (from c in db.Customers
                    select new {
                      c.CompanyName,
                      c.Orders
                    }).Skip(startRowIndex).Take(maxRows);
        return data.ToList();
    }
}
NorthwindDataContext db = new NorthwindDataContext();
DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer>(c => c.Orders);
opt.LoadWith<Order>(o => o.Order_Details);
db.LoadOptions = opt;

DataLoadOptions クラスを使用して LINQ to SQL エンジンの既定の動作を変更し、特定のリレーションシップによって参照されるデータがすぐに読み込まれるようにすることができます。図 3 のコードでは、顧客と同時に注文が読み込まれ、注文と同時にその詳細が読み込まれます。

LoadWith メソッドは、指定されたリレーションシップに従ってデータを読み込みます。AssociateWith メソッドでは、以下に示すように、関連するプリフェッチされたオブジェクトのフィルタリングが可能です。

opt.AssociateWith<Customer>(
     c => c.Orders.Where(o => o.OrderDate.Value.Year == 1997));

この例では、顧客データをフェッチするときに、1997 年に発行された注文だけがプリフェッチされます。関連データをプリフェッチして、フィルタを適用する必要がある場合は、AssociateWith メソッドを使用します。テーブル間で循環する参照がないことを確認するのはユーザーの役目です。たとえば、以下のように顧客の注文を読み込み、その後注文の顧客を読み込むような場合です。

DataLoadOptions opt = new DataLoadOptions();
opt.LoadWith<Customer> (c => c.Orders);
opt.LoadWith<Order> (o => o.Customer);

これですべてのデータが準備できたため、バインドについて考えましょう。この例では、2 つのレベルの ListView コントロールが巧みに機能します。最上位の ListView を Customer オブジェクトのコレクションにバインドし、最も内側の ListView をバインドされた各 Customer オブジェクトの Orders プロパティにバインドします。図 4 のコードは、3 つのレベルの階層型ビューのマークアップを示します。顧客が最初のレベルに表示され、最も外側の ListView の ItemTemplate プロパティによって表示されます。埋め込まれた ListView は、注文にバインドされます。最後に、埋め込まれた ListView の ItemTemplate に GridView が格納され、各注文の詳細が表示されます。

Figure 4 3 つのレベルの階層 (Close Figure 4)
<asp:ListView ID="ListView1" runat="server"
  DataSourceID="ObjectDataSource1"
  ItemPlaceholderID="lvItemPlaceHolder">

  <LayoutTemplate>
    <asp:PlaceHolder runat="server" ID="lvItemPlaceHolder" />
  </LayoutTemplate>

  <ItemTemplate>
    <asp:Panel runat="server" ID="panelCustomerInfo"
      cssclass="customerInfo">
      <%# Eval("CompanyName") %>
    </asp:Panel>
    <asp:panel runat="server" ID="panelCustomerDetails"
      cssclass="customerDetails">
      <asp:ListView runat="server"
        DataSource='<%# Eval("Orders") %>'
        ItemPlaceholderID="lvOrdersItemPlaceHolder">

        <LayoutTemplate>
          <ul>
            <asp:PlaceHolder runat="server"
              ID="lvOrdersItemPlaceHolder" />
          </ul>
        </LayoutTemplate>

        <ItemTemplate>
          <li>
            Order #<%# Eval("OrderID") %>
            <span class="orderDate">
              placed on <%#
              ((DateTime)Eval("OrderDate")).ToString
              ("ddd, dd MMM yyyy") %>
            </span>
            <span class="orderEmployee">
              managed by <b><%# Eval("Employee.LastName") %></b>
            </span>
            <asp:GridView runat="server"
              DataSource='<%# Eval("Order_Details") %>'
              SkinID="OrderDetailsGridSkin" >
            </asp:GridView>
          </li>
        </ItemTemplate>
      </asp:ListView>
    </asp:panel>
  </ItemTemplate>
</asp:ListView>

エクステンダを使用してユーザー体験を向上させる

率直に言うと、図 4 のコードから得られるユーザー インターフェイスは、あまり魅力的ではありません。階層型データ ビューを構築する際には、展開/折りたたみパネルが、ユーザー体験を向上させるために特に有効なソリューションとなります。ASP.NET AJAX Control Toolkit では、作成済みのエクステンダが提供されており、これを Panel サーバー コントロールに適用することで、各顧客と注文に関連付けられた情報に対してドロップダウン効果を追加できます。

CollapsiblePanelExtender コントロールを使用して、ページ コントロール ツリー内に、展開と折りたたみが実行されるパネルをスクリプトで定義します。言うまでもなく、ページの開発者が JavaScript を記述する必要はありません。パネルの展開と折りたたみで必要なスクリプトは、エクステンダ コントロールによってすべて自動的に挿入されます。では、エクステンダに対して設定可能なプロパティについて見てみましょう。

<act:CollapsiblePanelExtender runat="server" ID="CollapsiblePanel1"
     TargetControlID="panelCustomerDetails"
     Collapsed="true"
     ScrollContents="true"
     SuppressPostback="true"
     ExpandedSize="250px"
     ImageControlID="Image1"
     ExpandedImage="~/images/collapse.jpg"
     CollapsedImage="~/images/expand.jpg"
     ExpandControlID="Image1"
     CollapseControlID="Image1">
</act:CollapsiblePanelExtender>

折りたたみ可能なパネル エクステンダをサポートするには、図 4 のコードをわずかに変更することが必要です。特に、パネル panelCustomerInfo を編集して、下位のビューを展開したり折りたたんだりするために使用するボタンを追加する必要があります。パネルのマークアップを書き換える 1 つの方法を以下に示します。

<asp:Panel ID="panelCustomerInfo" runat="server">
  <div class="customerInfo">
    <div style="float: left;"><%# Eval("CompanyName") %></div>
    <div style="float: right; vertical-align: middle;">
      <asp:ImageButton ID="Image1" runat="server"
               ImageUrl="~/images/expand.jpg"
               AlternateText="(Show Orders...)"/>
    </div>
  </div>
</asp:Panel>

ボタンは、顧客名と同じ行にある、右揃えのイメージを使用して表示されます。エクステンダ上の TargetControlID プロパティは、折りたたみと展開の対象となる、ページ内のパネルを参照します。これは、注文とその詳細が物理的に格納されているパネルです。図 4 に示すように、これは panelCustomerDetails という名前のパネルです。

ExpandControlID 属性と CollapseControlID 属性は要素の ID を示し、これらの要素をクリックするとターゲット パネルを展開したり折りたたんだりできます。パネルの状態を反映させるために別々のイメージを使用する場合は、イメージ コントロールの ID も指定する必要があります。この情報は、ImageControlID 属性に格納されます。ImageControlID は、他の 2 つのプロパティ CollapsedImage および ExpandedImage に関連付けられ、これらのプロパティにイメージの URL が格納されます。

ExpandedSize プロパティは、展開されたパネルの最大の高さをピクセル単位で設定します。既定では、この高さを超える内容はすべて切り取られます。ただし、ScrollContents プロパティに true を設定すると、垂直スクロールバーが追加され、内容全体をスクロールできるようになります。

最後に、ブール型のプロパティ Collapsed でパネルの初期状態を設定でき、SuppressPostback では、パネルの展開を完全にクライアント側で操作するかどうかを示します。SuppressPostback が true の場合、パネルの展開または折りたたみで使用するポストバックは使用されません。これは、表示されるデータを更新できないことを意味します。頻繁に変化しない比較的静的なデータでは、ページのちらつきやネットワーク トラフィックが減るため、この方法は明らかに優れています。ただし、コントロール内にデータを動的に表示する必要がある場合でも、UpdatePanel コントロールを使用することで、ちらつきを最小限にすることができます。図 5 は、3 つのレベルのデータ ビューのユーザー インターフェイスを示します。

Figure 5 Data View with Three Levels
Figure 5 Data View with Three Levels(画像を縮小するには、ここをクリックします)
Figure 5 Data View with Three Levels
Figure 5 Data View with Three Levels (画像を拡大するには、ここをクリックします)

DataPager と ListView

ListView コントロールは、新しい DataPager コントロールを通じたページング機能を提供します。DataPager は汎用のページング コントロールであり、IPageableItemContainer インターフェイスを実装する任意のデータバインド コントロールで使用できます。ASP.NET 3.5 では、ListView がこのインターフェイスをサポートする唯一のコントロールです。

DataPager コントロールでは、組み込みまたはテンプレートベースのユーザー インターフェイスを表示できます。新しいページにジャンプするためにユーザーが DataPager コントロールをクリックすると、IPageableItemContainer インターフェイス上のメソッドが呼び出されます。このメソッドでは、次のデータバインド操作の中でデータの特定のページだけが表示されるように、ページ分割されたコントロール内の内部変数を設定することになっています。

ここで、データの右側のページを選択する際に、データバインド コントロール (この場合は ListView) が引き続き問題であることがわかります。ASP.NET の他のビュー コントロールと同様に、ListView コントロールはページング動作を外部コードに依存します。データ ソース プロパティを通じてデータをバインドする場合は、ユーザー コードでデータのページ分割を実現する必要があります。また、データ ソース コントロールを通じてデータをバインドする場合は、ページングをサポートするように、データ ソース コントロールを正しく構成する必要があります。

LinqDataSource コントロールと ObjectDataSource コントロールには、どちらもページング機能が組み込まれています。LinqDataSource には、既定のページングを有効または無効にするための AutoPage プロパティがあります。階層型のデータでは、LINQ データ コンテキストに適切な読み込みオプションが設定されていることも必要です。LinqDataSource のプログラミング インターフェイスには、データ コンテキスト オブジェクトに対して LoadOptions プロパティを設定するためのプロパティはありません。ただし、以下のように ContextCreated イベントを処理することで、新たに作成したデータ コンテキストにアクセスし、自由に構成することができます。

void LinqDataSource1_ContextCreated(
    object sender, LinqDataSourceStatusEventArgs e)
{
    // Get a reference to the data context
    DataContext db = e.Result as DataContext;

    if (db != null)
    {
       DataLoadOptions opt = new DataLoadOptions();
       opt.LoadWith<Customer>(c => c.Orders);
       opt.LoadWith<Order>(o => o.Employee);
       opt.LoadWith<Order>(o => o.Order_Details);
       db.LoadOptions = opt;
    }
}

この動作の代替方法として、ObjectDataSource コントロールを使用してデータを提供し、ページング ロジックを実装することもできます。その後、ビジネス オブジェクトで、LINQ to SQL または通常の ADO.NET を使用してデータにアクセスします。

DataPager と ListView を同時に使用するうえで私が遭遇した問題については説明しておく価値があります。最初は、同じコンテンツ プレースホルダでホストされる ListView と DataPager を含むコンテンツ ページがありました。以下に示すように、このページでは、PagedControlID プロパティを使用して、DataPager 内で ListView コントロールを参照していました。これは正常に動作していたのです。

<asp:DataPager ID="DataPager1" runat="server"
     PagedControlID="ListView1"
     PageSize="5"
     EnableViewState="false">
  <Fields>
     <asp:NextPreviousPagerField
        ShowFirstPageButton="true"
        ShowLastPageButton="true" />
  </Fields>
</asp:DataPager>

次に、DataPager を同じマスタ内の別のコンテンツ領域に移動しました。すると突然、DataPager が ListView コントロールと通信できなくなりました。問題は、ページ分割されたコントロールを探すために DataPager コントロールが使用するアルゴリズムにあったのです。このアルゴリズムは、2 つのコントロールが異なる命名コンテナによってホストされているとうまく動作しません。この問題を回避するには、ページ分割されたコントロールを、その完全な一意の ID (命名コンテナ情報を含む) を使用して識別する必要があります。ただし、残念ながら、この情報を宣言的に設定する簡単な方法はありません。

これには、ASP スタイルのコード ブロックは使用できません。サーバー コントロールのプロパティを設定するために使用した場合、リテラルのように扱われるためです。データ バインド式 <%# ... %> も使用できません。DataPager で必要なタイミングよりも遅く式が評価されるためです。つまり、Load イベントが遅すぎて、DataPager で例外が発生します。最も簡単な回避策は、以下のように、ページの Init イベントで PagedControlID プロパティをプログラム的に設定することです。

protected void Page_Init(object sender, EventArgs e)
{
   DataPager1.PagedControlID = ListView1.UniqueID;
}

複数の項目テンプレート

ListView は、他のテンプレートベースのコントロールやデータがバインドされたコントロールと同様に、バインドされた各データ項目に対して同じ項目テンプレートを繰り返し適用します。それでは、特定の項目のサブセットに対して項目テンプレートを変更したい場合はどうすればよいでしょうか。正直に言うと、私は、何年も ASP.NET プログラミングを行ってきましたが、複数の項目テンプレートを使用する必要があったことは一度もありません。実行時の条件に基づいて、DataGrid コントロールと GridView コントロール内の小規模な項目のグループの外観をカスタマイズしたことは何度かありますが、常に異なるスタイル属性を適用する必要があっただけです。

既存のテンプレートにプログラム的に新しいコントロール (ほとんどは Label コントロールやテーブル セル) を追加したのはごくわずかです。データ バインド イベントを発行するデータがバインドされたコントロールでは、少なくとも操作しようとしているコントロールの内部構造に関する詳しい知識を持っていれば、これはあまり難しい作業ではありません。

プログラム的にコントロールを挿入することは実際に有効に動作するソリューションの 1 つではありますが、私にとって魅力のあるものではありませんでした。そのため、Web ページ内の ListView ベースのメニューを変更するよう顧客に依頼されたとき、私は別の方法を試すことにしました。図 1 に示したのと同様のメニューにおいて、1 つのサブメニューの項目を垂直ではなく水平に表示する必要がありました。

ListView コントロールは、データ ソースをループして、以下のアルゴリズムを適用しながらマークアップを生成します。まず、項目の区切りが必要かどうかを確認します。区切りが必要な場合、テンプレートをインスタンス化し、データ項目オブジェクトを作成します。データ項目オブジェクトは、項目テンプレートのコンテナであり、ビュー内の項目のインデックスおよびバインドされたデータ ソースに関する情報が格納されます。項目テンプレートがインスタンス化されると、ItemCreated イベントが発生します。次にデータのバインドを行います。これが完了すると、ItemDataBound イベントが発生します。

ご覧のように、各項目のテンプレートをプログラム的に変更するために利用できる公開イベントはありません。ページの Init イベントや Load イベントでテンプレートを変更することはできますが、それではバインドされたすべての項目が対象になってしまいます。ItemCreated を処理し、そこで ItemTemplate プロパティを設定すると、変更は現在処理中の項目ではなく次の項目に影響します。そうすると、ItemCreating イベントが必要となりますが、ListView コントロールではそのようなイベントは発生しません。そのため、解決方法は、図 6 のように独自の ListView コントロールを作成することになります。

Figure 6 ItemCreating イベントの発生 (Close Figure 6)
namespace Samples.Controls
{
  public class ListViewItemCreatingEventArgs : EventArgs
  {
    private int _dataItemIndex;
    private int _displayIndex;

    public ListViewItemCreatingEventArgs(int dataItemIndex,
                                         int displayIndex) {
      _dataItemIndex = dataItemIndex;
      _displayIndex = displayIndex;
    }

    public int DisplayIndex {
      get { return _displayIndex; }
      set { _displayIndex = value; }
    }

    public int DataItemIndex {
      get { return _dataItemIndex; }
      set { _dataItemIndex = value; }
    }
  }

  public class ListView : System.Web.UI.WebControls.ListView
  {
    public event EventHandler<ListViewItemCreatingEventArgs>
                                               ItemCreating;

    protected override ListViewDataItem CreateDataItem(int
                           dataItemIndex, int displayIndex) {
      // Fire a NEW event: ItemCreating
      if (ItemCreating != null)
        ItemCreating(this, new ListViewItemCreatingEventArgs
                             (dataItemIndex, displayIndex));

      // Call the base method
      return base.CreateDataItem(_dataItemIndex, displayIndex);
    }
  }
}

CreateDataItem メソッドをオーバーライドすることで、項目テンプレートがインスタンス化される直前にコードを実行する機会ができます。CreateDataItem メソッドは、ListView クラス内で protected および virtual として宣言されています。図 6 に示すように、メソッドのオーバーライドは簡単です。まず、カスタム ItemCreating イベントを発生させ、基本メソッドを呼び出します。

ItemCreating イベントは、ユーザー コードにいくつかの整数 (データ ソース内の項目の絶対インデックスと、ページ固有のインデックス) を渡します。たとえばページ サイズが 10 の場合、ListView が第 2 ページの最初の項目の表示を処理している場合は、dataItemIndex に 11 個の項目が格納され、displayIndex に 1 個の項目が格納されます。新しい ItemCreating イベントを使用するには、以下のコードに示すように、カスタム ListView コントロール上でメソッドとハンドラを宣言するだけです。

<x:ListView runat="server" ID="ListView1"
   ItemPlaceholderID="itemPlaceholder"
   DataSourceID="ObjectDataSource1"
   OnItemCreating="ListView1_ItemCreating">
   <LayoutTemplate>
      <div>
         <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
      </div>
   </LayoutTemplate>
</x:ListView>

コードでイベントを処理します。

void ListView1_ItemCreating(
     object sender, ListViewItemCreatingEventArgs e)
{
    string url = "standard.ascx";
    if (e.DisplayIndex % DataPager1.PageSize == 0)
        url = "firstItem.ascx";

    ListView1.ItemTemplate = Page.LoadTemplate(url);
}

ここでは、データ項目を表示するために 2 つの異なるユーザー コントロールを使用しています。具体的なユーザー コントロールは、表示インデックスで特定されます。最初の項目を除き、すべての項目が同じテンプレートを共有します。図 7 に、ページの動作の様子を示します。

Figure 7 Multiple Item Templates
Figure 7 Multiple Item Templates(画像を縮小するには、ここをクリックします)
Figure 7 Multiple Item Templates
Figure 7 Multiple Item Templates (画像を拡大するには、ここをクリックします)

実際の一般的なページの複雑さを考えると、このソリューションは簡単すぎるように思われます。たいていの場合、表示するコンテンツに基づいて、異なるテンプレートを使用する必要があります。これには、カスタム ListView コントロールをさらに拡張して、データバインド処理内の項目テンプレートを変更する必要があります。図 8 のコードを参照してください。

Figure 8 内容に基づくテンプレートの選択 (Close Figure 8)
namespace Samples.Controls
{
  public class ListViewItemCreatingEventArgs : EventArgs
  {
    private int _dataItemIndex;
    private int _displayIndex;

    public ListViewItemCreatingEventArgs(int dataItemIndex,
                                         int displayIndex) {
      _dataItemIndex = dataItemIndex;
      _displayIndex = displayIndex;
    }

    public int DisplayIndex {
      get { return _displayIndex; }
      set { _displayIndex = value; }
    }

    public int DataItemIndex {
      get { return _dataItemIndex; }
      set { _dataItemIndex = value; }
    }
  }

  public class ListView : System.Web.UI.WebControls.ListView
  {
    public event EventHandler<ListViewItemCreatingEventArgs>
     ItemCreating;

    private int _displayIndex;
    private bool _shouldInstantiate = false;

    protected override void InstantiateItemTemplate(Control container,
     int displayIndex) {
      if (_shouldInstantiate) {
        base.InstantiateItemTemplate(container, displayIndex);
        _shouldInstantiate = false;
      }
    }

    protected override ListViewDataItem CreateDataItem(int
     dataItemIndex, int displayIndex) {
      // Fire a NEW event: ItemCreating
      if (ItemCreating != null)
        ItemCreating(this, new
          ListViewItemCreatingEventArgs(dataItemIndex,
          displayIndex));

      // Cache for later
      _displayIndex = displayIndex;

      // Call the base method
      return base.CreateDataItem(_dataItemIndex, displayIndex);
    }

    protected override void OnItemCreated(ListViewItemEventArgs e) {
      base.OnItemCreated(e);

      // You can proceed with template instantiation now
      _shouldInstantiate = true;
      InstantiateItemTemplate(e.Item, _displayIndex);
    }
  }
}

CreateDataItem メソッドは ItemCreating イベントを発生させ、後で使用するために表示インデックスをキャッシュします。また、InstantiateItemTemplate メソッドがオーバーライドされ、テンプレートの実際のインスタンス化が遅れています。これにはプライベートなブール型フラグが使用されています。前述のように、ListView は、項目テンプレートをインスタンス化した後でデータバインド処理を開始します。

ただし、図 8 のコードに示す実装では、ItemCreated イベントが発生するまで、項目テンプレートは実際にはインスタンス化されません。ItemCreated イベントが発生すると、データ項目オブジェクトは DataItem プロパティを通じて ListView 項目コンテナにバインドされます。以下のように、コードで ItemCreated イベントを処理することで、バインドされたデータ項目に基づいて使用する項目テンプレートを決定できます。

protected override void OnItemCreated(ListViewItemEventArgs e)
{
   base.OnItemCreated(e);

   _shouldInstantiate = true;
   InstantiateItemTemplate(e.Item, _displayIndex);
}

この場合、ベース メソッドが ItemCreated イベントをページに通知します。その後、カスタム ListView コントロールがブール型のフラグをリセットし、メソッドを呼び出して項目テンプレートをインスタンス化します。最後に、組み込みの ListView コントロールよりも若干後に項目テンプレートがインスタンス化されますが、ItemCreated イベント ハンドラ内で、バインドされたデータ項目の内容を参照した後に、各項目の ItemTemplate プロパティをプログラム的に設定できます (図 9 を参照)。図 10 にサンプル ページを示します。男性には青いテンプレートが使用され、女性にはピンクのテンプレートが使用されます。

Figure 9 項目テンプレートの設定 (Close Figure 9)
void ListView1_ItemCreated(object sender, ListViewItemEventArgs e)
{
    // Grab a reference to the data item
    ListViewDataItem currentItem = (e.Item as ListViewDataItem);
    Employee emp = (Employee) currentItem.DataItem;
    if (emp == null)
        return;

    // Apply your logic here
    string titleOfCourtesy = emp.TitleOfCourtesy.ToLower();
    string url = "forgentlemen.ascx";
    if (titleOfCourtesy == "ms." || titleOfCourtesy == "mrs.")
        url = "forladies.ascx";

    // Set the item template to use
    Samples.ListView ctl = (sender as Samples.Controls.ListView);
    ctl.ItemTemplate = Page.LoadTemplate(url);
}
Figure 10 A Standard Menu
Figure 10 A Standard Menu(画像を縮小するには、ここをクリックします)
Figure 10 A Standard Menu
Figure 10 A Standard Menu (画像を拡大するには、ここをクリックします)

まとめ

結局のところ、ASP.NET 3.5 の新しい ListView コントロールは、ASP.NET 1.0 から存在していた DataList コントロールを刷新したものです。ListView を使用すると、生成されるマークアップを詳細に制御し、データ ソース オブジェクトを完全にサポートすることができます。

このコラムでは、入れ子になった ListView コントロールを使用して、ページ分割可能な複数レベルのデータ ビューを構築する方法と、カスタム コントロールを派生させ、いくつかのメソッドをオーバーライドすることで、標準の ListView の表示処理を変更する方法について説明しました。最終的な結果として、複数の項目テンプレートをサポートするコントロールが得られました。ASP.NET の他のデータバインド コントロールでは、このような柔軟性は得られません。

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

Dino Esposito は、IDesign 社のアーキテクトであり、『Programming ASP.NET 3.5 Core Reference』の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。連絡先は cutting@microsoft.com (英語) ですが、weblogs.asp.net/despos のブログにも参加してみてください。


基礎: WCF でのコード アクセス セキュリティ (第 1 部)

基礎
WCF でのコード アクセス セキュリティ (第 1 部)
Juval Lowy

コンテンツ
CAS の概要
クライアント側の CAS
部分的に信頼されたクライアント
クライアント側の要求
基本的な WCF 要求
PartialTrustClientBase<T> を使用した構造化された要求
Invoke の要求を分析する
クライアント側の構造化要求を実装する

Microsoft® .NET Framework 1.0 で採用されたコード アクセス セキュリティ (CAS) は、アンマネージ コードと比較した場合の .NET の差別化機能の 1 つであると言えます。CAS は .NET Framework の基本構造に組み込まれており、マネージ コードのすべての操作に影響し、アンマネージ コードでは絶対に実現できないものです。

Windows® Communication Foundation (WCF) の最初のリリースでは CAS がサポートされていませんでした。System.ServiceModel アセンブリでは、部分的に信頼された呼び出し元が許可されていなかったため、CAS のサポートは無効になっていました。2 度目のリリースでは、一部の HTTP バインドで、限定的なシナリオでのみ基本的な CAS がサポートされました。この変更により、WCF プログラミング モデルや CAS に違反することなく、全面的に CAS をサポートする小規模なフレームワークを記述できるようになりました。

このコラムは CAS に関する 2 回構成のコラムです。その第 1 回では、WCF におけるコード アクセス セキュリティについて簡単に説明し、WCF サービスの部分的に信頼されたクライアントを有効にするためのソリューションを紹介します。

CAS の概要

.NET Framework では、24 種類の異なるセキュリティ アクセス許可が定義されており、これによってほぼすべての種類の操作が制御されています。たとえば、ファイル I/O、UI、リフレクション、セキュリティ、ネットワーク、データ アクセスなどに対するアクセス許可があります。特定のリソースには、1 種類のアクセス許可が適用されます。たとえば、ファイル I/O のアクセス許可に関しては特定のファイルから読み取るためのアクセス許可があり、UI アクセス許可に関しては、特定の種類のウィンドウを表示するためのアクセス許可があります。完全に拒否したり (ファイル I/O 操作をまったく行うことができない)、完全に許可する (無制限のファイル I/O アクセスを実行できる) ことも可能です。

アクセス許可はアクセス許可セットにグループ化され、すべてのアセンブリに 1 つのセットが割り当てられます。.NET Framework では、FullTrust (すべてのアクセス許可を意味します) や Execution (CPU へのアクセスだけを許可します) など、いくつかの標準的なアクセス許可セットが定義されています。管理者は、.NET 構成ツールを使用してカスタム アクセス許可セットを作成できます。開発者は、カスタム アクセス許可セットをプログラム的に定義したり、アクセス許可セット ファイルを使用して定義したりできます。または、アプリケーションで必要なアクセス許可セットを使用した ClickOnce アプリケーションのマニフェストを定義することができます。

CLR では、アセンブリを読み込むときに各アセンブリにそのアクセス許可が割り当てられます。アセンブリには、その身元を証明する何らかの形の証拠に基づいて、これらのアクセス許可が与えられます。アセンブリの読み込み元を調べる起源ベースの証拠や (たとえば、グローバル アセンブリ キャッシュ (CAC) から読み込まれたすべてのコードには、完全な信頼が与えられます)、アセンブリ自身のいくつかの特徴 (厳密な名前など) を調べるコンテンツベースの証拠があります。

各アプリケーション ドメインには、アプリケーション ドメイン セキュリティ ポリシーと呼ばれるアクセス許可セットが必ず割り当てられ、そのアプリケーション ドメインに読み込まれるすべてのアセンブリは、そのアクセス許可セットに制限されます。それ以外のアセンブリではセキュリティ例外が発生します。新しいアプリケーション ドメインは FullTrust アクセス許可セット付きで起動され、ローカル マシンを起源とするすべてのコードには既定で FullTrust が許可されるため、ほとんどの .NET ベースのアプリケーションはそのまま動作し、実際には CAS をまったく使用しません。これにより、コード (およびユーザー、データ、マシン、ネットワーク) は、ウイルスやワームから通常のユーザーのミスに至るまで、さまざまな問題の影響を受けやすくなります。

完全な信頼よりも低いレベルのアクセス許可で実行されるコードは、部分的に信頼されたコードと呼ばれます。マネージ コードの一部がリソースにアクセスしようとしたり、.NET Framework を使用して何らかの操作を行おうとしたりすると (アンマネージ コードとの相互動作を含む)、そのコードが含まれているアセンブリにその操作を実行するために必要なアクセス許可があるかどうかが .NET Framework によって確認されます。そのアセンブリに要求されたアクセス許可がないと、.NET Framework によってセキュリティ例外がスローされ、操作が中止されます。

悪意のある信頼性の低いアセンブリから信頼されているアセンブリが呼び出され、信頼性の低いアセンブリには実行のためのアクセス許可がない操作が実行される可能性があるため、操作を行おうとしているアセンブリのアクセス許可を要求するだけでは不十分です。そのため、スタック ウォークにより呼び出し元のスタック全体をたどり、スタック上のすべての呼び出し元が必要なアクセス許可を持っていることが確認されます。このスタック ウォークはセキュリティ要求と呼ばれ、実行しているアセンブリのアクセス許可にかかわらず実行されます。

コードはアクセス許可をアサート (つまり、スタックの上位のすべての呼び出し元が要求されるアクセス許可を持っていることを宣言) することができます。アクセス許可のアサーションには、スタック ウォーク動作を停止する効果があります。コードがアサートできるのは自身が既に持っているアクセス許可だけであり、特別なセキュリティ アサーション アクセス許可が追加で必要になります。

あるアクセス許可をアサートするときは、代わりに別のアクセス許可を要求することをお勧めします。開発者は、専用のアクセス許可クラスを使用するか、対応する属性セットを使用して、アクセス許可をプログラム的に要求またはアサートできます。また、アセンブリ レベル、クラス レベル、メソッド レベルで、アクセス許可を積極的に拒否することもできます。アクセス許可を拒否したり、コードを実行するのに必要な一部のアクセス許可のセットだけを許可したりすることで、おびき寄せ攻撃の危険性が減少します。コード アクセス セキュリティの詳細については、拙著『Programming .NET Components』第 2 版の第 12 章を参照してください。そこでは、100 ページ以上を費やしてこの基本的なテクノロジとその応用について説明しています。

クライアント側の CAS

.NET Framework 3.5 の WCF では、部分的な信頼で実行できるシナリオは限定されています。WCF では、部分的な信頼の下で HTTP バインド BasicHttpBinding、WSHttpBinding、WebHttpBinding (WSDualHttpBinding を除く) しか呼び出すことができず、セキュリティはまったく使用できないか、トランスポート セキュリティだけを使用できます。さらに、WSHttpBinding では、メッセージ セキュリティ、信頼性の高いメッセージング、トランザクションなどの機能を使用できません。部分的な信頼に対応しているすべてのバインドは、テキスト エンコードを使用する必要があります。クライアントから診断などの追加の WCF 機能を使用することはできません。部分的に信頼された環境で使用できるように、System.ServiceModel アセンブリでは、AllowPartiallyTrustedCallers 属性をアセンブリ定義に追加することで、部分的に信頼された呼び出し元を使用できます。

[assembly: AllowPartiallyTrustedCallers]

WCF の最初のリリースでは、この属性を省略すると、部分的な信頼のすべての使用ができなくなりました。.NET Framework 3.5 では、サポートされている機能の使用を一部だけに制限するのはバインドで行うようになりました。HTTP でない各バインドは、それがクライアント プロキシであれサービス ホストであれ、呼び出し元に対し完全な信頼を自発的に要求します。許可された HTTP バインド自身は完全な信頼を要求しませんが、使用するコンテキストに応じてアクセス許可を要求します。クライアント側では、これらのバインドは、実行アクセス許可 (実行フラグ付きのセキュリティ アクセス許可) と、サービスに接続するアクセス許可 (ターゲット URI への接続フラグ付きの Web アクセス許可) を要求します。

クライアント上では、これらの要求以外にも構成に対する制限があります。たとえば、構成ファイルには証明書ストアの参照を含めることができません (クライアント側の証明書)。証明書ストアにアクセスするには、WCF によって完全な信頼が要求されるためです。

WCF の機能を最大限に活用し、そのすべてを CAS のトレードオフなく (つまり完全な信頼を使用せずに) 行うことができるのが理想です。そのような機能としては、分散トランザクションから、信頼性の高い呼び出し、さまざまな種類のセキュリティ証明書、TCP チャネルや名前付きパイプなどのプロセス間通信 (IPC) チャネル上のイントラネット (または同じマシン上の) アプリケーション通信に至るまで、さまざまなものがあります。

部分的に信頼されたクライアント

部分的な信頼レベルがどのようなレベルでもクライアントから WCF のすべての機能とバインドを使用できるようにするには、バインドによる完全な信頼の要求を禁止する必要があります。そのための唯一の方法は、プロキシ自身が完全な信頼をアサートすることです。完全な信頼のアサートは PermissionSetAttribute を通じて行い、SecurityAction 列挙の Assert フラグを使用し、アクセス許可名としてタイプセーフな文字列 "FullTrust" を指定します。

[PermissionSet(SecurityAction.Assert,Name = "FullTrust")]

アクセス許可名は文字列で指定しますが、許可される値がコンパイラによって確認されるという点で、ある程度の安全性が追加されます。

また、ClientBase<T> の基本クラスのすべてのメソッドにクライアントが直接アクセスするのを防ぐ必要があります (完全な信頼が要求されます)。そのため、プロキシは、一般的に使用されるメソッド Close および Dispose を隠蔽する必要があります。プロキシは完全な信頼をアサートするため、プロキシ クラス自身が ClientBase<T> のメソッドやプロパティ (Channel またはコンストラクタ) にアクセスするのは問題ありません。

問題は、完全な信頼をアサートするために、プロキシ自体に完全な信頼が許可されていることが必要なことです。これは、部分的に信頼されたクライアントがそもそも提供できないものです。そのため、プロキシ クラスを別のアセンブリにし、public を指定して、そのアセンブリに完全な信頼を与える必要があります。これを実現するためには、コントロール パネル アプレット [.NET Framework 2.0 構成] を使用して、厳密な名前などのコンテンツベースの証拠を使用してプロキシのアセンブリを識別し、アセンブリに完全な信頼を与えます。

プロキシ アセンブリをクライアントの GAC にインストールすることもできます。GAC から実行されるすべてのアセンブリには完全な信頼が与えられるため、プロキシにも完全な信頼が与えられます。AllowPartiallyTrustedCallers 属性も忘れずに宣言する必要があります。これにより、部分的に信頼された呼び出し元がアセンブリを呼び出すことができます。

最後に、プロキシが使用するコントラクトの定義をプロキシのアセンブリに追加する必要があります (また、コントラクトを public に設定します)。これが必要なのは、呼び出しチェーンの上位にあるすべてのアセンブリの完全な信頼が WCF によって要求され、コントラクトが部分的に信頼されたアセンブリに含まれていると要求が失敗するためです。図 1 に、これらのコントラクトとプロキシ定義の例を示します。

Figure 1 プロキシによる完全な信頼のアサート (Close Figure 1)
[assembly: AllowPartiallyTrustedCallers]

[ServiceContract]
public interface IMyContract
{
  [OperationContract]
  void MyMethod();
}

[PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
public class MyContractClient :
  ClientBase<IMyContract>,IMyContract,IDisposable
{
  public MyContractClient() {}

  public MyContractClient(string endpointName) : base(endpointName) {}

  /* More constructors */

  public void MyMethod() {
    Channel.MyMethod();
  }
  public new void Close() {
    base.Close();
  }
  void IDisposable.Dispose() {
    Close();
  }
}

クライアント側の要求

残念なことに、図 1 に示した手法は、セキュリティ違反になる可能性があります。WCF のセキュリティ要求を抑制することで、部分的に信頼されたクライアントは、任意の WCF サービスを呼び出すことができるようになります。たとえば、TCP ソケットまたは Web サイトに接続するアクセス許可を持っていないクライアントがあるとします。そのクライアントはネットワークを直接使用できませんが、制限されたクライアント環境から WCF 経由で呼び出すことで、その制限を迂回できます。

その問題に対する解決策は、完全な信頼に対して空の WCF 要求をアサートする一方、クライアントが行おうとしている操作に応じて、適切な個別のセキュリティ アクセス許可を要求する、ClientBase<T> の専用のサブクラスを使用することです。そのようなプロキシ クラスが、図 2 に示す PartialTrustClientBase<T> クラスです。

Figure 2 PartialTrustClientBase<T> クラス (Close Figure 2)
public abstract class PartialTrustClientBase<T> :
  ClientBase<T>,IDisposable
  where T : class
{
  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  public PartialTrustClientBase() {}

  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  public PartialTrustClientBase(string endpointName) :
    base(endpointName) {}

  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  public PartialTrustClientBase(Binding binding,
    EndpointAddress remoteAddress) :
    base(binding,remoteAddress) {}

  //Useful only for clients that want full-brunt raw demands from WCF
  protected new T Channel
  {
    [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
    get {
      return base.Channel;
    }
  }

  [PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
  new public void Close() {
    base.Close();
  }
  void IDisposable.Dispose() {
    Close();
  }

  protected object Invoke(string operation,params object[] args) {
    if(IsAsyncCall(operation)) {
      DemandAsyncPermissions();
    }
    DemandSyncPermissions(operation);
    CodeAccessSecurityHelper.PermissionSetFromStandardSet(
      StandardPermissionSet.FullTrust).Assert();

    Type contract = typeof(T);
    MethodInfo methodInfo = contract.GetMethod(operation);
    return methodInfo.Invoke(Channel,args);
  }

  protected virtual void DemandAsyncPermissions() {
    CodeAccessSecurityHelper.DemandAsyncPermissions();
  }

  protected virtual void DemandSyncPermissions(string operationName) {
    this.DemandClientPermissions(operation);
  }

  bool IsAsyncCall(string operation) {
    if(operation.StartsWith("Begin")) {
      MethodInfo info = typeof(T).GetMethod(operation);
      object[] attributes = info.GetCustomAttributes(
        typeof(OperationContractAttribute),false);
      Debug.Assert(attributes.Length == 1);
      return (attributes[0] as
        OperationContractAttribute).AsyncPattern;
    }
    return false;
  }
}

PartialTrustClientBase<T> は、通常のプロキシ基本クラスに似ています。この場合でも、そこから派生したプロキシ クラスに対して完全な信頼を許可し、部分的に信頼された呼び出し元を許可する必要があります。ただし、図 1 と異なり、PartialTrustClientBase<T> は、クラス レベルで完全な信頼をアサートしません。代わりに、必要なときにだけローカルに完全な信頼をアサートします。また、PartialTrustClientBase<T> は、他の CAS アクセス許可を要求するためにも使用できます。

プロキシ クラスを PartialTrustClientBase<T> から派生させ、ここに示すようにプロキシで完全な信頼をアサートする場合、呼び出し側のクライアント上では要求が行われません。

[PermissionSet(SecurityAction.Assert,Name = "FullTrust")]
public class MyContractClient :
  PartialTrustClientBase<IMyContract>,IMyContract
{
  public MyContractClient() {}

  public MyContractClient(string endpointName) :
    base(endpointName) {}

  public void MyMethod() {
    Channel.MyMethod();
  }
}

このコードと図 1 の唯一の違いは、新しいバージョンの方が単純明快であることです。Close と Dispose の隠蔽が PartialTrustClientBase<T> で行われるようになったためです。この新しいプロキシでも、WCF によるすべてのセキュリティ要求が抑制され、部分的に信頼されたクライアントがおびき寄せ攻撃の危険にさらされ、部分的に信頼されたクライアントが意図した操作以上の操作を行うことができます。

基本的な WCF 要求

よりセキュリティに配慮した PartialTrustClientBase<T> の使用方法は、プロキシで完全な信頼をアサートしないことです。

public class MyContractClient :
  PartialTrustClientBase<IMyContract>,IMyContract
{
  public MyContractClient() {}
  public MyContractClient(string endpointName) :
    base(endpointName) {}
  public void MyMethod() {
    Channel.MyMethod();
  }
}

そのような使い方をサポートするため、PartialTrustClientBase<T> はそのコンストラクタと Close メソッドに対して完全な信頼をアサートします。さらに、PartialTrustClientBase<T> は ClientBase<T> の Channel プロパティを隠蔽し、get アクセサに対する完全な信頼をアサートします。これは、完全な信頼に対する WCF バインドの要求を抑止するのに十分です。要求はプロキシを構築するときと、チャネルをオープンおよびクローズするときに行われ、実際に使用するときには行われないためです。このようなプロキシ構成による効果として興味深いのは、クライアント コードが基本的な WCF セキュリティ要求 (すなわち、サービスの呼び出しをマーシャリングするために必要なすべてのセキュリティ要求) に支配されることです。

たとえば、プロキシが TCP バインドを使用している場合、プロキシはまず実行のためのクライアント アクセス許可を要求します (すべてのマネージ コードでそのアクセス許可が必要です)。次にプロキシは、クライアントがサービス マシン上の必要なポートに接続するためのアクセス許可と、無制限の DNS アクセス許可 (ホストのアドレスを解決するために必要) を要求します。さらに、TCP の使用とは無関係に、呼び出しのコンテキストに関係する、付帯的なアクセス許可要求がいくつかあります。

クライアントが Windows セキュリティを使用して、ユーザーが対話で使用する ID を送信する必要がある場合、プロキシは USERNAME 変数に対する環境アクセス許可を要求します。また、クライアントが代替 Windows 資格情報を送信する必要がある場合、プロキシはプリンシパルを制御するためのセキュリティ アクセス許可を要求します。非同期で呼び出しをディスパッチしたり、二重のコールバックを受信する必要がある場合、プロキシはポリシーと証拠を制御するためのアクセス許可を要求します (どちらもセキュリティ アクセス許可のフラグであり、スレッド間で呼び出しを行うときに必要です)。クライアントが信頼性の高いメッセージングを使用する必要がある場合、プロキシはポリシー制御も要求します。ユーザー名の資格情報を使ったメッセージ セキュリティを使用する場合で、サービス証明書のネゴシエーションを行い、ネゴシエート済みのサービス証明書の検証を行わない場合は、プロキシはポリシーと証拠を制御するためのアクセス許可も要求します。クライアントが WCF 診断機能とトレース機能を利用する場合は、プロキシは COMPUTERNAME 環境変数へのアクセスと (追跡のため)、アンマネージ コード アクセスを要求します (おそらくログ ファイルにアクセスするためで、ファイル I/O アクセス許可であるべきです)。

最後に、プロキシはアンマネージ コードを実行するためのクライアント アクセス許可も要求します。アンマネージ コード アクセスは、高度な特権が必要なセキュリティ アクセス許可であり、最も信頼できるコードにのみ許可されます。アンマネージ コード アクセス許可を与えると CAS が無効になります。これは、アンマネージ コードが CAS から除外されるためです。部分的に信頼された環境で動作するように設計されたクラスとフレームワークは、アンマネージ コード アクセスを要求せず (相互運用機能を使用する場合でも)、実行するアンマネージ操作の性質を示す、より限られたアクセス許可を要求する必要があります。TCP チャネルは、元々の設計で部分的に信頼されたクライアントを考慮していないため、アンマネージ コードを要求します。

WCF の機能の中には、完全に一致するアクセス許可の種類があるにもかかわらず、完全な信頼要求を使用するものがあります。トランザクションの伝達では完全な信頼が必要になります (分散トランザクション アクセス許可は使用されません)。また、証明書ストアへのアクセスでも完全な信頼が必要になります (証明書ストアのアクセス許可は使用されません)。図 3 は、バインドが既定の設定になっている場合の、トランザクション、信頼性、診断、非同期呼び出し、証明書ストア アクセス、メッセージ セキュリティなどのいくつかの主要なシナリオに対する、最後に変更した PartialTrustClientBase<T> のようなコードから実行された基本的な WCF セキュリティ要求を示します。

Figure 3 クライアント側の基本的な WCF セキュリティ要求 (Close Figure 3)

シナリオ | アクセス許可
TCP | 実行とアンマネージ コードのセキュリティ アクセス許可、無制限の DNS、ターゲット ホスト上のポートに接続するためのソケット アクセス許可
IPC | 実行、アンマネージ コード、制御ポリシー、制御証拠のセキュリティ アクセス許可
WS および WS デュアル | 実行とアンマネージ コードのセキュリティ アクセス許可、URI に接続するための Web アクセス許可
基本および Web | 実行のセキュリティ アクセス許可、URI に接続するための Web アクセス許可
MSMQ | 実行とアンマネージ コードのセキュリティ アクセス許可
非同期呼び出し、TCP 上の二重化 | ポリシーと証拠を制御するためのセキュリティ アクセス許可
RM over TCP | ポリシーを制御するためのセキュリティ アクセス許可
対話型のユーザー資格情報の Windows セキュリティ | USERNAME を読み取るための環境アクセス許可
代替資格情報の Windows セキュリティ | プリンシパルを制御するためのセキュリティ アクセス許可、USERNAME を読み取るための環境アクセス許可
診断トレース | アンマネージ コードのセキュリティ アクセス許可、COMPUTERNAME を読み取るための環境アクセス許可
ユーザー名資格情報、サービス証明書ネゴシエーションを使用しサービス証明書の検証を使用しないメッセージ セキュリティ | ポリシーと証拠を制御するためのセキュリティ アクセス許可、証明書を列挙するためのストアのアクセス許可
ユーザー名資格情報、サービス証明書ネゴシエーションを使用しサービス証明書の検証を使用しない TCP メッセージ セキュリティ | ポリシーと証拠を制御するためのセキュリティ アクセス許可
任意の証明書ストア アクセス、トランザクション伝達 | 完全な信頼
ユーザー名資格情報、サービス証明書ネゴシエーションを使用しないかサービス証明書の検証を使用するメッセージ セキュリティ、証明書資格情報 | 完全な信頼

アンマネージ コード アクセスを要求しない唯一のバインドは、基本バインドと Web バインドです。WS バインドは、既定でメッセージ セキュリティになり、結果的としてアンマネージ コード アクセス許可が要求されます。

PartialTrustClientBase<T> を使用した構造化された要求

図 3 からわかるように、トランザクション、信頼性の高いメッセージング、無制限のメッセージ セキュリティ対話、および証明書ストア アクセスなどの WCF の主要な側面は、すべて完全な信頼が必要であり、以前のバージョンの PartialTrustClientBase<T> は、部分的に信頼された環境では無意味になります。さらに、事実上すべての非 HTTP バインド (およびメッセージ セキュリティを使用した WS バインド) によるアンマネージ コード アクセスは受け入れられず、部分的に信頼されたクライアントに対する CAS という概念そのものが否定されます。

部分的に信頼されたクライアントから、WCF を適切かつ安全に正しく使用できるようにするために、PartialTrustClientBase<T> には以下のように定義された Invoke メソッドがあります。

protected object Invoke(string operation,params object[] args);

Invoke は、実行する操作の名前とそのパラメータを、オブジェクトのコンマ区切りの params 配列として受け取ります。Invoke では、Channel プロパティを使用して呼び出しを実行するのではなく、リフレクションが使用されます。以下に、Invoke メソッドを使用した、PartialTrustClientBase<T> から派生するプロキシを示します。

public class MyContractClient :
  PartialTrustClientBase<IMyContract>,IMyContract
{
  public MyContractClient() {}
  public MyContractClient(string endpointName) :
    base(endpointName) {}
  public void MyMethod() {
    Invoke("MyMethod");
  }
}

Invoke はまず、呼び出し元クライアントのシナリオとターゲット サービス エンドポイントに応じて、適切な CAS アクセス許可を要求します。クライアントにこれらのアクセス許可が与えられている場合、すなわち要求によってセキュリティ例外が発生しなかった場合は、Invoke はプログラム的に完全な信頼をアサートし、クライアントがサービスを呼び出すための正しいアクセス許可を持っていれば、操作の実行に進みます。私は、このような動作を "構造化されたアクセス許可要求" と呼んでいます。

Invoke では、宣言的に完全な信頼をアサートするための属性は使用できません。これは、Invoke が要求するより詳細なアクセス許可がマスクされるためです。その代わりに、図 2 の Invoke の実装は、まず呼び出しが非同期的に行われるかどうかを確認します (操作コントラクトの AsyncPattern フラグを使用します)。非同期的に呼び出される場合、Invoke はヘルパ メソッド DemandAsyncPermissions を使用して、適切なアクセス許可を要求します。次に Invoke は、ヘルパ メソッド DemandSyncPermissions を使用して、同期アクセス許可を要求します。呼び出し自体に対しては、Invoke は、図 4 に示す CodeAccessSecurityHelper クラスを使用して、完全な信頼をプログラム的にアサートします。

Figure 4 CodeAccessSecurityHelper クラス (Close Figure 4)
public enum StandardPermissionSet {
  Internet,
  LocalIntranet,
  FullTrust,
  Execution,
  SkipVerification
}

public static class CodeAccessSecurityHelper {
  public static PermissionSet PermissionSetFromStandardSet(
    StandardPermissionSet standardSet);

  public static void DemandClientPermissions<T>(this ClientBase<T> proxy)
    where T : class;

  public static void DemandAsyncPermissions();

  //More members

PermissionSetFromStandardSet メソッドは、標準的な .NET のアクセス許可セットを表す列挙値を受け取り、PermissionSet の対応するインスタンスを返します (図 5 を参照)。PermissionSet は、その名のとおりアクセス許可のコレクションですが、スーパー アクセス許可セットである完全な信頼も表します (これは、実際には個別のアクセス許可のセットではなく、単一のアクセス許可に近いものです)。PermissionSet クラスは IStackWalk インターフェイスをサポートし、スタック上のすべての呼び出し元がこれらのアクセス許可を持っていることをアサートすることで、アクセス許可セット内のアクセス許可の要求を停止する修飾子など、スタック ウォーク修飾子を設定できます。スタック ウォーク修飾子は、それを設定したメソッドが返ると自動的に削除されます。PermissionSet の静的なメソッド RevertAssert で明示的に削除することもできます。

Figure 5 PermissionState と PermissionSet (Close Figure 5)
public enum StandardPermissionSet {
  Internet,
  LocalIntranet,
  FullTrust,
  Execution,
  SkipVerification
}

public static class CodeAccessSecurityHelper {
  public static PermissionSet PermissionSetFromStandardSet(
    StandardPermissionSet standardSet);

  public static void DemandClientPermissions<T>(this ClientBase<T> proxy)
    where T : class;

  public static void DemandAsyncPermissions();

  //More members

PartialTrustClientBase<T> のヘルパ メソッド DemandAsyncPermissions と DemandSyncPermissions のどちらも、CodeAccessSecurityHelper に対応するメソッドを使用してその要求を実行します。

図 6 の表は、使用したバインドの作用として PartialTrustClientBase<T>.Invoke によって発行された構造化要求と、トランザクション、信頼性、証明書ストア アクセス、診断、コールバック、非同期呼び出しの使用など、シナリオのその他の面を示します。

Figure 6 PartialTrustClientBase<T> の構造化されたセキュリティ要求 (Close Figure 6)

シナリオ | アクセス許可
TCP | 実行と無制限の DNS のセキュリティアクセス許可、ターゲット ホスト上のポートに接続するためのソケット アクセス許可
IPC | 実行、制御ポリシー、制御証拠のセキュリティ アクセス許可
WS、基本 | 実行のセキュリティ アクセス許可と URI に接続するための Web アクセス許可
WS および WS デュアル | 実行のセキュリティ アクセス許可と URI に接続するための Web アクセス許可、コールバック アドレスに対するコールバックを受け付けるための Web アクセス許可、最低限の ASP.NET ホスティング アクセス許可
MSMQ | 実行のセキュリティ アクセス許可、キューに送信するための MSMQ アクセス許可
RM over TCP | ポリシーを制御するためのセキュリティ アクセス許可
TCP 上の二重化、非同期呼び出し (AsyncPattern)、ユーザー名資格情報、サービス証明書ネゴシエーションを使用しサービス証明書の検証を使用しないメッセージ セキュリティ | ポリシーと証拠を制御するためのセキュリティ アクセス許可
対話型のユーザー資格情報の Windows セキュリティ | USERNAME を読み取るための環境アクセス許可
代替資格情報の Windows セキュリティ | プリンシパルを制御するためのセキュリティ アクセス許可
トランザクション伝達 | 無制限の分散トランザクション アクセス許可
ユーザー名資格情報、サービス証明書ネゴシエーションを使用しないかサービス証明書の検証を使用するメッセージ セキュリティ、証明書資格情報 | ストアの列挙、ストアのオープン、証明書の列挙のためのストア アクセス許可
診断トレース | COMPUTERNAME を読み取るための環境アクセス許可、パス検出、追加、ログ ファイルへの書き込みのためのファイル I/O アクセス許可、

Invoke の要求を分析する

構造化された要求は、いくつかの要素に基づいて作成されています。まず、WCF によって発生する、クライアントに対する基本的な要求にできるだけ近づけるようにしてあります。そのため、WCF の荒削りな部分を滑らかにしました。これは、WCF が部分的な信頼で幅広く使用するために設計されていなかったためです。図 6 に示すバインドとシナリオは、完全な信頼やアンマネージ コード アクセスを要求しません。.NET Framework には、同様のコンテキストで部分的な信頼環境で使用するために設計された箇所が多数あり、ここでは、それらと同じ要求を基にしました。最後に、それ以外の場合には、完全な信頼の要求の抑制を埋め合わせるために、経験、CAS に関する知識、および常識を使用して、WCF アクティビティを、専用のアクセス許可タイプの要求にマッピングしました。

WS デュアル バインドを使用する場合、Invoke は、他の HTTP バインドと同様に、ターゲット エンドポイントに接続するための Web アクセス許可を要求します。ただし、コールバック オブジェクトのホストを可能にし、コールバック アドレスに対する呼び出しを受け付けるために、最低限の ASP.NET ホスティング アクセス許可と Web アクセス許可を要求します。MSMQ バインドを使用する場合、ターゲット キューにメッセージを送信するために、Invoke は MSMQ アクセス許可を要求します。

クライアントがトランザクションをサービスに伝達しようとすると、Invoke は無制限の分散トランザクション アクセス許可を要求します。この場合、トランザクション対応のバインドが使用され、トランザクション フローがそのバインド内で有効になり、操作レベルのトランザクション フローが許可され、クライアントはアンビエント トランザクションを持ちます。

プロキシが証明書ストアへのアクセスを試みると、証明書ストアを列挙し、ストアをオープンし、ストア内の証明書を列挙するためのアクセス許可が要求されます。これは、クライアントが証明書資格情報を使用する場合や、メッセージ セキュリティが使用され、クライアントがネゴシエートされたサービス証明書を必要とする場合、またはクライアントが証明書をネゴシエートせず、メッセージのセキュリティを高めるために使用する証明書を読み込む場合に起こります。

クライアントが WCF 診断を使用する場合、Invoke はコンピュータ名を読み取るための環境アクセス許可と、使用するログ ファイルとトレース ファイルに対するファイル I/O 許可を要求します。

クライアント側の構造化要求を実装する

既に説明したように、要求は CodeAccessSecurityHelper によって実行されます。その実装の一部を図 7 に示します。.NET Framework 内のすべてのアクセス許可タイプは、IPermission インターフェイスをサポートします。

Figure 7 CodeAccessSecurityHelper の実装 (抜粋) (Close Figure 7)
public static class CodeAccessSecurityHelper
{
  public static void DemandClientPermissions<T>(this ClientBase<T> proxy,
    string operationName) where T : class
  {
    DemandClientConnectionPermissions(proxy.Endpoint);
    DemandTransactionPermissions(proxy.Endpoint,operationName);
    DemandTracingPermissions();
    DemandClientSecurityPermissions(proxy);
    DemandEnvironmentPermissions(proxy);
    DemandClientStorePermissions(proxy.Endpoint);
  }

  internal static void DemandClientConnectionPermissions(
    ServiceEndpoint endpoint)
  {
    PermissionSet connectionSet = new PermissionSet(PermissionState.None);

    if(endpoint.Binding is NetTcpBinding)
    {
      connectionSet.AddPermission(new SocketPermission(
        NetworkAccess.Connect,TransportType.Tcp,
        endpoint.Address.Uri.Host,endpoint.Address.Uri.Port));

      connectionSet.AddPermission(new DnsPermission(
        PermissionState.Unrestricted));
    }

    /* Rest of the bindings */

    connectionSet.Demand();
  }

  internal static void DemandTransactionPermissions(
    ServiceEndpoint endpoint)
  {
    DemandTransactionPermissions(endpoint,null);
  }

  internal static void DemandTransactionPermissions(
    ServiceEndpoint endpoint, string operationName)
  {
    bool transactionFlow = false;
    bool flowOptionAllowed = false;

    if(endpoint.Binding is NetTcpBinding)
    {
      NetTcpBinding tcpBinding = endpoint.Binding as NetTcpBinding;
      transactionFlow = tcpBinding.TransactionFlow;
    }

    /* Checking other bindings */

    if(transactionFlow)
    {
      if(Transaction.Current != null)
      {
        //If operationName is null then at least one operation
        //needs to allow flow to trigger demand
        foreach(OperationDescription operation in
          endpoint.Contract.Operations)
        {
          string name = operationName ?? operation.Name;
          if(name != operation.Name)
          {
            continue;
          }
          foreach(IOperationBehavior behavior in operation.Behaviors)
          {
            if(behavior is TransactionFlowAttribute)
            {
              TransactionFlowAttribute attribute =
                behavior as TransactionFlowAttribute;
              if(attribute.Transactions !=
                TransactionFlowOption.NotAllowed)
              {
                flowOptionAllowed = true;
                break;
              }
            }
          }
          if(flowOptionAllowed)
          {
            break;
          }
        }
        if(flowOptionAllowed)
        {
          IPermission distributedTransactionPermission =
            new DistributedTransactionPermission(
            PermissionState.Unrestricted);
          distributedTransactionPermission.Demand();
        }
      }
    }
  }
  //Rest of the implementation
}

public interface IPermission : ...
{
   void Demand();
   //More members
}

アクセス許可を要求するには、必ずアクセス許可オブジェクトをインスタンス化し、Demand メソッドの実装を呼び出します。アクセス許可セットを作成する際、個別のアクセス許可を追加して、アクセス許可セットに対して Demand を呼び出し、そのセット内のすべてのアクセス許可を要求することができます。

CodeAccessSecurityHelper の拡張メソッド DemandClientPermissions は、PartialTrustClientBase<T> の代わりに要求の大部分を実行します (拡張を使用すると、どのプロキシ クラスでも使用できるようになります)。DemandClientPermissions には、それぞれの側面に対するアクセス許可を要求する一連のヘルパ メソッドがあります。図 7 は、DemandClientConnectionPermissions のコードを示します。これは、バインドに応じて接続アクセス許可を要求するために使用します。このメソッドは、プロキシで使用されるバインドの種類を調べ、適切なアクセス許可をアクセス許可セット オブジェクトに追加します。その後、アクセス許可セットに対して Demand を呼び出します。

DemandTransactionPermissions メソッドはまず、プロキシがトランザクション フローに対応しているバインドを使用していることを確認します。使用している場合は、呼び出し元のクライアントに、実際に伝達すべきアンビエント トランザクションがあるかどうか (Transaction.Current が NULL でないこと) を確認します。アンビエント トランザクションがある場合、エンドポイントのコントラクトの操作のコレクションをスキャンして、現在呼び出されている操作を探します。操作が見つかった場合、DemandTransactionPermissions は、その操作の動作のコレクションを取得します。DemandTransactionPermissions は、各動作を調べ、TransactionFlowAttribute を探します。この属性が、トランザクションの伝達を許可するように構成されている場合、DemandTransactionPermissions は分散トランザクション アクセス許可を要求します。同様にして、DemandClientPermissions は、他のヘルパ メソッドを使用して、適切なアクセス許可を要求します。

最後に、部分的に信頼されたクライアントとコールバックを有効にするため、クラス PartialTrustDuplexClientBase<T,C> を定義し、それを PartialTrustClientBase<T> とほぼ同じ内容で実装しました。1 つの例外は、コールバックを受け取るクライアントのために二重化のサポートを追加していることです。

これで、WCF におけるコード アクセス セキュリティと、適切なレベルのコードレベル アクセス許可を使用した、部分的に信頼されたクライアントを可能にするためのソリューションについての簡単な説明を終えます。次回は、部分的に信頼されたサービスと部分的に信頼されたホストについて説明します。また、WCF と .NET Framework の高度なプログラミング手法についても説明します。

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

Juval Lowy は IDesign 社に所属するソフトウェア アーキテクトで、WCF トレーニングと WCF アーキテクチャ コンサルティングに従事しています。彼は、シリコン バレー地域の Microsoft Regional Director も務めています。最近の著書としては、『Programming WCF Services』があります。連絡先は www.idesign.net (英語) です。


テストの実行: LINQ を使用して SQL ストアド プロシージャをテストする

テストの実行
LINQ を使用して SQL ストアド プロシージャをテストする
Dr. James McCaffrey

コンテンツ
テスト対象システム
テスト ハーネス
ハーネスを拡張する
より高速、よりコンパクト、より優れたデバッグ

バックエンド SQL Server® データベースにアクセスして操作するプログラムのテストが必要になることは、ごく一般的です。そのようなケースの多くでは、アプリケーションが SQL ストアド プロシージャを使用してバックエンド データと対話します。この種のシナリオにおいて、ストアド プロシージャをテスト対象システムの補助メソッドと見なすことができます。したがって、システムの他のモジュールと同様に、ストアド プロシージャをテストする必要があります。

SQL ストアド プロシージャのテストを実行するにはいくつかのアプローチがありますが、私は、LINQ を使用することでテストの自動化が飛躍的に容易になることを発見しました。今月のコラムでは、LINQ (具体的には LINQ to SQL プロバイダ) を使用して SQL ストアド プロシージャをテストする方法について説明します。この記事の内容は、C# および SQL に関して中級レベルのスキルを持ちながら LINQ の経験を持たない読者を対象にしています。

図 1 は、SQL ストアド プロシージャをテストするときに実行する必要のある主要なアクティビティをデモンストレーションするために私が作成した、小さな C# コンソール アプリケーション テスト ハーネスを示しています。ここでは、usp_DeleteMovie という名前の単純かつ代表的なストアド プロシージャをテストします。このストアド プロシージャは、dbMovies という名前の映画関連データのデータベースに含まれています。

Figure 1 Testing a Stored Procedure Using LINQ
Figure 1 Testing a Stored Procedure Using LINQ(画像を縮小するには、ここをクリックします)
Figure 1 Testing a Stored Procedure Using LINQ
Figure 1 Testing a Stored Procedure Using LINQ (画像を拡大するには、ここをクリックします)

それぞれのテスト ケースについて、テスト ベッド データベースを初期化し、特定の映画 ID を使って usp_DeleteMovie を呼び出し、最後に dbMovies の結果の状態が適切であるかどうかをチェックします。図 1 を見てもおわかりにならないと思いますが、私は LINQ を使用することで、ADO.NET と T-SQL を使用した場合よりも簡単にテスト ハーネスを作成しています。以降のセクションでは、ダミーの dbMovies データベースとテスト対象の usp_DeleteMovie ストアド プロシージャについて説明します。続いて、コードを示し、使用されている LINQ 手法を説明します。最後に、このコラムで示した例を各自の要件に応じて変更および拡張する方法についても説明します。

テスト対象システム

ハーネスについて掘り下げる前に、テスト対象システムのサンプル データベースを見て、テストの自動化を理解しておきましょう。一般的な開発環境において、開発者、テスト担当者、およびデータベース アーキテクトは、通常、各自が持つバックエンド データベースのローカル コピーを操作します。ここで、テスト対象データベースの現在のバージョンを作成する SQL スクリプトを使用できると仮定しましょう。実際のデータベース スクリプトはサイズも大きく複雑になると思いますが、図 2 に示すサンプルのデータベース作成スクリプトは、主要概念を明確にするために単純化してあります。

Figure 2 データベース作成スクリプト (Close Figure 2)
use master
go

if exists(select * from sys.sysdatabases where name='dbMovies')
  drop database dbMovies
go

create database dbMovies
go

use dbMovies
go

create table tblMain
(
  movID char(3) primary key,
  movTitle varchar(35) null,
  movRunTime int null
)
go

create table tblPrices
(
  movID char(3) primary key,
  movPrice money
)
go

create procedure usp_DeleteMovie
  @movID char(3)
as
  delete from tblMain where movID = @movID
  delete from tblPrices where movID = @movID
go

create procedure usp_GetMovieDataByPrice
  @movPrice money
as
  select m.movID, m.movTitle, p.movPrice, m.movRunTime
  from tblMain as m
  inner join tblPrices as p
    on m.movID = p.movID
  where p.movPrice = @movPrice
go

create procedure usp_GetMoviePrice
  @movID char(3),
  @price money out
as
  declare @ct int
  select @price = movPrice from tblPrices where movID = @movID
  select @ct = count(*) from tblPrices where movID = @movID
  return @ct
go

データベースの名前は dbMovies で、このデータベースには tblMain と tblPrices の 2 つのテーブルが含まれています。tblMain テーブルには、(主キーである) 映画 ID 列、タイトル列、上映時間 (分数) が格納される列があります。tblPrices テーブルには、映画 ID 列と、映画料金が格納される列があります。

このデータベースには、usp_DeleteMovie、usp_GetMovieDataByPrice、および usp_GetMoviePrice の 3 つのストアド プロシージャが含まれています。これらがユーザー定義ストアド プロシージャであることを示し、システム ストアド プロシージャ (sp_) や拡張ストアド プロシージャ (xp_) と区別するために、ストアド プロシージャ名を usp_ で開始しています。ここで、1) usp_DeleteMovie はデータベースの状態を変更するが明示的に値を返さないこと、2) usp_GetMovieDataByPrice は 1 つまたは複数の SQL 行セットを返すが dbMovies の状態を変更しないこと、3) usp_GetMoviePrice は 2 つのスカラ値 (1 つは戻り値、1 つは out 型パラメータ) を返すがデータベースの状態を変更しないことに注意してください。

これらは、SQL ストアド プロシージャをテストするときの 3 つの最も一般的な状況 (戻り値を返さずに状態を変更する、行セットを返すが状態を変更しない、スカラ値を返すが状態を変更しない) を表しています。この 3 つのシナリオについては後で簡単に説明します。ここで、図 1 のテストの対象となっている usp_DeleteMovie ストアド プロシージャを詳しく見てみましょう。このプロシージャは、次に示すように、映画 ID を入力パラメータとして受け取り、tblMain テーブルと tblPrices テーブル内の対応する行を削除します。

create procedure usp_DeleteMovie
  @movID char(3)
as
  delete from tblMain where movID = @movID
  delete from tblPrices where movID = @movID

テストの自動化という観点から、ストアド プロシージャに関連付けられた dbMovies データベースを既知の状態に設定することで準備する方法を理解しておく必要があります。さらに、ストアド プロシージャを呼び出す方法と、ストアド プロシージャによってデータベースの状態が適切に変更されたかどうかを調べる方法についても理解しておく必要があります。後でわかりますが、これらのタスクは、LINQ を使用することで、以前のアプローチを使用した場合よりもはるかに簡単に実行できます。

テスト ハーネス

それでは、図 1 に示す出力を生成したテスト ハーネスのコードを見ていきましょう。全体の構造は、図 3 に示してあります。ここで説明するすべての例の完全なソース コードが、MSDN® Magazine Web サイトのこのコラムに付属するコード サンプルに含まれています。

Figure 3 テスト ハーネスの構造 (Close Figure 3)
using System;
using System.Linq;
using System.Text;
using System.Data.Linq;
using System.Security.Cryptography;

namespace Run
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        Console.WriteLine("\nBegin test automation using LINQ");
        string connString =
          "server=(local);database=dbMovies;Trusted_Connection=true";
        Console.WriteLine
          ("\nStored procedure under test = usp_DeleteMovie");
        DbMovies db = new DbMovies(connString);

        string[] testCaseData = new string[] {
          "TC001:002:85-A0-D9-28-7B-AA-8D-5F-4C-43-B5-B6-A5-99-F4-63",
          "TC002:003:44-FA-D2-38-49-98-F0-BD-16-BD-9E-B6-0C-F1-73-35" };

        foreach (string testCase in testCaseData)
        {
          // parse inputs, expected state
          // call ResetDatabase()
          // call usp_DeleteMovie
          // call GetDatabaseState()
          // determine pass/fail
        }
        Console.WriteLine("\n====================================");
        Console.WriteLine("\nEnd test run");
      }
      catch (Exception ex)
      {
        Console.WriteLine("Fatal: " + ex.Message);
      }
    }

    static void ResetDatabase(string connString) { . . . }
    static string GetDatabaseState(string connString) { . . . }
  }
}

LINQ は、Visual Studio® 2008 に含まれている Microsoft® .NET Framework 3.5 の一部です。私は、Visual Studio 2008 を使用して、.NET Framework 3.5 をターゲットとする C# コンソール アプリケーションを作成しました。このとき、Visual Studio によって生成された "using System.Collections.Generic" ステートメントを削除しました。その理由は、私のハーネスではこの名前空間に含まれるメソッドを使用していないからです。Visual Studio では、既定により、System.Linq 名前空間に対する using ステートメントが追加されていることに注意してください。ここでは、System.Data.Linq アセンブリへの参照を追加し、対応する using ステートメントを追加してあります。

LINQ は、実際は、LINQ to SQL および LINQ to XML を含むテクノロジのコレクションを示すために使用される用語です。LINQ to SQL は、以前に DLINQ と呼ばれていたもので、今でもこの古い名称が多くのオンライン リファレンスで使用されています。LINQ を使用してストアド プロシージャをテストするために、System.Linq に含まれる基本 LINQ 機能と System.Data.Linq 名前空間に指定されている LINQ to SQL 機能の組み合わせを使用しています。さらに、データベースの状態を確認するために暗号化ハッシュ メソッドを使用するので、System.Security.Cryptography 名前空間を参照する using ステートメントも追加しました。先頭部分の WriteLine ステートメントの後には、テスト ベッド データベースに対する接続文字列を設定しました。

string connString =
  "server=(local);database=dbMovies;Trusted_Connection=true";

後ほど説明しますが、接続文字列をハーネスの補助ヘルパー メソッドに渡すようにすると便利です。その次にあるのが、重要な LINQ to SQL の概念です。ここでは、SQL データベースを表すオブジェクトをインスタンス化しています。

DbMovies db = new DbMovies(connString);

LINQ to SQL はマジックではないので、私のデータベースを自動的に理解してはくれません。テスト ハーネスの舞台裏として、DbMovies クラスを含む dbMovies データベースへのアクセスおよび操作に必要なすべての情報をカプセル化する C# コードを作成しました。では、このコードはどこで使用するのでしょう。

LINQ to SQL マッピング コードを入手するには、主に 3 つのアプローチがあります。sqlmetal.exe コマンド ライン ツールを使用する方法、Visual Studio 2008 オブジェクト リレーショナル デザイナ (O/R デザイナ) GUI ツールを使用する方法、およびマッピング コードを最初から作成する方法です。私自身は、柔軟性があり自動化に対応する sqlmetal.exe ツールを好んで使用します。ここでは、C# ラッピング コードを生成するために、Visual Studio 2008 コマンド シェルを起動し、テスト ハーネス プロジェクトのルート ディレクトリに移動し、次のコマンドを入力しています。

> sqlmetal.exe /server:(local) /database:dbMovies /sprocs
 /code:mapping.cs

ここでは、既定の Windows® 認証モードを使用して dbMovies データベースに接続していますが、sqlmetal.exe は必要に応じてユーザーおよびパスワード情報を受け取ることができます。さらに、/sprocs コマンド ライン スイッチを指定して、sqlmetal.exe でストアド プロシージャを dbMovies にラップするコードを生成することを指定し、/code スイッチを使用して、O/R デザイナ ツールによって生成されるような中間 .dbml (データベース マークアップ言語) ファイルではなくソース コードを出力することを指定しています。

mapping.cs ファイルを生成した後、ファイルを Visual Studio プロジェクトに追加します。mapping.cs ファイルには、SQL データベースにアクセスするために必要なすべてのラッピング コード (接続文字列を受け取るコンストラクタを含む DbMovies クラスなど) が格納されています。

public partial class DbMovies : System.Data.Linq.DataContext
{
  public DbMovies(string connection) :
    base(connection, mappingSource)
  {
    //...
  }
  // wrappers to tables, stored procedures, etc.
}

図 4 は、テスト ハーネスとテスト ベッド データベースの関係を簡単に示した図です。テスト ハーネスで DbMovies オブジェクトをインスタンス化した後、usp_DeleteMovie ストアド プロシージャをテストするためのテスト ケース データをいくつか設定しています。

Figure 4 Relation between Database and Test Harness
Figure 4 Relation between Database and Test Harness(画像を縮小するには、ここをクリックします)
Figure 4 Relation between Database and Test Harness
Figure 4 Relation between Database and Test Harness (画像を拡大するには、ここをクリックします)

string[] testCaseData = new string[] {
  "TC001:002:85-A0-D9-28-7B-AA-8D-5F-4C-43-B5-B6-A5-99-F4-63",
  "TC002:003:44-FA-D2-38-49-98-F0-BD-16-BD-9E-B6-0C-F1-73-35" };

ここでは 2 つのテスト ケースしか使用していませんが、現実のテスト シナリオでは、数百または数千のテスト ケースを使用する状況も考えられます。この例では、わかりやすくするために、テスト ケース データをテスト ハーネスに直接埋め込んでいます。フラット テキスト ファイル、XML ファイル、SQL テーブル、またはその他のデータ ストアの形式で外部に保存したデータを使用することもできます。一般に、外部テスト ケース データには、内部テスト ケース データよりも変更や共有が簡単であるという利点があります。

このテスト ケース データは、コロン文字で区切られた 3 つのフィールドから構成されています。最初のフィールドはテスト ケース ID です。2 つ目のフィールドは映画 ID を表し、テスト対象の usp_DeleteMovie ストアド プロシージャへの入力として使用されます。3 つ目のフィールドは、16 バイトの MD5 暗号化ハッシュ値を表す文字列であり、テスト対象のストアド プロシージャが呼び出された後のデータベースの予想される状態を表します。これについて、簡単に説明します。ここでは、テスト ケース データを使用して、メインの処理ループを制御しています。

foreach (string testCase in testCaseData)
{
  // parse inputs, expected state
  // call ResetDatabase()
  // call usp_DeleteMovie
  // call GetDatabaseState()
  // determine pass/fail
}

メインの処理ループの内部で、String.Split メソッドを使用して、テスト ケース データ文字列を個々の文字列変数に解析します。

Console.WriteLine("\n====================================\n");
string[] data = testCase.Split(':');
string caseID = data[0];
string movieID = data[1];
string expectedState = data[2];
Console.WriteLine("Test case ID = " + caseID);
Console.WriteLine("\nID of movie to delete = " + movieID);

ここで、テスト対象のストアド プロシージャを呼び出す前に、データベースを既知の状態に設定する必要があります。これは、SQL ストアド プロシージャ (および一般にデータベース) をテストする場合の基本的な原則です。多くのストアド プロシージャは、1 つまたは複数のテーブルのデータを挿入、削除、または更新することにより、関連付けられているデータベースの状態を変更します。ストアド プロシージャが適切に動作することを確認するには、テスト対象のデータベースの初期状態を調べて、ストアド プロシージャの実行後にデータベースが予想された状態になったかどうかを検証できるようにしておく必要があります。データベースを既知の状態に設定する処理は、従来のテクノロジと比較したときに LINQ が真価を発揮する処理です。

Console.WriteLine("Setting up test bed database");
ResetDatabase(connString);

Main メソッドを比較的クリーンに保つために、コードをインラインに配置する代わりに、ResetDatabase というヘルパー メソッドを作成して、ここでテスト ベッド データベースを設定しています。

static void ResetDatabase(string connString)
{
  // delete all existing data in TblMain and TblPrices
  // populate TblMain and TblPrices with "rich" test data
}

次に、接続文字列を ResetDatabase メソッドに渡します。最初に、次のようにテーブル内の既存のデータをすべて削除します。

DbMovies dbLocal = new DbMovies(connString);
var allRowsInTblMain = from t in dbLocal.TblMain select t;
var allRowsInTblPrices = from t in dbLocal.TblPrices select t;
dbLocal.TblMain.DeleteAllOnSubmit(allRowsInTblMain);
dbLocal.TblPrices.DeleteAllOnSubmit(allRowsInTblPrices);
dbLocal.SubmitChanges();

DbMovies オブジェクトをインスタンス化します。次に、LINQ コードを使用して、両方のテーブル内のすべての既存のデータを LINQ to SQL エンティティ コレクションとして取得します。LINQ の経験がない読者であれば、var キーワードを使用した暗黙のデータ型宣言メカニズムを見たことがないかもしれません。var は、"ここで割り当てコードに基づいて正確なデータ型を Visual Studio に決定させる" という意味です。言い換えると、次のような暗黙の型指定を使用する方法に対し、

var allRowsInTblMain = from t in dbLocal.TblMain select t;

次の方法も同様に有効です。

System.Linq.IQueryable<TblMain> allRowsInTblMain =
  from t in dbLocal.TblMain select t;

私は、LINQ に関して暗黙のデータ型指定を特に気に入っています。ResetDatabase コードを作成しているときにある問題に遭遇したのですが、この問題について触れておきます。最初、私は、ResetDatabase ヘルパー メソッドにローカル オブジェクトを作成するのではなく、1 つの静的なグローバル DbMovies オブジェクトをテスト ハーネスで使用しようとしました。しかし、その方法では、"既に使用されているキーを持つエンティティは追加できません" という例外が発生しました。これは、LINQ to SQL ではパフォーマンス上の理由からたくさんの情報がキャッシュされることを示しています。ストアド プロシージャを呼び出すと、LINQ to SQL は、関連するエンティティに関する情報を保持します。このとき、これらのエンティティに関連するデータを操作しようとすると、このエラーが発生します。簡単な対策は、LINQ to SQL を使用するときにグローバル オブジェクトの使用を避けることです。

エンティティ コレクションを取得したら、各テーブルに関連付けられている DeleteOnSubmit メソッドに渡します。この操作は、tblMain および tblPrices 内のすべての行に削除用のマークを付けるものですが、実際の削除操作は行いません。削除操作を実行するために、SubmitChanges ステートメントを発行します。その結果、削除要求を含むすべての未処理の変更が処理され、DbMovies オブジェクトによって表される実際の dbMovies データベースに反映されます。

これで、リッチなデータをテーブルに設定する準備が整いました。バックエンド データベースを持つアプリケーションを作成する場合、一般的には、開発プロセスにおいて使用する目的で通常のデータをデータベースに格納します。ただし、ストアド プロシージャを徹底的にテストするには、潜在的な問題を発見するために特別に用意されたデータが必要になります。そこで私は、いくつかのテスト ベッド データをハードコードしました。このやり方は、テスト用という点では特に優れた方法ではありませんが、アイデアを明瞭に保つことができます。

string[] data = new string[] {
  "001:Active Assembly:101:11.11",
  "002:Bug Bashing Bit:102:22.22",
  "003:Computing in C#:103:33.33",
  "004:Data is Digital:104:22.22"
};

現実的なシナリオでは、Null 値、重複する値、不正な値、境界値などを含めてさらに多くのデータを追加することになるでしょう。このリッチなテスト ベッド データを外部データ ストアから読み取る方法もありますが、実際のテスト ケース データを使用する場合に比べて、この設定データに関してそうする合理性はあまりありません。次に、図 5 に示すように、テスト ベッド データを解析し、挿入要求を設定します。

Figure 5 LINQ 挿入要求の作成 (Close Figure 5)
foreach (string entry in data)
{
  string[] tokens = entry.Split(':');
  TblMain tm = new TblMain();
  tm.MovID = tokens[0];
  tm.MovTitle = tokens[1];
  tm.MovRunTime = int.Parse(tokens[2]);
  TblPrices tp = new TblPrices();
  tp.MovID = tokens[0];
  tp.MovPrice = decimal.Parse(tokens[3]);
  dbLocal.TblMain.InsertOnSubmit(tm);
  dbLocal.TblPrices.InsertOnSubmit(tp);
}
dbLocal.SubmitChanges();

TblMain オブジェクトと TblPrices オブジェクトをインスタンス化します。これらのクラスは、sqlmetal.exe ツールによって生成されたコードに定義されています。LINQ は .NET Framework の単なる拡張機能なので、IntelliSense® コード補完機能や設計時の構文チェック機能などの、.NET 環境に関連する優れた機能をすべて利用できます。ここで、テスト ベッド データベースにリッチなデータを設定するときに回避する必要がある、テスト設計上の落とし穴について触れておきます。現実的なシナリオでは、テスト対象のデータベースは、データを挿入するストアド プロシージャを含みます。テスト ベッドにデータを設定するためにこのようなプロシージャを使用しないでください。テスト対象システムの一部を使用してテスト対象システムを効率的に使用します。ここで、テスト ハーネスのメインの処理ループに戻ります。データベースが既知の状態に設定されたら、テスト対象のストアド プロシージャを呼び出す準備が整います。ここでも、LINQ to SQL を使用することでこの操作が大幅に簡略化されます。

Console.WriteLine("Calling usp_DeleteMovie");
db.Usp_DeleteMovie(movieID);

Usp_DeleteMovie C# メソッドは、基底の usp_DeleteMovie ストアド プロシージャの使いやすい代用メソッドです。私を信じてください。これは、多くのパラメータがあるときは特に、ADO.NET を使用してストアド プロシージャを呼び出すよりもはるかに簡単です。この段階で、テスト対象のストアド プロシージャによって関連付けられているデータベースが適切に変更されたかどうかを判定する準備が整いました。たとえばデータベース状態を既知の構成にリセットするなど、データベースの状態を判定する操作は、ストアド プロシージャをテストする場合のもう 1 つの基本的な作業です。

Console.WriteLine("Checking resulting state of test bed database");
string actualState = GetDatabaseState(connString);
Console.WriteLine("Expected state = " + expectedState);
Console.WriteLine("Actual state   = " + actualState);

ここでは、予測されるデータベース状態と実際のデータベース状態を取得するために、GetDatabaseState というもう 1 つのヘルパー メソッドを使用します。

static string GetDatabaseState(string connString)
{
  // combine data from tblMain and tblPrices into an aggregate string
  // convert aggregate to an MD5 byte array
  // return string image of byte array
}

最初に、LINQ を使用して、テーブル内のすべてのデータを取得します。

DbMovies db = new DbMovies(connString);
var allRowsInTblMain = from t in db.TblMain select t;
var allRowsInTblPrices = from t in db.TblPrices select t;

次に、それぞれの列値の文字列表現を連結して集計文字列を作成します。

string s = "";
foreach (var record in allRowsInTblMain)
  s += record.MovID + record.MovTitle + record.MovRunTime;
foreach (var record in allRowsInTblPrices)
  s += record.MovID + record.MovPrice;

このとき、この集計文字列を実際の状態として使用し、この文字列を予測される文字列と比較することができます。ただし、テスト ベッド データベース内の値の文字列表現は非常に大きくなる可能性があります。また、一部の SQL データ型 (たとえばバイナリ型) は、文字列表現に適切にマップされません。多くの状況においてより良いアプローチは、データベース内の値のハッシュを使用する方法です。

MD5CryptoServiceProvider md5 =
  new MD5CryptoServiceProvider();
byte[] ba = md5.ComputeHash(Encoding.Unicode.GetBytes(s));
string cryptoHash = BitConverter.ToString(ba);
return cryptoHash;

次に、集計文字列の MD5 (メッセージ ダイジェスト バージョン 5) の 16 バイト値を計算します。このとき、SHA-1 (セキュア ハッシュ アルゴリズム バージョン 1) などの別のハッシュ アルゴリズムも簡単に使用できます。MD5 のような暗号化ハッシュは、任意のサイズのバイト配列を受け取り、常にサイズが固定されている指紋のような識別値を生成します。ここでは読みやすくするために、静的 System.BitConverter クラスを使用して、結果のバイト配列を文字列に変換しています。この段階で、テスト対象のストアド プロシージャによって dbMovies の状態が予想どおりに変更されたかどうかを判定できます。

if (expectedState == actualState)
  Console.WriteLine("\nTest case result = Pass");
else
  Console.WriteLine("\nTest case result = **FAIL**");

ここでは、テスト結果をシェルに表示するだけにしています。運用環境では、結果を外部ストレージに保存することもできます。ここで、テスト ケース データには予測されるデータベース状態 (たとえば 85-A0-D9-28-7B-AA-8D-5F-4C-43-B5-B6-A5-99-F4-63) が格納されていたことを思い出してください。これらの予測値はどこから来ているのでしょうか。予測される状態を調べるのは、ストアド プロシージャをテストする場合に最も時間のかかる作業です。ストアド プロシージャが呼び出されて初期状態が与えられた後のテスト ベッド データベースの予測される状態を手動で調べ、次にユーティリティ プログラムを使用して、予測される状態の値のすべてまたは一部の MD5 (または類似の) 暗号化ハッシュを計算する必要があります。

予測される状態を調べるために使用される一般的なアプローチのうち、誤ったアプローチがあります。予測される値を与えずにテスト ハーネスを実行し、データベースの結果の状態のハッシュ値を取得するやり方です。この方法では、単にテスト対象のストアド プロシージャが同じ (場合によっては誤った) 状態を生成することを確認するだけのテスト ケース データを作成することになります。

ハーネスを拡張する

では、SQL 行セットを返すのみで、関連付けられているデータベース内のデータには作用しない、テスト対象のストアド プロシージャを見ていきましょう。usp_GetMoveDataByPrice は、映画料金を受け取り、指定された料金のすべての映画について、映画 ID、タイトル、上映時間、および料金データを返します。usp_GetMovieDataByPrice を呼び出すには、次のようにコードを記述します。

DbMovies db = new DbMovies(connString);
decimal moviePrice = (decimal)22.22;
var results = dbc.Usp_GetMovieDataByPrice(moviePrice);

非常に簡潔です。ここでは、オブジェクトの結果に対して暗黙のデータ型を使用していますが、次のように結果に対して明示的なデータ型を使用することもできます。

System.Data.Linq.ISingleResult<Usp_GetMovieDataByPriceResult> results =
  db.Usp_GetMovieDataByPrice(moviePrice);

次に、オブジェクトの結果に返された行セットの正確さを検証するために、返された行セットの列値から構成される集計文字列を最初に取得します。

string s = "";
foreach (var r in results)
  s += r.MovID + r.MovTitle + r.MovRunTime + r.MovPrice;

この集計文字列は実際の戻り値として使用できます。また、前のセクションで説明したように、文字列のハッシュを計算することもできます。usp_GetMovieDataByPrice は tblMain および tblPrice のデータを変更しないので dbMovies の状態に明示的に作用しないことに注意してください。これは、テスト ハーネスにおいて、必要に応じて ResetDatabase ヘルパー メソッドの呼び出しを省略できることを意味します。

一般に、スカラ値を返すストアド プロシージャのテストは、予測される結果が 1 つなので、最も簡単なテスト シナリオです。例として、usp_GetMoviePrice を見てみましょう。このストアド プロシージャは次のように定義されています。

create procedure usp_GetMoviePrice
  @movID char(3),
  @price money out
as
  declare @ct int
  select @price = movPrice from tblPrices where movID = @movID
  select @ct = count(*) from tblPrices where movID = @movID
  return @ct

このストアド プロシージャは、事実上、2 つのスカラ値を返します。指定された ID を持つ映画の料金は out パラメータに格納され、指定された ID を持つ映画の数は明示的な戻り値となります。LINQ to SQL を使用して usp_GetMoviePrice を呼び出すには、次のようにコードを記述します。

DbMovies db = new DbMovies(connString);
string movieID = "002";
decimal? actualPrice = null;
int actualCount = dbc.Usp_GetMoviePrice(movieID, ref actualPrice);

decimal キーワードの後に書かれている疑問符に、馴染みのない読者もいることでしょう。これは、Null 許容型の例です。すべての SQL データ値に Null を設定できるので、sqlmetal.exe および O/R デザイナ ツールによって生成されるラッピング コードは、ほとんどのデータ メンバを Null 許容型として生成します。図 6 は、ADO.NET を使用して uspGetMoviePrice を呼び出す場合の一般的なコードです。これで、LINQ アプローチの方がはるかに単純であることに納得してもらえることでしょう。

より高速、よりコンパクト、より優れたデバッグ

LINQ および LINQ to SQL に関する私の最初の体験に基づいた計算では、LINQ を使用した場合、ストアド プロシージャのテストの自動化を作成するための時間と労力を少なくとも 50% は削減できると推定できます。LINQ は .NET Framework の一部なので、任意の .NET Framework 準拠の言語 (たとえば C#) を直接使用できるというメリットがあります。引用符とコンマの場所に誤りがないことを願いながら、次のようなコードを記述する必要はありません。

SqlCommand sc = new SqlCommand("INSERT INTO tblFoo VALUES('" +
someValue + "', '" + otherValue + "')";

LINQ を使用した場合、通常の C# スタイルでコードを記述でき、構文エラーがあれば即座に報告されます。

LINQ to SQL は、本質的に優れた設計の下位レベル コード (たとえば SqlDataReader などのクラス) のラッパーなので、テスト コードはよりサイズが小さくクリーンになり、作成、変更、管理が容易になります。LINQ は .NET 環境の一部であるため、Visual Studio のデバッガを使用して、テスト ハーネス内のエラーを発見できます。これは、LINQ 以前のアプローチでは、多くのシナリオにおいて面倒であったり不可能であったりしたことです。おそらく最も重要なのは、LINQ は使って満足できることです。

このコラムを研究している間、私自身、実験プロセスをおおいに楽しみました。LINQ は、開発者またはテスト担当者としての作業を簡単にするので、研究活動を助長します。このような特徴を持つ LINQ を使用することで、より徹底的なテストが可能になり、その結果、より優れたソフトウェア製品が生まれることになります。

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

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


サービス ステーション: WCF ルーターを構築する (第 1 部)

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

コンテンツ
既定のアドレス指定セマンティクス
ルーティング アーキテクチャ
ルーター コントラクト
メッセージを転送する
論理および物理アドレス指定
MustUnderstand ヘッダー

Windows® Communication Foundation (WCF) サービスをホストして利用するには、通常、いくつかの基本的な手順を実行する必要があります。その手順とは、サービスの実装、サービスに到達するためのエンドポイントの構成、サービスのホスト、Web サービス記述言語 (WSDL) ファイルの生成、またはメタデータ交換の有効化です。これにより、クライアントはサービスを呼び出すプロキシを生成し、関連付けられている構成でプロキシをインスタンス化するコードを記述して、サービス操作の呼び出しを開始できるようになります。詳細に確認する必要があるケースは非常にまれですが、最もシンプルなケースでも、適切な操作が呼び出されるようにアドレス指定セマンティクスやメッセージ フィルタを処理するために、クライアントとサービスのチャネルには互換性のある構成が不可欠です。

クライアントとターゲット サービス間でやり取りされるメッセージを受信し、その他のアクティビティ (ログ記録、優先順位のルーティング、オンライン/オフライン ルーティング、負荷分散など) を実行したり、セキュリティ境界を形成したりする場合には、クライアントとターゲット サービス間に中継サービスやルーター サービスを導入することが有効な場合もあります。そうした中継サービスを導入すると、アドレス指定とメッセージ フィルタの動作の一部を調整し、中継サービスに対応させる必要が生じます。

それでは、中継サービスを操作する方法を詳しく見ていきましょう。ここでは便宜上、中継サービスをまとめてルーターと呼びます。今回の記事では、ルーターのシナリオに重点を置きつつ、WCF アドレス指定およびメッセージ フィルタの概念についてお話します。また、ルーティングの構成に関するいくつかのオプションと、適切な設定についても説明します。このシリーズの第 2 部では、こうした基本事項を応用し、より高度で実用的なルーティングを実装する方法を取り上げます。

既定のアドレス指定セマンティクス

2007 年 6 月号の「サービス ステーション」コラム (msdn.microsoft.com/msdnmag/issues/07/06/ServiceStation) では、Aaron Skonnard 氏が、論理および物理エンドポイント アドレス指定、アドレス指定ヘッダー、およびメッセージ フィルタが WCF ではどのように扱われるかについて説明しています。ここでは、そうした基本的なアドレス指定機能の一部と、それらの機能がルーティング シナリオに及ぼす影響について見直します。Aaron 氏のコラムは、これらの WCF 機能の背後にある詳細情報を確認するうえでも役立つでしょう。

クライアントは通常、サービス記述から生成されたプロキシを使用してターゲット サービスにメッセージを直接送信します。クライアントとサービスは、互換性を確保するために、同等のコントラクトとエンドポイント構成を共有します。図 1 に示したサービス コントラクトと構成を検討することにより、サービスの重要なアドレス指定要件をいくつか導き出すことができます。

Figure 1 サービス コントラクトおよびエンドポイント構成 (Close Figure 1)
            Service Contract
[ServiceContract(Namespace =
"http://www.thatindigogirl.com/samples/2008/01")]
public interface IMessageManagerService
{
  [OperationContract]
  string SendMessage(string msg);

  [OperationContract]
  void SendOneWayMessage(string msg);
}
Endpoint Configuration
<system.serviceModel>
  <services>
    <service name="MessageManager.MessageManagerService"
      behaviorConfiguration="serviceBehavior">
      <endpoint
        address="http://localhost:8000/MessageManagerService"
        contract="MessageManager.IMessageManagerService"
        binding="basicHttpBinding" />
    </service>
  </services>
</system.serviceModel>

まず、SendMessage 操作に対する要求の Action アドレス指定ヘッダーは、次のようになります。

http://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessage

OperationContractAttribute で Action を指定していないため、この値はサービス コントラクトの名前空間、コントラクト名 (既定ではインターフェイス名)、および操作名 (既定ではメソッド名) から派生します。

次に、SendMessage からの応答の Action ヘッダーは、次のようになります。

http://www.thatindigogirl.com/samples/2008/01/IMessageManagerService/SendMessageResponse

OperationContractAttribute で ReplyAction を指定していないため、この値は Action プロパティと同様に派生し、"Response" というサフィックスが追加されます。

最後に、サービス エンドポイントを宛先とするメッセージの To ヘッダーは、次のようになります。

http://localhost:8000/MessageManagerService

この値は、endpoint 要素の address 属性から派生します。これは、エンドポイントの論理アドレスと見なされます。エンドポイントの物理アドレスが指定されていない場合、通常は論理アドレスが物理アドレスとして使用されます。つまり、クライアントは通常、To ヘッダーに一致する物理アドレスにメッセージを送信することになります。

サービス メタデータにはこうした要件を記述し、クライアントが互換性のあるプロキシと構成を生成できるようにします。クライアントに対して生成されるサービス コントラクトには、サービス側の同じ Action 設定と ReplyAction 設定が反映されます。また、クライアントのバインド構成には、適切な論理および物理アドレスを持つエンドポイントが反映されます。たとえば、次のクライアント エンドポイントは、図 1 のサービスと互換性があります。

<client>
  <endpoint
    address="http://localhost:8000/MessageManagerService"
    binding="basicHttpBinding"
    contract="localhost.IMessageManagerService"
    name="basicHttp" />
</client>

クライアント プロキシは、その論理アドレスおよび物理アドレスとして、クライアントの endpoint 要素の address プロパティを使用します。その結果、先に述べたように、メッセージは To ヘッダーに一致する物理アドレスに送信されます。プロキシが SendMessage 操作を呼び出すと、図 2 に示すように、To ヘッダーおよび Action ヘッダーを持つメッセージが送信されます。

Figure 2 プロキシから送信されるメッセージ (Close Figure 2)
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <To s:mustUnderstand="1"
      xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">
      http://localhost:8000/MessageManagerService
    </To>
    <Action s:mustUnderstand="1"
      xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">
      http://www.thatindigogirl.com/samples/2008/01
        /IMessageManagerService/SendMessage
    </Action>
  </s:Header>
  <s:Body>
    <SendMessage xmlns="http://www.thatindigogirl.com/samples/2008/01">
      <msg>test</msg>
    </SendMessage>
  </s:Body>
</s:Envelope>

To ヘッダーと Action ヘッダーの組み合わせにより、サービス モデル層のどのチャネル ディスパッチャがメッセージを処理し、そのディスパッチャがどの操作を呼び出すかを示します。既定では、To ヘッダーに一致する論理アドレスを持つエンドポイントと、Action ヘッダーに一致する操作が存在しなければなりません。図 3 に、この流れを示します。

Figure 3 Typical Addressing without a Router
Figure 3 Typical Addressing without a Router(画像を縮小するには、ここをクリックします)
Figure 3 Typical Addressing without a Router
Figure 3 Typical Addressing without a Router (画像を拡大するには、ここをクリックします)

以降では、ルーターがシナリオに導入された場合に、論理アドレスと物理アドレス、To ヘッダーと Action ヘッダー、およびメッセージ フィルタのルールが果たす役割について説明します。

ルーティング アーキテクチャ

ルーターの設計方法にはいくつかのバリエーションがありますが、ほとんどのルーターは、任意のサービスを宛先とするメッセージを受信できなければなりません。また、元のメッセージを適切なターゲット サービスに転送できる必要もあります。ルーターの設計には、パススルー ルーターとプロセッシング ルーターという 2 つの基本的なアプローチがあります。

パススルー ルーターは、クライアントに対して透過的です。クライアントはダウンストリーム サービスと関係を持ち、メッセージはルーターを通過します。クライアントは、互換性のあるトランスポート プロトコルとメッセージ エンコーダを使用してルーターにメッセージを送信する必要がありますが、サービス チャネルに必要なセキュリティ、信頼できるセッション、アプリケーション セッション、またはその他のメッセージング プロトコルは、クライアントで生成されたメッセージにより満たされます。ルーターがメッセージ ヘッダーや挿入ヘッダーを参照することもありますが、この場合も元のメッセージの要素は変更されず、そのままサービスに転送されます。この関係を図 4 に示します。

Figure 4 Operation of a Pass-Through Router
Figure 4 Operation of a Pass-Through Router(画像を縮小するには、ここをクリックします)
Figure 4 Operation of a Pass-Through Router
Figure 4 Operation of a Pass-Through Router (画像を拡大するには、ここをクリックします)

プロセッシング ルーターは、アプリケーションに対するメッセージを処理するうえで、よりアクティブな役割を果たします。したがって、クライアントは、トランスポート、エンコード、およびプロトコルの互換性については、ルーターと関係を持ちます。ただし、クライアントにはダウンストリーム サービスと互換性のあるメッセージを送信できる必要もあります。メッセージはルーターを介してサービスに送信されます。メッセージ本文と、サービスに必要なヘッダーは変更されません。

セキュリティ、信頼できるセッション、およびその他の通信プロトコルに関係するヘッダーやメッセージは、通常はルーターにより処理されます。また、ルーターは、ダウンストリーム サービスに対応する適切な通信プロトコルを持つ新しいメッセージを作成します。プロセッシング ルーターを使用した場合の互換性について、図 5 に示します。

Figure 5 Operation of a Processing Router
Figure 5 Operation of a Processing Router(画像を縮小するには、ここをクリックします)
Figure 5 Operation of a Processing Router
Figure 5 Operation of a Processing Router (画像を拡大するには、ここをクリックします)

どちらのルーティング構成も、実用的に実装できます。また、クライアントとサービス間で、これらを組み合わせたソリューションを実装することも可能です。

ルーター コントラクト

ルーターは、ダウンストリーム サービス宛てに送信されたメッセージを受信し、それらのメッセージを適切なサービスに転送する役割を担います。また、サービスからの応答を受信し、それらの応答をクライアントに返す役割も担います。一般的なルーター コントラクトは、任意のメッセージ要求または応答を処理する単一の操作を公開します。次の例では、この操作は ProcessMessage です。

[ServiceContract(Namespace =
"http://www.thatindigogirl.com/samples/2008/01")]
public interface IRouterService {
  [OperationContract(Action = "*", ReplyAction = "*")]
  Message ProcessMessage(Message requestMessage);
}

通常、OperationContractAttribute の Action プロパティおよび ReplyAction プロパティは、このコラムの前の部分で説明したとおり、サービス コントラクトの名前空間、コントラクト名、および操作名から派生します。既定では、メッセージが到着すると、チャネル ディスパッチャは Action ヘッダーと一致する操作を見つける必要があります。ただし、Action および ReplyAction が "*" に設定されている場合、チャネル ディスパッチャは、Action ヘッダーの値に関係なく、特定の操作にマップされていないすべてのメッセージをそのキャッチオール操作に送信します。あいまいさを避けるために、1 つの操作についてのみ Action プロパティまたは ReplyAction プロパティに "*" を指定できます。

一般的なルーターは、到達したすべてのメッセージを処理できる ProcessMessage などの単一の操作を提供します。Action および ReplyAction によって、メッセージがチャネル ディスパッチャにより ProcessMessage にマップされていることが保証されますが、操作シグネチャがすべてのメッセージを処理できる必要もあります。

この問題に対処するために、ProcessMessage は型指定のないメッセージを受け取り、これを Message 型で返します。ルーターは、この型によってヘッダー コレクションとメッセージ本文にアクセスできますが、一般的なアドレス指定ヘッダー以外には自動シリアル化は実行されません。それらはシリアル化解除され、厳密に型指定されたプロパティによってアクセスできるようになります。

これ以上のメッセージの処理が行われるかどうかは、ルーターの実装方法しだいです。基本的なルーターは、単純に型指定のないメッセージを受信してそのままダウンストリーム サービスに転送し、応答を待ちます。同様に、応答もそのままの形式で呼び出し側のクライアントに転送されます。

メッセージを転送する

ルーターは、メッセージを受信し、自身の要件に従って処理してから、さらに処理を進めるために適切なダウンストリーム サービスに転送します。図 6 に、前述のコントラクトの場合のシンプルなルーター実装を示します。ProcessMessage は、ChannelFactory<T> を使用してクライアント チャネル (プロキシ) を作成します。次に、このプロキシを使用してメッセージを特定のサービス エンドポイントに転送し、すべての応答を返します。

Figure 6 シンプルなルーターの実装 (Close Figure 6)
[ServiceBehavior(
  InstanceContextMode = InstanceContextMode.Single,
  ConcurrencyMode = ConcurrencyMode.Multiple)]
public class RouterService : IRouterService {
  public Message ProcessMessage(Message requestMessage) {
    using (ChannelFactory<IRouterService> factory =
      new ChannelFactory<IRouterService>("serviceEndpoint")) {

      IRouterService proxy = factory.CreateChannel();

      using (proxy as IDisposable) {
        return proxy.ProcessMessage(requestMessage);
      }
    }
  }
}

プロキシは通常、ターゲット サービス コントラクトに対して厳密に型指定されますが、この場合は、すべてのメッセージの転送とすべての応答の受信が可能でなければなりません。これは、ルーター コントラクトによって容易になります。このシンプルな例では、ルーターは単にメッセージをそのままターゲット サービスに転送し、すべての応答を返すだけです。ターゲット サービスでの操作が一方向である場合、応答は送信されません。

コントラクトは型指定のないメッセージで有効なため、図 7 に示すとおり、同じメッセージがサービスに転送されます。ここで、予期しないある変更がメッセージに加えられていることに注目する必要があります。メッセージがサービスに送信される前に、メッセージの To ヘッダーが変更されているのです。

Figure 7 Addressing Semantics through a Simple Router
Figure 7 Addressing Semantics through a Simple Router(画像を縮小するには、ここをクリックします)
Figure 7 Addressing Semantics through a Simple Router
Figure 7 Addressing Semantics through a Simple Router (画像を拡大するには、ここをクリックします)

プロキシは、既定では、そのエンドポイント構成の論理アドレスを使用して送信メッセージの To ヘッダーを設定するということを思い出してください。これは、既に To ヘッダーを持つ、変更のない Message インスタンスが渡される場合も同様です。すべてのサービスにおいて、To ヘッダーがいずれかのサービス エンドポイントの論理アドレスと一致している必要があるため、この動作は都合の良いものに思えるかもしれませんが、副作用が生じる場合もあります。一例を挙げると、更新された To ヘッダーに署名がされておらず、サービスでセキュリティが有効になっている場合、メッセージは拒否されます。

クライアントが、ターゲット デバイスに一致する To ヘッダーを持つメッセージを送信し、ヘッダーが一致していなくともルーターがこのメッセージを受信して、To ヘッダーを変更することなくルーターがサービスに転送する、という流れが理想的です。これは、次に説明するバインド構成を通じて実現できます。

論理および物理アドレス指定

ルーターをアプリケーション アーキテクチャに導入した場合、クライアントが、サービスのアドレスと一致する To ヘッダーを使用したうえでルーターにメッセージを送信できるというのが最良の状態です。これを実現する方法として、図 8 に示す構成のように、ClientViaBehavior を使用するようにクライアントを構成する方法が挙げられます。この構成により、クライアント プロキシは、エンドポイントの論理アドレスに基づく To ヘッダーを持つメッセージを生成し、そのメッセージをルーターの物理アドレスを介して送信するようになります。ただし、この動作で問題なのは、クライアントがルーターの存在と結び付けられることです。

Figure 8 ClientViaBehavior の使用 (Close Figure 8)
<client>
  <endpoint address="http://localhost:8000/MessageManagerService"
    binding="wsHttpBinding"
    bindingConfiguration="wsHttpNoSecurity"
    contract="localhost.IMessageManagerService"
    name="basicHttp"
    behaviorConfiguration="viaBehavior"/>
</client>
<behaviors>
  <endpointBehaviors>
    <behavior name="viaBehavior">
      <clientVia viaUri="http://localhost:8010/RouterService"/>
    </behavior>
  </endpointBehaviors>
</behaviors>

前述の動作を実現するもう 1 つの方法は、サービスがそのエンドポイントに listenUri 属性を構成するようにする方法です。これにより、サービスの論理アドレスがルーターの論理アドレスと同じになります。その一方で、物理アドレスはサービスに固有のアドレスとなります。次のサービスの構成を考えてみてください。

<endpoint address="http://localhost:8010/RouterService"
  contract="MessageManager.IMessageManagerService"
  binding="wsHttpBinding"
  bindingConfiguration="wsHttpNoSecurity"
  listenUri="http://localhost:8000/MessageManagerService"/>

この結果のサービス メタデータにより、ルーターのアドレスがクライアントにパブリッシュされるため、クライアント エンドポイントはルーターのアドレスを反映できます。実は、このソリューションはあまりお勧めできません。サービスがルーターに結び付けられることになるからです。サービスがアドレスを把握する必要がない状態が理想的です。

これに代わる方法として、ルーターやサービスに関連付けられていない URI を論理アドレスとしてサービスが使用するように構成してから、クライアントにメッセージの送信先の物理アドレスを手動で通知する (物理アドレスがメタデータに含まれないため) 方法が挙げられます。このタイプのエンドポイント構成の例を次に示します。

<endpoint address="urn:MessageManagerService"
  contract="MessageManager.IMessageManagerService"
  binding="wsHttpBinding"
  bindingConfiguration="wsHttpNoSecurity"
  listenUri="http://localhost:8000/MessageManagerService"/>

いずれの場合も、サービスはそのエンドポイント構成に一致する To ヘッダーを受信します。また、ルーターは先にメッセージを受信します。

クライアントとサービスがルーターに依存せずにやり取りできるように、ルーターが構成を引き受ける必要があります。したがって、To ヘッダーがルーターの論理アドレスに一致することはありません。既定では、サービスは EndpointAddressMessageFilter を使用し、構成されているエンドポイントのいずれかにメッセージの To ヘッダーが一致するかどうかを特定します。ルーターではこれは機能しないため、MatchAllMessageFilter をインストールする必要があります。

ServiceBehaviorAttribute は、AddressFilterMode プロパティを通じてこれをサポートします。このプロパティは、AddressFilterMode 列挙体のいずれかの値 (Exact (既定)、Prefix、または Any) に設定できます。ルーターのプレフィックスは、ルーターの受信するメッセージの宛先であるすべてのサービスと一致するとは限りません。そのため、次のように、どの To ヘッダーでも通過できるように構成するのが賢明です。

[ServiceBehavior(InstanceContextMode =
  InstanceContextMode.Single,
  ConcurrencyMode = ConcurrencyMode.Multiple,
  AddressFilterMode=AddressFilterMode.Any)]
public class RouterService : IRouterService

既定では、To ヘッダーはプロキシの論理アドレスに一致するように、そのエンドポイント構成に基づいて必ず更新されます (既に To ヘッダーのアドレスに適切な値が設定されていても、更新は行われます)。この動作を抑制し、ルーターが元の To ヘッダーを使用してサービスにメッセージを転送できるようにするには、手動でアドレス指定し、ルーターでバインド構成を使用する必要があります。これは標準のバインドで設定できるプロパティではありません。そのため、これを実現するには、カスタム バインドを使用する必要があります。

次のコード スニペットは、この機能を HTTP トランスポート チャネルに設定するための customBinding の部分です。

<customBinding>
  <binding name="manualAddressing">
    <textMessageEncoding />
    <httpTransport manualAddressing="true"/>
  </binding>
 </customBinding>

これにより、図 9 に示すアドレス指定の流れが容易になります。ここではヘッダーが変更されていません。

Figure 9 Addressing Semantics through a Router with Manual Addressing
Figure 9 Addressing Semantics through a Router with Manual Addressing(画像を縮小するには、ここをクリックします)
Figure 9 Addressing Semantics through a Router with Manual Addressing
Figure 9 Addressing Semantics through a Router with Manual Addressing (画像を拡大するには、ここをクリックします)

MustUnderstand ヘッダー

ここまでは、シンプルなルーティングの実装に重点を置きつつ、アドレス指定、フィルタ、およびバインド構成にもかかわりのある、ルーターの設計上の基本的な考慮事項を見てきました。このシンプルなルーティング ソリューションは、バインド用のセキュリティ、信頼できるセッション、その他の高度なプロトコルがサービスで有効化されていない場合にのみ機能します。図 10 は、ここまでの説明で想定しているバインド プロトコルを簡単に表したものです。

Figure 10 Service Contract and Endpoint Configuration
Figure 10 Service Contract and Endpoint Configuration(画像を縮小するには、ここをクリックします)
Figure 10 Service Contract and Endpoint Configuration
Figure 10 Service Contract and Endpoint Configuration (画像を拡大するには、ここをクリックします)

図 11 は、同様に、サービスにセキュリティや信頼できるセッションが必要な場合のパルスルー ルーターを表したものです。これらのプロトコルを有効にすると、セッションの確立、セキュリティ トークンの要求、およびその他の関係するメッセージングを行うために、クライアントおよびサービス チャネルで追加のメッセージが交換されるようになります。すべてのメッセージがルーターを通過できるため、こうしたプロトコル固有のメッセージもルーターを通過してサービスに到達できます (これは好ましい動作と言えます)。

Figure 11 Pass-Through Configuration with Security and Reliable Sessions
Figure 11 Pass-Through Configuration with Security and Reliable Sessions(画像を縮小するには、ここをクリックします)
Figure 11 Pass-Through Configuration with Security and Reliable Sessions
Figure 11 Pass-Through Configuration with Security and Reliable Sessions (画像を拡大するには、ここをクリックします)

ただし、サービスとやり取りされるメッセージに、受信側が認識する必要のあるヘッダーが含まれている場合に、問題が発生します。パススルー ルーターでは、セキュリティや信頼できるセッションは意図的に有効にされないため、関連付けられているプロトコル ヘッダーを処理するためのチャネルは存在しません。

次に示すように、ServiceBehaviorAttribute の ValidateMustUnderstand プロパティを false に設定することで、ルーター サービスは MustUnderstand ヘッダーを無視するようになります。

[ServiceBehavior(InstanceContextMode =
  InstanceContextMode.Single,
  ConcurrencyMode = ConcurrencyMode.Multiple,
  AddressFilterMode=AddressFilterMode.Any,
  ValidateMustUnderstand=false)]
public class RouterService : IRouterService

これにより、クライアントからの受信メッセージには対処できるようになりますが、ダウンストリーム サービスから返されるメッセージには依然として対処できません。

これに対処するには、次に示すようにルーターの実装も変更して、ダウンストリーム サービスを呼び出すためにチャネル ファクトリを初期化する際にこの動作を指定する必要があります。

using (ChannelFactory<IRouterService> factory =
  new ChannelFactory<IRouterService>("serviceEndpoint"))
{
  factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
  IRouterService proxy = factory.CreateChannel();

  // remaining code
}

これで、プロトコルとサービス メッセージは、ルーターを介してクライアントとサービス間で自由にやり取りされるようになりました。ただし、使用されているプロトコルが HTTP の場合を想定しています。

TCP や名前付きパイプなどの二重プロトコルが使用されている場合、つまり、サービスからクライアントへのメッセージの送信が可能な状態 (信頼できるセッションが有効な場合など) では、別の問題が発生します。こうした特殊なケースに対処するために使用できる高度なルーター構成もあるのですが、そのシナリオと実用性については、このシリーズの第 2 部で扱います。

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

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


Windows と C++: Windows Imaging Component の基礎

Windows と C++
Windows Imaging Component の基礎
Kenny Kerr

コンテンツ
はじめに
イメージをデコードする
イメージをエンコードする
WIC イメージング ファクトリ
ストリームを使用する
WPF による WIC
次回の予定

Microsoft® Windows® Imaging Component (WIC) は、イメージのエンコード、デコード、および操作を行うための拡張可能なフレームワークです。もともとは Windows Vista® と Windows Presentation Foundation (WPF) 用に設計されましたが、現在では、WIC は Windows Vista と Microsoft .NET Framework 3.0 以降で提供されるだけでなく、ネイティブなアプリケーションで使用するために、Windows XP と Windows Server® 2003 用のダウンロードとしても入手できます。

WIC は、WPF を強化するいくつかの強力なネイティブ フレームワークの 1 つであり、System.Windows.Media.Imaging 名前空間の実装で使用されているフレームワークです。このフレームワークでは単純で強力な API が COM インターフェイスを通じて公開されているため、C++ で記述されたネイティブなアプリケーションにも適しています。

WIC では、拡張性のあるイメージング コーデックを使用して、さまざまなイメージ形式がサポートされています。各コーデックではさまざまなイメージ形式がサポートされ、一般にエンコーダとデコーダの両方が提供されています。WIC には、PNG、JPEG、GIF、TIFF、HD Photo (HDP)、ICO、Windows BMP など、事実上すべての主要なイメージ形式の組み込みコーデックが含まれています。

HDP だけは、これまで聞いたことがない形式かもしれません。これは、もともと Windows Media Photo と呼ばれており、既存の形式の制限のいくつかを克服し、高いパフォーマンスとイメージ品質を提供するために Windows Vista と併せて開発されました。HDP の詳細については、microsoft.com/whdc/xps/wmphoto.mspx にある仕様を参照してください。幸運にも、WIC はこの新しいイメージ形式をサポートしているため、アプリケーションはその詳細を知らなくてもこの形式を使用できます。

今月は、WIC を使用してさまざまなイメージ形式をエンコードおよびデコードする方法などについて説明します。次回は、より高度な機能と、独自のイメージング コーデックを使用して WIC を拡張する方法について説明します。

はじめに

WIC API は、COM インターフェイス、関数、構造体、エラー コードと、さまざまなコーデック、コンテナ、形式を識別する GUID で構成されます。必要な宣言は、すべてヘッダー ファイル wincodec.h と wincodecsdk.h に格納されています。これらのファイルは、Windows SDK に付属しています (Visual Studio® 2008 にも付属しています)。また、WindowsCodecs.lib ライブラリにリンクすることも必要です。このライブラリでは、必要になるさまざまな定義が提供されます。プロジェクトのプリコンパイル済みヘッダーに以下の内容を追加することで、すべてが利用可能になります。

#include <wincodec.h>
#include <wincodecsdk.h>
#pragma comment(lib, "WindowsCodecs.lib")

WIC API は主に COM インターフェイスで構成されるため、インターフェイス ポインタの作成と管理を処理するには、Active Template Library (ATL) の CComPtr クラスを使用します。この処理を行う場合には、テンプレート クラス CComPtr が定義されているヘッダー ファイル atlbase.h も追加する必要があります。

#include <atlbase.h>

WIC API も COM ライブラリも使用します。そのため、この API を使用するすべてのスレッドで CoInitializeEx 関数を呼び出す必要があります。

最後に、WIC API は HRESULT を使用してエラーを記述します。この記事のサンプルでは、HR マクロを使用して、確認を必要とする HRESULT がメソッドから返される場所を明確に特定しています。これは、各自のエラー処理方式 (例外をスローするか、HRESULT を返すエラー処理など) で置き換えることができます。

イメージをデコードする

デコーダは、IWICBitmapDecoder インターフェイスで表されます。WIC では、デコーダ オブジェクトを作成するための方法がいくつかありますが、最も簡単なのは、特定のデコーダの CLSID を使用してインスタンスを作成する方法です。以下の例では、TIFF イメージのデコーダを作成します。

CComPtr<IWICBitmapDecoder> decoder;
HR(decoder.CoCreateInstance(CLSID_WICTiffDecoder));

図 1 に、WIC に含まれているコーデックと、さまざまなデコーダを作成するために使用可能な CLSID の一覧を示します。デコーダを作成したら、ストリームでデコーダを初期化する必要があります。このストリームには、デコーダが理解する形式でピクセルとオプションのメタデータが含まれています。

Figure 1 組み込み WIC コーデックの CLSID (Close Figure 1)

形式 | デコーダ | エンコーダ
BMP | CLSID_WICBmpDecoder | CLSID_WICBmpEncoder
PNG | CLSID_WICPngDecoder | CLSID_WICPngEncoder
ICO | CLSID_WICIcoDecoder | 使用不可
JPEG | CLSID_WICJpegDecoder | CLSID_WICJpegEncoder
GIF | CLSID_WICGifDecoder | CLSID_WICGifEncoder
TIFF | CLSID_WICTiffDecoder | CLSID_WICTiffEncoder
HDP | CLSID_WICWmpDecoder | CLSID_WICWmpEncoder

CComPtr<IStream> stream;

// Create stream object here...

HR(decoder->Initialize(
  stream,
  WICDecodeMetadataCacheOnDemand));

ストリームについてはこの記事で後述しますが、IStream は従来の COM ストリーム インターフェイスであり、MSDN® Magazine の 2007 年 4 月号 (msdn.microsoft.com/msdnmag/issues/07/04/Xml) で説明した XmlLite を含む多くの API で使用されています。

Initialize メソッドに対する第 2 パラメータは、デコーダがストリームからイメージ情報を読み取る方法を示します。WICDecodeMetadataCacheOnDemand は、必要な時にだけストリームからイメージ情報を読み取ることをデコーダに指示します。これは、イメージ形式が複数のフレームを含んでいるかサポートしている場合に特に便利です。また WICDecodeMetadataCacheOnLoad は、すべてのイメージ情報をすぐにキャッシュするようデコーダに指示します。デコーダに対する以降の要求は、メモリから直接実行されます。後述のように、マネージ コードではさらに意味を持ちます。

デコーダが初期化されたら、デコーダに自由に情報を照会することができます。照会を行う最も一般的な対象は、イメージを構成するフレームです。フレームは、ピクセルが格納された実際のビットマップです。イメージ形式は、フレームのコンテナと考えることができます。前述のように、イメージ形式によっては複数のフレームがサポートされています。

GetFrameCount 関数は、イメージ内のフレーム数を判断するために使用します。

UINT frameCount = 0;
HR(decoder->GetFrameCount(&frameCount));

フレーム数がわかったら、GetFrame メソッドを使用して個々のフレームを取得できます。

for (UINT index = 0; index < frameCount; ++index)
{
    CComPtr<IWICBitmapFrameDecode> frame;

    HR(decoder->GetFrame(index, &frame));
}

GetFrame で返される IWICBitmapFrameDecode インターフェイスは、読み取り専用ビットマップを表す IWICBitmapSource インターフェイスから派生しています。IWICBitmapFrameDecode は、メタデータやカラー プロファイルなど、フレームに関する情報を提供します。IWICBitmapSource は、ビットマップのサイズと解像度、ピクセル形式、カラー テーブルなどのその他のオプションの特性を提供します。IWICBitmapSource では、実際にビットマップからピクセルを読み取るために使用できる CopyPixels メソッドも提供されています。

GetSize メソッドを使用して、ピクセル単位で表現されたフレームの寸法を取得できます。

UINT width = 0;
UINT height = 0;
HR(frame->GetSize(&width, &height));

また、GetResolution メソッドを使用して、インチあたりのドット数 (dpi) 単位で表現されたフレームの解像度を取得できます。

double dpiX = 0;
double dpiY = 0;
HR(frame->GetResolution(&dpiX, &dpiY));

解像度はピクセル自体に影響を与えませんが、WPF で使用されるような論理座標系を使用する場合には、イメージの表示方法に影響を与えます。

フレームの最後の重要な属性は、ピクセル形式です。ピクセル形式は、メモリ内のピクセルの配置を表し、サポートされる色の範囲や色空間も意味します。GetPixelFormat メソッドはピクセル形式を返します。

GUID pixelFormat = { 0 };
HR(frame->GetPixelFormat(&pixelFormat));

ピクセル形式は、名前によってメモリ配置を明確に表す GUID で定義されます。たとえば、GUID_WICPixelFormat24bppRGB 形式は、各ピクセルが、カラー チャネルあたり 1 バイト、合計 24 ビット (3 バイト) の記憶域を使用することを示します。また、赤 (R)、緑 (G)、青 (B) の文字の順序により、最下位バイトから最上位バイトの順序を示します。たとえば、GUID_WICPixelFormat32bppBGRA 形式は、各ピクセルが、カラー チャネルあたり 1 バイト、アルファ チャネルに 1 バイト、合計 32 ビット (4 バイト) の記憶域を使用することを示します。この例では、青 (B) チャネルが最下位バイトになり、アルファ (A) チャネルが最上位バイトになります。

実際のピクセルは、CopyPixels メソッドを使用して取得できます。

HRESULT CopyPixels(
  const WICRect* rect,
  UINT stride,
  UINT bufferSize,
  BYTE* buffer);

rect パラメータは、ビットマップ内のコピー対象の四角形を指定します。このパラメータにゼロを設定すると、ビットマップ全体がコピーされます。stride についてはすぐ後で説明します。buffer パラメータと bufferSize パラメータは、ピクセルの書き込み先と、使用可能なスペースの量を示します。

stride は、ビットマップで混乱しやすい点です。stride は、スキャンライン間のバイト数です。一般に、ビットマップのピクセルを構成するビットは、行にパックされています。1 つの行は、ビットマップのピクセルの 1 行を格納するのに十分な長さです。stride は、行の長さをバイト単位で測定し、最も近い DWORD (4 バイト) に切り上げたものです。これにより 32 ビット/ピクセル (bpp) よりも少ないビットマップでは、パフォーマンスを保ちつつメモリ消費量を抑えることができます。ビットマップの stride を計算するには、以下の関数を使用します。

UINT GetStride(
  const UINT width, // image width in pixels
  const UINT bitCount) { // bits per pixel
  ASSERT(0 == bitCount % 8);

  const UINT byteCount = bitCount / 8;
  const UINT stride = (width * byteCount + 3) & ~3;

  ASSERT(0 == stride % sizeof(DWORD));
  return stride;
}

そこで、フレームが 32 bpp のビットマップを表すとすると、以下のように CopyPixels メソッドを呼び出すことができます。

const UINT stride = GetStride(width, 32);
CAtlArray<BYTE> buffer;
VERIFY(buffer.SetCount(stride * height));

HR(frame->CopyPixels(
  0, // entire bitmap
  stride,
  buffer.GetCount(),
  &buffer[0]));

この例では、ATL CAtlArray コレクション クラスを使用してバッファを割り当てていますが、もちろん任意の記憶域を使用できます。大規模なビットマップを効率的に扱うために、CopyPixels を何度か呼び出して、ビットマップの異なる部分を読み取ることができます。

イメージをエンコードする

エンコーダは、IWICBitmapEncoder インターフェイスで表されます。デコーダと同様に、WIC では、エンコーダを作成する方法がいくつかありますが、最も簡単なのは、エンコーダの CLSID を使用してインスタンスを作成する方法です。たとえば次のコードは、PNG イメージ用のエンコーダを作成します。

CComPtr<IWICBitmapEncoder> encoder;
HR(encoder.CoCreateInstance(CLSID_WICPngEncoder));

図 1 に、WIC に含まれており、さまざまなエンコーダを作成するために使用できる CLSID の一覧を示しています。エンコーダを作成したら、最終的にエンコードされたピクセルとオプションのメタデータを受け取るストリームでエンコーダを初期化する必要があります。

CComPtr<IStream> stream;

// Create stream object here...

HR(encoder->Initialize(
  stream,
  WICBitmapEncoderNoCache));

Initialize メソッドに対する第 2 パラメータにはあまり重要な意味がなく、現在では WICBitmapEncoderNoCache フラグだけがサポートされています。

エンコーダを初期化したら、フレームの追加を開始できます。CreateNewFrame メソッドは、ピクセルを構成して書き込むための新しいフレームを作成します。

CComPtr<IWICBitmapFrameEncode> frame;
CComPtr<IPropertyBag2> properties;

HR(encoder->CreateNewFrame(
  &frame,
  &properties));

CreateNewFrame は、新しいフレームを表す IWICBitmapFrameEncode インターフェイスと、IPropertyBag2 インターフェイスの両方を返します。後者はオプションであり、JPEG のイメージ品質や、TIFF の圧縮アルゴリズムなど、エンコーダ固有のプロパティを指定するために使用します。たとえば、JPEG イメージのイメージ品質を設定するには以下のようにします。

PROPBAG2 name = { 0 };
name.dwType = PROPBAG2_TYPE_DATA;
name.vt = VT_R4;
name.pstrName = L"ImageQuality";

CComVariant value(0.75F);

HR(properties->Write(
  1, // property count
  &name,
  &value));

イメージ品質の値は、0.0 が最低品質、1.0 が最高品質になります。

エンコーダ プロパティを設定したら、フレームを構成して書き込む前に Initialize メソッドを呼び出す必要があります。

HR(frame->Initialize(properties));

次の手順は、ピクセルを書き込む前に、新しいフレームの寸法とピクセル形式を設定することです。

HR(frame->SetSize(width, height));
GUID pixelFormat = GUID_WICPixelFormat24bppBGR;
HR(frame->SetPixelFormat(&pixelFormat));
ASSERT(GUID_WICPixelFormat24bppBGR == pixelFormat);

SetPixelFormat パラメータは [in, out] パラメータです。入力時には、目的のピクセル形式を指定します。出力時には、サポートされる最も近いピクセル形式が格納されます。これは、別のビットマップのピクセル形式に基づくなどして実行時に形式が設定されない限り、一般には問題になりません。

フレームへのピクセルの書き込みには、以下のように WritePixels メソッドを使用します。

HRESULT WritePixels(
  UINT lineCount,
  UINT stride,
  UINT bufferSize,
  BYTE* buffer);

lineCount パラメータは、書き込むピクセルのライン数を指定します。これは、WritePixels を何度も呼び出して完全なフレームを書き込めることを意味します。stride パラメータは、バッファ内のピクセルが行にパックされている方法を示します。stride の計算方法については前のセクションで説明しました。

WritePixels を呼び出して完全なフレームを書き込んだら、フレームの Commit メソッドを呼び出して、フレームの準備ができたことをエンコーダに知らせる必要があります。イメージを構成するすべてのフレームをコミットし終えたら、エンコーダの Commit メソッドを呼び出して、イメージを保存する準備ができたことをエンコーダに知らせる必要があります。

ここまで、イメージのエンコードとデコードの基礎について説明してきました。次に進む前に、簡単な例を示します。図 2 に、アイコンを構成する個々のビットマップを読み取ってマルチフレーム TIFF イメージにコピーするための CopyIconToTiff 関数を示します。

Figure 2 CopyIconToTiff (Close Figure 2)
 HRESULT CopyIconToTiff(
   IStream* sourceStream,
   IStream* targetStream) {

  // Prepare the ICO decoder

  CComPtr<IWICBitmapDecoder> decoder;
  HR(decoder.CoCreateInstance(CLSID_WICIcoDecoder));

  HR(decoder->Initialize(
    sourceStream,
    WICDecodeMetadataCacheOnDemand));

  // Prepare the TIFF encoder

  CComPtr<IWICBitmapEncoder> encoder;
  HR(encoder.CoCreateInstance(CLSID_WICTiffEncoder));

  HR(encoder->Initialize(
    targetStream,
    WICBitmapEncoderNoCache));

  UINT frameCount = 0;
  HR(decoder->GetFrameCount(&frameCount));

  for (UINT index = 0; index < frameCount; ++index) {
    // Get the source frame info

    CComPtr<IWICBitmapFrameDecode> sourceFrame;

    HR(decoder->GetFrame(index, &sourceFrame));

    UINT width = 0;
    UINT height = 0;
    HR(sourceFrame->GetSize(&width, &height));

    GUID pixelFormat = { 0 };
    HR(sourceFrame->GetPixelFormat(&pixelFormat));

    // Prepare the target frame

    CComPtr<IWICBitmapFrameEncode> targetFrame;

    HR(encoder->CreateNewFrame(
      &targetFrame,
      0)); // no properties

    HR(targetFrame->Initialize(0)); // no properties

    HR(targetFrame->SetSize(width, height));
    HR(targetFrame->SetPixelFormat(&pixelFormat));

    // Copy the pixels and commit frame

    HR(targetFrame->WriteSource(sourceFrame, 0));
    HR(targetFrame->Commit());
  }

  // Commit image to stream

  HR(encoder->Commit());

  return S OK;
}

この例では、WritePixels メソッドに代わる方法を利用して、さらに処理が簡単になっています。ソース フレームからピクセルをコピーしてターゲット フレームに書き込む代わりに、指定した IWICBitmapSource インターフェイスから直接ピクセルを読み取る WriteSource メソッドを使用しています。IWICBitmapFrameDecode インターフェイスは IWICBitmapSource から派生しているため、ソリューションが簡潔になります。

WIC イメージング ファクトリ

WIC では、WIC 関連のさまざまなオブジェクトを作成するためのイメージング ファクトリが提供されています。これは IWICImagingFactory インターフェイスを通じて公開されており、以下のようにして作成できます。

CComPtr<IWICImagingFactory> factory;
HR(factory.CoCreateInstance(CLSID_WICImagingFactory));

実装を識別する CLSID がわかっている場合に、特定のイメージ形式用のデコーダを作成する方法については既に説明しました。もちろん、おわかりのように、特定の実装を指定したり、イメージの形式をハードコーディングしたりしなくても済むのであれば、さらに便利です。

幸運にも、WIC ではそのためのソリューションが提供されています。デコーダを作成する前に、WIC は、特定のストリーム中にイメージ形式を示すパターンがないか調べることができます。最適な一致が見つかると、適切なデコーダが作成され、そのストリームで初期化されます。この機能は CreateDecoderFromStream メソッドで提供されます。

CComPtr<IWICBitmapDecoder> decoder;

HR(factory->CreateDecoderFromStream(
  stream,
  0, // vendor
  WICDecodeMetadataCacheOnDemand,
  &decoder));

第 2 パラメータはデコーダの製造元を示します。これはオプションですが、特定の製造元のコーデックを使用する場合に便利です。ただし、これはあくまでヒントであることに注意してください。特定の製造元の適合するデコーダがインストールされていない場合は、製造元とは無関係にデコーダが選択されます。

IWICImagingFactory では、CreateDecoderFromFilename メソッドと CreateDecoderFromFileHandle メソッドも提供されています。これは、それぞれファイルへのパスとファイル ハンドルに対して同じ機能を提供します。また、CLSID やストリームを指定せず、イメージ形式を指定してデコーダを作成することもできます。それには CreateDecoder メソッドを使用します。

CComPtr<IWICBitmapDecoder> decoder;

HR(factory->CreateDecoder(
  GUID_ContainerFormatIco,
  0, // vendor
  &decoder));

HR(decoder->Initialize(
  stream,
  WICDecodeMetadataCacheOnDemand));

同様に、CreateEncoder メソッドでは、その実装を考慮せずに、特定のイメージ形式用のエンコーダを作成できます。

CComPtr<IWICBitmapEncoder> encoder;

HR(factory->CreateEncoder(
  GUID_ContainerFormatBmp,
  0, // vendor
  &encoder));

HR(encoder->Initialize(
  stream,
  WICBitmapEncoderNoCache));

図 3 に、コンテナ形式とも呼ばれる、実装に依存しないイメージ形式を示す GUID の一覧を示します。

Figure 3 コンテナ形式の GUID (Close Figure 3)

形式 | GUID
BMP | GUID_ContainerFormatBmp
PNG | GUID_ContainerFormatPng
ICO | GUID_ContainerFormatIco
JPEG | GUID_ContainerFormatJpeg
GIF | GUID_ContainerFormatGif
TIFF | GUID_ContainerFormatTiff
HDP | GUID_ContainerFormatWmp

ストリームを使用する

有効な IStream の実装であれば、自由に WIC で使用できます。たとえば、XmlLite の記事で説明した CreateStreamOnHGlobal 関数や SHCreateStreamOnFile 関数を使用することも、独自の実装を記述することもできます。WIC では、便利で柔軟な IStream の実装も提供されています。

前のセクションで説明したイメージング ファクトリを使用し、以下のようにして初期化されていないストリーム オブジェクトを作成できます。

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

IWICStream インターフェイスは IStream から継承されており、ストリームをさまざまなバッキング ストレージに関連付けるためのいくつかのメソッドが提供されています。たとえば、InitializeFromFilename を使用して、特定のファイルにバッキングされたストリームを作成することができます。

HR(stream->InitializeFromFilename(
  L"file path",
  GENERIC_READ));

また、InitializeFromIStreamRegion を使用して、別のストリームのサブセットとしてストリームを作成したり、InitializeFromMemory を使用して、メモリ ブロック上にストリームを作成したりできます。

WPF による WIC

前述のように、WIC は WPF のイメージング機能のベースとなるフレームワークを提供しています。さまざまなイメージング クラスが System.Windows.Media.Imaging 名前空間で定義されています。マネージ コードから簡単に使用できることを示すために、図 4 に、図 2 の CopyIconToTiff 関数を、WPF のラッパー クラスを使用して C# で書き直したものを示します。

Figure 4 C# と WPF で書き直した CopyIconToTiff (Close Figure 4)
static void CopyIconToTiff(Stream sourceStream,
                           Stream targetStream) {

  IconBitmapDecoder decoder = new IconBitmapDecoder(
    sourceStream,
    BitmapCreateOptions.None,
    BitmapCacheOption.OnDemand);

  TiffBitmapEncoder encoder = new TiffBitmapEncoder();

  foreach (BitmapFrame frame in decoder.Frames) {
    encoder.Frames.Add(frame);
  }

  encoder.Save(targetStream);
}

値 BitmapCacheOption.OnDemand は、ネイティブ コードで使用したデコーダ オプション WICDecodeMetadataCacheOnDemand に対応します。同様に、値 BitmapCacheOption.OnLoad は、デコーダ オプション WICDecodeMetadataCacheOnLoad に対応します。

デコーダがイメージ情報をメモリに読み込むときにこれらのオプションが与える影響については、既に説明しました。ただし、これらのオプションをマネージ コードで扱う場合には、他にも注意すべき副作用があります。BitmapCacheOption.OnDemand を指定した場合に何が起きるかを考えてください。デコーダは、下位のストリームへの参照を保持し、ビットマップ デコーダ オブジェクトが作成された後のいつかの時点でそこから読み取ります。その際、ストリームがまだ使用可能であることが前提です。そのため、アプリケーションがあまり早くストリームを閉じないように注意する必要があります。これは、デコーダが使い終えるまでストリームを閉じないようにするという、ストリームの有効期間の管理の問題です。

これはネイティブなコードには影響しません。IStream インターフェイスは標準の COM インターフェイスであり、その有効期間は参照カウントによって制御されます。アプリケーションがこのインターフェイスに対するすべての参照カウントを解放した後でも、デコーダは必要な限り 1 つの参照を保持しています。さらに、ストリームは、すべてのインターフェイス ポインタが解放された後にだけ閉じられます。

次回の予定

WIC では、きわめて強力で柔軟なフレームワークが提供されており、各自のイメージング要件のベースとして利用できます。多数のコーデックと単純な API を使用して、すぐにその機能の多くを利用できます。

次回のコラムでは、WIC で提供される高度な機能について説明します。独自のコーデックを開発する方法と、パターンマッチ機能を含む登録および検出処理について詳しく説明します。また、組み込みコーデックの 1 つについて、その制限に対する対策を実施します。

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

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


{ End Bracket }: ソフトウェアの魔法

{ End Bracket }
ソフトウェアの魔法
Julia Lerman

ソフトウェア開発者はよく、 スティーブン・スピルバーグの映画『マイノリティ・リポート』に登場する透明のコンピュータ画面や、ドラマ『CSI』で使われている派手な指紋分析ソフトウェア プログラムなど、今日の映画やテレビでのハイテク コンピューティングのやたらと非現実的な描写を笑います。しかし、これらの映画や番組で披露された妙技はおそらく、思ったより早く実現されるでしょう。

2004 年、マイクロソフトのプラットフォーム エバンジェリズム チームの Carter Maslan は、現在 Microsoft® .NET Framework 3.0 に含まれるツールの多くを使用して、当時 "Longhorn" のコードネームで呼ばれていた OS (現在の Windows Vista®) の最初の概念的なアプリケーションのいくつかを記述しました。最初のサンプルである不動産市場の概念アプリケーションに関するビデオを見た後、テレビの犯罪ドラマのすばらしい空想の産物が実際に現実となりつつある、実は現実の方が芸術を真似していると思ったことを覚えています。

最近、私は Mix n Mash 08 というマイクロソフトでの終日のイベントに招待されました。このイベントは、3 月にラスベガスで開催される Microsoft MIX08 カンファレンスの先駆けです。MIX は、マイクロソフトが提供する新しい Web テクノロジに関するイベントです。Mix n Mash イベントでは、マイクロソフトが取り組んでいる、既に公表されているいくつかのプロジェクトを見て、現在の状況と今後のプランを話し合うことができました。

見所の 1 つは、Microsoft Surface コンピュータ (microsoft.com/surface) でした。サーフェイス コンピューティングは、中核の Windows Presentation Foundation (WPF) と、テーブル形式のマルチタッチ画面を組み合わせています。

これまでに Surface 用に構築されたアプリケーションは、視覚的に刺激的でおもしろいものからまじめなビジネス アプリケーションまでさまざまです。Surface は非常に驚きに満ちています。最初のデモンストレーションでは、水に小石が沈んでいる画像がありました。テーブルの上で手を動かすと、その画像の水が本物の水のように動くのです。しかし手はぬれません。

別のアプリケーションでは、フィンガー ペインティングを楽しみました。大人にはすばらしい息抜きです。次に、Zune が Surface 上に置かれました。すぐに Zune の格納メディアが姿を現し、テーブル上でメディアを操作できるようになりました。しかし別のアプリケーションでは、Surface がレストランのテーブルに変わり、テーブル上で食べ物や飲み物を注文したり支払いをすることができました。Surface は非常に進んでいるので、次世代のコンピューティングで可能なことを表した概念のように見えますが、今日現実に存在するのです。

1 日の終わりに、2008 年 7 月には Bill and Melinda Gates Foundation の業務に時間を費やすためにマイクロソフトの舵取りを降りることになっている Bill Gates に質問をする機会がありました。コンピューティングに関する彼のビジョンと、彼が去った後にどうなってほしいかをたずねました。マイクロソフトの技術的な動機付けの大部分は、コンピュータ サイエンスの最終的なゴールを確認したいという欲求に基づいていると彼は説明しました。彼が言うには、そのゴールとは、いつかコンピュータが「ハイパー インテリジェント」になることと、「それを "実現" できたときには、人がコンピュータにどうすべきかをたずねれば教えてくれる」ことです。これにはまだ数十年かかるでしょうが、マイクロソフトは現在、多少個人秘書のように機能するコンピュータと Bill Gates が表現するものを目標としています。

Bill Gates はまた、少し時間を割いてマイクロソフトの探求の過程についても説明しました。あるグループは、これから 10 年以内に特定のコミュニティ (開発者、インフォメーション ワーカー、消費者など) のためにコンピュータができることを想像する任務を負っているということです。このような探求により生まれたアイデアは、Microsoft Research に送られ、さらに掘り下げられます。これらのプロジェクトは定期的な探求サミットでその進捗が追跡されます。たとえば、Microsoft Research は現在、音声透かし技術、カラー バーコード技術、記憶維持のリサーチ、その他数百の、非常に革新的なテクノロジ リサーチに基づくイニシアチブに取り組んでいます。

空想といえば、テレビで犯罪ドラマの登場人物を構成する画面ピクセルに存在しないものが映っているように見えると、ばかばかしいことをしてみるのが最近のお気に入りの 1 つです。写真を拡大して、だれかのサングラスの反射に致命的なネタばれ情報を見つけると、とても愉快です。しかし、Mix n Mash 08 から戻ってからは、以前ほど笑っていません。これらのテレビ番組が提案しているようなものにますます興味が湧いているのです。確かに、犯罪捜査官は既に指紋および顔認識ソフトウェアを利用していますが、これらのテクノロジはいずれも、架空の犯罪ドラマに登場するものほど有能になるにはまだ長い道のりがあります。おそらく Microsoft Research のパターン認識および機械学習チームがこれに取り組むでしょう。Bill Gates は私たちにこのように言いました。「ソフトウェアの魔法は何をしようとしているのでしょう」

Julia Lerman は、20 年以上ソフトウェアの構築に携わる .NET コンサルタントです。.NET コミュニティで会議講演者、執筆者、Microsoft .NET MVP、および Vermont .NET User Group のリーダーとしてよく知られています。彼女の次の書籍のタイトルは『Programming Entity Framework』です。Julia のブログは thedatafarm.com/blog で公開中です。