 MSDN Magazine January 2008

MSDN Magazine January 2008

IIS 7.0: 統合 ASP.NET パイプラインでアプリケーションを拡張する

Mike Volodarsky は、IIS 7.0 を使用して、PHP コードには 1 行も触れずに PHP アプリケーションのパフォーマンスとセキュリティをアップグレードする方法を示しています。

Mike Volodarsky 国際化対応: ASP.NET AJAX アプリケーションの国際化

.NET Framework には優れた国際化対応機能がありますが、JavaScript にはその機能がありません。ASP.NET AJAX を使用する場合は、適用のために必要な作業を確認してください。

Guy Smith-Ferrier WCF シンジケーション: WCF および .NET Framework 3.5 を使用した HTTP プログラミング

.NET Framework 3.5 の Windows Communication Foundation の Web 中心の機能 (HTTP プログラミング モデルや新しいシンジケーション API など) について説明します。

Justin Smith SQL Server: 隠れたデータを明らかにしてアプリケーションのパフォーマンスを最適化する

SQL Server 2005 が収集する多くの役に立つデータを使用すると、データベースのパフォーマンスを改善できる部分がわかります。ここでは、パフォーマンスの改善が必要な部分について説明します。

Ian Stirk 検索機能: .NET Framework 3.5 でディレクトリのセキュリティ プリンシパルを管理する

.NET Framework 3.5 の新しい System.DirectoryServices.AccountManagement クラスの概要、およびこのクラスでディレクトリ サービスの操作を簡素化する方法について説明します。

Joe Kaplan and Ethan Wilansky 編集者のメモ: 新年の展望

読者アンケートの結果が届きました。今月は、Howard Dierking がこの結果をまとめてお伝えします。

Howard Dierking ツールボックス: 高度な単体テスト、オブジェクトのモック作成、アプリケーションのプロファイル、その他

今月は、高度な単体テスト、オブジェクトのモック作成、プロファイルなどについて説明します。

James Avery CLR 徹底解剖: マネージ コードとアンマネージ コード間でマーシャリングする

マーシャリングは、.NET 相互運用機能の重要な部分です。マーシャリングにより、マネージ コードからアンマネージ コードを呼び出すことができます。このコラムはマーシャリングを使い始めるときに役立ちます。

Yi Zhang and Xiaoying Guo データ ポイント: モバイル アプリケーションからデータにアクセスする

今月は John Papa が、アプリケーション サーバーのデータにアクセスできるモバイル アプリケーションの開発に注目します。

John Papa Advanced Basics: リフレクションとジェネリック

今月の Advanced Basics では、ジェネリックとリフレクションの能力について説明し、この 2 つを組み合わせてさらに効率的で柔軟性のある開発を行う方法を示します。

Ken Getz Cutting Edge: AJAX エクステンダを使用してコントロールをカスタマイズする

AJAX エクステンダを使用すると、通常の Web コントロールの動作と機能を拡張して、AJAX を単独で使用する場合よりもポストバックを削減し、はるかに優れた入力制御を行うことができます。

Dino Esposito Wicked Code: ASP.NET AJAX を使用してドラッグ アンド ドロップする

Jeff Prosise が ASP.NET AJAX を使用して Web アプリケーションにドラッグ アンド ドロップ機能を実装する方法について説明します。

Jeff Prosise 基礎: 非コモン コントロール用のテンプレート

Charles Petzold が、Windows Presentation Foundation (WPF) テンプレートでコモン コントロールを一般的でない視覚オブジェクトに変換する方法を示します。

Charles Petzold すてきな ASP.NET: ASP.NET コントロールで Silverlight をカプセル化する

ASP.NET ページに Silverlight を実装するためには、ASP.NET コントロール内に Silverlight 要素をカプセル化することができます。ここではその方法を説明します。

Fritz Onion { End Bracket }: 技術への情熱

Scott Hanselman が "5 時 1 分の開発者" という言葉と、あらゆる分野で成功するために情熱と職人気質が必要な理由について考えます。

Scott Hanselman


IIS 7.0: 統合 ASP.NET パイプラインでアプリケーションを拡張する

IIS 7.0

統合 ASP.NET パイプラインでアプリケーションを拡張する

Mike Volodarsky
この記事は、IIS 7.0 FastCGI コンポーネントのプレリリース版を基にしています。ここに記載されているすべての情報は、変更される場合があります。
コードのダウンロード : : PHPandIIS2008_01.exe (171KB)
この記事の内容 : :
  • ASP.NET の Integrated モード
  • ユーザー認証の追加
  • 検索エンジン フレンドリーな URL の有効化
  • 出力キャッシングによるパフォーマンスのアップグレード
この記事は次のテクノロジを使用しています:
IIS 7.0、.NET Framework
コンテンツ
IIS と PHP
アプリケーションを設定する
ギャラリーを保護する
フォーム認証
検索エンジン フレンドリーな URL
フレンドリーなハイパーリンクを生成する
パフォーマンスをテストする
ASP.NET 出力キャッシング
配置とテスト
まとめ

ほぼ 1 年前、筆者は MSDN® マガジンに IIS 7.0 の概要を執筆しました (msdn.microsoft.com/msdnmag/issues/07/03/IIS7 の「IIS 7.0: Windows Vista 用 Web サーバーの新機能」を参照してください)。それは、Windows Vista® と共に IIS 7.0 がリリースされる数か月前でした。それ以降、ユーザーには、新しい IIS 7.0 のコンポーネント化された拡張可能なアーキテクチャとその他の改善を直接経験するチャンスがありました。

Windows Vista が出荷されてから、私たちは IIS 7.0 を Windows Server® 2008 の信頼できる安全な Web サーバーとし、その安定性、パフォーマンス、およびホスティング環境のサポートに磨きをかけることに力を注いできました。また、柔軟性のある Web アプリケーション プラットフォームであることが IIS 7.0 にとってどのような意味を持つかを綿密に検討しました。ASP や ASP.NET のような Microsoft アプリケーション フレームワークの優れたプラットフォームとすることだけでなく、現在使用されている他のさまざまなアプリケーション フレームワークに対しても優れたプラットフォームにすることを望んでいました。これを促進するために、FastCGI のサポートを追加しました。FastCGI は、PHP、Ruby on Rails、Perl などのアプリケーション フレームワークを IIS にホストできるようにする公開 Web サーバー規格です。また、PHP の作成者である Zend Technologies と協力して、Windows® と IIS に堅牢でパフォーマンスの高い PHP 実装をもたらしました。

IIS 7.0 は、普及しているアプリケーション フレームワークの実稼働サポートを提供するだけではありません。新しい Microsoft® .NET Framework 拡張モデルにより、IIS 7.0 は ASP.NET の Integrated モードの能力を活用して、任意のフレームワークで開発されたアプリケーションに重要な機能を追加できます。これにより、ほとんどの場合はコードを 1 行も触らずに、アクセス制御や新しい URL スキームなどの優れた機能を追加したり、パフォーマンスを大幅に向上させたりすることができます。

この記事では、IIS 7.0 ASP.NET 統合機能を深く掘り下げて、ASP.NET を考慮して開発されていないアプリケーションを拡張します。既存の ASP.NET 機能でアプリケーションを補強する方法と、IIS レベルの ASP.NET 拡張機能を利用する新機能を開発してアプリケーションに追加する方法を示します。

使用するアプリケーションは、Qdig (qdig.sourceforge.net) という普及型の PHP イメージ ギャラリー アプリケーションです。PHP コードを 1 行も触らずに、いくつかの便利な新機能をイメージ ギャラリーに追加する方法を示します。最初に、ASP.NET メンバシップを使用してギャラリーをパスワードで保護し、認証機能を作成します。また、見にくいパラメータ化クエリ文字列の URL ではなく検索エンジン フレンドリーな URL を使用するようにアップグレードします。最後に、ASP.NET 出力キャッシュを使用してアプリケーションのパフォーマンスを大幅に改善します。

ただし、まずは IIS での新しい PHP サポートに隠された歴史を見てみましょう。この PHP サポートは、PHP のようなアプリケーション フレームワークで IIS 7.0 機能の完全なセットを利用できるようにするための中核です。

IIS と PHP

IIS は常に PHP をサポートしてきましたが、多くの現実の PHP アプリケーションは、実稼働環境にホストされていませんでした。それは、PHP アプリケーションを実行するために IIS が提供していた Common Gateway Interface (CGI) プロトコルの使用または PHP ISAPI 拡張機能の使用という 2 つの点に制限があるためです。

CGI では要求ごとに別々のプロセスが必要であるため、CGI を使用してホストされているアプリケーションは、Windows 上でパフォーマンスが悪くなります。反対に、IIS の高パフォーマンス マルチスレッド ISAPI インターフェイスを使用している PHP アプリケーションは、一部の普及している PHP 拡張機能にスレッド セーフが欠落しているため不安定になる場合があります。

これらの問題を解決しようとして、IIS チームは FastCGI コンポーネントを開発しました。公開 FastCGI プロトコルによって、PHP、および単一スレッド環境を必要とする他の多くのアプリケーション フレームワーク (Ruby on Rails、Perl、Python など) は、IIS での実行の信頼性が高くなります。標準の CGI 実装とは異なり、FastCGI では、それぞれが一度に複数の要求を処理しないワーカー プロセスのプールを維持することでプロセスを再利用できるため、パフォーマンスが大幅に向上します。FastCGI は、コミュニティ中心の開発およびテスト モデルの恩恵も受けます。

同時に、Zend Technologies は、Windows 上の PHP スクリプティング エンジンとコア拡張機能の全般的なパフォーマンスと安定性の向上に取り組み、多数のパフォーマンス改善と数十個の Windows 固有バグの解決を行いました。

ここで説明しているように、php.net/downloads では、IIS FastCGI プラットフォーム用に最適化された高速非スレッド セーフ ビルドと共に、Windows ホスティング用に最適化された PHP の第 3 リリースである PHP 5.2.3 が提供されています。IIS FastCGI サポートは、Windows Server 2008 のベータ 3 リリース以降に組み込まれており、Windows Vista、Windows XP、および Windows Server 2003 の Go-Live リリース用に独立した Tech Preview 版ダウンロードとして入手できます (iis.net/fastcgi)。Windows Vista SP1 の出荷時には、このコンポーネントはインストール パッケージの一部としても入手できるようになります。近い将来には、Windows XP および Windows Server 2003 用の最終バージョンもリリースされます。IIS FastCGI を実行する場合のオプションについては、mvolo.com/blogs/serverside/archive/2007/10/09/IIS-FastCGI-and-PHP_3A00_-What-you-absolutely-need-to-know-to-host-PHP-applications-on-IIS-6-and-IIS-7.aspx で今すぐ参照できます。

アプリケーションを設定する

FastCGI を搭載した IIS 7.0 によって、PHP アプリケーションの設定がかなり単純になります。まず、Web サイトを作成し、ローカル マシンから http://myphpgallery としてアクセスできるように myphpgallery ホスト ヘッダー バインドを追加しました (myphpgallery ホスト名を %windir%\system32\drivers\etc\hosts に追加して、マシンがその場所を認識できるようにもしました)。

次に、Qdig の最新バージョンを qdig.sourceforge.net からダウンロードし、Web サイトのルートに解凍しました。ギャラリーのテストを準備するために、多数のイメージを Web サイトのルートにドロップしました (サブディレクトリを作成し、イメージをそこにドロップすることもできます)。ここでは、Windows Vista に付属のサンプル イメージをいくつか使用しました。

Windows 用の最新の非スレッド セーフ PHP ビルド (入稿時点では PHP 5.2.3) を php.net/downloads からダウンロードし、C:\php に解凍しました。Qdig に必要ないくつかの微調整を php.ini に対して行う必要がありましたが、この時点ではかなりうまく行っていました。

  1. php-recommended.ini を php.ini に名前変更する
  2. "register_long_arrays=On" を設定する
  3. "extension=php_gd2.dll" で GD 拡張機能を有効にする
  4. "extension_dir=./ext" で正しい拡張パスを設定する

IIS の観点からは、PHP アプリケーションを実行するにはいくつかの手順のみで済みます。まず、Windows Vista の場合は go.microsoft.com/fwlink/?LinkId=104195、Windows Server 2008 および Windows Vista SP1 の場合は go.microsoft.com/fwlink/?LinkId=104196 の記事に従って、PHP/FastCGI ハンドラ マッピングを PHP-CGI.EXE に追加します。次に、index.php を既定のドキュメントとして追加します。

最後に、Qdig はサムネイルをその場で生成するため、IIS ワーカー プロセスがアプリケーションの qdig-files サブディレクトリに書き込めるように、このサブディレクトリへの書き込みアクセス権を IIS_IUSRS グループに付与する必要がありました。

これで準備完了です。この時点で、http://myphpgallery にアクセスすると、図 1 に示すようなイメージが表示されます。

図 1 Qdig ギャラリー
図 1 Qdig ギャラリー(画像を縮小するには、ここをクリックします)
図 1 Qdig ギャラリー
図 1 Qdig ギャラリー (画像を拡大するには、ここをクリックします)

ギャラリーを保護する

Qdig は、Web を介してイメージ コレクションを参照するという 1 つの処理を正常に行うことに重点を置いた単純なイメージ ギャラリーです。このため、Web アプリケーションで必要とされることの多い複雑な機能の一部がありません。このような機能の 1 つは、Web アプリケーションの一部またはすべてへのアクセスを制限できるようにするアクセス制御です。

あいにく、PHP でアクセス制御を実装するには、資格情報ストアと cookie ベースの認証を新たに実装する必要があり、この作業を正しく行うのはかなり困難です。対照的に、ASP.NET は、メンバシップ サービスと組み込み資格情報ストア プロバイダのセット、フォーム認証モジュール、および作成済みのログイン コントロールのセットを備えた完全なソリューションを提供することで、アクセス制御を簡単にします。

以前のバージョンの IIS では、このことは Qdig ギャラリーにとって大きな意味を持ちません。ASP.NET アプリケーションではないからです。ただし、IIS 7.0 では ASP.NET の Integrated モード エンジンがこのシナリオ専用にデザインされており、他のアプリケーション フレームワークを含め、任意のコンテンツで ASP.NET 機能を使用できます。ASP.NET の Integrated モードの機能を使用して、Qdig ギャラリーは、ネイティブ ASP.NET アプリケーションと同じように ASP.NET メンバシップ、フォーム認証、およびログイン コントロールを利用できます。実際に、フォーム認証を使用する既存の ASP.NET アプリケーションが既にある場合は、単純に Qdig ギャラリーにドロップし、わずか数ステップで他のアプリケーションと同じユーザー セキュリティをもたらすことができます。その結果、サイト全体で一貫したユーザー セキュリティが実現されます。

ギャラリーのフォーム認証ソリューションをゼロから実装するために、まず、ユーザー資格情報を格納するためのメンバシップ資格情報ストア プロバイダを選択する必要がありました。ASP.NET 2.0 には 2 つの組み込みプロバイダが付属しています。一方は SQL ServerTM 用で、もう一方は Active Directory® 用です。

SQL Server プロバイダは、多くのアプリケーションにとって優れたオプションである SQL Server 2005 Express Edition と共に使用できます。SQL Server 2005 Express Edition をメンバシップ プロバイダと共に使用する場合の最も優れた点は、初回使用時にデータベース テーブルを自動的に作成し、アプリケーションの App_Data サブディレクトリに配置する機能にあります。これには外部データベース配置は不要であり、アプリケーション ファイルで Xcopy できる資格情報データベースが生成されます。

SQL Server メンバシップ プロバイダは、既定では、ローカル SqlExpress インスタンスを使用する接続文字列を使用して、アプリケーションの App_Data ディレクトリ内にある aspnetdb.mdf データベースに接続するように構成されています。これを機能させるために、単純に次の 3 つの手順を実行しました。

  1. SQL Server 2005 Express Edition をダウンロードしてインストールし、名前付き SqlExpress インスタンス (既定) として実行しました (microsoft.com/sql/editions/express からダウンロードできます)。
  2. App_Data サブディレクトリをアプリケーションのルートに作成し、IIS_IUSRS から書き込み可能にしました。
  3. IIS マネージャを開き、アプリケーション用にいくつかのユーザーを作成しました。この操作を行うには、左側のツリー ビューで Web サイトを選択し、[.NET ユーザー] 機能アイコンをクリックします (図 2 を参照)。SQL Server Express Edition では一度に 1 つのユーザー ID しかデータベースにアクセスできないため、終了したら必ずツールを閉じてください。

図 2 IIS マネージャでユーザーを構成する
図 2 IIS マネージャでユーザーを構成する(画像を縮小するには、ここをクリックします)
図 2 IIS マネージャでユーザーを構成する
図 2 IIS マネージャでユーザーを構成する (画像を拡大するには、ここをクリックします)

IIS マネージャと ASP.NET メンバシップ インフラストラクチャ間の統合に注意してください。他の IIS 認証およびアクセス制御機能を構成する管理ツールから、ユーザーとロールの両方を直接管理できます。実際に、最初のユーザーを作成するときに、アプリケーションにメンバシップ データベースが存在しない場合は、IIS マネージャによってメンバシップ データベースが自動的に作成されます。

フォーム認証

メンバシップの準備ができたところで、アプリケーションに対してフォーム認証を有効にする必要があります。また、認証サービスを実装するフォーム認証モジュールを、ASP.NET 要求だけでなくアプリケーションのすべての要求に対して実行できるようにする必要があります (これは、下位互換性を保つための既定の動作です)。次のように、この処理はすべて Web サイトの web.config ファイルで直接実行できます。

<configuration>
    <system.webServer>
        <modules>
            <remove name="FormsAuthentication" />
            <add name="FormsAuthentication"
             type="System.Web.Security.FormsAuthenticationModule" />
        </modules>
    </system.webServer>
    <system.web>
        <authentication mode="Forms" />
    </system.web>
</configuration>

FormsAuthentication モジュールはサーバー レベルで既に宣言されていますが、これを削除し、再び宣言して、既定で managedHandler に設定されている preCondition 属性をクリアする必要があります。この設定では、モジュールが強制的にマネージ (ASP.NET) ハンドラへの要求に対してのみ実行されます。フォーム認証で PHP アプリケーションも保護する必要があるため、この設定を削除する必要があります。

この時点で、フォーム認証を使用するようにアプリケーションが構成されています。ただし、コンテンツが実際に認証済みユーザーを必要とすることは指定しませんでした。匿名認証を有効なままにして、サイトへの匿名アクセスを許可したため、認証は使用されません。実際には、ログイン ページへの匿名アクセスを許可し、残りのコンテンツの認証を要求することのみ必要です。宣言型の認証ルールを構成することで、この処理を行うことができます。web.config ファイル内の完成した構成を図 3 に示します。

Figure 3 Web.config (Close Figure 3)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <!-- The PHP handler mapping -->
        <handlers>
            <add name="PHP via FastCGI" path="*.php" verb="*"                            modules="FastCgiModule" scriptProcessor="
                c:\php\php-cgi.exe" resourceType="Unspecified" />
        </handlers>
        <!-- Add index.php as a default document -->
        <defaultDocument>
            <files>
                <add value="index.php" />
            </files>
        </defaultDocument>
        <security>
            <!-- Deny access to anonymous users -->
            <authorization>
                <add accessType="Deny" users="?" />
            </authorization>
        </security>
        <!-- Let the FormsAuthentication module run for all requests -->
        <modules>
            <remove name="FormsAuthentication" />
            <add name="FormsAuthentication" type=
                "System.Web.Security.FormsAuthenticationModule" />
        </modules>
    </system.webServer>
    <system.web>
        <!-- Turn on Forms Authentication for this application -->
        <authentication mode="Forms" />
    </system.web>
</configuration>

この構成では、PHP ギャラリーへのアクセスは、認証済みユーザーに対してのみ許可されます。認証されていないユーザーは、URL 認証機能によって拒否され、フォーム認証によってログイン ページにリダイレクトされます。ログイン ページで、ユーザーはメンバシップ サービスと SQL Server 資格情報データベースを使用してログインできます。

ジグソー パズルの最後のピースは、ログイン ページです。このページは、ログイン Web フォームを提供し、メンバシップ サービスでユーザーの資格情報を検証し、ユーザーに暗号化形式の認証ログイン チケットを発行することで、すべてを結び付けます。この時点で腕まくりをしてコーディングを始めようとしているなら、ちょっと待ってください。このログイン ページはかなりの部分が既に用意されています。行う必要があるのは、login.aspx という単純なページをアプリケーションのルートに作成することだけです (ログイン ページへのパスは、<forms> 構成セクションでオーバーライドできます)。この新規作成されたページでは、単純な Login コントロールが使用されます。

<html>
  <head>
    <title>Login to my blog</title>
  </head>
  <body>
    <form runat="server">
      <asp:Login runat="server" />
    </form>
  </body>
</html>

Login コントロールは、ログイン フォームを自動的に表示し、送信後にユーザー資格情報を検証し、フォーム認証チケットを発行します。このチケットは、非アクティブになるかユーザーがブラウザ ウィンドウを閉じたために失効するまで、Web サイト上の残りの時間についてユーザーを自動的に認証するために使用されます。

図 4 に、ログイン ページの整理したバージョンを示します。より便利なユーザー エクスペリエンスを提供するために、他のログイン関連コントロールをいくつか使用しています。LoginView コントロールでは、匿名ユーザーのビューか認証済みユーザーのビューかを選択できます。LoginName コントロールは認証済みユーザーの名前を提供し、LoginStatus コントロールは認証済みユーザーのログアウト リンクを提供します。先に進んで多数の書式設定プロパティの 1 つを設定するか、カスタム CSS でテーマ設定またはスキニングすることにより、クライアントに対するログイン コントロールの表示方法を指定できます。

Figure 4 手の込んだログイン ページ (Close Figure 4)
<html>
  <head>
    <title>Login to view the gallery</title>
  </head>
  <body>
    <form runat="server">
      <asp:LoginView runat="server">
        <AnonymousTemplate>
          Please log in to proceed.
          <br /><br />
          <asp:Login DestinationPageUrl="/"  runat="server" />
        </AnonymousTemplate>
        <LoggedInTemplate>
          Welcome <asp:LoginName runat="server" />!
          <br /><br />
          <asp:LoginStatus runat="server" />
        </LoggedInTemplate>
      </asp:LoginView>
    </form>
  </body>
</html>

これで準備完了です。ギャラリーにアクセスすると、図 5 に示すログイン ページに自動的にリダイレクトされます。ログインするには、前に作成したいずれかのユーザーのユーザー資格情報を入力する必要があり、それで作業を開始できます。

図 5 完成したログイン ページ
図 5 完成したログイン ページ(画像を縮小するには、ここをクリックします)
図 5 完成したログイン ページ
図 5 完成したログイン ページ (画像を拡大するには、ここをクリックします)

検索エンジン フレンドリーな URL

既定では、Qdig イメージ ギャラリーでは見にくいが機能するクエリ文字列 URL を使用してイメージ ギャラリーをナビゲートします。次に、筆者のギャラリーの Qdig URL の例を示します。

http://myphpgallery/index.php?Qwd=./Mike&Qif=Flower.jpg

特におもしろくも覚えやすくもありません。さらに重要なことに、検索エンジンがギャラリーのコンテンツを正しくインデックス付けすることが難しくなります。

ここで行う必要があるのは、Qdig が検索エンジン フレンドリーな URL を使用するようにすることです。検索エンジン フレンドリーな URL は、ユーザーの参照操作をはるかに直観的にし、検索エンジンが Web サイトを適切にインデックス付けする際にはるかに有効であるため、最近の Web 2.0 アプリケーションで流行しています。たとえば、前に示した URL のフレンドリー URL バージョンを次に示します。

http://myphpgallery/index.php/Mike/Flower.jpg

この URL では、サブギャラリーとイメージ名が URL パスに挿入されているため、直観的になっています。また、重要なキーワードが URL の絶対パスに配置されているため、検索エンジンがインデックスを付けやすくなっています。

興味深いことに、サーバーはこの URL を既定で index.php に正しくルーティングし、残りの "/Mike/Flower.jpg" を PATH INFO セグメントとして認識します。ただし、ここで問題になるのは、この URL が Qdig アプリケーションのスクリプトである index.php で期待される URL 形式に従っていないことです。URL で index.php スクリプトを呼び出すことができても、Qdig は必要なクエリ文字列パラメータを見つけられず、正しく動作する方法を知ることができません。

さいわい、今回も ASP.NET の Integrated パイプラインで救済できます。受信した URL をフレンドリーな形式から Qdig ネイティブ URL にその場でリライトできる小さな .NET モジュールを作成して、適切なギャラリー ビューがクライアントに返されるようにすることができます。ASP.NET の Integrated パイプラインのおかげで、このモジュールを ASP.NET コンテンツに対する要求の場合と同じ方法で PHP 要求に対して実行できます。

これを行うために、ASP.NET IHttpModule パターンを使用してマネージ モジュールを実装します。IIS 7.0 でのこのようなモジュールの構築の詳細については、mvolo.com/blogs/serverside/archive/2007/08/15/Developing-IIS7-web-server-features-with-the-.NET-framework.aspx にある「.NET Framework での IIS7 モジュールおよびハンドラの開発」を参照してください。

モジュールでは、次の処理を行います。

  1. 要求処理の BeginRequest ステージをサブスクライブします。
  2. フレンドリー形式の URL を使用した要求をインターセプトします。
  3. URL からサブギャラリーのパスとイメージ ファイル名を抽出します。
  4. Qdig index.php への要求をリライトし、サブギャラリー パスとイメージ ファイル名を Qdig で期待されるクエリ文字列パラメータで渡します。

このモジュールの作成は、実際にはかなり単純でした。最初に、System.Web.IHttpModule インターフェイスを実装する C# クラスを作成し、Init メソッド内のイベント ハンドラを BeginRequest イベントに関連付ける必要がありました。

public class QdigSEFModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += new EventHandler(OnBeginRequest);
    }
    ...
}

次に、図 6 に示すように、OnBeginRequest メソッドのリライト機能を実装します。

Figure 6 URL をリライトする (Close Figure 6)
public void OnBeginRequest(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Extract the gallery path and image filename from the url
    // code omitted for clarity ...

    // Build the rewritten url
    String rewrittenUrl = String.Format("~/index.php?Qwd=.{0}&Qif={1}",
                                        galleryPath,
                                        imageFileName);

    String originalQueryString =
             context.Request.ServerVariables["QUERY_STRING"];
    if (!String.IsNullOrEmpty(originalQueryString))
    {
        rewrittenUrl += "&" + originalQueryString;
    }

    // Rewrite the url
    context.Server.TransferRequest(rewrittenUrl);
}

.NET Framework 3.5 の新しい ASP.NET API である HttpServerUtility.TransferRequest は、IIS 7.0 ASP.NET Integrated パイプラインでの適切なリライトを可能にするために使用しています。この API は、現在の要求に対して要求処理を強制的に停止し、ターゲット URL に対する新しい子要求を実行します。これにより、マネージ モジュールは、転送先のコンテンツ タイプに関係なく、要求処理を別の URL に完全に転送できます。このモジュールの完全なソース コードは、この号のダウンロードに含まれています。

モジュールを作成した後で、アプリケーションに配置する必要があります。.NET アセンブリへのモジュールのコンパイルや、アプリケーションの BIN ディレクトリへの配置など、その方法はいくつかあります。ただし、最も簡単なオプションを追求します。モジュールのソース コードを単純にコピーし、それを QdigSEFRewriter.cs としてアプリケーションの App_Code サブディレクトリに保存します。ASP.NET コンパイル システムによって、アプリケーションの起動時にコードが自動的にコンパイルされ、ASP.NET ランタイムで使用可能になります。

最後の手順では、アプリケーションの web.config ファイルの <modules> 構成セクションに登録することで、モジュールをアプリケーションで実行できるようにします。このセクションには、フォーム認証などのいくつかの組み込み ASP.NET モジュールが既に既定で登録されています。アクセス制御用に作成された web.config ファイルの上に、次のような新しいモジュールを追加できます。

<system.webServer>
    <modules>
        <remove name="FormsAuthentication" />
        <add name="FormsAuthentication" type=
            "System.Web.Security.FormsAuthenticationModule" />
        <!-- Add the SEF url rewriting module -->
        <add name="QdigSEFUrlsModule" type="QdigModules.QdigSEFModule"/>
    </modules>
</system.webServer>

この時点で、Qdig のネイティブ クエリ文字列 URL の代わりにフレンドリーな URL を使用して、Qdig ギャラリーへのアクセスを開始できます。ここで、ギャラリーの Mike サブディレクトリ内の Flower.jpg ファイルにアクセスするには、単純に次のようにファイルを要求します。

http://myphpgallery/Mike/Flower.jpg/show

フレンドリーな URL をイメージ ファイルへの直接リンクと区別し、リライトする必要のない URL がモジュールによってリライトされることを防ぐために、フレンドリーな URL には /show 接尾辞を使用することを選択しました。当然、自分のニーズに適した方法でリライト スキームを変更できます。

フレンドリーなハイパーリンクを生成する

残念ながら、まだ完全には終わっていません。ギャラリーはフレンドリーな URL でアクセスできるようになりましたが、ギャラリー ページ自体にはまだ古い形式のハイパーリンクが含まれています。

<a href="http://index.php?Qwd=./Mike&amp;Qif=Arctica.jpg&amp;Qiv=thumbs&amp;
Qis=M" title="First Image"> ... </a>

ユーザーがこれらのリンクをクリックすると、見苦しいクエリ文字列 URL が 表示され、コンテンツをインデックス付けしている検索エンジンは、ページ本文のリンクに親ページのフレンドリー URL を関連付けることができません。

前の手順で有効にしたのと同じフレンドリー URL を使用して Qdig がハイパーリンクを生成するようにすることは、本当に簡単です。ただし、これを行うには、PHP スクリプトをリライトして別のハイパーリンクを生成する必要があります。この処理は行いたくありませんでした。

ASP.NET の応答フィルタ機能を使用すれば、この処理を行う必要がないことがわかりました。Qdig 応答をその場でフィルタ処理する別のマネージ モジュールを作成し、古い形式のハイパーリンクを新しいフレンドリー形式のリンクで置換できます。

これで、モジュールは次の処理を行うようになります。

  1. (ハンドラの実行前に) 要求処理の PreRequestHandlerExecute ステージをサブスクライブします。
  2. URL が Qdig index.php スクリプトに対するものかどうかをチェックします。
  3. 応答に、正規表現を使用してフレンドリーなハイパーリンクの出現をすべて置換する応答フィルタ ストリームを設定します。

モジュール クラスが行う処理は、フィルタ ストリームを選択的に定義することだけなので、モジュール クラスはかなり単純です (図 7 を参照)。実際の処理は、QdigSEFFilter クラスで行われます。このクラスは、Qdig ハイパーリンクのすべての出現を、新しい形式を使用したハイパーリンクで置換します。フィルタ クラスは、抽象 System.IO.Stream クラスを実装し、要求処理の最後に応答をフィルタ処理するために ASP.NET ランタイムによって使用されます。ASP.NET の Integrated パイプライン エンジンにより、応答フィルタは、ASP.NET によって生成されたのではない応答も操作できるため、応答がバッファされている限り、この応答フィルタを使用して、PHP 応答だけでなく静的ファイルや ASP ページも処理できます。IIS 7.0 では、既定ですべての応答がバッファされるため (構成されている制限まで)、このメカニズムが機能できます。

Figure 7 フレンドリーなハイパーリンクを生成する (Close Figure 7)
public class QdigSEFFilterModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.PreRequestHandlerExecute += new EventHandler(
            OnPreRequestHandlerExecute);
    }

    public void Dispose(){}

    public void OnPreRequestHandlerExecute(Object source, EventArgs e)
    {
        HttpApplication app = (HttpApplication)source;
        HttpContext context = app.Context;

        if (context.Request.Path.Equals(
                VirtualPathUtility.ToAbsolute("~/index.php")))
        {
            // wire up the response filter
            context.Response.Filter = new QdigSEFFilter(context);
        }
    }
}

ここで示すフィルタ実装は、ストリームにプッシュされた着信応答バイトをすべてバッファし、応答の文字セットを使用してこれらのバイトを文字列に変換し、URL の正規表現置換を実行します。次に、文字列を元の文字セットに再エンコードし、応答データをランタイムに再びプッシュします。モジュールおよび応答フィルタの完全な実装は、コード ダウンロードに含まれています。

ここでもモジュール コードを QdigSEFFilter.cs という別のソース ファイルとして App_Code ディレクトリに配置し、モジュールをアプリケーションの web.config の <modules> 構成セクションに登録します。この時点で、モジュール セクションは次のようになります。

<system.webServer>
    <modules>
        <remove name="FormsAuthentication" />
        <add name="FormsAuthentication" type=
            "System.Web.Security.FormsAuthenticationModule" />
        <!-- Add the SEF url rewriting module -->
        <add name="QdigSEFUrlsModule" type="QdigModules.QdigSEFModule"/>
        <!-- Add the response filter module -->
        <add name="QdigSEFFilterModule" type=
            "QdigModules.QdigSEFFilterModule" />
    </modules>
</system.webServer>

http://myphpgallery/ の Qdig ページをリフレッシュし、ログインした後で、ページ上のすべてのハイパーリンクはフレンドリーな URL を使用するように変更されました (図 8 を参照)。これまで、既存の ASP.NET 機能および ASP.NET の Integrated パイプライン用に開発された新機能でアプリケーションの機能にいくつかの重要なアップグレードを行った後に、元のアプリケーションは 1 行も変更する必要がありませんでした。

図 8 フレンドリーな URL を使用して修正したギャラリー
図 8 フレンドリーな URL を使用して修正したギャラリー(画像を縮小するには、ここをクリックします)
図 8 フレンドリーな URL を使用して修正したギャラリー
図 8 フレンドリーな URL を使用して修正したギャラリー (画像を拡大するには、ここをクリックします)

パフォーマンスをテストする

この時点で、アプリケーションのソース コードはまったく変更していませんが、Qdig にはかなりの機能が追加されました。このすべての新機能は、アプリケーションのパフォーマンスにどれぐらい影響するでしょうか。Windows 上の CGI メカニズムのプロセス起動のオーバーヘッドにより、従来、パフォーマンスは IIS プラットフォーム上の PHP アプリケーションの考慮事項でした。FastCGI は、このオーバーヘッドを除去することでアプリケーションのパフォーマンスを大幅に改善し、IIS を PHP およびその他の FastCGI 準拠アプリケーションをホストするためのはるかに魅力的なプラットフォームにするという約束を果たします。ただし、導入したアプリケーション拡張によってこのオーバーヘッドの大部分が再び追加される場合は、IIS での PHP アプリケーションの展開を考えたときに、これらのアプリケーション拡張はあまり魅力的ではなくなる可能性があります。

ただし実際には、追加した機能拡張は Qdig のパフォーマンスにほとんど影響しません。図 9 に、アプリケーションに対して実行したパフォーマンス テストの概要を示します。

Figure 9 パフォーマンスの結果 (Close Figure 9)

テスト | 1 秒あたりの要求数 (多いほどよい)
CGI を使用した Qdig | 32
FastCGI を使用した Qdig | 93
FastCGI を使用した Qdig およびフレンドリー URL | 88
CGI を使用した Hello.php | 51
FastCGI を使用した Hello.php | 2239

テストは、100 個の同時仮想クライアントを使用し、サーバー上の CPU を最大限に利用して、ギャラリー内部の一連の Qdig URL を要求することにより、Microsoft Web Capacity Analysis Tool (WCAT) 6.3 で実行されました。最後のテストでは、フレンドリーな URL を Qdig URL の代わりに使用して、変換機能を利用しました。テストを簡単にするために、認証は無効にしました。

CGI から FastCGI に移行することで、スループットが約 3 倍に向上します。2 つのフレンドリー URL モジュールを追加した後、スループットの低下はわずか 5% であり、これは無視できます。

あいにく、Qdig は極端に CPU を消費するため、FastCGI によって提供可能な向上の多くが打ち消されます。比較すると、単純な hello.php スクリプトでは、CGI から FastCGI に移行したときにスループットが 43 倍に向上しました。かなりコストの高い操作である応答フィルタを追加した後でも、全体的なスループットへの影響は、アプリケーション自体のオーバーヘッドに比べれば無視できる程度です。

パフォーマンスを向上させるためにアプリケーションの外部で行えることはほとんどないため、サーバー パフォーマンスの調整に関する限り、これはほとんど最悪の状況です。アプリケーション コード自体を変更することは、アプリケーション開発者でなければ不可能なことが多く、アプリケーション開発者であっても困難な場合があります。筆者の場合は、開始時にアプリケーションは変更しないと決めたため、アプリケーションの変更はもちろん想定外です。

ASP.NET 出力キャッシング

出力キャッシュは ASP.NET の機能であり、Web アプリケーションが生成した応答を同じリソースに対する後続の要求に対して再利用できるようにします。実際に ASP.NET は、ASP.NET アプリケーション内部のアプリケーション データをそのキャッシュ API でキャッシングし、アプリケーションの応答全体を出力キャッシュでキャッシングするために、機能の拡張セットを提供します。どちらの機能も、アプリケーションのパフォーマンスを拡張でき、バックエンド リソースに対する負荷を大幅に減らすことができます。IIS 7.0 での新機能は、ASP.NET 以外のコンテンツ タイプに ASP.NET 出力キャッシュを使用できることです。これこそが、イメージ ギャラリーを少し改良するために、筆者が使用しようとしている機能です。

大量のコーディング作業を必要とし、アプリケーションの処理オーバーヘッドのチャンクを除去することの多い段階的なアプリケーション パフォーマンスの調整とは対照的に、出力キャッシングは、一定割合の要求作業負荷に対するオーバーヘッドのほぼすべてを除去する傾向にあります。その効果は、同じコンテンツがアプリケーションのユーザーによってどれぐらいの頻度で要求されるかというアプリケーションの使用の局所性によって決まります。通常のアプリケーションでは、多くの場合に 90/10 ルールが適用されます。これは基本的に、要求の 90% がコンテンツの 10% に対するものであることを意味します。このコンテンツを出力キャッシュできれば、アプリケーションのスループットと容量が 90% 近く向上する可能性があります。

当然、簡単であればすべての人が行います。出力キャッシングには一連の制限事項があります。特に、動的コンテンツに関してはキャッシング ソリューションの展開が難しくなることがあります。主な制限はコンテンツの動的な性質から生じます。たとえば、Web サイトのページでは、クエリ文字列パラメータ、時刻、またはデータベース更新に基づいてさまざまな応答が生成されることがあります。これらの考慮を怠ると、ユーザーに間違ったキャッシュ応答が返される場合があり、これは望ましくありません。さいわい、ASP.NET 出力キャッシュは、これらの制限をすべて考慮しているため、アプリケーションに出力キャッシング ソリューションを展開するための機能豊富なプラットフォームが提供されます。

ASP.NET ページ パーサーは、<%@ OutputCache %> ディレクティブをサポートしているため、多くの出力キャッシング設定を ASPX ページで直接構成できます。実際に、多くの開発者はこの方法で ASP.NET アプリケーションの出力キャッシングを有効にします。Qdig は PHP で作成されているため、この機能を利用できません。代わりに、ASP.NET HttpCachePolicy API を使用して出力キャッシュを動的に構成する別のモジュールを作成し、出力キャッシュ モジュールが Qdig の応答を正しくキャッシュできるようにしています。

public class QdigOutputCacheModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.PostAuthorizeRequest +=
            new EventHandler(OnPostAuthorizeRequest);
        application.PostRequestHandlerExecute +=
            new EventHandler(OnPostRequestHandlerExecute);
    }

    public void Dispose(){}
}

今回、このモジュールは PostAuthorizeRequest および PostRequestHandlerExecute という 2 つのパイプライン イベントをサブスクライブします。最初の PostAuthorizeRequest は、出力キャッシュ モジュールが要求に対する既存の応答を検索し、それ以上処理せずに戻ろうとする直前に発生します。PostRequestHandlerExecute イベントは、ハンドラ (PHP) が応答を生成した直後、出力キャッシュ モジュールが後で使用するために応答を保存しようとする前に発生します。

PostAuthorizeRequest イベントでは、Qdig 応答がクエリ文字列パラメータによってどのように異なるかを出力キャッシュ モジュールに通知して、出力キャッシュ モジュールが正しい応答を見つけられるようにする必要があります (図 10 を参照)。クエリ文字列パラメータによって異なるキャッシュを構成して、ナビゲーション モードの変更や次のイメージに移動するための次のリンクのクリックなどのユーザー入力に応答することにより Qdig ギャラリーが正しく動作できるようにしています。この機能がないと、最初の要求で応答のキャッシングが行われ、どのイメージが要求されたかに関係なく、ギャラリーに対するその後のすべての要求に対してキャッシュの内容が返されることになります。また、キャッシュが各認証済みユーザーに独自のキャッシュ コピーを正しく提供するように、認証済みユーザーごとに変更します (GetVaryByCustomString の実装を参照)。

Figure 10 正しいキャッシュ バリエーションを確認する (Close Figure 10)
public void OnPostAuthorizeRequest(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Make sure we only process requests to QDIG's index.php.
    String requestPath = context.Request.Path;
    if (!requestPath.Equals(VirtualPathUtility.ToAbsolute
        ("~/index.php")))
        return;

    //  Set up the vary by querystring information
    HttpCacheVaryByParams varyByParams =
      context.Response.Cache.VaryByParams;
    varyByParams["Qwd"] = true; //  gallery path
    varyByParams["Qif"] = true; //  image to display
    varyByParams["Qiv"] = true; //  navigation mode
    varyByParams["Qis"] = true; //  image size
    varyByParams["Qtmp"] = true;//  other control information

    // if user is set, also vary by user (See global.asax)
    context.Response.Cache.SetVaryByCustom("AuthenticatedUser");
}

PostRequestHandlerExecute イベントでは、応答をキャッシュする必要があるかどうかを判断し、適切な設定を構成してキャッシュ可能にする必要がありました (図 11 を参照)。AddFileDependency 呼び出しに注意してください。省略されたコードでは、ギャラリー ディレクトリとイメージへの物理的なパスを判断し (指定されている場合)、ファイル依存関係を応答に追加します。ここで、出力キャッシュでサポートされている CacheDependency メカニズムを使用して、これらのリソースの変更を監視し、変更された場合はキャッシュ済みの応答を無効にします。これにより、応答を長時間キャッシュに保持し、基礎となるデータが変更された場合にのみキャッシュを無効にできます。だれかが新しいイメージをドロップするか、新しいサブギャラリーを作成した場合、キャッシュは影響を受けるキャッシュ済み応答を自動的に削除し、新しい要求は更新されたページを受け取ります。

Figure 11 キャッシュされる応答を構成する (Close Figure 11)
public void OnPostRequestHandlerExecute(Object source, EventArgs e)
{
    HttpApplication app = (HttpApplication)source;
    HttpContext context = app.Context;

    // Make sure we only process requests to QDIG's index.php.
    String requestPath = context.Request.Path;
    if (!requestPath.Equals(VirtualPathUtility.ToAbsolute
        ("~/index.php")))
        return;

    // Enable this response to be output cached
    context.Response.Cache.SetCacheability(HttpCacheability.Public);
    context.Response.Cache.SetExpires(DateTime.Now +
      TimeSpan.FromMinutes(5));

    // code omitted for clarity ...

    // add the dependencies to the response
    context.Response.AddFileDependency(physicalDirectoryPath);
    context.Response.AddFileDependency(physicalFileName);
}

CacheDependency メカニズムは強力な抽象化です。このメカニズムによって、Web サービスの呼び出しやレジストリ キーのチェックなど、あらゆる種類の基本データを監視できる独自の CacheDependency 実装を作成できます。組み込みの CacheDependency 実装には、ファイル、ディレクトリ、および SQL Server データベース テーブルの監視のサポートが含まれています。

配置とテスト

前に行ったのと同じように QdigOutputCache.cs ソース ファイルを App_Code ディレクトリにドロップすることで、最新モジュールを配置し、それを web.config に登録しました。

<system.webServer>
    <modules>
      ...
      <!-- Let the OutputCache module run for all requests -->
      <remove name="OutputCacheModule" />
      <add name="OutputCacheModule" type=
          "System.Web.Caching.OutputCacheModule" />
      <!-- Add the custom output caching module -->
      <add name="QdigOutputCacheModule" type=
          "QdigModules.QdigOutputCacheModule" />
    </modules>
</system.webServer>

QdigOutputCacheModule の追加に加えて、前にフォーム認証に対して行ったのと同様に、managedHandler の前提条件を除去するために OutputCacheModule も再び追加しました。これにより、ASP.NET OutputCacheModule は、PHP コンテンツの要求中に稼働し、出力キャッシング サービスを提供できます。

最後に、GetVaryByCustomString の実装を含む global.asax ファイルを作成しました (図 12 を参照)。GetVaryByCustomString は、モジュールがキャッシュされた応答を認証済みユーザーごとに変えられるようにする関数です。この関数は、認証済みユーザーごとに別々のキャッシュ コピーを維持して、あるユーザーが間違って別のユーザーのパーソナライズ ビューを取得することを防ぐため、重要です。Qdig はパーソナライズ ビューを提供しないため、これは問題になりませんが、いずれにしてもベスト プラクティスとして示します。

Figure 12 Global.asax による GetVaryByCustomString の実装 (Close Figure 12)
<%@ Application language="C#" %>
<script runat="server" language="c#">
public override string GetVaryByCustomString(HttpContext context,
    string s)
{
    if ("AuthenticatedUser".Equals(s))
    {
        // vary by authenticated user
        String currentUser = String.Empty;
        if (context.User != null)
        {
            currentUser = context.User.Identity.Name;
        }

        return currentUser;
    }
    else
    {
        // unknown vary string
        return null;
    }
}
</script>

ここで、パフォーマンスがさらに改善されたかどうかを確認しましょう。前のテストを含む完全な結果を図 13 のグラフに示します。

図 13 パフォーマンスの結果
図 13 パフォーマンスの結果(画像を縮小するには、ここをクリックします)
図 13 パフォーマンスの結果
図 13 パフォーマンスの結果 (画像を拡大するには、ここをクリックします)

予想どおり、出力キャッシングは期待を裏切りませんでした。Qdig のパフォーマンスは、ベースライン FastCGI と検索エンジン フレンドリー (SEF) URL ベンチマークでの毎秒 88 要求から、フルに利用されているサーバー上で毎秒 1386 要求に上昇しています。

全体として、ネイティブ PHP アプリケーションを CGI から FastCGI に移行し、フレンドリーな URL と出力キャッシングでアップグレードした後、最終的な結果は元のアプリケーションの 40 倍を超えるパフォーマンス向上を示しています。これは侮れません。

まとめ

ご覧のように、IIS 7.0 Integrated パイプラインを使用すると、任意のアプリケーションに強力な機能を追加でき、ASP.NET および .NET Framework の能力と便利さを他のフレームワークで作成されたアプリケーションにもたらすことができます。開発チームにとって、この機能は、既存のアプリケーションへの投資と開発者のスキル セットを利用するためのキーとなります。新しいプラットフォームに移行するためにアプリケーションを強制的にリライトする代わりに IIS 7.0 を使用すると、既存のアプリケーションでは、IIS および ASP.NET の既存の機能がもたらす追加的な改善に加え、IIS 7.0 のエンドツーエンド拡張モデルを使用して開発された新機能を利用できます。

この記事では、IIS 7.0 拡張 API で可能なことのほんのわずかしか説明していません。実行時の Web サーバーの拡張性を見ただけですが、IIS 7.0 では構成、管理、および GUI 管理レベルの拡張性も完全に提供されており、Web サーバーをニーズに合わせてアップグレードおよび形成するエキサイティングな機会が数多く作られます。IIS 7.0 の拡張についてもっと詳しく知るには、また、すぐに使用できるいくつかの IIS 7.0 プラグインをダウンロードするには、mvolo.com で筆者のブログを参照してください。

Mike Volodarsky は、Microsoft の Web プラットフォームとツール チームのテクニカル プログラム マネージャです。過去 4 年以上、ASP.NET 2.0 と IIS 7.0 の主要機能セットのデザインと開発を推進してきました。現在は、Windows Server 2008 にこれらのテクノロジの機能を活用する顧客を支援しています。

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


国際化対応: ASP.NET AJAX アプリケーションの国際化

国際化対応

ASP.NET AJAX アプリケーションの国際化

Guy Smith-Ferrier
コードのダウンロード : : InternationalizingASPNETAJAX2008_01.exe (167KB)
この記事の内容 : :
  • ASP.NET AJAX のグローバル化のサポート
  • JavaScript のサポートの必要性
  • アセンブリ ベースの AJAX のローカライズ
  • ファイル ベースの AJAX のローカライズ
この記事は次のテクノロジを使用しています:
ASP.NET AJAX
コンテンツ
アセンブリ ベースのローカライズ モデル
動作のしくみ
ファイル ベースのローカライズ モデル
ファイル ベース モデルの内部
ローカライズ モデルを選択する
ASP.NET AJAX フレームワークをローカライズする
フレームワークのリリース バージョンを作成する
ASP.NET AJAX をグローバル化する
まとめ

ASP.NET 2.0 には Web アプリケーションの優れたローカライズ モデルが含まれ、Microsoft® .NET Framework でも相当程度にグローバル化がサポートされています。ところが、サーバー側コードの国際化サポートは限定的です。ASP.NET AJAX アプリケーションには相当量のクライアント側コードが含まれていますが、これらのコードは標準の ASP.NET ローカライズ モデルではサポートされません。この記事では、ASP.NET AJAX アプリケーションのローカライズおよびグローバル化モデルについて説明し、JavaScript リソースをローカライズして限定的なグローバル化サポートを追加する方法を示します。この記事の内容は、ASP.NET および ASP.NET AJAX に関する知識を持ち、ASP.NET 2.0 アプリケーションの国際化の基本を理解している読者を対象にしています。

アセンブリ ベースのローカライズ モデル

Microsoft ASP.NET AJAX フレームワークには、JavaScript リソースのローカライズに関する 2 つのモデル (アセンブリ ベース モデルとファイル ベース モデル) があります。まず、アセンブリ ベースのモデルを検討してみましょう。

アセンブリ ベースのモデルではリソース (あらゆる ASP.NET または Windows® フォーム アプリケーションで使用される通常の .NET Framework の .resx ファイル) を使用します。リソースはアセンブリに埋め込まれ、取得する場合は ResourceManager クラスを使用します。このプロセスを説明するため、アラートを使用してローカライズ可能なメッセージを表示する JavaScript 関数を呼び出すボタンを 1 つ使用する例を示します。この例に従ってコードを作成する場合、この記事と厳密に同じ名前を使用してください。名前を変えると、正しい結果にならなかった場合に問題の原因を特定することが困難になる可能性があります。

では、まずは AJAX-Enabled Web Application プロジェクトを作成します ([ファイル]、[新規作成]、[プロジェクト]、[ASP.NET AJAX-Enabled Web Application] の順に選択)。このプロジェクト テンプレートには Visual Studio® 2005 SP1 が必要です (AJAX Web Site プロジェクトには結果アセンブリへの明確なアクセス方法がないため、この例で AJAX-Enabled Web Application プロジェクトの代わりに使用することはできません)。プロジェクト AJAXEnabledWebApplicationI18N1 を呼び出します (I18N は Internationalization (国際化) の略称。Internationalization のつづりは "I" で始まり、その後に 18 文字続いて "N" で終わるため)。次に、フォームにボタンと OnClientClick イベントを追加し、このイベントで JavaScript 関数 displayAlert を呼び出します。

<asp:Button ID="Button1" runat="server" OnClientClick="return displayAlert();"  Text="Button"/>

ここで、次のような JScript ファイル DisplayFunctions.js を追加します。

function displayAlert()
{
  window.alert(DisplayFunctionsResources.Greeting);
  return false;
}

displayAlert 関数では、window.alert を使用して DisplayFunctionsResources.Greeting を表示します。DisplayFunctionsResources.Greeting はまだ存在しませんが、これはローカライズ可能なリソースであり、ここでは厳密に型指定されたリソース クラスと同様に参照しています。これについては、後で詳しく説明します。[プロパティ] ウィンドウを使用して、DisplayFunctions.js ファイルの [ビルド アクション] を [埋め込まれたリソース] に設定します。これにより、コンパイラに対して、結果となる AJAXEnabledWebApplicationI18N1.dll アセンブリに DisplayFunctions.js をリソースとして埋め込むように通知されます。

次に、ユーザーが Web アプリケーションで使用するカルチャを設定しやすいようにするため、ページ ヘッダーに Culture 属性と UICulture 属性を追加し、値を auto に設定します。このページ属性は、通常 ASP.NET アプリケーションをローカライズする際に行うように [ツール] メニューの [ローカル リソースの生成] をクリックすれば自動的に追加されますが、この例では、ASP.NET AJAX のローカライズ モデルに ASP.NET ローカライズ モデルを使用する必要はないということを明確にするために、ここでは手動で追加します。

次に、リソース ファイルを追加し、DisplayFunctionsResources.resx という名前を付けます。Greeting という名前のリソース エントリを追加し、値として "Hello World" を設定します。この時点で、DisplayFunctions.js で DisplayFunctionsResources.Greeting への参照が示す内容がおわかりでしょう。次に、DisplayFunctionsResources.fr.resx という名前のリソース ファイルを追加し、リソース エントリ Greeting を追加して、値に "Bonjour Le Monde" を指定します。このリソース ファイルには、.NET Framework の .resx ファイルの標準名前付け規則を使用して、ファイル拡張子 .resx の前にカルチャ名 (この場合はフランス語の fr) を指定します。

次の手順の詳細については後述しますので、ともかく筆者を信じて以下の説明に従ってください。ここでは、Web アプリケーションのリソース (DisplayFunctions.js、DisplayFunctionsResources.resx、および DisplayFunctionsResources.fr.resx) がページで使用可能であることを WebResource ハンドラおよび ScriptResource HTTP ハンドラを使用して確認する必要があります。これらのハンドラは、すべてのリソースを自動的に使用可能にするわけではなく、リソースに公開を許可するマークを付けるアセンブリ属性を検索します。そこで、プロジェクトの AssemblyInfo.cs ファイルを開き、これらのアセンブリ属性をファイルに追加します。

[assembly: System.Web.UI.WebResource(
"AJAXEnabledWebApplicationI18N1.DisplayFunctions.js",
"text/javascript")]

[assembly: System.Web.UI.ScriptResource(
"AJAXEnabledWebApplicationI18N1.DisplayFunctions.js", "AJAXEnabledWebApplicationI18N1.DisplayFunctionsResources", "DisplayFunctionsResources")]

これらのリソースをページに公開するには、ページに ASP.NET AJAX ScriptManager を追加し、ローカライズ可能なリソースへのリンクを生成するように設定する必要があります。それには、ScriptManager の EnableScriptLocalization プロパティを true に設定します。また、ページで参照可能なリソースを指定するため、ScriptManager に ScriptReference を追加する必要があります。ScriptManager 要素の完成形は、次のようになります。

<asp:ScriptManager ID="ScriptManager1" runat="server"
EnableScriptLocalization="true">
  <Scripts>
    <asp:ScriptReference
      Name="AJAXEnabledWebApplicationI18N1.DisplayFunctions.js"
      Assembly="AJAXEnabledWebApplicationI18N1" />
  </Scripts>
</asp:ScriptManager>

ScriptReference には、JavaScript リソースの名前とリソースのアセンブリへの参照が指定されています。

これで、完成した Web アプリケーションを実行できます。ボタンをクリックすると、システムの既定の言語が英語であれば、英語のリソース ("Hello World") が表示されます。フランス語のリソースを Internet Explorer® で表示するには、[ツール] メニューの [インターネット オプション] を選択し、[言語] ボタンをクリックして、[追加] ボタンをクリックします。次に、[フランス語 (フランス)] を選択して [OK] を 2 回クリックすると、ページが更新されます。ここでアプリケーションのボタンをクリックすると、フランス語のリソース ("Bonjour Le Monde") が表示されます。

動作のしくみ

これでローカライズされた ASP.NET AJAX アプリケーションが動作するようになりましたが、フレームワークの実際のしくみは十分に理解されていないと思います。次に、このアプリケーションのしくみについて説明します。すべては web.config から始まります。HTTP ハンドラ セクションを見ると、ScriptResource ハンドラが指定されています。

<add verb="GET,HEAD" path="ScriptResource.axd"
type="System.Web.Handlers.ScriptResourceHandler,
System.Web.Extensions, Version=1.0.61025.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35"
validate="false"/>

この ScriptResourceHandler は、ScriptManager に EnableScriptLocalization="true" を追加すると、ページで参照されます。新規に作成した未変更の ASP.NET AJAX-Enabled Web Application でスクリプトのローカライズを有効にした場合、ソース HTML を表示すると、図 1 のようなコード行が表示されます。

Figure 1 WebResource および ScriptResource の参照 (Close Figure 1)
<script src="/WebResource.axd?d=wLnl3WuFAiWB3W8aelty5A2&amp;
t=632969072944906146" type="text/javascript">
</script>

<script src="/ScriptResource.axd?d=
ekhhG997JkU79ensgQhEZepkzUh3YLeLuz9-kWs0_QimDVEmoCSwcFNOHayaCQX6y2v7StlLcqRUUHTtwG4zJloCtMocAEx2dXPoMNyO-AE1&amp;
t=633132543078401746" type="text/javascript">
</script>

<script src="/ScriptResource.axd?d=
ekhhG997JkU79ensgQhEZepkzUh3YLeLuz9-kWs0_QimDVEmoCSwcFNOHayaCQX6y2v7StlLcqRUUHTtwG4zJjtbgQARGVzU-hP34jwIti3qwKit4BmBA5oDqgYMBzoU0&amp;
t=633132543078401746" type="text/javascript">
</script>

スクリプトのローカライズを有効にしても、有効にしなくても、WebResource の参照は存在しますが、すべてのハンドラで同じパラメータを使用するため、まずはここから始めます。

ASP.NET AJAX を理解する重要なポイントは、パラメータを理解することが動作の理解につながるということです。図 1 には 2 つのパラメータ (d および t) があります。パラメータ d はデータを表し、t は日時スタンプを表します。パラメータ t はリソースを保持するアセンブリの単純な DateTime (刻み間隔で表示) です。値 632969072944906146 は、2006 年 10 月 20 日 02:14:54 を示します。これは System.Web.dll の日時スタンプです。このパラメータの利点は、URI が変更されないかぎり、HTTP ハンドラから返される結果がブラウザによってキャッシュされるという点にあります。そのため、リソースを提供するアセンブリが変更された場合、その日時スタンプが変更され、ScriptManager は異なる URI を生成し (パラメータの値が異なるため)、キャッシュされた値も変更されます。

パラメータ d は暗号化されているため、そのまま見ても意味がわかりません。しかし、System.Web.UI.Page クラスの内部静的メソッド DecryptString および EncryptString を使用して、ページでこれらの文字列を暗号化および復号化できます。図 1 の WebResource のパラメータ d (wLnl3WuFAiWB3W8aelty5A2) を復号化すると、s|WebForms.js となります。縦棒は、単に文字列の異なる値を区切るためのものです。"s" は、データがスクリプトであることを示し、"WebForms.js" は、取得するリソースの名前を示します。WebForms.js リソースは System.Web.dll から取得されます。

ScriptResource のパラメータ d は、もう少し解釈のしがいがあります。図 1 のパラメータ d は、次の文字列の暗号化バージョンです。

ZSystem.Web.Extensions,1.0.61025.0,31bf3856ad364e35|MicrosoftAjax.js|en

また、次の文字列の暗号化バージョンです。

ZSystem.Web.Extensions,1.0.61025.0,31bf3856ad364e35|
MicrosoftAjaxWebForms.js|en

最初の文字 (Z) は、結果が GZip 圧縮形式で返されることを示します。次のパラメータは、リソースを検索可能なアセンブリの完全修飾名 (この例では ASP.NET AJAX System.Web.Extensions.dll) です。その次のパラメータは、取得するリソースの名前 (この例では MicrosoftAjax.js) を示し、最後のパラメータは取得するカルチャ (この例では英語を表す en) を示します。

サンプル アプリケーションの HTML ソースを表示すると、ScriptManager に ScriptReference 要素を追加した結果として生成される 3 番目の ScriptResource が表示されます。その内容は次のようになります。

<script src="/ScriptResource.axd?d=SIfun5X-G8AZLQEDQrC3E6MU4JIgMj1Y2u4fjk0_keEO3Avx4Kbf3v3a
WEvpImtLlh3gxoqTDlXW5ZBtINnuNCTcGSSWUdsCxEJu9l3X
fPtkrSmJXLzRqze5gVmu6Xzz0&amp;t=633137261664170000"
type="text/javascript">
</script>

このパラメータ d を復号化すると、ZAJAXEnabledWebApplicationI18N1|AJAXEnabledWebApplicationI18N1.DisplayFunctions.js| となります。これは、ScriptResource HTTP ハンドラが AJAXEnabledWebApplicationI18N1 アセンブリから AJAXEnabledWebApplicationI18N1.DisplayFunctions.js リソースを取得することを示します。このリソースの [ビルド アクション] を [埋め込まれたリソース] に設定して、アセンブリに埋め込まれるようにしたことを思い出してください。また、カルチャ (復号化されたパラメータ d の最後) が空であることに注意してください。ブラウザの言語を fr に設定すると、パラメータ d のカルチャの値も fr に設定されます。

このジグソー パズルの最後の 1 ピースは、パラメータ d の最初の値 Z です。圧縮を示す Z を指定するのではなく、u を指定すると、結果は非圧縮形式で返されます。そこで、この値を u に変更し、パラメータを再ビルドして (uAJAXEnabledWebApplicationI18N1|AJAXEnabledWebApplicationI18N1.DisplayFunctions.js|) 暗号化し、次のように ScriptResource HTTP ハンドラを直接呼び出します。

http://localhost:49573/ScriptResource.axd?d=t9iZ-3-wfmF_qcNwBHaQrktF2T3rVGzj376NN7kvehJFET0JsrpDTsYt9i
KmZpSmrU1GjtTCo9PIQmuAmMNNkN2Yc_ipVH7JL-jt_hmL9_v8
HtplfhERvz-WsOUxZ-1k0&t=633137261664170000

これで、ハンドルから返される内容が正確にわかります。次のような、非圧縮形式の判読可能な結果になります。

// JScript File
function displayAlert()
{
  window.alert(DisplayFunctionsResources.Greeting);
  return false;
}
DisplayFunctionsResources={
"Greeting":"Hello World"
};

これは、元の DisplayFunctions.js ファイルと DisplayFunctionsResources.resx の JavaScript のシリアル化の混合であることがおわかりでしょう。この混合リソースは、ScriptResource HTTP ハンドラからの要求に応じてシリアル化されます。JavaScript クラスの名前は DisplayFunctionsResources です。一見して、これは対応する .resx ファイルの名前だと思われるかもしれません。しかし、これは筆者の意図によるものです。前述の ScriptResource アセンブリの参照を思い出してください。

[assembly: System.Web.UI.ScriptResource
("AJAXEnabledWebApplicationI18N1.DisplayFunctions.js", "AJAXEnabledWebApplicationI18N1.DisplayFunctionsResources", "DisplayFunctionsResources")]

最後のパラメータ (DisplayFunctionsResources) は、ScriptResource HTTP ハンドラが生成する JavaScript クラスの名前です。この名前を選んだ理由は、厳密に型指定されたリソース クラスの名前付け規則に従って、クラスとファイルに同じ名前を使用することが便利だと考えたからです。ただし、これらの名前は変更してもかまいません。

カルチャを fr に変更した場合、ScriptResource HTTP ハンドラは、(.NET Framework 1.0 のリリース以来存在していた ResourceManager の動作により) DisplayFunctionResources.fr.resx リソースを使用して混合リソースを返します。

// JScript File
function displayAlert()
{
  window.alert(DisplayFunctionsResources.Greeting);
  return false;
}
DisplayFunctionsResources={
"Greeting":"Bonjour Le Monde"
};

ファイル ベースのローカライズ モデル

2 つの AJAX ローカライズ モデルの 2 つ目は、ファイル ベース モデルです。この場合、JavaScript ファイルはアセンブリに埋め込まれず、不変のままクライアントに返されます。明らかに、このモデルは識別が容易なアセンブリが存在しないプロジェクト タイプに適していますが、他にも利点があります。それでは、例を見ていきましょう。

ここでは、AJAX-Enabled Web Site を作成します ([ファイル]、[新規作成]、[Web サイト]、[ASP.NET AJAX-Enabled Web Site] の順に選択)。名前を使用する処理はないので、名前は自由に付けてかまいません。最初の例と同様にボタンを追加します。

<asp:Button ID="Button1" runat="server" OnClientClick="return displayAlert();"  Text="Button"/>

次に、次のようなコードで構成される JScript ファイル DisplayFunctions.js を追加します。

function displayAlert()
{
  window.alert("Hello World");
  return false;
}

この例では、テキストが固定的に記述され、ローカライズできないことに注意してください。

ここで、もう 1 つ JScript ファイルを作成し、DisplayFunctions.fr.js という名前を付けます。

function displayAlert()
{
  window.alert("Bonjour Le Monde");
  return false;
}

最初の例と同様に、ページ ヘッダーに Culture 属性と UICulture 属性を追加し、値を auto に設定します。最後に、ScriptManager に EnableScriptLocalization="true" を設定し、DisplayFunctions.js ファイルへの ScriptReference を追加します。

<asp:ScriptManager ID="ScriptManager1" runat="server"
EnableScriptLocalization="true">
  <Scripts>
    <asp:ScriptReference Path="DisplayFunctions.js"
                         ResourceUICultures="fr"/>
  </Scripts>
</asp:ScriptManager>

ScriptReference では、アセンブリ ベース モデルで使用した Name 属性と Assembly 属性の代わりに、Path 属性と ResourceUICultures 属性を使用します。ここで Web サイトを実行し、言語を選択なしからフランス語に切り替えると、適切にローカライズされたメッセージがボタンに表示されます。

ファイル ベース モデルの内部

ファイル ベース モデルは、アセンブリ ベース モデルとは明確に異なります。Internet Explorer でページを右クリックして [ソースの表示] を選択すると、生成された次のスクリプト タグに言語が指定されていないことがわかります。

<script src="DisplayFunctions.js" type="text/javascript"></script>

フランス語の場合のスクリプト タグは次のとおりです。

<script src="DisplayFunctions.fr.js" type="text/javascript"></script>

1 つ目の相違点は、ファイル ベース モデルでは、カルチャごとに独立した、完全な自己完結型の .js ファイルを使用するということです。リソースを保持し、JavaScript クラスに変換される .resx ファイルはありません。各 .js ファイルにロジックとリソースが含まれます。これには良い面と悪い面があります。良い面は、カルチャごとにロジックを簡単にカスタマイズできるということです。悪い面は、ロジックをカスタマイズしない場合、リソースを変更するためだけに各カルチャのロジックを重複する必要が生じるということです。メンテナンスが難しくなります。

前述した ScriptReference に戻ってみましょう。

<asp:ScriptReference Path="DisplayFunctions.js"ResourceUICultures="fr"/>

Path 属性は、JavaScript ファイル名 (DisplayFunctions.js) を示します。ResourceUICultures 属性は、一意のスクリプトが存在するカルチャの一覧を示します。この属性は必須です。ScriptResource HTTP ハンドラを使用すれば、ファイル ディレクトリから簡単に取得できるのだから、この属性は無駄だという意見もあるかもしれません。しかし、パフォーマンス向上のため、静的スクリプト ファイルは直接 IIS によって提供されるので、ScriptResource ハンドラには要求を評価する機会がありません。このプロセスのため、一覧をハードコーディングする必要があります。

これまでの説明では、ファイル ベース モデルはやや劣るように思われるかもしれませんが、若干の再編成によって有用性が高まります。アセンブリ ベース モデルで、ScriptResource HTTP ハンドラによってクライアントに返される JavaScript が、元の JavaScript ファイルと .resx ファイルからのリソースを表す JavaScript クラスとの混合物であったことを思い出してください。ファイル ベース モデルでも同じ方法を選択することが可能です。前述の例と同じ関数を作成するのではなく、次のようなコードを記述します。

function displayAlert()
{
  window.alert("Hello World");
  return false;
}

また、次のように記述します。

function displayAlert()
{
  window.alert("Bonjour Le Monde");
  return false;
}

このコードをリファクタリングして、各カルチャのロジックを同一のままにしておき、リソースを JavaScript ファイルの後半に移動することができます。次のようなコードを試しに作成してください。

function displayAlert()
{
  window.alert(DisplayFunctionsResources.Greeting);
}

DisplayFunctionsResources={
'Greeting':'Hello World'
}

また、次のように記述します。

function displayAlert()
{
  window.alert(DisplayFunctionsResources.Greeting);
}

DisplayFunctionsResources={
'Greeting':'Bonjour Le Monde'
}

これによって、各カルチャに同じ変更を適用しなければならないことに変わりありませんが、すべてのカルチャのファイルの前半が同一であるため、作業がはるかに楽になり、メンテナンス性が確実に向上します。

さらなる改良を検討してみましょう。ロジックを別の共通 JavaScript ファイル (DisplayFunctions.Common.js など) で管理し、ビルド時に、アセンブリ ベース モデルの ScriptResource HTTP ハンドラと同じ方法でカスタム ビルド タスクを使用して、ローカライズ バージョンを対応する .resx ファイルから生成する方法もあります。これは読者のために残された課題です。

ローカライズ モデルを選択する

2 つのモデルを理解したところで問題になるのは、どちらの方法を選択したらよいかということです。図 2 に両者の比較を示します。

Figure 2 ファイル ベースとアセンブリ ベースのローカライズ (Close Figure 2)

基準 | ファイル ベース | アセンブリ ベース
ScriptReference の属性 | Path、ResourceUICultures | Name、Assembly
ローカライズされたリソースをスクリプトで使用できる | いいえ | はい
スクリプトをローカライズできる | はい | いいえ
Web Application プロジェクトで使用できる | はい | はい
Web Site プロジェクトで使用できる | はい | いいえ
コントロール ライブラリで使用できる | はい | はい

ファイル ベース モデルの大きな利点は、クライアント側ロジックを簡単にカスタマイズして Web Site プロジェクトで使用できるということです。逆に、アセンブリ ベース モデルには、ローカライズされたリソースを簡単に利用し、JavaScript ファイルをアセンブリに埋め込むことができるという利点があります。そのため、このモデルはコントロール ライブラリに最適です。どちらのモデルが適しているかを規定することが読者の仕事になります。筆者の場合は、Visual Studio 2005 SP1 を使用しており、カルチャごとにロジックをカスタマイズする必要性が少ないので、アセンブリ ベース モデルが適しています。

ASP.NET AJAX フレームワークをローカライズする

.NET Framework にはローカライズ バージョンの .NET Framework 言語パックがあります。インストールされている場合は、これらの言語パックを利用して、エラー メッセージやダイアログなどのローカライズされたリソースを公開できます。ASP.NET AJAX フレームワークは、クライアントで使用するエラー メッセージなどのリソースを利用します。Microsoft 以外のソースからは .NET Framework 言語パックを新規に作成することはできませんが、ASP.NET フレームワークのローカライズ バージョンはだれでも作成できます。現時点で提供されている ASP.NET AJAX フレームワークは英語版のみですが、Visual Studio 2008 に組み込まれた時点では、ローカライズ バージョンも提供される予定です。ただし、ローカライズのサポートは .NET Framework でサポートされる言語のサブセットに限定されるため、たとえば ASP.NET AJAX フレームワークをウェールズ語にローカライズできるようになる可能性は低いでしょう。そのため、ASP.NET AJAX フレームワークをローカライズする方法が必要です。

さいわい、その方法は既に存在し、前述のファイル ベース ローカライズ モデルを基に構築されています。ASP.NET AJAX アセンブリには英語リソースが含まれています。Reflector を使用して System.Web.Extensions.dll を開くと (図 3 を参照)、既定で使用される JavaScript リソースが表示されます。

図 3 Reflector を使用して System.Web.Extensions.dll の JavaScript リソースを表示する
図 3 Reflector を使用して System.Web.Extensions.dll の JavaScript リソースを表示する(画像を縮小するには、ここをクリックします)
図 3 Reflector を使用して System.Web.Extensions.dll の JavaScript リソースを表示する
図 3 Reflector を使用して System.Web.Extensions.dll の JavaScript リソースを表示する (画像を拡大するには、ここをクリックします)

次のように、同じ JavaScript リソース名の ScriptReference を追加するだけで、ScriptManager に異なる JavaScript ファイル セットを指定することができます。

<asp:ScriptManager ID="ScriptManager1" runat="server"
  ScriptMode="Debug" EnableScriptLocalization="true">
  <Scripts>
    <asp:ScriptReference Name="MicrosoftAjax.js"
      Path="~/AjaxFramework/MicrosoftAjax.js"
      ResourceUICultures="fr" />
  </Scripts>
</asp:ScriptManager>

この動作を確認するには、新規に AJAX-Enabled Web Application プロジェクトを作成し、ScriptManager を上記のように変更し、フォルダを作成して AjaxFramework という名前を付け、MicrosoftAjax.Debug.js ファイルを ASP.NET AJAX MicrosoftAjaxLibrary\System.Web.Extensions\1.061025.0 フォルダから AjaxFramework フォルダにコピーします。次に、MicrosoftAjax.Debug.js を MicrosoftAjax.Debug.fr.js にコピーしてフレームワークのフランス語版を作成します。MicrosoftAjax.Debug.fr.js を開き、ファイルの最下部に移動して Sys.Res を見つけ、argumentType フィールドを探して値を "L\' objet ne peut pas être converti le type demandé." に変更します。これは、"オブジェクトを必要な型に変換できません" のフランス語訳です。

次に、フォームにボタンを追加し、OnClientClick イベントで showError を呼び出します。また、次の showError JavaScript 関数を default.aspx に追加します。

function showError()
{
  window.alert(Sys.Res.argumentType);
  return false;
}

ページに Culture 属性と UICulture 属性を追加し、値を auto に設定します。アプリケーションを実行します。ブラウザの言語がフランス語の場合、フランス語のリソースが表示されます。

フレームワークのリリース バージョンを作成する

問題は解決しましたが、まだデバッグ バージョンの段階です。ScriptManager の ScriptMode を Release に設定してアプリケーションを実行すると、MicrosoftAjax.js ファイルまたは MicrosoftAjax.fr.js ファイルを検索したときに、これらのファイルが見つからないため、エラーが発生します。最も単純な解決方法は、これらのファイルのデバッグ バージョンをコピーして、リリース バージョンのファイル名に変更することです。これで当面の問題は解決しますが、このままではリリース バージョンとデバッグ バージョンの内容は同一で、リリース バージョンは最適化されておらず、多くの不要なコメントや空白が残っています。

さいわい、ASP.NET AJAX には解決策があります。AJAX Control Toolkit には、まさにこの問題に対処するための JavaScriptCommentStripper という名前の MSBuild タスクがあります。このタスクは JavaScript ファイルを入力として受け取り、このファイルからコメントと空白を削除したファイルを出力します。図 4 に、debug.js ファイルをコメントと空白が削除されたファイルに変換する単純な MSBuild スクリプトを示します。この例の場合、MicrosoftAjax.Debug.js および MicrosoftAjax.Debug.fr.js ファイルから、2 つの追加ファイル MicrosoftAjax.Debug.js.Stripped および MicrosoftAjax.Debug.fr.js.Stripped が出力されます。出力結果に問題がなければ、これらのファイルの名前をそれぞれ MicrosoftAjax.js および MicrosoftAjax.fr.js に変更します。これで準備完了です。

Figure 4 JavaScriptCommentStripper MSBuild タスク (Close Figure 4)
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="StripComments">
  <UsingTask TaskName="JavaScriptCommentStripperTask"
  AssemblyFile="C:\Program Files\Microsoft ASP.NET\ASP.NET 2.0 AJAX
  Extensions\AJAXControlToolkit\Binaries\JavaScriptCommentStripper.dll"/>

  <ItemGroup>
    <SourceFiles Include="*.Debug*.js"/>
  </ItemGroup>

  <Target Name="StripComments">
    <JavaScriptCommentStripperTask SourceFiles="@(SourceFiles)"
    DestinationFiles="@(SourceFiles->'$(TargetDir)%(RecursiveDir)
    %(Filename)%(Extension).Stripped')"/>
  </Target>

</Project>

このタスクが JavaScript に与える効果を確認するには、処理前と処理後の JavaScript ファイルを開きます。図 5 に、元の Microsoft.Debug.js ファイルの先頭部分を示します。

Figure 5 Microsoft.Debug.js の先頭部分 (Close Figure 5)
//-----------------------------------------------------------------------
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------
// MicrosoftAjax.js
// Microsoft AJAX Framework.

Function.__typeName = 'Function';
Function.__class = true;

Function.createCallback = function Function$createCallback(method,
    context) {
    /// <param name="method" type="Function"></param>
    /// <param name="context" mayBeNull="true"></param>
    /// <returns type="Function"></returns>
    var e = Function._validateParams(arguments, [
        {name: "method", type: Function},
        {name: "context", mayBeNull: true}
    ]);

次に、Microsoft.Debug.js.Stripped ファイルの削除されたコードを示します。こちらのコードの方が明らかに短く、見づらくなっています。この例では、JavaScriptCommentStripper タスクによってファイル サイズが 260,096 バイトから 182,454 バイトに削減されています。

Function.__typeName = 'Function';Function.__class = true;
Function.createCallback = function Function$createCallback(method, context) {var e = Function._validateParams(arguments, [
    {name: "method", type: Function},
    {name: "context", mayBeNull: true}
]);

ASP.NET AJAX をグローバル化する

.NET Framework と比較すると、JavaScript のグローバル化のサポートは不十分という表現では言い足りません。.NET Framework のグローバル化クラスを広大な大海原とすれば、JavaScript のグローバル化のサポートは水たまり程度のものです。ところが、この水たまりをさらに広げるとなると、若干の問題が生じます。System.Globalization 名前空間の大部分は ASP.NET AJAX フレームワークで実装可能ですが、果たして ASP.NET AJAX フレームワークで実装する必要はあるでしょうか。各クラスのダウンロード サイズが増えるため、ビジネス ケースを評価する必要があります。その結果、ASP.NET AJAX フレームワークでのグローバル化のサポートは、.NET Framework の場合より相当に制限されます。このことを念頭に置いて、グローバル化機能を検討してみましょう。

既定では、ASP.NET AJAX のグローバル化のサポートは無効になっています。有効にするには、ScriptManager の EnableScriptGlobalization プロパティを true に設定する必要があります。この単純な操作で生成されたランタイム ページには、次のように、__cultureInfo 変数が指定されています。

var __cultureInfo = '{"name":"en-GB","numberFormat":{
"CurrencyDecimalDigits":2,
"CurrencyDecimalSeparator":".","IsReadOnly":true,
"CurrencyGroupSizes":[3],"NumberGroupSizes":[3],
"PercentGroupSizes":[3],"CurrencyGroupSeparator":",
","CurrencySymbol":"£","NaNSymbol":"NaN",
"CurrencyNegativePattern":1,"NumberNegativePattern":1,
"PercentPositivePattern":0,"PercentNegativePattern":0,
"NegativeInfinitySymbol":"-Infinity","NegativeSign":"-
...

この変数はサーバーの CultureInfo.CurrentCulture から生成され、サーバーのユーザー設定を優先します。ScriptManager には、ユーザー設定を無視するオプションはありません。

MicrosoftAjax.js には、この大きい変数への参照が含まれています。

Sys.CultureInfo.CurrentCulture = Sys.CultureInfo._parse(__cultureInfo);
delete __cultureInfo;

この変数は読み取られ、解析されて Sys.CultureInfo.CurrentCulture 変数に割り当てられた後、解放されます。

MicrosoftAjax.js のこの上の数行では、次のように、InvariantCulture が定義されています。

Sys.CultureInfo.InvariantCulture = Sys.CultureInfo._parse('{"name":"",
"numberFormat":
{"CurrencyDecimalDigits":2,"CurrencyDecimalSeparator":".",
"IsReadOnly":true,"CurrencyGroupSizes":[3],"NumberGroupSizes":[3],
"PercentGroupSizes":[3],"CurrencyGroupSeparator":",",
"CurrencySymbol":"\u00A4","NaNSymbol":"NaN",
"CurrencyNegativePattern":0,"NumberNegativePattern":1,
"PercentPositivePattern":0,"PercentNegativePattern":0,
"NegativeInfinitySymbol":"-Infinity","NegativeSign":"-
...

不足しているのは CurrentUICulture の値です。値は CurrentCulture と同じであると仮定することもできますし (心もとない仮定ではありますが)、CurrentCulture と同様のソリューションを作成して、クライアントに送信された生成済み HTML に適切な変数を挿入することもできます。

それでは、グローバル化をサポートする処理を検討していきましょう。AJAX-Enabled Web Application を作成し、ScriptManager の EnableScriptGlobalization を true に設定します。次に、ラベルとボタンを追加し、OnClientClick イベントを追加して、このイベントで JavaScript 関数 showToday への呼び出しを返します。

<script type="text/javascript">
function showToday()
{
  var date = new Date();
  $get('Label1').innerHTML = date.localeFormat("D");
  return false;
}
</script>

ページに Culture 属性と UICulture 属性を追加し、値を auto に設定します。ブラウザの言語を英語または言語選択なしに設定してアプリケーションを実行し、ボタンをクリックすると、[地域と言語のオプション] ダイアログのカルチャ設定の既定のフォーマットで日付が表示されます。ブラウザの言語を "fr" に変更してページを更新し、ボタンをクリックすると、ラベルにフランス語の日付形式で日付が表示されます (lundi 2 juillet 2007 など)。

JavaScript の Date クラスをご存知の読者なら、showToday 関数で使用されている localeFormat メソッドが存在しないことにお気付きでしょう。これは、ASP.NET AJAX フレームワークに含まれている型拡張の 1 つです。MicrosoftAjax.Debug.js でこれらのコードを見ることができます。以下に、Date クラスの localeFormat メソッドを示します。

Date.prototype.localeFormat = function Date$localeFormat(format) {
    /// <param name="format" type="String"></param>
    /// <returns type="String"></returns>
    var e = Function._validateParams(arguments, [
        {name: "format", type: String}
    ]);
    if (e) throw e;

    return this._toFormattedString(format,
Sys.CultureInfo.CurrentCulture);
}

これらの型拡張の完全な一覧については、ajax.asp.net/docs/ClientReference/Global/JavascriptTypeExtensions/default.aspx をご覧ください。

Sys.CultureInfo クラス、Sys.CultureInfo.InvariantCulture プロパティ、および Sys.CultureInfo.CurrentCulture プロパティと型拡張によって、ASP.NET AJAX フレームワークは限定的なグローバル化フレームワークを提供します。

まとめ

ASP.NET AJAX フレームワークは、これまでカスタム実装以外では見られなかったレベルで JavaScript にローカライズおよびグローバル化のサポートを提供します。このフレームワークでは 2 つのローカライズ モデルが用意され、いずれか要件に適した方を選択できます。グローバル化のサポートは、.NET Framework の豊富なサポートと比較すると基本的なものにとどまりますが、だからといって創造性を発揮できないわけではありません。固有の型拡張や JavaScript クラスを追加して、個別の要件をサポートすることができます。他のあらゆるテクノロジと同様、AJAX は今後もアップグレードを続けていくと思われますので、ASP.NET AJAX の今後のリリースでこのモデルが機能強化されることを期待してください。

Guy Smith-Ferrier は、MVP であり、INETA の講演および『.NET Internationalization』(Addison-Wesley、2006 年) の執筆も行っています。マイクロソフト認定プロフェッショナル デベロッパーとして欧米各地でのカンファレンスで講演を行い、各種雑誌で 50 を超える記事を執筆しています。また、英国で .NET Developer Network (dotnetdevnet.com) を運営しています。詳しくは、guysmithferrier.com をご覧ください。


WCF シンジケーション: WCF および .NET Framework 3.5 を使用した HTTP プログラミング

WCF シンジケーション

WCF および .NET Framework 3.5 を使用した HTTP プログラミング

Justin Smith
この記事は、Visual Studio 2008 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • HTTP メッセージの基本
  • URI に情報を追加する
  • WCF と HTTP
  • WCF を使用した RSS および ATOM
この記事は次のテクノロジを使用しています:
Windows Communication Foundation
コンテンツ
HTTP メッセージの基本
追加情報を指定する
REST と Web
WCF を使用した HTTP プログラミング
WCF コントラクトの HTTP 動詞
バインドとホスティング
新しいシンジケーション API
SyndicationFeed を作成する
まとめ

Microsoft® .NET Framework 3.0 の一部として初めてリリースされたとき、Windows® Communication Foundation (WCF) には、多様なトランスポートによる Plain Old XML (POX) および SOAP メッセージングの共通オブジェクト モデルが組み込まれていました。WCF は WS-* Web サービス標準のサポートも充実しているため、他の最新サービス プラットフォームとの相互運用性を比較的容易に確保できます。.NET Framework 3.0 の WCF は拡張性が高く、メッセージ ログ、アクティビティ追跡、サービス スロットル、インスタンス管理、スレッド制御などの堅牢な管理容易性機能を備えています。

.NET Framework 3.5 の WCF は、.NET Framework 3.0 の拡張ポイントを基に構築され、Web の原則に従ったサービスの構築に対する優れたサポートを提供します。WCF には、操作が容易な HTTP プログラミング モデル、JavaScript Object Notation (JSON) メッセージング機能、シンジケートされたコンテンツを作成および使用する新しいシンジケーション API などがあります。これらの機能セットを備えた WCF は、ASP.NET AJAX コントロール、SilverlightTM クライアント、ブラウザのいずれにおいても Web クライアントにサービスを接続するためのサービス プラットフォームに最適です。これらの機能は部分的な信頼のシナリオ (ASP.NET の中程度の信頼など) にも有用であるため、広く利用されているホスティング環境で WCF サービスをホストできます。また、さらにその機能を補完するために、サービスの稼動開始にかかる手間を飛躍的に軽減する新しいツールが Visual Studio® 2008 に統合されました。

SOAP および WS-* 標準との Web 中心の通信機能が 1 つのサービス スタックおよびオブジェクト モデルに統合されていることも .NET Framework 3.5 の WCF の優れている点です。これにより、SOAP および WS-* を使用して社内および社外との通信サービスを構築できます。また、同じサービスを構成して、Web のプロトコルで外部との通信を行うこともできます。WCF にサービスの通信を任せて、ユーザーはサービス公開の機能に専念できます。

この記事では、.NET Framework 3.5 の WCF の新しい Web 中心の機能について説明します。最初に、HTTP および Web の重要なアーキテクチャ上の原則に関するレベル設定について説明し、新しい WCF の HTTP プログラミング モデル、および新しいシンジケーション API へと話題を進めていきます。

HTTP メッセージの基本

Web は毎日のように使っていても、Web の動作の基本概念について考えることは少ないのではないでしょうか。この記事では、アーキテクチャの概念を説明することが趣旨ではなく、一見しただけではわかりにくい Web の重要な概念を説明します。筆者は、Web のプロトコルに従ってサービスを構築する場合には最初にこれらの原則を十分に理解しておく必要があると考えています。この話題を専門に扱った書籍も出版されているので、この記事では要点のみを取り上げます。

HTTP は Web のトランスポートです。HTTP は、要求/応答メッセージ交換パターン (MEP) を要求します。この要求は、ブラウザを使用して Web ページに移動するたびに発生します。リソースを要求すると、そのリソースを含む応答が返されます。HTTP トランスポートは、GET、POST、PUT、DELETE など、要求/応答 MEP 上に機能を追加する動詞 (メソッドとも呼ばれる) を定義します。HTTP 動詞間の相違を実例で示すため、HTTP POST の内容を検討してみましょう。次のコードは、空白およびコメントを削除したバージョンです。

POST /myservice/PostAlbum HTTP 1.1
HOST: www.cloudsample.net
<albumInfo>
  <albumId>15</albumId>
</albumInfo>

リッスンしているアプリケーションに送信されるのはメッセージのみです。この場合、メッセージは http://www.cloudsamples.net/myservice/PostAlbum に送信されます。メッセージには、albumId フィールドの値を格納したペイロードもあります。つまり、HTTP POST はリッスンしているアプリケーションにペイロードを送信する手段と考えることができます。

この例のペイロードは単純なので、ペイロード全体を数値 15 で表すことができます。これが機能するには、albumId フィールドの値に数値 15 が割り当てられることを受信側アプリケーションが認識している必要があります。これは、HTTP GET の特に重要な機能の 1 つです。たとえば、以前の HTTP POST から HTTP GET に移行すると、次のようになります。

GET /myservice/GetAlbum/15 HTTP 1.1
HOST: www.cloudsamples.net

HTTP GET は最適化された HTTP POST であり、ペイロードを不要にすることができます。URI はペイロードです。そのため、メッセージ サイズが小さくなり、それによって送信時間も短縮されます。メッセージ サイズの縮小と URI への依存は、さらに好結果をもたらします。その好結果とは、他のアプリケーション (プロキシやブラウザなど) で、同じリソースに対する以前の HTTP GET メッセージの結果をキャッシュすることが容易になるということです。HTTP GET は他のどの HTTP 動詞よりも使用頻度が高いという点に注意することが重要です。

HTTP GET の基本は、リソースの要求です。例として、Web 上のおもしろいリソース (たとえば http://www.cloudsamples.net/pictureservices) を探して、そのリソースを友人に電子メールで送信する場合を考えてみましょう。電子メールを受信した友人がリンクをクリックすると、自分が閲覧したのと同じページが表示されるようにしたいと思います。http://www.cloudsamples.net/pictureservices のリソースが突然 MSNBC の最新トピックに変わったら、だれでも驚きます。HTTP GET で使われる URI はリソースを示し、通常はリソースが変わることは想定しません。

HTTP GET が表す内容には明確な合意がありますが、他の HTTP 動詞には明確な合意がありません。HTTP DELETE メッセージを受信側アプリケーションに送信する場合を考えてみましょう。受信側アプリケーションは、状態を変更したり、何らかの操作を行う必要があるでしょうか。通常、HTTP DELETE は減算の状態変更または操作に関連付けられますが、その状態変更の詳細は定義されません。HTTP PUT についても同じことが言えます。通常、HTTP PUT は加算の状態変更または操作に関連付けられますが、変更の厳密な内容はアプリケーションによって異なります。

追加情報を指定する

HTTP GET を受信するアプリケーションは、URI に埋め込まれた情報を使用して、応答として送信するリソースを識別します。例として、次のような URI セットを見てみましょう。

contoso.com/artists/Flaming+Hammer/HitMe
contoso.com/artists/Northwind/Overdone

この場合、Contoso Corporation には音楽に関連するリソースを提供するアプリケーションがあります。これらの例を見れば、アーティスト名とアルバム名を URI に割り当てる方法がおわかりだと思います。

contoso.com/artists/[artist]/[album]

アプリケーションは http://contoso.com/artists/Northwind/Overdone の HTTP GET メッセージを受け取ると、Northwind の Overdone アルバムに関連するリソースを返します。この URI には明確なパターンがあります。URI は基本情報 (contoso.com/artists)、およびアーティストとアルバムの値を保持する URI セグメント ("ホール") で構成されています。

また、クエリ文字列パラメータに同じ種類の情報を埋め込む処理が一般的に行われます。前の例とは形式が異なりますが、結果は同じです。クエリ文字列パラメータに指定する URI ホールを次に示します。

contoso.com/artists/Flaming+Hammer?album=HitMe
contoso.com/artists/Northwind?album=Overdone

この場合の URI とクエリ文字列の構文は次のとおりです。

contoso.com/artists/[artist]?album=[album]

また、HTTP トランスポートでは、追加情報の指定に一連の拡張可能なヘッダーを使用します。この情報をキャッシュ、メッセージのデータ タイプ、送信側アプリケーションの名前、コンテンツの長さ、ホストのオペレーティング システムなどに関連付けることができます。図 1 に、共通ヘッダーを持つ HTTP GET メッセージ (1 ～ 8 行目) と応答メッセージ (9 ～ 14 行目) を示します。

Figure 1 HTTP GET 要求と応答 (Close Figure 1)
1    GET /PictureServices/Feed.svc/picture/Pictures;Bridge.jpg HTTP/1.1
2    Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
       application/x-ms-application, application/vnd.ms-xpsdocument,
       application/xaml+xml, application/x-ms-xbap,
       application/vnd.ms-excel, application/vnd.ms-powerpoint,
       application/msword, application/x-shockwave-flash,
       application/x-silverlight, */*
3    Accept-Language: en-us
4    UA-CPU: x86
5    Accept-Encoding: gzip, deflate
6    User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0;SLCC1;
       .NET CLR 2.0.50727; InfoPath.2; .NET CLR 1.1.4322;
       .NET CLR 3.5.20706; .NET CLR 3.0.590; MS-RTC LM 8)
7    Host: www.cloudsamples.net
8    Proxy-Connection: Keep-Alive

9    HTTP/1.1 200 OK
10   Content-Type: image/jpeg
11   Server: Microsoft-IIS/7.0
12   X-Powered-By: ASP.NET
13   Date: Sat, 15 Sep 2007 18:57:11 GMT
14   Content-Length: 106333

HTTP GET 要求の Accept ヘッダーは、クライアントが指定した受信データ形式を示しています。値リストの長さを見れば、クライアントが受信可能なデータはイメージ、Office ドキュメント、Silverlight アプリケーションなど、数種類あるということがおわかりでしょう。HTTP GET への応答には、Content-Type ヘッダー (10 行目の image/jpeg) の値によって形式が指定されたデータが含まれています。この単純なメカニズムにより、送信側アプリケーションと受信側アプリケーションとの間でデータ形式を調整できます。データ形式を調整するためのヘッダーは、Web Services Description Language (WSDL) や XML Schema Definition (XSD) の SOAP Web サービスの文法ほど表現力が高くはありませんが、Web に使用するには十分です。

REST と Web

HTTP トランスポートの主な部分を見ると、Web を緩やかに結合する重要な原則が指定されています。2000 年に、博士課程の学生 Roy Fielding が学位論文を完成させ、その中で、これらの原則を Representational State Transfer (REST) と呼ばれるアーキテクチャ スタイルに体系化しました (Roy の論文は ics.uci.edu/~fielding/pubs/dissertation/top.htm でご覧いただけます)。

REST のアーキテクチャ スタイルについては、書籍、ソフトウェア カンファレンス、数多くのブログで詳しく説明されています。この記事では REST のすべてを網羅することはできないので、以下の 3 つの重要なポイントに絞って説明します。

  • URI を使用する
  • HTTP GET が特殊である
  • Content-Type はデータ モデルである

この記事では、この 3 つのポイントを Web の原則と見なします。これらの原則をアプリケーションに採り入れることにより、相互運用性が飛躍的に向上し、アプリケーション機能を利用するユーザーの範囲も拡大します。相互運用性とユーザー範囲の拡大は、多くの Web サービス イニシアチブの主要な動因です。Web サービスは、結局のところ、機能を構造化メッセージング スキームによって公開する手段です。Web (HTTP 要求/応答) の原則では、選択可能なメッセージング スキームのサブセットを規定します。あらゆるプラットフォーム、アプリケーション、オペレーティング システム、ユーザーが Web の原則を理解しているため、Web サービス アプリケーションにこれらの原則を採り入れることでユーザー範囲が拡大し、相互運用性が向上します。

REST と WS-* の選択は二者択一ではないため、Web の原則を採用しても WS-* に悪影響はないということを認識しておくことが重要です。どちらの方式もあらゆる種類の通信要件に対処可能なので、構築するすべてのサービスにどちらか一方の方式を一様に適用しないことをお勧めします。たとえば、REST は 中継点へのメッセージ送信は不得手ですが、WS-* はこの点で優れています。一方、WS-* アプリケーションはブラウザのような単純なクライアントとの通信が不得手ですが、REST アプリケーションはこの点で優れています。

WCF を使用した HTTP プログラミング

これらの Web の基本に基づいて、.NET Framework 3.5 の WCF を使用して Web の原則に従ったアプリケーションを構築する場合を検討してみましょう。最初に HTTP プログラミング モデルの新しい構成要素について説明し、その後でシステム全体の話に移っていきます。

"URI を使用する" という原則を採用する場合は URI の構築および解析に習熟する必要があります。この作業を手動で行うと、間違いやすく、煩雑です。新しい System.UriTemplate 型および System.UriTemplateMatch 型は、URI の作成および解析という煩雑な作業を担い、WCF の新しい HTTP 機能の基本的な構成要素の一部となります。

UriTemplate 型は前述の URI ホールを定義する基本的な構成要素です。UriTemplate 型のコンストラクタは URI ホールを表す文字列を受け取ります。UriTemplate オブジェクトをインスタンス化すると、テキスト値をインスタンス作成時に定義したホールにバインドする 2 つのインスタンス メソッドが公開されます。これらのメソッドは、値が指定されたホールの URI を返します。

UriTemplate オブジェクトは、System.Uri オブジェクトのホールの値を抽出する Match インスタンス メソッドも公開します。Match インスタンス メソッドは 2 つの System.Uri オブジェクトを受け取ります。最初の Uri はベース アドレス、2 番目の Uri は照合対象です。System.UriTemplateMatch オブジェクトは Match メソッドから返されます。UriTemplateMatch オブジェクトには、UriTemplate のインスタンス作成時に定義されたホールの値のコレクションが格納されています。

図 2 のコードは、UriTemplate 型および UriTemplateMatch 型を使用して Uri をラウンドトリップする方法を示しています。このコードの出力結果は、次のようになります。

Figure 2 テンプレート ベースの URI を作成する (Close Figure 2)
// create a URI bound to the template
Uri baseAddress = new Uri(@"http://localhost:2000");
UriTemplate template = new UriTemplate("{artist}?album={album}");
Uri boundUri = template.BindByPosition(baseAddress, "Northwind",
    "Overdone");
Console.WriteLine(boundUri.ToString());

// retrieve the value of the artist segment
UriTemplateMatch match = template.Match(baseAddress, boundUri);
String bandName = match.BoundVariables["artist"];
Console.WriteLine("the name of the artist is  {0}", bandName);

http://localhost:2000/Northwind?album=Overdone
the name of the artist is  Northwind

System.UriTemplate 型および System.UriTemplateMatch 型では、名前空間に通常の WCF System.ServiceModel モニカがありません。これは、これらの型の機能上、WCF アプリケーション内でこのモニカを使用する必要がないためです。URI を作成または解析する必要がある場合にはきわめて便利です。

UriTemplate 型および UriTemplateMatch 型は、もともと使い方が簡単ですが、WCF チームは WCF サービスで使用するにあたって、この 2 つの型をさらに簡単に使用することを考えました。目標は、URI セグメントおよびクエリ文字列パラメータをアプリケーション機能に単純かつ直観的にマップする方法を提供することでした。それには、次のコードに示されているモデルが条件を満たします。

[ServiceContract]
public interface IPictureService
{
  [OperationContract]
  [WebGet(UriTemplate = "picture/{pictureId}")]
  Stream GetPicture(String pictureId);

  [OperationContract]
  [WebGet(UriTemplate = "picture/t/{pictureId}")]
  Stream GetPictureThumbnail(String pictureId);
}

UriTemplate は、WebGetAttribute のインスタンス プロパティとして指定され、サービス コントラクトに適用されます。中かっこで囲まれたトークンはメソッド パラメータの名前に割り当てられることに注意してください。このサンプルではパラメータは 1 つのみですが、複数のパラメータを追加し、任意の順序で並べて、クエリ文字列パラメータとして追加することもできます。ワイルドカードも使用できます。

実行時に、UriTemplate プロパティの値が UriTemplate コンストラクタに渡されます。Client ランタイムおよび Dispatcher は、この方法で UriTemplate プロパティを使用します。Dispatcher は、このプロパティを使用して処理に対する受信メッセージを照合し、Client は、このプロパティを使用してプロキシのメソッドが適切な URI に確実に送信されるようにします。これは、サービスに "URI を使用する" ための使いやすく、単純で効果的な方法です。

WCF コントラクトの HTTP 動詞
その他のリソース

  • Windows Communication Foundation デベロッパー センター
  • .NET Framework デベロッパー センター
  • Visual Studio 2008 のダウンロード
  • PictureServices サンプル アプリケーション

.NET Framework 3.5 の WCF を使用した HTTP プログラミングを利用すれば、サービス コントラクトの操作を HTTP 動詞に簡単にマップすることができます。名前からわかるように、WebGetAttribute を操作に適用すると、その操作を HTTP GET で使用できるようになります。前述のコード例に示されているように、WebGetAttribute は UriTemplate という名前のインスタンス プロパティを定義します。

WebGetAttribute 型には、他にもインスタンス プロパティがあります。特によく知られているのは RequestFormat および ResponseFormat です。その名が示すとおり、これらのプロパティの値は処理のメッセージ形式を指定します。RequestFormat プロパティおよび ResponseFormat プロパティは、WebMessageFormat 型です。WebMessageFormat 型は 2 つの値 (Xml と Json) を持つ列挙型です。WCF のインフラストラクチャは、これらのプロパティの値を使用してチャネル スタックに適切なメッセージ エンコーダを設定します。WebMessageFormat.Xml のプロパティ値は、XML エンコーダを使用するチャネル スタックになります。WebMessageFormat.Json に値を設定すると、結果は .NET Framework 3.5 に含まれる JSON エンコーダを使用するチャネル スタックになります。HTTP メッセージ交換の両側にプロパティが存在するため、アプリケーションは XML メッセージを受信して JSON メッセージを返したり、JSON メッセージを受信して XML メッセージを返すことができます。

HTTP 動詞に関連するその他の属性に WebInvokeAttribute があります。この属性を操作に適用すると、その操作を HTTP GET 以外の HTTP 動詞で使用できるようになります。HTTP GET とその他のすべての HTTP 動詞のオブジェクト モデルを分離すると、HTTP GET の使用頻度と他の HTTP 動詞の使用頻度が反映されます。WebInvokeAttribute のオブジェクト モデルは WebGetAttribute と同様ですが、Method インスタンス プロパティが含まれる点が異なります。このプロパティの値は、特定の HTTP 動詞を操作に関連付けます。Method プロパティは String 型であるため、値を任意の標準 HTTP 動詞または非標準の HTTP 動詞に設定できます。図 3 は、WebInvokeAttribute をサービス コントラクトで使用する方法を示しています。

Figure 3 WebInvokeAttribute を使用する (Close Figure 3)
[ServiceContract]
public interface IPictureService
{
  [OperationContract]
  [WebGet(UriTemplate = "picture/{pictureId}")]
  Stream GetPicture(String pictureId);

  [OperationContract]
  [WebGet(UriTemplate = "picture/t/{pictureId}")]
  Stream GetPictureThumbnail(String pictureId);

  [OperationContract]
  [WebInvoke(UriTemplate="update", Method="POST")]
  void UpdatePictureInfo(PictureInfo info);
}

UpdatePictureInfo メソッドには PictureInfo 型のパラメータがあります。実行時には、PictureInfo オブジェクトのシリアル化されたバージョンが HTTP POST メッセージのペイロードになります。この方法でデータを渡すと、アプリケーションは、他の方法では URI に指定できない複雑なデータ型を出力できます。

.NET Framework 3.5 の WCF では、HTTP ヘッダーとの通信も簡単になります。要求および応答の HTTP ヘッダーは、System.ServiceModel.Web.WebOperationContext 型で公開されます。WebOperationContext 型は、.NET Framework 3.0 の WCF で導入された System.ServiceModel.OperationContext 型を拡張した型であり、使用パターンはほとんど変わりません。どちらの型もサービス オブジェクトの実装内で使用するためのものです。WebOperationContext 型は、サービス オブジェクトへのアクセスに使用する HTTP ヘッダーの値の読み取りや設定および URI 情報の取得を簡素化するメンバを公開します。HTTP ヘッダーはコレクションとして保存され、特に一般的なものは個別のプロパティとして公開されます。次の例は、WebOperationContext 型を使用して HTTP 応答の Content-Type ヘッダーを設定することにより、Web のデータ モデルに従う方法を示します。

public Stream GetPicture(string pictureId)
{
  // retrieve the Stream (omitted)
  Stream stream;

  // set the Content-Type to image/jpeg
  WebOperationContext.Current.OutgoingResponse.ContentType =
    "image/jpeg";

  return stream;
}

バインドとホスティング

ここまでは、URI セグメントをアプリケーション機能にマップする方法、処理を HTTP 動詞にマップする方法、および HTTP ヘッダーを操作する方法を説明してきました。今度は、これらの機能を 1 つの機能サービスに結合する方法を説明します。それには、WCF で新しく登場したデータ型をもう少し見ていく必要があります。

.NET Framework 3.0 の WCF には、バインド、トランスポートの抽象化、およびエンドポイントのプロトコルの概念が導入されていました。.NET Framework 3.0 は、多様なトランスポートおよびプロトコルのオプションをサポートする複数のバインドを備えています。

.NET Framework 3.5 の WCF には、System.ServiceModel.WebHttpBinding という名前の新しいバインドがあります。このバインドは Web の原則を抽象化し、.NET Framework 3.0 のバインド型と同じ使用方法モデルに従っているため、他のバインドと同様の方法でエンドポイントに追加することができます。

.NET Framework 3.0 の WCF では、エンドポイント動作の概念も導入されました。動作は、メッセージング インフラストラクチャの実行パスを拡張する手段です。.NET Framework には複数の動作が用意されていますが、ユーザー固有の動作も簡単に作成できます。

.NET Framework 3.5 の WCF には、System.ServiceModel.Description.WebHttpBehavior という名前の新しいエンドポイント動作があります。この動作は複数のタスクを実行します。受信側アプリケーションでは、フィルタ インフラストラクチャを設定して、受信されたメッセージがサービス オブジェクトの適切なメソッドに確実にディスパッチされるようにするなどの処理を行います。.NET Framework 3.0 の WCF は、SOAP Action とターゲット アドレスの組み合わせをディスパッチ キーとして使用します。WebHttpBehavior で設定されたフィルタ メカニズムは、既存のフィルタ メカニズムを拡張し、UriTemplate の照合と HTTP 動詞を使用して判断します。

図 4 は、WebHttpBinding および WebHttpBehavior を使用して、Web の原則を採り入れた受信側アプリケーションを構築する方法を示しています。この方法の利点は、これらのエンドポイントのいずれかを既存の ServiceHost に追加することにより、1 つの ServiceHost で SOAP/WS-* エンドポイントと REST エンドポイントの両方を使用することができるということです。

Figure 4 WebHttpBinding および WebHttpBehavior を使用する (Close Figure 4)
ServiceHost host = new ServiceHost(typeof(IPictureContract),
    new Uri("http://localhost:5000"));
// instantiate a WebHttpBinding
WebHttpBinding binding = new WebHttpBinding();

// add an endpoint, using the WebHttpBinding
ServiceEndpoint ep = host.AddServiceEndpoint(
    typeof(IPictureContract),  binding, String.Empty);

// add the WebHttpBehavior to the endpoint
ep.Behaviors.Add(new WebHttpBehavior());

// open the ServiceHost to start listening
host.Open();

送信側アプリケーションも同じ使用方法モデルに従いますが、WebHttpBehavior を ChannelFactory<T> オブジェクトのエンドポイント動作のコレクションに追加する必要があります。一般的な WCF の慣行により、これらのオプションは構成ファイルでも指定可能です。

.NET Framework 3.5 の WCF では、この使い勝手のよいモデルをさらに改良しており、場合によっては WebHttpBinding または WebHttpBehavior を追加する必要もありません。そのために、新しい API には System.ServiceModel.Web.WebServiceHost および System.ServiceModel.Web.WebServiceHostFactory という 2 つの型が追加されています。WebServiceHost 型は、WebHttpBehavior をすべてのエンドポイントに自動的に追加し、実行時に追加検証を行って、すべてのエンドポイントに動作との互換性を確保します。これにより、ユーザーが WebHttpBehavior をエンドポイントに追加する必要がなくなります。

WebServiceHostFactory 型は、IIS でホストするシナリオで使用するために用意された型であり、この型を使用すれば、命令型または構成ファイルのホスティング情報が不要になります。.NET Framework 3.0 の WCF では、IIS でホストする WCF サービスのアクティベーション ターゲットとして .svc ファイルを導入しました。これらのファイルには、ASP.NET のページ ディレクティブと同様のディレクティブがあります。WebServiceHostFactory を .svc ファイル ディレクティブの Factory プロパティに追加すると、ServiceHost が作成され、WebHttpBinding との適切なコントラクトを使用するエンドポイントが追加され、そのエンドポイントに WebHttpBehavior が追加されて、ServiceHost が開きます。

<%@ ServiceHost
  Language="C#"
  Service="PictureService"
  Factory="System.ServiceModel.Web.WebServiceHostFactory" %>

サービスの web.config ファイルには WCF 関連のエントリは含まれません。

新しいシンジケーション API

RSS フィードを思い浮かべてください。私と同じタイプの読者であれば、すぐにニュース フィードまたはブログを思い付くことでしょう。シンジケーション (RSS や ATOM はシンジケーション形式です) を利用すれば、表現力がさらに高まります。理論上は、シンジケートされたコンテンツは一連のデータを表す単純な方法です。データの内容は、シドニーの半径 50 マイル圏内に生息するウォンバットの数、金額が 100 ドルを超える最近 10 件の注文、先月 Contoso 社が製造したブーメランの数など、何でもかまいません。

.NET Framework 3.5 には、シンジケートされたコンテンツの作成および使用をサポートする豊富な機能があります。.NET Framework 3.5 は、RSS 2.0 形式および ATOM 1.0 形式の作成および使用、シンジケーションの拡張機能を追加する各種手段、およびその他の形式を実装する機能をサポートします。このプログラミング モデルでは、開発者がシンジケーション形式の詳細を知っている必要がないため、簡単に使用できます。

バージョン 3.5 の時点では、シンジケートされたコンテンツを .NET Framework で作成または使用するための標準的な方法はありません。新しいシンジケーション機能は WCF チームによって作成されましたが、WCF には依存していません。シンジケーションに関連するすべての型は System.ServiceModel.Web.dll アセンブリに存在しますが、System.Syndication 名前空間の一部です。.NET Framework 3.5 のシンジケーション機能は、AppDomain (ASP.NET、WPF アプリケーション、NT サービスなど) をホストする任意のプロセスから使用できます。これは、この機能があれば、シンジケートされたコンテンツを HTTP に限らず、任意のトランスポートで提供または使用できるということを意味します。ただし、WCF で HTTP プログラミング モデルと結合する場合、シンジケーションを既存の SOAP/WS-* サービスに追加することもできます。

新しいシンジケーション API には、個々のシンジケーション フィードおよびフィード内の項目を抽象化する型と、フィードを特定の形式に変換できる型があります。System.Syndication.SyndicationFeed 型は、形式に依存しない方法でシンジケーション フィードを表現します。SyndicationFeed には SyndicationItem オブジェクトのリストが格納されています。SyndicationItem オブジェクトはフィード内の 1 項目を表すものであるため、SyndicationItem オブジェクトが存在しない SyndicationFeed は、豆が入っていないえんどう豆のさやのようなものです。

SyndicationFeed に一連の SyndicationItem オブジェクトが設定されていれば、そのフィードを SyndicationFeedFormatter<T> で特定の形式に変換できます。SyndicationFeedFormatter<T> には 2 つの派生型 (Rss20FeedFormatter および Atom10FeedFormatter) があります。名前が示すとおり、この 2 つの型は SyndicationFeed のインスタンスをそれぞれ RSS 2.0 形式および ATOM 1.0 形式に変換します。

SyndicationFeed を作成する

SyndicationFeed オブジェクトの作成方法は 2 とおりあります。1 つは、インスタンスを 1 つ作成してメンバを手動で設定する方法で、もう 1 つは、SyndicationFeed 全体を既存のフィードから設定する方法です。どちらの方法も簡単で、開発者が個々の通信形式の特性を意識する必要はありません。次のコードは、SyndicationFeed オブジェクトを手動で構築する方法を示しています。

SyndicationFeed feed = new SyndicationFeed();
feed.Title.Text = "The Cybertopian Chronicle";

Title は、設定可能な多数のプロパティのうちの 1 つにすぎません。その他のプロパティについては、マニュアルを参照してください。

既存のフィードを読み取り、そのフィードの内容に基づいて何らかの処理を行うのは一般的なことです。新しいシンジケーション API では、SyndicationFeed のインスタンスを作成して、その状態を既存のフィードから自動的に設定することができます。起動する必要があるのは、既存フィードの URI またはフィードの読み取りに使用する XmlReader の URI のみです。次のコードは、Web 上の既存フィードに接続し、そのフィードから情報を抽出する方法を示しています。

Uri feedUri = new Uri("http://blogs.msdn.com/justinjsmith/atom.xml");
SyndicationFeed feed = SyndicationFeed.Load(feedUri);
Console.WriteLine(feed.Title.Text);

// outputs "The Cybertopian Chronicle"

SyndicationItem 型には 35 以上のメンバが存在します。これらのメンバの多くは、項目 ID、最新更新時刻、発行日付、タイトル、実際のコンテンツなどのフィールドの設定または取得に関連するプロパティです。SyndicationItem に保存されたコンテンツの拡張を簡単にするメンバも多数あります。RSS および ATOM (Microsoft Simple List Extensions、Yahoo Media RSS、GeoRSS など) に対する各種の拡張も用意されており、SyndicationFeed と SyndicationItem を拡張して、既存の RSS または ATOM の拡張を追加することもできます。

フィードには大量の項目を保持することができます。サイズの大きいフィードの場合、一度にすべての項目を読み込むことはできません。SyndicationFeed はこのような場合に対処するため、項目の一連の SyndicationItem オブジェクトを IEnumerable<SyndicationItem> として公開します。この実装では、.NET Framework 2.0 の反復処理機能を利用するため、多数の SyndicationItem オブジェクトを簡単に処理できます。LINQ を使用して一連の SyndicationItem オブジェクトを反復処理することもできます。これにより、フィードから情報を抽出するために必要なコード量が大幅に削減されます。

シンジケーション API は、SyndicationFeed を RSS 2.0 形式および ATOM 1.0 形式に変換する複数の型を定義します。SyndicationFeed を 1 つビルドし、SyndicationItem オブジェクトを設定して、そのフィードを RSS と ATOM の両方で公開することができます。図 5 は、ATOM 1.0 フィードを取得し、RSS 2.0 に変換して、新しい RSS 表示形式でコンソールに出力する方法を示しています。

Figure 5 フィードを変換する (Close Figure 5)
// read an ATOM feed
Uri feedUri = new Uri("http://blogs.msdn.com/justinjsmith/atom.xml");
SyndicationFeed feed = SyndicationFeed.Load(feedUri);

// transform it to RSS
Rss20FeedFormatter formatter = new Rss20FeedFormatter(feed);
XmlWriter writer = XmlWriter.Create(Console.Out, null);

// write it to the Console
formatter.WriteTo(writer);
writer.Flush();

シンジケーション API を WCF の HTTP プログラミング モデルに組み込むと、カスタマイズされた URI からフィードを公開し、その URI の構成に基づいて RSS または ATOM を返すことができます。図 6 は、受信した HTTP GET からのクエリ文字列パラメータを使用し、RSS または ATOM フィードを返すサービス コントラクトの処理を定義する方法を示しています。操作コントラクトでの SyndicationFeedFormatter<SyndicationFeed> の使い方に注意してください。Rss20FeedFormatter および Atom10FeedFormatter は、SyndicationFeedFormatter<TSyndicationFeed> から派生しています。

Figure 6 カスタマイズされた URI からフィードを公開する (Close Figure 6)
[ServiceKnownType(typeof(Atom10FeedFormatter))]
[ServiceKnownType(typeof(Rss20FeedFormatter))]
[ServiceContract]
interface IPictureService {
  [OperationContract]
  [WebGet(UriTemplate="Pictures?format={format}")]
  SyndicationFeedFormatter<SyndicationFeed> Feed(String format);
}

class PictureService : IPictureService {

  public SyndicationFeedFormatter<SyndicationFeed> Feed(String format){
    // create the syndication feed
    SyndicationFeed feed = new SyndicationFeed();

    // add the items to the feed (omitted)

    // check the argument & return the right format
    if(format.ToLower() == "rss"){
      return new Rss20FeedFormatter(feed);
    }
    return new Atom10FeedFormatter(feed);

  }

}

まとめ

この記事では、HTTP トランスポートの基本、HTTP トランスポートを Web の原則に関連付ける方法、WCF を使用してこれらの原則をアプリケーションに採り入れる方法、新しいシンジケーション API を使用して RSS フィードおよび ATOM フィードを提供および使用する方法を検討してきました。これらの新しい機能をさらに具体的に確認するには、www.cloudsamples.net/pictureservices に掲載されている PictureServices サンプル アプリケーションを実行して、各機能を統合した実行結果をご覧ください。サンプルのオンラインでの実行、ソース コードの参照、サンプル全体のダウンロードを行うことができます。図 7 は、PictureServices の実行結果を示しています。

図 7 PictureServices のフィード
図 7 PictureServices のフィード(画像を縮小するには、ここをクリックします)
図 7 PictureServices のフィード
図 7 PictureServices のフィード (画像を拡大するには、ここをクリックします)

簡単に言うと、PictureServices では、ローカル コンピュータまたは Flickr フィードから画像を取得できます。このアプリケーションは単純なプロバイダ モデルを使用し、現在 3 つのプロバイダ (Windows デスクトップ サーチ、ユーザーのファイル システムのフォルダ、Flickr) を保有しています。PictureServices はプロバイダからファイルを取得すると、画像をシンジケートし、ローカルに提供します。ブラウザでフィードをクリックし、個々の画像を取得することができます。このサンプルには他にも機能があるので、いろいろ試してみてください。この記事で説明したテクノロジの詳細については、この記事の「その他のリソース」のリンクを参照してください。

Justin Smith は、Web サービスを得意分野とする Microsoft のテクニカル エバンジェリストです。専門は WCF および BizTalk サービスです。Justin は、『Inside Microsoft Windows Communication Foundation』(Microsoft Press、2007年) の執筆者でもあり、ソフトウェア開発カンファレンスで頻繁にプレゼンテーションを行っています。blogs.msdn.com/justinjsmith で彼のブログをご覧いただけます。


SQL Server: 隠れたデータを明らかにしてアプリケーションのパフォーマンスを最適化する

SQL Server

隠れたデータを明らかにしてアプリケーションのパフォーマンスを最適化する

Ian Stirk
コードのダウンロード : : DMVsinSQLServer2008_01.exe (155KB)
この記事の内容 : :
  • 動的管理ビューと動的管理関数
  • サーバーが待機する原因
  • よくあるインデックスの問題
  • 論理 I/O に関連する問題
この記事は次のテクノロジを使用しています:
SQL Server
コンテンツ
サーバーが待機する原因
読み取りと書き込み
データベース別の不足インデックス
コストのかかる不足インデックス
未使用のインデックス
コストのかかる使用中のインデックス
頻繁に使用されるインデックス
論理的に断片化したインデックス
I/O に関してコストのかかるクエリ
CPU に関してコストのかかるクエリ
コストのかかる CLR クエリ
最も頻繁に実行されるクエリ
ブロックの影響を受けるクエリ
再利用が最も少ないプラン
その他の作業

アプリケーションで発生するパフォーマンスの問題の多くは、データベース クエリがうまく実行されていないためです。ただし、データベースのパフォーマンスを改善する方法はいくつもあります。SQL ServerTM 2005 が収集する多くの情報を使用して、このようなパフォーマンスの問題の原因を明らかにできます。

SQL Server 2005 は、実行中のクエリに関するデータを収集します。メモリに保持され、サーバーが再起動した後で蓄積が開始されるこのデータを使用すると、テーブルのインデックス、クエリのパフォーマンス、サーバー I/O など、さまざまな問題とメトリックスを明らかにすることができます。このデータは、SQL Server 動的管理ビュー (DMV) および関連する動的管理関数 (DMF) を使用して照会できます。これらはシステム ベースのビューと関数であり、問題の診断とデータベース パフォーマンスのチューニングに使用できるサーバーの状態情報を提供します。

この記事では、SQL Server 2005 によって既に収集されている情報を使用してパフォーマンスを改善できる部分に焦点を当てます。この方法は、通常は基になるシステム データをクエリして既存のデータを収集および調査するので、面倒なことはほとんどありません。

この情報を取得する方法を示し、基になる DMV について説明し、データを解釈するときの注意点を明らかにし、パフォーマンスの改善を実現できる可能性のある他の部分を指摘します。これらを行うために、SQL Server 2005 によって収集されるデータのさまざまな面を詳しく示す一連の SQL スクリプトを提示して説明します。すべてのコメントを含む完全なスクリプトは、MSDN® Magazine の Web サイトからダウンロードできます。

説明する手順の一部はサーバー全体に対するもので、特定のサーバーでホストされるすべてのデータベースを含みます。必要があれば、クエリに名前を追加するなどして適切なフィルタを追加することで、特定のデータベースを対象にできます。

逆に、sys.indexes DMV と結び付いているクエリもあります。この DVM は、現在のデータベースに対する結果のみを報告するデータベース固有のビューです。このようなクエリの場合は、システム ストアド プロシージャの sp_MSForEachDB を使用してサーバー上の全データベースを反復処理するようにクエリを変更し、サーバー全体の結果を表示するようにしてあります。

特定のパフォーマンス メトリックスに最も関係の深いレコードを対象にするため、SQL TOP 関数を使用して返されるレコードの数を制限します。

サーバーが待機する原因

通常、ユーザーがパフォーマンスの低下を感じるのは、待機時間が続けて発生する場合です。SQL クエリが実行できる状態にもかかわらず他のリソースを待機しているときは常に、待機の原因に関する詳細を記録します。この詳細情報には、sys.dm_os_wait_stats DMV を使用してアクセスできます。図 1 に示すような SQL スクリプトを使用すると、蓄積されたすべての待機に関する原因を調査できます。

Figure 1 待ち時間の原因となる SQL クエリ レコード (Close Figure 1)
SELECT TOP 10
 [Wait type] = wait_type,
 [Wait time (s)] = wait_time_ms / 1000,
 [% waiting] = CONVERT(DECIMAL(12,2), wait_time_ms * 100.0
               / SUM(wait_time_ms) OVER())
FROM sys.dm_os_wait_stats
WHERE wait_type NOT LIKE '%SLEEP%'
ORDER BY wait_time_ms DESC;
図 1 待ち時間の原因となる SQL クエリ レコード
図 1 待ち時間の原因となる SQL クエリ レコード

このスクリプトを実行した結果では、待機の種類が、待機に費やされた合計時間の順に一覧表示されます。結果の例を見ると、待機の原因として I/O が比較的高いランクになっていることがわかります。通常、データは最初に読み込まれた後はメモリ内にあるので、私が注目するのは物理 I/O ではなく論理 I/O (メモリ内のデータの読み書き) だけであることに注意してください。

読み取りと書き込み

高い I/O 使用率は、データ アクセス メカニズムの問題を示している場合があります。SQL Server 2005 は、各クエリが目的を達成するために使用する読み取りと書き込みの総数を追跡します。これらの値を合計して、読み取りと書き込みを全体として最も多く実行しているデータベースを判別できます。

sys.dm_exec_query_stats DMV は、キャッシュされているクエリ プランに関する集計されたパフォーマンス統計を保持しています。これには、論理的な読み取りと書き込みの回数や、クエリが実行された回数などについての情報が含まれます。この DMV を sys.dm_exec_sql_text DMF と結合すると、データベースごとの読み取りと書き込みの回数を合計できます。SQL Server 2005 の新しい CROSS APPLY 演算子を使用してこの結合を処理していることに注意してください。図 2 は、読み取りと書き込みを最も多く使用しているデータベースを識別するためのスクリプトです。

Figure 2 読み取りと書き込みを最も多く使用するものを特定する (Close Figure 2)
SELECT TOP 10
        [Total Reads] = SUM(total_logical_reads)
        ,[Execution count] = SUM(qs.execution_count)
        ,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
GROUP BY DB_NAME(qt.dbid)
ORDER BY [Total Reads] DESC;

SELECT TOP 10
        [Total Writes] = SUM(total_logical_writes)
        ,[Execution count] = SUM(qs.execution_count)
        ,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
GROUP BY DB_NAME(qt.dbid)
ORDER BY [Total Writes] DESC;
図 2 読み取りと書き込みを最も多く使用するものを特定する
図 2 読み取りと書き込みを最も多く使用するものを特定する

結果では、最も多くの論理ページを読み書きしているデータベースがわかります。上部のデータは [Total Reads] (読み取り総数) 順に、下部のデータは [Total Writes] (書き込み総数) 順に並んでいます。

2 つのインスタンスの DatabaseName が NULL に設定されていることがわかります。この設定は、アドホックおよび準備済みの SQL ステートメントを示しています。この詳細情報は、ネイティブ SQL がどの程度使用されているのかを特定するのに役立ちます。ネイティブ SQL は、それ自体が多くの異なる問題の原因になる可能性があります (たとえば、クエリ プランが再利用されていないこと、コードが再利用されていないこと、およびセキュリティの部分に問題がある可能性があることを示します)。

tempdb の値が大きい場合は、一時テーブルの使いすぎ、多すぎる再コンパイル、または効率の悪いデバイスを示していることがあります。結果を見ると、データベースが主としてトランザクション (更新が多い) ではなくレポート (データの選択が多い) に使用されていることがわかります。各データベースの種類 (レポートまたはトランザクション) により、インデックス作成の必要性が異なります。このことについては、後でさらに詳しく検討します。

データベース別の不足インデックス

SQL Server がクエリを処理するとき、オプティマイザはクエリを満たすために使用するインデックスのレコードを保持します。インデックスが見つからない場合、SQL Server は不足インデックスのレコードを作成します。この情報は、sys.dm_db_missing_index_details DMV を使用して表示できます。

図 3 に示すスクリプトを使用すると、特定のサーバー上のどのデータベースでインデックスが不足しているかを示すことができます。インデックスはクエリ データを検索するための最善のパスを提供することが多いので、不足しているインデックスを発見することは重要です。この情報を利用して、I/O を減らし、全体的なパフォーマンスを向上させることができます。ここで使用するスクリプトは、sys.dm_db_missing_index_details を調べ、不足しているインデックスの数をデータベースごとに合計し、さらに調査の必要なデータベースを判別しやすくします。

Figure 3 不足しているデータベースを特定する (Close Figure 3)
SELECT
    DatabaseName = DB_NAME(database_id)
    ,[Number Indexes Missing] = count(*)
FROM sys.dm_db_missing_index_details
GROUP BY DB_NAME(database_id)
ORDER BY 2 DESC;
図 3 不足しているデータベースを特定する
図 3 不足しているデータベースを特定する

多くの場合、データベースはトランザクション システムとレポートベース システムに分けられます。指摘された不足インデックスをレポート データベースに適用するのは、比較的簡単な操作です。それに対し、トランザクション データベースの場合は、一般に、追加するインデックスが基になるテーブル データに与える影響をさらに詳しく調査する必要があります。

コストのかかる不足インデックス

クエリのパフォーマンスに対する影響のレベルは、インデックスごとに異なります。サーバー上の全データベースで最もコストのかかる不足インデックスを詳しく調べることで、パフォーマンスが最も大きく向上すると考えられる不足インデックスを見つけることができます。

sys.dm_db_missing_index_group_stats DMV は、SQL が特定の不足インデックスの使用を試みた回数を示します。sys.dm_db_missing_index_details DMV は、クエリで必要な列など、不足しているインデックスの構造を詳しく示します。これらの 2 つの DMV は、sys.dm_db_missing_index_groups DMV によってリンクされます。不足インデックスのコスト ([Total Cost] (総コスト) 列) は、ユーザー コストの合計の平均と、ユーザーに対する影響の平均と、ユーザーによるシークとスキャンの合計の 3 つの値を掛けた積です。

図 4 のスクリプトを使用すると、最もコストのかかる不足インデックスを識別できます。[Total Cost] (総コスト) の順番に並べられたクエリの結果では、最も重要な不足インデックスのコストと共に、データベース/スキーマ/テーブルの詳細および不足インデックスで必要な列が示されます。具体的には、このスクリプトは SQL の等値および不等値ステートメントで使用されている列を識別します。さらに、不足インデックスに含まれる列として使用する必要のある他の列を報告します。含まれる列を使用すると、基になるページからデータを取得することなく、さらに対象範囲の広いクエリを満たすことができ、結果として I/O 操作の数を減らし、パフォーマンスを改善できます。

Figure 4 不足インデックスのコスト (Close Figure 4)
SELECT  TOP 10
        [Total Cost]  = ROUND(avg_total_user_cost * avg_user_impact * (user_seeks + user_scans),0)
        , avg_user_impact
        , TableName = statement
        , [EqualityUsage] = equality_columns
        , [InequalityUsage] = inequality_columns
        , [Include Cloumns] = included_columns
FROM        sys.dm_db_missing_index_groups g
INNER JOIN    sys.dm_db_missing_index_group_stats s
       ON s.group_handle = g.index_group_handle
INNER JOIN    sys.dm_db_missing_index_details d
       ON d.index_handle = g.index_handle
ORDER BY [Total Cost] DESC;
図 4 不足インデックスのコスト
図 4 不足インデックスのコスト(画像を縮小するには、ここをクリックします)
図 4 不足インデックスのコスト
図 4 不足インデックスのコスト (画像を拡大するには、ここをクリックします)

結果では必要なインデックスでの列の作成順序は指定されないことに注意してください。これを決定するには、SQL コード ベースの全体を調査する必要があります。一般的なルールとして、選択される回数が最も多い列をインデックスの最初に置く必要があります。

また、不足インデックスのコストの計算では、ユーザー列 (user_seeks や user_scans など) のみが考慮されることも指摘しておきます。これは、システム列は統計、データベース整合性チェック (DBCC)、およびデータ定義言語 (DDL) コマンドを使用して表されることが多く、(データベース管理機能とは異なり) ビジネス機能の実現にはそれほど重要ではないためです。

非常に重要なことですが、基になるテーブルでデータの変更が発生すると、インデックスの追加によりコストが発生する可能性があることに特に注意する必要があります。このため、基になる SQL コード ベースの追加調査を実施する必要があります。

列として含めるように推奨される数が非常に多い場合は、汎用的な "SELECT *" ステートメントが使用されていることを示している可能性があるので、基の SQL を調査する必要があります。そうであった場合は、選択クエリの修正が必要になります。

未使用のインデックス

未使用のインデックスは、パフォーマンスに悪い影響を与える可能性があります。これは、基になるテーブル データを変更すると、インデックスの更新も必要になる場合があるためです。当然ですが、更新が必要になると余分な時間がかかり、ブロックの可能性が高くなることさえあります。

クエリを満たすためにインデックスが使用されていて、基になるテーブル データに対する更新の結果としてインデックスが更新されると、SQL Server は対応するインデックス使用の詳細を更新します。これらの使用の詳細を確認することで、未使用のインデックスを識別できます。

sys.dm_db_index_usage_stats DMV は、インデックスが使用される頻度と範囲を示します。この DMV は、インデックスの作成で使用される情報を含む sys.indexes DMV と結合されます。さまざまなユーザー列が未使用のインデックスを示す値 0 になっているかどうかを調査できます。前と同じ理由で、システム列の影響はやはり無視されます。図 5 のスクリプトを使用すると、最もコストのかかる未使用インデックスを識別できます。

Figure 5 最もコストのかかる未使用インデックスを特定する (Close Figure 5)
-- Create required table structure only.
-- Note: this SQL must be the same as in the Database loop given in the following step.
SELECT TOP 1
        DatabaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
        ,user_updates
        ,system_updates
        -- Useful fields below:
        --, *
INTO #TempUnusedIndexes
FROM   sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON  s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE  s.database_id = DB_ID()
    AND OBJECTPROPERTY(s.[object_id], 'IsMsShipped') = 0
    AND    user_seeks = 0
    AND user_scans = 0
    AND user_lookups = 0
    AND s.[object_id] = -999  -- Dummy value to get table structure.
;

-- Loop around all the databases on the server.
EXEC sp_MSForEachDB    'USE [?];
-- Table already exists.
INSERT INTO #TempUnusedIndexes
SELECT TOP 10
        DatabaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
        ,user_updates
        ,system_updates
FROM   sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON  s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE  s.database_id = DB_ID()
    AND OBJECTPROPERTY(s.[object_id], ''IsMsShipped'') = 0
    AND    user_seeks = 0
    AND user_scans = 0
    AND user_lookups = 0
    AND i.name IS NOT NULL    -- Ignore HEAP indexes.
ORDER BY user_updates DESC
;
'

-- Select records.
SELECT TOP 10 * FROM #TempUnusedIndexes ORDER BY [user_updates] DESC
-- Tidy up.
DROP TABLE #TempUnusedIndexes
図 5 最もコストのかかる未使用インデックスを特定する
図 5 最もコストのかかる未使用インデックスを特定する(画像を縮小するには、ここをクリックします)
図 5 最もコストのかかる未使用インデックスを特定する
図 5 最もコストのかかる未使用インデックスを特定する (画像を拡大するには、ここをクリックします)

このクエリの結果は、データの検索には使用されていなくても、基になるテーブルでの変更の結果として更新されたインデックスを示します。このような更新は、user_updates 列と system_updates 列で示されます。結果は、インデックスに適用されたユーザー更新の数の順に並んでいます。

インデックスが使用されていないことを確認するには、十分な情報を集める必要があります。四半期や 1 年に 1 回だけ実行されるクエリにとって重要なインデックスを誤って削除しては困ります。また、インデックスの中には、重複レコードの挿入を制限したりデータを並べ替えたりするために使用されるものもあります。未使用のインデックスを削除する前に、これらの要素も検討する必要があります。

クエリの基本形式は、現在のデータベースに対してのみ適用されます。これは、クエリが sys.indexes DMV と結合されていて、この DMV が現在のデータベースのみと関係しているためです。システム ストアド プロシージャ sp_MSForEachDB を使用することで、サーバー上の全データベースの結果を抽出できます。この処理に使用するパターンについては、「全データベースをループ処理する」のリンクを参照してください。このパターンは、サーバー上の全データベースを反復処理する必要のある、スクリプトの他のセクションでも使用します。また、ヒープ型のインデックスは除外してあります。これは、正式なインデックスを持たないテーブルのネイティブ構造を表しているためです。

コストのかかる使用中のインデックス

使用されているインデックスの中で、基になるテーブルの変更に関して最もコストのかかるインデックスを探すことも役立つ場合があります。このコストはパフォーマンスに悪影響を与えますが、インデックス自体はデータの検索にとって重要である場合があります。

sys.dm_db_index_usage_stats DMV を使用すると、インデックスが使用される頻度と範囲がわかります。この DMV を、インデックスの作成で使用される詳細情報を含む sys.indexes DMV と結合します。user_updates 列と system_updates 列を調べると、メンテナンスが最も多いインデックスがわかります。図 6 は、最もコストのかかるインデックスの識別に使用するスクリプトとその結果です。

Figure 6 最もコストのかかるインデックスを特定する (Close Figure 6)
-- Create required table structure only.
-- Note: this SQL must be the same as in the Database loop given in the following step.
SELECT TOP 1
        [Maintenance cost]  = (user_updates + system_updates)
        ,[Retrieval usage] = (user_seeks + user_scans + user_lookups)
        ,DatabaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
INTO #TempMaintenanceCost
FROM   sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON  s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
    AND OBJECTPROPERTY(s.[object_id], 'IsMsShipped') = 0
    AND (user_updates + system_updates) > 0 -- Only report on active rows.
    AND s.[object_id] = -999  -- Dummy value to get table structure.
;

-- Loop around all the databases on the server.
EXEC sp_MSForEachDB    'USE [?];
-- Table already exists.
INSERT INTO #TempMaintenanceCost
SELECT TOP 10
        [Maintenance cost]  = (user_updates + system_updates)
        ,[Retrieval usage] = (user_seeks + user_scans + user_lookups)
        ,DatabaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
FROM   sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON  s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
    AND i.name IS NOT NULL    -- Ignore HEAP indexes.
    AND OBJECTPROPERTY(s.[object_id], ''IsMsShipped'') = 0
    AND (user_updates + system_updates) > 0 -- Only report on active rows.
ORDER BY [Maintenance cost]  DESC
;
'

-- Select records.
SELECT TOP 10 * FROM #TempMaintenanceCost
ORDER BY [Maintenance cost]  DESC
-- Tidy up.
DROP TABLE #TempMaintenanceCost
図 6 最もコストのかかるインデックスを特定する
図 6 最もコストのかかるインデックスを特定する(画像を縮小するには、ここをクリックします)
図 6 最もコストのかかるインデックスを特定する
図 6 最もコストのかかるインデックスを特定する (画像を拡大するには、ここをクリックします)

この結果からは、メンテナンスが最も多いインデックスと、関係するデータベース/テーブルの詳細がわかります。[Maintenance cost] (メンテナンス コスト) 列は、user_updates 列と system_updates 列の合計です。インデックスの有用性 ([Retrieval usage] (検索使用率) 列で示されます) は、さまざまな user_* 列の合計として計算されます。インデックスを削除してかまわないかどうかを判断するときは、インデックスの有用性を考慮することが重要です。

データの一括変更が関係する場合は、これらの結果が、更新を適用する前に削除する必要のあるインデックスを識別するのに役立ちます。これらのインデックスは、すべての更新を行った後で再び適用できます。

頻繁に使用されるインデックス
全データベースをループ処理する

sys.indexes DMV はデータベース固有のビューです。したがって、sys.indexes に結合されるクエリは、現在のデータベースについての結果のみを報告します。ただし、システム ストアド プロシージャ sp_MSForEachDB を使用してサーバー上の全データベースを反復処理することで、サーバー全体の結果を表示できます。このような場合に私が使用するパターンを紹介します。

  1. コードの本文で必要な構造と似た構造を持つ一時テーブルを作成します。一時テーブルの構造を作成できるように、存在しないレコード (object_id が -999) を設定します。
  2. コードの本文が実行され、サーバー上の全データベースがループ処理されます。データベースごとに (TOP ステートメントを使用して) 検索されるレコードの数は、表示する必要のあるレコードの数と同じであることに注意してください。そうでないと、結果は、サーバー上の全データベースに対して TOP で検索したレコードの個数を正確に表さない可能性があります。
  3. レコードは一時テーブルから抽出され、関心のある列 (この場合は user_updates 列) の順に並べ替えられます。

DMV を使用して、最もよく使用されるインデックスを識別できます。これは、基になるデータへの最も一般的なパスです。このようなインデックスは、インデックス自体を改善または最適化できる場合、全体的なパフォーマンスを大幅に改善できる可能性があります。

sys.dm_db_index_usage_stats DMV には、シーク、スキャン、およびルックアップを通じてデータを検索するためにインデックスが使用される頻度に関する詳細情報が含まれます。この DMV は、インデックスの作成で使用される詳細情報を含む sys.indexes DMV と結合されます。[Usage] (使用率) 列は、すべての user_* フィールドの合計です。この計算は、図 7 のようなスクリプトを使用して行うことができます。このクエリの結果では、インデックスが使用された回数が、[Usage] (使用率) 列の順に示されます。

Figure 7 最も使用頻度の高いインデックスを特定する (Close Figure 7)
-- Create required table structure only.
-- Note: this SQL must be the same as in the Database loop given in the -- following step.
SELECT TOP 1
        [Usage] = (user_seeks + user_scans + user_lookups)
        ,DatabaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
INTO #TempUsage
FROM   sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE   s.database_id = DB_ID()
    AND OBJECTPROPERTY(s.[object_id], 'IsMsShipped') = 0
    AND (user_seeks + user_scans + user_lookups) > 0
-- Only report on active rows.
    AND s.[object_id] = -999  -- Dummy value to get table structure.
;

-- Loop around all the databases on the server.
EXEC sp_MSForEachDB    'USE [?];
-- Table already exists.
INSERT INTO #TempUsage
SELECT TOP 10
        [Usage] = (user_seeks + user_scans + user_lookups)
        ,DatabaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
FROM   sys.dm_db_index_usage_stats s
INNER JOIN sys.indexes i ON s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE   s.database_id = DB_ID()
    AND i.name IS NOT NULL    -- Ignore HEAP indexes.
    AND OBJECTPROPERTY(s.[object_id], ''IsMsShipped'') = 0
    AND (user_seeks + user_scans + user_lookups) > 0 -- Only report on active rows.
ORDER BY [Usage]  DESC
;
'

-- Select records.
SELECT TOP 10 * FROM #TempUsage ORDER BY [Usage] DESC
-- Tidy up.
DROP TABLE #TempUsage
図 7 最も使用頻度の高いインデックスを特定する
図 7 最も使用頻度の高いインデックスを特定する(画像を縮小するには、ここをクリックします)
図 7 最も使用頻度の高いインデックスを特定する
図 7 最も使用頻度の高いインデックスを特定する (画像を拡大するには、ここをクリックします)

最も使用頻度の高いインデックスは、基になるデータへの最も重要なアクセス ルートを表します。このようなインデックスを削除する必要がないのは明らかですが、インデックスを調べてそれらが最適な状態であることを確認する価値はあります。たとえば、インデックスの断片化が少ないこと (特に、順番に検索されるデータについて)、および基になる統計が最新の状態であることを確認する必要があります。また、テーブルの未使用のインデックスを削除する必要があります。

論理的に断片化したインデックス

論理的なインデックスの断片化は、そのインデックスのエントリで順番どおりになっていないものの割合を示します。これは、ページに空きがないタイプの断片化とは異なります。論理的な断片化は、インデックスを使用する任意の順序のスキャンに影響を与えます。可能な場合は、この断片化を除去する必要があります。断片化は、インデックスを再ビルドまたは再編成することで除去できます。

論理的な断片化が最も進んだインデックスは、次の DMV を使用して識別できます。sys.dm_db_index_physical_stats DMV を使用すると、インデックスのサイズと断片化に関する詳細情報を表示できます。この DMV は、インデックスの作成で使用される詳細情報を含む sys.indexes DMV と結合されます。

図 8 は、論理的な断片化が最も進んだインデックスの識別に使用するスクリプトです。結果は、断片化の割合の順に並べられ、全データベースで最も論理的な断片化が進んだインデックスと、それに関係するデータベース/テーブルを示します。このスクリプトは初めて実行するときには時間がかかる (数分) 場合があるので、スクリプトのダウンロードではコメント アウトしています。

Figure 8 論理的に断片化したインデックスを特定する (Close Figure 8)
-- Create required table structure only.
-- Note: this SQL must be the same as in the Database loop given in the -- following step.
SELECT TOP 1
        DatbaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
        ,[Fragmentation %] = ROUND(avg_fragmentation_in_percent,2)
INTO #TempFragmentation
FROM sys.dm_db_index_physical_stats(db_id(),null, null, null, null) s
INNER JOIN sys.indexes i ON s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE s.[object_id] = -999  -- Dummy value just to get table structure.
;

-- Loop around all the databases on the server.
EXEC sp_MSForEachDB    'USE [?];
-- Table already exists.
INSERT INTO #TempFragmentation
SELECT TOP 10
        DatbaseName = DB_NAME()
        ,TableName = OBJECT_NAME(s.[object_id])
        ,IndexName = i.name
        ,[Fragmentation %] = ROUND(avg_fragmentation_in_percent,2)
FROM sys.dm_db_index_physical_stats(db_id(),null, null, null, null) s
INNER JOIN sys.indexes i ON s.[object_id] = i.[object_id]
    AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
      AND i.name IS NOT NULL    -- Ignore HEAP indexes.
    AND OBJECTPROPERTY(s.[object_id], ''IsMsShipped'') = 0
ORDER BY [Fragmentation %] DESC
;
'

-- Select records.
SELECT TOP 10 * FROM #TempFragmentation ORDER BY [Fragmentation %] DESC
-- Tidy up.
DROP TABLE #TempFragmentation
図 8 論理的に断片化したインデックスを特定する
図 8 論理的に断片化したインデックスを特定する(画像を縮小するには、ここをクリックします)
図 8 論理的に断片化したインデックスを特定する
図 8 論理的に断片化したインデックスを特定する (画像を拡大するには、ここをクリックします)

I/O に関してコストのかかるクエリ

I/O は、クエリが行う読み取り/書き込みの回数の基準です。この値を使用すると、クエリの効率がわかります。多くの場合、使用する I/O の多いクエリはパフォーマンス改善のための対象として適しています。

sys.dm_exec_query_stats DMV は、物理的および論理的な読み取り/書き込みの詳細やクエリの実行回数など、キャッシュされているクエリ プランについての集計されたパフォーマンス統計を提供します。この DMV には、実際の SQL を、それを含む親 SQL から抽出するために使用されるオフセットが含まれます。この DMV は、I/O が関係する SQL バッチに関する情報を含む sys.dm_exec_sql_text DMF と結合されます。基になる個別の SQL クエリを取得するために、さまざまなオフセットがこのバッチに適用されます。このスクリプトを図 9 に示します。平均 I/O の順に並べ替えられた結果では、平均 I/O、合計 I/O、個別のクエリ、親クエリ (個別のクエリがバッチの一部である場合)、およびデータベース名が示されます。

Figure 9 I/O に関して最もコストのかかるクエリを特定する (Close Figure 9)
SELECT TOP 10
 [Average IO] = (total_logical_reads + total_logical_writes) / qs.execution_count
,[Total IO] = (total_logical_reads + total_logical_writes)
,[Execution count] = qs.execution_count
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
         (CASE WHEN qs.statement_end_offset = -1
            THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
          ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)
        ,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
ORDER BY [Average IO] DESC;
図 9 I/O に関して最もコストのかかるクエリを特定する
図 9 I/O に関して最もコストのかかるクエリを特定する(画像を縮小するには、ここをクリックします)
図 9 I/O に関して最もコストのかかるクエリを特定する
図 9 I/O に関して最もコストのかかるクエリを特定する (画像を拡大するには、ここをクリックします)

I/O はデータの量を反映するので、[Individual Query] (個別のクエリ) 列に表示されるクエリは、I/O を減らしてパフォーマンスを改善できる部分を決定する際の参考になります。クエリをデータベース チューニング アドバイザに渡して、クエリのパフォーマンスを改善するためにインデックス/統計を追加する必要があるかどうかを判断することもできます。統計には、基になるデータの分散と密度に関する詳細が含まれます。この情報は、クエリ オプティマイザが最適なクエリ アクセス プランを決定するときに使用されます。

また、これらのクエリのテーブルと不足インデックスに関する説明のセクションで示されるインデックスの間に関係があるかどうかを調べるのも役に立ちます (ただし、インデックスを追加すると基のテーブル データの更新にかかる時間が増えるので、更新量の多いテーブルにインデックスを作成する場合は、その影響を調査することが重要です)。

スクリプトは、読み取りのみまたは書き込みのみを報告するように変更できます。それぞれ、レポート データベースまたはトランザクション データベースを調べる場合に便利です。また、合計値または平均値について報告し、それに従って並べ替えることもできます。読み取りの値が大きい場合は、インデックスが不足しているか不完全であること、またはクエリやテーブルの設計が正しくないことを示している可能性があります。

sys.dm_exec_query_stats DMV を使用する結果を解釈するときは、若干の注意が必要です。たとえば、クエリ プランはいつでもプロシージャ キャッシュから削除できますが、この場合、すべてのクエリがキャッシュされているとは限りません。これは結果に影響を与えますが、それでも結果が最もコストのかかるクエリを示していることに変わりはありません。

CPU に関してコストのかかるクエリ

もう 1 つの有効な方法は、CPU 使用率に関して最もコストのかかるクエリを分析することです。この方法では、パフォーマンスの低いクエリがよくわかります。ここで使用する DMV は、I/O に関するクエリの調査で使用したものと同じです。図 10 のクエリを使用すると、CPU 使用率で測定して最もコストのかかるクエリを識別できます。

Figure 10 CPU に関して最もコストのかかるクエリを特定する (Close Figure 10)
SELECT TOP 10
 [Average CPU used] = total_worker_time / qs.execution_count
,[Total CPU used] = total_worker_time
,[Execution count] = qs.execution_count
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
         (CASE WHEN qs.statement_end_offset = -1
            THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
          ELSE qs.statement_end_offset END -
qs.statement_start_offset)/2)
,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
ORDER BY [Average CPU used] DESC;
図 10 待ち時間の原因となる SQL クエリ レコード
図 10 待ち時間の原因となる SQL クエリ レコード(画像を縮小するには、ここをクリックします)
図 10 待ち時間の原因となる SQL クエリ レコード
図 10 待ち時間の原因となる SQL クエリ レコード (画像を拡大するには、ここをクリックします)

このクエリは、平均 CPU 使用率、合計 CPU 使用率、個別のクエリと親クエリ (個別のクエリがバッチの一部である場合)、および対応するデータベース名を返します。また、前に説明したように、クエリに対してデータベース チューニング アドバイザを実行し、さらに改善が可能かどうかを判断するのも有効な場合があります。

コストのかかる CLR クエリ

SQL Server では、CLR の利用が増加しています。したがって、CLR を最も多く使用するクエリを判別することが役立つ場合があります。この場合の CLR には、ストアド プロシージャ、関数、およびトリガが含まれます。

sys.dm_exec_query_stats DMV には、total_clr_time に関する詳細およびクエリの実行回数が含まれます。また、実際のクエリを、それを含む親クエリから抽出するために使用されるオフセットも含まれます。この DMV は、SQL バッチに関する情報を含む sys.dm_exec_sql_text DMF と結合されます。基になる SQL を取得するために、さまざまなオフセットが適用されます。図 11 は、最もコストのかかる CLR クエリの識別に使用するクエリです。

Figure 11 最もコストのかかる CLR クエリを特定する (Close Figure 11)
SELECT TOP 10
 [Average CLR Time] = total_clr_time / execution_count
,[Total CLR Time] = total_clr_time
,[Execution count] = qs.execution_count
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
         (CASE WHEN qs.statement_end_offset = -1
            THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
          ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)
,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats as qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
WHERE total_clr_time <> 0
ORDER BY [Average CLR Time] DESC;
図 11 最もコストのかかる CLR クエリを特定する
図 11 最もコストのかかる CLR クエリを特定する(画像を縮小するには、ここをクリックします)
図 11 最もコストのかかる CLR クエリを特定する
図 11 最もコストのかかる CLR クエリを特定する (画像を拡大するには、ここをクリックします)

このクエリは、平均 CLR 時間、合計 CLR 時間、実行カウント、個別のクエリ、親クエリ、およびデータベース名を返します。繰り返しますが、クエリに対してデータベース チューニング アドバイザを実行し、さらに改善が可能かどうかを判断するのも有効です。

最も頻繁に実行されるクエリ

前述のコストのかかる CLR クエリのサンプルを変更し、最も頻繁に実行されるクエリを識別できます。ここでも同じ DMV を適用します。非常に頻繁に実行されるクエリのパフォーマンスを改善すると、あまり実行されない大きいクエリを最適化するよりも、大幅にパフォーマンスが向上する可能性があります (健全性チェックの場合は、CPU または I/O の累積使用率が最も高いクエリを調査することで、これを検証できます)。頻繁に実行されるクエリの改善のもう 1 つの利点は、ロックの回数とトランザクションの長さが減少する可能性もあることです。もちろん、最終的な結果は、システム全体の応答が改善されることです。

最も頻繁に実行されるクエリは、図 12 のクエリを使用して識別できます。このクエリを実行すると、実行カウント、個別のクエリ、親クエリ (個別のクエリがバッチの一部である場合)、および関連するデータベースが表示されます。繰り返しますが、データベース チューニング アドバイザでクエリを実行し、さらに改善が可能かどうかを判断するのも有効です。

Figure 12 最も頻繁に実行されるクエリを特定する (Close Figure 12)
SELECT TOP 10
 [Execution count] = execution_count
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
         (CASE WHEN qs.statement_end_offset = -1
            THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
          ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)
,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
ORDER BY [Execution count] DESC;
図 12 最も頻繁に実行されるクエリを特定する
図 12 最も頻繁に実行されるクエリを特定する(画像を縮小するには、ここをクリックします)
図 12 最も頻繁に実行されるクエリを特定する
図 12 最も頻繁に実行されるクエリを特定する (画像を拡大するには、ここをクリックします)

ブロックの影響を受けるクエリ

ブロックの影響を最も大きく受けるクエリは、通常、実行時間の長いクエリです。このようなクエリを識別した後、それを分析して、ブロックが減少するように書き直すことができるか、または書き直す必要があるかどうかを判断できます。ブロックの原因としては、一貫性のない順序でのオブジェクトの使用、トランザクション スコープの競合、未使用のインデックスの更新などがあります。

既に説明した sys.dm_exec_query_stats DMV には、ブロックの影響を最も大きく受けるクエリの識別に使用できる列が含まれます。total_elaspsed_time と total_worker_time の差を execution_count で割ると、平均ブロック時間を算出できます。

sys.dm_exec_sql_text DMF には、ブロックが関係する SQL バッチに関する詳細情報が含まれます。基になる SQL を取得するために、さまざまなオフセットがこれに適用されます。

図 13 に示すクエリを使用すると、ブロックの影響を最も大きく受けるクエリを識別できます。結果では、平均ブロック時間、合計ブロック時間、実行カウント、個別のクエリ、親クエリ、および関連するデータベースが表示されます。結果は [Average Time Blocked] (平均ブロック時間) の順に並んでいますが、[Total Time Blocked] (合計ブロック時間) で並べ替えるのも有効です。

Figure 13 最も頻繁にブロックされるクエリを特定する (Close Figure 13)
SELECT TOP 10
 [Average Time Blocked] = (total_elapsed_time - total_worker_time) / qs.execution_count
,[Total Time Blocked] = total_elapsed_time - total_worker_time
,[Execution count] = qs.execution_count
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
         (CASE WHEN qs.statement_end_offset = -1
            THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
          ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)
,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt
ORDER BY [Average Time Blocked] DESC;
図 13 最も頻繁にブロックされるクエリを特定する
図 13 最も頻繁にブロックされるクエリを特定する(画像を縮小するには、ここをクリックします)
図 13 最も頻繁にブロックされるクエリを特定する
図 13 最も頻繁にブロックされるクエリを特定する (画像を拡大するには、ここをクリックします)

クエリを調べると、設計の問題 (インデックスの不足など) やトランザクションの問題 (正しくない順序で使用されているリソース) などが見つかる可能性があります。データベース チューニング アドバイザでも改善の可能性が指摘される場合があります。

再利用が最も少ないプラン

ストアド プロシージャを使用する利点の 1 つは、クエリ プランがキャッシュされて、クエリをコンパイルせずに再利用できることです。これにより、時間とリソースが節約され、パフォーマンスが向上します。再利用が最も少ないクエリ プランを識別し、そのプランが再利用されない理由をさらに調査できます。一部を書き換えることで再利用を最適化できる場合があります。

図 14 は、再利用が最も少ないプランのクエリを識別するために筆者が作成したスクリプトです。このスクリプトでは、既に説明した DMV と併せて、まだ説明していない dm_exec_cached_plans を使用しています。この DMV には、SQL Server でキャッシュされたクエリ プランに関する詳細も含まれています。ご覧のとおり、結果では、プランが使用された回数 ([Plan usage] (プランの使用回数) 列)、個別のクエリ、親クエリ、およびデータベース名が表示されます。

Figure 14 再利用が最も少ないプランのクエリを特定する (Close Figure 14)
SELECT TOP 10
 [Plan usage] = cp.usecounts
,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2,
         (CASE WHEN qs.statement_end_offset = -1
            THEN LEN(CONVERT(NVARCHAR(MAX),
qt.text)) * 2 ELSE qs.statement_end_offset END -
qs.statement_start_offset)/2)
,[Parent Query] = qt.text
,DatabaseName = DB_NAME(qt.dbid)
,cp.cacheobjtype
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt
INNER JOIN sys.dm_exec_cached_plans as cp on qs.plan_handle=cp.plan_handle
WHERE cp.plan_handle=qs.plan_handle
ORDER BY [Plan usage] ASC;
図 14 再利用が最も少ないプランのクエリを特定する
図 14 再利用が最も少ないプランのクエリを特定する(画像を縮小するには、ここをクリックします)
図 14 再利用が最も少ないプランのクエリを特定する
図 14 再利用が最も少ないプランのクエリを特定する (画像を拡大するには、ここをクリックします)

示されている個別のクエリを調査することで、プランが頻繁に再利用されない理由 (存在する場合) を明らかにできます。可能性のある 1 つの理由は、実行のたびにクエリが再コンパイルされることです。このような状況は、さまざまな SET ステートメントまたは一時テーブルがクエリに含まれている場合に発生することがあります。再コンパイルとプランのキャッシュの詳細については、「SQL Server 2005 のバッチのコンパイル、再コンパイル、およびプランのキャッシュに関する問題」(microsoft.com/technet/prodtechnol/sql/2005/recomp.mspx) という記事を参照してください。

クエリを複数回実行する機会は何回もあったということを確認してください。関連する SQL トレース ファイルを使用すると、これを確認できます。

その他の作業

さまざまな DMV によって公開されるメトリックスは、永続的に保存されるのではなく、メモリ内に保持されるだけであることに注意してください。SQL Server 2005 を再起動すると、これらのメトリックスは失われます。

DMV からの出力に基づいて定期的にテーブルを作成し、タイムスタンプを付けて結果を保存できます。その後、タイムスタンプの順番にこれらのテーブルを調査して、アプリケーションの変更による影響、または特定のジョブや時間ベースの処理の影響を明らかにすることができます。たとえば、月末処理のプロシージャではどのような影響があるでしょうか。

同様に、特定のトレース ファイルの作業負荷をそのようなテーブルと相関させて、不足インデックスや最も使用頻度の高いクエリなどに対する特定の作業負荷 (毎日または月末など) の影響を判定できます。ここで紹介したスクリプトを編集してこのようなテーブルを作成し、継続的なメンテナンス タスクの一部として定期的に実行できます。

また、この記事で解説したスクリプトを使用するカスタム レポートを、Visual Studio 2005 で作成することもできます。これらは SQL Server Management Studio に簡単に統合して、さらにデータをわかりやすく表示できます。

可能な限り、ここで説明した方法を、トレースや比率分析などの他の方法論と組み合わせてみてください。そうすることで、データベース パフォーマンスの改善に必要な変更がさらに完全にわかるようになります。

この記事では、通常の動作過程で SQL Server 2005 が蓄積する豊富な情報の有用性について紹介しました。この情報を照会することで、クエリのパフォーマンスを改善する継続的な活動に役立つ良い結果が得られます。たとえば、サーバーが待機する原因を発見したり、パフォーマンスに悪影響のある未使用のインデックスを特定したり、最も頻繁に使用されるクエリや最もコストのかかるクエリを判別したりできます。この隠れたデータの調査を始めれば、さまざまな可能性が広がります。DMV について学ぶ必要のあることはまだたくさんあります。この記事によってさらなる究明への意欲がかき立てられることを期待します。

Ian Stirk は、1987 から IT 業界に身を置き、開発者、設計者、アーキテクトなどとして働いてきました。彼は、M.Sc.、MCSD、MCDBA、および SCJP の資格を持っています。Ian は、イギリスのロンドンで働く Microsoft テクノロジ関連のフリーのコンサルタントです。連絡先は Ian_Stirk@yahoo.com です。


検索機能: .NET Framework 3.5 でディレクトリのセキュリティ プリンシパルを管理する

検索機能

.NET Framework 3.5 でディレクトリのセキュリティ プリンシパルを管理する

Joe Kaplan and Ethan Wilansky
この記事は、Visual Studio 2008 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
コードのダウンロード : : DSAccountManagement2008_01.exe (160KB)
この記事の内容 : :
  • System.DirectoryServices.AccountManagement クラス
  • Active Directory ドメイン サービス
  • Active Directory ライトウェイト ディレクトリ サービス (AD LDS)
  • ユーザー、コンピュータ、およびグループのプリンシパルを管理する
この記事は次のテクノロジを使用しています:
.NET Framework 3.5、Visual Studio 2008
コンテンツ
ディレクトリ サービスのプログラミング アーキテクチャ
コンテキストを確立する
ユーザー アカウントを作成する
グループおよびコンピュータを作成する
グループ メンバシップを管理する
検索する
一致するオブジェクトを検索する
難しい検索操作を簡単にする
ユーザーを認証する
拡張性モデル
最後に

ディレクトリは重要であるにもかかわらず、習得されることが稀なエンタープライズ アプリケーション開発コンポーネントです。Windows® プラットフォームの場合、Microsoft は 3 つの主要なディレクトリ プラットフォームを提供しています。それは、Active Directory® ドメイン サービス、すべての Windows コンピュータに存在するローカル セキュリティ アカウント マネージャ (SAM) データ ストア、および比較的新しい Active Directory ライトウェイト ディレクトリ サービス (AD LDS) です。AD LDS は、以前は Active Directory Application Mode または単に ADAM とも呼ばれていました。多くのエンタープライズ開発者は最低限 SQL プログラミングの基本に関する知識を持っていますが、それに比べると、ディレクトリ サービスのプログラミングの経験がある開発者はごく少数です。

Microsoft® .NET Framework の元のバージョンでは、System.DirectoryServices 名前空間内でディレクトリ サービスのプログラミングを行う一連のクラスを提供していました。これらのクラスは、既存の COM ベースの OS コンポーネント (具体的には Active Directory サービス インターフェイス (ADSI)) 上の単純なマネージ相互運用層でした。このプログラミング モデルもそれなりに高機能ではありましたが、完全な ADSI モデルと比べると、より一般的で、機能も限られています。

.NET Framework 2.0 では、System.DirectoryServices に機能が追加され、2 つの新しい名前空間 (System.DirectoryServices.ActiveDirectory および System.DirectoryServices.Protocols) が提供されました (この記事では、それぞれ ActiveDirectory 名前空間および Protocols 名前空間と呼ぶことにします)。ActiveDirectory 名前空間に、サーバー、ドメイン、フォレスト、スキーマ、レプリケーションなど、ディレクトリのインフラストラクチャ レベル コンポーネントの厳密に型指定された管理を行うための新しいクラスが豊富に導入されました。Protocols 名前空間はこれとは方向性が異なり、ライトウェイト ディレクトリ アクセス プロトコル (LDAP) のプログラミングに使用する代替 API を提供します。この代替 API は、Windows LDAP サブシステム (wldap32.dll) を直接操作し、ADSI COM 相互運用層を完全にスキップします (図 1 を参照)。

図 1 Microsoft ディレクトリ サービスのプログラミング アーキテクチャ
図 1 Microsoft ディレクトリ サービスのプログラミング アーキテクチャ(画像を縮小するには、ここをクリックします)
図 1 Microsoft ディレクトリ サービスのプログラミング アーキテクチャ
図 1 Microsoft ディレクトリ サービスのプログラミング アーキテクチャ (画像を拡大するには、ここをクリックします)

ところが、ADSI には、Users や Groups など、開発者がセキュリティ プリンシパルの管理に使用していた厳密に型指定されたインターフェイスがありません。System.DirectoryServices のより一般的なクラスを使用してこれらのタスクの多くを実行することも可能ですが、使いやすさに欠け、多くのタスクはきわめて不明瞭です。Microsoft は、.NET Framework 3.5 のこの問題に対処するために、セキュリティ プリンシパルの管理専用の新しい名前空間 System.DirectoryServices.AccountManagement を追加しました (この記事では、System.DirectoryServices.AccountManagement を AccountManagement 名前空間と呼ぶことにします)。

この新しい名前空間には、3 つの主な目標があります。それは、3 つのディレクトリ プラットフォーム間のプリンシパルの管理操作を簡素化すること、基本となるディレクトリの違いに関係なくプリンシパルの管理操作を統一すること、これらの操作に対して信頼できる結果を返すことです。これらの目標を達成することで、ユーザーが細かい注意事項や特殊なケースを意識する必要がなくなります。

.NET の計画が固まるのを待って、Microsoft は .NET 機能を利用したこれらの機能の API をさらに改良し、AD LDS などの新しいディレクトリ プラットフォームのサポートを強化して、ADSI の機能を改善しました。

ディレクトリ サービスのプログラミング アーキテクチャ

先に進む前に、この記事のコードをダウンロードし、これらの手法を使用するための前提条件を満たしていることを確認してください。では、始めましょう。図 1 は、System.DirectoryServices の全体的なプログラミング アーキテクチャを示しています。AccountManagement 名前空間は ActiveDirectory 名前空間と同様、System.DirectoryServices の上の抽象層に属します。System.DirectoryServices も ADSI の上の抽象層です。AccountManagement 名前空間は、高速認証などの小規模の機能セットでは Protocols 名前空間にも依存しています。図 1 の紺色の箇所は、AccountManagement が依存するディレクトリ サービスのプログラミング アーキテクチャの部分を示しています。

図 2 に、AccountManagement 名前空間の主な型を示します。.NET Framework 2.0 で追加された名前空間とは異なり、AccountManagement の対象領域は比較的小規模です。列挙型や例外クラスなど、サポートされる一部の型を除き、名前空間は主に 3 つのコンポーネントで構成されます。そのコンポーネントとは、厳密に型指定された User、Group、Computer の各オブジェクトを表す Principal 派生クラスのツリー、基になるストアへの接続の確立に使われる PrincipalContext クラス、ディレクトリ内のオブジェクトの検索に使われる PrincipalSearcher クラス (サポートされる型を使用) です。

図 2 System.DirectoryServices.AccountManagement の重要なクラス
図 2 System.DirectoryServices.AccountManagement の重要なクラス(画像を縮小するには、ここをクリックします)
図 2 System.DirectoryServices.AccountManagement の重要なクラス
図 2 System.DirectoryServices.AccountManagement の重要なクラス (画像を拡大するには、ここをクリックします)

AccountManagement 名前空間は、サポートされる 3 つのディレクトリ プラットフォーム上で操作を行うために内部でプロバイダ設計パターンを使用します。そのため、各種 Principal クラスのメンバは、基になるディレクトリ ストアが異なっていても同様に動作します。この設計は、簡潔さと一貫性を確保するうえで重要です。

コンテキストを確立する

PrincipalContext クラスを使用してターゲット ディレクトリへの接続を確立し、ディレクトリに対する操作を実行するための資格情報を指定します。この方法は、ActiveDirectory 名前空間で DirectoryContext クラスを使用してコンテキストを確立する場合と同様です。

PrincipalContext コンストラクタには、コンテキストの確立に的確なオプションを提供する多彩なオーバーロードがあります。System.DirectoryServices の DirectoryEntry クラスを使用する場合、多くの PrincipalContext オプションは既に見慣れたものですが、PrincipalContext の 3 つのオプション (ContextType、名前、コンテナ) を使用すると、DirectoryEntry クラスの入力パラメータよりはるかに詳細な指定が可能です。そのため、適切な入力パラメータの組み合わせを指定できます。System.DirectoryServices および ADSI では、PrincipalContext コンストラクタのこれら 3 つのパラメータをパスと呼ばれる 1 つの文字列に結合します。各コンポーネントを区切って、パスの各部分の意味をわかりやすくします。

ターゲット ディレクトリのタイプを指定するには、ContextType 列挙型 (Active Directory ドメイン サービスの場合は Domain、AD LDS の場合は ApplicationDirectory、ローカル SAM データベースの場合は Machine) を使用します。これに対して、System.DirectoryServices を使用する場合はパス文字列のプロバイダ コンポーネント (通常は "LDAP" または "WinNT") を使用してターゲット ストアを指定します。次に、ADSI がこの値を内部的に読み取り、適切なプロバイダを読み込みます。

AccountManagement 名前空間のクラスを使用すると、この作業が簡単になり、確実に Framework でサポートされるプロバイダのみを使用することになるため、一貫性も高まります。ADSI および System.DirectoryServices のプログラミングに多い、悩ましいプロバイダのスペルミスや不正なケースの問題も避けられます。とはいっても、AccountManagement は IIS や Novell ディレクトリ サービスなどのあまり一般的でない ADSI プロバイダをサポートしていません。

このような場合には、PrincipalContext コンストラクタの名前パラメータを使用して接続先ディレクトリの名前を指定します。このパラメータには、サーバー、コンピュータ、またはドメインの名前を指定できます。このパラメータが空の場合、AccountManagement は現在のセキュリティ コンテキストに基づいて既定のコンピュータまたはドメインを接続先に指定することを覚えておいてください。ただし、AD LDS ストアに接続する場合は、名前パラメータの値を指定する必要があります。

コンテナ パラメータには、コンテキストを確立するディレクトリのターゲット場所を指定します。Machine ContextType を使用する場合は、SAM データベースが階層化されていないため、このパラメータを指定しないでください。ApplicationDirectory を使用する場合は、AD LDS がディレクトリのルート オブジェクトの推測に使用する defaultNamingContext 属性を公開しないため、コンテナ パラメータの値を指定する必要があります。Domain ContextType の場合、このパラメータは省略可能です。省略すると、AccountManagement は defaultNamingContext を使用します。

その他のパラメータ (ユーザー名、パスワード、ContextOptions 列挙型) には、必要に応じてプレーン テキストの資格情報および使用する各種接続セキュリティ オプションを指定します。

すべてのディレクトリが Windows ネゴシエート認証方式をサポートしています。コンピュータ ストアのオプションを指定しなければ、Windows ネゴシエート認証が使用されます。ただし、Domain および ApplicationDirectory の既定値は、署名およびシールを使用した Windows Negotiate です。

Active Directory ドメイン サービスおよび AD LDS は、LDAP の単純なバインドもサポートしています。Active Directory ドメイン サービスでは、一般にこのバインドを使用することは避けられますが、AD LDS では、AD LDS ストアのユーザーがプリンシパルを操作するためにこのバインドが必要な場合があります。

ユーザー名またはパスワード パラメータが未指定の場合、AccountManagement は現在の Windows セキュリティ コンテキストを使用します。資格情報を指定する場合、サポートされるユーザー名の形式は SamAccountName、UserPrincipalName、および NT4Name です。図 3 に、コンテキストを確立する 3 つの方法を示します。

Figure 3 コンテキストを確立する 3 つの例 (Close Figure 3)
// create a context for a domain called Fabrikam pointed
// to the TechWriters OU and using default credentials
PrincipalContext domainContext = new PrincipalContext(
  ContextType.Domain,"Fabrikam","ou=TechWriters,dc=fabrikam,dc=com");

// create a context for the current machine SAM store with the
// current security context
PrincipalContext machineContext = new PrincipalContext(
  ContextType.Machine);

// create a context for an AD LDS store pointing to the
// partition root using the credentials for a user in the AD LDS store
// and SSL for encryption
PrincipalContext ldsContext = new PrincipalContext(
    ContextType.ApplicationDirectory, "sea-dc-02.fabrikam.com:50001",
    "ou=ADAM Users,o=microsoft,c=us",
    ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind,
    "CN=administrator,OU=ADAM Users,O=Microsoft,C=US ", "pass@1w0rd01");

AccountManagement の PrincipalContext と System.DirectoryServices の DirectoryEntry クラスには、バインド操作の発生時に関して他にも若干の重要な相違点があります。PrincipalContext はオブジェクトの作成時に基になるディレクトリに接続およびバインドしますが、DirectoryEntry は接続を強制する別の操作が行われるまでバインドしません。そのため、PrincipalContext では、接続がディレクトリへのバインドに成功したかどうかがすぐにフィードバックされます。

ユーザー アカウントを作成する

これで、AccountManagement が PrincipalContext を使用してコンテナに接続およびバインドする方法については大分理解できたと思います。次に、DirectoryServices の一般的な操作である、ユーザー アカウントの作成について説明します。各コード サンプルでは、1 つの必須属性に値を割り当て、2 つのオプション属性を追加し、パスワードを設定してユーザー アカウントを有効にし、変更をディレクトリにコミットします。

ここでは、図 3 の例 1 の domainContext 変数を使用して、新しい UserPrincipal を作成します。

// create a user principal object
UserPrincipal user = new UserPrincipal(domainContext,
     "User1Acct", "pass@1w0rd01", true);

// assign some properties to the user principal
user.GivenName = "User";
user.Surname = "One";

// force the user to change password at next logon
user.ExpirePasswordNow();

// save the user to the directory
user.Save();

domainContext は、操作に使用するディレクトリおよびセキュリティ コンテキストへの接続を確立します。次に、1 行のコードで新しいユーザー オブジェクトを作成し、パスワードを設定して、そのパスワードを有効にします。その後、GivenName プロパティと Surname プロパティを使用して、対応するディレクトリ属性を基になるストアに設定します。オブジェクトを基になるディレクトリ ストアに保存する前にパスワードが期限切れになり、ユーザーは最初のログオン時にパスワードを変更する必要があります。

図 4 に、System.DirectoryServices でユーザー アカウントを作成する手順との比較を示します。1 行目のコードのコンテナ変数 container は DirectoryEntry クラス オブジェクトであり、接続のパスを使用しています。このパスはプロバイダ、ドメイン、およびコンテナ (TechWriters OU) を指定します。また、このパスは現在のユーザーのセキュリティ コンテキストを使用して接続します。container 変数は、ユーザー プリンシパルを作成した前述の例の domainContext と同様です。

Figure 4 System.DirectoryServices を使用したアカウントの作成 (Close Figure 4)
    DirectoryEntry container =
    new DirectoryEntry("LDAP://ou=TechWriters,dc=fabrikam,dc=com");
// create a user directory entry in the container
DirectoryEntry newUser = container.Children.Add("cn=user1Acct", "user");

// add the samAccountName mandatory attribute
newUser.Properties["sAMAccountName"].Value = "User1Acct";

// add any optional attributes
newUser.Properties["givenName"].Value = "User";
newUser.Properties["sn"].Value = "One";

// save to the directory
newUser.CommitChanges();

// set a password for the user account
// using Invoke method and IadsUser.SetPassword
newUser.Invoke("SetPassword", new object[] { "pAssw0rdO1" });

// require that the password must be changed on next logon
newUser.Properties["pwdLastSet"].Value = 0;

// enable the user account
// newUser.InvokeSet("AccountDisabled", new object[]{false});
// or use ADS_UF_NORMAL_ACCOUNT (512) to effectively unset the
// disabled bit
newUser.Properties["userAccountControl"].Value = 512;

// save to the directory
newUser.CommitChanges();

このコードは AccountManagement の例より多少長いだけでなく、より複雑です。そのため、さらに、ディレクトリの基になるデータ モデルに関する知識および一定のアカウント管理機能の処理方法に関する知識 (初期パスワードの設定や、無効フラグの取り消しによるユーザー オブジェクトの有効化など) が必要になります。これは、リフレクション ベースの Invoke メソッドおよび InvokeSet メソッドを使用して基底の ADSI COM インターフェイス メンバを呼び出す場合にやや不都合です。この複雑さが原因で、多くの開発者がディレクトリ サービスのプログラミングに苦労します。

また、AD LDS またはローカル SAM データベースに同じユーザー アカウントを作成する場合には、さらに大きな相違点があります。AD LDS と Active Directory ドメイン サービスではユーザー アカウントを有効化するメカニズムが異なり (AD LDS では、Active Directory ドメイン サービスの userAccountControl 属性ではなく、msds-userAccountDisabled 属性を使用します)、SAM ストアでは ADSI インターフェイス メンバを呼び出す必要があります。このように 3 つのディレクトリ ストア間のアカウント管理機能に一貫性がないということは、AccountManagement 名前空間が克服しなければならない主な設計課題の 1 つでした。現在では、UserPrincipal オブジェクトの作成に使用する PrincipalContext を変更するだけでディレクトリ ストアを簡単に切り替えることができるようになり、アカウント管理機能に一貫性がもたらされました。

.NET Framework 2.0 で導入された Protocols 名前空間では、これらの要件に対処していません。この名前空間の目的は、システムレベルの LDAP プログラマに対して、LDAP ベース アプリケーションを構築するためのより高機能で柔軟な API を提供することです。ところが、System.DirectoryServices より LDAP モデル上の抽象化が少ないため、各種ディレクトリ間の相違を簡素化するためには役立ちません。また、非 LDAP ディレクトリであるローカル SAM データベースで使用することは考慮されていません。この記事に付属しているオンライン サンプル コードには、図 3 の後に示した AccountManager の例と同様のサンプルがあります。実行するタスクは同じですが、このサンプルはコード行が 3 倍になります。

図 5 の PrincipalContext は、SAM ストアをターゲットにするためにコンピュータを参照する ContextType 列挙型を示しています。次のパラメータは、コンピュータの名前または IP アドレスを指定し、最後の 2 つの値はアカウントを作成する権限を持つアカウントの資格情報を指定します。

Figure 5 SAM データベース ユーザー アカウントを作成する (Close Figure 5)
PrincipalContext principalContext = new PrincipalContext(
    ContextType.Machine. "computer01", "adminUser", "adminPassword");

UserPrincipal user = new UserPrincipal(principalContext,
    "User1Acct", "pass@1w0rd01", true);

//Note the difference in attributes when accessing a different store
//the attributes appearing in IntelliSense are not derived from the
//underlying store
user.Name = "User One";
user.Description = "User One";
user.ExpirePasswordNow();
user.Save();

Name プロパティと Description プロパティを設定しているのは、SAM ストアには Active Directory ドメイン サービスのような givenName、sn、または displayName 属性がないためです。AccountManagement は 3 つのディレクトリ ストア間の操作性の統一を試みていますが、基になるモデルに違いがあります。ただし、基になるストアで使用できない属性を取得しようとすると、InvalidOperationException がスローされます。

図 3 および図 5 は、ユーザー アカウントを作成する 2 つの例です。これらの例は、ストアを操作する際の AccountManagement 名前空間に固有の一貫したプログラミング モデルを示しています。図 6 の PrincipalContext は、図 3 のように、ContextType.ApplicationDirectory を使用して AD LDS ストアを指定しています。次のパラメータは AD LDS サーバーを示しています。この場合、sea-dc-02.fabrikam.com は AD LDS インスタンスをホストするサーバーの完全修飾ドメイン名 (FQDN) であり、このインスタンスは SSL 通信のポート 50001 でリッスンしています。コードのダウンロードでは、ポート 50000 上での非 SSL 通信を使用しています。この通信はセキュリティ保護されていませんが、自作コードのテストには役立ちます。

Figure 6 ADAM または AD LDS ユーザー アカウントを作成する (Close Figure 6)
PrincipalContext principalContext = new PrincipalContext(
    ContextType.ApplicationDirectory,
    "sea-dc-02.fabrikam.com:50001",
    "ou=ADAM Users,o=microsoft,c=us",
    ContextOptions.SecureSocketLayer | ContextOptions.SimpleBind,
    "CN=administrator,OU=ADAM Users,O=Microsoft,C=US",
    "P@55w0rd0987");

UserPrincipal user = new UserPrincipal(principalContext,
    "User1Acct", "pass@1w0rd01", true);

user.GivenName = "User";
user.Surname = "One";

user.Save();

次のパラメータは、CRUD (Create/Read/Update/Delete: 作成/読み取り/更新/削除) 操作を実行するコンテナを示します。この例では、CRUD 操作の対象として AD LDS に保存されたユーザーを指定しているため、LDAP の単純なバインドを使用する必要があります。また、SSL と組み合わせてセキュリティを設定しています。AD LDS はセキュリティで保護された DIGEST 認証をネイティブにサポートしていますが、ADSI はこの認証をサポートしていません。今度の例も前述の 2 つの例とほぼ同様ですが、PrincipalContext だけが大幅に異なります。

AccountManagement 名前空間には、パスワードの有効期間やアカウントのアンロックなどの豊富な一連のアカウント管理機能があります。ここでそのすべてを取り上げる余地はありませんが、重要なことは、これらの機能を使用することで各種ディレクトリ ストア間の操作に一貫性と信頼性がもたらされ、このような機能の実装に伴う困難が解消されるということです。

グループおよびコンピュータを作成する

ここまでで、ユーザー アカウントの作成は単純で、DirectoryServices ストア間で一貫していることがわかりました。今度は、この一貫性を、サポートされる他の 2 つの DirectoryServices オブジェクトであるグループおよびコンピュータの作成にまで拡張します。GroupPrincipal クラスおよび ComputerPrincipal クラスは、UserPrincipal クラスと同様に Principal 抽象クラスを継承し、UserPrincipal クラスと同様に動作します。たとえば、Active Directory ドメイン サービス、AD LDS、または SAM アカウント データベースに Group01 という名前のグループを作成するには、次のコードを使用します。

GroupPrincipal group = new GroupPrincipal(principalContext,
    "Group01");
group.Save();

いずれの場合も、違いは、異なるストアでコンテキストを確立する PrincipalContext クラスにあります。コンピュータ オブジェクトを作成するコードは、コンテキストを使用し、オブジェクトをプリンシパル コンテキストのターゲットに保存するプリンシパル オブジェクトの作成コードと同様のパターンに従います。

ComputerPrincipal computer = new ComputerPrincipal(domainContext);
computer.DisplayName = "Computer1";
computer.SamAccountName = "Computer1$";
computer.Enabled = true;
computer.SetPassword("p@ssw0rd01");
computer.Save();

ここでも、サポートされるすべての ID ストアの操作モデルを統一する役割を AccountManagement が果たしています。このサンプルは、ドメインに参加可能なコンピュータ オブジェクト (sAMAccountName 属性の末尾に $ が必要) を作成するための適切な構文を示していますが、表示名および共通名を設定しているため、$ は付加されていません。SAM データベースおよび AD LDS にはコンピュータ クラスが存在しないため、ドメインベースの PrincipalContext の内部でこのタイプのオブジェクトを作成できるのは AccountManagement のみです。また、多様なグループ スコープ (グローバル、ユニバーサル、ドメイン ローカル) を備え、セキュリティ グループと配布グループの両方があるのは Active Directory ドメイン サービスのみです。そのため、GroupPrincipal クラスには、必要に応じて値を設定できるように null 許容型のプロパティが用意されています。

グループ メンバシップを管理する

AccountManagement 名前空間はグループ メンバシップの管理も簡素化します。AccountManagement の導入以前は、種類の異なるストアのグループ管理には多数の特異性や非一貫性が見られました。メンバの多いグループの管理をプログラムで行うことは困難でした。また、SAM グループのメンバシップの管理には COM 相互運用機能を使用し、Active Directory ドメイン サービスおよび AD LDS のグループ管理には LDAP 属性を使用する必要がありました。現在は、GroupPrincipal クラスの Members プロパティを使用してグループのメンバシップを列挙し、メンバを管理することができます。それだけで適切に動作します。

一見単純そうで実は難しいその他の操作に、ユーザーが属するすべてのグループの取得があります。AccountManagement には、この操作に役立つ複数のメソッドがあります。Principal 基本クラスには、任意の Principal 型のグループ メンバシップを取得する 2 つの GetGroups メソッドと、プリンシパルがグループのメンバであるかどうかを確認する 2 つの IsMemberOf メソッドがあります。また、UserPrincipal には、UserPrincipal 型の完全に拡張されたセキュリティ グループを返す、特殊な GetAuthorizationGroups メソッドがあります。図 7 は、GetAuthorizationGroups メソッドの使い方を示しています。

Figure 7 GetAuthorizationGroups メソッドを使用する (Close Figure 7)
string userName = "user1Acct";

// find the user in the identity store
UserPrincipal user =
    UserPrincipal.FindByIdentity(
        adPrincipalContext,
        userName);

// get the groups for the user principal and
// store the results in a PrincipalSearchResult object
PrincipalSearchResult<Principal> results =
    user.GetAuthorizationGroups();

// display the names of the groups to which the
// user belongs
Console.WriteLine("groups to which {0} belongs:", userName);
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

さらに、AccountManagement で簡素化できる難しい操作としては、信頼されたドメインまたは外部セキュリティ プリンシパル間のグループ メンバシップの拡張があります。Principal クラスの GetGroups(PrincipalContext) メソッドは、この難しい操作を自動的に処理してくれます。

検索する

ディレクトリ内のオブジェクトの検索もプログラマが苦労する作業の 1 つです。LDAP は、開発が普段取り組んでいる構文と比べて特に複雑なクエリ言語ではありませんが、あまり馴染みがありません。また、LDAP の基本を知っていても、多くの場合、一般的なタスクを行うための使い方を見つけることが困難です。

ここでも、AccountManagement の FindByIdentity メソッドを使用してオブジェクトを検索すれば、これらのタスクは簡単になります。このメソッドは、UserPrincipal、GroupPrincipal、および ComputerPrincipal の各クラスの派生元となる Principal 抽象クラスの一部です。そこで、これらの Principal 型を検索する必要がある場合は、FindByIdentity が役立ちます。

FindByIdentity には 2 つのオーバーロードがあり、どちらも検索する PrincipalContext と値を受け取ります。値には、サポートされる任意の ID 型 (SamAccountName、Name、UserPrincipalName、DistinguishedName、Sid、Guid) を指定できます。もう 1 つのオーバーロードでも、値として指定する ID 型を明示的に定義できます。

単純なオーバーロードから始まり、FindByIdentity を使用して前述の例で作成したユーザー アカウントを返す方法を次に示します。

UserPrincipal user = UserPrincipal.FindByIdentity(principalContext, "user1Acct");

principalContext オブジェクトに保存されているコンテキストを取得した後、FindByIdentity メソッドを使用してプリンシパル オブジェクト (この場合は UserPrincipal) を取得します。サポートされる ID ストアのいずれかにコンテキストを確立した後の ID 検索のコードは常に同じです。

2 番目の FindByIdentity コンストラクタでは、ID 形式を明示的に指定できます。このコンストラクタを使用する場合、値の形式を、指定する ID 型に一致させる必要があります。たとえば、次のコードを実行すると、オブジェクトが指定されたディレクトリおよび場所に存在すれば、識別名を使用して UserPrincipal が適切に返されます。

UserPrincipal user = UserPrincipal.FindByIdentity(
    adPrincipalContext,
   IdentityType.DistinguishedName,
   "CN=User1Acct,OU=TechWriters,DC=FABRIKAM,DC=COM");

これに対して、次のコードでは、IdentityType 列挙型で DistinguishedName 形式を指定しているにもかかわらず値の形式が異なるため、UserPrincipal は返されません。

UserPrincipal user = UserPrincipal.FindByIdentity(
    adPrincipalContext,
   IdentityType.DistinguishedName,
   "user1Acct");

形式は重要です。たとえば、IdentityType が GUID の場合、値には標準の COM GUID 形式を指定し、IdentityType が SID の場合、値にはセキュリティ記述子定義言語 (SDDL) 形式を指定する必要があります。この記事のコード サンプルでは、適切な形式を示す 2 つのメソッド (FindByIdentityGuid および FindByIdentitySid) を使用しています。これらのメソッドでディレクトリ ストアから一致する形式を検索するには、GUID または SID の値を変更する必要があります。いずれかの形式の取得は、PrincipalSearcher クラスを使用すれば簡単に行うことができます。詳しくは後述します。

これで Principal オブジェクトの検索およびバインドが完了し、簡単に操作できるようになりました。たとえば、次のような方法でユーザーをグループに追加できます。

// get a user principal
UserPrincipal user =
    UserPrincipal.FindByIdentity(adPrincipalContext, "User1Acct");
// get a group principal
GroupPrincipal group =
    GroupPrincipal.FindByIdentity(adPrincipalContext, "Administrators");
// add the user
group.Members.Add(user);
// save changes to directory
group.Save();

ここでは、FindByIdentity メソッドを使用してユーザーを検索した後、グループを検索しています。プリンシパル オブジェクトを取得した後、グループの Members プロパティの Add メソッドを呼び出して、ユーザー プリンシパルをグループに追加します。最後に、グループの Save メソッドを呼び出して変更をディレクトリに保存します。

一致するオブジェクトを検索する

高機能の Query by Example (QBE) 機能および PrincipalSearcher クラスを使用して、定義された条件に基づいてオブジェクトを検索することもできます。QBE および PrincipalSearcher クラスの詳細については後述しますが、まずは単純な検索の例を検討してみましょう。図 8 は、ユーザーが無効で、名前または共通名が "user" というプレフィックスで始まるすべてのユーザー アカウントを検索する方法を示しています。

Figure 8 PrincipalSearcher を使用する (Close Figure 8)
// create a principal object representation to describe
// what will be searched
UserPrincipal user = new UserPrincipal(adPrincipalContext);

// define the properties of the search (this can use wildcards)
user.Enabled = false;
user.Name = "user*";

// create a principal searcher for running a search operation
PrincipalSearcher pS = new PrincipalSearcher();

// assign the query filter property for the principal object
// you created
// you can also pass the user principal in the
// PrincipalSearcher constructor
pS.QueryFilter = user;

// run the query
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Disabled accounts starting with a name of 'user':");
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

PrincipalContext 変数 adPrincipalContext は Active Directory ドメインを示していますが、AD LDS アプリケーション パーティションを示すことも簡単にできます。コンテキストを確立した後、新しい UserPrincipal オブジェクトを作成します。これは、検索操作のプリンシパルのメモリ内表現です。このプリンシパルを作成したら、検索結果を制限するプロパティを設定します。次の 2 行のコードは、設定可能な制限 (ユーザー名が特定の値で始まるすべての無効なユーザー アカウント) を示しています。Name 属性のプロパティ値にはワイルドカードを使用できます。

検索フィルタを設定する LDAP 言語を使い慣れている読者なら、QBE が斬新で、直観性の点で勝っている理由がすぐにわかるでしょう。次に、QBE を使用してクエリ操作に使用するサンプル オブジェクトを設定します。QBE が一般的な DirectoryServices 検索言語より簡単であることをはっきり示すため、図 8 で作成している QBE オブジェクトに相当するフィルタの設定を LDAP 言語で行った場合のコードを次に示します。

(&(objectCategory=person)(objectClass=user)(name=user*)(userAccount
Control:1.2.840.113556.1.4.803:=2))

ご覧のとおり、LDAP 言語の方がかなり複雑なうえ、Active Directory LDS のユーザー スキーマは LDAP 言語の userAccountControl 属性ではなく、msDS-UserAccountDisabled 属性を使用するため、このコードは AD LDS には使用できません。ここでも、AccountManagement がこれらの背後の相違に対処します。

図 8 に示されているように QBE オブジェクトの設定が完了したら、PrincipalSearcher オブジェクトを作成し、このコードで先に Principal オブジェクトで作成しておいた QueryFilter プロパティを割り当てます。QueryFilter プロパティを設定する代わりに、PrincipalSearcher コンストラクタにユーザー プリンシパルを渡すこともできます。次に、クエリを実行し、PrincipalSearcher の FindAll メソッドを呼び出して、返された結果を PrincipalSearchResult ジェネリック リストに割り当てます。PrincipalSearchResult リストに、返された Principal オブジェクトが保存されます。最後に、プリンシパルのリストを列挙し、返された各プリンシパルの Name 属性を表示します。

QBE は参照属性を処理しません。そのため、QBE オブジェクトが所有していない属性をオブジェクトのメモリ内表現の設定に使用することはできません。

foreach ループを使用すれば、はるかに多くの処理を行うことができます。たとえば、無効になっているユーザー アカウントを有効にしたり、これらのユーザー アカウントを削除したりすることができます。読み取り操作の直後に他の ID ストアを指定する場合、返される属性がそのストアに存在する必要があります。たとえば、AD LDS のユーザーには sAMAccountName 属性が存在しないため、結果にこの属性を返そうとしても意味がありません。

難しい検索操作を簡単にする

他の高機能なメソッド FindBy を PrincipalSearchResult クラスと組み合わせて使用することにより、他の方法では取得が難しいユーザー プリンシパルおよびコンピュータ プリンシパルに関する情報を取得できます。図 9 は、今日パスワードを変更した各ユーザーの名前を取得する方法を示しています。この例では、FindByPasswordSetTime メソッドと PrincipalSearchResult クラスを使用しています。基になる pwdLastSet 属性が Large 整数としてディレクトリに保存されているため、AccountManagement を使用しないと、この処理は複雑になります。

Figure 9 今日パスワードをリセットしたユーザーを取得する (Close Figure 9)
// get today's date
DateTime dt = DateTime.Today;

// run a query
PrincipalSearchResult<Principal> results =
    UserPrincipal.FindByPasswordSetTime(
        adPrincipalContext,
        dt,
        MatchType.GreaterThanOrEquals);

Console.WriteLine("users whose password was set on {0}",
    dt.ToShortDateString());
foreach (Principal result in results)
{
    Console.WriteLine("name: {0}", result.Name);
}

この記事のコード サンプルには、他の FindBy メソッドを使用した例もあります。これらの操作は、すべて図 9 で示した例と同様です。

FindBy メソッドは、他の方法では取得するのが困難な情報を簡単に取得する便利な手段です。ただし、QBE 機能を使用して結果をさらにフィルタする必要がある場合には不適切です。関連付けられた属性が読み取り専用であるため、QBE オブジェクトで設定することはできません。これは、QBE が参照するオブジェクトの属性をユーザーが設定できないのと同様です。QBE を使用するには、この例のプリンシパル オブジェクトの同等の読み取り専用プロパティを AdvancedSearchFilter プロパティと組み合わせて使用します。詳しくは後述します。図 10 に、その他の FindBy メソッドと、FindBy メソッドの代わりに検索に使用可能な同等の読み取り専用プロパティの一覧を示します。

Figure 10 他の FindBy メソッド (Close Figure 10)

メソッド名 | 読み取り専用プロパティ | 説明
FindByLogonTime | LastLogonTime | 指定された時間内にログオンしたアカウント。
FindByExpirationTime | AccountExpirationDate | 指定された時間内に有効期限が切れたアカウント。
FindByPasswordSetTime | LastPasswordSetTime | 指定された時間内にパスワードが設定されたアカウント。
FindByLockoutTime | AccountLockoutTime | 指定された時間内にロックアウトされたアカウント。
FindByBadPasswordAttempt | LastBadPasswordAttempt | 指定された時間内に試行された不正なパスワード操作。
相当するメソッドなし | BadLogonCount | 指定された回数だけログオンを試行した結果、ログオンに失敗したアカウント。

QBE を設定していると、読み取り専用プロパティに値を設定できません。では、検索操作でこのプロパティを使用するには、どうすればよいでしょうか。結果セットを取得し、結果セットを列挙するときに、読み取り専用プロパティを使用して条件テストを行うことができます。コードでは最初に読み取り専用プロパティの結果をフィルタなしで取得し、読み取り専用プロパティで返された結果セットをフィルタする必要があるため、結果セットが大規模になる可能性がある場合にはこの方法はお勧めできません。コード サンプルの PrincipalSearchEx6v2 メソッドは、この最適とまではいえない方法を示しています。

Directory Services チームは、AuthenticablePrincipal クラスに AdvancedSearchFilter プロパティを追加することによって、この QBE の制限に対処しました。AdvancedSearchFilter を使用すれば、読み取り専用プロパティに基づいて検索を行った後、QBE メカニズムで設定可能な他のプロパティと組み合わせることができます。図 11 は、UserPrincipal クラスの LastBadPasswordAttempt 読み取り専用プロパティを使用して、今日不正なパスワード操作を試行したユーザーの一覧を返す方法を示しています。

Figure 11 読み取り専用プロパティを使用した AdvancedSearchFilter (Close Figure 11)
DateTime dt = DateTime.Today;

// create a principal object representation to describe
// what will be searched
UserPrincipal user = new UserPrincipal(adPrincipalContext);

user.Enabled = true;

// define the properties of the search (this can use wildcards)
user.Name = "*";

//add the LastBadPasswordAttempt >= Today to the query filter
user.AdvancedSearchFilter.LastBadPasswordAttempt
    (dt, MatchType.GreaterThanOrEquals);

// create a principal searcher for running a search operation
// and assign the QBE user principal as the query filter
PrincipalSearcher pS = new PrincipalSearcher(user);

// run the query
PrincipalSearchResult<Principal> results = pS.FindAll();

Console.WriteLine("Bad password attempts on {0}:",
    dt.ToShortDateString());
foreach (UserPrincipal result in results)
{
    Console.WriteLine("name: {0}, {1}",
           result.Name,
           result.LastBadPasswordAttempt.Value);
}

ユーザーを認証する

ディレクトリベースのアプリケーションを構築する開発者は、多くの場合、特に AD LDS を使用している場合は、ディレクトリに保存されているユーザーの資格情報を認証する必要があります。.NET Framework 3.5 が登場する前は、プログラマは、このタスクを実行するために System.DirectoryServices の DirectoryEntry クラスを使用して LDAP のバインド操作を内部で強制実行していました。この処理には注意が必要です。セキュリティで保護されていないコードや処理に時間がかかるコードなど、できの悪いコードを作成する余地が大いにあります。また、ADSI はこの種の操作を目的にしていないため、頻繁に使用すると、LDAP 接続の内部キャッシュの方法が原因で処理に失敗する可能性があります。

前述したように、.NET Framework 2.0 の System.DirectoryServices.Protocols アセンブリには、接続ベースのプログラミング メタファを使用する低レベルの LDAP クラスがあります。この設計により、ADSI に固有の制限を解消できますが、コードの作成は複雑になります。

.NET Framework 3.5 では、使用している環境を問わず、プログラマは ASP.NET の ActiveDirectoryMembershipProvider の実装による高機能と使いやすさを AccountManagement で利用できます。また、AccountManagement 名前空間を使用すれば、必要に応じてローカル SAM データベースに対する資格情報を認証できます。

PrincipalContext クラスの 2 つの ValidateCredentials メソッドは、資格情報を検証します。このメソッドを使用するには、まず、検証するディレクトリを使用して PrincipalContext のインスタンスを作成し、適切なオプションを指定します。次に、コンテキストを取得した後、指定されたユーザー名とパスワードの値に基づいて ValidateCredentials が true または false のどちらを返すかをテストします。図 12 は、AD LDS でユーザーを認証する例を示しています。

Figure 12 AD LDS のユーザーを認証する (Close Figure 12)
// establish context with AD LDS
PrincipalContext ldsContext =
    new PrincipalContext(
        ContextType.ApplicationDirectory,
        "sea-dc-02.fabrikam.com:50000",
        "ou=ADAM Users,O=Microsoft,C=US");

// determine whether a user can validate to the directory
Console.WriteLine(
    ldsContext.ValidateCredentials(
        "user1@adam",
        "Password1",
        ContextOptions.SimpleBind +
        ContextOptions.SecureSocketLayer));

この方法は、多数の異なるユーザー資格情報セットをすばやく効率的に検証する場合に特に有効です。対象のディレクトリ ストアの PrincipalContext オブジェクトを 1 つ作成し、ValidateCredentials を呼び出すたびにこのオブジェクトを再利用します。PrincipalContext ではディレクトリへの接続を再利用できるため、パフォーマンスとスケーラビリティが向上します。また、ValidateCredentials の呼び出しはスレッドセーフであるため、インスタンスをこの操作の複数のスレッド間で使用できます。PrincipalContext の作成に使用する資格情報は ValidateCredentials を呼び出しても変更されないという点に注意してください。コンテキストおよびメソッドの呼び出しには個別の接続が確保されます。

既定では、AccountManagement はセキュリティで保護された Windows ネゴシエート認証を使用し、AD LDS に対する単純なバインドを行う場合には SSL の使用を試みます。実行する認証の種類および使用する接続保護 (該当する場合) を常に明示的に指定することをお勧めしますが、少なくとも既定では慎重を期しています。

Windows Server® 2003 以降の Active Directory ドメイン サービスおよび AD LDS には、高速な認証操作を行うための高速同時バインド機能があります。このバインド機能は、ユーザーのセキュリティ トークンを実際に構築しないでユーザーのパスワードを検証します。通常のバインド操作と異なり、高速同時バインドでは LDAP 接続の状態はバインドされません。高速同時バインドを使用して、同じ接続で繰り返しバインド操作を実行し、失敗したパスワードの試行を確認することができます。この機能は ADSI または System.DirectoryServices のオプションにはありませんが、Protocols 名前空間のオプションとして公開されています。

AccountManagement では可能な限り高速同時バインドを使用し、このオプションを自動的に有効にします。図 1 の Protocols 層の上に AccountManagement 層が表示されているのはそのためです。この機能は、ネットワーク上でプレーン テキストの資格情報を渡す単純なバインド モードでのみ有効です。そのため、高速同時バインドは常に SSL と組み合わせてセキュリティ保護する必要があります。

拡張性モデル
ディレクトリ サービスのリソース

  • .NET Framework 3.5 Beta 2 のダウンロード
  • System.DirectoryServices.AccountManagement 名前空間の概要
  • System.DirectoryServices.AccountManagement 名前空間のドキュメント
  • Active Directory ライトウェイト ディレクトリ サービスについて
  • Windows Server 2003 Active Directory アプリケーション モード
  • System.DirectoryServices.Protocols の概要
  • System.DirectoryServices.ActiveDirectory の概要

AccountManagement が本領を発揮するもう 1 つの分野に拡張性モデルがあります。多くの開発者は、Active Directory ドメイン サービスおよび AD LDS のカスタム プロビジョニング システムの構築に各種の Principal 派生クラスを使用することを選択するでしょう。多くの場合 (特に AD LDS を使用する場合)、組織はディレクトリにカスタム スキーマ拡張を追加して、ユーザーおよびグループの固有のメタデータをサポートします。

.NET Framework のオブジェクト指向設計および属性ベースの拡張可能なメタデータを使用した AccountManagement では、カスタム スキーマをサポートするカスタム セキュリティ プリンシパル クラスを簡単に作成できます。Principal の派生クラスのいずれかを継承し、クラスおよびプロパティに適切な属性を指定するだけで、組み込み型で既にサポートされている属性と同様に、カスタム プリンシパル クラスでこれらのディレクトリ属性の読み取りおよび書き込みを行うことができます。

AccountManagement の拡張性メカニズムは Active Directoryドメイン サービスまたは AD LDS に保存されたセキュリティ プリンシパルで使用するための機能である点に注意してください。そのため、Microsoft 以外の LDAP ディレクトリをあまり意識していません。Microsoft 以外の LDAP ディレクトリでプロビジョニングのフレームワークを構築する場合、Protocols 名前空間の低レベル クラスを使用する必要があります。また、SAM スキーマには拡張性がないため、拡張性モデルはローカル SAM アカウントで使用することは考慮されていません。

アプリケーションのセキュリティ プリンシパルを保存するために標準 LDAP ユーザー クラスを使用する AD LDS ディレクトリの場合を考えてみましょう。ここでは、LDAP ディレクトリ スキーマを拡張して、ユーザー オブジェクトを識別する特殊な属性 msdn-subscriberID もサポートします。図 13 は、ユーザー オブジェクトを準備し、この属性に対して作成、読み取り、書き込み操作を提供する機能を持つカスタム クラスの作成方法を示しています。

Figure 13 サンプルの MsdnUser クラス (Close Figure 13)
[DirectoryObjectClass("user")]
[DirectoryRdnPrefix("CN")]
class MsdnUser : UserPrincipal
{
    public MsdnUser(PrincipalContext context)
        : base(context) { }

    public MsdnUser(
        PrincipalContext context,
        string samAccountName,
        string password,
        bool enabled
        )
        : base(
           context,
           samAccountName,
           password,
           enabled
           )
    {
    }

    [DirectoryProperty("msdn-subscriberID")]
    public string MsdnSubscriberId
    {
        get
        {
            object[] result = this.ExtensionGet("msdn-subscriberID");
            if (result != null) {
                return (string)result[0];
            }
            else {
                return null;
            }
        }
        set { this.ExtensionSet("msdn-subscriberID", value); }
    }
}

このコードは UserPrincipal クラスを継承し、2 つの属性 (DirectoryObjectClass および DirectoryRdnPrefix) で修飾されています。この 2 つの属性は、プリンシパル拡張クラスに必要です。DirectoryObjectClass 属性は、サポートされるストア (Active Directory ドメイン サービスまたは AD LDS) がこのオブジェクトのインスタンスをディレクトリに作成するときに objectClass ディレクトリ属性に使用する値を指定します。この例では既定の AD LDS ユーザー クラスを使用していますが、任意の値を指定できます。DirectoryRdnPrefix 属性は、ディレクトリ内のこのクラスのオブジェクト名に使用する RDN (相対識別名) 属性名を指定します。Active Directory ドメイン サービスでは、RDN プレフィックスは常にセキュリティ プリンシパル クラスの CN であり、変更はできません。AD LDS の場合はこの点で柔軟性があり、必要に応じて別の RDN を使用することもできます。

この例のクラスには、文字列を返す MsdnSubscriberID プロパティがあります。このクラスには、プロパティ値の保存に使用する LDAP スキーマ属性を指定する DirectoryProperty 属性が設定されます。基になるフレームワークは、この値を使用して、この Principal 型に対する検索操作を最適化します。

get プロパティおよび set プロパティの実装では、Principal 基本クラスのプロテクトされた ExtensionGet メソッドおよび ExtensionSet メソッドを使用して、基になるプロパティ キャッシュの値の読み取りおよび書き込みを行います。これらのメソッドは、まだデータベース ストアまたは ID ストアに保存されていないオブジェクトの値をメモリに保存する機能をサポートしています。また、これらのメソッドは、既存のオブジェクトの値の読み取りおよび書き込みもサポートしています。LDAP ディレクトリではさまざまな型の属性をサポートし、属性に複数の値を指定できるため、これらのメソッドでは値の読み取りおよび書き込みに object[] 型を使用します。この柔軟性は好ましいものですが、厳密に型指定されたスカラ文字列値をオブジェクト型配列に指定する場合、実装例が示すように、多少余分な作業が必要になります。できあがったカスタム MsdnUser クラスは、簡単にプログラミングできるインターフェイスをユーザーに提供します。

厳密に型指定された値をディレクトリ スキーマに指定する機能は、この拡張性モデルの特に便利な機能の 1 つです。単純な文字列型以外に、.NET Framework の豊富な型システムを使用して、たとえば、Active Directory ドメイン サービスの jpgPhoto 属性を表現する際に、通常は System.DirectoryServices から値を読み取って取得する既定の byte[] を使用するところを、代わりに System.Drawing.Image または System.IO.Stream として表すなどの処理を行うこともできます。

この記事のコード サンプルでは、これらの機能についてもいくつかの例を示しています。また、標準 LDIF 形式ファイル msdnschema.ldf を使用したスキーマ拡張のサンプル コードも示しているので、これを使用して MsdnUser クラスでテスト ディレクトリを拡張することもできます。「ディレクトリ サービスのリソース」の有益なリンクも参照してください。

最後に

AccountManagement は、Microsoft の豊富なディレクトリ サービス プログラミング モデルにぜひとも追加する必要があったマネージ コードです。AccountManagement 名前空間を使用して、開発者は厳密に型指定された一連のプリンシパルを共通 CRUD や検索操作に利用できるようになりました。

この名前空間ではディレクトリ サービスのプログラミングのベスト プラクティスがカプセル化されているため、セキュリティで保護された高パフォーマンスのマネージ コードを作成することができます。また、AccountManagement は拡張可能であるため、Active Directory ドメイン サービスおよび AD LDS のカスタム ディレクトリ オブジェクトで完全に操作できます。

Joe Kaplan は、Accenture の社内 IT 組織で .NET Framework を使用したエンタープライズ アプリケーションの構築に携わっています。アプリケーションのセキュリティ、フェデレーション ID 管理、ディレクトリ サービスのプログラミングを専門とし、この分野で Microsoft MVP に認定されています。共著書に『The .NET Developer's Guide to Directory Services Programming』(Addison-Wesley、2006 年) があります。

Ethan Wilansky は、Microsoft ディレクトリ サービスの MVP および EDS エンタープライズ設計者です。EDS Innovation Engineering の一環として、現在、カスタム SharePoint アプリケーションの構築を中心とする開発チームを率い、ディレクトリ サービス プログラミング ソリューションに関して EDS のアドバイザーも務めています。


編集者のメモ: 新年の展望

編集者のメモ
新年の展望
Joshua Trupin

これまで Microsoft にしばらく勤務した中で学んだ最も重要なことの 1 つは、メトリックスの重要性です。些細とはいえない作業においては、ある種の尺度を持つことで、実際にプロジェクトに着手する前に成功または失敗がどのようなものであるかを定義できます。これは、ビジネスで実施するテスト駆動型開発と考えてみてください。

"MSDN Magazine" も Microsoft の一員であり、この点においては変わりません。先日、毎年の読者アンケートの結果を受け取り、自分たちが開発者コミュニティにどれほど貢献できているかを知る非常に良い機会となりました。Excel ピボットテーブルや PowerPoint デッキの作成のほか非常に多くの分析がまだ必要ですが、興味を引いたいくつかのデータ要素について、この機会を利用して皆さんにご紹介したいと思います。なんと言っても、アンケートは皆さんの全体的なご意見を反映しているので、来年取り上げるトピックを決めるための資料として活用することになります。

最初の質問は、取り上げてほしい最も興味のあるテクノロジについてでした。大多数の読者は、ソフトウェア設計をより詳細に調べることに興味をお持ちでした。この分野は、筆者自身も開発者として関心のある分野です。しかしながら、自分の個人的な好みで読者に影響を及ぼす判断を下してしまうおそれがあるため、この方向に進むことを若干躊躇しています。ご想像いただけると思いますが、このデータ要素は筆者が以前からやりたかった事項に関する重要な個人的評価なのです。

アンケートの上位にランクインしたその他のテクノロジは、Web サービス開発、データベース開発 (アンケート結果が示すほどには、多数のコンテンツを現在用意していません)、および Web クライアント開発でした。

この質問には、直接テキストを入力できるフィールドもあり、繰り返し「私はプログラマではありません」というおもしろい回答が寄せられました。

2 番目に興味深いデータ要素は、完成度の異なるテクノロジを取り上げる必要性のある範囲に関連していました。圧倒的多数の方々が、昨年までにリリースされたテクノロジに焦点を置いた記事に最も興味があると回答しました。その他のオプション、従来のテクノロジ、およびプレリリースのテクノロジまで網羅した説明が好まれるようです。

最後に、最初の 2 つほど簡単に取りかかれないにもかかわらず興味深いデータ要素は、コンピュータ サイエンスの資格を持つ読者の数です。コンピュータ サイエンスの副専攻を持ち、キャリアの大部分をプロ開発者として過ごしてきて、筆者はこの種の学習が、最新のプロ開発者さらには MSDN Magazine の読者層に対しどれほどの重要性を持つかということをしばしば考えてきました。実は、Microsoft の世界的な採用担当者に実施した最近のインタビューで、コンピュータ サイエンスの資格を Microsoft がどの程度重要視しているかについてたずねました (すみませんが、この答えは来月までお待ちください)。しかし、毎年の読者アンケートの結果によると、学士号を持つ読者の多くは、コンピュータ サイエンスの学士号を持っています。

今年のアンケートに参加いただけなかった方々にも、私たちがこれらのアンケートを通じて皆さんからお寄せいただくご意見に特別な興味を持っていることをおわかりいただけたかと思います。次回のアンケートの機会にはぜひご参加ください。なお、これらのデータに基づいて適切な判断を下せるかどうかは、アンケートの質問が適切であるかにかかっています。2008 年に取り上げてほしいトピックや、本誌に関するご意見がありましたらお聞かせください。いつでも mmeditor@microsoft.com まで英語でご連絡ください。最後に、MSDN Magazine が皆さんとの絆をより深めていけることを願っています。そのためにも、ぜひ皆さんのご意見をお寄せください。

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Shawn Burke、Ben Carter、Steve Lasker、Bertrand Le Roy、Eilon Lipton、Thomas Marquardt、Diana Milirud、Larry Olson、Ladi Prosek、Travis Querec、Dave Reed、Jeff Schwartz、Varun Sekhri、Bill Staples、Chris Tavares、Stephen Toub、Varun Vaswani、および Buck Woody です。


ツールボックス: 高度な単体テスト、オブジェクトのモック作成、アプリケーションのプロファイル、その他

ツールボックス
高度な単体テスト、オブジェクトのモック作成、アプリケーションのプロファイル、その他
James Avery

コンテンツ
高度な単体テスト
モックを使用してテストの質を向上する
アプリケーションのパフォーマンスを定量化する
継続的な統合環境を実装する

高度な単体テスト

単体テストは、多くの Microsoft® .NET Framework プロジェクトにおいて、非主流なテクノロジから徐々に標準のプロセスになりつつあります。.NET 開発者の多くは、.NET の優れた単体テスト用フレームワークである nUnit を使用した経験が少なくとも何度かはあります。nUnit は .NET アプリケーションの単体テストに必要な大部分のシナリオを網羅しますが、MbUnit 2.4 を使用すると、単体テストをレベルアップすることができます。MbUnit は、Jonathan "Peli" de Halleux (現在は Microsoft Research の一員です) が立ち上げたオープン ソースの単体テスト用フレームワークであり、現在は Andy Stopford と有能な開発者のチームが管理しています。

MbUnit の基本は、ほとんどの xUnit テスト用フレームワークが備えている一般的な機能 (setup 属性と teardown 属性を含むテスト フィクスチャ、使い慣れたテスト属性、多数のアサーション、コンソール テスト実行ツール、および使いやすいウィンドウ テスト実行ツール) です。ただし、MbUnit は他の単体テスト用フレームワークとは異なり、非常に便利で革新的な複数の追加機能を備えています。

行テストは、単体テストにパラメータを渡すことのできる機能です。調査するさまざまな各シナリオ用に個別のテストを記述する代わりに、パラメータを含む 1 つのテストを記述し、便利な属性を使用してデータを渡すことができます。たとえば、書籍名と ISBN 番号を受け取って、その書籍の在庫があるかどうかをチェックするメソッドがあるとします。この場合、在庫のある書籍、在庫のない書籍、およびデータベースにない書籍のそれぞれのテストを記述するのではなく、書籍名と ISBN 番号をパラメータとして受け取る 1 つのテスト メソッドを記述できます。次に、Row 属性を使用して、シナリオごと、および予想される結果について異なる 3 つの値のセットを指定できます。これにより、記述する必要のあるテスト用コードの量が減るだけでなく、将来的に、テストがわかりやすく保守しやすいものになります。

同様の機能としてペアテストがあります。このテストでは、ファクトリを使用してデータを単体テストに送ることができます。ファクトリを使用すると、多くのコードを記述せずに大量のデータ シナリオを簡単にテストできます。前述の例では、何百または何千もの書籍と ISBN 番号を返すファクトリを作成できます。MbUnit では、以前の組み合わせを基に、冗長な組み合わせのデータがテストされないようにすることができます。

MbUnit には多数のパフォーマンス指向テスト属性も用意されています。duration 属性を使用すると、テストの実行にかかる時間を判断できます。また、MbUnit では、読み取りおよび PerfCounter 属性を使用したパフォーマンス カウンタの値に基づくアサーションを行うことのできるテストを記述できます。

最後に、MbUnit には Rollback 属性というもう 1 つの便利な機能があります。この属性は、テストをトランザクションにラップして、テストが完了したらそのトランザクションをロールバックします。この方法を使用すると、データベースがテスト データでいっぱいになることはありません。この機能は、.NET Framework 1.1 (COM+ トランザクションを使用) と .NET Framework 2.0 (新しい組み込みのトランザクションのサポートを使用) の両方で使用できます。

価格 : 無料 (ソース コードを入手可能)

www.mbunit.com

.NET 開発者に高度な単体テスト サポートを提供する MbUnit
.NET 開発者に高度な単体テスト サポートを提供する MbUnit (画像を縮小するには、ここをクリックします)
.NET 開発者に高度な単体テスト サポートを提供する MbUnit
.NET 開発者に高度な単体テスト サポートを提供する MbUnit (画像を拡大するには、ここをクリックします)

モックを使用してテストの質を向上する

作成する単体テストの数が多いほど、それらの実行にかかる時間が長くなります。単体テストは数分で終わるのが理想です。そうすれば、開発者はアプリケーションを構築するたびに単体テストを実行できます。ただし、現在の大部分のアプリケーションはデータベース、Web サービス、および他の無数の外部の依存関係を多用しています。このような依存関係のいずれかを使用する大量の単体テストを数分で終わらせる方法はありません。

実際に、単体テストをすばやく実行するには、何かを利用して外部の依存関係をシミュレートするしかありません。ここでモックの登場です。モックは、外部の依存関係の代替となるオブジェクトです。外部の依存関係の代わりにモック オブジェクトを使用すると、コードを通常どおり実行できます。これにより、テストをすばやく実行できるようになるだけでなく、(おそらくはより重要な点として) 外部の依存関係ではなくコードの機能のテストに重点を置くことができます。さらに、コードと外部の依存関係との対話をテストすることもできます。ビジネス層から正常にオブジェクトが返されるかをただテストするだけでなく、データ アクセス層で特定のメソッドを呼び出してそのオブジェクトを取得するかどうかをテストできます。

.NET ではさまざまなモック作成用フレームワークを使用できますが、モックが必要とする内容と返す値の定義の問題をそれぞれが少し違った方法で処理します。Rhino.Mocks は、Oren Eini (別名 Ayende Rahien) が開発したオープン ソースのフレームワークであり、厳密に型指定されたモック オブジェクトを提供する点で卓越しています。使用可能なほとんどのモック作成用フレームワークでは、ハードコーディングされた文字列を使用してメソッド名とパラメータを定義する必要があります。つまり、発生する可能性のある問題については、実行時に実際にテストを試行するまでわかりません。厳密に型指定されたモック オブジェクトの使用を許可することにより、Rhino.Mocks ではコンパイルのすべてのメリットが得られ、早い段階でエラーをキャッチするのに役立ちます。

Rhino.Mocks は、許可されているモック作成の種類においても柔軟性があります。通常のモック オブジェクトは、予期しないメソッドが呼び出された場合に例外をスローします。ただし、Rhino.Mocks では、例外をスローするのではなく、予期しないメソッドの呼び出しに対して null または 0 を返す動的なモック オブジェクトを提供します。最後に、Rhino.Mocks では部分モックが許可されています。これにより、期待値が定義されていない場合に例外がスローされないようにすることができますが、0 または null を返すのではなく、オブジェクトの元のメソッドを呼び出して値を返します。この方法でモック作成できるのは、オブジェクトで必要なメソッドのみです。

この他にも、Rhino.Mocks には、ジェネリック型のサポート、コールバック、デリゲートとイベントのモック作成などの多数の便利な機能が用意されています。

価格 : 無料 (ソース コードを入手可能)

www.rhinomocks.com

アプリケーションのパフォーマンスを定量化する

アプリケーション作成の際の主な目標の 1 つはパフォーマンスですが、ほとんどの開発者はこの目標を定量化する方法がほとんどない状態で作業しています。通常、どの画面またはページの実行速度が遅いかを特定するのはユーザーに任されており、その後で開発者が調べるパフォーマンスの問題の原因については推理ゲームのようなものです。開発者が本当に必要とするのは、アプリケーションがどのように実行されているかを定量化し、ボトルネックをすばやく特定する方法です。dotTrace は、JetBrains のアプリケーション プロファイラであり、この両方のタスクを迅速かつ簡単に行うことができます。

dotTrace を使用すると、Windows® アプリケーション、Web アプリケーション (IIS または Visual Studio® Development Server を使用)、および Windows サービスをプロファイルできます。dotTrace では、各メソッドおよびコード行の実行にかかる時間を表示できるパフォーマンスのプロファイルと、使用中のメモリの量およびオブジェクトごとのメモリの量を示すメモリのプロファイルを行うことができます。パフォーマンスのプロファイルは、.NET Framework Version 1.1 と 2.0 で構築されたアプリケーションで利用できます。メモリのプロファイルは、.NET Framework Version 2.0 で構築されたアプリケーションでのみ利用できます。

dotTrace によるパフォーマンスのプロファイルでは、各メソッドが呼び出された回数、それらのメソッドの実行にかかった合計時間、および特定のメソッドが使用する全体的な時間の割合がわかります。このような統計データを調べて、パフォーマンスがあまり高くないアプリケーションの部分を簡単に見つけ出すことができます。さらに簡単な方法としては、パフォーマンスの低い 100 個のアプリケーションの部分を示すホット スポット機能があります。

dotTrace のメモリのプロファイルには 2 つのモードがあります。メモリのダンプを取得して、現在のメモリ内のオブジェクトおよびそれらが占有する領域を表示できます。または、差分モードを使用して、特定の期間のメモリを取得し、新しいオブジェクトと終了したオブジェクトおよびプロファイルの実行中のその 2 つの違いを表示できます。どちらのメモリのプロファイルもメモリ リークの発見には非常に役立ちます。

dotTrace の優れた機能の 1 つに Visual Studio との統合があります。これにより、数回のマウス クリックだけで簡単にソリューションのプロファイルを開始できます。dotTrace を使用すると、次にアプリケーションの全体または一部の高速化が必要になったときに、最小限の作業でパフォーマンスを最大限まで向上させるには何を最適化する必要があるのかを正確に把握できます。

価格 : 499 ドル

www.jetbrains.com/profiler

dotTrace ではさまざまな方法でスナップショットに保存されたプロファイル データを表示できる
dotTrace ではさまざまな方法でスナップショットに保存されたプロファイル データを表示できる (画像を縮小するには、ここをクリックします)
dotTrace ではさまざまな方法でスナップショットに保存されたプロファイル データを表示できる
dotTrace ではさまざまな方法でスナップショットに保存されたプロファイル データを表示できる (画像を拡大するには、ここをクリックします)

継続的な統合環境を実装する

継続的な統合環境の設定および構成に関する情報はインターネット上にたくさんあり、それらに関するさまざまな書籍や記事も散乱しています。ただし、Paul M. Duvall、Steve Matyas、Andrew Glover 共著の『Continuous Integration: Improving Software Quality and Reducing Risk』(2007 年、Addison-Wesley Professional) は、筆者の知る限り、継続的な統合を使用する重要なすべての理由およびソフトウェア プロジェクトで継続的な統合を設定して使用するためのパターンとベスト プラクティスを 1 つに集めた最初の試みです。

この書籍の導入部分には、継続的な統合のメリットを強調するわかりやすいシナリオと、統合を使用しない場合の危険性が示されています。また、自分のチームやマネージャに継続的な統合の導入を促そうとしている担当者にとって有利な状況を作り出すための十分な情報が掲載されています。さらに、最初のセクションには、実際のビルドに関するいくつかのベスト プラクティス (たとえば、専用のビルド サーバーとビルド メトリックスを用意する、など) が説明されています。

次のセクションでは、継続的な統合のベスト プラクティスを示しています。データベースの統合、自動化されたテストの実行とそこからのフィードバックの提供、コードの検査と分析、展開、およびフィードバックの提供に関する章で構成されています。これらの章の大部分はテクノロジに依存しない内容です。各セクションでは固有のツールやコード サンプルが使用されていますが、現在お使いの継続的な統合プラットフォームに関係なく、記載されているアドバイスのほとんどを適用できます。

巻末には役立つ付録があります。1 つ目は、継続的な統合環境を構築するためのツールの一覧です。2 つ目の付録では、継続的な統合環境に組み込むツールを選択するための最適な方法、および継続的な統合の主要なサーバーの詳細なガイドが示されています。

価格 : 44.99 ドル

www.integratebutton.com

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

James Avery は、Infozerk Inc. という会社で独自の .NET コンサルティング業務を行っています。彼の最新の著書は、『Windows Developer Power Tools』(O'Reilly、2006 年) です。連絡先は javery@infozerk.com です。また、www.dotavery.com/blog で彼のブログをご覧いただけます。


CLR 徹底解剖: マネージ コードとアンマネージ コード間でマーシャリングする

CLR 徹底解剖
マネージ コードとアンマネージ コード間でマーシャリングする
Yi Zhang and Xiaoying Guo

コンテンツ
[InAttribute] と [OutAttribute]
キーワード out および ref と、参照による受け渡し
戻り値
StringBuilder とマーシャリング
コピーと固定
メモリの所有権
逆 P/Invoke とデリゲートの有効期間
P/Invoke Interop Assistant
ぜひ試してみてください

事実を認めましょう。完全な世界などありません。すべてをマネージ コードで開発している企業はほとんどありません。マネージ コードの上には、処理する必要のある大量のレガシ アンマネージ コードがあります。マネージ プロジェクトをアンマネージ プロジェクトに統合するには、どうすればよいでしょうか。マネージ アプリケーションからアンマネージ コードを呼び出したり、アンマネージ アプリケーションからマネージ コードを呼び出したりする形式をとるのでしょうか。

さいわい、Microsoft® .NET Framework 相互運用機能はマネージ コードとアンマネージ コードの間にチャネルを開いており、その接続ではマーシャリングが非常に重要な役割を果たします。マーシャリングにより、この 2 つのコード間でデータ交換が可能になるからです (図 1 を参照)。[MarshalAs]、[StructLayout]、[InAttribute]、[OutAttribute] などの属性や、C# の out や ref のような言語キーワードなど、アンマネージ環境とマネージ環境の間で CLR がデータをマーシャリングする方法に影響する要因は多数あります。

図 1 マネージ コードとアンマネージ コードのギャップを埋める
図 1 マネージ コードとアンマネージ コードのギャップを埋める(画像を縮小するには、ここをクリックします)
図 1 マネージ コードとアンマネージ コードのギャップを埋める
図 1 マネージ コードとアンマネージ コードのギャップを埋める (画像を拡大するには、ここをクリックします)

このように多数の要因があるため、マーシャリングを正しく行うのは難しい場合があります。マーシャリングを正しく行うには、アンマネージ コードとマネージ コードの両方で多くの詳細を理解する必要があります。このコラムでは、毎日のマーシャリングの試行で直面する、基本的だが混乱しやすいトピックについて説明します。カスタム マーシャリング、複雑な構造体のマーシャリング、その他の高度なトピックについては説明しませんが、基礎知識を固めれば、このようなマーシャリングに取り組む準備ができます。

[InAttribute] と [OutAttribute]

最初に説明するマーシャリングのトピックでは、System.Runtime.InteropServices 名前空間にある属性タイプ InAttribute および OutAttribute の使用に関する考慮事項を説明します (これらの属性をコードに適用する場合、C# および Visual Basic® では [In] および [Out] という略語形式を使用できますが、混乱を避けるために、ここでは完全な名前を使用します)。

メソッド パラメータと戻り値に適用する場合、これらの属性はマーシャリングの方向を制御するため、方向属性と呼ばれています。[InAttribute] は、呼び出しの開始時に、呼び出し元から呼び出し先にデータをマーシャリングするよう CLR に指示します。[OutAttribute] は、呼び出し先から呼び出し元に戻るときに再びマーシャリングするよう CLR に指示します。呼び出し元と呼び出し先は、アンマネージ コードとマネージ コードのどちらでもかまいません。たとえば、P/Invoke 呼び出しでは、マネージ コードがアンマネージ コードを呼び出しています。一方、逆 P/Invoke では、アンマネージ コードが関数ポインタを通じてマネージ コードを呼び出すことができます。

[InAttribute] と [OutAttribute] は 4 通りの組み合わせで使用できます。それは、[InAttribute] のみ使用、[OutAttribute] のみ使用、[InAttribute, OutAttribute] を使用、そしてどちらも使用しないことです。どちらの属性も指定しない場合は、方向属性を調べるように CLR に指示することになります。通常は、既定で [InAttribute] が適用されます。ただし、StringBuilder クラスの場合は、どちらの属性も指定されていない場合に [InAttribute] と [OutAttribute] の両方が適用されます (詳細については、後述の StringBuilder に関するセクションを参照してください)。また、C# で out キーワードと ref キーワードを使用すると、図 2 に示すように、適用される属性が変更されることがあります。パラメータにキーワードが指定されていない場合、そのパラメータは既定で入力パラメータであることを意味します。

Figure 2 out および ref、関連付けられた属性 (Close Figure 2)

C# キーワード | 属性
(指定なし) | [InAttribute]
out | [OutAttribute]
ref | [InAttribute]、[OutAttribute]

図 3 のコードについて考えます。3 つのネイティブ C++ 関数があり、これらはすべて、arg に対して同じ変更を加えています。また、文字列操作に strcpy を使用しているのは、説明のみ目的としていることに注意してください。実稼働コードでは、msdn.microsoft.com/msdnmag/issues/05/05/SafeCandC にあるこれらの関数の安全なバージョンを代わりに使用する必要があります。

Figure 3 out の方向属性 (Close Figure 3)
MARSHALLIB_API void __stdcall Func_In_Attribute(char *arg)
{
    printf("Inside Func_In_Attribute: arg = %s\n", arg);
    strcpy(arg, "New");
}

MARSHALLIB_API void __stdcall Func_Out_Attribute(char *arg)
{
    printf("Inside Func_Out_Attribute: arg = %s\n", arg);
    strcpy(arg, "New");
}

MARSHALLIB_API void __stdcall Func_InOut_Attribute(char *arg)
{
    printf("Inside Func_InOut_Attribute: arg = %s\n", arg);
    strcpy(arg, "New");
}

異なる点は、次の C# コードに示すように、P/Invoke シグネチャで方向属性を使用してこれらの関数を呼び出す方法だけです。

[DllImport(@"MarshalLib.dll")]
public static extern void Func_In_Attribute([In]char[] arg);
[DllImport(@"MarshalLib.dll")]
public static extern void Func_Out_Attribute([Out]char[] arg);
[DllImport(@"MarshalLib.dll")]
public static extern void Func_InOut_Attribute([In, Out]char[] arg);

これらの関数を P/Invoke によってマネージ コードから呼び出し、"Old" を char 配列として関数に渡した場合は、次の出力が得られます (デモンストレーションの目的で要約してあります)。

Before Func_In_Attribute: arg = Old
Inside Func_In_Attribute: arg = Old
After Func_In_Attribute: arg = Old

Before Func_Out_Attribute: arg = Old
Inside Func_Out_Attribute: arg =
After Func_Out_Attribute: arg = New

Before Func_InOut_Attribute: arg = Old
Inside Func_InOut_Attribute: arg = Old
After Func_InOut_Attribute: arg = New

結果をもっと詳しく見てみましょう。Func_In_Attribute では、元の値が渡されていますが、Func_In_Attribute の内部で行われた変更は戻されていません。Func_Out_Attribute では、元の値が渡されず、Func_Out_Attribute の内部で行われた変更が戻されています。Func_InOut_Attribute では、元の値が渡され、Func_Out_Attribute の内部で行われた変更が戻されています。ただし、わずかな変更を加えると、まったく異なる結果になることがあります。今度は、次に示すように、Unicode を使用するようにネイティブ関数を変更しましょう。

MARSHALLIB_API void __stdcall Func_Out_Attribute_Unicode(wchar_t *arg)
{
    wprintf(L"Inside Func_Out_Attribute_Unicode: arg = %s\n", arg);
    printf("Inside Func_Out_Attribute_Unicode: strcpy(arg, \"New\")\n");
    wcscpy(arg, L"New");
}

ここで、C# 関数を宣言し、[OutAttribute] のみ適用して、CharSet を CharSet.Unicode に変更します。

[DllImport(@"MarshalLib.dll", CharSet=CharSet.Unicode)]
public static extern void Func_Out_Attribute_Unicode([Out]char[] arg);

その出力を次に示します。

Before Func_Out_Attribute_Unicode: arg = Old
Inside Func_Out_Attribute_Unicode: arg = Old
After Func_Out_Attribute_Unicode: arg = New

興味深いことに、[InAttribute] がない場合でも元の値が渡されています。[DllImportAttribute] は、Unicode をマーシャリングするよう CLR に指示します。CLR 内の char 型も Unicode であるため、CLR には、char 配列を固定し、char のアドレスを直接渡すことによって、マーシャリング プロセスを最適化する機会があります (コピーと固定の詳細な説明は後述します)。ただし、この動作に依存する必要があるというわけではありません。その代わり、CLR の既定のマーシャリング動作に依存しない場合は、方向属性の正しいマーシャリングを常に使用する必要があります。この既定の動作の典型的な例は、int 引数の場合です。[InAttribute] int arg を指定する必要はありません。

[OutAttribute] は無視される場合があります。たとえば、[OutAttribute]int には意味がないため、[OutAttribute] は CLR によって無視されます。同じことが [OutAttribute] string にも当てはまります。文字列は不変だからです。

インターフェイス定義 (IDL) ファイルにも、[in] 属性と [out] 属性があります。これは、CLR の [InAttribute] および [OutAttribute] と同じであると考えることができます。

キーワード out および ref と、参照による受け渡し

前に、C# の out キーワードと ref キーワードは、[InAttribute] および [OutAttribute] に直接マップできることを示しました。実際に、out と ref は、CLR がマーシャリングするデータ型を変更することもできます。データを out または ref として渡すことは、参照による受け渡しと同じです。ILDASM を使用して、中間言語 (IL) の対応する関数シグネチャを調べると、型の横にアンパサンド文字 (&) があることがわかります。これは、引数を参照によって渡す必要があることを意味します。参照によって渡す場合、間接指定のレベルが CLR によって追加されます。図 4 に例をいくつか示します。

Figure 4 マーシャリングの結果 (Close Figure 4)

C# シグネチャ | アンマネージ シグネチャ | MSIL シグネチャ | CLR で参照される実際の MSIL シグネチャ
基本型 |  |  |
int arg | int arg | int | [in] int
out int arg | int *arg | [out] int & | [out] int &
ref int arg | int *arg | int & | [in, out] int &
構造体 |  |  |
MyStruct arg | MyStruct arg | MyStruct | [in] MyStruct
out MyStruct arg | MyStruct *arg | [out] MyStruct & | [out] MyStruct &
ref MyStruct arg | MyStruct *arg | MyStruct & | [in, out] MyStruct &
文字列 |  |  |
string arg | char *arg | string | [in] string
out string arg | char **arg | [out] string & | [out] string &
ref string arg | char **arg | string & | [in, out] string &
クラス |  |  |
MyClass arg | MyClass *arg | MyClass | [in] MyClass
out MyClass arg | MyClass **arg | [out] MyClass & | [out] MyClass &
ref MyClass arg | MyClass **arg | MyClass & | [in, out] Myclass &

図 5 の表に、out と ref について説明した内容をまとめてみましょう。

Figure 5 既定の属性 (Close Figure 5)

C# シグネチャ | MSIL シグネチャ | 既定の方向属性
<type> | type | [InAttribute]
out <type> | [OutAttribute] type & | [OutAttribute]
ref <type> | type & | [InAttribute, OutAttribute]

参照によって渡す場合、方向属性が指定されていないと、CLR は [InAttribute] と [OutAttribute] を自動的に適用します。なぜなら、図 4 の Microsoft Intermediate Language (MSIL) シグネチャには "string &" しかないからです。これらのいずれかの属性が指定されている場合、CLR は、次の例で示すように、既定の動作の代わりにそれらの属性に従います。

public static extern void
      PassPointerToComplexStructure(
        [In]ref ComplexStructure
        pStructure);

上記のシグネチャによって、ref の既定の方向動作が上書きされ、[InAttribute] のみになります。このケースでは、P/Invoke を行う場合、ComplexStructure へのポインタ (値型) が CLR 側からネイティブ側に渡されますが、呼び出し先は、どの変更も pStructure ポインタで指された ComplexStructure から見えるようにできません。図 6 に、方向属性とキーワードの組み合わせのその他の例を示します。

Figure 6 その他の属性とキーワード (Close Figure 6)

C# シグネチャ | アンマネージ IDL シグネチャ | MSIL シグネチャ
Out |  |
[InAttribute] out int arg | コンパイラ エラー CS0036。out パラメータに in 属性を指定することはできません。 | N/A
[OutAttribute] out int arg | [out] int *arg | [out] int &
[InAttribute, OutAttribute] out int arg | コンパイラ エラー CS0036。out パラメータに in 属性を指定することはできません。 | N/A
Ref |  |
[InAttribute] ref int arg | [in] int *arg | [in] int &
[OutAttribute] ref int arg | コンパイラ エラー CS0662 ref パラメータの Out 属性だけを指定することはできません。In と Out 属性の両方を使用するか、どちらも使用しないでください。 | N/A
[InAttribute, OutAttribute] ref int arg | [in, out] int *arg | [in] [out] int &

戻り値

これまでは、引数についてのみ説明してきました。関数から返される値についてはどうでしょうか。CLR は、[OutAttribute] を使用して、通常の引数と同じように戻り値を自動的に処理します。また、CLR は、PreserveSigAttribute によって制御されるプロセスである関数シグネチャに対する変換を実行できます。P/Invoke シグネチャへの適用時に [PreserveSigAttribute] が false に設定されている場合、CLR は HRESULT 戻り値をマネージ例外にマップし、[out, retval] パラメータを関数の戻り値にマップします。そのため、次のマネージ関数シグネチャは、

public static string extern GetString(int id);

アンマネージ シグネチャになります。

HRESULT GetString([in]int id, [out, retval] char **pszString);

[PreserveSigAttribute] が true (P/Invoke の既定) に設定されている場合、変換は行われません。COM 関数では、通常 [PreserveSigAttribute] は既定で false に設定されますが、これを変更する方法がいくつかあります。詳細については、TlbExp.exe および TlbImp.exe に関する MSDN® のドキュメントを参照してください。

StringBuilder とマーシャリング

CLR マーシャラには、StringBuilder 型の認識が組み込まれており、他の型とは異なる方法でこの型を処理します。既定では、StringBuilder は [InAttribute, OutAttribute] として渡されます。StringBuilder は、実行時に必要なバッファのサイズを決定できる Capacity プロパティがあるため特殊であり、動的に変更できます。したがって、マーシャリング プロセス中に、CLR は StringBuilder を固定すること、StringBuilder で使用される内部バッファのアドレスを直接渡すこと、このバッファのコンテンツをネイティブ コードで変更可能にすることができます。

StringBuilder を最大限に利用するには、次のすべてのルールに従う必要があります。

  1. StringBuilder を参照によって (out または ref を使用して) 渡さないでください。参照で渡すと、CLR はこの引数のシグネチャが wchar_t * ではなく wchar_t ** であると予想し、StringBuilder の内部バッファを固定できません。パフォーマンスが大幅に低下します。
  2. アンマネージ コードで Unicode を使用している場合は、StringBuilder を使用します。そうしないと、CLR は文字列のコピーを作成し、Unicode と ANSI 間で変換する必要があるため、パフォーマンスが低下します。通常は、StringBuilder を Unicode 文字の LPARRAY または LPWSTR としてマーシャリングする必要があります。
  3. StringBuilder の容量は必ず事前に指定し、バッファを保持するのに十分な容量があることを確認します。アンマネージ コード側のベスト プラクティスは、文字列バッファのサイズを引数として受け入れ、バッファ オーバーランを回避することです。COM では、IDL の size_is を使用してサイズを指定することもできます。

コピーと固定

CLR がデータ マーシャリングを実行する場合、2 つのオプションがあります。それは、コピーと固定です (msdn2.microsoft.com/23acw07k を参照)。

既定では、CLR はマーシャリング中に使用されるコピーを作成します。たとえば、マネージ コードが文字列を ANSI C-String としてアンマネージ コードに渡している場合、CLR は文字列のコピーを作成して ANSI に変換し、一時的なポインタをアンマネージ コードに渡します。そのコピーはかなり低速な場合があり、パフォーマンスの問題が発生する可能性があります。

特定のケースでは、CLR はマネージ オブジェクトをガベージ コレクタ (GC) ヒープに直接固定して、呼び出し中に再配置できないようにすることで、マーシャリング プロセスを最適化できます。マネージ オブジェクト (またはマネージ オブジェクト内の場所) へのポインタが、アンマネージ コードに直接渡されます。

固定は、次のすべての条件が満たされている場合に実行されます。まず、ネイティブ コードがマネージ コードを呼び出すのではなく、マネージ コードがネイティブ コードを呼び出している必要があります。第 2 に、型が blittable であるか、特定の条件で blittable になれる必要があります。第 3 に、参照 (out または ref) によって渡されていない必要があります。第 4 に、呼び出し元と呼び出し先は同じスレッド コンテキストまたはスレッド アパートメントに存在している必要があります。

2 つ目のルールは、詳細な説明が必要です。blittable 型は、マネージ メモリとアンマネージ メモリの両方に共通の表現を持つ型です。そのため、blittable 型には、マーシャリング時の変換が不要です。blittable ではないが blittable になることのできる型の代表的な例は、char 型です。既定では、Unicode または ANSI にマッピングできるため、blittable ではありません。ただし、char は CLR では常に Unicode であるため、[DllImportAttribute(CharSet= Unicode)] または [MarshalAsAttribute(UnmanagedType.LPWSTR)] を指定すると blittable になります。次の例では、arg を PassUnicodeString に固定できますが、PassAnsiString に固定することはできません。

[DllImport(@"MarshalLib.dll", CharSet = CharSet.Unicode)]
public static extern string PassUnicodeString(string arg);

[DllImport(@"MarshalLib.dll", CharSet = CharSet.Ansi)]
public static extern string PassAnsiString(string arg);

メモリの所有権

関数呼び出し中に、関数は引数に対して 2 種類の変更を行います。それは、参照変更とインプレース変更です。参照変更には、ポインタが指す変更が関係します。ポインタが割り当て済みメモリの一部を既に指している場合は、メモリを指しているポインタが失われる前に、そのメモリを解放することが必要な場合があります。インプレース変更には、参照によって指されている場所のメモリの変更が関係します。

これらの変更のどちらを行うかは、引数の型と、最も重要である呼び出し先と呼び出し元のコントラクトによって決まります。ただし、CLR はコントラクトを自動的に調べることはできないため、図 7 に示すように、型に関する共通の知識に依存する必要があります。

Figure 7 CLR 型の認識 (Close Figure 7)

IDL シグネチャ | 変更の種類
[In] Type | 変更不可
[In] Type * | 変更不可
[Out] Type * | インプレース変更
[In, Out] Type * | インプレース変更
[In] Type ** | 変更不可
[Out] Type ** | 参照変更
[In, Out] Type ** | 参照変更またはインプレース変更

前に説明したように、参照によって受け渡すときに 2 レベルの間接指定があるのは参照型だけであるため (ただし、"[MarshalAs(UnmanagedType.LPStruct)]ref Guid" のようにいくつかの例外があります)、図 8 に示すように、参照型へのポインタまたは参照のみ変更できます。

Figure 8 型の変更ルール (Close Figure 8)

C# シグネチャ | 変更の種類
int arg | 変更不可
out int arg | インプレース変更
ref int arg | インプレース変更
string arg | 変更不可
out string arg | 参照変更
ref string arg | 参照変更またはインプレース変更
[InAttribute, OutAttribute] StringBuilder arg | インプレース変更
[OutAttribute] StringBuilder arg | インプレース変更

インプレース変更のメモリ所有権について心配する必要はありません。呼び出し元が呼び出し先のメモリを割り当て、呼び出し元がメモリを所有するからです。例として、"[OutAttribute] StringBuilder" を見てみましょう。参照によって渡していないため、対応するネイティブ型は char * (ANSI を想定) です。データは in ではなく out にマーシャリングされます。メモリは、呼び出し元 (この場合は CLR) によって割り当てられます。メモリのサイズは、StringBuilder オブジェクトの容量によって決まります。呼び出し先は、メモリについて考慮する必要はありません。

文字列を変更するために、呼び出し先は、メモリ自体を直接変更します。ただし、参照変更を行う場合は、メモリの所有者を認識することが重要です。そうしないと、予期しない結果が発生することがあります。所有権の問題については、CLR は次の COM スタイルの規則に従います。

  • [in] として渡されたメモリは呼び出し元が所有し、呼び出し元が割り当て、解放する必要があります。呼び出し先は、そのメモリを解放したり変更したりしようとしてはなりません。
  • 呼び出し先によって割り当てられ、[out] として渡されるか返されるメモリは、呼び出し元が所有し、呼び出し元が解放する必要があります。
  • 呼び出し先は、呼び出し元から [in, out] として渡されたメモリを解放し、新しいメモリを割り当て、古いポインタ値を上書きして out として渡すことができます。新しいメモリは呼び出し元が所有します。これには、char ** などの 2 レベルの間接指定が必要です。

相互運用環境では、呼び出し元/呼び出し先が CLR/ネイティブ コードになります。ピン解除の場合、上記のルールは、ネイティブ コードで CLR から [out] として渡されたメモリのブロックへのポインタを受け取る場合に、そのメモリを解放する必要があることを意味します。一方、CLR がネイティブ コードから [out] として渡されたポインタを受け取る場合は、CLR がそのポインタを解放する必要があります。明らかに、最初のケースではネイティブ コードが割り当て解除を行う必要があり、2 つ目のケースではマネージ コードが割り当て解除を行う必要があります。

これにはメモリの割り当てと割り当て解除が関係するため、最大の問題はどの関数を使用するかということです。HeapAlloc/HeapFree、malloc/free、new/delete など、選択肢は多数あります。ただし、CLR は BSTR 以外の場合に CoTaskMemAlloc/CoTaskMemFree を使用し、BSTR の場合に SysStringAlloc/SysStringAllocByteLen/SysStringFree を使用するため、これらの関数を使用する必要があります。そうしないと、Windows® の特定のバージョンでは、メモリ リークやクラッシュが発生します。malloc されたメモリが CLR に渡され、Windows XP ではプログラムがクラッシュせず、Windows Vista® ではクラッシュしたケースがありました。

これらの関数の他に、CoGetMalloc から返された、システムで実装されている IMalloc インターフェイスも、内部的には同じヒープを使用しているため正常に動作します。ただし、CoGetMalloc は将来変更される可能性があるため、常に CoTaskMemAlloc/CoTaskMemFree と SysStringAlloc/ SysStringAllocByteLen/SysStringFree を使用するのが最善です。

それでは例を見てみましょう。GetAnsiStringFromNativeCode は char ** 引数を [in, out] として受け取り、char * を [out, retval] として返します。char ** 引数の場合は、CoTaskMemFree を呼び出して CLR によって割り当てられたメモリを解放し、CoTaskMemAlloc を使用して新しいメモリを割り当て、ポインタを新しいメモリ ポインタで上書きすることを選択できます。後で、CLR はメモリを解放し、マネージ文字列用のコピーを作成します。戻り値については、CoTaskMemAlloc を使用してメモリの新しい部分を割り当て、呼び出し元に返すことのみ必要です。戻った後で、新規に割り当てたメモリは、CLR によって所有されています。CLR は、最初に新しいマネージ文字列をそのメモリから作成し、CoTaskMemFree を呼び出して解放します。

最初のオプションについて考えてみましょう (図 9 を参照)。対応する C# 関数宣言は次のとおりです。

Figure 9 ポインタを使用する (Close Figure 9)
MARSHALLIB_API char *__stdcall GetAnsiStringFromNativeCode(char **arg)
{
    char *szRet = (char *)::CoTaskMemAlloc(255);
    strcpy(szRet, "Returned String From Native Code");

    printf("Inside GetAnsiStringFromNativeCode: *arg = %s\n", *arg);
    printf("Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg);
        *arg = CoTaskMemAlloc(100); strcpy(*arg, \"Changed\")\n");

    ::CoTaskMemFree(*arg);
    *arg = (char *)::CoTaskMemAlloc(100);
    strcpy(*arg, "Changed");

    return szRet;
}

class Lib
{
    [DllImport(@"MarshalLib.dll", CharSet= CharSet.Ansi)]
    public static extern string GetAnsiStringFromNativeCode(
        ref string inOutString);
}

次の C# コードで GetAnsiStringFromNativeCode の呼び出しを行います。

string argStr = "Before";
Console.WriteLine("Before GetAnsiStringFromNativeCode : argStr = \"" +
    argStr + "\"");
string retStr = Lib.GetAnsiStringFromNativeCode(ref argStr);
Console.WriteLine("AnsiStringFromNativeCode() returns \"" + retStr +
    "\"" );
Console.WriteLine("After GetAnsiStringFromNativeCode : argStr = \"" +
    argStr + "\"");

この場合、出力は次のようになります。

Before GetAnsiStringFromNativeCode : argStr = "Before"
Inside GetAnsiStringFromNativeCode: *arg = Before
Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg); *arg = CoTaskMemAlloc(100); strcpy(*arg, "Changed")
AnsiStringFromNativeCode() returns "Returned String From Native Code"
After GetAnsiStringFromNativeCode : argStr = "Changed"

呼び出そうとしているネイティブ関数がこの規則に従っていない場合は、自身でマーシャリングを行って、メモリの破損を回避する必要があります。アンマネージ関数に対する関数は必要なものを何でも返すことができるため、メモリの破損は簡単に発生します。毎回メモリの同じ部分を返したり、malloc/new で割り当てられたメモリの新しいブロックを返したりすることがあります。これもコントラクトによって決まります。

メモリ割り当ての他に、渡されるメモリのサイズも非常に重要です。StringBuilder のケースで説明したように、CLR が結果を十分に保持できる大きさのメモリ断片を割り当てられるように Capacity プロパティを変更することが非常に重要です。また、文字列を [InAttribute, OutAttribute] として (out または ref、およびその他の属性を使用せずに) マーシャリングすることは、文字列が十分に大きいかどうかがわからないためお勧めしません。MarshalAsAttribute の SizeParamIndex フィールドと SizeConst フィールドを使用して、バッファのサイズを指定できます。ただし、これらの属性は、参照によって渡す場合には使用できません。

逆 P/Invoke とデリゲートの有効期間

CLR では、デリゲートをアンマネージ関数ポインタとして呼び出せるように、デリゲートをアンマネージ環境に渡すことができます。実際に行われるのは、CLR がサンクを作成することです。このサンクは、ネイティブ コードから実際のデリゲートに呼び出しを転送し、さらに実際の関数に転送します (図 10 を参照)。

図 10 サンクを使用する
図 10 サンクを使用する(画像を縮小するには、ここをクリックします)
図 10 サンクを使用する
図 10 サンクを使用する (画像を拡大するには、ここをクリックします)

通常は、デリゲートの有効期間について心配する必要はありません。デリゲートをアンマネージ コードに渡すたびに、CLR によって、デリゲートが呼び出し時に有効であることが確認されます。

ただし、ネイティブ コードが呼び出しの範囲を越えてポインタのコピーを保持し、後でそのポインタを通じてコールバックすることを意図している場合は、GCHandle を使用して、ガベージ コレクタがデリゲートから収集するのを明示的に防ぐことが必要な場合があります。固定された GCHandle は、プログラムのパフォーマンスに大きな悪影響を与える可能性があることに注意する必要があります。さいわい、この場合は、サンクがアンマネージ ヒープで割り当てられ、GC が認識している参照を通じて間接的にデリゲートを参照しているため、固定された GC ハンドルを割り当てる必要はありません。したがって、サンクが移動する可能性はなく、デリゲート自体が有効な場合、ネイティブ コードは常にアンマネージ ポインタを通じてデリゲートを呼び出すことができます。

Marshal.GetFunctionPointerForDelegate は、デリゲートを関数ポインタに変換できますが、デリゲートの有効期間を保証するための処理は何も行いません。次の関数宣言について考えます。

public delegate void PrintInteger(int n);

[DllImport(@"MarshalLib.dll", EntryPoint="CallDelegate")]
public static extern void CallDelegateDirectly(
    IntPtr printIntegerProc);

Marshal.GetFunctionPointerForDelegate を呼び出し、返された IntPtr を格納し、呼び出そうとしている関数に IntPtr を渡す場合は、次のようになります。

IntPtr printIntegerCallback = Marshal.GetFunctionPointerForDelegate(
    new Lib.PrintInteger(MyPrintInteger));

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

CallDelegateDirectly(printIntegerCallback);

CallDelegateDirectly を呼び出す前にデリゲートが収集され、CallbackOnCollectedDelegate が検出されたという MDA エラーが返される可能性があります。このエラーを解決するために、メモリにデリゲートへの参照を格納するか、GC ハンドルを割り当てることができます。

ネイティブ コードがアンマネージ関数ポインタを CLR に返す場合、実際の関数コードの保持はネイティブ コードが行う必要があります。通常は、コードが動的に読み込まれた DLL 内にあるか、その場で生成されるのでない限り、問題にはなりません。

P/Invoke Interop Assistant

これまでに説明したすべての属性とルールを理解し、記憶することは、少々困難に思えます。結局のところ、マネージ コードの開発者のほとんどは、Win32® API 関数の P/Invoke シグネチャを迅速に調べて、コードに貼り付け、処理できることのみが必要です。ここで、P/Invoke Interop Assistant (MSDN マガジンの Web サイトで入手できます) が役立ちます。このツールは、C++ からマネージ P/Invoke シグネチャへの変換と、その逆方向の変換を効率的に支援できます。Win32 関数、データ型、および定数のデータベースも付属しているため、Win32 P/Invoke を C# または Visual Basic のソース ファイルに追加する共通タスクは非常に簡単になります。ツール パッケージには、SigImp および SigExp という 2 つのコマンド ライン ツールが含まれています。これらのツールは、バッチ ファイル処理に使用できます。パッケージには、両方のツールの機能を含む GUI ツールもあります。

GUI ツールは、単純な変換を行うのに便利です。SigExp、SigImp Search、SigImp Translate Snippet という 3 つのタブがあります。

SigExp は、マネージ シグネチャをアンマネージ シグネチャに変換します。これは、マネージ アセンブリを反映して、すべての P/Invoke 宣言と COM でインポートされた型を検索します。この入力から、対応するネイティブ C シグネチャが生成されます (図 11 を参照)。

図 11 P/Invoke Interop Assistant GUI ツール - SigExp
図 11 P/Invoke Interop Assistant GUI ツール - SigExp(画像を縮小するには、ここをクリックします)
図 11 P/Invoke Interop Assistant GUI ツール - SigExp
図 11 P/Invoke Interop Assistant GUI ツール - SigExp (画像を拡大するには、ここをクリックします)

SigImp Search と SigImp Translate Snippet は、アンマネージ シグネチャをマネージ シグネチャに変換します。これらは、手動で入力されたネイティブ関数シグネチャのネイティブ型、関数、定数、およびスニペットからマネージ シグネチャと定義を C# または Visual Basic で生成します。

SigImp Search では、ユーザーはコードを生成するマネージ コード言語を選択し、生成を実行するネイティブ型、プロシージャ、または定数を選択できます。ツールでは、Windows SDK ヘッダー ファイルから収集されたサポートされる型、メソッド、および定数の一覧が表示されます (図 12 を参照)。

図 12 P/Invoke Interop Assistant GUI ツール - SigImp Search
図 12 P/Invoke Interop Assistant GUI ツール - SigImp Search(画像を縮小するには、ここをクリックします)
図 12 P/Invoke Interop Assistant GUI ツール - SigImp Search
図 12 P/Invoke Interop Assistant GUI ツール - SigImp Search (画像を拡大するには、ここをクリックします)

SigImp Translate Snippet では、ユーザーはツールで独自のネイティブ コード スニペットを作成できます。ツールでは、図 13 に示すように、同等のマネージ コードが生成され、メイン ウィンドウに表示されます。

図 13 P/Invoke Interop Assistant GUI ツール - SigImp Translate Snippet
図 13 P/Invoke Interop Assistant GUI ツール - SigImp Translate Snippet(画像を縮小するには、ここをクリックします)
図 13 P/Invoke Interop Assistant GUI ツール - SigImp Translate Snippet
図 13 P/Invoke Interop Assistant GUI ツール - SigImp Translate Snippet (画像を拡大するには、ここをクリックします)

P/Invoke Interop Assistant の GUI ツールまたはコマンド ライン ツールの詳細については、ツールに付属のドキュメントを参照してください。

ぜひ試してみてください

ご存知のように、マーシャリングは複雑なトピックであり、ニーズに合わせてマーシャリング プロセスを変更するために使用できる手法は数多くあります。ここで紹介したアイデアをいくつか試してみることをお勧めします。マーシャリングという迷路で進む道を見つけるのにきっと役立ちます。

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

Yi Zhang は、中国のサーバーおよびツール ビジネスの一部である Silverlight Shanghai チームのソフトウェア開発エンジニアです。現在は、相互運用機能と CLR に取り組んでいます。

Xiaoying Guo は、中国のサーバーおよびツール ビジネスの一部である Silverlight Shanghai チームのプログラム マネージャです。現在、CLR 内のアンマネージ/マネージ相互運用機能の分野、および Mac 上の CoreCLR に取り組んでいます。Xiaoying は、Silverlight の地元顧客との連携にも従事しています。


データ ポイント: モバイル アプリケーションからデータにアクセスする

データ ポイント
モバイル アプリケーションからデータにアクセスする
John Papa

コードのダウンロード : : DataPoints2008_01.exe (179KB)
コンテンツ
モバイル開発
はじめに
自動バインド
フィールドをバインドする
モバイルのバインド オプション
モバイル アプリケーションの微調整
最後に

データ アクセスは、Windows Mobile® デバイス用のアプリケーションを Microsoft® .NET Compact Framework で開発する際の重要な一面です。既存のアーキテクチャを使用してモバイル アプリケーションとアプリケーション サーバーの間でデータを送受信することにより、DataSet、カスタム オブジェクト、またはスカラ値でデータを受け渡すことができます。

今回のデータ ポイントでは、Smartphone 用のモバイル アプリケーションを開発するときの、データ アクセス戦略に関するベスト プラクティスについて検討します。インターネット経由でデータを表示および保存できるデータ駆動型の Smartphone アプリケーションを設計する方法を紹介します。DataSet、カスタム オブジェクト、およびエンティティのジェネリック List<T> を使用してデータを取得するためのメソッドを公開するサーバー アプリケーションと通信する方法を示します。

モバイル開発
モバイル開発のための基本ツール
  • .NET Compact Framework 2.0 SP2
  • Windows Mobile 6 Professional および Standard SDK
  • Windows Mobile 5.0 SDK for Smartphone
  • Windows Mobile 5.0 SDK for Pocket PC
  • ActiveSync 4.5 (Windows XP)
  • Windows Mobile デバイス センター (Windows Vista)
  • Microsoft Device Emulator 2.0
  • Windows Mobile 6 Emulators
  • Windows Mobile 5.0 Smartphone Emulators
  • Windows Mobile 5.0 Smartphone Emulator 横長画面対応
  • Windows Mobile 5.0 Pocket PC Emulators
  • Motorola Q Emulators

モバイル アプリケーションを開発するには、最初に .NET Compact Framework (およびその最新の Service Pack) をインストールする必要があります。最新の更新は、.NET Compact Framework 2.0 SP2 です。取りかかる前に、アプリケーションの対象となるプラットフォーム (Windows Mobile 5.0 または Windows Mobile 6) とデバイスについて、いくつかの事柄を決定する必要があります。これらの決定は、インストールする必要のある SDK に影響します。

Smartphone、Pocket PC、または Pocket PC Phone のいずれが対象かにより、使用できる Windows Mobile 5.0 SDK のバージョンが異なります。Windows Mobile 6 SDK も対象のプラットフォームによって異なるバージョンを使用できますが、名前付け規則が変更されており、若干わかりにくくなっています。図 1 は、SDK のさまざまなバージョンと、対象となるデバイスの一覧です。当然のことですが、各デバイス種類を対象にする場合は、各 SDK をインストールする必要があります。

Figure 1 Windows Mobile SDK (Close Figure 1)

対象デバイス | 必要な Windows Mobile 5.0 SDK | 必要な Windows Mobile 6 SDK
Smartphone | Windows Mobile 5.0 SDK for Smartphone | Windows Mobile 6 Standard SDK
Pocket PC | Windows Mobile 5.0 SDK for Pocket PC | Windows Mobile 6 Professional SDK
Pocket PC Phone | Windows Mobile 5.0 SDK for Pocket PC | Windows Mobile 6 Professional SDK

ここでは、Smartphone 用の Windows Mobile 5 を使用します。ただし、バージョンに関係なくデータ アクセスの概念は同じなので、バージョンは重要ではありません。

.NET Compact Framework 2.0 SP2 および適切な Windows Mobile SDK をインストールした後、ActiveSync (Windows® XP 用) または Windows Mobile デバイス センター (Windows Vista® 用) もインストールする必要があります。これらは、モバイル デバイスのエミュレータがネットワークまたはインターネットと通信するために必要です。

さらに、対象のデバイスとよく似たエミュレータもダウンロードしてインストールする必要があります。このコラムでは、320×240 の横長の画面を使用します。対象デバイスの画面サイズが異なる場合は、適切なエミュレータをすべて使用して、デバイス サイズごとにアプリケーションをテストする必要があります。

Windows Vista または Windows Mobile 6 SDK を使用する場合は、Device Emulator 2.0 をインストールする必要があります。多くの前提条件があるように思われるでしょうから、このコラムのダウンロードおよび MSDN Magazine Webサイト (msdn.microsoft.com/msdnmag/issues/08/01/datapoints) に、リンクの一覧を付けておきます。

はじめに

アプリケーションがエミュレータでは動作してもデバイスでは動作しないことがないように、エミュレータとデバイス自体の両方でテストすることを強くお勧めします。私の方法は、モバイル アプリケーションの開発プロセス全体を通してエミュレータを使用し、特定のチェックポイントでは実際のモバイル デバイスに展開するというものです。

デバッグを行う前に、Visual Studio® の [ツール] メニューからデバイス エミュレータを実行してください。使用するエミュレータを選択したら、それを右クリックして [接続] をクリックすると、エミュレータによって Smartphone が起動します。Smartphone がエミュレータで実行されたら、デバイス エミュレータ マネージャのアイテムをもう一度右クリックして、[クレードルに接続] をクリックします。クレードルを使用することで、エミュレータはネットワークと通信できます。最後に、Visual Studio で、[デバイス] ツール バーのドロップダウン リストからエミュレータを選択します (図 2 を参照)。

図 2 ターゲット エミュレータを選択する
図 2 ターゲット エミュレータを選択する

データを取得するため、このサンプル アプリケーションは IIS Web サーバー上の Web サービスと通信します。この Web サービスは、DataSet などの ADO.NET オブジェクトとカスタム エンティティの両方を渡すことができるため、モバイル アプリケーションでも問題なく動作します。最初に、Web サービス プロジェクトを作成し、CustomerWebService という名前の WebService を追加します。次の 3 つのメソッドを WebService に追加し、それぞれを WebMethod 属性で修飾します。

[WebMethod]
public Customer GetCustomer(string customerID) {
  ...
}

[WebMethod]
public DataSet GetCustomerDataSet(string customerID) {
  ...
}

[WebMethod]
public List<Customer> FindCustomerList() {
  ...
}

GetCustomer メソッドは、1 人の顧客のレコードをカスタム エンティティの形式で取得します。GetCustomerDataSet メソッドも 1 人の顧客のレコードを取得しますが、このメソッドは DataSet で顧客を返します。FindCustomerList メソッドは顧客の一覧を取得して List<Customer> を返すので、顧客エンティティとジェネリックを利用します。この Web サービスの完全なコードは、ダウンロード可能なコードに含まれています。

Web サービスを作成したら、Windows Mobile 5.0 Smartphone アプリケーションをソリューションに追加します。Windows Mobile 5.0 と Windows Mobile 6 の両方の SDK をインストールしている場合は、このダイアログから対象のアプリケーションを選択できます。このプロジェクトでは、Windows Mobile 5.0 の Smartphone デバイス アプリケーションを作成します。

この Smartphone アプリケーションには、ドロップダウン リストで顧客の一覧を表示するスタートアップ フォームを用意します。ユーザーは、1 人の顧客を選択し、別のページで顧客の詳細を見ることができます。顧客の一覧は Web サービスの FindCustomerList Web メソッドから取得します。このWeb メソッドは、List<Customer> をフォームに返します。一覧は、手動または自動でドロップダウン リストにバインドできます (後で、両方の手法を示します)。

自動バインド

ユーザーが最初に目にする画面は CustomerPicker フォームです。このフォームにドロップダウン リストを追加し、ddlCustomer という名前を付けます。自動バインドを利用して、FindCustomerList Web メソッドから取得する List<Customer> にこのドロップダウン リストをバインドします。新しいデータ ソースをプロジェクトに追加し、ドロップダウン リストをバインドするエンティティの種類を選択します。自分で作成した Web サービスを参照するので、アプリケーションでは WebMethod が返すエンティティの種類を認識しています。このため、ドロップダウン リストを厳密に型指定されたエンティティと簡単にバインドできます。

[データ ソース] ウィンドウに移動し、[新しいデータ ソースの追加] をクリックします。ウィザードがポップアップします。オブジェクト型の追加を選択し、[次へ] をクリックして、表示される一覧から Customer エンティティを探します (図 3 を参照)。新しいデータ ソースを追加しても一覧に目的のエンティティが表示されない場合は、最初に Web サービス プロジェクトのコンパイルが成功していることを確認した後、データ ソースの追加に戻ってもう一度試してください。Web サービスがエンティティを公開している場合、先に DLL をコンパイルしておかないと、エンティティは一覧に表示されません。

図 3 Customer データ ソースを作成する
図 3 Customer データ ソースを作成する(画像を縮小するには、ここをクリックします)
図 3 Customer データ ソースを作成する
図 3 Customer データ ソースを作成する (画像を拡大するには、ここをクリックします)

データ ソースを追加したら、新しい Customer データ ソース (図 4 を参照) を [データ ソース] ウィンドウからドロップダウン リストにドラッグします。フォームに customerBindingSource という名前の BindingSource コントロールが自動的に作成されます。このコントロールは、Customer エンティティとコントロールの間の双方向のバインドを処理します。

図 4 Customer データ ソース&#xA;を使用する
図 4 Customer データ ソース&#xA;を使用する

エミュレータでアプリケーションをテストできる状態にするには、自動バインドの設定作業があと 2 つあります。次に行う作業は、ドロップダウン リストの DisplayMember プロパティと ValueMember プロパティの設定です。DisplayMember プロパティには CompanyName を設定し、ValueMember プロパティには CustomerID を設定します。データ ソースは厳密に型指定されているので、これらの値は、ドロップダウン リスト コントロールのプロパティ ウィンドウのドロップダウン リストにあります。

最後の作業は、BindingSource コントロールにデータの取得場所を指定することです。この手順は簡単で、Web サービス プロキシ クラスのインスタンスを作成し、適切なメソッドを呼び出して、メソッドが返す List<Customer> を BindingSource コントロールの DataSource プロパティに設定するだけです。

private void CustomerPicker_Load(object sender, EventArgs e)
{
    using (RemoteService.CustomerWebService svc =
                        new RemoteService.CustomerWebService())
        customerBindingSource.DataSource = svc.FindCustomerList();
}

これで、アプリケーションを実行してテストし、顧客の一覧がドロップダウン リストに表示されるかどうかを確認できます。Visual Studio で対象デバイスを設定してあること、ActiveSync® または Windows Mobile デバイス センター (該当するもの) を実行していること、およびエミュレートしているデバイスがクレードルに接続されていること (デバイス エミュレータ マネージャを使用) を確認します。

フィールドをバインドする

CustomerPicker フォームを作成してテストしたら、次に CustomerDetail という名前の別のフォームを作成します。このフォームには、選択した顧客が表示されます。Customer データ ソースをフォームにドラッグすると詳細情報のコントロールが作成されるようにデータ ソースの設定を変更した後、[データ ソース] ウィンドウから表示するプロパティをカスタマイズします。その後、コントロールを CustomerDetail フォームにドラッグします。図 5 はカスタマイズした後のデータ ソースです。

図 5 カスタマイズしたデータ ソース
図 5 カスタマイズしたデータ ソース

次に、Web サービスの GetCustomer Web メソッドから選択した Customer レコードを取得する必要があります。前のフォームで選択された CustomerID を使用して、GetCustomer Web メソッドを呼び出します。返される Customer エンティティを、CustomerDetail フォームの customerBindingSource コントロールのデータ ソースに設定します。再びエミュレータでアプリケーションをテストすると、図 6 のような画面が表示されます。

図 6 エミュレータで実行中の&#xA;アプリケーション
図 6 エミュレータで実行中の&#xA;アプリケーション

モバイルのバインド オプション

BindingSource コントロールを使用してモバイル デバイス アプリケーションに自動バインドを実装する方法はとても簡単です。BindingSource コントロールは Windows フォーム アプリケーションの場合と同じように動作しますが、重要な違いは BindingNavigator コントロールがないことです。モバイル アプリケーションには BindingNavigator コントロールがないので、あるレコードから次のレコードに移動する機能が必要な場合は、そのためのコードを作成する必要があります。この種の機能は項目の大きいリストを取得するときに使用するのが普通なので、私は使わないようにしています。通常、モバイル デバイスの帯域幅は限られているので、大きいリストを取得するとモバイル アプリケーションのパフォーマンスに影響があります。ですから、帯域幅を節約する必要があることを思い出させてくれるので、BindingNavigator がないことは悪いことではありません。

BindingSource は、エンティティから値を取得してフォームのコントロールに設定する処理を手動で行う手間を省いてくれます。レコードを保存するときも、BindingSource を使用すれば、フォームのコントロールから値を取得してエンティティに戻す面倒な処理を手動で行う必要はありません。BindingSource の欠点は、データの読み込みが手動の場合より若干遅くなる可能性があることです。

それでも、テストでのパフォーマンスの違いが大きすぎるのでない限り、BindingSource コントロールを使用することをお勧めします。もっとも、ほとんどの場合は十分なパフォーマンスが得られます。BindingSource コントロールを用いた自動バインドは、同じデータのセットを複数のコントロールにバインドする必要がある場合にも非常に便利です。

手動バインドへの変更は簡単に行うことができます。必要な手順は、BindingSource コントロールを削除し、フォームのコントロールのデータ ソース設定を解除し、エンティティから対応するコントロールに各値を手動で読み込むためのコードを記述するだけです。次に例を示します。

using (RemoteService.CustomerWebService svc =
  new RemoteService.CustomerWebService()) {
    RemoteService.Customer customer = svc.GetCustomer(_customerID);
    companyNameTextBox.Text = customer.CompanyName;
    customerIDLabel1.Text = customer.CustomerID;
    ...
  }

手動での手順を使用する場合は、ユーザーがデータを保存するときにコントロールからデータを抽出する方法を考慮する必要があります。コントロールから値を取得してエンティティに設定するだけの簡単な手順ですが、ユーザーが情報を保存できるようにする場合は、この余分な手順を処理する必要があります。手動バインドでは、Customer エンティティを CustomerDetail フォームのプライベート フィールドとして格納する方法がよく使用されます。このようにすると、コントロールからデータを取り出してエンティティに戻す必要がある場合に、Customer エンティティに簡単にアクセスできます。

モバイル アプリケーションの微調整

帯域幅に制約があり、モバイル デバイスのプロセッサはデスクトップ コンピュータより遅いので、アプリケーションで必要な最低限のデータだけを受け渡すことが重要です。このサンプル アプリケーションでは、Customer エンティティの一覧を取得していますが、使用するのは各エンティティの 2 つのプロパティだけです。CustomerPicker 画面はすべての顧客のすべてのデータにアクセスしてそれを取得する必要があるので、これでは帯域幅のたいへんな浪費です。

もっと効率のよい方法は、各顧客の CustomerID と CompanyName だけを取得するメソッドを作成することです。このようなパフォーマンスの問題はデスクトップ アプリケーションでは発生しないことがあるので、モバイル アプリケーションを調整するときにこのようなことを忘れずに考慮することが重要です。

最後に

モバイル開発に着手する前に、十分に準備しておくことが重要です。正しいバージョンのエミュレータ マネージャ、適切なエミュレータ、正しい SDK、および必要なバージョンの ActiveSync または Windows Mobile デバイス センター があることを確認します。私がアプリケーションを Windows Vista で実行したときは、Windows Mobile デバイス センター 6.1 と Device Emulator Manager 2 をインストールするまで、エミュレータは Web サービスと通信できませんでした。Windows Mobile 5.0 が対象の場合であっても、Windows Vista で実行するときは、ソフトウェアに対するこれらの重要な更新をインストールする必要があります。

ActiveSync をエミュレータに接続しようとしてエラーが発生する場合は、DMA 経由で通信するように ActiveSync が設定されていることを確認してください。Web サービスへのアクセスでエラーが発生する場合は、Web サービスが localhost ではなくサーバーの名前を使用していることを確認してください。localhost は、PC では問題なく動作しますが、エミュレータが localhost を調整しようとすると、コンピュータの Web サーバーが指定されていることを認識しません。もう 1 つのよいヒントとして、エミュレートしているデバイスがクレードルに接続され、ローカル PC と通信できることを確認してください。

実際のモバイル デバイスにアプリケーションを展開する方法については、2006 年 12 月のデータ ポイント「Smartphone での RSS フィード」(msdn.microsoft.com/msdnmag/issues/06/12/DataPoints) を参照してください。

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

John Papa は、ASPSOFT (aspsoft.com) で働く上級 .NET コンサルタントです。野球を熱狂的に愛し、夏の夜のほとんどを家族および忠犬 Kadi (カーディ) と共にヤンキースのために費やします。C# MVP でもある John は、ADO、XML、および SQL Server に関する何冊かの書籍の執筆を担当しました。また、主に業界のカンファレンス (VSLive など) での講演やブログ (codebetter.com/blogs/john.papa) などで活躍しています。


Advanced Basics: リフレクションとジェネリック

Advanced Basics
リフレクションとジェネリック
Ken Getz

コードのダウンロード : : AdvancedBasics2008_01.exe (181KB)
コンテンツ
調査を開始する
メソッドを調べる
型のインスタンスを作成する
ジェネリック型のインスタンスを作成してメソッドを呼び出す
型引数を使用する
ジェネリック型引数の制約

最近、私がプログラミング好きであることを知って、友人の 1 人が、ドキュメント化が必要な新しいアセンブリのすべてのメンバの並べ替え可能な一覧を Microsoft® Excel® にエクスポートできる簡単なツールを作成してほしいと頼んできました。そのツールでは、プロパティ、メソッド、イベント、およびインターフェイスに関する情報を提供し、各メンバのすべてのパラメータを一覧にする必要がありました。私は以前にリフレクションを使用したことがあったので、この依頼はおもしろそうだと思いました。

ただし、私は最初に、書式が正しく設定されていないメンバ名を探し続けました。たとえば、自分のプログラムを使用して mscorlib アセンブリの内容を一覧にすることで、Dictionary`2 や ReadOnlyCollection`1 などの名前を見つけました。

このような名前はそれなりの理由があって存在していることがわかります。Microsoft .NET Framework では、ジェネリックでない型とメソッドの場合とは異なるジェネリックな型とメソッドの情報を公開します。これにより、ジェネリックに関連するリフレクション機能について調べることができ、現在ではドキュメント化することもできました。ジェネリックは .NET Framework 2.0 時点での新機能ですが、リフレクションを使用してジェネリックな型やメソッドについて調べたり、それらを作成または呼び出したりすることはさほど難しくはありません。私が作成したサンプル アプリケーション (MSDN® Magazine Web サイトで入手できます) では、私が説明したジェネリックとリフレクションの共通部分で公開された多くの機能が使用されています。

ジェネリックを初めて使用する場合は、「.NET Framework のジェネリックの概要」(msdn2.microsoft.com/ms172193) を参照してください。最も重要な点として、私の目標は既存のリフレクション ベースのアプリケーションを拡張してジェネリックな型やメソッドを処理する方法を示すことなので、この記事で説明する条件をよく確認してください。

調査を開始する

アセンブリがあれば、リフレクションを使用して簡単にそのメンバを調べることができます。それぞれの型のインスタンスを後から作成するために型がジェネリックかどうかを判断するには、Type.IsGenericType プロパティを使用する必要があります。次のサンプルでは、TestForGenericType プロシージャが mscorlib アセンブリの内容を読み込み、すべてのパブリックなジェネリック型の情報を表示します。

Dim asm As Assembly = Assembly.ReflectionOnlyLoad("mscorlib")
For Each typ As Type In asm.GetTypes()
    If typ.IsPublic AndAlso typ.IsGenericType Then
        AddToResults(typ.Name)
    End If
Next

このサンプルでは、AddToResults メソッドと DisplayResults メソッド (ここでは示しません) が単に出力テキストを作成して、それを警告に表示します。これにより、作成した型を調べることができます。また、このサンプルには、図 1 に示すように、複数のサンプル クラスを格納する TestClasses.vb ファイルが含まれています。

Figure 1 TestClasses.vb のクラス (Close Figure 1)
Public Class GenericClass(Of T)
    ' This is not a generic method.
    Public Sub Swap(ByRef item1 As T, ByRef item2 As T)
        Dim temp As T = item1
        item2 = item1
        item1 = temp
    End Sub

    ' This is a generic method and a generic method definition.
    Public Sub Swap(Of U)(ByRef item1 As U, ByRef item2 As U)
        Dim temp As U = item1
        item2 = item1
        item1 = temp
    End Sub

    Public Function GenericMethod(Of M)(ByVal item1 As M) As String
        Return String.Format("You passed in: {0}", item1)
    End Function

End Class

Public Class BaseClass(Of T, U)
    Public Items As New Dictionary(Of T, U)

    Public Sub New()
        Debug.Print("In BaseClass, GetType(T) = " & GetType(T).Name)
        Debug.Print("In BaseClass, GetType(U) = " & GetType(U).Name)
    End Sub
End Class

TestForGenericType はさらに以下の 3 つのクラスをテストします。

AddToResults("GenericClass(Of Integer) is generic: {0}", _
  GetType(GenericClass(Of Integer)).IsGenericType)
AddToResults("NonGenericClass is generic: {0}", _
  GetType(NonGenericClass).IsGenericType)
AddToResults( _
  "BaseClass(Of Integer, String) is generic: {0}", _
  GetType(BaseClass(Of Integer, String)). _
  IsGenericType)

このコードは、前に説明した各ジェネリック型の構築されたバージョンを作成します (構築された型は、各プレースホルダ型の特性を指定したジェネリック型です)。GenericClass と BaseClass はジェネリック型ですが、NonGenericClass は異なります。.NET Framework がジェネリック型の Name プロパティを返すときは、ジェネリック パラメータの数を追加します。そのため、Dictionary クラスは Dictionary`2 として表示されます。

メソッドを調べる

メソッドについても同じ並べ替えの方法を繰り返すことができます。メソッドを動的に実行する場合は、そのメソッドがジェネリックかどうかを把握しておく必要があります。これは、すべての型プレースホルダを指定したメソッドのバージョンを構築できるようにするためです。メソッドに 2 つの型引数のセット (1 つはメソッド全体を記述するもので、もう 1 つはメソッドのパラメータを指定するもの) が含まれている場合、そのメソッドはジェネリックであると見なされます。たとえば、GenericClass には Swap メソッドのオーバーロードされたバージョンが 2 つあります。最初のバージョンはジェネリックではありません。そのパラメータ型は、クラスの型プレースホルダによって定義されます。ただし、2 番目のバージョンはジェネリック型です。この場合、コンパイラはプレースホルダの型をプロシージャの最初のパラメータの型から推測します (2 つの異なる型を渡してプロシージャを呼び出そうとすると、コンパイル エラーが発生します)。TestForGenericMethod プロシージャでは、MethodInfo.IsGenericMethod プロパティを使用してメソッドがジェネリックかどうかを判断し、次のコードを呼び出してプロパティをテストします。

Dim typ As Type = GetType(GenericClass(Of Integer))

For Each method As MethodInfo In typ.GetMethods( _
  BindingFlags.Public Or BindingFlags.Instance _
  Or BindingFlags.DeclaredOnly)

  AddToResults(method.Name & " is generic: {0}", _
   method.IsGenericMethod)
Next

このコードは、Type.GetMethods メソッドの呼び出しで BindingFlags 列挙体を使用するため、直接クラスで宣言されるパブリック インスタンス メソッドだけを取得します。出力は次のようになります。

Swap is generic: True
GenericMethod is generic: True
Swap is generic: False

型のインスタンスを作成する

型のインスタンスを作成する場合は、その型がジェネリック型定義であるか構築されたジェネリック型かを把握しておく必要があります。ジェネリック型定義には 1 つ以上の未指定の型プレースホルダがあります。つまり、ジェネリック型定義はそのパラメータ型を認識しません。前の例で見たように、構築されたジェネリック型の型プレースホルダは特定の型に置き換えられています。構築されたジェネリック型でない型のインスタンスは作成できません。インスタンスの作成を試行する前に、型を指定する必要があります。型の状態を判断するには、図 2 に示す TestForGenericTypeDefinition プロシージャのコードのように、Type.IsGenericTypeDefinition プロパティを使用します。

Figure 2 ジェネリック型定義をテストする (Close Figure 2)
Dim type1 As Type = GetType(Dictionary(Of ,))
Dim type2 As New Dictionary(Of String, DateTime)
Dim type3 As Type = type2.GetType()
Dim type4 As Type = type3.GetGenericTypeDefinition()

AddToResults("type1 is generic type definition: {0}", _
  type1.IsGenericTypeDefinition)
AddToResults("type3 is generic type definition: {0}", _
  type3.IsGenericTypeDefinition)
AddToResults("type4 is generic type definition: {0}", _
  type4.IsGenericTypeDefinition)
AddToResults()

AddToResults("type1 is type4: {0}", type1 Is type4)

構築されたジェネリック型に対応する Type オブジェクトを取得する方法は既に説明しました。図 2 の最初の行に示すように、ジェネリック型定義にも同じ方法を使用できます (必要なそれぞれの型プレースホルダの間をコンマで区切ってください)。

Dim type1 As Type = GetType(Dictionary(Of ,))

構築された型があれば、Type.GetGenericTypeDefinition メソッドを使用してジェネリック型の定義を取得できます。次のコードは、ジェネリック クラス Dictionary の構築されたインスタンスを作成し、その型を取得して、構築されたジェネリック型のジェネリック型定義を取得します。

Dim type2 As New Dictionary(Of String, DateTime)
Dim type3 As Type = type2.GetType()
Dim type4 As Type = type3.GetGenericTypeDefinition()

最後に、このプロシージャは type4 と type1 の参照を比較します。これらの参照は、まったく同じ (つまり、Dictionary インスタンスのジェネリック型定義) である必要があります。プロシージャの出力は次のようになります。これは、type1 と type4 が同じジェネリック型定義であることを示します。

type1 is generic type definition: True
type3 is generic type definition: False
type4 is generic type definition: True

type1 is type4: True

構築されたジェネリック型である type3 はジェネリック型定義ではありません。

同様の方法を使用して、調べている型が標準の型、ジェネリック型、ジェネリック型定義のいずれかを判断できます。MethodInfo.IsGenericMethod プロパティと MethodInfo.IsGenericMethodDefinition プロパティを次に示します。

Dim type1 As Type = GetType(GenericClass(Of ))

Dim method As MethodInfo = type1.GetMethod("GenericMethod")
If method.IsGenericMethodDefinition Then
    Dim miConstructed As MethodInfo = _
        method.MakeGenericMethod(GetType(Integer))
    AddToResults("Is generic method definition: {0}", _
        miConstructed.IsGenericMethodDefinition)
    AddToResults("Is generic method: {0}", _
        miConstructed.IsGenericMethod)
End If

すべてのジェネリック メソッド定義はジェネリック メソッドでもありますが、すべてのジェネリック メソッドがジェネリック メソッド定義であるというわけではありません。メソッドの型プレースホルダの値を指定した場合、そのメソッドはジェネリック メソッドですが、ジェネリック メソッド定義ではありません。このサンプルには、MethodInfo.MakeGenericMethod メソッドも示されています。このメソッドを使用すると、ジェネリック メソッド定義を取得してその型プレースホルダの型を指定することにより、定義からジェネリック メソッドを構築できます。サンプル プロシージャは、次の出力を表示します。

Is generic method definition: False
Is generic method: True

ここでも、このサンプルはジェネリック型定義で動作しています。

Public Function GenericMethod(Of M)(ByVal item1 As M) As String
    Return String.Format("You passed in: {0}", item1)
End Function

Integer 型を渡して MakeGenericMethod メソッドを呼び出すことにより、コードは実行時に次のジェネリック メソッドの作成を開始します (item1 は呼び出し側コードから渡した型を表します)。

Public Function GenericMethod (ByVal item1 As Integer) As String
    Return String.Format("You passed in: {0}", item1)
End Function

ジェネリック型のインスタンスを作成してメソッドを呼び出す

目標の 1 つがジェネリック型のインスタンスの作成とおそらくは構築されたジェネリック メソッドの呼び出しを可能にすることであるとすると、さらにいくつかの手法が必要です。型のインスタンスを作成するために、型の依存関係のチェーン全体の任意の場所に、未指定の型プレースホルダを含むジェネリック型定義やジェネリック メソッド定義を配置することはできません。すべての型プレースホルダが指定されているジェネリック型はクローズ ジェネリック型と呼ばれます (同じ用語がメソッドにも適用されます)。作成できるのはクローズ ジェネリック型のインスタンスだけであり、呼び出すことができるのはクローズ ジェネリック メソッドだけです。

ただし、継承階層が複雑なジェネリック型を渡す場合はどうでしょうか。型の先祖にオープンな型が存在しないようにするにはどうすればよいでしょうか。これを確認するために、クローズ ジェネリック型とオープン ジェネリック型のどちらを使用しているかを断定的に示す Type.ContainsGenericParameters プロパティを取得できます。このプロパティが True を返す場合は、オープン ジェネリック型を使用しているため、インスタンスを作成できません。

TestForOpenOrClosedGeneric プロシージャは、次のように、TestClasses ファイルの 2 つのクラスを使用してこの動作を示します。

Public Class BaseClass(Of T, U)
    Public Items As New Dictionary(Of T, U)
End Class

Public Class DerivedClass(Of V)
    Inherits BaseClass(Of Integer, V)

End Class

TestForOpenOrClosedGeneric プロシージャは、図 3 の DisplayGenericTypeInfo プロシージャを呼び出します。DisplayGenericTypeInfo プロシージャは、複数の異なる型の情報を表示します。

Figure 3 DisplayGenericTypeInfo プロシージャ (Close Figure 3)
Private Sub DisplayGenericTypeInfo(ByVal t As Type)
    AddToResults("{0}", t)

    Dim indent As String = vbTab
    AddToResults("{0}Generic type definition: {1}", _
        indent, t.IsGenericTypeDefinition)

    AddToResults("{0}Generic type: {1} ", _
        indent, t.IsGenericType)

    AddToResults("{0}Unassigned generic parameters: {1}", _
        indent, t.ContainsGenericParameters)

    Dim args() As Type = t.GetGenericArguments()
    AddToResults("{0}List type arguments ({1}):", _
        indent, args.Length)
    For Each argType As Type In args
        AddToResults("{0}{0}{1}", indent, argType)
    Next

    AddToResults("{0}{1}", indent, _
        IIf(t.ContainsGenericParameters, "OPEN", "CLOSED"))
End Sub

型が指定されていると、DisplayGenericTypeInfo プロシージャは最初に型名、型がジェネリック型定義であるかどうか、および型がジェネリック型であるかどうかを表示します。次に、ジェネリック パラメータが型に含まれているかどうかが表示されます (ジェネリック型定義の場合は、ジェネリック パラメータが含まれているため、インスタンス化できません)。さらに、図 3 のコードは、GetGenericArguments メソッドを呼び出してすべてのジェネリック引数のリストを取得し、出力に各ジェネリック引数を表示します。最後に、このコードは ContainsGenericParameters プロパティを使用して、型がオープンかクローズかを示します (特定の MethodInfo がオープン ジェネリック メソッドかクローズ ジェネリック メソッドかを判断するために、同じ並べ替えのロジックを MethodInfo のインスタンスに適用できます)。

TestForOpenOrClosedGeneric メソッドには、ジェネリック定義および構築された型の両方として複数のサンプル メソッドをテストする次のコードが含まれています。それぞれの出力を予測できるかどうかを確認してください。

Dim baseType As Type = GetType(BaseClass(Of ,))
DisplayGenericTypeInfo(baseType)

Dim derivedType As Type = GetType(DerivedClass(Of ))
DisplayGenericTypeInfo(derivedType)
DisplayGenericTypeInfo(derivedType.BaseType)

Dim constructedType As Type = GetType(DerivedClass(Of String))
DisplayGenericTypeInfo(constructedType)

DisplayGenericTypeInfo(constructedType.BaseType)

CreateGenericTypeGivenGenericTypeDefinition サンプル プロシージャは、ジェネリック型定義が指定されている場合にクローズ ジェネリック型を作成する方法を示しています。メソッド定義に適用されるこの方法については既に説明しました。MethodInfo.MakeGenericMethod メソッドを呼び出してその定義からジェネリック メソッドを構築した場合と同様に、Type.MakeGenericType メソッドを呼び出してその定義からジェネリック型を構築できます。図 4 に示すように、このサンプル コードは、構築された型を実行時に作成し、その型をデザイン時に作成済みの構築された型と比較します。

Figure 4 構築された型を比較する (Close Figure 4)
Dim type1 As Type = GetType(BaseClass(Of ,))
DisplayGenericTypeInfo(type1)

' Create an array of types to substitute for the type
' parameters.
Dim args() As Type = {GetType(String), GetType(DateTime)}

' Create a Type object representing the constructed generic
' type.
Dim constructed As Type = type1.MakeGenericType(args)
DisplayGenericTypeInfo(constructed)

Dim type2 As Type = GetType(BaseClass(Of String, DateTime))
AddToResults("Constructed types equal: {0}", _
   constructed Is type2)
AddToResults("Generic types equal: {0}", _
  type2.GetGenericTypeDefinition Is type1)

ジェネリック メソッド定義が指定されている場合にジェネリック メソッドを呼び出すには、MethodInfo.MakeGenericMethod メソッドを呼び出して、各メソッドの型プレースホルダに対応する Type オブジェクトが格納されている配列を渡す必要があります。新しく構築されたメソッドがあれば、MethodInfo.Invoke メソッドを呼び出して、ホスト オブジェクトのインスタンスとメソッドのパラメータの配列を指定できます。もちろん、パラメータの型は、ジェネリック メソッドのインスタンスの構築時に指定したプレースホルダ型の型と正確に一致する必要があります。

図 5 の CallGenericMethodGivenGenericMethodDefinition サンプル プロシージャは、この方法を示しています。このサンプルでは、まず GenericClass の GenericMethod メソッドに対応する MethodInfo のインスタンスを取得します。このメソッドは、1 つの型プレースホルダを受け取り、実行時に指定した値を格納する文字列を単に返します。

Figure 5 CallGenericMethodGivenGenericMethodDefinition (Close Figure 5)
Dim type1 As Type = GetType(GenericClass(Of Integer))
Dim mi As MethodInfo = type1.GetMethod("GenericMethod")
DisplayGenericMethodInfo(mi)

Dim argTypes() As Type = {GetType(String)}
Dim miConstructed As MethodInfo = mi.MakeGenericMethod(argTypes)
DisplayGenericMethodInfo(miConstructed)

' Invoke the method, creating an instance of the
' generic class first.
Dim host As New GenericClass(Of Integer)

' Supply the method parameters, and then invoke the method:
Dim args() As Object = {"test value"}
AddToResults("{0}", miConstructed.Invoke(host, args))
AddToResults()

' Construct another version of the same
' generic method, this time taking an integer
' as the parameter:
argTypes(0) = GetType(Integer)
miConstructed = mi.MakeGenericMethod(argTypes)
DisplayGenericMethodInfo(miConstructed)

' Supply the method parameters, and then invoke the method:
args(0) = 13
AddToResults("{0}", miConstructed.Invoke(host, args))

Public Function GenericMethod(Of M)(ByVal item1 As M) As String
    Return String.Format("You passed in: {0}", item1)
End Function

このコードの処理は、特定のジェネリック クラスを構築し、そのクラスの型プレースホルダの型を指定することによって行います。

Dim type1 As Type = GetType(GenericClass(Of Integer))
Dim mi As MethodInfo = type1.GetMethod("GenericMethod")
DisplayGenericMethodInfo(mi)

既に説明したように、型の先祖に未指定の型プレースホルダがある (つまり、クローズ ジェネリック メソッドではない) 場合、ジェネリック メソッドを呼び出すことはできません。これを証明するには、プロシージャをそのまま 1 回実行した後に、type1 のジェネリック型定義を指定するようにプロシージャを変更します。コードはコンパイルされますが、実行時に失敗します。

Dim type1 As Type = GetType(GenericClass(Of ))

このサンプルは、ジェネリック メソッドとその各ジェネリック引数の情報を表示する DisplayGenericMethodInfo プロシージャを呼び出します。

次に示すように、このコードはさらに Type のインスタンスの配列を作成し、MethodInfo.MakeGenericMethod メソッドを呼び出して構築されたジェネリック メソッドを作成します。

Dim argTypes() As Type = {GetType(String)}
Dim miConstructed As MethodInfo = mi.MakeGenericMethod(argTypes)
DisplayGenericMethodInfo(miConstructed)

構築されたジェネリック メソッドがあれば、このコードは GenericClass のインスタンスを作成し (この方法は次で説明します)、メソッドに渡すパラメータの配列を作成し、構築されたジェネリック メソッドを実行するために MethodInfo.Invoke を呼び出します。

Dim host As New GenericClass(Of Integer)

' Supply the method parameters, and then invoke the method:
Dim args() As Object = {"test value"}
AddToResults("{0}", miConstructed.Invoke(host, args))

この動作を確認するために、同じジェネリック メソッド定義の複数の異なる構築されたバージョンを作成できます。次に、このコードは新しいジェネリック メソッド定義を作成します。今回は、文字列ではなく整数を渡すことを示します。先ほどと同様に、コードは構築されたジェネリック メソッドを作成して呼び出します。

argTypes(0) = GetType(Integer)
miConstructed = mi.MakeGenericMethod(argTypes)
DisplayGenericMethodInfo(miConstructed)

' Supply the method parameters, and then invoke the method:
args(0) = 13
AddToResults("{0}", miConstructed.Invoke(host, args))

CreateClosedGenericInstance サンプル メソッドは、同様の手法を使用しますが、代わりに構築されたジェネリック型を作成します。前の例では、コードは構築された型をデザイン時に作成しました。この例では、実行時に作成します。

型引数を使用する

ここまでにジェネリック引数の情報を取得するコードを見てきましたが、そのために調べておく必要のある項目がいくつかあります。RetrieveTypeParameterInfo サンプル プロシージャは、複数の異なるジェネリック型とジェネリック型定義を使用して、それぞれのジェネリック引数の情報を表示します。Type.GetGenericArguments メソッドは配列を返し、現在の型がジェネリック型定義の場合、配列にはそのジェネリック型定義の型パラメータが型定義に表示される順序で格納されます。現在の型が構築されたクローズ型の場合、配列にはジェネリック型パラメータに割り当てられた型が格納されます。現在の型がジェネリック型定義の場合、配列には型パラメータが格納されます。現在の型が構築されたオープン型の (つまり、すべてのパラメータが指定されているわけではなく、ContainsGenericParameters プロパティが True を返す) 場合、配列には型と型パラメータの両方が格納されます。この 2 つを区別するために IsGenericParameter プロパティを使用できます。

RetrieveTypeParameterInfo プロシージャは次のコード内に含まれています。

Dim type1 As Type = GetType(DerivedClass(Of String))
DisplayGenericParameterInfo(type1)

Dim type2 As Type = type1.GetGenericTypeDefinition
DisplayGenericParameterInfo(type2)

Dim type3 As Type = type2.BaseType
DisplayGenericParameterInfo(type3)

このコードが表示するのは、構築されたジェネリック型、対応するジェネリック型定義、およびジェネリック型定義の基本型 (BaseClass サンプル クラス) のパラメータの情報です。

DisplayGenericParameterInfo はほとんどの処理をこのサンプルで行います。DisplayGenericParameterInfo には図 6 のコードが含まれています。このコードは、型名を表示した後で、型がジェネリック型定義かどうか、型がジェネリック型かどうか、および型にジェネリック パラメータが含まれているかどうかを示します。ジェネリック型の場合、コードはジェネリック引数の配列を取得します。

Figure 6 ジェネリック パラメータの情報を表示する (Close Figure 6)
AddToResults("{0}", t)

Dim indent1 As String = vbTab
Dim indent2 As String = vbTab & vbTab

AddToResults("{0}Generic type definition: {1}", _
    indent1, t.IsGenericTypeDefinition)

AddToResults("{0}Generic type: {1}", _
    indent1, t.IsGenericType)

AddToResults("{0}Unassigned generic parameters: {1} ", _
    indent1, t.ContainsGenericParameters)

If t.IsGenericType Then
    ' If a generic type, display the arguments:
    Dim args As Type() = t.GetGenericArguments()

    AddToResults("{0}List type arguments ({1}):", _
        indent1, args.Length)

    For Each argType As Type In args
        If argType.IsGenericParameter Then
            AddToResults("{0}{1} at position {2}", _
            indent2, argType, argType.GenericParameterPosition)
        Else
            AddToResults("{0}{1}", indent2, argType)
        End If
    Next argType
End If

Dim args As Type() = t.GetGenericArguments()

次にコードは、配列内にある Type の各インスタンスについて、ジェネリック パラメータ (構築された型の Type) があるかどうかを判断します。ジェネリック パラメータがある場合は、GenericParameterPosition プロパティ値を表示します。ない場合は、型名を表示します (ジェネリック パラメータではない型の GenericParameterPosition プロパティを取得すると、ランタイム例外が発生します)。

ジェネリック パラメータのソースを確認する

いずれかのコードがジェネリック パラメータを渡す場合は、そのパラメータがジェネリック型用であるかジェネリック メソッド用であるかを判断する必要が生じることもあります。ジェネリック パラメータのソースは、調べている型、それを囲む型、またはジェネリック メソッドである可能性があります。それでは、ソースを確認するにはどうすればよいでしょうか。最初に、型の DeclaringMethod プロパティをチェックします。この値が Nothing でない場合は、MethodInfo のインスタンスであり、型パラメータのソースがジェネリック メソッドであることがわかり、MethodInfo の IsGenericMethodDefinition プロパティは True を返します。ソースがジェネリック メソッドでない場合、DeclaringMethod プロパティは Nothing を返し、DeclaringType プロパティを取得できます。この型は常にジェネリック型定義です。型の IsGenericParameter プロパティが True を返す場合に取得できるのは、DeclaringMethod プロパティのみです。それ以外の場合は、例外が発生します。

ジェネリック型引数の制約

最後に、ジェネリック型引数の制約を調べる必要が生じることもあります。これらの制約は、型引数に関するルールを適用し、ジェネリック型がアプリケーションの要件に準拠するようにします。特定のクラスを継承する型パラメータ、1 つ以上の特定のインターフェイスを実装する型パラメータ、値型である型パラメータ、参照型である型パラメータ、および既定のコンストラクタを提供する型パラメータを適用する制約を追加できます。どの制約が型引数に適用されているかをプログラムで確認し、制約の要件を満たす型を動的に構築することもできます。

制約を調べるには、2 組の手順が必要です。型引数が継承または実装する必要のある 1 つのクラスまたは 1 つ以上のインターフェイスの情報を取得するには、Type.GetGenericParameterConstraints メソッドを呼び出してください。このメソッドは、Type オブジェクトの配列を返します。各オブジェクトは、1 つのクラスまたはインターフェイスを示します。

可変性の制約およびその他の特殊な制約の型の情報を取得するには、2 つの異なるビット マスクを使用する必要があります。可変性の制約の場合は、型の GenericParameterAttributes プロパティを取得します。次に、GenericParameterAttributes.VarianceMask ビット マスクを戻り値に適用する必要があります。型引数に可変性の制約があるかどうかを判断するには、結果を GenericParameterAttributes.Covariant または Contravariant 列挙値と比較します。

特殊な制約の情報を取得するには、GenericParameterAttributes.SpecialConstraintMask ビット マスクを適用します。今回それぞれの特殊な制約の状態を確認するには、結果を ReferenceTypeConstraint、NotNullableValueTypeConstraint、DefaultConstructorConstraint の各列挙値と比較します。この処理は、IsBitSet ヘルパ プロシージャを使用して特定のビットが設定されているかどうかを判断することにより、ExamineGenericParameterAttributes メソッドがすべて行います。

Private Function IsBitSet( _
        ByVal value As Integer, ByVal bitValue As Integer) As Boolean
    Return (value And bitValue) = bitValue
End Function

ExamineGenericParameterAttributes プロシージャには、上記の手順に従う図 7 のコードが含まれています。

Figure 7 ジェネリック パラメータの属性を調べる (Close Figure 7)
' Select the variance flags.
Dim variance As GenericParameterAttributes =  _
     parameterAttributes And _
    GenericParameterAttributes.VarianceMask

Dim indent As String = vbTab
If variance = GenericParameterAttributes.None Then
    AddToResults("{0}No variance flag", indent)
Else
    If IsBitSet(variance, GenericParameterAttributes.Covariant) Then
        AddToResults("{0}Variance flag = Covariant", indent)
    Else
        AddToResults("{0}Variance flag = Contravariant", indent)
    End If
End If

' Select the constraint flags.
Dim constraints As GenericParameterAttributes = _
    parameterAttributes And _
    GenericParameterAttributes.SpecialConstraintMask

If constraints = GenericParameterAttributes.None Then
    AddToResults("{0}No special constraints", indent)
Else
    If IsBitSet(constraints, _
        GenericParameterAttributes.ReferenceTypeConstraint) Then
            AddToResults("{0}ReferenceTypeConstraint", indent)
    End If
    If IsBitSet(constraints, _
        GenericParameterAttributes.NotNullableValueTypeConstraint) Then
            AddToResults("{0}NotNullableValueTypeConstraint", indent)
    End If
    If IsBitSet(constraints, _
        GenericParameterAttributes.DefaultConstructorConstraint) Then
            AddToResults("{0}DefaultConstructorConstraint", indent)
    End If
End If

サンプルの DisplayConstraintInfo プロシージャには、渡される型のパラメータの制約の情報を表示する図 8 のコードが含まれています。

Figure 8 制約の情報を表示する (Close Figure 8)
Private Sub DisplayConstraintInfo(ByVal typ As Type)
    AddToResults("{0}", typ)

    Dim indent As String = vbTab

    Dim params() As Type = typ.GetGenericArguments()
    For Each paramType As Type In params
        ' Handle special constraints:
        ExamineGenericParameterAttributes(paramType)

        ' Loop through type constraints:
        Dim constraints As Type() = _
            paramType.GetGenericParameterConstraints()

        For Each constraintType As Type In constraints
            AddToResults("{0}{1}", indent, constraintType)
        Next constraintType
        AddToResults()
    Next
End Sub

既に説明したように、このプロシージャはすべてのパラメータをループ処理し、それぞれについて最初に ExamineGenericParameterAttributes プロシージャを呼び出し (可変性の制約およびその他の特殊な制約を処理します)、GetGenericParameterConstraints メソッドの呼び出しの結果として返されたすべての型をループ処理します。

ExamineConstraints サンプル プロシージャを次に示します。

DisplayConstraintInto(GetType(TestClass1(Of )))
DisplayConstraintInto(GetType(TestClass2(Of )))
DisplayConstraintInto(GetType(TestClass3(Of )))

このプロシージャは、3 つの異なるクラスの DisplayConstraintInfo プロシージャの動作を調べます。これらのすべてのクラスには異なる引数の制約があります。サンプル クラスは図 9 のようになります。

Figure 9 サンプル クラス (Close Figure 9)
Public Class TestClass1( _
        Of T As {IComparable, IDisposable, Class, New})

    ' T must implement IComparable and IDisposable.
    ' T must be a reference type.
    ' T must supply a default constructor.
End Class

Public Class TestClass2(Of T As {NonGenericClass})
    ' T must inherit from NonGenericClass.
End Class

Public Class TestClass3(Of T As {Structure})
    ' T must be a value type.
End Class

ユーザーが行う処理

これで、リフレクションとジェネリックの共通部分で .NET Framework が公開するすべてのメソッドとプロパティの説明は終了です。ユーザーが行う必要があるのは、サンプル アプリケーションをダウンロードして、すべてのコードと出力に従い、関連ドキュメントのトピックを参照することです。これで、リフレクションとジェネリックに関するどのような問題にも取り組む準備が整ったはずです。

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

Ken Getz は、MCW Technologies のシニア コンサルタントであり、AppDev (www.appdev.com) のコースウェアの作成者でもあります。Ken は、『ASP .NET Developers Jumpstart』(Addison-Wesley、2002 年)、『Access Developer's Handbook』(Sybex、2002 年)、および『VBA Developer's Handbook, 2nd Edition』(Sybex、2001 年) の共著者です。連絡先は keng@mcwtech.com です。


Cutting Edge: AJAX エクステンダを使用してコントロールをカスタマイズする

Cutting Edge
AJAX エクステンダを使用してコントロールをカスタマイズする
Dino Esposito

コンテンツ
はじめに
コントロールの機能を拡張する
エクステンダ コントロールの詳細
入力エクステンダ
透かしと入力
確認とモダリティ
拡張されたチェックボックス
制限された入力
より良い Web ページのために

入力コントロールは、すべてのアプリケーションで不可欠ですが、Web アプリケーションが組織の顔であるときには特に重要です。ユーザーがアプリケーションを判断し、さらには信頼性をも判断するうえで、これらのコントロールが主要な基準となる可能性があります。

Windows® プログラミングおよび Web プログラミングでは両方とも、コントロールを作成することによってユーザー インターフェイスが構築されます。ただし、使用可能な一連のコントロールは特に充実しているわけではありません。Windows フォームの入力コントロールは、依然として Win32® コントロールに基づいています。また、Web コントロールは HTML の <INPUT> タグのラッパーとたいして変わりません。ASP.NET AJAX アプリケーションは、クライアント側のユーザー操作に大きく依存しているため、これらのアプリケーションを作成する開発者にとっては特に、より優れた入力コントロールが必要であることは明らかです。

この 2 部シリーズでは、ASP.NET 3.5 の入力機能に重点を置いて説明しますが、ここで説明する情報のほとんどは、ASP.NET AJAX Extensions がインストールされている ASP.NET 2.0 にも適用可能です。AJAX Control Toolkit (以降、ACT と呼ぶ) に含まれている JavaScript で稼動する動作のいくつかを使用して ASP.NET 入力コントロールを拡張する方法を説明します。今月は、ACT 入力拡張の基本セットについて説明します。来月は、より高度な機能について説明します。

はじめに

ACT は Web コントロールの共有ソース ライブラリで、codeplex.com/AtlasControlToolkit からダウンロードできます。ACT では、スクリプト コードの定義済みのブロックを使用して既存の ASP.NET サーバー コントロールを拡張します。ACT エクステンダは、組み込みおよびカスタムの両方の ASP.NET コントロールに適用できます。

厳密に言うと、ACT コンポーネントでは TextBox や Panel などのコントロールの上に追加の JavaScript レイヤを追加します。そのため、AJAX の使用は厳しい要件ではありません。ただし、ACT コンポーネントの実装は Microsoft® AJAX Library の JavaScript 機能だけでなく、ASP.NET AJAX Extensions のサーバー ランタイムにも大きく依存しています。

ACT ライセンス規則は asp.net/ajax/ajaxcontroltoolkit に記載されています。CodePlex における ACT プロジェクト ページには次の 2 つのダウンロードがあります。AJAX Extensions をインストール済みの ASP.NET 2.0 および ASP.NET 3.5 (codeplex.com/AtlasControlToolkit/Release/ProjectReleases.aspx を参照) です。

ACT を ASP.NET アプリケーションで使用するには、プロジェクトでアセンブリを参照し、ACT を使用する任意のページで ACT を登録する必要があります。次のように、@Register ディレクティブを使用し、任意のプレフィックスを指定すると、ページ単位で ACT を登録できます。

  <%@ Register Assembly="AjaxControlToolkit"
               Namespace="AjaxControlToolkit"
               TagPrefix="act" %>

また、アプリケーションのすべてのページに対し、web.config ファイルでこのライブラリを登録することができます。以下は、使用する構成スクリプトです。

  <pages>
     <controls>
        <add tagPrefix="act"
             namespace="AjaxControlToolkit"
             assembly=" AjaxControlToolkit" />
     </controls>
  </pages>

ACT コントロールは Visual Studio ツールボックスに自動的に表示されませんが、図 1 に示すように、新規のタブの追加は非常に簡単です。

図 1 Visual Studio 2008 の ACT
図 1 Visual Studio 2008 の ACT

コントロールの機能を拡張する

ASP.NET などのオブジェクト指向フレームワークでは、各サーバー コントロールはクラスであり、新規コントロールを派生させることでクラスをさらにカスタマイズできます。たとえば、マスク テキストボックス コントロールが必要であるとします。まず、組み込みの TextBox クラスから派生する新しい MaskedTextBox クラスを作成します。派生クラスでは、マスク入力フィールドに必要なさまざまな動作を用意するために必要なプロパティとクライアント側機能を追加します。後で、数値テキストボックスや増減の入力ボックスが必要な場合は、別のカスタム コントロール クラスを作成することができます。ただし、追加機能ごとに新規コントロールを作成することは、最適なアイデアであるとはいえません。図 2 に示すように、その結果、フラットな階層を形成するコントロールが増えることになります。新しいコントロールは、同じ基本クラスに対してそれぞれわずかな動作しか追加しません。

図 2 派生コントロールとエクステンダ
図 2 派生コントロールとエクステンダ(画像を縮小するには、ここをクリックします)
図 2 派生コントロールとエクステンダ
図 2 派生コントロールとエクステンダ (画像を拡大するには、ここをクリックします)

さいわいなことに、コントロールを拡張する別の方法として、既存のコントロールの特定のインスタンスの動作を拡張する方法があります。この場合、追加の動作は ASP.NET エクステンダを使用して追加します。必要な数の追加の動作を特定のコントロールのインスタンスに関連付けることにより、新規のクラスを作成せずにコントロールの機能を強化できます。

ASP.NET サーバー コントロールと同様に、エクステンダは拡張コントロールへの参照を取得し、スクリプト コードを埋め込むことによって、その動作を変更します。エクステンダ コントロールは、開発者がさらに詳細な動作を構成できるように独自のいくつかのプロパティを備えることができます。ターゲット ASP.NET コントロールとエクステンダのセットとの関連付けは、通常、ページのマークアップの宣言で指定されます。次のコードは、ユーザーが上下ボタンをクリックするだけで数値を選択できるように、数値を増減するインターフェイスをテキストボックスに追加する方法を示します。

<asp:TextBox ID="Children" runat="server" />
<act:NumericUpDownExtender ID="UpDown1" runat="server"
     TargetControlID="Children"
     Width="100" />

ご覧のように、このページには通常の TextBox コントロールと NumericUpDownExtender コントロールのインスタンスが含まれています。エクステンダの TargetControlID プロパティは、エクステンダの対象コントロールの ID を設定します。Width プロパティは、エクステンダ固有であり、エクステンダの対象コントロールの最終的な幅とエクステンダが関与する追加の UI 要素を定義します。この場合は、上下ボタンのペアです (図 3 を参照)。エクステンダのプロパティは、分離コード クラス内で、宣言によっても、プログラムによっても設定できます。

図 3 NumericUpDown &#xA;エクステンダの動作
図 3 NumericUpDown &#xA;エクステンダの動作

エクステンダ コントロールの詳細

ACT には、特定のユーザー インターフェイスのニーズを処理する従来のサーバー コントロールと、既存のコントロールに特定の動作を追加するエクステンダが含まれています。たとえば、ACT には、タブベースのインターフェイスを設定する TabContainer コントロールだけでなく、リストボックスの項目を移動できるドラッグ アンド ドロップ機能を持つ ReorderList コントロールもあります。ただし、ほとんどの場合、ACT はエクステンダで構成されます。

図 4 を見ると、エクステンダが、AJAX Extensions アセンブリで定義されている基本クラス ExtenderControl から継承していることがわかります。内部的には、カスタム エクステンダ コントロール クラスは、GetScriptReferences メソッドと GetScriptDescriptors メソッドをオーバーライドします。GetScriptReferences メソッドは、動作を実装するためクライアント ページに埋め込むスクリプト ファイルのリストを返し、GetScriptDescriptors メソッドはクライアント側プロパティとサーバー側プロパティのマッピング リストを返します。このようにして、クライアントで特定のプロパティに割り当てられた任意の値は、サーバー上に正しく反映されます。このような比較的標準的なコードとは別に、エクステンダは単なる JavaScript コードであり、DOM を使用して、より豊富な UI 要素をエンド ユーザーに提供します。

Figure 4 フォーカス機能をターゲット コントロールに追加する (Close Figure 4)
[
[TargetControlType(typeof(Control))]
[ClientScriptResource("IntroAjax.FocusBehavior"), "focusBehavior.js")]
public class FocusExtender : AjaxControlToolkit.ExtenderControlBase
{
    [ExtenderControlProperty]
    [RequiredProperty]
    public string HighlightCssClass
    {
        get { return GetPropertyValue("HighlightCssClass", ""); }
        set { SetPropertyValue("HighlightCssClass", value); }
    }

    [ExtenderControlProperty]
    public string NoHighlightCssClass
    {
        get { return GetPropertyValue("NoHighlightCssClass", ""); }
        set { SetPropertyValue("NoHighlightCssClass", value); }
    }
}

図 4 に示すサンプルのエクステンダは、ほぼすべての ASP.NET コントロール、つまり、Control から派生するすべてのコントロールに、フォーカス動作を追加します。フォーカス機能は、リンクされた focusbehavior.js ファイルで実装されています。このスクリプト ファイルは、おそらく、エクステンダ コントロールを含むアセンブリ内に埋め込まれ、必要に応じてクライアントにダウンロードされます。focusbehavior.js ファイルは、Microsoft AJAX Library のプログラミング モデルを使用し (筆者の 2007 年 12 月のコラム msdn.microsoft.com/msdnmag/issues/07/12/CuttingEdge を参照)、その中心は、focus イベントおよび blur イベントのドキュメント オブジェクト モデル (DOM) ハンドラを登録する初期化メソッドです。focus ハンドラでは、コードでターゲット要素の CSS クラスを設定します。blur ハンドラでは、コードで CSS クラスをリセットします。詳細については、このコラムに付属のソース コードを確認してください。すべてのことを考慮すると、エクステンダを作成する最も簡単な方法は、ACT の機能を使用することです。ダウンロードしたツールキットのインストーラを実行し、テンプレートを設定したら、[Add AJAX Control Extender] (AJAX Control Extender の追加) を選択して、エクステンダおよび動作の基礎を取得します。詳細については、asp.net/AJAX/AjaxControlToolkit/Samples/Walkthrough/CreatingNewExtender.aspx を参照してください。

入力エクステンダ

既に説明したように、ACT はボタン、テキストボックス、パネル、リストボックスなどに機能を追加するいくつかのエクステンダを備えています。ACT のコンポーネントは、パネル、入力、汎用 UI、アニメーションなどのいくつかのカテゴリに分類できます。コンポーネントの完全な一覧とドキュメントおよびサンプル コードについては、asp.net/ajax/control-toolkit を参照してください。また、図 5 に、ACT の入力エクステンダの完全な一覧を示します。

Figure 5 ACT の入力エクステンダ (Close Figure 5)

入力エクステンダ | ターゲット コントロール | 主要な目的 | 説明
AutoComplete | TextBox | ユーザー エクスペリエンスの改善 | 編集フィールドの入力時に、入力候補のリストを提供します。
Calendar | TextBox | 入力の制限 | カスタマイズ可能な日付形式を使用してクライアント側の日付選択機能を提供します。
ConfirmButton | IButtonControl | 誤りの防止 | ボタンをクリックすると、確認ダイアログ ボックスをポップアップ表示します。
FilteredTextBox | TextBox | 入力の制限 | 特定の文字セットに一致するテキストだけを入力できるようにします。
HoverMenu | Control | ユーザー エクスペリエンスの改善 | ツールヒントなど、特定のコントロールの横にマウスを重ねると、関連付けられているパネル コントロールのコンテンツを表示します。
ListSearch | ListControl | ユーザー エクスペリエンスの改善 | 文字の一部を入力すると、一覧内の項目をユーザーが検索できるようにします。
MaskedEdit | TextBox | 入力の制限 | 特定の入力マスクに一致するテキストだけを入力できるようにします。
ModalPopup | Control | ユーザー エクスペリエンスの改善 | ブラウザの window.alert メソッドを使用せずに、従来のモーダル ダイアログ ボックスを実装します。基本的には、パネルのコンテンツを表示し、基になるページに対する操作を防止します。
MutuallyExclusiveCheckBox | CheckBox | 入力の制限 | チェックボックスの論理グループを定義して、各グループで 1 つのチェックボックスのみをチェックできるようにします。
NumericUpDown | TextBox | 誤りの防止 | 次の値または前の値を選択する上向きボタンおよび下向きボタンを表示します。
Slider | TextBox | 誤りの防止 | スライダのようなユーザー インターフェイスを使用して、範囲内の数値を選択します。
TextBoxWatermark | TextBox | 誤りの防止 | 入力を開始すると表示されなくなり、テキストボックスが空になると再び表示される、透かしテキストを追加します。
ToggleButton | CheckBox | ユーザー エクスペリエンスの改善 | チェック ボタンをレンダリングするカスタム イメージを表示できます。
ValidatorCallout | BaseValidator | ユーザー エクスペリエンスの改善 | エラー メッセージ用のバルーン形式の吹き出しをポップアップ表示することにより、バリデータのユーザー インターフェイスを改善します。

透かしと入力

Web ページで最も煩わしい要素の 1 つは、ラベルの付いていないテキストボックスまたはラベルやテキストボックスが揃っていないページです。これは、ユーザーが選択するフォント サイズやその他の設定に起因する場合がありますが、この結果としてユーザーにはテキストボックスが示す内容が何であるかわかりません。このための優れた解決策として、テキストの透かしがあります。これは、ユーザーによるボックスへの入力が始まると消えるテキストボックスの既定のテキストです (図 6 を参照)。次のコードは、こうした効果を得るための方法を示します。

図 6 入力エクステンダ
図 6 入力エクステンダ(画像を縮小するには、ここをクリックします)
図 6 入力エクステンダ
図 6 入力エクステンダ (画像を拡大するには、ここをクリックします)

<act:TextBoxWatermarkExtender runat="server" ID="TextBoxWatermark1"
     TargetControlID="TextBox1"
     WatermarkText="Type Last Name Here"
     WatermarkCssClass="watermarked" />

エクステンダは focus、blur、および keydown イベントを処理するスクリプト コードを埋め込みます。コントロールがフォーカスを得ると、透かしは表示されなくなります。コントロールがフォーカスを失うと、テキストが入力されていない場合は、透かしが再び表示されます。透かしテキストは、ページがポストバックされると消去されます。これを行うには、DOM の送信イベントのハンドラを使用します。透かしエクステンダ コントロールを使用すると、コントロールに適用するテキストと CSS スタイルを指定できます。特殊な CSS スタイルを使用して透かしテキストと通常の入力テキストを視覚的に差別化する必要がある場合があります。

HTML には、データを受け入れる 1 つの主要な要素である <input> 要素があります。数値、文字列、日付、通貨の値などのためにこの要素を使用する必要があります。多くの場合は、数値入力を特定の値の範囲に制限する必要があります。確かに検証レイヤを使用してこのビジネス ルールを実施できますが、入力ボックスで実行する方が適切です。

Slider および NumericUpDown エクステンダでは、ユーザーは、特定の範囲に該当する数値のみを入力できます。Slider エクステンダでは、関連付けられている TextBox が非表示になり、最小値と最大値の間の各目盛りを移動するグラフィカルなスライダで置き換えられます。基になる TextBox は、プログラムで必ず適切な任意の値に設定されます。ただし、割り当てられる値が、スライダで設定される数値の範囲と互換性を持つ必要があります。互換性がない場合、スライダは暗黙的に数値を修正します。次の例について検討してみましょう。

<act:SliderExtender runat="server" ID="SliderExtender1"
     TargetControlID="Income"
     Minimum="0"
     Maximum="200000"
     Steps="41"
     BoundControlID="IncomeAsText" />

Income (収入) という TextBox コントロールにスライダが適用され、0 ～ 200000 の範囲の値が視覚的に割り当てられます。Steps プロパティは、提供する必要のある目盛りの数を示します。上の設定では、スライダが 5000 ずつ移動します。スライダをサーバー上でプログラムにより設定すると、まず値が数値に変換され、次に最も近い目盛りの値にマッピングされます。文字列を渡した場合、スライダはその文字列を無視し、既定で範囲の初期値に設定します。JavaScript を使用してクライアント上の基になるテキストボックスの値を設定すると、次のポストバックでも値が正しく認識されますが、ユーザー インターフェイスには直ちに反映されません。クライアント内からスライダの値をプログラムで変更するには、次のコードを使用します。

$find("SliderExtender1").set_Value(145678);

$get ではなく $find を使用するのはなぜでしょうか。$get 関数は、document.getElementById を省略したものです。このため、DOM 要素のみを検索できます。$find 関数は Sys.Application.findComponent を表し、プログラムで作成された Microsoft AJAX Library の任意のコンポーネントに適用されます。次のスクリプトを使用すると、エクステンダがページに追加されます。

Sys.Application.add_init(
  function() {
      $create(AjaxControlToolkit.SliderBehavior,
           {"BoundControlID":"IncomeAsText",
            "Maximum":200000,
            "Steps":41,
            "id":"SliderExtender1"},
           null,
           null,
           $get("Income"));
  }
);

これは Microsoft AJAX Library コンポーネントであるため、$find 関数を使用してのみ検索できます。

BoundControlID プロパティは、現在のスライダ値を動的に表示する DOM 要素を参照します。ほとんどの場合は、このために <span> または Label を使用します。内部的に、スライダでは、INPUT 要素とその他すべての HTML 要素が区別されます。前者の場合は、値プロパティを設定します。それ以外の場合は、一致する DOM 要素の innerHTML プロパティを使用します。

スライダ スクリプトは基になる TextBox を非表示にするため、ページの読み込み中に要素が点滅する場合があります。CSS スタイルを使用して、宣言で要素を非表示にすることができます。最後に、通常のテキストボックスはインラインで表示されるのに対して、スライダのボックスはブロックで配置されることに注意してください。

NumericUpDown エクステンダは、バインドされた使用可能な値の一覧の次の値または前の値を選択します。名前が示す内容とは無関係に、NumericUpDown は数量を指定するだけのツールではありません。RefValues プロパティを使用して、一覧の値を作成できます。

<act:NumericUpDownExtender ID="UpDown1" runat="server"
     Width="120"
     RefValues="None;1;2;3;4;5;More than 5;"
     TargetControlID="Children" />

NumericUpDown は、リモート サービスから参照値を取得できます。ユーザーが上向きボタンまたは下向きボタンをクリックするたびに、サービスが非同期に呼び出されます。サービスがスクリプトから呼び出し可能であり、次のシグネチャを持つメソッドを含む必要があることを覚えていてください。

public int YourMethod(int current, string tag)

ServiceDownPath、ServiceDownMethod、ServiceUpPath、および ServiceUpMethod の各プロパティを通じてリモート サービスを使用するように、エクステンダを構成します。最後に、スクリプトを介して上向きボタンおよび下向きボタンを自動生成する必要はありません。これらのボタンには、ページで定義され、TargetButtonDownID および TargetButtonUpID プロパティを通じて参照されるボタンを使用できます。

確認とモダリティ

重要なサーバーの手順を実行するデータを収集する入力フォームは、ユーザーにその目的を確認する機会を提供した後の方が、より適切に開始されます。たとえば、サーバー上のデータを永続的に削除する前に確認ステップを実装することが賢明です。クライアント側の確認ステップは、モーダル ダイアログ ボックスなどをポップアップ表示することにより、JavaScript によってのみ実装可能です。最も単純な方法では、次のように ConfirmButton エクステンダを使用します。

<act:ConfirmButtonExtender ID="ConfirmButtonExtender1" runat="server"
     TargetControlID="Button1"
     ConfirmText="Are you sure you want to
                 "send this data?&#10;Really sure?" />

エクステンダはバインドされたボタン コントロール (Button だけでなく、LinkButton や ImageButton も) のクリック イベントをフックし、警告メッセージ ボックスをポップアップ表示します。このメッセージをキャンセルすると、ポストバックは実行されません。メッセージの表示にはブラウザの window.alert メソッドが使用されます。エクステンダなしで手動で同じ機能のコードを作成できることは明白ですが、エクステンダの方が単純で再利用しやすくなります。

より充実したユーザー インターフェイスをさらにカスタマイズするには、他の部分に注目する必要があります。ModalPopup エクステンダは、調べてみる価値のあるエクステンダです。ModalPopup ではマークアップのコードにモダリティが追加され、通常はパネルが使用されます。ModalPopup は、ボタン コントロールにバインドされており、指定の DIV タグをポップアップ表示し、基になるページを無効にします。最上位の DIV タグの要素は、この要素が提供するユーザー インターフェイスを使用する場合以外は、取り消すことはできません。最上位の DIV 内の要素以外の要素をクリックするアクションは失われ、いつまでも目的のターゲットに到達しません。ModalPopup が実行するスマートなトリックによって、表示されない DIV が追加され、ブラウザ ウィンドウ全体を網羅します。このレイヤでは、すべてのユーザー アクションを取り消し、そのアクションが基になるコントロールに到達しないようにします。巧妙な CSS コーディングを使用すると、最上位の DIV の下にある要素を灰色表示にするなどの効果を追加できます (図 7 を参照)。

図 7 ModalPopup エクステンダ
図 7 ModalPopup エクステンダ(画像を縮小するには、ここをクリックします)
図 7 ModalPopup エクステンダ
図 7 ModalPopup エクステンダ (画像を拡大するには、ここをクリックします)

以下の ModalPopup エクステンダのコードでは、PopupControlID プロパティはポップアップ表示されるマークアップ要素を直接参照しています。

<act:ModalPopupExtender ID="ModalPopupExtender1" runat="server"
     TargetControlID="LinkButton1"
     PopupControlID="Panel1"
     BackgroundCssClass="modalBackground"
     OkControlID="Button1"
     OnOkScript="yes()"
     OnCancelScript="no()"
     CancelControlID="Button2" />

多くの場合、この要素に ASP.NET パネル コントロールまたはプレーンな DIV 要素を使用する傾向にあります。BackgroundCssClass プロパティは、モーダル ダイアログが有効な間、基になるページに適用される CSS スタイルを指定します。図 7 に示す灰色表示の効果を得るには、次の属性を使用する必要があります。

.modalBackground
{
    background-color:Gray;
    filter:alpha(opacity=70);
    opacity:0.7;
}

ほぼすべてのモーダル ダイアログ ボックスには、[OK] ボタンと [キャンセル] ボタンがペアで備わっています。これらのボタンにリンク、プッシュ、イメージの各ボタンを使用したり、任意のテキスト ラベルを使用したりすることができます。OkControlID プロパティおよび CancelControlID プロパティを使用して、パネルのどのコントロールが [OK] ボタンまたは [キャンセル] ボタンを表すかをエクステンダに通知します。同様に、OnOkScript プロパティおよび OnCancelScript プロパティを使用して、ユーザーがモーダル ダイアログ ボックスをクリックして確定するか、または取り消したときに実行される JavaScript 関数を示します。

拡張されたチェックボックス

MutuallyExclusiveCheckBox は、ユーザーによる正しいデータの入力を促進し、システムの余分なクライアント検証ステップを省略するのに役立つエクステンダです。このエクステンダでは、チェックボックスの論理グループを定義し、各グループで 1 つだけチェックマークを付けるようにできます。

<asp:CheckBox runat="server" ID="beginner" Text="Beginner" />
<asp:CheckBox runat="server" ID="intermed" Text="Intermediate" />
<act:MutuallyExclusiveCheckBoxExtender runat="server" ID="Mutual1"
     TargetControlID="beginner"
     Key="Expertise" />
<act:MutuallyExclusiveCheckBoxExtender runat="server" ID="Mutual2"
     TargetControlID="intermediate"
     Key="Expertise" />

このエクステンダでは、Expertise という名前の相互に排他的な 2 つのチェックボックスのグループを定義しています。通常では、複数の選択を許可するにはチェックボックスを使用し、ユーザーに 1 つのオプションのみを強制的に選択させるにはラジオ ボタンを使用します。それなら、ここで RadioButtonList コントロールを使用しないのはなぜでしょうか。お分かりのように、RadioButtonList コントロールでは、すべてのオプションが選択解除された初期状態を指定できますが、一度選択すると、すべてが選択解除された状態に戻すことができません。つまり、ある項目の選択を解除するには別の項目を選択するしかなく、常にいずれかの項目が選択されています。MutuallyExclusiveCheckbox エクステンダを使用するチェックボックスのグループには、このような制限がありません。

チェックボックスは、通常、ブラウザに埋め込まれているビットマップを使用して表示されます。プログラムでビットマップを変更することは、公開されているパブリック プロパティがないため、簡単ではありません。ToggleButton エクステンダでは、バインドされているチェックボックス要素を非表示にし、チェックボックスのレイアウトをエミュレートする、動的に生成されるマークアップのコードで置き換えます。ただし、チェックボックスがオンおよびオフの場合にユーザー定義ビットマップのペアが使用される場合は除きます。

act:ToggleButtonExtender ID="ToggleButtonExtender1" runat="server"
    TargetControlID="CheckBox1"
    ImageHeight="19"
    ImageWidth="19"
    UncheckedImageUrl="~/images/DontLike.gif"
    CheckedImageUrl="~/images/Like.gif" />

ToggleButton エクステンダを CheckBoxList コントロールに適用することはできません。このエクステンダは個別のチェックボックスでのみ動作します。標準のチェックボックスの外観を変更することができるのは良いことです。

制限された入力

ユーザーによる間違いを防ぐには、望ましくない文字、無効な式、または不正な型のデータをフィルタで除去することにより制御を行う必要があります。標準の TextBox コントロールを使用すると、文字列をその他の任意のデータ型に変換するサーバーでの検証によって、正しい入力を実施することができます。AJAX では、入力データ型はさらに制御されています。たとえば、カレンダー エクステンダを使用すれば、ユーザーが日付以外のデータを入力することはほぼ不可能になります。ASP.NET にはサーバーのカレンダー コントロールが付属していますが、カレンダー エクステンダはまったく異なります。カレンダー エクステンダは、クライアント上でユーザー インターフェイスを完全に作成し、クライアント上で完全に機能し、ユーザーが月や日を検索して移動するときにポストバックをまったく生成しません。さらに、特定のブラウザが JavaScript をサポートしていない場合は、従来のテキストボックスが表示されます。

<act:CalendarExtender ID="CalendarExtender1" runat="server"
     TargetControlID="Birth"
     Format="dd/MM/yyyy" />

上のコードは、関連付けられているテキストボックスにフォーカスが移ったときにポップアップ カレンダーを表示するのに十分なコードです。または、ユーザーがページ ボタンをクリックしたときにポップアップを表示できます。ボタンの ID は、PopupButtonID プロパティを使用して設定します。Format プロパティは、ユーザーがカレンダー ポップアップを取り消したときにテキストボックスに書き込む日付の形式を示します (図 8 を参照)。

図 8 Calendar エクステンダ
図 8 Calendar エクステンダ

FilteredTextBox エクステンダは、ユーザーがテキストボックスに無効な文字を入力するのを防ぎます。基本的には、キーボード操作をフックし、不要なキーストロークをフィルタで除外する JavaScript を追加します。エクステンダを構成して、特定の文字を拒否したり、指定した文字だけを受け入れたりするようにできます。この設定では、FilterMode プロパティを使用します。このプロパティは、適用するフィルタのタイプ (数値のみ、小文字または大文字のみ、および文字のカスタム セット) を特定します。透かし付きの TextBox は数値のみを受け入れるようにフィルタリングできないことに注意する必要があります。フィルタ レイヤでは、実際、設定された透かしテキストは自動的に削除されます。

より良い Web ページのために

エクステンダは、ASP.NET コントロールに動作を追加するための強力でスマートな方法です。ACT では、組み込みコントロールを強化するための 30 を超えるエクステンダを提供します。ACT には、アニメーション、ドラッグ アンド ドロップ機能、オートコンプリートを追加するエクステンダのほかに、最も重要な入力機能を強化するためのエクステンダがあります。ACT のダウンロードでは、オリジナルのソース コード付きまたはソース コードなしでダウンロードできます。また、ほとんどの複雑なエクステンダは相互に依存しています。たとえば、Calendar エクステンダには Popup エクステンダが必要です。

このコラムでは、より効果的なページを作成できる最も単純で一般的に使用されるエクステンダを紹介しました。次のコラムでは、マスク編集やオートコンプリートなどのより高度な機能について説明します。

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

Dino Esposito は Solid Quality Learning の指導者であり、『Introducing ASP.NET AJAX』(Microsoft Press、2007 年) の著者です。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。連絡先は cutting@microsoft.com です。また、weblogs.asp.net/despos のブログにも参加してみてください。


Wicked Code: ASP.NET AJAX を使用してドラッグ アンド ドロップする

Wicked Code
ASP.NET AJAX を使用してドラッグ アンド ドロップする
Jeff Prosise

コードのダウンロード : : WickedCode2008_01.exe (296KB)
コンテンツ
ドラッグ ドロップの動作
DragDropManager
ドラッグ元を構築する
ドロップ先を構築する
ドラッグ元とドロップ先を作成する
ドラッグ アンド ドロップ以外の機能

AJAX は Web ユーザー インターフェイスに大きな変革をもたらし、Visual Studio® のユーザーは ASP.NET AJAX によって AJAX を利用できるようになりました。ASP.NET AJAX は 3 つのダウンロードで構成されています。テスト済みの AJAX の中核機能のセットを提供する ASP.NET AJAX Extensions (asp.net/ajax/downloads)、製品グループがフィードバックを希望する試験的な機能を含む ASP.NET AJAX Futures (asp.net/downloads/futures)、およびさまざまな AJAX コントロールと独自のコントロールを作成するための SDK を提供する ASP.NET AJAX Control Toolkit (asp.net/ajax/ajaxcontroltoolkit/samples) です。

この 3 つのうち、Futures のリリースは開発者コミュニティからの注目が最も低いものでした。残念なことですが、これは Futures の Community Technology Preview (CTP) が、ASP.NET AJAX の今後のバージョンがどのようなものであるかを簡単に紹介するだけにとどまらず、最近の最先端の Web アプリケーションを構築するために使用できる機能の集まりであるためです。その適例がドラッグ アンド ドロップです。

Futures の隠れた機能として、PreviewDragDrop.js ファイルにはブラウザ ベースの優れたドラッグ アンド ドロップ ユーザー インターフェイスのサポートが用意されています。使用するモデルは、旧 OLE ドラッグ ドロップ モデルにならって作成されています。このモデルでは、ドラッグ元が IDragSource インターフェイスを実装し、ドロップ先が IDropTarget インターフェイスを実装します。システムは、ドラッグ元とドロップ先を接続するドラッグ ドロップ マネージャを提供します。Futures のドラッグ ドロップ マネージャは、Sys.Preview.UI._DragDropManager という JavaScript クラスのインスタンスです。このクラスは、自動的にインスタンス化され、Sys.Preview.UI.DragDropManager というグローバル変数を通じて使用できます。

私は、これまで数か月もの間、PreviewDragDrop.js を使用してカスタム ドラッグ元とカスタム ドロップ先の指定が可能な実際のドラッグ ドロップを実装する方法を示すサンプルを記述しようと考えていました。そしてやっとその機会を得ました。結果は非常に良いできあがりです。私はその過程で、カスタム ドラッグ ビジュアルのサポートを追加して機能を強化する方法など、DragDropManager に関する多くのことを学びました。このモデルに慣れて、JavaScript のクラスの派生とインターフェイスの実装の概念をよく理解すると、DragDropManager による Web UI の可能性が広がります。

ドラッグ ドロップの動作

コードについて説明する前に、この記事に付属のサンプル プロジェクトをダウンロードして、ざっと目を通しておいてください。Visual Studio の [Web サイトを開く] コマンドを使用してプロジェクトを開き、ブラウザでホーム ページ (DragDropDemo.aspx) を表示します。このサンプルを実行するには、ASP.NET AJAX Extensions をインストールする必要があります。Futures の CTP はサイトの Bin フォルダに既に用意されているため、ダウンロードしてインストールする必要はありません。

ページの上部には 5 つの色の見本があり、下部には "Drop It Here" というラベルの付いた空のボックスがあります (図 1 を参照)。色の見本はドラッグ元であり、空のボックスはドロップ先です。いずれかの色の見本をマウスでつかみ、下部のボックスまでドラッグします。カーソルがボックスに入ると、ボックスの色が白から薄い灰色に変わります。これは、ドロップ先の強調表示と呼ばれる機能です。ここで、色の見本をボックスの上にドロップします。ボックスの色が色の見本に合わせて変わります。

図 1 DragDropDemo.aspx の動作
図 1 DragDropDemo.aspx の動作(画像を縮小するには、ここをクリックします)
図 1 DragDropDemo.aspx の動作
図 1 DragDropDemo.aspx の動作 (画像を拡大するには、ここをクリックします)

別の色の見本をマウスでつかみ、画面の周辺に移動します。色の見本をドロップできるのはドロップ先だけであり、カーソルは、ドロップがそこで行われた場合に受け入れられるかどうかを示します。また、色の見本をドラッグするときは、何をドラッグしているのかがわかるように、カーソルの横に色の見本が部分的に透明な状態で表示されます (ドラッグ ビジュアル)。後で説明しますが、ドラッグ ビジュアルを作成するコードを記述します。ただし、カーソルによるドラッグ ビジュアルの移動および各マウスの動きによるカーソルの更新を含むすべての処理は ASP.NET AJAX が行います。

このページを管理するカスタム ロジックは、Web サイトの Scripts フォルダにある ColorDragDrop.js というスクリプト ファイルにあります。ColorDragDrop.js は、ScriptManager が読み込むカスタム スクリプト ファイルです。実際には、ソース ビューで DragDropDemo.aspx を開くと、ScriptManager が 3 つのスクリプト ファイルを読み込みます。このデモで使用する重要な ASP.NET AJAX 基本クラスが格納されている PreviewScript.js、ColorDragDrop.js で使用するドラッグ ドロップ サポートが格納されている PreviewDragDrop.js、および ColorDragDrop.js 自身です。

DragDropManager

ASP.NET AJAX を使用して優れたドラッグ ドロップ ユーザー インターフェイスを作成するための最初の手順は、Sys.Preview.UI._DragDropManager クラス (その後、DragDropManager と呼ばれるようになりました) について理解することです。実際のところ、このクラスは、ホストのブラウザの種類に応じて Sys.Preview.UI.IEDragDropManager または Sys.Preview.UI.GenericDragDropManager のインスタンスにほとんどの処理を委任します。そのパブリック インターフェイスは、10 数個のメソッドで構成されます。そのうちの 3 つは、ドラッグ ドロップの実装において非常に重要な役割を果たします。

  • startDragDrop - ドラッグ ドロップ操作を開始します。
  • registerDropTarget - オブジェクトをドロップ先として登録します。
  • unregisterDropTarget - ドロップ先の登録を解除します。

DragDropManager の主な役割は、startDragDrop メソッドが呼び出されたときに、実行中のマウス イベントを監視し、ドラッグ元およびカーソルの下のドロップ先 (存在する場合) に適切な通知を送信し、現在の場所でドロップするとどうなるかを視覚的に示すようにカーソルを調整することです。そのために、mousemove や mouseup などの重要なイベントの独自のハンドラを登録します。DragDropManager は、これらのイベント ハンドラから、ドラッグ元の IDragSource のメソッドを呼び出し、ドロップ先の IDropTarget のメソッドを呼び出します。

たとえば、ペイロードを通信するカーソルがドロップ先の境界内に移動すると、DragDropManager はドラッグ元の get_dragDataType、get_dragMode、getDragData の各メソッドを呼び出してペイロードの情報を取得します。次に、ドロップ先の canDrop メソッドに結果を渡して、ドラッグ元とドロップ先の間で操作が可能かどうかを判断します。ドロップが行われる場合、DragDropManager は drop メソッドを呼び出し、ドラッグ元から取得したデータをもう一度渡すことにより、ドロップ先に通知を送信します。

DragDropManager では、ドロップ先として登録されたオブジェクトへの参照の内部配列を保持しているため、カーソルがドロップ先の上にあるかどうかを判断できます。オブジェクトを配列に追加するには DragDropManager の registerDropTarget メソッドが呼び出され、削除するには unregisterDropTarget が呼び出されます。ドロップ先の登録を解除して、DragDropManager がそれらのドロップ先の代わりに登録する関連イベント ハンドラの登録を解除できるようにすることが重要です。この処理が行われていないと、メモリ リークが発生することがあります。

DragDropManager は、ドラッグ元とドロップ先を結び付ける役割を果たします。DragDropManager を使用しないと、ドラッグ アンド ドロップ ユーザー インターフェイスを動作させるために (特にさまざまなブラウザでインターフェイスを使用する場合は) 大量のコードを記述しなければならないでしょう。

ドラッグ元を構築する

ドラッグを開始する前に、ドラッグする対象 (ドラッグ元) を指定する必要があります。ASP.NET AJAX では、ドラッグ元は、PreviewDragDrop.js で定義された IDragSource インターフェイスを実装する単なるオブジェクトです。図 2 は、そのインターフェイスに属するメソッドを示しています。

Figure 2 IDragSource のメソッド (Close Figure 2)

メソッド | 説明
get_dragDataType | ドラッグされるデータの種類を返します。
getDragData | ドラッグされるデータを返します。
get_dragMode | ドラッグ モードを返します (移動またはコピー)。
onDragStart | ドラッグ元のドラッグを開始するときに呼び出されます。
onDrag | ドラッグ元がドラッグされるときに繰り返し呼び出されます。
onDragEnd | ドラッグの終了時に呼び出されます。

JavaScript では明示的にインターフェイスをサポートしていないとすると、JavaScript にインターフェイスを実装するのは奇妙に思えるかもしれません。さらに言えば、クラス、継承、名前空間、およびオブジェクト指向プログラミング (OOP) のその他の原則も明示的にサポートされていません。また、ASP.NET AJAX の半数を占めるクライアントである Microsoft® AJAX Library は、JavaScript に完全に実装されたクラス ライブラリです。これは、普及している JavaScript プログラミング手法を使用する OOP のフェイクであり、独自のクラスの作成や、それらのクラスでのインターフェイスの実装、および別のクラスからのクラスの派生に使用できます。

ColorDragDrop.js には、JavaScript クラスのペア (ColorDragSourceBehavior と ColorDropTargetBehavior) が格納されています。ColorDragSourceBehavior は IDragSource を実装します。両方とも、Custom.UI という名前空間に属しています。

Microsoft AJAX Library 形式の JavaScript クラスを作成するには、まず、クラス名と同じ名前の関数を定義します。この関数は、クラス コンストラクタとして機能します。

Custom.UI.ColorDragSourceBehavior = function(element, color)
{
    Custom.UI.ColorDragSourceBehavior.initializeBase(this, [element]);
    this._mouseDownHandler = Function.createDelegate(this,
        this.mouseDownHandler);
    this._color = color;
    this._visual = null;
}

次に、JavaScript プロトタイプ プロパティを使用して、クラスのすべてのインスタンスに含めるメソッドを定義します。

Custom.UI.ColorDragSourceBehavior.prototype =
{
    // Methods here
}

JavaScript では厳密な型指定や型のリフレクションをサポートしないため、それらは Microsoft AJAX Library によって行われます。registerClass や registerNamespace などの Type クラス (MicrosoftAjax.js に存在し、ASP.NET AJAX の中核を成します) のメソッドを使用すると、作成したクラスを登録して、それらを名前空間に割り当てることができます。後から、Microsoft AJAX Library によって JavaScript の組み込みの Object 型に追加される getTypeName などのメソッドを使用して、登録した型を取得できます。

Type クラスが提供するインフラストラクチャのもう 1 つの重要な要素は、基本クラス メソッドを、派生クラスの対応するメソッドから呼び出すメカニズムです。クラス コンストラクタから、initializeBase を呼び出して、基本クラスのコンストラクタを呼び出します。また、派生クラスのメソッド オーバーライドから、callBaseMethod を呼び出して、基本クラスの同等のメソッドを呼び出します。

図 3 の ColorDragSourceBehavior クラスでは、これらのすべてのツールを使用して、通常の DOM 要素をドラッグ元に変換するために必要なロジックを 1 つの使いやすいクラスにカプセル化します。registerClass の呼び出しからは、ColorDragSourceBehavior が、IDragSource インターフェイスおよび MicrosoftAjax.js で定義されている Sys.UI.Behavior という ASP.NET AJAX 基本クラスから派生していることがわかります。IDragSource では ColorDragSourceBehavior をドラッグ元として機能させることができます。Sys.UI.Behavior では、ColorDragSourceBehavior のインスタンスを DOM 要素に追加してその要素の動作を変更できます。

Figure 3 ColorDragSourceBehavior (Close Figure 3)
Custom.UI.ColorDragSourceBehavior = function(element, color)
{
    Custom.UI.ColorDragSourceBehavior.initializeBase(this, [element]);
    this._mouseDownHandler = Function.createDelegate(this,
        this.mouseDownHandler);
    this._color = color;
    this._visual = null;
}

Custom.UI.ColorDragSourceBehavior.prototype =
{
    // IDragSource methods
    get_dragDataType: function()
    {
        return 'DragDropColor';
    },

    getDragData: function(context)
    {
        return this._color;
    },

    get_dragMode: function()
    {
        return Sys.Preview.UI.DragMode.Copy;
    },

    onDragStart: function() {},

    onDrag: function() {},

    onDragEnd: function(canceled)
    {
        if (this._visual)
            this.get_element().parentNode.removeChild(this._visual);
    },

    // Other methods
    initialize: function()
    {
        Custom.UI.ColorDragSourceBehavior.callBaseMethod(this,
            'initialize');
        $addHandler(this.get_element(), 'mousedown',
            this._mouseDownHandler)
    },

    mouseDownHandler: function(ev)
    {
        window._event = ev; // Needed internally by _DragDropManager

        this._visual = this.get_element().cloneNode(true);
        this._visual.style.opacity = '0.4';
        this._visual.style.filter =
          'progid:DXImageTransform.Microsoft.BasicImage(opacity=0.4)';
        this._visual.style.zIndex = 99999;
        this.get_element().parentNode.appendChild(this._visual);
        var location =
            Sys.UI.DomElement.getLocation(this.get_element());
        Sys.UI.DomElement.setLocation(this._visual, location.x,
            location.y);

        Sys.Preview.UI.DragDropManager.startDragDrop(this,
            this._visual, null);
    },

    dispose: function()
    {
        if (this._mouseDownHandler)
            $removeHandler(this.get_element(), 'mousedown',
                this._mouseDownHandler);
        this._mouseDownHandler = null;
        Custom.UI.ColorDragSourceBehavior.callBaseMethod(this,
            'dispose');
    }
}

Custom.UI.ColorDragSourceBehavior.registerClass
    ('Custom.UI.ColorDragSourceBehavior', Sys.UI.Behavior,
    Sys.Preview.UI.IDragSource);

get_dragDataType や getDragData など、ColorDragSourceBehavior が実装するほとんどのメソッドは、IDragSource インターフェイスのメンバです。ただし、ColorDragSourceBehavior は、IDragSource のメソッドではないメソッドをいくつか実装します。たとえば、initialize メソッドは mousedown イベントのハンドラを登録し、ドラッグ元から DragDropManager.startDragDrop を呼び出してドラッグ ドロップ操作を開始できるようにします。dispose メソッドは、イベント ハンドラの登録を取り消して、メモリ リークが発生しないようにします。

IDragSource のメソッド自体は非常に単純です。get_dragDataType は、ColorDragSourceBehavior がドロップ先に提供するデータの種類 (この場合は色のデータ) を特定するための "DragDropColor" という文字列を返します。getDragData は、ドラッグ元によって表される色を定義する文字列を返します。get_dragMode は、ドロップがそこで行われた場合に、ドラッグ元が移動ではなくコピーされることを示す Sys.Preview.UI.DragMode.Copy を返します (代わりに Sys.Preview.UI.DragMode.Move を使用することもできます。これは、PreviewDragDrop.js で Sys.Preview.UI.DragMode.Copy と共に定義されます)。

ColorDragSourceBehavior でもう 1 つ注目すべきことは、ドラッグ ビジュアルの処理方法です。mousedown イベント ハンドラは、DragDropManager で startDragDrop を呼び出すことによりドラッグ ドロップ操作を開始します。ただし、startDragDrop を呼び出す前に、ハンドラは cloneNode を呼び出して、ドラッグ元によって表される DOM 要素を複製します。次に、ハンドラは複製されたノードの不透明度を 40% に設定し、ノードをブラウザの DOM に挿入して、新しいノードへの参照を startDragDrop のパラメータとして DragDropManager に渡します。現在、DragDropManager ではドラッグ ビジュアルを管理します。具体的には、ドラッグ ビジュアルがカーソルの横に表示されるように、その動きをアニメーション化します。

ドロップ先を構築する

ColorDragDrop.js の ColorDropTargetBehavior クラス (コード サンプルに含まれています) は、DOM 要素をドロップ先に変換するロジックをカプセル化します。このクラスは Sys.UI.Behavior から派生するため、DOM 要素に追加できます。また、IDropTarget インターフェイスを実装するため、ドロップ先として機能できます。図 4 は、IDropTarget のメンバを示しています。

Figure 4 IDropTarget のメソッド (Close Figure 4)

メソッド | 説明
get_dropTargetElement | ドロップ先である DOM 要素への参照を返します。
canDrop | ドロップが受け入れられるかどうかを示す true または false を返します。
drop | ドロップ先の上にドロップが行われるときに呼び出されます。
onDragEnterTarget | ペイロードを通信するカーソルがドロップ先に入るときに呼び出されます。
onDragInTarget | ペイロードを通信するカーソルがドロップ先の中に移動するときに呼び出されます。
onDragLeaveTarget | ペイロードを通信するカーソルがドロップ先から出るときに呼び出されます。

ColorDropTargetBehavior の onDragEnterTarget メソッドと onDragLeaveTarget メソッドは、前述のドロップ先の強調表示を実行します。ペイロードを通信するカーソルがドロップ先に入ると、DragDropManager は onDragEnterTarget を呼び出します。ペイロードの種類が適切な (DragDropColor) 場合、onDragEnterTarget はドロップ先のフィールド内の現在の背景色を保存し、背景色を薄い灰色に変更します。カーソルがドロップ先から出たときに、引き続きペイロードを通信している状態の場合、DragDropManager はドロップ先の onDragLeaveTarget メソッドを呼び出します。そのメソッドの ColorDropTargetBehavior の実装により、ドロップ先の元の背景色が復元されます。

ペイロードを通信するカーソルがドロップ先の上を移動するたびに、DragDropManager はドロップ先の canDrop メソッドを呼び出して、ドロップ先がドロップを受け入れるかどうかを判断します。ColorDropTargetBehavior.canDrop は、ペイロードの種類をチェックして、DragDropColor の場合は true を返し、そうでない場合は false を返します。このプロセスにより、カーソルを介して、必要な視覚的なフィードバックを DragDropManager クラスからユーザーに提供できます。

ドロップが行われる場合は、ドロップ先の drop メソッドが呼び出されます。ColorDropTargetBehavior.drop メソッドは、ドラッグ元から色を抽出し、その色に応じた独自の背景色 (実際には、ドロップ先として機能する DOM 要素の色) を設定します。

drop メソッドに渡される dragMode パラメータは、ドラッグ モード (移動またはコピー) を指定します。最終的には、ドラッグ モードに従うためにブラウザの DOM を操作するかどうかはドラッグ先が決定します。DragDropDemo.aspx でサポートされるのはコピー モードのみです。そのため、ドロップ先は、独自の背景色をドラッグ元に含まれる色に設定することにより、ドラッグ元を "コピー" します。移動モードもサポートされていると仮定すると、ドロップ先では、ドロップが正常に完了した後にドラッグ モードをチェックして、dragMode が Sys.Preview.UI.DragMode.Move に設定されている場合は、ドラッグ元に関連付けられている DOM 要素を削除 (または位置変更) する必要があります。

ドラッグ元とドロップ先を作成する

ColorDragSourceBehavior と ColorDropTargetBehavior が実装されると、ドラッグ元とドロップ先を含むページの作成が簡単になります。図 5 は、DragDropDemo.aspx の関連するマークアップとコードを示しています。

Figure 5 DragDropDemo.aspx からの抜粋 (Close Figure 5)
<asp:ScriptManager ID="ScriptManager1" runat="server">
    <Scripts>
        <asp:ScriptReference Name="PreviewScript.js"
            Assembly="Microsoft.Web.Preview" />
        <asp:ScriptReference Name="PreviewDragDrop.js"
            Assembly="Microsoft.Web.Preview" />
        <asp:ScriptReference Path="~/Scripts/ColorDragDrop.js" />
    </Scripts>
</asp:ScriptManager>

<fieldset>
    <legend>Pick a Color</legend>
    <table cellpadding="8">
        <tr>
            <!-- Drag sources -->
            <td><div id="RedDragSource"
    style="width: 32px; height: 32px; background-color: red" /></td>
            <td><div id="YellowDragSource"
    style="width: 32px; height: 32px; background-color: yellow" /></td>
            <td><div id="GreenDragSource"
    style="width: 32px; height: 32px; background-color: green" /></td>
            <td><div id="MagentaDragSource"
    style="width: 32px; height: 32px; background-color: magenta" /></td>
            <td><div id="BlueDragSource"
    style="width: 32px; height: 32px; background-color: blue" /></td>
        </tr>
    </table>
</fieldset>

<div style="width: 100%; height: 128px">&nbsp;</div>

<fieldset>
    <legend>Drop It Here</legend>
    <!-- Drop target -->
    <div id="DropTarget" style="width: 100%; height: 196px">
        &nbsp;
    </div>
</fieldset>

<script type="text/javascript">
function pageLoad()
{
    //
    // Make the "DragSource" DIVs drag sources.
    //
    var source1 = new Custom.UI.ColorDragSourceBehavior
        ($get('RedDragSource'), 'red');
    var source2 = new Custom.UI.ColorDragSourceBehavior
        ($get('YellowDragSource'), 'yellow');
    var source3 = new Custom.UI.ColorDragSourceBehavior
        ($get('GreenDragSource'), 'green');
    var source4 = new Custom.UI.ColorDragSourceBehavior
        ($get('MagentaDragSource'), 'magenta');
    var source5 = new Custom.UI.ColorDragSourceBehavior
        ($get('BlueDragSource'), 'blue');

    source1.initialize();
    source2.initialize();
    source3.initialize();
    source4.initialize();
    source5.initialize();

    //
    // Make the "DropTarget" DIV a drop target.
    //
    var target = new Custom.UI.ColorDropTargetBehavior
        ($get('DropTarget'));
    target.initialize();
}
</script>

ドラッグ元として機能する色の見本は、赤、黄、緑などに設定された背景色を持つ DIV にすぎません。JavaScript の pageLoad 関数は、5 つの ColorDragSourceBehavior オブジェクトをインスタンス化し、それぞれをいずれかの DIV に割り当てることによって、DIV をドラッグ元に変換します。ドラッグ元の initialize メソッドを呼び出すと、各ドラッグ元で mousedown イベント ハンドラを登録できるようになります。

ドロップ先も DIV です。あるステートメントが ColorDropTargetBehavior オブジェクトをインスタンス化し、そのインスタンスを DIV に関連付けて、DIV をドロップ先に変換します。2 つ目のステートメントが ColorDropTargetBehavior の initialize メソッドを呼び出し、ドロップ先として DragDropManager に登録できるようにします。

ColorDragSourceBehavior と ColorDropTargetBehavior は、色をドラッグ アンド ドロップできるように、特別に構築されています。これらと同様のクラスを構築して、他のドラッグ ドロップのシナリオをサポートすることができます。たとえば、写真共有サイトを構築しているときに、ユーザーがフォルダ間で写真をドラッグして写真を整理できるようにするとします。この場合、DIV を使用してフォルダを表現し、PhotoDropTargetBehavior クラスを記述して DIV をドロップ先に変換します。これに応じて、PhotoDragSourceBehavior クラスを構築して、DOM イメージ要素をドラッグ可能な写真にすることができます。構築するクラスは、アプリケーション固有のものになりますが、このコラムで紹介したドラッグ元とドロップ先のクラスで示す場合と同じパターンに従います。

ドラッグ アンド ドロップ以外の機能

ASP.NET AJAX は AJAX の便利な機能を Web ページに追加するためのすばらしいツールであり、ASP.NET AJAX Futures のリリースによってコア プラットフォームの機能が強化されます。ドラッグ アンド ドロップ ユーザー インターフェイスのサポートはその一例です。他にも、アニメーション、クライアント側のデータ バインド、およびそれ以外の便利な DOM の抽象化があります。これらの機能がいつの日か中心的な存在になるかどうかはともかく、現時点では非常に役立ちます。次に作成する ASP.NET AJAX アプリケーションをより優れたものにするためにも、これらの機能の使用を検討してください。

Futures PreviewGlitz.js ファイルのクラスを使用して Web ページにすてきなアニメーションを追加する方法の別の例については、私のブログの記事「Creating Sophisticated Animations with the Microsoft AJAX Library (Microsoft AJAX Library を使用して洗練されたアニメーションを作成する)」(wintellect.com/cs/blogs/jprosise/archive/2007/04/04/Creating-Sophisticated-Animations-with-the-Microsoft-AJAX-Library.aspx) を参照してください。

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

Jeff Prosise は、MSDN Magazine に寄稿している編集者で、『プログラミング Microsoft .NET』(日経 BP ソフトプレス、2002 年) など数冊の著書があります。また、ソフトウェアのコンサルティングおよび Microsoft .NET Framework 専門の教育機関である Wintellect (www.wintellect.com) の共同創立者でもあります。Jeff の連絡先は、wicked@microsoft.com です。


基礎: 非コモン コントロール用のテンプレート

基礎
非コモン コントロール用のテンプレート
Charles Petzold

コードのダウンロード : : Foundations2008_01.exe (784KB)
コンテンツ
要素とコントロール
既定のテンプレートと DLL
コントロール派生の基本
要素とコントロールの階層
コードおよび XAML
コマンドを生成する
テンプレートを置き換える

コモン コントロールを一般的ではない視覚オブジェクトに変換する方法を活用するユーザーのために、Windows® Presentation Foundation (WPF) では、テンプレートとして知られているすばらしい機能を提供します。従来から、コントロールの機能と視覚的な外観は、コントロールのコードの深い内部で結び付けられていました。WPF では、コントロールの機能は依然としてコード内で実装されますが、外観はそのコードから分離され、XAML で定義されているテンプレートの形式を取ります。新しいテンプレート (ほとんどの場合、XAML ではコードを含まない) を作成することにより、プログラマや設計者は、コントロールのコードを変更する必要なく、コントロールの視覚的な外観を完全に改訂できます。

1 年前の最初のコラムでは、ScrollBar、ProgressBar、および Slider の各コントロールのテンプレートを設計する方法を説明しました。ただし、テンプレート作成機能には、次のような裏側の面もあります。つまり、新しいカスタム コントロールを設計する場合、コントロールの視覚的な外観用の既定のテンプレートを提供し、さらに、コントロールを使用するプログラマがそのテンプレートを置き換えることができるようにする必要があるということです。このようなコントロールの構造化は厳密には必要ありません。実際、私の著書『Applications = Code + Markup』(Microsoft Press®、2006 年) では、置き換え可能なテンプレートを定義しているカスタム コントロールはありません。ただし、読者の皆さんを含め、そういったコントロールを使用する必要のあるユーザーにとっては、コントロールで置き換え可能なテンプレートが定義されていた方がはるかに満足でしょう。

このコラムの目的は、完全に機能し、人目をひくコントロールを作成することではありません。ダイナミック リンク ライブラリで配布されるコントロール用の置き換え可能な既定のテンプレートを定義するしくみを確立することです。ここで説明するテンプレート作成技法の多くは、既存の WPF コントロールを調査することによって学習したものです。同じコードを作成する場合は、『Applications = Code + Markup』の第 25 章の DumpControlTemplate プログラムを使用すると、便利な XAML 形式ですべての標準 WPF コントロールの既定のテンプレートを抽出できます。

要素とコントロール

前の Windows クライアント プログラミング環境での経験のあるプログラマはすぐに、WPF クラス階層の変わった点に気づきます。ネイティブの Windows API では、たとえば、画面上の視覚的な外観を持つものはすべて "window" として分類されますが、Windows フォームでは、すべて "control" として分類されます。ただし、WPF では、Control クラスは FrameworkElement から派生します。これは、最も有名な TextBlock、Image、Decorator、および Panel など他の多くの視覚オブジェクトと同じです。では、厳密に要素とコントロールの違いとは何でしょうか。

1 番目に、Control クラスは、Foreground、Background、および 5 つのフォント関連プロパティを含む非常に便利なプロパティのコレクションを FrameworkElement クラスに追加します。Control はこれらのプロパティを直接使用しません。これらのプロパティは Control から派生するクラスに役立ちます。

2 番目に、Control クラスは IsTabStop プロパティと TabIndex プロパティを追加します。これは、コントロールが多くの場合、タブ キーのナビゲーション チェーン内で停止される必要があることを示唆していますが、要素にはその必要がありません。要するに、要素は検索対象であり、コントロールは操作対象であるということです (ただし、要素でもフォーカスを取得したり、キーボード、マウスおよびスタイラス ペンの入力に応答したりすることができます)。

3 番目に、Control クラスは ControlTemplate 型の Template プロパティを定義します。このテンプレートは、主に、視覚的な外観を構成する要素とコントロールのビジュアル ツリーであり、多くの場合、プロパティの変更とイベントに基づいてこの視覚的な外観を変えるトリガを含んでいます。

この 3 番目の機能からわかるように、Control から派生するクラスにはカスタマイズ可能な外観がありますが、FrameworkElement から派生するその他のクラスにはカスタマイズ可能な外観がありません。特に、TextBlock と Image には視覚的な外観はありますが、これらのビジュアルをカスタマイズすることは合理的ではありません。これらの要素は、表示する書式付きテキストまたはビットマップに何も追加しないためです。その一方で、ScrollBar はさまざまな外観を持ちながら、機能は同じです。これこそが、テンプレートの目的です。

プログラマにとっては、おそらくこれが、要素とコントロールの最大の相違点です。FrameworkElement から派生する場合、画面上に要素の視覚要素とその子要素をレンダリングするには、MeasureOverride、ArrangeOverride、および OnRender をオーバーライドする必要があります。Control から派生する場合、コントロールのビジュアルは Template プロパティの ControlTemplate オブジェクト内でビジュアル ツリーによって定義されるため、通常これらのメソッドをオーバーライドする必要はありません。

WPF には、ContentControl を使用して Control から派生する UserControl というクラスが含まれます。 この UserControl は、多くの場合、単純なカスタム コントロールの基本クラスとして推奨され、多くの目的で適切に機能します。たとえば、著書の第 25 章の DatePicker コントロールは、UserControl から派生を行います。ただし、Control と UserControl との重要な違いに注意してください。UserControl から派生する場合、XAML でビジュアル ツリーを定義できますが、このビジュアル ツリーは UserControl の Content プロパティの子です。UserControl は、独自の単純な既定のテンプレートを備えています。Border 内に ContentPresenter を単にネストしているだけなので、ユーザーはおそらくこれを置き換えることはしません。

UserControl から派生するクラスのビジュアル ツリーは、置き換えのためのものではありません。そのため、そのクラスのコードとビジュアル ツリーをより緊密に結合することができます。逆に、Control から派生し、置き換え可能な既定のテンプレートを提供する場合は、コードとビジュアル ツリー間の操作が単純かつ適切に文書化されている必要があります。

既定のテンプレートと DLL

このコラムでは、カレンダー コントロールを試してみることにしました。この記事のソース コードは、CalendarTemplateDemo という Visual Studio® ソリューションです。CalendarControls という名前の DLL を作成するライブラリ プロジェクトと、これらのコントロールを使用する 4 つのデモンストレーション プログラムが含まれています。

CalendarControls ライブラリには、CalendarMonth、CalendarDay、および CalendarDayNotes という CalendarControls 名前空間の 3 つのコントロールのコードと既定のテンプレートが含まれています。 これらの各クラスは、独自の C# ファイル内に定義されています。最初の 2 つのクラスは Control から派生し、CalendarDayNotes は CalendarDay から派生します。

コントロールを使用してアプリケーションがテンプレートを交換できるように、DLL で既定のテンプレートを定義するためには非常に厳密なルールがあります。これらの規則を順守しない場合、既定のテンプレートを持たないコントロールを作成したり、アプリケーションが置き換えることのできないテンプレートを持つコントロールを作成したりする可能性があります。DLL プロジェクトは、ResourceDictionary というルート要素を含む generic.xaml という名前のファイルを含む Themes という名前のディレクトリを含む必要があります。リソースは、DLL でのコントロールを対象とする Style 要素です。CalendarControls ライブラリの generic.xaml ファイルは、図 1 のようになります。

Figure 1 CalendarControls 用の Generic.xaml (Close Figure 1)
<ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cc="clr-namespace:CalendarControls">
    <Style TargetType="cc:CalendarMonth" />
        ...
    </Style>

    <Style TargetType="cc:CalendarDay" />
        ...
    </Style>

    <Style TargetType="cc:CalendarDayNotes" />
        ...
    </Style>
</ResourceDictionary>

このファイルの Style の各要素には、コントロールの既定のテンプレートを設定するための Setter 要素が少なくとも 1 つ含まれている必要があります。

<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="cc:CalendarMonth">
            ...
        </ControlTemplate>
    </Setter.Value>
</Setter>

ControlTemplate は、コントロールのビジュアル ツリーを含みます。これは、オプションで、テンプレート内で使用するいくつかのリソースを定義する Resources セクションを含みます。テンプレートが他のプロパティまたはイベントでの変更に反応するしくみを定義する Triggers セクションが含まれることが非常によくあります。

また、筆者が行ったように、これらの既定のテンプレートを別のファイルに分離することもできます。CalendarControls プロジェクトの Themes ディレクトリには、CalendarMonthStyle.xaml、CalendarDayStyle.xaml、および CalendarDayNotesStyle.xaml という 3 つのファイルが含まれます。これらの各ファイルには、ResourceDictionary のルート要素と、特定のコントロールを対象とする Style 型の単一の子が含まれます。図 2 では、generic.xaml ファイルがこれら 3 つのリソース ファイルを参照しています。

Figure 2 Generic.xaml から Themes を参照する (Close Figure 2)
<ResourceDictionary
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cc="clr-namespace:CalendarControls">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary
            Source="CalendarControls;Component/themes/
                CalendarMonthStyle.xaml" />
        <ResourceDictionary
            Source="CalendarControls;Component/themes/
                 CalendarDayStyle.xaml" />
        <ResourceDictionary
            Source="CalendarControls;Component/themes/CalendarDayNotesStyle.xaml" />
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

これらのスタイルが CalendarMonth、CalendarDay、および CalendarDayNotes 型のオブジェクトと正常に結合されることを確保するには、これらのクラスが DefaultStyleKey プロパティの正しい値を返す必要があります。このため、これらのクラスの静的コンストラクタは、クラスの型を返すためにそのプロパティの既定値を変更します。これは、次のように、DefaultStyleKey 依存関係プロパティのメタデータをオーバーライドすることで実行できます。

static CalendarMonth()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarMonth),
        new FrameworkPropertyMetadata(typeof(CalendarMonth)));
    ...
}

OverrideMetadata に対する最初の引数は、メタデータを変更するクラスの型です。FrameworkPropertyMetadata コンストラクタに対する引数は、DefaultStyleKey プロパティの新しい既定値を示し、これはクラスの型でもあります。

DLL のアセンブリ情報ファイル (通常、AssemblyInfo.cs という名前のファイル) は、次のアセンブリ属性を配置するにはきわめて適切な場所です。

[assembly: ThemeInfo(ResourceDictionaryLocation.None,
                     ResourceDictionaryLocation.SourceAssembly)]

つまり、テンプレートのテーマ特有のセットは存在しませんが、コントロール用の汎用テンプレートは、コントロール自体と同じアセンブリ (DLL) 内にあります。

コントロール派生の基本

Control から派生するときは、いくつかの新しいパブリック プロパティを定義する必要があります。ほとんどの場合、データ バインドおよびアニメーションのターゲットとすることができるように、これらのプロパティを依存関係プロパティでサポートする必要があります。プロパティが取得専用であり、バインディングのターゲットにできない場合でも、依存関係プロパティを使用すれば、これらのプロパティの変更時に他のオブジェクトに通知されるメカニズムが提供されます。

他の複数のクラスで既に定義されているプロパティを定義する必要があり、Control から継承しない場合は、プロパティ全体を再定義しないでください。代わりに、既存の依存関係プロパティで AddOwner を呼び出します。AddOwner からの戻り値をパブリックで静的な読み取り専用フィールドとして保存し、CLR プロパティでその値を使用します。

継承されたプロパティの既定値を変更する必要がある場合は、いくつかの選択肢があります。クラスのコンストラクタでプロパティの値を設定するのは、あまり良いアイディアではありません。これは、こうしたローカル設定は優先度がきわめて高く、スタイルまたはプロパティによってオーバーライドできないためです。優先度の低いアプローチは、前の DefaultStyleKey プロパティで示したように、依存関係プロパティで OverrideMetadata を呼び出すことです。Control クラス自体では、この方法で IsTabStop の既定値 true を設定します。

別の優先度の低いアプローチでは、コントロールの既定のテンプレートと同じ Style 要素で Setter を使用してプロパティを設定します。多数の WPF コントロールでは、この方法を使用して、SnapsToDevicePixels、MinWidth、または MinHeight など、コントロールの視覚的な外観に関連するプロパティを設定します。継承されたプロパティの値が変更されるたびにクラスに通知する必要がある場合は、OverrideMetadata を使用して、追加のコールバック メソッドをインストールできます。

Control から派生するときの最大の課題の 1 つは、ニーズのすべてに対応し、さらに代替のテンプレートを作成しようとするユーザーを悩ますことのない方法で、コードとテンプレート間の操作を定義することです。通常、コントロール コードは、いくつかの方法でテンプレートを格納します。

  • コードは、TemplateBinding マークアップ拡張を通じてテンプレートでアクセス可能なプロパティを定義します。
  • コードは、テンプレートがトリガとして使用できるプロパティおよびイベントを定義します。
  • コードは、テンプレート内のボタンによって起動可能な RoutedCommand プロパティまたはフィールドを定義します。
  • コードは、特定のヘルパ要素がテンプレートにあることを前提としています。定義済みの名前を使用して、これらの要素を参照する場合もあります。

これらすべての方法について、例を示します。

場合によっては、Control の派生クラスは、新しいテンプレートにアクセスし、そのテンプレートへのリンクを確立できるように、テンプレートの置き換えを認識する必要があります。Control クラスは仮想 OnTemplateChanged メソッドを定義しますが、これをオーバーライドして Template プロパティの変更時に通知を受けることができます。ただし、筆者の経験では、OnTemplateChanged メソッドは、テンプレートがまだ適用されていないため (WPF 用語を使用するため)、必要なリンクを実行するのに非常に適した場所であるとはいえません。それよりもはるかに優れた戦略は、OnApplyTemplate メソッドをオーバーライドすることです。奇妙なことに、このメソッドは FrameworkElement によって定義されますが、既定の実装を持ちません。

要素とコントロールの階層

CalendarControls ライブラリの MonthCalendar コントロールは、その年の 1 つの月を表示します。TwoCalendars プログラムでは、図 3 に示すように、MonthCalendar の 2 つのインスタンスを表示します。1 つは英語でもう 1 つはフランス語です。

図 3 MonthCalendar の 2 のインスタンス
図 3 MonthCalendar の 2 のインスタンス(画像を縮小するには、ここをクリックします)
図 3 MonthCalendar の 2 のインスタンス
図 3 MonthCalendar の 2 のインスタンス (画像を拡大するには、ここをクリックします)

既定のテンプレートは、Win32® の既定のテンプレートとまったく同じように見えるという WPF 設計哲学に準拠しています。Win32 の既定のテンプレートは、通常、多少平凡なものですが、ここに表示されるカレンダーは特に平凡です。ただし、筆者のこのプロジェクトの最初からの目的は、必ずしも日曜日 (フランス語では dimanche) から始まるのではないカレンダーを正しく実装することです。ただし、グレゴリオ暦の範疇を超えるという危険は冒しませんでした。

これらの例を比較的に単純に保つために、日付を選択する概念をまったく実装しないことにしました。ただし、次に説明するように、特定の日を強調表示させる IsToday というプロパティがあります。

WPF で学習した最も大きな教訓の 1 つは、複雑なコントロールを、より単純なコントロールと要素から構築できるということです。図 3 に表示されている内容で、CalendarMonth のコード部分によって定義されているものはありません。すべてが既定の XAML テンプレートによって定義されている部分であり、全体が枠で囲まれ、背景が提供されています。上部のボタンは RepeatButton コントロールであり、ひと月または 1 年を前後に移動できます。ボタンの間に TextBlock があります。曜日は、ItemsPanel として UniformGrid を使用する StatusBar 上の StatusBarItem オブジェクトです。月の日付も UniformGrid で表示されます。

今から説明する理由から、個々の日付は、CalendarDay という個別のコントロールにする必要があると判断しました。CalendarDay も Control から派生しますが、既定のテンプレートは TextBlock を含む Border です。CalendarMonth コントロールは月を表示するときに、28、29、30、または 31 個の CalendarDay オブジェクトを生成します。図 4 に、これら 2 つのカレンダーを表示する TwoCalendars.xaml ファイルを示します。

Figure 4 TwoCalendars.xaml (Close Figure 4)
<!-- TwoCalendars.xaml by Charles Petzold, Sept. 2007 -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:cc="clr-namespace:CalendarControls;assembly=CalendarControls"
  x:Class="Petzold.TwoCalendars.TwoCalendars"
  Title="Two Calendars Demonstration">

  <Window.Resources></Window.Resources>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto" />
      <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <cc:CalendarMonth Grid.Column="0" Margin="24" />

    <cc:CalendarMonth Grid.Column="1" Margin="24"
      Culture="fr-FR" />
  </Grid>
</Window>

コードおよび XAML

CalendarDay は、CalendarMonth より単純で、全体を示すことができるほど短いので、最初に焦点を当てたいプロジェクトの部分です。図 5 に CalendarDay.cs を示します。これはクラスの一部ではないことに注意してください。既定のテンプレートは CalendarDay クラスの一部ではなく、CalendarDay 型のオブジェクトに自動的に適用されます。

Figure 5 CalendarDay.cs (Close Figure 5)
// CalendarDay.cs by Charles Petzold, Sept. 2007
using System;
using System.Windows;
using System.Windows.Controls;

namespace CalendarControls
{
    public class CalendarDay : Control
    {
        // Static constructor: Change defaults.
        static CalendarDay()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarDay),
                new FrameworkPropertyMetadata(typeof(CalendarDay)));

            IsTabStopProperty.OverrideMetadata(typeof(CalendarDay),
                new FrameworkPropertyMetadata(false));
        }

        // Date dependency property and property.
        public static readonly DependencyProperty DateProperty =
            DependencyProperty.Register("Date",
                typeof(DateTime),
                typeof(CalendarDay),
                new PropertyMetadata(DateChangedCallback));

        public DateTime Date
        {
            set { SetValue(DateProperty, value); }
            get { return (DateTime)GetValue(DateProperty); }
        }

        // IsToday dependency property and property.
        static readonly DependencyProperty IsTodayProperty =
            DependencyProperty.Register("IsToday",
                typeof(bool),
                typeof(CalendarDay),
                new PropertyMetadata(false));

        public bool IsToday
        {
            set { SetValue(IsTodayProperty, value); }
            get { return (bool)GetValue(IsTodayProperty); }
        }

        // Day read-only dependency property and property.
        static readonly DependencyPropertyKey DayKey =
            DependencyProperty.RegisterReadOnly("Day",
                typeof(string),
                typeof(CalendarDay),
                new PropertyMetadata());

        public static readonly DependencyProperty DayProperty =
            DayKey.DependencyProperty;

        public string Day
        {
            protected set { SetValue(DayKey, value); }
            get { return (string)GetValue(DayProperty); }
        }

        // DateChangedCallback method
        static void DateChangedCallback(DependencyObject obj,
           DependencyPropertyChangedEventArgs args)
        {
            CalendarDay calday = obj as CalendarDay;
            calday.Day = calday.Date.Day.ToString();
        }
    }
}

このクラスは、依存関係プロパティでサポートされる、3 つの新しいプロパティを定義します。それらは、DateTime 型の Date、Bool 型の IsToday、文字列型の Day という取得専用プロパティです。Day プロパティは全面的にテンプレートで使用するためのものです。つまり、Date プロパティが変更するたびに、クラスは DateTime オブジェクトの Day プロパティを文字列に変換し、自身の Day プロパティに設定します。

CalendarDayStyle.xaml ファイルには、CalendarDay コントロール用のテンプレートが含まれます。多くの場合、テンプレートは Border 要素で始まります。このプロパティは、TemplateBinding 拡張を使用して Control クラスで定義される同じ名前のプロパティに設定されます。これらのプロパティがテンプレート内で明示的に参照されない場合、コントロールの視覚的な外観に影響はありません。

テンプレートで参照する必要のない Control のプロパティは、ビジュアル ツリーから継承されたプロパティです。これらの継承されたプロパティは、Foreground および 5 つのフォント関連のプロパティです。そのようなプロパティに対する変更は、TextBlock によって自動的に継承されます。これは、CalendarDay クラスで定義される Day プロパティを表示します。HorizontalContentAlignment プロパティと VerticalContentAlignment プロパティでは、セル内でそのテキストを整列する方法を特定します。このテンプレートは、次に示すような Triggers セクションで終了します。

<Trigger Property="IsToday" Value="True">
  <Setter Property="Background"
    Value="{DynamicResource
    {x:Static SystemColors.HighlightBrushKey}}" />
  <Setter Property="Foreground"
    Value="{DynamicResource
    {x:Static SystemColors.HighlightTextBrushKey}}" />
</Trigger>

CalendarDay で定義されている IsToday プロパティが true の場合、Background プロパティと Foreground プロパティは強調表示される項目のシステム ブラシに設定されます。また、この Triggers セクションには MultiTrigger 要素と EventTrigger 要素を含んだり、EventTrigger 要素でアニメーションを起動したりすることができます。

CalendarDay には Border 要素が含まれますが、枠は表示されません。CalendarMonth のテンプレートは同様に定義され、月全体を囲む枠も表示されません。Control では、Border 要素自体と同様に、BorderBrush の既定値が null に定義され、BorderThickness の既定値がゼロに定義されるため、枠は表示されません。どうしても各日付が枠内に表示されないと気がすまないと感じる方もいるでしょう。テンプレートのプロパティをより実用的な値に設定するのはどうでしょう。

<Border BorderThickness="1"
        BorderBrush="Black" ...

このアプローチの問題は、今後コントロールを操作するプログラマが、これらの値を変更するために既定のテンプレートを変更する必要があることです。このテンプレートは、少なくともハードコードされたプロパティの値を持つ必要があります。読者がコントロールと既定のテンプレートの設計者である場合、リソース ファイルの Style 要素を使用して、これらのプロパティをより適した既定値に設定することができます。コントロールのコンシューマである場合は、コントロールを使用するときにこれらのプロパティを設定できます。

たとえば、次のように、TwoCalendars.xaml ファイル内で最初のカレンダーを変更することができます。

<cc:CalendarMonth BorderThickness="1"
                  BorderBrush="Black" ...

これにより、カレンダー全体の周囲に枠が表示されます。ただし、CalendarDay の枠はどのように設定すればよいのでしょうか。CalendarDay 要素は TwoCalendars.xaml ファイルに記載されていません。これは、CalendarMonth が CalendarDay オブジェクトをすべて内部的に生成しているためです。

解決策は CalendarDay をターゲットとするスタイルを使用することです。次のコードを TwoCalendars.xaml の Resources セクションに挿入してください。

<Style TargetType="cc:CalendarDay">
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="BorderBrush"
            Value="{DynamicResource
                {x:Static SystemColors.ControlTextBrushKey}}" />
    <Setter Property="HorizontalContentAlignment"
            Value="Center" />
    <Setter Property="VerticalContentAlignment"
            Value="Center" />
    <Setter Property="FontStyle" Value="Italic" />
</Style>

このスタイルは枠だけでなく、コンテンツ整列プロパティを設定して、セル内の日付を中央に配置し、数字を斜体で表示します。

既定の CalendarMonth テンプレートは、CalendarMonthStyle.xaml ファイルに含まれます。このテンプレートは Border で始まり、ボタンおよび月名、曜日、およびカレンダー グリッド自体という 3 つの垂直のセルのグリッドが続きます。グリッドの一番上のセルは、ボタンと月名の 5 つの水平のセルを含む別のグリッドです。

CalendarMonth クラスでは、MonthName、AbbreviatedMonthName、書式付きの AbbreviatedYearMonth など、テンプレートがテキストの情報を表示するために使用できる複数のプロパティが使用可能になります (CalendarMonth は、この情報のほとんどを DateTimeFormatInfo から取得します)。また、CalendarMonth は、DayNames プロパティと AbbreviatedDayNames プロパティとして使用可能な 2 つの配列に曜日名を格納します。DateTimeFormatInfo 内の配列は常に日曜日から始まるため、これらは DateTimeFormatInfo クラスの同じ名前を持つプロパティと少し異なります。

CalendarMonth で定義されている FirstDayInWeek プロパティは、月初めの曜日と DateTimeFormatInfo の FirstDayOfWeek プロパティに基づきます。

CalendarMonthStyle.xaml にあるテンプレートには、独自の Resources セクションがあります。これは、特にマークアップに明示的に記述されない要素タイプについてテンプレート内でスタイルを設定する場合に便利です。たとえば、曜日を表示する方法に注目してください。AbbreviatedDayNames プロパティは、ItemsPanel として UniformGrid を持つ StatusBar に割り当てられます。内部的に、AbbreviatedDaysNames の配列内の文字列は、StatusBarItem コントロールの Content プロパティになります。StatusBarItem はこのマークアップに実際に記述されませんが、Style で StatusBarItem コントロールをターゲットにして、コンテンツを中央に配置し、名前の間にスペースを挿入できます。

指定した要素にアクセスする

これまでの例では、XAML ファイルが TemplateBinding 拡張およびトリガを通じて参照するプロパティをクラスで定義する方法を見てきました。ただし、この方法は、クラスがテンプレートの特定の要素にアクセスするときにさらに役立つ場合があります。この例では、CalendarMonth クラスは特定の月のすべての CalendarDay オブジェクトを生成する必要があり、これらのオブジェクトは、最終的にある種のパネルとなる必要があります。コードが参照する名前をこのパネルに割り当てると最も役立つ場合があります。

ご覧のように、CalendarMonth テンプレートの 2 番目の UniformGrid では、PART_Panel という名前を指定しています。この名前は、複数の WPF コントロール用の既定のテンプレートで定義される名前と同様です。CalendarMonth クラス定義の先頭には、この名前と、コードによる特定が予期される要素のタイプを示す属性が付いています。

[TemplatePart(Name = "PART_Panel", Type = typeof(Panel))]

この属性は、ビジュアル デザイナに役立てるためのもので、必須ではありません。ただし、テンプレートのこの要素は Panel の派生となることができます。

ApplyTemplate への呼び出しに応じて、CalendarMonth.cs のこのステートメントは、次のように、このパネルを取得し、フィールドとして格納します。

pnl = Template.FindName("PART_Panel", this) as Panel;

これは、通常の FindName メソッドではないことに注意してください。これは、ControlTemplate (および ItemsPanelTemplate、DataTemplate、HierarchicalDataTemplate) の派生元である FrameworkTemplate によって定義される FindName メソッドです。そのため、CalendarMonth オブジェクトの Template プロパティを使用してアクセスします。

この pnl オブジェクトが null の場合はどうなるのでしょうか。つまり、テンプレートが PART_Panel という名前のものを含まず、Panel から PART_Panel という名前の要素は派生されません。クラスがテンプレートで指定された要素を見つけることができない場合は、暗黙的にできる限り処理を続ける必要があります。

テンプレート内の名前と型をヘルパ要素に関連付ける場合は、できる限り汎用的であるようにします。この場合、MonthCalendar には Children プロパティを含む要素が必要です。そのため、Panel が妥当に見えます。MonthCalendar には特定の種類のパネルは必要ありませんが、UniformGrid works が非常にうまく機能することは明らかです。

コマンドを生成する

CalendarMonth 用のテンプレートには、前後に移動できるボタンを含む必要があります。これらのボタンを押すと反応するコードはどのような方法で記述するのでしょうか。最も便利な方法は、RoutedCommand 型のパブリックで静的な読み取り専用フィールドか取得専用プロパティをクラスで定義することです。既存の WPF コントロールは、これらの RoutedCommand オブジェクトをフィールドまたはプロパティとして定義すると一貫性が保たれません。ScrollBar クラスは、これらのオブジェクトを読み取り専用フィールドとして定義し、LineDownCommand、LineLeftCommand などの名前を指定します。Slider クラスは、DecreaseLarge、DecreaseSmall、IncreaseLarge、および IncreaseSmall という名前の取得専用プロパティとして定義します。ここでは、次のように ScrollBar アプローチを選択し、RoutedCommand オブジェクトをフィールドとして定義しました。

publicstaticreadonlyRoutedCommand NextMonthCommand =
    new RoutedCommand("NextMonth",
    typeof(CalendarMonth));

これらは静的であることに注意してください。テンプレートは、次のように、コントロールの Command プロパティに割り当てられている完全修飾名を参照します。

<ToggleButton Command="
CalendarMonth.NextMonthCommand" ...

非常に多くのコントロールが、RoutedCommand 型のオブジェクトを設定可能な Command プロパティを定義するわけではありません。設定できるのは、ButtonBase、MenuItem、および Hyperlink のみです。

CalendarMonth コンストラクタは、オブジェクトを CommandBindings コレクションに追加することによって、これらの RoutedCommand オブジェクトを実行メソッドにリンクします。

CommandBindings.Add(new
  CommandBinding(NextMonthCommand,
  NextMonthExecuted));

また、コントロールが有効であるかどうかを示す Boolean をコードで設定できる実行可能なメソッドを指定することもできます。有効でない場合は、そのコントロールは自動的に無効になります。

CalendarMonth.cs の NextMonthExecuted メソッドは、次のようになります。

void NextMonthExecuted(object sender,
ExecutedRoutedEventArgs args)
{
    Date = Date.AddMonths(1);
}

RoutedCommand オブジェクトを使用するメリットの 1 つは、Execute メソッドを呼び出すことで、コードからごく簡単にこのオブジェクトを呼び出すことができることです。たとえば、いくつかのキーボード入力でコマンドを起動することもできます。ただし、多くの場合は、InputGesture 型のオブジェクト (KeyGesture および MouseGesture の派生元の抽象クラス) を RoutedCommand に関連付けて、明示的なキーボード処理を行わずに、このジョブを完了できます。CalendarMonth の静的コンストラクタでは、次のように、4 つすべての RoutedCommand フィールドに KeyGesture オブジェクトを追加します。

NextMonthCommand.InputGestures.Add(
    new KeyGesture(Key.PageDown));

これらのジェスチャは RoutedCommand オブジェクトの元のフィールド定義で追加することもできましたが、コンストラクタで InputGestureCollection 全体を指定する必要があるため、やや面倒です。

テンプレートを置き換える

この演習全体の真のテストは、既定のテンプレートを置き換え、完全に外観の異なるカレンダーを作成するアプリケーションの作成によって行われます。NewCalendar プログラムでは、月の日付を縦に表示する CalendarMonth の新しいテンプレートを定義します。このプログラムでは、これらの月次カレンダーを 12 個作成し、横に並べて表示します (図 6 参照)。

図 6 NewCalendar の表示
図 6 NewCalendar の表示

また、CalendarDay から新しいクラスを派生することによってカレンダーを強化することも望ましい方法ですが、始めは可能とは思えません。CalendarMonth は、CalendarDay の 28、29、30、または 31 個のインスタンスを生成します。CalendarDay から派生する場合は、そのロジックを変更するために CalendarMonth からも派生する必要があります。これを避けるために、Type 型の DayType という、CalendarMonth のさらに別のプロパティを定義しました。既定では、このプロパティは typeof(CalendarDay) に相当しますが、CalendarDay から派生するクラスの型に設定することもできます。

CalendarDayNotes は CalendarDay から派生し、テンプレートに PART_TextBox という名前の TextBox 型のコントロールが含まれていることを想定します。それらの TextBox コントロールに入力された内容はいずれも、CalendarDayNotes によって、日付から派生したファイル名で特定される小さなファイルに保存されます。次回プログラムを実行すると、それらの入力内容がすべて読み込まれます。CalendarDayNotes の既定のテンプレートはかなり平凡ですが、ReminderCalendar プログラムにはやや手の込んだテンプレートが含まれており、図 7 に示すように、月の日付が、長い文字で、グラデーションのかかった色で表示されています。

図 7 ReminderCalendar
図 7 ReminderCalendar

このようなやや手の込んだ日付の作成では、CalendarDay から派生させる必要さえありません。また、CalendarControls ライブラリには Viewport3D から派生する MoonDisk という名前のクラスがあり、月の球面に当たる光の方向を正しく表す DateTime プロパティがあります。MoonPhaseCalendar では、世界最速のカレンダーは作成されません。これは、各月に最大 31 個の Viewport3D オブジェクトを生成する必要があるためです。ただし、図 8 に示すように、平均的なカレンダーよりちょっとおもしろいカレンダーが表示されます。

図 8 MoonPhaseCalendar の表示
図 8 MoonPhaseCalendar の表示

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

Charles Petzold は MSDN Magazine の寄稿編集者です。最新の著書には『3D Programming for Windows』(Microsoft Press、2007 年) があります。


すてきな ASP.NET: ASP.NET コントロールで Silverlight をカプセル化する

すてきな ASP.NET
ASP.NET コントロールで Silverlight をカプセル化する
Fritz Onion

コードのダウンロード : : ExtremeASPNET2008_01.exe (700KB)
コンテンツ
Silverlight を使用する
カスタム Silverlight コントロールを作成する
ASP.NET AJAX および IScriptControl
asp:Xaml コントロールおよび asp:Media コントロール

世界中の ASP.NET 開発者の多くが知りたがっているのは、アプリケーションに SilverlightTM を組む込む方法と場所です。これに対してはっきりとした答えはありません。なぜなら、多岐に渡るアプローチを実行することができるためです。ページ全体を Silverlight コントロールとして作成し、すべてをそのページで実行することができます。または、もっと使用される可能性の高いアプローチとしては、Silverlight のメリットを享受できるページの部分を特定し、最も多くのメリットを追加するリッチな UI 要素を組み込み、基礎のアプリケーション構造をそのままに保つことができます。

後者のアプローチは、ページへの "リッチな島々の追加" と呼ばれることもあります。さらに、Silverlight を使用すると、スクリプトで実行可能なメソッドやイベントを介してそれらの島々から周囲のコンテンツにたくさんの使用可能な橋がかかります。

今月のコラムでは、Silverlight コンテンツをカプセル化するカスタム サーバー コントロールを構築する技法を探索します。Silverlight コンテンツをカスタム ASP.NET コントロールにラップすることには、さまざまな利点があり、Silverlight が特に魅力的な技法となります。この技法により、他の任意のサーバー側コントロールを使用する場合と同じくらい Silverlight コンテンツの統合が容易になり、導入の機会が大幅に拡大します。Silverlight コントロールでのハンドラの追加、プロパティの設定、およびメソッドの呼び出しのプロセスは、他の任意のコントロールと厳密に同じです。また、このアプローチを使用すると Silverlight 固有の JavaScript の煩雑さのないページを維持することができるため、管理と展開がさらに容易になります。

Silverlight を使用する

Silverlight コンテンツをホストするカスタム コントロールの詳細な作成方法の説明に移る前に、Silverlight コンテンツをクライアントにレンダリングするために完了しておく必要のある内容を正確に説明する必要があります。そのため、まず、ASP.NET ページで XAML ファイルをホストし、そのページとレンダリングされる Silverlight コンテンツとの対話を追加する方法を説明します。コントロールのパーツを組み立てる方法は、コントロールを使用して最終的にカプセル化する動作を示すページを作成すると、より明確になります。

まず、Silverlight を使用してレンダリングする XAML ファイルを記述します。目標は、XAML を、さりげなくコントロールを表示するくらいに単純でありながら、興味を引くことができる程度に複雑になるようにすることです。そのため、タイトル付きの単一の球体をレンダリングすることにしました。図 1 に示す XAML では、タイトルとして使用される TextBlock と放射状のグラデーション ブラシを使用して描画される楕円を定義します。また、この XAML では Canvas の Resources の部分で 2 つの Storyboard を定義します。これらの Storyboard はそれぞれ、球体を 2 秒間拡大させるアニメーションと 2 秒間縮小させるアニメーションを記述します。最終的には、Silverlight でこの XAML をホストし、次に、JavaScript ハンドラを使用して、タイトル テキストを設定し、ユーザーがレンダリングされた球体をクリックするとアニメーションが呼び出されるようにします。

Figure 1 Sphere.xaml ファイル (Close Figure 1)
<!-- File: Sphere.xaml -->
<Canvas
    xmlns="http://schemas.microsoft.com/client/2007"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="300" Height="300"
    Background="White"
    >
  <Canvas.Resources>
    <Storyboard x:Name="growAnimation">
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Width)"
        To="250" Duration="0:0:2" />
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Height)"
        To="250" Duration="0:0:2" />
    </Storyboard>
    <Storyboard x:Name="shrinkAnimation">
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Width)"
        To="200" Duration="0:0:2" />
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Height)"
        To="200" Duration="0:0:2" />
    </Storyboard>
  </Canvas.Resources>

  <TextBlock x:Name="titleText" Width="200" Height="24"
        Canvas.Left="94" Text="[Title]" TextWrapping="Wrap" />

  <Ellipse Width="200" Height="200"
     x:Name="ellipse" Canvas.Left="47" Canvas.Top="41">
    <Ellipse.Fill>
      <RadialGradientBrush GradientOrigin="0.75,0.25"
         Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
        <GradientStop Color="Yellow" Offset="0" />
        <GradientStop Color="Green" Offset="1" />
      </RadialGradientBrush>
    </Ellipse.Fill>
  </Ellipse>

</Canvas>

Web ページにこの XAML を組み込むには、まず、Silverlight プラグインを作成する必要があります。これを実行する最も簡単な方法は、Silverlight 1.0 SDK に付属の Silverlight.js スクリプト ファイルへの参照を追加することです。このファイルでは、Silverlight.createObject と呼ばれるメソッドを定義しており、このメソッドを使用してプラグインを作成できます。Silverlight プラグインは、ページの HTML 要素 (通常は <div>) に関連付けられている必要があります。そのため、一般的には、<div> 要素を定義し、<div> 内に埋め込まれているスクリプト ブロック内に createObject への呼び出しを配置します。

図 2 に、Silverlight コントロールのインスタンスを作成し、<div> 要素に関連付ける方法を示します。このページでは、図 1 に示す Sphere.xaml ファイルがそのページ自体と同じディレクトリ内にあることを前提としています。Web ブラウザでこのページを表示すると、図 3 に示すすてきな緑色の球体が表示されます。

Figure 2 Silverlight コントロールの作成と関連付け (Close Figure 2)
<%@ Page Language="C#" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <script type="text/javascript" src="silverlight.js"></script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="slControlHost">
    <script type="text/javascript">
        Silverlight.createObject(
        "Sphere.xaml",
        document.getElementById('slControlHost'), "slControl",
        { width:'300', height:'300', version:'1.0' },
        { onError:null, onLoad:null },
        null);
    </script>
    </div>
    </form>
</body>
</html>
図 3 緑色の球体を表示するコントロール
図 3 緑色の球体を表示するコントロール

次の作業は、XAML を操作して、タイトル テキストをプログラムで変更し、前述のアニメーションの拡大、縮小を開始することです。これを実行する最も単純な方法は、XAML で定義されているオブジェクトのイベントにハンドラを宣言で割り当てることです。この例では、アニメーションの拡大および縮小は球体へのユーザーのクリックに応じて行われます。ここで実行するアニメーションは、球体の現在のサイズによって異なります。

まず、球体を定義する楕円の MouseLeftButtonDown イベントにハンドラを追加し、次に、ページの JavaScript に対応するメソッドを配置します。同様に、TextBlock の Loaded イベントを処理し、球体の上に表示されるテキストをカスタム文字列に設定します。XAML に追加するハンドラは次のようになります。

<Ellipse MouseLeftButtonDown="javascript:onSphereButtonDown" ...
<TextBlock Loaded="javascript:onTextLoaded" ...

また、図 4に、ASP.NET ページの新規スクリプト ブロックに追加する対応する JavaScript メソッドを示します。

Figure 4 新しいスクリプト ブロックに追加された JavaScript メソッド (Close Figure 4)
<script type="text/javascript">
    function onSphereButtonDown(sender, args)
    {
        // Run the grow or shrink animation,
        // depending on whether it is currently
        // 'grown' or 'shrunk'
        var animationName = (sender.Width==200) ?
              "growAnimation" : "shrinkAnimation";

        var sl = sender.getHost();
        var animation = sl.content.findName(animationName);
        if (animation)
          animation.begin();
    }

    function onTextLoaded(sender, args)
    {
      sender.Text = "My Growing Sphere";
    }
</script>

これで、ASP.NET ページへの Silverlight コンテンツの手動での組み込みが完了します。このページをブラウザで表示すると、タイトル テキストは "My Growing Sphere" となります。ユーザーが球体をクリックすると、最初のクリックでは拡大し、次のクリックでは縮小します。ご覧のように、ASP.NET ページへの Silverlight コンテンツの 組み込みは比較的容易です。また、そのオブジェクト モデル全体がページ上のスクリプトに公開されるので、操作も簡単です。

ただし、この例の球体のように単純な要素の場合は、そのコントロールをページのデザイン画面にドラッグするだけでアプリケーションに組み込むことができるように、カスタム コントロールとしてパッケージ化することをお勧めします。ここで、Silverlight のカプセル化が登場します。

カスタム Silverlight コントロールを作成する

まず、次のように新規のカスタム ASP.NET コントロールを作成することにより、Silverlight のカプセル化の作業を行います。

namespace MsdnMagazine
{
    public class SilverlightSphere  : Control
    {
    }
}

このサンプルでは、System.Web.UI.Control から派生しています。WebControl からの派生は、このクラスで定義されている追加のスタイル プロパティがこのコントロールに適用されないため、行いませんでした。

次に、手動で含めた JavaScript ハンドラをレンダリングする必要があります。ASP.NET コントロールに JavaScript を組み込む最も簡単な方法は、JavaScript ファイルを定義し、コンパイル済みのアセンブリにリソースとして埋め込むことです。これを実行するには、次のように 2 つのイベント ハンドラを、SilverlightSphere.js という別の JavaScript ファイルに移動します。

   // File: SilverlightSphere.js
   function onSphereButtonDown(sender, args)
   {
       // Same implementation as before
   }

   function onTextLoaded(sender, args)
   {
      // Same implementation as before
   }

次に、新規の SilverlightSphere.js ファイルと元の Silverlight.js ファイルをコントロール プロジェクトにリソースとして埋め込みます。これは、依存ファイルを使用することなく単一のアセンブリとしてコントロールを展開できるようにするためです。また、XAML ファイルをリソースとして埋め込み、ASP.NET 内の WebResource.axd ハンドラのメカニズムを使用して、その XAML ファイルと 2 つの JavaScript ファイルの両方を参照し、埋め込みリソースを抽出するという方法も合理的です。

Visual Studio® で 3 つのファイルすべてに対して [ビルド アクション] を [埋め込まれたリソース] に設定した後 (コマンド ライン コンパイラの /res)、アセンブリ レベルの属性 System.Web.UI.WebResource を使用して、これらのリソースが WebResource.axd で処理されるためのアクセス許可を与え、その応答に MIME タイプを関連付けます。これを行うには、次の宣言を使用します (ここで、SlSphere はコントロール プロジェクトの名前です)。

[assembly: WebResource("SlSphere.SilverlightSphere.js",
                       "text/javascript")]
[assembly: WebResource("SlSphere.Silverlight.js",
                       "text/javascript")]
[assembly: WebResource("SlSphere.Sphere.xaml", "text/xml")]

これで、JavaScript ファイルは埋め込みリソースとして筆者のアセンブリにコンパイルされます。今度は ClientScriptManager クラスの RegisterClientScriptResource メソッド (Page.ClientScriptManager からアクセスする) を使用して、ファイルへの参照をレンダリングされるページに含めることができます。次のように Control 基本クラスから継承された OnInit メソッドのオーバーライドで、ファイルごとにこのメソッドを呼び出します。

protected override void OnInit(EventArgs e)
{
  Page.ClientScript.RegisterClientScriptResource(this.GetType(),
                      "SlSphere.Silverlight.js");
  Page.ClientScript.RegisterClientScriptResource(this.GetType(),
                      "SlSphere.SilverlightSphere.js");

  base.OnInit(e);
}

このコントロールの最後の最も重要な部分は、仮想 Render メソッドの実装です。このメソッドでは、2 つのことを実現する必要があります。1 つ目は、Silverlight プラグインのホスト HTML 要素として機能させるために <div> タグをレンダリングする必要があります。2 つ目は、埋め込みスクリプトをレンダリングして、Silverlight プラグインを作成する必要があります。

XAML ファイルへの参照は、ClientScriptManager クラスの GetWebResourceUrl への呼び出しを使用して生成されます。これにより、アセンブリにリソースとして埋め込んだ XAML ファイルのコンテンツを使用して、Silverlight コントロールが初期化されます。最後に、Silverlight プラグイン自体の一意の識別子を指定する必要があります。この例では、コントロールの ID (this.ClientID) を取得し、末尾に "_ctrl" を連結することによって実行しています (図 5 参照)。

Figure 5 仮想 Render メソッドの実装 (Close Figure 5)
Const string _silverlightCreateScript = @"Silverlight.createObject(
  '{0}, document.getElementById('{1}'), '{2},
  {{ width:'300', height:'300', version:'1.0' }},
  {{ onError:null, onLoad:null }},
  null);";

protected override void Render(HtmlTextWriter writer)
{
  string script = string.Format(_silverlightCreateScript,
      Page.ClientScript.GetWebResourceUrl(GetType(),
               "SlSphere.Sphere.xaml"),
               this.ClientID, this.ClientID + "_ctrl");

  writer.AddAttribute(HtmlTextWriterAttribute.Id,
                      this.ClientID);
  writer.RenderBeginTag(HtmlTextWriterTag.Div);
  writer.AddAttribute(HtmlTextWriterAttribute.Type,
                      "text/javascript");
  writer.RenderBeginTag(HtmlTextWriterTag.Script);

  writer.Write(script);

  writer.RenderEndTag();
  writer.RenderEndTag();

  base.Render(writer);
}

これで、ASP.NET ページに Silverlight を展開するために必要なコードをカプセル化するサーバー側のコントロールが完成 (埋め込み JavaScript と XAML リソースで完了) しました。次の宣言を使用して、このコントロールのインスタンスをページ上に作成できます。

<%@ Register Assembly="SlSphere" Namespace="MsdnMagazine"
             TagPrefix="csc" %>
...
<csc:SilverlightSphere ID="_silverlightSphere" runat="server" />

このコードで実行できる内容は魅力的です。どのような ASP.NET アプリケーションでも Silverlight でホストされる拡大、縮小する球体を容易に組み込むことができます。そのうえ、Silverlight のコンテンツのホスティングの詳細について心配する必要がありません。

高度な XAML レンダリングを使用して、もっと興味深く複雑なコントロールを構築することももちろん可能です。ただし、コントロールの追加について検討し始めると、サーバー コントロールのプロパティ値を使用して XAML をインストルメント化する方法に関する問題がすぐに発生します。たとえば、筆者のコントロールの Title プロパティを公開し、XAML ファイルの titleText 要素の Text プロパティにマッピングする必要があるとします。現在の実装では、これを手際よく実行できる明快な方法はありません。

現在の実装におけるもう 1 つの欠点は、XAML が埋め込みリソースとして固定されるということです。これは展開においては便利ですが、Silverlight および XAML の主要なメリットの 1 つは、設計と動作との分離によって XAML が交換可能になるという点です。コントロールに XamlUrl プロパティを定義して、埋め込み XAML コンテンツのオーバーライドを許可することができます。このアプローチは難しくはありませんが、XAML で JavaScript ハンドラの名前を公開するという脆弱性がもたらされます。この場合、設計者は必ず XAML の最新バージョンを使用する必要があります。

ASP.NET AJAX および IScriptControl

この問題の重要な点は、クライアント側のスクリプトにサーバー側のプロパティを導入する方法を理解することです。さいわいにも、ASP.NET AJAX 拡張では、System.Web.Extensions アセンブリで定義されている IScriptControl インターフェイスを介してサーバー側コントロール プロパティをクライアント側の要素に結び付ける明確な方法を提供しています。このインターフェイスでは、GetScriptReferences および GetScriptDescriptors の 2 つのメソッドを定義しています。これらのメソッドは、実装されてページの ScriptManager コントロールに結び付けられている場合、JavaScript ファイルへの参照の生成と、JavaScript クラスのインスタンスの宣言をそれぞれ行います。

public interface IScriptControl
{
  IEnumerable<ScriptDescriptor> GetScriptDescriptors();
  IEnumerable<ScriptReference> GetScriptReferences();
}

記述子は、サーバー コントロールによって定義されているプロパティに初期化されたプロパティを持つ、サーバー側コントロールのクライアント側の代理として考えることができます。記述子は、サーバー コントロール プロパティとクライアント プロパティ間のリンクを提供します。Silverlight および JavaScript ハンドラがこのリンクを使用できます。また、記述子は、名前空間およびクラスの下にコントロールのクライアント側の機能のすべてを分離する明確なスコープ メカニズムを提供しています (少なくとも、ASP.NET AJAX クライアント ライブラリは JavaScript で名前空間とクラスを定義しているため)。

Silverlight コンテンツをホストするカスタム ASP.NET AJAX コントロールを作成する場合は、まず、Silverlight コントロールを操作するために定義されたメソッドとプロパティを使用してクライアント側のクラス定義を作成します。このコントロールに Title プロパティと XamlUrl プロパティの両方を追加すると合理的であるため、まず、これらのプロパティを持つクラスを定義し、そのクラスに MsdnMagazine.SilverlightAjaxSphere という名前を付けます。

// File: SilverlightAjaxSphere.js
Type.registerNamespace('MsdnMagazine');

MsdnMagazine.SilverlightAjaxSphere = function(element) {
    MsdnMagazine.SilverlightAjaxSphere.initializeBase(
               this, [element]);

    this._title = null;
    this._xamlUrl = null;
}

今度は、クラスのプロトタイプ用のメソッドを作成する必要があります。まず、初期化から始めます (図 6 参照)。最初のコントロールの実装で行ったように <div> 要素内にスクリプトを埋め込むのではなく、今回は、クライアント側のクラスの initialize メソッド内に Silverlight プラグインを作成します。これは、このメソッドが呼び出される前に、クラスに定義した _xamlUrl プロパティにサーバー側値が入力されている場合に限り行うことができることに注意してください。また、Sys.UI.Control 基本クラスから継承した get_id メソッドを使用して、サーバー側コントロールによりレンダリングされる <div> 要素の識別子にアクセスすることもできます。

Figure 6 クラス プロトタイプ用メソッドの作成 (Close Figure 6)
// File: SilverlightAjaxSphere.js
MsdnMagazine.SilverlightAjaxSphere.prototype = {
    initialize : function() {
      MsdnMagazine.SilverlightAjaxSphere.callBaseMethod(
             this, 'initialize');
      var hostId = this.get_id() + 'Host';

      Silverlight.createObject(this._xamlUrl,
        $get(this.get_id()), hostId,
        { width:'300', height:'300', version:'1.0' },
        { onError:null,
          onLoad:Function.createDelegate(this, this._onXamlLoaded) },
        null);

    },

    dispose : function() {
        MsdnMagazine.SilverlightAjaxSphere.callBaseMethod(this,
              'dispose');
    },

    // Once called, Silverlight control is initialized and
    // Xaml elements can be accessed.
    _onXamlLoaded : function(root) {
      var root = $get(this.get_id() + 'Host');
      var sphere = root.content.findName('ellipse');

      if (sphere) {
        sphere.addEventListener('MouseLeftButtonDown',
                    Function.createDelegate(this,
                     this._onSphereButtonDown));
      }
      else
        throw Error.invalidOperation(
              "You must have an ellipse element.");

      var title = root.content.findName('titleText');
      if (title)
        title.Text = this._title;
    },

     _onSphereButtonDown : function(sender, args)
    {
      // same implementation as before
    },

    // property accessors
    //
    get_title : function() {
        return this._title;
    },

    set_title : function(value) {
        if (this._title !== value)
            this._title = value;
    },

    get_xamlUrl : function() {
      return this._xamlUrl;
    },

    set_xamlUrl : function(value) {
      if (this._xamlUrl !== value)
        this._xamlUrl = value;
    }
}

MsdnMagazine.SilverlightAjaxSphere.registerClass(
      'MsdnMagazine.SilverlightAjaxSphere', Sys.UI.Control);

if (typeof(Sys) !== 'undefined')
  Sys.Application.notifyScriptLoaded();

筆者のクラスで initialize メソッドが呼び出されるときは、Silverlight プラグインはちょうど作成中であり、XAML はまだ評価されていません。そのため、Sys.Silverlight の createObject メソッドに定義された onLoad イベント ハンドラを使用して、XAML のロード後に呼び出されるコールバックを指定します。これにより、イベント ハンドラを設定し、個別の XAML 要素でプロパティを初期化することができます。筆者が _onXamlLoaded と呼んでいるこのハンドラには Silverlight のルート要素への参照が渡されます。これを使用して XAML 内の指定した要素を取得できます。

指定した XAML 要素を操作するときは、ユーザーができる限り協調的である必要があります。実際にコンテンツを変更したり、イベント ハンドラを定義したりする前に、指定した要素が存在しているかどうかを確認する必要があります。要素が存在せず、コントロールの操作にとって重要でない場合 (筆者のコントロールの XAML の titleText 要素など)、その要素の初期化ステップを省略してください。この方法では、設計者が XAML から削除する特定の要素を選択することが可能で、コントロールも動作します。

クラスの実装の残りの部分では、title と xamlUrl のプロパティ アクセサを定義し、_onSphereButtonDown 関数を実装して、前に行ったように、アニメーションの拡大、縮小を開始します。ただし、今回、メソッドはクラス内にカプセル化されます。

クライアント側スクリプトの最終ステップでは、新規クラスを登録します。このクラスでは Sys.UI.Control として基本クラスを指定します。

残る作業は、IScriptControl インターフェイスを介してこのクラスを使用するサーバー側コントロールの作成です (図 7 を参照)。クライアント側クラスと同様に、サーバー コントロールは Title および XamlUrl の 2 つのプロパティを公開します (これらのプロパティは、ViewState によってサポートされます。これは、すべてのサーバー側コントロール プロパティが常に ViewState によってサポートされる必要があるためです)。XamlUrl プロパティを URL に初期化します。前のコントロール実装で使用した同一の GetWebResourceUrl メソッドを使用して埋め込み XAML ファイルを参照します。OnInit のオーバーライドでこのプロパティを設定することにより、プロパティが既定でこの URL に設定されるようにします。ただし、状態がライフサイクルの後半で読み込まれるため、クライアントによって指定されるオーバーライド バージョンがあればそのバージョンが必ず優先されます。

Figure 7 サーバー側のコントロール (Close Figure 7)
// File: SilverlightAjaxSphere.cs
//
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;

[assembly: WebResource("SlAjaxSphere.SilverlightAjaxSphere.js",
                       "text/javascript")]
[assembly: WebResource("SlAjaxSphere.Silverlight.js",
                       "text/javascript")]
[assembly: WebResource("SlAjaxSphere.Sphere.xaml", "text/xml")]

namespace MsdnMagazine
{
  public class SilverlightAjaxSphere  : Control, IScriptControl
  {
    public string Title
    {
      get { return (string)(ViewState["title"] ?? "[title]"); }
      set { ViewState["title"] = value; }
    }

    public string XamlUrl
    {
      get { return (string)(ViewState["xamlurl"] ??
                            string.Empty); }
      set { ViewState["xamlurl"] = value; }
    }

    protected override void OnInit(EventArgs e)
    {
      // Default the XamlUrl to my embedded resource.
      // If this is set in the
      // markup explicitly, it will be overridden.
      XamlUrl = Page.ClientScript.GetWebResourceUrl(GetType(),
                   "SlAjaxSphere.Sphere.xaml");

      base.OnInit(e);
    }

    public IEnumerable<ScriptReference> GetScriptReferences()
    {
      ScriptReference sr1 = new ScriptReference(
        " SlAjaxSphere.Silverlight.js",
        GetType().Assembly.FullName);
      ScriptReference sr2 = new ScriptReference(
        " SlAjaxSphere.SilverlightAjaxSphere.js",
        GetType().Assembly.FullName);

      return new ScriptReference[] { sr1, sr2 };
    }

    public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
    {
      ScriptControlDescriptor scd = new ScriptControlDescriptor(
                 "MsdnMagazine.SilverlightAjaxSphere",
                 this.ClientID);
      scd.AddProperty("title", this.Title);
      scd.AddProperty("xamlUrl", this.XamlUrl);

      return new ScriptDescriptor[] { scd };
    }

    protected override void OnPreRender(EventArgs e)
    {
      if (!DesignMode)
      {
        ScriptManager sm = ScriptManager.GetCurrent(Page);
        if (sm == null)
          throw new HttpException(
              "A ScriptManager control must exist on the current page.");
        sm.RegisterScriptControl(this);
        sm.RegisterScriptDescriptors(this);
      }

      base.OnPreRender(e);
  }
  protected override void Render(HtmlTextWriter writer)
  {
      writer.AddAttribute(HtmlTextWriterAttribute.Id,
                this.ClientID);
      writer.RenderBeginTag(HtmlTextWriterTag.Div);
      writer.RenderEndTag();

      base.Render(writer);
    }
  }
}

次の 2 つのメソッド、GetScriptDescriptors メソッドと GetScriptReferences メソッドには、IScriptControl インターフェイスの実装が含まれています。JavaScript ファイルをアセンブリ内にリソースとして埋め込むために、ScriptReference のオーバーロードされたコンストラクタを使用します。これはリソース名とアセンブリ名を取得します。これは、RegisterClientScriptResource が前のコントロール実装で行った方法とほぼ同じ方法で埋め込みスクリプト リソースへの参照を生成します。GetScriptDescriptors は、サーバー側コントロールのプロパティを使用して初期化されるクライアント側の MsdnMagazine.SilverlightAjaxSphere クラスのインスタンスの作成にとって重要です。

ScriptControlDescriptor クラスでは、コンストラクタ内のクライアント側クラスの名前と関連付けられているクライアント側要素の ID を取得します。次に、クラスの AddProperty メソッドを使用して、新しいクライアント側クラスの各プロパティの初期値を指定します。コントロールの現在のプロパティ値を渡すことによって、クライアント側クラスが、必ずコントロールで定義されている現在のプロパティ値を使用して作成されるようにします。

それでも、まだ、OnPreRender のオーバーライドで最後のコードを処理する必要があります。これは、スクリプト記述子の場合と同様に、ページ上の現在の ScriptManager を使用してスクリプト コントロールとしてコントロールを登録します (ScriptManager がない場合は例外をスローします)。これにより、ScriptManager はコントロール上で IScriptControl の 2 つのメソッドを呼び出し、スクリプト参照を取得し、初期のクライアント側のクラスを作成します。Render メソッドは前のコントロール実装から変わらず、Silverlight プラグインに対するホストとして機能する <div> 要素を作成するだけです。

これで、Silverlight をカプセル化するサーバー側コントロールが完成します。この実装では、コントロールは XAML の要素の初期化 (または、必要に応じて、XAML 全体を置き換える) に使用されるプロパティを公開します。次の宣言では、ページ上に筆者の新しいコントロールのインスタンスを作成します。タイトル テキストは、カスタム文字列に設定されています。

<%@ Register Assembly="SlAjaxSphere" Namespace="MsdnMagazine"
             TagPrefix="sas" %>
...
<sas:SilverlightSphere ID="_silverlightSphere" runat="server"
                       Title="My custom title!" />

asp:Xaml コントロールおよび asp:Media コントロール

asp:Xaml および asp:Media という 2 つの新しい ASP.NET AJAX コントロールが、2007 年 5 月にリリースされた ASP.NET Futures で導入されました (asp:Media は asp:XAML から継承することに注意してください)。これらの両方のコントロールは、Silverlight の独自のカプセル化を提供し、両方とも前のセクションで示したサンプルとほぼ同じ方法で作成されます。実際は、これらを元にして筆者のコントロールを思い付きました。

asp:Xaml コントロールを使用すると、コントロールを XAML ファイルと関連付けることができます。このコントロールには、Silverlight プラグイン作成用のプロパティが含まれており、プラグインの作成と、XAML ファイルの読み込みに関するすべての詳細を処理します。たとえば、XAML ファイルを使用すると、次の宣言を使用して Sphere.xaml ファイルを表示できます。

<asp:Xaml runat="server" ID="_sphereXaml" Windowless="true"
          Width="300px" Height="300px"
          XamlUrl="~/Sphere.xaml"  />

asp:Media コントロールにより、ASP.NET ページへのメディア コンテンツ (ビデオまたは音声) の埋め込みが容易になり、Silverlight をカプセル化するサーバー側の ASP.NET コントロールを作成する際の可能性を実際に具現化しています。メディア コントロールの興味深い機能の 1 つは、メディアを表示するために選択できる 8 種類の異なるスキン (つまり、XAML ファイル) を提供するということです。または、独自のスキンを設計することができます。図 8 に、次のような方法で Expression スキンを使用してビデオを表示するメディア コントロールの例を示します。

図 8 ビデオを表示するメディア コントロール
図 8 ビデオを表示するメディア コントロール

<asp:Media runat="server" ID="_butterflyVideo"
           MediaSkin="Expression" MediaUrl="~/Butterfly.wmv" />

独自のインストルメント化された Silverlight サーバー側コントロールを作成するもう 1 つのオプションは、asp:Xaml コントロールから直接派生し、筆者が説明した 2 番目のコントロール実装で実行した方法とほぼ同じ方法でプロパティとメソッドを使用して拡張することです。このコラムのコード サンプルには、asp:Xaml から継承する SilverlightSphere コントロールのもう 1 つのバージョンが含まれています。

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

Fritz Onion は、Web 開発のカリキュラムを提供している Microsoft .NET のトレーニング プロバイダである Pluralsight の共同創立者です。 また、『Essential ASP.NET』(Addison Wesley、2003年) と『Essential ASP.NET 2.0』(Addison Wesley、2006年) の著者でもあります。連絡先は pluralsight.com/fritz です。


{ End Bracket }: 技術への情熱

{ End Bracket }
技術への情熱
Scott Hanselman

最近 ブログ社会で議論されているトピックに、私はいつも以上に興味を覚えました。午後 5 時 1 分になると基本的には脳をオフにして帰宅する開発者に向けて、"5 時 1 分の開発者" という言葉は非難がましく使われていたのです。多くの開発者はこの言葉を不快に感じて、自分たちが 1 日に何時間働いているかを主張して立場を守ろうとし始めていました。その他にも "独創性のない開発者" などの言葉が書き込まれていましたが、どのようなレッテルも拒否している開発者がいました。

私にとって "5 時 1 分の開発者" というレッテルは、仕事の時間やタイムカードの時間とは何の関係もありません。それは情熱の欠如を意味するのです。レッテルには関係なく、ソフトウェアの分野で、またはどのような分野であっても、他より抜きん出るには多くの情熱、創造性、関心、活動、熱意、刺激が必要であると私は考えます。皆さんの仕事は旋盤を回したり、世界中を驚かせる画期的な新しい旋盤を設計したりすることかもしれませんが、成功したいのであれば (同僚と比べるかどうかに関係なく) 情熱が大きな役割を果たします。

私の家の周りには木がたくさんあります。真面目な話、私は器用ではありません。電動のこぎりを使ったら間違いなくけがをするでしょう。さらに重要な点ですが、私は木工作業にまったく情熱がないのです。他の人が作業するのを見るのは楽しいですし、その努力は評価しますが、木を使ってリビングの家具を自作しなければならなくなったら、私は間違いなく 5 時 1 分にタイムカードを切って退社するでしょう。この満足感の欠如が、木片をぞんざいにくっつけて作った椅子が壊れないことを祈る職人気質離れを生むのです。また、職人気質の感覚も仕事に対する情熱と同様に重要です。"仕事" と "技術" という言葉には、大きな意味の違いがあります。職人は、見習いから始めて熟練者に付いて学び、やがて一人前の職人へと育っていきます。情熱、才能、および責務の意識を持った職人によって作成されたソフトウェアは、すぐにわかるでしょう。ソフトウェア管理者は、他人の業務に関して自己責任を負います。これは、ソフトウェアの場合であれば、インストールから実行までユーザーが行う操作です。

私は、このトピックに対する自分の考えを自分のブログに掲載しました。ブログではよくあることですが、真に貴重な意見は自分の投稿ではなく、他の読者からのコメントの中にありました。Max Pool からはすぐに次のようなコメントが寄せられました。

職業をたずねられて「私はソフトウェア開発者です」と答える人は、誇りを持って自分の人格と自分の職業とを結び付けています。この簡単な答えから、毎日の仕事にどのくらい情熱を持っているかについて多くのことがわかります。

Paul Brazelton はすべてを考慮した結果として、次のような考えを寄せてくれました。

私は父親であり、ランナーであり、ボートの漕ぎ手であり、環境問題の研究家であり、人間であり、そしてもちろん、プログラマです。私は生きている限り学び続けます。プログラミング言語だけでなく、すべてについて学ぶことを愛しています。テクノロジは好きですが、この世界には他にも愛すべきものがたくさんあり、私たちの時間は短く限られています。

明らかに、情熱、職人気質、および責務は、あらゆるソフトウェア エンジニア (またはテスト担当者やマネージャ) のより大きな労働倫理の一部としてあるべきものです。ときに、人は情熱の欠如を勤務年数や、仕事の種類や、ツールの品質のせいにします。確かに、ツールがうまく動かないと作業に影響します。私は、今のところ特にソフトウェアに期待しています。これまでより多くの仕事をこれまでより早く終わらせることがどんどん簡単になっています。

開発者として働いていて、物事が自分にとって新鮮であり続けているのは、選択肢があるおかげだと考えています。常に新しいものばかりが登場するのではなく、より優れた技法、新しい考え方、従来のテクノロジを探求する新しい人々も常に存在します。今後数回にわたって、IronRuby と、CLR で有効になる多くの新しい動的言語について詳しく説明します。ASP.NET で実行される Model-View-Controller (MVC) Web フレームワークと、管理サイト全体を自動的に作成するコード生成ツールについて見ていきます。現在は、Xbox® のゲームと Media Center 用のプラグインを作成しています。また、是が非でも愛用の Ceiva Digital Picture Frame をハッキングするつもりです。もちろん、これらはすべて仕事が終わった後に行います。これで優れた開発者になれるでしょうか。それとも、ただ何かに取り付かれているだけでしょうか。おそらくどちらも少しあてはまるのでしょう。

この種の情熱 (または強迫観念) は、教わることができるのでしょうか。教わる必要があるのでしょうか。私は自分が職人なのかただの変人なのかはわかりません。しかし、仕事を楽しみ、仕事に誇りを持っていて、毎日何かを学んでいることはわかっています。自分の技術に情熱を持てないのであれば、なぜそれをやるのでしょうか。私なら毎日仕事をしながら、仕事を好きになるための何かを見つけずにいるなんて考えられません。基本的な質問に立ち返ると、自分の技術を大切に思うかという問題になります。おそらく仕事に対してはそう思わなくとも、技術に対しては思うかもしれません。James Curran はその苦しみを感じています。

私は 5 時 1 分のプログラマとして主張します。私は技術には情熱を持っていますが、日中の仕事には興味が持てません (帰宅する電車内でラップトップを使ってオープン ソース プロジェクトに取り組んでいます)。

結局は、情熱を傾けられるものを見つけ、それを維持する方法を見つけることにかかわってくるのです。

Scott Hanselman は、マイクロソフトの開発部門で DevDiv コミュニティの連絡係を担当しており、自分にできるあらゆる方法でソフトウェア開発に役立つ情報を広めようとしています。彼のブログは www.hanselman.com、ポッドキャストは www.hanselminutes.com で公開されています。また、彼は www.asp.net に寄稿しています。