: MSDN Magazine September 2007

September 2007
SEPTEMBER 2007 ボリューム 21 番号 9

• ASP.NET: ScriptManager コントロールを使用して Web アプリケーションで AJAX を利用する
Ben Rush

• 検索: ASP.NET の統合検索機能でサイトを検索する
Marco Bellinaso

• ネットワーク: .NET Framework 3.5 での接続
Mariya Atanasova、Larry Cleeton、Mike Flasko および Amit Paka

• IIS 7.0: WAS を使用して WCF サービスを拡張する (非 HTTP プロトコルのサポート)
Dominick Baier、Christian Weyer、および Steve Maine

• 単体テスト: テスト代替の連続性について検討する
Mark Seemann

Code Download (718 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: COM 接続ポイント
Thottam R. Sriram

• 基本的な本能: ラムダ式
Timothy Ng

• Cutting Edge: AJAX アプリケーション アーキテクチャ、第 1 部
Dino Esposito

• チーム システム: 作業項目と取り消しのサポート
Brian A. Randell

• 基礎: ワークフロー通信
Matt Milner

• セキュリティに関するブリーフィング: 要求に基づく ID の詳細
Keith Brown

• .NET の問題: CryptoRandom を使う
Stephen Toub および Shawn Farkas

• { End Bracket }: WPF を活用した閲覧方法 "Turning the Pages"
Tim Sneath

各出版物

• 編集者のメモ: ニューヨークの夏
Joshua Trupin

• ツールボックス: ASP.NET コントロール、FTP、その他の機能
Scott Mitchell


ASP.NET: ScriptManager コントロールを使用して Web アプリケーションで AJAX を利用する

ASP.NET

ScriptManager コントロールを使用して Web アプリケーションで AJAX を利用する

Ben Rush
この記事の内容 : :
  • ASP.NET AJAX における ScriptManager コントロールの役割
  • ScriptManager コントロールの暗黙的な使用と明示的な使用
  • ASP.NET AJAX の Web サービス サポート
  • ASP.NET AJAX ページのライフサイクル
この記事は次のテクノロジを使用しています:
ASP.NET AJAX
コンテンツ
ScriptManager コントロールを使用したスクリプトを記述する
AJAX と ScriptManager コントロール
Web サービスと ScriptManager コントロール
認証と個人用設定
動作のしくみ
ScriptManager コントロールにオブジェクトを登録する
ASP.NET AJAX で AJAX 技術を使用する
スクリプトに関する補足情報

今日では、Web サイトの使用者が、公開者としてその内容、方向性、および実効性に関与することがあります。このような新しいスタイルの Web サイトの例としては、ソーシャル ネットワーキング サイト、ブログ、オンライン写真誌、Wiki などがあります。今はまだこのような Web サイトの黎明期であり、毎日のように新しい Web サイトが誕生しています。現在最高のコンテンツを提供している Web サイトであっても、その Web サイトの発展にユーザーが関与できないとしたら、すぐに役に立たなくなってしまうでしょう。

開発者の仕事は、自分の裁量でさまざまなツールを利用して、平均的使用者のニーズに応えることです。Web サイトの魅力を高めるには、まず Web ブラウザそのものに手を付けるのが王道です。なぜなら、Web ブラウザは使用者に最も近い場所にあるからです。ただし、現在の Web ブラウザが備えている豊富な機能を活用するのは、必ずしも容易ではありません。ブラウザと通信するコードを記述するのはかなりたいへんな作業です。なぜなら、ブラウザと OS の組み合わせは無限にあり、あまりにも一貫性がないからです。リッチ クライアント アプリケーションと Web アプリケーションを並行開発するには、プラットフォームを一本化すると便利です。そうすると、あるブラウザ環境においてユーザーの使い勝手を洗練するために行った作業を、別のブラウザ環境に対して繰り返し行う必要がなくなります。

Microsoft の ASP.NET AJAX は、Web アプリケーション開発におけるこのようなニーズに応えるためにリリースされたものです。私がこの記事を書いた目的は、ASP.NET AJAX の中核コンポーネントである ScriptManager コントロールについての理解を深めてもらうこと、および、ScriptManager コントロールを使用することによる ASP.NET AJAX の高度なプログラミング方法を示すことです。ScriptManager は、Web フォーム上に配置するサーバー側コントロールであり、ASP.NET AJAX の真価を引き出すのに役立ちます。ScriptManager コントロールの主な役割は、Web フォーム上の他のすべての ASP.NET AJAX コントロールを調整すること、および、Web ブラウザに適切なスクリプト ライブラリを追加して、ASP.NET AJAX のクライアント側が適切に機能するようにすることです。ScriptManager コントロールに、他のコントロール、Web サービス、およびクライアント スクリプトを登録することもできます。

サーバー側コントロールである ScriptManager は、ASP.NET ページ ライフサイクル内で発生するイベントに反応します。ScriptManager コントロールは、それらのイベントに基づいて、ASP.NET AJAX によって使用されるすべてのコントロール、オプション、およびコードの動作を調整します。また、ScriptManager コントロールは、特定のイベントをフックし、イベント発生時に通知を受け取り、環境に応じて設定を構成します。ASP.NET ページの表示サイクルの間、このプロセスが繰り返されます。なお、多くの場合は、ScriptManager コントロールによって設定を構成すると、ASP.NET AJAX をシームレスに使用するために必要な設定が行われたことになります。

まず、ScriptManager コントロールによって有効利用できる、ASP.NET AJAX の主要機能について説明します。その後、サーバー上での ScriptManager コントロールのライフサイクルについて説明します。ScriptManager コントロールの内部動作を把握すると、ScriptManager コントロールの Web アプリケーション開発用オプションに対する理解を深めること、および、ScriptManager コントロールを最大限に利用する方法を習得することができます。

まず、ASP.NET AJAX の中核要素であるスクリプトから説明します。実際、ASP.NET AJAX のすべての機能は、そのスクリプト ライブラリに依存します。その後、ASP.NET AJAX における AJAX サポート機能、Web サービスと通信する方法、および、認証について説明します。各項目の説明の中で、ScriptManager コントロールを使用してオプションを調整する方法を示します。

ScriptManager コントロールを使用したスクリプトを記述する

図 1 に示すコードは、ASP.NET AJAX でクラスを定義する標準的な方法を示したものです。この記事では、クライアント スクリプト ライブラリの内容については説明しません。なお、ASP.NET AJAX のスクリプト拡張機能を使用してクラスを作成する一般的な手順は、次のとおりです。

  1. ASP.NET AJAX に名前空間を登録します。
  2. コンストラクタ メソッドを作成します。
  3. メンバ メソッドとその機能を入力することにより、クラス プロトタイプを作成します。
  4. ASP.NET AJAX にクラスを登録します。
  5. ScriptManager コントロールによって追加されたクライアント側スクリプトに対して、型定義の末尾 (Sys.Application.notifyScriptLoaded メソッドの呼び出し) に達したことを通知します。

このクラスの機能は、クライアントにのみ公開されます。ただし、ScriptManager コントロールを使用すると、ASP.NET AJAX におけるスクリプト記述の興味深い特徴を利用できます。その特徴とは、クラスの機能をクライアント上の JavaScript とサーバー上の Microsoft® .NET Framework コードの両方に公開できるというものです。たとえば、あるコントロールの使用者がそのインスタンスでプロパティを次のように設定したとします。

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        this.MyCustomButton.AlertMessage = "Hey, stop clicking me!";
    }
}

この場合、ブラウザ側で次のようなスクリプトを記述することによって、そのコントロール インスタンスと通信できることが、直観的にわかります。

<script type="text/javascript">
    function get_text()
    {
        var mb = $find("MyCustomButton");
        var text = mb.get_AlertMessage();
        // do something with the text
    }
</script>

サーバー側とクライアント側の両方で、コントロールへの参照として MyCustomButton という同じ名前を使用しているという点に注目してください。また、AlertMessage プロパティとも通信しますが、その値がサーバーとクライアントの境界をシームレスに越えているように見えます。これは自然なように見えますが、ASP.NET AJAX が登場する前は、このような統合型サーバー/クライアント プログラミング モデルを利用するのは簡単でなく、大量のカスタム コードを記述する必要がありました。現在では、この統合型サーバー/クライアント パラダイムが ASP.NET AJAX に組み込まれ、完全にサポートされています。

このようにサーバーとクライアントを密に統合するには、IScriptControl および IExtenderControl という 2 つの新しいインターフェイスを使用します。これらのインターフェイスを使用するには、これらのインターフェイスから ASP.NET Web コントロールを継承し、必要なインターフェイス メソッドを実装し、そのコントロールをそのページの ScriptManager コントロールに登録します。たとえば、次のサーバー側コントロール コードのスケルトンでは、IScriptControl インターフェイスに必要なメソッドを実装しています。

class CustomButton : Button, IScriptControl
{
    IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
    {
        ...
    }

    IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
    {
        ...
    }
}

GetScriptDescriptors メソッドおよび GetScriptReferences メソッドから、コントロール コードをサーバー オブジェクトおよびクライアント オブジェクトとして論理的に表すために必要な情報が、ScriptManager コントロールにすべて返されます。最終結果は前述のようになります。つまり、オブジェクト インスタンスがサーバーとクライアントの境界を越えます。

GetScriptReferences メソッドからは、コントロール コードに必要な、スクリプト ファイルのリストが返されます。GetScriptReferences から返される必要のあるスクリプト ファイルの 1 つに、コントロールに対する定義がスクリプト クラス (つまり、サーバー側コントロールのクライアント スクリプト バージョン) として記述されています。

一方、GetScriptDescriptors メソッドからは、クライアント クラスのプロパティとイベントを記述した ScriptDescriptors オブジェクトが返されます。ScriptDescriptors オブジェクトは、クライアント クラスに対するメタデータ (プロパティ、プロパティ値など) の格納場所であると見なすことができます。

図 2 に、前述のサーバー側コントロールの詳しい例を示します。この図を見ると、GetScriptDescriptors メソッドと GetScriptReferences メソッドの内容がわかります。

Figure 2 GetScriptDescriptors メソッドと GetScriptReferences メソッドを使用する (Close Figure 2)
private string _alertMessage = null;

public string AlertMessage
{
    get { return _alertMessage; } set { alertMessage = value; }
}

public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
{
    ScriptControlDescriptor descriptor = new ScriptControlDescriptor(
        "CustomButton", this.ClientID);
    descriptor.AddProperty("AlertMessage", this._alertMessage);
    return new ScriptDescriptor[] { descriptor };
}

public IEnumerable<ScriptReference> GetScriptReferences()
{
    ScriptReference reference = new ScriptReference(
        "MyCustomContent.JScript1.js", "MyCustomContent");
    return new ScriptReference[] { reference };
}

GetScriptDescriptors メソッドの中で、ScriptDescriptor オブジェクトのインスタンスを作成します。ScriptDescriptor オブジェクトに対するコンストラクタの名前は、私が記述する CustomButton コントロールからとっています。続いて、AlertMessage プロパティとその値 (プライベート メンバである _alertMessage 内に保持される) を ScriptDescriptor オブジェクトに追加します。これで、クライアント側コードと通信する AlertMessage プロパティが、ASP.NET AJAX に記述されました。

GetScriptReferences メソッドの中で、.js ファイルを指す ScriptReference オブジェクトを作成して返している点に、注目してください。この .js ファイルには、そのコントロールに対するクライアント側コードが保持されます。つまり、すべてのクライアント側機能が保持されます。

これで、このコントロールを ScriptManager に登録できる状態になりました。登録すると、そのコントロールが存在していることが、ScriptManager コントロールによって認識されます (この登録手順については、後で説明します)。ページが表示される際、ScriptManager コントロールはさまざまなイベントに反応します。具体的には、通知を受信し、GetScriptDescriptors メソッドと GetScriptReferences メソッドを呼び出します。この 2 つのメソッドから、ScriptManager コントロールに、ScriptDescriptors オブジェクトと ScriptReferences オブジェクトが返されます。これらのオブジェクトには、クライアント側オブジェクトをサーバー側の対応オブジェクトにフックするために必要な情報が、すべて記述されています。この記事の例では、ScriptDescriptor オブジェクトに、AlertMessage プロパティが指定された CustomButton という名前のカスタム コントロールが記述されています。また、GetScriptDescriptors メソッド内から設定されたプロパティ値も記述されています。

ScriptManager コントロールによって、外部 .js ファイルである MyCustomContent.JSScript1.js への参照が取得されます。このスクリプト ファイルへの参照は、GetScriptReferences メソッドから返された ScriptReference オブジェクトの形式になります。この .js ファイルの中で、このコントロールのクライアント側クラスに対するプロトタイプとフッターは、図 3 のように定義されています。

Figure 3 コントロール クライアントのプロトタイプ (Close Figure 3)
CustomButton.prototype = {
    initialize : function() {
        // initialize the base
        CustomButton.callBaseMethod(this,'initialize');

        this._onclick =
            Function.createDelegate(this, this._onclick);

        Sys.UI.DomEvent.addHandler(
            this.get_element(), 'click', this._onclick);
    },
    dispose : function() {
        // release the handlers
        $clearHandlers(this.get_element());

        // call to the base to do its dispose
        CustomButton.callBaseMethod(this,'dispose');
    },
    get_AlertMessage: function(){
        return this._alertMessage;
    },
    set_AlertMessage: function(value){
        this._alertMessage = value;
        return;
    },
    _onclick: function(e){
        alert(this._alertMessage);
        return;
    }
}
CustomButton.registerClass("CustomButton",Sys.UI.Control);
if(typeof(Sys)!=='undefined') Sys.Application.notifyScriptLoaded();

最初に注目すべき点は、このクライアント側クラスには AlertMessage プロパティに対する get_ メソッドと set_ メソッドが含まれているということです。ScriptManager コントロールによって、この AlertMessage プロパティが、ScriptDescriptor オブジェクトを使用してサーバー上で指定した値に設定されます。サーバー側コードから AlertMessage プロパティを公開したので、サーバー上でこのプロパティに設定された値が、クライアント側オブジェクト上で公開される AlertMessage プロパティに反映されます。

AJAX と ScriptManager コントロール

多くの開発者は、ASP.NET AJAX を初めて使用する際、まず UpdatePanel コントロールを利用します。ScriptManager コントロールがページ上に配置されており、かつ、UpdatePanel コントロール内になんらかのコントロールが配置されている場合、AJAX の機能を利用して、UpdatePanel コントロール内のコントロールを非同期更新することができます。Visual Studio® のデザイナ画面では通常、図 4 のようなレイアウトになります。

図 4 単純な非同期型コントロール
図 4 単純な非同期型コントロール

実際のレイアウトは、この図ほ単純ではないこともあります。このレイアウト内の Button をクリックすると、ポストバック イベントが発生し、UpdatePanel コントロールによって捕捉されます。続いて、UpdatePanel コントロールからそのポストバック イベントが部分ポストバックとして再発行され、UpdatePanel コントロールの中身が非同期更新されます (ブラウザでページ全体を再読み込みする必要はありません)。

ただし、思ったとおりに動作せずに混乱することもよくあります。そのような場合でも、興味深いさまざまな手段が用意されています。たとえば、UpdatePanel コントロールと AJAX を使用してスクリプト参照を解除する、新しい魅力的な方法を利用できます。以前は、次のようにスクリプトを全体ポストバックの結果としてページに追加していました。

protected void Button1_Click(object sender, EventArgs e)
{
    Page.ClientScript.RegisterStartupScript(
        this.GetType(),"myscript","alert('hello world!');");
}

ただし、この新しいメソッドは、ASP.NET AJAX では正しく動作しない可能性があります。なぜなら、ClientScriptManager コントロールでは部分ポストバックと部分表示が認識されないので、ClientScriptManager コントロールに登録されたスクリプト コードが、部分ポストバックに対して出力されるページ内に含まれないからです。

このボタンをクリックしたときにクライアント スクリプトを出力ページに追加するには、次のように ScriptManager コントロールを利用します。

protected void Button1_Click(object sender, EventArgs e)
{
    ScriptManager.RegisterStartupScript(
        this,this.GetType(),"myscript","alert('hello world!');",true);
}

結果は同じですが、メソッドがわずかに異なります。ClientScriptManager コントロールのメソッドが、静的メソッド (RegisterStartupScript など) として ScriptManager コントロールに追加されています。ASP.NET AJAX を利用しており、かつ、部分ポストバックを使用してスクリプトを処理する場合、ScriptManager コントロールによって公開されているメソッドを使用する必要があります。

高度な例をもう 1 つ示します。図 5 のコントロール レイアウトを見てください。通常は、LinkButton をクリックすると、Web フォームの全体ポストバックが実行されます。では、LinkButton をクリックしたときに、UpdatePanel コントロールを非同期更新するには、どうすればよいでしょうか。つまり、LinkButton をクリックしたときに、LinkButton が UpdatePanel コントロールの内部にあるかのように UpdatePanel コントロールを動作させるには、どうすればよいでしょうか。このように動作させるには、次のように、ScriptManager コントロールの RegisterAsyncPostBackControl メソッドを使用します。

図 5 LinkButton をクリックしたときに非同期更新する
図 5 LinkButton をクリックしたときに非同期更新する

protected void Page_Load(object sender, EventArgs e)
{
    // register the LinkButton1 control as one that can
    // cause partial postbacks.
    ScriptManager1.RegisterAsyncPostBackControl(LinkButton1);
}

この場合、LinkButton をクリックすると、UpdatePanel コントロールの中身が非同期更新されます。つまり、LinkButton が UpdatePanel コントロール内にあるかのように動作します。

逆の動作も可能です。たとえば、UpdatePanel コントロール内の要素をクリックしたときに、ページ全体を更新する (全体ポストバックを実行する) こともできます。このように動作させるには、次のように、ScriptManager コントロールの RegisterPostBackControl メソッドを使用します。

protected void Page_Load(object sender, EventArgs e)
{
    // Button1 will cause a full post-back no matter
    // where it is on the page.
    ScriptManager1.RegisterPostBackControl(Button1);
}

このコードでは、Button1 が UpdatePanel コントロール内に配置されていたとしても、Button1 コントロールがクリックされると、ページの全体ポストバックが実行されます。

さらに詳しく見ていきましょう。ここまでは、ScriptManager を使用して、UpdatePanel コントロールの部分ポストバック イベントをトリガするコントロールを調整する方法について、説明しました。では、UpdatePanel コントロールの表示を更新する際に、UpdatePanel コントロール外のコントロールも更新するには、どうすればよいでしょうか。この動作も、ScriptManager コントロールを使用することによって可能です。

ScriptManager コントロールの RegisterDataItem メソッドを使用すると、UpdatePanel コントロール外のコントロールやデータを簡単に更新できます。RegisterDataItem メソッドを使用すると、UpdatePanel コントロールによってポストバックが実行されたときに、ユーザーが選択した他のデータをクライアントに送信できます。クライアント側で記述したスクリプト内で、これらのデータを使用できます。

図 6 のコントロールを例にとって説明します。この例において、Calendar コントロール内でクリックされた値で Label を更新します。この処理はシンプルなように見えるかもしれませんが、Calendar コントロールは UpdatePanel コントロール内にあり、一方、Label は UpdatePanel コントロール外にあります。UpdatePanel コントロールを使用して Label を更新するには、どうすればよいでしょうか。この答えは簡単です。サーバー側コードで RegisterDataItem メソッドを使用した場合、UpdatePanel コントロール外のデータをクライアント側コードに送信できます。クライアント側では、RegisterDataItem メソッドから送信されたデータを使用して、Label を更新することができます。

図 6 UpdatePanel コントロール外にあるコントロールを更新する
図 6 UpdatePanel コントロール外にあるコントロールを更新する

protected void Calendar1_SelectionChanged(object sender, EventArgs e)
{
    ScriptManager1.RegisterDataItem(
        Label1, Calendar1.SelectedDate.ToShortDateString());
}

RegisterDataItem メソッドでは、更新したいコントロールを第 1 引数として指定し、コントロールの更新に使用する未加工のデータを第 2 引数として指定します。ScriptManager コントロールは、ユーザーから渡されたデータをラップし、それを部分ポストバック イベントに対する応答内の一要素としてクライアントに送信します。クライアント側コードで、イベント完了後に ScriptManager コントロールから送信されたデータを取得します。次に例を示します。

<script type="text/javascript">
    Sys.WebForms.PageRequestManager.getInstance().add_pageLoading(
        PageLoadingHandler);
    function PageLoadingHandler(sender,args){
        var dataItems = args.get_dataItems();
        if($get('Label1')!==null){
            $get('Label1').innerHTML = dataItems['Label1'];
        }
        return;
    }
</script>

このスクリプト コードを見てください。このコードでは、さまざまな処理が実行されます。まず、クライアント側の PageRequestManager クラスによって、pageLoading イベントに備えた登録が行われます。次に、pageLoading イベント用の PageLoadingHandler イベント ハンドラが実装されます。続いて、第 2 引数 arg によって、データ項目の名前/値のコレクションが取得されます。最後に、サーバー上の RegisterDataItem メソッドに対する第 1 引数として指定したコントロールの名前を使用して、必要な値が取得されます。

Web サービスと ScriptManager コントロール

ASP.NET AJAX では、スクリプトと Web サービスの間で非同期通信すること、応答 (エラーを含む) を処理すること、および応答を加工することが、簡単にできるようになりました。これにより、ページの有用性と独自性が大幅に向上します。ASP.NET AJAX を使用すると、ブラウザからサーバーに至るあらゆる場所で、最新の Web 技術を直観的な方法で利用できます。

ScriptManager コントロールは、ASP.NET AJAX アプリケーションで必要となるすべてのサービスに対する、記録場所のような働きをします。図 7 に、Visual Studio で表示される、ScriptManager コントロールに対するプロパティ メニューを示します。

図 7 ScriptManager コントロールのプロパティ
図 7 ScriptManager コントロールのプロパティ

Scripts コレクションが強調表示されており、その下に Services コレクション (Web サービス用のコレクション) があります。クライアント側コードの通信相手となるすべてのサービスを ScriptManager コントロールに登録する必要があります。サービス参照を ScriptManager コントロールに登録するには、Services コレクションを展開し、参照を追加するだけです (図 8 を参照)。

図 8 Web サービス参照を追加する
図 8 Web サービス参照を追加する(画像を縮小するには、ここをクリックします)
図 8 Web サービス参照を追加する
図 8 Web サービス参照を追加する (画像を拡大するには、ここをクリックします)

この作業を行うと、内部ではどのような処理が行われるのでしょうか。詳細情報が簡潔に表示されます。ただし、ScriptManager コントロールを使用してサービスを参照しているページのソースを表示すると、次のようなコードが見つかることがあります。

<script src="WebService.asmx/jsdebug" type="text/javascript"></script>

この外部参照は、ScriptManager コントロールによって出力ページに追加されたものです。なぜなら、この Web サービスが ScriptManager コントロールに登録されているからです。Web サービスの名前の末尾に /jsdebug が付加されている点に注目してください。リリース ビルドでは、/js のみが付加されます。/js が付加されている場合、この Web サービスに対する要求が ASP.NET AJAX パイプラインによって認識されると、この Web サービスのメソッドをラップしているスクリプト クラス (プロキシ スクリプト) が返されます。このプロキシ スクリプト クラスの各メソッドによって、対応する Web サービス メソッドに対する非同期呼び出しが実行されます。したがって、Web サービス メソッドを呼び出すには、ASP.NET AJAX の Web サービス フレームワークによって作成されたスクリプト クラスの、対応メソッドを呼び出すだけでかまいません。これは実に簡単です。

WebServices という名前のサービスによって Add メソッドが公開されている場合を考えてみましょう。次に例を示します。

[WebMethod]
public int Add(int a, int b) { return a + b; }

次のスクリプトからこのメソッドを呼び出すことができます。

function CallAdd()
{
    // method will return immediately
    // processing done asynchronously
    WebService.Add(0,6, OnMethodSucceeded, OnMethodFailed);
}

この例では、フレームワークから渡された自動生成されたスクリプト プロキシ クラスによって、Web サービス メソッド呼び出しがラップされているので、スクリプトから Web サービスと通信できます。このように、WebService.Add から、対応する Web サービス メソッドである Add が呼び出されます。

Web サービスを呼び出す際、通常、2 つのコールバックが定義されます。1 つは成功時用、もう 1 つは失敗時用です。Web サービス呼び出しは非同期で実行されるので、呼び出しが実際に完了したかどうかを知るためにコールバックが必要になります。たとえば、次の 2 つのメソッドではそれぞれ、成功時用コールバックと失敗時用コールバックが実装されています。

function OnMethodSucceeded(result, eventArgs)
{
    var label = Sys.UI.DomElement.getElementById('Label1');
    var sb = new Sys.StringBuilder();
    sb.append('You got back a value of ');
    sb.append(result.toString());
    label.innerHTML = sb.toString();
}

function OnMethodFailed(error)
{
    alert(error.get_message());
}

要求が送信され、結果が Web サービスから非同期で受信されるので、最終的な結果は、UpdatePanel コントロールを使用した場合に似たものになります。ブラウザで全体ポストバックを実行しなくても、Web サービスから受信したデータでコンテンツを更新できます。前述のサンプル コードでは、WebMethod の Add メソッドを使用した Web サービス呼び出しの結果を基にして、Label1 を非同期更新しています。最終結果は、図 9 のようになります。全体ポストバックは実行されず、最終結果は完全にシームレスになります。

図 9 更新されたラベル
図 9 更新されたラベル

認証と個人用設定

ASP.NET AJAX の高度な機能 (認証、個人用設定など) でも、Web サービスが使用されます。ASP.NET AJAX の認証サービスでは、次に示す 2 つのメソッドが実装されます。1 つは、ユーザーをログインさせるためのメソッド、もう 1 つは、ユーザーをログアウトさせるためのメソッドです。

Sys.Services.AuthenticationService.login
Sys.Services.AuthenticationService.logout

ASP.NET AJAX ではフォーム認証が使用されます。つまり、有効なフォーム認証 Cookie が Web サーバーによってブラウザのセッションに挿入されると、ユーザーがログインされます。ASP.NET AJAX で Cookie データがセッションに挿入されるしくみは、全体ポストバックの場合と同じです。つまり、使用される機構に違いはありません。Cookie がブラウザのセッションに挿入されると、クライアントが認証を受け、限定されたページとコンテンツが表示されます。

クライアント側スクリプトで、特定のユーザーを認証してログインまたはログアウトさせることができるので、ユーザーベースの高度な対話型システムを構築できます。AuthenticationService とスクリプトを使用してユーザーをログインさせる例を次に示します。

<script type="text/javascript">
    function MyMethod(username, password)
    {
        Sys.Services.AuthenticationService.login(username,
            password,false,null,null,null,null,"User Context");
    }
</script>

ASP.NET AJAX アプリケーションで認証を有効にする手順については、ajax.asp.net で詳しく説明されているので、この記事では説明しません。ただし、当然のことですが、認証を有効にすると、スクリプトによってフォーム認証が簡単に実装されます。

標準の ASP.NET 認証サービスが自分のニーズに合わない場合、ユーザー独自の認証サービスを作成して ScriptManager コントロールに登録することができます。認証機能を実装する Web サービスに必要なメソッドは、次のとおりです。

[WebMethod]
public bool Login(string userName,
    string password, bool createPersistentCookie)
{
    ... // code to check user credentials and log user in
}

[WebMethod]
public void Logout()
{
    ... // code to log user out.
}

実装コードを記述したら、新しい認証サービスを ScriptManager コントロールに登録する必要があります。新しい認証サービスを登録すると、今までクライアント上で ASP.NET AJAX の既定の認証サービスと通信していたすべてのコードで、既定の認証サービスの代わりに、ユーザーが新たに定義した認証サービスが使用されるようになります。したがって、ユーザー独自の認証サービスを使用する際、クライアント側スクリプトに変更を加える必要はありません。

認証サービスを ScriptManager コントロールに宣言的に登録する例を次に示します。

<asp:ScriptManager ID="ScriptManager1"
  runat="server" >
    <AuthenticationService
  Path="WebService.asmx" />
</asp:ScriptManager>

この時点で実装せずに、ASP.NET AJAX で、ユーザー独自のプロファイル サービスを利用することもできます。プロファイル サービスは、認証サービスと同じ方法で ScriptManager コントロールに登録できます。プロファイル サービスを使用すると、Web サイトを特定ユーザー用にカスタマイズできます。カスタマイズはクライアント側スクリプトから実行されるので、ユーザーにとっては直観的かつシームレスです。ajax.asp.net でサンプルが公開されています。

動作のしくみ

ScriptManager コントロールを使用するプロセスは、2 つの基本的なフェーズに分かれています。第 1 フェーズでは、その環境で ASP.NET AJAX の豊富な機能がすべてサポートされているかどうかが検査され、機能のサポートに必要な要素がすべて設定されます。第 2 フェーズでは、クライアント側コードとの非同期通信が実際に実行されます。つまり、クライアント側スクリプトによって、必要なページ更新を実行できます。ScriptManager はサーバー側コントロールであり、また、ASP.NET での Web プログラミングはイベント駆動型なので、ScriptManager コントロールで重要なのは、イベントへの登録および応答方法です。ScriptManager コントロールが応答するイベントの概要を図 10 に示します。

図 10 ASP.NET AJAX でクラスを定義する
図 10 ASP.NET AJAX でクラスを定義する

ScriptManager コントロールにオブジェクトを登録する

ScriptManager コントロールにスクリプト参照とサービス参照を宣言的に追加できます。また、ScriptManager コントロールの Services プロパティと Scripts プロパティを使用したプログラムを作成して、スクリプト参照とサービス参照を追加することもできます。さらに、クライアントに送信する特別なデータを選択することや、ScriptManager コントロールと連係動作するコントロールを特別なスクリプト コントロールとして設計することもできます。これらの機能を利用するには、オブジェクトを ScriptManager コントロールに登録する必要があります。

ScriptManager コントロールでは、これらのスクリプト参照とサービス参照に対して何が行われているのでしょうか。ScriptManager コントロールと ASP.NET AJAX 機能をホストしているページがブラウザによって初めて要求されたとき、そのページの子コントロールの初期化ステージで ScriptManager コントロールの OnInit メソッドが呼び出されます。OnInit メソッドでは、多くの重要な処理が実行されます。たとえば、そのページに配置されている ScriptManager コントロールが 1 つだけであるかどうか、現在そのページに対して部分ポストバックが実行されているかどうか、などが検査されます。しかし、OnInit メソッドで実行される最も重要な処理は、ホスト ページによって生成される一連のイベント (InitComplete、PreRenderComplete、PreRender など) に備えて登録を行うことです。親ページでこれらのイベントが発生した場合、そのことが ScriptManager コントロールによって認識される必要があります。なぜなら、ASP.NET AJAX はこれらのページ イベントに基づいて動作するからです。

スクリプト参照とサービス参照をブラウザにロードするための最も重要なページ イベントは、そのページの PreRenderComplete イベントです。このイベントに対するハンドラは OnPagePreRenderComplete (図 11 を参照) です。このハンドラは、ScriptManager コントロール自体のメソッドです。

Figure 11 ScriptManager コントロールの OnPagePreRenderComplete イベント (Close Figure 11)
private void OnPagePreRenderComplete(object sender, EventArgs e)
{
    if (!IsInAsyncPostBack)
    {
        if (SupportsPartialRendering)
        {
            IPage.ClientScript.GetPostBackEventReference(
                new PostBackOptions(this, null, null, false,
                false, false, false, true, null));
        }

        RegisterGlobalizationScriptBlock();
        RegisterScripts();
        RegisterServices();
    }
    else RegisterScripts();
}

OnPagePreRenderComplete イベント ハンドラは、ScriptManager コントロールに登録したすべてのスクリプト (スクリプト コントロールに必要なスクリプト、ScriptManagerProxy コントロールから参照されるスクリプトなど) とサービスを処理し、そのページの PreRenderComplete イベントに渡すものです。そのページで部分ポストバックが実行中でない場合、スクリプトおよびサービスと共にグローバリゼーション スクリプト ブロックが登録されます。部分ポストバックが実行中の場合は、スクリプトだけが登録されます。部分ポストバックと全体ポストバックの両方に備えてスクリプトを登録する必要があります。なぜなら、ScriptManager コントロールにはいつでもスクリプトを登録できるからです。

スクリプトとサービスはすべてこのフェーズで登録されますが、このことは何を意味するのでしょうか。また、登録したスクリプト参照とサービス参照は、どのように変換されてページに出力されるのでしょうか。

今はまだ最初のページ要求を処理している段階なので、部分ポストバックは実行していません。このため、まだ ClientScriptManager を使用してスクリプトを登録することができます。たとえば、RegisterScripts メソッドは、ScriptManager コントロールに登録されている各スクリプトを順に処理し、そのページの既定の ClientScriptManager インスタンスに渡すものです。ページの表示時に、ClientScriptManager が呼び出され、スクリプト参照が出力ページに追加されます。これは、ASP.NET AJAX 登場前の時代の処理によく似ています。

RegisterScripts メソッドは、非同期ポストバック時にも呼び出されます (図 11 に示す else 句を参照)。RegisterScripts メソッドからは、ClientScriptManager が呼び出されますが、ClientScriptManager では、非同期ポストバック時に何をすべきかが不明なので、何の処理も行われません。代わりに、RegisterScripts メソッドから RegisterScriptIncludeInternal という内部メソッドが呼び出されます。この内部メソッドは、スクリプト参照を内部配列に格納し、後で使用できるようにするものです。

Web サービス参照についてはどうでしょうか。前述のとおり、Web サービス参照を ScriptManager コントロールに登録すると、スクリプト参照がページに追加されます。このスクリプト参照によって、ブラウザから指定 URL への要求が行われます。ASP.NET AJAX フレームワークでは、Web サービスを実装しているクラスが検査され、クライアント側コードで使用可能なプロキシ スクリプト クラスが返されます。ブラウザで、この自動生成されたプロキシ クラスがダウンロードされます。このプロキシ クラスは、Web サービス参照です。

自動生成されたプロキシ クラスへのスクリプト参照を生成するため、RegisterScripts メソッドで、各 Web サービス参照が順に処理され、その後次のメソッドが呼び出されます。

private string GetProxyPath(Control containingControl, bool debug)
{
    if (debug)
        return GetServicePath(containingControl, true) +
            RestHandlerFactory.ClientDebugProxyRequestPathInfo;
    else
        return GetServicePath(containingControl, true) +
            RestHandlerFactory.ClientProxyRequestPathInfo;
}

GetProxyPath メソッドによって生成されるスクリプト参照は、通常のスクリプト参照と同じように、つまり、初めてのページ要求に対して ClientScriptManager を使用し、スクリプト参照を内部配列に格納することによって、ページに追加されます。

この内部配列の実体は、複数個の配列です。前述のとおり、初めてのページ要求の場合、ScriptManager コントロールでは、スクリプト参照とサービス参照をそのページに対する ClientScriptManager クラスに追加するという、従来型の方法がとられます。ただし、ClientScriptManager クラスを使用する方法は、部分ポストバックの実行中は利用できません。代わりに ScriptManager コントロールで、他のメソッドを用意する必要があります。このときに、内部配列が使用されます。ご存じのとおり、部分ポストバックに対する応答は、適切な形式で解析がしやすいデータ ブロックです。クライアント側フレームワークでは、このデータ ブロックが検査および使用されます。ScriptManager コントロールの Render イベントが呼び出され、続いて、Render イベントから PageRequestManage クラスの ProcessScriptRegistration メンバ メソッドが呼び出されます (図 12 を参照)。

Figure 12 PageRequestManager クラスの ProcessScriptRegistration メソッド (Close Figure 12)
private void ProcessScriptRegistration(HtmlTextWriter writer)
{
    owner.ScriptRegistration.RenderActiveArrayDeclarations(
        updatePanelsToRefresh, writer);
    owner.ScriptRegistration.RenderActiveScripts(
        updatePanelsToRefresh, writer);
    owner.ScriptRegistration.RenderActiveSubmitStatements(
        updatePanelsToRefresh, writer);
    owner.ScriptRegistration.RenderActiveExpandos(
        updatePanelsToRefresh, writer);
    owner.ScriptRegistration.RenderActiveHiddenFields(
        updatePanelsToRefresh, writer);
    owner.ScriptRegistration.RenderActiveScriptDisposes(
        updatePanelsToRefresh, writer);
}

ASP.NET AJAX では、このスクリプト参照が出力ページに追加されます。各メソッドに Render フェーズ用の HtmlTextWriter が渡され、必要なデータが HtmlTextWriter に書き込まれます。各メソッド (RenderActiveScripts など) によって、値が格納されているそれぞれの内部配列が参照されます。

ASP.NET AJAX で AJAX 技術を使用する

Fiddler HTTP デバッガ プロキシ アプリケーション (fiddlertool.com/fiddler) を利用すると、ご使用のコンピュータ上で、Internet Explorer® を通過するすべての Web トラフィックをトレースできます。ajax.asp.net で AJAX のサンプルを探し、Fiddler によって行われる処理を調べてください。部分ポストバック イベントを呼び出すたびに HTTP POST が発生することがわかります。HTTP POST には、通常の HTTP ヘッダーに加えて、次のようなヘッダーも含まれています。このヘッダーは、初めて目にする方も多いと思います。

x-microsoftajax: Delta=true

このヘッダーは重要です。ScriptManager コントロールによってこのヘッダーが認識されると、出力ページに参照が受動的に挿入されるのではなく、FORM POST 要求内で送信されたデータが検査され、応答が、クライアント側スクリプトで認識可能な形式でクライアントに表示されます。

ScriptManager コントロールによって、最初の出力ページにサービス参照とスクリプト参照が追加されると共に、クライアント側機能が初期化されます。ページの初回の表示時に、2 つの開始スクリプトが ClientScriptManager に登録されます。1 つ目のスクリプトは、クライアント側ランタイムを初期化するメソッドを呼び出すものです。2 つ目のスクリプトは、前述の PageRequestManager クラスのクライアント側バージョンを初期化するものです。AJAX を理解するうえで、PageRequestManager を初期化する、この 2 つ目の初期化スクリプトは特に重要です。

このクライアント側スクリプトは、RenderPageRequestManagerScript メソッドによって出力ページに追加されます。私は、読みやすさを考慮してこのメソッドから大量のプラミング コードを省きましたが、通常は次のようになります。

internal void RenderPageRequestManagerScript(HtmlTextWriter writer)
{
    ...
    writer.Write(_owner.UniqueID);
    ...
    writer.Write(_owner.IPage.Form.ClientID);
    ...
    RenderUpdatePanelIDsFromList(writer, _allUpdatePanels);
    ...
    writer.Write(GetAsyncPostBackControlIDs(true));
    ...
    writer.Write(GetPostBackControlIDs(true));
    ...
}

UpdatePanel、PostBackControl ID、および AsyncPostBackControl ID がページに追加される方法に注目してください。ASP.NET AJAX を使用する際に必要なコントロールは、この 3 つだけです。クライアント側の PageRequestManager クラスは、ScriptManager コントロールに登録されている各コントロールによって生成されるすべてのイベントを追跡します。RenderPageRequestManagerScript メソッドは、PageRequestManager が監視するコントロールを伴う、クライアント上で動作する PageRequestManager を初期化するものです。

クライアントでポストバック イベントが発生すると、PageRequestManager によって、そのイベントが RenderPageRequestManagerScript メソッド内に記述されたスクリプトによって識別されるコントロールからトリガされたものであるかどうかが判別されます。そうである場合、PageRequestManager によって、ポストバック イベントが取り消されて再パッケージ化されます。ポストバック イベントから再パッケージ化されたデータは、クライアント側の Sys.Net.WebRequest クラスによって、サーバーに送信されます (Sys.Net.WebRequest はパブリック クラスであり、クライアント側コードから利用できます)。Sys.Net.WebRequest クラスによって、サーバーに送信される POST 要求内に、x-microsoftajax: Delta=true というヘッダーが設定されます。

サーバー側では、ScriptManager コントロールのインスタンスが生成され、そのページのコントロール ツリーにロードされています。ScriptManage はサーバー側コントロールなので、ASP.NET の LoadPostData メソッドによって、FORM POST 要求内でデータが通知されます。このメソッドでは、個々のコントロールが FORM POST 要求によってフィルタリングされ、適切な情報が抽出されます (これは標準的なイベントであり、ASP.NET AJAX に特有のものではありません)。図 10 のイベント図を見ると、ページ上で、InitComplete イベントが発生した直後に LoadPostData メソッドが実行されることがわかります。LoadPostData メソッド内では、ScriptManager コントロールによって、FORM POST 要求を発生させたコントロール (そのコントロールが配置されている UpdatePanel コントロールを含む) が識別されます。ポストバック イベントを発生させたコントロールの ID は、後で使用できるように保持されます。

この時点で、ScriptManager コントロールによって、部分ポストバックが発生していることが認識されており、また、そのポストバックを発生させたコントロールが特定されています。したがって、クライアントへの応答を作成することができます。ScriptManager コントロールによって、そのホスト ページに対する既定の Render メソッドが完全に上書きされ、ASP.NET ページの表示処理が引き継がれます。皆さんは今まで、ScriptManager コントロールを、ASP.NET AJAX アプリケーション内のさまざまな設定を調整するものにすぎないと考えていたはずです。しかしこの記事の説明で、ScriptManager コントロールをさまざまな用途で利用できることがわかったはずです。なぜなら、ScriptManager コントロールは間違いなく新しいページ クラスになり、また、出力ページにコントロールを表示するための独自のフォーマットを十分に備えているからです。

ScriptManager コントロールによって、2 つのオブジェクトに対する既定の表示が上書きされます。1 つはページ自体に対するオブジェクト、もう 1 つはページ上の Web フォームに対するオブジェクトです。したがって、ASP.NET ページ フレームワークによってページの表示が要求された場合、ScriptManager コントロール内に実装されているコードが呼び出されます。ScriptManager コントロールは、ポストバックを発生させたコントロールとそれらの相互関連 (他のコントロールと共に UpdatePanel コントロール内に配置されているか、それとも、UpdatePanel コントロールにリンクされているポストバック イベントを発生させたか) を認識することにより、表示を要求される子コントロールと無視されるコントロールを判断します。ASP.NET の Page クラスに対する既定の動作が、それぞれの子コントロールの表示を要求することである場合、ScriptManager コントロールは、必要と思われる子コントロールだけを要求します。

注目すべき点がもう 1 つあります。Form オブジェクトの表示に関する優先処理においては、RegisterDataItem メソッドを使用して ScriptManager コントロールに登録したすべての追加データが、ScriptManager コントロールによって処理され、クライアントに送信されます。したがって、Form オブジェクトがデータの表示を要求されるときまでに、そのデータをクライアントに送信できる状態にしておくことが重要です。クライアントに送信されるデータは、未加工の形式または JavaScript Object Notation (JSON) シリアル化形式でエンコードされます。

最後に、クライアント フレームワーク側で、サーバーからの非同期応答が取得され、データが解析されます。ScriptManager コントロールによって、すべてのコントロール ID と新しいマークアップが応答の中に追加されています。したがって、クライアント フレームワーク側では、ブラウザのドキュメント オブジェクト モデル上でスクリプトを実行するだけで、ページ内容を更新することができます。このプロセス全体が非同期で実行された場合、ブラウザの表示内容は即座に自動更新されるので、Web ページの使い勝手が向上します。

スクリプトに関する補足情報

ASP.NET AJAX は強力なテクノロジです。ASP.NET AJAX の豊富な機能を Web アプリケーションで利用するには、この記事で説明したとおりに ScriptManager コントロールを使用する必要があります。

ScriptManager コントロールでは、ASP.NET AJAX によって実装されているさまざまなコードを処理します。ここまでの説明で、UpdatePanel などのコントロールの既定の動作が自分のニーズに合わない場合に ScriptManager が役に立つということがわかっていただけたと思います。ScriptManager コントロールでは、スクリプト機能と AJAX に加え、認証や個人用設定などの高度な機能もサポートされています。

スクリプトとサービスを、そのページの PreRenderComplete イベントを使用して、ScriptManager コントロールに登録する必要があります。スクリプト参照は、全体ポストバックまたは部分ポストバックの実行時に、ページに追加できます。他のオブジェクトは、スクリプトの登録時に、ScriptManager コントロールにすべてのスクリプト参照とサービス参照を渡すよう要求されます。

最後に、ASP.NET AJAX がクライアント上に実装される際、クライアントのホスト ページに対する既定の表示用実装コードが無効になります。それ以降、ページ表示サイクルの制御が ScriptManager コントロール側に移ります。ScriptManager コントロールによって、応答がクライアントに送信されます。クライアント側では、この応答内容をすぐに解析して使用し、ブラウザでデータを更新することができます。既定のページ表示を無効にすることにより、適切なコントロールを適切な形式で表示できます。ScriptManager コントロールに登録されているデータも表示できます。

Ben Rush は、ASP.NET 技術を専門としている、米国中西部在住の Microsoft .NET コンサルタントです。Ben の詳細については、ben-rush.net/blog (英語) を参照してください。

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


検索: ASP.NET の統合検索機能でサイトを検索する

検索

ASP.NET の統合検索機能でサイトを検索する

Marco Bellinaso
コードのダウンロード : : Search2007_09.exe (164KB)
この記事の内容 : :
  • 検索ソリューションを選択する
  • ASP.NET の検索フレームワークを構築する
  • Live Search プロバイダを実装する
  • SharePoint プロバイダを実装する
この記事は次のテクノロジを使用しています:
ASP.NET、Live Search、SharePoint
コンテンツ
検索インフラストラクチャを構築する
Live Search プロバイダを構築する
SharePoint 2007 のプロバイダを構築する
カスタム検索ページを作成する
将来に向けて: SearchDataSource コントロール

コンテンツをどれだけ多く提供しても、また、コンテンツがどんなに優れていても、顧客は探しているものが見つからなければ、他のサイトに移動してしまいます。したがって、何百もの検索テクノロジ ベンダが存在するのも不思議ではありません。では、どのソリューションを選択するべきでしょうか。

Windows Live™ Search はインターネット上のコンテンツをクロールし、検索範囲を自分のサイトだけに限定できる Web サービスを通じて開発者に検索機能を公開します。検索結果はニーズに合わせて表示できます。

Microsoft® Office SharePoint® Server 2007 も適切な選択肢です。これはサイト構築および管理の完全なソリューションで、堅牢な検索機能も提供します。図 1 に、カスタム ASP.NET ページに統合された SharePoint 検索の結果を示します。

図 1 SharePoint 検索
図 1 SharePoint 検索(画像を縮小するには、ここをクリックします)
図 1 SharePoint 検索
図 1 SharePoint 検索 (画像を拡大するには、ここをクリックします)

企業内検索にのみ関心がある場合は、Microsoft Office SharePoint Server (MOSS) Standard または Enterprise バージョン以外にも、Search エディションを選択できます。これらのバージョンの主な違いは、インデックスを作成できるドキュメントの最大数と外部データベースなどの高度なコンテンツのインデックスを作成する機能です。ただし、Search エディションの Standard バージョンでも、ファイル共有、Exchange パブリック フォルダ、Windows SharePoint Services (WSS) サイト、および外部 Web サイトのインデックスを作成できます。

検索インフラストラクチャを構築する

非常に多くの検索プロバイダが存在するため、サイトでは、基になる検索エンジンの仕様に依存しない抽象化された実装を使用することが重要です。これにより、検索ソリューションとして Live Search、SharePoint、またはその他のプロバイダのどれを使用するかを後で決めることができます。この記事では、Web サイトにすぐに追加できる、プラグ可能なインフラストラクチャとツールセットを構築します (コードは MSDN® Magazine Web サイトからダウンロードできます)。その一環として、Live Search および SharePoint の両方の検索プロバイダを実装して、独自のアプリケーションでどちらも利用できるようにします。

実装には次の機能が含まれます。

  • 複数の単語と AND/OR 演算子の組み合わせ、完全に一致する語句、単語の除外、ワイルドカード文字 (クエリ エンジンでサポートされている場合) などの高度なクエリのサポート。
  • 結果の複数ページ表示。1 ページに表示する項目数を設定できます。
  • 複数の言語のサポート。言語によるフィルタ、ランク付けなどの機能も含まれます。
  • 見つかった項目の要約または説明テキストにおける、検索単語の強調表示。

クエリの実行に使用する検索エンジンを web.config ファイルで構成する機能も含まれます。そのために、簡略化されたプロバイダ モデルの設計パターンを実装するオブジェクト モデルを構築します。つまり、実際には特定の検索エンジンを使用して検索を実行する複数のプロバイダ クラス (データアクセス層のクラス) と、構成設定で指定されたプロバイダを動的にインスタンス化して使用する SearchEngine クラス (ビジネスロジック層のクラス) を構築します。

図 2 に、高度な SearchEngine クラスのコードを示します。このクラスは、ResultsPerPage、QueryText、Highlight などのプロパティを公開し、ExecuteQuery というパラメータなしのメソッドを含んでいます。このメソッドは、クラスの静的コンストラクタで初期化された具象プロバイダ クラスの ExecuteQuery メソッドにデリゲートされます (そのため、アプリケーションの有効期間に一度しか作成されません)。

Figure 2 SearchEngine クラス (Close Figure 2)
public class SearchEngine
{
    private static ISearchProvider _provider = null;

    static SearchEngine()
    {
        if (!string.IsNullOrEmpty(
            WebConfigurationManager.AppSettings["Search.Provider"]))
        {
            _provider = (ISearchProvider)
                Activator.CreateInstance(Type.GetType(
                    WebConfigurationManager.AppSettings[
                        "Search.Provider"]));
        }
        else _provider = new LiveSearchProvider();
    }

    public SearchEngine()
    {
        if (!string.IsNullOrEmpty(WebConfigurationManager.AppSettings[
                "Search.ResultsPerPage"]))
            this.ResultsPerPage = Convert.ToInt32(
                WebConfigurationManager.AppSettings[
                    "Search.ResultsPerPage"]);

        if (!string.IsNullOrEmpty(
            WebConfigurationManager.AppSettings[
                "Search.Culture"]))
            this.Culture = WebConfigurationManager.AppSettings[
                "Search.Culture"];

        if (!string.IsNullOrEmpty(
            WebConfigurationManager.AppSettings[
                "Search.HighlightEnabled"]))
            this.Highlight = Convert.ToBoolean(
                WebConfigurationManager.AppSettings[
                    "Search.HighlightEnabled"]);
    }

    public string ProviderName
    {
        get { return _provider.GetType().Name; }
    }

    private int _resultsPerPage = 10;
    public int ResultsPerPage
    {
        get { return _resultsPerPage; }
        set { _resultsPerPage = value; }
    }

    private int _pageIndex = 0;
    public int PageIndex
    {
        get { return _pageIndex; } set { _pageIndex = value; }
    }

    private string _culture = "en-US";
    public string Culture
    {
        get { return _culture; } set { _culture = value; }
    }

    private string _queryText = "";
    public string QueryText
    {
        get { return _queryText; } set { _queryText = value; }
    }

    private bool _highlight = true;
    public bool Highlight
    {
        get { return _highlight; } set { _highlight = value; }
    }

    public SearchResults ExecuteQuery()
    {
        return _provider.ExecuteQuery(this.QueryText,
            this.Culture, this.ResultsPerPage,
            this.PageIndex, this.Highlight);
    }
}

次に示すように、プロバイダ名とその他の設定は、web.config ファイルの <appSettings> セクションにあります。

<add key="Search.Provider" value="LiveSearchProvider" />
<add key="Search.ResultsPerPage" value="5"/>
<add key="Search.Culture" value="en-US"/>
<add key="Search.HighlightEnabled" value="True"/>

Activator.CreateInstance の呼び出しによって返されたプロバイダ オブジェクトは、すべてのプロバイダ クラスによって実装されるインターフェイスである ISearchProvider 型の静的なプライベート参照に保存されます。このインターフェイスには、プロバイダでクエリを実行するために必要なすべての情報を入力として受け取る ExecuteQuery メソッドが 1 つあります。このインターフェイスは次のように定義されています。

public interface ISearchProvider
{
    SearchResults ExecuteQuery(string queryText, string culture,
        int resultsPerPage, int resultsPageIndex, bool hightlight);
}

ExecuteQuery メソッドは、SearchResults クラスのインスタンスを返します。このクラスには、結果の合計数 (TotalResults)、結果のリスト (WebResults)、およびクエリ テキストのスペルの修正候補を返すための静的フィールドが含まれています。このクラスのコードを次に示します。

public class SearchResults
{
    public int TotalResults = 0;
    public List<string> Suggestions = new List<string>();
    public List<ResultItem> WebResults = new List<ResultItem>();
}

Suggestions は文字列の集合で、WebResults は ResultItem オブジェクトの集合です。ResultItem クラスは、結果の URL、タイトル、および説明を返すプロパティを公開します。図 3 に実際のコードを示します。

Figure 3 ResultItem クラス (Close Figure 3)
public class ResultItem
{
    public ResultItem(string url, string title, string description)
    {
        this.Url = url;
        this.Title = title;
        this.Description = description;
    }

    private string _url;
    public string Url
    {
        get { return _url; } set { _url = value; }
    }

    private string _title;
    public string Title
    {
        get { return _title; } set { _title = value; }
    }

    private string _description;
    public string Description
    {
        get { return _description; } set { _description = value; }
    }
}

これで基本アーキテクチャの準備が完了したので、プロバイダ クラスの実装を開始できます。

Live Search プロバイダを構築する

Live Search Web サービスを使用すると、live.com で実行できるものと同じ種類の検索クエリをプログラムで実行できます。この Web サービスは soap.search.msn.com/webservices.asmx (英語) にあります (soap.search.msn.com/webservices.asmx?wsdl でサービスの WSDL を入手できます)。Visual Studio® 2005 の [Web 参照の追加] オプションを使用して、コードで検索の実行に使用するプロキシ クラスを作成できます。このサービスをインスタンス化する方法を次に示します。

MSNSearchService searchEngine = new MSNSearchService();

検索エンジン クラスのインスタンスを作成したら、次のように検索要求を作成します。

SearchRequest searchRequest = new SearchRequest();

無料サービスでは、一日に実行できるクエリが 25,000 個に制限されることに注意してください。Live では、一意の "アプリケーション ID" でユーザーを追跡することによってクエリをカウントします。この ID は、ユーザーが search.msn.com/developer (英語) で取得して、SearchRequest の AppID プロパティの値として使用する必要があります。

searchRequest.AppID = "185A8FADA7931D06D119C2FC1FF104";

SearchRequest オブジェクトの Query プロパティも設定する必要があります。このプロパティには、live.com の場合と同様に検索テキストを指定します。複数の単語を指定できます。その場合は、AND 演算子が暗黙的に適用されます。OR 演算子を明示的に使用すると、指定した単語のいずれかを含むテキストを検索できます。完全に一致する語句を検索するには、引用符でその語句を囲みます。かっこを使用して単語をグループ化したり、マイナス記号を使用して単語を除外することもできます。次の例では、"ajax" と"code example" の両方、および "Marco" または "Francesco" のどちらか一方を含み、"xml" または "Visual Basic" を含まないドキュメントを検索します。

searchRequest.Query =
    "ajax AND \"code example\" AND (Marco OR Francesco)" +
    " –(xml Visual Basic)";

このクエリでは、live.com からクエリを実行する場合と同様に、Web 全体を検索します。結果を自分のサイトのページに限定するには、次のようにクエリの最後に site:www.yoursite.com を追加します。

searchRequest.Query =
    "ajax site:msdn.microsoft.com/msdnmag";

すべての検索キーワードの一覧については、help.live.com (英語) を参照してください。検索文字列および複数のフィルタやキーワードの組み合わせを試す最善の方法は、クエリ ビルダ ツールを使用することです。このツールは、dev.live.com/livesearch/sdk (英語) にあります (図 4 を参照)。このツールを使用すると、クエリで Web サービスを呼び出すために必要な C# コードも生成されるので、ツールボックスには不可欠です。

図 4 Live Search SDK のクエリ ビルダ
図 4 Live Search SDK のクエリ ビルダ(画像を縮小するには、ここをクリックします)
図 4 Live Search SDK のクエリ ビルダ
図 4 Live Search SDK のクエリ ビルダ (画像を拡大するには、ここをクリックします)

その他の SearchRequest プロパティでは、SafeSearch レベル (わいせつな画像やテキストを結果から除外するには Moderate を使用)、フラグ (結果内でクエリの単語を強調表示するかどうかなど)、および現在のカルチャの CultureInfo などを設定できます。

searchRequest.SafeSearch = SafeSearchOptions.Moderate;
searchRequest.Flags = SearchFlags.MarkQueryWords;
searchRequest.CultureInfo = "en-US";

次に、Live Search から返される結果の種類を指定する必要があります。Web ページ、スペルの修正候補 (たとえば、"membershipa" と入力すると、"membership" が修正候補として表示される)、住所と電話番号、ニュース、インラインの回答や画像などの種類があります。自分のサイトのページを返す Web 検索の場合、通常必要になるのは Web 結果とスペルの修正候補のみです。その場合は、SearchRequest のプロパティの値として、2 つの SourceRequest オブジェクトの配列を作成します。

SourceRequest[] sourceRequests = new SourceRequest[2];
searchRequest.Requests = sourceRequests;

次に、SourceRequest オブジェクトを作成し、その型 (Web と Spelling)、結果に必要なフィールド (スペルの修正候補についてはタイトルのみ、Web 結果についてはタイトル、URL、説明などのすべての一般的なフィールド) を指定して、実際に 2 つのソース要求を定義します。必要な結果の件数 (多くの場合、スペルの修正候補では 3 件で十分、Web 結果では通常 10 ～ 20 件が適当) と先頭の結果のオフセット (すべての結果の中から表示を開始する位置を示すインデックスで、複数ページ表示を行ううえで重要) を指定することもできます。

sourceRequests[0] = new SourceRequest();
sourceRequests[0].Source = SourceType.Web;
sourceRequests[0].ResultFields =
    ResultFieldMask.All | ResultFieldMask.SearchTagsArray;
sourceRequests[0].Count = 20;
sourceRequests[0].Offset = 0;

sourceRequests[1] = new SourceRequest();
sourceRequests[1].Source = SourceType.Spelling;
sourceRequests[1].ResultFields = ResultFieldMask.Title;
sourceRequests[1].Count = 3;
sourceRequests[1].Offset = 0;

定義済みの SearchRequest オブジェクトを入力に指定して MSNSearchService の Search メソッドを呼び出すことによって、クエリを実行します。

SearchResponse searchResponse = searchEngine.Search(searchRequest);

Search の呼び出しによって、SearchResponse 型のオブジェクトが返されます。その Responses プロパティは、SourceResponse 型のオブジェクトの配列で、上記で定義した各 SourceRequest に 1 つずつあります。サンプルでは、2 つのオブジェクトの配列があり、1 つは Web 結果用、もう 1 つはスペルの修正候補用です。SourceResponse オブジェクトには Total というプロパティがあります。このプロパティは、実行されたクエリで見つかった結果の合計数を示します。Total は返される結果の件数 (通常はページング可能なレポートの現在のインデックスに基づくサブセット) ではなく、結果の合計数であることに注意してください。実際の結果は、SourceResponse の Results プロパティを使用して取得できます。このプロパティは、Result オブジェクトの集合です。Result オブジェクトには、Title、Url、Description などのプロパティがあります。これらは、対応する SourceRequest の定義で何を返すように指定したかによって、情報が格納されている場合もあれば、空の場合もあります。

// Process the Web results
int total =
  searchResponse.Responses[0].Total;
foreach (Result result in
  searchResponse.Responses[0].Results)
{
    // access the result.Title, result.
    //Url, and other values...
}

// Process the spelling results
foreach (Result result in
  searchResponse.Responses[1].Results)
{
    // print the result.Title value...
}

必要な作業は以上です。要約すると、2 つの SourceRequest オブジェクトで SearchRequest を定義し、Web 結果とスペルの修正候補を返すように指定して、強調表示と検索レベルの Moderate を有効にしました。次に、その SearchRequest に対して検索エンジンの Search メソッドを呼び出し、生成される SourceResponses の Results 集合を処理しました。図 5 に、LiveSearchProvider クラスのすべてのコードを示します。このクラスは、前に定義した ISearchProvider インターフェイスを実装します。コードは、この項でこれまでに紹介したコードとほぼ同じです。もちろん、必要な情報のほとんどは、クラスにハードコードされていませんので、入力パラメータとして渡されるか、または構成ファイルから読み取られます。

Figure 5 Live Search の ISearchProvider (Close Figure 5)
public class LiveSearchProvider : ISearchProvider
{
    public SearchResults ExecuteQuery(string queryText, string culture,
        int resultsPerPage, int resultsPageIndex, bool highlight)
    {
        SearchResults results = new SearchResults();

        // create the search engine and search request
        MSNSearchService searchEngine = new MSNSearchService();
        SearchRequest searchRequest = new SearchRequest();

        // define the search requests: one for web results...
        SourceRequest[] sourceRequests = new SourceRequest[2];
        sourceRequests[0] = new SourceRequest();
        sourceRequests[0].Source = SourceType.Web;
        sourceRequests[0].ResultFields =
            ResultFieldMask.All | ResultFieldMask.SearchTagsArray;
        sourceRequests[0].Count = resultsPerPage;
        sourceRequests[0].Offset = resultsPageIndex * resultsPerPage;

        // ...and one for spelling suggestions
        sourceRequests[1] = new SourceRequest();
        sourceRequests[1].Source = SourceType.Spelling;
        sourceRequests[1].ResultFields = ResultFieldMask.Title;
        sourceRequests[1].Count = 3;
        sourceRequests[1].Offset = 0;

        // set the text to be searched, the App ID, and more
        searchRequest.Requests = sourceRequests;
        string siteFilter = " site:" +

            WebConfigurationManager.AppSettings[
                "LiveSearch.TargetSite"];
            searchRequest.Query = queryText;
        if (!string.IsNullOrEmpty(
            WebConfigurationManager.AppSettings[
                "LiveSearch.TargetSite"]))
            searchRequest.Query += siteFilter;
        searchRequest.SafeSearch = SafeSearchOptions.Moderate;
        searchRequest.AppID =
            WebConfigurationManager.AppSettings["LiveSearch.AppID"];
        searchRequest.Flags = highlight ?
            SearchFlags.MarkQueryWords : SearchFlags.None;
        searchRequest.CultureInfo = culture;

        // run the query and get the response
        SearchResponse searchResponse =
            searchEngine.Search(searchRequest);

        // get the spelling suggestions
        foreach (Result result in searchResponse.Responses[1].Results)
        {
            results.Suggestions.Add(result.Title.Trim()
                .Replace(siteFilter, "")
                .Replace("\xe000", "").Replace("\xe001", ""));
        }

        // get the web results from the Web SourceResponse
        results.TotalResults = searchResponse.Responses[0].Total;
        foreach (Result result in searchResponse.Responses[0].Results)
        {
            results.WebResults.Add(new ResultItem(result.Url,
                result.Title.Replace("\xe000", "")
                    .Replace("\xe001", ""),
                result.Description.Replace("\xe000", "<b>")
                    .Replace("\xe001", "</b>")));
        }

        return results;
    }
}

SharePoint 2007 のプロバイダを構築する

SharePoint Portal 2007 の検索機能は、非常に強力です。ここでは、特定の目標において最も重要になる機能についてのみ説明しますが、システム管理者や開発者向けのガイドが多数発行されていますので、それらのガイドを読めば知識をさらに深めることができます。

では、SharePoint の Web ベースの管理アプリケーションから始めましょう。[共有サービス] の [検索の設定] で、新しい検索コンテンツ ソースを作成します。これで、インデックスを作成する対象、その方法、およびタイミングを定義します。SharePoint Portal では、一般的なインターネット Web サイト、SharePoint Web サイト (コンテンツをデータベースから直接取得するため、特定の方法で処理される)、ファイル共有内のドキュメント、カスタム データベースなどのインデックスを作成できます。この例では、最初のオプションを選択し、インデックスを作成するサイトのルート URL を指定します。SharePoint はその URL からインデックスの作成を開始し、リンクを再帰的にたどって処理を続行します (リンクをたどる深さを指定したり、同じドメイン上で見つかったすべてのページのインデックスを作成するように指定したりできます)。コンテンツ ソースを作成するページについては、図 6 を参照してください。

図 6 検索コンテンツ ソースの追加
図 6 検索コンテンツ ソースの追加(画像を縮小するには、ここをクリックします)
図 6 検索コンテンツ ソースの追加
図 6 検索コンテンツ ソースの追加 (画像を拡大するには、ここをクリックします)

コンテンツ ソースの作成中に、ページの一番下で新しいソースの確認後すぐにインデックス作成プロセスを開始するように指定したり、増分インデックス作成のスケジュールを設定したりできます。たとえば、サーバーの負荷が軽い午前 0 時に毎日処理を実行するように設定できます。図 7 にこれらの構成を示します。

図 7 インデックス作成スケジュールの構成
図 7 インデックス作成スケジュールの構成(画像を縮小するには、ここをクリックします)
図 7 インデックス作成スケジュールの構成
図 7 インデックス作成スケジュールの構成 (画像を拡大するには、ここをクリックします)

最後の構成手順では、検索を実行するときに目的のソースに限定して検索を行えるように、検索範囲を作成します。これが特に重要になるのは、それぞれにインデックスが作成された複数の Web サイトがあり、1 つのサイトのコンテンツのみ検索し、他のサイトのコンテンツは検索しない場合です。この操作を完了するには、図 8 のウィザードのフィールドに設定を入力するだけです。特定の URL またはコンテンツ ソース以外のすべてを含む範囲を作成することもできます。

図 8 検索範囲ウィザード
図 8 検索範囲ウィザード(画像を縮小するには、ここをクリックします)
図 8 検索範囲ウィザード
図 8 検索範囲ウィザード (画像を拡大するには、ここをクリックします)

新しいコンテンツ ソースを定義するときに、[検索の設定] ページでコマンドをクリックしてコンテンツ ソースを更新すると、そのソースに含まれているドキュメントの数が再計算されます。この便利な情報は、すべてのコンテンツ ソースの一覧が表示されるページから参照できます。

では、インデックスを検索するコードを記述してみましょう。SharePoint には 2 つの方法が用意されています。それは、ローカル オブジェクト モデル (SharePoint DLL アセンブリ) を使用する方法と、SharePoint Web サービスを使用する方法です。最初のアプローチは、サイトが SharePoint と同じサーバーで実行されている場合のみ使用できますが、そのようなケースはほとんどありません。したがって、サイトが同じサーバー上で実行されているかどうかにかかわらず呼び出せる Web サービスを使用する方法をお勧めします。Visual Studio で YourSharePointServerName/_vti_bin/search.asmx に Web 参照を追加して、QueryService プロキシ クラスを作成します。コーディング作業の最も重要な部分は、SQL に似た言語と XML を組み合わせて正しいクエリを作成することです。クエリの SQL 部分の例を次に示します。

string sqlQuery = "SELECT Path, Rank, Title, HitHighlightedSummary
    FROM MSDNMagazine..scope() WHERE FREETEXT('ajax asynchronous')
    ORDER BY Rank DESC";

インデックスは SQL Server のデータベースではなくファイルに格納されていますが、上記のように、SQL Server の全文検索を使用する通常の SQL クエリによく似ています。FROM 句では、テーブル名ではなく検索範囲名を指定し、その後に "..scope()" を追加しています。WHERE フィルタでは、高度な複数の条件を指定できるように FREETEXT 述語を使用しています。このクエリは、SELECT の指定に従って、見つかった結果のパス、タイトル、ランク、および説明を返します。SQL はクエリ エンジンにそのままの状態では渡されません。取得する結果の件数、開始オフセット (Live Search と違って最小値は 0 ではなく 1)、カルチャなどの他の情報を定義する XML 要求の一部として埋め込まれます。図 9 に、完全な要求を示します (SQL クエリが周囲の XML にどのように挿入されているかに注意してください)。

Figure 9 SharePoint XML 検索要求 (Close Figure 9)
string xmlQuery = string.Format(@"
<QueryPacket urn:Microsoft.Search.Query"">
  <Query>
    <SupportedFormats>
      <Format>urn:Microsoft.Search.Response.Document:Document</Format>
    </SupportedFormats>
    <Context>
      <QueryText type=""MSSQLFT"" language=""en-US"">{0}</QueryText>
    </Context>
    <Range>
      <StartAt>1</StartAt>
      <Count>20</Count>
    </Range>
    <EnableStemming>true</EnableStemming>
    <TrimDuplicates>true</TrimDuplicates>
    <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
    <ImplicitAndBehavior>true</ImplicitAndBehavior>
    <IncludeRelevanceResults>true</IncludeRelevanceResults>
    <IncludeSpecialTermResults>true</IncludeSpecialTermResults>
    <IncludeHighConfidenceResults>true</IncludeHighConfidenceResults>
  </Query>
</QueryPacket>", sqlQuery);

実際にクエリを実行して結果を取得するには、QueryService オブジェクトをインスタンス化してその資格情報を適切な資格情報に設定し (SharePoint はこれらの資格情報に関連付けられている権限に従って結果をフィルタしますが、ここではパブリック Web サイトの検索範囲を対象にしているため、結果はフィルタされません)、入力に XML 要求を指定して Query メソッドを呼び出すだけです。

QueryService queryService = new QueryService();
queryService.UseDefaultCredentials = true;
string xml = queryService.Query(xmlQuery);

図 10 に XML 応答の一部を示します。

Figure 10 SharePoint 検索 Web サービスによって返される XML 応答のサンプル (Close Figure 10)
<ResponsePacket xmlns="urn:Microsoft.Search.Response">
  <Response>
    <Range>
      <StartAt>1</StartAt>
      <Count>10</Count>
      <TotalAvailable>11</TotalAvailable>
      <Results>
        <Document xmlns="urn:Microsoft.Search.Response.Document">
          <Action>
            <LinkUrl>
              http://msdn.microsoft.com/msdnmag/issues/07/02/cuttingedge
            </LinkUrl>
          </Action>
          <Properties xmlns=
              "urn:Microsoft.Search.Response.Document.Document">
            <Property>
              <Name>PATH</Name>
              <Type>String</Type>
              <Value>
                http://msdn.microsoft.com/msdnmag/issues/07/02/cuttingedge
              </Value>
            </Property>
            <Property>
              <Name>RANK</Name>
              <Type>Int64</Type>
              <Value>419</Value>
            </Property>
            <Property>
              <Name>TITLE</Name>
              <Type>String</Type>
              <Value>Cutting Edge: Perspectives on ASP.NET AJAX –
                     MSDN Magazine, February 2007</Value>
            </Property>
            <Property>
              <Name>HITHIGHLIGHTEDSUMMARY</Name>
              <Type>String</Type>
              <Value>Ways to &lt;c0&gt;AJAX&lt;/c0&gt; in ASP.NET
                    &lt;ddd/&gt; First, there's the acronym-it stands for
                    &lt;c1&gt;Asynchronous&lt;/c1&gt; JavaScript and XML.
                    &lt;ddd/&gt;  with ASP.NET &lt;c0&gt;AJAX&lt;/c0&gt;,
                    but it can be used as the core code to add basic
                    &lt;c0&gt;AJAX&lt;/c0&gt; capabilities to ASP.NET 1.
                    &lt;ddd/&gt; </Value>
            </Property>
          </Properties>
        </Document>
        <Document>
            ...
        </Document>
        <!-- more Document nodes -->
      </Results>
    </Range>
    <Status>SUCCESS</Status>
  </Response>
</ResponsePacket>

コードの残りの部分では、XmlDocument を使用して XML テキストを解析します。図 11 に、SharePointSearchProvider の完全な実装を示します。

Figure 11 SharePoint の ISearchProvider (Close Figure 11)
public class SharePointSearchProvider : ISearchProvider
{
    public SearchResults ExecuteQuery(string queryText, string culture,
        int resultsPerPage, int resultsPageIndex, bool highlight)
    {
        SearchResults results = new SearchResults();

        // prepare the SharePoint XML query string
        string sqlQuery = string.Format(@"
                SELECT Path, Rank, Title, HitHighlightedSummary
                FROM {0}..scope() WHERE FREETEXT('{1}')
                ORDER BY Rank DESC",
            WebConfigurationManager.AppSettings[
                "SharePointSearch.TargetScope"],
            queryText.Replace("'","''").Replace("\"","\"\""));

            string xmlQuery = string.Format(@"
<QueryPacket urn:Microsoft.Search.Query"">
  <Query>
    <SupportedFormats>
      <Format>urn:Microsoft.Search.Response.Document:Document</Format>
    </SupportedFormats>
    <Context>
      <QueryText type=""MSSQLFT"" language=""{1}"">{0}</QueryText>
    </Context>
    <Range>
      <StartAt>{2}</StartAt>
      <Count>{3}</Count>
    </Range>
    <EnableStemming>true</EnableStemming>
    <TrimDuplicates>true</TrimDuplicates>
    <IgnoreAllNoiseQuery>true</IgnoreAllNoiseQuery>
    <ImplicitAndBehavior>true</ImplicitAndBehavior>
    <IncludeRelevanceResults>true</IncludeRelevanceResults>
    <IncludeSpecialTermResults>true</IncludeSpecialTermResults>
    <IncludeHighConfidenceResults>true</IncludeHighConfidenceResults>
  </Query>
</QueryPacket>",
            sqlQuery, culture,
            (resultsPageIndex * resultsPerPage) + 1, resultsPerPage);

        // run the query and get the response
        QueryService queryService = new QueryService();
        queryService.UseDefaultCredentials = true;
        string xml = queryService.Query(xmlQuery);

        // if there were no errors in the query, parse the results
        if (xml.IndexOf("<Status>SUCCESS</Status>") > -1)
        {
            XmlDocument xmlResults = new XmlDocument();
            xmlResults.LoadXml(xml.Replace(
                " xmlns=\"urn:Microsoft.Search.Response\"",
                "").Replace("xmlns=" +
                "\"urn:Microsoft.Search.Response.Document.Document\"",
                "").Replace(" xmlns=" +
                "\"urn:Microsoft.Search.Response.Document\"", ""));

            results.TotalResults = Convert.ToInt32(
                xmlResults.SelectSingleNode(
                    "//Response/Range/TotalAvailable").InnerText);

            foreach (XmlNode resNode in xmlResults.SelectNodes(
                "//Response/Range/Results/Document/Properties"))
            {
                string url = "";
                string title = "";
                string description = "";

                foreach (XmlNode propNode in resNode.ChildNodes)
                {
                    if (propNode.ChildNodes[0].InnerText == "PATH")
                        url = propNode.ChildNodes[2].InnerText;
                    else if (propNode.ChildNodes[0].InnerText ==
                            "TITLE")
                        title = propNode.ChildNodes[2].InnerText;
                    else if (propNode.ChildNodes[0].InnerText ==
                        "HITHIGHLIGHTEDSUMMARY")
                        description =
                            propNode.ChildNodes[2].InnerText;
                }

                // if hightlighting is enabled, replace all
                // <c?></c?> instances with <b></b>, otherwise
                // with an empty string
                if (highlight)
                {
                    description = Regex.Replace(
                        description, @"<c\d+>", "<b>");
                    description = Regex.Replace(
                        description, @"</c\d+>", "</b>");
                }

                results.WebResults.Add(
                    new ResultItem(url, title, description));
            }
        }

        return results;
    }
}

QueryService が Query 以外に QueryEx メソッドも公開している点に注目してください。QueryEx メソッドは、結果を未加工の XML ではなく DataTable で返します。これにより、解析が不要になるため、開発者は処理が容易になります。ただし、結果の合計数は返されません。この合計数は、検索ページにページ分割機能を実装するために必要になります (すべての結果を常に取得する必要があるためにメモリ内でページ分割を行う場合は例外です。ただし、この方法はお勧めしません)。したがって、ここでは代わりに基本的な Query メソッドを使用します。

カスタム検索ページを作成する

これで、高度なインターフェイスとその基になるプロバイダ コードが完成しました。あとはカスタム Search.aspx を作成するだけです。完全な実装はダウンロード可能なソース コードにあるので、ここでは最も重要な部分についてのみ説明します。検索ボックスは、テキスト ボックスと送信ボタンで構成されます。ページの結果レポート セクションでは、Repeater コントロールによって、スペルの修正候補がそのページ自身を指すハイパーリンクとして表示され、修正候補のテキストはクエリ文字列の "q" パラメータで渡されます。

<asp:Repeater runat="server" ID="rptSuggestions">
    <ItemTemplate>
        <asp:HyperLink runat="server" ID="lnkSuggestionUrl"
            NavigateUrl='<%# "~/Search.aspx?q=" +
                DataBinder.Eval(Container, "DataItem") %>'
            Text='<%# DataBinder.Eval(Container, "DataItem") %>' />
    </ItemTemplate>
    <SeparatorTemplate>, </SeparatorTemplate>
</asp:Repeater>

なぜハイパーリンクの代わりに LinkButton コントロールが使用されていないのか不思議に思われるかもしれません。これは、LinkButton コントロールを使用すると、更新されたクエリ文字列を使用してページが再要求されずにポストバックが発生するため、ユーザーがページのショートカットを保存して後で同じ結果を取得できなくなるからです。

これと同じ原理は、同じページを指すハイパーリンクがあり、クエリ テキストで同じ "q" パラメータを使用し、表示する結果ページのインデックスを "p" パラメータで指定するページ分割バーにも当てはまります。実際の Web 結果は DataList コントロールを使用して表示されます。このコントロールは、次のように、各結果のタイトルと見つかったページのハイパーリンクを説明テキストと共に表示します。

<asp:DataList ID="dlstResults" runat="server" ...styles here...>
    <ItemTemplate>
        <asp:HyperLink runat="server" ID="lnkResultUrl"
            NavigateUrl='<%# Eval("Url") %>'
            Text='<%# Eval("Title") %>' /><br /><%# Eval("Description") %>
    </ItemTemplate>
</asp:DataList>

分離コードも単純です。UI の開発者には、内部で使用されている検索エンジンに関する知識は何も必要ありません。実際、どの検索エンジンが使用されているのかさえ知る必要がありません。このロジックがすべて SearchEngine クラスにラップされているからです。次のコードに、このクラスの使用方法と、結果を UI コントロールにバインドする方法を示します。

SearchEngine search = new SearchEngine();
search.PageIndex = this.PageIndex;
search.QueryText = txtQueryText.Text;
SearchResults results = search.ExecuteQuery();

rptSuggestions.DataSource = results.Suggestions;
lblTotal.Text = results.TotalResults.ToString();
dlstResults.DataSource = results.WebResults;
Page.DataBind();

結果は図 1 と同じです。

将来に向けて: SearchDataSource コントロール

2007 年 5 月にリリースされた ASP.NET Futures (asp.net/downloads/futures) (英語) には、SearchDataSource という新しいデータ ソース コントロールが追加されています。このコントロールは、ObjectDataSource や SqlDataSource などの他のすべてのデータ ソース コントロールと同様に、Repeater、DataList、GridView などのコントロールにバインドして、表示するデータを自動的に提供します。もちろん、提供されるデータは、web.config ファイルで構成されている登録済みのプロバイダ クラスによって実行された Web 検索の結果です。

<search enabled="true">
   <providers>
      <add name="WindowsLiveSearchProvider"
         type="Microsoft.Web.Preview.Search.WindowsLiveSearchProvider,
              Microsoft.Web.Preview"
         appID="185A8FADA7931D06D119C2FC1FF104"
         siteDomainName="msdn.microsoft.com/msdnmag" />
   </providers>
</search>

データ ソース コントロールは、Visual Studio デザイナを使用して視覚的にページに追加するか、次のマークアップを使用して追加できます。

<asp:TextBox ID="txtQuery" runat="server" />
<asp:Button ID="btnSearch" runat="server"  Text="Search" />

<asp:SearchDataSource ID="SearchDataSource1" runat="server" >
   <SelectParameters>
       <asp:ControlParameter ControlID="txtQuery" Name="query"
                             PropertyName="Text" />
   </SelectParameters>
</asp:SearchDataSource>

TtxtQuery テキスト ボックスの Text プロパティにバインドされる query というコントロール パラメータがあることがわかります。ポストバック中に、データ ソース コントロールは自動的に更新され (プログラムでコントロールの DataBind メソッドを呼び出して強制的に更新することも可能)、クエリはそのテキスト ボックスに含まれている値を使用して実行されます。そしてその結果は、関連付けられているデータ コントロールにバインドされます (DataSourceID="SearchDataSource1")。DataList または Repeater コントロールを使用して結果を表示した場合は、そのテンプレート定義は、カスタム ソリューションについて前に示したコードと同じになります。これは、コントロールにバインドされた結果が SearchResult 型で、WebResult クラスと同様に Title、Url、および Description プロパティを含んでいるからです。したがって、バインド式は同じです。

独自のプロバイダを作成するには、SearchProviderBase を継承するクラスを定義します。その Search メソッドをオーバーライドして特定の検索エンジンの実装を提供し、Initialize メソッドをオーバーライドして構成設定を読み取ります。

public class SharePointSearchProvider : SearchProviderBase
{
    public SharePointSearchProvider() : base() { }

    public override SearchResult[] Search(SearchQuery searchQuery)
    { ... }

    public override void Initialize(string name,
        NameValueCollection config) { ... }
}

SearchDataSource コントロールのアプローチは、プロバイダ モデルの設計パターンを完全に実装するため (この記事で開発したソリューションのような簡略化されたバージョンではありません)、非常に洗練されています。また、このコントロールを視覚的にフォームにドロップしてデータ コントロールにバインドできる機能は、コードを記述する必要がないため、魅力的です。ただし、この記事の執筆時点では、このコントロールには多くの制限があります。マイクロソフトは近いうちに機能とメンバを追加して、基本プロバイダ クラスと入力の SearchQuery 型を拡張すると思います。そうなれば、ここで示したソリューションを SearchDataSource モデルに簡単に移植できます。必要な作業は、検索の実装を、ISearchProvider インターフェイスを実装するクラスから SearchProviderBase を継承するクラスに移行し、入力パラメータを ExecuteQuery の入力値からではなくプロバイダのプロパティまたは SearchQuery のプロパティから読み取ることです。マークアップ コードは実質的に変更しません。SearchResult 型と WebResult 型が同じプロパティを公開しているからです。

Marco Bellinaso は、シェアウェアやデジタル商品を販売するための無料オンライン サービスである www.bytecommerce.com (英語) の創設者です。また、イタリアのマイクロソフト カンファレンスでの講演も頻繁に行っており、『ASP.NET 2.0 Web サイト プログラミング / 問題 - 設計 - ソリューション』(Wrox 社発行) の著者でもあります。Marco は Code Architects S.r.l. に勤務し、クライアントのコンサルティングと商用開発ツールの開発を担当しています。Marco の連絡先は、mbellinaso@gmail.com (英語) です。


ネットワーク: .NET Framework 3.5 での接続

ネットワーク

.NET Framework 3.5 での接続

Mariya Atanasova and Larry Cleeton and Mike Flasko and Amit Paka
この記事の内容 : :
  • Socket クラスのパフォーマンス
  • 国際化 URL
  • System.Net.PeerToPeer 名前空間
この記事は次のテクノロジを使用しています:
.NET Framework
コンテンツ
Socket クラスのパフォーマンス
International Resource Identifier のサポート
System.Net.PeerToPeer 名前空間
まとめ

Microsoft® .NET Framework では、System.Net 名前空間が HTTP や SMTP などの多数のネットワーク プロトコルの機能を公開しています。Visual Studio® 2008 (以前のコードネームは "Orcas") に付属する、リリース予定の .NET Framework 3.5 には、これらの中核となるネットワーク層に対するパフォーマンスおよび機能の強化が多数含まれています。この記事では、System.Net チームによって加えられた次の 3 つの主な変更点について説明します。

  • 高パフォーマンスなソケット API
  • URI に対する International Resource Identifier のサポート
  • ピアツーピア (P2P) 名前空間

リリース予定の .NET Framework 3.5 では、Framework 自体に変更が加えられています。この記事で説明する機能は、MSDN® からダウンロード可能な Beta 1 リリースの Visual Studio 2008 で利用できます。

Socket クラスのパフォーマンス

.NET Framework Version 2.0 の System.Net.Sockets 名前空間には、Windows® WinSock Win32® API のほとんどすべての機能を提供する Socket クラスが用意されています。これらの機能は、マネージ コードの開発者向けに設計されたメソッドとプロパティを含む 1 つのクラスにまとめられています。Socket クラスには、Send や Receive などの一連の同期メソッドと、さまざまな状況に対応するための多様なパラメータ オーバーロードが含まれています。これらの同期メソッドは使いやすく、ソケットを使用する単純なネットワーク タスクには非常に適しています。また、Socket クラスには、.NET Framework 全体で広く使用されている非同期プログラミング モデル (APM) に基づく一連の非同期メソッドも含まれています (詳細については、msdn.microsoft.com/msdnmag/issues/07/03/ConcurrentAffairs を参照してください)。これらの非同期メソッドは、Socket クラスの非同期的な使用を比較的容易にし、多数のソケットを処理したり、多数のソケットに対する多数の送受信操作を処理したりする方法を提供します。

Version 2.0 の Socket クラスは、ネットワーク ソケットを使用する必要があるさまざまなクライアント アプリケーションと、一部のサーバー型およびサービス型のアプリケーションに適しています。一部のサービス アプリケーションのシナリオには Version 2.0 の Socket クラスでは対応できませんが、Windows WinSock API を直接使用するネイティブ言語で対応できます。Version 2.0 の Socket クラスの最大の問題点は、単一のソケット I/O 操作を実行するときや、多数のソケットに対する I/O 操作を同時に維持するために必要な基になるオブジェクトを割り当てるときに、過度の CPU サイクルを使用することです。

.NET Framework 3.5 では、共通言語ランタイム (CLR) が同時に使用する多数の Overlapped オブジェクトをより効率的に管理できます。CLR の Overlapped オブジェクトは、非同期 I/O 操作の管理に使用されるネイティブ Windows OVERLAPPED 構造体を効率的にラップします。Socket に対する進行中の非同期 I/O 操作ごとに、Overlapped オブジェクトのインスタンスが 1 つ作成されます。また、各ソケットに対する保留中の非同期受信 I/O 操作を維持しながら、60,000 以上のソケットを接続できます。

Version 2.0 の Socket クラスは、Windows I/O 完了ポートを使用して非同期 I/O 操作を完了します。これにより、多数のソケットを開くことができるようにアプリケーションを簡単にスケール アップできます。.NET Framework は、完了ポートの読み取りと非同期 I/O 操作の完了を行う完了スレッドを提供する System.Threading.ThreadPool クラスを実装しています。リリース予定の .NET Framework 3.5 の開発では、完了ポートの読み取りと、IAsyncResult オブジェクトでのアプリケーションの完了デリゲートの呼び出しや I/O 完了イベント オブジェクトの通知との間のコード パスで生じるオーバーヘッドを抑えることに重点が置かれました。

.NET Framework の APM は、Begin/End パターンとも呼ばれます。これは、非同期操作を開始するために Begin メソッドが呼び出され、IAsyncResult オブジェクトを返すためです。デリゲートは、非同期操作が完了したときに呼び出される Begin メソッドのパラメータとして、オプションで指定できます。また、スレッドが IAsyncResult.AsyncWaitHandle を待機するように設定することもできます。コールバックが呼び出されるか待機が通知されると、非同期操作の結果を取得するために End メソッドが呼び出されます。このパターンは柔軟性が高く、比較的簡単に使用できるので、.NET Framework 全体で広く使用されています。

ただし、非常に多数の非同期ソケット操作を行うときには不都合な点もあります。操作ごとに IAsyncResult オブジェクトを作成する必要があり、それを再利用できないという点です。そのため、オブジェクトの割り当てとガベージ コレクションが頻繁に実行され、パフォーマンスが影響を受ける可能性があります。この問題に対処するために、新しいリリースには、ソケットの非同期 I/O を実行する別のパターンのメソッドが用意されています。この新しいパターンでは、ソケット操作ごとに操作コンテキスト オブジェクトを割り当てる必要はありません。

これはまったく新しいパターンではなく、既存のパターンに根本的な変更を加えたものです。Socket クラスには、イベントベースの完了モデルのバリエーションを使用するメソッドが追加されました。Version 2.0 の場合、次のようなコードを使用して Socket に対する非同期送信を開始します。

void OnSendCompletion(IAsyncResult ar) { }

IAsyncResult ar = socket.BeginSend(buffer, 0, buffer.Length,
    SocketFlags.None, OnSendCompletion, state);

新しいバージョンでは、次のようにすることもできます。

void OnSendCompletion(object src, SocketAsyncEventArgs sae) { }

SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
sae.Completed += OnSendCompletion;
sae.SetBuffer(buffer, 0, buffer.Length);
socket.SendAsync(sae);

このコードには、いくつかの注目すべき違いがあります。まず、操作のコンテキストをラップするオブジェクトは、IAsyncResult オブジェクトではなく SocketAsyncEventArgs オブジェクトです。アプリケーションは SocketAsyncEventArgs オブジェクトを作成および管理し、再利用することもできます。ソケット操作のすべてのパラメータは、SocketAsyncEventArgs オブジェクトのプロパティとメソッドによって指定されます。完了状態も、SocketAsyncEventArgs オブジェクトのプロパティによって指定されます。さらに、イベント ハンドラ コールバック完了メソッドを使用する必要があります。

これらのすべての変更によって、.NET Framework 3.5 の System.Net.Sockets クラスのパフォーマンスとスケーラビリティは大幅に向上しました。これらの改善点のうち 2 つは、既存のアプリケーションによって自動的に実現されます。3 つめの改善点である Socket の新しいメソッドは、アプリケーションを変更しないと使用できませんが、最も要求の高いソケットベースのアプリケーションでスケーラビリティを向上することができます。

International Resource Identifier のサポート

Web アドレスは通常、非常に限られた文字のセットで構成される Uniform Resource Identifier (URI) を使用して表されます。基本的に、これには英語のアルファベットの大文字および小文字、0 ～ 9 の数字、およびその他のいくつかの ASCII 記号 (コンマやハイフンなど) のみが含まれます。

この構文は、日本語やヘブライ語など、ラテン アルファベット以外の文字セットに依存する地域にとってはあまり便利ではありません。たとえば、www.BaldwinMuseumOfScience.com のようなアドレスについて考えてみましょう。英語を話すユーザーにとっては、このアドレスを理解するのも覚えるのも簡単です。しかし、英語を話さないユーザーにとっては、この URL は記号をランダムに組み合わせたものとたいして変わりません。また、英語だけを話すユーザーが中国語の長いアドレスを覚えるのも非常に困難です。

International Resource Identifier (IRI) では、非 ASCII 文字、正確に言えば Unicode/ISO 10646 文字がサポートされます。つまり、ドメイン名に Unicode 文字を含めることができます。これにより、http://微軟香港.com のような URL を使用できるようになります。

既存の System.Uri クラスは、RFC 3987 (faqs.org/rfcs/rfc3987.html (英語) を参照) に基づく IRI をサポートするように拡張されました。IRI 機能を有効にするように明確に選択しない限り、現在のユーザーにとっては、このクラスの動作は .NET Framework 2.0 と変わりません。これは、3.5 リリースと以前のバージョンとの間のアプリケーションの互換性を確保するためです。

この機能を選択するには、2 つの変更を行う必要があります。まず、machine.config ファイルに次の要素を追加します。

<section name="uri" type="System.Configuration.UriSection, System,
                          Version=2.0.0.0, Culture=neutral,
                          PublicKeyToken=b77a5c561934e089" />

次に、ドメイン名に国際ドメイン名 (IDN) 解析を適用するかどうか、および、IRI 解析規則を適用するかどうかを指定します。これは、コンピュータ全体の machine.config か、個別のアプリケーションの app.config で指定できます。次に例を示します。

<configuration>
   <uri>
      <idn enabled="All" />
      <iriParsing enabled="true" />
   </uri>
</configuration>

IDN を有効にすると、ドメイン名に含まれるすべての Unicode のラベルが Punicode のラベルに変換されます。Punicode 名は ASCII 文字のみを含み、常にプレフィックス "xn--" で始まります。これは、インターネット上で現在展開されているほとんどの DNS サーバーが ASCII 文字のみをサポートしているためです。IDN を有効にすると、Uri.DnsSafeHost プロパティの値だけが影響を受けます。微軟香港.com の場合、Uri.DnsSafeHost には xn--g5tu63aivy37i.com が格納され、Uri.Host には Unicode 文字が格納されます。

idn 要素の enabled 属性に使用できる IDN の値は、使用している DNS サーバーに応じて、次の 3 つがあります。

  • "All" - すべてのドメインに IDN 名 (Punicode) を使用します。
  • "AllExceptIntranet" - すべての外部ドメインに IDN 名を使用し、すべての内部ドメインに Unicode 名を使用します。これは、イントラネットの DNS サーバーが Unicode 名をサポートしている場合にのみ適用されます。
  • "None" - これは既定値で、.NET Framework 2.0 の動作と一貫性があります。

IRI 解析を有効にする (iriParsing enabled = "true") と、RFC 3987 の最新の IRI 規則に従って正規化と文字のチェックが行われます (faqs.org/rfcs/rfc2396.html (英語) を参照してください)。Uniform Resource Identifier および Uri クラスの詳細については、msdn2.microsoft.com/system.uri にあるオンライン ドキュメントを参照してください。

System.Net.PeerToPeer 名前空間

.NET Framework 3.5 に追加されたエキサイティングな新しい名前空間 System.Net.PeerToPeer は、System.Net.dll アセンブリに含まれており、ピアツーピア (P2P) アプリケーションを簡単に作成するために必要なコア ビルディング ブロックを提供します。この名前空間は、一般的な P2P アプリケーションの、検出、接続、および通信という 3 つのフェーズに従って設計されました。検出フェーズでは、ピアとそれに関連付けられたネットワーク上の場所を動的に検索します。接続フェーズでは、ピア間のネットワーク接続を確立します。通信フェーズでは、ピア間でデータをやり取りします。

System.Net.PeerToPeer 名前空間の機能には、検出フェーズと接続フェーズを容易にするためのさまざまなオプションが用意されています。また、Socket、HTTPWebRequest、Windows Communication Foundation ピア チャネルなどの補完的なテクノロジが、通信フェーズに対するソリューションを提供します。

ピア間の通信を行うには、まず各ピアが相手のピアを検出し、名前やその他の種類の識別子から、互いのネットワーク上の場所 (アドレス、プロトコル、およびポート) を解決する必要があります。ピアが互いを検出して場所を解決する方法は、接続が一時的なものであること、DNS 内にアドレス レコードがないこと、および IP アドレスが動的であることにより、複雑になります。.NET Framework 3.5 でサポートされるピア名解決プロトコル (PNRP) 機能は、すべてのリソースを一連のネットワーク エンドポイントにサーバーレスで名前解決することにより、検出およびピア間の通信を実行できるようにします。PNRP は、他のピアが解決する PeerName の公開と、他のピアによって公開された PeerName の解決および関連付けられたメタデータの取得という 2 つのコア タスクを行います (PNRP プロトコルの動作の詳細については、microsoft.com/p2p (英語) を参照してください)。

System.Net.PeerToPeer 名前空間では、PeerName は通信のエンドポイントを表します。これには、コンピュータ、サービス、アプリケーションなど、メタデータを関連付けるあらゆるものを指定できます。PeerName には、セキュリティで保護された PeerName と、セキュリティで保護されていない PeerName の 2 つの形式があります。セキュリティで保護された PeerName は公開キーと秘密キーのペアに基づいており、PNRP への登録時にスプーフィングされる可能性はありません。各 PeerName 文字列には Authority セクションがあり、その後にピリオドと Classifier セクションが続きます。Authority は PeerName の種類 (セキュリティで保護されるまたは保護されない) に基づいてコンピュータによって生成されますが、Classifier はユーザー定義の文字列です。

セキュリティで保護された PeerName の場合、Framework によって Authority として 40 文字の 16 進数文字列が自動的に作成されます。この 16 進数文字列は PeerName に関連付けられた公開キーのハッシュであり、このような PeerName の登録がスプーフィングされるのを防ぐために使用されます。セキュリティで保護された PeerName を作成する方法を次に示します。

PeerName p = new PeerName("My PeerName", PeerNameType.Secured);

セキュリティで保護されていない PeerName の場合、Authority コンポーネントは常に文字 0 となります。セキュリティで保護されていない PeerName は単なる文字列であり、セキュリティは保証されません。

PeerName p = new PeerName("My PeerName", PeerNameType.UnSecured);

PeerName を作成したら、PeerNameRegistration オブジェクトのインスタンスを作成することにより、PeerName を関連するメタデータに関連付けます。通常、PeerName は IP アドレスに関連付けられますが、後で説明するようにコメント文字列やバイナリ データ BLOB に関連付けることもできます。次のコードでは、PeerEndPoint インスタンスを登録エンドポイントのコレクションに追加することにより、IPAddress を PeerName に明示的に関連付けています。

PeerName peerName = new PeerName("My PeerName", PeerNameType.Secured);

PeerNameRegistration pnReg = new PeerNameRegistration();
pnReg.PeerName = peerName;
pnReg.EndPointCollection.Add(new IPEndPoint(
    IPAddress.Parse("<ip address to associate with the name>"), 5000));

pnReg.Comment = "up to 39 unicode char comment";
pnReg.Data = System.Text.Encoding.UTF8.GetBytes(
    "A data blob up to 4K associated with the name");

これは PeerNameRegistration クラスの有効な使用方法ですが、一般的なシナリオとして、ローカル コンピュータに割り当てられているすべてのアドレスを PeerName に関連付ける場合があります。その場合は、PeerNameRegistration.UseAutoEndPointSelection プロパティを true に設定し、PeerNameRegistration.EndPointCollection への追加を行わないようにするだけです。

これで、PeerNameRegistration オブジェクトによって、すべての関連メタデータが PeerName に割り当てられました。最後に、他のピアが名前を解決できるように PeerName をクラウドに公開します。PNRP では、クラウドは PNRP に参加しているコンピュータのセットであり、名前を公開したり解決したりするスコープを定義します。名前を公開するときは、名前を公開するクラウド (スコープ) を決定する必要があります。

現在、PNRP はリンク ローカル クラウドとグローバル クラウドの 2 つのクラウドを使用しています。PeerName をリンク ローカル クラウドに公開した場合、同じリンク上にある他のピアだけが名前を解決できます。グローバル クラウドに PeerName を公開した場合は、インターネット上のすべてのユーザーが名前を解決できます。グローバル クラウドに PeerName を公開するには、Cloud.Global 列挙体を使用して PeerNameRegsitration オブジェクトの Cloud プロパティにグローバル クラウドを割り当て、登録オブジェクトに対して Start メソッドを呼び出すだけです。Start メソッドの呼び出しが完了すると名前が公開され、リモート ピアが名前を解決できるようになります。図 1 に、PeerName の作成と公開に使用するコードを示します。

Figure 1 PeerName の作成と公開 (Close Figure 1)
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.PeerToPeer;

namespace CreateAndPublish
{
    class Program
    {
        static void Main(string[] args)
        {
            // Creates a secured PeerName.
            PeerName peerName = new PeerName(
                "MyWebServer", PeerNameType.Secured);

            PeerNameRegistration pnReg = new PeerNameRegistration();
            pnReg.PeerName = peerName;
            pnReg.Port = 80;

//Starting the registration means the name is published
//for other peers to resolve.
            pnReg.Start();

            Console.WriteLine("Registration of Peer Name: {0} complete.",
                peerName.ToString());

            Console.WriteLine("Press any key to stop the registration " +
                             "and close the program");
            Console.ReadKey();

            pnReg.Stop();
        }
    }
}

ここまでは PeerName を作成して公開する方法を見てきましたが、今度は PeerName を解決する方法を見てみましょう。まず、PeerNameResolver クラスのインスタンスを作成します。これを使用して、同期 (図 2 を参照) または非同期で名前を解決します。非同期で名前を解決する場合は、同じ PeerNameResolver を使用して複数の PeerName を解決できることに注意してください。つまり、最初の解決処理が完了する前に別の PeerName に対する複数の非同期の解決処理を開始できるため、並列実行する解決処理ごとに新しいリゾルバ オブジェクトのインスタンスを作成する必要はありません。

Figure 2 同期の名前解決 (Close Figure 2)
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.PeerToPeer;
using System.Net;

namespace SyncResolve
{
    class Program
    {
        // The application accepts the peer name to resolve as the first
        // and only command line parameter.
        static void Main(string[] args)
        {
            // Create a resolver object to resolve a peername.
            PeerNameResolver resolver = new PeerNameResolver();
            PeerName peerName = new PeerName(args[0]);
            // Resolve the PeerName - this is a network operation and will
            // block until the resolve request is completed.
            PeerNameRecordCollection results = resolver.Resolve(peerName);

            // Show the data returned by the resolve operation.
            Console.WriteLine("Records from resolution of PeerName: {0}",
                peerName);
            Console.WriteLine();
            int count = 1;
            foreach (PeerNameRecord record in results)
            {
                Console.WriteLine("Record #{0} results...", count);

                Console.WriteLine("Comment:");
                if (record.Comment != null)
                {
                    Console.WriteLine(record.Comment);
                }

                Console.WriteLine("Data:");
                if (record.Data != null)
                {
                    //Assumes the data is an ASCII formatted string
                    Console.WriteLine(
                        System.Text.Encoding.ASCII.GetString(record.Data));
                }

                Console.WriteLine("Endpoints:");
                foreach (IPEndPoint endpoint in record.EndPointCollection)
                {
                    Console.WriteLine("\t Endpoint:{0}", endpoint);
                    Console.WriteLine();
                }

                count++;
            }

            Console.ReadKey();
        }
    }
}

この時点で、図 1 および図 2 のコードを使用して、PeerName の作成と解決を試みることができます。ただし、PNRP の機能を試すときに、名前を公開したのと同じプロセスで PeerName を解決することはできません。また、ここで説明した PNRP API は Windows の PNRP インフラストラクチャを使用しており、Windows XP (support.microsoft.com/kb/920342 から PNRP の更新をインストールする必要があります)、Windows Vista®、および Windows Server 2008 でサポートされます。

.NET Framework 3.5 には、System.Net.PeerToPeer.Collaboration 名前空間も導入されています。Collaboration は 2 つのコンテキストで機能します。1 つは "近くの人との接続" で、共同作業インフラストラクチャにログインしている同じサブネット上のユーザーが対象です (詳細については、microsoft.com/technet/network/p2p/pnm.mspx (英語) を参照してください)。もう 1 つのコンテキストは "連絡先" で、Windows アドレス帳 (連絡先フォルダのユーザー ディレクトリにあります) に追加されているユーザーが対象です。"連絡先" コンテキストでは、ユーザーどうしが同じサブネット上にいる必要はありません。

どちらのコンテキストでも、ユーザーはアプリケーションやオブジェクトを共有したり、招待を送信したり、他のユーザーが関係するイベントの通知を受け取ったりできます。たとえば、ある営業担当者が、空港で搭乗を待っている間に同僚たちと "ミーティング" を行う必要があるとします。この営業担当者が、自分のラップトップを起動し、会社の主な基幹業務 (LOB) アプリケーションを起動し、そのアプリケーションを使用して、同僚たちが空港で自分と一緒に座っているのか、それともまだオフィスにいるのかを動的に調べることができるのが理想的です。アプリケーションは、まず共同作業インフラストラクチャにサインインし、サインインのスコープを指定する必要があります。次の例では、PeerScope.All には、インターネットと peer-near-me スコープの両方が含まれています。

PeerCollaboration.Signin(PeerScope.All);

サインインが完了すると、アプリケーションは、次のコードを使用して、この営業担当者の近くにいるすべてのユーザーを検索します。

PeerNearMeCollection peers = PeerCollaboration.GetPeersNearMe();

PeerNearMeCollection には、呼び出し元と同じサブネット上に存在する各ピアごとに 1 つの PeerNearMe クラスのインスタンスが含まれます。したがって、アプリケーションは PeerNearMe インスタンスという形式で、"近くにいるすべてのユーザー" のリストを持っていることになります。PeerNearMe インスタンスには、リモート ピアのネットワーク上の場所または IPEndPoint (IP アドレスとポート) を指定するプロパティが含まれています。

この営業担当者のアドレス帳に格納されているすべての連絡先を検索するには、次のコードによって、アプリケーションがすべての連絡先を取得する必要があります。

ContactManager contactManager = PeerCollaboration.ContactManager();
PeerContactCollection contacts = contactManager.GetContacts();

ContactManager は Windows アドレス帳を表します。これは、System.Net.PeerToPeer.Collaboration インフラストラクチャが使用する連絡先ストアです。

これで (PeerNearMe および PeerContact で構成される) すべてのピアのリストが生成されたので、この営業担当者は対話する同僚を選択し、それぞれの同僚にすばやく招待を送信することができます。System.Net.PeerToPeer.Collaboration のコンテキストでは、招待はリモート ピアに特定のアプリケーションの起動を要求するメカニズムです。2 つのピア コンピュータ間でネットワーク接続を確立するには、1 つのピアが着信データをアクティブに待機している必要があります。このメカニズムにより、1 つのピアが、実行する必要があるアプリケーションを別のピアに通知することができます。

今度は、先の例の営業担当者が、会社の LOB アプリケーションを使用して別の同僚と対話するとします。営業担当者は、同僚が LOB アプリケーションを実行していることを確認する必要があります。そのために、営業担当者は同僚に招待を送信します。同僚のシステムが招待を受信すると、特定のユーザーから特定のアプリケーションを起動するよう求められていることを通知するダイアログ ボックス (図 3) が表示されます。このダイアログで、招待を受け入れるか拒否するかを選択できます。受信者が [Accept] (受け入れる) をクリックした場合、招待に表示されたアプリケーション (この例では LOB アプリケーション) が受信者の PC 上で自動的に起動します。ただし、アプリケーションが受信者の PC に既にインストールされていることが前提となります。

図 3 ピアからのアプリケーション起動の招待
図 3 ピアからのアプリケーション起動の招待(画像を縮小するには、ここをクリックします)
図 3 ピアからのアプリケーション起動の招待
図 3 ピアからのアプリケーション起動の招待 (画像を拡大するには、ここをクリックします)

これで、両方のシステム上で必要なアプリケーションが実行されている状態になり、共同作業の準備が整いました。この時点で、LOB アプリケーションは別のネットワーク テクノロジ (Windows Communication Foundation ピア チャネル、Socket、HTTP など) を使用して通信を行います。

招待については説明したので、次は System.Net.PeerToPeer.Collaboration 名前空間を使用した招待について、それを可能にするために必要な関連コードを示しながら説明します。P2P インフラストラクチャでは、アプリケーションは、コンピュータ上の任意の実行可能ファイルです。招待をサポートするには、アプリケーションが、招待する側と招待される側の両方のコンピュータで同じ GUID を使用してピア共同作業インフラストラクチャに登録されている必要があります。次のコードは、ローカル コンピュータにインストールされている実行可能ファイルに基づいてアプリケーションを作成および登録する方法を示しています。

PeerApplication application = new PeerApplication(
    appGuid, "Collaboration Application", bytes, pathToApp,
    arguments, PeerScope.Internet);
PeerCollaboration.RegisterApplication(
    application, PeerApplicationRegistrationType.AllUsers);

招待によって起動された登録アプリケーションは、PeerCollboration.ApplicationLaunchInfo を使用して、どの連絡先またはエンドポイントが招待を送信したかを照会できます。これにより、起動されたアプリケーションはリモート ピアからの招待によって起動されたことを認識し、招待の送信元のピアに接続できます。招待を送信するには、次のように、GetPeersNearMe から返されたピア オブジェクトに対して Invite を呼び出すか、GetContacts から返された連絡先に対して Invite を呼び出します。

PeerInvitationResponse pir = peerNearMe.Invite(
    app, "Hello World", data);

PeerInvitationResponse pir = contact.Invite(
    app, "Hello World", data);

では、営業担当者が空港にいる同僚と通信し、それぞれが別の場所に移動した後で再び通信する場合について考えてみましょう。営業担当者は、その同僚を自分のコンピュータの連絡先として追加する必要があります。そのためには、GetPeersNearMe の呼び出しで返されたピア クラスを使用して peer.AddToContactManager を実行します。いったん営業担当者と同僚がお互いを連絡先として追加すれば、どこにいても相手を検索して対話できるようになります。

.NET Framework 3.5 の共同作業 API を使用して、データを共有することもできます。たとえば、会社のすべての営業担当者が名刺を電子的に共有する必要がある場合、ピア オブジェクトを使用できます。ピア オブジェクトは、リモート ピアが表示できる BLOB データ (イメージ ファイルなど) です。オブジェクトを共有するには、"近くの人との接続" スコープを指定して同じサブネット上のユーザーにオブジェクトの表示を許可するか、"インターネット" スコープ内で、ユーザーの場所にかかわらず連絡先に含まれるユーザーにオブジェクトの表示を許可することができます。たとえば、オブジェクトを作成し、"近くの人との接続" スコープ内で共有するには、次のようにします。

PeerObject object = new PeerObject(
    objectGuid, bytes, PeerScope.NearMe);
PeerCollaboration.SetObject(object);

プレゼンスと変更の通知も、P2P の共同作業における重要な要素です。たとえば、営業担当者が、空港にいる Steve という同僚がサインインして通信可能な状態になりしだい、彼とチャットする必要があるとします。PeerNearMe クラスには、同じサブネット上のピアを更新する PeerNearMeChanged イベントが用意されています。ピアに変更が加えられると、アタッチされているデリゲートが変更情報と共に呼び出されます。これにより、営業担当者は、Steve がネットワークに入った (またはネットワークから出た) ときに通知を受け取ることができます。必要なコードは次のようになります。

PeerNearMe.PeerNearMeChanged += PeerNearMeChangedCallback;
...
void PeerNearMeChangedCallback(
    object sender, PeerNearMeChangedEventArgs args)
{
        // Check which PeerNearMe has changed and what the change was
        // from the args parameter.
}

別の場所にいる連絡先の変更通知を受け取るには、さらにいくつかの手順が必要です。このプロセスは相互信頼を必要とします。つまり、通知を受け取るユーザーは連絡先を監視することを選択する必要があり、相手のユーザーは、通知を受け取るユーザーが監視を行うことを許可する必要があります。まず、通知を受け取るユーザーが、連絡先に対して Subscribe メソッドを呼び出すことによって連絡先の監視を選択します。次に、監視対象のピアが、通知を受けるユーザーを連絡先として追加し、監視を許可する必要があります。そのためには、連絡先の SubscribeAllowed プロパティを SubscriptionType.Allowed に設定します。この手順が完了すると、監視対象のピアの名前、オブジェクト、アプリケーションなどへの変更を追跡できるようになります。

たとえば、営業担当者が、連絡先に含まれる顧客からオンライン状態であることを通知されしだい、その顧客とチャットを行う必要がある場合はどうでしょうか。営業担当者は、次のコードを使用することによって、必要な顧客の状態の変更を受け取れるようになります。

custContact.PresenceChanged += ContactPresenceChangedCallback;
...
void ContactPresenceChangedCallback(
    object sender, PresenceChangedEventArgs args)
{
    if (args.PeerPresenceInfo.PresenceStatus ==
        PeerPresenceStatus.Online)
    {
        // Start chatting with the customer
    }
}

変更情報には、特定のオブジェクトに関するものと、アプリケーションに関するものとがあります。PeerObject クラスの ObjectChanged イベントにデリゲートをアタッチすると、そのオブジェクトの変更に関する情報と、変更されたオブジェクトの発生元 (連絡先や PeerNearMe など) に関する情報が提供されます。同様に、PeerApplication の ApplicationChanged イベントにデリゲートを追加すると、そのアプリケーションの変更に関する情報が提供されます。

たとえば、ある特定のアプリケーションが、連絡先リストに含まれる連絡先によって公開されているアプリケーションのリストから変更されたかどうかを監視するには、PeerApplication の ApplicationChanged イベントにデリゲートを追加する必要があります。

peerApplication.ApplicationChanged += AppChangedCallback;
...
void AppChangedCallback(
    object sender, ApplicationChangedEventArgs args)
{
    // Check what the change was and which contact and endpoint it
    // originated from the args parameter.
}

営業担当者がこれから忙しくなるので、営業担当者の状態を変更して、すぐには対応できないことをピアに警告する必要がある場合について考えてみましょう。これを行うには、次のようにして、PeerCollaboration.LocalPresence プロパティに関連付けられているデータを変更します。

PeerCollaboration.LocalPresenceInfo = new PeerPresenceInfo(
    PeerPresenceStatus.Away, "Talking with Customer");

まとめ

この記事では、.NET Framework 3.5 に追加される主要な新しいネットワーク機能について簡単に説明しました。リリースが待ちきれない場合は、msdn2.microsoft.com/aa700831 (英語) から最新の CTP リリースをダウンロードし、これらの新機能を試してください。System.Net および Windows ネットワーク テクノロジの最新情報を入手するには、Windows Network Developer Platform チームのブログ (blogs.msdn.com/wndp (英語)) にアクセスしてください。

Mariya Atanasova は System.Net チームのテスト担当のソフトウェア設計エンジニアで、blogs.msdn.com/mariya (英語) で彼女のブログをご覧いただけます。

Larry Cleeton は System.Net チームのソフトウェア設計エンジニアで、blogs.msdn.com/lcleeton (英語) で彼のブログをご覧いただけます。

Mike Flasko は System.Net チーム、Winsock チーム、および Winsock Kernel チームのプログラム マネージャで、blogs.msdn.com/mflasko (英語) で彼のブログをご覧いただけます。

Amit Paka は System.Net チームのソフトウェア設計エンジニアで、blogs.msdn.com/amitpaka (英語) で彼のブログをご覧いただけます。


IIS 7.0: WAS を使用して WCF サービスを拡張する (非 HTTP プロトコルのサポート)

IIS 7.0

WAS を使用して WCF サービスを拡張する (非 HTTP プロトコルのサポート)

Dominick Baier and Christian Weyer and Steve Maine
この記事は、Windows Server 2008 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
コードのダウンロード : : WAS2007_09.exe (178KB)
この記事の内容 : :
  • IIS 6.0 と IIS 7.0 のアーキテクチャとプロセス モデル
  • IIS 6.0 での Web サービスのホスト方法
  • IIS 7.0 による堅牢な WCF サービスのホスト
  • Windows Process Activation Service (WAS) の動作
  • 非 HTTP プロトコルのサポート
この記事は次のテクノロジを使用しています:
IIS、WAS
コンテンツ
IIS 6.0 のアーキテクチャとプロセス モデル
IIS 7.0 と WAS
WAS の徹底解剖
構成とマルチプロトコルのアドレス指定
リッスンに必要な情報をリスナが取得する方法
非 HTTP プロトコル経由でのメッセージ ベースのアクティブ化
ワーカー プロセスの初期化
リスナから取得したデータのワーカーへの送信
WAS での WCF サービスのホスト
WAS がホストするサービスの有効期間管理
WAS がホストするサービスのセットアップの自動化
WAS の拡張

この数年、サービス指向アプリケーションに関していろいろな議論が交わされてきました。そうした中から、実際のフレームワーク、ランタイム、その他のサービス指向の接続システムを設計、構築、および展開するための公式のツールが生み出されました。その良い例の 1 つが、Windows® Communication Foundation (WCF) です。これを使用すると、マネージ コードでサービスとサービス コンシューマを作成できます。

WCF の強みの 1 つは、すべての Windows プロセス (コンソール アプリケーション、Windows フォーム アプリケーション、Windows Presentation Foundation (WPF) UI アプリケーションなど) で WCF ベースのサービスをホストできることです。構成された ID の代わりに、バックグラウンドで動作する長期 Windows NT® サービスで、WCF サービスを自己ホストさせることもできます。HTTP ベースのエンドポイントを使用する WCF サービスも、IIS 内でホストできます。従来型の Web サービスが ASP.NET や ASMX によって実装されるのに似ています。

IIS 7.0 によって堅牢な WCF サービスを提供するには、Windows Process Activation Service (WAS) と呼ばれる IIS の新機能について理解することが重要です。WAS は、IIS 7.0 の最も基本的なコンポーネントです。これを使用すると、IIS パッケージ全体をインストールしなくても HTTP 以外の WCF サービスをホストできます。

WAS とそのアプリケーションでの使用方法を説明する前に、IIS 6.0 のホスト方法と IIS 7.0 の新しいプロセスが優れている理由について、見ていきましょう。

IIS 6.0 のアーキテクチャとプロセス モデル

Windows Vista® と Windows Server® 2008 の新しいプロセス アクティブ化メカニズムを理解するには、IIS 6.0 のアーキテクチャとプロセス モデルに精通している必要があります。Windows Server 2003 と IIS 6.0 のアーキテクチャは 2 つの基本部分 (リスナ プロセスと一連のワーカー プロセス) に分けられます。IIS 6.0 のリスナ プロセスは w3svc サービスに実装されています。これは長期 Windows NT サービスで、Windows Service Control Manager (SCM) によってアクティブ化されます。リスナ プロセスは HTTP 経由で到着するメッセージを受けて、それを適切なワーカー プロセス (w3wp.exe) にディスパッチします。そのワーカー プロセスが、最終的に要求を処理するアプリケーション コードをホストします。

ネットワーク経由で要求が到着すると、その要求は、カーネル モードの HTTP スタック (http.sys) によって処理され、リスナ プロセスに配信されます。w3svc プロセスは要求 URI を参照し、これを使用して、特定の IIS アプリケーション プールにある特定の IIS アプリケーションに要求をマップします。そのアプリケーション プールをホストしているのが、ワーカー プロセス (w3wp) のインスタンスです。これを図 1 に示します。要求 URI とアプリケーションのマッピングは、IIS メタベースに格納された構成情報に基づきます。これはそれぞれ metabase.xml と mbschema.xml として、どちらも %windir%\system32\inetsrv フォルダに格納されています。w3svc がマップ先のアプリケーションを決定すると、内部データ構造を検索してマップ先のワーカー プロセスとアプリケーション プールに解決します。

図 1 IIS 6.0 の基本アーキテクチャ
図 1 IIS 6.0 の基本アーキテクチャ(画像を縮小するには、ここをクリックします)
図 1 IIS 6.0 の基本アーキテクチャ
図 1 IIS 6.0 の基本アーキテクチャ (画像を拡大するには、ここをクリックします)

ターゲットの IIS サーバーの状態によって、この時点で次の 2 つの処理のどちらかが行われます。ワーカー プロセスとアプリケーション プールが、それ以前にそのプールに対する要求を受信して既に実行中である場合、アクティブ化は必要ありません。したがって、要求は単に、待機しているワーカー プロセスにディスパッチされます。現在の要求を処理するワーカー プロセスが存在しない場合、リスナ プロセスは、要求をディスパッチする前に w3wp の新しいインスタンスを作成する必要があります。

IIS 6.0 ワーカー プロセスは、軽量の実行形式です。アクティブ化の要求に応えて起動する場合、最初に簡素なアンマネージ shim DLL (w3wphost.dll) をロードします。これによって、w3svc がワーカー プロセスと通信できるようになります。この shim は aspnet_isapi.dll のロードも行います。これは、ASP.NET のマネージ コンポーネントと IIS のアンマネージ コンポーネントのインターフェイスを実装します。

aspnet_isapi.dll はワーカー プロセスに共通言語ランタイム (CLR) をロードして、既定のアプリケーション ドメインを作成します。そのドメインに、ASP.NET のマネージ ホスティング コンポーネントが常駐します。これらのマネージ ホスティング コンポーネントは、オンデマンドで追加のアプリケーション ドメインを作成し (IIS アプリケーションごとに 1 つ)、要求の URL に基づいてドメインに要求をルーティングします。

IIS 7.0 と WAS

IIS 6.0 のプロセス モデルを理解したら、IIS 7.0 と WAS も理解できます。IIS 6.0 の主なアーキテクチャ コンポーネント (リスナ、ワーカー、およびアプリケーション マネージャ) は、すべて WAS にも含まれます。相違点は、IIS 7.0 と WAS の実装では非 HTTP シナリオをサポートすることです。サービス ベースのアプリケーション アーキテクチャには、このシナリオの方が適していることがあります。

WAS での管理性、信頼性、およびスケーラビリティは、Windows Server 2003 上の IIS 6.0 のときとまったく変わりません。また、オンデマンドのアクティブ化、プロセス状態の監視、全社規模の管理性、高速の障害対応などの問題に対応する優れた IIS 機能はすべて、IIS 6.0 では HTTP ベースのアプリケーションでしか使用できませんでしたが、今後は HTTP ベース以外のアプリケーションやサービスでも使用できるようになります。

WAS の徹底解剖

IIS 6.0 では、w3svc サービスは実際は 2 つの役割を持っていました。http.sys に登録され、受信する HTTP トラフィックの直接の受信者であったため、HTTP リスナとして動作しました。また、w3wp の新しいインスタンスの作成と要求の適切なディスパッチを行う、プロセス アクティブ化コンポーネントの所有者でもありました。IIS 7.0 では、これら 2 つの役割は別個の Windows NT サービスにリファクタされます。w3svc プロセスの HTTP リスナとしての役割は変わりませんが、構成やプロセスのアクティブ化を行うそれぞれのコンポーネントは WAS に組み込まれます。WAS は、構成マネージャ、プロセス マネージャ、およびアンマネージ リスナ アダプタ インターフェイスという、3 つの部分で構成されます。

構成マネージャは、applicationhost.config (IIS 7.0 ではこのファイルがメタベースの代わりとなります) からアプリケーションとアプリケーション プールの構成を読み取ります。プロセス マネージャはアプリケーション プールを既存のワーカー プロセスにマップし、アクティブ化要求に応えて、新しいアプリケーション プールをホストする w3wc の新しいインスタンスを作成します。アンマネージ リスナ アダプタ インターフェイスは、外部リスナが受信したアクティブ化要求を WAS に伝えます。

w3svc サービスは、カーネル レベルの http.sys による通信を行い、リスナ アダプタ インターフェイスを使用して、HTTP アクティブ化要求を WAS に伝えます。

WCF はリスナ アダプタ インターフェイスを使用して、サポート対象の非 HTTP プロトコル (つまり、TCP、名前付きパイプ、および MSMQ) 経由で受信したアクティブ化要求を伝えます。WCF が非 HTTP プロトコル経由で実際に要求を受信する処理は、SMSvcHost.exe 内部でホストされます。ここでホストされるのは、NetTcpPortSharing、NetTcpActivator、NetPipeActivator、および NetMsmqActivator という、4 つの長期 Windows NT サービスです (図 2 を参照)。

図 2 IIS 7.0 と WAS の基本アーキテクチャ
図 2 IIS 7.0 と WAS の基本アーキテクチャ(画像を縮小するには、ここをクリックします)
図 2 IIS 7.0 と WAS の基本アーキテクチャ
図 2 IIS 7.0 と WAS の基本アーキテクチャ (画像を拡大するには、ここをクリックします)

NetTcpPortSharing これは、WCF の TCP のポート共有サービスです。集中型の TCP リスナを実装して、複数プロセスが同じ TCP ポートをリッスンできるようにします。このサービスは、IIS 7.0 がインストールされていなくても使用可能です。

NetTcpActivator これは、WCF の TCP アクティブ化サービスです。TCP のアクティブ化要求を WAS に伝えます。

NetPipeActivator これは、WCF の名前付きパイプのアクティブ化サービスで、名前付きパイプのアクティブ化要求を WAS に伝えます。

NetMsmqActivator これは、WCF の MSMQ のアクティブ化サービスで、MSMQ のアクティブ化要求を WAS に伝えます。

これらのサービスはすべて同じバイナリに含まれていますが、対応する Windows NT サービスは別々で、個別に停止/起動できるため、攻撃対象領域とオーバーヘッドの両方が削減されます。これらはすべてリスナ サービスの例で、同様の方式で動作します。そのため、ここでは TCP のアクティブ化を取り上げますが、名前付きパイプおよび MSMQ のアクティブ化の例としても使えます。違いは、それぞれ特定の種類のネットワーク リソースが使用されるということだけです。

構成とマルチプロトコルのアドレス指定

アプリケーションを WAS からアクティブ化するために、まずアプリケーションを構成する必要があります。WAS でホストされている各アプリケーションに対応する構成エントリは、%windir%\system32\inetsrv\config\applicationhost.config にあります。この情報には、アプリケーションとそれを一意に識別する URL フラグメントをホストするアプリケーション プールなどのアイテムが含まれています。この例については、後で示します。IIS 6.0 の場合と同様に、各アプリケーションはアプリケーション プールとサイトの両方と関連付けられています。各サイトでは、それがサポートするネットワーク プロトコルとアドレスがバインドされています。たとえば管理者が、HTTP をポート 80 に、TCP をポート 7777 にバインドするように、既定のサイトを構成したとします。このサイトが、別々のアプリケーション プール内にある 2 つのアプリケーション (それぞれ /Foo と /Bar をリッスンする) をホストします。この構成では、/Foo アプリケーションは http://myserver.com/Foo と net.tcp://myserver.com:2323/Foo でリッスンし、/Bar アプリケーションは http://myserver.com/Bar と net.tcp://myserver.com:2323/Bar でリッスンします。受信方法にかかわらず (各アプリケーション プールにはそれぞれのワーカー プロセスがあるため)、/Foo に対するすべての要求はワーカー プロセス 1 に、/Bar に対するすべての要求はワーカー プロセス 2 に送られます。通常、アプリケーションのサイトはそのアプリケーションに関連するネットワーク アドレスを制御するのに対し、アプリケーション プールはそれをホストすることになるワーカー プロセス インスタンスを制御します。アプリケーション プールごとに 1 つのワーカー プロセスというルールは、Web ガーデンを使用しない場合にのみ有効です。Web ガーデンの場合、各アプリケーション プールは所有する一連のワーカー プロセスにマップし、そのマップするワーカー プロセスの最大数はアプリケーション プールの構成で指定されます。

リッスンに必要な情報をリスナが取得する方法

リスナはメッセージを受信する必要があります。そのため、ソケットを開く (またはパイプ ハンドルを開く、MSMQ の読み取りを開始するなどの) 必要があります。ただし、正しいメッセージを受信するには、WAS から必要なアドレス指定情報を取得する必要があります。これは、リスナの起動中に行われます。そのプロトコルのリスナ アダプタは、WAS リスナ アダプタ インターフェイスの関数を呼び出し、現在 net.tcp プロトコルでリッスン中であることを知らせ、ここで渡す一連のコールバック関数を使用して必要な情報を返すように要求します。それに応えて、WAS は、該当のプロトコル経由でメッセージを受信するようにセットアップされているアプリケーションに関する構成をすべてコールバックします。上記の例では、TCP リスナに、TCP を使用するように構成された 2 つのアプリケーション (*:7777/Foo および *:7777/Bar) があることが伝えられます。また、WAS は各アプリケーションに、一意のリスナ チャネル ID を割り当てます。この ID を使用して、要求と要求の送信先のアプリケーションが関連付けられます。

リスナ プロセスは、WAS から提供される構成情報を使用して、ルーティング テーブルを構築します。要求が到着すると、リスナ プロセスは、このテーブルを使用して、受信した要求をリスナ チャネル ID にマップします。リスナがサポートしているプロトコルを実装するということは、このしくみでマッピングが行われるということです。重要なのは、各リスナ サービスは到着するメッセージを確認できる必要があるということです。つまり、宛て先がリスナ チャネル X であると判断し、それに応じてその要求を WAS にディスパッチできるということです。WCF は、TCP、MSMQ、または名前付きパイプ経由で到着したメッセージの宛て先を示すために URI を使用しますが、他のプロトコルも、おそらくそれぞれに適した何らかの方法でこのマッピングを実装しています。

リスナ サービスが WAS に接続されて構成情報を受信すると、それ自体のネットワーク リソースを開いてメッセージのリッスンを開始できます。TCP の場合は、それによって、NetTcpActivator がソケットを開き、Socket.Accept の非同期呼び出しが作成されます。この時点で、メッセージが到着するまで、リスナは基本的にスリープ状態になります。

非 HTTP プロトコル経由でのメッセージ ベースのアクティブ化

リスナ プロセスが待機ソケットに到達すると、リスナはアクティブになり、バイト単位の読み取りを開始します。ここで、到着したメッセージの最終的な宛て先を判断するために十分なだけの情報が読み取られた後は、リスナが WAS にコールバックしてワーカー プロセスを起動するまでの間、一時停止状態になります。TCP の場合は、SOAP メッセージの前後にあるフレーミング プロトコルから情報を読み取ることで、宛て先アドレスが判断されます。リスナは、宛て先 URI を取得すると、その URI を内部ルーティング テーブルのインデックスとして使用し、URI を解決して、対応する (つまり、そのアドレスに WAS が割り当てた) リスナ チャネル ID を取得します。

リスナ サービスは、リスナ アダプタ インターフェイスのアンマネージ WebhostOpenListenerChannelInstance メソッドを呼び出して、ワーカー プロセスをアクティブ化して一時停止している要求を処理するように WAS に要求します。WebhostOpenListenerChannelInstance は、他のパラメータに加えて、リスナ チャネル ID とデータの BLOB (バイト配列形式の) を取得します。これは、アクティブ化されたプロセスにおいて受信側の機能を初期化するために使用されます。この BLOB は、実装されている各プロトコル リスナが新しくアクティブ化されたコンポーネントを初期化できるようにするだけのためのものなので、WAS には関係ありません。

WAS が WebhostOpenListenerChannelInstance への呼び出しを受信すると、WAS のプロセス マネージャ部分がワーカー プロセスの新しいインスタンスを作成します。その後、ワーカー プロセスは、リスナが受信したメッセージを処理する前に、自身である種の初期化を実行する必要があります。この間、リスナはネットワークからのデータの受信を続行しますが、ワーカー プロセスから初期化が完了したという通知を受けるまでは、ワーカーにメッセージをディスパッチできません。

ワーカー プロセスの初期化

ワーカー プロセスの内部にアクティブになっている重要なコンポーネントがいくつかあります。図 3 を参照してください。ワーカー プロセスのアーキテクチャを図 4 に示します。

Figure 3 ワーカー プロセスの重要なコンポーネント (Close Figure 3)

コンポーネント | 説明
プロセス ホスト | ワーカー プロセスに CLR をロードして、既定のアプリケーション ドメインを初期化します。
アプリケーション マネージャ | アプリケーション レベルのアクティブ化要求に応えて一意のアプリケーション ドメインを作成し、その有効期間を管理します。
プロセス プロトコル ハンドラ | プロトコル固有のプロセス初期化ロジックを実装します。
アプリケーション ドメイン プロトコル ハンドラ | アクティブ化されたアプリケーション ドメインに常駐し、プロトコル固有のアプリケーション ドメインの初期化を実行します。
図 4 ワーカー プロセスの初期化アーキテクチャ
図 4 ワーカー プロセスの初期化アーキテクチャ(画像を縮小するには、ここをクリックします)
図 4 ワーカー プロセスの初期化アーキテクチャ
図 4 ワーカー プロセスの初期化アーキテクチャ (画像を拡大するには、ここをクリックします)

IIS はマネージ コード コンポーネントを一切実装しないため、ワーカー プロセスに CLR をロードするジョブは ASP.NET プロセス ホストが処理します。WAS は、aspnet_isapi.dll によって公開される API を呼び出して、ワーカー プロセス内にプロセス ホストを作成します。WAS は、この関数にコールバック インターフェイスを渡します。マネージ プロセス ホストは、そのインターフェイスを使用して、それ以降の WAS との通信を行うことができます。その後、新しく作成されたプロセス ホストが WAS に返されるので、WAS とワーカー プロセスの間の 2 方向通信が可能になります。

WAS はワーカー プロセスから取得したプロセス ホストのインスタンスを使用して、プロトコル固有の初期化ルーチンを開始します。プロセス ホストのアクティブ化システム StartProcessProtocolListenerChannel は、その初期化ルーチンにプロトコル キー ("net.tcp" など) とコールバック インターフェイスを渡します。プロトコル固有のハンドラは、そのインターフェイスを使用して、WAS に応答します。

プロセス ホストは、ワーカー プロセス内の新しいプロセス プロトコル リスナ チャネルを開始するという要求を受信すると、最初に構成内のプロトコル キーを検索します。WAS のユーザーは、リスナを開始する前にこのキーを登録しておく必要があります。たとえば、WCF は、net.tcp に対するプロセス プロトコル ハンドラとアプリケーション ドメイン プロトコル ハンドラの両方を登録します。図 5 に示した構成データは、%windir%\Microsoft.NET\Framework\v2.0.50727\CONFIG 内のマスタ web.config ファイルにあります。

Figure 5 net.tcp プロトコル ハンドラの構成 (Close Figure 5)
<system.web>
  <protocols>
    <add name="net.tcp"
     processHandlerType=
      "System.ServiceModel.WasHosting.TcpProcessProtocolHandler,
       System.ServiceModel.WasHosting, Version=3.0.0.0, Culture=neutral,
       PublicKeyToken=b77a5c561934e089"
     appDomainHandlerType=
      "System.ServiceModel.WasHosting.TcpAppDomainProtocolHandler,
       System.ServiceModel.WasHosting, Version=3.0.0.0, Culture=neutral,
       PublicKeyToken=b77a5c561934e089" validate="false" />
  </protocols>
</system.web>

プロセス ホストは、構成内のプロトコル キーを解決すると、構成されたプロセス プロトコル ハンドラ タイプの新しいインスタンスを既定のアプリケーション ドメインに作成し、その中の StartListenerChannel を呼び出します。このメソッドには、2 つの別個のコールバック インターフェイスがあります。一方は、WAS との通信を可能にします (つまり、事実上、プロセス ホストに渡された WAS コールバック オブジェクトと同じものです)。もう一方は、直接プロセス ホストとの通信を可能にします (図 6 を参照)。

図 6 プロトコル ハンドラの登録
図 6 プロトコル ハンドラの登録(画像を縮小するには、ここをクリックします)
図 6 プロトコル ハンドラの登録
図 6 プロトコル ハンドラの登録 (画像を拡大するには、ここをクリックします)

プロセス プロトコル ハンドラ (PPH) は、特定のプロトコルのホスティング モデルを強力に制御します。たとえば PPH は、初期化ルーチンをこの時点で単純に停止して、それ以降の要求をすべて既定のアプリケーション ドメインで処理することができます。しかし、WCF は IIS 6.0 のアプリケーション プール モデルを維持しています。このモデルでは、各アプリケーションは、それ自体のアプリケーション ドメイン内でアクティブ化されるので、それぞれ個別に監視およびリサイクルを行うことができます。そのため、WCF PPH は、アプリケーションとアプリケーション ドメインの有効期間を制御する ASP.NET アプリケーション マネージャと連携する必要があります。

プロセス ホストは、StartListenerChannel に IAdphManager インターフェイスを渡します。PPH は、そのインターフェイスを使用して、アプリケーション マネージャの機能にアクセスできます。アクティブ化要求の送信先のアプリケーションを判断するために、PPH は WAS にコールバックして、リスナが最初に WebhostOpenListenerChannelInstance を呼び出したときに提供したデータ BLOB を要求します。返されたバイト配列にある種の処理を実行した後で、WCF PPH は、送信先のアプリケーションを一意に識別するアプリケーション キー (/w3svc/1/Foo など) にアクセスします。原則として、WCF リスナにはアプリケーション キーがあり、リスナ チャネルに送信したデータ BLOB にそれが含まれているためです。WAS は BLOB で渡されるデータを一切制限しません。WAS の観点からは、ただの不透明なバイト配列にすぎないからです。これで、アプリケーション キー、プロトコル キー、およびリスナ コールバック インターフェイスを IAdphManager.StartAppDomainProtocolListenerChannel に渡して、そのアプリケーションのアプリケーション ドメインをアクティブ化できるようになりました。

通常、アプリケーション マネージャは、すべてのアプリケーション ドメインのテーブルのインデックスとして、アプリケーション キーを使用します。StartAppDomainProtocolListener への呼び出しを受信すると、このテーブルを調べて、先に別のプロトコル経由で到着した要求によって、そのアプリケーションが既にアクティブ化されているかどうかを確認します。テーブルにない場合、プロトコル キーを使用して、プロセス ホストがこのキーを使用してプロセス プロトコル ハンドラ タイプを検索したときと同様に、プロトコル固有のアプリケーション ドメイン プロトコル ハンドラ タイプを検索します。アプリケーション プロトコル ハンドラ タイプが解決されると、アプリケーション マネージャは、その新しいインスタンスをターゲット アプリケーション ドメインに作成します。最後に、アプリケーション マネージャは、AppDomainProtocolHandler.StartListenerChannel を呼び出して、再度 WAS にコールバック インターフェイスを渡します。それによって、そのアプリケーション ドメインは、必要に応じて WAS と通信できます。

アプリケーション ドメイン プロトコル ハンドラがインスタンス化されると、ワーカー プロセスの内部は図 7 のようになります。この時点で、WAS は、ターゲット アプリケーションが完全にアクティブ化されたと判断し、そのアプリケーションにはそれが停止するまで関与しません。

図 7 アプリケーション ドメイン プロトコル ハンドラの初期化
図 7 アプリケーション ドメイン プロトコル ハンドラの初期化(画像を縮小するには、ここをクリックします)
図 7 アプリケーション ドメイン プロトコル ハンドラの初期化
図 7 アプリケーション ドメイン プロトコル ハンドラの初期化 (画像を拡大するには、ここをクリックします)

リスナから取得したデータのワーカーへの送信

ワーカー プロセスとアプリケーション ドメインのアクティブ化は、より大きな目的であるアプリケーションへの実際のメッセージの配信への第 1 歩です。WAS は、アクティブ化サービスであってメッセージ配信サービスではないため、この部分には関与しません。代わりに、各プロトコルが、リスナとワーカーとの通信を、それぞれに適した方法で自由に実装できます。このアーキテクチャによって、さまざまなプロトコル固有の最適化が可能になるので、それぞれの実装において、使用するプロトコルの特質を十分に活用することができます。たとえば、WCF MSMQ アクティブ化サービスは、MSMQ キューの名前をデータ BLOB に格納して渡すことによって、アクティブ化されたアプリケーションが、データ ソースを直接読み取り、完全にリスナをバイパスできるようにしています。

WAS での WCF サービスのホスト

さあ、いよいよ、WAS で実際に WCF サービスをホストする泥臭い仕事にかかりましょう。最初に、HTTP ベースのサービスをセットアップしますが、これは IIS 6.0 での作業とほとんど変わりません。その後で、TCP、名前付きパイプ、および MSMQ のサポートを追加し、使用可能な構成オプションの詳細について説明します (単純なサービスのソース コードについては、図 8 を参照してください)。

Figure 8 WCF サービスのサンプル (Close Figure 8)
[ServiceContract(Name = "WasDemoServiceContract",
                Namespace = "urn:msdnmag")]
public interface IDemoService
{
  [OperationContract(Name="Ping", Action="Ping",
                     ReplyAction="PingReply")]
  string Ping(string data);
}

[ServiceBehavior(
  Name="WasDemoService", Namespace="urn:msdnmag",
  InstanceContextMode=InstanceContextMode.PerCall,
  ConcurrencyMode=ConcurrencyMode.Single)]
public class DemoService : IDemoService
{
  public string Ping(string data)
  {
    return "pinged with: " + data;
  }
}

最初に、WAS で新しいアプリケーションを作成します。それには、IIS 7.0 管理ツールか Visual Studio® を使用できます。仮想ディレクトリには、サービス (コンパイル済みの場合は /bin ディレクトリ内で、ソース コードの場合は /App_Code フォルダ内)、.svc サービス エンドポイント、および構成ファイル、の 3 つを含める必要があります。

.svc ファイルは、URI とサービスの実装との接続を作成します (.asmx ファイルによく似ています)。.svc ファイル内の @ServiceHost ディレクティブは、Service 属性で指定されたタイプの ServiceHost インスタンスを作成するように、WCF の機能に指示します。

<% @ServiceHost Service="DemoService" %>

WAS の構成情報は、web.config ファイルに格納されます。全体的な WCF の構成は、system.serviceModel セクションで行います。ここで注意することは、WAS でホストするときにはベース アドレスを指定しないことです。ベース アドレスは、Web サイトと仮想ディレクトリ システムによって判断されるからです (基本構成ファイルについては、図 9 を参照してください)。構成されたサービスの動作によってメタデータが公開され、svcutil.exe や Visual Studio の "Add Service Reference" ダイアログなどのツールを .svc ファイルに直接指定して、クライアント側のプロキシを生成することができます。

Figure 9 サンプル サービスの基本構成 (Close Figure 9)
<configuration>
  <system.serviceModel>
    <services>
      <service name="DemoService" behaviorConfiguration="Behavior">
        <endpoint address="" binding="wsHttpBinding"
         bindingNamespace="urn:msdnmag" contract="IDemoService" />
      </service>
    </services>
    ...
    <behaviors>
      <serviceBehaviors>
        <behavior name="Behavior">
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

HTTP トランスポートを使用する WCF エンドポイントは、共通 IIS 処理パイプラインを横一列で処理します。これまでに ASP.NET Web サービスを使用したことがある場合は、見慣れているように感じるかもしれませんが、内部では少し違った処理をしています。実際には、HTTP ベースのエンドポイントには、ASP.NET 互換と非互換の、2 つの操作モードがあります。既定では、ASP.NET 互換はオフです。つまり、HttpContext.Current は Null で、HttpHandler ではエンドポイントのアクティブ化は発生しませんが、HttpModule で発生します。HttpModule は、ASP.NET PostAuthenticateRequest パイプライン イベントで ServiceHost に要求を渡します。この意味するところは、第一に、ASP.NET の拡張性モデルに慣れているユーザーにとっては、必要な操作は PostAuthenticateRequest によるパイプライン通知を受信することだけであるということです。そこから、処理は EndRequest イベントに直接ジャンプします。同時に、WCF サービスは ASP.NET のホスト機能 (セッション状態など) にはアクセスできなくなります。これは意図的なものです。WCF サービス ホストには、それらのすべての機能 (セッション、認証など) に対するトランスポートに依存しない同等の機能があるため、サービスを特定のホスティング環境と関連付けないでください。

ASP.NET 互換がどうしても必要な場合は (たとえば、サービスと同じアプリケーション ドメインでホストされている ASP.NET アプリケーションとの間でセッション状態を共有する場合)、次の構成スイッチを使用して互換機能をオンにします。

<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />

さらに、サービスで ASP.NET 互換を明示的に有効にする必要があります。

[AspNetCompatibilityRequirements(
    RequirementsMode=AspNetCompatibilityRequirementsMode.Required)]
class DemoService : IDemoService { ... }

この 2 つのスイッチの組み合わせによって、サービス要求の内部処理の方法が変わります。この 2 つのスイッチを使用すると、要求は HttpHandler を使用して WCF ランタイムにディスパッチされ、処理パイプライン全体の動作が ASP.NET Web サービスに似たものになります。さらに、現在の HttpContext が使用可能になり、通常どおり使用されます。ASP.NAT 互換モードで実行している WCF サービスは、HTTP エンドポイントのみをサポートします。

WAS で HTTP ベースのサービスをホストする方法がわかったところで、非 HTTP エンドポイントをサービスに追加しましょう。前に説明したとおり、プロトコル リスナは実際のトランスポートを開いて、サービスを実行するアプリケーション ドメインに接続をディスパッチします。これらのプロトコル リスナは Windows NT サービスです。services.msc を開くと、Net.tcp Listener Adapter の他に名前付きパイプや MSMQ のリスナがあるのがわかります。非 HTTP アクティブ化が正常に実行されるために、これらのリスナは、TCP のポート共有サービスと共に、起動され実行されている必要があります。

リスナ アダプタのホスト名とポート構成は、WAS の構成ファイルである applicationHost.config にあります。設定の編集は、手動、appcmd.exe コマンドライン ツール、または IIS 7.0 構成 GUI のいずれかの方法で行えます。これらの設定はバインドと呼ばれ、サイト レベルで実行されます (図 10 を参照)。Windows Vista と Windows Server 2008 とで、付随する WAS のバージョンに若干違いがあります。Windows Vista では、バインドは事前構成されず、グラフィカルにそれらを追加するための GUI も現時点ではありません。必要な appcmd.exe コマンドについては、後で説明します。Windows Server 2008 では、バインドは既定で構成されます。

図 10 IIS 6.0 の基本アーキテクチャ
図 10 IIS 6.0 の基本アーキテクチャ(画像を縮小するには、ここをクリックします)
図 10 IIS 6.0 の基本アーキテクチャ
図 10 IIS 6.0 の基本アーキテクチャ (画像を拡大するには、ここをクリックします)
図 10 IIS 6.0 の基本アーキテクチャ
図 10 IIS 6.0 の基本アーキテクチャ

<bindings>
  <binding protocol="https" bindingInformation="*:443:" />
  <binding protocol="http" bindingInformation="*:80:" />
  <binding protocol="net.tcp" bindingInformation="808:*" />
  <binding protocol="net.pipe" bindingInformation="*" />
  <binding protocol="net.msmq" bindingInformation="localhost" />
  <binding protocol="msmq.formatname" bindingInformation="localhost" />
</bindings>

プロトコル固有の構成設定は、bindingInformation 属性で指定します。たとえば、TCP バインドを行うには、bindingInformation 属性に、ポートと共に、リスナが接続を受け入れるホスト名を指定します。* を指定するとすべてのホスト名が有効になります。

最後の手順は、サービスをホストするアプリケーションでプロトコルを有効にすることです。Windows Server 2008 Beta 3 の場合と同様、このための GUI はありません。applicationHost.config を手動で構成する必要があります。ツールやプログラムを使用してこれを行う方法については、後で説明します。構成ファイルでそのアプリケーションの要素を探し、enabledProtocols 属性を追加します。次に例を示します。

<application path="/WasDemo"
    enabledProtocols="http,net.tcp,net.pipe,net.msmq">
  <virtualDirectory path="/" physicalPath="C:\MSDN\WasDemo\web" />

次に、ここで指定したプロトコル経由で到達できるように、WCF サービス構成に新しくエンドポイントを追加します。

<endpoint address="" binding="netTcpBinding"
  bindingNamespace="urn:msdnmag" contract="IDemoService" />
<endpoint address="" binding="netNamedPipeBinding"
  bindingNamespace="urn:msdnmag" contract="IDemoService" />

メタデータからクライアント プロキシを生成すると、構成ファイルには各エンドポイントのクライアント要素が含まれます。そのクライアント コードでプロキシ コンストラクタにエンドポイント名を渡すことによって、使用するトランスポートを指定できます。

WasDemoServiceContractClient proxyTcp =
  new WasDemoServiceContractClient(
    "NetTcpBinding_WasDemoServiceContract");
Console.WriteLine(proxyTcp.Ping("Hello"));

MSMQ エンドポイントをサービスに追加する場合は、さらに手順が必要になります。最初に、Message Queuing MMC スナップイン (または任意のプログラム的なアプローチ) を使用して、キューを作成する必要があります。さらに、そのキューにアクセス制御リスト (ACL) を設定して、ワーカー プロセス (既定では NETWORK SERVICE) がキューを読み取って確認できるようにする必要があります。

重要なことは、キューの名前が .svc ファイル名からマシン名を除いたものと同じである場合にのみ、MSMQ エンドポイントのアクティブ化が正常に動作することです。つまり、サービスのエンドポイントが /server/app/service.svc の場合は、キュー名を app/service.svc にする必要があります。

サービス構成の中で、そのサービスがリッスンするキューの名前を指定する必要があります。

<endpoint address="net.msmq://localhost/private/wasdemo/service.svc"
  binding="netMsmqBinding" bindingNamespace="urn:msdnmag"
  contract="IServiceMsmq" />

非 HTTP エンドポイントは、IIS 処理パイプラインを渡すことはなく、WCF ランタイムに直接ルートされます。つまり、前処理や後処理の要求に HttpModule を使用できないことになります。さらに、HttpApplication クラス (global.asax) の Application_Start および Application_End は起動されません。したがって、そのようなサービスのコードを起動またはクリーンアップする場合は、ServiceHost クラスのイベントを使用する必要があります。次に、その動作を詳細に見てみましょう。

WAS がホストするサービスの有効期間管理

(たとえば Windows NT で) サービスを自己ホストするとき、そのサービスの有効期間は一定です。Open を呼び出すと実行し、Close を呼び出すと停止します (ここでは致命的な障害については無視します)。さらに、Windows NT サービスは通常はブート中に起動します。WAS でホストする場合は、この点が違います。

最初に、WAS はオンデマンドでアクティブ化を行います (ご存知のように WAS の "A" は Activation (アクティブ化) の "A" です)。つまり、サービスをホストするアプリケーション ドメインは、要求メッセージが到着したときにだけ作成されます。構成可能なアイドル期間が終了すると、アプリケーション ドメインは停止します。WAS または ASP.NET ランタイムによって、アプリケーション ドメインまたはワーカー プロセス全体がリサイクルされることが決まる場合がありますが、その理由はいくつかあります。

最初に、WAS のアプリケーション プールの設定によって、ワーカー プロセスの定期的なリサイクルが指定されます。既定では 29 時間ごとです (この重要な数値は、リサイクルが毎日同時刻に発生するのをできるだけ避けるように、意図的に選択されています)。

アプリケーション ドメインがリサイクルされる理由は、他にもあります。まず、このアプリケーション ドメインに影響する machine.config、web.config、または applicationHost.config の構成設定が変更された場合です。次に、/bin ディレクトリや /App_Code ディレクトリ、またはそのコンテンツが修正された場合、(.aspx、.asmx、.svc などの) 再コンパイルの数が machine.config または web.config の <compilation numRecompilesBeforeAppRestart /> で指定された上限 (既定では 15) を超えた場合や、仮想ディレクトリの物理パスが修正された場合です。最後に、コード アクセス セキュリティ (CAS) ポリシーが修正されたり、アプリケーションのサブディレクトリが削除されたりした場合です。この場合は、セッションまたはインスタンス、および静的変数など、メモリ内の状態がすべて失われます。要約すると、購入した直後の WAS でのホストは、セッション方式のサービスにも単独のサービスにも適していないということです。より適しているのは、ステートレスな呼び出しごとのサービスです。

WCF ServiceHost タイプには、Opening と Closing という 2 つのイベントがあります。それらは、サービスの起動時と停止時にコードを実行する唯一の適切な方法です。問題は、ServiceHost の新しいインスタンスの作成とそれに対する Open の呼び出しの間に、それらのイベントのハンドラを連結する必要があることです。前に説明したように .svc ファイルで @ServiceHost ディレクティブを使用している場合は、これは不可能です。この場合に可能な方法は、カスタム サービス ホスト ファクトリをホストすることです。これによって、より強力な制御が得られるので、上記のイベントを処理できるようになります。

これを実行するには、ServiceHostFactoryBase というクラスから派生して CreateServiceHost メソッドを実装する必要があります。このメソッドは、ホスティング環境からサービス タイプ名と基本アドレスを受信し、ServiceHostBase のインスタンスを返します。ユーザーは、適切な ServiceHost を作成し、要件に応じて構成し、それを WCF ランタイムに返します。これを行うと、WCF の拡張モデルにプログラムでアクセスできるようになり、イベント ハンドラを連結できます (図 11 を参照)。

Figure 11 CreateServiceHost の使用 (Close Figure 11)
public class HostFactory : ServiceHostFactoryBase
{
  public override ServiceHostBase CreateServiceHost(
    string constructorString, Uri[] baseAddresses)
  {
    Type service = Type.GetType(constructorString);
    ServiceHost host = new ServiceHost(service, baseAddresses);
    // hook up event handlers
    host.Opening += OnOpening;
    host.Closing += OnClosing;
    return host;
  }
}

既存の .svc エンドポイントとカスタム ファクトリを接続するには、次のように、@ServiceHost ディレクティブに Factory 属性を追加する必要があります。

<%@ServiceHost Service="Service" Factory="HostFactory" %>

WAS がホストするサービスのセットアップの自動化

ここまで述べてきたとおり、WAS がホストする WCF サービスのセットアップには、次のようないくつかの手順が必要です。

  1. 物理ディレクトリ (および、必要な場合は ACL) のセットアップ
  2. アプリケーションおよび WAS の仮想ディレクトリのセットアップ
  3. 有効なプロトコルの構成
  4. キューの作成と ACL の設定 (MSMQ を使用する場合)

WAS には、Windows Management Instrumentation (WMI)、コマンドライン ツール (appcmd.exe)、新しいマネージ API など、構成のためのさまざまなインターフェイスがあります。WMI と appcmd.exe は、スクリプト ファイルやバッチ ファイルに適しています。一方、新しいマネージ API を使用すると、MSI パッケージなど複雑な展開シナリオの構成を簡単に行うことができます。appcmd.exe の例については、図 12 を参照してください。

Figure 12 appcmd.exe を使用した WAS アプリケーションの設定 (Close Figure 12)
REM adding bindings to the default web site
appcmd.exe set site "Default Web Site" –
    +bindings.[protocol='net.tcp',bindingInformation='808:*']
appcmd.exe set site "Default Web Site" –
    +bindings.[protocol='net.pipe',bindingInformation='*']
appcmd.exe set site "Default Web Site" –
    +bindings.[protocol='net.msmq',bindingInformation='localhost']

REM add a new application
appcmd add app /site.name:"Default Web Site" /path:/WasDemo
    /physicalpath:c:\etc\WasDemo

REM enabled protocols for application
appcmd.exe set app "Default Web Site/WasDemo"
    /enabledProtocols:http,net.pipe,net.tcp,net.msmq

マネージ構成 API は、新しい Microsoft.Web.Administration.dll アセンブリに含まれ、その中心は ServerManager クラスです。ここには、アプリケーション プール、ワーカー プロセス、サイト、アプリケーション ファイル、および構成ファイルに対応する、完全なオブジェクト モデルが含まれています。この API を使用してアプリケーションをセットアップするには、コードをほんの数行書くだけです。図 13 を参照してください。また、図 14 は、NETWORK SERVICE アカウントについて、キューおよび最低限の ACL をセットアップする小型のヘルパ メソッドを示しています。

Figure 14 キューの作成と ACL の設定 (Close Figure 14)
private static void SetupMsmq(string queuename)
{
  // create queue – in this case a transactional queue is created
  MessageQueue queue = MessageQueue.Create(queuename, true);
  queue.Label = queuename;

  // set ACL
  queue.SetPermissions(GetAccountName(
    WellKnownSidType.NetworkServiceSid),
    MessageQueueAccessRights.ReceiveMessage);
}

private static string GetAccountName(WellKnownSidType wellKnownSid)
{
  NTAccount account = (NTAccount) new SecurityIdentifier(wellKnownSid,
    null).Translate(typeof(NTAccount));
  return account.Value;
}
Figure 13 マネージ構成 API の使用 (Close Figure 13)
private static void SetupWasApp(string virtualPath, string physicalPath,
  string enabledProtocols)
{
  ServerManager manager = new ServerManager();

  // creating the application
  manager.Sites[0].Applications.Add(virtualPath, physicalPath);
  manager.CommitChanges();

  // setting up the enabled protocols
  Application wasApp = manager.Sites[0].Applications[virtualPath];
  wasApp.EnabledProtocols = enabledProtocols;
  manager.CommitChanges();
}

WAS の拡張

WAS は拡張可能で、カスタム WCF トランスポート チャネルに対する独自のサポートを作成することができます。Widndows SDK には UDP (User Datagram Protocol) ベースの WAS のアクティブ化を拡張するサンプルが含まれていますが、独自機能を追加して WAS を拡張することは簡単ではありません。カスタム プロセスとアプリケーション プロトコル ドメイン ハンドラの実装に加えて、個々のターゲット トランスポート メカニズムで受信メッセージをリッスンするためのコードを作成することになります。これを十分に説明するには、このトピックだけの詳細な記事が必要になるでしょう。

ただし、もう 1 点、重要なポイントを説明する必要があります。WAS の要求実行数の上限は、使用する OS によって異なるということです。Windows Server 2008 では制限がありませんが、Windows Vista Ultimate および Windows Vista Business では接続数の上限が 10 です。Windows Vista の家庭向けバージョンである Windows Vista Home Basic および Windows Vista Home Premium では、Windows Vista Starter と同様、要求実行数の上限が 3 です。実装を計画するときには、この点を念頭においてください。

Dominick Baier は、ドイツの thinktecture 社 (thinktecture.com) のセキュリティ コンサルタントです。また、DevelopMentor (develop.com) で、セキュリティおよび WCF のカリキュラムの指導も行っており、Developer Security MVP でもあり、『Developing More Secure Microsoft ASP.NET 2.0 Applications』(Microsoft Press 発行、2006 年) の著者でもあります。彼のブログは、leastprivilege.com (英語) にあります。

Christian Weyer は thinktecture の共同設立者かつ主設計者であり、Java、COM、DCOM、COM+、Web Services、WCF、およびその他のテクノロジを使用する分散アプリケーションのモデル化と実装を行ってきました。連絡先は、thinktecture.com/staff/christian (英語) です。

Steve Maine は、マイクロソフトの Connected Framework チームのシニア プログラム マネージャです。現在は .NET Framework 3.5 でリリースされる予定の WCF に対する、多くの Web 対象のプログラミング モデル機能に取り組んでいます。彼のブログは、hyperthink.net/blog (英語) にあります。


単体テスト: テスト代替の連続性について検討する

単体テスト

テスト代替の連続性について検討する

Mark Seemann
コードのダウンロード : : Testing2007_09.exe (271KB)
この記事の内容 : :
  • テスト代替による単体テスト
  • ダミー、スタブ、スパイ、フェイク、およびモック
  • 手動モックと動的モック
  • 各テスト代替の使用に適した状況と方法を理解する
この記事は次のテクノロジを使用しています:
単体テスト
コンテンツ
ダミーの単体テスト
スタブを使用する条件を識別する
テストにスパイを追加する
単体テストのモックを作成する
値を返す
フェイクを作成する
手動モック
実際面でのモック
タイプを比較する

単体テストはこの数年で急速に普及しましたが、開発者の理解がその全般的な概念に留まり、個々の側面についての理解は不十分である場合がほとんどです。これは特に、テストの目的に応じてコンポーネント サーバーを効果的に置き換える方法の理解において顕著です。多くの場合、このような代替はスタブまたはモックと呼ばれますが、後述するように、代替の連続性においてはこの 2 つのタイプが比較的広い範囲に位置付けられています。

Gerard Meszaros (xunitpatterns.com) は、テストの目的に応じて実際のコンポーネント サーバーの代わりに使用するオブジェクトの一般的な名称として、"テスト代替 (test double)" という用語 ("代役 (stunt double)" と同意) を提案しています。このテスト代替そのものは一般的な用語であり、個々の実装の説明には図 1 のような個別の名称を使用します。

Figure 1 定義済みのテスト代替 (Close Figure 1)

テスト代替のタイプ | 説明
ダミー | 最も単純かつ基本的なタイプのテスト代替です。ダミーは実装を含まず、ほとんどの場合、パラメータ値として必要ではあるものの、特に使用されない場合に利用されます。Null をダミーと考えることもできますが、実際のダミーはインターフェイスまたは基本クラスの派生であり、実装は一切含まれません。
スタブ | スタブはダミーよりも高度なテスト代替であり、インターフェイスまたは基本クラスの最低限の実装です。通常、void を返すメソッドは実装を一切含みませんが、値を返すメソッドはハードコードされた値を返すことが一般的です。
スパイ | テスト スパイはスタブと似ていますが、メンバを呼び出すインスタンスをクライアントに渡す点が異なります。スパイはまた、呼び出されたメンバを記録するため、期待どおりにメンバが呼び出されたかどうかを単体テストで検証できます。
フェイク | フェイクにはより複雑な実装が含まれ、これが継承するタイプのメンバ間での対話を処理することが一般的です。実稼動状態の実装の一部を省略するため、フェイクは完全な実稼動状態の実装とはなりませんが、これに近い実装となります。
モック | モックはモック ライブラリによって動的に作成されます (一般に他のテスト代替は、テスト開発者がコードを使用して作成する)。テスト開発者は、インターフェイスや基本クラスを実装する実際のコードを参照することはありませんが、呼び出し対象の特定のメンバなどを除き、戻り値を返すようにモックを構成できます。その構成に応じて、モックはダミー、スタブ、またはスパイのように動作します。

理論面では各タイプが区別されていると言うこともできますが、実際面ではその差異は明確ではありません。したがってテスト代替は、図 2 に示すように、連続性の中に位置付けられるという理解が妥当であると考えます。横軸の左端には実装を一切提供しないダミーが位置し、その反対側には完全な実稼動状態にある実装が位置します。ダミーと実稼動状態の実装はそれぞれ明確に定義されますが、スタブ、スパイ、およびフェイクを明確に定義することは困難です。たとえば、テスト スパイとフェイクの境界を決めることは簡単ではありません。また、モックは極めて複雑にも単純にも設定できることから、この連続性の中で広い範囲に位置付けられます。

図 2 テスト代替の範囲
図 2 テスト代替の範囲(画像を縮小するには、ここをクリックします)
図 2 テスト代替の範囲
図 2 テスト代替の範囲 (画像を拡大するには、ここをクリックします)

このような説明は観念的で理解が難しいと思われるかもしれませんが、そのようなことはありません。ここでは多数のサンプルを使用して、この概念について具体的に説明します。ただし、スペースの制約により、さまざまなクラスを取り上げるのではなく、簡単な実装に絞って説明していきます。よく理解できない部分がある場合には、MSDN® Magazine Web サイトから、この記事のすべてのコードをダウンロードできます。

ダミーの単体テスト

モックとダミーと比較すると、テスト代替の連続性においてはモックがやや特殊な位置を占めます。したがってダミーの方がはるかに理解しやすいため、ここではダミーに関連するサンプルから説明を始めることにします。

ここでは、特に身近なところで、既に有効性が証明されているオンライン ショップの注文処理のシナリオをサンプルとして使用します。図 3 の単純な Order クラスについて検討します。最も重要な点として、コンストラクタが IShopDataAccess のインスタンスを受け取ることに注意してください。これは、次のように定義されます。

Figure 3 Order クラス (Close Figure 3)
public class Order
{
    private int orderId_;
    private IShopDataAccess dataAccess_;
    private OrderLineCollection orderLines_;

    public Order(int orderId, IShopDataAccess dataAccess)
    {
        if (dataAccess == null)
        {
        throw new ArgumentNullException("dataAccess");
        }
        this.orderId_ = orderId;
        this.dataAccess_ = dataAccess;
        this.orderLines_ = new OrderLineCollection(this);
    }

    public OrderLineCollection Lines
    {
        get { return this.orderLines_; }
    }

    public void Save()
    {
        this.dataAccess_.Save(this.orderId_, this);
    }

    internal IShopDataAccess DataAccess
    {
        get { return this.dataAccess_; }
    }
}

public interface IShopDataAccess
{
    decimal GetProductPrice(int productId);
    void Save(int orderId, Order o);
}

Order クラスの単体テストを実行するには、IShopDataAccess のテスト代替を設定する必要があります。これは、null が渡された場合に、コンストラクタが例外をスローするためです。この場合に作成できる最も単純なテスト代替はダミーですが、これは Visual Studio® 2005 で簡単に作成できます。単体テスト プロジェクトで新しいクラスを作成してこれを DummyShopDataAccess とし、これを通じて IshopDataAccess を実装します。Visual Studio でそのインターフェイス名のスマート タグをクリックして [Implement interface IShopDataAccess] (インターフェイス IShopDataAccess の実装) を選択すると、Visual Studio がこのインターフェイスのすべてのメンバを作成します。メンバの実装はすべてシグネチャを除いて同一であるため、ここでは Save メソッドを示します。

public void Save(int orderId, Order o)
{
    throw new Exception("The method or operation is not implemented.");
}

Visual Studio では 10 秒もかからずにダミーを作成できます。クラスのメンバ数にかかわらず、インターフェイス宣言を含むクラス宣言の 1 行のコードを書くだけです。

これで DummyShop DataAccess クラスが作成されたので、Order クラスの単純な単体テストを次のように簡単に記述できます。

[TestMethod]
public void CreateOrder()
{
    DummyShopDataAccess dataAccess = new DummyShopDataAccess();

    Order o = new Order(2, dataAccess);
    o.Lines.Add(1234, 1);
    o.Lines.Add(4321, 3);

    Assert.AreEqual<int>(2, o.Lines.Count);
    // More asserts could go here...
}

テスト ターゲットで実行されるメソッドでは IShopDataAccess インターフェイスは使用されないため、ダミーがコンストラクタの強力な入力検証を切り抜けることだけが必要になります。したがって、これでダミーの準備が整ったことになります。実際に IShopDataAccess インスタンスを使用するメンバ (Save メソッドなど) に呼び出しをかけると、DummyShopDataAccess を使用した場合には、この呼び出しによって例外がスローされるため、直ちにテストが停止します。このようなテストでは、他のタイプのテスト代替も使用できます。次に、これについて説明します。

スタブを使用する条件を識別する

単体テストがテスト ターゲットのメンバを呼び出すと、次にこれがテスト代替のメンバを呼び出すため、少なくとも例外をスローしない方策が必要になります。この条件を満たす最も単純なタイプのテスト代替がスタブです。

次のサンプルでは、Order クラスの Save メソッドを呼び出す単体テストを記述します。この単体テストは次に IShopDataAccess インターフェイスの Save メソッドを呼び出すため、これが例外をスローしないように Save メソッドを実装する必要があります。このメソッドは void を返すことから、その実装は極めて簡単です。

public void Save(int orderId, Order o) { }

セマンティクスに関心がある場合には、この実装が実際にはダミーとスタブのどちらに該当するかについて、判断に迷うことも考えられます。この疑問は、このコード行に多くの実装が存在することについての指摘につながる場合もあります。仮にここで、他のメンバによる例外のスローをそのままにしておくとすれば、このテスト代替が実質的にはダミーであると判断することも極めて妥当です。一方、ダミーは必要なパラメータを埋めることだけを目的として使用されますが、この実装を別の目的に使用することもできます。これは、クライアントが停止することなくその Save メソッドを呼び出すことができるためですが、その場合にはこの実装をスタブと判断することも可能です。

ダミーとスタブのいずれかに対して厳格な定義を提案することもできますが、その名称を除けば、このようなあいまいさによって実質的な影響が発生することはほとんどありません。これによって、テスト代替は明確なタイプに区分されるものではなく、それぞれが連続性の中に位置付けられるというこの記事の主調を分かりやすく説明できます。このケースでは、新しいクラスである StubShopDataAccess を呼び出すことにします。これは、この後で他の一部のメンバに対してさらなる実装の追加を予定しているためです。

StubShopDataAccess を使用して、次のように単体テストを記述します。

[TestMethod]
public void SaveOrder()
{
    StubShopDataAccess dataAccess = new StubShopDataAccess();

    Order o = new Order(3, dataAccess);
    o.Lines.Add(1234, 1);
    o.Lines.Add(4321, 3);

    o.Save();
}

Order クラスで Save を呼び出すと、StubShopDataAccess の Save メソッドが呼び出されますが、これは一切応答しません。したがって、エラーが発生しないという単純な理由によってテストは成功するものの、テスト ターゲットの検証は行われません。

テストにスパイを追加する

この記事のテーマはテスト代替の検証ですが、単体テストの目的はテスト ターゲット (この場合は Order クラス) が目的どおりに動作することの実証であることを念頭に置いてください。Save メソッドの場合には、このメソッドが IShopDataAccess インターフェイスの Save メソッドを呼び出すことが目的となりますが、定義済みのテストはその検証を行いません。これは、Order.Save が実装なしの場合、すなわち、IShopDataAccess.Save を呼び出さない場合にも、テストが成功するためです。

Save メソッドが呼び出されたことを検証するには、テスト代替がその呼び出しの有無を記録する必要があります。検証のためにメンバの呼び出しを記録することは、テスト スパイ特有の重要な特徴であることから、ここでは図 4 のように、新しいクラスを作成してこれを SpyShopDataAccess とします。

Figure 4 スパイ クラス (Close Figure 4)
internal class SpyShopDataAccess : IShopDataAccess
{
    private bool saveWasInvoked_;

    #region IShopDataAccess Members

    // Other IShopDataAccess members ommitted for brevity

    public void Save(int orderId, Order o)
    {
        this.saveWasInvoked_ = true;
    }

    #endregion

    internal bool SaveWasInvoked
    {
        get { return this.saveWasInvoked_; }
    }
}

この場合も、テスト スパイのタイプがテスト代替の連続性において明確な範囲区分を持たない点を指摘しておきます。その上で、IShopDataAccess インターフェイスの他のメソッドが引き続き例外をスローする状況を考えてみます。その場合、これはダミーとスパイの混合となるでしょうか。あるいは、他のメソッドがハードコードされた戻り値を返すものの、呼び出しを記録しないとすれば、これをスタブとみなすこともできます。

すべてのメンバの呼び出しの記録についても、さまざまな方法があります。図 4 では、フラグを設定した最も単純なアプローチを使用していますが、次のように呼び出し回数を記録することもできます。

private int saveInvocationCount_;

public void Save(int orderId, Order o)
{
    this.saveInvocationCount_++;
}

internal int SaveInvocationCount
{
    get { return this.saveInvocationCount_; }
}

この実装は最初の実装よりも明らかに多くの情報をキャプチャしますが、そのことをもってこのスパイが他のスパイよりも強力であると言えるでしょうか。すべての呼び出しの入力パラメータを記録することによって、より強力なスパイを作成できますが、そのためには実装するコードを増やす必要があります。またこれによって、テスト代替の連続性におけるスパイの位置が "完全な実装" 側に寄ることになります。スタブとスパイの境界はあいまいであり、テスト代替を連続性における位置付けで把握することの妥当性を、ここで再度指摘しておきます。

SpyShopDataAccess クラスにより、Order が実際に保存されていることを検証するための単体テストを次のように記述できます。

[TestMethod]
public void SaveOrderWithDataAccessVerification()
{
    SpyShopDataAccess dataAccess = new SpyShopDataAccess();

    Order o = new Order(4, dataAccess);
    o.Lines.Add(1234, 1);
    o.Lines.Add(4321, 3);
    o.Save();

    Assert.IsTrue(dataAccess.SaveWasInvoked);
}

この極めて単純なテストは、Save メソッドが起動されたことのみを検証し、その回数や使用されたパラメータについては検証しません。このタイプのデータの検証が有効であることも少なくありませんが、そのためには高度なテストスパイの記述やモックの使用によって、すべての呼び出しをデフォルトで記録する必要があります。

単体テストのモックを作成する

モックは、ダミー、スタブ、スパイ、およびフェイクとは大きく異なり、必ずテスト開発者によって作成されます。モックは、実行時に作成される動的なモック オブジェクトでのメソッドの呼び出しによって作成されます。

2004 年 10 月の MSDN Magazine に掲載された私の記事である「単体テスト: 困ったときの Mock オブジェクト - NMock を使用した .NET コードのテスト」 (msdn.microsoft.com/msdnmag/issues/04/10/NMock) では、動的モックの基礎と NMock と呼ばれる特殊なモック ライブラリの使用方法について説明しました。ここでは、Rhino Mocks (ayende.com/projects/rhino-mocks.aspx で入手可能) (英語) という別の無料モック ライブラリを使用します。

IShopDataAccess インターフェイスを実装する新しいコード ファイルを作成する代わりに、図 5 のように、Rhino Mocks に指示して実行時にインターフェイスを実装するオブジェクトを作成します。モックが目的どおりに動作するためには、インターフェイスでの呼び出し対象となるメンバについて期待値を設定する必要があります。モック ライブラリはこれを別の方法で設定しますが、Rhino Mocks は期待値の記録から設定できます。基本的に、モックには 2 つのモードがあります。すなわち、単体テストが期待値を定義する記録モードと、クライアントがモックでメンバを呼び出すリプレイ モードです。

Figure 5 動的モック (Close Figure 5)
[TestMethod]
public void SaveOrderAndVerifyExpectations()
{
    MockRepository mocks = new MockRepository();
    IShopDataAccess dataAccess = mocks.CreateMock<IShopDataAccess>();

    Order o = new Order(6, dataAccess);
    o.Lines.Add(1234, 1);
    o.Lines.Add(4321, 3);

    // Record expectations
    dataAccess.Save(6, o);

    // Start replay of recorded expectations
    mocks.ReplayAll();

    o.Save();

    mocks.VerifyAll();
}

図 5 の例では、期待値の対象が Save メソッドへの 1 つの呼び出しに限られるため、期待値は簡単に設定できます。すべての期待値の記録の終了後、ReplayAll を呼び出してモックをリプレイ モードに切り替えます。

Order オブジェクトで Save を呼び出すと、これによってモックの Save メソッドが呼び出されます。モックはリプレイ モードであることから、期待されるパラメータによってこのメソッドが呼び出されたことを記録します。Save が呼び出されたものの、別のパラメータが使用された場合には、モックは例外をスローします。最後に VerifyAll を呼び出すと、モックで Save メソッドが呼び出されたことが確認されます。これ以外の場合には、例外がスローされます。

モックは期待される呼び出しが行われたことを検証するため、その動作はテスト スパイに類似します。しかし、モックは実行時にのみ存在するため、テスト代替の連続性におけるモックの位置付けを確定することは困難です。これが、この連続性においてモックが極めて広範囲に位置付けられるとみなすことを妥当と考える理由です。

値を返す

ここまでは、一切の値を返さない 1 つのメソッド (Save) の代替のみについて説明してきました。既に説明したように、void を返すメソッドのスタブは簡単に実装できます。値を返すメソッドの場合には、状況はやや複雑になります。ここでは Order サンプルを拡張して、値を返す方法について説明します。

Order クラスには、さまざまな OrderLine オブジェクトが含まれます。各 OrderLine オブジェクトは、製品 ID、数量、およびこれが属する Order への参照を含んでいます。また、各行の合計を算出するために、OrderLine クラスには次の Total プロパティが含まれています。

private decimal? total_;

public decimal Total
{
    get
    {
        if (!this.total_.HasValue)
        {
            decimal unitPrice =
                this.owner_.DataAccess.GetProductPrice(
                    this.productId_);
            this.total_ = unitPrice * this.quantity_;
        }
        return this.total_.Value;
    }
}

この実装が IShopDataAccess の GetProductPrice を呼び出すことに注意してください。つまり、Total プロパティをテストするには、使用されるすべてのテスト代替が必ずGetProductPrice から値を返さなければなりません。これを最も簡単に実行するには、次のように、ハードコードされた値を返します。

public decimal GetProductPrice(int productId)
{
    return 25;
}

StubShopDataAccess でのこの実装により、次のような単体テストを記述できます。

[TestMethod]
public void CalculateSingleLineTotal()
{
    StubShopDataAccess dataAccess = new StubShopDataAccess();

    Order o = new Order(7, dataAccess);
    o.Lines.Add(1234, 2);

    decimal lineTotal = o.Lines[0].Total;
    Assert.AreEqual<decimal>(50, lineTotal);
}

最初の注文行では製品 1234 の数量が 2 であり、StubShopDataAccess によって返される単位価格が 25 であるため、この行の合計は 50 になります。

この実装は極めて単純ですが、各製品の単位価格が異なるために複数の注文行が存在する Order をテストできないため、その柔軟性は高いとは言えません。1 つの解決策として、GetProductPrice の実装を次のように変更することが考えられます。

public decimal GetProductPrice(int productId)
{
    switch (productId)
    {
        case 1234:
            return 25;
        case 2345:
            return 10;
        default:
            throw new ArgumentException("Unexpected productId");
    }
}

テスト代替にどのように条件付きロジックを含めるかによって、実装が複雑になることに注意してください。ハードコードされた値を返す一方で条件付きロジックを含むテスト代替が、なおスタブに該当するのかについて疑問が生じる場合もあります。

基本的に、任意の数の case ステートメントを追加できますが、前述の単体テスト サンプルでお分かりのように、テスト コードだけから見ると、スタブが返す値は明確ではありません。

フェイクを作成する

さらに構成可能なテスト代替の作成が有効な場合もあります。IShopDataAccess の場合、このインターフェイスは実際にはデータベースの抽象化であることから、1 つのアプローチによって基本的なメモリ内データベースが作成されます。図 6 の FakeShopDataAccess クラスは、参照整合性などの一般的にデータベースに求められる機能を備えていないことから、原始的なデータベースとなります。これには、基本的にデータベース テーブルの代替として機能するコレクション オブジェクトが含まれます。製品を製品 ID でインデックス化する必要があるため、ここでは KeyedCollection<int, Product> から単純に派生する ProductCollection クラスを作成しました (図では表示していません)。この Product クラスは基本的に製品データのプロパティを収集したものであり、これだけを目的として作成された、別のテスト専用クラスです。

Figure 6 基本のテスト データベース (Close Figure 6)
internal class FakeShopDataAccess : IShopDataAccess
{
    private ProductCollection products_;

    internal FakeShopDataAccess()
    {
        this.products_ = new ProductCollection();
    }

    #region IShopDataAccess Members

    public decimal GetProductPrice(int productId)
    {
        if (this.products_.Contains(productId))
        {
            return this.products_[productId].UnitPrice;
        }
        throw new ArgumentOutOfRangeException("productId");
    }

    public void Save(int orderId, Order o) { }

    #endregion

    internal IList<Product> Products
    {
        get { return this.products_; }
    }
}

FakeShopDataAccess がその Save メソッドに渡された注文を保存しないことに気付かれるかもしれません。これは、IShopDataAccess インターフェイスには保存された注文を返すメンバが含まれないために、故意にこの機能を省略した結果です。このクラスを Save メソッドの検証に使用する場合には、Order オブジェクトを内部辞書に追加して、その辞書のコンテンツを検証することもできます。この場合には、テスト スパイの機能をフェイクに追加することになるため、これもまた、テスト代替のタイプ間の境界があいまいであることを示す証拠となります。

FakeShopDataAccess クラスでは、図 7 で確認できるように、データ アクセス層での処理が明確になります。テスト ターゲット (Order オブジェクト) の作成前に、製品とその単位価格のリストを作成および設定します。テストを実行すると、GetProductPrice はこのリストに含まれるリクエスト対象製品の単位価格を返します。

Figure 7 フェイク データベースによるテスト (Close Figure 7)
[TestMethod]
public void CalculateLineTotalsUsingFake()
{
    FakeShopDataAccess dataAccess = new FakeShopDataAccess();
    dataAccess.Products.Add(new Product(1234, 45));
    dataAccess.Products.Add(new Product(2345, 15));

    Order o = new Order(9, dataAccess);
    o.Lines.Add(1234, 3);
    o.Lines.Add(2345, 2);

    Assert.AreEqual<decimal>(135, o.Lines[0].Total);
    Assert.AreEqual<decimal>(30, o.Lines[1].Total);
}

このテストが、スタブを使用する同種のテストよりも判読しやすいことに注意してください。つまりこのテストでは、スタブ コードに移動することなく、返される値を確認できます。フェイクの作成のデメリットは、作成に手数がかかる場合がある点です。FakeShopDataAccess の場合には、最終的に FakeShopDataAccess、ProductCollection クラス、そして Product クラスの 3 つのクラスを作成しました。IShopDataAccess は極めて単純なインターフェイスですが、より複雑なインターフェイスの場合には、このデメリットが大きな意味を持つようになります。

手動モック

単体テストの重要原則の 1 つは、極めて単純なコードの記述による複雑なコードのテストです。複雑なフェイクの作成は、この原則に反するとも言えます。フェイク自体の単体テストを検討するとすれば、それはフェイクが複雑すぎることを意味する可能性もあります。

この難問に対する最終回答が動的モックとなりますが、必ずしも完全なモック ライブラリを使用する必要はありません。モック ライブラリはテスト プロジェクトへの参照を必要とする場合の特別なアセンブリであり、チームベースのプロジェクトで作業している場合には、共通のモック ライブラリの標準化という問題を解決して、これをすべての開発者のマシンにインストール (または、そのバイナリをソース管理下に配置) し、アプリケーションの展開に応じてその他のすべての詳細情報をマシンに配置する必要があります。そのため、このような場合には、手動モックと名付けたテスト代替の迅速な作成が妥当と考えます。

フェイクを使用せずに図 7 のテストを書き直すと同時に、テスト代替の透過性を維持する場合について考えてみます。このテストの唯一の対象である IShopDataAccess I メンバが GetProductPrice メソッドであることから、図 8 の手動モック クラスを作成して、この特殊なメソッドを処理します。コンストラクタは Converter<int, decimal> を受け取ることに注意してください。コンストラクタはこれを保存して GetProductPrice メソッドの実装に使用します。

Figure 8 手動モック クラス (Close Figure 8)
internal class ProductPriceMockShopDataAccess : IShopDataAccess
{
    private Converter<int, decimal> implement_;

    internal ProductPriceMockShopDataAccess(
        Converter<int, decimal> productPriceCallback)
    {
        this.implement_ = productPriceCallback;
    }

    #region IShopDataAccess Members

    public decimal GetProductPrice(int productId)
    {
        return this.implement_(productId);
    }

    public void Save(int orderId, Order o)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Converter<int, decimal> は、int を入力として受け取り、decimal を返すデリゲートです。IShopDataAccess の定義により、これが GetProductPrice と同じシグネチャであることが分かります。つまり、Converter<int, decimal> を使用して GetProductPrice を実装することが可能であり、これがまさに図 8 での作業です。

この手動モックには実装そのものは含まれません。これは、図 9 で示すように、単体テストが使用する基盤となります。この単体テストでは、GetProductPrice method の呼び出しによって呼び出される匿名のメソッドを使用して、モックを初期化します。これは基本的に動的モックの構成方法と同様で、初めにモックの期待値を定義し、次にテスト ターゲットを作成してこれを実行します。

Figure 9 手動モックの使用 (Close Figure 9)
[TestMethod]
public void CalculateLineTotalsUsingDelegate()
{
    ProductPriceMockShopDataAccess dataAccess =
        new ProductPriceMockShopDataAccess(delegate(int productId)
    {
        switch (productId)
        {
            case 1234:
                return 45;
            case 2345:
                return 15;
            default:
                throw new ArgumentOutOfRangeException("productId");
        }
    });

    Order o = new Order(10, dataAccess);
    o.Lines.Add(1234, 3);
    o.Lines.Add(2345, 2);

    Assert.AreEqual<decimal>(135, o.Lines[0].Total);
    Assert.AreEqual<decimal>(0, o.Lines[1].Total);
}

図 9 の単体テストは、図 7 のフェイクを使用するテストよりも複雑に見えますが、これは錯覚です。確かにテスト処理自体は詳細になりますが、テストをサポートするコードの全体サイズは小さくなります。これは、フェイクの場合には 3 つのサポート対象クラス (フェイク自体を含む) が必要になりますが、手動モックで必要となるのはモックそのものだけに限られるためです。

モックにデリゲートを使用するメリットとしては、テスト開発者が各単体テスト用に新しい実装を記述する点が挙げられます。これによって戻り値や動作を変更できるだけでなく、実行するスパイのレベルを決定できます。値を返すだけの場合には、図 9 のようなコードを使用しますが、事後検証のためにメソッドの呼び出しを記録する場合でも、匿名メソッドによって外部変数へのアクセスが可能になるため、簡単にこれを実現できます。

図 8 の ProductPriceMockShopDataAccess クラスの主なデメリットは、これが GetProductPrice メソッドだけをモックの対象とし、他のメンバについては NotImplementedException をスローする点です。当然のことながら、このアプローチを汎用化することによって、手動モックの概念を深めることができます。IShopDataAccess に対する汎用手動モックで実行可能なことの 1 つとしてインターフェイスのずべてのメンバに対する Converter<TInput, TOutput> の定義が挙げられますが、インターフェイスに多数のメンバが存在する場合には、これが煩雑な作業となる可能性もあります。

次のサンプルのように、あらゆるメンバの実装に使用可能なデリゲートを定義するアプローチの方が効果的と考えられます。

public delegate void ImplementationCallback(MemberData member);

このデリゲートは単純に、void を返し、入力として MemberData オブジェクトを受け取るメソッドを定義します。この MemberData クラスは簡単であるため説明は省略しますが、これについてはこの記事のコード ダウンロードに含まれています。これは基本的にプロパティを収集したものであり、呼び出し対象メンバ、パラメータ値のリスト、そして実装によって提供する必要がある戻り値の設定に使用されるプロパティが含まれます。すべてのメソッドが戻り値を必要とするわけではない (Save メソッドが void を返すなど) ため、この戻り値はオプションです。

ImplementationCallback デリゲートにより、図 10 に示すような汎用性の高い手動モックを作成できます。既に説明したように、コンストラクタを使用して、実装を含むデリゲートをモックに設定します。各メンバの実装は、次の共通パターンに従います。すなわち、最初に MemberData オブジェクトを構築し、呼び出し対象のメソッド名とすべてのパラメータの名前および値を含めます。次に、デリゲートの呼び出し時に、メンバ変数をパラメータとして使用します。メソッドが値を返す場合には、その値はメンバ変数から抽出して返されます。実装するデリゲートによって、ReturnValue プロパティが設定されます。

Figure 10 汎用手動モック (Close Figure 10)
internal class MockShopDataAccess : IShopDataAccess
{
    private ImplementationCallback implement_;

    internal MockShopDataAccess(ImplementationCallback callback)
    {
        this.implement_ = callback;
    }

    #region IShopDataAccess Members

    public decimal GetProductPrice(int productId)
    {
        MemberData member = new MemberData("GetProductPrice");
        member.Parameters.Add(new ParameterData("productId", productId));

        this.implement_(member);

        return (decimal)member.ReturnValue;
    }

    public void Save(int orderId, Order o)
    {
        MemberData member = new MemberData("Save");
        member.Parameters.Add(new ParameterData("orderId", orderId));
        member.Parameters.Add(new ParameterData("o", o));

        this.implement_(member);
    }

    #endregion
}

図 11 のように、このアプローチを使用するテストは前述のテストに似ています。MemberData には呼び出されたメンバについての情報が含まれるため、期待されるメソッドへの呼び出しの有無を初めに確認し、これに該当しない場合には例外をスローするアプローチが実際的です。呼び出し対象のメソッドが GetProductPrice メソッドの場合、次にテスト コードが、関連する戻り値を MemberData 入力パラメータに設定します。この値は、MockShopDataAccess がその呼び出し元に返す値です。

Figure 11 汎用モックの使用 (Close Figure 11)
[TestMethod]
public void CalculateLineTotalsUsingManualMock()
{
    MockShopDataAccess dataAccess =
        new MockShopDataAccess(delegate(MemberData member)
    {
        if (member.Name == "GetProductPrice")
        {
            int productId = (int)member.Parameters["productId"].Value;
            switch (productId)
            {
                case 1234:
                    member.ReturnValue = 45m;
                    break;
                case 2345:
                    member.ReturnValue = 15m;
                    break;
                default:
                    throw new ArgumentOutOfRangeException(
                        "productId");
            }
        }
        else
        {
            throw new InvalidOperationException("Unexpected member");
        }
    });

    Order o = new Order(11, dataAccess);
    o.Lines.Add(1234, 3);
    o.Lines.Add(2345, 2);

    Assert.AreEqual<decimal>(135, o.Lines[0].Total);
    Assert.AreEqual<decimal>(30, o.Lines[1].Total);
}

汎用手動モックの作成が次のような反復可能なパターンに従っていることに気が付かれたかもしれません。つまり、最初にタイプとそのメンバを作成し、次に各メンバで MemberData オブジェクトを作成してデリゲートを呼び出し、該当する場合には ReturnValue プロパティを返すというパターンです。この実装パターンは自動化できるため、リフレクションおよび CodeDOM ロジックを使用して、正確に同一の動作を行う実行時オブジェクトを作成および開始できます。これはこの記事の趣旨からは逸脱しますが、このアプローチを説明するための基本的な概念実証がコード ダウンロードに含まれています。これで、DelegateMock クラスはデリゲートベースのモックをその場で作成できます。この単体テストは、下記に示すとおり、モックの作成行を除いて図 11 のテストとほとんど同一です。

IShopDataAccess dataAccess =
    DelegateMock.Create<IShopDataAccess>(
        delegate(MemberData member){...});

通常のモックと同様に、この手動モックは動的に作成されて、実行時にのみ存在します。現実に、DelegateMock と動的モックとを比較すると、モック対象のインターフェイスや基本クラスの実装方法だけが異なります。

デリゲートベース モックのメリットは、これによって提供される高度な自由性とその理解の容易さにあります。動的モックの理解や習得を難しいと感じる開発者は少なくありませんが、デリゲートとそのコードの記述方法を習得している開発者は多数にのぼります。期待値、戻り値、呼び出しの記録などのすべてを明示的にコードに記述する必要があることから、大部分の開発者はその手順を熟知しており、新たなオブジェクト モデルを学習する必要はありません。

実際面でのモック

手動モックにはさまざまなメリットがある一方で、デメリットも存在します。その 1 つは、各単体テストにおけるデリゲートベースの実装の設定が煩雑な作業となることです。特に、テスト スパイのように、呼び出しの記録が必要な場合にこれが顕著になります。しかし、最大のデメリットは、実装としてのデリゲートの記述が煩雑かつ誤りの多い作業となることです。

即効性のあるモックとしては手動モックが便利ではあるものの、個人的には動的モックの方が好ましいと考えています。優れたモック ライブラリによって、構成が容易であるだけでなくテスト スパイとしても機能するモックを作成できます。

この記事で紹介した行合計のサンプルは、Rhino Mocks を使用すると極めて簡単に記述できます。次のように、モック自体を 2 行のコードで作成でき、サポート クラスは不要です。

MockRepository mocks = new MockRepository();
IShopDataAccess dataAccess = mocks.CreateMock<IShopDataAccess>();

これで、dataAccess 変数を使用して、前述のサンプルと同様に新しい Order インスタンスを作成できます。モックは記録モードで作成されるため、期待値と戻り値は次のようにわずか 2 行で定義できます。

Expect.Call(dataAccess.GetProductPrice(1234)).Return(45m);
Expect.Call(dataAccess.GetProductPrice(2345)).Return(15m);

1 行目では、モックが値 1234 の GetProducPrice メソッドへの呼び出しを期待し、この呼び出しに応じて 45 を返すことを単純に記述しています。2 行目では、製品 ID 2345 についての同様の期待値を明確に設定しています。

デフォルトでは、モックはテスト スパイのように動作するため、追加の作業をすることなく、テストの最後に mocks.VerifyAll を呼び出し、すべての期待値が行われたことを確認できます。

タイプを比較する

図 12 は、さまざまなタイプのテスト代替とそのメリットおよびデメリットをまとめたものです。この表ではそれぞれのタイプが明確に分類されるように表記していますが、この記事はあくまでも、その境界があいまいであり、すべてのテスト代替が連続性の中で位置付けられることを説明することを目的としています。この区別は主にセマンティック値に起因するとも言えますが、個人的には、抽象概念についての理解は使用される用語によって決まるため、該当する事象を可能な限り正確に説明することが重要であると確信しています。

Figure 12 テスト代替のメリットとデメリット (Close Figure 12)

テスト代替のタイプ | メリット | デメリット
ダミー | 作成が極めて容易。 | 有効性が低い。
スタブ | 作成が容易。 | 柔軟性に限界がある。単体テストの観点からは透過的ではない。メンバが正しく呼び出されたことを検証できない。
スパイ | メンバが正しく呼び出されたことを検証できる。 | 柔軟性に限界がある。単体テストの観点からは透過的ではない。
フェイク | 各種のシナリオに使用できるほぼ完全な実装を提供。 | 作成が困難。複雑になることから、フェイク自体の単体テストを要する場合がある。
モック | テスト代替を効率的に作成できる。メンバが正しく呼び出されたことを検証できる。単体テストの観点からは透過的である。 | 学習曲線の勾配が急になる。

私の考えでは、優れた動的モック ライブラリは、少なくともほとんどの状況において、ダミー、スタブ、スパイの代わりに使用できます。極めて複雑なインターフェイスや基本クラスに対してテスト代替を作成する必要がある場合には、モックよりもフェイクの方が有効に機能することも少なくありません。

モックでは、各テストについて明確に定義されたすべての期待値を設定する必要があります。テスト ターゲットとテスト代替間での対話が増えるため、多数の期待値の定義に関連する作業と実行可能なフェイクの記述に要する作業とを比較すると、前者の作業の方がはるかに多くの時間と労力を要する可能性があります。モックをフェイクに変更するポイントは、残念ながら理論よりも感覚で判断することになります。

Mark Seemann は、デンマークのコペンハーゲンの Microsoft Services に勤務し、マイクロソフトの顧客やパートナーによるエンタープライズレベル アプリケーションの立案、設計、および開発に対するサポート業務に従事しています。Mark には blogs.msdn.com/ploeh (英語) のブログを介して連絡できます。


編集者のメモ: ニューヨークの夏

編集者のメモ
ニューヨークの夏
Joshua Trupin

まだ 6 月だというのに、 2007 年 9 月号を仕上げているとこですが、外は炎天下です。まるで気温が 1 億度あるかのような暑さです。ニューヨークは今週「蒸発警報」発令中で、敢えて街に出た人々は緩んだアスファルトにくるぶしまで沈んでいます。火を噴きそうな勢いで走り回るタクシーのライトが、オゾンだらけの空を照らし出しています。そう、これがいつものニューヨークの夏です。こんな日々を過ごしている人々にとって、アウトドアがすばらしいなんて考えられません。だから、屋内にとどまってエアコンの設定を強くして (地球温暖化なんて少なくともあと 3 年は先の話)、今月号の MSDN Magazine を楽しむことにしましょう。

という訳で屋内で過ごさざるを得ない時間を有効に活用して、今月号に載せた Web 2.0 テクノロジの中核的な情報を集めました。バスで隣に乗り合わせた人が AJAX テクノロジについて話しているのを聞いたことはありませんか。もっと知りたいと思ったでしょう。今月は、ScriptManager (ASP.NET AJAX の中核) を徹底的に検討します。その名前が示しているように、これはスクリプトを管理するコンポーネントです。内部の動作を本当に理解したいのであれば、ここから始めることです。

アプリケーションの心臓部から一歩下がって、Dino Esposito の Cutting Edge コラムでは、AJAX アプリケーション アーキテクチャについて検討する 2 部構成のシリーズが始まりました。AJAX は Web 2.0 アプリケーションの中核なので、今まさに飛び込んだばかりの人にとってはこのシリーズは重要な読み物です。

ある程度の期間 ASP.NET プラットフォームでアプリケーションを構築してきた人なら、検索によってサイトを拡張する方法を知りたいでしょう。Marco Bellinaso は、ASP.NET コードを検索エンジン (Live Search や SharePoint が提供するものなど) に組み込んで、高度な検索機能を簡単に実装する方法を説明しています。

この Web 関連の機能の他に、Microsoft .NET Framework 3.5 のネットワーク名前空間についての記事も入手しました。今のところ、命名規則の責任者がだれかは正確にはわかりませんが、この名前空間は System.Net と呼ばれています。.NET Framework の中の何かを「○○.Net」と呼ぶのは、ちょっと混乱しますね。私たちは特に専門家というわけではありませんが、System.Networking のほうがわかりやすくはないでしょうか。こんなことを考えるのは、この圧倒的な暑さにやられているからかもしれません。

「Web 2.0」という名前も、どうにかならなかったものでしょうか。もっとましな名前をだれか思いつかなかったのでしょうか。Web 1.1、とか? しかしまあ、もうこの方向に進み始めてしまったということなのでしょうね。

聞き慣れない Web 2.0 のように何か違和感はありますが、MSDN Magazine のサイト (msdn.microsoft.com/msdnmag) では拡張された Web 機能を実際によく使っています。私たちの画期的な Code Explorer 機能には気付かなかったかもしれませんが、新しいコード サンプルはすべて、これを使っています。サンプル全体をダウンロードする代わりに、サイト上で ZIP ファイル内をナビゲートして必要な個々のファイルを検索し、クリックすると、表示することもできます。

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

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

どうしてこんな面倒なことをしたか、疑問ですか? これまで以上に、MSDN Magazine を使いやすくしたいからです。それに、これも屋内でできる作業でしたからね。炎天下に外出したい人はいないでしょう?£J.T.

Thanks to Paul Andrew、Mason Bendixen、Naren Datha、Shawn Farkas、Kit George、Matt Gibbs、Ladi Prosek、Cosmin Radu、Karthik Raman、Varun Sekhri、Richard Turner、および Danny van Velzen です。


ツールボックス: ASP.NET コントロール、FTP、その他の機能

ツールボックス
ASP.NET コントロール、FTP、その他の機能
Scott Mitchell

コンテンツ
ASP.NET アプリケーションを魅力的にする
使いやすい包括的な FTP ライブラリ
イメージ処理に関する充実したサポート
推薦図書

ASP.NET アプリケーションを魅力的にする

ASP.NET アプリケーションの大半は、インターネットでアクセスできるデータ入力フォームにすぎません。このため開発者は、重要ではあっても面白味のない仕事 (レポート作成、データ入力フォームのデザイン、入力チェックなど) にほとんどの時間を費やすことになるのが普通です。サードパーティ製のコントロール スイートで、通常はグリッド コントロールが主力製品になっていることを見ても、やはりそれが「普通」のようです。

アプリケーションの本質が最優先ではありますが、直感的で美しいユーザー インターフェイスの重要性を軽視するべきではありません。Developer Express の ASPxperience Suite 1.1 にはグリッド コンポーネントは含まれていませんが、興味深く魅力的な方法で情報を提示することによって Web アプリケーションの美しさと使いやすさを拡張する、18 個の Web コントロールが含まれています。

このスイートを構成する 18 個のコントロールは、ナビゲーション、レイアウト、およびアプリケーション フロー コントロールに特化されています。たとえば、ASPxDataView コントロールは表形式のデータを列と行で表示します。ASP.NET のブロック式の GridView コントロールとの違いは、ASPxDataView コントロールは、複数の列や行にデータを展開したり、詳細なカスタマイズ化に対応したりできるので、より高度なページング インターフェイスを提供できることです。ASPxSiteMapControl を使用すると、さまざまな方法で Web サイトのナビゲーション構造を美しく表示できます。表示方法は、プログラムや、階層的データ ソース (XmlDataSource や SiteMapDataSource など) から指定できます。

ASPxperience Suite で筆者が最も感銘を受けたコントロールは、ASPxCloudControl と ASPxTimer です。ASPxCloudControl は、タグ クラウドを表示します。タグ クラウドは、重み付けリストを簡潔に視覚的に表現するものです。タグ クラウドのそれぞれのアイテムには何らかの値が関連付けられており、他のアイテムに比べて値が大きいアイテムのテキストは大きく表示されます。このインターフェイスによって、サイトにアクセスした人は、あるアイテムの値が他と比較してどの程度大きいか、または小さいかを簡単に把握できます。ASPxTimer コントロールを使用すると、クライアントに表示するタイマ コンポーネントを、タイマ関連の JavaScript コードをまったく書かずに作成できます。そのうえ、表示されるタイマ コンポーネントに、クライアント側でタイマの開始と停止や、tick イベントへの応答ができる API を含めることができます。

これらのコントロールの外観は高度なカスタマイズが可能で、プログラム、または書式設定ウィザードでの宣言によって、構成できます。さらに、ASPxperience Suite コントロールの多くは、AJAX テクノロジを使用しているか、使用するように構成可能なので、ユーザー エクスペリエンスを向上させることができます。Internet Explorer®、Firefox、Netscape、Opera、Safari など、すべての主要ブラウザがサポートされています。

価格: 1 開発者ライセンス - 249 ドル、ソース コードを含むライセンス - 299 ドル。

www.devexpress.com (英語)

直感的な ASPxCloudControl (画像を縮小するには、ここをクリックします)

直感的な ASPxCloudControl (画像を拡大するには、ここをクリックします)

使いやすい包括的な FTP ライブラリ

FTP (File Transfer Protocol) は、よく知られているオープン プロトコルで、サーバーとクライアント間のネットワーク経由でのファイル交換に使用されます。これは多くのサーバーおよびクライアントに実装されています。たとえば、Microsoft® Internet Information Services (IIS) には FTP サービスが含まれます。他にも多数の無料または商用のスタンドアロン FTP クライアント アプリケーションがあります。ご使用の Web ブラウザも、FTP クライアントとして機能させることもできます。

FTP クライアント アプリケーションが多数あることは、エンド ユーザーにとって有益です。一方、開発者の関心は、プログラム可能なインターフェイスによる FTP との通信にあります。Microsoft .NET 2.0 Framework には、FTP サーバーとの間でファイルを送信および受信するためのクラスが含まれています (System.Net 名前空間の FtpWebRequest クラスおよび FtpWebResponse クラスを参照)。.NET Framework に含まれない機能を付加したサードパーティ製の FTP ライブラリもあります。Rebex の FTP for .NET 2.0 はそのようなライブラリの 1 つで、.NET アプリケーションと .NET Compact Framework アプリケーションの両方をサポートし、FTP サーバーへの接続やファイルのアップロードとダウンロードを実行する、包括的な API を備えています。FTP for .NET では、同期操作と非同期操作、バイナリ ファイルと ASCII ファイルの転送、および転送関連以外の FTP 機能 (FTP サーバー上のファイルやフォルダの作成および削除、など) を実行できます。

FTP for .NET で最も気に入ったことは、API が簡潔で使いやすいことです。API の中核になっているのは、Ftp クラスです。FTP サーバーとの通信を開始するには、このクラスのインスタンスを作成してその Connect メソッドを呼び出し、FTP サーバー名とポート番号を渡すだけです。匿名のアクセスを許可しない FTP サーバーに対しては、資格情報を提供するための Login メソッドを使用します。ファイルのアップロードとダウンロードは、PutFile メソッドと GetFile メソッドを呼び出すだけです。FTP for .NET の他の機能も、同様に単純な API によって拡張できます。つまり、この製品によって、FTP に共通の注意を要するタスクを、数行の簡潔で読みやすいコードで処理できるのです。さらに、簡単に使用を開始できるように、FTP for .NET には、コマンドラインと Windows® Forms アプリケーションの両方でこのライブラリを使用した場合を示すサンプルも付属しています。

FTP は、すべての通信がプレーンテキスト形式で行われるため、必然的に安全ではありません。安全な通信のための代替 FTP 仕様もあります。FTP/SSL (FTP over SSL とも呼ばれます) と、SSH File Transfer Protocol (SFTP) の 2 つです。Rebex では、別の製品で FTP/SSL と SFTP の規格をサポートしています。

価格: 1 開発者ライセンス - 249 ドル、ソース コードを使用可能なライセンス - 999 ドル。

www.rebex.net/ftp.net (英語)

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

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

イメージ処理に関する充実したサポート

イメージの操作や、ユーザーによる編集を可能にするために必要なアプリケーションを作成したり使用したりしたことがあるなら、System.Drawing 名前空間のクラスにかなりの時間を費やしたことでしょう。残念ながら、.NET に組み込まれたイメージ処理クラスのインターフェイスはそれほど低レベルで、ごく簡単なタスクにも膨大な時間、労力、およびコードが必要になります。そのうえ、フレームワークに含まれているイメージ関連のインターフェイス コントロールは限られたものしかありません。それに対する良いニュースは、多くのサードパーティ製品で、複雑な GDI+ の代わりに強力で使いやすいイメージ処理 API を使用できることです。そのような製品の 1 つが Atalasoft の DotImage Photo Pro 5.0 で、堅牢なイメージ処理 API に加えて、Windows フォームや ASP.NET のアプリケーションに対応するイメージ処理コントロールが含まれています。

イメージの処理を開始するには、新しい Workspace オブジェクトを作成し、その Open メソッドを呼び出してイメージのパスまたは URL を渡すだけです。Workspace をロードしたら、ApplyCommand メソッドを呼び出してイメージ操作をいくつでも実行します。たとえば、影の追加、グレースケールの適用、クロッピング、透明度の調整、パレットの変更、サイズ変更、変形 (例: イメージのスキュー、回転、ページめくり) などの効果があります。この簡潔で単純な API を使用すると、コードが短く読みやすくなります。たとえば、イメージを開いてサムネイルを保存するのに必要なコードは、わずか 3 行です。

DotImage Photo Pro には、Web アプリケーションと Windows アプリケーションの両方に対応する UI イメージ処理コントロールも含まれます。ユーザーが Windows アプリケーションからイメージを操作できるようにするには、フォームに WorkspaceViewer コントロールを追加します。ユーザーがイメージの操作 (マウスの移動、イメージ上でのクリック、イメージでの領域の定義など) を行うと、そのコントロールによって対応するイベントが発生します。イメージを Web アプリケーションに表示するには、ASP.NET の組み込み Image コントローラ、または DotImage の WebImageViewer コントロールのどちらかを使用できます。イメージの表示以外に、WebImageViewer を使用して、イメージのオープン、保存、および操作の際に、AJAX 技術を活用して Web サーバーと効率的に通信する、機能豊富なイメージ編集インターフェイスを作成することもできます。

DotImage Photo Pro は、多くの一般的なイメージ コーデック (JPEG、PNG、BMP、TIFF、GIF、PCX、PSD など) を処理できます。この API は、拡張性を念頭において設計されており、独自のエンコーダまたはデコーダを作成して実行時にプラグインできるようになっています。また、バーコード認識、OCR (光学式文字認識)、高度な写真加工 (効果) などの機能を追加するアドオンもあり、Atalasoft の Web サイトで購入できます。

価格: 1 開発者ライセンス - 999 ドル

www.atalasoft.com (英語)

堅牢なイメージ処理 API を提供する DotImage Photo Pro
堅牢なイメージ処理 API を提供する DotImage Photo Pro (画像を縮小するには、ここをクリックします)
堅牢なイメージ処理 API を提供する DotImage Photo Pro
堅牢なイメージ処理 API を提供する DotImage Photo Pro (画像を拡大するには、ここをクリックします)

推薦図書

コンサルタントや教育という仕事を通して出会う人々の中には、所有する既存の基幹業務アプリケーションでは現在のニーズが満たされないという人が少なくありません。その理由には、アプリケーションを拡張できないことや、会社が製品内容やサービスを拡大したのでそれに対応するようにアプリケーションを更新する必要があることなどがあります。このような出会いがあると、マイクロソフトの開発ツールでは非常に簡単にデータベースとデータ駆動型アプリケーションを作成できることに、いつも驚きます。

データベースのスケーラビリティ、拡張性、および保守性は、設計の質に直接関係します。そのため、データ駆動型アプリケーションのデータ モデルを作成する開発者は、優れた設計の原則をしっかりと理解することが重要です。Louis Davidson、Kevin Kline、Kurt Windisch 著の『Pro SQL Server 2005 Database Design and Optimization』(2006 年、Apress 発行) (英語) は、そのような原則について紹介し、SQL Server™ 2005 にそれらの原則を実装する方法を説明しています。この書籍の前半部分では、エンティティ、リレーション、データ整合性、制約などのデータベース設計上の概念を説明しています。

後半部分では、データ モデル以外のデータベース設計を取り上げています。たとえば、第 7 章では、認証、権限、暗号化、監査などのセキュリティ関連のトピックについて説明しています。別の章では、クエリの最適化とインデックスの作成と使用に焦点を当てています。最も興味深かったのはアーキテクチャに関するコードレベルでの意思決定に関する章です。アドホックな SQL クエリとストアド プロシージャの比較や、T-SQL と CLR の間でのデータベース オブジェクト選択についての議論がありました。

データベース設計は、データ駆動型アプリケーションの作成には不可欠です。読者の専門分野にかかわりなく、『Pro SQL Server 2005 Database Design and Optimization』によって、データベースの設計スキルが向上し、よりよいアプリケーションを作成できると信じています。

価格: 59.99 ドル

www.apress.com (英語)

ご意見やご質問は、

Scott toolsmm@microsoft.com.

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

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

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


CLR 徹底解剖: COM 接続ポイント

CLR 徹底解剖
COM 接続ポイント
Thottam R. Sriram

コードのダウンロード : : CLRInsideOut2007_09.exe (252KB)
コンテンツ
サンプル シナリオ
COM サーバーで接続ポイントを作成する
クライアント
Add および ConnectionPoint インターフェイスを取得する
マネージ クライアント
まとめ

COM の典型的なシナリオでは、クライアント オブジェクトがサーバー オブジェクトをインスタンス化し、それらのオブジェクトを呼び出します。ただし、特別なメカニズムがなければ、それらのサーバー オブジェクトが向きを変えてクライアント オブジェクトにコールバックするのは非常に困難です。COM 接続ポイントは、このような特別なメカニズムを提供して、サーバーとクライアント間の双方向通信を可能にします。接続ポイントを使用して、サーバーは、特定のイベントがサーバーで発生したときにクライアントを呼び出すことができます。

接続ポイントがあれば、サーバーは、インターフェイスを定義することにより、発生させることができるイベントを指定します。これらのイベントがサーバーで発生するときに実行されるアクションが指定されているクライアントについては、クライアント自体をサーバーに登録します。その結果、クライアントは、サーバーによって定義されたインターフェイスの実装を提供します。

クライアントが自身をサーバーに登録できる標準メカニズムが用意されています。COM は、このための IConnectionPointContainer インターフェイスと IConnectionPoint インターフェイスを提供します。

COM 接続ポイント サーバーのクライアントは、C++ および C# マネージ コードで記述できます。C++ クライアントは、シンク インターフェイスの実装を提供するクラスのインスタンスを登録します。マネージ クライアントは、個別のイベントのデリゲートを登録して、イベント通知メソッドごとに 1 つのシンクを作成します。マネージ環境では、クライアントが自身を登録できる方法が 2 つあります。それぞれについて、このコラムの後半で詳しく説明します。

Web 上にはイベントおよび相互運用機能についての実用的なサンプルはほとんどありません。このコラムでは、Active Template Library (ATL) 接続ポイント サーバーの作成に重点を置いて説明します。これには、COM メソッドの公開、クライアントによって実装されるイベント インターフェイスの定義、およびサーバーからイベントを発生させるコードの実装が含まれます。また、シンクの実装を提供するサンプル C++ クライアントおよびサンプル C# クライアントを示し、さらに登録してサーバーからイベントをリッスンできる方法を 2 つ示します。最後に、マネージ イベント シンクの実装で推奨される方法について説明します。

サンプル シナリオ

このシナリオでは、サーバーは COM メソッドを公開します。

HRESULT Add(int nFirst, int nSecond)

また、サーバーは、ConnectionPointContainer および接続ポイントを定義して、クライアントの登録ができるようにします。さらに、サーバーは、2 つのメソッドを持つインターフェイス _IAddEvents を定義します。

HRESULT AdditionStarted()
HRESULT AdditionCompleted(int nResult)

クライアントは、_IAddEvents の実装を提供し、サーバーで Add メソッドを呼び出します。サーバーは、クライアントで AdditionStarted メソッドおよび AdditionCompleted メソッドを呼び出して、これを正しく通知します。次に、クライアントは、これらのイベントに関連付けられている適切なアクションを実行します。

COM サーバーで接続ポイントを作成する

2007 年 1 月の CLR 徹底解剖では、簡単な ATL COM サーバーの作成方法について詳しく説明しました (msdn.microsoft.com/msdnmag/issues/07/01/CLRInsideOut を参照してください)。今回のコラムでは、読者が ATLConnectionPointServer という名前の ATL COM サーバーを既に作成していることを前提としています。したがって、まだ作成していない場合は、前のコラムを先に読んでおくことをお勧めします。

さて、ここでは、サーバーによって実装される COM インターフェイスを定義し、それを接続ポイントにする必要があります。この COM サーバーの上部に接続ポイントを作成する方法は単純です。まず、Visual Studio® でクラス ビューを開き、簡単な ATL オブジェクトを作成します。後は、ATLConnectionPointServer を右クリックしてクラスを追加し、簡単な ATL オブジェクトを選択して、クラスの名前を Add にするだけです。ウィザードに従って操作するときは、[Supports: Connection Points](接続ポイントのサポート) を選択します。

これで、クライアントから呼び出し可能なサーバー インターフェイス IAdd ができました。サーバーを構築すると、ここで定義した 2 つのインターフェイスがあることに気付くでしょう。1 つは IDispatch を実装する IAdd、もう 1 つはdispinterface _IAddEvents です。

次に、インターフェイス IAdd に新しいメソッド Add を追加します。これは、2 つの整数を受け取り、HRESULT を返します。まず、[IAdd] を右クリックして、[Add Methods](メソッドの追加) を選択します。メソッドのシグネチャは次のようになります。

HRESULT Add([in] int nFirst, [in] int nSecond)

ここで、ATLConnectionPointServer.idl を開き、メソッド AdditionStarted および AdditionCompleted を _IAddEvents インターフェイスに追加します。これを図 1 に示します。

Figure 1 AdditionStarted および AdditionCompleted の追加 (Close Figure 1)
library ATLConnectionPointServerLib
{
    importlib("stdole2.tlb");
    [
        uuid(7F45FEA6-4D7C-489C-A852-19BA8B29D8AB),
        helpstring("_IAddEvents Interface")
    ]
    dispinterface _IAddEvents
    {
        properties:
        methods:
        [id(1), helpstring("AdditionStarted")]HRESULT AdditionStarted();
        [id(2), helpstring("AdditionStarted")]
            HRESULT AdditionCompleted(int nResult);
    };
    [
        uuid(15B6C26A-0416-4C8F-9533-89F318355E31),
        helpstring("Add Class")
    ]
    coclass Add
    {
        [default] interface IAdd;
        [default, source] dispinterface _IAddEvents;
    };
};

この時点でプロジェクトをコンパイルすると、_IAddEvents_CP.h という名前のファイルが自動生成されます。このファイルは、ATL によって生成されたもので、空の CProxy_IAddEvents クラスを含んでいます。これは、接続ポイントが完成してフックされたときにイベント起動を提供するクラスです。

クラス ビューを開き、[CAdd] を右クリックして、[追加] をポイントし、[接続ポイントの追加] をクリックします。次のウィザードで、[_IAddEvents] を選択します。ここで _IAddEvents_CP.h ファイルを開くと、2 つのメソッド (Fire_AdditionStarted および Fire_AdditionCompleted) の自動生成コードが含まれます。これは、クライアント シンク オブジェクトがサーバーに登録するときに、そのオブジェクトにコールバックするコードです。

サーバーの実装も完了に近づいてきました。残る作業は、Add メソッドのサーバーへの実装とサーバーからのイベント起動のトリガ ポイントの実装です。

Add.cpp を開き、追加した Add メソッドの実装を提供します。実装は、次のようになります。

STDMETHODIMP CAdd::Add(int nFirst, int nSecond)
{
    // Fire AdditionStarted event
    Fire_AdditionStarted();

    int nResult = nFirst + nSecond;
    Sleep(1000); // simulate the addition taking a long time

    // Fire AdditionCompleted event
    Fire_AdditionCompleted(nResult);
    return S_OK;
}

ここでソリューションをコンパイルするだけで、サーバーの準備は完了です。

クライアント

次に、クライアントに進みます。最初に C++ クライアント、続いてマネージ クライアントについて説明します。

クライアントは、5 つの主要なタスクを実行する必要があります。

  • _IAddEvents インターフェイスの実装を提供する。
  • サーバーの Add インターフェイスへのインターフェイス ポインタを提供する。
  • Add インターフェイスの ConnectionPoinContainer の ConnectionPoint を取得し、シンク インターフェイスを追加する。
  • Add メソッドを呼び出し、サーバーでイベントが発生するまでイベントを待機する。
  • 正常に閉じて終了する。

クライアントを実装するには、ConnectionPointClient という名前の新しい C++ プロジェクトを開き、そのプロジェクトに新しい C++ ソース ファイルを追加します。ATLConnectionPointServer.h ファイルと ATLBase.h ファイルをプロジェクトに追加します。シンクは、サーバーが定義した _IAddEvents を実装します。このインターフェイスには、AdditionStarted と AdditionCompleted の 2 つのメソッドがあります。これらの 2 つのメソッドの実装を図 2 に示します。

Figure 2 AdditionStarted および AdditionCompleted (Close Figure 2)
class CSink : _IAddEvents
{
    private:
    DWORD       m_dwRefCount;
    public:

    CSink::CSink() {m_dwRefCount = 0;}
    CSink::~CSink()    {}

    HRESULT STDMETHODCALLTYPE AdditionStarted()
    {
        printf("C++ SINK: Addition started event fired ... \n");
        return S_OK;
    };

    HRESULT STDMETHODCALLTYPE AdditionCompleted(int nResult)
    {
        printf("C++ SINK: Addition completed event fired ... \n");
        printf("C++ SINK: Addition result: %d \n",nResult);
        return S_OK;
    };
...

簡素化のため、dispinterface をクライアントに実装しておきました。サンプル コードは、これを自動的に行う ATL クライアントを提供しています。この実装のシンクは、それが呼び出されたという事実と、追加が完了したときの結果を出力するだけのものです。これでシンクが実装されたので、先に進みましょう。

Add および ConnectionPoint インターフェイスを取得する

シンクが実装されたので、このシンクをサーバーに登録するクライアントで作業を行います。クライアントは、次の 3 つの主要なタスクを処理します。

  • サーバーの Add インターフェイスへのインターフェイス ポインタを取得する。
  • Add インターフェイスから ConnectionPoinContainer の ConnectionPoint を取得する。
  • シンク インターフェイスをサーバーに登録する。

まず、次のようにサーバーへのインターフェイス IAdd を取得します。

CoInitialize(NULL);

hr = CoCreateInstance(
    CLSID_Add, NULL, CLSCTX_ALL, IID_IAdd, (void **)&pAdd);
if(hr != S_OK) { return; }

次に、シンク実装を登録できるように、サーバーの接続ポイントを取得する必要があります。このためには、次のように IAdd インターフェイスから ConnectionPoinContainer を取得します。

// Using the interface for add,
// query for IConnectionPointContainer interface
hr = pAdd->QueryInterface(
    IID_IConnectionPointContainer,(void **)&pCPC);
if ( !SUCCEEDED(hr) ) { return; }

これで ConnectionPoint に到達できます。

// Using the IConnectionPointContainer,
// get the IConnectionPoint interface
hr = pCPC->FindConnectionPoint(DIID__IAddEvents,&pCP);
if ( !SUCCEEDED(hr) ) { return; }

ここでクライアントは、そのシンク実装のインスタンスを作成し、それをサーバーに登録する必要があります。このためには、次のようにクライアントでシンクのインスタンスを作成し、その IUnknown インターフェイス ポインタを取得します。

// Create an instance of the sink object to pass
// to the server
pSink = new CSink();
if ( NULL == pSink ) { return; }

// Get the interface pointer to CSink's IUnknown pointer, which you
// will pass to the server
hr = pSink->QueryInterface (IID_IDispatch,(void **)&pSinkUnk);
if(!SUCCEEDED(hr)) { return; }

クライアントの処理も完了に近づいてきました。残る作業は、サーバーへのシンクの登録、サーバーの呼び出し、およびクリーンアップです。クライアントは、シンクのインスタンスをサーバーに登録します。

// Pass the sink interface to the server through the Advise
hr = pCP->Advise(pSinkUnk,&dwAdvise);
if(!SUCCEEDED(hr)) { return; }

これで、クライアント シンク インターフェイスをサーバーに登録できました。

クライアントは、サーバーの Add メソッドを呼び出し、そのメソッドで要求される 2 つの引数を渡します。追加の結果は、Add 呼び出しから直接返されるのではなく、AdditionCompleted イベントを通じて返されます。ここで、取得した IAdd インターフェイス ポインタで Add メソッドを呼び出します。

pAdd->Add(1, 5);

この呼び出しでイベント起動がトリガされ、それによってクライアントが呼び出されるはずです。この時点で、取得したインターフェイスをすべて解放することによってクライアントをクリーンアップできます (図 3 を参照してください)。

Figure 3 クライアントをクリーンアップする (Close Figure 3)
// Release the IConnectionPointContainer interface.
if(pCPC != NULL) pCPC->Release();

// Unadvise the event call back we registered.
if(pCP != NULL) { pCP->Unadvise(dwAdvise); }

if(pSinkUnk != NULL) { pSinkUnk->Release(); }

// Disconnect from the server.
if(pCP != NULL) { pCP->Release(); }

// Release interfaces.
if(pAdd != NULL) { pAdd->Release(); }

CoUninitialize();
return;

これでクライアントでの作業は終了です。クライアントをコンパイルして、実行できます。

cl COMConnectionPointClient.cpp

実行すると、次のような出力結果が得られます。

C++ SINK: Addition started event fired ...
C++ SINK: Addition completed event fired ...
C++ SINK: Addition result: 6

マネージ クライアント

今度は、同じ ConnectionPointServer をマネージ コードから使用する場合について説明します。マネージ クライアントは、COM クライアントよりもずっと単純です。クライアントは、2 つの方法で実装できます。まず、推奨する方法から説明します。

最初に、次のコマンドで Microsoft® .NET Framework Type Library to Assembly Converter ツールである tlbimp.exe を実行して、サーバー DLL をマネージ コードにインポートし、ATLConnectionPointServerLib.dll を取得します。

tlbimp ATLConnectionPointServer.dll

図 4 で示す例のように、マネージ プロジェクトで結果のアセンブリを参照し、クライアントにシンク インターフェイスの実装を提供する必要があります。ManagedSink クラスは、_IAddEvents インターフェイスで定義されているように、2 つのメソッド AdditionStarted および AdditionCompleted を実装します。これによって、シンク イベント ハンドラが完成し、使用できるようになりました (COM クライアントと比べるとあまりにも簡単すぎると思いませんか?)。

Figure 4 シンク インターフェイスの実装を提供する (Close Figure 4)
public class ManagedSink :_IAddEvents
{
    public void AdditionStarted()
    {
        Console.WriteLine("C# SINK: Addition started event fired ...");
    }

    public void AdditionCompleted(int nResult)
    {
        Console.WriteLine("C# SINK: Addition completed event fired ...");
        Console.WriteLine("C# SINK: Addition result: {0}", nResult);
        return;
    }
};

COM クライアントと同じように、シンクをサーバーに登録する必要があります。これでイベントを起動したときにサーバーがシンクを呼び出すことができます。ただし、マネージ クライアントが自身をサーバーに登録する方法にはいくつか違いがあります。

COM クライアントは、インターフェイス _IAddEvents を実装する、そのシンク オブジェクトのインスタンスを登録しました。次の呼び出しで COM クライアントを登録したことを思い出してください。

// Pass the sink interface to the server through the Advise
hr = pCP->Advise(pSinkUnk,&dwAdvise);
if(!SUCCEEDED(hr)) { return;}

マネージ クライアントの場合は、個別のメソッドをデリゲートとしてサーバーに登録します。このためには、シンク オブジェクトのインスタンスを作成します。

ManagedSink ms = new ManagedSink();

サーバー オブジェクトのインスタンスを作成し、AdditionStarted および AdditionCompleted イベント ハンドラを、次のように個別に追加します。

AddClass a = new AddClass();
a.AdditionStarted += ms.AdditionStarted;
a.AdditionCompleted += ms.AdditionCompleted;

クライアントは、各イベント ハンドラ用に 2 つの異なるインターフェイスをサーバーに登録します。クライアントでデリゲートを追加するためのこれらの呼び出しは、クライアントのランタイム呼び出し可能ラッパー (RCW) の参照カウントを追加します。呼び出しが完了したら、次のようにイベント ハンドラを削除することによって、参照カウントを解放する必要があります。

a.Add(1, 5);
a.AdditionStarted -= ms.AdditionStarted;
a.AdditionCompleted -= ms.AdditionCompleted;

最後に、ManagedClient.cs をコンパイルします。

csc /r:ATLConnectionPointServerLib.dll ManagedClient.cs

次に、実行可能ファイルを実行します。次のような出力結果が得られます。

C# SINK: Addition started event fired ...
C# SINK: Addition completed event fired ...
C# SINK: Addition result: 6

まとめ

ATL での dispinterface のクライアント実装の記述は、少し複雑になることがあります。ここで説明したサンプルは、独自の呼び出し実装を用意しておくことによって、複雑にならないようにしています。

このコラムの執筆にあたって、協力や助言をくださった Cosmin Radu、Ladi Prosek、Mason Bendixen、Varun Sekhri、そして Claudio Caldato に感謝します。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Thottam R. Sriram はカナダのモントリオールにある Concordia 大学でコンピュータ サイエンスの修士号を取得しました。現在、彼は CLR チームのプログラム マネージャとして COM 相互運用機能に取り組んでいます。CLR チームに参加する前は、Windows チームに携わっていました。彼のブログは、blogs.msdn.com/thottams (英語) で公開されています。


基本的な本能: ラムダ式

基本的な本能
ラムダ式
Timothy Ng

コンテンツ
ラムダ式とは
コールバックとしてのラムダ式
ラムダ式が追加された理由
型の推定
内部でのコード生成
ラムダ式と変数変換
ラムダ式を最大限に活用する

Visual Basic® 2008 で導入されたラムダ式は、どのプログラマにとっても役立つ新たなツールです。ラムダ式は、関数内で定義される呼び出し可能なエンティティであり、何の制約もなく利用できます。したがって、ラムダ式を関数から返したり他の関数に渡したりできます。ラムダ式が Visual Basic 2008 (以前のコード名は "Orcas") に追加されたのは、LINQ (Language Integrated Queries) をサポートするためです。これは、Visual Basic でのデータ プログラミングを可能にするものです (後で詳しく説明します)。ラムダ式を使用するにつれ、能力と柔軟性が向上することがわかるでしょう。これからラムダ式の基本的な概念のサンプルを示し、その利点と、それを使用して表現力に富むプログラムを記述する方法を説明していきます。

ラムダ式とは

基本的なラムダ式の定義の例を次に示します。ここでは、doubleIt を、整数を受け取って整数を返すラムダ式として定義します。ラムダ式は、入力を効率的に受け取り、それを 2 倍した結果を返します。

Dim doubleIt As Func(Of Integer, Integer) = _
    Function(x As Integer) x * 2

Func 型も Visual Basic 2008 で新しく導入されました。これは、実質的にはデリゲートで、最後のジェネリック パラメータとして指定された戻り値の型を持ち、その前にジェネリック パラメータとして最大 4 つの引数を指定できます (実際には複数の Func デリゲートがあり、それぞれが特定の数のパラメータを受け取ります)。Func デリゲート型は、System 名前空間のアセンブリ System.Core.dll で定義されます。Visual Basic で作成されたすべての新規プロジェクトは、自動的に System.Core.dll への参照を保持するので、すぐに Func 型を利用できます。

次のコードは Func のさまざまなオーバーロードを示しています。

Dim f0 As Func(Of Boolean)
Dim f1 As Func(Of Integer, Boolean)
Dim f4 As Func(Of Integer, Integer, Integer, Integer, Boolean)

f0 はブール型を返すデリゲート、f1 は整数を受け取ってブール型を返すデリゲート、f4 は 4 つの整数引数を受け取ってブール型を返すデリゲートです。注意すべき重要なポイントは、ラムダ式がデリゲートとして型指定される点、つまり Visual Basic 2005 のデリゲートと同じように呼び出し可能なエンティティである点です。

最初のコードの代入式の右側は、新しいラムダ式の構文です。これは、キーワード Function で始まり、その後に 1 つの引数リストと 1 つの式が続きます。

前の例では、ラムダ式は、1 つの引数 x (整数) を受け取っていました。しかし、お気付きのように return ステートメントはありません。これは、Visual Basic コンパイラが、式に基づいて既に型を認識しているからです。このため、return ステートメントは不要です。この場合、x が整数なので、2 * x も整数になり、ラムダ式の結果も整数になります。

ラムダ式の不思議な力は、次に示すように通常のデリゲートと同様に単純にそれを呼び出すことができることです。

Dim doubleIt As Func(Of Integer, Integer) = _
    Function(x As Integer) x * 2
Dim z = doubleIt(20)

このコードを実行すると、z に格納される値は 40 になります。ここで作成したのは、基本的に、入力した任意の整数値を 2 倍にするラムダ式です。

次に、もう少し複雑な例、ラムダ式ファクトリについて考えてみましょう。

Dim mult As Func(Of Integer, Func(Of Integer, Integer)) = _
    Function(x As Integer) Function(y As Integer) x * y

Mult は、かなり複雑なラムダ式です。これは、1 つの整数引数を入力として受け取り、別のラムダ式を返します。返される式は 1 つの整数引数を入力として受け取って整数を返します。構文の記述には少し注意が必要ですが、次のような一連の行と書式を使用すると、正確なネスト構造を示すのに役立ちます。

Dim mult As Func(Of Integer, Func(Of Integer, Integer)) = _
    Function(x As Integer) _
        Function(y As Integer) x * y

もう少しわかりやすくいうと、外側のラムダ式に、別のラムダ式が含まれており、これをコンパイラが return ステートメントとして使用します。内側のラムダ式のシグネチャは、外側のラムダ式の戻り値の Func(Of Integer, Integer) デリゲートのシグネチャと一致するので、コンパイラは、ステートメントをエラーなしでコンパイルします。

このようなラムダ式はどのように使用するのでしょうか。

Dim mult_10 = mult(10)
Dim r = mult_10(4)

この 1 行目では、mult_10 を mult(10) と定義しています。Mult(10) は、引数を受け取ってそれを 10 倍するラムダを返すので、mult_10 の型は Func(Of Integer, Integer) です。2 行目では、値 4 を渡して mult_10 を呼び出しているので、結果 r は 40、r の型は整数になります。

基本的に、mult はラムダ式ファクトリです。これは、1 つ目の引数によってカスタマイズされるラムダ式を返します。お気付きかもしれませんが、内側のラムダ式は、外側のラムダ式のパラメータを参照しますが、内側のラムダ式の有効期間は外側の式の有効期間より長くなります。この変数の変換については後で説明します。

コールバックとしてのラムダ式

ラムダ式は単なるデリゲートなので、デリゲートを使用できるあらゆる場所で使用できます。次に、デリゲートを受け取り、リスト内の要素ごとにデリゲートを呼び出すメソッドについて考えてみましょう。

Delegate Function ShouldProcess(Of T)(element As T) As Boolean

Sub ProcessList(Of T)( _
        elements As List(Of T), shouldProcess As ShouldProcess(Of T))
    For Each elem in elements
        If shouldProcess(elem) Then
            ' Do some processing here
        End If
    Next
End Sub

これは、デリゲートを使用するアプリケーションとしてかなり標準的なものです。メソッド ProcessList は、リストの各要素をループし、その項目の処理が必要かどうかをチェックし、標準的な処理を行います。

これを Visual Basic 2005 で使用するには、次のように (赤字のコードに注意してください) デリゲートと同じシグネチャを持つクラスまたはモジュールで関数を定義し、その関数のアドレスを ProcessList プロシージャに渡す必要がありました。

Class Person
    Public age As Integer
End Class

Function _PrivateShouldProcess(person As Person) As Boolean
    Return person.age > 50
End Function

Sub DoIt()
    Dim list As New List(Of Person)
    ' Obtain list of Person from a database, for example
    ProcessList(list, AddressOf _PrivateShouldProcess)
End Sub

これは、ひいき目に見ても面倒な作業です。多くの場合、コード ドキュメントを掘り下げてデリゲートが押し付けるシグネチャを調べ、それと正確に一致させる必要があるからです。また、ProcessList を多数の異なる shouldProcess 関数で呼び出す必要がある場合、多くの小さいプライベート関数を記述することになるのでコードが煩雑になります。

次の関数をラムダ式でどのように呼び出すことができるかを見ていきましょう。

Class Person
    Public age As Integer
End Class

Sub DoIt()
    Dim list As New List(Of Person)
    ' Obtain list of Person from a database, for example
    ProcessList(list, Function(person As Person) person.age > 50)
End Sub

私はラムダ式の簡潔で洗練されたところを大変気に入っています。処理ロジックの実行に独自の関数を作成する必要はありません。デリゲートは、使用される場所で定義されます。プライベート メソッドで定義されているデリゲートを別の場所にあるメソッドで使用するとそのデリゲートの局所性が失われますが、それに比べてこれははるかに良い方法です。

ラムダ式がいかに強力かつ便利であり、またコードを読んだり管理したりするのも簡単であることがおわかりいただけたと思います。型の推定など、より高度な機能を使用すると、その力はより発揮されます。

注意すべき制限の 1 つは、ラムダ式が厳密に 1 つの式であることです。Visual Basic 2008 では、ラムダ式に 1 つの式しか指定できません。このコラムではさらに、Visual Basic 2008 で導入された新しい 3 項演算子について説明します。これを使用すると、条件式を構築できますが、現在の機能ではラムダ式での任意のステートメントの指定はサポートされません。

ここで、ラムダ式の基になる概念を詳しく説明する前に、そもそもなぜラムダ式が導入されたかを説明します。

ラムダ式が追加された理由

LINQ クエリをサポートするために、いくつかの機能を追加する必要がありましたが、その中に Visual Basic とラムダ式が含まれていました。次のような Visual Basic のクエリ ステートメントがあるとします。

Dim q = From p In Process.GetProcesses() _
        Where p.PriorityClass = ProcessPriorityClass.High _
        Select p

このクエリ ステートメントをコンパイルできるようにするために、内部で多くの処理が行われます。大まかにいうと、コンパイラは、Process.GetProcesses コレクションを反復処理し、それに Where フィルタを適用し、Where 句のフィルタと一致したプロセスのリストを返します。

Where 句内に、Visual Basic の式 PriorityClass = ProcessPriorityClass.High があることに注意してください。このフィルタを実行するために、コンパイラは Where フィルタのラムダ式を作成し、それをプロセス リスト内の各要素に適用します。

Dim q = Process.GetProcesses().Where( _
            Function(p) p.PriorityClass = ProcessPriorityClass.High)

基本的に、ラムダ式は、メソッドの生成とそのデリゲートへの割り当てをすばやく簡単に行う手段をコンパイラに提供します。これはすべて自動的に行われます。デリゲートと関数の組み合わせでは得られないラムダ式の利点は、コンパイラが自動的にラムダ式の型の推定を行うことです。前の例にある引数 p の型は、その使用法によって推定されます。この場合は、Where 引数がラムダ式の型を定義し、引数であるラムダ式の型がコンパイラによって推定されます。コンパイラでの型の推定機能は、Visual Basic に追加された強力な機能です。その働きを見ていきましょう。

型の推定

強力な型の推定メカニズムが導入されたことで、各変数の型がわからなくてもよくなりました。さらに、型の推定によって、これまで不可能だったシナリオが可能になりました。ラムダ式を使用するときに型を推定する 3 種類の方法を見てみましょう。

ラムダ式の引数の型を推定する このシナリオは、ラムダを割り当てるデリゲート型があり、その引数を正確に指定することができない場合に大変便利です。

Dim lambda As Func(Of Integer, Integer) = Function(x) x * x

例では、lambda 変数が Func(Of Integer, Integer) として型指定されています。これは、整数引数を 1 つ受け取って整数引数を返すデリゲートです。結果的に、コンパイラは、lamda 引数 x が整数で、lambda の戻り値が整数になると自動的に推定します。

ラムダ式の引数の型を推定するメリットは、デリゲートを受け取るメソッドを呼び出す場合にも得られます。前の例を変更して考えてみましょう。

Delegate Function ShouldProcess(Of T)(element As T) As Boolean

Sub ProcessList(Of T)( _
        elements As List(Of T), shouldProcess As ShouldProcess(Of T))
    ' Method body removed for brevity
End Sub

この場合、ProcessList 関数は、ラムダ式を受け取ります。

プロシージャを次のように呼び出すことができます。

Sub DoIt()
    Dim list As New List(Of A)
    ' fill or obtain elements in list
    ProcessList(list, Function(a) a.x > 50)
End Sub

ここでは前のようにラムダ引数の型は指定していませんが、コンパイラはそれが Person であると推定します。こうした処理はどのように行われているのでしょうか。この例では、実際には複数のレベルの型の推定が行われます。

まず、コンパイラは、ProcessList が、入力として List(Of T) と ShouldProcess(Of T) を受け取るジェネリック プロシージャであることを確認します。ProcessList の呼び出しで、コンパイラは、リストが最初の引数であることと、そのリストが List(Of Person) であることを確認します。2 つ目の引数には型 T が何かについての手掛かりはないので、コンパイラは、T が Person 型であると判断します。次に、ShouldProcess(Of T) のジェネリック引数が Person であると推定し、そこから 2 番目の引数が ShouldProcess(Of Person) であると推定します。最後に、ラムダ式が自身の引数の型を提供していなかったので、コンパイラは、ShouldProcess(Of Person) のデリゲートのシグネチャに基づいて引数の型を認識し、パラメータ (a) の型を Person と推定します。これは、型の推定のきわめて強力なモデルです。ラムダを構築するときにデリゲート引数の型を認識しておく必要はありません。この難しい仕事はコンパイラに任せることをお勧めします。とはいえ、さいわいにも推定された型が何かを示すのに役立つ IntelliSense® およびツールヒントがあるので、生産性は (それほどではないにしても) 確実に上がります。

結果の型を推定する このシナリオは、デリゲート型がないのでそれをコンパイラに合成させる場合に大変便利です。これは、Visual Basic だけで利用できる機能です。

Dim lambda = Function(x As Integer) x * x

この例では、ラムダ式は、完全に型指定されています (lambda の引数 x は整数型であり、整数 * 整数 = 整数なので、コンパイラは戻り値が整数であると推定します)。しかし、lambda 変数には型がありません。このため、コンパイラは、ラムダ式図形と一致する匿名デリゲートを合成し、そのデリゲート型を lambda に割り当てます。

これは、優れた機能です。なぜなら、ラムダ式を、そのデリゲート型を静的に構築しておかなくてもその場で作成できることを意味するからです。たとえば、複数の変数に適用しなければならない条件があったり、図 1 のようにそれを複数の場所で適用する必要があったりする状況に陥ることは、どれくらいの頻度で起こるでしょうか。私は、コードを記述していてこのような状況に遭遇する回数はかなり多いです。通常は、こうしたことを考慮せずに、条件チェックを関数内にあちこちに配置するのではなく、1 か所で行えるようにします。

Figure 1 反復条件チェック (Close Figure 1)
Class Motorcycle
    Public color As String
    Public CC As Integer
    Public weight As Integer
End Class

Sub PrintReport(motorcycle As New Motorcycle)
    If motorcycle.color = "Red" And motorcycle.CC = 600 And _
       motorcycle.weight > 300 And motorcycle.weight < 400 Then
       ' do something here
    End If

    ' do something here

    If motorcycle m.color = "Red" And motorcycle.CC = 600 And _
       motorcycle.weight > 300 And motorcycle.weight < 400 Then
       ' do something here
    End If
End Sub

しかし、チェックを使用するのはこの関数だけで、他ではまったく使用されない場合もあります。この関数をサポートするだけのために使用されるランダムなヘルパ関数を記述することで、自分のクラスを煩雑にしたくありません。これは保守性に悪影響を及ぼします。たとえば他の人がこの関数を呼び出す一方で私がそれを変更する必要がある場合はどうなるでしょうか。また、多数のプライベート メソッドを持つクラスでは、名前による追跡が非常に困難になることがあります。さらに、IntelliSense リストのエントリが増える一方なので、IntelliSense の有用度が低くなる可能性があります。さらに、ロジックの局所性にも影響があります。個別のプライベート メソッドを作成する場合は、それを使用するメソッドから物理的に近いところに配置するのが私の好みです。多くの人々が同じコード ベースで作業する場合は、この局所性を長期間維持するのが難しいことがあります。図 2 のように、ラムダ式を使用してコンパイラにデリゲート クラスを自動作成させると、この問題に対処できます。

Figure 2 ラムダ式とデリゲート (Close Figure 2)
Sub PrintReport(motorcycle As New Motorcycle)
    Dim check = Function(m As Motorcycle) m.color = "Red" And _
                                          m.CC = 600 And _
                                          m.weight > 300 And _
                                          m.weight < 400
    If check(motorcycle) Then
        ' do something here
    End If

    ' do something here

    If check(motorcycle) Then
        ' do something here
    End If
End Sub

私は、Motorcycle クラスで一部の条件をチェックするロジックを不利益のあるプライベート メソッドに組み込むことは考えないようにしていました。しかし、ラムダ式であれば話は別です。メソッドと同じ感覚でラムダ式を呼び出せるように、コンパイラが自動的に (内部で) デリゲート型を作成し、必要な作業をすべて結び付けるからです。

このアプローチは私の大のお気に入りです。ロジックを実装の近く (メソッド本体内) に配置すると、それが考慮されて (1 つのコピーだけ)、コンパイラが保守の大部分を引き受けるからです。これは、ラムダ式の本体として任意の複雑な式を構築できるので効果的です。

遅延バインディング £ オブジェクトを推定する このシナリオでは、lambda 変数もラムダ式も型指定されません。

Dim lambda = Function(x) x * x

ここでは、匿名デリゲートもコンパイラによって生成されますが、lambda の型は System.Object です。これは、このシナリオで遅延バインディングが有効になるのは、Option Strict が off の場合であることを意味します。

これは、遅延バインディングに依存している人にとっては非常に好都合なシナリオです。遅延バインディング処理は、ラムダ式で完全にサポートされているので、上の例では、* 演算子が lambda に指定する型で定義されている限り有効です。

Dim a = lambda(10)
Dim b = lambda(CDec(10))
Dim c = lambda("This will throw an exception because " & _
               "strings don't support the * operator")

この例でわかるように、ランタイム型に * 演算子がある限り、すべて適切に機能します。このラムダ式の特性は、Visual Basic の遅延バインディング モデルに非常にうまく適合します。

内部でのコード生成

これまでラムダ式について説明してきましたが、今度はコンパイラが生成するコードの種類について見てみましょう。前の例について考えます。

Sub TestLambda()
    Dim doubleIt As Func(Of Integer, Integer) = _
        Function(x As Integer) x * 2
    Console.WriteLine(doubleIt(10))
End Sub

Func はデリゲートであり、デリゲートは単に関数へのポインタであることがわかっています。さて、コンパイラはどのようにしてこの不思議な力を活用しているのでしょう。この場合、コンパイラは、新しい関数を出力し、デリゲートを新しい関数をポイントするように設定します。

Function $GeneratedFunction$(x As Integer) As Integer
    Return x * 2
End Sub

Sub TestLambda()
    Dim doubleIt As Func(Of Integer, Integer) = _
        AddressOf $GeneratedFunction$
    Console.WriteLine(doubleIt(10))
End Sub

コンパイラは、基本的にラムダ式を受け取り、その内容で新しい関数を作成し、代入ステートメントを変更して、ラムダ式が生成された関数のアドレスを受け取るようにします。この場合、関数は、ラムダ式を使用するメソッドを含む同じ親の下に生成します。TestLambda がクラス C で定義されている場合、生成された関数は C で定義されます。なお、生成された関数は呼び出し可能ではなく、private としてマークされます。

ラムダ式と変数変換

前の例で、ラムダ式の本体は、ラムダに渡された変数を参照していました。しかし、ラムダ式の力は、変数変換で大きく発揮されます。前に触れたように、コンパイラは特定のシナリオで "不思議な力を発揮" します。このようなシナリオを見ていく前に、ラムダ計算と呼ばれる数学の一分野の基本的な概念について説明します。ラムダ式は同様の概念に基づいています。

ラムダ計算の基本的な概念は、関数が自由変数または束縛変数を持てるということです。自由変数は、それを含むメソッドの変数 (ローカルおよびパラメータ) で定義される変数です。束縛変数は、ラムダのシグネチャで定義される変数、または基本クラスなど、ラムダを含むクラスのメンバです。

ラムダ式では束縛変数と自由変数を区別することが非常に重要です。それらがラムダ式のセマンティクス、生成されるコード、そして最終的にはプログラムの正確さに影響するからです。ここで、束縛変数と自由変数を含むラムダ式の例を示します。

Dim y As Integer = 10
Dim addTen As Func(Of Integer, Integer) = Function(ByVal x) x + y

ここで、x は、ラムダ式の仮パラメータなので、ラムダ内の束縛変数と見なされます。また、y は、ラムダ式でそれを含むメソッドに属する変数なので自由変数と見なされます。

ラムダ式を定義した後はデリゲート型のように扱われることを思い出してください (たとえば、メソッドからラムダ式を返すことができます)。次の例について考えてみましょう。

Function MakeLambda() As Func(Of Integer, Integer)
    Dim y As Integer = 10
    Dim addTen As Func(Of Integer, Integer) = Function(ByVal x) x + y
    Return addTen
End Function

Sub UseLambda()
    Dim addTen = MakeLambda()
    Console.WriteLine(addTen(5))
End Sub

このコードは、UseLambda が呼び出されたときに "15" をコンソールに出力します。しかし、どのように処理されているのかが気になっているかもしれません。関数 MakeLambda は y をローカル変数として定義し、ラムダは y を使用していますが、ラムダは関数 MakeLambda から値を返します。関数 UseLambda は MakeLambda からラムダを受け取り、そのラムダを実行します。変数 y はなぜかラムダで "記憶されている" ように見えます。

これはある種の舞台裏のトリックでしょうか。y は、MakeLambda のメソッド内でのみ有効です。MakeLambda から返されるラムダを実行すると、MakeLambda はスコープ外となるので、そのスタック スペースは削除されます。しかし、y はスタックで定義されていました。そして、何らかの形でラムダと "くっついて" いるようでした。

この粘着性は、一般的に変数変換として呼ばれる魔法のようなものです。この場合、変数 y は変換変数と呼ばれます。また、おわかりのように、変換変数は強力です。コンパイラは、変数の状態をキャプチャし、それを通常の有効期間が過ぎても保持するために、多数の下準備をします。

具体的に説明すると、コンパイラは、自由変数を持つラムダ式を見つけたときに、その自由変数をクロージャと呼ばれるクラスに変換します。クロージャの有効期間は、そこで変換された自由変数の有効期間より長くなります。コンパイラは、クロージャ インスタンス内の変数にアクセスするために、メソッド内の変数アクセスを書き換えます。

まず、MakeLambda の例をもう一度見てみましょう。

Function MakeLambda() As Func(Of Integer, Integer)
    Dim y As Integer = 10
    Dim addTen As Func(Of Integer, Integer) = Function(ByVal x) x + y
    Return Lambda
End Function

前に分析したように、x はラムダのパラメータに束縛されていますが、y は自由変数です。コンパイラは、これを検出して、ラムダ式の定義と自由変数をキャプチャするクロージャ クラスの作成に進みます。

Public Class _Closure$__1
    Public y As Integer
    Public Function _Lambda$__1(ByVal x As Integer) As Integer
        Return x + Me.y
    End Function
End Class

クロージャ変数は、変数 y をキャプチャし、それをクロージャ クラスに保存することがおわかりでしょう。次に自由変数は、クロージャ クラスで束縛変数に変換されます。

コンパイラは、次のようにラムダ式を含むメソッドも書き換えます。

Function MakeLambda() As Func(Of Integer, Integer)
    Dim Closure As New _Closure$__1
    Closure.y = 10
    Return AddressOf Closure._Lambda$__1
End Function

ここで、コンパイラがクロージャ変数を作成し、クロージャ変数に変換されるローカル変数 y を書き換え、変数を初期化し、クロージャ クラスに書き込まれるラムダ式のアドレスを単純に返すことがわかります。

コンパイラはラムダ式の自由変数を変換するだけであることに注意することが重要です。変数の状態はクロージャでキャプチャされます。これは、ラムダ式が存在する限り存在します。

別の例を見てみましょう。

Sub Test()
    Dim y As Integer = 10
    Dim Lambda As Func(Of Integer, Integer) = Function(ByVal x) x + y
    y = 20
    Console.WriteLine(Lambda(5))
End Sub

この関数を実行するとどんな値が表示されますか。答えが 25 であれば、正解です。では、なぜ 25 なのでしょう。コンパイラは、次のようにすべての自由変数 y をキャプチャしてクロージャのコピーに書き換えます。

Sub Test()
    Dim Closure As New $CLOSURE_Compiler_Generated_Name$
    Closure.y = 10
    Dim Lambda = AddressOf Closure.Lambda_1
    Closure.y = 20
    Console.WriteLine(Lambda(5))
End Function

おわかりのように、ラムダ式が実行されるときまでに、y の値が 20 に変更されているので、ラムダ式が実行されたときに 5 + 20 が返されます。

これは、ループに関していえば実に重要です。複数の自由関数が 1 つのクロージャにキャプチャされるので、たとえば、値が変化するキャプチャ済み変数を含むラムダ式を使用するスレッドを起動した場合に予期しない動きをすることがあります。

Sub Test()
    For I = 1 To 5
        StartThread(Function() I + 10)
    Next
End Function

この例では、StartThread が新しいスレッドを作成し、ラムダ式の結果をコンソールに出力するとします。I はクロージャにキャプチャされるので、スレッドがラムダ式を実行するときまでに for ループが変数 I を変更している可能性があります。その場合、プログラムが予想どおりに 11、12、13、14、および 15 を出力しないことがあります。代わりに、キャプチャされた変数のスコープを for ループ内にする必要があります。

Sub Test()
    For I = 1 To 5
        Dim x = I
        StartThread(Function() x + 10)
    Next
End Function

このコードは、今度はクロージャに値 x をキャプチャします。プログラムは予想どおりに 11、12、13、14、および 15 を出力します。どの変数が変換されるか、いつラムダ式が実行されるか、そして変換変数がいつ変更される可能性があるかを知ることはきわめて重要です。それによって、プログラムをあらゆる状況で確実に正しく動作させることができるからです。

ラムダ式を最大限に活用する

Visual Basic 2008 では、ラムダ式の本体として指定できるのは 1 つの式だけですが、完全に型指定された単純な条件式を実行できる新しい 3 項キーワードも導入されています。

Dim x = If(condition, 10, 20)

If キーワードは IIF 関数呼び出しとほぼ同じです。ただし、If キーワードが完全にタイプセーフである点を除きます。前の例の場合、これは、コンパイラは、If キーワードの両方の分岐が整数を返すことを推定するので、型の推定規則を適用して、x の型を整数と判断することを意味します。IIF を使用すると、x は型オブジェクトなります。

If キーワードはラムダ式で使用できます。

Dim x = Function(c As Customer) _
    If(c.Age >= 18, c.Address, c.Parent.Address)

この例において、Customer クラスがあり、お察しのとおりその定義に顧客の現在の住所を示す Address プロパティが含まれているとします。ラムダ式は、3 項式を使用して、「顧客が 18 歳以上である場合は、その住所を返す」という入力引数の条件を適用します。それ以外の場合は親の住所を返します。

ここで、型の推定が始まり、コンパイラは、ラムダ式の戻り値の型が Address であると判断します。続いて、(前に説明したように) x のデリゲート型を作成します。ここでデリゲート型は、入力として Customer を受け取り Address を返します。

ラムダ式の詳細については、私のブログ (blogs.msdn.com/timng (英語)) を購読できます。今後、ラムダ式 (およびその他の Visual Basic 2008 の言語機能) について詳しく説明する予定です。

ご意見やご質問は、

こちら instinct@microsoft.com.

まで英語でお送りください。
Timothy Ng は、マイクロソフトの Visual Basic コンパイラ チームのソフトウェア開発エンジニアで、Visual Studio の次期リリースである Visual Studio 2008 向けに、型の推定、フレンド アセンブリ、式ツリー、匿名型などの機能を開発しました。Tim の連絡先は timng@microsoft.com (英語) です。


Cutting Edge: AJAX アプリケーション アーキテクチャ、第 1 部

Cutting Edge
AJAX アプリケーション アーキテクチャ、第 1 部
Dino Esposito

コードのダウンロード : : CuttingEdge2007_09.exe (333KB)
コンテンツ
AJAX による救済
AJAX のアーキテクチャ
AJAX フレームワークとは
ASP.NET AJAX Extensions
部分的なレンダリングの概要
AJAX ポストバックの詳細
部分的なレンダリングの最適化手法
部分的なレンダリングの考察

システム管理者、設計者、または開発者の仕事は、AJAX の影響を大きく受けます。管理者は、セキュリティ レベルを、新種の攻撃にも対抗できる高いレベルに維持する必要があります。イントラネット管理者は、JavaScript がどのブラウザでも有効になっていることを保証する必要があります。Web 設計者は、かつては不可能または非実用的であった機能が AJAX で実現できるようになったことで、それに関する新しい課題に取り組む必要があります。開発者は、新しい API と新しいプログラミング方法全般をよく理解する必要があります。では、アーキテクトにとって AJAX はどのような意味があるのでしょうか。

AJAX アプリケーションは、最新の概念と新しい基盤が導入するものなので、困難な面もありますがやりがいもあります。AJAX パラダイムはクライアント環境とサーバー環境の両方にまたがっているため、アーキテクトの役割は不可欠です。クライアント上のロジックと処理、サーバー上に残すもの、およびクライアントとサーバーでやり取りできるデータ オブジェクトを決定するために、アーキテクチャについての明確なビジョンが重要です。

AJAX による救済

Web は当初、情報の静的ページを共有するために使用されていました。しかし、わずか数年で、はるかに動的な手段に進化しました。現在では、Web はきわめて対話性の高いページで構成されていますが、まだページ全体の遷移に基づくパラダイムに大きく依存しています。ユーザーがページと対話するほど、ページを作成してユーザーのブラウザに返す回数が増えます。この方法で対話的なページを処理すると、ページの更新が遅くなったりページがちらつくなどの、多くの悪影響が生じます。これは、ユーザー エクスペリエンスの低下につながり、的確とは言えない方法です。AJAX は、このパラダイムに待望の変化をもたらし、より明快な Web アプリケーションを実現します。また、新世代のアプリケーションの基盤を作ります。AJAX はまさにエンド ユーザーのためのものです。AJAX の機能を使用すると、Web アプリケーションはさらに対話性および応答性が高くなります。つまり、高速で使いやすくなります。対話と応答 (処理が遅い場合にはフィードバックも) がすべてスマートに処理されるため、ユーザーはページで多くの処理を実行できます。全体的に、ユーザー エクスペリエンスが大幅に向上します。

AJAX では、コードがブラウザで実行されることが多いため、クライアント ページに渡す必要があるスクリプトが多くなります。しかし、これではいくつかの大きな疑問が残ります。これはどのような種類のスクリプトでしょうか。このスクリプトはだれが記述するのでしょうか。このスクリプトはどのようなアーキテクチャの原則とパターンに従うのでしょうか。

このコラムでは、AJAX についてアーキテクチャの観点から説明し、最適な開発者およびアーキテクトを選択するために役立つ情報を提供します。また、設計者や管理者には、作業の簡略化に役立つ十分なコンテキスト情報を提供します。これはかなり大きなトピックになるので、2 部に分けて説明します。来月のコラムで続きを説明するので、見逃さないでください。

AJAX のアーキテクチャ

AJAX を検討するにあたっては、図 1 を参照してください。この図は、アーキテクチャの観点での移動の意味を示しています。従来の Web アプリケーションは、すべてをサーバー上で処理します。アプリケーションがスクリプト コードを生成して、クライアント上でデータ検証の処理などのタスクを実行するのはごくまれです。

図 1 従来の Web パラダイムと AJAX パラダイム
図 1 従来の Web パラダイムと AJAX パラダイム(画像を縮小するには、ここをクリックします)
図 1 従来の Web パラダイムと AJAX パラダイム
図 1 従来の Web パラダイムと AJAX パラダイム (画像を拡大するには、ここをクリックします)

AJAX アプリケーションは、クライアント側のフレームワークを使用して Web サーバーの呼び出しの発行を処理します。すると、AJAX のサーバー側のフレームワークが要求を処理して、データ フィードをクライアントに返します。多くの場合、これは JavaScript Object Notation (JSON) データ ストリームですが、XML、RSS、CSV などの他の形式も使用できます。

クライアントはデータ フィードを受け取り、JavaScript を使用して UI を更新します。サーバーは、要求に対する応答として、生データを指定された形式にエンコードして返します。帯域幅の消費は最小限で済み、要求の完了に時間がかからないためアプリケーションの処理速度は向上します。また、UI の更新を有効にするときに目に見えるポストバックは発生しません。ただし、これにより多くの問題が解決される一方で、クライアント上のアクションが増えるため、新しい問題が生じます。たとえば、新しいコーディング手法、新しいセキュリティの危機、アクセシビリティの懸念などです。

AJAX フレームワークとは

Web ページで AJAX が動作するようにするには、いくつかの条件を満たす必要があります。まず、ブラウザで JavaScript がサポートされている必要があります。インターネットに常時接続する必要もあります。AJAX アプリケーションはオフラインでは動作できません。すべての要求がページ レベルで行われる場合は、AJAX を使用しないアプリケーションの場合と同様に、ブラウザは、キャッシュされたページ間を移動できるため、オフラインで処理を続行できます。AJAX アプリケーションでは、これと同じ動作を、ブラウザと連携するスクリプトベースのフレームワークで明示的にコーディングする必要があります。この領域はまだ多くの問題が残されており、役に立つ新しいツールがいくつか開発されています。AJAX アプリケーションをオフラインで動作できるようにすることは、多くのソフトウェア ベンダの課題です。

重要な AJAX アプリケーションには特殊なフレームワークが必要です。通常、フレームワークは、互いに独立したクライアント部分とサーバー部分で構成されます。

AJAX ページのスクリプト コードは大きく分けて 2 種類あります。それは、好みのフレームワークで作成するシステムレベルのコードと、ページの期待する動作を実装するユーザーレベルのコードです。システムレベルのコードは、Web サーバーに非同期要求を送信して応答を処理するエンジンの役割を果たします。ユーザーレベルのコードは、基本的にはドキュメント オブジェクト モデル (DOM) スクリプトを使用してページの UI を更新します。ここで、前述の疑問の 1 つが生じます。だれがスクリプトを記述するのでしょうか。この疑問に答えるために、ここでは特定の AJAX フレームワーク、ASP.NET AJAX Extensions に重点を置いて説明します。

ASP.NET AJAX Extensions

ASP.NET 2.0 の拡張である ASP.NET AJAX Extensions には、新しい Web サイトや既存の Web サイトで使用できる AJAX 機能が用意されています。ASP.NET AJAX には 2 つのプログラミング モデルがあります。それは、部分的なレンダリングとリモート サービスです。これらのモデルでは、根本的に異なる 2 つのアプリケーション アーキテクチャを想定しているため、長所と短所も異なります。おそらく AJAX のほとんどの機能にはトレードオフが伴うことに既にお気付きの方もいるでしょう。

簡単に言うと、部分的なレンダリングでは、従来の ASP.NET 2.0 アプリケーションと同様のアーキテクチャを使用できます。ちらつきのないページ更新を実装できる新しいサーバー側のツールが追加されただけです。

一方、リモート サービスでは、比較的スクリプトへの依存度が高い AJAX フロント エンドによって呼び出されるサービス指向のバック エンドを使用します。認証、データのページング、ソートなど、ほとんどすべての基本アプリケーション プロセスを設計し直す必要があります。サーバー側のコードをアプリケーション固有のサービスに組み込んで、データ交換の形式 (JSON など) を選択する必要があります。最後に、細心の注意を払って UI レベルのタスクに限定してコーディングを行い、ほとんどのビジネス ロジックをクライアントから離れたところに保持して、フロント エンドを準備する必要があります。

リモート サービスを使用すると、さらに詳しく AJAX を体験できます。一方、部分的なレンダリングでは、既存のアプリケーションの機能の拡張により、スムーズなページ遷移が可能になります。

これらの 2 つのアプローチは、ある程度は組み合わせて使用できます。たとえば、従来の Web アーキテクチャをそのまま使用し、ちらつきのない更新を随所に追加して、主要な機能をサービス指向の方法でリファクタリングできます。ASP.NET AJAX Extensions にはいくつかの主要な構成要素が用意されていますが、優れた AJAX アプリケーションを作成するための作業の大部分は、ユーザーに委ねられています。図 2 に、ASP.NET AJAX Extensions フレームワークのクライアント コンポーネントとサーバー コンポーネントを示します。

Figure 2 ASP.NET AJAX Extensions フレームワークのコンポーネント (Close Figure 2)

ファイル | 場所 | 説明
MicrosoftAjax.js | クライアント | オブジェクト指向の構造で JavaScript を拡張する関数が含まれます。JavaScript の組み込みオブジェクトのプロトタイプを拡張し、StringBuilder などの新しいヘルパ クラスを定義します。
MicrosoftAjaxWebForms.js | クライアント | ネットワーク スタックを形成する JavaScript クラスおよびクライアント側の部分的なレンダリング エンジンを実装するすべてのクラスが含まれます。
MicrosoftAjaxTimer.js | クライアント | 新しい AJAX System.Web.UI.Timer サーバー コントロールのクライアント側のオブジェクト モデルを表します。これは、基本的にはブラウザのタイマ オブジェクトのサーバー ラッパーです。
System.Web.UI.ScriptManager | System.Web.Extensions | AJAX ライブラリ、リモート サービスのプロキシ クラス、スクリプト ファイルのローカライズされたバージョン、グローバル化データなど、適切な JavaScript ファイルとクライアント側のデータのダウンロードを調整します。
System.Web.UI.UpdatePanel | System.Web.Extensions | AJAX ポストバックを使用して、ちらつきのない方法で更新できるページの領域を定義します。
System.Web.UI.UpdateProgress | System.Web.Extensions | 長時間にわたる部分的なレンダリング操作が行われている間に、ユーザーに進行状況を表示するために使用できるサーバー側のテンプレートを定義します。
System.Web.UI.Timer | System.Web.Extensions | 子クライアントのタイマがタイムアウトしたときに、AJAX の方法でポストバックを行うヘルパ サーバー コントロールです。

部分的なレンダリングの概要

Jeff Prosise が 2007 年 6 月に執筆した記事「Wicked Code」(msdn.microsoft.com/msdnmag/issues/07/06/WickedCode) では、ASP.NET AJAX の部分的なレンダリングについてわかりやすく説明されています。アーキテクチャの観点からは、部分的なレンダリングによって新たに追加されるものはありません。従来のアプリケーションを AJAX 機能で簡単に拡張できるだけです。その機能の中で最も重要なのが、ちらつきのないページ更新です。

部分的なレンダリングを使用するのに、多くの新しいスキルを習得する必要はありません。既存のコードが受ける影響はわずかです。また、部分的なレンダリングはわかりやすいフォールバック メカニズムを備えており、セキュリティやアクセシビリティなど、アプリケーションにおいて AJAX を採用したことで問題が生じる可能性がある側面に対応できます。基本的に、アップグレード プロセスで既存コードの特定のブロックを AJAX に変換するときに問題が発生した場合は、それらのコード ブロックを元に戻して次に進むことができます。

部分的なレンダリング要求は、一般に AJAX ポストバックと呼ばれます。これは、実際に行われる処理を完璧に表す、きわめて適切な用語です。AJAX ポストバックは従来の ASP.NET ポストバックに似ていますが、クライアント側の ASP.NET AJAX ライブラリで定義されているスクリプト コードによって実行される点が異なります。外見は純粋な AJAX リモート呼び出しのようですが、AJAX ポストバックは ASP.NET ランタイム コンポーネントへの通常のポストバック要求に似ています。サーバー上では、要求はポストバック要求の通常のライフサイクルに従って処理され、Init、Load、PreRender などのイベントが発生します。サーバー上の AJAX ポストバックが従来の ASP.NET ポストバックと異なる点は、最終的なマークアップのレンダリングに使用するアルゴリズムだけです。この異なるアルゴリズムが、パフォーマンスを向上し、ページのちらつきをなくすのに重要な役割を果たします。ただし、アプリケーション モデルは ASP.NET の場合と同じです。図 3 に、AJAX ポストバック要求の変更されたライフサイクルを示します。

図 3 AJAX ポストバックのライフサイクル
図 3 AJAX ポストバックのライフサイクル

AJAX ポストバックと ASP.NET ポストバックでは、レンダリング段階の実装以外はすべてが同じです。AJAX ポストバックでは Load や PreRender などの通知イベントを発生させます。また、viewstate 情報を処理して、TextChanged や Click などの状態変更イベントおよびポストバック イベントを発生させます。

ASP.NET AJAX ページには、ScriptManager コントロールのインスタンスを 1 つ含める必要があります。このコントロールが ASP.NET AJAX ページの中枢になります。ページを必要なフレームワーク スクリプト ファイルにリンクして、AJAX ポストバックの発生を検出すると部分的なレンダリングを調整します。ScriptManager コントロールは、要求の HTTP ヘッダーをチェックして、その要求が AJAX ポストバックであるかどうかを判断します。次は MicrosoftAjaxWebForms.js ファイルの抜粋で、AJAX 要求をトリガする前に HTTP ヘッダーを設定しています。

   request.get_headers()['X-MicrosoftAjax'] =
       'Delta=true';

何によって AJAX ポストバックのレンダリング段階が変更されるのかについては、System.Web.Extensions アセンブリから抜粋した次のコードをよく見てください。

   protected override void OnPreRender(EventArgs e) {
       base.OnPreRender(e);
       if (IsInAsyncPostBack)
           PageRequestManager.OnPreRender();
   }

具体的には、上記のコードは ScriptManager コントロールのコードです。ご覧のとおり、スクリプト マネージャはプリレンダリング イベント ハンドラを登録します。イベントが発生すると、ハンドラはそのイベントが AJAX ポストバックに関係しているかどうかを検出して、関係している場合は、PageRequestManager 内部クラスからメソッドを呼び出します。PageRequestManager クラスの OnPreRender メソッドは次のとおりです。

   internal void OnPreRender()
   {
       _owner.SetRenderMethodDelegate(
          new RenderMethod(this.RenderPageCallback));
   }

このメソッドは、単純に現在の要求のマークアップをレンダリングするように特定のサブルーチンを設定します。SetRenderMethodDelegate は、System.Web.UI.Control クラスで定義されているメソッドです。このメソッドは一般的な使用には向いていませんが、基本的にページまたはコントロールがコンテンツのレンダリングに使用するイベント ハンドラを割り当てます。上記のコードでは、メソッドが現在のページで呼び出され、最終的に現在の要求のレンダリング プロセス全体をオーバーライドします。したがって、AJAX ポストバックのマークアップを処理する実際のコードは、PageRequestManager クラスの内部メソッドである RenderPageCallback メソッドで定義されています。ScriptManager コントロールの詳細については、今月号の MSDN® Magazine に掲載されている Ben Rush による記事を参照してください。

基本的には、AJAX ポストバックの変更されたレンダリング プロセスは、ポストバックに関連するページ内のすべての更新可能なパネルをループ処理して、それぞれのマークアップを累積します。累積されたすべてのマークアップは、非表示フィールドおよびエラー情報と共に応答ストリームに追加されて、クライアントに渡されます。次に、サンプル ページのコンテキストにおける部分的なレンダリングの一般的な呼び出しを詳しく見てみましょう。

AJAX ポストバックの詳細

ASP.NET ページを部分的にレンダリングされたページにするには、まずスクリプト マネージャをページに追加する必要があります。次に、更新可能な領域を UpdatePanel コントロールでラップして、個別に定義します。次に例を示します。

<asp:ScriptManager runat="server" />
<asp:UpdatePanel runat="server" ID="UpdatePanel1">
   <ContentTemplate>
      <%-- Markup of the region goes here --%>
   </ContentTemplate>
</asp:UpdatePanel>

UpdatePanel コントロールを使用しても、領域用に生成された表示可能なマークアップは変更されません。元のマークアップが単に <div> タグで囲まれるだけです。

<div id="UpdatePanel1">
      <%-- Markup of the region goes here --%>
</div>

AJAX ポストバックは、何によってトリガされるのでしょうか。また、だれによって、どのように管理されるのでしょうか。スクリプト マネージャは、ページに 1 つまたは複数の UpdatePanel コントロールを検出すると、次のようなスクリプトのブロックを生成します。

<script type="text/javascript">
Sys.WebForms.PageRequestManager._initialize('ScriptManager1',
        document.getElementById('form1'));
Sys.WebForms.PageRequestManager.getInstance()._updateControls(
    ['tUpdatePanel1','tUpdatePanel2'], [], [], 90);
</script>

_initialize メソッドは、クライアント側の PageRequestManager オブジェクトの静的メソッドです (MicrosoftAjaxWebForms.js を参照してください)。PageRequestManager クラスのグローバル インスタンスを作成して初期化します。このクラスはシングルトンとして機能し、その唯一の利用可能なインスタンスは、後で getInstance メソッドを使用して取得できます。上記のコードの 2 つ目のステートメントは、UpdatePanel コントロールの配列をクライアント フレームワークに登録します。サーバー側の各 UpdatePanel コントロールは、その ID を使用して参照されます。

主要なアクションは、_initialize メソッドの中で実行されます。前述したように、このコードでは、クラスのシングルトン インスタンスを作成してから初期化します。このときに、DOM フォーム オブジェクトの送信イベントのハンドラが登録されます。つまり、ページからフォームが送信されるたびに、AJAX スクリプトが実行され、通常のブラウザ ポストバックではなく XMLHttpRequest を使用して要求が行われます。元の一連のフォーム フィールドは保持され、サーバー側のスクリプト マネージャが利用できるようにいくつかの情報が追加されます。したがって、AJAX ポストバックでは、通常の ASP.NET ポストバックより詳しい情報がアップロードされます。

viewstate および他の非表示フィールドが処理され、要求と共にサーバーにアップロードされます。その応答として、更新された viewstate が、新しい非表示フィールド (ある場合)、および (おそらくは短くなった) マークアップと共にダウンロードされます。具体的には、応答に含まれるマークアップは、ポストバック中に変更された更新可能な領域のマークアップだけです。リストには、ポストバックをトリガした UpdatePanel コントロールおよび入れ子になったパネル、UpdateMode プロパティが Always に設定されているページ内の UpdatePanel コントロール、およびプログラムによって更新された UpdatePanel コントロールが含まれます。次のコード例は、プログラムで実行時の条件に基づいてパネルを更新する方法を示しています。

   UpdatePanel1.Update()

図 4 に示すサンプル ページと返される応答について考察します。さまざまなツールを使用して、受信 HTTP パケットと送信 HTTP パケットを監視できます。このコラムでは、Nikhil Kothari 氏の Web Development Helper ツールを使用しました。このツールは、projects.nikhilk.net/Projects/WebDevHelper.aspx (英語) から無償でダウンロードできます。

Figure 4 サンプル ページ (Close Figure 4)
<%@ Page Language="VB" CodeFile="Test.aspx.vb" Inherits="Test" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Test :: Partial Rendering</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ScriptManager runat="server" ID="ScriptManager1" />

    <asp:UpdatePanel runat="server" ID="UpdatePanel1">
        <ContentTemplate>
            <asp:TextBox runat="server" ID="TextBox1" />
            <asp:Button runat="server" ID="Button1" Text="Update" />
            <hr />
            <asp:Label runat="server" ID="Label1" />
        </ContentTemplate>
    </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>

AJAX ポストバックの応答は、Size、Type、ID、および Content の各列を含むレコードから成るテーブルとして表示できるテキスト ストリームです。各レコードはデルタ ノードとして参照されます。図 5 に、サンプル ページについて取得できるテーブルを示します。

Figure 5 サンプル ページのデルタ ノード テーブル (Close Figure 5)

サイズ | 型 | ID | コンテンツ
249 | updatePanel | UpdatePanel1 | <input name="TextBox1" type="text" value="Test" id="TextBox1" /> <input type="submit" name="Button1" value="Update" id="Button1" /> <hr /> <span id="Label1"></span>
52 | hiddenField | __VIEWSTATE |
56 | hiddenField | __EVENTVALIDATION |
0 | asyncPostBackControlIDs |  |
0 | postBackControlIDs |  |
13 | updatePanelIDs |  | tUpdatePanel1
0 | childUpdatePanelIDs |  |
12 | panelsToRefreshIDs | UpdatePanel1 |
2 | asyncPostBackTimeout | 90 |
9 | formAction |  | Test.aspx
25 | pageTitle |  | Test :: Partial Rendering

デルタ ノードは、AJAX ポストバック中に行われた変更を示します。すべての応答に同じデルタ ノード セットが含まれるとは限りません。要求のサーバー ライフサイクルの間に実行された処理や元のマークアップの構造によって異なります。

図 6 に、現在サポートされているすべてのデルタ ノードのリストを示します。デルタ テーブルを処理する JavaScript コードは、MicrosoftAjaxWebForms.js ファイルにあります。

部分的なレンダリングの最適化手法

図 6 をよく見ると、応答が 2 つの大きなブロックで構成されているのがわかります。それは、マークアップと viewstate です。イベント検証データ (ASP.NET 2.0 のセキュリティ関連機能)、カスタム非表示フィールド、スクリプト、およびその他のタイプのノードは、通常は全部合わせてもわずか数十バイトです。viewstate のサイズが大きいのは、ASP.NET ページの以前からの問題です。ASP.NET AJAX の部分的なレンダリングでは、残念ながらこの問題を解決できません。それでも、viewstate のサイズをできるだけ小さくして、ダウンロード時間を短縮することは必要です。それでは、ページのマークアップのサイズを小さくするには、どうしたらよいでしょうか。基本的には、特定のクリックに必要な最小限のマークアップのみ返す、小さなパネルを使用します。

Figure 6 部分的なレンダリングのデルタ ノード型 (Close Figure 6)

デルタ ノード型 | 説明
updatePanel | 部分的なレンダリング領域の更新されたマークアップが格納されます。
hiddenField | 非表示フィールドの内容が格納されます。
arrayDeclaration | スクリプト マネージャの適切な RegisterXXX メソッドを使用して応答に追加された配列宣言が格納されます。
scriptBlock | スクリプト マネージャの適切な RegisterXXX メソッドを使用して応答に追加されたスクリプト ブロックが格納されます。指定したスクリプトは、使用した RegisterXXX メソッドに基づいて、実行されるか、またはページに挿入されます。
expando | スクリプト マネージャの適切な RegisterXXX メソッドを使用して応答に追加された expando プロパティが格納されます。指定した値は、応答が処理されるときに、指定したプロパティ ID に割り当てられます。
onSubmit | スクリプト マネージャの適切な RegisterXXX メソッドを使用して応答に追加された onsubmit スクリプト ブロックが格納されます。
asyncPostBackControlIDs | 呼び出しで更新パネルの非同期トリガとして登録されたコントロールの ID が格納されます。
postBackControlIDs | 呼び出しで更新パネルのポストバック トリガとして登録されたコントロールの ID が格納されます。
updatePanelIDs | 呼び出しに関連する更新パネルの ID が格納されます。
asyncPostBackTimeout | 要求がタイムアウトするまでの時間 (秒) が格納されます。
childUpdatePanelIDs | 呼び出して更新された、入れ子になった更新パネルの ID が格納されます。
panelsToRefreshIDs | プログラムで更新されたパネルなど、呼び出し中に更新された更新パネルの ID が格納されます。
formAction | アクション フォームの URL が格納されます。
dataItem | サーバー上で生成された追加情報が格納されます。この情報は、クライアント コンポーネントによって使用されます。
dataItemJson | サーバー上で生成された JSON シリアル化の追加情報が格納されます。この情報は、クライアント コンポーネントによって使用されます。
scriptDispose | スクリプト マネージャの RegisterDispose メソッドを使用して応答に追加された破棄スクリプトが格納されます。指定したスクリプトは、指定した DOM 要素のクライアントで実行されます。
pageRedirect | リダイレクト先の新しい URL が格納されます。
error | ポストバック中に例外が発生した場合に、エラー情報が格納されます。
pageTitle | ページの新しいタイトルが格納されます。
focus | 入力フォーカスを保持する新しいコントロールの ID が格納されます。

図 4 のコードをもう一度見てください。ページのロジックはかなり単純ですが、重要なポイントを示しています。基本的には、ユーザーがボタンをクリックすると、TextBox の内容を使用してラベルが更新されます。

ページには、テキスト ボックス、ボタン、区切り記号、およびラベルをすべてラップする UpdatePanel コントロール 1 つのみが含まれています。論理的には、これで問題ありません。自律的に更新するページ内の領域を識別する必要がある場合は、これが有効な方法です。しかし、この方法で定義すると、領域に含まれるコードの大半は、ポストバック間で更新されないコードです。たとえば、TextBox コントロールと Button コントロールは、このページでは更新されず、<hr> 区切り記号は静的なプレーン テキストです。

適切な方法は、UpdatePanel に Label コントロールのみ含まれるようにすることです。この Label コントロールは、ボタンによってトリガされたポストバックで更新される唯一のサーバー コントロールです。一般に、コントロール間のポストバック関係を慎重に識別する必要があります。基本的には何が何を更新するのかを調べます。また、特定のユーザー アクションに対するコントロールの数が最小限になるように更新可能なパネルを設計することが必要です。図 4 に示すサンプルのコンテキストでは、次の方法が適切です。

<asp:TextBox runat="server" ID="TextBox1" />
<asp:Button runat="server" ID="Button1" Text="Update"
     OnClick="Button1_Click" />
<hr />
<asp:UpdatePanel runat="server" ID="UpdatePanel1">
   <ContentTemplate>
       <asp:Label runat="server" ID="Label1" />
   </ContentTemplate>
   <Triggers>
      <asp:AsyncPostBackTrigger ControlID="Button1" />
   </Triggers>
</asp:UpdatePanel>

この例では、UpdatePanel コントロールには Label コントロールしか含まれていません。つまり、TextBox コントロールと Button コントロールについては、変更されていないマークアップは返されません。既定では、UpdatePanel コントロールが更新されるのは、その子コントロールのいずれか 1 つでポストバックが発生するか、またはページ内の別の UpdatePanel が更新されるときです。この動作は、UpdateMode、ChildrenAsTriggers、Triggers などのパブリック プロパティで変更できます。

特に、UpdateMode プロパティでは、UpdatePanel を無条件で更新するか (既定)、または特定の条件を満たした場合のみ更新するかを指定できます。条件付き更新を行う場合は、まず UpdateMode を Conditional に設定します。これは、更新可能なパネルを最適化するうえで重要な設定です。

<asp:UpdatePanel runat="server" ID="UpdatePanel1"
     UpdateMode="Conditional">

ただし、条件付き更新が適切なのは、ページに複数のパネルがある場合だけです。

ページを正規化された複数のパネルに分割するのは困難な場合があります。そのような場合には、子がポストバックをトリガしないようにする手法が役立ちます。ChildrenAsTriggers プロパティは、パネルの子コントロールを AJAX ポストバック イベントのトリガとして機能させるかどうかを指定する、ブール型プロパティ (既定では true) です。次のコードでは、Button1 コントロールによってパネルが更新されないようにしています。

<asp:UpdatePanel runat="server" ID="UpdatePanel1"
     ChildrenAsTriggers="false">
   <ContentTemplate>
       <asp:Button runat="server" ID="Button1" Text="Update"
          OnClick="Button1_Click" />
       <asp:Label runat="server" ID="Label1" />
   </ContentTemplate>
</asp:UpdatePanel>

このように構成されたパネルで子ボタンをクリックすると、AJAX ポストバックが発生して、そのボタンに関連付けられているポストバック コード (たとえば、Button1_Click メソッド) が実行されはしますが、マークアップは返されないためパネルは更新されません。

条件付き更新に関連する 3 つ目のプロパティは Triggers です。これはコレクション プロパティで、パネルの更新を実行するコントロール イベントのリストを示します。

<asp:UpdatePanel runat="server" ID="UpdatePanel1">
   <ContentTemplate>
       <asp:Label runat="server" ID="Label1" />
   </ContentTemplate>
   <Triggers>
      <asp:AsyncPostBackTrigger ControlID="Button1"
           EventName="Click" />
   </Triggers>
</asp:UpdatePanel>

このコード例では、Button1 の Click イベントによってパネルが更新されます。Button1 コントロールはページ内のどこにでも配置できます。ここではサーバー イベントについて説明していることに注意してください。つまり、ユーザーがドロップダウン リストで選択内容を変更したときにパネルを更新する場合は、まず AutoPostBack 属性をドロップダウン リストに追加する必要があります。

AJAX ポストバック中にサーバーで状態変更イベントが発生したときにパネルを更新するように構成することもできます。次のコードについて考えます。

<asp:TextBox runat="server" ID="TextBox1" />
<asp:UpdatePanel runat="server" ID="UpdatePanel1">
   <ContentTemplate>
       <asp:Label runat="server" ID="Label1" />
   </ContentTemplate>
   <Triggers>
      <asp:AsyncPostBackTrigger
           ControlID="TextBox1"
           EventName="TextChanged" />
   </Triggers>
</asp:UpdatePanel>

ラベルは、TextBox1 の内容が変更されるたびに更新されます。

Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles TextBox1.TextChanged
   Label1.Text = TextBox1.Text
End Sub

TextChanged イベントおよびその他の状態変更イベントは、サーバー側のイベントで、ポストバックの結果として発生します。ラベルを更新するには、ページにボタンまたは自動ポストバック コントロールを含めて、ポストバックをトリガする必要があります。

図 7 のグラフは、更新可能なパネルに最適化手法を適用することで得られる可能性がある利点をまとめたものです。部分的なレンダリングでは、要求パケットのサイズが少し大きくなりますが、まったく最適化されていない場合でも、返される応答のサイズは従来の ASP.NET シナリオより小さくなります。もちろん、ページがちらつくこともありません。

図 7 更新可能なパネルの最適化の利点
図 7 更新可能なパネルの最適化の利点(画像を縮小するには、ここをクリックします)
図 7 更新可能なパネルの最適化の利点
図 7 更新可能なパネルの最適化の利点 (画像を拡大するには、ここをクリックします)

ただし、viewstate のサイズが非常に大きいページでは、部分的なレンダリングの利点があまり得られないことがあります。たとえば、5 KB のマークアップを節約できても、30 KB の viewstate を送信する必要がある場合は、部分的なレンダリングの効果はわずかです。

ただし、viewstate よりも、他の場所で発生した問題の方が大きく影響している場合もあります。たとえば、ASP.NET ページにカレンダー コントロールがあり、更新可能なパネルを使用して更新を常にスムーズに行えるようにするとします。

<asp:UpdatePanel runat="server" ID="UpdatePanel1">
   <ContentTemplate>
       <asp:Calendar runat="server" ID="Calendar1" />
   </ContentTemplate>
</asp:UpdatePanel>

このコード例のマークアップは 7 KB 以上で、スタイルを追加すると 10 KB まで増大する可能性があります。カレンダーの viewstate を無効にしても、合計サイズのわずか 10% に相当する約 1 KB のデータしか節約できません。これは、Calendar がマークアップの観点で負荷の大きいコントロールだからです。viewstate は、このコントロールとほとんど関係がありません。ASP.NET AJAX ページでは、日付選択機能に Calendar コントロールを使用しないことをお勧めします。代わりに、AJAX Control Toolkit に用意されているエクステンダ (CalendarExtender コントロール) を使用します。これをプレーンな TextBox に追加すると、日付選択を行う際に 10 KB のマークアップを効果的に節約できます。

次のコードは、テキスト ボックスに入力フォーカスが移動するたびにカレンダーをポップアップ ウィンドウに表示します。

<asp:textbox runat="server" ID="TextBox1" />
<act:CalendarExtender ID="CalendarExtender1" runat="server"
    TargetControlID="TextBox1"
    OnClientDateSelectionChanged="updateLabel"
    Format="dd/MM/yyyy" />

<asp:UpdatePanel runat="server" ID="UpdatePanel1"
     UpdateMode="Conditional">
    <ContentTemplate>
        <asp:label runat="server" ID="Label1" />
    </ContentTemplate>
</asp:UpdatePanel>

カレンダーはすべてクライアント上で作成されます。必要なのは小さなアイコンを 2 つダウンロードすることだけです。選択した日付は、エクステンダによって対応するテキスト ボックスに自動的に書き込まれます。ただし、JavaScript コードを少し記述すると、クライアントの日付選択イベントを処理して、必要な処理を実行できます。

部分的なレンダリングの考察

部分的なレンダリングは、AJAX の機能を使用して ASP.NET Web サイトを洗練されたものにする最も簡単な方法です。新しいスキルも新しいアーキテクチャも必要ありません。部分的なレンダリングを行うだけで、既存の Web サイトをさらに優れたものにすることができます。ただし、部分的なレンダリングには、リモート サービスを直接クライアントから呼び出す純粋な AJAX アプローチと比較すると、パフォーマンスが大幅に低下するという問題があります。

経験上、はじめての AJAX バージョンのサイトを手堅く構築したい場合は、その最善のアプローチとして部分的なレンダリングをお勧めします。その後、従来の ASP.NET アーキテクチャからの移行を徐々に行って、バックエンド サービスの構築やプレゼンテーション層の充実に着手します。次回の Cutting Edge では、AJAX のリモート サービス アプローチについて説明します。アーキテクチャの観点からきちんと整理して説明しますが、アプリケーションを完全に設計し直す必要があるかもしれません。また、いくつかの新しい問題も発生します。

ご意見やご感想は、

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Esposito は、Solid Quality Learning の指導者であり、『Programming Microsoft ASP.NET 2.0』(Microsoft Press 発行、2005 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。cutting@microsoft.com (英語) で連絡を取るか、weblogs.asp.net/despos (英語) のブログに参加してください。


チーム システム: 作業項目と取り消しのサポート

チーム システム
作業項目と取り消しのサポート
Brian A. Randell

コードのダウンロード : : TeamSystem2007_09.exe (197KB)
コンテンツ
変更点
作業項目のサポート
作業項目のクエリを返す
[Check-In] ダイアログを更新する
最後に

2007 年 1 月のこのコラム (msdn.microsoft.com/msdnmag/issues/07/01/TeamSystem) では、Team Foundation Server バージョン管理サブシステムと連携する Microsoft® Word 2003 アドインの作成方法を説明しました。2007 年 4 月のコラム (msdn.microsoft.com/msdnmag/issues/07/04/TeamSystem) では、作業項目トラッキング サブシステムの詳細を説明しました。今月のコラムでは、作業項目のサポートをアドインに追加する方法を説明します。また、アドインの最初のバージョンにはなかった取り消し機能のサポートについても説明します。

変更点

アドインに関する最初のコラムでは、Microsoft Visual Studio® 2005 Tools for the 2007 Microsoft Office System (Visual Studio 2005 Tools for Office Second Edition、略称 VSTO 2005 SE) のベータ リリースを使用しました。その後リリースされた最終バージョンでは、Office 2003 および 2007 Office system 用のアプリケーション アドインの作成をサポートしています。そのため、この記事に従って作業を行う場合は、アドインの初回 "リリース" を RTM バージョンにアップグレードする必要があります。アップグレード手順は、ソリューションを開き、VSTO 2005 SE がインストールされたコンピュータ上で再コンパイルするだけです。新しいバージョンの動作確認をしたら、次に、保留中の変更の取り消しのサポートを追加します。この機能を使用するには、tfsvcUtil クラスと ThisAddin クラスを変更する必要があります。

tfsvcUtil に、現在変更中のドキュメントへの完全パスを受け取り、ブール値を返す新しい共有メソッド UndoPendingChanges を追加します。この新しいメソッドの基本機能は、既存の CheckInDocument メソッドを模倣しています。このメソッドは、Team Foundation Server への有効な接続が確立されていること、および有効なワークスペースが読み込まれていることを確認します。この確認が完了すると、渡されたドキュメントに対する PendingChanges オブジェクトの配列を取得します。保留中の変更が返されたことを前提にして、ユーザーのワークスペースの Undo メソッドが呼び出され、PendingChanges 配列を渡します。このメソッドから整数値 1 が返されるはずです。戻り値が 1 の場合、メソッドは true を返し、それ以外の場合、新しく定義した MSG_UNDO_NOT_ZERO 定数を使用して tfsUtilException がスローされます。図 1 にメソッド一覧を示します。

Figure 1 tfsvcUtil の UndoPendingChanges メソッド (Close Figure 1)
Public Shared Function UndoPendingChanges( _
  ByVal docPath As String) As Boolean
  If serverValidated Then
    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim pc As PendingChange() = _
      m_userWorkspace.GetPendingChanges(docPath)

    If pc IsNot Nothing AndAlso pc.Length > 0 Then
      Dim retval As Integer = m_userWorkspace.Undo(pc)
      ' Retval should equal the number of items 'undone'
      If retval = 1 Then
        Return True
      Else
        Throw New tfsUtilException(String.Format( _
          MSG_UNDO_NOT_ZERO, docPath, retval))
      End If
    Else
      Return False
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

ThisAddIn クラスに加える必要がある変更はきわめて簡単です。今回もスペースの制約上、Word アドイン固有のコードの詳細については説明を省きます (変更内容を確認するには、このコラムのダウンロードと以前のバージョンとを比較してください)。言うまでもなく、新しいボタン [Undo Pending Changes] をツールバーに追加し、このボタンのクリック ハンドラを追加し、状態変更を処理してボタンの有効化/無効化を制御するコードを追加するには、クラスを変更する必要があります。基本的に、[Undo Pending Changes] ボタンの有効/無効の状態は、既存のチェックイン ボタンの有効/無効の状態を反映する必要があります。

作業項目のサポート

作業項目とチェックインの関連付けのサポートを追加するには、3 つの主要な変更を行う必要があります。まず、tfsvcUtil クラスを変更して作業項目ストアへの接続をサポートします。次に、CheckInDocument メソッドを変更して作業項目の関連付けを行います。最後に、既存の frmCheckIn ダイアログを変更して、チェックインの一環としてユーザーが選択する作業項目の一覧表示をサポートします。

作業項目をプログラミングするには、Microsoft.TeamFoundation.WorkItemTracking.Client.dll アセンブリへの参照を TFSUtil プロジェクトから追加する必要があります。以前のコラムで説明したように、チーム エクスプローラでは、既定で、サポートするアセンブリがグローバル アセンブリ キャッシュ (GAC) にインストールされます。ただし登録は行われないので、Visual Studio 2005 の [参照の追加] ダイアログには表示されません。レジストリを変更して [参照の追加] ダイアログにアセンブリが表示されるようにするか、手動でアセンブリを参照する必要があります。アセンブリは %Program Files%\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies\ にあります。

参照を追加したら、tfsvcUtil クラスを変更し、ソース ファイルの先頭に次の Imports ステートメントを追加する必要があります。

Imports Microsoft.TeamFoundation.WorkItemTracking.Client

現在のところ、アドインを Team Foundation Server ボックスに接続したときに、接続されるのは、バージョン管理サービスのみです。既存の接続メソッドを変更して、作業項目ストアに接続されるようにすることも可能ですが、アドインのユーザーが作業項目を関連付けなければ、接続メソッドに不要なオーバーヘッドが付加される場合があります。そこで、代わりに、別に呼び出される新しいメソッドを追加して作業項目ストアに接続します。このメソッドをパブリックにしても、多くの場合、作業項目関連の処理を行う前に既存のメソッドからこのメソッドが呼び出されます。作業項目ストアへの接続が事前に確立されていれば、このメソッドでは何も行いません。

新しい共有メソッド ConnectWIS を tfsvcUtil クラスに追加する必要があります。このメソッドはパラメータを受け取らず、値を返しません。メソッド本体は単純です。まず、有効な Team Foundation Server インスタンスが存在するかどうかを確認します。ユーザーはこのメソッドを呼び出す前に接続を開始する必要があるため、有効なインスタンスが存在しない場合は例外がスローされます。ただし、アドインのアーキテクチャを変更して、ユーザーの既定の Team Foundation Server インストールへのログインをサポートすることが可能です。サーバーが使用可能な場合、TeamFoundationServer オブジェクトとして型指定された m_tfs インスタンスの GetServiceMethod メソッドを使用して、作業項目ストアへの接続が確立されます。

Public Shared Sub ConnectWIS()
  If Not m_tfs Is Nothing Then
    If tfsvcUtil.m_tfsWIS Is Nothing Then
      tfsvcUtil.m_tfsWIS = CType( _
        m_tfs.GetService(GetType(WorkItemStore)), WorkItemStore)
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Sub

ご覧のように、作業項目ストアの参照が、WorkItemStore オブジェクトとして型指定されたクラスレベル変数 m_tfsWIS にキャッシュされています。この変数を次のようにクラスに追加する必要があります。

Private Shared m_tfsWIS As WorkItemStore = Nothing

変数を追加したら、既存バージョンに一致するように CheckInDocument メソッドのオーバーロード バージョンを作成して、WorkItemCheckinInfo インスタンスの配列を受け取る入力パラメータを追加する必要があります。次に、既存メソッドの本体を切り取って、新しいメソッドに貼り付けます。元のメソッドを変更し、WorkItemCheckinInfo 配列に Nothing を渡して新しいバージョンを呼び出すようにします。変更後のバージョンは、次のような 1 行のコードになります。

Return tfsvcUtil.CheckInDocument(docPath, comment, Nothing)

次に、CheckInDocument のコードを変更して、WorkItemCheckinInfo オブジェクトがメソッドに渡されたかどうかを確認する必要があります。その場合、コード内で ConnectWIS メソッドを呼び出して、作業項目ストアに接続されたことを確認します。この処理が完了したら、チェックイン メモおよびポリシー オーバーライドに Nothing を渡して、WorkItemCheckinInfo インスタンスの配列を受け取る CheckIn のオーバーロード バージョンを呼び出します。図 2 に、完全なコード リストを示しています。

Figure 2 作業項目をサポートするチェックイン (Close Figure 2)
Public Shared Function CheckInDocument( _
  ByVal docPath As String, ByVal comment As String, _
  ByVal SelectedWorkItems() As WorkItemCheckinInfo) _
  As Integer

  If serverValidated Then
    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim pc As PendingChange() = _
      m_userWorkspace.GetPendingChanges(docPath)

    If pc IsNot Nothing AndAlso pc.Length > 0 Then
      If SelectedWorkItems IsNot Nothing _
        AndAlso SelectedWorkItems.Length > 0 Then

        tfsvcUtil.ConnectWIS()

        Return m_userWorkspace.CheckIn( _
           pc, comment, Nothing, SelectedWorkItems, Nothing)
       Else
         Return m_userWorkspace.CheckIn(pc, comment)
      End If
    Else
      Return -1
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

作業項目のクエリを返す

この時点で、作業項目関連のサポートはチェックイン プロセスに追加されていますが、さらに tfsvcUtil クラスを拡張して、現在のチーム プロジェクトに関する作業項目クエリのリストを返す処理をサポートする必要があります。また、クエリを実行して呼び出し元に作業項目を返すメソッドを用意し、呼び出し元がその作業項目を統合されたチェックイン エクスペリエンスの一環としてユーザーに表示することができるようにすることも必要です。

それには、2 つのメソッドを追加します。1 つは、StoredQueriesCollection を返す GetWIQ メソッドです。もう 1 つは、クエリ名を文字列パラメータとして受け取り、WorkItemsCollection オブジェクトを返す RunWIQ メソッドです。これらのメソッドの使い方については、2007 年 4 月のコラム (msdn.microsoft.com/msdnmag/issues/07/04/TeamSystem) で詳しく説明しています。図 3 に、コード例を示しています。

Figure 3 GetStoredQueries メソッドと RunWIQ メソッド (Close Figure 3)
Public Shared Function GetStoredQueries(ByVal docPath As String) _
  As StoredQueryCollection

  If IsServerReady Then
    tfsvcUtil.ConnectWIS()

    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim vcTeamProject As TeamProject = _
      m_userWorkspace.GetTeamProjectForLocalPath(docPath)

    m_wisProject = m_tfsWIS.Projects(vcTeamProject.Name)

    Return m_wisProject.StoredQueries()
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

Public Shared Function RunWIQ(ByVal docPath As String, _
  ByVal WIQ As StoredQuery) As WorkItemCollection

  Dim wiqlToExecute As String = WIQ.QueryText.ToLower()
  Dim params As Hashtable = Nothing
  Dim wic As WorkItemCollection = Nothing

  If wiqlToExecute.Contains(MACRO_PROJECT) Then
    If params Is Nothing Then
      params = New Hashtable
    End If
    params.Add(MACRO_PROJECT.Substring(1), WIQ.Project.Name)
  End If
  If wiqlToExecute.Contains(MACRO_ME) Then
    If params Is Nothing Then params = New Hashtable
    params.Add(MACRO_ME.Substring(1), _
      m_tfsVCS.AuthenticatedUser.Substring( _
      m_tfsVCS.AuthenticatedUser.IndexOf("\"c) + 1))
  End If

  If params IsNot Nothing Then
    wic = m_tfsWIS.Query(wiqlToExecute, params)
  Else
    wic = m_tfsWIS.Query(wiqlToExecute)
  End If

  If wic.Count = 0 Then
    Return Nothing
  Else
    Return wic
  End If
End Function

[Check-In] ダイアログを更新する

関連付けられた作業項目をチェックインで処理するメイン コードの作成は完了したので、次に、既存の [Check-In] ダイアログを変更して作業項目の関連付けをサポートする必要があります。まず、既存のフォームを大きくしてサイズ プロパティを 640×480 程度にまで拡大し、レイアウトを調整するスペースを作ります。次に、パネル コントロールをフォームに追加して、pnlComments という名前を付けます。次に、既存のテキスト ボックス txtComment をフォームから切り取り、新しいパネルに貼り付けて、Dock プロパティを Fill に設定します。テキスト ボックスを切り取ると、既存の句が削除されるため、txtComment_TextChanged イベント ハンドラに Handles 句を再追加する必要があります。

左のフォームに追加される grpOptions という名前の GroupBox コントロールが必要です。2 つの RadioButton コントロールをグループ ボックスに追加し、それぞれ rdoComments および rdoWorkItems という名前を付けます。AutoSize プロパティを False に変更し、Appearance プロパティを Button に変更します。rdoComments オプション ボタンの Checked プロパティを True に設定します。最後に、フォームを図 4 のように配置します。

図 4 コメント ページで変更後の [Check-In] ダイアログ
図 4 コメント ページで変更後の [Check-In] ダイアログ(画像を縮小するには、ここをクリックします)
図 4 コメント ページで変更後の [Check-In] ダイアログ
図 4 コメント ページで変更後の [Check-In] ダイアログ (画像を拡大するには、ここをクリックします)

基本的な UI を変更したら、[Work Items] 切り替えボタンがクリックされたときに作業項目をグリッドに表示する機能を追加する必要があります。パネル コントロールを pnlWorkItems という名前のフォームに追加します。パネル内に、2 つのパネル pnlSQCombo と pnlGridHolder を追加します。pnlSQCombo の Dock プロパティを Top に設定し、pnlGridHolder の Dock プロパティを Fill に設定します。次に、pnlSQCombo にラベルとコンボ ボックス コントロールを追加し、pnlGridHolder に DataGridView コントロールを追加する必要があります。コンボ ボックスに cboStoredQueries という名前を付け、グリッドに dgvWorkItems という名前を付けます。スマート タグを使って、グリッドをパネル コントロール内でドッキングします。pnlWorkItems のサイズと位置を pnlComments に合わせて調整し、Visible プロパティを False に設定します。

ユーザーが [Work Items] 切り替えボタンをクリックすると、作業項目を使用可能にするために必要な処理がクリック イベント ハンドラで実行されます。作業項目を常にドキュメントのチェックインに関連付ける必要がある場合を除き、フォームの読み込み時に余分なオーバーヘッドを生じることは無意味です。処理には相当量のコードが必要であり、その大半は UI を正しく設定するために必要な汎用的な Windows® フォームのコードです。

[Work Items] 切り替えボタンがクリックされたときの処理の順序は次のとおりです。チェックイン動作コンボ ボックスのデータ ソースを読み込み、グリッドの列を定義し、ストアド クエリを読み込んで、My Work Items クエリを実行し、クエリの結果をグリッド コントロールにバインドします。図 5 に、ユーザーが [Work Items] 切り替えボタンをクリックしたときに呼び出されるコードを示します。

Figure 5 rdoWorkItems_Click イベント ハンドラ (Close Figure 5)
Private Sub rdoWorkItems_Click(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles rdoWorkItems.Click
  ' Load ComboBox Data Sources
  LoadCheckInActionComboBox()

  ' Define Grid Layout
  DefineWorkItemGrid()

  ' Load WIQs
  LoadStoredQueries()
  For Each sq As StoredQuery In msq
    If sq.Name = "My Work Items" Then
      Me.cboStoredQueries.SelectedItem = sq
      Exit For
    End If
  Next

  ' Run WIQ
  LoadGridData()

  Me.pnlComments.Visible = False
  Me.pnlWorkItems.Visible = True
End Sub

rdoWorkItems_Click イベント ハンドラは、処理に必要な複数のプロシージャを呼び出します。LoadCheckInActionComboBox は、グリッドに表示されるチェックイン動作コンボ ボックスのデータ ソースを初期化します。Microsoft の [Check-In] ダイアログを確認すると、選択した作業項目の種類に応じて、チェックイン動作コンボ ボックスに [Associate] のみ、または [Associate] と [Resolve] が表示されます。この動作を模倣するには、フォームでのバックグラウンド処理が必要です。

それには、まず、次のように 2 つのクラスレベルの配列を定義します。

Private listAssociate(0) As String
Private listAssociateResolve(1) As String

次に、選択された作業項目の種類に応じて、チェックイン動作コンボ ボックスのデータ ソースを 2 つの配列間で切り替えます。LoadCheckInActionComboBox で文字列値を配列に読み込みます。

イベント ハンドラで DefineWorkItemGrid が実行され、グリッド コントロールが初期化されます。このメソッドを実行すると、バインドされていないチェック ボックス コントロール、データがバインドされた 4 つのテキスト列 (Work Item Type、Work Item ID、Title、State)、およびチェックイン動作用のバインドされていないコンボ ボックス コントロールが追加されます。次に、rdoWorkItems_Click で LoadStoredQueries が呼び出されます。このメソッドは、先に作成した GetStoredQueries メソッドを呼び出し、結果を cboStoredQueries コンボ ボックス コントロールにデータ バインドします。

rdoWorkItems_Click メソッドのコードは、My Work Items クエリが見つかるまでストアド クエリのリストを検索し、見つかったら、このクエリを cboStoredQueries コンボ ボックス コントロールで現在選択されているクエリとして設定します。次に、このメソッドは LoadGridData を実行し、My Work Items クエリで返された作業項目を取得して、その結果をグリッドにバインドします。最後に、rdoWorkItems_Click はコメント パネルを非表示にし、作業項目パネルを表示します。

グリッドの動作を Microsoft バージョンと同様にするには、2 つのグリッド関連イベントのイベント ハンドラを記述する必要があります。そのイベントの 1 つは CellFormatting です。グリッドの 1 つの列には WorkItem オブジェクトの Type プロパティが表示されます。このプロパティは WorkItemType オブジェクトとして型指定されています。グリッドにこのオブジェクトの Name プロパティを表示したいところですが、ToString メソッドを実行すると、オブジェクトから返されるのは Name プロパティではなく、完全修飾された型名です。Name プロパティを表示するには、CellFormatting イベント ハンドラでセルの値を変更する必要があります。次のコードは、フォーマットする列が [Work Item Type] 列であるかどうかを確認します。

Private Sub dgvWorkItems_CellFormatting(ByVal sender As Object, _
  ByVal e As DataGridViewCellFormattingEventArgs) _
  Handles dgvWorkItems.CellFormatting

  If e.ColumnIndex = typeColumnIndex Then
    If e.Value IsNot Nothing Then
      Dim wit As WorkItemType = CType(e.Value, WorkItemType)
      e.Value = wit.Name
      e.FormattingApplied = True
    End If
  End If
End Sub

[Work Item Type] 列であれば、セルの値は null ではないため、セルから WorkItemType インスタンスが取得され、セルの値は Name プロパティの値に変更されます。

次に必要な処理は、Current-CellDirtyStateChanged イベントのイベント ハンドラの実装です。このハンドラで、選択された作業項目の種類に応じて、チェックイン動作コンボ ボックスのデータ ソースを変更します。このチェック ボックス列の状態が変更されたときだけこのコードを実行するようにします。

データ ソースの内容を確認するため、現在の行にバインドされた WorkItem オブジェクトにアクセスし、GetNextState メソッドに Microsoft.VSTS.Actions.Checkin の文字列値を渡して呼び出します。このコードでは、チェックインから有効な状態変更が行われたかどうかを確認し、有効な状態変更が行われている場合、コンボ ボックスに [Associate] と [Resolve] の両方を表示する必要があることを示す有効な文字列が返されます。有効な文字列が返されなかった場合、[Associate] のみを表示します。このようにして、適切なデータ ソースを設定し、現在の編集をコミットして、チェック ボックスの状態を変更します。

次に、チェック ボックスの値を確認し、値がオン (True) に設定されている場合、チェックイン動作コンボ ボックスは有効化され、アクティブ コントロールになって、編集が開始されます。チェック ボックスがオフ (False) の場合、コンボ ボックスはリセットされます。図 6 に詳細を示します。

Figure 6 完成した CurrentCellDirtyStateChanged イベント ハンドラ (Close Figure 6)
Private Sub dgvWorkItems_CurrentCellDirtyStateChanged( _
  ByVal sender As Object, ByVal e As System.EventArgs) _
  Handles dgvWorkItems.CurrentCellDirtyStateChanged

  Dim c As DataGridViewCell = dgvWorkItems.CurrentCell

  If c.ColumnIndex = checkedColumnIndex Then
    Dim dcb As DataGridViewComboBoxCell = _
      CType(dgvWorkItems(comboBoxColumnIndex, _
      c.RowIndex), DataGridViewComboBoxCell)

    Dim currentRow As DataGridViewRow = dgvWorkItems.Rows(c.RowIndex)
    Dim wi As WorkItem = CType(currentRow.DataBoundItem, WorkItem)

    Dim nextState As String = _
      wi.GetNextState("Microsoft.VSTS.Actions.Checkin")

    If String.IsNullOrEmpty(nextState) Then
      dcb.DataSource = listAssociate
    Else
      dcb.DataSource = listAssociateResolve
    End If

    dgvWorkItems.CommitEdit( _
      DataGridViewDataErrorContexts.CurrentCellChange)

    If CBool(c.Value) Then
      dcb.ReadOnly = False
      dcb.Value = dcb.Items.Item(0)

      dgvWorkItems.CurrentCell = dcb
      dgvWorkItems.BeginEdit(True)
    Else
      dcb.Value = Nothing
      dcb.ReadOnly = True
    End If
  End If
End Sub

最後に、作業項目を処理するため、FormClosing イベントで WorkItemCheckinInfo オブジェクトの配列を作成するコードを追加する必要があります (図 7 を参照)。この配列を作成するには、グリッドの各行のチェック ボックス列がオンの場合に WorkItemCheckinInfo インスタンスを作成します。WorkItemCheckinInfo インスタンスを作成するときに、チェックイン動作を指定します。このメソッドを作成する以外に、フォームにパブリック プロパティ WorkItems を追加して、ダイアログを閉じた後に配列を取得できるようにする必要があります。このプロパティを FormClosing メソッドの最後で設定します。

Figure 7 frmCheckIn ダイアログの FormClosing イベント ハンドラ (Close Figure 7)
Private Sub frmCheckIn_FormClosing( _
  ByVal sender As Object, ByVal e As FormClosingEventArgs) _
  Handles Me.FormClosing

  Dim chkCol As DataGridViewCell
  Dim cboCol As DataGridViewCell

  Dim wiciList As New List(Of WorkItemCheckinInfo)
  Dim cia As WorkItemCheckinAction = WorkItemCheckinAction.None

  For Each row As DataGridViewRow In Me.dgvWorkItems.Rows
    chkCol = row.Cells(checkedColumnIndex)
    If CBool(chkCol.Value) Then
      ' Item is selected
      cboCol = row.Cells(comboBoxColumnIndex)
      Select Case cboCol.Value.ToString()
        Case "Associate"
          cia = WorkItemCheckinAction.Associate
        Case "Resolve"
          cia = WorkItemCheckinAction.Resolve
        Case Else
          cia = WorkItemCheckinAction.None
      End Select

      wiciList.Add(New WorkItemCheckinInfo( _
        CType(row.DataBoundItem, WorkItem), cia))
    End If
  Next

  If wiciList.Count > 0 Then
    Me.WorkItems = wiciList.ToArray()
  Else
    Me.WorkItems = Nothing
  End If
End Sub

最後に

フォームに、ストアド クエリ コンボ ボックスの SelectedValueChanged イベントを処理するコードが追加されていることにお気付きでしょう。作業項目をサポートするアドインに必要な最後の処理は、Word アドインの ThisAddIn クラスの cbbCheckInDoc_Click メソッドを変更することです。変更後のメソッドで行う処理は、ダイアログの新しい WorkItems プロパティにデータが存在するかどうかを確認するだけです。データが存在すれば、新しいバージョンの CheckInDocument メソッドを呼び出します。データが存在しなければ、古いバージョンの CheckInDocument メソッドを実行します。図 8 に、作業項目を処理する完成したチェックイン エクスペリエンスを示します。

図 8 作業項目をサポートする完成した [Check-In] ダイアログ
図 8 作業項目をサポートする完成した [Check-In] ダイアログ(画像を縮小するには、ここをクリックします)
図 8 作業項目をサポートする完成した [Check-In] ダイアログ
図 8 作業項目をサポートする完成した [Check-In] ダイアログ (画像を拡大するには、ここをクリックします)

この時点でアドインを実行すると、ソース コントロールへのドキュメントの追加、コメントおよび関連する作業項目付きのチェックイン、チェックアウト、保留中の変更の取り消しを行うことができます。ここまでで、相当な機能ができあがりましたが、まだ追加の余地はあります。次回のコラムでは、チェックイン メモやポリシー サポートの追加などの興味深い機能について説明します。

ご意見やご質問は、

こちら mmvsts@microsoft.com.

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


基礎: ワークフロー通信

基礎
ワークフロー通信
Matt Milner

コードのダウンロード : : Foundations2007_09.exe (165KB)
コンテンツ
通信のコンテキスト
ホストとの通信
ワークフロー インスタンスとの通信
双方向通信
ローカル通信に関する注意事項
まとめ

ワークフローはシステムと人の間の調整を行うためのものであり、それはワークフロー インスタンス自体の境界を越えた通信を意味します。送信するのが電子メールかインスタント メッセージ (IM) か、ASP.NET などのホスト プロセス内のユーザー インターフェイスと直接対話するのか、Web サービスと対話するのかということに関係なく、ほとんどのワークフローで通信が重要なコンポーネントになります。今月は、Windows® Workflow Foundation (WF) の通信アーキテクチャを紹介します。さらに、ワークフローからのデータの送信と実行中のワークフロー インスタンスへのデータの送信についても説明します。また、おまけとして、実行中のワークフローにデータを送信して応答を受ける方法もお見せします。

通信のコンテキスト

Windows WF にアプローチしようとしている開発者の中には、なぜ通信アーキテクチャが存在する必要があるのかと疑問に思っている方もいるかもしれません。結局のところ、ワークフローは実行されている Microsoft® .NET Framework クラスのインスタンスにすぎないのではないか? 他の .NET クラスのメソッドを呼び出し、ワークフロー インスタンス自体のメンバを実行するだけではだめなのか? しかし、いったん Windows WF がワークフロー インスタンスをどのように管理するかについて知れば、この従来の .NET アプローチではうまくいかないことがわかってきます。

ワークフローが .NET オブジェクトで構成されているのは事実ですが、ワークフローは WorkflowRuntime によって管理されてもいます。ワークフローはビジネス ロジックまたはビジネス プロセスをモデル化したものなので、論理的に数日間に及ぶ工程全体にわたって、存続と復元を何度も繰り返しながら存在することもあります。さらに、ワークフローは多くの場合、ホストの処理コードとは異なるスレッド上で実行されるため、ワークフローとの通信ではスレッド化を考慮する必要があります。これらやその他の考慮事項のため、ワークフローでの通信には、ある程度の間接性が必要になります。このことから、対話のための機能の豊富なフレームワークが提供されています。

Windows WF での通信は、多くの場合、ローカル (同じホスト アプリケーション内にあるワークフローとオブジェクトの間の通信) とリモート (ホスト プロセス外部のアプリケーションやサービスが関与する通信) に分類されます。どちらの種類の通信も同じアーキテクチャが基盤になっています。この記事では、そのコア テクノロジに焦点を当てます。このコラムの今後の記事では、Windows WF を Windows Communication Foundation (WCF) と共に使用して行うリモート通信について詳しく説明していきます。

ホストとの通信

独自のワークフローを、ホスト アプリケーション内で実行されているコードと対話させるには、まず、ホスト内のオブジェクトの参照を取得する必要があります。Windows WF では、ワークフロー インスタンスからホスト内のオブジェクトへの通信の管理に、共通 .NET サービス プロバイダ モデルが使用されます (詳細については、msdn2.microsoft.com/system.iserviceprovider を参照してください)。このモデルは、サービス プロバイダ、サービス、およびコンシューマが必要で、.NET Framework の基本クラス ライブラリのさまざまな多くの領域で使用されます。

Windows WF では、WorkflowRuntime がメインのサービス プロバイダで、アクティビティがサービスのプライマリ コンシューマです。サービス自体は開発者が記述するクラスであり、それが WorkflowRuntime に読み込まれます。図 1 に示すように、アクティビティは実行時にサービス プロバイダにサービスを照会し、そのサービスに対する操作を呼び出します。

図 1 ホスト サービスとの通信
図 1 ホスト サービスとの通信

これらがどのように組み合わされて機能するかを見るために、シンプルな通知アクティビティを考えましょう。このアクティビティの目的は、ユーザー インターフェイスに何らかのメッセージを表示することにより、対話しているユーザーにメッセージで警告を送ることです。最初の手順は、アクティビティが使用できるサービス インターフェイスとサービス実装の作成です。図 2 に示したインターフェイスとクラスは、これを使用してユーザーに通知を送ることのできる、非常にシンプルなサービスです。

Figure 2 ユーザーに通知するためのサービス (Close Figure 2)
public interface INotificationService
{
    void SendNotification(string notification);
}

public class ConsoleNotificationService : INotificationService
{
    public void SendNotification(string notification)
    {
        Console.WriteLine(notification);
    }
}

サービスを定義し終えたら、それをサービス プロバイダである WorkflowRuntime に追加する必要があります。通常、これは WorkflowRuntime が作成されたときにホスティング アプリケーション内で行われ、コード内または構成内で実現されます。コードで ConsoleNotificationService をランタイムに追加する例を次に示します。

ConsoleNotificationService notificationService =
    new ConsoleNotificationService();
workflowRuntime.AddService(notificationService);

この時点で、サービスは WorkflowRuntime 内でホストされており、サービスにアクセスしてそれと対話することができるコンシューマに対して利用可能な状態になっています。ほとんどの場合、これらのサービスにはカスタム アクティビティからアクセスすることになります。この記事用のコードのダウンロードには、このサービスのコンシューマとして機能する NotifyActivity というカスタム アクティビティが含まれています。このアクティビティには、ユーザーへの通知に使用するテキストを識別する Message という名前のプロパティが 1 つあります。このアクティビティは、通知サービスに対して操作を呼び出すことができるように、Execute メソッドで通知サービスの参照を取得する必要があります。IServiceProvider インターフェイスは、すべてのサービス プロバイダにより実装されており、GetService という名前の単一のメソッドを定義します。GetService メソッドは、取得するサービスの型を識別する System.Type を単一のパラメータとして受け取ります。渡す型は、サービス クラスの実際の型でも、そのサービスによって実装されているインターフェイスでもかまいません。

アクティビティを記述する場合、サービスへのアクセスを許可する鍵となるクラスは ActivityExecutionContext です。これは、ワークフロー ランタイムがアクティビティ上で呼び出すすべてのメソッドにパラメータとして渡されます。ActivityExecutionContext は、IServiceProvider インターフェイスを実装します。また、ジェネリック ヘルパー メソッドも提供するので、結果をオブジェクトから特定のサービス型にキャストする必要はありません。このメカニズムは、次に示すように NotifyActivity によって使用されます。

INotificationService service =
    executionContext.GetService<INotificationService>();
service.SendNotification(Message);

アクティビティにおいてサービス プロバイダが利用可能な箇所は、アクティビティの Initialize メソッドにもあります。この場合、IServiceProvider パラメータを Initialize メソッドで定義します。これを使用して、アクティビティを初めて初期化するときにサービスにアクセスできます。違いの理由は、CreateWorkflow を呼び出すと、アクティビティが作成され、そのアクティビティの Initialize メソッドが呼び出されますが、ワークフローが実行され始めるまでは ActivityExecutionContext は存在しないことです。IServiceProvider 内で渡すことにより、アクティビティは、ワークフローが実行を開始するより前にサービスと対話できるようになります。

ここまでで、サービスを使用してホスト内のコードと通信する基本的な方法を見てきましたが、このタイプの通信を使用する場合は、心に留めておかなければならない重要な考慮事項がいくつかあります。まず 1 点目は、前述の例ではサービス クラスの 1 つのインスタンスしかランタイムに追加していないため、このサービスはシングルトンとして機能するという点です。ところが、一般には異なる複数のスレッド上でワークフローの複数のインスタンスを実行することになり、それらすべてがサービスにアクセスします。この場合、すぐにスレッド セーフが問題になります。サービス クラス内で状態を管理している場合は、適切な同期セマンティクスを使用していることを確認したいでしょうし、場合によっては個々のワークフロー インスタンスの状態を管理したいことでしょう。後者の場合は、System.Workflow.Runtime.WorkflowEnvironment.WorkflowInstanceId プロパティを使用して、自分のコードを呼び出している現在のワークフロー インスタンスのインスタンス ID を取得することができます。この ID は、状態のコレクションのインデックスとして使用できます。

スレッド化関連で考慮すべき点の 2 つ目は、サービス クラス内のコードはワークフロー インスタンスが実行されるのと同じスレッド上で実行されるということです。構成を変更しないで DefaultWorkflowScheduler を使用すると、コードはホスト スレッドとは異なるスレッドで実行されます。これが原因で問題が発生する最も明白なケースは、ワークフローを Windows フォーム アプリエーション内でホストする場合です。ワークフローがコントロールまたはフォームと対話する場合には、これらの要素に変更を加える箇所で、コントロールの Invoke メソッドおよび委任を使用する必要があります (Windows フォームでのスレッドセーフ プログラミングの詳細については、msdn2.microsoft.com/system.windows.forms.control.invokerequired を参照してください)。

最後に、アクティビティ コードはサービス プロバイダに、具体的なクラス型ではなくインターフェイスに基づいてサービスを要求することに注意してください。アクティビティでインターフェイス ベースのプログラミングを使用することの主な利点は、ホスト アプリケーションに関してアクティビティを中立にできることです。この例ではコンソール ベースの通知を使用していますが、このアクティビティを Windows フォームまたは ASP.NET アプリケーションでホストされるワークフローで再利用するとします。このアクティビティはインターフェイスを使用しているため、ホスト アプリケーションは、ホスト内で使用するためにサービスの適切な実装を挿入することができます。たとえば Windows フォームの場合、INotificationService インターフェイスの実装も行う新しいクラスを記述できますが、クラスは、Console.WriteLine を行う代わりにメッセージ ボックスを表示することもできます。ASP.NET の実装が、メッセージを表示する Web ページにユーザーをリダイレクトする場合もあります。重要な点は、アクティビティはホスティング コードに関連付けられており、ホストがランタイムへのサービスの追加を行うため、アクティビティはこの問題には関知しないということです。

しかし、アクティビティからメッセージを送りたいだけの場合はどうでしょう。本当にこのサービス モデルを使う必要があるでしょうか。結論から言うと、答えは「ノー」です。アクティビティの中で直接コードを記述して、たとえば電子メール メッセージを送信したりできます。しかし、柔軟性を確保するために、アクティビティ内に直接コードを記述するのは避けたいところです。代わりに、ホストが電子メール メッセージ送信の実装の詳細を定義できるようにします。おそらく多くの環境では SMTP が使われますが、環境によっては、アクティビティのコンシューマが別の方法でメッセージを扱う必要があることもあります。サービスを使用し、インターフェイスをコーディングすることにより、アクティビティをより柔軟にすることができます。アクティビティ内に直接コードを記述しない主な理由として、他に、状態を管理する必要がある場合や、オブジェクトをメモリ内に生かしておく必要がある場合が挙げられます。サービスとの通信に使用するサーバーまたは Web サービス プロキシへの接続を検討してみてください。ワークフローに複数のアクティビティがある場合、またはワークフローの複数のインスタンスがあり、それらがその接続を使用する場合は、アクティビティの外部かつワークフローの外部でそれを管理できた方がよいでしょう。ランタイムでホストされるサービスを使用するということは、ランタイムが続いている限りサービス インスタンスが存在するということを意味しますが、ワークフロー インスタンスはそうではありません。

ワークフロー インスタンスとの通信

通信の初期化に加えて、ワークフローは通信を受け取ることもできなければなりません。ワークフロー インスタンス内で通信を受信するメカニズムは、内部キュー システムをベースにしています。つまり、アクティビティは、データが送られてきたときに通知を受け取れるように、キューを作成して登録します。WorkflowRuntime に読み込まれたホスト アプリケーションまたはサービスは、図 3 に示すようにキューにデータを格納することによって、ワークフロー インスタンスとの通信を開始します。

図 3 データをワークフロー インスタンスのキューに格納する
図 3 データをワークフロー インスタンスのキューに格納する

1 つ前の記事 (msdn.microsoft.com/msdnmag/issues/06/12/WindowsWorkflow を参照) では、データが送られてきたときにアクティビティが通知を受け取るために、どのようにしてキューを作成し、登録するのかについて説明しました。そのため、今回は、このプロセスのアクティビティの部分には触れません。通知のためのキューの作成と登録はアクティビティが行う必要があると言うだけで十分でしょう。キューの作成は、そのアクティビティでの必要に応じて、アクティビティが初期化されたときか、またはアクティビティがサブスクライブ/実行されたときに行えます。

ホストの視点からすると、ワークフロー インスタンスに情報を送信するときにいくつかの問題が浮上します。情報の送信先のインスタント ID とキュー名の取得、ワークフロー インスタンスが読み込まれていることの確認などです。場合によっては、情報がワークフロー インスタンスに正常に送信されときの通知も含まれます。ワークフローのインスタンス ID に関しては、この情報を確認するのは、ランタイムに読み込まれたホスト アプリケーションまたはサービスです。ID は、交換されるデータ構造の一部として含めたり、HTTP cookie や SOAP ヘッダーなどのコンテキスト プロパティ内で管理したり、または、独立したデータ ストア内で管理して、ワークフローに送信されたデータに基づいて検索されるようにすることもできます。どの実装を使うかに関係なく、ホストは、インスタンスのハンドルを取得するには必ずワークフローの ID を解決できなければなりません。

インスタンス ID を使用することにより、ホストは、WorkflowRuntime に対して GetWorkflow メソッドを使用して、ワークフロー インスタンスの参照を取得することができます。このメソッドは、WorkflowInstance クラスを返します。これはワークフローを表し、そのワークフローにデータを送信するために使用されるオブジェクトです。GetWorkflow を呼び出すと、ランタイムによって、必要に応じてワークフローが永続ストアから読み込まれていることが確認されるため、プログラムでこの重要な手順を管理する必要がなくなります。

ワークフローと通信するために必要な 2 つ目のデータは、データの配信先となるキューの名前です。1 つのワークフローで同時に複数のアクティビティにデータの到着を待機させることができるため、単一のワークフロー インスタンス上に複数の異なるキューが存在する可能性があります。キュー名には、単純な文字列値ではなく、IComparable インターフェイスが使用されます。これにより、その構成と型に基づいて、アクティビティは、複雑で一意のキュー名を作成することができます。

ホスト アプリケーションのコードは、データの送信先となるキューの名前を知る必要がありますが、インスタンス ID と違ってワークフロー自体が何らかの情報を提供することができます。WorkflowInstance クラスには、ワークフロー インスタンス上で現在利用可能なキューに関する情報を提供するために WorkflowQueueInfo オブジェクトのコレクションを返す GetWorkflowQueueData メソッドが用意されています。

public class WorkflowQueueInfo
{
    public ICollection Items { get; }
    public IComparable QueueName { get; }
    public ReadOnlyCollection<string>
        SubscribedActivityNames { get; }
 }

コードを見るとわかるように、この操作で返される情報には、キューの名前だけでなく、既にキューに格納されている項目のリストや、そのキューにデータが送信されたときに通知を受け取るように登録されているアクティビティも含まれています。多くの場合、この情報を使用して、必要なキューのキュー名を解決できます。

ユーザー インターフェイスの対話方法は、GetWorkflowQueueData を使用するうえで重要です。多くのアプリケーションが、ユーザーがプロセスと対話する方法について、ユーザーに対していくつかのオプションを提示するようになっており、ビジネス プロセスの現在の状態に基づいてそれらのオプションにフィルタをかけることができる必要があります。たとえば、顧客の注文を管理している場合なら、商品を発送した後には返品のために使用するボタンしか表示されないようにしたいものでしょう。このワークフローでは、この種の動作を、戻り値を信号化する要求を待つアクティビティを使用してモデル化します。ホスト アプリケーションは、ワークフロー インスタンスにキュー データを照会することにより、ビジネス プロセスの現在の状態に合わせてユーザー インターフェイスを調整することができます。

インスタンス ID とキュー名の管理についての最後の注意点は、この情報を管理するアクティビティは、ランタイム サービスと組み合わせて使用できることです。アクティビティは、キューを作成し、キュー イベントを受け取るように登録する際、ランタイムのサービスにも登録し、サービスが必要なキーを解決するうえで役立つ、インスタンス ID、キュー名、およびその他のデータを提供することができます (図 4 を参照)。たとえば、アクティビティで Web ページから送られてくるデータが必要な場合は、ページ名とユーザー ID をキーとして使用することでしょう。ホスト (ASP.NET) では、ランタイム サービスに対する操作を呼び出して、ページ名、ユーザー ID、およびキューに送信するデータを渡します。これを受け取ったランタイム サービスは、ページ名とユーザー ID に基づいてインスタンス ID とキュー名を検索して、データをキューに渡すことができます。

図 4 ランタイム サービスへの登録
図 4 ランタイム サービスへの登録

ホストまたはサービスが、ワークフロー インスタンスへのデータをキューに格納するということは既に何度も説明しましたが、これをどのようにして行うかについてはまだ説明していませんでした。WorkflowInstance クラスには、次に示すように、データをキューに格納するための 2 つのメソッドが用意されています。

public void EnqueueItem(IComparable queueName, object item,
    IPendingWork pendingWork, object workItem);

public void EnqueueItemOnIdle(IComparable queueName, object item,
    IPendingWork pendingWork, object workItem);

どちらのメソッドも同じパラメータを受け取ります。パラメータには、キュー名とキューに格納されるオブジェクトがあります (残りの 2 つのパラメータについては、この後すぐに説明します)。ここでは、インスタンス ID とキュー名がどうしてそんなに重要なのかを明らかにしましょう。WorkflowInstance オブジェクトを取得するには、インスタンス ID が必要です。また、そのインスタンスのキューにデータを格納するには、キュー名が必要です。2 つのメソッドの違いは、データのキューへの配信を扱う方法にあります。

EnqueueItem メソッドは、キューが作成されデータの到着を待機している状態にワークフローが達していることを前提として、データのキューへの配信を直ちに試みます。シナリオによっては、これがまさに実行してほしい処理になるでしょう。別の状況として、アクティビティが必要なキューを作成する時点までワークフローが達していない場合は、このような動作によって競合が発生します。GetWorkflowQueueData を呼び出し、キューが作成されるまでチェックを続けることも可能ですが、これは煩雑で面倒です。

EnqueueItemOnIdle メソッドは、このような競合状態を解決することを目的としたメソッドです。データをキューに格納するためにこのメソッドを使用すると、ランタイムはワークフローがアイドル状態になる (現在実行中のすべてのアクティビティが何らかの入力待ち状態になる) まではデータをキューに配信しません。これにより、ホスト アプリケーションに対して、ワークフローがデータを受け取る準備ができている、つまりアクティビティがキューの作成を実際に完了しているということが、高い確率で保証されます。

EnqueueItemOnIdle を使用した場合でも、必要なアクティビティを実行する前に、ワークフローが他のアクティビティを待機している可能性はあります。たとえば、図 5 を見てください。この図は、ReceiveData アクティビティの前に発生する遅延アクティビティを示しています (この記事用のコードのダウンロードから入手できます)。データがこの遅延の前にキューに格納された場合、遅延に関するアイドル イベントは発生しますが、キューはまだ利用できません。このシナリオでは、データをキューに格納するのに代替メソッドを使用しなければならない場合があります。キューへの格納を試みることも 1 つの方法です。ただし、失敗した場合は、少しの時間待ってから再びデータのキューへの格納を試みます。

図 5 複数のアイドル ポイント
図 5 複数のアイドル ポイント

ホストにとっての次の大きな課題は、キューに送信されたアイテムが実際にワークフローに対して実行されたときに通知を受け取ることができるようにする方法です。これには、互いに排他的ではないもののまったく異なる 2 つアプローチ、例外とトランザクションを使用することができます。例外アプローチを使用すると、どちらかのメソッドが呼び出されたときに System.InvalidOperationException をキャッチできます。たとえば、使用している名前のキューが見つからない場合は、例外によってそのことが伝えられます。例外の Message プロパティは、操作がなぜ失敗したかについて何らかの意味のある理由を提供します (意味があるのは、ユーザーにとってではなく開発者にとってです)。例外は常時にキャッチする必要があります。それによって、ホストは、データがワークフローに到達しなかったことを知ることができます。

トランザクションを使用して、メッセージがキューに正常に配信されてワークフローが現在のトランザクションをコミットし終えたときに通知を受け取るようにすることができます。これが、EnqueueItem および EnqueueItemOnIdle に渡すパラメータの残りの 2 つが関与する部分です。IPendingWork インターフェイスは、ワークフローの作業バッチに追加されてトランザクションに参加することができるクラスを識別します。workItem パラメータは、IPendingWork を実装するクラスが、トランザクションをコミットまたは完了するときに使用できる状態またはデータです。図 6 に、IPendingWork インターフェイスを実装しており、データがキューに配信されたときに通知を受け取るために使用できるシンプルなクラスを示します。

Figure 6 データ配信の通知の取得 (Close Figure 6)
public class EnqueuePendingWorkItem : IPendingWork
{
    private ManualResetEvent resetEvent;
    private bool result;

    public bool Success
    {
        get { return result; } set { result = value; }
    }

    public void WaitForCommit()
    {
        if (resetEvent != null) resetEvent.WaitOne();
        else throw new InvalidOperationException("Already completed");
    }

    public EnqueuePendingWorkItem()
    {
        resetEvent = new ManualResetEvent(false);
    }

    public void Commit(System.Transactions.Transaction transaction,
        System.Collections.ICollection items)
    {
       //option to do work here using the workflow's tx
       Console.WriteLine("Commit called on pending work item");
    }

    public void Complete(bool succeeded,
        System.Collections.ICollection items)
    {
        Success = succeeded;
        resetEvent.Set();
        resetEvent = null;
    }

    public bool MustCommit(System.Collections.ICollection items)
    {
        return true;
    }
}

IPendingWork インターフェイスには、継承インターフェイスによって実装される必要のあるメソッドとして Commit、Completed、MustCommit の 3 つがあります。Commit メソッドでは、ワークフローのトランザクションの中で作業を完了させることができます。Completed メソッドは、トランザクションの結果を示すクラスへのコールバックです。最後の MustCommit メソッドによって、IPendingWork アイテムは、自身が現在コミットされなければならないかどうかを示すことができます。この最後のメソッドによる影響についてもう少し詳しく説明しましょう。

次のコードでは、このクラスを使用するためにこれを EnqueueItem メソッドの呼び出しに追加してから、WaitForCommit メソッドを使用してトランザクションが完了したことを知ることができるようにしています。これにより、成功したか失敗したかを調べることができます。

EnqueuePendingWorkItem workItem = new EnqueuePendingWorkItem();
instance.EnqueueItemOnIdle(
    "ReceiveData:FirstInput", inputs, workItem, "");
workItem.WaitForCommit();

ワークフローと同じトランザクション内でアクションを取ることができるため、特に、永続化サービスを使用する場合に、ホスト側でトランザクション内でワークフローと一貫した手順を実行することができます。ただし、トランザクションを使用してデータをキューに送信する場合は、トランザクションがいつコミットされ、データが処理されたことをいつ知るかということについて理解することが重要です。図 7 の表に、永続化サービス、トランザクション スコープ、および MustCommit メソッドを IPendingWork アイテムに対して使用してから、トランザクションが実際にコミットされるときまでの対話を示しています。

Figure 7 キューに格納されたデータでのトランザクションのコミット (Close Figure 7)

TransactionScope | MustCommit | PersistenceService | 結果
False | True | False | OnIdle
False | False | False | Hang
True | True または False | False | エラー
True | True または False | True | EndOfTxScope

この表からわかる重要なことは、ハングする状況を確実に避けるには、作業アイテムが常に MustCommit メソッドから True を返さなければならないということです。永続化サービスと TransactionScope を使用すると、ワークフローがアイドル状態になるより前にトランザクションに強制的にコミットさせることができます。ワークフローでのトランザクションの詳細については、msdn.microsoft.com/msdnmag/issues/07/06/CuttingEdge を参照してください。

双方向通信

今までの説明でお気付きかもしれませんが、ワークフローからサービス操作を呼び出して応答の値を受け取ることはできるものの、データを直接ワークフローに送信して返された応答を受け取る方法はありません。キューへの格納は一方向操作です。ところが、ワークフローにデータを送信してデータを処理させ、ホスト アプリケーションで使用できる応答を受け取りたいことはよくあります。たとえば、ASP.NET アプリケーションでは、ページの処理の一部としてワークフローにデータを送信し、応答を待ち、ワークフローから返されたデータをページ内で使用したりロジックを表示したりできます。

これを行うには、ワークフロー内のカスタム アクティビティにオブジェクトを送信する方法と、アクティビティがオブジェクトを更新したことを知らせてくるまで待機する手段が必要です。これを行う最も簡単な方法は、ワークフローに入力を提供し、返される必要のある情報を格納するプロパティを持つ、メッセージまたはデータ構造を送信することです。このメッセージは、カスタム アクティビティが結果を反映してオブジェクトを更新したことをホストに知らせるための手段も備えている必要があります。

MSDN® Magazine の Web サイトから入手できるこの記事用のコードのダウンロードには、このタイプのパターンを使用してデータをワークフローに送信してから応答を待つ方法を示す RequestResponseData アクティビティが含まれています。図 8 に、ホストによってキューに格納され、アクティビティによって更新されてから、出力が取得できる状態になっていることをホストに伝えるために使用される TwoWayMessage クラスを示します。

Figure 8 要求 - 応答メッセージ クラス (Close Figure 8)
[Serializable]
public class TwoWayMessage
{
    ManualResetEvent resetEvent;
    bool isResponse;

    public TwoWayMessage()
    {
        resetEvent = new ManualResetEvent(false);
        isResponse = false;
    }

    private Dictionary<string,string> input;
    public Dictionary<string,string> InputValues
    {
        get { return input; } set { input = value; }
    }

    private Dictionary<string,string> output;
    public Dictionary<string,string> OutputValues
    {
        get { return output; } set { output = value; }
    }

    public void SendResponse(Dictionary<string, string> returnValues)
    {
        OutputValues = returnValues;
        this.isResponse = true;
        resetEvent.Set();
    }

    public void WaitForResponse()
    {
        resetEvent.WaitOne();
        resetEvent = null;
    }
}

図からわかるように、このクラスは、値の入力と出力を可能にし、さらに ManualResetEvent を使用して、応答が設定されたときにアクティビティがそれを知らせることを可能にしています。呼び出し側であるホストは、WaitForResponse メソッドを使って、出力値が設定されるまでの間、そのスレッドをブロックします。一方、アクティビティは SendResponse メソッドを使って、出力値を設定し、完了したことを通知します。サンプル コードの RequestResponseData アクティビティは、このサンプルのアクティビティ実装を提供します。これは、データを受け取り、そのすべての子を順に実行し、最後に応答を返す複合アクティビティです。図 9 に、ワークフロー内で使用されているこのアクティビティを示します。アクティビティ内に含まれているコード アクティビティは、出力値を設定するロジックを表します。

図 9 要求 - 応答
図 9 要求 - 応答

ローカル通信に関する注意事項

観察力の優れた読者の方は、しばしば Windows WF でのローカル通信と呼ばれるものが何なのかについて私がまだ説明していないことにお気付きかもしれません。ローカル通信とは、この記事で今まで説明してきた内容の大部分を抽象化する一連のアクティビティとランタイム サービスのことを言います。これらのアクティビティの目的は、対話の相手となるランタイム サービスである ExternalDataExchangeService と共に、通信アーキテクチャの上によりシンプルな .NET 開発モデルを提供することです。開発者は、CallExternalMethod アクティビティと HandleExternalEvent アクティビティを使用して、通信およびホストとの間での送受信の通信のための .NET メソッドとイベントをモデル化します。場合によっては、これが最もシンプルなオプションであることもありますが、この記事の目標は、制限があることが判明するかもしれない単一の抽象化ではなく、通信のコア アーキテクチャの理解を助けることです。Web 上には、これらのアクティビティを使った例や、ここで説明した通信アーキテクチャの上に構築された ExternalDataExchange サービスの例がいくつもあります。

まとめ

Windows WF の通信機能は、システムおよび人と対話する実用的なワークフローを構築するために非常に重要です。通信アーキテクチャを理解することは、トラブルシューティングに要する時間を節約し、一般的な例に当てはまらない難しい問題を解決することも可能にします。

ご意見やご質問は、

こちら instinct@microsoft.com.

まで英語でお送りください。
Matt Milner は、.NET、Web サービス、Windows WF、WCF、BizTalk Server などのマイクロソフト テクノロジを専門とする独立系ソフトウェア コンサルタントです。Matt は、Pluralsight のインストラクタとして、Workflow、BizTalk Server、および WCF のコースで教鞭をとっています。夫人の Kristen と 2 人の息子とミネソタ州に居を構えています。


セキュリティに関するブリーフィング: 要求に基づく ID の詳細

セキュリティに関するブリーフィング
要求に基づく ID の詳細
Keith Brown

コードのダウンロード : : SecurityBriefs2007_09.exe (206KB)
コンテンツ
共通点を探す
従来のクライアント識別情報の表現
ClaimSet オブジェクトと Claim オブジェクト
身元証明データの変換と IAuthorizationPolicy インターフェイス
セキュリティ トークン サービス
信頼とフェデレートされた識別情報
次回お会いできるのを楽しみにしています。

ほとんどの企業アプリケーションでは、基本的なユーザー セキュリティ機能が必要になります。

ユーザー認証機能は必須です。また、多くの企業アプリケーションでは、特定機能の利用を制限し、特権ユーザーだけがそれらの機能を利用できるようにする必要があります。中には、ユーザーによる処理を監査しなければならない企業アプリケーションもあります。Windows® を使用している場合、これらのセキュリティ機能は OS に組み込まれており、また、通常はアプリケーションにも簡単に統合することができます。統合 Windows 認証を利用すると、ユーザーが独自の認証プロトコルを開発したり、ユーザー データベースを管理したりする必要はありません。また、アクセス制御リスト (ACL)、偽装、および各種の機能 (グループなど) を利用すると、非常に短いコードを記述するだけで承認処理を実装できます。これらのアドバイスは、どの OS にも当てはまります。ほとんどの場合、セキュリティ機能を自社開発するよりも、ご使用の OS に備わっているセキュリティ機能を利用することをお勧めします。

Windows アカウントを持っていないユーザーもカバーするには、どうすればよいでしょうか。また、そもそも Windows を使用していないユーザーの場合はどうでしょうか。このようなユーザーもカバーする必要のあるアプリケーションは増えていますが、従来型のアドバイスでは、このようなニーズに応えることができません。この記事では、Microsoft® .NET Framework 3.0 で導入された新しい識別情報モデルについて説明します。このモデルは、このような問題に対処するのに役立ちます。

共通点を探す

Windows の場合、企業アプリケーションにアクセスする際に使用される最も一般的な資格情報は、ユーザーのドメイン アカウントです。統合 Windows 認証を使用しているアプリケーションは、クライアントを表す Kerberos チケットを受け取ります。一方、Secure Sockets Layer (SSL) を使用しているアプリケーションは、クライアントに関する X.509 証明書を受け取ることがあります。Kerberos チケットと X.509 証明書の内容には、大きな違いがあります。また、その内容を解析、検査、および送信する OS コードも、大きく異なります。ただし、Kerberos チケットと X.509 証明書が実際に表しているものをよく見ると、この両方の資格情報の間に多くの共通点があることがわかります。

具体的に説明するため、また、一部の読者が知らない可能性のある用語について説明するため、例を挙げます。この例において Alice は、自分の Windows ドメイン アカウントを使用して購入サービスを利用したいユーザーであるとします。Alice の所属ドメインにあるドメイン コントローラによって Alice が認証され、Kerberos チケットが生成されます。この Kerberos チケットには、一連のセキュリティ ID (SID) が埋め込まれます。これらの SID は、Alice のユーザー アカウントと所属ドメイン グループを表しており、ドメイン コントローラから発行された署名と共に Kerberos チケットに埋め込まれます。識別情報管理分野の言い回しで表現すると、発行者 (ドメイン コントローラ) が対象者 (Alice) にセキュリティ トークンを付与したことになります。Alice はこのセキュリティ トークンを使用して自分の身元を証明できます。

この概念は、Alice が証明書を使用する場合にも当てはまります。証明書も一種のセキュリティ トークンです。証明書の場合、発行者は証明機関、対象者は Alice です。Kerberos チケットと証明書はどちらも、発行者によって作成された、対象者に関する署名付き情報であり、本質は同じです。つまり、信頼された機関が 1 人の対象者の身元を証明しているという点で共通しています。異なるのは証明手段だけです。どちらの署名付き情報も、身元証明データの集まりであると見なすことができます。次のように考えてください。ドメイン コントローラは、Alice の Kerberos チケット内の SID リストに署名することによって、Alice の身元を証明しています。各 SID は身元証明データとなります。一方、証明機関は、Alice の名前と公開鍵に署名することによって、Alice の身元を証明しています。名前と公開鍵は、証明書に埋め込まれている身元証明データの例です。

私が使っている用語をやや学術的であると感じるかもしれませんが、少しの間辛抱してください。なぜなら、これらの抽象概念は実践で非常に役立つからです。この新しい識別情報モデルの目的は、特定種類の資格情報に依存せず、アプリケーションのセキュリティを損なわない方法で、識別情報を抽象化することです。この識別情報モデルを .NET Framework 3.0 でコーディングした場合、Kerberos チケットや証明書だけでなく、Security Assertion Markup Language (SAML) トークンも扱うことができます。これにより、フェデレートされた識別情報などの興味深い識別情報アーキテクチャを構築する道が開けます。

従来のクライアント識別情報の表現

ここまでは抽象概念について説明しました。ここからは具体的に説明します。まず、単純な Windows Communication Foundation (WCF) サービスについて説明します。このサービスは、クライアントから送信された Windows 資格情報または X.509 証明書を受け付けるものです。WCF サービスでは、引数を持たないメソッドが 1 つだけ公開されています。このメソッドがクライアントから呼び出されると、そのクライアントの詳細な識別情報が出力されます。図 1 にこの Hello メソッドを示します。このバージョンでは、.NET Framework 1.0 で導入された従来型の IIdentity インターフェイスを使用して、クライアントの識別情報を検査しています。

Figure 1 Hello メソッド (Close Figure 1)
public void Hello() {
    ServiceSecurityContext sctx = ServiceSecurityContext.Current;
    if (null == sctx) {
        Console.WriteLine("Security context is null."+
                          "User must not be authenticated.");
        return;
    }
    IIdentity id = sctx.PrimaryIdentity;
    Console.WriteLine("Primary identity type: {0}", id.GetType().Name);
    Console.WriteLine("AuthenticationType: {0}", id.AuthenticationType);
    Console.WriteLine("IsAuthenticated: {0}", id.IsAuthenticated);
    Console.WriteLine("Name: {0}", id.Name);
}

このメソッドではまず、呼び出しに対する ServiceSecurityContext を取り込んでいます。WCF ではこの方法で、クライアントの詳細な識別情報をサービスに伝えています。このメソッドでは null であるかどうかを検査しています。null は匿名クライアントを意味します。次に、PrimaryIdentity プロパティを取り込み、その内容を出力しています。Windows 資格情報を使用するようにこのサンプル コードを構成した場合、出力結果は次のようになります。

Primary identity type: WindowsIdentity
AuthenticationType: NTLM
IsAuthenticated: True
Name: GROMIT\Alice

この例における具象型は WindowsIdentity です。WindowsIdentity では、IIdentity よりも豊富な情報が公開されます。WindowsIdentity にダウンキャストすることにより、Alice の所属グループのリストを取得すること、Alice の SID を取得すること、および、その SID を使用して Active Directory® で Alice のユーザー レコードを探すことができます。また、環境によっては Alice を偽装できます。

同じサンプル コードを異なる WCF 構成で実行した場合の例を示します。今度は、クライアントから X.509 証明書が送信されたとします。

Primary identity type: X509Identity
AuthenticationType: X509
IsAuthenticated: True
Name: CN=SampleClient; 33BB8518E4B7...

この例における Name プロパティは、クライアント証明書に埋め込まれている共通名と、その拇印の組み合わせです。詳細な情報 (発行者、有効期限など) が必要な場合は、X509Identity にダウンキャストしてみてください。ただし、このクラスが内部クラスに設定されていることが、すぐにわかります。この制約にもかかわらずダウンキャストできたとしても、WindowsIdentity にダウンキャストした場合とは大きく異なるプログラミング モデルを扱うことになります。これは、新しい識別情報モデルが抱えている問題の 1 つです。なぜなら、この識別モデルでは、クライアントから受け取る資格情報の種類にかかわらず、プログラミング モデルは 1 つだからです。次項では、Hello メソッドのサンプルを修正し、身元証明データを認識できるようにしています。身元証明データ認識機能によって識別情報がどのように見えるかを確認してください。

ClaimSet オブジェクトと Claim オブジェクト

図 2 は、.NET Framework 3.0 で新たに導入された識別情報モデルである System.IdentityModel 内のクラスを使用して、図 1 の Hello メソッドを修正したものです。このモデルでは、一歩下がって、対象者、発行者、および身元証明データを抽象化することによって、識別情報を扱っています。発行者は、対象者の身元を証明し、身元証明データに署名してセキュリティ トークンを作成します。特定の発行者から発行された各情報は、ClaimSet としてモデル化されます。ClaimSet は、Claim オブジェクトとその Issuer プロパティの集まりです。Issuer プロパティも ClaimSet です。

Figure 2 System.IdentityModel を使用した Hello メソッド (Close Figure 2)
public void Hello() {
    ServiceSecurityContext sctx = ServiceSecurityContext.Current;
    if (null == sctx) {
        Console.WriteLine("Security context is null." +
                          "User must not be authenticated.");
        return;
    }
    AuthorizationContext actx = sctx.AuthorizationContext;

    Console.WriteLine();
    Console.WriteLine("HelloService.Hello sees {0} claim set(s):",
        actx.ClaimSets.Count);
    Console.WriteLine();

    foreach (ClaimSet cs in actx.ClaimSets) {
        Console.WriteLine("------------------------------");
        displayClaimSet(cs);
    }
}

const string CLAIM_FORMAT_STRING = "{0, -5} {1, -15} {2}";

private void displayClaimSet(ClaimSet cs) {
    Console.WriteLine();
    Console.WriteLine(CLAIM_FORMAT_STRING,
        "RIGHT", "CLAIM_TYPE", "RESOURCE");

    // display claims for subject of this claimset
    foreach (Claim c in cs) {
        displayClaim(c);
    }

    Console.WriteLine();
    Console.Write("ISSUED BY: ");

    // display claims for issuer of this claimset
    if (null == cs.Issuer) {
        Console.WriteLine("null");
    }
    else if (object.ReferenceEquals(cs, cs.Issuer)) {
        // self-asserted claims (issuer is the same as subject)
        Console.WriteLine("self");
    }
    else {
        displayClaimSet(cs.Issuer);
    }
}

// print a compact display of the supplied claim
private void displayClaim(Claim c) {
    string right;
    if (c.Right.Equals(Rights.Identity)) {
        right = "ID";
    }
    else if (c.Right.Equals(Rights.PossessProperty)) {
        right = "PP";
    }
    else right = c.Right;

    // make things a little more readable
    string claimType = c.ClaimType.Replace(
        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/",
        ".../");
    string value = stringizeResource(c.Resource);

    Console.WriteLine(CLAIM_FORMAT_STRING, right, claimType, value);
}

private string stringizeResource(object resource) {
    // sometimes the claim's value will simply be a string
    string stringResource = resource as string;
    if (null != stringResource) return stringResource + " (string)";

    SecurityIdentifier sid = resource as SecurityIdentifier;
    if (null != sid) return getAccountName(sid) + " (SecurityIdentifier)";

    X500DistinguishedName dn = resource as X500DistinguishedName;
    if (null != dn) return dn.Name + " (X500DistinguishedName)";

    RSACryptoServiceProvider key = resource as RSACryptoServiceProvider;
    if (null != key) return string.Format(
        "{0} bit RSA public key (RSACryptoServiceProvider)",
        key.KeySize);

    // for anything else, just display the type name
    return string.Format("({0})", resource.GetType().Name);
}

private string getAccountName(SecurityIdentifier sid) {
    try {
        return sid.Translate(typeof(NTAccount)).ToString();
    }
    catch (IdentityNotMappedException) {
        return sid.Value;
    }
}

コードの内容を詳しく説明する前に、出力結果の一部を示します。この出力結果を見れば、ClaimSet が表しているものをイメージできると思います。図 3 を見てください。これは、クライアントで Windows 資格情報が使用されている場合の、修正後の Hello メソッドの出力結果です。図 2 の displayClaimSet ヘルパ メソッドは、ClaimSet 内の身元証明データのリストの出力書式を整え、発行者のチェーンをたどり、各発行者の ClaimSet を順に出力するものです。当然のことですが、Windows 資格情報では、ClaimSet の中身のほとんどは SID です。この出力結果を詳しく見てみると、System.IdentityModel で身元証明データがどのように表されているかがわかります。

Figure 3 修正後の Hello メソッドの出力結果 (Close Figure 3)
HelloService.Hello sees 1 claim set(s):

------------------------------

RIGHT CLAIM_TYPE RESOURCE
ID    .../sid    GROMIT\Alice (SecurityIdentifier)
PP    .../sid    GROMIT\Alice (SecurityIdentifier)
PP    .../name   GROMIT\Alice (string)
PP    .../sid    GROMIT\None (SecurityIdentifier)
PP    .../sid    Everyone (SecurityIdentifier)
PP    .../sid    BUILTIN\Users (SecurityIdentifier)
PP    .../sid    NT AUTHORITY\INTERACTIVE (SecurityIdentifier)
PP    .../sid    NT AUTHORITY\Authenticated Users (SecurityIdentifier)
PP    .../sid    NT AUTHORITY\This Organization (SecurityIdentifier)
PP    .../sid    LOCAL (SecurityIdentifier)
PP    .../sid    NT AUTHORITY\NTLM Authentication (SecurityIdentifier)

ISSUED BY:
RIGHT CLAIM_TYPE RESOURCE
ID    .../sid    S-1-5 (SecurityIdentifier)
PP    .../sid    S-1-5 (SecurityIdentifier)

ISSUED BY: self

Claim クラスの重要なプロパティは、ClaimType、Right、Resource の 3 つです。図 3 には、2 種類の身元証明データ (SID と名前) があります。これらの身元証明データに対する ClaimType プロパティ値は Uniform Resource Indentifier (URI) ですが、displayClaim ヘルパ メソッド内で URI の先頭部分を除去することによって、URI を短縮しています。短縮しない場合、出力結果がかなり見にくくなります。

想定されるすべての種類の身元証明データが一元的に定義されているわけではなく、また、その必要もありません。必要なのは、発行者が使用している身元証明データの種類を使用する側 (この例では Hello メソッド) で認識できるということです。実際には、定義されている身元証明データの種類は、コンテキストによって異なります。たとえば、Information Card Profile を読み取る場合、情報カードと併用する目的で定義されている身元証明データの種類は、十数個です。

Claim クラスでは、ClaimType プロパティだけでなく、Right プロパティも公開されています。Right プロパティは通常、必要な身元証明データを探すのに役立ちます。図 3 をもう一度見てください。表示されている SID の中で、先頭の SID だけが対象者の身元 (Alice) を表しています。残りの SID はグループを表しています。出力結果内の RIGHT 列は、Claim.Right の値です。この値は、厳密に言えば displayClaim ヘルパ メソッド内で短縮された URI です。現時点で System.IdentityModel で定義されている Right プロパティ値は、Identity と PossessProperty の 2 つだけです。見てわかるように、WCF では、"この例におけるユーザーの SID は、身元証明データとして使用されるべきである" と見なされます。この出力結果内のすべての身元証明データのうち、ユーザーの識別に使用したいのは SID です。その他の身元証明データのプロパティ値は、すべて PossessProperty です。

Claim.Resource は、身元証明データの値です。図 2 の身元証明データ ダンプ コードには、stringizeResource というヘルパ メソッドがあります。このメソッドは、身元証明データとその種類名の出力書式を整えるものです。Resource プロパティはオブジェクト型なので、身元証明データの種類に応じてほとんどのデータを格納できます。たとえば、図 3 の SID は、.NET Framework 2.0 で導入された SecurityIdentifier クラスのインスタンスによって表されます。

簡単に言うと、Claim クラスは、身元証明データの種類と値を表します。また、多くの場合、その身元証明データを使用して対象者または発行者を一意に特定できるかどうか、を判断する手掛かりとなります。ClaimSet は、これらの Claim オブジェクトと、発行者の ClaimSet への参照の集まりです。図 3 で、発行者が SID S-1-5 で表されている点に注目してください。この SID は、"NT 権限" を表します。"NT 権限" は基本的に OS を意味します。この例では、Alice はローカル アカウントを使用しています。Alice がローカル アカウントを使用していない場合、発行者は Alice の所属ドメインの SID によって特定されます。

発行者チェーンをたどると、自身を参照している (つまり、自身の ClaimSet を指している) 発行者に行き着きます。図 2 の displayClaimSet メソッドは、Object.ReferenceEquals を使用してこのことを調べる方法を示しています。自身を参照している発行者に行き着くのは、ルート証明機関 (ドメイン コントローラ、認証機関など) に達したときです。このことに注意する必要があります。そうしないと、発行者チェーンをたどってルート発行者に達したときに、無限ループに陥る可能性があります。

図 4 を見てください。これは、Alice が証明書を送信した場合の、Hello メソッドの出力結果です。この場合、WCF では、Claim.Right が使用され、"その証明書の拇印を使用してクライアントを識別できる" ということが暗示されます。また、私はサンプル コードの中で、自己署名された証明書を使用してごまかしているので、Issuer プロパティは対象者の身元証明データ セットを指しています。これは、身元証明データが対象者自身から発行されたことを意味します。身元証明データを表す際にさまざまなクラスが使用されていますが、全体的な構造は、前述のサンプル コードによく似ています。

Figure 4 Alice が証明書を送信したときの Helllo メソッドの出力結果 (Close Figure 4)
HelloService.Hello sees 1 claim set(s):

------------------------------

RIGHT CLAIM_TYPE      RESOURCE
ID    .../thumbprint  (Byte[])
PP    .../thumbprint  (Byte[])
PP    .../x500distinguishedname CN=SampleClient (X500DistinguishedName)
PP    .../dns         SampleClient (string)
PP    .../name        SampleClient (string)
PP    .../rsa         2048 bit RSA public key (RSACryptoServiceProvider)

ISSUED BY: self

図 5 は、クライアントが情報カードを使用してサービスの認証を受けている場合の、Hello メソッドの出力結果です。この例では、クライアント ユーザーの電子メール アドレスと名が、身元証明データとして送信されています。また、個人情報カードが使用されているので、発行者は RSA 公開鍵によって識別されます。情報カードの詳細、および、個々の ClaimSet の意味については、MSDN® Magazine の 2006 年 10 月号および 2006 年 5 月号で Windows CardSpace について私が執筆した記事 (msdn.microsoft.com/msdnmag/issues/06/10/SecurityBriefs (英語) および msdn.microsoft.com/msdnmag/issues/06/05/SecurityBriefs (英語)) を参照してください。

Figure 5 情報カードを使用した場合の Hello メソッドの出力結果 (Close Figure 5)
HelloService.Hello sees 2 claim sets:

------------------------------

RIGHT CLAIM_TYPE      RESOURCE
PP    .../hash        (Byte[])

ISSUED BY: self
------------------------------

RIGHT CLAIM_TYPE       RESOURCE
PP    .../emailaddress alice@contoso.com (string)
PP    .../givenname    Alice (string)

ISSUED BY:
RIGHT CLAIM_TYPE      RESOURCE
ID    .../rsa         2048 bit RSA public key (RSACryptoServiceProvider)
PP    .../rsa         2048 bit RSA public key (RSACryptoServiceProvider)

ISSUED BY:
RIGHT CLAIM_TYPE      RESOURCE

ISSUED BY: self

身元証明データ認識機能を利用してクライアントの身元を調べるシステムを構築することには、さまざまな利点があります。明らかなメリットとしては、"1 つのプログラミング モデルであらゆる種類のクライアント識別情報を検査できる" という点が挙げられます。ClaimSet を使用してプログラミングを行うだけで済みます。実際には、受け付けるのが証明書の場合と、Windows 資格情報または個人情報カードの場合とでは、想定する身元証明データの種類が異なります。実際には、ClaimSet クラスから派生するクラスがあります。たとえば、X509CertificateClaimSet クラスや WindowsClaimSet クラスなどです。これにより、受け付ける証明書の種類がわかっていれば、プログラミングがやや簡単になります。プログラミング モデルを 1 つにするのは良いことです。ただし、身元証明データの真価がわかるのは、対象者、発行者、および身元証明データの概念を使用して、ほぼすべての種類の認証処理と承認処理をモデル化できることを実感したときです。このことを示すために、身元証明データの変換について説明します。

身元証明データの変換と IAuthorizationPolicy インターフェイス

さまざまな種類の資格情報を受け付ける WCF サービスを構築する必要があるとします。また、役割に基づいて機能の利用を承認するとします。たとえば、"マネージャ" 役割が割り当てられているユーザーは、"事務員" 役割だけが割り当てられているユーザーよりも、多くの機能を利用できます。身元証明データを使用して役割をモデル化できます。また、身元証明データ変換機能を利用して、受け取る可能性のある多様な資格情報形式を扱うことができます。

System.IdentityModel で公開されている IAuthorizationPolicy インターフェイスを使用することにより、身元証明データを自在に変換できます。IAuthorizationPolicy インターフェイスには Evaluate という重要なメソッドがあります。Evaluate メソッドの引数は、次のクラスのインスタンスです。

public abstract class EvaluationContext {
    // Methods
    protected EvaluationContext();
    public abstract void AddClaimSet(
        IAuthorizationPolicy policy, ClaimSet claimSet);
    public abstract void RecordExpirationTime(DateTime expirationTime);

    // Properties
    public abstract ReadOnlyCollection<ClaimSet> ClaimSets { get; }
    public abstract int Generation { get; }
    public abstract IDictionary<string, object> Properties { get; }
}

このコンテキストではさまざまなことができます。ただし、心配な点があります。それは、クライアントから渡された身元証明データ セットを検査してさまざまなマッピングを行い、その後自身の身元証明データ セットを追加できるという点です。たとえば、クライアントが証明書を送信したとします。自社の承認ポリシーにおいて、証明書内の対象者名を役割のセットにマッピングすること、および、使用するアプリケーションを発行者として指定する新しい ClaimSet クラス内で、それらの役割を身元証明データとして公開することができます。クライアント ユーザーから Windows 資格情報が渡された場合は、類似した ClaimSet クラスを使用して、そのユーザーの所属グループを同じ役割にマッピングすることができます。この変換が実行されたら、サービスにおいて、ローカルで発行された ClaimSet を探し、その ClaimSet からユーザーの役割を読み取るだけで済みます。

WCF では、このモデルが十分にサポートされています。サービスの構成ファイルで、ServiceAuthorization 動作を結び付けることができます。この動作は、WCF に対して、サービスのメソッド (Hello など) を呼び出す前に IAuthorizationPolicy インターフェイスの実装コードを呼び出すように指示するものです。これにより、サービスによって受け付けられる大量のクライアント資格情報を扱うコードを一本化できます。役割に基づくセキュリティを使用する予定の場合、一歩進んで、ServiceAuthorization 動作の PrincipalPermissionMode 属性を Custom に設定し、次に、評価コンテキストを使用して、役割が含まれている IPrincipal のインスタンスを指定することができます。これにより、サービスのメソッドで PrincipalPermission 属性を使用し、役割に基づいてアクセス権限を付与することができます。構成ファイル内における ServiceAuthorization 動作の例を次に示します。

<serviceAuthorization principalPermissionMode="Custom">
  <authorizationPolicies>
    <add policyType="MyAuthorizationPolicy, MyAssembly"/>
  </authorizationPolicies>
</serviceAuthorization>

評価コンテキストを使用してカスタム プリンシパルを WCF に伝える方法を、次に示します。

string[] roles = lookupRolesForUser(evaluationContext);
GenericIdentity id = new GenericIdentity(clientName);
evaluationContext.Properties["Principal"] =
    new GenericPrincipal(id, roles);

WCF では、サービスのメソッドが呼び出される前に、指定したプリンシパルに基づいて Thread.CurrentPrincipal が設定されます。この記事では、カスタム プリンシパルを指定する方法だけを説明します。なぜなら、多くの人が PrincipalPermission を使用する方法を好んでいるからです。この方法の便利さに気付くでしょう。身元証明データをもう一度見てください。身元証明データは便利なものです。ユーザー独自の ClaimSet を作成して役割セットを表すには、どうすればよいでしょうか。その方法を示すサンプル コードを抜粋して次に示します。このサンプル コード全体は、MSDN Magazine の Web サイトにあるこの記事のページで入手可能です。

string clientName = getClientNameFromClaims(EvaluationContext);
Claim c1 = new Claim(ClaimTypes.Name, clientName, Rights.Identity);
Claim c2 = new Claim("urn:role", "foo", Rights.PossessProperty);
Claim c2 = new Claim("urn:role", "bar", Rights.PossessProperty);
ClaimSet newClaimSet = new DefaultClaimSet(
    DefaultClaimSet.System, c1, c2, c3);

これが自作コードから発行された内部の身元証明データ セットであることを示すため、よく知られている System という身元証明データ セットを使用しました。System 身元証明データ セットは、Windows の SYSTEM アカウントに対して何の処理も行いません。これは、検索しやすい単純な身元証明データ セットにすぎません。役割を、Right プロパティ値が PossessProperty である身元証明データとして、カスタム URI に追加しているという点に注目してください。皆さんはおそらく、身元証明データ タイプの URI に対して、"urn:role" よりも複雑なものを作成したいでしょう。ただし、System.IdentityModel では役割の身元証明データに対する URI が定義されていないので、ご使用のアプリケーションに適した URI を選ぶことができます。

新しい身元証明データ セットを作成したら、AddClaimSet メソッドを使用して、その身元証明データ セットを評価コンテキストに追加することができます。アプリケーション内のコードでは、自分で発行したこの ClaimSet を探すだけでかまいません。また、ユーザーが認証用として証明書と Windows アカウントのどちらを使用している場合でも、そのユーザーに対する役割のリストが取得されます。

セキュリティ トークン サービス

多様な種類の資格情報を、役割に基づく前述の単純な身元証明データ セットにマッピングする、IAuthorizationPolicy インターフェイスを使用した実装コードを、時間をかけて記述したとします。この機能を必要とするサービスはたくさんあるでしょう。このコードを再利用する方法の 1 つは、このクラスを専用アセンブリに変換し、このクラスを必要とするすべてのサービスに関連付けることです。

もっと興味深い方法は、セキュリティ トークン サービス (STS) を実装するための基礎として承認ポリシーを使用し、ユーザー独自のセキュリティ トークンを発行することです。STS は、WS-Trust という Web サービス プロトコルを使用して、セキュリティ トークンを発行するサービスです。STS では、ある種類のセキュリティ トークンを受け付け、別の種類のセキュリティ トークンを発行することができます。また、変換が必要な身元証明データを扱うことができます。たとえば、クライアント証明書を、アプリケーションが認識可能な役割セットが埋め込まれた SAML トークンにマッピングすることができます。このシナリオの場合、アプリケーションは、ユーザー独自の STS によって発行 (および署名) された ClaimSet を探します。

この記事が執筆された 2007 年 6 月時点では、STS を実装することは可能ですが、簡単ではありません。Microsoft は STS の実装を容易にするように努めているところです。ユーザー独自の STS を実装しているかどうかにかかわらず、STS の実装が簡単になれば、身元証明データを認識するアプリケーションを構築することによって、STS を容易に利用できるようになります。

信頼とフェデレートされた識別情報

WCF などの通信フレームワークでは、暗号技術が使用されています。これにより、セキュリティ トークンの送信者が本当に対象者であること、および、セキュリティ トークン内の身元証明データの署名者がそのセキュリティ トークン内で指定されている発行者であることが、保証されます。ただし、この高度なフレームワークにおいても、発行者をどの程度信頼するかについては、考慮されていません。発行者を信頼しない場合、その発行者が対象者に対して作成した身元証明データを信頼しないことになります。そのため、身元証明データ セットを受信したとき、発行者は常に特定されます。また、身元証明データ セットを処理する際に、発行者を最初に検査する必要があります。

単一の信頼された発行者からのトークンを受け付けるコードは、簡単に記述できます。受信した身元証明データ セットが信頼できる機関から発行されたものであるかどうかを確認するだけです。その後、それらの身元証明データを使用してセキュリティに関する判断を行います。基本的に、ユーザーを役割にマッピングする処理、多様な種類のセキュリティ トークンを扱う処理などの面倒な処理は、STS に任されます。

もっと高度な処理について考えてみましょう。Windows 資格情報と X.509 証明書だけを受け付けるのではなく、信頼されたパートナー企業の STS によって発行された署名付き SAML トークンも受け付ける場合はどうでしょうか。これは、フェデレートされた識別情報の領域に入ります。フェデレートされた識別情報は非常に便利なものです。パートナー企業内の外部ユーザーに対するユーザー アカウントを管理することに悩む必要はありません。その代わりに、それらのパートナー企業から、SAML トークン形式の署名付き情報を受け付けることができます。私は、Active Directory フェデレーション サービスについて執筆した記事 (msdn.microsoft.com/msdnmag/issues/06/11/SingleSignOn) の中で、企業間の信頼関係構築を自動化することのさまざまなメリットについて、説明しています。

フェデレートされた識別情報とは、要するに身元証明データの変換です。パートナー企業の STS によって、クライアント側の操作が簡単になります。具体的には、特定の OS とプラットフォームにおいて、パートナー企業のユーザーにとって最も使いやすい資格情報を入力として受け付けます。たとえば、クライアントで Windows が実行されている場合、STS で Kerberos を使用することにより、ユーザーを自動認証し、SAML トークンを発行することができます。パートナー企業が、他の強力な認証プロトコルを備えた別の OS を使用している場合もあります。ただし、そのパートナー企業の STS では、その認証プロトコルを使用して、ユーザーをシームレスに認証し、SAML トークンを発行することができます。一方、フェデレートされたパートナー企業内のユーザーは、あなたの会社と同じようなアプリケーションを使用している場合でも、シングル サインオンのメリットを享受できます。

あなたの会社で STS を使用すると、アプリケーションの使い勝手が良くなります。STS により、信頼されたパートナー企業から発行された SAML トークンだけが受け付けられ、使用しているアプリケーション用の新しいトークンが発行されます。使用しているアプリケーションは、STS によって発行された身元証明データ セットを受け付けるだけでよいので、処理が簡単です。図 6 に、このフェデレートされたシステムにおけるメッセージの流れを示しますクライアント ユーザーはまず、自社のアカウント STS にアクセスし、SAML トークンを取得します。次に、この SAML トークンをリソース STS に送信し、サービスによって使用される新しい SAML トークンを取得します。

図 6 フェデレートされたシステムにおける認証用メッセージの流れ
図 6 フェデレートされたシステムにおける認証用メッセージの流れ

セキュリティ トークン サービスは、セキュリティ領域間または技術領域間の境界を越えることができます。また、セキュリティ トークン サービスでは、セキュリティ トークンの種類を変換すると同時に、セキュリティ トークン内の身元証明データを変換することができるので、認証ポリシーと承認ポリシーを一元化できる優れた方法であると言えます。

これらのフェデレートされた識別情報モデルについては、今後詳しく説明する予定です。さしあたって、"フェデレートされた識別情報ソリューションは普及しつつある" という私の見解を信用していただければさいわいです。身元証明データ認識アプリケーションを開発中の場合は、このモデルに当てはめることができます。今後のこの分野のさらなる進化にご期待ください。

次回お会いできるのを楽しみにしています。

身元証明データ認識アプリケーションを開発すると、Windows プラットフォームにおける将来の識別情報管理機能に備えることができます。たとえば、従来型のトークン形式 (Windows 資格情報、X.509 証明書など) だけでなく、情報カードを受け付けることができるようになります。また、今後フェデレートされた識別情報が普及してきたときに、フェデレートされた識別情報を実装できます。

身元証明データについて習得するための第 1 ステップは、System.IdentityModel の内容をよく調べることです。System.IdentityModel は、.NET Framework 3.0 で導入された、身元証明データに基づく新しいプログラミング モデルです。この記事に付属しているサンプル コードを使用して、現在ご使用のアプリケーションに身元証明データ認識機能を組み込む方法を確認してください。

ご質問やご意見は

Keith briefs@microsoft.com.

まで英語でお送りください。
Keith Brownは、Microsoft .NET の第一のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Keith は、Pluralsight の Applied .NET Security コースの作成者で、『The .NET Developer's Guide to Windows Security』 (印刷物および Web で提供) をはじめとする数冊の書籍の著者でもあります。詳細については、www.pluralsight.com/keith


.NET の問題: CryptoRandom を使う

.NET の問題
CryptoRandom を使う
Stephen Toub and Shawn Farkas

コードのダウンロード : : NETMatters2007_09.exe (151KB)

質問 System.Random クラスを使ってアプリケーションで乱数を生成しています。同僚にコードをレビューしてもらい、RNGCryptoServiceProvider を使った方がよいのではないかというアドバイスを受けました。このアドバイスに従うつもりなのですが、コード内で Random を使っている箇所をすべて変更する必要はないようにしたいと思っています。ところが、RNGCryptoServiceProvider で公開されているメソッドには Random と同様の機能が見当たりません。この変更を簡単に行う方法はないでしょうか?

お探しのような機能をアダプタと呼びます。デザイン パターンの点では、アダプタはあるクラスの機能を取得し、その機能を他のクラスによって提供される (想定される) インターフェイスに合わせて調整します。今回の場合は、RNGCryptoServiceProvider の機能を Random のインターフェイスに合わせて変更します。さいわい、Random はシールされておらず、パブリック メソッドはすべて仮想です。このことは、Microsoft® .NET Framework の設計者が、インターフェイスを変更せずに固有の乱数生成アルゴリズムを提供する派生実装が作成されることを想定していることを意味します。そこで、ここでは Random から派生した型を実装し、そのすべてのパブリック メソッドをオーバーライドして、RNGCryptoServiceProvider を使用した新しい実装を作成することができます。

抽象 RandomNumberGenerator クラスから派生した RNGCryptoServiceProvider で 2 つのパブリック メソッド (GetBytes および GetNonZeroBytes) を公開します。GetBytes メソッドは、バイト配列を受け取り、そのバイト配列に、暗号化に使用する厳密な値のランダム シーケンスを格納します。GetNonZeroBytes も機能は同様ですが、格納される値が 0 以外であるという点が異なります。

GetNonZeroBytes を使うと、各バイトの範囲が [0,255] ではなく、[1,255] に限定されて生成可能な値の数が少なくなるため、今回の目的には GetBytes メソッドの方が好都合です。完全なランダム数値の 1 つの条件は、各ビットに 0 が格納される確率と 1 が格納される確率が等しいということであるため、GetNonZeroBytes を使用する必要がある場合はまれです。0 のビットが 15 個以上連続する場合は、この条件に反します。したがって、各バイトに 0 を指定できない場合は条件に反します (それでも 2 バイト 0x8001 で 0 を最大 14 個連続させることが可能ですが、15 個 0 を連続させるには、最低でも 1 バイトは 0 にする必要があります)。GetNonZeroBytes を使用して 0 が連続するビット数のカウンタを保持した場合、0 のビットが 14 個見つかれば、次のビットは確実に 1 であることがわかります。このビットは 1 である確率が 100 パーセントであり、0 である確率は 0 パーセントであるため、自動的にシーケンスが予測しやすくなり、完全にランダムとはいえません。GetNonZeroBytes が必要になる数少ない例として、RSA 暗号化用の PKCS #1 パディングを生成する場合があります。

とにかく、この暗号化に使用する厳密な乱数生成で行う必要がある処理は、一見 Random のように見えて、実は基になる RNGCryptoServiceProvider の GetBytes メソッドを使用して Random のメソッドを実装するクラスの作成です。この実装を、図 1 に示します。

Figure 1 より "ランダムな" Random (Close Figure 1)
public class CryptoRandom : Random
{
    private RNGCryptoServiceProvider _rng =
        new RNGCryptoServiceProvider();
    private byte[] _uint32Buffer = new byte[4];

    public CryptoRandom() { }
    public CryptoRandom(Int32 ignoredSeed) { }

    public override Int32 Next()
    {
        _rng.GetBytes(_uint32Buffer);
        return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF;
    }

    public override Int32 Next(Int32 maxValue)
    {
        if (maxValue < 0)
            throw new ArgumentOutOfRangeException("maxValue");
        return Next(0, maxValue);
    }

    public override Int32 Next(Int32 minValue, Int32 maxValue)
    {
        if (minValue > maxValue)
            throw new ArgumentOutOfRangeException("minValue");
        if (minValue == maxValue) return minValue;
        Int64 diff = maxValue - minValue;
        while (true)
        {
            _rng.GetBytes(_uint32Buffer);
            UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);

            Int64 max = (1 + (Int64)UInt32.MaxValue);
            Int64 remainder = max % diff;
            if (rand < max - remainder)
            {
                return (Int32)(minValue + (rand % diff));
            }
        }
    }

    public override double NextDouble()
    {
        _rng.GetBytes(_uint32Buffer);
        UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);
        return rand / (1.0 + UInt32.MaxValue);
    }

    public override void NextBytes(byte[] buffer)
    {
        if (buffer == null) throw new ArgumentNullException("buffer");
        _rng.GetBytes(buffer);
    }
}

CryptoRandom クラスは Random から派生し、2 つのコンストラクタ (パラメータなしのコンストラクタと Int32 "シード" 値を受け取るコンストラクタ) を提供します。パラメータなしのコンストラクタは、シード値を受け取るコンストラクタに Environment.TickCount をシード値としてデリゲートするだけです。シード値を受け取るコンストラクタは、シード値を使用して擬似乱数系列の開始値を計算します。ここでの "シーケンス" の使い方に注意してください。同一のシード値を指定すると、Random は常に "ランダム" 値の同一のシーケンスを生成します。このようなシーケンスは、再現性のある "ランダムな" 結果が保証されるデバッグ シード値を指定できるため、デバッグが容易ですが、乱数性能が低くなります。

シードとして使用可能な Int32 の値は 2^32 個しか存在しないため、Random で生成可能な一意のシーケンスは最大でも 2^32 個です。実際には、現在の実装に基づいて、使用可能な値はこれよりはるかに少なくなります。さらに重要なことは、暗号的に厳密なシーケンスが存在しないため、攻撃者が、シーケンスで生成された数値の履歴を分析し、将来の "ランダム" 値を予測することが可能になるということです。

RNGCryptoServiceProvider では開始シード値を開発者から受け取らず、シードを内部的に使用します。シードの内容の詳細については、CryptGenRandom 関数のドキュメント (msdn2.microsoft.com/aa379942) を参照してください。RNGCryptoServiceProvider には、文字列を受け取るコンストラクタやバイト配列を受け取るコンストラクタなど、複数のコンストラクタがありますが、実際にパラメータを処理に使用するのは暗号化サービス プロバイダ (CSP) パラメータを受け取るコンストラクタのみです。ここではこのコンストラクタは無視します。Random とまったく同一の API を確保するために、CryptoRandom クラスは Random で公開されている 2 つのコンストラクタを公開していますが、シードは無視されます (シードに関する情報は基になる RNGCryptoServiceProvider に渡されません)。同じ API を公開することにより、既存コードの検索と置換がきわめて容易になります。

CryptoRandom 全体のポイントは、RNGCryptoServiceProvider クラスを Random の公開インターフェイスに合わせて変更し、CryptoRandom にプライベート RNGCryptoServiceProvider メンバが格納されるようにすることです。格納されたインスタンスはランダム値に関するすべての要求に使われます。また、CryptoRandom はバイト配列を格納するプライベート フィールドも保持し、RNGCryptoServiceProvider の GetBytes メソッドで使用します。今回の実装では、GetBytes のすべての呼び出しに同一サイズのバイト配列を使用します。また、Random クラスが非スレッドセーフであるため、CryptoRandom もスレッドセーフにする必要はありません。そこで、このバイト バッファを 1 つ作成したら、それをすべての呼び出しで再利用するのが賢明です (.NET Framework 2.0 の現在の実装のように、RNGCryptoServiceProvider のパラメータなしコンストラクタでスレッドセーフ インスタンスを作成し、プライベート メンバを作成してプライベートな静的メンバを用意することで、CryptoRandom のインスタンスごとに新しい RNGCryptoServiceProvider インスタンスを作成しなくても済むようにする方法もありましたが、スレッドセーフは現在のところドキュメントに記載されておらず、クラスのコントラクトやインターフェイスにも組み込まれていないため、今回の実装ではこの方法を採用しませんでした)。

構造に配慮すると、5 つのパブリック メソッドを実装可能です。最も実装が簡単なメソッドは、バイト配列を受け取り、そのバイト配列にランダムな値を格納する NextBytes です。NextBytes メソッドは、署名と動作が RNGCryptoServiceProvider の GetBytes メソッドと同じであるため、簡単に実装できます。

_rng.GetBytes(buffer);

次に、[0.0, 1.0) の範囲でランダムな実数を返す NextDouble メソッドを実装します。このような値を生成する標準的なソリューションとして、非負の整数を最大有効整数より大きい値で除算する方法があります。この実装は次のようになります。

_rng.GetBytes(_uint32Buffer);
UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);
return rand / (1.0 + UInt32.MaxValue);

これではまだ完全な解決策とは言えません。ランダムな倍精度値を生成するには、ランダムな倍精度値とは何かを最初に定義しておく必要があります。そこで、各ビット パターンが等しい確率で生成されるようにするのか、それとも等間隔で分布する多数のランダム実数を生成するのかを決めます。.NET Framework の実数の実装の性質上、これらの定義は違うものを意味します。多くの場合、完全にランダムなビットを生成しない 2 番目の定義を採用しますが、この場合、低い値になるほど数値の範囲全体に歪みが生じます。今回は、前述のコード例に示した除算方式を使用します。この方法では、各ビット パターンに登場する確率は等しくなくても、指定された範囲内で均等に分布する数値が生成されます。この方法では、前述の最初の定義に基づく乱数性能の診断を行うことになりますが、NextDouble メソッドの各有効ビット パターンに等しい確率で数値が登場することを求めるユーザーはめったにいないと思われます。

NextDouble 生成式の入力の乱数性能は分子のみです。分母は除算処理のために乱数性能が多少損なわれます。ここで必要な処理は、除算を行って結果を正規化し、ランダム ビットを 0 以外にシフトすることです。この処理を行うには、固有の浮動小数点ライブラリを実装する必要がありますが、固有の浮動小数点実装の作成方法はこのコラムでは取り上げません (その説明を必要とする読者も少ないでしょう)。ここでは、既存の乱数生成を最適化するため、なるべく多くのランダム ビットを等式に含めるように試みます。分子は乱数性能を組み込むことができる唯一の箇所であり、Double は 64 ビット長であるため、UInt32 を生成して UInt32.MaxValue+1 で除算するのではなく、ランダムな UInt64 を生成して UInt64.Max + 1 で除算することにより、入力のランダム ビット数を倍に増やします。

ここで新たな問題が生じます。倍精度浮動小数点数は実数を近似値で保持するため、バイナリ浮動小数点演算用の IEEE 754 標準でコンパイルされる System.Double の小数桁数の精度は最大でも 17 桁にすぎません。ところが、UInt64.MaxValue (18446744073709551615) には 20 桁の小数点精度が必要です。その結果、.NET 浮動小数点演算 UInt64.MaxValue は、UInt64.MaxValue + 1 と等しくなり、NextDouble の式で UInt32 の代わりに UInt64 を使用すると、範囲が [0.0, 1.0) から [0.0, 1.0] に変わり、System.Random の実装の設計に違反することになります。そこで、図 1 のような実装にしましたが、この方法に不満があるなら、他によりよい (ただし、おそらくより複雑な) 方法があるかもしれません。

次に、Next のパラメータなしのオーバーロードを実装する必要があります。このメソッドは、ランダムな非負の Int32 値を取得します。この実装を行うには、4 つのランダム バイトを生成して上位ビットを削除し、System.BitConverter クラスを使って残りのビットを Int32 に変換します。

_rng.GetBytes(_uint32Buffer);
return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF;

4 つのランダム バイトを生成すると、範囲が [Int32.MinValue, Int32.MaxValue] のランダム値を取得できますが、Next のセマンティクスに合わせて、実際に必要な値の範囲は [0, Int32.MaxValue] です。32 桁のランダム ビットの下位 31 ビットのみを使用すると、負の値が非負の同等の値に変換されてこのマッピングになります。負の数と非負の数は個数が同じであり、1 ビット削除することによって一対一で対応付けられるため、各値が返される確率は等しくなります。

ランダムな Int32 を一定の間隔で生成する Next のオーバーロードに移ると、話はさらに詳細になります。Next の 2 番目のオーバーロードは最大値のみを受け取りますが、このオーバーロードの最小値には明示的に 0 が指定されているため、最小値と最大値を受け取る 3 番目のオーバーロードのサブケースになります。したがって、2 番目のオーバーロードは 3 番目のオーバーロードにデリゲートすることができます (Next のパラメータなしのオーバーロードも 3 番目のオーバーロードのサブケースであり、明示的な最小値と明示的な最大値を持ちますが、固有に処理する方が適しています。その理由は後述します)。

ランダムな Int32 を一定の間隔で生成する方法には、明快な方法が 2 つと、やや不明快な方法が 1 つあります。最初の方法は、ランダムな倍精度小数点の乗算を用います。

return (int)(minValue + (NextDouble() * (maxValue – minValue)));

2 番目の方法では、ランダムな UInt32 を取得して処理します。

result = minValue + (randomUInt32 % (maxValue – minValue))

以上の 2 つの方法には問題があります。ランダムな倍精度小数点の乗算を行う最初の方法には、前述したように、生成される倍精度小数点数が暗号的に完全にランダムではないという条件があるために欠点があります。2 番目の方法 (ランダムな整数を取得して範囲の剰余を求める) は、範囲が 2 の偶数乗である場合には問題はありません (完全にランダムな入力ビットから選択していることが前提になります)。これは、GetBytes から生成された UInt32 値には 2^32 個の有効値があるためです。これらの値を対象範囲内のすべての値の間で均等に分布させるには、対象範囲のサイズが入力範囲を等分する大きさである必要があります。2^32 などの乗数の場合、この条件を満たすには、対象範囲のサイズも 2 の乗数でなければなりません。対象範囲のサイズが 2 の乗数でない場合、"ランダムな" 結果は低い値をより重く評価します。

この歪みを示す一例として、範囲が [21, 27] の数値が必要であると仮定してみましょう (話を簡単にするために、32 ビットではなく、8 ビットを選択することにしますが、実際に使用する 32 ビットにも同じ原則が当てはまります)。前述の等式に従って、0 ～ 255 の乱数を取得し、6 + 21 で割った剰余を求めます。乱数の範囲が [0, 251] の場合、[0,5] の各数値がちょうど 42 回ずつ登場するため、問題はありません。ところが、251 になると、0 (252)、1 (253)、2 (245)、および 3 (255) が剰余演算の結果値になります。この場合、剰余演算で 0、1、2、3 が選択される確率は 43/256 で、4 および 5 が選択される確率は 42/256 です。したがって、21、22、23、または 24 が返される確率は ~16.80 パーセントであるのに対し、25 および 26 が返される確率は ~16.41 パーセントになります。長期的に見ると、攻撃者が出力結果を推測しようとした場合、常に範囲内のなるべく小さい値を選んだ方が成功する確率が高いということです。

このデータから、図 1 のような 3 番目のオプションを選択することにしました。上記で説明した剰余演算に関する問題は、対象範囲の一部の値が他の値より多少優先される可能性があることが原因で発生します。この問題を修正するには、このような優先が検出された場合に再試行されるようにします。前述の例で説明したように、ランダムに選択した値が次の条件を満たした場合に優先が起こったことになります。

RandomValue >= RandomRange - (RandomRange % TargetRange)

前述の例では、RandomRange は 256、TargetRange は 6 であるため、RandomValue が >= 252 の場合に優先が起こります。対象範囲が小さい場合、優先が起こる確率はきわめて低くなります。対象範囲が大きい場合 (サイズが 2^31+1 の対象範囲など)、優先が発生する確率は 50 パーセント以上にはなりません。このように、ランダム値を生成して、優先が発生する範囲に当たるかどうかを確認することができます。優先が発生する範囲にある場合は、新しいランダム値を指定して処理し直します。優先が発生する範囲にない場合は、2 番目の方法のところで示した式の結果を返します。このアルゴリズムは理論上は無限ループに陥る可能性がありますが、その可能性はきわめて低いものです。多くの場合、せいぜい数回繰り返して終わります。不正なランダム値を取得する確率が 50 パーセントという最悪の場合でも、適切な値が見つかるまでの繰り返し処理の回数が過剰にならないようにする必要があります。たとえば、反復処理を 10 回行っても適切な値が見つからない確率は 0.1 パーセント未満です。反復処理を 20 回行った後では、その確率は 0.00001 パーセント未満にまで下がります。この方法がパフォーマンスの問題で要件に合わない場合は、前述の他のいずれかの方法にいつでも戻すことができます。

最後にもう 1 つ説明しておくことがあります。パラメータなしの Next のオーバーロードを、乱数性能を損なわずに他のオーバーロードで実装できることはもうおわかりでしょう。パラメータなしの Next に必要な対象範囲は 2^31 であり、これは 2 の乗数であるため、2^31 の乱数範囲を等分します。このような場合、ループ処理は不要です。例示した Next の実装でのビット操作 (0x7FFFFFFF との &) は、2^31 の対象範囲の剰余で求めたランダム値と結果は同じですが、パラメータなしの Next の実装が簡単なため、このメソッドの既存コードをデリゲートして削除することも可能であるにもかかわらず、そのままにしておくことを選びがちです。

この場合、現在 Random を使用しているシナリオに CryptoRandom を問題なく使用できます。コード内で Random クラスの宣言を置き換える必要もありません。CryptoRandom は Random から派生しているため、Random のインスタンスを CryptoRandom のインスタンスに置き換えるだけで済みます。

ただし、これにはお断りしておかなければならないことがあります。このコラムの冒頭で触れたように、Random を拡張するのであれば、RNGCryptoServiceProvider の設計者はなぜこの記事で説明した方法で既存クラスから派生せず、固有の RandomNumberGenerator 基本クラスを作成したのかと疑問に思われるかもしれません。前述したように、RNGCryptoServiceProvider のインターフェイスは、完全に暗号化された乱数を返す System.Random インターフェイス全体を実装することができないという点が異なります。代わりに、暗号的にランダムなビットの組み合わせをインターフェイスに合わせて操作する必要があり、その過程で乱数性能が劣化します。開発者は、RNGCryptoServiceProvider を受け取って暗号的にランダムなビットを指定し、各自の要件に適した方法で操作することができます。CryptoRandom クラスをそのまま使用できれば、それに越したことはありませんが、そのまま使用できない場合は、少なくとも固有の実装を行う場合に必要な関連の問題やトレードオフを理解しておく必要があります。

注意しなければならないのは、CryptoRandom はパフォーマンスの低下をもたらすという点です。Random の実装では、暗号化に使用する厳密な乱数を生成しないため、RNGCryptoServiceProvider と比べると非常に高速です。私たちの非科学的テストでは、Random は CryptoRandom の 100 倍高速でした。また、Random で生成される再現性のあるシーケンスを利用している場合、CryptoRandom で生成される再現性のないシーケンスでは満足できないでしょう。ともあれ、これが重要なポイントなのです。

ご意見やご質問は、

こちら netqa@microsoft.com.

まで英語でお送りください。
Stephen Toub は、Microsoft の並列コンピューティング プラットフォーム チームのシニア プログラム マネージャです。MSDN Magazine の寄稿編集者でもあります。

Shawn Farkas は、Microsoft の CLR チームのソフトウェア開発エンジニアとして .NET Framework のすべての暗号化クラスを担当しています。


{ End Bracket }: WPF を活用した閲覧方法 &quot;Turning the Pages&quot;

{ End Bracket }
WPF を活用した閲覧方法 "Turning the Pages"
Tim Sneath

アレクサンドリア図書館 は、西暦紀元前 300 年に世界中の知識を 1 か所に集めるという壮大な目的の下に建設され、最盛期には約 75 万巻の書物を所蔵していました。現代の世界で、第一級のコレクションを所蔵している 1 つが英国図書館です。その 2000 万もの書物や写本の中には、現存する最も珍しい作品がいくつか含まれています。最古の印刷本である『Diamond Sutra (金剛経)』、メルカトルによる『First Atlas of Europe』、『Lindisfarne Gospels (リンデスファーン福音書)』、レオナルド ダ ヴィンチの個人的なノート、『マグナ カルタ』、および最古の 2 つのキリスト教の聖書のうちの 1 つである『Codex Sinaiticus (シナイの写本)』を所蔵しています。このような珍しい収蔵品の扱いには、もちろん最大限の注意を払う必要があります。仮にこれらが公開されても、ガラス ケース内で十分に保護され、直接手にできる人はほんの数人に限られます。

さいわいにも、現在、これらの作品は、多くの人々が参照できるように初のデジタル化が進められています。すばらしいことに、デジタル化された書物には、学芸員による解説が追加されており、また、書物はまるで実際に目の前に存在するかのようで、読書を豊かで対話的な体験に変えています。英国を拠点とするソフトウェア開発者との協力の下、英国図書館は、"Turning the Pages" という名前の Windows® ベースのアプリケーションを開発しました。これは、増え続ける図書館の所蔵品の中で、最も貴重なものの仮想複写版を 3 次元空間で提供します (www.bl.uk/onlinegallery/ttp/ttpbooks.html を参照してください)。このアプリケーションは、ttpdownload.bl.uk/browserapp.xbap で入手でき、Microsoft® .NET Framework 3.0 を搭載したコンピュータで利用できます。これを使用すると、まるで実際に本を手にしているかのように、本をデスクトップで開き、各ページをめくることができます。

これらの作品のデジタル化には、興味深い課題がいくつかありました。書物が傷みやすく貴重であるため、所蔵品を損傷する危険を冒さずに画像をキャプチャできる特殊なオフセット カメラを使って各ページをスキャンする必要がありました。次に、学芸員が、物理的に各書物のページをめくって撮影し、仮想モデルのリファレンスを作成しました。ご想像いただけるかと思いますが、18 世紀の書物の重い上質皮紙のページは、17 世紀の紙のページと同じようには動きません。このような細かいところへの配慮が、本物を扱っているように感じさせるために大切なのです。

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

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

常に反響がある Turning the Pages の機能の 1 つは、15 世紀の祈祷書『Sherborne Missal』のような、作品のページを飾る多くのイルミネーションの金箔を際立たせるために使用された反射素材です。ページをめくるとすぐに、イラストの一部に光が当たりきらきらと輝きます。

Turning the Pages は、Windows Presentation Foundation の 3D クラス ライブラリを使用して実装されます。これは DirectX® の抽象化でもあります。各ページは、ジオメトリのメッシュとして表されます。これに、1 つ以上の画像マテリアルが適用されます。画像は、特定の圧縮率で高い品質が得られる HD Photo 形式を使用して保存されています。鏡面反射性を必要とする書物については、ページごとに、モノクロ マスクを含む 2 つ目の画像も使用して、ページ内の金箔で飾られた部分だけが輝くようにしています。最後に、フレームベースのコールバックが手動でジオメトリ モデルを変形して、マウスをクリックしたときにページをめくるような効果を作成します。

クライアント プラットフォーム テクノロジのエバンジェリストとして、私は人々がすばらしいユーザー体験を重ねる様子を見ることが大好きです。Web は、ソフトウェア開発に強力な影響を与えることを証明しています。Web によって、コンテンツ作成プロセスが大衆化され、会社はソフトウェアをお金に換える新しい方法を獲得し、またアプリケーション全体を再配布しなくてもマイナー アップデートができる摩擦のない配布モデルが提供されています。

しかし、これらの利点を得るために、私たちはすべてのパーソナル コンピュータで利用できるとてつもない潜在能力を完全に活用する能力を犠牲する態勢になっていました。最小公分母の制限に拘束されないアプリケーションを構築できるように、また開発者および設計者が協力して、モデル化している環境をより適切に表現するユーザー インターフェイスを生み出せるようにするためには、HTML や CSS より強力なツール セットが必要です。配布は Web を介する一方でレンダリングの表示には WPF を利用する Turning the Pages のようなアプリケーションは、これらの 2 つの世界がどのように衝突しているかの例として、興味深いものです。今後数年間で、開発者がインターネットベースのより豊かな対話的アプリケーションを提供する方法を見出して、だれもが使用しているソフトウェアに大きな変革がもたらされると私は確信しています。

Tim Sneath は、マイクロソフトの Silverlight and WPF Technical Evangelism チームのグループ マネージャです。彼のミッションは、開発者がマイクロソフト プラットフォームで魅力的なアプリケーションを作成する過程を見ることと、母親にコンピュータが彼女を撃退しようとしているわけではないということを納得させることです。