: MSDN Magazine January 2007

January 2007
JANUARY 2007 ボリューム 22 番号 1

• 最小限の特権: アプリケーションで Windows Vista のユーザー アカウント制御を有効に活用する
Chris Corio

• Vista および Office: マネージ プレビュー ハンドラ フレームワークで独自の方法でデータを表示する
Stephen Toub

• SideShow ガジェット: Windows SideShow デバイスのガジェットを記述する
Jeffrey Richter

• デスクトップ セキュリティ: Windows Vista 用の資格情報プロバイダを使用したカスタム ログイン機能の作成
Dan Griffin

• 現在の Windows: Unicode 5.0 を使用したアプリケーションの世界的勢力の拡張
Julie D. Allen、Michael S. Kaplan、Cathy Wissink

• リークが発生するアプリケーションのデバッグ: マネージ コードでのメモリ リークの識別と回避
James Kovacs

Code Download (2150 KB) HTMLHelp Format
コラム

• CLR の完全理解: COM 相互運用機能の概要
Thottam R. Sriram

• データ ポイント: 列式、データ関係、および計算
John Papa

• Cutting Edge: サーバー側の ASP.NET ページ
Dino Esposito

• 基礎: テンプレートを使用した WPF コントロールのカスタマイズ
Charles Petzold

• Team System: Team Foundation Server でのバージョン管理
Brian A. Randell

• すてきな ASP.NET: AJAX Extensions を使用してクライアント側から Web サービスを呼び出す
Fritz Onion

• セキュリティに関するブリーフィング: プロトコル遷移の使用£最前線からのヒント
Keith Brown

• {End Bracket}: スーパーボールと見積り精度の向上
James Waletzky

各出版物

• 編集者のメモ: 新しい年に新しい Vista を
Joshua Trupin

• ツール ボックス: コーディングにかかる時間を短縮する、圧縮ファイルを管理する
Scott Mitchell


最小限の特権: アプリケーションで Windows Vista のユーザー アカウント制御を有効に活用する

最小限の特権

アプリケーションで Windows Vista のユーザー アカウント制御を有効に活用する

Chris Corio
この記事の内容 : :
  • ユーザー アカウント制御
  • 標準ユーザーで実行できるようにアプリケーションをデザインする
  • 必要に応じて特権を昇格する
この記事は次のテクノロジを使用しています:
Windows Vista
コンテンツ
ログオン時の起動
フィルタ処理したトークンを作成する
フィルタ処理したトークンで実行できるようにアプリケーションをデザインする
管理者特権を必要とするアプリケーションをデザインする
インストール中の特権
MSI に特権をマークする
管理者特権でアプリケーションを実行する
アプリケーション マニフェストを使用して必要な特権をマークする
COM オブジェクトをマークする
特権が昇格することをユーザーに警告する
異なるセキュリティ コンテキストのプロセス間で通信する
低い権限の Internet Explorer
Windows Vista で Windows サービスのセキュリティを保護する
アプリケーション互換性に共通する問題
インストール時の問題
アプリケーション実行時の典型的な問題
まとめ

私はここ数年多くの開発者と作業を行ってきましたが、ユーザー アカウント制御 (UAC) のしくみについての開発者の考え方や最終的な理解が非常にあいまいであることがわかりました。

実際、標準ユーザーにとって適切なアプリケーションの作成方法を理解するには時間がかかりますが、これをきちんと理解すれば、Windows® の優れたプログラマになれることは間違いありません。この記事の目的は、こうした知識を迅速かつ容易に得るために必要な情報を提供することです。

Windows Vista™ において、ユーザーが作業の実行に使用する特権を低く抑えるためにマイクロソフトが用意した解決策が UAC です。マイクロソフトでは、オペレーティング システムやコンピュータ全体の構成に影響を与える可能性のある特権を必要としないでユーザーが日常業務を行える環境に戦略的に移行しています。このような環境であれば、複数のユーザーが相互の状態や設定に影響を与えることもなくなります。

こうした環境の移行には、実用的な理由が数多くあります。UAC は、アプリケーションの実行に既定で使用する特権を制限します。企業では、Administrators グループではなく Users グループに所属するユーザーがアプリケーションを実行できるようになり、企業の重要なセキュリティ ポリシーを適用できるようになります。その結果、ユーザーがコンピュータに影響を与えることが少なくなり、総保有コスト (TCO) が低下します。家庭では、マルウェアがシステムに影響を与える可能性がなくなり、UAC の同意や資格情報の確認なしにサービスやドライバをインストールすることはできなくなります。ソーシャル エンジニアリングの危険性は残りますが、昇格した特権の入手経路が整理されたので、今後実装されるテクノロジによって、ユーザーに確認する必要なくコンピュータ全体の特権を使用して実行できるアプリケーションの制御が可能になります。

UAC の主な目的は、アプリケーションの実行に使用する既定のユーザー トークンを、Users グループのメンバのユーザー トークンと同等のものにすることです。UAC では、まず、昇格された特権を持つユーザーが対話形式でログオンする際に、制限付き (つまり、フィルタ処理した) トークンを作成します。制限付きの操作の実行に特権の昇格が必要になると、セキュリティが確保されたデスクトップで認証が要求されます。UAC には、アプリケーション互換性をサポートするテクノロジや、特権を昇格したプロセスのセキュリティを保護するテクノロジなどもあります。

ログオン時の起動

UAC は、ユーザーがコンピュータにログオンするときに動作を開始します。ローカル セキュリティ機関 (LSA) は、対話形式のログオン処理中にユーザーの資格情報を受け取り、初回のログオンを実行します。このとき、ユーザーのトークンを評価し、特権を昇格して実行する必要のある操作が定義されているかどうかを確認します。ユーザーが昇格した特権を持っていると判断すると、ユーザーのトークンをフィルタ処理し、フィルタ処理したトークンを使用して 2 回目のログオンを実行します。フィルタ処理したトークンと完全な特権を備えたトークンの両方を持つ最も一般的なユーザーは、Administrators グループのメンバです。管理者として完全な特権を備えたトークンは、Program Files や HKEY_LOCAL_MACHINE (HKLM) など、コンピュータ全体からアクセスされる場所に情報を書き込むことができるため、その操作はコンピュータ全体に影響します。しかし、フィルタ処理したトークンは Users グループのメンバと同じで管理者特権がなく、ユーザーの対話型デスクトップ セッションの作成に使用されます。

デスクトップ セッションや explorer.exe は、常に、Users グループのメンバのトークンに似たトークンを使って作成されます。[スタート] メニューから開始されるプロセスやエクスプローラ ウィンドウでダブルクリックして開始されるプロセスは特権を昇格する必要がなく、このフィルタ処理したトークンを単純に継承します。そのため、既定では、すべてのアプリケーションが標準ユーザー トークンで実行されます。マニフェストやアプリケーション互換性の設定で管理者特権が必要であるとマークされたプロセスは、UAC からユーザーに特権の昇格を確認します。図 1 と図 2 は、同意を求めるダイアログ ボックスと資格情報を確認するダイアログ ボックスを示しています。ユーザーが特権の昇格に同意すると、アプリケーション情報サービスが、完全な特権を備えたトークンを使ってプロセスを作成します。

図 1 同意を求めるダイアログ ボックス
図 1 同意を求めるダイアログ ボックス(画像を縮小するには、ここをクリックします)
図 1 同意を求めるダイアログ ボックス
図 1 同意を求めるダイアログ ボックス (画像を拡大するには、ここをクリックします)
図 2 資格情報を確認するダイアログ ボックス
図 2 資格情報を確認するダイアログ ボックス(画像を縮小するには、ここをクリックします)
図 2 資格情報を確認するダイアログ ボックス
図 2 資格情報を確認するダイアログ ボックス (画像を拡大するには、ここをクリックします)

フィルタ処理したトークンを作成する

ユーザー アカウント制御では、図 3 に記載したグループを、昇格した特権を備えたグループと定義します。LSA では、これらのグループのメンバシップまたは特権のいずれかがユーザーの初回トークンに含まれていることがわかると、対話形式のログイン中に CreateRestrictedToken API バージョンを使用してフィルタ処理したトークンを作成し、完全な特権を備えたトークンを保存します。これら 2 つのトークンはリンクされ、TokenLinkedToken という新しい情報型と GetTokenInformation API を併用して、フィルタ処理したトークンから完全な特権を備えたトークンを取得できます。ただし、UAC はサービス、ネットワーク、またはバッチ ログオンに作用しないことに注意してください。

Figure 3 昇格した特権を備えるグループ (Close Figure 3)

ビルトイン Administrators
Power Users
Account Operators
Server Operators
Printer Operators
Backup Operators
RAS Servers Group
Windows NT 4.0 App Compat Group
Network Configuration Operators
Domain Administrators
Domain Controllers
Certificate Publishers
Schema Administrators
Enterprise Administrators
Group Policy Administrators

ユーザーが図 3 に示したグループに属していなくても特定の特権を持っていれば、こうした特権を削除した状態にフィルタ処理したトークンを作成します。こうした特権には、SeCreateTokenPrivilege、SeTcbPrivilege、SeTakeOwnershipPrivilege、SeBackupPrivilege、SeRestorePrivilege、SeDebugPrivilege、SeImpersonatePrivilege、および SeRelabelPrivilege があります。

ユーザーが Administrators グループに所属している場合、フィルタ処理したトークンでは、Administrators グループのメンバシップに DENY ONLY が設定されます。これにより、AccessCheck では、このグループを使用したリソースへのアクセスが許可されません。また、コンピュータに影響するすべての特権をトークンから削除します (これが、explorer.exe および特権が昇格されていないプロセスの作成に使用される既定のトークンです)。このトークンは、Windows Vista に付属する whoami.exe を使用して表示できます。コマンド ウィンドウで whoami.exe /all を実行するだけです。図 4 と図 5 は、トークンで一覧されるグループ メンバシップとトークンの特権をそれぞれ示しています。

Figure 5 フィルタ処理したトークンに含まれる特権 (Close Figure 5)

特権 | 説明
SeShutdownPrivilege | システムをシャットダウンします。
SeChangeNotifyPrivilege | 走査チェックをバイパスします。
SeUndockPrivilege | ドッキング ステーションからコンピュータを取り外します。
SeIncreaseWorkingSetPrivilege | プロセス ワーキング セットを増加します。
SeTimeZonePrivilege | タイム ゾーンを変更します。
Figure 4 フィルタ処理したトークンに一覧されるグループ メンバシップ (Close Figure 4)

グループ名 | 属性
グループ名 | 属性
Everyone | 固定グループ、既定で有効、有効なグループ
BUILTIN\Administrators | 拒否のみに使用されるグループ
BUILTIN\Users | 固定グループ、既定で有効、有効なグループ
Mandatory Label\Medium Mandatory Level | 固定グループ、既定で有効、有効なグループ

フィルタ処理したトークンで実行できるようにアプリケーションをデザインする

ほとんどのアプリケーションでは、実行時に管理者特権は必要ありません。実行中にセッション間の状態を保持せず、ローカル セキュリティ ポリシーの変更などの処理を行わないアプリケーションは、標準ユーザー トークンで適切に実行されます。アプリケーションの特定部分で管理者特権が必要になる場合は、こうした部分を別のプロセスに分離します。これについては後で説明します。

標準ユーザー アプリケーションの開発時に実行できる最も重要な手順は、標準ユーザーで実行してそのアプリケーションをテストすることです。運用環境でアプリケーション エラーが発生する最も一般的な原因の 1 つに、開発者が標準ユーザーとしてそのアプリケーションのテストを行っていなかったことが挙げられます。このテストは忘れずに行ってください。

次に重要な手順は、アプリケーション バイナリとユーザーごとの構成データの保存先を決定することです。完全にユーザー単位の (つまり、アプリケーションのバイナリと構成の両方がユーザー固有である) アプリケーションは、その状態をすべてユーザー プロファイル (%userprofile%) で管理します。レジストリでは、すべての状態をユーザーのハイブ HKEY_CURRENT_USER (HKCU) に書き込む必要があります。多くの場合、バイナリを %ProgramFiles% ディレクトリにインストールすることが適しています。ただし、この場合には管理者特権が必要になります。このことについては、後で説明します。

ゲームのハイ スコアを保存するときなど、システム単位で状態を保持し、ユーザーがその状態に書き込めるようにする必要がある場合もあります。このようなデータは %allusersprofile% ディレクトリに格納することをお勧めします。ここにアプリケーション固有のディレクトリを作成し、ユーザーがそのディレクトリに書き込めるようにできます。ただし、他のユーザーがこのファイルを変更できることに注意してください。

管理者特権を必要とするアプリケーションをデザインする

率直に言えば、管理者特権を必要とするようなアプリケーションは、作成しないでください。アプリケーションで特権の昇格が確実に必要な場合を除いて、標準ユーザー トークンを使用するアプリケーションを作成します。管理者が呼び出す必要のある API はほとんどありません。

当然ながら、インストール時やセッション間の状態を保持する場合など、管理者特権の使用が必要になる状況もあります。これは、通常、アプリケーションがサービスとして実装されることを意味します。UAC はサービスには適用されません。Windows サービスに使用できるセキュリティ設定もありますが、これについては後で説明します。アプリケーションがローカル コンピュータ上にセキュリティ モデルを実装する場合も、管理者特権が必要になります。たとえば、Group Policy Application Distribution and Management (ADM) や Systems Management Server (SMS) を使用して展開される設定がある場合は、ローカル コンピュータ上のアプリケーションでもその設定を更新することになります。これには、管理者特権が必要です。

インストール中の特権

通常、アプリケーションのインストールには管理者特権が必要です。これは、多くの場合、アプリケーション バイナリを Program Files ディレクトリ (標準ユーザーの場合は読み取り専用) に書き込むためです。つまり、バイナリをこの場所に書き込むアプリケーションには、管理者特権が必要であることをマークする必要があります。Windows インストーラが UAC と統合されたため、インストール パッケージ (MSI ファイル) は、さらに魅力的なインストール方法になりました。インストール パッケージを指定する方法には、ユーザー単位のインストールとコンピュータ単位のインストールの 2 種類があります。コンピュータ単位のインストールの場合のみ、必要に応じて、管理者特権が要求されます。

Program Files ディレクトリにインストールするときに、ユーザーは特権の昇格を確認されますが、アプリケーション バイナリをこのディレクトリに格納することには確固としたメリットがあります。そのディレクトリにインストールするには管理者特権が必要なので、企業内の標準ユーザーや保護者による制限を受けるユーザーは、自由にアプリケーションをインストールできなくなります。また、こうしたアプリケーションはコンピュータ全体で 1 つのバイナリ セットを共有するので、標準ユーザーがバイナリを変更できないように保護されます。その結果、バグ修正後や機能の追加時にバイナリを更新する際に、アプリケーションの状態やバージョンの判断が容易になります。

アプリケーションのデザイン時に、各ユーザーに適用するアプリケーション設定が必要になることがあります。ユーザーはこのような設定を使用して、ビューの表示方法や表示するメニューに関するカスタム情報を指定します。この機能を提供するには、Program Files ディレクトリの下に既定のテンプレートを作成してから、そのテンプレートをユーザーの AppData ディレクトリ %localappdata% にコピーして変更することをお勧めします。このデータは、アプリケーションの初回実行時にコピーできます。その結果、ユーザーごとに状態の設定が可能になります。一方、上記のハイ スコアの例のように、コンピュータ全体で 1 つの状態を用意し、各ユーザーがこの状態を変更できるようにするには、この状態を %allusersprofile% に格納します。

UAC 環境でインストールによって問題が発生する別の要因は、更新ですProgram Files ディレクトリのバイナリを更新する場合、プロセスでバイナリを上書きするには、管理者特権またはシステム特権が必要です。Windows インストーラを使用するメリットの 1 つは、MSI パッチ (MSP) ファイルを使用して修正プログラムを適用できることです。Windows インストーラ 3.1 (Windows XP 用の必須更新プログラム) 以降、オリジナルの MSI ファイルに含まれていた証明書で MSP ファイルに署名されていれば、そのファイルがシステムに適用されます。詳細については、MSDN® の「User Account Control (UAC) Patching」 (http://msdn2.microsoft.com/en-us/library/aa372388.aspx) (英語) を参照してください。

アプリケーション開発者は、MSI ファイルを使用する以外に、管理者特権で実行するように指定されているバイナリを統合したり (これにより、企業内では標準ユーザーによる更新が禁止されます)、必要なすべての更新を実行するサービスを含めたりできます。ただし、サービスにセキュリティ上の脆弱性があると、ISV がマルウェアの攻撃を受ける原因になるので、サービスを配布することはあまりお勧めしません。MSP ファイルが最善の方法です。

MSI に特権をマークする

Windows Vista より前は、MSI ファイルでアプリケーションのショートカットをユーザーの場所にインストールするか、コンピュータ上のすべてのユーザー用にインストールするかをマークするのに ALLUSERS プロパティを使用していました。このようなショートカットには、DesktopFolder、ProgramMenuFolder、StartMenuFolder、および StartupFolder がありました。ユーザーごとの Program Files ディレクトリのようなものは存在しなかったため、アプリケーション バイナリは、通常、Program Files ディレクトリに書き込まれていました。

残念ながら、アプリケーション互換性の理由により、Windows インストーラでは ALLUSERS プロパティのみを基準にするしかなく、ユーザーに資格情報を問い合わせるかどうかを判断できませんでした。代わりに、追加のビットを MSI ファイル内に割り当て、ユーザーに資格情報を問い合わせるかどうかを判断していました。この追加ビットが Word Count Summary プロパティのビット 3 です。このビットに 1 を設定すると、そのパッケージはユーザー単位の MSI と見なされ、ユーザーには管理者トークンの問い合わせが行われません。

管理者だけがパッケージをパブリック プロファイルにインストールできるように指定するには、ALLUSERS="1" または ALLUSERS="2" を設定し、Word Count Summary プロパティのビット 3 に 0 を設定します。標準ユーザーがインストールできるユーザー単位のインストールであることを指定するには、ALLUSERS="" を設定するかこのプロパティを定義せずに、Word Count Summary プロパティのビット 3 に 1 を設定します。

管理者特権でアプリケーションを実行する

アプリケーションの実行に管理者特権が必要になることもあります。たとえば、HKLM にコンピュータ全体の設定を設定するハードウェアやアプリケーションと直接対話するコードを記述することができます。可能な場合は、管理者特権の必要性を限られたコード部分に限定するようにアプリケーションをデザインします。または、完全な管理者特権を使って起動されたアプリケーションと通信するようにします。

ユーザーの完全な管理者トークンを使用してプロセスを作成する方法は、MSI に基づく昇格以外に 2 とおりあります。アプリケーション情報サービス (AIS) では、プロセスの作成時および CoCreateAsAdmin モニカを使用した COM オブジェクトの作成時に、バイナリが管理者特権を必要とするかどうかをチェックします。ここで重要なのは、プロセスの作成時に昇格が行われる点です。プロセス トークンは実行時に追加される特権やグループ メンバシップを持つことはなく、作成時にのみ特権を持ちます。

ここで一言注意しておきますが、ShellExecute を呼び出してプロセスを作成する場合しか UAC から特権の昇格をユーザーに確認する問い合わせは行われません。つまり、昇格を要求する CreateProcess ファミリの呼び出しを行った場合に ERROR_ELEVATION_REQUIRED が返されます。

アプリケーション マニフェストを使用して必要な特権をマークする

AIS では、新しいプロセスの作成時にバイナリを調べ、特権の昇格が必要かどうかを判断します。まず、アプリケーションのリソースに埋め込まれたアプリケーション マニフェストをチェックします。このチェックは、アプリケーション互換性マークなどのアプリケーションのマークや UAC のインストーラ検出よりも優先して行われます。UAC のインストーラ検出については、後で説明します。マニフェストでは実行レベルを定義して、バイナリの実行に必要な特権を Windows に通知します。定義する実行レベルには、asInvoker、highestAvailable、および requireAdministrator の 3 つの選択肢があります。

AIS では "asInvoker" 実行レベルがマークされたバイナリを検出すると、何も操作を行わず、プロセスはそのプロセスを作成した親プロセスのプロセス トークンを継承します。"requireAdministrator" 実行レベルは語句どおりの意味で、管理者グループのメンバであるユーザー トークンを使ってプロセスを作成する必要があることを定義します。このプロセスの作成を試みたユーザーが管理者でなければ、自身の資格情報の入力を求めるダイアログ ボックスが表示されます。

highestAvailable 実行レベルはやや複雑です。この実行レベルは、ユーザーがリンク トークンを持っていれば、より特権の高いトークンを使用してアプリケーションを実行することを示します。この実行レベルは、一般に、Users グループと Administrators グループ用に UI がデザインされているアプリケーションに使用されます。このレベルでは、アプリケーションが確実にユーザーの完全な特権を取得します。Backup Operators グループと Network Operators グループのユーザーはリンク トークンを持っていても、資格情報が確認されることに注意してください。この場合、資格情報を確認するダイアログ ボックスには、ユーザー タイルと、Administrators グループのメンバの資格情報が表示されます。図 6 は、アプリケーション マニフェストの例です。

Figure 6 AdminAppl.exe の AdminApp.exe.manifest (Close Figure 6)
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity version="1.0.0.0"
     processorArchitecture="X86" name="AdminApp" type="win32"/>
  <description>Description of your application</description>
  <!-- Identify the application security requirements. -->
  <ms_asmv2:trustInfo xmlns:ms_asmv2="urn:schemas-microsoft-com:asm.v2">
    <ms_asmv2:security>
      <ms_asmv2:requestedPrivileges>
        <ms_asmv2:requestedExecutionLevel
          level="requireAdministrator"
          uiAccess="false"/>
        </ms_asmv2:requestedPrivileges>
       </ms_asmv2:security>
  </ms_asmv2:trustInfo>
</assembly>

アプリケーション マニフェストのマークは、DLL ではなく EXE のみに関連します。これは、UAC がプロセスの作成中に DLL を調査しないためです。このマニフェストをネイティブ アプリケーションに埋め込むには、マニフェスト ファイルをソース ファイルと同じディレクトリにコピーし、マニフェストをリソース ファイルに追加して、マニフェストを埋め込むことを指定します。.rc ファイルの次の行では、AdminApp.exe.manifest として保存した場合に上記のマニフェストが埋め込まれることになります。

#define MANIFEST_RESOURCE_ID 1
MANIFEST_RESOURCE_ID RT_MANIFEST "AdminApp.exe.manifest"

COM オブジェクトをマークする

COM オブジェクトの作成時に CoCreateAsAdmin モニカを使用して、COM オブジェクトを昇格した特権で実行するように設定できます。これは、特権を昇格した状態で実行され、アプリケーションの UI に公開されるオブジェクトを作成する際に非常に役立ちます。

COM オブジェクトを完全な特権を持つトークンで実行できるようにするにはいくつか追加設定が必要です。UAC の特権の昇格を確認するダイアログ ボックスに表示する表示名を指定すると同時に、昇格した特権でオブジェクトを作成できることを示すマークを指定する必要があります。また、次のレジストリ キーをオブジェクトの登録に追加する必要があります。

HKEY_LOCAL_MACHINE\Software\Classes\CLSID\
 {CLSID}\LocalizedString = <displayname>
HKEY_LOCAL_MACHINE\Software\Classes\CLSID\
 {CLSID}\Elevation\Enabled = 1

COM オブジェクトが HKLM に登録されておらず、これらの情報を指定していない場合は、特権を昇格した COM オブジェクトの作成に失敗します。オブジェクトの作成に成功すると、フィルタ処理したトークンでプロセスから呼び出しが行われる場合、特権の昇格がユーザーに問い合わされ、その COM オブジェクトをホストするために新しいプロセスが作成されます。特権を昇格した COM オブジェクトの作成の詳細については、CoCreateAsAdmin モニカに関する MSDN ドキュメント (msdn2.microsoft.com/ms679687.aspx) (英語) を参照してください。

特権が昇格することをユーザーに警告する

UAC に関するもう 1 つの課題は、低い特権で実行されているアプリケーションと、管理者として実行されているアプリケーションの違いを認識していない可能性のあるユーザーへの通知です。この違いを理解するには、特権昇格の確認が行われる際の標準ワークフローが重要になります。アイコンに盾の画像が付いたユーザー インターフェイス要素をユーザーがクリックすると必ず、昇格シーケンスが開始されます。アプリケーションを起動することで昇格が行われる場合は、アイコンに盾が付いています。アプリケーション内で昇格を行う場所にはすべて盾が付きます。

コモン コントロールの多くが更新され、盾を追加できるようになりました。これはボタンやリンクをクリックすると、特権の昇格を必要とする新しいプロセスが作成される設定を公開する場合に特に役立ちます。盾の画像を追加するコードは非常に単純です。たとえば、標準ボタン コントロール (PUSHBUTTON、DEFPUSHBUTTON) は、BS_ICON スタイルや BS_BITMAP スタイルを設定する必要なく、表示されるテキストにアイコンを追加できるように拡張されています。

図 7 ボタン上の盾
図 7 ボタン上の盾

(図 7 にある) 盾アイコンを表示するには (commctrl.h で定義する) 次のマクロを呼び出します。このマクロは、SETSHIELD メッセージをボタンに送信します。

Button_SetElevationRequiredState(hwndButton, true);

盾を表示する同様のスタイルは、Aero™ ウィザードのボタン、Syslink コントロールや Hyperlink コントロール、Commandlink ボタンや Command ボタンなどに使用できます。

異なるセキュリティ コンテキストのプロセス間で通信する

Windows Vista には、Windows Integrity Mechanism (WIM) と呼ばれる認証テクノロジもあります。WIM では、今後の承認でオブジェクトのラベルとして使用されるさまざまなセキュリティ レベルを定義します。このレベルには、低 (Low)、中 (Medium)、高 (High)、およびシステム (System) があります。UAC の採用により、同じデスクトップ セッション内に複数のプロセスが存在する可能性があります。同じデスクトップ セッション内でも、あるプロセスは完全な管理者特権を持ち、あるプロセスは標準ユーザー トークンで実行されます。UAC では WIM を使用して、特権レベルが異なるプロセス間での Windows メッセージの送信をブロックします。

デスクトップの作成に使用されるトークンは、整合性レベル "中" で実行されます。フィルタ処理した管理者トークンのサンプルを参照してください。既定では、整合性レベル "中" ですべてが実行されます。特権昇格の問い合わせにユーザーが同意すると、プロセスが完全な特権を備えたトークンを使用して作成され、整合性レベル "高" が設定されます。以前、低い特権の UI から高い特権の UI にメッセージを送信して、より高い特権でプロセスを実行しようとする、"壊滅的攻撃" という攻撃がありました。この攻撃を回避するため、ほとんどの Windows メッセージは、低い特権のプロセスから高い特権のプロセスに送信されなくなりました。これは整合性レベルに基づいて行われます。

ただし、整合性レベルの異なるプロセス間で通信する方法は依然として存在します。共有メモリまたはリモート プロシージャ コール (RPC) を使用できます。RPC エントリ ポイントや IPC メカニズムを作成するときは、標準ユーザーで実行されているプロセスから接続できるように指定してください。これは、オブジェクトのセキュリティ記述子で指定する必要があります。

低い権限の Internet Explorer

WIM と UAC に関してもう 1 つ、低い権限の Internet Explorer®、つまり、ユーザーが Web を参照中にそのユーザーを保護するために、整合性レベル "低" を使用する設定も可能です。整合性レベル "低" で実行されるプロセスは、システム上の非常に限られたディレクトリやファイルしか操作できません。Internet Explorer がホストする ActiveX® コントロールをアプリケーションの一部として実装している場合、そのコントロールをテストして正しく動作することを確認します。詳細については、ホワイト ペーパー (http://www.microsoft.com/japan/msdn/ie/general/protectedmode.aspx) を参照してください。

Windows Vista で Windows サービスのセキュリティを保護する

Windows Vista には、サービスをロックダウンするための新機能が多数あります。開発者は、サービスに必要な特権レベルを指定できます。また、サービスには適用可能なセキュリティ識別子 (SID) を含めることができます。SID を使用してリソースのセキュリティを保護することで、そのサービスのみがリソースに書き込むことができるようになります。サービスからネットワークにアクセスしないようにもできます。詳細については、http://www.microsoft.com/japan/whdc/system/vista/Vista_Services.mspx で入手できる Windows サービスに関するドキュメントを参照してください。

Windows Vista でも、これらの新しいセキュリティ パラメータを sc.exe を使用して定義できます (図 8 参照)。Privileges は、スラッシュ (/) で区切られた特権のリストを含む文字列です。たとえば、バックアップと復元の特権を指定するには、Privileges に SeBackupPrivilege/SeRestorePrivilege を設定します。

Figure 8 Sc.exe のコマンド (Close Figure 8)

コマンド | 構文 | 目的
Privs | sc <server> privs [service name} [privileges] | サービスに必要な特権を設定します。
Qprivs | sc <server> qprivs [service name] buffersize | サービスの必要な特権を照会します。
Sidtype | sc <server> sidtype [service name] [type] | サービスの SID をプロセス トークンに適用します。
Qsidtype | sc <server> qsidtype [service name] | サービスの SID の設定を取得します。

Sc.exe を使用して、指定したサービスの SID を取得することもできます。次のコマンドは、service name という名前のサービスの SID を返します。

Sc showsid [service name]

この情報をプログラムで変更するには、ChangeServiceConfig2 という新しい API を使用します。指定したサービスの特権を変更するには、以下のパラメータ値を指定して ChangeServiceConfig2 を呼び出します (特権の変更は、次にサービスが開始されるときに有効になります)。

dwInfo パラメータには SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO を指定し、lpInfo バッファは SERVICE_REQUIRED_PRIVILEGES_INFO 構造体を指すようにします。この構造体には、1 つで複数の必要な特権を示す文字列を含めます。

サービスにサービス SID を設定するには、プログラムで以下のパラメータ値を指定して ChangeServiceConfig2 を呼び出し、フラグを設定します (この変更は、次にシステムを起動したときに有効になります)。dwInfo パラメータには SERVICE_CONFIG_SERVICE_SID_INFO を設定し、lpInfo バッファは SERVICE_SID_INFO 構造体を指すように設定します。この構造体には、SID 型を保持する 1 つの DWORD メンバを含めます。

サービスの所有者にとって有効な、関連する 2 つのパブリック関数 LookupAccountName と LookupAccountSID があります。LookupAccountName は SID を受け取り、その SID に関連付けられたサービス名を返します。それに対して、LookupAccountSID はサービス名を受け取り、そのサービス名に関連付けられた SID を返します。これらの設定値を照会するには、QueryServiceConfig2 API を使用します。

サービスを保護するもう 1 つの方法は、サービスからネットワークへのアクセスを制限することです。サービスに対するファイアウォールの制限を構成するには、INetFwServiceRestriction インターフェイスを使用します。

最後に、低い特権のプロセスから接続できる RPC インターフェイスの公開には注意してください。こうしたインターフェイスは、ユーザーの代わりに実行できる作業でユーザーによる特権の昇格が許可されないように、慎重にデザインおよびテストする必要があります。

アプリケーション互換性に共通する問題

UAC を導入する際にチームが直面した最大の課題の 1 つは、UAC の導入がアプリケーション エコシステムに大きな影響を与えることでした。これまでの多くのアプリケーションは、Users グループのメンバで実行されるようにはデザインされていませんでした。マイクロソフトでは、アプリケーション互換性に共通する問題を把握するのにかなりの時間を費やし、こうした問題のいくつかを解決するために Windows Vista に新たなテクノロジを追加しました。この記事の最後のセクションでは、こうした問題点をいくつか示し、その解決方法を簡単に説明します。

UAC に含まれる最初のアプリケーション互換性テクノロジは、インストーラ検出と呼ばれます。ほとんどのインストーラは Program Files ディレクトリにバイナリを書き込むため、ほぼ確実に管理者特権が必要になります。インストーラ検出は EXE の名前とリソースをスキャンして、アプリケーションがインストーラかどうかを判断するようにデザインされています。たとえば、実行可能ファイルの名前または説明に "install" または "setup" という文字列が含まれている場合、その実行可能ファイルはインストーラとしてマークされます。そのため、アプリケーション マニフェストを含まない setup.exe というアプリケーションが管理者特権のないトークンによって起動されると、UAC の特権の昇格確認が行われます。

UAC 関連のもう 1 つのテクノロジはファイルとレジストリの仮想化と呼ばれ、実行時にファイルやレジストリの問題を解決できるようにデザインされています。多くのアプリケーションは、ログ ファイルや構成を Program Files ディレクトリや Windows ディレクトリに書き込みます。標準ユーザーがこれらのディレクトリに書き込むことは許可されていないため、こうした書き込みはすぐに失敗します。ファイルの仮想化ではこのような問題を解決するために、ファイル システム内の保護された特定の場所に書き込まれるファイルを受け取り、書き込みをユーザーのプロファイルにリダイレクトします。書き込まれるファイルがバイナリの場合、仮想化は行われません。レジストリの仮想化では、HKLM\Software への書き込みをレジストリ内のユーザーごとの場所にリダイレクトすることで、レジストリに対して同じ処理を行います。これらのテクノロジにより、従来のアプリケーションのアプリケーション互換性が大幅に高まります。

バイナリにアプリケーション マニフェストが含まれる場合、またはバイナリが 64 ビット用にネイティブにコンパイルされている場合、これらのテクノロジが無効になることに注意してください。これらのテクノロジがデザインされたのは、標準ユーザーを意識してデザインされていない従来のアプリケーションの互換性を単に支援するためです。残念ながら、こうしたテクノロジですべての問題が解決されるわけではありません。次に、他にも共通する問題を見てみましょう。

インストール時の問題

最も頻繁に発生する問題の 1 つは、MSI ファイル内で CustomActions をマークしていないことです。多くの場合、CustomActions には権限の借用を防止する msidbCustomActionTypeNoImpersonate を指定しないので、CustomActions は Local System で実行されます。Windows Vista では既定のトークンが Users グループのメンバのトークンと同様に扱われ、CustomActions では管理者特権が必要な操作を試みるため、マークがない MSI ファイルでも失敗しません。通常は、msidbCustomActionTypeNoImpersonate 属性を CustomActions に追加するだけで、この問題を解決できます。

テスト中に発生したもう 1 つの典型的な問題は、インストールの一環として起動されるアプリケーションに関係します。インストール終了時にアプリケーションを起動することは一般的に行われます。残念ながら、多くの場合、アプリケーションは間違ったユーザー コンテキストで起動されます。つまり、特権を昇格した資格情報を指定してインストールを実行し、そのまま特権を昇格したユーザー トークンでアプリケーションが起動されます。この問題を解決するには、ブートストラップ EXE を作成することをお勧めします。このブートストラップは、一連のバイナリを一時的な場所に展開後、インストールを開始する別のプロセスを起動します (図 9 参照)。インストールを開始したプロセスが完了したら、バイナリを展開したプロセスに制御を返し、アプリケーションを起動します。このようにすれば、アプリケーションは標準ユーザー トークンで実行されます。

図 9 ブートストラップを使用したアプリケーションのインストールと実行
図 9 ブートストラップを使用したアプリケーションのインストールと実行(画像を縮小するには、ここをクリックします)
図 9 ブートストラップを使用したアプリケーションのインストールと実行
図 9 ブートストラップを使用したアプリケーションのインストールと実行 (画像を拡大するには、ここをクリックします)

インストール時に頻繁に直面する最後の問題は、アプリケーションの初回実行時に管理者特権が必要な操作を行うことです。このような操作には、Program Files へのバイナリのコピーや更新、ドライバのインストールなど、ユーザーが管理者であることを想定した操作があります。こうした問題の対処方法として、標準ユーザーでアプリケーションをインストールおよび実行した場合の動作をテストすることをお勧めします。

多くのアプリケーションには、アプリケーション バイナリを更新するメカニズムがいくつかあります。多くの場合、こうした更新は低い特権のトークンで実行され、Program Files ディレクトリ内のバイナリを上書きを試みるため失敗します。アプリケーションで自身のバイナリを更新する場合、更新機能を別のプロセスに分離し、そのプロセスに管理者特権が必要であることをマークします。または、MSP ファイルを使用して、修正プログラムの署名に基づいて特権の昇格を実行することもできます。そのためには、オリジナルの MSI ファイルの MsiPatchCertificate テーブルを設定する必要があります。

アプリケーション実行時の典型的な問題

実行時に発生するよくある間違いの 1 つに、ユーザーが管理者かどうかを確認する不必要なチェックがあります。たとえば、多くのゲームで、管理者グループのメンバかどうかが最初に確認されます。管理者でなければユーザーにエラーを報告するゲームも数多くありますが、実際に管理者特権を必要とする API を呼び出すことはありません。当然ながら、このようなチェックを行う必要はありません。ただし、管理者特権が必要なアプリケーションを記述し、マニフェストを設定する場合は、アプリケーションに弾力性を持たせるために、このチェックを追加し、ユーザーに管理者特権がないときはアプリケーションを終了します。

多くのアプリケーションは、実行開始直後に HKEY_CLASSES_ROOT (HKCR) へのクラスの登録を試みます。アプリケーションに管理者特権がなければ、この場所に情報を書き込むことができず、操作は失敗します。このようなクラスの登録は、インストール中に実行するか、ユーザーのクラスのルート HKCU\Software\Classes にリダイレクトします。

もう 1 つ発生すると考えられる注意すべき問題は、標準ユーザーが Global 名前空間にオブジェクトを作成できないことです。たとえば、標準ユーザーはこの名前空間内に名前付きパイプや共有メモリを作成できません。Global 名前空間は、セッション間で状態を保持するサービスやアプリケーションから使用することのみを目的としています。代わりに、Local 名前空間を使用します。この名前空間には、標準ユーザーが書き込むことができます。

オブジェクトを開く際に MAX_ALLOWED を使用することも問題です。Windows XP では、通常、アプリケーションを実行するユーザーは管理者であると想定されていました。そのため、開発者がファイルやレジストリ キーを開く際に MAX_ALLOWED を使用した場合、常に完全な管理者権限でファイルを開いていたため、この環境では問題が生じていませんでした。Windows Vista では、この点が変わりました。開発者がアプリケーションを作成するときは、ユーザーがオブジェクトを開く際に必要な権限を指定する場合はより慎重に行う必要があります。

まとめ

この記事では、Windows Vista で開発を行う際の新しい環境の概要について説明しました。最も基本的な変更点は、既定ではアプリケーションが低い特権のトークンで実行されるため、コンピュータや他のユーザーに影響を与える可能性がないことです。Windows Vista の UAC に関連するトピックの詳細については、MSDN および TechNet で以前に掲載された UAC に関するホワイト ペーパーを参照してください。また、UAC によってユーザーやシステム管理者の作業が大幅に容易になることも忘れないでください。

Chris Corio は、マイクロソフトの Windows Security チームのプログラム マネージャで、現在、Windows のセキュリティを保護するためのアプリケーション セキュリティ テクノロジや管理テクノロジに注目しています。Chris への問い合わせは、chris.corio@microsoft.com まで (英語で) 電子メールをお送りください。

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


Windows Vista および Office: マネージ プレビュー ハンドラ フレームワークで独自の方法でデータを表示する

Windows Vista および Office

マネージ プレビュー ハンドラ フレームワークで独自の方法でデータを表示する

Stephen Toub
この記事の内容 : :
  • Windows および Office 用のマネージ アドインを作成する
  • マネージ プレビュー ハンドラ フレームワークを実装する
  • 任意のファイル拡張子用のカスタム プレビュー ハンドラを開発する
  • COM 相互運用機能
この記事は次のテクノロジを使用しています:
.NET Framework、Windows Vista、Outlook
コードのダウンロード : : PreviewHandlers2007_01.exe (444KB)
コンテンツ
プレビュー ハンドラをホストする
PreviewHandler フレームワーク
PreviewHandler を実装する
COM インターフェイスを使用する
ハンドラを登録する
デバッグする
まとめ

Microsoft Windows と Office のすべてのバージョンに、データを表示、操作、認識する機能を向上する新しい方法や手法が導入されます。

同様に、カスタム機能をプラグインするための新しい拡張ポイントや強化された拡張ポイントも提示されます。Windows Vista™ と 2007 Microsoft® Office system では、これら 2 つの分野に対して行われた機能強化が併合され、Windows Vista と Outlook® 2007 用にカスタム プレビュー ハンドラを作成できるようになりました。

Outlook 2003 には電子メールの閲覧ウィンドウが用意され、メッセージを開かなくても、メッセージの内容を簡単に表示できるようになりました。メール フォルダのリスト ビューでメッセージを選択するだけで、メッセージが横または下のウィンドウに表示されます。Outlook 2007 ではこの概念を拡張し、添付ファイルをダブルクリックして適切なビューアで開く必要なく、同じプレビュー ウィンドウでメッセージの添付ファイルを表示できるようになります。

Outlook には、Word 文書、PowerPoint® プレゼンテーション、Excel スプレッドシート、フォント ファイル、ビデオ ファイルとオーディオ ファイル、および一般的に添付ファイルとして送信されるその他のさまざまな種類のファイル用のプレビュー ハンドラが同梱されます。ただし、これはほんの序の口に過ぎません。Windows Vista では、シェル内の任意のフォルダからアクセスできる、同様のプレビュー ウィンドウをサポートします。このプレビュー ウィンドウを有効にするには、フォルダのメニューの [整理] をクリックし、[レイアウト] をポイントして、[プレビュー ペイン] をクリックします (図 1 参照)。

図 1 Windows Vista のフォルダでプレビューを有効にする
図 1 Windows Vista のフォルダでプレビューを有効にする

Outlook と Windows Vista はどちらも基になるプレビュー メカニズムが同じなので、開発者は、ファイルの種類用に対応したカスタム プレビュー ハンドラを実装および登録すると、Outlook 2007 や Windows Vista ですぐにそのファイルの種類のプレビュー機能を使用できるようになります。

この記事では、プレビュー ハンドラの実装に必要なものと、マネージ コードを使用した実装方法について説明します (Vista の Windows® SDK には、ネイティブ C++ で作成されたサンプルのプレビュー ハンドラが含まれています)。この記事のコード サンプルには、独自のプレビュー ハンドラを簡単に実装できるようにするフレームワークを含みます。また、サンプルのプレビューア (PDF、XML、ZIP、MSI、BIN、CSV、XPS、XAML ファイル用のプレビューアなど) もいくつか用意しています。

プレビュー ハンドラをホストする

これまでに Windows シェル用のマネージ アドインの実装を試みたことがあれば、この概念について少し不安があるかもしれません。マイクロソフトとしては、最終的に、シェルのアドインをマネージ コードで実装しないことを強くお勧めします。このようなアドインは、サポート対象のシナリオとは見なしていません。アドインがインプロセスでシェル (explorer.exe) に読み込まれること、特定のプロセスに読み込める共通言語ランタイム (CLR) のバージョンが 1 つだけであること、およびあるバージョンのランタイムを対象に作成されたマネージ コードが以前のバージョンのランタイムを実行するプロセスでは実行されない可能性があることがその理由です。では、マネージ コードで作成されたシェルのアドインが 2 つある場合 (1 つは .NET Framework 1.1 を対象とし、もう 1 つは .NET Framework 2.0 を対象とします) はどうなるでしょうか。.NET Framework 2.0 対象のアドインが最初に読み込まれると、おそらく問題には気付きません。つまり、.NET Framework 1.1 対象のアドインは、CLR 2.0 に対して正常に読み込まれ実行されます。ただし、.NET Framework 1.1 対象のアドインが最初に読み込まれると、.NET Framework 1.1 CLR が explorer.exe に読み込まれます。.NET Framework 2.0 対象のアセンブリを .NET Framework 1.1 で読み込むことはできません。そのため、.NET Framework 2.0 対象のアドインを、.NET Framework 1.1 対象のアドインの後に読み込むと失敗します。

バージョン管理に関する同様の問題が Windows Vista に存在するため、マイクロソフトでは、サムネイル プロバイダやプロパティ ハンドラなどのシェル内の新しい拡張ポイントでも、シェルのアドインをマネージ コードで実装しないことを依然として推奨しています (新しい拡張ポイントは、Windows Vista シェルがインプロセスで使用するのではなく、Windows Vista の検索インデックスがアウトプロセスで使用します)。

ただし、プレビュー ハンドラに関して良い知らせもあります。プレビュー ハンドラは、少なくともシェルに関する限り、常にアウトプロセスで読み込まれます。プレビュー ハンドラは COM コンポーネントとして実装されますが、ホストするのは Windows Vista シェルではありません。プレビュー ハンドラは、プレビュー ハンドラの代理ホスト (prevhost.exe) がホストするか、ローカル COM サーバーとして実装されます。後者のマネージ コードの実装については、この記事では説明しません (関連する内容の概要については、Ryan Gregg が執筆した「プレビュー ハンドラと Windows XP」を参照してください)。さらに、マイクロソフトが推奨するサービス モデルとして prevhost.exe を使用することをお勧めします。

PreviewHandler フレームワーク

有効なプレビュー ハンドラにするには、インターフェイスをいくつか実装する必要があります (実装するすべてのインターフェイスについては、windowssdk.msdn.microsoft.com/aa361576.aspx を参照してください)。実装するインターフェイスには、IPreviewHandler (shobjidl.h)、IObjectWithSite (ocidl.h)、IOleWindow (oleidl.h)、および IInitializeWithFile、IInitializeWithStream、IInitializeWithItem (propsys.h) などがあります。また、IPreviewHandlerVisuals (shobjidl.h) など、必要に応じて実装するインターフェイスもあります。プレビュー ハンドラは、サポートを拡張するためにこのインターフェイスを実装できます。

ネイティブ コードでプレビュー ハンドラを作成する場合、これらのインターフェイスがすべて既に定義済みで、参考に示したヘッダー ファイルからインクルードできることを考えると、すぐに作成を開始できるでしょう。ただし、マネージ コードでプレビュー ハンドラを作成するには、最初に、これらの各インターフェイスの COM 相互運用機能の定義を作成または入手する必要があります。今回行った定義を図 2 に示します。

Figure 2 COM インターフェイスの相互運用機能 (Close Figure 2)
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("8895b1c6-b41f-4c1c-a562-0d564250836f")]
interface IPreviewHandler
{
    void SetWindow(IntPtr hwnd, ref RECT rect);
    void SetRect(ref RECT rect);
    void DoPreview();
    void Unload();
    void SetFocus();
    void QueryFocus(out IntPtr phwnd);
    [PreserveSig]
    uint TranslateAccelerator(ref MSG pmsg);
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("8327b13c-b63f-4b24-9b8a-d010dcc3f599")]
interface IPreviewHandlerVisuals
{
    void SetBackgroundColor(COLORREF color);
    void SetFont(ref LOGFONT plf);
    void SetTextColor(COLORREF color);
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b7d14566-0509-4cce-a71f-0a554233bd9b")]
interface IInitializeWithFile
{
    void Initialize([MarshalAs(UnmanagedType.LPWStr)] string pszFilePath,
    uint grfMode);
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f")]
interface IInitializeWithStream
{
    void Initialize(IStream pstream, uint grfMode);
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("fc4801a3-2ba9-11cf-a229-00aa003d7352")]
public interface IObjectWithSite
{
    void SetSite([In,MarshalAs(UnmanagedType.IUnknown)] object pUnkSite);
    void GetSite(ref Guid riid,
                 [MarshalAs(UnmanagedType.IUnknown)] out object ppvSite);
}

[ComImport]
[Guid("00000114-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleWindow
{
    void GetWindow(out IntPtr phwnd);
    void ContextSensitiveHelp(
             [MarshalAs(UnmanagedType.Bool)] bool fEnterMode);
}

プレビュー ハンドラを実装するためのマネージ フレームワークでは、これらのインターフェイスの機能の大半を、PreviewHandler という抽象基本クラスにラップしています。

public abstract class PreviewHandler :
    IPreviewHandler, IPreviewHandlerVisuals, IOleWindow, IObjectWithSite
{ ... }
これにより、厄介な実装の詳細がまったく表示されなくなります (実際には、ご覧のとおり、まったく厄介ではありません)。ただし、IInitializeWithFile も IInitializeWithStream も PreviewHandler に実装されないことに注意してください。これらは次のように PreviewHandler から派生する 2 つの抽象クラスに実装されます。
public abstract class StreamBasedPreviewHandler :
    PreviewHandler, IInitializeWithStream { ... }

public abstract class FileBasedPreviewHandler :
    PreviewHandler, IInitializeWithFile { ... }

カスタム プレビュー ハンドラを実装するには、StreamBasedPreviewHandler または FileBasedPreviewHandler から新しいクラスを派生し、メソッドを 1 つオーバーライドします。XmlPreviewHandler クラスの実装を次に示します。

public sealed class XmlPreviewHandler : FileBasedPreviewHandler
{
    protected override PreviewHandlerControl
        CreatePreviewHandlerControl()
    {
        return new XmlPreviewHandlerControl();
    }
}

CreatePreviewHandlerControl メソッドは、StreamBasedPreviewHandlerControl または FileBasedPreviewHandlerControl のいずれかから派生した、独自の型のインスタンスを返します。どちらも PreviewHandlerControl 抽象基本クラスから派生します。

public abstract class FileBasedPreviewHandlerControl :
   PreviewHandlerControl { ... }

public abstract class StreamBasedPreviewHandlerControl :
   PreviewHandlerControl { ... }

public abstract class PreviewHandlerControl : Control
{
    public abstract void Load(FileInfo file);
    public abstract void Load(Stream stream);
    public virtual  void Unload() { ... }
}

Load メソッドは、その名前からもわかるように、ファイルやストリームを読み込んでプレビューするときに呼び出されます。同様に、Unload メソッドは現在のプレビューを終了するときに呼び出されます。その後、カスタム PreviewHandlerControl では、適切な型 (FileBasedPreviewHandlerControl または StreamBasedPreviewHandlerControl) から派生し、Load メソッドをオーバーライドして、ファイルやストリームの表示に必要な Windows フォーム コントロールを作成します。XML プレビュー ハンドラの場合は、WebBrowser コントロールを作成して、そのコントロールに XML ドキュメントを読み込むだけです。その結果、Windows Vista シェルのユーザーに、Internet Explorer® と同等の優れた形式で XML が表示されます。

public class XmlPreviewHandlerControl : FileBasedPreviewHandlerControl
{
    public override void Load(FileInfo file)
    {
        WebBrowser browser = new WebBrowser();
        browser.Dock = DockStyle.Fill;
        browser.Navigate(file.FullName);
        Controls.Add(browser);
    }
}

図 4 Outlook 2007 での ZIP ファイルのプレビュー
図 4 Outlook 2007 での ZIP ファイルのプレビュー(画像を縮小するには、ここをクリックします)
図 4 Outlook 2007 での ZIP ファイルのプレビュー
図 4 Outlook 2007 での ZIP ファイルのプレビュー (画像を拡大するには、ここをクリックします)

基本クラス PreviewHandlerControl の Unload 実装では、Controls コレクションからすべてのコントロールを破棄および削除します。コントロールにとってその機能で十分であれば、オーバーライドの必要はありません。

派生した PreviewHandler クラスにいくつか属性を適用する以外は、カスタム プレビュー ハンドラの作成はこれで終わりです。図 3 は、ZIP プレビュー ハンドラの完全な実装を示します。また、図 4 は、Outlook 2007 で実行した場合の結果を示します (ダウンロードに含まれる機能豊富な ZIP プレビュー ハンドラは、.zip ファイル内のファイルとディレクトリのツリー ビュー、ファイル アイコンをサポートします。また、アーカイブに格納されているファイルをダブルクリックして表示することもできます)。このクラスでは、(.NET Framework 2.0 に含まれる) Visual J#® 2005 ZIP ライブラリを使用して、ZIP ファイル内のすべてのファイルの一覧を表示します。これらのプレビュー ハンドラが Windows Vista と Outlook の両方で動作する例を示すために、図 5 に Windows Vista シェルで動作する XmlPreviewHandler を示します。

Figure 3 ZIP プレビュー ハンドラ (Close Figure 3)
[PreviewHandler("ZIP Preview Handler", ".zip",
                "{c0a64ec6-729b-442d-88ce-d76a9fc69e44}")]
[ProgId("MsdnMag.ZipPreviewHandler")]
[Guid("853f35e3-bd13-417b-b859-1df25be6c834")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public sealed class ZipPreviewHandler : FileBasedPreviewHandler
{
    protected override PreviewHandlerControl CreatePreviewHandlerControl()
    {
        return new ZipPreviewHandlerControl();
    }

    private sealed class ZipPreviewHandlerControl :
        FileBasedPreviewHandlerControl
    {
        public override void Load(FileInfo file)
        {
            ListView listView = new ListView();
            listView.Dock = DockStyle.Fill;
            listView.BorderStyle = BorderStyle.None;

            listView.FullRowSelect = true;
            listView.HeaderStyle = ColumnHeaderStyle.Nonclickable;
            listView.MultiSelect = false;
            listView.View = View.Details;

            listView.Columns.Add("File Name", -2);
            listView.Columns.Add("Size", -2);
            listView.Columns.Add("Comment", -2);

            ZipFile zip = new ZipFile(file.FullName);
            Enumeration entryEnum = zip.entries();
            while (entryEnum.hasMoreElements())
            {
                ZipEntry entry = (ZipEntry)entryEnum.nextElement();
                if (!entry.isDirectory())
                {
                    listView.Items.Add(new ListViewItem(new string[] {
                        entry.getName(), entry.getSize().ToString(),
                        entry.getComment() }));
                }
            }
            zip.close();

            Controls.Add(listView);
        }
    }
}
図 5 Windows Vista での XML のプレビュー
図 5 Windows Vista での XML のプレビュー(画像を縮小するには、ここをクリックします)
図 5 Windows Vista での XML のプレビュー
図 5 Windows Vista での XML のプレビュー (画像を拡大するには、ここをクリックします)

PreviewHandler を実装する

フレームワークを使用してマネージ プレビュー ハンドラを実装する方法を理解したところで、PreviewHandler 基本クラスの内容に注目し、プレビュー ハンドラのしくみについて理解を深めましょう。

根本的には、PreviewHandler は単なる Windows フォーム コントロールのコンテナです。コントロールは、プライベート メンバ変数 _previewControl に格納されます。これは、カスタム プレビュー ハンドラの実装 (たとえば、XmlPreviewHandlerControl や ZipPreviewHandlerControl) によって実装された GetPreviewHandlerControl メソッドから返されるコントロールに初期化されます。

protected PreviewHandler()
{
    _previewControl = CreatePreviewHandlerControl();
    IntPtr forceCreation = _previewControl.Handle;
}

これは非常に小さなコンストラクタですが、注目すべき複雑な問題がいくつかあります。まず、ここでは、重要な .NET Framework デザイン ガイドラインに違反しています。このガイドラインでは、コンストラクタから仮想メソッドを呼び出してはいけないことが明示されています (これは、FxCop ルール ConstructorsShouldNotCallBaseClassVirtualMethods によって検出されます)。.NET では、ISO C++ とは異なり、仮想呼び出しの対象は、現在構成している基本型 (仮想) ではなく、大多数の派生型 (オーバーライド) にあります。ここでの問題は、基本型のコンストラクタが実行される時点で、派生型のコンストラクタがまだ実行されていないことです。つまり、派生したインスタンスでメソッドのオーバーライドが呼び出された後に、そのインスタンスの構築が完了するため、同じクラスにシールされていない仮想メソッドをコンストラクタから呼び出してはいけないというルールです。これは、(構築コードでまだ実行されない) コンストラクタのセットアップに依存する CreatePreviewHandlerControl のオーバーライドでは何もしてはいけないことを意味するため、覚えておいてください。ただし、作成したフレームワークに基づいて、CreatePreviewHandlerControl で適切な種類のコントロールのインスタンスを作成して返せば、このルールに従うのは難しいことではありません。

当然ながら、最初からこのガイドラインがわかっているのに、なぜ、コンストラクタから CreatePreviewHandlerControl を呼び出してこのルールを破ったのかという疑問が生じます。これは、VI スレッド上にコントロールを作成する必要があるためです。これにより、解決すべき 2 つ目の問題が発生します。上記のコンストラクタの最後で、_previewControl.Handle の値を取得したのに、その結果を処理していないことに気付くでしょう。これは、コントロールの Handle プロパティの get アクセサを呼び出すために行いました。つまり、結果が欲しかったのではなく、アクセサを実行したかっただけです。Control.Handle の get アクセサを呼び出すと、コントロールの基になるウィンドウが強制的に作成されます。これは重要なことです。なぜなら、プレビュー ハンドラ コンポーネントのインスタンスを作成し、そのコンストラクタを呼び出すスレッドはシングル スレッド アパートメント (STA) スレッドですが、後からインターフェイスのメンバを呼び出すスレッドはマルチスレッド アパートメント (MTA) スレッドだからです。ご存知のとおり、Windows フォーム コントロールは、STA スレッドで実行するように作成されているため、MTA スレッドから使用すると異常終了する場合があります。したがって、Windows フォームのプレビュー ウィンドウを作成する最高のタイミングは、プレビュー ハンドラのコンストラクタ内になります。その後、プレビュー コントロールとの操作を必要とする他のインターフェイス メソッドの呼び出しは、STA のメイン スレッドにマーシャリングする必要があります。コントロールの ISynchronizeInvoke インターフェイスを使用すると、これを簡単に実現できます。スレッドのこの 2 つの問題は、ネイティブのプレビュー ハンドラを実装する場合には発生しません。また、この問題は、CLR が COM 相互運用機能を処理する方法の結果として発生する可能性があります。

(Windows フォームに依存する実装について説明していますが、ActiveX コントロールを使用するのがプレビューを表示する最も簡単な方法になる場合もあります。この説明については、「ActiveX コントロールを使用する」を参照してください。)

COM インターフェイスを使用する

信じられないかもしれませんが、コンストラクタが PreviewHandler の実装全体で最も難しい部分です。実装の残りの部分では、COM インターフェイスで行われた呼び出しを Windows フォームにルーティングし、基本的なブックキーピングを処理するだけです。最も簡単なインターフェイスから説明を始めましょう。

IOleWindow ホスティング アプリケーションは、IOleWindow を使用して、埋め込み先でのアクティブ化に関与するウィンドウへのハンドル、つまり、コントロールへのハンドルを取得できます (プレビュー ハンドラの場合、ContextSensitiveHelp は呼び出しません)。これにより、次に示すように、インターフェイスの実装が非常に簡単になります。

void IOleWindow.GetWindow(out IntPtr phwnd)
{
    phwnd = _previewControl.Handle;
}

void IOleWindow.ContextSensitiveHelp(bool fEnterMode)
{
    throw new NotImplementedException();
}

IObjectWithSite IObjectWithSite も単純です。このインターフェイスは、コンテナに関連付けられたサイトへのポインタをオブジェクトに提供する際に使用されます。実際には、SetSite に提供されるサイトは、プレビュー ハンドラのウィンドウを含む IPreviewHandlerFrame です。このため、SetSite メソッドは、提供された IUnknown インターフェイス ポインタを (GetSite が要求時に返すことのできる) プライベート メンバに格納し、これを IPreviewHandlerFrame にキャストします (IPreviewHandlerFrame は SetSite によって格納もされます)。このキャストの結果、内部では QueryInterface が呼び出されます。

private object _unkSite;
private IPreviewHandlerFrame _frame;

void IObjectWithSite.SetSite(object pUnkSite)
{
    _unkSite = pUnkSite;
    _frame = _unkSite as IPreviewHandlerFrame;
}

void IObjectWithSite.GetSite(ref Guid riid, out object ppvSite)
{
    ppvSite = _unkSite;
}

IInitializeWithFile と IInitializeWithStream 続いて、IInitializeWithFile と IInitializeWithStream をそれぞれ、FileBasedPreviewHandler と StreamBasedPreviewHandler に実装します。これらが呼び出されると、プレビューするファイルの完全パスまたは IStream がプレビュー ハンドラに提供されます。これらのメソッドの実装に必要なのは、次の数行のコードのみです。

// in FileBasedPreviewHandler
private string _filePath;
void IInitializeWithFile.Initialize(string pszFilePath, uint grfMode) {
    _filePath = pszFilePath;
}

// in StreamBasedPreviewHandler
private IStream _stream;
void IInitializeWithStream.Initialize(IStream pstream, uint grfMode) {
    _stream = pstream;
}

これらのインターフェイスの唯一のメソッド Initialize には、プレビューするファイルのパス、またはファイルを表す IStream を、ファイルを開く方法を示すファイル モードと共に提供します。ファイル パスやストリームをメンバ変数に格納し、後からアクセスできるようにします。プレビュー ハンドラは読み取り専用なので、ここでは 2 番目のパラメータを無視しました (実際には、すべてのプレビュー ハンドラで 2 番目のパラメータを無視し、ファイルを読み取り専用で開きます。また、それ以降のファイルの削除、読み取り、および書き込みを許可します)。IInitializeWithFile と IInitializeWithStream は簡単に実装できますが、さらに詳しい説明が必要です。

プレビュー ハンドラの実装に関する MSDN® ドキュメントを読むと、IInitializeWithFile ではなく IInitializeWithStream を実装するように強く推奨されていることがわかります。IInitializeWithStream が IInitializeWithFile よりも優れている点は、主に IInitializeWithStream を使用すると、プレビュー ハンドラが、ZIP ファイルに格納されたテキスト ファイルなど、依存ファイルに格納されていないデータをプレビューできることが挙げられます (IInitializeWithFile だけを実装するプレビューアを使用して、ストリームとしてのみ使用できるデータをプレビューしようとすると、シェルはストリームのコピーをローカル ファイルに保存してから、そのファイルをプレビューします。そのため、明らかに、ストリームを直接プレビューする方が効果的です)。可能な限り、このアドバイスに従ってください。ただし、これが必ずしも可能または実用的であるとは限りません。それは、ほとんどのファイルの種類用の多くのプレビューアが、ファイル パスや URL しかサポートしない API を使用して簡単に読み込んで表示するためです。たとえば、サンプルの MsiPreviewHandler で MSI ファイルを開く際に使用する MsiOpenDatabase 関数には、対象の MSI ファイルへのファイル パスが必要です。この関数は IStream を受け取りません。IStream は、IInitializeWithFile ではなく IInitializeWithStream を実装した場合に唯一必要なデータです。

今回のフレームワークでは、既に説明した PreviewHandler の 2 つのサブクラスを使用して、このモデルをサポートすることにしました。FileBasedPreviewHandler と StreamBasedPreviewHandler の 2 つのサブクラスです。それぞれ、一方のインターフェイスのみを実装し、両方実装することはありません。PreviewHandler 自体が両方を実装した場合、シェルは常に IInitializeWithStream の実装を使用します。IInitializeWithStream の実装は、セキュリティと分離に関する理由で好まれます (実際、Outlook では、以前からの名残で IInitializeWithFile の実装が好まれます)。派生クラスでは、適切な PreviewHandler サブクラスから派生することによって、使用するインターフェイスを選択できます。

もう 1 つの重要な注意点は、Initialize でファイルやストリームのデータを読み込まないようにすることです。代わりに、ここで行ったように、ファイルのパスまたはストリームへの参照を格納します。また、プレビュー ハンドラは、実際にプレビューを表示するよう要求されたときのみファイルの内容を読み込む必要があります。また、プレビューが表示されている間、排他アクセスのためにファイルをロックしてはいけません。ユーザーはプレビューを表示しながら、対象のファイルを開いて編集できます。最も重要な点は、ユーザーがファイルをプレビューしたまま削除できることです。

IPreviewHandler これにより、プレビュー ハンドラでサポートする必要のある中核のインターフェイス IPreviewHandler が提供されます (このインターフェイスの実装を図 6 に示します)。IPreviewHandler は、実装する必要がある 7 個のメソッドを公開します。この 7 個のメソッドとは、SetWindow、SetRect、DoPreview、Unload、SetFocus、QueryFocus、および TranslateAccelerator です。

Figure 6 IPreviewHandler の実装 (Close Figure 6)
private void InvokeOnPreviewThread(MethodInvoker d)
{
    _previewControl.Invoke(d);
}

private void UpdateWindowBounds()
{
    if (_showPreview)
    {
        InvokeOnPreviewThread(delegate()
        {
            NativeWin32.SetParent(_previewControl.Handle, _parentHwnd);
            _previewControl.Bounds = _windowBounds;
            _previewControl.Visible = true;
        });
    }
}
void IPreviewHandler.SetWindow(IntPtr hwnd, ref RECT rect)
{
    _parentHwnd = hwnd;
    _windowBounds = rect.ToRectangle();
    UpdateWindowBounds();
}

void IPreviewHandler.SetRect(ref RECT rect)
{
    _windowBounds = rect.ToRectangle();
    UpdateWindowBounds();
}

protected abstract void Load(PreviewHandlerControl c);

void IPreviewHandler.DoPreview()
{
    _showPreview = true;
    InvokeOnPreviewThread(delegate()
    {
        Load(_previewControl);
        UpdateWindowBounds();
    });
}

void IPreviewHandler.Unload()
{
    _showPreview = false;
    InvokeOnPreviewThread(delegate()
    {
        _previewControl.Visible = false;
        _previewControl.Unload();
    });
}

void IPreviewHandler.SetFocus()
{
    InvokeOnPreviewThread(delegate() { _previewControl.Focus(); });
}

[DllImport("user32.dll")]
private static extern IntPtr GetFocus();

void IPreviewHandler.QueryFocus(out IntPtr phwnd)
{
    IntPtr result = IntPtr.Zero;
    InvokeOnPreviewThread(delegate() { result = GetFocus(); });
    phwnd = result;
    if (phwnd == IntPtr.Zero) throw new Win32Exception();
}

uint IPreviewHandler.TranslateAccelerator(ref MSG pmsg)
{
    if (_frame != null) return _frame.TranslateAccelerator(ref pmsg);
    return 1; // S_FALSE
}

ファイルをプレビューしようとするときに、ホストから、既に説明した初期化インターフェイスの 1 つを使用して、ファイルまたはストリームに関する情報をプレビュー ハンドラに渡します。プレビュー ウィンドウを含むビュー ウィンドウのハンドラは、SetWindow メソッドを使用してプレビュー ハンドラに渡されます。これにより、プレビュー ハンドラは、そのビュー ウィンドウをプレビュー ウィンドウの親として設定できます。その後、ホストは、SetRect メソッドを使用して、プレビュー ウィンドウのサイズを適切に設定できます。それ以降のある時点で、ホストが DoPreview メソッドを呼び出すと、プレビューが表示されます。プレビューが表示されている間、ホストは、ビュー ウィンドウのサイズが変更されるたびに、再度 SetRect を呼び出すことができます。最後にプレビューを終了する場合、ホストは、Unload メソッドを呼び出し、プレビューを終了することをプレビュー ハンドラに指示します。これは、プレビューされているアイテムから読み込まれたリソースをアンロードすることを意味します。プレビュー ハンドラ自体がアンロードされるという意味ではありません。プレビュー ハンドラの同じインスタンスを、複数のプレビューで再利用できます。

今回は実装をサポートするために、いくつかのヘルパ関数を使用しています。最初に、InvokeOnPreviewThread ヘルパは、MethodInvoker デリゲート (System.Windows.Forms 名前空間で定義された、void で宣言された、パラメータなしのデリゲート) を受け取ります。これは、プレビュー コントロールの ISynchronizeInvoke.Invoke メソッドを使用して、メイン UI スレッドで実行されます (このコントロールは STA スレッドからインスタンスが作成されるようにプレビュー ハンドラのコンストラクタで作成されたことに注意してください)。次に、UpdateWindowBounds ヘルパを使用して、プレビュー コントロールの親ウィンドウを設定し、そのウィンドウを適切な位置に移動して表示します。この機能は、複数の IPreviewHandler インターフェイス メソッドがこの情報を変更し、変更した情報でプレビューを変更するように要求するときに、ヘルパ メソッドで役立ちます。ActiveX コントロールを使用する

この記事で説明したプレビュー ハンドラは、ListView などの Windows フォーム コントロールを使って実装しました。 ただし、一部のファイル形式では、プレビューを表示する最も簡単な方法は、ファイル形式が関連付けられている ActiveX® コントロールを使用することです。COM 相互運用機能により、Windows フォームでは、その他のコントロールと同様に ActiveX コントロールをホストできます。ActiveX コントロールのマネージ ラッパーを取得するには、Windows フォーム ActiveX コントロール インポータ aximp.exe を使用する方法が最も簡単です (詳細については、http://msdn.microsoft.com/library/ja/?url=/library/ja/cptools/html/cpgrfwindowsformsactivexcontrolimporteraximpexe.asp を参照してください)。これにより、相互運用機能アセンブリが生成されます。このアセンブリへの参照は、プロジェクトに後から手動で追加する必要があります。代わりに Visual Studio ツールボックスに ActiveX コントロールを追加し、そのコントロールのインスタンスをデザイン画面のいずれかにドラッグ アンド ドロップすると、Visual Studio でアセンブリが生成され、参照が自動的に追加されます。

また、コントロールのラッパーを手動で作成する方法もあります。単純なコントロールやそれほど多くの機能にアクセスする必要のないコントロールでは、この手法の方が魅力的な場合があります。これは、Adobe PDF 形式用のプレビュー ハンドラを作成する際に使用した方法です。

電子メール メッセージでは PDF 形式の添付ファイルを多数受信するため、添付ファイルのプレビュー ウィンドウ内で PDF を正しく表示できると非常に便利です。Office 2007 は PDF ファイル形式でドキュメントを保存できます (ダウンロードについては、http://www.microsoft.com/downloads/details.aspx?familyid=4d951911-3e7e-4ae6-b059-a2e79ed87041&displaylang=ja を参照してください)。ただし、この形式は読み取り用として開くことができません。そのため、PdfPreviewHandler を作成しました。PdfPreviewHandler を使用すると、Adobe Reader に付属する ActiveX コントロールをホストして、PDF ファイルをプレビューできます。コントロールをホストするには、AxHost から派生したクラスでコントロールをラップします。

public class PdfAxHost : AxHost {
    public PdfAxHost() :
        base("ca8a9780-280d-11cf-a24d-444553540000") {}

    object _ocx;
    protected override void AttachInterfaces(){_ocx = base.GetOcx(); }

    public void LoadFile(string fileName) {
        _ocx.GetType().InvokeMember(
            "LoadFile", BindingFlags.InvokeMethod, null,
            _ocx, new object[] { fileName });
    }
}

Aximp.exe はこの処理を実行しますが、さらに多くの処理も実行します (すべてのメソッドおよびイベントの高機能ラッパーを提供します)。コントロールをラップし、1 つのメソッド (LoadFile) を公開するのに必要な最低限の処理を行いました。このメソッドでは、そのコントロールで呼び出すことができるようにする必要があります。LoadFile は、表示されるファイルの場所を引数として受け取り、その場所をコントロールに表示します。その後、他のコントロールと同様に、このコントロールを使用できます。

public override void Load(FileInfo file)
{
    PdfAxHost viewer = new PdfAxHost();
    Controls.Add(viewer);
    IntPtr forceCreation = viewer.Handle; // creates the OCX
    viewer.Dock = DockStyle.Fill;
    viewer.LoadFile(file.FullName);
}

SetWindow と SetRect はこのようなヘルパ メソッドです。SetWindow は、親ウィンドウ ハンドルと新しいバインドをプレビュー ウィンドウに提供します。SetRect は、新しいバインドだけを提供します。SetFocus は、プレビュー コントロールの Focus メソッドの呼び出しに変換されるだけで、QueryFocus は、プレビュー ウィンドウのスレッドから user32.dll の GetFocus 関数を呼び出した結果を返します。

残りは、TranslateAccelerator、Unload、および DoPreview です。今回の PreviewHandler の実装では、今のところ、アクセラレータ キーのサポートは十分ではありません。このため、TranslateAccelerator は、SetSite に格納された IPreviewFrameHandler の TranslateAccelerator メソッドにデリゲートするだけです。タブを設定できる複数のコントロールがプレビュー ハンドラに表示される場合は、より堅牢なタブ処理を実現するために TranslateAccelerator を拡張します。

IPreviewHandlerFrame には、もう 1 つのメソッド GetWindowContext があります。プレビューアは GetWindowContext を使用して、IPreviewHandlerFrame::TranslateAccelerator に転送するアクセラレータをフィルタ処理する際に使用できるテーブルを取得できます。この目的は、パフォーマンスを向上することです。ただし、ネイティブ コードでも、パフォーマンスは大幅には向上しません。また、アクセラレータ テーブルの管理に関する問題を考えると、おそらくあまり意味がありません。プレビュー ハンドラはこのメソッドを使用する必要がありません。すべてを TranslateAccelerator に転送するだけで十分です。つまり、GetWindowContext については気にしないでください。

Unload メソッドは、プレビュー ウィンドウを非表示にしてから、この記事で既に説明した PreviewHandlerControl.Unload 仮想メソッドにデリゲートします。また、インターフェイス上でおそらく最も重要なメソッド DoPreview の実装は難しくありません。DoPreview は、プレビューを実際に表示する場合に呼び出されます。ここでの実装では、PreviewHandlerControl を渡して、抽象 Load メソッドを呼び出します。派生実装では、その後、IInitializeWithFile から格納されたパスを指定して FileBasedPreviewHandler の Load メソッドを、または IInitializeWithStream で提供されたストリームを指定して StreamBasedPreviewHandlerControl の Load メソッドを呼び出します。

// in FileBasedPreviewHandler
protected override void Load(PreviewHandlerControl c)
{
    c.Load(new FileInfo(_filePath));
}

// in StreamBasedPreviewHandler
protected override void Load(PreviewHandlerControl c)
{
    c.Load(new ReadOnlyIStreamStream(_stream));
}
(System.IO.Stream から派生したラッパーに COM IStream を含めるので、派生クラスでは、その他の .NET ストリームのように読み取り専用で IStream を使用できます)。DoPreview メソッドはデータが読み込まれたら、UpdateWindowBounds を呼び出して、プレビューが正しく表示されるようにします。

ハンドラを登録する

Windows Vista シェルと Outlook 2007 はどちらも Windows レジストリを参照して、使用できるプレビュー ハンドラを判断したり、プレビュー ハンドラが関連付けられているファイルの種類を確認したりできます。

その他の COM クラスと同様、プレビュー ハンドラにクラス ID を割り当てる必要があります。図 2 にも記載しましたが、これは、クラスに適用される GuidAttribute の仕事です。

    [Guid("853f35e3-bd13-417b-b859-1df25be6c834")]
各プレビュー ハンドラには異なる GUID が設定されていることがわかります。作成するカスタム プレビュー ハンドラにも一意な ID を割り当てる必要があります。この ID は、プレビュー ハンドラの登録の一環として使用されます。Windows に付属する uuidgen.exe ユーティリティと guidgen.exe ユーティリティを使用すれば、簡単に GUID を生成できます。

図 7 登録済みのプレビュー ハンドラの一覧
図 7 登録済みのプレビュー ハンドラの一覧(画像を縮小するには、ここをクリックします)
図 7 登録済みのプレビュー ハンドラの一覧
図 7 登録済みのプレビュー ハンドラの一覧 (画像を拡大するには、ここをクリックします)

まず、HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers の PreviewHandlers キーに REG_SZ 値を追加します。この場合、値の名前はプレビュー ハンドラに割り当てられた GUID です。実際には使用されませんが、このエントリの値はプレビュー ハンドラの表示名にします。組み込みのプレビュー ハンドラすべてでこの処理が行われるため、レジストリ内のこのリストを調べるだけで、登録されているプレビュー ハンドラを簡単に確認できます (図 7 参照)。すべて正しく登録されたら、Outlook セキュリティ センター内に登録済みのすべてのプレビュー ハンドラのリストを確認することもできます (図 8 参照)。

図 8 Outlook セキュリティ センターのプレビュー ハンドラのリスト
図 8 Outlook セキュリティ センターのプレビュー ハンドラのリスト(画像を縮小するには、ここをクリックします)
図 8 Outlook セキュリティ センターのプレビュー ハンドラのリスト
図 8 Outlook セキュリティ センターのプレビュー ハンドラのリスト (画像を拡大するには、ここをクリックします)

次に、shellex キーの下に対象の拡張子用に特殊なレジストリ キーを作成する必要があります。この特殊なキーには、特定の識別子 "{8895b1c6-b41f-4c1c-a562-0d564250836f}" を名前として付けます。このキーでは、キーに格納されているデータがプレビュー ハンドラを表すことをシステムに通知します。そのキーの既定値には、その拡張子に使用するプレビュー ハンドラの GUID を設定します。

using (RegistryKey extensionKey =
       Registry.ClassesRoot.CreateSubKey(extension))
using (RegistryKey shellexKey = extensionKey.CreateSubKey("shellex"))
using (RegistryKey previewKey = shellexKey.CreateSubKey(
       "{8895b1c6-b41f-4c1c-a562-0d564250836f}"))
{
    previewKey.SetValue(null, previewerGuid, RegistryValueKind.String);
}

特定のプレビュー ハンドラを複数のファイル拡張子に使用できる場合は、単純に、必要な数の拡張子に対してこの手順を繰り返すことができます。フレームワークは、プレビュー ハンドラの PreviewHandlerAttribute を探します。この属性は、ハンドラの表示名、ハンドラがサポートする拡張子、および AppID を指定します (AppID についてはこの後すぐに説明します)。ZipPreviewHandler と同様、サポートしている拡張子を単純な文字列にできます。

[PreviewHandler("ZIP Preview Handler", ".zip",
                "{c0a64ec6-729b-442d-88ce-d76a9fc69e44}")]

また、BinPreviewHandler と同様、サポートしている拡張子を、セミコロンで区切った拡張子のリストで指定することもできます。

[PreviewHandler("Binary Preview Handler", ".bin;.dat",
                "{e92d3c10-89c8-4543-91b9-7a74305e9df4}")]

このようなシナリオでは、(ダウンロードに含まれる) 登録関数は、リストに含まれる各拡張子をループし、各拡張子用のハンドラを登録します (Windows Vista には、サポート対象として登録されているよりも多くのファイルの種類に対応できるプレビュー ハンドラがいくつかあります。その他のファイルの種類のプレビューについては、「実践する」を参照してください)。

実践する

Windows Vista に組み込まれているプレビュー ハンドラはほんの一部です。 ただし、実際は、プレビュー ハンドラによって、既定でサポート対象として登録されているよりも多くのファイルの種類に対応できる場合もあります。たとえば、Microsoft Windows テキスト プレビュー ハンドラを見てみましょう。ご想像のとおり、このプレビュー ハンドラは .txt ファイルを表示します。ただし、このプレビュー ハンドラは、.txt だけを処理するように制限されているわけではありません。開発者は、電子メールの添付ファイルとして C#、Visual Basic®、および C++ のコード ファイルを受け取ることがよくあります。そのため、既定のビューアで、Visual Studio® を開くことなく、Outlook 内でこの添付ファイルをプレビューできることを望みます。もちろん、これは可能です。Microsoft Windows テキスト プレビュー ハンドラでプレビューする拡張子に .cs、.vb、.cpp、および .h ファイルを登録するだけです。次の .reg ファイルでこの登録を行います。

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.cs\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"

[HKEY_CLASSES_ROOT\.vb\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"

[HKEY_CLASSES_ROOT\.cpp\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"

[HKEY_CLASSES_ROOT\.h\shellex\{8895b1c6-b41f-4c1c-a562-0d564250836f}]
@="{1531d583-8375-4d3f-b5fb-d23bbd169f22}"

もっと賢明な方法は、HKEY_CLASSES_ROOT にあるファイル拡張子をすべて列挙する単純なアプリケーションまたはスクリプトを作成することです。それぞれの Content Type 値が設定されているかどうか、および設定されている場合は Content Type の値が "text/" で始まるかどうかを確認します。Content Type の値が "text/" で始まり、その拡張子にプレビュー ハンドラがまだ構成されていなければ、前述のレジストリ スクリプトで行ったように、テキスト ハンドラでプレビューするように設定できます。ほんの数分の作業で、プレビューできるファイルの種類の数を大幅に増やすことができます。

当然ながら、さまざまなファイルの種類をプレビューできるのは、テキスト プレビュー ハンドラだけではありません。自作のハンドラでも、この柔軟性を発揮できる場合があります。この記事で説明した XmlPreviewHandler を考えてみましょう。コード サンプルを見ると、実際には、プレビュー ハンドラの名前が InternetExplorerPreviewHandler であることがわかります。その名前に変更したのは、実際にはプレビュー ハンドラが XML ファイル形式固有のものではないからです。つまり、このハンドラは、XML を表示する際に使用するコントロール (WebBrowser コントロール) 固有のものです。このプレビュー ハンドラは、WebBrowser コントロールを使用して、プレビューするファイルに移動するため、このハンドラは実にさまざまなファイルの種類に使用できます。このようなファイルの種類の多くは、既に他のプレビュー ハンドラに対応付けられていますが、対応付けられていないものもあります。たとえば、Windows Vista で配布される新しい XML Paper Specification (XPS) ファイル形式には組み込みのプレビュー ハンドラがありませんが、Internet Explorer で XPS ドキュメントを表示できます。そのため、サポート対象のファイル拡張子のリストに .xps を追加することで、InternetExplorerPreviewHandler を拡張しました。このようなちょっとした操作により、XPS 用のプレビュー ハンドラを用意できました。

ファイルの種類に登録するだけでなく、Prog ID にも登録できます。たとえば、HKEY_CLASSES_ROOT\.xml の既定値は "xmlfile" であるため、実際には、XML プレビュー ハンドラは、HKEY_CLASSES_ROOT\xmlfile\ShellEx\{8895b1c6-b41f-4c1c-a562-0d564250836f} に登録されます。

[PreviewHandler("XML Preview Handler", "xmlfile",
                 "{88235ab2-bfce-4be8-9ed0-0408cd8da792}")]

このように、実際には同じ種類のファイル拡張子が複数ある場合、各拡張子に同じ Prog ID を使用することで、プレビューアの登録が一度で済みます (たとえば、HKEY_CLASSES_ROOT\.jpeg と HKEY_CLASSES_ROOT\.jpg の両方に登録する代わりに HKEY_CLASSES_ROOT\jpegfile に登録できます)。実際には、Windows シェル チームは、Prog ID と拡張子が一対一で対応している場合でも、新しいファイルの種類に新しい Prog ID を定義し、実際の拡張子ではなくその Prog ID にプレビュー ハンドラを登録することをお勧めしています。

次の (マネージ ハンドラの) レジストリの変更には AppID が関与しています。この理由は、Windows Vista シェル用のマネージ アドインの作成について既に説明したことに関連しています。前述のとおり、代理ホスト (prevhost.exe) では、既定でプレビュー ハンドラがシェルからアウトプロセスで読み込まれます。システムでは効率化のために、実行する prevhost.exe のインスタンスの数をできるだけ少なくする必要があるため、複数の種類のプレビュー ハンドラが同じ外部プロセスに読み込まれます。つまり、シェルは、異なるバージョンの CLR を対象とするマネージ プレビュー ハンドラを読み込もうとはしませんが、prevhost.exe は読み込む可能性があります。また、これにより、最初に発生した問題が再び発生します。この解決策として、プレビュー ハンドラのインスタンスを、その同じ種類のプレビュー ハンドラ専用の代理ホストに読み込む必要があることをシステムに指定します。これにより、異なるバージョンの CLR を対象とする複数のプレビュー ハンドラが同じ代理プロセスに読み込まれないことが保証されます。プレビュー ハンドラの COM クラスの登録 (これについてはすぐに説明します) では、使用する代理ホストに関する正確な構成の詳細に ID を指定します。その ID を AppID と呼びます。マネージ プレビュー ハンドラが登録されると、そのマネージ プレビュー ハンドラのみで使用される新しい AppID の登録が HKCR\AppID で行われます。

using (RegistryKey appIdsKey = Registry.ClassesRoot.OpenSubKey(
       "AppID", true))
using (RegistryKey appIdKey = appIds.CreateSubKey(appID))
{
    appIdKey.SetValue("DllSurrogate",
                   @"%SystemRoot%\system32\prevhost.exe",
                   RegistryValueKind.ExpandString);
}

この AppID が代理ホストとしてまだ prevhost.exe を指していることに注意してください。

後は、プレビュー ハンドラの COM コンポーネントの登録を追加するだけです。この大部分は、.NET Framework SDK に付属する regasm.exe ツールを使用して簡単に実行できます。

regasm /codebase ManagedPreviewHandler.dll

(mscorlib の System.Runtime.InteropServices 名前空間にある RegistrationServices クラスの RegisterAssembly メソッドを使用する数行のコードを記述すると、インストーラからも実行できます。)

ただし、既に説明したように、作成した新しい AppID を指すようにコンポーネントの登録を変更する必要があります。また、Outlook 2007 などのホストで登録済みのハンドラの一覧を表示する際に使用される表示名を追加する必要もあります。

using (RegistryKey clsidKey = Registry.ClassesRoot.OpenSubKey("CLSID"))
using (RegistryKey idKey = clsidKey.OpenSubKey(previewerGuid, true))
{
    idKey.SetValue("AppID", appID, RegistryValueKind.String);
    idKey.SetValue("DisplayName", name, RegistryValueKind.String);
}

運用中のプレビュー ハンドラの場合、実際には、DisplayName に、ローカライズされた Win32 バイナリ リソース文字列 (たとえば、"@myhandler.dll,-101") を指す REG_SZ 値を設定する必要があります。残念ながら、Visual C#® 2005 の IDE には、マネージ アセンブリに Win32 リソース ファイルを含める方法が用意されていません。この設定を行うには、Win32 リソース ファイルの組み込みを可能にする /win32res スイッチが csc.exe でサポートされているため、コマンド ラインにこのスイッチを指定する必要があります。この記事と登録コードを示す目的から、ここではローカライズされていない値を使用しました。

この登録のロジックはすべて、PreviewHandler の 1 つのメソッドにラップされています。

protected static void RegisterPreviewHandler(
    string name, string extensions, string previewerGuid, string appID)

同様に、対応する登録解除メソッドもあります。

regasm.exe ツールは、mscorlib の System.Runtime.InteropServices 名前空間から ComRegisterFunctionAttribute をサポートします。この属性は、regasm を使用して登録されるように、アセンブリ内の静的メソッドに適用できます。そのため、そのアセンブリ内の型が ComVisible であることがわかると、型情報は、(型の COM クラスがレジストリに登録された後) ComRegisterFunction 属性がマークされたメソッドに渡されます。これにより、COM 相互運用機能のアセンブリが登録されるときに既に説明したすべての登録を処理するメソッドを簡単に作成できます (図 9 参照)。ここでは、ComUnregisterFunctionAttribute がマークされている、対応する登録解除関数を含めました。

Figure 9 COM 登録 (Close Figure 9)
[ComRegisterFunction]
public static void Register(Type t)
{
    if (t != null && t.IsSubclassOf(typeof(PreviewHandler)))
    {
        object[] attrs = (object[])t.GetCustomAttributes(
            typeof(PreviewHandlerAttribute), true);
        if (attrs != null && attrs.Length == 1)
        {
            PreviewHandlerAttribute attr =
                attrs[0] as PreviewHandlerAttribute;
            RegisterPreviewHandler(attr.Name, attr.Extension,
                t.GUID.ToString("B"), attr.AppID);
        }
    }
}

これが完了したら、プレビュー ハンドラをインストールおよび登録するために、コマンド プロンプトから次の 2 行を実行する必要があります。

gacutil -i MsdnMagPreviewHandlers.dll
regasm /codebase MsdnMagPreviewHandlers.dll
同様に、インストール済みのプレビュー ハンドラの登録を解除するには、コマンド プロンプトから次の 2 行を実行する必要があります。
regasm /unregister MsdnMagPreviewHandlers.dll
gacutil -u MsdnMagPreviewHandlers

このフレームワークを使用する独自のカスタム プレビュー ハンドラを作成し、この記事と同じアセンブリにそのハンドラを書き込む場合、そのハンドラを動作させるために処理を行う必要はありません。フレームワークを使用してプレビュー ハンドラを作成し、別のアセンブリに書き込む場合は、アセンブリに次のクラスを追加するだけです。

internal sealed class PreviewHandlerRegistration
{
    [ComRegisterFunction]
    internal static void Register(Type t) {
        PreviewHandler.Register(t);
    }
    [ComUnregisterFunction]
    internal static void Unregister(Type t) {
        PreviewHandler.Unregister(t);
    }
}

まず、既に説明したように、MsdnMagPreviewHandlers.dll をインストールして登録します。次に、アセンブリをインストールして登録します。regasm は、アセンブリの PreviewHandlerRegistration クラスで ComRegisterFunction を検出すると、アセンブリ内の各プレビュー ハンドラの ComRegisterFunction を呼び出します。この関数は、MsdnMagPreviewHandlers.dll に含まれる、既に説明したすべての機能にデリゲートします。

図 10 Outlook 2007 の XamlPreviewHandler
図 10 Outlook 2007 の XamlPreviewHandler(画像を縮小するには、ここをクリックします)
図 10 Outlook 2007 の XamlPreviewHandler
図 10 Outlook 2007 の XamlPreviewHandler (画像を拡大するには、ここをクリックします)

この方法を実証するために、この記事に関するダウンロードに XAML ファイル用のプレビュー ハンドラを含めました。このプレビュー ハンドラは、XamlPreviewHandler クラスと PreviewHandlerRegistration クラスを格納する独自のアセンブリに実装されます。図 10 は、Outlook 2007 で動作しているハンドラのスクリーンショットです (XAML ファイルがプレビューされています。このファイルは、blogs.msdn.com/wpfsdk/archive/2006/05/23/Animating_XAML_Clip_Art.aspx (英語) で入手できる、Windows Presentation Foundation SDK チームのブログから引用しています)。XamlPreviewHandler も FileBasedPreviewHandler ではなく StreamBasedPreviewHandler から派生することに注意してください。Windows Presentation Foundation が提供する XamlReader クラスでは、ストリームからの読み込みがサポートされているため、IInitializeWithFile ではなく IInitializeWithStream を実装する方が適しています。

public override void Load(Stream stream)
{
    Frame f = new Frame();

    XamlReader reader = new XamlReader();
    f.Content = reader.LoadAsync(stream);

    ElementHost xamlHost = new ElementHost();
    xamlHost.Child = f;
    xamlHost.Dock = DockStyle.Fill;
    Controls.Add(xamlHost);
}

デバッグする

インプロセス プレビュー ハンドラは、プレビュー ハンドラをデバックする際に代理ホスティング プロセス (既定では prevhost.exe) で実行されるため、ホスト プロセスにデバッガをアタッチする必要があります。これにはいくつかの方法があります。まず、ホスト プロセスが開始されるのを待ってから、既存のプロセスにアタッチするデバッガの機能を使用します。ただし、特に、マネージ プレビュー ハンドラごとに新しい AppID を作成するという提案に従うと、prevhost.exe のインスタンスが複数存在する可能性があります。prevhost.exe の複数のインスタンスが実行されている場合は、接続先のインスタンスを把握する必要があります。(マイクロソフトが最近買収した) Sysinternals 社の Process Explorer (http://www.microsoft.com/technet/sysinternals/utilities/ProcessExplorer.mspx) (英語) のようなツールを使用して、prevhost.exe の適切なインスタンスが起動されたときに使用されたコマンド ライン引数を検証できます。代理ホストの実行可能ファイル prevhost.exe は、ホストする COM コンポーネントを起動時に通知する必要があるため、コマンド ラインでプレビュー ハンドラの GUID を渡します。

prevhost.exe {8FD75842-96AE-4AC9-A029-B57F7EF961A8} -Embedding

prevhost.exe の各インスタンスのコマンド ライン引数を確認して、デバッグするプレビュー ハンドラに対応する GUID を持つインスタンスを探すことができます。その後、プロセス ID を使用して、デバッガを適切なプロセスにアタッチできます。

プロセスが既に実行中の場合、プレビュー ハンドラは既に構築されているため、そのコードの一部が実行されています。このプレビュー ハンドラの起動をデバッグするには、別の解決策が必要です。1 つの方法としては、コンストラクタの先頭で System.Diagnostics.Debug.Break を呼び出します。これにより、デバッガが起動し、その時点でプロセスにアタッチされるため、コンポーネント全体をデバックできるようになります。

ただし、デバッガのアタッチは必ずしも最適な解決策ではありません。場合によっては、従来の "printf 形式" のデバッグが必要になります。このデバッグでは、プレビュー ハンドラの実行時に監視できる情報を出力します。この場合は、System.Diagnostics.Trace クラスを使用することをお勧めします。既定では、Trace の出力先である TraceListenerCollection には、TraceListener が含まれます。TraceListener は、トレースしたデータを OutputDebugString に書き込むため、待機中のデバッガは出力テキストを表示できます。Windows SDK には、これらのトレースを簡単に表示できるようにする Debug Monitor (DbMon.exe) が付属しています。プレビュー ハンドラと Windows XP

Ryan Gregg

この記事で提供したフレームワークを使用すると、インプロセス プレビュー ハンドラの作成は、マネージ コードでは単純な作業になります。ただし、Windows XP と Windows Vista の両方の Office Outlook 2007 で機能するプレビュー ハンドラの作成は複雑です。Windows Vista 上でインプロセス プレビュー ハンドラをホストする (prevhost.exe と呼ばれる) プロキシ アプリケーションは、Windows XP では使用できません。Windows XP 用のプレビュー ハンドラは、ホスティングを実行するために別のアプリケーションに依存しないで、ローカル COM サーバーとして作成する必要があります。

残念ながら、.NET Framework 2.0 では、.NET アプリケーションをローカル COM サーバーとして登録するのに必要な機能の結び付けの大半を提供しません。マネージ プレビュー ハンドラの開発者であれば、プレビュー ハンドラのネイティブ コードの実装によく似たコードを記述する必要があります。記述する必要がある内容の概要は次のとおりです。

一般的なローカル COM サーバーは、クラス ライブラリとしてではなく、Windows アプリケーションとしてコンパイルされます。COM は、ローカル サーバーの新しいインスタンスを要求するときに、アプリケーションを埋め込みモードで起動する必要があることを示す特殊なフラグ セットを指定して、ローカル サーバー アプリケーションを起動します。これにより、アプリケーションは、UI を使用しないで起動され、クラス ファクトリを COM に登録します。これと同じサーバーには、多くの場合、インプロセス プレビュー ハンドラの実装よりも優先される追加の論理コンポーネントが 2 つ含まれています。この論理コンポーネントは、COM サーバーの実装とクラス ファクトリです。

COM サーバーの実装では、プレビュー ハンドラの呼び出し時に COM によって起動された実行可能ファイルに、実行中のアプリケーション フレームワークを提供します。アプリケーションの Main メソッド内では、プレビュー ハンドラをホストするアプリケーションが応答を停止しないように、コマンド ライン パラメータを解析して、各クラス ファクトリの登録を COM を使用して初期化し、プロセスを安全にシャットダウンするタイミングを監視して、メッセージ ループをポンプする必要があります。大半のローカル COM サーバーでは、Windows レジストリでサーバーの登録および登録解除を行うためのコマンド ライン スイッチもサポートしています。regasm.exe には、ローカル サーバーとして登録するメソッドが用意されていないため、ローカル COM サーバーの登録に適切なレジストリ キーを作成するメソッドも作成する必要があります。これらのキーは、COM ドキュメントに文書化されています。

クラス ファクトリ コンポーネントは、COM が提供する IClassFactory インターフェイスを実装します。さらに、このコンポーネントは、クラス オブジェクトを COM に登録し、COM 呼び出し元のクラス オブジェクトのインスタンスを作成します。特に、クラス ファクトリは、CoRegisterClassObject と CoRevokeClassObject を呼び出して、プレビュー ハンドラの識別に使用される特定の GUID に対してクラス ファクトリが使用できる (または無効である) ことをシステムに通知する必要があります。クラス ファクトリは、必要に応じて、プレビュー ハンドラ クラスの新しいインスタンスを作成し、そのインスタンスを COM 呼び出し元にマーシャリングします。これらのインスタンスは追跡する必要があります。また、サーバー アプリケーションがすべての外部参照が解放されるタイミングを特定して正常にシャットダウンできるように、ガベージ コレクションを強制的に実行する必要がある場合があります。

Windows XP と Windows Vista の両方の Outlook 2007 で機能するマネージ プレビュー ハンドラを作成する作業は非常に複雑です。この補足記事では必要な部分を取り上げていますが、重要な詳細情報については説明していません。ローカル COM サーバーの開発者は、COM の動作、スレッドのアパートメント状態の違い、およびマネージ コードを使用した Win32® API の操作方法を把握する必要があります。この知識がなければ、失敗する可能性があります。

マネージ コードでローカル COM サーバーのプレビュー ハンドラを作成することは、インプロセス ハンドラを作成するほど簡単ではない場合がありますが、これは実行している OS に関係なく、プレビュー ハンドラがすべての Office 2007 ユーザーの役に立つようにする場合に見落とすことのできない点です。これらの組み合わせ方法の詳細や実際の例については、次の役に立つリソースを確認してください。

  • COM Fundamentals (http://msdn2.microsoft.com/en-US/library/ms694505.aspx) (英語) : Microsoft Component Object Model に関する MSDN ドキュメントです。
  • PInvoke.net (www.pinvoke.net) (英語) : マネージ コードで Win32 API 呼び出しを定義するユーザーを支援するために設けられた優れたサイトです。
  • Building COM Servers in .NET (www.codeproject.com/useritems/BuildCOMServersInDotNet.asp) (英語) : ローカル COM サーバーの実装の役立つサンプルを提供します。

Ryan Gregg はマイクロソフトに勤務する Office Outlook のプログラム マネージャで、主に拡張プラットフォームとカスタム フォームを担当しています。彼は、Outlook の拡張に関するブログ (英語) をほぼ定期的に blogs.msdn.com/rgregg で公開しています。

テストを行うには、既に説明した各インターフェイス メソッドの先頭にトレース ステートメントを追加することをお勧めします。または、Visual Studio 2005 の新しいトレースポイント機能と合わせて Debugger.Break を使用できます。これにより、シェルや Outlook 2007 とプレビュー ハンドラの対話方法をより明確に理解できます。

まとめ

プレビュー ハンドラは、Windows Vista シェルと Outlook 2007 のどちらにとってもすばらしい付加機能です。マネージ コードの生産性のメリット (既存のフレームワークを使用した場合、XAML プレビュー ハンドラを作成し、そのハンドラを起動して実行するのに 7 分しかかかりませんでした) が加わったため、独自のファイルの種類だけでなく既存のファイルの種類も表示する非常に強力なプラットフォームになりました。プレビュー ハンドラは日常業務における生産性を向上するので、そのサポート対象のファイルの種類が増えるほど生産性が向上すると考えられます。では、お気に入りの拡張子用のプレビュー ハンドラのコーディングを始めてください。

Stephen Toub は、MSDN Magazine のテクニカル エディタです。


SideShow ガジェット: Windows SideShow デバイスのガジェットを記述する

SideShow ガジェット

Windows SideShow デバイスのガジェットを記述する

Jeffrey Richter
この記事の内容 : :
  • SideShow とその関連 API
  • ガジェットを開発、インストール、および構成する
  • デバイス キャッシュを管理する
  • ガジェットのイベントと通知
この記事は次のテクノロジを使用しています:
Windows Vista
コードのダウンロード : : SideShow2007_01.exe (169KB)
コンテンツ
SideShow デバイスについて
使用と構成
SideShow ガジェットとデバイスのアーキテクチャ
ガジェットをインストールする
デバイス キャッシュを管理する
ガジェットの有効期間
コンテンツ
機能、イベント、および通知
まとめ

現在のアプリケーションは、エンド ユーザーと共有する情報を大量に保持しています。ただし、画面上に表示される情報は限られているものであり、ありとあらゆる情報を編成することは、ソフトウェア開発者だけでなくエンド ユーザー側にも負担をしいることになります。さらに、情報に優先順位を付ける方法やタイミングは、ユーザーによって異なります。たとえば、ミーティングの直前であれば、ミーティングの開始時間と開催場所をすぐに把握する必要があります。車の運転中であれば、あとどれくらいで右折または左折するのかを把握する必要があります。また、一般的なこととしては、新しく受信した電子メール メッセージを確認できるようにしたいとも思っています。このように、交通情報、運航情報、気象情報、金融取引情報など、さまざまな情報が必要な場面が考えられます。適切な情報を適切なタイミングでユーザーに提供するというのは、難しい問題です。

ガジェットは、このような問題を軽減することを目的としています。ガジェットは、ユーザーが必要とする情報を適切なタイミングと場所で提供するアプリケーションです (また、ガジェットはアプリケーションの一部であることもあります)。現在、マイクロソフトでは 3 種類のガジェットを提供しています。Windows® サイドバーのガジェットは、ユーザーのデスクトップ上または Windows Vista™ サイドバーでユーザー情報を提供します。Windows Live™ のガジェットは、Web ブラウザを使用して任意のコンピュータ上でユーザー情報を提供します。そして、Windows SideShow™ のガジェットは、SideShow 対応デバイスのドライバを経由して補助ハードウェアでユーザー情報を提供します。この記事では、SideShow ハードウェア デバイスを取り上げ、マネージ API を使用して SideShow デバイスと通信する SideShow ガジェットの記述方法を紹介します。

SideShow デバイスについて

Windows Vista では、複数の補助ディスプレイ デバイスとの通信がサポートされています。補助ディスプレイ デバイスには、さまざまな形状やサイズのものがあり、その機能も異なります。SideShow デバイスには、まだ製造されていないものもありますが、近いうちに入手できるようになります。

デバイスには、コンピュータの電源がオンになると電源がオンになるものもあれば、コンピュータの電源状態に関係なく電源をオンにできるものもあります。また、充電式のバッテリーを個別に備えているデバイスもあります。カラー表示対応のもの、グレースケールやモノクロ表示のもの、テキストしか表示できないものやビットマップを表示できるものもあります。データをキャッシュするためのオンボード メモリの量はデバイスによって異なり、オンボード メモリを持たないデバイスもあります。キャッシュされたデータを操作するボタンがあるものもあれば、タッチパネルを備えたものもあります。

コンピュータとデバイスが物理的に接続されていなくても通信できるように、Bluetooth などのワイヤレス テクノロジが組み込まれているものもあります。また、メモリにダウンロードした音楽や映像を再生して音声を聴くためのヘッドフォンの差し込み口があるものもあります。

図 1 は、SideShow デバイスが組み込まれている一般的なノート型パソコンと SideShow デバイスを拡大したものです。この SideShow デバイスの解像度は 220×176 で、色深度は 16 ビットです。このデバイスには、Menu、左、右、上、下、および Enter ボタンがあります。この SideShow デバイスの電力は自己供給によるもので、このデバイスの電力はコンピュータの電源を使用して供給されています。しかし、ユーザーは、コンピュータの電源が入っていなくても、このデバイスを使用することができます。このデバイスは、電源がオンになっていると 4 ミリワットの電力を消費します。これに対して、コンピュータの電源がオンになっている場合、コンピュータ全体の消費電力量は 75 ワット前後になります。

図 1 SideShow デバイスが組み込まれた一般的なノート型パソコン
図 1 SideShow デバイスが組み込まれた一般的なノート型パソコン(画像を縮小するには、ここをクリックします)
図 1 SideShow デバイスが組み込まれた一般的なノート型パソコン
図 1 SideShow デバイスが組み込まれた一般的なノート型パソコン (画像を拡大するには、ここをクリックします)

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

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

ノート型パソコンを閉じていても、コンピュータの電源がオフになっていても、ユーザーは、電子メールを読んだり、予定表を調べたり、デジタル写真を見たり、マッピングの方向を調べたり、現在の日付や時刻を確認したりすることができます。もちろん、独自に作成したアプリケーションをカスタマイズして、アプリケーションのデータを SideShow デバイスにエクスポートすることは可能です。この方法については、この記事の後半で説明します。図 2 は、一般的な Windows Media® Center のリモート制御デバイスです。ユーザーは、補助ディスプレイを使用して、楽曲を選択し、Media Center で再生することができます。 このデバイスの優れている点は、テレビやモニタの電源が入っていなくても、音楽を聴けることです。また、映画を見ているときに、Media Center PC から Windows SideShow デバイスに番組情報を送信して、モニタで再生中の映像を妨げることなく、番組情報を読むことができます。さらに、リモコンを使用して、テレビ番組のデータを見たり、録画の予約をすることもできます。

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

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

SideShow デバイスには、LCD ディスプレイを搭載したキーボード、デジタル ピクチャー フレーム、携帯電話などがあります。1 台のコンピュータに対して、複数の SideShow デバイスを同時に接続することができます。たとえば、図 1 のような一般的なノート型パソコンに対して、キーボードとリモコンが同時に通信することができます。このようなディスプレイには、同じような種類の情報を表示させることも、異なる種類の情報を表示させることもできます。

使用と構成

エンド ユーザーは、Windows SideShow コントロール パネル アプレットを使用してサイドショー デバイスを構成します。このアプレットを起動すると、図 3 のような画面が表示されます。

図 3 デバイスとガジェットを制御する SideShow アプレット
図 3 デバイスとガジェットを制御する SideShow アプレット(画像を縮小するには、ここをクリックします)
図 3 デバイスとガジェットを制御する SideShow アプレット
図 3 デバイスとガジェットを制御する SideShow アプレット (画像を拡大するには、ここをクリックします)

左側のパネルには、現在インストールされている SideShow ハードウェアがインストール日時の順で表示されます。メイン パネルはグリッド形式になっています。1 列目には、SideShow デバイスと通信できるインストール済みのアプリケーションが表示されます。これが SideShow ガジェットです。既定では、Windows Vista には 2 つのガジェットが同梱されています。受信トレイ - Windows メールと Windows Media Player です。図 3 には、私がインストールした他のガジェットが表示されています。1 つは、Microsoft® Office Outlook 2007 に同梱されている Office Outlook® 2007 予定表で、もう 1 つは私が記述した Jeff's SideShow Gadget という名前のガジェットです。 2 列目以降のグリッド列は、コンピュータに接続している SideShow デバイスを表します。図 3 には、Windows SideShow Simulator-0 と Logitech G-15 Keyboard という 2 つの SideShow デバイスが表示されています。

ガジェットが通信できる SideShow デバイスを指定するには、グリッドでチェック ボックスをオンにします。また、グリッドの上部にある SideShow デバイスを選択して、デバイスを構成することもできます。たとえば、[Windows SideShow Simulator-0] をクリックすると、図 4 のようなウィンドウが表示されます。このウィンドウでは、画面の外観と動作 (テーマ、言語、フォント サイズ、明るさ、バックライトなど)、セキュリティとプライバシーの設定 (デバイスのロック解除に PIN を使用する、ログオフ時にすべてのデータを削除するなど)、およびガジェット (ガジェットの有効化または無効化、表示順序の変更) を構成したり、デバイスを取り外したりすることができます。

図 4 SideShow デバイスの構成
図 4 SideShow デバイスの構成(画像を縮小するには、ここをクリックします)
図 4 SideShow デバイスの構成
図 4 SideShow デバイスの構成 (画像を拡大するには、ここをクリックします)

多くの SideShow デバイスは、ガジェットのデータをキャッシュするのに使用するオンボード メモリを備えています。ガジェットから SideShow デバイスにデータを送信するには、ガジェットが実行されている必要があります。つまり、コンピュータの電源がオンになっている必要があります。 Windows Vista では、コンピュータのスリープ状態を解除してガジェットを実行するタイミングと頻度をユーザーが構成できます。この構成は、図 3 の [自動的にスリープを解除するようにコンピュータを設定する] オプションで設定できます。このオプションをクリックすると、図 5 のようなウィンドウが表示されます。 コンピュータはスリープ状態から解除されると、コントロール パネルの電源オプション アプレットで指定されている時間が経過するまでスリープ状態になりません。ただし、自動的にスリープを解除する機能をサポートしていないコンピュータがあること、コンピュータがスリープ状態に入ることを妨げるアプリケーションがあることに注意してください。

図 5 ガジェットがデバイスのスリープを解除する方法の構成
図 5 ガジェットがデバイスのスリープを解除する方法の構成(画像を縮小するには、ここをクリックします)
図 5 ガジェットがデバイスのスリープを解除する方法の構成
図 5 ガジェットがデバイスのスリープを解除する方法の構成 (画像を拡大するには、ここをクリックします)

SideShow ガジェットとデバイスのアーキテクチャ

図 6 は、SideShow ガジェット アプリケーションが SideShow ハードウェア デバイスと通信する方法を図示したものです。各ハードウェア デバイスにはユーザー モード デバイス ドライバが用意されている必要があり、このドライバは Windows Vista にインストールされている必要があります。Windows がデバイスと通信する際には、オペレーティング システムが WUDFHost.exe プロセスを作成し、このプロセスにデバイス ドライバを読み込みます。そのため、デバイス ドライバごとに WUDFHost.exe のインスタンスが存在します。ユーザー モード デバイス ドライバを使用することには多くの利点があります。たとえば、開発やデバッグが容易であること、システムの安定性とセキュリティが強化されることなどがあります。Windows ユーザーモード デバイス ドライバ フレームワークの詳細については、「ユーザー モード ドライバ フレームワーク (UMDF)」を参照してください。

図 6 SideShow ガジェットの通信アーキテクチャ
図 6 SideShow ガジェットの通信アーキテクチャ(画像を縮小するには、ここをクリックします)
図 6 SideShow ガジェットの通信アーキテクチャ
図 6 SideShow ガジェットの通信アーキテクチャ (画像を拡大するには、ここをクリックします)

SideShow ガジェット アプリケーションは、ネイティブ コードまたはマネージ コードで記述できます。ネイティブ アプリケーションでは、COM コンポーネントと AuxiliaryDisplayApi.dll に実装されているメソッドを使用して接続先のデバイスを制御します (この DLL 名は SideShow の元の名前が Auxiliary Display であったことの名残です)。この DLL は Windows Vista に同梱されており、すべての Windows Vista コンピュータで利用できます。ただし、この DLL は Windows Vista Starter Edition や Home Basic Edition では機能しません。COM コンポーネントの主な目的は、種類の異なるデバイスが接続されているかどうかや、接続されているデバイスでサポートされている機能 (画面のサイズ、色など) が異なることについて、開発者が考慮する必要をなくすことです。

マネージ SideShow ガジェットは、Microsoft.SideShow 名前空間で定義されている型を使用して、接続されているデバイスを制御します。 これらの型は、Microsoft.SideShow.dll アセンブリ ファイルに実装されています (これは、私が Windows Vista チームのためにデザインして実装したもので、Microsoft ダウンロード センターからダウンロードできます)。内部では、これらの型は AuxiliaryDisplayApi.dll の COM コンポーネントと直接通信します。残念ながら、リリースのタイミングの問題により、このアセンブリは、すべての Windows Vista に同梱されているかどうかは保証できません。このアセンブリを使用してマネージ SideShow ガジェットを記述した場合は、この DLL がエンド ユーザーのコンピュータ上に存在することを確認する必要があります。

開発者がガジェット コードをテストするのを支援するために、Windows Vista SDK には SideShow シミュレータ アプリケーションが収録されています。このアプリケーションを Windows Vista で実行すると、SideShow ハードウェア デバイスが列挙されます。このシミュレータが動作するようにするには、まず、コマンド プロンプトから次のコマンドを実行する必要があります。

WindowsSideShowVirtualDevice.exe /regserver

このコマンドは、シミュレータで使用する COM コンポーネントを登録します。この COM コンポーネントの登録は、各コンピュータで 1 回だけ行う必要があります。コンポーネントを登録したら、この SDK に収録されている VirtualSideShow.exe アプリケーションを実行して、いつでもガジェットをテストできます。

Microsoft.SideShow.dll アセンブリでは、Microsoft.SideShow 名前空間と Microsoft.SideShow.SimpleContentFormat 名前空間にある多数の型が定義されているため、簡単に SideShow ガジェットを作成したり、SideShow の機能を既存のアプリケーションに統合したりすることができます。図 7 に最も重要かつ使用頻度の高いクラスを示します。

Figure 7 SideShow 関連の重要なクラス (Close Figure 7)

クラス | 説明
GadgetRegistration | レジストリにガジェットをセットアップするのをサポートする静的クラス。
SideShowGadget | アプリケーション コードとユーザーが接続した 1 つ以上の SideShow ハードウェア デバイス間の接続を表す抽象基本クラス。このクラスは Component から派生したクラスで、Windows フォームにドロップすることができます。
ScfSideShowGadget | SideShowGadget の派生クラス。このクラスでは、アプリケーションが Simple Content Format (SCF) XML 文字列や画像を SideShow デバイスに送信できます。
Scf | 静的メソッドを提供する静的クラス。このクラスを使用すると、ソース コードで SCF XML を簡単に作成できます。
DeviceCapabilities | このクラスのオブジェクトは、ハードウェア デバイスの機能を表します。ガジェット アプリケーションで、このクラスを使用すると、ガジェットからデバイスに送信するデータを微調整できます。

ガジェットをインストールする

SideShow ガジェットの開発時には、まずガジェットの GUID を作成する必要があります。この GUID は、ガジェットの ID となり、Windows Vista でガジェットを操作する場合には、この ID が使用されます。ガジェットのインストール時には、インストール プログラムで、ガジェットの GUID に基づいて、次のような新しいレジストリ サブキーを作成する必要があります。

HKEY_CURRENT_USER\SOFTWARE\Microsoft\SideShow\Gadgets\{<GUID>}

コンピュータを使用するすべてのユーザー用にガジェットをインストールする場合は、HKEY_CURRENT_USER の代わりに HKEY_LOCAL_MACHINE を使用します。レジストリ サブキーを作成したら、このサブキーの下に、図 8 に示すいくつかのレジストリ値を追加する必要があります。

Figure 8 ガジェットのインストールに関連するレジストリ値 (Close Figure 8)

レジストリ値 | 説明
Icon | コントロール パネルとデバイス自体に表示するアイコンを参照するファイルのパス (オプションで Win32® アイコン リソースの ID 番号を指定できます)。このレジストリ値が指定されない場合は、既定のアイコンが表示されます。32 色の 16x16、32x32、および 48x48 ピクセルのサイズの画像 (アルファ チャネル付き) を使用することをお勧めします。
FriendlyName | コントロール パネルでガジェットの名前として表示する文字列またはパス名と Win32 文字列リソース ID (ローカライズされた文字列の場合)。この値を指定しないと、ユーザーには文字列が表示されません。
StartCommand | ガジェットのコードを含む実行可能ファイルのパス。ユーザーのログオン時、ユーザーの簡易切り替え時、コンピュータがスリープ状態から再開したとき、またはユーザーがデバイスでガジェットを有効化したときに、Windows Vista では、この値を使用してアプリケーションを自動的に起動します (ここでは、高度な処理はほとんど行われていません。Windows Vista では、ガジェット アプリケーションを頻繁に呼び出すため、ガジェット アプリケーションのサイズは小さくする必要があります。また、ガジェット アプリケーションでは、同じアプリケーションの別のインスタンスが既に実行されている場合、またはデバイスが接続されていない場合には、自身を終了する必要があります)。
Endpoints | サポートされているエンドポイントの一覧。これは iCal 形式または SCF のいずれかになります。iCAl は、予定表の情報を公開するために設計されたテキスト表現です (iCAl については、www.ietf.org/rfc/rfc2445.txt (英語) を参照してください)。Office Outlook 2007 予定表ガジェットでは、このエンドポイントを使用して iCal データを SideShow デバイスに送信し、デバイスでユーザーの予定表のデータを表示しています。iCal エンドポイントをサポートしているガジェットとデバイスは、まだあまりありません。これに対して、SCF は、相互に参照する一連の XML コンテンツ ページです。ほとんどのアプリケーションでは SCF 形式が使用されるため、この記事では、この形式について取り上げています。
CacheAlgorithm | ガジェットのコンテンツを管理する方法に関するヒントをデバイスに通知する値 (デバイスのキャッシュ メモリ量はごく限られたものですが、多数のガジェットを 1 つのデバイスに接続することが可能です)。ガジェットでは、最新のアイテム (0) を維持するのか (既定の動作)、古いアイテム (1)、頻繁にアクセスするアイテム (2)、または最近アクセスしたアイテム (4) を維持するのかをデバイスに通知できます。
OnlineOnly | この値が 1 の場合、ユーザーは、Windows とガジェット アプリケーションの両方が実行されている場合にのみ、デバイス上にあるガジェットを操作できます。
PropertyPage | ガジェットでは、Windows コントロール パネルからユーザーが起動できる構成設定ダイアログ ボックスを提供できます。このようなダイアログ ボックスを提供するには、ガジェットでは IpropertyPage インターフェイスを実装した COM コンポーネントを定義する必要があります。また、ガジェットでは、セットアップ中に COM コンポーネントの GUID を示す、このレジストリ値を追加する必要があります。このレジストリ値を指定すると、コントロール パネルでは、ガジェット アプリケーションの名前に下線が表示され、ユーザーがガジェットをクリックして、ガジェットの構成ダイアログ ボックスを呼び出せるようになります。

ガジェットのセットアップ プログラムでは、独自のレジストリ サブキーを作成し、必要なレジストリ値を設定する必要があります。通常、インストール プログラムでは、.reg ファイルに追加するレジストリ値を格納し、アプリケーションのセットアップ時に、このファイルを呼び出すという方法を取ります。セットアップ プログラムでは、セットアップが完了する直前に、次のコマンドを実行する必要があります。このコマンドにより、Windows Vista ではユーザーにガジェット アプリケーションがインストールされたことを示すヒントが表示されます。

SchTasks.exe /run /tn Microsoft\Windows\SideShow\GadgetManager

GadgetManager タスクでは、デバイスがインストールされていない場合、前回の実行以降新しいガジェットがインストールされていないことを検出した場合、またはユーザーが [新しいガジェットがインストールされると通知する] チェック ボックス (図 3 参照) をオフにしてこの機能を無効にしている場合、ヒントは表示されません。

ガジェットのインストールが完了したら、ユーザーはコントロール パネルで、ガジェットを構成したり、任意のハードウェア デバイスと接続したりすることができます。ただし、セキュリティ上の理由により、既定では、新しくインストールされたガジェットは、どのデバイスとも通信できません。

ガジェットは実行中、接続しているデバイスに定期的にデータを送信します。コンピュータの電源をオフにしても、デバイスではいくらかのデータがキャッシュされているので、ユーザーはコンピュータの電源をオンにしなくてもデータを利用することができます。ユーザーがコントロール パネルを使用してガジェットにデバイスを追加または削除した場合、ガジェットが実行中であれば、デバイスが追加または削除されたことを示す通知を Windows から受信します。

ガジェットは、オンライン専用として指定することができます。つまり、ユーザーは、コンピュータと Windows にインストールされているガジェットの両方が実行されている場合にのみ、デバイス上のガジェットを操作できるようにすることができます。ユーザーが、このように設定されているデバイス上のガジェットを操作したときに、コンピュータがスリープ状態または休止状態にあると、デバイスでは、コンピュータの電源をオンにするかどうかを確認するメッセージが表示されます。ユーザーが同意すると、コンピュータの電源がオンになります。コンピュータが実行されていて、かつユーザーがログインしている場合、デバイスはガジェットと通信します。 コンピュータが完全にシャットダウンされている場合、デバイスでは、ガジェットがデバイスと通信できるようにコンピュータの電源を手動でオンにして、ログインするように促すメッセージが表示されます。

デバイス キャッシュを管理する

デバイスのキャッシュ メモリの容量には制限がありますが、ユーザーは 1 つのデバイスに複数のガジェットを接続できるため、デバイスでは効率的にメモリを管理する必要があります。メモリの管理方法はデバイスによって異なりますが、この記事では、マイクロソフト製のファームウェアを使用しているデバイスの場合について説明します。

現在のマイクロソフト製のファームウェア デバイスでは、デバイス上のメモリは、デバイスに接続されているすべてのガジェット間で均等に割り当てられます。そのため、デバイスに 3 つのガジェットが接続されている場合、各ガジェットでは 3 分の 1 のメモリを利用できます。割り当て分よりも少ないメモリしか必要ないガジェットがある場合、他のガジェットでは割り当てられている量よりも多くのメモリを使用できます。割り当て分よりも少ないメモリしか使用していないガジェットでは、割り当て分よりも多いメモリを使用しているガジェットに割り当てられているメモリを使用できます。

また、割り当て分よりも多いメモリを使用しているガジェットで、さらにメモリが必要な場合は、割り当て分よりも多いメモリを使用しているガジェットの中で、優先度が最も低いガジェットに割り当てられているメモリを使用することができます。図 3 のウィンドウでは、ガジェットの表示順序を変更するオプションが用意されています。先頭にあるガジェットの優先度が最も高く、割り当てられるメモリの量が多くなり、この一覧の下に行くにつれて、優先度は低くなり、他のガジェットが多くのメモリを必要とする場合には、これらのガジェットに割り当てられているメモリが使用されます。あるガジェットに割り当てられているメモリを他のガジェットで使用すると、デバイスでは、ガジェットの CacheAlgorithm レジストリ値に設定されているキャッシュ ポリシーが使用されます。

ガジェットの開発者は、デバイスに渡されたコンテンツが、必ずしもデバイス上に保持されるわけではないということに注意する必要があります。ユーザーが、削除されたコンテンツにアクセスする場合、デバイスでは Windows で実行されているガジェットと通信して、削除されたコンテンツを再度取得する必要があります。不足しているコンテンツを許容待ち時間内にガジェットから取得できない場合、デバイスではユーザーに対してコンテンツがないことを示す通知が表示されます (許容待ち時間は、デバイスによって異なりますが、シミュレータでは 3 秒です)。ネイティブ SideShow API とは異なり、マネージ SideShow ライブラリでは、すべてのコンテンツが Windows にキャッシュされ、ガジェットのプロセスが Windows Vista 上で実行されていればコンテンツをデバイスに自動送信します。

ガジェットの有効期間

ガジェットが SideShow デバイスに情報をダウンロードするには、Windows でガジェットが実行されている必要があります。基本的に、ガジェットには 2 つの種類があります。UI ガジェットと非 UI ガジェットです。UI ガジェットは、ユーザーが実行したり、操作したりするアプリケーションです。たとえば、ユーザーが Windows エクスプローラから起動する Windows フォーム アプリケーションは UI ガジェットです。ユーザーがアプリケーションを操作すると、デバイスにデータがダウンロードされます。ユーザーがアプリケーションを終了しても、情報はデバイス上に格納されているため、コンピュータの電源をオフにしても情報にアクセスできます。このアプリケーションの有効期間は、ユーザーが決めることができます。Microsoft Office Outlook 2007 ガジェットは、UI ガジェットの一例です。

非 UI ガジェットにはユーザー インターフェイスがありません。このガジェットはバックグラウンドで何かを監視する目的で実行され、定期的にデバイスにデータを送信するので、コンピュータの電源がオフになっていても、データを参照することができます。非 UI ガジェットの存在をユーザーから隠すために、Windows では自動的に非 UI ガジェットを起動するメカニズムが用意されています。ユーザーがアカウントを使用してログオンしたり、スリープ状態から再開したり、SideShow コントロール パネル アプレットを使用してガジェットとデバイスの接続を確立または切断したときに、Windows では、すべての非 UI ガジェットの StartCommand レジストリ値を実行して、ガジェットを自動的に起動します。

Windows では、単に登録されているすべてのガジェットを起動しているだけで、高度なアルゴリズムは使用していません。つまり、ガジェット アプリケーションは既に実行されている可能性がありますが、ユーザーがデバイスの接続を確立または切断した場合、Windows では、単にアプリケーションを再度実行することになります。そのため、非 UI ガジェットは、起動時に別のインスタンスが既に実行されているかどうかを確認し、別のインスタンスが実行されている場合は、自身の実行を終了する必要があります。また、一般的に、非 UI ガジェットは、ユーザーがガジェットからすべてのデバイスを切断したときに終了します。詳細については、この記事の「機能、イベント、および通知」を参照してください。

他のインスタンスによってデバイスに送信されたデータが上書きされることを防ぐため、UI ガジェットは、常に 1 つのインスタンスだけが実行されるようにすることをお勧めします。また、一般的には、UI ガジェットでは UI が表示されるため自動的には起動しないことをお勧めします。そのため、大半の UI ガジェットには、StartCommand レジストリ値は設定しないことをお勧めします。

コンテンツ

すべてのガジェットでは、デバイスにコンテンツを渡します。デバイスには、デバイスに接続されているすべてのガジェットを表示するホーム ディスプレイがあります。図 9 の左上にある画像は、ホーム ディスプレイの例です。ご覧のように、各ガジェットの隣にはアイコン、名前、テキスト (オプション) が表示されます。アイコンと名前は、Icon レジストリ値および FriendlyName レジストリ値に基づいて表示されます。ここに表示されるテキストは、概要コンテンツと呼ばれるもので、ガジェットの実行中に SideShowGadget の AddGlanceContent メソッドを呼び出すことによってガジェットからデバイスに送信されます。

ガジェット画面の操作

図 9a デバイスのホーム ページ
図 9a デバイスのホーム ページ(画像を縮小するには、ここをクリックします)
図 9a デバイスのホーム ページ
図 9a デバイスのホーム ページ (画像を拡大するには、ここをクリックします)
図 9b ガジェットのホーム ページ (コンテンツ ID 1)
図 9b ガジェットのホーム ページ (コンテンツ ID 1)(画像を縮小するには、ここをクリックします)
図 9b ガジェットのホーム ページ (コンテンツ ID 1)
図 9b ガジェットのホーム ページ (コンテンツ ID 1) (画像を拡大するには、ここをクリックします)
図 9c テキスト ページ (コンテンツ ID 2)
図 9c テキスト ページ (コンテンツ ID 2)(画像を縮小するには、ここをクリックします)
図 9c テキスト ページ (コンテンツ ID 2)
図 9c テキスト ページ (コンテンツ ID 2) (画像を拡大するには、ここをクリックします)
図 9d (コンテンツ ID 1000 のイメージを表示している) イメージ ページ (コンテンツ ID 3)
図 9d (コンテンツ ID 1000 のイメージを表示している) イメージ ページ (コンテンツ ID 3)(画像を縮小するには、ここをクリックします)
図 9d (コンテンツ ID 1000 のイメージを表示している) イメージ ページ (コンテンツ ID 3)
図 9d (コンテンツ ID 1000 のイメージを表示している) イメージ ページ (コンテンツ ID 3) (画像を拡大するには、ここをクリックします)

ユーザーがデバイス上のガジェットを操作すると、デバイスでは、前回ガジェットが Windows で実行されていたときにダウンロードしたコンテンツが表示されます。現在、SideShow では 2 種類のコンテンツがサポートされています。Simple Content Format (SCF) と iCAl です (iCAl については、この記事では説明しません)。SCF コンテンツは、ガジェット アプリケーションからデバイスに送信される一連の XML 文字列です。デバイスでは、この XML 文字列を解析してデバイスにコンテンツを表示します。

新しい SCF 形式では、ガジェット アプリケーションが 3 種類のコンテンツを定義することが許可されています。そのコンテンツの種類とは、テキストとイメージ、他のコンテンツへのナビゲーションを行う項目で構成されているメニューとコンテキスト メニュー、および他のコンテンツへのナビゲーションによって生じる確認事項をユーザーに表示するダイアログ ボックスです。SCF の XML スキーマの詳細については、windowssdk.msdn.microsoft.com/ms744133.aspx (英語) を参照してください。

マネージ SideShow アセンブリ (Microsoft.SideShow.dll) には、多数の静的メソッドを含むパブリックな静的クラス Scf が含まれているため、コードで簡単に XML 文字列を作成できます。図 10 に示す DownloadDataToDevices メソッドは、ScfSideShowGadget を開いて概要コンテンツの文字列を送信する方法を例示しています。このサンプルでは、ScfSideShowGadget の AddContent メソッドを使用して、SCF メニュー、SCF テキスト、および SCF イメージのコンテンツをデバイスに送信する方法も例示しています。ご覧のとおり、各コンテンツはコンテンツの ID 番号で識別されます。ガジェットの最初のページ (図 9 の左下の画像) は、ホーム ページです。このコンテンツのコンテンツ ID は常に 1 になります (コンテンツ ID は、ScfSideShowGadget の HomePageContentId 定数で表されます)。ユーザーが、See Text メニュー項目を選択すると、デバイスは、コンテンツ ID が 2 のページに移動します。また、ユーザーが See Image メニュー項目を選択すると、デバイスは、コンテンツ ID が 3 のページに移動します。ページ 3 (イメージ ページ) には、イメージの表示方法 (中央揃え、自動調整) を記述した XML が含まれており、実際の画像を参照しています (この画像にはコンテンツ ID 1000 を割り当ててあります)。コードでは、アセンブリ リソースから、この画像を抽出しています。

Figure 10 DownloadDataToDevices メソッド (Close Figure 10)
private static void DownloadDataToDevices() {
   // Construct a SCF SideShow gadget for our gadget's Guid
   using (ScfSideShowGadget gadget = new ScfSideShowGadget(s_gadgetId))
   {
      // Add Glance content showing date/time of last update
      gadget.AddGlanceContent(
         String.Format("Content updated on {0}{1:D}.",
            Environment.NewLine, DateTime.Now));

      // The home page is a menu with an item that goes to some
      // text and another item that goes to an image
      gadget.AddContent(
         Scf.Menu(ScfSideShowGadget.HomePageContentId,
            "Simple Gadget Menu", ScfSelectAction.Target,
            Scf.Item(2, "See text"), Scf.Item(3, "See image")));

      // This is the text page
      gadget.AddContent(
         Scf.Content(2, "This is the Text Page",
            Scf.Txt(ScfAlign.Left, true, Color.Red,
               "This is some text")));

      // This is the image page
      gadget.AddContent(
         Scf.Content(3, "This is the Image Page",
            Scf.Img(1000, ScfAlign.Center, ScfImageFit.Auto,
               "alt picture text")));

      // This is the image itself (that appears on the image page)
      Stream imageStream = Assembly.GetEntryAssembly().
         GetManifestResourceStream(
            "SimpleSideShowGadget.Aidan And Jeff.jpg");
      gadget.AddContent(1000, ImageContentTransforms.ReduceColorDepth |
         ImageContentTransforms.StretchToFit,
         Image.FromStream(imageStream));
   }
}

機能、イベント、および通知

ガジェット アプリケーションでは、ガジェットに接続しているデバイスの数、およびこれらのデバイスの機能について Windows に照会することができます。たとえば、デバイスの機能としては、DeviceId、ScreenType (Bitmap または Text)、ScreenWidth、ScreenHeight、ClientAreaWidth、ClientAreaHeight、ColorDepth、および SupportedImageFormats などがあります。

ガジェット アプリケーションでは、デバイスの機能に基づいて、デバイスに送信するコンテンツを判断します。この記事のサンプル アプリケーションでは、ガジェット アプリケーションが、接続しているデバイスの数を照会する方法、これらのデバイスの機能について照会する方法を例示しています。

ガジェット アプリケーションでは、いくつかのイベント (図 11 参照) を登録することによって、実行中に、デバイスからイベントを受け取ることができます。すべてのガジェット型の基本クラスである SideShowGadget 型では、エンドポイントに関係なく、すべてのガジェットに適用できるイベントを提供しています。SideShowGadget 型から派生している ScfSideShowGadget 型では、SCF ガジェット エンドポイント固有の追加のイベントを提供しています。アプリケーションでは、これらのイベントに応答して、コンテンツを追加または削除することができます。高度な機能を備えたガジェットには、これらのイベントに応答するために、ガジェットが常に実行されている必要があるというものもあります。ガジェットを常に実行する必要がある場合、そのガジェットの OnlineOnly レジストリ値に 1 を設定する必要があります。

Figure 11 ガジェットが応答できるデバイスのイベント (Close Figure 11)

イベント | 説明
DeviceAdded | ユーザーがデバイスをガジェット アプリケーションに接続したときに発生します。このイベントを受け取るメソッドには、デバイスの機能に関する情報も渡されます。
DeviceRemoved | ユーザーがガジェット アプリケーションからデバイスを切断したときに発生します。このイベントを受け取るメソッドには、デバイスの機能に関する情報も渡されます。
AllDevicesRemoved | ユーザーがガジェットからすべてのデバイスを切断したときに発生します。通常、非 UI ガジェットは、このイベントを受け取ると終了します。
ContentMissing | ユーザーが、デバイス上に存在しないコンテンツにアクセスしようとしたときに発生します。このイベントを受け取るメソッドには、存在しないコンテンツの ID が渡されます。既に説明したように、マネージ ラッパーでは、コレクションに含まれているすべてのコンテンツを保存します。(CacheAlgorithm レジストリ値で設定されている) ガジェットのキャッシュ ポリシーに従って、デバイスでコンテンツが破棄されても、マネージ ラッパーが自動的にコンテンツをデバイスに再送するので、コードで、このイベントを受け取ることはありません。これは、ネイティブ API ではなく、マネージ API を使用する大きな利点の 1 つです。
GadgetEnter | ユーザーがデバイス上のガジェットを操作したときに発生します。
GadgetExit | ユーザーがデバイス上にあるガジェットを終了したときに発生します。
ContentNavigate | SCF ガジェット専用のイベントで、ユーザーがあるコンテンツ ページから別のコンテンツ ページに移動したときに発生します。このイベントを受け取るメソッドには、移動元ページの ID、移動先ページの ID、このページ移動を発生させたボタンが渡されます。
MenuSelect | SCF ガジェット専用のイベントで、ユーザーがメニューから項目を選択したときに発生します。このイベントを受け取るメソッドには、移動元ページの ID、移動先ページの ID、メニュー項目 ID、およびメニュー項目の選択を発生させたボタンが渡されます。
ContextMenuSelect | SCF ガジェット専用のイベントで、ユーザーがコンテキスト メニューから項目を選択したときに発生します。このイベントを受け取るメソッドには、移動元ページの ID、移動先ページの ID、前の項目の ID、メニュー ページ ID、およびメニュー項目 ID が渡されます。

ガジェットは、SideShowGadget の ShowNotification メソッドを呼び出して、実行中に SideShow デバイスに通知を送信することができます。ガジェット アプリケーションは、会議のアラーム、電子メールのアラート、インスタント メッセージの受信、またはシステム通知などについての通知を表示します。通知は、ハードウェア デバイス上に表示されるという点を除いて、メッセージ ボックスと似ています。通知は、ID 番号、アイコン、見出し、メッセージ、および有効期限で構成されています。デバイスでは、ユーザーが通知を取り消すか、または通知の有効期限になるまで、通知が表示されます。また、アプリケーションでは、SideShowGadget の RevokeNotification メソッドを呼び出し、通知の ID 番号を渡すことによって、通知を取り消すことができます。

まとめ

この記事では、Windows SideShow の機能の目的と、エンド ユーザーがガジェットを操作および構成する方法について詳しく説明しました。また、開発者向けの情報として、SideShow のサポートを新規または既存のマネージ アプリケーションに統合する方法を紹介しました。これらのマネージ ラッパーを使用すると、SideShow の機能を独自のアプリケーションに簡単に統合できます。

個人的には、この新しい Windows SideShow の機能は、ユーザーがデータにアクセスして操作する方法に変化をもたらすので、非常に興味深いものだと感じています。これから 2 ～ 3 年の間に、SideShow をサポートする新しいハードウェア デバイスが利用できるようになると思います。特に、Media Center のリモコンと携帯電話で SideShow がサポートされるのを楽しみにしています。この非常に便利なデバイスが、PDA や携帯電話と並ぶ一般的なデバイスになるのは時間の問題でしょう。

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


デスクトップ セキュリティ: Windows Vista 用の資格情報プロバイダを使用したカスタム ログイン機能の作成

デスクトップ セキュリティ

Windows Vista 用の資格情報プロバイダを使用したカスタム ログイン機能の作成

Dan Griffin
この記事の内容 : :
  • 新しい資格情報プロバイダ アーキテクチャ
  • GINA ベースの認証の廃止
  • 多要素認証
  • 資格情報プロバイダの開発およびデバッグ
この記事は次のテクノロジを使用しています:
Windows Vista、C++
コードのダウンロード : : CredentialProviders2007_01.exe (241KB)
コンテンツ
新旧システムの比較
ハイブリッド資格情報プロバイダ
要件
設計
ハイブリッド資格情報プロバイダ
ハイブリッド実装
改善の余地
テストおよびデバッグ
スマート カードおよび初期化

W
indows Vista には、開発者がプラットフォームとの統合を進めるための新しいオプションが多数用意されています。劇的に変更されたのが、新しい資格情報プロバイダ モデルです。OS によりサポートされる新しいユーザー認証シナリオを簡単に実装できるようになりました。これは、GINA (Graphical Identification and Authentication) モデルという、率直に言えば開発者にとって理解するのも実装するのも難しく、マイクロソフトにとってサポート コストが高くつくことで知られるモデルに取って代わったものです。

ところで、Windows® ログオン プラグイン インターフェイスの改良が、なぜ大事件なのでしょうか。ログオン画面は、ユーザーがコンピュータの電源を入れたときに最初に見る画面です。この部分を資格情報プロバイダが受け持つようになったことで、ログオン操作をカスタマイズしたり、組織のニーズに最も合った認証方式を統合したりする作業が簡単になりました。要するに、資格情報プロバイダを使用することで、堅牢なセキュリティの開発および実装が容易になります。

新旧システムの比較

GINA ベースのログオン アーキテクチャについては詳しく説明しません。ですが、新しいアーキテクチャと、それに伴う変更点についての理解を深めるため、新旧のアーキテクチャを簡単に比較しておきます。

Windows Vista™ より前の環境では、すべてのセッションに winlogon のインスタンスがあり、このインスタンスがセッションの対話型ログオン シーケンスを受け持ちます (図 1 に、Windows XP および Windows Server® 2003 の古いログオン アーキテクチャを示します)。新しく起動したシステムでは、コンソールの対話型ログオンが常にセッション 0 で実行されます。セッション 0 は、システム サービスや、ローカル セキュリティ機関プロセスなどの重要なプロセスをホストします (つまり、図 1 に示した以外にも、多数のプロセスがセッション 0 で実行されます)。

図 1 GINA ログオン アーキテクチャ
図 1 GINA ログオン アーキテクチャ

コンピュータの登録済み GINA が、winlogon プロセス空間に読み込まれます ("GINA チェーン" と呼ばれる構成も可能ですが、構成が複雑なためテストおよびサポートが困難です)。最終的に、GINA によって LogonUser および関連する認証 API が呼び出されます。

Windows Vista では、セッション 0 が対話型ログオンに使用されることはありません (図 2 参照)。このしくみはセキュリティ上優れています。コンピュータごとのプロセスをユーザー プロセスから完全に隔離するセッション境界が設けられています。また、カーネルのグローバル名前空間が、ユーザー アプリケーションにより作成されたオブジェクトから既定で隔離されるので、より厳密に制御できるようになりました。

図 2 新しいログオン アーキテクチャ
図 2 新しいログオン アーキテクチャ

セッション 0 以外のすべてのセッションには、これまでどおり winlogon のインスタンスがあります。図に、システムに登録され、新しい LogonUI プロセスにより読み込まれた資格情報プロバイダを示します。

ログオンのグラフィックをレンダリングするコンポーネントについても、重要な変更が行われました。以前は、グラフィック レンダリングは GINA により処理されていたので、サードパーティのコンポーネントがこれを行うことも可能でした。新しいアーキテクチャでは、オペレーティング システムの埋め込みコンポーネントである LogonUI がレンダリングを行います。

新しいモデルでは、プロバイダごとのユーザー プロンプト動作をどのように実現するのでしょうか。資格情報プロバイダ アーキテクチャでは、プロバイダごとに UI 要素を列挙する必要があります。たとえば、あるシナリオで、編集ボックスとキャプションが 2 つずつ、チェック ボックスとビットマップが 1 つずつ必要だと、資格情報プロバイダが LogonUI に指定するとします。その場合、資格情報プロバイダに代わって LogonUI がこれらのコントロールをレンダリングします。このしくみによって、先ほど説明した、進歩を続けるさまざまな認証シナリオの見た目と方式を一貫させる目標をひとまず実現できます。

マイクロソフトの資格情報プロバイダ担当の開発チームは、外部の開発者の便宜を図り、COM ベースのプラグインを使用したモデルの提供を検討しました。しかし、Windows Vista の開発サイクルの初期段階では、新しいインターフェイスの最初の内部設計が、GINA と同様に完全に LoadLibrary と関数ポインタに依存していました。この最初の設計に教訓を得て、その後 COM をベースに再設計を行い、使いやすいインターフェイスが完成しました。ここで、credprov インターフェイスについて理解するためにサンプル コードを見てみましょう。

ハイブリッド資格情報プロバイダ

この新しいプラグイン モデルは絶好のタイミングで登場しました (満を持しての登場かもしれません)。開発者は、マイクロソフトが提供するアーキテクチャとの整合性を保ちながら、多要素認証シナリオの要求に簡単に応えることができます。

そうは言っても、新しいインターフェイスは抽象的です。これを抽象的なまま説明するのは実に退屈です。より興味を持って理解できるように、新しい資格情報プロバイダの設計、開発、およびテストについて説明します。なお、この説明に加え、マイクロソフトが既に提供しているドキュメント (詳細については、「その他のリソース」を参照してください) を併せて読むことでいっそう理解が深まります。

便利な新機能を紹介するため、"ハイブリッド資格情報プロバイダ" というサンプルを作成しました。ハイブリッド資格情報プロバイダを使用すると、ユーザー名、パスワード、およびドメイン名をスマート カードに保存できます。カードを挿入した時点で、自動的にログオンします (サンプル コードは MSDN®Magazine Web サイトからダウンロードできます)。コードは、一から記述したわけではなく、次の 3 つのソースのコードを組み合わせました。

  • Microsoft® Windows SDK に収録されているパスワードベースの資格情報プロバイダ サンプル。
  • 同じく Windows SDK に収録されている古い PropCert サンプル。証明書ベースのスマート カードの資格情報を読み取る Win32® スレッドが中核になっています。
  • MSDN Magazine 2006 年 11 月号の私の記事にあるサンプル コード。この記事は、マネージ コードを使用した Windows スマート カード サブシステムへの接続について説明しています。

2006 年 11 月号の私の記事にあるサンプル コードについては、さらに説明が必要です。資格情報プロバイダ アーキテクチャとそのホストでサポートされるのは、ネイティブ コードのみです。最初の記事はマネージ コードに特化し、新しいスマート カード モジュール インターフェイスを公開するネイティブ ヘルパ DLL を扱っています。ハイブリッド資格情報プロバイダは、このヘルパ DLL を基に構築されています。この DLL のすべてのソース コードは、2006 年 11 月号の記事に付属のダウンロードから入手できます。

つまり、ハイブリッド資格情報プロバイダのほとんどの部分は既存のコードをベースにしています。したがって、テストとデバッグの時間を最小限に抑えることができました。実際、中核的なデバッグ フェーズには 1 日もかかりませんでした。この事実が、新しいインターフェイスの使いやすさを物語っています。

ここからは、この資格情報プロバイダのサンプルで実現できることついて詳しく説明します。

要件

ハイブリッド資格情報プロバイダを企画する際、以下の要件を念頭に置きました。

  • スマート カードベースにする
  • コードを最大限に再利用する
  • 追加で必要な構成やインフラストラクチャを最小限にする

ここからハイブリッド方式、つまりパスワード (セキュリティのため) とスマート カード (利便性のため) の併用を思いつきました。ハイブリッド資格情報プロバイダの概念はユーザー名とパスワードが基になっているので、Platform SDK からサンプル パスワード プロバイダを取り出す作業から始めました。次に、SDK から取り出した PropCert サンプルを追加しました。このサンプルには、スマート カード リーダー、カード、およびデジタル証明書を列挙するロジックが含まれています。私は、PropCert の証明書ベースのロジックを、私自身の資格情報データを読み取る新しいコードに置き換えて、2 つのサンプルどうしを単純に接続するだけでよいと考えました。

スマート カードのパスワード ログオン情報を読み取るので、必然的にスマート カードをその資格情報で初期化するツールも必要です。初期化ツールの説明は記事の最後で行います。

以上の要件を念頭に置き、資格情報プロバイダ アーキテクチャの設計と、そのサンプル コードの設計への影響を確認します。

設計

まず、資格情報プロバイダ アーキテクチャの設計について、実行時の資格情報プロバイダの観点から説明します。

ハイブリッド サンプルについてはこれから詳しく説明しますが、動作中の新しい資格情報プロバイダ アーキテクチャを分析するための基盤としてこのサンプルを使用しています。説明しやすくするために、サンプル コードにはデバッグ トレースを含めてあります。トレースでは、実装されている各資格情報プロバイダ ルーチンから OutputDebugString を呼び出します。呼び出しの際は、2 つの省略形を使用しています。新しい ICredentialProvider インターフェイス (図 3 に抜粋) を呼び出す際は、前に "Provider::" と付けます。ICredentialProviderCredential インターフェイス (図 4 参照) を呼び出す際は、前に "Credential::" と付けます。資格情報プロバイダ関連のすべてのインターフェイスは、新しいパブリック ヘッダー credentialprovider.h で定義されます。

Figure 4 ICredentialProviderCredential インターフェイス (Close Figure 4)
ICredentialProviderCredential : public IUnknown
{
    HRESULT STDMETHODCALLTYPE Advise(
        /* [in] */ ICredentialProviderCredentialEvents *pcpce);

    HRESULT STDMETHODCALLTYPE UnAdvise( void);

    HRESULT STDMETHODCALLTYPE SetSelected(
        /* [out] */ BOOL *pbAutoLogon);

    HRESULT STDMETHODCALLTYPE SetDeselected( void);

    HRESULT STDMETHODCALLTYPE GetFieldState(
        /* [in] */ DWORD dwFieldID,
        /* [out] */ CREDENTIAL_PROVIDER_FIELD_STATE *pcpfs,
        /* [out] */ CREDENTIAL_PROVIDER_FIELD_INTERACTIVE_STATE *pcpfis);

    HRESULT STDMETHODCALLTYPE GetStringValue(
        /* [in] */ DWORD dwFieldID,
        /* [string][out] */ LPWSTR *ppsz);

    HRESULT STDMETHODCALLTYPE GetBitmapValue(
        /* [in] */ DWORD dwFieldID,
        /* [out] */ HBITMAP *phbmp);

    HRESULT STDMETHODCALLTYPE GetCheckboxValue(
        /* [in] */ DWORD dwFieldID,
        /* [out] */ BOOL *pbChecked,
        /* [string][out] */ LPWSTR *ppszLabel);

    HRESULT STDMETHODCALLTYPE GetSubmitButtonValue(
        /* [in] */ DWORD dwFieldID,
        /* [out] */ DWORD *pdwAdjacentTo);

    HRESULT STDMETHODCALLTYPE GetComboBoxValueCount(
        /* [in] */ DWORD dwFieldID,
        /* [out] */ DWORD *pcItems,
        /* [out] */ DWORD *pdwSelectedItem);

    HRESULT STDMETHODCALLTYPE GetComboBoxValueAt(
        /* [in] */ DWORD dwFieldID,
        DWORD dwItem,
        /* [string][out] */ LPWSTR *ppszItem);

    HRESULT STDMETHODCALLTYPE SetStringValue(
        /* [in] */ DWORD dwFieldID,
        /* [string][in] */ LPCWSTR psz);

    HRESULT STDMETHODCALLTYPE SetCheckboxValue(
        /* [in] */ DWORD dwFieldID,
        /* [in] */ BOOL bChecked);

    HRESULT STDMETHODCALLTYPE SetComboBoxSelectedValue(
        /* [in] */ DWORD dwFieldID,
        /* [in] */ DWORD dwSelectedItem);

    HRESULT STDMETHODCALLTYPE CommandLinkClicked(
        /* [in] */ DWORD dwFieldID);

    HRESULT STDMETHODCALLTYPE GetSerialization(
        /* [out] */ CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE
                    *pcpgsr,
        /* [out] */ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs,
        /* [out] */ LPWSTR *ppszOptionalStatusText,
        /* [out] */ CREDENTIAL_PROVIDER_STATUS_ICON
                    *pcpsiOptionalStatusIcon);

    HRESULT STDMETHODCALLTYPE ReportResult(
        /* [in] */ NTSTATUS ntsStatus,
        /* [in] */ NTSTATUS ntsSubstatus,
        /* [out] */ LPWSTR *ppszOptionalStatusText,
        /* [out] */ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon);
};
Figure 3 ICredentialProvider インターフェイス (Close Figure 3)
ICredentialProvider : public IUnknown
{
    HRESULT STDMETHODCALLTYPE SetUsageScenario(
        /* [in] */ CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
        /* [in] */ DWORD dwFlags);

    HRESULT STDMETHODCALLTYPE SetSerialization(
        /* [in] */ const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION
            *pcpcs);

    HRESULT STDMETHODCALLTYPE Advise(
        /* [in] */ ICredentialProviderEvents *pcpe,
        /* [in] */ UINT_PTR upAdviseContext);

    HRESULT STDMETHODCALLTYPE UnAdvise( void);

    HRESULT STDMETHODCALLTYPE GetFieldDescriptorCount(
        /* [out] */ DWORD *pdwCount);

    HRESULT STDMETHODCALLTYPE GetFieldDescriptorAt(
        /* [in] */ DWORD dwIndex,
        /* [out] */ CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR **ppcpfd);

    HRESULT STDMETHODCALLTYPE GetCredentialCount(
        /* [out] */ DWORD *pdwCount,
        /* [out] */ DWORD *pdwDefault,
        /* [out] */ BOOL *pbAutoLogonWithDefault);

    HRESULT STDMETHODCALLTYPE GetCredentialAt(
        /* [in] */ DWORD dwIndex,
        /* [out] */ ICredentialProviderCredential **ppcpc);
};

上記の点を念頭に置き、図 5 を参照してください。サンプル シナリオで発生するデバッグ イベント (大部分のイベントは詳細を説明する予定です) の一覧です。呼び出しシーケンスを生成するシナリオは単純です。まず、ドメインに参加している Windows Vista ワークステーションを起動します。次に、自分のユーザー名、パスワード、およびドメイン名を使用してスマート カードを構成します。コンピュータに接続したリーダーにスマート カードを挿入します。その後、システムを再起動します。

Figure 5 ハイブリッド資格情報プロバイダの呼び出しシーケンス (Close Figure 5)
1. [The system boots]

2. [LogonUI.exe process is created]

3. [Credential provider DLLs are loaded]

4. Provider::CreateInstance

5. [User presses Ctrl+Alt+Del]

6. Provider::SetUsageScenario (CPUS_LOGON)

7. Credential::Initialize

8. Provider::Advise

9. Provider::GetCredentialCount

10. Provider::GetCredentialAt (dwIndex = 0)

11. Provider::GetFieldDescriptorCount

12. Provider::GetFieldDescriptorAt (dwIndex = 0)

13. Provider::GetFieldDescriptorAt (dwIndex = 1)

14. Provider::GetFieldDescriptorAt (dwIndex = 2)

15. Provider::GetFieldDescriptorAt (dwIndex = 3)

16. Provider::GetFieldDescriptorAt (dwIndex = 4)

17. Credential::GetBitmapValue (dwFieldID = 0; tile image)

18. Credential::GetStringValue (dwFieldID = 1; user name field)

19. Credential::GetFieldState (dwFieldID = 1; user name field)

20. Credential::GetStringValue (dwFieldID = 2; password field)

21. Credential::GetFieldState (dwFieldID = 2; password field)

22. Credential::GetSubmitButtonValue (dwFieldID = 3; submit button)

23. Credential::GetFieldState (dwFieldID = 3; submit button)

24. Credential::GetStringValue (dwFieldID = 4; domain name field)

25. Credential::GetFieldState (dwFieldID = 4; domain name field)

26. Credential::Advise

27. Credential::GetSerialization

28. Credential::UnAdvise

29. Provider::UnAdvise

30. [The WinLogon process calls LogonUser]

31. Credential::Advise

32. Credential::ReportResult (ntsStatus = 0)

33. Credential::UnAdvise

最初に、winlogon によってコンソール セッション LogonUI プロセスが起動します。LogonUI の生成時、HKLM\Software\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers に登録されているすべての資格情報プロバイダが列挙されます。各プロバイダ DLL が読み込まれ、Provider::CreateInstance の呼び出しを受け取ります。ハイブリッド資格情報プロバイダの場合、この呼び出しの結果 CHybridProvider が作成されます (図 5 の手順 1 ～ 4 を参照してください)。

以上で、ログオン画面が表示されます。ユーザーが Ctrl + Alt + Del キーを押すと、各資格情報プロバイダは Provider::SetUsageScenario の CPUS_LOGON 通知を受け取ります。この通知により、ユーザーが対話型ログオンを要求していることが資格情報プロバイダに通知されます。これを受けて、挿入されているスマート カードから資格情報が読み取られます。資格情報が見つかった場合、CHybridCredential がインスタンス化され、現在の CHybridProvider に関連付けられます。その後、Credential::Initialize が呼び出されます (図 5 の手順 5 ～ 7 を参照してください)。

次に、LogonUI は読み込まれている各資格情報プロバイダごとに Provider::Advise を呼び出します。Advise の目的は、資格情報プロバイダが UI の可視要素 (まだ 1 つも存在していませんが) に求められる変更を非同期に LogonUI に通知できるしくみを提供することです。そのしくみの良い使用例が、組み込みのスマート カード プロバイダです。初期化の後、カードを挿入すると利用可能な資格情報が増加し、カードを抜き取ると利用可能な資格情報が減少します。カードの抜き差しがあると、このしくみにより LogonUI に通知が行われます。

ICredentialProviderEvents : public IUnknown
{
    HRESULT STDMETHODCALLTYPE CredentialsChanged(
       /* [in] */ UINT_PTR upAdviseContext);
};

単純化のため、ハイブリッド資格情報プロバイダはカードの抜き差しを動的に処理するようになっていません。したがって、Advise を介して渡された ICredentialProviderEvents インターフェイスの追跡も行いません。

次に LogonUI が行うインターフェイス呼び出しは、図 5 の手順 9 にある Provider::GetCredentialCount への呼び出しです。(スマート カードを挿入したことで) ハイブリッド資格情報が作成されると、ハイブリッド資格情報プロバイダがいくつかの操作を行います。まず、GetCredentialCount *pdwCount 出力パラメータを 1 に設定します。このパラメータは、資格情報プロバイダにより列挙される資格情報タイルの枚数を表します (ハイブリッド資格情報プロバイダはタイルを 1 つしか処理できません)。初めて Windows Vista をインストールしてドメインに参加させるとき、レンダリングされたタイルの枚数を基に、マイクロソフトのパスワード資格情報プロバイダが LogonUI に返した pdwCount 値を推論できます。

次に、GetCredentialCount *pdwDefault 出力パラメータを 0 に設定します。この値は、各資格情報プロバイダが保持している資格情報の配列で使用するインデックスです。インデックスは 0 から始まります。インデックスには、指定された一連の資格情報オブジェクトの有効期間が保持されていればよく、各資格情報プロバイダで資格情報を追跡する実際の実装は、実装者によって異なります。

複数のプロバイダで既定の資格情報を列挙することも可能です。たとえば、現在のシナリオで、組み込みのパスワード資格情報プロバイダが独自の既定の資格情報を列挙する場合があります。既定と既定でない資格情報が複数ある中から、ユーザーが混乱なく選択できるようにするにはどうすればよいでしょうか。通常は、資格情報ごとにタイルを表示し、既定の資格情報を表すタイルにフォーカスを設定しておきます。既定の資格情報が複数ある場合、ぞれぞれの資格情報を列挙するための一連の優先順位付け規則によって、正しい既定の資格情報が選択されます。各資格情報について、自動ログオンを伴わない既定の資格情報が既に存在し、その資格情報によって自動ログオンが行われる場合、その資格情報が既定の資格情報になります。資格情報が LLO (last-logged-on) プロバイダのもので、自動ログオンを伴う既定の資格情報が存在していない場合、その資格情報が既定の資格情報になります。最後に、まだ既定の資格情報が存在しない場合、その資格情報が既定の資格情報になります。まとめると、ハイブリッド資格情報プロバイダの自動ログオン セマンティクスにより、この決定が行われます。列挙されたハイブリッド資格情報に有効なログオン情報が含まれていれば、タイルは表示されません。これについて説明を続けます。

優先順位付け規則に注目して LLO プロバイダを説明しましたが、ユーザーがログインしている状況であるか、ログオン後にデスクトップ ロックやパスワード変更などを行った状況であるかによって、LLO の意味が変わることも指摘しておきます。ログオン時の LLO プロバイダは、コンソールに前回ログオンしたときに使用したプロバイダです。ログオン後の LLO プロバイダは、そのセッションへのログオンのみに使用したプロバイダです。考え方は次のとおりです。常にスマート カードでログオンする場合、スマート カード資格情報プロバイダの既定のタイルが再起動後も優先されます。しかし、スマート カードを使用せず、パスワードでログオンした場合、そのセッションの間は、ロックを解除するときにパスワード資格情報プロバイダのタイルが有効になります。

このハイブリッド資格情報プロバイダは常に *pbAutoLogonWithDefault 出力パラメータを TRUE に設定します。この設定が行われていると、LogonUI には、まずユーザーに問い合わせるのではなく、先にこのプロバイダの既定の資格情報にログイン情報があるかどうかを問い合わせるように通知されます。組み込みのパスワード資格情報プロバイダでも、パスワード自動ログオン情報をレジストリに保存する (オプション) ことで同じ機能を実現できます。実は、この動作はコンピュータ上でパスワードを持たないユーザーが 1 人しかいないことを Windows Vista が検出した場合の既定の動作です。複数の資格情報プロバイダが *pbAutoLogonWithDefault を TRUE に設定している場合の LogonUI の動作は定義できません。

GetCredentialCount の後、LogonUI は Provider::GetCredentialAt を呼び出します。ハイブリッド資格情報プロバイダを使用する場合、この資格情報プロバイダの資格情報の最大数を反映し、このルーチンは最大 1 回しか呼び出されません。資格情報プロバイダは呼び出しを受けて、要求されたインデックスに対応する資格情報インスタンスを示す ICredentialProviderCredential ポインタを返します。

次に、LogonUI が、資格情報で見つかる UI 要素の最大個数を受け取るための Provider::GetFieldDescriptorCount を呼び出します。たとえば、サンプルのパスワード資格情報プロバイダには、ビットマップ、ユーザー名入力フィールド、パスワード入力フィールド、送信ボタン、およびドメイン名入力フィールドの 5 つのフィールドがあります。ハイブリッド資格情報プロバイダには、実際にレンダリングされることがなくてもこれらと同じ UI 要素があります。以上で図 5 の手順 11 が完了です。

LogonUI は続けて、UI 要素ごとに 1 回ずつ Provider::GetFieldDescriptorAt を呼び出して、要素の種類を取得します。たとえば、ビットマップのインデックスに対応する呼び出しを受けて、CREDENTIAL_PROVIDER_FIELD_TYPE の CPFT_TILE_IMAGE が返されます。ハイブリッド資格情報プロバイダでは、入力可能なテキスト フィールド、および読み取り専用のテキスト フィールドを使用していません。スマート カードの PIN の入力を要求するようにハイブリッド資格情報プロバイダを変更する場合、CPFT_PASSWORD_TEXT を使用します。スマート カードから読み取ったユーザー名をプロンプトに表示することができます。ただし、技術的に言えば、ユーザー名は同じカードに保存されているパスワードと対応するので、読み取り専用にすることをお勧めします。それには、(CPFT_EDIT_TEXT ではなく) CPFT_LARGE_TEXT フィールド型を使用します (すべてのオプションの一覧については、credentialprovider.h を参照してください)。

フィールド記述子を列挙した後、各資格情報フィールドの種類に基づき資格情報プロバイダの呼び出しシーケンスが作成されます。たとえば CPFT_TILE_IMAGE フィールド型の場合は、Credential::GetBitmapValue を呼び出します。ユーザー名編集ボックスに使用する CPFT_LARGE_TEXT などのテキスト値の場合は、後で Credential::GetStringValue および Credential::GetFieldState を呼び出します。

ハイブリッド資格情報プロバイダに必要なすべてのログオン情報 (ユーザー名、パスワード、およびドメイン名) を既にスマート カードから読み取ったので、各テキスト フィールドに対応する文字列は使用可能であり、GetStringValue の ppwz 出力パラメータにより返されます。この時点で、ユーザーは入力を行っていないので、他の資格情報プロバイダは GetStringValue を受けて NULL 文字列値を返す可能性があります。なお、テキスト フィールドの名前を GetFieldDescriptorAt で取得し、フィールド内の現在のテキスト値を GetStringValue で取得する点が混乱を招くので気を付けてください (フィールドの名前またはラベルは、空の編集コントロールのキュー テキストとして表示されます)。

さまざまな UI 要素の記述がすべて終わった後、LogonUI は Credential::Advise を呼び出します (図 5 の手順 26 参照)。これは、前に呼び出した Provider::Advise インターフェイスと同様の目的を果たします。各資格情報が UI 要素の状態に影響する変更を LogonUI に非同期に通知できます。たとえば、いずれかの資格情報タイルの選択が解除されたときに、このしくみがサンプルのパスワード資格情報プロバイダで使用されます。その際、資格情報オブジェクトにより ICredentialProviderCredentialEvents の SetFieldString (図 6 参照) が使用され、パスワード フィールドが消去されます。これは、ユーザーが Windows XP のログオン画面でパスワードの一部のみを入力した後何もせずにいた場合に起こる動作に似ています。最終的にログオン ダイアログはタイムアウトして、テキストが消去されます。

Figure 6 ICredentialProviderEvents インターフェイス (Close Figure 6)
ICredentialProviderCredentialEvents : public IUnknown
{
    HRESULT STDMETHODCALLTYPE SetFieldState(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ CREDENTIAL_PROVIDER_FIELD_STATE cpfs);

    HRESULT STDMETHODCALLTYPE SetFieldInteractiveState(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ CREDENTIAL_PROVIDER_FIELD_INTERACTIVE_STATE cpfis);

    HRESULT STDMETHODCALLTYPE SetFieldString(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [unique][string][in] */ LPCWSTR psz);

    HRESULT STDMETHODCALLTYPE SetFieldCheckbox(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ BOOL bChecked,
        /* [in] */ LPCWSTR pszLabel);

    HRESULT STDMETHODCALLTYPE SetFieldBitmap(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ HBITMAP hbmp);

    HRESULT STDMETHODCALLTYPE SetFieldComboBoxSelectedItem(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ DWORD dwSelectedItem);

    HRESULT STDMETHODCALLTYPE DeleteFieldComboBoxItem(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ DWORD dwItem);

    HRESULT STDMETHODCALLTYPE AppendFieldComboBoxItem(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [string][in] */ LPCWSTR pszItem);

    HRESULT STDMETHODCALLTYPE SetFieldSubmitButton(
        /* [in] */ ICredentialProviderCredential *pcpc,
        /* [in] */ DWORD dwFieldID,
        /* [in] */ DWORD dwAdjacentTo);

    HRESULT STDMETHODCALLTYPE OnCreatingWindow(
        /* [out] */ HWND *phwndOwner);
};

ユーザー認証の完了という点から見ると、次の呼び出しが一番の見どころです。GetCredentialCount の *pbAutoLogonWithDefault パラメータが TRUE に設定された結果、既定の資格情報にユーザーを認証するための情報が既に十分含まれていることが明確になりました (まだ、ユーザーの入力を取得するのはもちろん、UI 要素のレンダリングも行っていません)。ここで、Credential::GetSerialization ルーチンを呼び出して、ユーザー名とパスワード、およびドメイン名があればそれも含めて取得します。資格情報プロバイダは、3 つの項目を Kerberos が受け入れる形式にマーシャリングして、このルーチンに返す戻り値を準備します。シリアル化した資格情報を準備できたら、CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE 型の出力パラメータによって、不備のない資格情報を返すことを LogonUI に通知します。通知の種類は、値 CPGSR_RETURN_CREDENTIAL_FINISHED によって区別されます。繰り返しになりますが、サンプル コードで credentialprovider.h および GetSerialization の実装を参照してください。以上で図 5 の手順 27 が完了です。

GetSerialization の後、マーシャリングされた資格情報は LogonUI によって winlogon に渡され、さらに winlogon によって LogonUser が呼び出されローカル セキュリティ機関 (LSA) に渡されます。その前に、LogonUI は Credential::UnAdvise および Provider::UnAdvise を呼び出して、両者にそれぞれの Events インターフェイスの通知を受け入れられないことを通知します。ログオンを中断するので、UI の変更は無意味です (この後はデスクトップを表示するのが理想です)。その他のリソース

  • Credential Provider Samples (英語)
  • スマート ストレージ : マネージ コードおよび Windows Vista スマート カード API を介したデータの保護
  • Windows シンボル パッケージのダウンロード
  • Windows スマート カード API ドキュメント (英語)
  • カード モジュール API ドキュメント (英語)

winlogon が取得した LogonUser の結果は、LogonUI に戻され、その後 (GetSerialization のフォーカスを失っていない) 資格情報インスタンスに通知されます。ただし、LogonUser から返される状態コードを受け取る前に、資格情報インスタンスには UI 要素を変更するコールバック インターフェイスが再度渡されます (図 5 の手順 31 を参照してください)。

認証結果は Credential::ReportResult ルーチン経由で資格情報に返されます。資格情報プロバイダ (または資格情報オブジェクト) が認証結果を求め、この時点で UI の変更を開始する理由は何でしょうか。

ReportResult を使用するシナリオでは、認証の失敗が起こることがよくあります。その一例が、ユーザー パスワードの期限切れです。ユーザーのパスワードが有効期限に近づくと、ReportResult の ntsSubstatus パラメータから返される認証副状態によってそのことが示されます。それを受け、組み込みのパスワード資格情報プロバイダは、ユーザーにパスワードの変更を (オプションで) 要求します。このプロンプトには、パスワード変更ダイアログ自体はもちろん、さまざまな UI 要素が必要です。したがって、ICredentialProviderCredentialEvents インターフェイス ポインタを使用して、プロンプト用のフィールドに必要な変更が行われます。

認証の成功を受けた場合に資格情報プロバイダが取る動作にも、興味深いものがあります。たとえば、組み込みのスマート カード資格情報プロバイダは、成功通知を合図に、認証に使用したカードが取り出されたかどうかの監視を開始します。監視は、オプションである、セッションの取り出し時ロック ポリシーを適用するために行われます。資格情報処理シーケンスが完了すると、LogonUI は Credential::UnAdvise によって資格情報プロバイダに対し、ICredentialProviderCredentialEvents インターフェイス ポインタの参照をデクリメントするよう通知します。

ハイブリッド資格情報プロバイダ

資格情報プロバイダの新しいアーキテクチャと使用方法についての説明が終了したので、サンプル ハイブリッド資格情報プロバイダの設計を詳しく見ることにします。図 2 に示した、Windows Vista 対話型ログオン アーキテクチャの大まかなレイアウトを思い出してください。図 7 は、その図を拡張して、Windows スマート カード API スタックが含まれるようにし、新しい資格情報プロバイダに注目したものです。

図 7 の最大のポイントは、ハイブリッド資格情報プロバイダが直接的および間接的に Windows スマート カード API とインターフェイスで接続していることです。直接インターフェイスは SCardEstablishContext、SCardListReaders などの、スマート カードを検出するためのパブリック ルーチンを経由します。間接インターフェイスは、カード モジュール API を経由します。この API を使用することで、資格情報プロバイダが、カード固有の低レベルのコマンドを使用しなくても、スマート カードからユーザー資格情報ファイルを読み取ることができます。サンプルでは、スマート カード関連のロジックのほとんどを ScHelp.lib というヘルパ ライブラリで抽象化しています (後の「ハイブリッド実装」で詳しく説明します)。資格情報プロバイダ アーキテクチャの有能さと繊細さを、ハイブリッド資格情報プロバイダの自動ログオン動作に見ることができます。

図 7 ハイブリッド資格情報プロバイダ
図 7 ハイブリッド資格情報プロバイダ

まず、自動ログオンとは、既に見たとおり、ハイブリッド資格情報を取得できる場合に、プロンプトが一切表示されないことを示します。ユーザーが Ctrl + Alt + Del キーを押した瞬間に、自動的に認証が試みられます。

自動ログオン動作が実装されていると、ユーザーの混乱を招く場合があります。たとえば、組み込みのパスワード プロバイダは、ドメインに参加していなくてパスワード入力が不要なシナリオでも、常に既定でタイルをレンダリングしますが、それとは動作が異なるためです。

ハイブリッド資格情報プロバイダは、簡単にすべてのシナリオで最初にタイルのクリックをユーザーに要求するように変更できます。GetCredentialCount の現在の実装では *pbAutoLogonWithDefault が TRUE に設定されています。この設定を FALSE にすると、タイルが 1 枚以上表示されるようになります (他のプロバイダがタイルを使用しない自動ログオンを設定している場合、そちらが優先されます)。

ユーザーがタイルをクリックすると、LogonUI により資格情報プロバイダの ICredentialProviderCredential::SetSelected メソッドが呼び出されます。資格情報クラスがこれを受けて *pbAutoLogon を TRUE に設定すると、LogonUI により ICredentialProviderCredential::GetSerialization が呼び出されます。その後の認証の時点では最初に UI 要素の変更のレンダリングは行われません。言い換えると、認証が成功してから、次にユーザーのデスクトップが表示されます。

ハイブリッド実装

最小限の変更で、パスワード資格情報プロバイダをハイブリッド実装に変換できました。グラフィカルな比較ツール (windiff.exe など) で SDK の CSampleProvider.cpp と CHybridProvider.cpp を比較してみてください。同様に、CSampleCredential.cpp と CHybridProvider.cpp も比較してください。変更されたコード行の大半が、"Sample" を "Hybrid" にグローバルに置換したものです。

CHybridProvider.cpp に加えた実質的な変更は、SetUsageScenario の処理です。資格情報プロバイダは、この呼び出しを受けてスマート カードから資格情報を読み取ります。読み取りを行うのは、ScHelp ライブラリの ScHelpInit ルーチンです。このルーチンには、スマート カード ロジックの大半が抽象化されています。ScHelpInit は、スマート カード サブシステムに接続し、挿入されている最初のカードを検出します。カードが見つかった場合は資格情報を解析し、その中の文字列を返します。

CHybridCredential.cpp への主な変更は、挿入されたスマート カードの資格情報ファイルの一部として読み取られるドメイン名文字列 (オプション) を処理できるようにしたことです。GetSerialization では、カードから読み取ったドメイン名を、Kerberos に渡すシリアル化された認証データで使用します。ドメイン名を読み取らない場合、パブリックの GetComputerName の呼び出し結果が使用されます。

PropCert サンプルを ScHelp ライブラリに変換するための変更は、比較的大規模でした。このヘルパ ライブラリのいくつかの領域は、まったく価値がありません。

まず、メインとなる PropCert スレッド ルーチンが同期的に呼び出されています。個別のスレッドは使用されません。ただし、カード関連の処理は非同期に実行するべきです。同期から非同期へは比較的単純に変更できます。実は、複数の資格情報タイルから選択できるようにハイブリッド資格情報プロバイダを拡張する場合、この変更は重要です。一部の古いスマート カードは I/O 操作に時間がかかるので、このような場合に、スマート カードのデータをバックグラウンドで読み取りながら、タイルを速やかに並べることができます。この変更の実装を完了するには、ある種の通知メカニズムを設置して、資格情報の取得状況の変更をスマート カード スレッドが資格情報プロバイダに通知できるようにする必要があります。資格情報プロバイダは、同じ情報を CredentialsChanged で LogonUI に通知します。

ScHelp.lib の残りのロジックは、ルーチン _ReadCreds、_Connect、_UnpackCred (図 8 参照)、および ScHelpPackCred です。最後の 2 つは、スマート カードに保存されているパスワード資格情報ファイルのシリアル化およびその解除を行います。最初の 2 つは、既に簡単に説明した、スマート カード リーダーおよびカードを列挙し、列挙された最初のカードの読み取りロックを取得し、カードに対応するカード モジュールにバインドし、資格情報ファイルが見つかればそれをカードから読み取るロジックを実装します。

Figure 8 UnpackedCred 関数 (Close Figure 8)
// Break down the credential byte array
DWORD WINAPI UnpackCred(
    __in_bcount(cbCred)     PBYTE pbCred,
    __in                    DWORD cbCred,
    __out                   LPWSTR *ppwszUserName,
    __out                   LPWSTR *ppwszPassword,
    __out                   LPWSTR *ppwszDomainName)
{
    DWORD status = ERROR_SUCCESS;
    DWORD cbUserName = 0;
    DWORD cbPassword = 0;
    DWORD cbDomainName = 0;
    DWORD cbCurrent = 0;

    *ppwszUserName = NULL;
    *ppwszPassword = NULL;
    *ppwszDomainName = NULL;

    try
    {
        // Read the user name
        cbUserName = (DWORD) sizeof(WCHAR) * (1 + wcslen(
            (LPWSTR) (pbCred + cbCurrent)));
        if (cbUserName > cbCred - cbCurrent)
        {
            status = ERROR_INVALID_PARAMETER;
            leave;
        }

        if (NULL == (*ppwszUserName = (LPWSTR) Alloc(cbUserName)))
        {
            status = ERROR_NOT_ENOUGH_MEMORY;
            leave;
        }

        if (FAILED(StringCbCopy(
            *ppwszUserName, cbUserName, (LPWSTR) (pbCred + cbCurrent))))
        {
            status = ERROR_INSUFFICIENT_BUFFER;
            leave;
        }

        cbCurrent += cbUserName;

        // Read the password
        cbPassword = (DWORD) sizeof(WCHAR) * (1 + wcslen(
            (LPWSTR) (pbCred + cbCurrent)));
        if (cbPassword > cbCred - cbCurrent)
        {
            status = ERROR_INVALID_PARAMETER;
            leave;
        }

        if (NULL == (*ppwszPassword = (LPWSTR) Alloc(cbPassword)))
        {
            status = ERROR_NOT_ENOUGH_MEMORY;
            leave;
        }

        if (FAILED(StringCbCopy(
            *ppwszPassword, cbPassword, (LPWSTR) (pbCred + cbCurrent))))
        {
            status = ERROR_INSUFFICIENT_BUFFER;
            leave;
        }

        cbCurrent += cbPassword;

        // Read the domain name (if any)
        cbDomainName = (DWORD) sizeof(WCHAR) * (1 + wcslen(
            (LPWSTR) (pbCred + cbCurrent)));
        if (sizeof(WCHAR) == cbDomainName)leave;
        else if (cbDomainName > cbCred - cbCurrent)
        {
            status = ERROR_INVALID_PARAMETER;
            leave;
        }

        if (NULL == (*ppwszDomainName = (LPWSTR) Alloc(cbDomainName)))
        {
            status = ERROR_NOT_ENOUGH_MEMORY;
            leave;
        }

        if (FAILED(StringCbCopy(*ppwszDomainName, cbDomainName,
            (LPWSTR) (pbCred + cbCurrent))))
        {
            status = ERROR_INSUFFICIENT_BUFFER;
            leave;
        }
    }
    finally {}

    return status;
}
GINA の終焉

GINA が廃止された理由 この質問は、思ったより複雑です。マイクロソフトには、サードパーティの開発者をサポートしてきたという世間に恥じない実績があるので、パブリック インターフェイスのサポートを安易に廃止することはめったにありません。それでも、GINA の廃止には強い根拠があり、最終的に廃止が適切な決断と判断しました。

まず、Windows NT® の時代に比べて、多要素認証の要求が高まっています。今では、スマート カード、バイオメトリクス、およびワンタイム パスワードを使用したソリューションが企業で広く展開されています。各認証テクノロジが進化すると、コアの Windows 資格情報エンジンと、ユーザーにプロンプトを表示する GUI との間にある抽象レイヤに、大きな負担がかかります (たとえば、パスワードを入力するように指示するための画像による合図や描画領域は、指紋リーダーに指を置くように指示する合図や領域とは異なります)。同時に、ユーザーを混乱させないために、できる限り一貫した操作性を実現することがマイクロソフトには求められています。

また、Windows Vista で Windows ログオン プロセス (winlogon.exe) のアーキテクチャを完全に見直しました。その取り組みの中心的な目標は、プラグインをできる限り完全に winlogon プロセス空間の外に移動することでした。この目標は、信頼性を考慮したうえで生まれました。たとえば、サーバーのセッション 0 で実行される winlogon.exe に、記述に問題のある GINA が読み込まれてソフトウェア フォールトが発生した場合、この重要なプロセスが強制終了し、サーバー自体がダウンする可能性があります。GINA をアウト プロセスで実行するように変更できたとしても、対話的に資格情報を収集する複雑なシナリオで、統制の取れた一貫した操作性を提供する設計になっていないという問題が残ります。

改善の余地

このハイブリッド資格情報プロバイダの設計は、実装の中で相当なコードの再利用を実現すると同時に、最初に設定した要件を満たすと自負していますが、現状では実装の一部に展開の準備が整っていない部分があります。欠点の 1 つ、データを非同期にスマート カードから読み取るべきである点については既に説明しました。それ以外に改善の余地があると思われる点について、重大なものから順に説明します。

まず、ユーザーの資格情報が、セキュリティで保護された状態でスマート カードに保存されていません。ユーザーのパスワードを保存するカードベースのファイルは、正しい PIN が入力されて初めて読み取ることができるようになるのが理想的です。しかし、現在のカード モジュール インターフェイスの制限により、そのような実装は困難です。つまり、あらかじめ定義されているカード ファイル アクセス条件のセットに、"ユーザーのみ読み取り可" というオプションはありません (カード モジュールの設計にこの制限を設ける決定に関与した本人が、何の因果か、今その決定に苦しめられています)。今後のバージョンで、製品チームがカード モジュール インターフェイスを拡張すると考えています。

それまでは、スマート カードのパスワード ファイルを暗号化しておき、カードが盗まれても PIN を入力しないとパスワードを解読できないようにすることをお勧めします。暗号化には、Crypto API だけでなく、新しい Windows Vista の CNG ("次世代の" Crypto API) も便利です。RSA キーの組をスマート カードに作成して保存するだけでも十分ですが、RSA 公開キーでパスワード ファイルを直接暗号化するのではなく、対称キーと Advanced Encryption Standard (AES) などのアルゴリズムを使用することが、暗号化のベスト プラクティスです。RSA キーは、対称キーの暗号化に使用します。

暗号化したパスワード ファイルの設計を完了するため、対応する暗号化キーが含まれるように既存のパスワード ファイル形式を変更できます。その場合、暗号化アルゴリズムおよびキー サイズに依存することに伴うバージョン管理の問題を考慮してください。自分が選択したアルゴリズムはいつかハッキングされると考えてください。また、これまで説明した設計は、暗号によりセキュリティで保護されたデータ整合性チェックを含んでいません。攻撃者は理論上、カードの内容を書き換えるには PIN を知っている必要があるので、そのようなチェックがなくても大したことではないと思われるかもしれません。しかし、多層防御の点で、私ならそのような機能は必要だと考えます。

現在の実装の第 2 の制限は、カード 1 枚でサポートされる資格情報が 1 つに限られていることです。図 8 にある、サンプル コードの ScHelp.cpp!_UnpackCred ルーチンを見てください。このルーチンは、スマート カードから読み取ったと思われるパスワード ファイルのシリアル化を解除します。資格情報解析ロジック全体では、スマート カード 1 枚あたり 1 つの資格情報しか処理されません。しかし、複数のドメイン資格情報を使い分けて業務を行う必要があるユーザーもいます。資格情報の使い分けをサポートするように資格情報プロバイダを拡張する方法と、ユーザーに複数のカードを発行する方法のどちらを選択しますか。前者は実装が複雑になり、後者は展開管理が複雑になります。

余談ですが、_UnpackCred を使用して、セキュリティで保護されたバッファ解析技術のデモンストレーションを試みました (再度図 8 を参照してください)。攻撃者がスマート カードを偽造し、ネットワークのワークステーションに挿入するとします。アプリケーション レベルでこの脅威を緩和する主な方法は、スマート カードから読み取るデータの有効性に対して、一切の前提がないものとしてコードを記述することです。埋め込まれた文字数が正しいことや、文字列の形式が適切であることなどを前提としないでください。すべての文字列の最初にある要素カウントが資格情報の未解析部分の長さを超えないこと、および実際の文字列長が文字列に割り当てられているバッファを超えないことを単に確認するだけにします。

現在のハイブリッド資格情報プロバイダについて最後に説明する制限は、1 台のコンピュータでサポートされるスマート カード リーダーが 1 台のみということです。たとえば、それぞれ異なる資格情報で初期化したスマート カードを挿入した 2 台のスマート カード リーダーを装着してシステムを起動した場合、どちらのスマート カードが優先されるかは、スマート カード サブシステムにカード リーダーが列挙された順により変わります。修正には、ScHelp.h で定義されている SCHELP_CONTEXT 構造体のセマンティクスを次のように変更する必要があります。

typedef struct _SCHELP_CONTEXT
{
    LPWSTR wszUserName;
    LPWSTR wszPassword;
    LPWSTR wszDomainName;
} SCHELP_CONTEXT, *PSCHELP_CONTEXT;

SCHELP_CONTEXT 構造体には、資格情報プロバイダ コードと ScHelp コードのデータ交換が定義されています。この構造体が 1 つの資格情報しかサポートしないことは明らかです。単純配列および 1 つにリンクした一覧を導入すると、この構造体を強化できます。機能強化を行う場合、現在、プロバイダ インスタンス 1 つにつき 1 つの資格情報しかサポートされないようにハードコードされている CSampleProvider の _rgpCredentials メンバの処理も忘れずに変更します。

テストおよびデバッグ

今回の資格情報プロバイダのテストは、既に書いたとおり、それほど困難ではありませんでした。テスト方針では、サンプル コードのライブ デバッグ、および前に説明したトレース情報の生成の柔軟性を最大に高めるため、LogonUI にユーザーモードのデバッガを追加できるようにするつもりでした。また、LogonUI がシステムとして実行され、テスト対象にしている対話型ログオン シナリオにはセキュリティで保護されたデスクトップからしかアクセスできないので、資格情報プロバイダのさまざまな COM インターフェイスを使用して、簡単な自己完結型のテスト プログラムを記述すれば時間の節約になることもわかっていました。ですが、私の技術的なバックグラウンドが原因で、テスト プログラムを記述するのは断念し、ライブ デバッグを進めました。ただし、この方法はお勧めしません。

テスト プログラムを記述しなかったので、堅牢なデバッグ環境をセットアップしなければならないというプレッシャーがありました。カーネルモード開発を頻繁に行う方以外にとって、カーネル デバッガベースのテスト環境を適切に構成する作業は思いどおりにいかないものです。そのために推奨する方法を大まかに示します。

まず、標準のカーネル デバッグ構成で 2 台のコンピュータをセットアップします。1 台は信頼できる開発システム (デバッガ)、もう 1 台は Windows Vista テスト システム (デバッグ対象) にします。2 台のコンピュータをシリアル ケーブルで接続します。

デバッグ対象には、Windows XP を搭載した安全なブート パーティションを忘れずに構成します。構成 (特に、2 台のコンピュータをシリアル ケーブルで相互に通信させる) には、多少の試行錯誤が必要です。シリアル接続をテストする最善の方法は、2 台のコンピュータの Windows XP を起動し、ハイパーターミナルを実行します ([すべてのプログラム]、[アクセサリ]、[通信]、[ハイパーターミナル] を順に選択します)。2 台のコンピュータで、使用しているシリアル ポートを指定し、デバッガとのデータ通信速度を選択します。一方のコンピュータのハイパーターミナル ウィンドウに入力した文字が、もう一方のコンピュータにエコーされたら完了です。エコーされない場合、別のシリアル ポート、接続速度、またはケーブルを試します。

安全なブート パーティションを構成する 2 番目の理由を説明するため、話をいったん先に進めます。テスト中の資格情報プロバイダが原因でホスト LogonUI プロセスが強制終了またはデッドロックした場合、Windows Vista にログインできなくなります。私の場合、HybridCredProv.dll の最初のビルド構成でこの現象が発生しました。再配布可能な msvcr80.dll をランタイム ライブラリとして使用していました。このバイナリをデバッグ対象の system32 ディレクトリにコピーし忘れたのが最初の間違いです。しかしその影響は、資格情報プロバイダを読み込めなかっただけです。

次に Windows Vista を起動しても、問題は収拾しませんでした。このとき、LogonUI が起動して、資格情報プロバイダが読み込まれるところまで確認しましたが、UI が表示されませんでした。デバッガで、msvcr80.dll のスタートアップ コードが、他のスレッドとプロセス ローダー ロックのデッドロックを起こしていました。これを繰り返さないよう、静的にリンクしたランタイム ライブラリが使用されるように HybridCredProv.dll のビルド構成を変更しました。

要するに、安全なブート パーティションを用意しておくことで、資格情報プロバイダをテストするときに、構成の問題を簡単に修正できます。

デバッグ構成に話を戻します。次は、マイクロソフトから (ntsd.exe および i386kd.exe が含まれている) 最新のデバッガ パッケージを入手して、2 台のコンピュータにインストールします。Windows Vista のパブリック デバッグ シンボルをインストールします (シンボルをローカルに完全にコピーしたものに対してデバッグを行うことをお勧めします)。この手順を不要と考える読者もいるでしょうが、ライブ デバッグの途中でシンボルがないためにデータが不足すると困ります。自分の経験を基に、以下にアドバイスを記載します。

  • クリーンなスタック トレースを常に取得することが重要です。
  • 同様のスタック トレースを取得するために、オペレーティング システムの (ntdll.pdb などの) シンボル ファイルが必要になる場合があります。
  • システムレベルのデバッグ シナリオで、ネットワーク アクセスができない予期しない状況になることがあるので、その場合ローカル シンボルを使用する必要があります。

システム レジストリの Image File Execution Options を使用して、LogonUI.exe がユーザーモード デバッガ (ntsd.exe) 内で起動されるように構成します。ユーザーモード デバッガの出力は、カーネル デバッガにリダイレクトされます。カーネル デバッガが必要な理由は、セキュリティで保護されたデスクトップからしかアプリケーションを表示できないので、テスト コンピュータのコンソールからアプリケーションおよび付属のデバッガと対話処理を行うための信頼できる方法が他にないためです。

シンボルを ntsd.exe に読み込むパスを指定する方法は、-y コマンドライン オプションと、_NT_SYMBOL_PATH 環境変数の 2 つがあります。-y コマンドライン オプションは、Image File Execution Options を構成する場合にお勧めします。ただし私は、テスト コンピュータのシステム全体に対して一度の設定で済む _NT_SYMBOL_PATH 環境変数が気に入っています。

最後に、資格情報プロバイダが使用不能になって、コンピュータへのアクセスを復旧する必要がある場合、セーフ モードで起動すると Windows Vista がパスワード プロバイダのみを読み込み、[セーフ モードとネットワーク] で起動するとスマート カード資格情報プロバイダも読み込みます (セキュリティに配慮してこのフォールバック動作を無効にするポリシーがあります)。セーフ モードで起動したら、レジストリを編集できます。

スマート カードおよび初期化

ハイブリッド資格情報プロバイダのテストを意味のあるものにするには、もちろん資格情報が必要です。つまり、ユーザー名、パスワード、およびオプションとしてドメイン名が記載された適切な形式の資格情報ファイルが含まれたスマート カードを準備する必要があります。ダウンロードできるサンプル コードには、そのための WriteCred.exe というテスト ユーティリティが付属しています。既定のカード リーダーに挿入されたスマート カードを自分の資格情報で初期化するには、次のコマンドライン オプションを使用します。

WriteCred.exe -p <PIN> -u <UserName> -d <DomainName> -w <PassWord>

ハイブリッド資格情報プロバイダの動作とは異なり、WriteCred の現在の実装はドメイン名パラメータを必須としています。また、パスワード パラメータはユーザー名に対応する Windows ログオン パスワードです。PIN パラメータは、スマート カードに資格情報データ ファイルを書き込むために必要です。

最後に、テストに関して、WriteCred ツールおよびハイブリッド資格情報プロバイダ自体は、Microsoft Base Smart Card Crypto Provider と互換性のあるスマート カードのみをサポートします。互換性のあるスマート カードの一覧の最新情報については、"カード モジュール スマート カード" を Web 検索してください。

Dan Griffin は、ワシントン州シアトル在住のソフトウェア セキュリティ コンサルタントです。Dan はかつてマイクロソフトの Windows セキュリティ開発チームに 7 年間在籍していました。Dan の連絡先は www.jwsecure.com (英語) です。この記事についてフィードバックを寄せていただいたマイクロソフトの Brian McNeill と Eric Perlin に感謝の意を表します。


現在の Windows: Unicode 5.0 を使用したアプリケーションの世界的勢力の拡張

現在の Windows

Unicode 5.0 を使用したアプリケーションの世界的勢力の拡張

Julie D. Allen and Michael S. Kaplan and Cathy Wissink
この記事の内容 : :
  • OS での Unicode のサポートの重要性
  • Unicode 5.0 の新機能
  • Windows Vista での Unicode 5.0 のサポート
  • Unicode のセキュリティに関する考慮事項
この記事は次のテクノロジを使用しています:
Unicode 5.0、Windows Vista
コンテンツ
Unicode が重要な理由
Unicode 5.0 の新機能
変化の理由
まとめ

主要なソフトウェア ベンダは、ローカライズまたは翻訳した多くの製品に加えて、ラテン語以外の書記体系を処理できる製品も提供しています。

しかし、まだまだ道のりは遠く、ソフトウェアの国際化の最終的な目標は、本当に多くの人々が、言語、書記体系、場所に関係なく、お互いにやり取りできるようにすることです。簡潔に言うと、人々は自分の言語で自分の言葉を使って相手とやり取りしたいのです。

マイクロソフトを始めとする企業は、Unicode Consortium のメンバとして、この業界が言語と地域を包括的にサポートするという目標にさらに近づくように支援しています。この記事では、この特定の標準に対する取り組みの中でマイクロソフトが担う役割について説明します。また、さらに重要なこととして、Unicode 5.0 の新機能とそれらの新機能を Windows Vista™ に実装する方法について説明します。

Unicode が重要な理由

マイクロソフトは多くの理由から Unicode Consortium に積極的に参加してきました。まず、書記体系、場所、プラットフォームとは関係なく、データの整合性、安定性、および相互運用性を確保することが、グローバルなソフトウェア開発の基本理念に含まれています。この業界では、実行するプラットフォームに関係なく一貫して動作する国際化標準、それも特にエンコード用のものが必要です。このような標準は、サポート対象の言語と文化の主要関係者を代表するものである必要もあります。Unicode Consortium では、主要関係者から情報を得ながら標準を開発しています。主要関係者には、業界、政府機関、言語機関、および関連する標準化団体が挙げられます。世界中のデータを格納、転送、操作する世界規模のシステムが 1 つ存在することにより、国際的なソフトウェアの開発や展開が合理化し、大幅に加速します。

Unicode がマイクロソフトにもたらすメリットが明らかになったのは、Unicode をサポートするシングル ソースのコードベースを採用したことによって開発時間が減少したときでした。Windows® 2000 では、英語の製品が出荷されてからローカライズされたバージョンを用意できるまでに数か月かかっていましたが、Windows XP では数週間短い時間で用意できました。

Unicode Consortium は、世界中の言語と文化をテクノロジで表現できることを保証する点でも重要な団体であり、特に Script Encoding Initiative (unicode.org/pending/about-sei.html (英語) 参照) と協力して活動を行っています。このイニシアチブは、マイクロソフトを始めとするソフトウェア ベンダにより、各ベンダの新しいさまざまな市場プログラムを使用してサポートされています。マイクロソフトの成果には、Language Interface Packs (LIPs) プログラムおよび Enabling Language Kits (ELKs) プログラム、また Microsoft® Locale Builder や Microsoft Keyboard Layout Creator などの開発ツールがあります。実際の Unicode をざっと確認するには、「ELKs および LIPs」、および図 1 と 図 2 を参照してください。

図 1 イヌクティトット語の文字
図 1 イヌクティトット語の文字(画像を縮小するには、ここをクリックします)
図 1 イヌクティトット語の文字
図 1 イヌクティトット語の文字 (画像を拡大するには、ここをクリックします)
図 2 マラヤーラム語の文字
図 2 マラヤーラム語の文字(画像を縮小するには、ここをクリックします)
図 2 マラヤーラム語の文字
図 2 マラヤーラム語の文字 (画像を拡大するには、ここをクリックします)

Unicode 5.0 の新機能

これまでの主要なバージョンと同様、2006 年に Addison-Wesley 社から『The Unicode Standard, 5.0』 (英語) という書籍が関連するデータ ファイルと共に出版されました。この書籍は大幅に書き換えられていて、ページには新しく明確な説明、新しく改善された表や図解などが含まれています。この書籍には、初めて 『Unicode Standard Annexes』が収録されており、双方向アルゴリズム、識別子、改行のプロパティ、テキストの境界などのトピックに関する情報が含まれています。この別冊は、開発者や実装者に改善されたガイダンスを提供するために入念に編集され書き直されました。

書籍の中の一致する節と定義の番号が Version 5.0 に付け直され、Version 4.0 で使用されていた C12b や D30e などの番号による混乱が取り除かれました (付録 D には、以前のバージョンで使用されていた番号を示す便利な表が含まれています)。

Unicode 5.0 には、インド語のスクリプトを表示するためのより明確なガイドラインが含まれています。CJK 文字については、現在の書籍に、IICore radical-stroke index が含まれており、東アジア市場にとって最も重要な約 10,000 個の CJK 文字のサブセットに焦点を当てています。この新しいインデックスにより、標準の重要な文字の検索が非常に容易になります。珍しい文字を検索するときには、完全な CJK radical-stroke index をオンラインで利用できます。文字のプロパティが拡張され向上したのです。

セキュリティに関する考慮事項と信頼性に関する情報 (実装者にとって非常に重要なトピック) が大幅に増えています。Version 5.0 は Unicode Collation Algorithm、Common Locale Data Repository、International Standard ISO/IEC 10646 などの他の重要な標準に対して調整された更新内容と同期を取ってあります (「フィッシングと Unicode のセキュリティ」を参照してください)。フィッシングと Unicode のセキュリティ

国際ドメイン名に関連する RFC は、ドメイン名に使用されている Unicode のすべての文字で生じる可能性がある視覚的なスプーフィングに関連する潜在的な問題を指摘することに大きな労力を使いましたが、最も早期の導入者はそのような警告を気に留めていませんでした。 Unicode 文字の視覚的なスプーフィングは、さまざまな IETF ドキュメントでずっと昔から潜在的なセキュリティ リークとして叫ばれていましたが、このスプーフィングを使用して、ソフトウェア コミュニティから注目を浴びました。

この種のスプーフィングは、空の Web サイトの名前が文字の 1 で、その名前がスプーフィング実行者によって数字の 1 に置き換えられた場合などに発生します。文字は同じように見えるので、一時的なユーザーだったら自分が犯人の Web サイトに空のユーザー名とパスワードを入力しているとは思いもよらないでしょう。URL に使用できる文字に、同じに見えても他の言語の文字とは異なる多くの言語や書記体系が含まれている場合、URL をスプーフィングする機会が飛躍的に増加します。

マイクロソフトの Michel Suignard が記述した「Unicode Technical Report #36, Unicode Security Considerations」 (unicode.org/reports/tr36) (英語) および「Unicode Technical Standard #39, Unicode Security Mechanisms」 (unicode.org/reports/tr39) (英語) の両方に、Unicode をサポートしているアプリケーションまたはプラットフォームでこのような問題を回避できる方法についての説明があります。

Version 5.0 では、キリル語、ギリシャ語、ヘブライ語、カンナダ語、ラテン語、数学、音声記号拡張、記号といった新しい文字が追加されています。また、ティフィナグ文字やフェニキア文字など、少数の歴史的スクリプトもいくつか追加されています。ティフィナグ文字のスクリプトは、通称ベルベル語やアマジグ語と呼ばれるさまざまな言語を話す、モロッコなどに在住の 2 千万人を超える人々によって使用されます (ついでに言うと、"ティフナグ" という語はフェニキア文字を意味します)。フェニキア語は古代地中海を渡ったスクリプトであり、現代的な用法のアラビア語、ギリシャ語、ヘブライ語などのスクリプトの前身です。書籍『Unicode Standard, Version 5.0』はスクリプトについて興味をそそる詳細だけでなく、役立つ実装情報の宝庫です。これは不可欠な参考文献です。図 3 と 4 では別の文字セットをもう少し示します。

図 3 クメール語の文字
図 3 クメール語の文字(画像を縮小するには、ここをクリックします)
図 3 クメール語の文字
図 3 クメール語の文字 (画像を拡大するには、ここをクリックします)
図 4 ラオ語の文字
図 4 ラオ語の文字(画像を縮小するには、ここをクリックします)
図 4 ラオ語の文字
図 4 ラオ語の文字 (画像を拡大するには、ここをクリックします)

変化の理由

前のバージョンの Windows では、マイクロソフト製品は Unicode の数バージョン分ほど遅れを取っていました。たとえば、Windows XP には、多くの Unicode バージョン (Unicode 1.1 ～ 3.1) から得たデータが同梱されていました。そのため、どの機能がデータを利用していた場合でも、その機能が照合順序、文字のプロパティ、改行、双方向アルゴリズム、UTF-8 変換のどれなのかに応じて、必要な Unicode バージョンを利用できました。この混合アプローチは、開発サイクルの短縮に対応する手段である一方で、次のようなさまざまな問題を引き起こしました。

  • UTF-8 変換などの操作に対するアルゴリズム サポートが不十分である。
  • 後にビジネス ニーズとなるサポート対象言語の範囲が不十分である。
  • セキュリティの影響を受けやすい問題への対応が不足していることが後のバージョンで明らかになった。

このような問題がより一層明らかになったのは、マイクロソフトが Windows XP Service Pack 2 (SP2) から、LIPs をサポートするために ELKs を同梱し始めたときです。利用可能な ELKs および LIPs の一覧に含まれている言語でも検討の対象から除外する必要のある言語もありました。たとえばモンゴル語やイ語などの言語には、Unicode のプロパティ、文字種、および照合順序の中核となるサポートが存在しませんでした。さらに、マイクロソフトでは、Unicode のサポート範囲を、企業にとって検討対象である戦略的なビジネス ケースのみに限定しようとすると、多くの場合は効果がないことを身をもって知りました。これは、昨日の弱気のビジネス ケースが今日の戦略上の権益となることが多々あるからです。

その上、Windows と Microsoft .NET Framework を完全に最新バージョンの Unicode 標準に更新すると、マイクロソフト製品、標準自体、および関連する言語コミュニティの問題において期待していた国際的なサポートという点で一部のギャップが埋まります。

Windows Vista での Unicode 5.0 サポートに対する顧客の要求は、ISV が ISV 自身では拡張できなかった、文字種、プロパティ、文字型の値、既定の照合重み、標準化のサポートなどのコア サービスにまで及びます (私たちは、Unicode 5.0 の各文字のフォントと表示に関するサポートを出荷しようとしませんでした。特に Unicode と Windows 間でサポート対象が異なることを考えると、驚くほどに時間がかかる難しい作業になると考えたからです。目標は言語をサポートすることであり、前述のコア操作に関しては言語をサポートすることができました。フォント作成ツールや Microsoft Keyboard Layout Creator のようなツールへの文書体裁やキーボードのサポートなど、その他の拡張可能な詳細は残っていますが)。

Windows と .NET Framework のバージョン サポートが Unicode 1.1 ～ Unicode 3.1 くらいを対象にしているとすると、Unicode 5.0 の更新内容を説明することは非常に複雑です。新しい言語とスクリプトのサポートは標準への更新という点では人々にとって理解しやすい部分だと思いますが、新しいバージョンの標準には、Unicode サポートの点で顧客の要求に応じた必須機能が多数追加されています。特に Windows については、次の Unicode 関連データが更新され、追加の文字が含まれています。

  • Unicode のセキュリティ機能が向上。
  • 正規化と国際ドメイン名の更新。
  • 単純な文字種や他の一般的な操作を最新バージョンでサポート。
  • 多くの新しい文字が追加されたために Japanese/JIS X 213 と Hong Kong /HKSCS の両方に必要な、Windows XP に追加された Extension B の単純なコード ポイント順を超える照合順序のサポート。この特定の項目は、マイクロソフトが照合順序に取り組んだ結果の機能であって、Unicode の機能ではありません。ここでは、Unicode Collation Algorithm の例に従って、最新バージョンの Unicode の文字を既定の表に追加します。
  • Unicode での数学のサポート。
  • 南アジアと東南アジアのスクリプトの強力なモデルが、近年の実装者の入力により向上しました。
  • 標準によって提供されるデータとプロパティの優れた検証により、Unicode 全体をより安定した一貫性のあるものにする標準に準拠したモデルに対する多くの作業。

言語と文化のサポートの拡張だけでなく Unicode の一貫性のある実装が、顧客からの Windows Vista に対する重要な要求だったので、プラットフォーム全体に Unicode 標準の最新バージョンの統合を提供する、国際的な標準の戦略や実装プランを開発することが不可欠でした。

マイクロソフトは Unicode 5.0 を早期に導入した企業の 1 つです。一方、業界の他の企業もこのリリース バージョンを既に使用しているか、近い将来にアップグレードすることを計画しています。この最新バージョンの標準を採用すると、開発者、ISV、エンド ユーザーなど、さまざまな人々がメリットを得られます。マイクロソフト製品に有益な新しいすべての Unicode 機能は、業界の他社製品にとっても同じくらい重要です。技術上の明白なメリットの中には、互換性のある UTF-8 変換を利用するソフトウェアや、フィッシング対策機能を加えたブラウザなどがあります。ELKs および LIPs

Enabling Language Kits (ELKs) および Language Interface Packs (LIPs) は、Windows で新しい言語に対応できるように時間をかけてきた結果として開発されたものです。

ELKs では、新しいロケール情報、新しいキーボード、新しい並べ替え、新しいフォント、新しいシェイプ エンジンなど、ロケールでサポートする必要のあるすべてのコンポーネントをロケールごとにインストールすることができます。また、この作業のすべてが、主要なシステム リリースを待たずに行われます。

LIPs にはそれとは大きく異なる目標があります。LIPs はローカライズ フレームワークを表し、Windows を別の言語にローカライズするのに必要なコストと時間を削減できます。各 LIP は頻出文字列を翻訳する簡易ローカライズで、全体のうち 20 パーセントの頻出文字列をローカライズしておけば、ユーザーから見ると 80 パーセントが翻訳された状態になる、80/20 ルールを適用しています。

まとめ

マイクロソフトにとって、Unicode 5.0 にアップグレードするビジネス ケースは明白で説得力のあるものでした。Unicode の取り組みをサポートすることに関心がある他の組織は、Unicode Consortium に参加することや、重要なプロジェクトに積極的に関与することに対する多くのメリットを検討する必要があります。Unicode 5.0 にアップグレードすると、言語のさらに包括的な文字セットが提供されますが、もっと重要なことは、ASCII の世界から離れたことのない人々を含め、世界中の人々にとってインターネットの安全性と機能性が高まるということでしょう。

Julie D. Allen は Unicode Consortium でシニア エディタおよびプロジェクト マネージャとして働いています。彼女は、バージョン 2.1 以降の Unicode 標準に携わってきました。また、ワシントン大学でゲルマン語学の博士号を取得し、英文学科の修士課程でライティング スキルを磨きました。

Michael S. Kaplan は、マイクロソフトでテクニカル リードとして働いており、照合順序、キーボード、ロケールを中心に扱っています。彼は、Windows 9x での Microsoft Layer for Unicode と、Microsoft Keyboard Layout Creator の両方の開発の第一人者でした。また、2000 年に Sams Publishing から出版された『Internationalization with Visual Basic』 (英語) の著者でもあります。彼のブログ (blogs.msdn.com/michkap) (英語) で、国際化についての新しい投稿をご覧いただけます。

Cathy Wissink はマイクロソフトのグループ プログラム マネージャで、Global Platform and Technology Services グループに所属しています。1991 年は Windows NT 3.1 チームでリンギストの見習いをしていました。多くの Windows プロジェクトに携わった後、2000 年にマイクロソフトに入社しました。彼女はワシントン大学でゲルマン語言語学の MA と PhC (Candidate of Philosophy Certificate) を取得し、シアトル大学の Adult Education and Training で MEd を取得しています。


リークが発生するアプリケーションのデバッグ: マネージ コードでのメモリ リークの識別と回避

リークが発生するアプリケーションのデバッグ

マネージ コードでのメモリ リークの識別と回避

James Kovacs
この記事の内容 : :
  • マネージ アプリケーションでのメモリ リークについて
  • .NET アプリケーションで使用されるアンマネージ メモリ
  • .NET ガベージ コレクタの処理を支援する
この記事は次のテクノロジを使用しています:
.NET Framework
コードのダウンロード : : MemoryLeaks2007_01.exe (163KB)
コンテンツ
.NET アプリケーションのメモリ
リークの発生をチェックする
スタック メモリでリークが発生している場合
アンマネージ ヒープ メモリでリークが発生している場合
マネージ ヒープ メモリでリークが発生している場合
まとめ

マネージ コードのメモリ リークについて多くの開発者が示す最初の反応は、「そんなはずはない」というものです。

最終的にはガベージ コレクタ (GC) ですべてのメモリ管理が行われると思っていませんか。しかし、ガベージ コレクタで処理されるのはマネージ メモリだけです。Microsoft® .NET Framework ベースのアプリケーションでは、多くの場所でアンマネージ メモリが使用されます。共通言語ランタイム (CLR) 自体が使用したり、アンマネージ コードとの相互運用時にプログラマが明示的に使用したりします。GC が適切に機能せず、マネージ メモリが効率的に処理されないこともあります。通常、このような状況は、GC による処理の実行を妨げる小さな (またはそれほど小さくない) プログラミング エラーが原因で発生します。優れたメモリ ユーザーとしては、リークがなく、必要なメモリが効率的に使用されることを保証するアプリケーションにする必要があります。

.NET アプリケーションのメモリ

おそらくご存じのことと思いますが、.NET アプリケーションでは数種類のメモリが使用されます。使用されるメモリの種類には、スタック、アンマネージ ヒープおよびマネージ ヒープがあります。確認のために、次に簡単に示しておきましょう。

スタック アプリケーションの実行中に、ローカル変数、メソッドのパラメータ、メソッドからの戻り値などの一時的な値がスタックに格納されます。スタックはスレッド単位に割り当てられ、スレッドが処理を実行するためのスクラッチ領域として機能します。メソッドの呼び出し用に確保されたスタック領域は、メソッドから戻るときに自動的にクリーンアップされるため、スタックをクリーンアップするのは GC の役割ではありません。ただし、GC では、スタックに格納されているオブジェクトへの参照は認識されることに注意してください。メソッド内でオブジェクトのインスタンスが作成されるとき、そのインスタンスへの参照 (プラットフォームに応じて 32 ビット整数または 64 ビット整数) はスタックに保持されますが、オブジェクト自体はマネージ ヒープに格納され、変数がスコープ外になるとガベージ コレクタによって処理されます。

アンマネージ ヒープ アンマネージ ヒープは、ランタイム データ構造、メソッド テーブル、Microsoft 中間言語 (MSIL)、JIT コードなどに使用されます。アンマネージ コードでは、オブジェクトのインスタンスを作成する方法に応じて、アンマネージ ヒープまたはスタック上にオブジェクトを割り当てます。マネージ コードでは、アンマネージ Win32® API の呼び出すか、COM オブジェクトのインスタンスを作成することによって、アンマネージ ヒープ メモリを直接割り当てることができます。CLR 自体は、データ構造体とコードに、幅広くアンマネージ ヒープを使用します。

マネージ ヒープ マネージ ヒープには、マネージ オブジェクトが割り当てられ、ガベージ コレクタの処理対象の領域です。CLR では、世代別に最適化を行う GC が使用されます。GC は、ガベージ コレクションの対象にならなかった期間でオブジェクトを分類するという点で、世代別です。これによりパフォーマンスが向上します。すべてのバージョンの .NET Framework で、(最も新しいものから最も古いものまで) Gen0、Gen1 および Gen2 という 3 世代を使用します。GC では、マネージ ヒープ内でオブジェクトを再配置して断片化を解消し、連続した空きメモリを確保することで最適化を行います。大きなオブジェクトの移動は負荷の高い作業なので、GC ではこのようなオブジェクトを最適化を行わない別の大きなオブジェクト ヒープに割り当てます。マネージ ヒープと GC の詳細については、Jeffrey Richter の 2 部構成のシリーズ「ガベージ コレクション入門: Microsoft .NET Framework の自動メモリ管理 Part Ⅰ」および「ガベージ コレクション入門: Microsoft .NET Framework の自動メモリ管理 Part Ⅱ」を参照してください。この資料は .NET Framework 1.0 を基に執筆されていますが、中核となる概念はバージョン 1.1 でも 2.0 でも変更されていません。ただし、.NET GC は、以前のバージョンよりも強化されています。

リークの発生をチェックする

アプリケーションでメモリ リークが発生していることを示す兆候は数多くあります。たとえば、OutOfMemoryException がスローされることがあります。仮想メモリからディスクへのスワップが開始されることにより、アプリケーションの応答が大幅に遅くなることもあります。タスク マネージャで、メモリの使用量が徐々に (または急激に) 増加していることを確認できる場合もあります。メモリ リークが発生している可能性があるときは、まず、どの種類のメモリでリークが発生しているかを判断する必要があります。これにより、適切な領域でデバッグ作業を行うことができます。PerfMon を使用してアプリケーションのパフォーマンス カウンタ Process/Private Bytes、.NET CLR Memory/# Bytes in All Heaps、および .NET CLR LocksAndThreads/# of current logical Threads を調べます。Process/Private Bytes カウンタは、あるプロセス専用に割り当てられ、システムの他のプロセスとは共有できないすべてのメモリを報告します。.NET CLR Memory/# Bytes in All Heaps カウンタは、Gen0、Gen1、Gen2 および大きなオブジェクト ヒープを組み合わせた合計サイズを報告します。.NET CLR LocksAndThreads/# of current logical Threads カウンタは、AppDomain の論理スレッド数を報告します。アプリケーションの論理スレッド数が予想外に増加している場合、スレッド スタックでリークが発生しています。Private Bytes が増加していても、# Bytes in All Heaps が安定した状態であれば、アンマネージ メモリでリークが発生しています。どちらのカウンタも共に増加している場合、マネージ ヒープのメモリでリークが発生しています。

スタック メモリでリークが発生している場合

スタック領域が不足したため、マネージ環境で StackOverflowException がスローされる場合もありますが、メソッドの呼び出し中に使用されるすべてのスタック領域は、メソッドから戻ると再利用されます。したがって、スタック領域でリークが発生するのは、実際には 2 つの場合しかありません。1 つは、大量のスタック リソースを使用するメソッドを呼び出し、そのメソッドから戻らず、関連するスタック フレームが解放されない場合です。もう 1 つは、スレッド、つまりスレッドのスタック全体でリークが発生する場合です。アプリケーションでバックグラウンド タスクを処理するワーカー スレッドを作成し、作成したワーカー スレッドを適切に終了しなかった場合、スレッド スタックでリークが発生する可能性があります。現在のデスクトップ バージョンとサーバー バージョンの Windows® のスタック サイズは、既定では 1MB です。そのため、アプリケーションの Process/Private Bytes と、.NET CLR LocksAndThreads/# of current logical Threads が対応して定期的に 1MB ずつ増加している場合、スレッド スタックでのリークの発生が原因である可能性があります。図 1 は、(意図的に間違った) マルチスレッド ロジックによって発生する不適切なスレッドのクリーンアップの例を示しています。

Figure 1 誤りのあるスレッドのクリーンアップ (Close Figure 1)
using System;
using System.Threading;

namespace MsdnMag.ThreadForker {
  class Program {
    static void Main() {
      while(true) {
        Console.WriteLine(
          "Press <ENTER> to fork another thread...");
        Console.ReadLine();
        Thread t = new Thread(new ThreadStart(ThreadProc));
        t.Start();
      }
    }

    static void ThreadProc() {
      Console.WriteLine("Thread #{0} started...",
        Thread.CurrentThread.ManagedThreadId);
      // Block until current thread terminates - i.e. wait forever
      Thread.CurrentThread.Join();
    }
  }
}

スレッドが起動され、スレッド ID を表示して、そのスレッド自体への参加を試みます。この参加により、呼び出し元のスレッドは、もう一方のスレッドが完了するまで待機することになります。ところが、もう一方のスレッドは自分自身なので、自身が完了するまで待機します。つまり、鶏が先か卵が先かという状況に陥ります。タスク マネージャでこのプログラムを監視し、Enter キーを押すたびに、メモリ使用量が 1MB ずつ、つまりスレッド スタックのサイズ単位に増加することを確認します。

Thread オブジェクトへの参照はループのたびに毎回削除されますが、GC ではスレッド スタック用に割り当てられたメモリは再利用されません。マネージ スレッドの有効期間は、それを作成する Thread オブジェクトの有効期間とは無関係です。この関係は、関連する Thread オブジェクトへの参照がすべて失われたという理由だけで、処理を実行中のスレッドを GC で終了してしまうことを防ぐために非常に有効です。そのため、GC では Thread オブジェクトのガベージ コレクションは行いますが、実際のマネージ スレッドのガベージ コレクションは行いません。マネージ スレッドは、その ThreadProc から戻るまで、または明示的に終了されるまで完了しません (つまり、そのスレッド スタックのメモリは解放されません)。そのため、マネージ スレッドが適切に終了されないと、そのスレッド スタックに割り当てられたメモリでリークが発生します。

アンマネージ ヒープ メモリでリークが発生している場合

メモリの総使用量が増加していて、論理スレッド数とマネージ ヒープのメモリ使用量が増加していなければ、アンマネージ ヒープでリークが発生しています。この場合、アンマネージ コードとの相互運用、ファイナライザの中断、アセンブリのリークなど、アンマネージ ヒープでのリーク発生の一般的な原因について調べます。

アンマネージ コードとの相互運用。メモリ リークの原因の 1 つは、アンマネージ コードとの相互運用に関係します。アンマネージ コードとの相互運用は、COM 相互運用機能により、P/Invoke や COM オブジェクトで C スタイルの DLL が使用されるときなどに行われます。GC ではアンマネージ メモリを認識しないので、ここでのリークは、アンマネージ メモリを使用するマネージ コードでのプログラミング エラーによるものです。アプリケーションとアンマネージ コードを相互運用している場合は、コードをステップ スルーして、アンマネージ呼び出し前後のメモリ使用量を調べ、メモリが適切に再利用されていることを確認します。適切に再利用されていない場合には、従来のデバッグ手法を使用してアンマネージ コンポーネントでのリークを検出します。

ファイナライザの中断。オブジェクトのファイナライザが呼び出されず、そのファイナライザにオブジェクトが割り当てたアンマネージ メモリをクリーンアップするコードが含まれているときは、非常に発見しにくいリークが発生します。通常の状況下ではファイナライザが呼び出されますが、CLR ではこの呼び出しが保証されません。今後変更する可能性はありますが、現在のバージョンの CLR ではファイナライザ スレッドを 1 つしか使用しません。オフラインのデータベースへの情報の記録を試みる、不適切な動作を行うファイナライザを考えてみます。不適切な動作をするファイナライザがデータベースに誤ったアクセスを繰り返し、その処理から戻らないと、"適切な動作を行う" ファイナライザが実行される機会がありません。この問題は、別のファイナライザの動作だけでなく、ファイナライザ キューのファイナライザの順序にも依存するので、非常に散発的にしか明らかにならない可能性があります。

CLR は AppDomain が破棄されるときに、キュー内のすべてのファイナライザを実行することでファイナライザ キューをクリアしようとします。ファイナライザが処理を停止すると、CLR では AppDomain の破棄を完了できません。このことを考慮して、CLR ではこのプロセスにタイムアウトを実装し、タイムアウト後、ファイナライザのプロセスを停止します。通常、これは大きな問題ではありません。ほとんどのアプリケーションには AppDomain が 1 つしか存在しないので、プロセスがシャットダウンされればこの AppDomain が破棄されます。OS のプロセスがシャットダウンされると、そのリソースはオペレーティング システムによって回復されます。残念ながら、ASP.NET や SQL Server™ などのホスティング環境では、AppDomain の破棄はホスティング プロセスの破棄を意味しません。同じプロセスでもう 1 つの AppDomain が作成されます。ファイナライザが実行されなかったためにコンポーネントによってリークが発生したアンマネージ メモリは、参照もアクセスもされず、領域を占有して待機したままになります。時間が経過するにつれてさらに多くのメモリでリークが発生すると、障害が引き起こされることもあります。

.NET 1.x での唯一の解決策は、プロセスを破棄し、再開することです。.NET Framework 2.0 ではクリティカルなファイナライザが導入され、ファイナライザがアンマネージ リソースをクリーンアップし、AppDomain の破棄中に必ず実行機会が与えられます。詳細については、Stephen Toub の記事「.NET Framework の信頼性機能でコードを実行し続ける」を参照してください。

アセンブリのリーク。アセンブリのリークは比較的頻繁に発生します。これは、アセンブリがいったんロードされると、AppDomain がアンロードされるまでアンロードできないことが原因で発生します。ほとんどの場合、アセンブリが動的に生成およびロードされない限り、問題にはなりません。では、動的なコード生成のリーク、特に XmlSerializer のリークを詳しく見てみましょう。

動的なコード生成のリーク。コードの動的な生成が必要な場合があります。アプリケーションに拡張性を持たせるために、Microsoft Office と同様のマクロ スクリプティング インターフェイスを用意することがあります。また、エンド ユーザーが独自の債権の種類を作成できるように、債券価格エンジンで価格ルールを動的に読み込む必要があることも考えられます。アプリケーションは Python の動的言語ランタイムまたはコンパイラかもしれません。多くの場合、パフォーマンス上の理由から、マクロ、価格ルール、コードを MSIL にコンパイルすることを考えます。実行中に MSIL を生成する場合は、System.CodeDom を使用できます。

図 2 のコードでは、アセンブリがメモリに動的に生成されます。これは繰り返し呼び出しても問題ありません。残念ながら、マクロ、価格ルール、またはコードを変更すると、動的アセンブリを再生成する必要があります。古いアセンブリは使用されなくなりますが、そのアセンブリをロードした AppDomain はアンロードされても、古いアセンブリはメモリから削除されません。その結果、コード、JIT メソッドなどのランタイム データ構造に使用されるアンマネージ ヒープのメモリでリークが発生します (マネージ メモリでは、動的に生成されたクラスの静的フィールドの形式でもリークが発生しています)。この問題を検知する魔法の公式はありません。System.CodeDom を使用して MSIL を動的に生成している場合、コードを再生成しているかどうかをチェックします。再生成している場合、アンマネージ ヒープ メモリでリークが発生しています。

Figure 2 アセンブリをメモリに動的に生成する (Close Figure 2)
CodeCompileUnit program = new CodeCompileUnit();
CodeNamespace ns = new
  CodeNamespace("MsdnMag.MemoryLeaks.CodeGen.CodeDomGenerated");
ns.Imports.Add(new CodeNamespaceImport("System"));
program.Namespaces.Add(ns);

CodeTypeDeclaration class1 = new CodeTypeDeclaration("CodeDomHello");
ns.Types.Add(class1);
CodeEntryPointMethod start = new CodeEntryPointMethod();
start.ReturnType = new CodeTypeReference(typeof(void));
CodeMethodInvokeExpression cs1 = new CodeMethodInvokeExpression(
  new CodeTypeReferenceExpression("System.Console"), "WriteLine",
    new CodePrimitiveExpression("Hello, World!"));
start.Statements.Add(cs1);
class1.Members.Add(start);

CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerResults results = provider.CompileAssemblyFromDom(
  new CompilerParameters(), program);

この問題の解決には、主に 2 つの方法があります。1 つは、動的に生成された MSIL を子 AppDomain にロードする方法です。生成されたコードが変更され、更新された MSIL をホストするために新しいコードが作成されるときに、子 AppDomain をアンロードできます。この方法は、すべてのバージョンの .NET Framework で有効です。

.NET Framework 2.0 で導入されたもう 1 つの方法は、動的メソッドとも呼ばれる、簡易コード生成です。DynamicMethod を使用する場合は、メソッド本体を定義するために MSIL オペレーション コードを明示的に作成し、DynamicMethod.Invoke から直接、または適切なデリゲートによって DynamicMethod を呼び出します。

DynamicMethod dm = new DynamicMethod("tempMethod" +
  Guid.NewGuid().ToString(), null, null, this.GetType());
ILGenerator il = dm.GetILGenerator();

il.Emit(OpCodes.Ldstr, "Hello, World!");
MethodInfo cw = typeof(Console).GetMethod("WriteLine",
  new Type[] { typeof(string) });
il.Emit(OpCodes.Call, cw);

dm.Invoke(null, null);

動的メソッドの主なメリットは、MSIL と関連するすべてのコード生成のデータ構造がマネージ ヒープに割り当てられることです。そのため、DynamicMethod への最後の参照がスコープ外になると、GC によってメモリを再利用できるようになります。

XmlSerializer のリーク。XmlSerializer などの .NET Framework の一部では、動的なコード生成が内部で使用されます。次の典型的な XmlSerializer コードについて考えます。

XmlSerializer serializer = new XmlSerializer(typeof(Person));
serializer.Serialize(outputStream, person);

XmlSerializer コンストラクタでは、リフレクションを使用して Person クラスを分析することで、XmlSerializationReader と XmlSerializationWriter から派生した 1 組のクラスを生成します。これにより一時 C# ファイルが作成され、作成されたファイルが一時アセンブリにコンパイルされて、最終的にそのアセンブリがプロセスにロードされます。このようなコード生成も、比較的負荷の高い作業です。そのため、XmlSerializer では、型ごとに一時アセンブリがキャッシュされます。つまり、次回 Person クラスの XmlSerializer が作成されるときは、アセンブリを新しく生成するのではなく、キャッシュされているアセンブリを使用します。

既定では、XmlSerializer によって使用される XmlElement 名はクラス名です。したがって、Person は次のようにシリアル化されます。

<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Id>5d49c002-089d-4445-ac4a-acb8519e62c9</Id>
 <FirstName>John</FirstName>
 <LastName>Doe</LastName>
</Person>

クラス名を変更しないで、ルート要素名の変更が必要になることがあります (既存のスキーマとの互換性のために、ルート要素名が必要になります)。そのため、場合によっては Person を <PersonInstance> としてシリアル化する必要があることがあります。便利なことに、次のようにルート要素名を 2 番目のパラメータとして受け取る XmlSerializer コンストラクタのオーバーロードがあります。

XmlSerializer serializer = new XmlSerializer(typeof(Person),
  new XmlRootAttribute("PersonInstance"));

アプリケーションで Person オブジェクトのシリアル化またはシリアル化解除が開始されると、OutOfMemoryException がスローされるまですべての処理が実行されます。この XmlSerializer コンストラクタのオーバーロードでは、動的に生成されたアセンブリがキャッシュされませんが、XmlSerializer のインスタンスが新しく作成されるたびに新たな一時アセンブリが生成されます。アプリケーションでは、一時アセンブリの形式でアンマネージ メモリのリークが発生します。

リークを解決するには、クラスの XmlRootAttribute を使用し、シリアル化された型のルート要素名を変更します。

[XmlRoot("PersonInstance")]
public class Person {
  // code
}

属性が型に直接適用されると、XmlSerializer ではその型用に生成されたアセンブリがキャッシュされ、リークは発生しません。ルート要素名の動的な切り替えが必要な場合、アプリケーションでは、要素名を取得するファクトリを使用することで XmlSerializer インスタンス自体のキャッシュを実行できます。

XmlSerializer serializer = XmlSerializerFactory.Create(
  typeof(Person), "PersonInstance");

XmlSerializerFactory は、Dictionary<TKey, TValue> に、PersonInstance ルート要素名を使用する Person の XmlSerializer が含まれるかどうかをチェックするために今回作成したクラスです。含まれる場合は、そのインスタンスを返します。含まれない場合はインスタンスを新しく作成し、ハッシュ テーブルに格納して呼び出し元に返します。

マネージ ヒープ メモリでリークが発生している場合

では、マネージ メモリでリークが発生している場合を考えましょう。マネージ メモリの処理のほとんどを、GC が代行します。そのため、GC が処理の実行に必要な情報を提供する必要があります。ただし、GC での効率的な処理が妨げられ、マネージ メモリが必要以上に使用される結果になることがよくあります。こうした状況には、大きなオブジェクト ヒープの断片化、不必要なルート参照、およびミッドライフ クライシスがあります。

大きなオブジェクト ヒープの断片化 オブジェクトのサイズが 85,000 バイト以上の場合、そのオブジェクトは大きなオブジェクト ヒープに割り当てられます。このサイズは、オブジェクトの子オブジェクトを含むサイズではなく、オブジェクト自体のサイズであることに注意してください。たとえば、次のクラスについて考えます。

public class Foo {
  private byte[] m_buffer = new byte[90000]; // large object heap
}

Foo インスタンスには、バッファへの 4 バイト (32 ビット フレームワーク) 参照または 8 バイト (64 ビット フレームワーク) 参照と、.NET Framework が使用するその他のハウスキーピング データしか含まれないので、このインスタンスは通常の世代別のマネージ ヒープに割り当てられます。ただし、バッファは大きなオブジェクト ヒープに割り当てられます。

大きなオブジェクト ヒープは、他のマネージ ヒープとは異なり、大きなオブジェクトを移動する負荷を考慮して、最適化が行われません。そのため、大きなオブジェクトが割り当てられ、解放され、クリーンアップされると、ギャップが生じます。使用パターンによっては、この大きなオブジェクト ヒープのギャップが、現在割り当てようとしている大きなオブジェクトに必要な量よりもはるかに大きくなることがあります。今月のダウンロードに付属する LOHFragmentation アプリケーションでは、大きなオブジェクト ヒープにランダムにバイト配列を割り当て、解放することによりこの状態を示します。アプリケーションの実行時に作成されるバイト配列のサイズと、バイト配列を解放することで残されたギャップのサイズがうまく一致する場合もあります。また、必要なメモリのサイズが、現在割り当てられているバイト配列のサイズよりもはるかに大きくなる場合もあります。大きなオブジェクト ヒープの断片化を表示するには、CLRProfiler などのメモリ プロファイラを使用します。図 3 の赤い部分は割り当て済みのバイト配列で、白い部分は未割り当ての領域です。

図 3 CLRProfiler の大きなオブジェクト ヒープ
図 3 CLRProfiler の大きなオブジェクト ヒープ(画像を縮小するには、ここをクリックします)
図 3 CLRProfiler の大きなオブジェクト ヒープ
図 3 CLRProfiler の大きなオブジェクト ヒープ (画像を拡大するには、ここをクリックします)

大きなオブジェクト ヒープの断片化を防ぐ解決策はありません。CLRProfiler などのツールを使用して、アプリケーションでメモリが使用方法や、大きなオブジェクト ヒープ上にある具体的なオブジェクトの種類について調べます。バッファの再割り当てによって断片化が生じる場合、再使用されるバッファの固定セットを管理します。多くの文字列の結合によって断片化が発生する場合、作成される一時文字列の数を System.Text.StringBuilder クラスで削減できるかどうかを調べます。基本的な方針は、大きなオブジェクト ヒープにギャップを生じさせている大きな一時オブジェクトへのアプリケーションの依存度合いを軽減する方法を決めることです。

不必要なルート参照 GC がメモリを再利用できるタイミングを判断する方法について考えましょう。CLR がメモリを割り当てるときに十分なメモリを確保できないと、ガベージ コレクションが実行されます。GC では、すべてのスレッドのコール スタックの静的フィールドやスコープ内のローカル変数など、すべてのルート参照を列挙します。こうした参照にアクセス可能のマークを付け、続いてこれらのオブジェクトに含まれるすべての参照も同様にアクセス可能のマークを付けます。アクセス可能な参照をすべて調べるまで、このプロセスを続行します。マークが付いていないオブジェクトにはアクセスできないため、ガベージになります。GC は、マネージ ヒープを最適化し、ヒープの新しい場所を参照するように参照を整理して、CLR に制御を返します。十分なメモリが解放されると、この解放されたメモリを使用して割り当てを続行します。十分なメモリが解放されない場合、オペレーティング システムから追加のメモリを要求します。

ルート参照を空にするのを忘れると、GC ではできるだけ早く効率的にメモリを解放することができず、アプリケーションで使用されていないメモリ領域が大きくなります。データベース クエリや Web サービス呼び出しのようにリモート呼び出しを行う前に一時オブジェクトの大きなグラフを作成するメソッドなどでは、問題が捕らえにくくなる可能性があります。リモート呼び出しの実行中にガベージ コレクションが実行されると、グラフ全体にアクセス可能のマークが付けられ、ガベージ コレクションが行われません。ガベージ コレクションが行われなかったオブジェクトは次の世代に移行され、ミッドライフ クライシスが生じるため、さらに負荷の高い処理になります。

ミッドライフ クライシス ミッドライフ クライシスによって、アプリケーションが外出して、ポルシェを購入することになるわけではありません。しかし、マネージ ヒープ メモリの使いすぎと、GC で多くのプロセッサ時間が消費される原因にはなります。既に説明したように、GC では世代別アルゴリズムを使用します。このアルゴリズムでは、経験則に基づいて、オブジェクトがしばらく存在し続けると、おそらくさらに長い間存在し続けると予測します。たとえば、Windows フォーム アプリケーションでは、アプリケーションが開始するとメイン フォームが作成され、アプリケーションが終了するとメイン フォームが閉じられます。メイン フォームが参照されていることを GC で絶えず確認するのは無駄な作業です。システムでは、割り当て要求を満たすためにメモリが必要になると、まず Gen0 のコレクションを実行します。そこで十分なメモリが使用可能にならなければ、Gen1 のコレクションを実行します。それでも割り当て要求が満たされなければ、Gen2 のコレクションを実行します。これはマネージ ヒープ全体を対象とする負荷の高い作業になります。Gen0 のコレクションでは、最近割り当てられたオブジェクトのみをコレクションの対象とするので比較的負荷は低くなります。

オブジェクトが Gen1 (または最悪 Gen2) まで存在し、その直後に終了すると、ミッドライフ クライシスが発生します。その結果、負荷の低い Gen0 のコレクションが、はるかに負荷の高い Gen1 (または Gen2) のコレクションに変換されます。これはどのように発生するのでしょうか。次のコードを見てください。

class Foo {
  ~Foo() { }
}

このオブジェクトは、Gen 1 コレクションで常に再利用されるようになります。AppDomain の突然の中断がなければ、ファイナライザ ~Foo() により、オブジェクトのメモリが解放される前に実行される、オブジェクトのクリーンアップ コードを実装できます。GC の役割は、できるだけ多くのマネージ メモリをできるだけ早く解放することです。ファイナライザは、ユーザーが作成するコードで、どんなことでも実行できます。お勧めはしませんが、ファイナライザで、データベースへの記録や Thread.Sleep(int.MaxValue) の呼び出しなど、不適切な処理を行うこともできます。そのため、GC はファイナライザで参照されないオブジェクトを見つけると、そのオブジェクトをファイナライザのキューに配置し、移動します。このオブジェクトはガベージ コレクションが行われなかったので、次の世代に変換されます。これを検出するためのパフォーマンス カウンタ .NET CLR Memory-Finalization Survivors があります。これは、ファイナライザが原因で前回ガベージ コレクションが行われなかったオブジェクトの数です。最終的には、ファイナライザのスレッドでオブジェクトのファイナライザが実行され、その結果ガベージ コレクションが行われます。しかし、単純なファイナライザを追加するだけで、負荷の低い Gen0 コレクションから Gen1 コレクションに変換されてしまいます。

ほとんどの場合、マネージ コードの作成時にファイナライザは必要ありません。ファイナライザは、クリーンアップが必要なアンマネージ リソースへの参照をマネージ オブジェクトが保持しているときにのみ必要です。そのような場合でも、ファイナライザを実装するのではなく、SafeHandle-derived 型を使用してアンマネージ リソースをラップします。また、アンマネージ リソースや IDisposable を実装する別のマネージ型を使用している場合、Dispose パターンを実装して、オブジェクトのユーザーが積極的にリソースをクリーンアップし、関連するファイナライザの処理を回避するようにします。

オブジェクトで他のマネージ オブジェクトへの参照のみが保持される場合、GC は参照されないオブジェクトのクリーンアップを行います。これは、子オブジェクトで delete を呼び出す必要のある C++ とはまったく異なります。ファイナライザが空か、単に子オブジェクトへの参照がない場合は削除してください。これらは、オブジェクトを古い世代に不必要に変換させる原因となり、パフォーマンスに悪影響を与え、クリーンアップする負荷を高くしています。

ミッドライフ クライシスは他にも発生する場合があります。それは、データベースのクエリのようにブロッキング呼び出しを行う前にオブジェクトを保持していたり、他のスレッドでブロッキングを行ったり、Web サービスを呼び出したりする場合です。このような呼び出し中に、1 回以上コレクションが行われ、負荷の低い Gen0 オブジェクトが次の世代に変換されて、メモリの使用量とコレクションのコストが再び大幅に上がります。

イベント ハンドラとコールバックで発生する、さらにわかりにくいケースもあります。例として ASP.NET を使用しますが、同様の問題はどのアプリケーションでも起こります。負荷の高いクエリを実行し、結果を 5 分間キャッシュする場合を考えます。クエリはページ固有で、クエリ文字列パラメータに基づきます。キャッシュの動作を監視するために、キャッシュからアイテムが削除されるときにイベント ハンドラがログに記録します (図 4 参照)。

Figure 4 キャッシュから削除されたアイテムを記録する (Close Figure 4)
protected void Page_Load(object sender, EventArgs e) {
  string cacheKey = buildCacheKey(Request.Url, Request.QueryString);
  object cachedObject = Cache.Get(cacheKey);
  if(cachedObject == null) {
    cachedObject = someExpensiveQuery();
    Cache.Add(cacheKey, cachedObject, null,
      Cache.NoAbsoluteExpiration,
      TimeSpan.FromMinutes(5), CacheItemPriority.Default,
      new CacheItemRemovedCallback(OnCacheItemRemoved));
  }
  ... // Continue with normal page processing
}

private void OnCacheItemRemoved(string key, object value,
                CacheItemRemovedReason reason) {
  ... // Do some logging here
}

無害に見えるこのコードには、大きな問題が含まれています。これらすべての ASP.NET Page インスタンスは、長期間存在するオブジェクトになりました。OnCacheItemRemoved はインスタンス メソッドで、CacheItemRemovedCallback デリゲートには暗黙の this ポインタが含まれます。この場合 this は Page インスタンスです。デリゲートは Cache オブジェクトに追加されます。そのため、Cache から Page インスタンスへのデリゲートに依存関係が存在するようになります。ガベージ コレクションが行われるときに、Page インスタンスは、ルート参照である Cache オブジェクトからアクセス可能なままです。Page インスタンス (および表示中に作成されるすべての一時オブジェクト) は、コレクションが行われるまで少なくとも 5 分間待機する必要があります。その間に、Gen2 に世代が変換されます。幸い、この例には簡単な解決策があります。コールバック関数をスタティックにします。Page インスタンスへの依存関係がなくなり、Gen0 オブジェクトとして負荷の低いコレクションが行われます。

まとめ

メモリ リークまたはメモリの過剰使用を発生させる、.NET アプリケーションのさまざまな問題について説明しました。.NET ではメモリに関する心配事が減りますが、アプリケーションのメモリ使用に注意し、適切で効率的に動作することを保証する必要があります。アプリケーションが管理されているからといって、優れたソフトウェア エンジニアリング手法を投げ出し、GC が行う魔法に頼ってはいけません。開発やテストのプロセス中は、アプリケーションのメモリのパフォーマンス カウンタを監視し続ける必要があります。その価値はあります。アプリケーションの適切な動作が顧客満足につながることを忘れないでください。

James Kovacs は、アルバータ州カルガリー在住のフリー設計者、開発者、兼トレーナーの、いわゆる何でも屋です。.NET Framework、セキュリティ、およびエンタープライズ アプリケーションの開発を専門にしています。ソリューション アーキテクチャの Microsoft MVP でもあり、ハーバード大学で修士号を取得しました。James の電子メール アドレスは jkovacs@post.harvard.edu で、Web サイトは www.jameskovacs.com (英語) です。


編集者のメモ: 新しい年に新しい Vista を

編集者のメモ
新しい年に新しい Vista を

新年を迎えるにあたって ここ MSDN Magazine (自宅で執筆するのは 22 回目です) には、たいへんな幸運に恵まれた執筆者がいます。Windows Vista チームが製品の発表を今月号に合わせるというスケジュールを選んだため、今月はこの OS に関する記事が満載です。

まず、Windows Vista ユーザー向けの重要なセキュリティ機能である、ユーザー アカウント制御を紹介します。ユーザーが管理者であることを想定していた時代は終わりました。Chris Corio が開発向けにこの機能の意味を説明します。

Windows Vista および Office 2007 では、プレビュー ハンドラという新しい種類のアドインをサポートします。テクニカル エディタ Stephen Toub が、独自のプレビュー ハンドラを作成する秘訣をお教えします。

著名な MSDN Magazine コラムニストの 1 人 Jeffrey Richter が、Windows SideShow についていくつか説明します。SideShow は、ラップトップに接続された 2 台目のディスプレイで機能するテクノロジです。いちいち携帯電話を開かなくてもだれが電話をかけてきたかがわかる折りたたみ式携帯電話と同様に、SideShow ではラップトップを開かなくても、スケジュール、電子メール、メモなどの重要な情報を表示できます。

Unicode 5.0 の最新情報と、Unicode 5.0 が Windows Vista でどのようにサポートされているかについて紹介します。お使いのアプリケーションではフェニキア文字をサポートしていますか。このコラムを読めばわかります。

プログラマ向け Windows Vista ツアーでは、新しい資格情報プロバイダ モデルについても説明します。GINA に代わる資格情報プロバイダでは、新しいユーザー認証シナリオを簡単に実装できるようになります。

もちろん、Windows Vista の機能をいくつか紹介するだけで終わりにするつもりはありません。幸先の良いスタートを切るために、「これがなくては生活できない」と思えるほどのコラムを 3 つ新しく追加します。

Windows プログラミングの第一人者 Charles Petzold が、.NET Framework 3.0 の新しいコラムの執筆陣に名前を連ねました。Charles は、Windows Communication Foundation コミュニティの代弁者 Juval Lowy と Windows Workflow Foundation に詳しい Matt Milner とコラムの執筆を分担し、Windows Communication Foundation について執筆しています。とにかく、さまざまな "Foundation" について説明します。

名前が思い付くすべてのコミュニティの代弁者 Brian A. Randell は、新しい定期コラムで Visual Studio Team System を取り上げています。このコラムは、人気を集めそうな "Team System" という名前にしました。

他にもすばらしいお知らせがあります。今やグローバルな時代ですが、"セキュリティ" を 7 か国語でそれぞれ何と言うかご存じですか。この Web サイトでそれがわかります。数年前は、記事の翻訳作業に苦労することがありました。別の言語で読む必要がある記事に移動し、ページが読み込まれるまで待機して、さらに自動翻訳ツールでサイトが翻訳されるのに長時間待たされました。2006 年 11 月号から、待ち時間がなくなり、この処理が効率的になります。好きな言語ですべての記事をお読みいただけます。"seguranca"、"sécurité"、"seguridad"、"Sicherheit"、"безопасность"、"安全性" のうち、どの言葉で情報を検索しても、この Web サイトに適切なページが表示されます。ちなみに、これらの言語は、順にポルトガル語 (ブラジル)、フランス語、スペイン語、ドイツ語、ロシア語、簡体字中国語です。

この新しい翻訳パラダイムはすばらしい成果です。Web の第一人者 Mike Richter が、Redmond の Brad McCabe のチーム、および MSDN Magazine と TechNet Magazine のテクニカル エディタ Stephen Toub と Joshua Hoffman と共に、このパラダイムのデザインと実装を手掛けました。

MSDN Magazine の新しい公式ブログなど、その他の優れたコンテンツもオンラインで提供します。このブログは定期的に更新されるため、特に注目すべきコンテンツです。MSDN Magazine とマイクロソフトの開発について知ることで、そのメリットを得ることができます。MSDN Magazine のブログは、blogs.msdn.com/msdnmagazine (英語) にあります。では楽しいネット サーフィンを!£J.T.

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Snesha Arumugam、Mason Bendixen、Matt Gibbs、Ben Gillis、Chris Guzak、Buck Hodges、Kerem Karatal、Aidan Low、Brian McNeil、Kevin Moore、Eric Perlin、Mark Pustilnik、Rob Relyea、Jon Rosenberg、および Ravinder Vuppula です。

Aero、Active Directory、ActiveX、IntelliSense、Internet Explorer、Microsoft、MSDN、Outlook、PowerPoint、Visual Basic、Visual C++、Visual C#、Visual J#、Visual Studio、Windows、Win32、Windows Live、Windows Media、Windows NT、Windows Server、Windows SideShow、および Windows Vista は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。ここに記載されている商標または会社名は各社の所有物です。 MSDN Magazine に記載されているコードまたはその他の情報に関し、明示または黙示にかかわらず一切の表明または保証を行うものではありません。MSDN Magazine は、コードまたはその他の情報の使用に対していかなる責任も負わないものとします。MSDN および Microsoft は、米国またはその他の国におけるマイクロソフトの登録商標または商標です。MSDN Magazine、MSDN、および Microsoft のロゴは、CMP が許可を得て使用しています。MSDN Magazine は、CMP Technology LLC によって発行されています。CMP Technology LLC は、Microsoft Corporation の系列企業ではなく独立した企業です。Microsoft Corporation は、MSDN Magazine の編集内容のみに責任を負うものとします。

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


ツール ボックス: コーディングにかかる時間を短縮する、圧縮ファイルを管理する

ツール ボックス
コーディングにかかる時間を短縮する、圧縮ファイルを管理する
Scott Mitchell

コンテンツ
コーディングにかかる時間を短縮する
圧縮ファイルを管理する
HTTP トラフィックを監視する
推薦図書

コーディングにかかる時間を短縮する

Visual Studio には、多数の組み込みの生産性向上ツールが用意されていますが、サードパーティ製の製品を使用してさらに生産性を上げることができます。AxTools が提供する CodeSMART G2 for Visual Studio には、反復的なコーディングの作業にかかる時間を大幅に短縮できる多数のウィンドウ、ツール、ウィザード、およびダイアログ ボックスが用意されています。

CodeSMART をインストールすると、Visual Studio® にいくつかのエクスプローラ ウィンドウが追加されます。これらのウィンドウは、他の Visual Studio のウィンドウと同様にドッキング、自動表示、または自動非表示の状態にすることができます。Code Explorer ウィンドウは、Visual Studio のソリューション エクスプローラとクラス ビュー ウィンドウの両方の良い面を取り入れているため、このウィンドウでは、プロジェクトに含まれているファイルを参照したり、各ファイルを構成しているクラスやメンバをドリル ダウンしたりすることができます。さらに、Code Explorer では、メソッドごとに Code Flow フォルダがあります。このフォルダには、メソッドの条件ステートメントや制御フローが一覧表示 (およびネスト表示) されます。Code Explorer でノードをダブルクリックすると、該当箇所に移動できます。また、History ペインには、次回すばやくアクセスできるようにエントリが追加されます。

CodeSMART により追加されるエクスプローラ ウィンドウ
CodeSMART により追加されるエクスプローラ ウィンドウ (画像を縮小するには、ここをクリックします)
CodeSMART により追加されるエクスプローラ ウィンドウ
CodeSMART により追加されるエクスプローラ ウィンドウ (画像を拡大するには、ここをクリックします)

デザインについての機能もあります。現在開いているデザイナ画面に含まれるコントロールと、選択したコントロールのプロパティが、CodeSMARD の Designer Explorer に一覧表示されます。デザイナに新しいコントロールを追加するときに Text プロパティや Name プロパティを設定するダイアログ ボックスを自動的に表示するように、CodeSmith を構成することができます。

エクスプローラ ウィンドウとデザインタイム ダイアログ ボックスを使用するだけでも作業時間を短縮することはできますが、CodeSMART の最も優れた機能は AutoText と SmartComplete です。AutoText 機能は Microsoft® Word のオートコンプリートと非常によく似ています。オートコンプリートは、単語や一般的な表現の最初の 2、3 文字を入力すると残りの部分が自動的に入力される機能ですが、AutoText では、短く単純なキーの組み合わせを頻繁に使用する長いコード ブロックにマップします。たとえば、「tc」と入力し、その後に空白文字を入力すると、try...catch ブロックが挿入され、カーソルは try の中に移動されます。SmartComplete は、言語のキーワードやステートメントなどを表示する IntelliSense® と似ていますが、カスタマイズしてユーザー定義の項目を含めることができます。Visual Studio 2005 ユーザーにとっては、AutoText や SmartComplete はコード スニペットの類似機能になりますが、AutoText や SmartComplete の規則は、間単に作成または変更することができます。

CodeSMART には、これ以外にもコードをビルドするダイアログ ボックス、さまざまなコードの書式、ドキュメント作成ツール、プログラム統計レポートなどが用意されています。同じコードを繰り返し記述するのに多くの時間を費やしていると感じる場合は、CodeSMART を使用すると作業時間を大幅に短縮することができます。

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

www.axtools.com (英語)

圧縮ファイルを管理する

1989 年に登場した ZIP ファイル形式は、ファイルを簡単にアーカイブおよび圧縮できるので一般的な圧縮形式として利用されています。ZIP ファイルを操作するための .NET Framework ベースのサードパーティ製のコンポーネントは多数あります。その 1 つに、ComponentAce 社の ZipForge.NET Version 1.0 があります。

ZipForge.NET は、ZIP ファイルを作成、暗号化、検索、および展開するための使いやすい API を提供しています。たった 4 行のコードを記述するだけで、新しい ZIP ファイルを作成し、作成した ZIP ファイルにバイト配列やストリームからファイルやコンテンツを追加することができます。同様に、アーカイブのコンテンツも簡単に展開できます。

ZipForge.NET API には、アーカイブのコンテンツを検索するのに役立つ多数の Find メソッドが用意されています。これらの Find メソッドを使用して、ZIP ファイルのすべてのコンテンツを列挙したり、一部のファイル (たとえば、すべての JPG ファイルなど) に対してのみ操作を行ったりすることができます。

より複雑な ZIP 関連機能も同じくらい簡単に実行できます。たとえば、アーカイブをパスワードで保護する必要がある場合は、Password プロパティを設定します。必要な作業は、これだけです。サイズの大きいアーカイブを処理する場合には、OnFileProgress イベントや OnOverallProgress イベントのイベント ハンドラを使用して処理の進行状況を監視できます。

価格 : Personal Edition - 無料、Commercial Edition (1 ユーザー ライセンス、ソース コード再配布可能バージョン) - 149 ユーロ (約 125 ドル)

www.componentace.com (英語)

HTTP トラフィックを監視する

サーバー側とクライアント側で使用されているテクノロジの論理的、物理的、および時間的な違いにより、Web アプリケーションのデバッグ作業が困難になるのは、よくあることです。バグには、バックエンド データベース、アプリケーション層、ダイナミック Web ページなど、サーバー側の層で発生するもの、ブラウザに送信される HTML や JavaScript によって発生するもの、これらの層の間でシリアル化または転送されるデータによって発生するものがあります。

以前は、ほぼすべてのバグは、サーバー上で実行されるコードのエラーによるものでした。しかし、現在、Web ページは非常に複雑になり、多数のインラインまたは外部のスタイルシートやスクリプトの定義を含んでいます。

HttpWatch によるトラフィックの記録 (画像を縮小するには、ここをクリックします)

HttpWatch によるトラフィックの記録 (画像を拡大するには、ここをクリックします)

Web アプリケーションのバグを追跡する際には、まず、クライアント側で受信しているマークアップ言語やスクリプトを調査することをお勧めします。また、サーバーに返しているデータの種類も調べる必要があります。[表示] メニューの [ソース] をクリックして、受信しているマークアップ言語を調査することは可能ですが、Simtec Limited 社が提供している HttpWatch 4.x のようなツールを使用すると、この作業を簡単かつ短時間で行うことができます。

HttpWatch はインストールすると、Microsoft® Internet Explorer® のツール バー アイコンから直接起動できます。HttpWatch をアクティブ化すると、すべての HTTP トラフィックが記録され、生データやデータを確認するためのさまざまな概要レポートが提供されます。

一般的な Web ページにアクセスしたときに発生する HTTP 要求の数には驚くと思います。もちろん Web ページ自体に対する要求は行われていますが、それ以外には、ページに含まれている画像、外部のカスケード スタイル シートやスクリプト リソースへの要求も行われています。つまり、膨大な量のデータがやり取りされています。このような膨大なデータ量に対処するため、HttpWatch には、コンテンツの種類、URL、HTTP ステータス コード、HTTP ヘッダーなどで表示する結果を限定したり、強調表示したりできるようにするフィルタが用意されています。

HttpWatch には、HTTP トラフィックの生データと共に、選択した要求のヘッダー、Cookie、クエリ文字列、POST データなどを表示する多数の概要タブが用意されています。また、これらの多くの概要レポートは、検索またはエクスポートすることができます。

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

www.httpwatch.com (英語)

推薦図書

大規模かつハイレベルなフレームワークでプログラミングを行っている開発者は、開発環境について熟知することが非常に重要です。多くの .NET 開発者と同様、私も毎日 Visual Studio を使用して何時間も作業しています。Visual Studio のオプション、ショートカット キー、ウィンドウ、ツール バー、機能を習得するにつれて、生産性が向上し、IDE と格闘する時間が短くなりました。

Visual Studio の環境について特にわかりやすく説明していると感じた書籍は『Microsoft Visual Studio 2005 Unleashed』 (Sams、2006) (英語) です。これは 870 ページもある大作で、Lars Powers 氏と Mike Snell 氏が Visual Studio 2005 を構成している多数のメニュー、構成設定、プロジェクトの種類、機能を細部にわたって調査して書き上げたものです。

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

この書籍は 4 部構成になっています。第 1 部では Visual Studio の概要を説明し、第 2 部では環境について詳しく説明しています。第 2 部では、変更履歴の記録、コード アウトラインの作成、スマート タグやスマート タスク、IntelliSence、タスク一覧など、開発作業の生産性を向上させるための補助ツールについて章立てで説明しています。他の章では、多数のデバッグ オプション、Visual Studio のオートメーション オブジェクト モデルの操作方法、およびマクロ、アドイン、ウィザードの記述方法について説明しています。

第 3 部では、Visual Studio におけるさまざまな作業方法について説明しています。第 3 部は、データベース プロジェクトにストアド プロシージャを追加したり、Windows® Forms アプリケーション用のカスタム コントロールを作成したりする方法など、特定の作業を実行する方法を調べる場合に有益です。プログラミングの慣習やコードについての説明はなく、Visual Studio を使用して特定のタスクを完了する手順のみが示されています。

第 4 部では、Visual Studio 2005 Team System について説明しています。ソース管理の使用、作業項目の記録、モデリング、テスト作業など、一般的な作業について説明します。Visual Studio Team System を使用していない場合でも、第 2 部と第 3 部で豊富な情報が提供されているため、この書籍は十分に価値のある参考資料になります。

Visual Studio の画面と共に多くの時間を過ごしている場合は、ぜひ『Microsoft Visual Studio 2005 Unleashed』をご覧ください。この書籍は机上に常備され、Visual Studio に関する疑問を解決する際に最初に手に取る資料になることでしょう。

価格 : 59.99 ドル

www.samspublishing.com (英語)

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

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

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

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


CLR の完全理解: COM 相互運用機能の概要

CLR の完全理解
COM 相互運用機能の概要
Thottam R. Sriram

コードのダウンロード : : CLRInsideOut2007_01.exe (1480KB)
コンテンツ
簡単な ATL COM サーバー
COM クライアントを作成する
Tlbimp を使用して COM DLL を変換する
マネージ クライアントを作成する
構造体と文字列をマーシャリングする
マネージ コードから P/Invoke を使用する
インターフェイスを使用してマネージ コードを呼び出す
マネージ コードとアンマネージ コードをデバッグする
まとめ

COM はすばらしいテクノロジです。共通言語ランタイム (CLR) が非常に強力なプラットフォームとなる理由の 1 つに、Microsoft® .NET アプリケーションとアンマネージ COM コンポーネントのシームレスな対話を可能にするという側面があります。しかし、Web を検索したところ、COM 相互運用機能のごく基本的な概念を紹介している実用的なサンプルはほとんど見つかりませんでした。このコラムの目的は、その基本的な概念を説明し、このテクノロジを使い始めるユーザーにとって役に立つ実用的な例を紹介することです。

まずは、簡単な Active Template Library (ATL) COM サーバーの作成から始め、アンマネージ COM クライアントを使用してこのサーバーのメソッドにアクセスしてみます。次に、マネージ クライアントを使用して同じことを実行します。アンマネージからマネージへの変換を示すために、さまざまな DLL を見ていきます。また、アンマネージ DLL 内のエクスポートされたメソッドに P/Invoke を使用してアクセスする方法についても説明します。最も理解しにくい部分は、複雑な構造体のマーシャリングです。これに関してはこのコラムでも完全な説明はしていません。完全な説明をしようとすれば、このテーマだけでもコラムが 1 つ (または本が 1 冊) できてしまいます。インターフェイスを使用して、アンマネージ コードからマネージ コードを呼び出す方法を説明します (デリゲートを使用してこれを行うこともできますが、このコラムではその方法については説明しません)。

最後に、パブリック シンボルを使用した、COM 相互運用機能プロジェクトのデバッグについて説明します。ここでは、WinDbg.exe、アンマネージ デバッグ、および SOS を使用したマネージ デバッグについて、ごく基本的な事項を紹介します。呼び出しはどちらの方向にも行われるので、マネージ、アンマネージ両方のスタックを示します。

簡単な ATL COM サーバー

では、簡単な COM サーバーの作成から始めましょう。クライアントはサーバーをインプロセスでホストし、サーバーのメソッドを実行します。

開発を簡単にするために、ATL COM を使用します。COMInteropSample という名前のディレクトリを作成し、新しい Visual C++® ATL プロジェクトを作成して、MSDNCOMServer といった名前を付けます。[ソリューションのディレクトリを作成] チェック ボックスはオフにしますが、それ以外は既定の設定を使用します。

ソリューションに新しいクラスとインターフェイスを追加します。ソリューション エクスプローラがまだ表示されていない場合は、ソリューション エクスプローラを開きます。MSDNCOMServer を右クリックして、[追加] をポイントし、[クラス] をクリックします。[ATL] を選択し、[ATL シンプル オブジェクト] を選択して、[追加] をクリックします。クラスの短い名前として「MyCOMServer」と入力し、それ以外は既定の設定を使用します。[完了] をクリックしてクラスを追加します。

作成されたインターフェイスに新しいメソッドを追加します。クラス ビューを開き、IMyCOMServer インターフェイスを右クリックして、[追加] をポイントし、[メソッドの追加] をクリックします。メソッドに MyCOMServerMethod といった名前を付け、[完了] をクリックします。

メソッドをクラスに実装します。MyCOMServer.cpp を選択してソースを表示すると、ウィザードでインターフェイスに追加したメソッドが表示されます。TODO セクションに次のコードを追加します。

wprintf_s(L"Inside MyCOMServerMethod");
コードをコンパイルして、サーバー DLL を生成します。

これで、クラス CMyCOMServer にインターフェイス IMyCOMServer を実装し、既定の出力ストリームにメッセージを出力する ATL COM サーバーが完成しました。インターフェイスの GUID は、図 1 に示すように IDL ファイルのインターフェイス定義で属性として定義されています。

Figure 1 IMyCOMServer の定義 (Close Figure 1)
[
    object,
    uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
    nonextensible,
    helpstring("IMyCOMServer Interface"),
    pointer_default(unique)
]
interface IMyCOMServer : IUnknown {
    [helpstring("method MyCOMServerMethod")]
    HRESULT(MyCOMServerMethod)( void);
};

[
    uuid(3BA5DF7B-F8EF-4EDE-A7B5-5E7D13764ACC),
    version(1.0),
    helpstring("MSDNCOMServer 1.0 Type Library")
]
library MSDNCOMServerLib
{
    importlib("stdole2.tlb");
    [
        uuid(2D6D2821-A7FB-4F99-A61A-2286A47CD4D1),
        helpstring("MyCOMServer Class")
    ]
    coclass MyCOMServer
    {
        [default] interface IMyCOMServer;
    };
};

COM クライアントを作成する

では、ネイティブ (アンマネージ) クライアントを使用して COM サーバーにアクセスしてみましょう。ここでも、作業を簡単にするため、Visual Studio® とそのウィザードを使用します。空の Visual C++ プロジェクトを新しく作成します。ソリューション エクスプローラから [ソース ファイル] を選択し、新しい C++ ファイルを追加します。ファイルに COMClient.cpp といった名前を付け、このファイルに図 2 のコードを貼り付けます。

Figure 2 COMClient.cpp (Close Figure 2)
#include "MSDNCOMServer.h"
#include "wchar.h"
#include "MSDNCOMServer_i.c"

void main()
{
    // Initialize COM
    IMyCOMServer *pUnk = NULL;
    CoInitialize(NULL);

    // Create the com object
    HRESULT hr = CoCreateInstance(CLSID_MyCOMServer,
        NULL, CLSCTX_ALL, IID_IMyCOMServer, (void **)&pUnk);
    if(S_OK != hr)
    {
        wprintf(L"Error creating COM object ... %d", hr);
        return;
    }

    // Call a method on the COM object
    pUnk->MyCOMServerMethod();

    // Free resources and uninitialize COM
    pUnk->Release();
    CoUninitialize();
    return;
}

サーバーからヘッダー ファイル (MSDNCOMServer.h と MSDNCOMServer_i.c) を追加し、クライアントをコンパイルおよび実行します。コンソールに "Inside MyCOMServerMethod" と出力されます。これは COM サーバーからのメッセージです。

おめでとうございます。これで、COM サーバーを作成、コンパイルして DLL を生成し、COM クライアントを作成して、クライアントから COM サーバーのメソッドを呼び出すところまで実行しました。

Tlbimp を使用して COM DLL を変換する

COM DLL は作成できたので、この DLL にマネージ クライアントからアクセスする方法を考えてみましょう。Visual Studio コマンド プロンプトを開き、COM DLL を作成したディレクトリに移動して、次のコマンドを実行します。

tlbimp MSDNCOMServer.dll

Tlbimp.exe は .NET Framework SDK に付属するタイプ ライブラリ インポータ ツールです。このコマンドを実行すると、MSDNCOMServerLIB.dll というマネージ DLL が生成されます。この DLL は、アンマネージ COM DLL のマネージ ラッパーとして機能します。

アンマネージ クライアントは既に実行されているので、元のアンマネージ COM DLL と Tlbimp.exe によって生成されたマネージ DLL について詳しく見てみましょう。図 3 に、元の COM DLL の Oleview からのダンプを示します。

Figure 3 MSDNCOMServer.dll からの Oleview ダンプ (Close Figure 3)
// Generated .IDL file (by the OLE/COM Object Viewer)
// typelib filename: MSDNCOMServer.dll
[
    uuid(3BA5DF7B-F8EF-4EDE-A7B5-5E7D13764ACC),
    version(1.0),
    helpstring("MSDNCOMServer 1.0 Type Library"),
    custom(DE77BA64-517C-11D1-A2DA-0000F8773CE9, 100663662),
    custom(DE77BA63-517C-11D1-A2DA-0000F8773CE9, 1158351043),
    custom(DE77BA65-517C-11D1-A2DA-0000F8773CE9, Created by MIDL version
           6.00.0366 at Fri Sep 15 13:10:42 2006)
]
library MSDNCOMServerLib
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface IMyCOMServer;

    [
      uuid(2D6D2821-A7FB-4F99-A61A-2286A47CD4D1),
      helpstring("MyCOMServer Class")
    ]
    coclass MyCOMServer {
        [default] interface IMyCOMServer;
    };

    [
      odl,
      uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
      helpstring("IMyCOMServer Interface"),
      nonextensible
    ]
    interface IMyCOMServer : IUnknown {
        [helpstring("method MyCOMServerMethod")]
        HRESULT_stdcall MyCOMServerMethod();
    };

};

Lutz Roeder の .NET Reflector などの .NET デコンパイラを使用して、Tlbimp によって生成されるマネージ ライブラリを表示すると、図 4 のような内容が表示されます。

Figure 4 マネージ MSDNCOMServerLib (Close Figure 4)
namespace MSDNCOMServerLib
{
    [ComImport, Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE"),
     InterfaceType(1), TypeLibType(0x80)]
    public interface IMyCOMServer
    {
        [MethodImpl(MethodImplOptions.InternalCall,
         MethodCodeType=MethodCodeType.Runtime)]
        void MyCOMServerMethod();
    }

    [ComImport, CoClass(typeof(MyCOMServerClass)),
     Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE")]
    public interface MyCOMServer : IMyCOMServer
    {
    }

    [ComImport, TypeLibType(2),
     Guid("2D6D2821-A7FB-4F99-A61A-2286A47CD4D1"),
     ClassInterface(0)]
    public class MyCOMServerClass : IMyCOMServer, MyCOMServer
    {
        // Methods
        [MethodImpl(MethodImplOptions.InternalCall,
         MethodCodeType=MethodCodeType.Runtime)]
        public virtual extern void MyCOMServerMethod();
    }
}

多くのことが起こりました。まず、ライブラリ MSDNCOMServerLib が名前空間 MSDNCOMServerLib に変換されていることがわかります。また、インターフェイス IMyCOMServer には属性の GUID が付加され、コクラス MyCOMServer は、IMyCOMServer から派生するパブリック インターフェイス MyCOMServer と、インターフェイス IMyCOMServer、MyCOMServer を実装するクラス MyCOMServerClass に変換されています。

マネージ クライアントを作成する

ここまでで、COM サーバーとアンマネージ COM クライアントの作成プロセス、および Tlbimp を使用してアンマネージ DLL がマネージ DLL に変換されるときにどのような変換が起こるかを見てきました。では、Tlbimp によってマネージ コードにエクスポートされたライブラリを利用して、マネージ COM クライアントを作成してみましょう。

using System;
using MSDNCOMServerLib;

class Test
{
    static void Main()
    {
        MyCOMServerClass comServer = new MyCOMServerClass();
        comServer.MyCOMServerMethod();
    }
}

作成するマネージ コードは驚くほど簡潔です。このコードを ManagedClient.cs という名前のファイルに保存し、次のコマンドを使用してコンパイルしました。

csc ManagedClient.cs /reference:MSDNCOMServerLib.dll

実行可能ファイル ManagedClient.exe を実行すると、アンマネージ クライアントを実行したときと同じ結果が得られます。

構造体と文字列をマーシャリングする

それでは、サーバーへのパラメータとして文字列と構造体を追加してみましょう。そのためには、サーバー自体にもクライアントにも変更が必要です。これを実現するために、まずサーバーにメソッドを定義する必要があります。

最初に、IDL ファイルを編集して、図 5 に示すコードを追加します。構造体の定義を追加し、MyCOMServerMethod の宣言を、2 つのパラメータ (文字列と構造体) を受け取るように変更しました。

Figure 5 MyCOMServerMethod を変更する (Close Figure 5)
struct MyTestStruct
{
    int nIndex;
    [string] WCHAR* sName;
    int nValue;
};

[
    object,
    uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
    nonextensible,
    helpstring("IMyCOMServer Interface"),
    pointer_default(unique)
]
interface IMyCOMServer : IUnknown{
    [helpstring("method MyCOMServerMethod")]
    HRESULT(MyCOMServerMethod)(
        BSTR *sTestString, struct MyTestStruct *pMyTestStruct);
};

次に、サーバーのヘッダー ファイルを編集して、IDL で定義されている次のようなメソッドのプロトタイプを追加します。

class ATL_NO_VTABLE CMyCOMServer :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMyCOMServer, &CLSID_MyCOMServer>,
    public IMyCOMServer
{
public:
    STDMETHOD(MyCOMServerMethod)(
        BSTR *sTestString, struct MyTestStruct *pMyTestStruct);
};

メソッドの実装を変更して、プロトタイプの変更内容を反映し、渡される文字列を操作するコードを追加します。最終的なコードを図 6 に示します。これでサーバーへの変更は完了したので、再コンパイルします。

Figure 6 変更した CMyCOMServer (Close Figure 6)
// MyCOMServer.cpp : Implementation of CMyCOMServer
#include "stdafx.h"
#include "MyCOMServer.h"

// CMyCOMServer
STDMETHODIMP CMyCOMServer::MyCOMServerMethod(
    BSTR *sString, struct MyTestStruct *pStruct)
{
    if((NULL == sString) || (NULL == pStruct) || (NULL ==
        pStruct->sName))
    {
        wprintf_s(L"Invalid input\r\n");
        return E_FAIL;
    }

    // Print the input to the COM Method
    wprintf_s(
        L"Inside MyCOMServerMethod: Param-1 = %s Param-2 = %s\r\n",
        *sString, pStruct->sName);

    // Modifying the input string
    SysFreeString(*sString);
    *sString = SysAllocString(L"Changed String from COM Server");
    if(NULL == *sString)
    {
        wprintf_s(L"Allocation failed ...\r\n");
        return E_FAIL;
    }

    // Modifying the string in the structure
    CoTaskMemFree(pStruct->sName);
    pStruct->sName = reinterpret_cast<WCHAR *>
        (CoTaskMemAlloc(sizeof
             (L"Changed Structure from COM Server")+10));
    if(NULL == pStruct->sName)
    {
        wprintf_s(L"Allocation failed ...\r\n");
        return E_FAIL;
    }

    size_t size = 0;
    size = wcslen(L"Changed Structure from COM Server");
    wcscpy_s(pStruct->sName, size+1,
        L"Changed Structure from COM Server");

    //Print the changed strings in COM Server
    wprintf_s(
        L"Exiting MyCOMServerMethod: Param-1 = %s Param-2 = %s\r\n",
        *sString, pStruct->sName);
    return S_OK;
}

マネージ クライアントの新しい COM メソッドにアクセスするために、マネージ クライアントが格納されているディレクトリに DLL をコピーします。次のコマンドを再実行して、新しい MSDNCOMServerLib.dll を生成します。生成した MSDNCOMServerLib.dll には新しいメソッドが含まれています。

tlbimp MSDNCOMServer.dll

Ildasm を使用すると、新しいメソッドのシグネチャや実装を確認することができます。Ildasm は .NET Framework SDK に付属するツールの 1 つです。確認してみると、図 7 に示したコードがマーシャラによって呼び出しに追加されていることがわかります。

Figure 7 COM サーバーのマーシャリング コード (Close Figure 7)
namespace MSDNCOMServerLib
{
    [ComImport, Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE"),
     InterfaceType(1), TypeLibType(0x80)]
    public interface IMyCOMServer
    {
        [MethodImpl(MethodImplOptions.InternalCall,
         MethodCodeType=MethodCodeType.Runtime)]
        void MyCOMServerMethod(
            [MarshalAs(UnmanagedType.BStr)] ref string sTestString,
            ref MyTestStruct pMyTestStruct);
    }

    [ComImport, CoClass(typeof(MyCOMServerClass)),
     Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE")]
    public interface MyCOMServer : IMyCOMServer { }

    [ComImport, TypeLibType(2),
     Guid("2D6D2821-A7FB-4F99-A61A-2286A47CD4D1"),
     ClassInterface(0)]
    public class MyCOMServerClass : IMyCOMServer, MyCOMServer
    {
        // Methods
        [MethodImpl(MethodImplOptions.InternalCall,
         MethodCodeType=MethodCodeType.Runtime)]
        public virtual extern void MyCOMServerMethod(
            [MarshalAs(UnmanagedType.BStr)] ref string sTestString,
            ref MyTestStruct pMyTestStruct);
    }

    [StructLayout(LayoutKind.Sequential, Pack=4)]
    public struct MyTestStruct
    {
        public int nIndex;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string sName;
        public int nValue;
    }
}

入力の ref 文字列が入出力 BSTR にマーシャリングされています。構造体はマーシャリングされていませんが、構造体の中の文字列は LPWStr にマーシャリングされています。

これでサーバーも、Tlbimp を実行して生成したマネージ DLL も準備できたので、マネージ クライアントの変更に進みます。文字列と構造体を渡し、戻り値が変更されたかどうかをチェックするように変更します。図 8 に、変更したクライアント コードを示します。

Figure 8 文字列と構造体を渡す (Close Figure 8)
using System;
using MSDNCOMServerLib;
using System.Runtime.InteropServices;

class Test
{
    [STAThread]
    static void Main()
    {
        // Calling COM methods from managed Client
        String param1 = "Original String from Managed Client";

        MyTestStruct param2 = new MyTestStruct();
        param2.sName = "Original Structure from Managed Client";

        MyCOMServerClass comServer = new MyCOMServerClass();
        comServer.MyCOMServerMethod(ref param1, ref param2);

        Console.WriteLine("Param1: {0} Param2: {1}\r\n\r\n",
            param1, param2.sName);
    }
}

マネージ クライアントはまだ簡潔なままです。文字列と、アンマネージ DLL からインポートされた構造体のインスタンスを作成しています。それでは、上記のコードを今までと同じ方法でコンパイルし、実行可能ファイルを実行しましょう。次のような出力結果が得られます。

Inside MsyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from COM Server

マネージ コードの Main からの呼び出しがアンマネージ サーバーに届き、入力として渡されたものが出力されます。サーバーでは文字列を変更して出力します。変更された文字列がクライアントに返されているかどうかを確認するには、クライアントでも文字列を出力するようにします。

同じ COM DLL にアンマネージ関数 (COM 以外) を作成するには、まずメソッドを定義して、それをアンマネージ DLL に実装する必要があります。MSDNCOMServer.cpp を選択して、次のコードをファイルの末尾に貼り付けます。

STDAPI MyNonCOMMethod()
{
    wprintf_s(L"Inside MyNonCOMMethod\r\n");
    return true;
}

このメソッドにアクセスできるようにするには、このメソッドを DLL からエクスポートする必要があります。これは、COMServer.def ファイルにこの関数のエントリを追加することによって行います。

; COMServer.def : Declares the module parameters.

LIBRARY      "COMServer.DLL"

EXPORTS
    DllCanUnloadNow       PRIVATE
    DllGetClassObject     PRIVATE
    DllRegisterServer     PRIVATE
    DllUnregisterServer   PRIVATE
    MyNonCOMMethod        PRIVATE

サーバーをコンパイルして新しい DLL を生成します。生成した DLL には、COM メソッドとエクスポートされた関数が含まれています。

マネージ クライアントが格納されているディレクトリにこの DLL をコピーし、これに対して Tlbimp を実行します。Ildasm を使用すると、エクスポートされた関数がマネージ DLL に用意されていることが確認できます。

マネージ コードから P/Invoke を使用する

次は、P/Invoke を使用してアンマネージ関数を呼び出すように、マネージ クライアントを変更します。そのためには、マネージ コードでメソッドを定義して呼び出しを行う必要があります。マネージ メソッドの宣言はアンマネージの宣言を反映し、実際の実装がどこにあるかを CLR に通知します。図 9 に、コードの変更内容を示します。

Figure 9 P/Invoke を使用する (Close Figure 9)
class Test
{
    [DllImport("MSDNCOMServer.dll")]
    static extern bool MyNonCOMMethod();

    [STAThread]
    static void Main()
    {
        // Calling COM methods from managed Client
        ...

        // Calling unmanaged function using pInvoke
        MyNonCOMMethod();
    }
}

DllImport ステートメント、および MyNonCOMMethod メソッドへの呼び出しを元のマネージ クライアントに追加しました。クライアントをコンパイルして実行すると、次のような出力結果が得られます。

Inside MyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from COM Server

Inside MyNonCOMMethod

MyNonCOMMethod への呼び出しが確かに実行され、正しい結果が出力されています。

インターフェイスを使用してマネージ コードを呼び出す

最後に、逆 P/Invoke を使用してアンマネージ関数からマネージ呼び出しを行います。これはあまり一般的な手法ではありませんが、とても便利です。マネージ インターフェイスまたはデリゲートをアンマネージ コードに渡すことができ、アンマネージ コードからデリゲートやインターフェイスのメソッドを呼び出すことができます。まず、アンマネージ コードから呼び出しを行えるように、マネージ インターフェイスを準備しましょう。そのためには、ManagedClient.cs で図 10 に示すようなインターフェイスを宣言します。

Figure 10 マネージ インターフェイスを宣言する (Close Figure 10)
using System;
using MSDNCOMServerLib;
using System.Runtime.InteropServices;

[Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44"),
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
public interface IReversePInvoke
{
    void ManagedMethod(ref int val);
}

class ManagedClass : IReversePInvoke
{
    public void ManagedMethod(ref int nVal)
    {
        Console.WriteLine("ManagedMethod: " + nVal);
        nVal = 20;
        Console.WriteLine("ManagedMethod Value changed to: " + nVal);
    }
}

[DllImport("MSDNCOMServer.dll")]
static extern bool MyNonCOMMethod(IReversePInvoke ptr);

[STAThread]
static void Main()
{
    ...
    ManagedClass man = new ManagedClass();
    MyNonCOMMethod(man);
}

このコードで追加した宣言は、IReversePInvoke、このインターフェイスを実装するクラス、およびアンマネージ DLL の DLLImport メソッドを変更してインターフェイス ポインタを受け取るようにした DLLImport メソッドです。MyNonCOMMethod に ManagedClass インスタンスを渡すために、メソッドが呼び出される場所も変更しました。

これでクライアントをコンパイルすることができますが、まだ、インターフェイスをパラメータとして受け取るようにサーバーを変更していないので、正常に実行することはできません。そこで、マネージ実行可能ファイルに対して Tlbexp (.NET Framework SDK に含まれるツールで、タイプ ライブラリからマネージ アセンブリを生成します) を実行し、.tlb ファイルを生成します。生成したファイルを Oleview で開き、インターフェイスのプロトタイプをコピーして、サーバーの IDL ファイルに次のように貼り付けます。

[
    odl,
    uuid(7DAC8207-D3AE-4C75-9B67-92801A497D44)
]
interface IReversePInvoke : IUnknown {
    HRESULT _stdcall ManagedMethod([in, out] long* val);
};

これで、サーバーにインターフェイスを用意できました。メソッドのシグネチャを変更し、インターフェイスのマネージ メソッドへの呼び出しを追加する必要があります。その方法を次に示します。

STDAPI MyNonCOMMethod(IReversePInvoke *ptr)
{
    int param = 10;

    wprintf_s(L"Inside MyNonCOMMethod\r\n");
    HRESULT hr = ptr->ManagedMethod(&param);
    wprintf_s(L"Value in MyNonCOMMethod = %d\r\n", param);
    wprintf_s(L"Exiting MyNonCOMMethod\r\n");

    return true;
}

これで、マネージ インターフェイスを受け取りマネージ インターフェイスのメソッドを呼び出すアンマネージ関数ができました。これをコンパイルして DLL を生成し、この DLL に対して Tlbimp を実行して最新のマネージ相互運用機能 DLL を生成します。Tlbimp によって生成されたマネージ DLL に対してマネージ コードをコンパイルします。次のような出力結果が得られます。

Inside MyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from
COM Server

Inside MyNonCOMMethod
ManagedMethod: 10
ManagedMethod Value changed to: 20
Value in MyNonCOMMethod = 20
Exiting MyNonCOMMethod

呼び出しは確かにマネージ コードに対して行われ、期待どおりに "10" と出力されました。マネージ メソッドによって値は 20 に変更され、その値はアンマネージ関数で出力されます。

マネージ コードとアンマネージ コードをデバッグする

すべてのコードが正常に実行できたことを願っています。しかし、当然ながら、バグがまったく存在しないコードを作成できることはめったにありません。いつかは、相互運用機能を使用したコードのデバッグ方法を知る必要に迫られるときが来ます。

デバッグを成功させるには、コンピュータにシンボルがすべて読み込まれている必要があります。シンボルを使用すると、コードで何が起こっているかをデバッガがより適切に理解、解釈できるようになります。マイクロソフトでは、デバッガがマイクロソフト提供の DLL 用のシンボルにアクセスするために接続できる公開サーバーを提供しています。この公開シンボル サーバーを使用するように windbg デバッガを構成するには、まず windbg を実行します。

windbg managedclient.exe

次に、以下のコマンド構文を使用してシンボル サーバーを指定します。

.SymFix
.sympath+ http://msdl.microsoft.com/download/symbols
.Reload

次に、mscorwks DLL、COMServerLib DLL、および COMServer DLL が読み込まれたときに停止するブレークポイントを設定するように windbg に指示する必要があります。これを実現するには sxe ld コマンドを使用します。

sxe ld mscorwks
sxe ld COMServerLib
sxe ld COMServer

デバッガでのアプリケーション実行を開始するには、「g」 ("go" の意味) と入力して Enter キーを押します。アプリケーションの実行中、COMServer DLL が読み込まれるとアプリケーションはデバッガに割り込みを行うので、このときに別のブレークポイントを MyCOMServerMethod に設定することができます (ブレークポイントが実際に設定されたことを確認するには、bl コマンドを使用します)。

bp COMServer!CMyCOMServer::MyCOMServerMethod (wchar_t **,
    struct MyTestStruct *)

mscorwks は読み込まれたので、今度は windbg に SOS デバッガ拡張機能を読み込ませます。この拡張機能を使用すると、デバッガとマネージ アプリケーションの対話をより堅牢にすることができます。

.loadby sos mscorwks

COM サーバーから逆 P/Invoke を使用して呼び出される ManagedMethod メソッドにブレークポイントを設定するには、SOS の !bpmd コマンドを使用します。

!bpmd ManagedClient ManagedClass.ManagedMethod

こうしてブレークポイントを設定したら、再び g コマンドを使用して先に進みます。MyCOMServerMethod のアンマネージ ブレークポイントに到達したら、図 11 に示すように windbg の kL コマンドを使用して現在のスタック トレースを表示することができます (図 11 では、変数の値を調べるための他の windbg コマンドを使用する方法も紹介しています)。

Figure 11 スタック トレースを取得してメモリを調べる (Close Figure 11)
0:000> kL
ChildEBP RetAddr
0012f328 79f21268 MSDNCOMServer!CMyCOMServer::MyCOMServerMethod
0012f3fc 0090a28e mscorwks!CLRToCOMWorker+0x196
0012f438 00f800e5 CLRStub[StubLinkStub]@90a28e
01271c08 79e88f63 ManagedClient!Test.Main()+0x75
0012f490 79e88ee4 mscorwks!CallDescrWorker+0x33
0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3
0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c
0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20
0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call_RetArgSlot+0x18
0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220
0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6
0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398
0012ff68 79f05f61 mscorwks!ExecuteEXE+0x59
0012ffb0 79011b5f mscorwks!_CorExeMain+0x11b
0012ffc0 7c816fd7 mscoree!_CorExeMain+0x2c
0012fff0 00000000 KERNEL32!BaseProcessStart+0x23

0:000> dv
           this = 0x00fc2fb8
        sString = 0x0012f364
        pStruct = 0x00195ef8
           size = 0x12f330

0:000> dt sString
Local var @ 0x12f334 Type wchar_t**
0x0012f364
 -> 0x001a61e4  "Original String from Managed Client"

0:000> dt -r pStruct
Local var @ 0x12f338 Type MyTestStruct*
0x00195ef8
   +0x000 nIndex   : 0
   +0x004 sName    : 0x001a4f38  "Original Structure from Managed Client"
   +0x008 nValue   : 0

再び g コマンドで先に進んでいくと、ManagedMethod への呼び出し箇所で中断が発生するので、ここで SOS のコマンド !clrstack を使用してマネージ スタックを調べます。

0:000> !clrstack
OS Thread Id: 0x87c (0)
ESP       EIP
0012ef80 00f80168 ManagedClass.ManagedMethod(Int32 ByRef)
0012f148 79f1ef33 [GCFrame: 0012f148]
0012f2a0 79f1ef33 [ComMethodFrame: 0012f2a0]
0012f454 79f1ef33 [NDirectMethodFrameSlim: 0012f454] Test.MyNonCOMMethod(IReversePInvoke)
0012f464 00f80119 Test.Main()
0012f69c 79e88f63 [GCFrame: 0012f69c]

まとめ

詳細については、まずコミュニティ主導型の Web サイト PInvoke.net (英語) を参照してください。Adam Nathan の COM 相互運用機能に関する大作『.NET and COM: The Complete Interoperability Guide』 (英語) が復刻されました (彼のブログは Adam Nathan's Blog (英語) です)。WinDbg のダウンロードとリファレンスを含む、デバッグに関する一般的な情報はこちら、SOS デバッグ拡張機能 (SOS.dll) のリファレンスはこちら (英語) で参照できます。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Thottam R. Sriram はカナダのモントリオールにある Concordia 大学でコンピュータ サイエンスの修士号を取得しました。現在、彼は CLR チームのプログラム マネージャとして COM 相互運用機能に取り組んでいます。CLR チームに参加する前は、Windows チームに携わっていました。このコラムの執筆に力を貸してくれた、COM 相互運用機能チームのメンバである Mason Bendixen、Claudio Caldato、Alessandro Catorcini、Ben Gillis、および Snesha Arumugam に感謝します。


データ ポイント: 列式、データ関係、および計算

データ ポイント
列式、データ関係、および計算
John Papa

コードのダウンロード : : DataPoints2007_01.exe (191KB)

ADO.NET でできることは、データベースからのデータ取得やデータベースへのデータ保存だけではありません。ADO.NET には、ビジネス ロジック用や、実際のアプリケーションでの表示用にデータを操作できる多くの機能が用意されています。このコラムの例で示すように、ADO.NET はデータ アクセス用のツールであるだけでなく、データ操作にも使用できます。

今月のコラムでは、ADO.NET を使用したデータ操作について最もよく寄せられるいくつかの質問にお答えします。今回は、式に基づく列と DataRelation を使用して特定の目的を実現する方法について説明します。また、DataTable の Compute メソッドや SetOrdinal メソッドなど、他のいくつかの機能を使用して一般的なビジネス ニーズに対処する方法についても説明します。

Q DataTable を使用して、注文情報を個別に、およびロールアップされたグループ (日、月、および年ごと) で表示することを考えています。また、注文情報を編集できるようにし、編集直後に変更を親 DataTable の合計値に反映する必要があります。各データ セットは、フォーム上の別のグリッドに表示するつもりです。データを DataTable に読み込んだ後、ADO.NET の DataColumn の式を使用して、グループごとに注文合計を持つ、グループ化された情報を作成する方法はありますか。

A SQL ステートメントで集計式を使用するか、式に基づく DataColumn を作成することによって、集計フィールドを簡単に作成できます。これら 2 つの方法は、動作と公開する機能が異なります。

今回のシナリオと要件から、SQL ステートメントで集計列を使用する方法は最も効果的な解決策とはいえません。式に基づく DataColumn を一連の DataTable に追加する方法が、適切な解決策です。

この問題を詳しく説明する前に、最終目標を確認しましょう (図 1 を参照)。このサンプル フォームには、5 つのグリッドが表示されています。一番上のグリッドには、個別の注文明細を含みます。このグリッドでは、各注文行の価格、数量、および値引を編集できます。これらの値を編集すると、その行の価格が計算された後、他の 4 つのグリッドにこの変更を反映して、更新後の合計値が自動的に再計算され、表示されます。2 番目のグリッドには注文ごとの行を含み、変更後の注文価格の合計を示す集計列があります。3 ～ 5 番目のグリッドには、各注文の合計が日、月、および年ごとにそれぞれ表示されます。

図 1 日、月、および年ごとに表示された注文合計
図 1 日、月、および年ごとに表示された注文合計(画像を縮小するには、ここをクリックします)
図 1 日、月、および年ごとに表示された注文合計
図 1 日、月、および年ごとに表示された注文合計 (画像を拡大するには、ここをクリックします)

各グリッドは異なる DataTable にバインドされます。1 つ目の DataTable では、注文明細行をデータベースから読み込み、そこに式に基づく列を追加します。DataAdapter の Fill メソッドを使用してデータをこの DataTable に読み込んだ後、decimal 型の ExtendedPrice という列を追加します。この列は、次のように各 DataRow の UnitPrice、Quantity、および Discount の値に基づいて値を計算する式列として定義されます。

ds.Tables["OrderLines"].Columns.Add("ExtendedPrice",
    typeof(decimal), "(UnitPrice * Quantity) * (1 - Discount)");

OrderLines DataTable には、このフォームの中核となるデータが含まれています。この DataTable を読み込んで式列を作成したら、グループ化する最初の DataTable にデータを格納して DataSet に追加できます (図 2 を参照)。SQL ステートメントでは、各注文の OrderID と OrderDate を取得してから、DataTable に結果を格納します。OrderTotal 列をこの DataTable に追加する前に、まず DataRelation を作成して、この Orders DataTable の式から OrderLines DataTable の列にアクセスできるようにする必要があります。

Figure 2 合計を計算する式列 (Close Figure 2)
using(SqlCommand cmd = new SqlCommand("SELECT o.OrderID, o.OrderDate " +
    "FROM Orders o WHERE YEAR(o.OrderDate) = @year " +
    "ORDER BY o.OrderDate DESC", cn))
{
    cmd.CommandType = CommandType.Text;
    cmd.Parameters.AddWithValue("@year", year);
    SqlDataAdapter adpt = new SqlDataAdapter(cmd);
    DataTable orders = new DataTable("Orders");
    adpt.Fill(orders);
}
ds.Tables.Add(orders);
ds.Relations.Add("Orders2OrderLines", ds.Tables["Orders"].Columns["OrderID"], ds.Tables["OrderLines"].Columns["OrderID"]);
orders.Columns.Add("OrderTotal", typeof(decimal),
    "Sum(Child.ExtendedPrice)");

DataRelation は、2 つの DataTable の OrderID 列間に設定します。これで、次の式の結果が格納される OrderTotal 式列を作成して Orders DataTable に追加できます。

Sum(Child.ExtendedPrice)

上記のコードでは、かっこの内側の処理が実行されてから外側の処理が実行されます。Child 演算子は、子テーブル (OrderLines DataTable) にアクセスして ExtendedPrice 列を取得するよう OrderTotal DataColumn に指示します。また Sum 演算子は、再度 DataRelation を使用して OrderID の値が親 DataTable の OrderID に対応する OrderLines DataTable の行の ExtendedPrice 列にアクセスし、そこに格納されている値の合計を計算します。最終的に、各注文の合計が Orders DataTable の OrderTotal 列に表示されます。

DataRelation が複数存在する場合は、特定の DataRelation を示すように構文を変更する必要があります。図 2 の例を次のように変更します。

Sum(Child(Orders2OrderLines).ExtendedPrice)

同じ手法で、OrdersByDay DataTable にデータを格納できます。OrdersByDay DataTable には、データベースから受注日の重複しない一覧を読み込みます。次に、この OrdersByDay DataTable の OrderDate 列と Orders DataTable の OrderDate 列の間に DataRelation を設定します。その結果、次のような式を使用して、OrdersByDay DataTable の OrderTotalByDay 列に注文合計をロールアップできます。

ds.Relations.Add("OrdersByDay2Orders", ds.Tables["OrdersByDay"].Columns["OrderDate"],
 ds.Tables["Orders"].Columns["OrderDate"]);

dayOrders.Columns.Add("OrderTotalByDay", typeof(decimal),
    "Sum(Child.OrderTotal)");

最後の 2 つのグリッドは、同じ方法で読み込んだ DataTable にバインドします。この手法を拡張することで、他の集計を表示できます。たとえば、基本テーブルから顧客情報を取得すれば、特定の顧客の注文合計を含むグリッドを表示するのは簡単です。DataView を使用して、グリッドを式列に基づいてフィルタ処理することもできます。

Q 親 DataTable (Orders) と子 DataTable (Order Lines) があります。親 DataTable には、OrderTotal の式列が含まれています。親テーブルのいくつかの列を子テーブルの各行に表示する必要があります。親から値を取得する方法はありますか。

A 式に基づく列で Child 演算子を使用すると、子 DataTable の列にアクセスできますが、同様に Parent 演算子を使用すれば、親 DataTable の列にアクセスできます。たとえば、親テーブルの列の値にアクセスするには、前の例のコードを変更して次のコードを追加します。

ds.Tables["OrderLines"].Columns.Add("OrderTotalForOrderLinesTable",
    typeof(decimal), "Parent.OrderTotal");

このコードでは、Orders DataTable から OrderTotal の値を取得する式列を OrderLines という子テーブルに追加します。子と親の DataTable を作成し、これらの DataTable 間に DataRelation を設定して、OrderTotal 列を作成した後、この新しい列を作成する必要があります。この方法は、グリッドにバインドされる子テーブルで DataView を作成する際によく使用されます。

式列で Parent 演算子を使用すると、親 DataTable のデータが子 DataTable に複製されます。2 つのテーブルは同期されるので、親 DataTable の値が変更されると、子テーブルにも変更後の値が表示されます。

Q DataTable の DefaultView にバインドされた DataGridView があります。DataTable のソースは、アプリケーションのデータベースからデータを取得するストアド プロシージャです。すべてのフィールドが特定の順序で返されますが、これらのフィールドを異なる順序で DataGridView に表示する必要があります。DataGridView の列の順序を簡単に変更する方法はありますか。

A SQL ステートメント内のフィールドの順序を変更することが実用的でない場合、DataTable 内の列の順序の変更が必要になることがあります。DataColumn では、DataTable 内の列の順序を変更できる SetOrdinal メソッドを公開しています。

順序位置は 0 から始まるので、1 つ目の列が 4 番目に表示されるように移動するには、次のコードを使用します。

myDataTable.Columns[0].SetOrdinal(3);

次のように、列を順序位置ではなく列の名前で参照することもできます。

myDataTable.Columns["OrderID"].SetOrdinal(3);

位置を変更する列の格納先を確保するために、順序位置が 3 番目より前の列が 1 つずつ前の位置に移動されます。

Q 顧客の一覧を表示する画面があります。ユーザーはこの一覧から特定の顧客を選択し、その顧客のすべてのデータを表示することができます。その後、一覧から別の顧客を選択して、その顧客の情報を表示することができます。データベースに 1 回だけアクセスしてすべての顧客データを DataTable に読み込む方法か、顧客の一覧と ID だけを取得し、ユーザーが顧客を選択したらその顧客の情報をデータベースから取得する方法を使用できます。どちらの方法が適切でしょうか。

A これはよく寄せられる質問です。大量のデータを 1 回で取得する必要がありますか、それとも少量のデータを複数回取得する必要がありますか。顧客の一覧とすべての顧客情報をデータベースから取得し、その結果を DataSet のメモリに格納すると、使用しない情報も大量に取得することになります。しかし、データベースのアクセス回数は少なくなります。たとえば、顧客テーブルに 1,000 人の顧客が格納されていて、顧客ごとに 40 個の列を取得すると、40,000 個の値が取得され、DataTable に格納されます。これに対して、顧客の一覧に必要なのは表示値 (CompanyName など) と ID (CustomerID など) だけです。残りの列は、ユーザーが顧客の一覧から特定の顧客を選択した場合のみ画面に表示します。

もう 1 つの選択肢は、CompanyName と CustomerID のみを含む顧客の一覧を読み込む方法でしたね。この場合、ユーザーが顧客を選択すると、アプリケーションは再度データベースに呼び出しを実行し、特定の顧客の詳細情報を取得することになります。この手法では、最初に読み込まれるデータは非常に少なくなりますが、データベースへのアクセス回数は増えます。

どちらの解決策が適しているかどうかは、その他の要素に応じて決まります。たとえば、顧客データが小さく、すばやく取得でき、負荷テストでもパフォーマンスへの影響がなかった場合は、最初にすべてのデータを取得する手法が適しています。しかし、この解決策にはデータが頻繁に変更される場合にデータがすぐに古くなるという大きな欠点があります。したがって、最初にすべてのデータをメモリに読み込むことを選択する場合、データの読み込みにかかる時間、このデータが使用するメモリ領域の大きさ、実際に読み込むデータの大きさ、およびデータが読み込まれている間にデータベースのデータが変更される可能性があるかどうか (画面に表示されるデータが古くなる原因になります) を検討してください。一般にこの方法は、データの使用中に変更が行われない、小さいデータ セットにのみ使用することをお勧めします。

2 つ目の選択肢は、ユーザーが要求したときだけ各顧客の詳細情報を取得する方法です。通常、この手法を使用すると、読み込むデータが少なくなるので、より短時間で一覧を読み込むことができます。また、固定的に読み込まれるデータは顧客の一覧だけなので、より新しいデータが提供されます。この手法の欠点は、ユーザーが顧客を選択するたびにアプリケーションからデータベースにアクセスすることです。このため、キー フィールド (CustomerID) を使用して顧客データを取得するなど、顧客の詳細を取得する呼び出しを最適化してください。こうした顧客データを取得する呼び出しが明らかな遅延を引き起こしている場合、ネットワーク帯域幅、クエリ自体、またはアプリケーションのアーキテクチャを介してデータを渡すときの問題が、この遅延の原因になっている可能性があります。この手法に決めた場合は、こうした遅延が発生したときに、遅延の原因を調べ、その問題を解決できるかどうかを判断する必要があります。

結局、この種の決定には単純な答えはありません。ほとんどの場合、状況によって異なります。しかし通常は、最小限のデータしか含まない一覧を読み込み、ユーザーが特定のエントリを選択したときに追加のデータを読み込む方法が適しているようです。ここでの問題はパフォーマンスだけではないことに注意してください。データが新しいか古いかも検討する必要があります。

Q DataTable にバインドされた売上情報が読み込まれる DataGridView を用意する必要があります。ユーザーはこのアプリケーションで、グリッド内の売上データに関する計算を実行したり、こうした計算に含める行や列を選択したりできる必要があります。データベースに再度アクセスしないで、DataTable に関する計算を実行する方法はありますか。

A この場合、Compute メソッドを使用することで目的を達成できます。式に基づく列は一度に 1 つの行に対する計算しか実行しませんが、DataTable の Compute メソッドに式とフィルタを渡して実行すると、複数の行に対する計算を実行できます。フィルタで Compute メソッドの計算対象となる列を限定し、式に実行する集計関数式を指定します。たとえば、米国在住の顧客の平均注文合計を計算する列を DataTable に追加できます。Compute メソッドは、データベースに再度アクセスすることなく複数の行に対してすばやく計算を実行するための優れた手段です。

図 3 は、注文の一覧と、各注文の合計が表示されたサンプル フォームを示しています。このサンプルには、式とフィルタに必要なすべての基本要素が含まれています。ユーザーは [Filter] グループ ボックスの左側で、フィルタ処理するフィールド、フィールドに適用する演算子、およびフィルタ条件を選択できます。次のようにフィールドの一覧を DataColumn の一覧にバインドすることによって、フィールドの一覧にデータを格納します。

DataColumn[] colList = new DataColumn[orders.Columns.Count];
orders.Columns.CopyTo(colList, 0);
ddlFilterColumn.DataSource = colList;
ddlFilterColumn.DisplayMember = "ColumnName";

図 3 Compute メソッドを使用した計算
図 3 Compute メソッドを使用した計算(画像を縮小するには、ここをクリックします)
図 3 Compute メソッドを使用した計算
図 3 Compute メソッドを使用した計算 (画像を拡大するには、ここをクリックします)

このコードでは、DataColumn 配列を作成し、この配列に DataTable の列を格納して、ComboBox にバインドしています。DataColumn の配列を ComboBox にバインドすることにより、選択した DataColumn の他の使用可能なプロパティにアクセスできます。たとえば、選択する列が文字列の場合、値を引用符で囲む必要があります。DataColumn は ComboBox にバインドされているので、値を引用符で囲む必要があるかどうかをコードで判断できます。

[Calculation] グループ ボックスには、集計関数の一覧と、選択した集計関数が集計の対象にする列の一覧が含まれます。列の一覧は、ddlFilterColumn ComboBox と同じ方法で ddlCalculationColumn ComboBox にバインドします。集計関数の一覧には、Compute メソッドで使用できる 7 つの関数が読み込まれます。読み込まれる関数は、Sum、Count、Avg、Min、Max、StDev、および Var です。

ボタンがクリックされると、図 4 に示したコードが実行されます。このコードでは、まずフィルタ用に選択した列の種類を判断します。列が文字列または日付の場合、条件値を引用符で囲みます。フィルタと式を構成してから、それらを Compute メソッドに渡すと、結果がテキスト ボックスに渡されます。

Figure 4 計算を実行する (Close Figure 4)
if (!ValidateControls()) return;

Type colType = ((DataColumn) (ddlFilterColumn.SelectedItem)).DataType;
string filterCriteria = string.Empty;
if (ddlFilterOperator.Text == "Like")
{
    filterCriteria = string.Format("'{0}%'", txtFilterCriteria.Text);
}
else
{
    filterCriteria = (colType.Name == "String" ||
                      colType.Name == "DateTime") ?
        string.Format("'{0}'", txtFilterCriteria.Text) :
                               txtFilterCriteria.Text;
}
string filter = string.Format("{0} {1} {2}", ddlFilterColumn.Text,
    ddlFilterOperator.Text, filterCriteria);
string expression = string.Format("{0}({1})", ddlAggregate.Text,
    ddlCalculationColumn.Text);

object value = this.orders.Compute(expression, filter);
txtResult.Text = value.ToString();

ご意見やご質問は、

John mmdata@microsoft.com.

まで英語でお送りください。
John Papa は、ASPSOFT (aspsoft.com) のシニア .NET コンサルタントです。夏はほぼ毎晩家族と彼の忠犬 Kadi と一緒にヤンキースを応援して過ごす熱狂的な野球ファンでもあります。C# MVP でもある John は、ADO、XML、および SQL Server に関する何冊かの書籍の執筆を担当しました。また、主に業界のカンファレンス (VSLive など) での講演やブログ (codebetter.com/blogs/john.papa) (英語) などで活躍しています。


Cutting Edge: サーバー側の ASP.NET ページ

Cutting Edge
サーバー側の ASP.NET ページ
Dino Esposito

コードのダウンロード : : CuttingEdge2007_01.exe (276KB)
コンテンツ
Temporary ASP.NET Files フォルダの内容
情報保持ファイル
ページ クラスの動的ソース コード
ランタイム パブリック API
エクスプローラ ツールの作成
サンプル ページの調査
まとめ

ASP.NET 開発者であれば、ASP.NET が .aspx リソース内のコードを処理する方法、そのマークアップがその場で解析され Visual Basic® クラスまたは C# クラスに変換される方法などについて、だいたいご存じだと思います。しかし、次のことはどうでしょう。ASP.NET によって生成されたファイルはどこに保存され、ページ要求を処理するときにどのように使用されるのでしょうか。先月、この処理について調べ始めました。今月は、よくある落とし穴にはまらないように、サーバーで何が行われるのかを調べていきます。また、ASP.NET の一時ファイルの保存場所と、ページの応答を処理するときに使用する、動的に生成されるクラスのソース コードを確認します。さらに、ASP.NET 2.0 または ASP.NET AJAX (以前のコード ネームは "Atlas") アプリケーションで使用できるエクスプローラ ツールを作成します。このツールでは、実際にページで実行されるコードを調べてデバッグできます。作業に取りかかる前に、いくつかのことを知っておく必要があります (先月のコラムと同様、今回の内容の大半は、ASP.NET のしくみの中でも文書化されていない詳細に基づいています。このような実装の詳細は、今後のバージョンの Microsoft® .NET Framework で変更される可能性があります)。

Temporary ASP.NET Files フォルダの内容

ASP.NET ページ要求を処理するとき、少しだけ一時ファイルが必要です。Web サーバー コンピュータに ASP.NET 2.0 をインストールすると、次のようなフォルダ階層が作成されます。

%WINDOWS%\Microsoft.NET\Framework\v2.0.50727

ここでのバージョン番号は、ASP.NET 2.0 の製品版を示しています。ASP.NET の各リリース バージョン (各バージョンの中間ビルドを含む) には、一意のバージョン番号が付けられます。また、サイド バイ サイドの実行をサポートするために、バージョンによって異なるツリーのフォルダが作成されます。そのため、アプリケーションがどのバージョンの ASP.NET 用に設計されているかを指定することが非常に重要です。ASP.NET 1.x および ASP.NET 2.0 で実行するアプリケーションは、それぞれが物理的に別のフォルダに基づいています。Microsoft.NET\Framework フォルダには、インストールした ASP.NET のバージョンの数だけ vX.X.XXXX 形式の名前が付いたサブフォルダが見つかります (図 1 参照)。

図 1 ASP.NET 1.0、1.1、2.0、および 3.0 のランタイム ファイル
図 1 ASP.NET 1.0、1.1、2.0、および 3.0 のランタイム ファイル(画像を縮小するには、ここをクリックします)
図 1 ASP.NET 1.0、1.1、2.0、および 3.0 のランタイム ファイル
図 1 ASP.NET 1.0、1.1、2.0、および 3.0 のランタイム ファイル (画像を拡大するには、ここをクリックします)

インストールしたバージョンのルート フォルダには、多くの子ディレクトリが見つかります。CONFIG フォルダには、machine.config やすべてのサイトで使用される基本 web.config など、コンピュータの構成ファイルが格納されます。ASP.NETWebAdminFiles には、Visual Studio® 2005 から実行できる Web サイト管理ツールを構成するソース ファイルが格納されます。最後に、Temporary ASP.NET Files フォルダには、ページとリソースを処理するために作成される、すべての一時ファイルとアセンブリが格納されます。このサブツリーを調べて、Web ページ用に動的に作成されたファイルを見つける必要があります。Temporary ASP.NET Files ディレクトリは動的に作成されたファイルが格納される既定の場所ですが、web.config ファイルの <compilation> セクションを使用すると、アプリケーションごとにこの場所を構成できます。

<compilation tempDirectory="d:\MyTempFiles" />

アプリケーションがコンピュータ上で初めて実行されるとき、一時ファイルのディレクトリに新しいサブフォルダが作成されます。このコンパイル サブフォルダの名前はアプリケーションの IIS 仮想ディレクトリの名前と一致します。Visual Studio 2005 の埋め込み Web サーバーでアプリケーションの単純なテストを行う場合、サブフォルダには Web アプリケーションのルート フォルダの名前が使用されます。Web サーバーの root フォルダからページを呼び出すと、一時ファイルは root サブフォルダに見つかります (図 2 参照)。

図 2 テスト Web サーバー上の Web アプリケーション
図 2 テスト Web サーバー上の Web アプリケーション(画像を縮小するには、ここをクリックします)
図 2 テスト Web サーバー上の Web アプリケーション
図 2 テスト Web サーバー上の Web アプリケーション (画像を拡大するには、ここをクリックします)

アプリケーションのコンパイル サブフォルダには、ハッシュされた名前に基づくディレクトリのコレクションがあります。一時ファイルが見つかる通常のパスは次のとおりです (最後の 2 つのディレクトリには偽の現実的な名前が付けられています)。

\v2.0.50727\Temporary ASP.NET Files\MyWebApp\3678b103\e60405c7

次のステートメントを使用すると、特定のアプリケーションの一時ファイルが配置されているサブフォルダへのパスをプログラムで取得できます。

Dim tempFilesFolder As String = HttpRuntime.CodegenDir

アプリケーションが変更されて再コンパイルが必要になったとき、ASP.NET はコンパイル フォルダを清掃して定期的に古いリソースを削除します。ただし、特にテスト コンピュータでは、Temporary ASP.NET Files のルートにあるサブツリーのサイズが大幅に増加する場合があります。管理者の方は、Temporary ASP.NET Files にあるディレクトリに目を配り、すべてのディレクトリが現在アクティブなアプリケーションを参照していることを確認することをお勧めします。アクティブなアプリケーションのサブツリーを誤って削除してしまうことがあっても、心配しないでください。プリコンパイルされたページとリソースはすべて失いますが、アプリケーションは最初のコンパイル状態にリセットできます。ただしその次の要求で、構成に従ってページ単位またはページのバッチごとに新しいコンパイルがトリガされるため、結局は情報もページも失わないことになります (ただ、初回はこの要求で遅延が発生します)。それでは、特定のアプリケーションのコンパイル フォルダの内容を見てみましょう。

情報保持ファイル

アプリケーションの各ページのページ コンパイル処理によって、次の名前のファイルが生成されます。

[page].aspx.[folder-hash].compiled

[page] プレースホルダは .aspx リソースの名前を表します。[folder-hash] プレースホルダはファイル名を一意なものにするハッシュ値です。この値によって、最初は個別のフォルダに属していた同じ名前のファイルからあいまいさが排除されます。このようなファイルには ASP.NET ランタイムがページ要求を処理するときに使用される HTTP ハンドラのアセンブリと型の名前をすばやく取得できるようにする重要な情報が含まれます。そのため、情報保持ファイルと呼ばれます。また、情報保持ファイルには、ファイルの内容が前回アクセス時以降に変更されたかどうかを検出するために使用されるファイル ハッシュ値が含まれます。

アプリケーションを構成するすべての .aspx ページは同じ一時フォルダにコンパイルされます。それらの .aspx ページが同じ名前で異なるフォルダに存在する場合も同様です。それはどんなしくみなのでしょうか。アプリケーションに test.aspx という名前の 2 つのページが含まれていて、各ページが異なるフォルダ (Folder1 と Folder2) に格納されているとします。両方のページは同じ一時フォルダにコンパイルされますが、ハッシュ値によって区別できます。ハッシュ値はファイル名だけでなくパス情報にも基づいて計算されるのでそれぞれが異なる値になります。そのため、2 つの test.aspx ページには、次のようにフォルダ ハッシュ値だけが異なる情報保持ファイル名が付けられることになります。

Test.aspx.cdcab7d2.compiled
Test.aspx.9d86a5d7.compiled

ハッシュ値の内部に格納されたキャッシュによって、ASP.NET ランタイムは特定のページ URL のハッシュ値を識別し、対応する情報保持ファイルをすばやく見つけることができます。情報保持ファイルが見つからない場合、ASP.NET はその場でページをコンパイルします。この動作は、プリコンパイルを行わずにアプリケーションを配置した場合に起こります。一方、サイトをプリコンパイルすると、各構成ページの情報保持ファイルが作成され Bin フォルダに配置されます。

情報保持ファイルはプレーンな XML ファイルです。図 3 はサンプル情報保持ファイルの内容を示しています。

Figure 3 情報保持ファイルのサンプル (Close Figure 3)
<?xml version="1.0" encoding="utf-8"?>
<preserve resultType="3" virtualPath="/WebSite2/Default.aspx"
          hash="bce9c4d05"
          filehash="2831dc3af5add65c"
          flags="110000"
          assembly="App_Web_ap-sequk"
          type="ASP.default_aspx">
    <filedeps>
        <filedep name="/WebSite2/Default.aspx" />
        <filedep name="/WebSite2/Default.aspx.vb" />
    </filedeps>
</preserve>

図 4 はファイルの属性を詳述しています。<fileDeps> セクションは現在のページが依存しているファイルを示しています。依存関係が変更されると、ページの再コンパイルが行われます。FileHash 値は依存関係の状態のスナップショットを表し、Hash は現在のページ ファイルの状態のスナップショットを表します。注目すべきは、ファイル変更通知に完全に基づいているファイルへの変更をその場で検出するメカニズムは、Web アプリケーションを停止または再起動するとすぐに失敗することです。ページと依存関係の状態をハッシュ値で保持することにより、いつでも変更を検出できます。

Figure 4 情報保持ファイル内の属性 (Close Figure 4)

属性 | 説明
Assembly | コンパイルされたページを含んでいるアセンブリの名前。
FileHash | ページが依存するすべてのソース ファイルに変更が加えられたかどうかを確認するために使用されるハッシュ値。
Flags | ハッシュ値を作成して依存ファイル内の変更を監視するなど、内部操作の状態を保持するために使用される整数値。
Hash | 現在のページが変更されたかどうかを確認するために使用されるハッシュ値。
ResultType | この情報保持ファイルが作成されたときのビルド結果の種類を示します。利用可能な値は BuildResultTypeCode というパブリックでない列挙から取得します。
Type | コンパイルされたページを表す種類の名前。
VirtualPath | コンパイル対象ページの仮想パス。

型属性は、要求の処理に使用される、動的に作成されるクラスの名前を設定します。既定の型名は ASP.[page]_aspx で、[page] はページ ファイルの名前を表します。.aspx ファイルの @Page ディレクティブに ClassName 属性を設定することにより、この名前を変更できることに注意してください。ルート名前空間は変わらないため、型名は ASP.[ClassName] なります。

アセンブリ属性は、要求を処理するためにページ クラスを含んでいる、動的に作成されたアセンブリの名前を示します。このようなアセンブリの名前と内容は、web.config ファイルの <compilation> セクションの設定によって異なります。

既定では、アプリケーション ページはバッチ モードでコンパイルされます。つまり、ASP.NET は 1 つのアセンブリにできるだけ多くのコンパイルされていないページを詰め込もうとします。1 つのアセンブリにパッケージ化されるページ数とアセンブリ全体のサイズは、maxBatchSize 属性と maxBatchGeneratedFileSize 属性を使って制限できます。既定では、バッチ コンパイルごとに 1,000 ページと、1MB 以下のアセンブリを保持できます。通常、大量のページを初めてコンパイルするときの待ち時間が長すぎないようにする必要があります。また、少量のページを処理したり、各ページのコンパイルを開始したりするときに、大きなアセンブリをメモリに読み込むことはしたくありません。maxBatchSize 属性と maxBatchGeneratedFileSize 属性によって、初回の遅延とメモリの使用量のバランスが適切になります。サイトをプリコンパイルすることにした場合 (2006 年 1 月公開の Fritz Onion 氏の「Extreme ASP.NET column」 (英語) を参照)、初回の遅延については心配無用ですが、Web サーバーのメモリが過負荷にならないように最適なバッチ パラメータについて検討する必要があります。

バッチ処理が有効になっている場合、アプリケーションの最初の 1,000 ページ (実際のページ数は maxBatchSize によって異なります) は App_Web_[random] というアセンブリにコンパイルされます。[random] は 8 文字の不規則な文字の並びです。バッチ処理が無効になっている場合は、各ページが独自のアセンブリを作成します。アセンブリの名前は次の形式になります。

App_Web_[page].aspx.[folder-hash].[random].dll

バッチ処理を無効にするには、web.config ファイルに以下を追加します。

<compilation batch="false" />

サンプル アプリケーションのコンパイル フォルダを検証すると、次のように、名前に CBMResult が含まれたコンパニオン情報保持ファイルと、それと同じ名前の .ccu ファイルが見つかります。

test.aspx.cdcab7d2.compiled
test.aspx.cdcab7d2_CBMResult.ccu
test.aspx.cdcab7d2_CBMResult.compiled

一覧の最初のファイルは情報保持ファイルです。残りの 2 ファイルはどういうものでしょうか。CCU はコード コンパイル単位 (Code Compile Unit) のことで、動的なページ クラスのソース コードを生成するために作成される CodeDOM ツリーを参照します。CCU ファイルはバイナリ ファイルで、ページの CodeDOM ツリーのシリアル化されたバージョンが含まれます。CBMResult ファイルは、CCU が最新かどうか、CCU の配置場所、および CCU が基づいてるファイルを確認するために使用される情報保持ファイルです。

CBMResult ファイルは Visual Studio 2005 デザイナや IntelliSense® など、ClientBuildManager クラスと通信するモジュールによって使用されます。これらのモジュールはステートメントを完了させるためにページの構造をクエリします。CCU ファイルは、これらの要求を処理する準備ができたページの CodeDOM 構造の最新のコピーを保持しています。

ページ クラスの動的ソース コード

既に説明したとおり、.aspx リソースは Visual Basic クラスまたは C# クラスに解析されます。このクラスは System.Web.UI.Page から継承されます。また、System.Web.UI.Page から継承されるクラスから継承されることもあります。多くの場合、実際には動的ページ クラスには、次のプロトタイプがあります。

Namespace ASP
    Public Class test_aspx
        Inherits Test : Implements System.Web.IHttpHandler
        ...
    End Class
End Namespace

たとえば、Test クラスはページのコード ファイル クラスで定義され、このクラスにはページのコンパニオン クラス ファイルに記述するイベント ハンドラとヘルパ ルーチンが含まれます。Visual Studio 2005 で作業しているときに既に気付いたかもしれませんが、このコード ファイル クラスにはページのメンバの定義が不足しています。.aspx ソース ファイルで見つかる各 runat=server タグについては、コード ファイルで定義されている対応する型のメンバを求める必要があります。ASP.NET ランタイム システムは、すべてのメンバと、2 つの関連プロパティ (Profile と ApplicationInstance) を含む部分的な Test クラスを生成します。図 5 は特定の .aspx リソースの要求を処理できるようにするクラスのセットを示しています。

Figure 5 .aspx 要求の処理に使用されるランタイム クラス (Close Figure 5)
'Generated by ASP.NET to add member definitions
Partial Public Class Test
    Implements System.Web.SessionState.IRequiresSessionState

    Protected WithEvents TextBox1 As System.Web.UI.WebControls.TextBox
    Protected WithEvents Button1 As System.Web.UI.WebControls.Button
    Protected WithEvents Msg As System.Web.UI.WebControls.Label
    Protected WithEvents form1 As System.Web.UI.HtmlControls.HtmlForm

    Protected ReadOnly Property Profile() _
            As System.Web.Profile.DefaultProfile
        Get
            Return CType(Me.Context.Profile, _
                System.Web.Profile.DefaultProfile)
        End Get
    End Property

    Protected ReadOnly Property ApplicationInstance() _
            As System.Web.HttpApplication
        Get
            Return CType(Me.Context.ApplicationInstance, _
                System.Web.HttpApplication)
        End Get
    End Property
End Class

'Your code file
Partial Class Test : Inherits System.Web.UI.Page
    Protected Sub Button1_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Button1.Click
    End Sub
End Class

'Created to build the expected markup for the .aspx resource
Namespace ASP
    Public Class test_aspx
        Inherits Global.Test
        Implements System.Web.IHttpHandler
        ...
    End Class
End Namespace

図 5 に記載されているクラスは、2 つの異なるソース ファイルに及んでいます。最初のファイルには、コード ファイル内のクラスを満たす部分クラスと、要求を処理するためにそこから派生した実際のページ クラスが含まれています。2 番目のファイルは、プロジェクトで作成したコード ファイルのコピーです。これらのファイル名は、アセンブリ名にちなんで付けられます。名前の形式は、[assembly].X.vb です (C# の場合は拡張子が .cs になります)。X は一意のファイル名を付けるための 0 から始まる連続するインデックスです。

サンプル test.aspx ページのコンパイル フォルダの内容を見ると、次の例のように 3 番目のファイルが作成されることがわかります。

Namespace __ASP
   Friend Class FastObjectFactory_app_web_test_aspx_cdcab7d2_xg83msu0
      Private Sub New()
          MyBase.New
      End Sub

      Shared Function Create_ASP_test_aspx() As Object
          Return New ASP.test_aspx
      End Function
  End Class
End Namespace

クラス名は FastObjectFactory というプレフィックスの付いたページ アセンブリの名前です。クラスは Create_XXX という 1 つの共有関数 (C# で記述された場合は静的関数) を特色としています。XXX はインスタンス化するページ クラスの名前です。名前のとおり、ASP.NET ランタイムで頻繁に行われるページ インスタンスの作成操作をすばやく行うために使用するヘルパ クラスです。このようなクラスの作成は、ページのコンパイルに比べてかなり少ない時間で行うことができます。一方、ファクトリ クラスでは Activator.CreateInstance を使用して間接的なオブジェクトの作成を行うよりも大幅に速度が上がります。

ファクトリ クラスの内容は、バッチ コンパイルの設定によって変わります。既定では、バッチ処理が有効になっている場合、ファクトリ クラスにはバッチ処理されたページの数だけ Create_XXX 関数が含まれます。ファクトリ クラスの名前はバッチ アセンブリの名前と一致します。

' Used to serve test.aspx
Shared Function Create_ASP_test_aspx() As Object
   Return New ASP.test_aspx
End Function

' Used to serve default.aspx
Shared Function Create_ASP_default_aspx() As Object
   Return New ASP.default_aspx
End Function

バッチ処理が無効になっている場合、ファクトリ クラスは個別のページ アセンブリの名前と一致します。また、共有関数 (特定のページのファクトリ) が 1 つだけ含まれます。この場合、アプリケーションの各ページは独自のファクトリ クラスを所有することになります。

ランタイム パブリック API

これまでに説明した情報を武器にすると、コンパイル フォルダの内容を調べるのはそれほど難しくありません。それでも、必要な情報をすばやく見つけることができるツールがあれば便利でしょう。すぐに ASP.NET アプリケーションの動的に生成されたソース コードをナビゲートするエクスプローラ ツールをデザインするつもりですが、まずは .NET Framework 2.0 のランタイム API をいくつか見てみましょう。特に詳しく知っておく必要がある 2 つのクラスは、HttpRuntime と ClientBuildManager です。

HttpRuntime クラスは、さまざまなシステム パスに関する情報を返す多くの共有プロパティを特色としています。システム パスには、現在のアプリケーションの Bin フォルダ、ASP.NET のインストール、コンパイル フォルダ、現在の AppDomain ID などが含まれます。次のコードを使用して、現在の AppDomain に読み込まれたアセンブリの一覧を簡単に取得することもできます。

Dim listOfAssemblies() As Assembly
listOfAssemblies = AppDomain.CurrentDomain.GetAssemblies()

このコードは ASP.NET に固有のものではありませんが、ASP.NET アプリケーションから呼び出すと、ページに生成されたすべてのアセンブリを含む、AppDomain のアセンブリの配列が返されます。

ClientBuildManager クラスは、HttpRuntime の CodeGenDir プロパティと同じ情報を返す CodeGenDir 以外に、それほど多くの情報プロパティを備えていません。ただし、構成情報 (サポートされているブラウザなど) の読み取りとアプリケーションのプリコンパイルを行うためのメソッドを数多く備えています。クラスのメソッドの 1 つである Get は、AppDomain をシャットダウンするような重要な変更を監視するアプリケーション ディレクトリの一覧を返します。返されるディレクトリは、App_Browsers、App_Code、App_GlobalResources、App_WebReferences、および Bin です。

エクスプローラ ツールの作成

デバッグするとき、実行中のページのソース コードだけでなく他のランタイム情報にもすばやくアクセスできると、多くの場面で役立ちます。この機能を提供するツールは、ASP.NET アプリケーションと互換性がある必要があり、制限付きの構成にするか、まったく制限のない構成にすることが求められます。ASP.NET ランタイム情報のみを提供するのであれば、Nikhil Kothari 氏が作成したすばらしいWeb Development Helper ツール (英語) が最適です。ツールは、Microsoft Internet Explorer® ユーザー インターフェイスの COM ベースのプラグインである、ブラウザ ヘルパ オブジェクト (BHO) として実装されます。BHO はこのコラム用に作成したスパイ ツールの優れたホスト環境になるでしょう。ただし、残念ながら、面倒くさいのでその案は進めていません。そのため、独自のツールを HTTP モジュールとして記述しました。この HTTP モジュールは、ページとブラウザ間に存在し、クエリ文字列を検索し、明示的に呼び出す場合に起動します。HTTP モジュールを ASP.NET アプリケーションにインストールするには、web.config に 1 行を追加する必要があるだけです。また、HTTP モジュールはオンとオフを簡単に切り替えることができます。

<httpModules>
   <add name="AspExplorerModule" type="Samples.AspExplorerModule" />
</httpModules>

図 6 はエクスプローラ HTTP モジュールのコードの大部分を示しています。モジュールは PostMapRequestHandler アプリケーション イベントに登録して、ページ クラスをフックします。PostMapRequestHandler イベントは、ASP.NET ランタイムが要求の処理に必要な HTTP ハンドラ オブジェクトを確認したときに起動されます。要求のクエリ文字列に source=true パラメータが含まれていて、ハンドラが System.Web.UI.Page から継承されるクラスであれば、モジュールは動作を開始します。

Figure 6 ASP エクスプローラの HTTP モジュール (Close Figure 6)
Namespace Samples
    Public Class AspExplorerModule : Implements IHttpModule
        Private _app As HttpApplication

        Public Sub Dispose() Implements IHttpModule.Dispose
        End Sub

        Public Sub Init(ByVal context As HttpApplication) _
                Implements IHttpModule.Init
            If context Is Nothing Then
                Throw New ArgumentNullException("[context] argument")
            End If

            ' Cache the HTTP application reference
            _app = context

            ' Map the app event to hook up the page
            AddHandler context.PostMapRequestHandler, _
                New EventHandler(AddressOf OnPostMapRequestHandler)
        End Sub

        Private Sub OnPostMapRequestHandler( _
                ByVal source As Object, ByVal e As EventArgs)
            ' Get the just mapped HTTP handler and cast to Page
            Dim theHandler As IHttpHandler = _app.Context.Handler
            Dim thePage As Page = TryCast(theHandler, Page)

            ' If OK, register an event handler for PreRender
            If thePage IsNot Nothing AndAlso IsSourceRequest() Then
                HookUpPage(thePage)
            End If
        End Sub

        Private Function IsSourceRequest() As Boolean
            Return String.Equals( _
                app.Request.QueryString.Item("source"), "true", _
                StringComparison.OrdinalIgnoreCase)
        End Function

        Sub HookUpPage(ByVal thePage As Page)
            AddHandler thePage.PreRenderComplete, _
                New EventHandler(AddressOf PreRenderComplete)
        End Sub

        Private Sub PreRenderComplete( _
                ByVal sender As Object, ByVal e As EventArgs)
            Dim thePage As Page = DirectCast(sender, Page)
            thePage.SetRenderMethodDelegate( _
                New RenderMethod(AddressOf RenderSource))
        End Sub

        Private Sub RenderSource( _
                ByVal output As HtmlTextWriter, _
                ByVal container As Control)
            Dim thePage As Page = DirectCast(container, Page)

            Dim source As String
            source = GetWebPageInfo(thePage)
            output.Write(source)
        End Sub

        Private Function GetWebPageInfo(ByVal thePage As Page) As String
            ...
        End Function

    End Class
End Namespace

ASP エクスプローラ モジュールはページ クラスをフックして、PreRenderComplete イベントに独自のハンドラを登録します。この方法で設計された HTTP モジュールは、要求のランタイム処理に変更がないだけでなく、ページのコンパイルにも干渉しません。source パラメータがクエリ文字列に指定されていて、かつ、true に設定されている場合、モジュールが動作します。図 6 が示すように、モジュールで行っているのは、あまり知られていない Page クラス メソッドの SetRenderMethodDelegate を使用してページに表示デリゲートを登録することだけです。表示デリゲートがページに指定されている場合、ラップされたメソッドが標準の表示をオーバーライドします。つまり、モジュールのインストール後に test.aspx を呼び出すと、ページの標準出力が表示されます。test.aspx?source=true を呼び出すと、モジュールが収集できるページに関するすべてのランタイム情報が表示されます。

ASP エクスプローラのソース コードでは、現在のページの情報保持ファイルの内容をマップするクラスを定義します。このコードは情報保持ファイルを読み取り、図 7 のクラスに情報をコピーします。SourceFiles プロパティはそのページを構成するすべてのソース ファイルを含むようにデザインされたコレクションです。コレクションには情報保持ファイルでは見つからない情報が設定されますが、コレクションは依然としてコンパイル フォルダから取得できます。具体的には、特定のページに関連するすべてのソース ファイルが含まれています。これらのソース ファイルは、動的なページ アセンブリの名前で始まる名前を持つ .vb ファイルまたは .cs ファイルです。GetWebPageInfo メソッド (図 6 参照) はすべての情報をキャプチャし、元のモードで要求の出力を作成します。ページの出力には、ランタイム情報だけでなく、動的なページ クラスのソース コードも含まれます。図 8 は実行中の ASP エクスプローラを示しています。

Figure 7 情報保持ファイルから情報を読み込むヘルパ クラス (Close Figure 7)
Public Class PreservationInfo
    Private _file As String
    Private node As XmlNode

    Public Sub New(ByVal file As String)
        _file = file
        Load()
    End Sub

    Private Sub Load()
        Dim doc As New XmlDocument
        doc.Load(_file)
        node = doc.DocumentElement

        If node Is Nothing OrElse _
                Not String.Equals(node.Name, "preserve") Then Return

        Dim buf As String

        ' ResultType
        buf = node.Attributes("resultType").Value
        Int32.TryParse(buf, ResultType)

        ' VirtualPath
        VirtualPath = node.Attributes("virtualPath").Value

        ' Hash
        Hash = node.Attributes("hash").Value

        ' FileHash
        FileHash = node.Attributes("filehash").Value

        ' Flags
        buf = node.Attributes("flags").Value
        Int32.TryParse(buf, Flags)

        ' Assembly
        AssemblyName = node.Attributes("assembly").Value

        ' Type
        TypeName = node.Attributes("type").Value

        ' Dependencies
        Dim list As XmlNodeList = node.SelectNodes("filedeps/filedep")
        FileDeps = Array.CreateInstance(GetType(String), list.Count)

        For i As Integer = 0 To list.Count - 1
            FileDeps(i) = list.Item(i).Attributes("name").Value
        Next

        ' Source files
        Dim searchPath As String = String.Format("{0}.*", AssemblyName)
        Dim files() As String = _
            Directory.GetFiles(HttpRuntime.CodegenDir, searchPath)

        SourceFiles = Array.CreateInstance(GetType(String), files.Length)
        For i As Integer = 0 To files.Length - 1
            Dim fileInfo As New FileInfo(files(i))
            If fileInfo.Extension = ".vb" Or _
                   fileInfo.Extension = ".cs" Then
                SourceFiles(i) = files(i)
            End If
        Next
    End Sub

    Public ResultType As Integer
    Public VirtualPath As String
    Public Hash As String
    Public FileHash As String
    Public Flags As Long
    Public AssemblyName As String
    Public TypeName As String
    Public FileDeps() As String
    Public SourceFiles() As String
End Class
図 8 実行中の ASP エクスプローラ モジュール
図 8 実行中の ASP エクスプローラ モジュール(画像を縮小するには、ここをクリックします)
図 8 実行中の ASP エクスプローラ モジュール
図 8 実行中の ASP エクスプローラ モジュール (画像を拡大するには、ここをクリックします)

サンプル ページの調査

ツールは既に持っているので、ASP.NET が各 .aspx ファイル用に生成するコードの構造を簡単に見てみましょう。興味深いのは、ASP.NET ランタイムで提供される解析機能とコンパイル機能を使用しないと、ASP.NET ページを実行するために自分でコードを記述しなければならないということです。

動的なページ クラス (図 5 の test_aspx クラス) は、System.Web.UI.Page クラスのいくつかのメソッドをオーバーライドします。それらのメソッドには、FrameworkInitialize、ProcessRequest、GetTypeHashCode があります。ProcessRequest では特に何も起きず、そのベース メソッドを呼び出すだけです。GetTypeHashCode はページのコントロール階層を一意に識別する、ページのハッシュ コードを返します。ハッシュ値は、ページがコンパイルされ定数としてソースに挿入されるときに、その場で計算されます。

最も興味深いオーバーライドは FrameworkInitialize です。このメソッドは、ページのコントロール ツリーの作成を管理し、__BuildControlTree というプライベート メソッドを呼び出します。このメソッドはページ クラスの Controls コレクションに、.aspx source の runat=server タグに対応するコントロールの最新のインスタンスを設定します。__BuildControlTree はサーバー側のタグをすべて解析し、各タグのオブジェクトを作成します。

<asp:textbox runat="server" id="TextBox1" text="Type here" />

以下は、上記のマークアップ用に取得する一般的なコードです。

Private Function __BuildControlTextBox1() As TextBox
    Dim __ctrl As New TextBox()
    Me.TextBox1 = __ctrl
    __ctrl.ApplyStyleSheetSkin(Me)
    __ctrl.ID = "TextBox1"
    __ctrl.Text = "Type here"
    Return __ctrl
End Function

コントロールにイベント ハンドラまたはデータ バインド式がある場合はどうでしょうか。まずは Click イベント ハンドラが設定されたボタンについて考えてみましょう。次の 1 行を追加する必要があります。

__ AddHandler __ctrl.Click, AddressOf Me.Button1_Click

データ バインド式 <%# ... %> の場合に生成されるコードは、DataBinding イベントが使用されること以外は似ています。

AddHandler __ctrl.DataBinding, AddressOf Me.DataBindingMsg

ハンドラと関連付けられるコードは、バインドされたコントロールとバインドされているコードの性質によって異なります。Label コントロールの Text プロパティの場合、次のようなコードになります。

Public Sub DataBindingMsg(ByVal sender As Object, ByVal e As EventArgs)
   Dim target As Label = DirectCast(sender, Label)
   target.Text = Convert.ToString(..., _
       CultureInfo.CurrentCulture);
End Sub

Convert.ToString に渡される式は、まさに <%# ... %> 式の中のコードです。型キャストも呼び出される型に依存しています。

マスタ ページとテーマが存在する場合、ソース ファイルの数と依存関係の一覧は増えますが、ASP エクスプローラ ツールを使用すればそれらを思いのままに見つけ出すことができます。

まとめ

ASP.NET は所有しているリソースの種類の動的なコード コンパイルをオンデマンドで実行します。この機能により、Web アプリケーションの迅速な反復開発が非常に容易になりますが、ファイルをディスクに書き込めるようにするには ASP.NET が必要です。コンパイル フォルダは ASP.NET マジックのほとんどが行われる重要なフォルダです。難しい問題を診断およびデバッグする場合もありますが、このフォルダは面白半分にのぞき込むことができます。もちろん、ここで説明した機能の多くは ASP.NET 内部のものです。そのため、将来のビルドで特に説明もなく変更される可能性があります。でも現時点では、これが ASP.NET 2.0 のしくみです。また、ASP エクスプローラ ツールをご自由にお使いください。ASP.NET AJAX アプリケーションもどうぞ。適切に動作します。

Dino の連絡先は cutting@microsoft.com.

(英語) です。
Dino Esposito は、Solid Quality Learning の指導者であり、Programming Microsoft ASP.NET 2.0 (Microsoft Press 発行、2005 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで講演しています。連絡先は、cutting@microsoft.com (英語) です。また、weblogs.asp.net/despos (英語) のブログにも参加してみてください。


基礎: テンプレートを使用した WPF コントロールのカスタマイズ

基礎
テンプレートを使用した WPF コントロールのカスタマイズ
Charles Petzold

コードのダウンロード : : Foundations2007_01.exe (158KB)
コンテンツ
既定値のダンプ
ビジュアル ツリー
テンプレートの Triggers セクション
より高度に
ScrollBar の詳細
Button コマンド
3D スライダの作成
適度にすることをお忘れなく

Windows Vista™ と Microsoft® .NET Framework 3.0 のリリースにより、開発者が学習、検討、使用できる、新しいテクノロジが提供されました。新しいツール、ライブラリ、およびパラダイムによって、マネージ アプリケーションの作成方法が変わり、強力な機能が提供されます。この新しいコラムを毎月掲載し、アプリケーションの開発に使用する基になるテクノロジについて説明します。既にお馴染みの業界専門家が、Windows® Presentation Foundation、Windows Communication Foundation、および Windows Workflow Foundation を順番に調べていきます。では、始めましょう。

従来、Windows で既存のコントロールをカスタマイズするプロセスは、4 つの手順で構成されていました。カスタマイズは思いつきで始まります。その後、調査に入ります。調査では、必ずや涙を流し歯ぎしりすることになります。そして最終的には、全面的な書き直しが必要になります。コントロールのビジュアルと機能を結びつけるコードにはほとんど手を入れることができないため、多くの場合、コントロールをカスタマイズするのは困難です。このコードはコントロールには不可欠なので、すべてを受け入れるか、完全に無視して置き換える必要があります。

コントロールをカスタマイズするときの苦痛を間違いなく感じていたのは、.NET Framework 3.0 の一部として利用できる Windows Presentation Foundation の開発者でした。そこで、彼らは "テンプレート" と呼ばれるエキサイティングで強力なソリューションを考え出しました。

Windows Presentation Foundation テンプレートは、概念を理解するにはしばらくかかりますが、とてもシンプルで、しかもとても強力です。私は (テンプレートと混同することの多い) Windows Presentation Foundation のスタイルについてはすばやく理解しましたが、テンプレートを理解するのには時間が必要でした。

視覚的な外観を持つ Windows Presentation Foundation の事前定義済みの各コントロールには、その外観全体を定義するテンプレートも用意されています。このテンプレートは ControlTemplate 型のオブジェクトで、Control クラスによって定義される Template プロパティに設定されます。

アプリケーションで Windows Presentation Foundation コントロールを使用するとき、既定のテンプレートを独自にデザインしたテンプレートと置き換えることができます。コントロールの基本機能 (キーボードとマウスの処理をすべて含む) は保持しますが、コントロールの外観はまったく別のものにすることができます。Windows Presentation Foundation のコントロール (スマートとは言えない面が多々ありますが) が "外観なし (lookless)" と示されている場合は、この状態を意味します。コントロールには既定の外観がありますが、この外観はコントロール内部のしくみには関係ありません。

コードでテンプレートを記述するのはかなり厄介です。Extensible Application Markup Language (XAML) を使用する方がはるかに簡単です。テンプレートは全体を XAML で表現できるので、ビジュアル デザイン ツールを使用すると便利です。既存の Windows Presentation Foundation のコントロールと動作が同じで外観の異なるカスタム コントロールをプログラミングで作成しようとしているなら、今すぐやめてください。カスタム コントロールはテンプレートを使えば確実に作成できます。

ダウンロード可能なソース コードは、7 個のスタンドアロン XAML ファイルで構成されています。これらのファイルについては、このコラム全体を通して説明していきます。このコラムの執筆にあたり、C# コードのコンパイルはしませんでした。.NET Framework 3.0 SDK がインストールされている場合、XAMLPad または XAML Cruncher を使用してファイルを編集できます。これらは、私の書籍『Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation』に記載したのと同様のプログラムです。

Windows Presentation Foundation ではコントロール コンテンツを表示するために他の種類のテンプレートもサポートしていますが、このコラムでは、ControlTemplate 型のオブジェクトのみについて説明します。

既定値のダンプ

視覚的な外観を持つ Windows Presentation Foundation の事前定義済みの各コントロールには、既定のテンプレートが用意されています。カスタム テンプレートを記述することに関心がある場合は、既定のテンプレートの既定値について学習してください。

私の書籍の第 25 章に記載されている DumpControlTemplate は、コントロールの既定のプロパティ Template を便利な XAML 形式で表示します (ソース コードをダウンロードできます)。しかし、テンプレート ダンプ機能を自分で記述する場合、以下のコードを使用して重要な手順を実行できます。

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = new string(' ', 4);
settings.NewLineOnAttributes = true;
StringBuilder strbuild = new StringBuilder();
XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings);
XamlWriter.Save(ctrl.Template, xmlwrite);

このコードは、ctrl が Control から派生するクラスのインスタンスであり、ctrl が画面上に描画されることを想定しています (私の経験上、ctrl が画面上に描画されない場合は Template プロパティが Null です)。Template プロパティが Null の場合、対象のコントロールには視覚的な外観を持たないコントロールが含まれるため、XamlWriter.Save の呼び出しにより例外が発生します。このコードの末尾で、strbuild の ToString を呼び出すことにより、テンプレートを含む完全な XAML ドキュメントが提供されます。

この既定のテンプレートの XAML は、自分で記述する XAML よりも少し冗長になる場合があります。たとえば、自分では次のコードを記述するとします。

<Trigger Property="IsEnabled" Value="False">

しかし XamlWriter.Save によって生成される XAML ファイルでは、次のようなマークアップが見つかります。

<Trigger Property="UIElement.IsEnabled">
    <Trigger.Value>
        <s:Boolean>False</s:Boolean>
    </Trigger.Value>

s プレフィックスは .NET System 名前空間の xmlns 名前空間の宣言で定義されます。

ビジュアル ツリー

まずは、かなりシンプルですが重要なコントロールである CheckBox のテンプレートを見てみましょう。標準の CheckBox よりもう少し印象的に見えるものを求めているとします。たとえば、CheckBox のコンテンツ全体にわたって、ユーザーの選択内容を緑色の大きなチェックマークや赤い大きな X で表示することが考えられます。BigCheckCheckBox.xaml ファイルで、このような CheckBox 用のテンプレートを示します。

XAML ファイルでは、テンプレートは ControlTemplate 型の要素です。小さい Windows Presentation Foundation プログラムでは、ControlTemplate 要素は通常 XAML ファイルのルート要素の Resources セクションで定義されます。大きいアプリケーションの場合や、複数のアプリケーションで共有するテンプレートを定義する場合、ControlTemplate 要素は ResourceDictionary のルート要素が含まれた独自の XAML ファイルにあります。

どちらの場合でも、ControlTemplate 要素には通常 3 つの部分があります。まずは省略可能な Resources セクションがあります。このセクションではテンプレートで使用するスタイルまたはブラシを定義できます (BigCheckCheckBox.xaml のテンプレートには Resources セクションはありません)。次に、このテンプレートでは、テンプレートのビジュアル ツリーの定義を提供します。このツリーは、コントロールの必要な外観を要素のレイアウトやおそらくは他のコントロールとして説明します。最後は Triggers セクションです。このセクションは、ビジュアル ツリーの要素がコントロールのプロパティの変更に応じてどのように変更されるかを示します。図 1 と図 2 に、カスタム CheckBox のさまざまな ControlTemplate 要素を示します。最初の図のコードはビジュアル ツリーを示し、2 番目の図では Triggers セクションを含む ControlTemplate 要素が続きます。

Figure 2 Triggers セクション (Close Figure 2)
<ControlTemplate.Triggers>
    <Trigger Property="IsChecked" Value="True">
        <Setter TargetName="path"
                Property="Data"
                Value="M 0 5 L 3 10 10 0" />

         <Setter TargetName="path"
                Property="Stroke"
                Value="Green" />
    </Trigger>

    <Trigger Property="IsChecked" Value="{x:Null}">
         <Setter TargetName="path"
                Property="Data"
                Value="M 0 2.5 A 5 2.5 0 1 1 5 5
                       L 5 8 M 5 10 L 5 10" />

        <Setter TargetName="path"
                Property="Stroke"
                Value="Blue" />
    </Trigger>

    <Trigger Property="IsEnabled" Value="False">
        <Setter Property="Foreground"
                Value="{DynamicResource
                   {x:Static SystemColors.GrayTextBrushKey}}" />
    </Trigger>
    ...
</ControlTemplate.Triggers>
Figure 1 CheckBox を説明するビジュアル ツリー (Close Figure 1)
<ControlTemplate x:Key="templateBigCheck"
                 TargetType="{x:Type CheckBox}">

    <Border BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}"
            Background="{TemplateBinding Background}">

        <Grid>
            <!-- ContentPresenter displays content of CheckBox -->
            <ContentPresenter
                Content="{TemplateBinding Content}"
                ContentTemplate="{TemplateBinding ContentTemplate}"
                Margin="{TemplateBinding Padding}"
                HorizontalAlignment="{TemplateBinding
                                        HorizontalContentAlignment}"
                VerticalAlignment="{TemplateBinding
                                        VerticalContentAlignment}" />

            <!-- This Border displays a semi-transparent red X -->
            <Border>
                <Border.Background>
                    <VisualBrush Opacity="0.5">
                        <VisualBrush.Visual>
                            <Path Name="path"
                                  Data="M 0 0 L 10 10 M 10 0 L 0 10"
                                  Stroke="Red"
                                  StrokeStartLineCap="Round"
                                  StrokeEndLineCap="Round"
                                  StrokeLineJoin="Round" />
                        </VisualBrush.Visual>
                    </VisualBrush>
                </Border.Background>
            </Border>
        </Grid>
   </Border>

図 1 に示している BigCheckBox.xaml の抜粋では、ControlTemplate 開始タグの使用とビジュアル ツリーを例示しています。このテンプレートはリソースなので、リソース名を持つ x:Key 属性を含んでいる必要があります。ここで、TargetType 属性が使用されていることに気付くでしょう。これは厳密に必要なわけではありませんが、各プロパティの前にクラス名を付ける必要性をなくすことによってテンプレートの残りの部分をシンプルにします。

このビジュアル ツリーの最上位の要素は Border で、1 つの子に対して親の役割を果たすことができる要素です。このサンプルでは、この Border の 3 つのプロパティが TemplateBinding マークアップ拡張を使用して割り当てられます。TemplateBinding はビジュアル ツリーに含まれている要素のプロパティをコントロールのプロパティにバインドするために使用されます。

TemplateBinding 式でより興味深いのは、ContentPresenter 要素です。この要素は ContentControl から派生するボタンなどのコントロールのコンテンツを書式設定します。ContentPresenter を使用すると、Button または CheckBox の内部にほとんど何でも表示できます。TemplateBinding が ContentPresenter の Margin プロパティをテンプレート化されている CheckBox コントロールの Padding プロパティに設定するために使用されることに注意してください。Margin (余白) は要素の外部にある余分な空白を示し、Padding (スペース) はコントロール内部のコンテンツによって占有されていない空白を示します。これで、このバインドの論理的根拠がわかります。

ContentPresenter は別の Border と共に 1 つのセル Grid パネルの中に示されます。Grid セルは、多くの場合、層に含まれている必要がある複数の子をホストするために使用されます。そのため、2 番目の Border は ContentPresenter の前面に存在します。この Border には、赤い X マークを含む 50 パーセントの透明度のブラシが設定されます。赤い X マークを描画する Path 要素にパスの名前が割り当てられていることに注意してください。この名前はテンプレートの Triggers セクションで参照されます。

テンプレートの Triggers セクション

ControlTemplate の最後のセクションは、通常は Trigger 要素専用です。Trigger 要素では、コントロールのプロパティへの変更に基づいて、ビジュアル ツリーを構成する要素のプロパティを変更します。図 2 で、カスタム CheckBox テンプレートの Triggers セクションの大部分を示します。

既定では、CheckBox の IsChecked プロパティは false で、CheckBox は赤い X マークを表示します。最初の Trigger 要素は、IsChecked が true のときに、この要素の Data プロパティと Stroke プロパティが緑色のチェック マークを表示するように設定される必要があることを示します。次の Trigger 要素は、IsChecked が Null 値の場合に対応し、青い疑問符を表示します (IsChecked の Null 値は、CheckBox が 3 つの状態の操作に設定されている場合に発生します)。最後の Trigger 要素は、IsEnabled プロパティが false のときに、コントロールの Foreground プロパティを灰色のブラシに変更します (BigCheckCheckBox.xaml ファイルには、コントロールに入力フォーカスが置かれたときにコントロールのコンテンツの周囲に点線を表示する追加の Trigger 要素があります)。

テンプレートの Triggers セクションは非常に広範囲に及ぶ場合があります。多くの場合 IsMouseOver プロパティの Trigger 要素が含まれているため、マウスがコントロールの上に置かれたときにコントロールが反応します。

ある要素では次のように CheckBox を作成するテンプレートを参照できます。

<CheckBox Template="{StaticResource templateBigCheck}" ...

図 3 は、BigCheckCheckBox.xaml ファイルによって表示される 3 つの CheckBox コントロールを示しています。そのうち 2 つにはテキスト コンテンツ、3 つ目 (IsThreeState プロパティが true に設定され、IsChecked プロパティが Null に設定されているコントロール) にはビットマップが含まれます。

図 3 カスタム テンプレートを持つ CheckBox コントロール
図 3 カスタム テンプレートを持つ CheckBox コントロール(画像を縮小するには、ここをクリックします)
図 3 カスタム テンプレートを持つ CheckBox コントロール
図 3 カスタム テンプレートを持つ CheckBox コントロール (画像を拡大するには、ここをクリックします)

より高度に

より複雑なコントロールに対してテンプレートの概念をどの程度採用できるかについて疑問を抱いているかもしれません。一部のコントロールには、移動部分が付いていたり、ユーザー操作が絡んでいたりします。そのような疑問を解決するため、このコラムの残りの部分では、RangeBase から派生する 3 つのコントロールのテンプレートを見ていきます。対象の各コントロールは、ProgressBar、ScrollBar、および Slider です。

正しく動作させるため、より複雑なコントロール用のテンプレートには、特定の名前を持つ特定の種類の要素が必要です。たとえば、ProgressBar のビジュアル ツリーには FrameworkElement 型 (または FrameworkElement から派生する型) の 2 つの要素 (PART_Track および PART_Indicator) が必要です。これら 2 つの要素はテンプレートの "名前付き部分" と呼ばれます。名前は ProgressBar クラスの SDK ドキュメントに TemplatePart 属性として示されています。テンプレートにこれらの名前を持つ 2 つの要素が含まれていない場合、コントロールは正しく動作しません。

ProgressBar が既定の水平方向の向きで表示される場合、コントロールの内部ロジックにより、PART_Indicator という要素の Width プロパティが PART_Track という要素の ActualWidth プロパティの何分の 1 かに設定されます。その割合は ProgressBar の Minimum プロパティ、Maximum プロパティ、および Value プロパティに基づいています。ProgressBar の方向が水平の場合、代わりに 2 つの要素の Height プロパティと ActualHeight プロパティが使用されます。

ProgressBar コントロールには実際には 2 つの方向を表すための 2 つの既定のテンプレートがあります (ScrollBar と Slider についても同様です)。新しい ProgressBar で両方向をサポートする場合、2 つの個別のテンプレートを記述し、ProgressBar についても定義する Style 要素の Triggers セクションで選択する必要があります。

図 4 は BareBonesProgressBar.xaml ファイルを抜粋したもので、ControlTemplate オブジェクトを持つ完全な ProgressBar 要素を、その要素のプロパティとして示しています。また、このファイルにはバインドを使用して ProgressBar をテストするために使用される ScrollBar が含まれています。ScrollBar を操作するとき、青い四角形 (PART_Indicator 要素) の幅がゼロから赤い四角形 (PART_Track 要素) の幅に変わります。BareBonesProgressBar.xaml で PART_Track 四角形に明示的な幅が指定されている場合でも、明示的なサイズを指定することはできるだけ避けてください。

Figure 4 必要最低限の ProgressBar テンプレート (Close Figure 4)
<ProgressBar Margin="50" HorizontalAlignment="Center"
             Value="{Binding ElementName=scroll, Path=Value}">
    <ProgressBar.Template>
        <ControlTemplate>
            <StackPanel>
                <Rectangle Name="PART_Track"
                           Height="20" Width="300" Fill="Red" />

                <Rectangle Name="PART_Indicator"
                           Height="20" Fill="Blue" />
            </StackPanel>
        </ControlTemplate>
    </ProgressBar.Template>
</ProgressBar>

Indicator 要素が Track 要素の内部にあることは一般的なので、Track の Border 要素を使用することを検討するでしょう。しかし注意が必要です。その Border にもゼロ以外の BorderThickness を指定する場合、その太さは Border 要素の全幅の一部となり、枠線の幅はわずかながら下回ります。表示目的で枠線の太さがゼロ以外に設定された Border を使用する場合、その最初の枠線内では、トラックを枠線の太さがゼロに設定された別の Border にします。また、インジケータ用の 2 番目の Border の中には別のものを配置します (枠線または背景のプロパティを設定せずに Border 要素を使用している場合、Border の子孫で枠線がなく背景のないクラスの Decorator を使用することを検討してください)。

図 5 は、ThermometerProgressBar.xaml ファイルの比較的広範囲の ControlTemplate オブジェクトを示しています。このテンプレートには、Resources セクションはありますが、Triggers セクションはありません。ビジュアル ツリーでは枠線と角に対して明示的な座標を使用しますが、ProgressBar の全体サイズは定義されません。その定義は、たとえば次のように、テンプレートを使用して ProgressBar を定義するマークアップによって行います。

<ProgressBar
       Template="{StaticResource
       templateThermometer}"
       Orientation="Vertical" Minimum="0"
       Maximum="100"
       Width="50" Height="350" ...

Figure 5 温度計の ProgressBar の ControlTemplate (Close Figure 5)
<ControlTemplate x:Key="templateThermometer"
                 TargetType="{x:Type ProgressBar}">

    <!-- Define two brushes for the thermometer liquid -->
    <ControlTemplate.Resources>
        <LinearGradientBrush x:Key="brushStem"
                             StartPoint="0 0" EndPoint="1 0">
            <GradientStop Offset="0" Color="Red" />
            <GradientStop Offset="0.3" Color="Pink" />
            <GradientStop Offset="1" Color="Red" />
        </LinearGradientBrush>

        <RadialGradientBrush x:Key="brushBowl"
                             GradientOrigin="0.3 0.3">
            <GradientStop Offset="0" Color="Pink" />
            <GradientStop Offset="1" Color="Red" />
        </RadialGradientBrush>
   </ControlTemplate.Resources>

   <!-- Two-row Grid divides thermometer into stem and bowl -->
   <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!-- Second grid divides stem area in three columns -->
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="25*" />
                <ColumnDefinition Width="50*" />
                <ColumnDefinition Width="25*" />
            </Grid.ColumnDefinitions>

            <!-- This border displays the stem -->
            <Border Grid.Column="1" BorderBrush="SteelBlue"
                    BorderThickness="3 3 3 0"
                    CornerRadius="6 6 0 0" >

                <!-- Track and Indicator elements -->
                <Decorator Name="PART_Track">
                    <Border Name="PART_Indicator"
                            CornerRadius="6 6 0 0"
                            VerticalAlignment="Bottom"
                            Background="{StaticResource brushStem}" />
                </Decorator>
            </Border>
        </Grid>

        <!-- The bowl outline goes in the main Grid second row -->
        <Ellipse Grid.Row="1"
                 Width="{TemplateBinding Width}"
                 Height="{TemplateBinding Width}"
                 Stroke="SteelBlue" StrokeThickness="3" />

        <!-- Another grid goes in the same cell -->
        <Grid Grid.Row="1" >
            <Grid.RowDefinitions>
                <RowDefinition Height="50*" />
                <RowDefinition Height="50*" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="25*" />
                <ColumnDefinition Width="50*" />
                <ColumnDefinition Width="25*" />
            </Grid.ColumnDefinitions>

            <!-- This is to close up the gap between bowl and stem -->
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="SteelBlue"
                    BorderThickness="3 0 3 0"
                    Background="{StaticResource brushStem}" />
        </Grid>

        <!-- Another ellipse to fill up the bowl -->
        <Ellipse Grid.Row="1"
                 Width="{TemplateBinding Width}"
                 Height="{TemplateBinding Width}"
                 Stroke="Transparent" StrokeThickness="6"
                 Fill="{StaticResource brushBowl}" />
    </Grid>
</ControlTemplate>

結果の ProgressBar を図 6 に示します。この ProgressBar の Orientation プロパティは Vertical に設定する必要があります。そうしないと、正しく動作しません。このテンプレートを使用するたびに忘れずに Orientation を設定してください。代わりに、Orientation を設定しテンプレートの参照も行う ProgressBar の Style を定義することもできます (すぐにこの技法の例をお見せします)。

図 6

このコラムに関するダウンロード可能なコードには SpeedometerProgressBar.xaml もあり、図 7 で示すより大胆な ProgressBar を生成します。

図 7 速度計 の ProgressBar
図 7 速度計 の ProgressBar

これについては、いくらか注意が必要です。テンプレートには 2 つの四角形が含まれていますが、以下に示すように高さや塗りつぶしの色が設定されていないので、これらの四角形は表示されません。

  <Rectangle Name="PART_Track" Width="180" />
  <Rectangle Name="PART_Indicator" />

PART_Track 要素の幅は半円の角度に設定されています。赤いポインタの Polygon 要素は、次のように、Angle プロパティが PART_Indicator の ActualWidth にバインドされている RotateTransform に従います。

<RotateTransform
   Angle="{Binding ElementName=PART_Indicator,
                 Path=ActualWidth}" />

ScrollBar の詳細

ProgressBar と ScrollBar は共に RangeBase 抽象クラスから派生します。RangeBase から提供されるのは、Value プロパティ、ValueChanged イベント、および Minimum、Maximum、SmallChange、LargeChange の定義だけです (ProgressBar では Minimum、Maximum、および Value の各プロパティだけを使用します)。

ScrollBar は ProgressBar よりも複雑で、いくつかの方法でユーザーの入力に反応するようにできます。この動作は標準の ScrollBar の 5 個の子コントロールに分散されます。移動可能なつまみは Thumb というコントロールです。つまみの両側には、直接、[Page Up] コマンドや [Page Down] コマンドを実装する 2 つの RepeatButton コントロールがあるのが一般的です (RepeatButton は通常の Button に似ていますが、複数の Click イベントを使ってマウスを押したままにする場合の応答とは異なります)。これら 2 つの RepeatButton コントロールのサイズは ScrollBar の Value プロパティが変更されるたびに変更されます。また、どちらか一方がゼロに縮小されることがあります。ScrollBar の両端には、通常は [Line Up] コマンドと [Line Down] コマンドを実装する矢印を示すラベルの付いた一定サイズの RepeatButton コントロールがあと 2 つあります。

Thumb コントロールと内部の RepeatButton コントロールは、Track という要素の子であり、対話、移動、およびサイズ変更のすべてを行います。ScrollBar のコア機能と煩雑なロジックのほとんどは、Track によって処理されます。

ScrollBar の ControlTemplate を定義するとき、ビジュアル ツリーには PART_Track という Track 要素を含める必要があります。それが ScrollBar テンプレート の名前付き部分に必要な唯一のものです。

Track はコントロールではなく、FrameworkElement から派生します。また、Template プロパティは Control によって定義されるので Template プロパティはありません。Track のテンプレートを提供することはできませんが、Track 要素を構成する 3 つのコントロール用のテンプレートを提供できます。

Track では 3 つの子 Thumb (Thumb 型)、DecreaseRepeatButton (RepeatButton 型)、および IncreaseRepeatButton (RepeatButton 型) に対応する 3 つのプロパティを定義します。既定では、これら 3 つのプロパティは Null です。これは、テンプレートのビジュアル ツリーがこれら 3 つのコントロールの明示的な定義を含んでいる必要があることを意味します。また ScrollBar の両端に 2 つのボタンが必要な場合、ビジュアル ツリーにはこれら 2 つのボタンの要素を含める必要があります。これら 5 つの子コントロールに独自のテンプレートを提供するか、既存のテンプレートを使用して一部のプロパティを割り当てることができます。

Button コマンド

標準の ScrollBar テンプレートに含まれている 4 つの RepeatButton コントロールでは ScrollBar の内部ロジックと通信できる必要があります。これを実現するには、ScrollBar クラスが静的な読み取り専用フィールドとして定義する、事前定義済みの RoutedCommand オブジェクトを使用します。ScrollBar では、ScrollBar が実行できるさまざまな操作に対応する、17 個以上の静的な読み取り専用フィールドを定義します。これらのコマンドには ScrollBar コンテキスト メニューで使用するためのものと、ScrollViewer コントロールの一部として使用される ScrollBar 用のものがあります。特に Line や Page という語で始まるコマンドはテンプレート用です。

図 8 は NoFrillsScrollBar.xaml ファイルの ScrollBar テンプレートを示しています。Track 要素と両端にある 2 つの RepeatButton コントロールは、Grid パネルに構成されます。Track 要素のこれらのプロパティが 2 つ以上の RepeatButton コントロールと Thumb に設定されます。

Figure 8 シンプルな ScrollBar テンプレート (Close Figure 8)
<ControlTemplate x:Key="templateNoFrillsScroll"
                 TargetType="{x:Type ScrollBar}">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <RepeatButton Grid.Column="0"
                      Command="ScrollBar.LineLeftCommand"
                      FontFamily="Wingdings" Content="E" />

        <Track Grid.Column="1" Name="PART_Track">
            <Track.DecreaseRepeatButton>
                <RepeatButton Command="ScrollBar.PageLeftCommand" />
            </Track.DecreaseRepeatButton>

            <Track.IncreaseRepeatButton>
                <RepeatButton Command="ScrollBar.PageRightCommand" />
            </Track.IncreaseRepeatButton>

            <Track.Thumb>
                <Thumb />
            </Track.Thumb>
        </Track>

        <RepeatButton Grid.Column="2"
                      Command="ScrollBar.LineRightCommand"
                      FontFamily="Wingdings" Content="F" />
    </Grid>
</ControlTemplate>

これは完全に機能する ScrollBar ですが、図 9 に示すように、4 つの RepeatButton コントロールが通常のボタンのように見えます。両端の 2 つのボタンは最初は非常に狭かったので、Wingdings フォントの指差し文字をコンテンツに設定することで、サイズを増加しました。それ以外では、5 つのコントロールには Command プロパティと Grid.Column アタッチ プロパティ以外のプロパティ セットは含まれていません。

図 9 図 8 に基づいた ScrollBar
図 9 図 8 に基づいた ScrollBar(画像を縮小するには、ここをクリックします)
図 9 図 8 に基づいた ScrollBar
図 9 図 8 に基づいた ScrollBar (画像を拡大するには、ここをクリックします)

図 8 のテンプレートは水平方向の ScrollBar を意図しています。垂直方向の操作では Grid 内に 3 列ではなく 3 行が必要であり、Left と Right ではなく Up と Down という語を含む ScrollBar クラスの RoutedCommand フィールドを参照します。

既定の ScrollBar テンプレートでは、一部のコントロールを描画するために、Microsoft.Windows.Themes 名前空間から ScrollChrome クラスを使用します。また、ビジュアル ツリー内のコントロールのプロパティの多くが属性ではなくスタイルを使用して定義されているため、テンプレートが必要以上に長く見えることにも気付きます。たとえば、次のようなコードを使用する場合があります。

<RepeatButton IsFocusable="False" ...
見つかるのは、次のようなコードです。
<RepeatButton ... >
    <RepeatButton.Style>
        <Style TargetType="RepeatButton">
            ...
            <Setter Property="UIElement.IsFocusable">
                <Setter.Value>
                    <s:Boolean>False</s:Boolean>
                </Setter.Value>
            </Setter>
            ...

上記のプロパティは最初はテンプレートの Resources セクションの Style 要素で定義されていたので、既定のテンプレートにはこのようなマークアップが含まれているのだと思います。しかし、重要なのは、独自のテンプレートを記述するとき、要素に直接プロパティを設定できることを知っていることです。

既定のテンプレートは各 RepeatButton の IsFocusable プロパティと IsTabStop プロパティを false に設定します。NoFrillsScrollBar.xaml で試すと、その理由がわかります。最も左のボタンをクリックします。ここでは、Tab キーを数回押します。入力フォーカスが RepeatButton から RepeatButton に移るのがわかります。これは明らかに予想していた動作とは異なります。

テンプレートに 4 つの RepeatButton コントロールがあるため、すべてのコントロールに同じプロパティを設定するには Style 要素を使用するのが最も簡単であると考えられます。NoFrillsScrollBar.xaml の ControlTemplate 開始タグの直後に次のマークアップを追加すると、フォーカスの問題を解決できます。

<ControlTemplate.Resources>
    <Style TargetType="{x:Type RepeatButton}">
        <Setter Property="Focusable" Value="False" />
        <Setter Property="IsTabStop" Value="False" />
    </Style>
</ControlTemplate.Resources>

また、ControlTemplate の Resources セクションを使用して、ScrollBar のビジュアル ツリーを構成する子コントロール用のテンプレートを定義することもできます。私はこれを SpringLoadedScrollBar.xaml ファイルで行いました。図 10 はそのテンプレートのビジュアル ツリー セクションを示しています。

Figure 10 他のテンプレートを参照するビジュアル ツリー (Close Figure 10)
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="50" />
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="50" />
    </Grid.ColumnDefinitions>

    <!-- Line-left button on left side -->
    <RepeatButton Grid.Column="0"
                  Command="ScrollBar.LineLeftCommand"
                  Foreground="{TemplateBinding Foreground}"
                  Template="{StaticResource templateArrow}" />

    <!-- Named track occupies most of the ScrollBar -->
    <Track Grid.Column="1" Name="PART_Track" >
        <Track.DecreaseRepeatButton>
            <RepeatButton Command="ScrollBar.PageLeftCommand"
                          Foreground="{TemplateBinding Foreground}"
                          Template="{StaticResource templateSpring}" />
        </Track.DecreaseRepeatButton>

        <Track.IncreaseRepeatButton>
            <RepeatButton Command="ScrollBar.PageRightCommand"
                          Foreground="{TemplateBinding Foreground}"
                          Template="{StaticResource templateSpring}" />
        </Track.IncreaseRepeatButton>

        <Track.Thumb>
            <Thumb Background="{TemplateBinding Foreground}" />
        </Track.Thumb>
    </Track>

    <!-- Line-right button on right side -->
    <RepeatButton Grid.Column="2"
                  Command="ScrollBar.LineRightCommand"
                  Foreground="{TemplateBinding Foreground}"
                  Template="{StaticResource templateArrow}"
                  LayoutTransform="-1 0 0 1 0 0" />
</Grid>

テンプレート内の 4 つの RepeatButton 要素で、Foreground プロパティを ScrollBar 自体の Foreground プロパティに設定し、Resources セクションで既に定義済みの Template を設定します。これら以外のテンプレート (図 10 には示していません) では RepeatButton の Foreground プロパティを使用して外観の色を付けます。図 11 は 2 つの ScrollBar コントロールを示しています。このテンプレートを使用していますが、Foreground の設定は異なっています。

図 11 異なる Foreground 設定の使用
図 11 異なる Foreground 設定の使用(画像を縮小するには、ここをクリックします)
図 11 異なる Foreground 設定の使用
図 11 異なる Foreground 設定の使用 (画像を拡大するには、ここをクリックします)

4 つのボタンに DropShadowBitmapEffect 要素を追加しました。実際、私はその効果がとても好きだったので、マウスをボタンの上に置いたときに追加の影を設定する Triggers セクションを追加することに決めました。

バネ仕掛けの ScrollBar 用のテンプレートは水平方向の向きを想定しています。また、高さがデバイス固有の単位の 50 のときに最適に見えます。これらの設定はテンプレート自体の一部ではなく、テンプレートにはそれらの設定が入り込む余地はありません。それらの設定は Style 定義に配置することをお勧めします。この例についてはすぐにお見せします。

ScrollBar の Thumb のサイズは ScrollBar ViewportSize プロパティに基づいていて、通常は、現在ビューに存在するドキュメントの比率を示すために使用されます。ViewportSize を設定せずに Thumb サイズをオーバーライドする方法が見つかりませんでした。より大きい Thumb が必要な場合、代わりに Slider 用のテンプレートを記述する必要があります。実際には Slider の外観を ScrollBar とほぼ同様にするテンプレートを記述できます。

3D スライダの作成

ScrollBar とは異なり、既定の Slider コントロールの両端には小さい 2 つの変更ボタンが付いていません。ですが、そのようなコントロールを Slider テンプレートに追加できます。Slider は 6 つの RoutedCommand オブジェクトを静的な取得のみのプロパティとして定義し、DecreaseSmall、IncreaseSmall、DecreaseLarge、および IncreaseLarge を含んでいます。Slider ではオプションで、片側または両側に目盛りを表示することもできます。

既定の Slider テンプレートを調べるときに最初に気付くことは、既定の ScrollBar テンプレートの長さの 3 倍を超える 1,000 行が含まれていることです。この理由の一部は、Slider テンプレートが Microsoft.Windows.Themes のクラスに依存しないことにあります。その代わり、すべてがテンプレートの中に直接作成されています。

このテンプレートでは、目盛りがスライダの片側だけに表示される場合に備えてさまざまな形のつまみが定義されています。また、テンプレートにはこのつまみに色を付けるために使用される多くのグラデーション ブラシが含まれています。Slider テンプレートに省略可能な TickBar 要素を含める場合、この要素の Visibility プロパティを Visibility.Collapsed に設定する必要があります。ControlTemplate の Triggers セクションでは、Visibility プロパティを Slider の TickPlacement プロパティに基づいて Visibility.Visible に設定する Trigger 要素が含まれている必要があります。この理由から、TickBar 要素に名前を割り当てる必要はありますが、特別な名前にする必要はありません。

カスタムの垂直方向のスライダについては、サウンド ミキシング ボードのレバーに似たつまみを求めていました。私の目標は、このレバーを 3 次元の外観を持つ、段階的に角度が異なるプラスティック部分のように見せることでした。3 次元的なものの見方を模倣するには、図が上下に移動するように変更する必要があります。レバーがスライダの上部にあるときはレバーの下部が見え、レバーがスライダの下部にあるときはレバーの上部が見える必要があります。

Windows Presentation Foundation には、この手の作業に理想的な 3D グラフィック機能がいくつか用意されいます。Slider3D.xaml ファイルにはテンプレートが含まれています。図 12 は別々の位置に設定された 5 個の 3D Slider コントロールを示しています。レバーの形が位置によってどのように変化するかに注目してください。

図 12 3D スライダ コントロール
図 12 3D スライダ コントロール(画像を縮小するには、ここをクリックします)
図 12 3D スライダ コントロール
図 12 3D スライダ コントロール (画像を拡大するには、ここをクリックします)

垂直方向のスライダは通常、3 列の Grid パネルで始まります。2 つの外側の列は 2 つの TickBar 要素用です。既定の垂直方向の Slider テンプレートでは、中央の Grid に溝のイメージを提供する Border 要素と、Thumb と 2 つの RepeatButton コントロールを含んでいる Track 要素が含まれています。私のテンプレートで同じアプローチを使用して、背景を Transparent に設定した単なる Border 要素として 2 つの RepeatButton コントロールを定義しました。これらのコントロールでは溝が不明瞭ですが、マウスには応答します。

つまみの高さを 50 ピクセルにすることに決めました (ここでは、Grid 内の ColumnDefinition のおかげで既に 50 ピクセルになっています)。また、ビジュアル ツリーには Background が透明に設定された別の Border 要素が含まれています。この時点で、つまみは見えませんが、スライダは完全に機能します。この透明に設定された Border 要素の子は Viewport3D 要素です。ここが面白さが増すところです。

私が思うに、3D グラフィックのプログラミングには基本的には 2 つの部分があります。単調でつらい作業とつまらない作業です。単調でつらい作業のほとんどは、MeshGeometry3D 要素の定義に関連します。この要素は 3D 座標空間で連結された三角形のコレクションとして完全に定義される表示可能なオブジェクトです。つまみについては、上部をカットした長方形のピラミッドであるオブジェクトを定義しました。

<MeshGeometry3D
    Positions="-2 -1 0, -1 -0.25 4, -2 1 0, -1 0.25 4,
                2 -1 0,  1 -0.25 4,  2 1 0,  1 0.25 4"

    TriangleIndices="0 1 2, 1 3 2, 0 2 4, 2 6 4,
                     0 4 1, 1 4 5, 1 5 7, 1 7 3,
                     4 6 5, 7 5 6, 2 3 6, 3 7 6"

    TextureCoordinates="0 1, 0.2 0.6, 0 0, 0.2 0.4,
                        1 1, 0.8 0.6, 1 0, 0.8 0.4" />

Positions 属性は図の 8 個の頂点それぞれに対する 3 次元の座標で構成されています。図の下部 (Z が 0 の部分) では X 値の範囲が -2 ～ +2、Y 値の範囲が -1 ～ +1 となっています。上部の四角形 (Z が 4 の部分) では X 値の範囲が -1 ～ +1、Y 値の範囲が -0.25 ～ +0.25 となっています。

TriangleIndices 属性の 3 つの値から成る各組は、Positions 配列へのインデックスの観点から見ると、図の表面を定義しています。たとえば、TriangleIndices の最初の組は 0、1、および 2 です。これは、Positions コレクションの最初、2 番目、および 3 番目の頂点を表します。TextureCoordinates コレクションには、図の 3 次元の各頂点に対する 2 次元の点が含まれています。これらの点は、3 次元のオブジェクトの外側を説明するために使用される GeometryDrawing に基づいたブラシの点に対応しています。

<GeometryDrawing Brush="LightGray"
    Geometry="F 1 M 0 0 L 1 0 L 1 1 L 0 1 Z
              M 0.2 0.4
              L 0.8 0.4 0.8 0.6 0.2 0.6 Z
              M 0 0 L 0.2 0.4
              M 1 0 L 0.8 0.4
              M 1 1 L 0.8 0.6
              M 0 1 L 0.2 0.6">
    <GeometryDrawing.Pen>
        <Pen Brush="DarkGray"
             Thickness=".05" />
    </GeometryDrawing.Pen>
</GeometryDrawing>

このブラシは単に薄い灰色のオブジェクト全体を説明し、両端に灰色のアクセントを付けます。

3D グラフィックのプログラミングに注意を向けているときは、照明とカメラを使って作業しています。照明を使うとき、通常は DirectionalLight と AmbientLight を組み合わせます。前者はそれ自体で非常にはっきりした色になりますが、後者はすべてが白っぽくなる傾向があります。テンプレート内のカメラは最初はレバーの Z 軸をまっすぐ下に見るように設定されていますが、レバーを上下に表示する回転変換が用意されています。Slider の Value プロパティに TemplateBinding を使用してこれを取得することはできなかったので、代わりに、次のような通常のバインドを使用しました。

<AxisAngleRotation3D Axis="1 0 0"
    Angle="{Binding RelativeSource={RelativeSource
        AncestorType={x:Type Slider}}, Path=Value}" />

Value プロパティはカメラの回転角度を決定するため、Minimum プロパティと Maximum プロパティにそれぞれ -25 と 25 を設定する必要がありました。これにより、カメラが 50 度以上の角度にスイングできるようになり、レバーは固定されます。50 度未満でも問題ありませんが、3D 効果は弱まります。Slider3D.xaml ファイルでは、次に示すように、Style を使用してこれら 2 つのプロパティ、および Orientation プロパティと Template プロパティを設定します。

<Style x:Key="styleSlider3D"
       TargetType="Slider">
    <Setter Property="Orientation" Value="Vertical" />
    <Setter Property="Minimum" Value="-25" />
    <Setter Property="Maximum" Value="25" />
    <Setter Property="Template"
            Value="{StaticResource templateSlider3D}" />
</Style>

このテンプレートを使用する Slider は実際にはテンプレートではなくこのスタイルを参照します。

適度にすることをお忘れなく

これまで見てきたように、少しの XAML のコーディング作業で、目的どおり、標準のコントロールの外観をまったく異なるものにすることができました。私が知っている大半のプログラマと同様であれば、馬鹿騒ぎをして、ロジックのあらゆる意義や良識に違反するテンプレートを記述し始めるでしょう。

テンプレートで気が狂いそうになる前に、Windows のすばらしい価値の 1 つが一貫性のあるユーザー インターフェイスだということを覚えておいてください。このことを知っていれば、ユーザーはどうすべきかわかります。そのため、創造力を備えた本能で動くことはやめて、適度にテンプレートを使用してください (MSDN®Magazine の編集者はしぶしぶ私にこのセクションを追加させました)。

ご意見やご質問は、

こちら mmnet30@microsoft.com.

まで英語でお送りください。
Charles Petzold は MSDN Magazine の寄稿編集者であり、『Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation』 (英語) (2006 年 Microsoft Press 発行) の著者でもあります。彼の Web サイトは www.charlespetzold.com (英語) です。


Team System: Team Foundation Server でのバージョン管理

Team System
Team Foundation Server でのバージョン管理
Brian A. Randell

コンテンツ
Team Foundation Server について
サーバーへの接続
バージョン管理下のドキュメント
調整としあげ
まとめ

後で知ったのですが、このコラムの計画が持ち上がったのは 2004 年 2 月のことです。当時私は、"Burton" というコードネームで呼ばれていた次期製品のソフトウェアのデザイン レビューを行うため、Redmond のマイクロソフト キャンパスにいました。レビューのどのセッションでも、手を挙げては、「拡張できる部分はあるのですか」と同じ質問を繰り返していました。丸 2 日間、返ってくる「ええ、もちろんカスタマイズできます」という答えに毎回私は顔をほころばせました。このコラムでは全面的に、その後 Visual Studio® Team System と名前を変えた Burton のカスタマイズについて扱います。

いつものようにコラムの最初に、Visual Studio Team System をクライアント、サーバーの両方で拡張したり機能強化する方法について簡単に説明します。その間に、Team Foundation Server バージョン管理リポジトリに Microsoft® Word 文書をチェックインおよびチェックアウトする Word のアドインを作成する方法を説明します。その前に、msdn.microsoft.com/vstudio/extend (英語) で Visual Studio 2005 SDK を入手してください。SDK には、Visual Studio 製品全体のカスタマイズおよび拡張についてのドキュメントとサンプルが収録されています。このアドインを作成するには、Visual Studio 2005 Professional (または Team Suite を含んだロールベースの SKU)、Visual Studio 2005 Tools for Office Second Edition Beta、および Visual Studio 2005 チーム エクスプローラが必要です。このコラムは Team System の拡張について扱うので、Word インフラストラクチャにコードをフックする具体的な方法は扱いません。ただし、参照用にすべてのコードを記載します。Office アドインの記述の詳細については、msdn.microsoft.com/office/tool/vsto (英語) を参照してください。

Team Foundation Server について

まず、Team Foundation Server と図 1 に示す今回のクライアント側アドイン (さらに言えば、他のクライアント) との基本的な対話処理を理解する必要があります。Team Foundation Server を操作している場合、まず有効なサーバーに接続する必要があります。そのためには、サーバー名、使用するプロトコル (HTTP または HTTPS)、およびポートを確認しておく必要があります。また、バージョン管理の操作を行うには、作業フォルダを伴ったワークスペースが必要です。ワークスペースは、Team Foundation Server のバージョン管理下にある成果物をクライアント側にコピーしたものであり、実作業から切り離された領域です。各ユーザーがクライアント コンピュータに複数のワークスペースを所有できます。各ワークスペースでは、作業フォルダと呼ばれるローカル ファイル パスと、バージョン管理リポジトリのパスとのマッピングが複数サポートされます。Team Foundation Server と共に Visual Studio 2005 のソース コード管理機能を初めて使用するときに、チーム エクスプローラ ツールによって、コンピュータ名が付いた既定のワークスペースが作成されます。アドインを使用して Team Foundation Server バージョン管理リポジトリに文書を出し入れするには、これらすべての操作をアドインで行う必要があります。

図 1 Team Foundation Server バージョン管理への Word 文書のチェックイン
図 1 Team Foundation Server バージョン管理への Word 文書のチェックイン(画像を縮小するには、ここをクリックします)
図 1 Team Foundation Server バージョン管理への Word 文書のチェックイン
図 1 Team Foundation Server バージョン管理への Word 文書のチェックイン (画像を拡大するには、ここをクリックします)

マイクロソフトは、Team System をプログラミングする API を複数のアセンブリに分割しました。アドインが Team Foundation Server との対話処理で使用する基本的な API は、チーム エクスプローラの一部としてインストールされる、Microsoft.TeamFoundation.Client.dll、Microsoft.TeamFoundation.VersionControl.Client.dll、Microsoft.TeamFoundation.VersionControl.Common.dll という 3 つのアセンブリに含まれています。チーム エクスプローラのセットアップ アプリケーションを実行すると、これらのアセンブリがグローバル アセンブリ キャッシュ (GAC) にインストールされます。ただし登録は行われないので、Visual Studio 2005 の参照の追加ダイアログには表示されません。Windows レジストリを編集して参照の追加ダイアログにアセンブリが表示されるようにする (support.microsoft.com/kb/306149 参照) か、手動でアセンブリを参照する必要があります。参照できるアセンブリは %Program Files%\Microsoft Visual Studio 8\Common7\IDE\PrivateAssemblies\ にあります。

サーバーへの接続

最初に、クラス ライブラリを作成して、先ほど説明したアセンブリを参照します。Team Foundation Server API を使用して面倒な動作を行うため、このライブラリに、共有メンバのセットを含んだ tfsvcUtil というクラス (Visual C#® のスタティック) を作成します。なお、アドインが動作するには、有効な Team Foundation Server に接続する必要があります。Team Foundation Server を実行するコンピュータにチーム エクスプローラを使用して接続すると、現在の資格情報を使用して接続が試みられます。資格情報が無効な場合、有効なユーザー名とパスワードの入力を要求するダイアログが表示されます。アドインも同様の動作を取る必要があります。

Team Foundation Server を操作するには、まずルート オブジェクト TeamFoundationServer のインスタンスを取得する必要があります。この API を使用すると、同じ接続を再利用する場合のニーズに合わせ、いくつかの方法でサーバーに接続できます。アドインの有効期間中は 1 つの参照のみを使用して、TeamFoundationServerFactory.GetServer メソッドを使用します。このメソッドは具体的に、サーバー名と、ICredentialsProvider を実装するオブジェクトへの参照を共に受け取るオーバーロードです。認証が失敗した場合に代替の資格情報を提供できるようにするため、GetServer を呼び出す前に UICredentialsProvider のインスタンスを作成します。GetServer を呼び出したら、GetServer が返した TeamFoundationServer インスタンスで Authenticate メソッドを呼び出して、サーバーで認証を行います。接続できたら、Team Foundation Server バージョン管理サービスへの参照を取得する必要があります。

繰り返しになりますが、マイクロソフトは Team Foundation Server を設計する際、拡張性を考慮しました。この製品が複数のサービスとして構成されていることから、それを見て取ることができます。Team Foundation Server には、バージョン管理、作業項目トラッキング、およびチーム ビルドに関連するコア サービスが用意されています。各コア サービスには、特定の機能との対話処理に使用する独自の一連のクラスがあります。バージョン管理サービスを使用するときの最上位オブジェクトは VersionControlServer クラスです。この種類のオブジェクトのインスタンスを取得するには、有効な TeamFoundationServer 参照で GetService メソッドを呼び出します。図 2 に、これまで説明した動作を行うために必要なコードを示します。コードには、基本的なエラー処理や、この後使用するクラス レベルの変数が追加されています。

Figure 2 接続と認証 (Close Figure 2)
' Team Foundation Server Version Control Utilities
Public Class tfsvcUtil
  Private Shared serverValidated As Boolean = False
  Private Shared m_tfs As TeamFoundationServer = Nothing
  Private Shared m_tfsVCS As VersionControlServer = Nothing
  Private Shared m_userWorkspace As Workspace = Nothing
  Private Shared m_serverName As String = Nothing

  Private Const MSG_UNEXPECTED As String = _
    "Unexpected exception in tfsUtil.{0}."

  Public Shared ReadOnly Property IsServerReady() As Boolean
    Get
      Return serverValidated
    End Get
  End Property

  Public Shared Sub Connect(ByVal fqServerName As String, _
      ByVal showLoginUI As Boolean)
    Try
      m_serverName = fqServerName
      If showLoginUI Then
        Dim icp As ICredentialsProvider = New UICredentialsProvider
        m_tfs = TeamFoundationServerFactory.GetServer( _
          m_serverName, icp)
      Else
        m_tfs = TeamFoundationServerFactory.GetServer(m_serverName)
      End If

      m_tfs.Authenticate()

      m_tfsVCS = CType(m_tfs.GetService( _
        GetType(VersionControlServer)), VersionControlServer)

      serverValidated = True
    Catch ex As Exception
      Throw New tfsUtilException( _
        String.Format(MSG_UNEXPECTED, "Connect"), ex, m_serverName)
    End Try
  End Sub

  Public Shared Sub Connect(ByVal serverName As String, _
    ByVal protocol As String, ByVal port As Integer, _
    ByVal showLoginUI As Boolean)

    m_serverName = String.Format("{0}://{1}:{2}", _
      protocol, serverName, port)

    tfsvcUtil.Connect(m_serverName, showLoginUI)
  End Sub
End Class

図 2 のコードをよく見ると、2 つのバージョンの Connect メソッドが使用されています。一方は、プロトコル、サーバー名、およびポートを含んだ Team Foundation Server の完全修飾名を指定する必要があります。もう一方は、サーバー接続データを個別の項目として受け取ります。どちらのバージョンも、認証が失敗した場合にログイン UI を表示するかどうかを指定するパラメータを公開しています。したがって、渡された資格情報が不十分な場合にコードを失敗させても、そのコードを再利用できます。

サーバーに接続できたら、次に文書を保存する場所を指定します。ドキュメントのバージョン管理を行うには、バージョン管理を有効にした 1 つ以上のチーム プロジェクトを作成する必要があります。現状ではチーム プロジェクトを作成するための API は提供されていないので、ドキュメントのバージョン管理を行う前には、この作業を手動で行う必要があります。チーム プロジェクトのルートの名前は $/%TeamProjectName% です。この場所から、任意の数のフォルダおよびサブフォルダを定義できます。VersionControlServer オブジェクトには、ServerItem プロパティにアクセスするために使用できる GetTeamProject メソッドがあり、ルートを示す正しい文字列を取得できます。ここで 1 点、注意してください。新しいチーム プロジェクト ウィザードを使用するときに、ソース管理フォルダを定義せずにチーム プロジェクトを作成した場合、GetTeamProject を呼び出すと Microsoft.TeamFoundation.VersionControl.Client.VersionControlException 例外が発生します。ServerItem プロパティが返す文字列は、文書のフォルダ構造を組み立てる際の基盤になります。

唯一の問題は、サーバーへの接続です。ドキュメントをサーバーに出し入れするには、クライアント コンピュータの構成も必要です。既に説明したとおり、Team Foundation Server バージョン管理には、バージョン管理のクライアント側を管理するため、ワークスペースの概念が採用されています。ファイルのチェックインまたはチェックアウトを行う場合や、ファイルのバージョン管理が行われているかどうかをチェックする場合、ワークスペースが必要です。説明したとおり、チーム エクスプローラを使用して、バージョン管理下のファイルを初めて操作するとき、コンピュータ名が付いた既定のワークスペースが作成されます。ワークスペースは、Visual Studio 2005 の [ファイル] メニューの [ソース管理] の [ワークスペース] で管理します。しかし、アドインは既存のワークスペースを使用できないので、必要に応じてワークスペースを作成するコードを追加する必要があります。

ワークスペースを作成する前に、既存のワークスペースの有無、およびバージョン管理に追加する文書の場所が有効なチーム プロジェクトに既にマップされているかどうかを確認できます。まず Workstation クラスから作成した Workstation オブジェクトを使用して、ソース コード管理に追加する文書の WorkspaceInfo インスタンスを取得します。

' Get workstation object
Dim userWorkstation As Workstation = Workstation.Current

' We need the path of the of the document:
Dim docPath As String = Environment.GetFolderPath( _
  Environment.SpecialFolder.MyDocuments) & "\Test.doc"
Dim docWSInfo As WorkspaceInfo = _
  userWorkstation.GetLocalWorkspaceInfo(docPath)

文書のパスがサーバー パスにマップされている場合、GetLocalWorkspaceInfo を呼び出すとハイドレーションが行われたインスタンスが返されます。それ以外は Nothing を返します。ここで、ワークスペースとフォルダ マッピングの問題を理解する必要があります。マッピングが存在するかどうかを確認するとき、GetLocalWorkspaceInfo では完全一致は検索されません。部分一致が行われます。たとえば、C:\Docs\Specs\MySpec.doc というパスを取得し、C:\Docs\Specs\ のフォルダ マッピングがある場合、期待どおりに WorkspaceInfo インスタンスが返されます。しかし、C:\Docs のマッピングが存在する場合、API は C:\Docs\Specs\ のマッピングがなくても有効なインスタンスを返します。一般的に、このような暗黙的な関連付けがよく選択されます。Team Foundation Server では、既存のマッピングのファイル システムのサブフォルダを再帰的にバージョン管理リポジトリのサブフォルダにマップします。マッピングがない場合、Workspace オブジェクトの CreateMapping メソッドでマッピングを作成します。有効な WorkspaceInfo オブジェクトがあるときに Workspace を取得するには、VersionControlServer インスタンスで GetWorkspace を呼び出します。

バージョン管理下のドキュメント

ここまでで、アドインの中核機能である、Word 文書のチェックインおよびチェックアウトを実行する準備が整いました。Team Foundation Server を実行するコンピュータにログインして、有効なチーム プロジェクトおよびローカル ワークスペースにアクセスできる場合、ファイルのバージョン管理を行うことができます。この作業に必要な関連メソッドは Workspace クラスのメンバです。今回のアドインは、PendAdd、PendEdit、および CheckIn を呼び出す必要があります。

PendAdd は、ドキュメントのバージョン管理を初めて行うときに使用するメソッドです。API のドキュメントを調べると、6 つのオーバーロードがあることがわかります。アイテムを 1 つキューに入れるか、一度にまとめて入れるかは、柔軟に選択できます。このメソッドには再帰をサポートするバージョンがいくつかあり、ロック レベルを調整できます。今回のアドインでは一度に 1 つの文書をチェックインするので、ファイル名を受け取る最初のオーバーローで十分です。PendAdd を呼び出すと、チェックイン用のキューに入っているアイテムの数を表す整数を返します。実際にチェックするには、GetPendingChanges メソッドを使用して、保留中の変更の一覧を取得するため Workspace にクエリする必要があります。マイクロソフトはこのメソッドを 9 つのバージョンにオーバーロードしました。このアドインには、入力として文字列を受け取るバージョンを使用します。PendAdd に渡しているファイル パスは 1 つなので、1 が返されます。GetPendingChanges を呼び出すと、要素が 1 つ含まれた PendingChange 配列が返されます。他のバージョンの GetPendingChanges を使用するときは、注意が必要です。他のバージョンを使用すると、Visual Studio 2005 で編集しているコードなど、準備ができていないファイルをチェックインできるためです。

保留中の変更が保存された配列を取得したら、PendingChange 配列インスタンスおよびチェックイン コメントを、実際のチェックイン操作を行う CheckIn メソッドに渡します。CheckIn メソッドが成功すると、チェックインする Changeset の数を表す整数が返されます。Changeset とは、Team Foundation Server バージョン管理リポジトリに保存されているアイテムに対する一連のコミット済みの変更のことです。また、作業項目のデータ、チェックイン メモ、およびポリシー オーバーライドの情報も含んでいます。チェックイン プロセスの一環で、Team Foundation Server クライアント API により、その文書についての読み取り専用のビットがオンになります。図 3 に、最初の PendAdd およびその後の CheckIn を実行する、tfsvcUtil クラスの 2 つのメソッドを示します。コードを調べると、Workspace を取得するために必要なコードが追加されていることがわかります。既定のワークスペースを必要に応じて取得または作成するヘルパ メソッド GetDefaultWorkspace もあります。このコードは、Connect メソッドが正常に実行されていることを前提としています。

Figure 3 Add Document to Version Control (Close Figure 3)
Public Shared Function AddDocumentToVCR( _
  ByVal docPath As String, _
  ByVal serverPath As String) As Boolean

  If serverValidated Then
    ' We need the Workstation
    Dim userWorkstation As Workstation = Workstation.Current
    ' We need the Workspace
    Dim docWSInfo As WorkspaceInfo = _
      userWorkstation.GetLocalWorkspaceInfo(docPath)

    If docWSInfo Is Nothing Then
      ' there's no existing mapping
      ' so let's get the default workspace
      m_userWorkspace = GetDefaultWorkspace(userWorkstation)
    Else
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

      ' Now the question is does the workspace
      ' we're working with have a mapping
      Dim docWorkingFolder As New WorkingFolder(serverPath, _
        IO.Path.GetDirectoryName(docPath))
      ' It appears that CreateMapping is happy to let us try and
      ' create a mapping even if an entry already exists
      ' This eliminates the need to search
      m_userWorkspace.CreateMapping(docWorkingFolder)

    Return m_userWorkspace.PendAdd(docPath) = 1
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function

Public Shared Function CheckInDocument(ByVal docPath As String, _
  ByVal comment As String) As Integer

  If serverValidated Then
    If m_userWorkspace Is Nothing Then
      Dim userWorkstation As Workstation = Workstation.Current
      Dim docWSInfo As WorkspaceInfo = _
        userWorkstation.GetLocalWorkspaceInfo(docPath)
      m_userWorkspace = m_tfsVCS.GetWorkspace(docWSInfo)
    End If

    Dim pc As PendingChange() = m_userWorkspace.GetPendingChanges(docPath)

    If pc IsNot Nothing AndAlso pc.Length > 0 Then
      Return m_userWorkspace.CheckIn(pc, comment)
    Else
      Return -1
    End If
  Else
    Throw New tfsUtilException(MSG_SERVER_NOT_VALIDATED)
  End If
End Function
Private Shared Function GetDefaultWorkspace( _
    ByVal userWorkstation As Workstation) As Workspace

  ' Get the users default's workspace
  Dim defaultWorkspaceInfo As WorkspaceInfo = _
    userWorkstation.GetLocalWorkspaceInfo(m_tfsVCS, _
      Environment.MachineName, m_tfs.AuthenticatedUserName)

  Dim defaultWorkspace As Workspace = Nothing
  If defaultWorkspaceInfo Is Nothing Then
    ' Need to create it
    defaultWorkspace = m_tfsVCS.CreateWorkspace( _
      Environment.MachineName, m_tfs.AuthenticatedUserName, _
      "Auto-generated by tfsUtil")
  Else
    ' Get other
    defaultWorkspace = m_tfsVCS.GetWorkspace(defaultWorkspaceInfo)
  End If

  Return defaultWorkspace

End Function

Workspace オブジェクトで PendEdit を呼び出して、編集のためファイルをチェックアウトします。マイクロソフトはこのメソッドもオーバーライドして、5 つのバージョンを用意しました。先ほどと同じく、1 つのファイル名を受け取るバージョンを使用します。この操作に必要なコードについては、このコラムに付属のダウンロード (msdn.microsoft.com/msdnmag/code07.aspx) を参照してください。このバージョンのメソッドは、PendAdd とほとんど変わりません。

調整としあげ

アドインを Team Foundation Server と連携させるために必要な中核のコードは準備がほぼ整いましたが、まだ作業がわずかに残っています。

  • Team Foundation Server にサーバー名、プロトコル、ポートなどの情報を渡す。
  • チーム プロジェクトおよびサーバー パスを指定する。
  • チェックイン操作の中で文書およびコメントを指定する。
  • すべてのフォームおよびコードをコマンド バーににフックし、Word 文書関連のイベントを処理する Word 2003 アプリケーション アドインを作成する。

上記で説明したコードを Word 2003 にホストしたアドインから動作させる場合、今挙げた作業の GUI が必要です。実は、マイクロソフトはバージョン管理用にチーム エクスプローラの関数および UI をマネージ コードで記述しましたが、Team Foundation Server の情報を指定したり文書をバージョン管理に追加したりするために実際に使用されるダイアログ ボックスは公開されていません。ダウンロード用のコードに、上記の要件を満たす 3 つの Windows フォームがあります。

まず必要なフォームは frmAddTFS です。図 4 を見ると、マイクロソフトのダイアログを模倣していることがわかります。このダイアログで、サーバー名、プロトコル、およびポートを指定できます。フォームに含まれているコードはわずかです。このコードを拡張して、Team Foundation Server を実行しているコンピュータを Active Directory® を使用して検索したり、SSL が有効かどうかを確認したりできます。

図 4 [Add Team Foundation Server] フォーム
図 4 [Add Team Foundation Server] フォーム

2 番目のフォーム frmAddtoSCC (図 5 参照) は、最初のダイアログに比べデザインと実装が複雑です。マイクロソフトのダイアログを模倣するため、tfsvcUtil クラスにヘルパ メソッド GetAllProjects および GetFolders を追加しました。GetAllProjects は名前のとおりの動作を 1 行のコードで行います。GetFolders は、VersionControlServer オブジェクトの GetItems メソッドで、指定されたパスのレベルにあるフォルダを取得します。ノードを展開するとこのメソッドが呼び出され、文書をリポジトリのどこに保存するかを指定できるようにするため、次のレベルのフォルダが取得されます。この他、ツリー ビュー コントロールを読み込んだり、サブフォルダの追加のサポートを処理したりするコードもあります。

図 5 [Add Document to Version Control] フォーム
図 5 [Add Document to Version Control] フォーム(画像を縮小するには、ここをクリックします)
図 5 [Add Document to Version Control] フォーム
図 5 [Add Document to Version Control] フォーム (画像を拡大するには、ここをクリックします)

frmCheckIn (図 1 で見たダイアログ) には、チェックインしている文書の説明、およびコメント入力用のテキスト ボックスが表示されます。チーム エクスプローラ版を使用している場合、このダイアログでは単純すぎます。今後のコラムで、このフォームを機能強化して作業項目およびチェックイン メモをサポートする予定です。

最後に、Word アドインに関する注意点を示します。アドインの詳細の説明は割愛して、何点か指摘します。まず、ダウンロード版の tfsvcUtil には、Disconnect、GetDocumentVCStatus、および IsDocumentUnderVersionControl という 3 つのヘルパ メソッドがあります。Disconnect は、Team Foundation Server を実行するコンピュータを変更できる消去用メソッドです。GetDocumentVCStatus は、アクティブな文書のバージョン管理の状態を取得して、コマンド バーのボタンと同期を取るために使用されます。IsDocumentUnderVersionControl は、文書が格納されているフォルダをワークスペースにマップしている場合に、文書のバージョン管理が行われているかどうかを確認します。

次に、Team Foundation Server クライアント API は文書に排他ロックをかけてチェックインを行います。このしくみが機能するためには、文書を閉じてチェックインを行った後で再度文書を開く必要があります。

まとめ

Visual Studio Team System はほぼすべての領域で拡張が可能であり、特にバージョン管理サービスの拡張性が優れています。豊富な API を使用して、ドキュメントの共有および管理が容易な Team Foundation Server バージョン管理リポジトリに Word 文書を保存する簡単なアドインを作成しました。

Brian の連絡先は mmvsts@microsoft.com.

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


すてきな ASP.NET: AJAX Extensions を使用してクライアント側から Web サービスを呼び出す

すてきな ASP.NET
AJAX Extensions を使用してクライアント側から Web サービスを呼び出す
Fritz Onion

コードのダウンロード : : ExtremeASPNET2007_01.exe (160KB)
コンテンツ
AJAX を使用して Web サービスを呼び出す
動作のしくみ
シリアル化
まとめ

ASP.NET は、その誕生以来、基本的にサーバー側のテクノロジと見なされてきました。もちろん、ASP.NET でクライアント側の JavaScript を生成することはありました。その代表的なものは、検証コントロールでしたが、最近では Web パーツのインフラストラクチャでスクリプトが生成されることもあります。しかし、これらの処理は、サーバー側のプロパティをクライアント側の動作に変換するという単純なものでした (この処理のおかげで、開発者は次の POST 要求を受信するまでクライアントに対する処理について考慮する必要がありませんでした)。クライアント側の JavaScript や DHTML を使用して、より対話的なページを作成する必要のある開発者は、ASP.NET 2.0 スクリプトのコールバック機能のサポートを利用して各自でコーディングを行う必要がありました。昨年、この状況は一変しました。

2005 年 9 月に開催された Microsoft Professional Developer's Conference で、マイクロソフトは、"Atlas" というコードネームの ASP.NET の新しいアドオンをリリースすることを発表しました。Atlas は、クライアント側の JavaScript、DHTML、および XMLHttpRequest オブジェクトを活用することに重点を置いたアドオンです。このアドオンは、AJAX に対応したより対話的な Web アプリケーションの作成を支援することを目的としています。このフレームワークは、Microsoft® AJAX Library および ASP.NET 2.0 AJAX Extensions という正式な名称が決まり、クライアント側のデータ バインド、DTHML のアニメーションや動作、UpdatePanel を使用したクライアントから返される POST 要求の高度な傍受など、さまざまな強力な機能を提供します。これらの機能の基盤となっているのは、クライアント側の JavaScript の呼び出しで解析および処理しやすい形式で、サーバーから非同期にデータを取得できる機能です。今月のコラムでは、ASP.NET 2.0 AJAX Extentions に対応したページでクライアント側の JavaScript からサーバー側の Web サービスを呼び出すことができる非常に便利な新機能を紹介します。

AJAX を使用して Web サービスを呼び出す

wsdl.exe ユーティリティを使用してプロキシを作成するか、または Visual Studio® の Web 参照の追加機能のいずれかを使用して Microsoft .NET Framework で Web サービスを使用した経験があれば、.NET 型を使用して Web サービスを呼び出すことには慣れていると思います。実際、.NET 型のプロキシを使用して Web サービスのメソッドを呼び出す方法は、他のクラスのメソッドを呼び出す場合とまったく同じです。 プロキシは、渡されたパラメータに基づいて XML の準備を行い、受け取った XML 形式の応答をプロキシのメソッドで指定された .NET 型に変換します。開発者が .NET Framework を使用して Web サービスのエンドポイントを使用できるという手軽さは、非常に強力で、今日のサービス指向アプリケーションの実現を支える柱の 1 つでもあります。

ASP.NET 2.0 AJAX Extensions を使用すると、ブラウザで実行するクライアント側の JavaScript に対しても、まったく同様に Web サービスのプロキシをシームレスに生成できます。サーバーでホストする .asmx ファイルを作成し、クライアント側の JavaScript クラスから、このサービスのメソッドを呼び出すことができます。たとえば、図 1 はランダムなデータを使用した架空の株価情報を取得する機能を実装した単純な .asmx サービスです。

Figure 1 StockQuoteService.asmx (Close Figure 1)
<%@ WebService Language="C#"
               Class="MsdnMagazine.StockQuoteService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
// From Microsoft.Web.Extensions.dll assembly
using Microsoft.Web.Script.Services;

namespace MsdnMagazine
{
    [WebService(Namespace = "http://msdnmagazine.com/ws")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    public class StockQuoteService : WebService
    {
        static Random _rand = new Random(Environment.TickCount);

        [WebMethod]
        public int GetStockQuote(string symbol)
        {
            return _rand.Next(0, 120);
        }
    }
}

このサービスには、標準的な .asmx Web サービスの属性以外に、クライアント側の JavaScript でも利用できる ScriptService 属性が追加されています。この .asmx ファイルを ASP.NET AJAX 対応の Web アプリケーションに展開した場合は、.aspx ファイルの ScriptManager コントロールに ServiceReference を追加することで、JavaScript からこのサービスのメソッドを呼び出すことができます (このコントロールは、Visual Studio で ASP.NET AJAX 対応の Web サイトのテンプレートを使用して Web サイトを作成すると、既定の .aspx ページに自動で追加されます)。次に例を示します。

<asp:ScriptManager ID="_scriptManager" runat="server">
  <Services>
    <asp:ServiceReference Path="StockQuoteService.asmx" />
  </Services>
</asp:ScriptManager>

これで、クライアント側の任意の JavaScript ルーチンでは、MsdnMagazine.StockQuoteService クラスを使用して、Web サービスのメソッドを呼び出すことができます。呼び出しの基になっているメカニズムは、基本的に非同期なので、同期メソッドはありません。代わりに、各プロキシ メソッドは、標準的な入力パラメータ以外に、追加のパラメータを 1 つ持っています。この追加のパラメータは、メソッドが完了したときに非同期に呼び出されるクライアント側の他の JavaScript 関数への参照を保持します。図 2 のサンプル ページでは、クライアント側の JavaScript を使用して、株価情報を取得する Web サービスの呼び出し結果をページ上にあるラベル (span) に出力します。

Figure 2 サンプル Web サービスのクライアント ページ (Close Figure 2)
<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
 <title>ASP.NET AJAX Web Services: Web Service Sample Page</title>

 <script type="text/javascript">
    function OnLookup()
    {
      var stb = document.getElementById("_symbolTextBox");
      MsdnMagazine.StockQuoteService.GetStockQuote(
        stb.value, OnLookupComplete);
    }

    function OnLookupComplete(result)
    {
      var res = document.getElementById("_resultLabel");
      res.innerHTML = "<b>" + result + "</b>";
    }
  </script>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="_scriptManager" runat="server">
      <Services>
        <asp:ServiceReference
             Path="services/StockQuoteService.asmx" />
      </Services>
    </asp:ScriptManager>
    <div>
    <h1>ASP.NET AJAX Web Services: Web Service Sample Page</h1>
    Enter symbol:
    <asp:TextBox runat="server" id="_symbolTextBox" />
    <br />
    <input onclick="OnLookup();" id="_lookupButton" type="button"
           value="Lookup" />
    <br />
    <asp:Label runat="server" id="_resultLabel" />
    </div>
    </form>
</body>
</html>

クライアント側からの Web サービスの呼び出しで問題が発生した場合には、そのことをクライアント側に通知する必要があります。したがって、エラー、中断、またはタイムアウトが発生した場合に呼び出せる別のメソッドを追加することをお勧めします。たとえば、次のように OnLookup メソッドを変更し、問題が発生したことを通知するための OnError メソッドを追加できます。

function OnLookup()
{
  var stb = document.getElementById("_symbolTextBox");
  MsdnMagazine.StockQuoteService.GetStockQuote(
    stb.value, OnLookupComplete, OnError);
}

function OnError(result)
{
  alert("Error: " + result.get_message());
}

このような方法を使用すると、Web サービスの呼び出しでエラーが発生した場合には、クライアント側で警告メッセージを表示することができます。また、クライアント側からの任意の Web サービス呼び出しに userContext パラメータを含めることができます。これは、Web サービスのメソッドに最後のパラメータとして渡される任意の文字列で、成功した場合に呼び出されるメソッドにも、失敗した場合に呼び出されるメソッドにも追加のパラメータとして設定されます。この場合、userContext の戻り値として要求した株式の標識記号を渡して、OnLookupComplete メソッドで株式の標識記号を表示するのが望ましい処理でしょう。

function OnLookup()
{
  var stb = document.getElementById("_symbolTextBox");
  MsdnMagazine.StockQuoteService.GetStockQuote(
    stb.value, OnLookupComplete, OnError, stb.value);
}

function OnLookupComplete(result, userContext)
{
  // userContext contains symbol passed into method
  var res = document.getElementById("_resultLabel");
  res.innerHTML = userContext + " : <b>" + result + "</b>";
}

同じ Web サービスを何度も呼び出し、失敗した場合に呼び出すメソッドと成功した場合に呼び出すメソッドが同じ場合は、成功時および失敗時の既定のコールバック メソッドをグローバル レベルで設定できます。グローバル レベルでコールバック メソッドを設定すると、Web サービスを呼び出すたびにコールバック メソッドを指定する必要がなくなります (ただし、メソッドごとに、グローバル レベルで定義したコールバック メソッドをオーバーライドすることはできます)。次に、呼び出しごとにコールバック メソッドを設定するのではなく、グローバル レベルで既定の成功時および失敗時のコールバック メソッドを設定する OnLookup メソッドのサンプルを示します。

// Set default callbacks for stock quote service
MsdnMagazine.StockQuoteService.set_defaultSucceededCallback(
                 OnLookupComplete);
MsdnMagazine.StockQuoteService.set_defaultFailedCallback(
                 OnError);
function OnLookup()
{
  MsdnMagazine.StockQuoteService.GetStockQuote(stb.value);
}

また、Web サービス メソッドの完全な .asmx ファイルを作成する代わりに、Web サービスをページ クラスに直接埋め込むこともできます。呼び出す必要があるメソッドの完全な Web サービス エンドポイントを作成するのが妥当な作業でない場合は、 クライアント側の JavaScript から呼び出せるページにサーバー側のメソッドを追加し、このメソッドに WebMethod 属性を追加することで、ページで Web サービスのメソッドを公開できます (メソッドは、ページに直接追加するか、または分離コードに追加します)。このようにすると、クライアント側の PageMethods オブジェクトから Web サービスのメソッドを呼び出せるようになります。図 3 の例は、株価情報サービスのサンプルを完全に書き直したものです。図 1 では、個別のサービスとして記述していましたが、ここでは Web サービス全体がページに含まれています。

Figure 3 ページ内に組み込まれた株式情報サービス (Close Figure 3)
<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>ASP.NET AJAX Web Services: Web Service Sample Page</title>
  <script runat="server">
        private static Random _rand = new Random();

        [WebMethod]
        public static float GetStockQuoteFromPage(string symbol)
        {
            return _rand.Next(0, 120);
        }
  </script>

  <script type="text/javascript">
  function OnLookup()
  {
    var stb = document.getElementById("_symbolTextBox");
    PageMethods.GetStockQuoteFromPage(stb.value,
                  OnLookupComplete);
  }

  function OnLookupComplete(result)
  {
    var res = document.getElementById("_resultLabel");
    res.innerHTML = "<b>" + result + "</b>";
  }
  </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <h1> ASP.NET AJAX Web Services: Web Service Sample Page </h1>
     Enter symbol:
        <asp:TextBox runat="server" id="_symbolTextBox" />
        <br />
        <input onclick="OnLookup();" id="_lookupButton"
               type="button" value="Lookup" />
        <br />
        <asp:Label runat="server" id="_resultLabel" />
    </div>
    </form>
</body>
</html>

ただし、これらのクライアント側のプロキシは、ASP.NET .asmx エンドポイント、Windows Communication Foundation .svc エンドポイント、またはページに直接埋め込まれた WebMethod からのみ生成できるもので、任意の Web サービスを呼び出す汎用的なメカニズムではないということに注意してください。実際、基になっている XmlHTTPRequest オブジェクトには、セキュリティ上の理由により、ページが読み込まれたドメインと同じドメインからの要求しか受け付けないという制限があります。そのため、この技法は、クライアント側でプロキシがサポートされているかどうかにかかわらず、任意の Web サービスを呼び出す場合には使用できませんでした。外部の Web サービスを呼び出す必要がある場合には、wsdl.exe または Visual Studio の Web 参照の追加機能を使用して生成した .NET プロキシ クラスを呼び出す独自のアプリケーションにブリッジ .asmx エンドポイントを設定してみることをお勧めします。

動作のしくみ

標準的な .asmx Web サービスをほとんど変更することなく、同じブラウザで、クライアント側の JavaScript からアクセスできるという事実に、初めは驚くかもしれません。この仕掛けは、ASP.NET AJAX 対応のすべての Web サイトの構成ファイルに追加される新しい .asmx HTTP ハンドラの登録にあります。

<httpHandlers>
  <remove verb="*" path="*.asmx"/>
  <add verb="*" path="*.asmx"
       type="Microsoft.Web.Services.ScriptHandlerFactory"
       validate="false"/>
</httpHandlers>

この新しく登録されたハンドラは、.asmx エンドポイントに対する標準の Web サービス要求が行われた場合に、標準の Web サービス ハンドラ (System.Web.Services.Protocols.WebServiceHandlerFactory) を呼び出します。しかし、要求で URL の末尾に /js という文字列があったり、mn= variable という形式 (?mn=GetStockQuote など) でクエリ文字列が含まれている場合、ハンドラは、Web サービスのクライアント側のプロキシを作成する一連の JavaScript を返すか (/js の場合)、または WebService から派生したクラスで定義されている対応するメソッドを呼び出し、JavaScript Object Notation (JSON) エンコード文字列で応答をパッケージ化します (?mn= の場合)。

ページに (ScriptManager コントロールの ServiceReference 要素を介した) .asmx サービスへのクライアント側からの参照が含まれている場合、ハンドラは .asmx ファイルを参照するスクリプト要素に /js を追加し、クライアント側でプロキシを作成します。たとえば、以前に作成した株価情報ページでは、次のスクリプト要素を使用してページを表示していました。

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

もちろん、このスクリプト要素は、このプロキシと対話するために必要なクライアント側の機能を提供する Microsoft AJAX Library のための追加されたスクリプト参照と併せて使用していました。このエンドポイントを自分で呼び出すには、次のような JavaScript (一部省略) が必要になります。

Type.registerNamespace('MsdnMagazine');
MsdnMagazine.StockQuoteService=function() {
  this._timeout = 0;
  this._userContext = null;
  this._succeeded = null;
  this._failed = null;
}
MsdnMagazine.StockQuoteService.prototype={
GetStockQuote:Sys.Net._WebMethod._createProxyMethod(this,
     "GetStockQuote",
     "MsdnMagazine.StockQuoteService.GetStockQuote",
     "symbol"), ...
}

この JavaScript では、ScriptManager コントロールが配置されているすべてのページに含まれる (名前空間や WebMethod クラスのような) Microsoft AJAX Library の機能を使用しています。この JavaScript で作成されたプロキシのメソッドは、クエリ文字列 (この場合は ?mn=GetStockQuote) を使用して .asmx エンドポイントを呼び出すように初期化されます。そのため、クライアントから MsdnMagazine.StockQuoteService.GetStockQuote を呼び出すと、このメソッドは同じ .asmx エンドポイントの非同期な Web 要求に変換されます。このクライアント側のプロキシの生成とサーバー側の JavaScript による Web サービス呼び出しのサーポートにより、クライアント側の呼び出しを、直感的な方法で .asmx Web サービスに含めることができます。

シリアル化

AJAX ベースの Web サービスの既定のシリアル化の形式は JSON です。この前のセクションで示したページでは、Web サービスの要求と応答の本文は次のようになっています。

Request: {"symbol":"ABC"}
Response: 51

これは、.asmx Web サービスを呼び出すときに見慣れている標準的な XML の形式でないことは確かです。.asmx エンドポイントは、XML 形式でシリアル化されるように構築されているので、ASP.NET 2.0 AJAX Extensions の主要な追加機能の 1 つは JSON シリアライザであるといえます。実際、このシリアライザには 2 種類あります。1 つは、クライアント側で使用する JavaScript に実装されたもので、もう 1 つは、AJAX クライアントが .asmx エンドポイントを呼び出したときにサーバー側で使用する .NET に実装されたものです。サーバー側のシリアライザは、Microsoft.Web.Script.Serialization.JavaScriptSerializer クラスから、クライアント側のシリアライザは Sys.Serialization.JavaScriptSerializer クラスから利用できます。シリアル化の形式として XML ではなく JSON を利用する主な利点の 1 つは、JSON 文字列を評価するだけで JavaScript に含まれているオブジェクトを逆シリアル化できることです。そのため、クライアント側のシリアライザ クラスを逆シリアル化する方法は非常に簡単です (エラー チェックはなくなりました)。

Sys.Serialization.JavaScriptSerializer.deserialize=
    function(){eval('('+data+')');}

これに対して、JavaScriptSerializer の serialize メソッドでは、これよりも多くの処理が行われます。JSON を使用するもう 1 つの利点は、同等の XML を使用した場合よりもコンパクトであることです。

標準的な Web サービスで型を XML 形式にシリアル化するのに使用されている XmlSerializer と同様に、JavaScriptSerializer を使用して任意の .NET 型を JSON 形式にシリアル化できます。これに必要な処理は、JavaScriptSerializer クラスの Serialize メソッドを呼び出すことだけです。図 4 は、複雑な Person 型 (図 5 参照) をシリアル化するサンプル コンソール アプリケーションです (このアプリケーションには、ASP.NET AJAX Extensions のインストール時にグローバル アセンブリキャッシュ (GAC) にインストールされた Microsoft.Web.Extensions.dll へのアセンブリ参照が含まれている必要があります)。

Figure 5 サンプルの Person 型 (Close Figure 5)
namespace MsdnMagazine.SampleTypes
{
    public class Person
    {
        private string _firstName;
        private string _lastName;
        private int _age;
        private bool _married;

        public bool Married
        {
            get { return _married; } set { _married = value; }
        }

        public int Age
        {
            get { return _age; } set { _age = value; }
        }

        public string FirstName
        {
            get { return _firstName; } set { _firstName = value; }
        }

        public string LastName
        {
            get { return _lastName; } set { _lastName = value; }
        }
    }
}
Figure 4 JSON シリアル化コンソール アプリケーション (Close Figure 4)
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.Web.Script.Serialization;

class App
{
    static void Main()
    {
        Person p = new Person();
        p.FirstName = "Bob";
        p.LastName = "Smith";
        p.Age = 33;
        p.Married = true;

        JavaScriptSerializer jss = new JavaScriptSerializer();
        string serializedPerson = jss.Serialize(p);
        Console.WriteLine(serializedPerson);
    }
}

コンソール アプリケーションの出力は、JSON 形式の Person クラスです。これは、次のようになります。

{"Married":true,"Age":33,"FirstName":"Bob","LastName":"Smith"}

XmlSerializer と同様に、JavaScriptSerializer では、型に含まれていて、パブリックにアクセス可能なデータのみをシリアル化します。多重参照を解決するためのサポートは提供されていません。ただし、このシリアライザでは、標準的な .asmx Web サービスでシリアル化できるすべての型 (DataSet を含む) を問題なくシリアル化することができます。複雑なサービスを .asmx ファイルで定義されている SOAP ベースの Web サービスと同じくらい簡単に処理できることを知っていれば、さらに複雑な Web サービスを構築することが可能になります。

図 6 の例は、定義済みの 2 つの Person オブジェクトを取得し、その属性を適宜変更する Marry メソッドを実装した MarriageService という名前の Web サービスです (付属の ASP.NET ページは、今月のコード サンプルに含まれています)。

Figure 6 MarriageService.asmx (Close Figure 6)
<%@ WebService Language="C#"
    Class="MsdnMagazine.MarriageService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using Microsoft.Web.Script.Services;
using MsdnMagazine.SampleTypes;

namespace MsdnMagazine
{
    [WebService(Namespace = "http://msdnmagazine.com/ws")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    [GenerateScriptType(typeof(Person))]
    public class MarriageService : WebService
    {
        [WebMethod]
        public Person[] Marry(Person[] couple)
        {
          if (couple.Length != 2)
            throw new ArgumentException("2 persons only!");
          if (couple[0].Married || couple[1].Married)
            throw new ArgumentException("Tell your spouse first!");

          couple[0].LastName += "-" + couple[1].LastName;
          couple[1].LastName = couple[0].LastName;
          couple[0].Married = couple[1].Married = true;
          return couple;
        }
    }
}

クライアント側のスクリプトでは XML を使用することもできます。Web サービスを定義するときに使用する標準的な WebMethod 属性に加えて、Microsoft.Web.Script.Services 名前空間には ScriptMethod という名前の新しい属性があります。この属性には、Json または Xml のどちらかを設定できる ResponseFormat プロパティがあります (既定値は Json です)。

namespace PS
{
    [ScriptService]
    [WebService(Namespace = "http://pluralsight.com/ws")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class StockQuoteService : WebService
    {
        [WebMethod]
        public int GetStockQuote(string symbol)
        {
            return (new Random()).Next(0, 120);
        }
    }
}
シリアル化された応答は、次のようになります。
<?xml version="1.0" encoding="utf-8"?><int>74</int>

このメソッドをクライアント側の JavaScript から呼び出して XML 応答を処理するタイミングは開発者の裁量で決められます。変換処理を行うか、または既に MSXML を使用している場合、この方法が役立ちます。

まとめ

ASP.NET 2.0 AJAX Extensions には、クライアント側のコードから特定の ASP.NET 2.0 アプリケーション サービスにアクセスするために、ProfileService と AuthenicationService という 2 種類の組み込みのサービスが含まれています。これらの 2 つのクライアント側のプロキシ クラスを使用すると、各クライアントのプロファイル値を設定および取得したり、(既定の Membership プロバイダを使用して) 認証を行ったり、認証 Cookie を付与したりする処理をクライアント側のスクリプト内だけで行うことができます。

ASP.NET 2.0 AJAX Extensions に関する多くの説明やデモンストレーションでは、応答性の高いユーザー インターフェイスを実現する華やかなコントロールに注目していますが、このアドオンの最も優れていて役に立つ機能は、クライアント側の JavaScript から Web サービスを直接呼び出せることです。.NET Framework と JSON シリアライザ、馴染みのある .asmx Web サービスとの直接統合、バッチ処理のサポート、および外部 Web サービスへの自動生成ブリッジなど、Web サービスに対する手厚いサポートが、もっとも感動的な機能になるかもしれません。

Fritz の連絡先は xtrmasp@microsoft.com..

(英語) です。
Fritz Onion は、Web 開発のカリキュラムを提供している Microsoft .NET のトレーニング プロバイダである Pluralsight の共同創立者です。また、『Essential ASP.NET』 (Addison Wesley, 2003) (英語) と『Essential ASP.NET 2.0』 (Addison Wesley, 2006) (英語) の著者でもあります。Fritz の連絡先は pluralsight.com/fritz (英語) です。


セキュリティに関するブリーフィング: プロトコル遷移の使用－最前線からのヒント

セキュリティに関するブリーフィング
プロトコル遷移の使用－最前線からのヒント
Keith Brown

コードのダウンロード : : SecurityBriefs2007_01.exe (172KB)
コンテンツ
特権のジレンマ
高い特権の一部を取り除く
カーネル オブジェクトとハンドル
カーネル オブジェクトと ACL
サンプル ゲートウェイ
セキュリティの考慮事項

以前の Security Briefs コラムで、プロトコル遷移と制約付き委任について詳しく取り上げました。そこでは、基になる S4U Kerberos 拡張について詳しく説明し、この拡張を実際に機能させる方法を紹介しました。プロトコル遷移の詳細については、素人の説明でもよければ、私の書籍、『The .NET Developer's Guide to Windows Security』 (英語) の「Item 63」をご覧ください。

Windows Server® 2003 が広範に展開されるようになり、プロトコル遷移を使用してイントラネットにセキュリティが確保されたゲートウェイを組み込もうとしている読者からメールが届くようになりました。バックエンドで Kerberos を使用するメリットを活用し、フロントエンドでいくつかの認証形式を選択して使用することを考えている場合、プロトコル遷移は優れた機能です。

プロトコル遷移では、ゲートウェイ (Web ポータルなど) で有効な技法を使ってユーザーを認証してから、Windows® アカウントのログオンを確立できます。ログオンが確立できれば、Windows アカウントの権限を借用でき、Windows アカウントを使用してサーバー上のリソースにアクセスできます。つまり、グループ、アクセス制御リスト (ACL)、承認マネージャ (AzMan) の役割、COM+ の役割など、Windows に組み込まれたユーザー セキュリティ機能をすべてバックエンドで使用できます。

たとえば、IIS がユーザー アカウントへの資格情報のマッピングをサポートするように、Windows アカウントをマッピングする機能は多くのゲートウェイに一般的な機能ですが、プロトコル遷移ではゲートウェイが使用するすべてのアカウントのパスワードをゲートウェイに保存することなく、こうしたマッピングを可能にします。通常、ログオン セッションの確立にはパスワードが必要ですが、プロトコル遷移ではゲートウェイを信頼し、対応するパスワードを認識しないで、ログオン セッションを確立できます。ただし、このように確立されたセッションには、到達できるネットワークの範囲に制限があります。つまり、ゲートウェイの Allowed-to-Delegate-to (A2D2) リストに記載されたサービスのセッションのみに Kerberos チケットが発行されます。

パスワードを保存するゲートウェイとプロトコル遷移を使用するゲートウェイに攻撃を加えることを考えてみます。攻撃者がユーザーのパスワードを入手すると、そのユーザーがパスワードを変更するまでは、攻撃者が望めばネットワーク上の任意の場所でそのユーザーの権限を借用できます。しかし、攻撃者がプロトコル遷移で信頼されているゲートウェイを侵害する場合、検索するパスワードがありません。確かに、"重要なので委任できない" とマークされていないアカウントを持つ任意のユーザーでログオンすることはできますが、こうした資格情報を使用してアクセスできるのは、A2D2 リストに記載されているサービスのみです。損傷を受ける領域や時間は制限されます。このような場合は、攻撃者を発見したらすぐにシャットダウンできます。それに対してユーザーのパスワードを入手した攻撃者は、安全な場所からの攻撃を容易に続けることができます。

ゲートウェイにこの種の信頼を行う方法は、ゲートウェイ プロセスのセキュリティ プリンシパルにプロトコル遷移を使用する権限を許可し、権限を委任できる特定のサービスを一覧することです。図 1 には、このコラムのコードのテストに使用したラボ環境の例を示します。これは、MyWebIdentity というユーザー アカウントの [委任] プロパティ ページです。このアカウントは、サンプル Web ポータルを実行する IIS 6.0 アプリケーション プールに割り当てられる特権が特別低いアカウントです。ご覧のように、MyWebIdentity では [指定されたサービスへの委任でのみこのユーザーを信頼する] オプションと [任意の認証プロトコルを使う] オプションが選択されているので、プロトコル遷移向けに構成されています。ここでは、A2D2 リストを使用して MyWebIdentity を制約し、プロトコル遷移を使用して、FileServer というドメイン内のコンピュータのファイル システムのみに Kerberos チケットを取得できることに注意してください。CIFS は Common Internet File System の略で、Microsoft® ファイル サーバーの名前です。

図 1 プロトコル遷移のアカウントの構成
図 1 プロトコル遷移のアカウントの構成(画像を縮小するには、ここをクリックします)
図 1 プロトコル遷移のアカウントの構成
図 1 プロトコル遷移のアカウントの構成 (画像を拡大するには、ここをクリックします)

ところで、通常のユーザー アカウントでどのようにして [委任] タブを表示したか不思議に思われたかもしれません。これは、そのユーザー アカウントにサービス プリンシパル名 (SPN) を割り当てたことによります。これにより、このアカウントは人間が使用するものではなく、サービスに使用するようにデザインされたものであることを Active Directory® に通知します。[委任] タブは、少なくとも 1 つ SPN が割り当てられたセキュリティ プリンシパルにのみ表示されます。これが、一般にコンピュータ アカウントにしかこのタブが表示されない理由です。SPN の詳細については、「What is a Service Principal Name」 (英語) を参照してください。

特権のジレンマ

プロトコル遷移は実際に優れた機能ですが、パスワードを認識しないでユーザーのログオン セッションを確立する基本機能というのは恐ろしい機能でもあります。技術的には、ログオン セッションを確立するためにプロトコル遷移の動作に権限を許可する必要さえありません。Windows Server 2003 では、だれでも S4U2Self Kerberos 拡張を使用してログオンを確立できます。プロトコル遷移を使用する権限を許可している場合を除いて、他のどのサーバーとも対話できなくなります。では、S4U Kerberos 拡張に関する以前のコラムで説明したように、WindowsIdentity の S4U ログオン コンストラクタを使用するコードで通常ユーザーが特権を昇格するとしたら、これを防ぐにはどのようにすればよいのでしょうか。

string admin = "administrator@mydomain.local";
using (WindowsIdentity id = new WindowsIdentity(admin)) // S4U logon
using (WindowsImpersonationContext ctx = id.Impersonate()) {
    string SAM = @"c:\windows\repair\sam";
    Console.WriteLine(File.ReadAllText(SAM));
}

このコードでは、特権をドメイン管理者のレベルに一時的にアップグレードすることで、ローカル コンピュータのセキュリティ アカウント データベースを攻撃しています。

明らかに、これが可能だとしたら、まずいことになります。でも、大丈夫です。可能ではありません。お分かりのように、攻撃者は上記のコードの最初の数行は正常に実行できますが、S4U ログオンを実行したときに受け取るセキュリティ トークンは非常に権限の低いもので、権限の借用はサポートされません。管理者のアカウント用に受け取ったトークンは管理者の識別のみに使用でき、管理者の権限を借用することはできないので、File.ReadAllText 呼び出しは失敗します。

高い特権を備えたユーザーが WindowsIdentity の S4U ログオン コンストラクタを使用してトークンを取得した場合のみ権限の借用に成功します。内部では、このコンストラクタは Win32® API の LsaLogonUser を呼び出し、呼び出し側プロセスの ID が SeTcbPrivilege (SYSTEM が既定で保持します) を保持しているかどうかをチェックします。これは Windows で最も強力な特権で、事実上オペレーティング システム自体の一部であるか、TCB (Trusted Computing Base) であることを示しています。既に信頼されていれば、どのようなことでも実行でき、セキュリティ システムによって権限を借用できる S4U ログオンが発行されます。

読者の皆さんは、この辺りから混乱に陥るのではないでしょうか。プロトコル遷移を使いたいと考えても、ゲートウェイ プロセスを SYSTEM で実行しない限り、この機能を使って確立したログオンでは使い道がありません。このようなログオンでは、権限を借用してバックエンド リソースにアクセスできません。先へ進み、ゲートウェイの権限を昇格して SYSTEM で実行し、すぐに後悔することになる読者もいれば、このような措置を拒み、従来からある他の解決策を探す読者もいるでしょう。

高い特権の一部を取り除く

このジレンマは、ゲートウェイ プロセスを半分に分解することで解決できます。ここに、TCB 特権を必要とするコードが文字どおり 1 行存在します。

new WindowsIdentity(userPrincipalName)

この 1 行のコードは高い特権を必要とするため、ゲートウェイ全体の特権レベルを引き上げるという無謀なものです。リモート ユーザからの入力を受け付け、常時実行され、高い特権で実行されるプロセスがあれば、必ず危険な状況に陥ります。この 1 行のコードをゲートウェイ プログラム全体から削除し、これを高い特権を備えたヘルパ プロセスに移動することが解決策です。これは Win32 システムのベテランの開発者であれば難しいことではありませんが、プラットフォームや Win32 の初心者であれば非常に難しいことのように思えるかもしれません。そこでこのコラムの残りの部分では、この 1 行の非常に重要なコードをホストする、セキュリティが確保されたログオン サービスをビルドする方法を示すことにしましょう。この方法を示す過程で、Microsoft .NET Framework を使用してプロセス間でトークンを受け渡す方法、System.Security.AccessControl を拡張する方法、高い特権を備えたプロセスをホストするために COM+ を使用する方法など、さまざまなことを数多く学ぶことになります。

カーネル オブジェクトとハンドル

各 Windows プロセスには、ファイル、セマフォ、トークンなどの Windows カーネル オブジェクトを参照するハンドル テーブルがあります。Win32 ハンドルはこのテーブルのインデックスとなる整数なので、多くの場合、ハンドルを開いたプロセス内でのみ意味があります。たとえば、ファイル ハンドルの値をクリップボードに書き込むことはできません。また、異なるプロセスでハンドルの値を読み取り、その値をハンドルとして使用して作業を開始することは要求できません。

開いたハンドルを別のプロセスに転送する場合は、Win32 DuplicateHandle API を使用しなければなりません。ログオン サービスが取得する予定の WindowsIdentity はこうしたハンドルの 1 つをラップするので、通信用にこれをシリアル化することは現実的な選択肢ではありません。対象プロセスのハンドル テーブルにエントリを挿入するには、DuplicateHandle を使用します。

では、ここで計画を示しましょう。ゲートウェイ サービスでは、セキュリティの確保が容易なプロセス間通信技法をいくつか使用して、ログオン サービスへの呼び出しを行う予定です。ゲートウェイではこの呼び出しの一環として 2 つの引数を提供します。1 つはログオンするユーザーの名前です。もう 1 つはゲートウェイのプロセスのプロセス ID です。この 2 つの引数により、ログオン サービスは取得したトークン ハンドルを送り出す方向を認識します。その後、ログオン サービスから呼び出し側のハンドル テーブルのインデックスを表す整数のハンドル値が返されます。呼び出し側ではこの値を Win32 ハンドルとして使用して、このハンドルをラップする独自の WindowsIdentity を構築できます。

次に、このコントラクトを表す .NET インターフェイスを示します。

public interface ILogonService {
    int LogonUserViaProtocolTransition(string upn, int callerPID);
}

このインターフェイスを最適に実装する方法を決めるために、次の要件を検討します。まず、このログオン サービスをホストするプロセスは TCB 特権で実行する必要があり、その最善の方法は SYSTEM として実行することです。次に、ILogonService インターフェイスのセキュリティを注意深く保護し、指定したゲートウェイだけにこのインターフェイスを使用させる必要があります。こうしておかないと、このインターフェイスが特権レベルを昇格する道具として使われ、ローカル管理者権限を備えたドメイン アカウントでログオンしたユーザーはだれでも特権を昇格できます。幸いなことに、この 2 つの問題を容易に解決できるテクノロジが Windows Server 2003 に組み込まれています。それがおなじみの COM+ です。

COM+ は過去の遺物であると言われているのを耳にしたことがあります。そんなことはありません。私見ですが、このような方法で特権の一部を取り除く際に実行できる唯一の方法が COM+ です。System.EnterpriseServices を使用することで、選択した言語で COM+ コンポーネントを記述できます。その後、COM+ ホスティング モデルを利用して、SYSTEM として実行されるサービスでコンポーネントを自動的にホストできます。さらに、そのサービス用にコードを 1 行も記述する必要がありません。また、COM+ 組み込みの役割ベースのセキュリティも忘れないでください。これにより、コンポーネントのログオン メソッドへのアクセスを制限でき、ゲートウェイの実行を承認されたセキュリティ プリンシパルのみがログオン サービスの使用を許可されます。

LogonService アセンブリをビルド後、RegSvcs.exe を使用してこのアセンブリを COM+ カタログに登録し、コンポーネント サービス管理ツールを開いて、LogonService の COM+ サーバー パッケージを見つけます。このアセンブリを SYSTEM として実行されるサービスにするために必要なことは、このパッケージのプロパティ シートを表示して、[アクティブ化] タブに移動し、[アプリケーションを NT サービスとして実行する] チェック ボックスをオンにします。この作業を行った後 [ID] タブに戻ると、[ローカル システム] ID オプションが使用可能になります。このオプションをオンにすれば、ログオン サービスを SYSTEM として実行するための構成作業は完了です。

カーネル オブジェクトと ACL

Windows カーネル オブジェクトについてもう 1 つ注意しておくことは、送り返すトークンも含めて、各オブジェクトのセキュリティがアクセス制御リスト (ACL) で保護されることです。技術的には、ハンドルをコピーして呼び出し側に戻すことになるので、呼び出し側でその特定のハンドルのみを使用している限りは、ACL が問題になることはありません。ただし、特に、呼び出し側でトークン ハンドルを操作する WindowsIdentity のような .NET ラッパーを使う予定がある場合は、呼び出し側のセキュリティ プリンシパルがそのトークンを使用する権限を保証するためにそのトークンの ACL を調整するのが賢明です。そうしないと、呼び出し側に提供したトークンを使用する権限が呼び出し側で拒否されることになります。

残念ながら、.NET Framework にはトークンを直接操作できるクラスはありません。現状では、最も近い .NET Framework のトークン クラスが WindowsIdentity です。しかし、よい機会ですので、トークンの ACL の読み取りと書き込みをサポートするように、System.Security.AccessControl のクラスを拡張しました。このことは以前に説明したことがあるのはわかっていますが、今回は AccessControl 名前空間を開発した Windows チームのメンバにお願いしました。トークンをサポートするように NativeObjectSecurity を拡張することは驚くほど簡単でした。今回の TokenSecurity の実装を図 2 に示します (完全なコードはこのコラムに付属するダウンロードから入手できます)。多くのコードが存在するように見えますが、詳しく調べてみると、行っているのはトークンの ACL をカスタマイズすることだけです。ここで最も興味深いコードは TokenRights の列挙で、残りのすべてのコードはひな型です。

Figure 2 トークンのセキュリティをサポートするための AccessControl の拡張 (Close Figure 2)
public enum TokenRights : int {
    AssignPrimary       = 0x0001,
    Duplicate           = 0x0002,
    Impersonate         = 0x0004,
    Query               = 0x0008,
    QuerySource         = 0x0010,
    AdjustPrivileges    = 0x0020,
    AdjustGroups        = 0x0040,
    AdjustDefault       = 0x0080,
    AdjustSessionId     = 0x0100,
    Read            = 0x00020008,
    Write           = 0x000200E0,
    Execute         = 0x00020000,
    All             = 0x000F00FF, // TOKEN_ALL_ACCESS_P
    AllPlusSessionId= 0x000F01FF, // TOKEN_ALL_ACCESS
    MaximumAllowed  = 0x02000000,
    AccessSystemSecurity = 0x01000000,
}

public class TokenAccessRule : AccessRule {
    public TokenAccessRule(IdentityReference identity,
        TokenRights tokenRights, AccessControlType aceType)
        : base(identity, (int)tokenRights,
            false, InheritanceFlags.None,
            PropagationFlags.None, aceType) {
    }
    public TokenRights TokenRights {
        get { return (TokenRights)base.AccessMask; }
    }
}

public class TokenAuditRule : AuditRule {
    public TokenAuditRule(IdentityReference identity,
        TokenRights tokenRights, AuditFlags auditFlags)
        : base(identity, (int)tokenRights,
            false, InheritanceFlags.None,
            PropagationFlags.None, auditFlags) {
    }
    public TokenRights TokenRights {
        get { return (TokenRights)base.AccessMask; }
    }
}

public class TokenSecurity : NativeObjectSecurity {
    public TokenSecurity()
        : base(false, ResourceType.KernelObject) {
    }
    public TokenSecurity(SafeTokenHandle token)
        : this(token,
               AccessControlSections.Owner |
               AccessControlSections.Group |
               AccessControlSections.Access) {
    }
    public TokenSecurity(SafeTokenHandle token,
        AccessControlSections includeSections)
        : base(false, ResourceType.KernelObject,
            token, includeSections) {
    }
    public override Type AccessRightType {
        get { return typeof(TokenRights); }
    }
    public override Type AccessRuleType {
        get { return typeof(TokenAccessRule); }
    }
    public override Type AuditRuleType {
        get { return typeof(TokenAuditRule); }
    }
    public override AccessRule AccessRuleFactory(
        IdentityReference identity, int accessMask,
        bool isInherited, InheritanceFlags inheritanceFlags,
        PropagationFlags propagationFlags,
        AccessControlType aceType) {

        return new TokenAccessRule(identity,
            (TokenRights)accessMask, aceType);
    }
    public override AuditRule AuditRuleFactory(
        IdentityReference identity, int accessMask,
        bool isInherited, InheritanceFlags inheritanceFlags,
        PropagationFlags propagationFlags, AuditFlags auditFlags) {

        return new TokenAuditRule(identity,
            (TokenRights)accessMask, auditFlags);
    }
    public void AddAccessRule(TokenAccessRule rule) {
        base.AddAccessRule(rule);
    }
    public void AddAuditRule(TokenAuditRule rule) {
        base.AddAuditRule(rule);
    }
    public void Persist(SafeTokenHandle token,
        AccessControlSections includeSections) {
        base.Persist(token, includeSections);
    }
}

ログオン サービスの実装を図 3 に示します。おそらく最初に気が付くのは、COM+ カタログでのホスティングのために LogonService アセンブリとクラスを準備する System.EnterpriseServices 属性です。特に、呼び出し側を認証し、ログオン サービスを呼び出すための特定の役割に所属させることを要求する security 属性に注意してください (ここでは ProtocolTransitionUsers という役割を使用したので、ログオン サービスを展開し、COM+ カタログに構成する際に、ゲートウェイを実行するセキュリティ プリンシパルがこの役割のメンバになるようにしました)。

Figure 3 ログオン サービス (Close Figure 3)
[assembly: ApplicationName("PluralsightLogonService")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(true,
  Authentication = AuthenticationOption.Privacy,
  ImpersonationLevel = ImpersonationLevelOption.Identify,
  AccessChecksLevel = AccessChecksLevelOption.ApplicationComponent)]

 public interface ILogonService {
    int LogonUserViaProtocolTransition(string upn, int callerPID);
}

[ComponentAccessControl(true)]
[SecureMethod]
public class LogonService : ServicedComponent, ILogonService {
    public LogonService() { }

    [SecurityRole("ProtocolTransitionUsers")]
    [SecurityRole("Marshaler")]
    public int LogonUserViaProtocolTransition(
        string upn, int callerPID) {

        SecurityIdentifier callerSID = null;
        CoImpersonateClient();
        try {
            callerSID = WindowsIdentity.GetCurrent().User;
        }
        finally {
            CoRevertToSelf();
        }

        using (SafeProcessHandle targetProcess = OpenProcess(
                    PROCESS_DUP_HANDLE, false, callerPID)) {
            if (targetProcess.IsInvalid) {
                Marshal.ThrowExceptionForHR(
                    Marshal.GetLastWin32Error());
            }

            // this is the line of code that requires high privilege
            using (WindowsIdentity identity =
                new WindowsIdentity(upn)) {

                SafeTokenHandle token = new SafeTokenHandle(
                    identity.Token, false);

                throwIfUserIsAdmin(token);
                grantPermission(token, callerSID);

                // push the token handle into the caller's process
                int targetHandle;
                if (!DuplicateHandle(
                        GetCurrentProcess(), token,      // source
                        targetProcess, out targetHandle, // target
                        0, false, DUPLICATE_SAME_ACCESS)) {
                    Marshal.ThrowExceptionForHR(
                        Marshal.GetLastWin32Error());
                }
                return targetHandle;
            }
        }
    }

    private void grantPermission(SafeTokenHandle token,
                                 SecurityIdentifier callerSID) {
        // use our extension to System.Security.AccessControl!
        TokenSecurity tokenSecurity = new TokenSecurity(token,
            AccessControlSections.Access);
        tokenSecurity.AddAccessRule(new TokenAccessRule(callerSID,
            TokenRights.All, AccessControlType.Allow));
        tokenSecurity.Persist(token, AccessControlSections.Access);
    }

    //Pinvoke stubs
    ...
}

また、CoImpersonateClient と CoRevertToSelf への呼び出しにも気付くかもしれません。これらは、ほとんどの DCOM プログラマが使い慣れた Win32 API です。この呼び出しにより、ログオン サービスを呼び出しているセキュリティ プリンシパルがわかるので、そのセキュリティ ID を変数に取り込み、後からその変数を使用して、そのトークンに権限を許可します。

S4U ログオン コンストラクタを使用してユーザーがログオンを行うコードの残りの部分では、トークンをコピーして呼び出し側のプロセスに戻しています。幸いなことに、ログオン サービスは SYSTEM として実行されるので、希望する任意のプロセスにハンドルをコピーする権限を既定で備えています。ご存じのように、仕様上、SYSTEM にはすべてのプロセスを含めて、システム上のすべてのオブジェクトに対して完全な権限があります。ここで、throwIfUserIsAdmin の呼び出しに注意してください。このヘルパ メソッドでは、返されたトークンにローカル管理特権があるかどうかをチェックします。このヘルパでは、この解決策全体が無意味にならないように、侵害されたゲートウェイがログイン サービスを使用して管理特権を取得するのを防ぎます。

最後に、このサンプル コードでは、ハンドルの管理を支援するために、SafeHandle から派生したカスタム クラスを作成しました。一般に、多くの P/Invoke 作業を行う場合、これが優れた考え方であることに気付くでしょう。この部分のコードは理解しやすいもので、付属のダウンロードの SafeHandles.cs ファイルに含まれています。"Shawn Farkas SafeHandle" という語句でインターネットを検索してください。私がお気に入りの Microsoft .NET Framework チームのメンバの 1 人が書いたブログ エントリが見つかります。そのブログでは、独自の SafeHandle クラスを簡単にビルドする方法が示されています。

サンプル ゲートウェイ

サンプル ゲートウェイをお見せする前に、実に重要なことを強調しておきます。「プロトコル遷移の使い方を間違えるのは簡単です。」この機能からログオンしたユーザーを正しく認証していることを確認する方法はありません。ドメイン管理者が Web アプリケーションの ID のプロトコル遷移を有効にすると、ドメイン コントローラが通常行う仕事 (ユーザーの認証) をゲートウェイに委任します。

その結果、考えられる最も厳密な方法でログオンするユーザーを認証し、プロトコル遷移を実行するために特別な特権のみを使用して、そのユーザーを表す該当の Windows ユーザー アカウントにログオンすることがゲートウェイの仕事になります。

このコラムはプロトコル遷移に注目しているので、フロントエンドでユーザーを認証する多くの方法とは異なり、このゲートウェイ サンプルではログオンするユーザーの認証をまったく行いません。ユーザー名と、そのユーザーの S4U ログオンを使用して読み取りを試みるネットワーク上のファイルへのパスを入力することだけが許可されます。特権の低いゲートウェイ プロセスからプロトコル遷移を使い始めるには、このコラムのサンプル ゲートウェイを調べ、読者にとって意味のある何らかのフロントエンド認証手法を追加してコードを拡張できます。繰り返しになりますが、このコラムのゲートウェイ サンプルを単純にコピーして、独自のアプリケーションに貼り付けるのはやめてください。この方法では、攻撃に広く門戸を開いたままになります。

図 4 に、サンプル ゲートウェイが使用する Web フォームを示します。入力としてユーザー プリンシパル名とファイル パスを指定します。その結果、そのユーザーの S4U ログオンを確立するためにログオン サービスを呼び出し、返されるハンドルを WindowsIdentity にラップして、そのユーザーの権限を借用しファイルを読み取ります。ゲートウェイと同じコンピュータ上のファイルまたはネットワーク上のファイルを開くことができます。ファイルの読み取りを予定しているすべてのサーバーを含めるように A2D2 リストを構成する必要があります。実際には、各コンピュータで SYSTEM として実行される Workstation サービスというデーモンを経由するファイル システム固有の条件ですが、ゲートウェイを実行するユーザー アカウントだけでなく、ゲートウェイのコンピュータ アカウントの A2D2 リストの設定も必要になります。

図 4 サンプル ゲートウェイ Web アプリケーション
図 4 サンプル ゲートウェイ Web アプリケーション(画像を縮小するには、ここをクリックします)
図 4 サンプル ゲートウェイ Web アプリケーション
図 4 サンプル ゲートウェイ Web アプリケーション (画像を拡大するには、ここをクリックします)

図 5 に、ログオン サービスを呼び出す Web ページを実行するコードを示します。トークン ハンドルから WindowsIdentity を構築するときは、WindowsIdentity コンストラクタが受け取ったハンドルをコピーすることに注意することが重要です。元のハンドルは閉じる必要があります。そのため、このコラムでは C# using ステートメントで SafeTokenHandle クラスを注意深く使用しました。このクラスでは using ブロックの最後で元のトークン ハンドルを閉じています。ゲートウェイのように長時間実行されるサービスの場合、特にトークンのような重要なハンドルの場合はこのことが重要です。

Figure 5 サンプル ゲートウェイ Web アプリケーションの分離コードファイル (Close Figure 5)
public partial class _Default : System.Web.UI.Page {
    protected void btnRead_Click(object sender, EventArgs e) {
        // connect to our privileged logon service (runs in COM+)
        ILogonService logonService = new LogonService();
        int tokenHandleValue =
            logonService.LogonUserViaProtocolTransition(
                txtUser.Text, GetCurrentProcessId());

        // WindowsIdentity ctor dups the incoming handle, so we use
        // SafeTokenHandle to ensure that the original gets closed
        using (SafeTokenHandle token = new SafeTokenHandle(
            tokenHandleValue))
        using (WindowsIdentity id = new WindowsIdentity(
            token.DangerousGetHandle()))
        using (WindowsImpersonationContext wic = id.Impersonate()) {
            txtOutput.Text = File.ReadAllText(txtPath.Text);
        }
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern int GetCurrentProcessId();
}

セキュリティの考慮事項

すべてのセキュリティ対策には何らかのトレードオフが必要です。このソリューションでは、ゲートウェイ プロセスの特権レベルが大幅に低減されます。つまり、ゲートウェイを SYSTEM として実行する必要がなくなります。プロセスの制御権の獲得に成功した攻撃者は、(プロトコル遷移ログオンを取得するログオン サービスを呼び出すことを含めて) そのプロセスが実行できるすべてのことを同様に実行できることを忘れないでください。攻撃者 SYSTEM として実行できれば、ローカル セキュリティ アカウント データベースを読み取ることができます。その結果、そこで見つかった脆弱なパスワードのハッシュを解読できれば、ネットワーク上の他のシステムにもアクセスできる可能性があります。ゲートウェイを低い特権で実行すれば、この種の攻撃に対して徹底的な防御が可能です。では、弱点はあるでしょうか。2 番目のプロセスを導入すると、そのプロセスを呼び出すために、コンテキスト切り替えを行うオーバーヘッドが追加されます。ログオン サービス COM+ コンポーネントのインストールが必要になったので、ソリューションの展開が複雑になります。お使いのシステムではこうしたトレードオフにはどのような価値があるのでしょう。これを判断し、決定するのは読者ご自身です。

ご質問やご意見は

Keith briefs@microsoft.com.

まで英語でお送りください。
Keith Brown は、Microsoft .NET の最高のトレーニング プロバイダである Pluralsight の共同創立者です。Keith は、Pluralsight の Applied .NET Security コースの作成者で、『The .NET Developer's Guide to Windows Security』 (印刷物および Web で提供) をはじめとする数冊の書籍の著者でもあります。詳細については、pluralsight.com/keith (英語) をご覧ください。


{End Bracket}: スーパーボールと見積もり精度の向上

{End Bracket}
スーパーボールと見積もり精度の向上
James Waletzky

私の机の上にある入れ物には、いくつのスーパーボールが入っていると思いますか。 ボールのサイズも入れ物の大きさもわからず、入れ物にどれくらいボールが入っているのかということもわからない状況で、論理的な推測を行うのは容易なことではありません。このような推測を行うのは、暗闇の中を手探りで進んでいるようなものです。この問題をソフトウェア開発に当てはめてみたらどうでしょうか。きっと馴染みのある問題だと思います。このコラムでは、プロジェクトの初期段階で開発作業量を見積もるのが非常に困難である理由と、このような見積もりの精度を向上するための対策について説明します。

開発初期段階で見積もりに使用する手法の 1 つに SWAG (Silly Wild-Ass Guess) があります。SWAG は、ソフトウェア開発プロジェクトの開発作業量の見積もりに使用する直感的な方法です。プロジェクト マネージャは、プロジェクトのデザインについてはおろか、プロジェクトについて十分な情報がない状態で、SWAG を使用して納品日を割り出さなければならないことがあります。私の机の上にあるスーパーボールの数を当てられる可能性は、さまざまな可変要素を伴うプロジェクトの開発作業量を正確に見積もれる可能性と同じくらい低いものになるでしょう。

機能の特定、要件の洗い出し、デザインの策定などを行い、プロジェクトから不確定要素を取り除くことによって、開発作業量が明確になります。正確な見積もりを出すのに必要なすべての情報が揃っていなくても、この時点では、不足している情報を把握しているため、今後の計画を立てることができます。私の机の上にあるスーパーボールの直径が 1.5 インチで、入れ物の大きさはパンを 1 つ入れられるくらいで、ボールが入れ物の 4 分の 3 まで入っているという情報を提供したら、皆さんの推測結果は、かなり正確なものになると思います。ソフトウェア プロジェクトの場合も、最終的には、ある程度の可変要素とリスクを排除して、かなり正確な見積もりを出せるようになります。しかし、プロジェクトの初期段階では、多数の不確定要素が存在します。

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

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

幸い、この作業を早い段階で行える方法があります。1970 年代に Barry Boehm 氏によって普及した不定期にメンバ召集するチーム ベースの見積もり技法である広域デルファイ法を採用してからは、私のプロジェクトの初期段階における見積もりの精度が大幅に向上しました。広域デルファイ法は手軽な方法ですが、見積もりの精度は大幅に向上します。この技法のプロセスは、次のとおりです。

  1. 複数名の作業者を集めます。広域デルファイ法で推奨されている人数は、2 ～ 5 人です。同じような種類のアプリケーションを作成した経験がある開発者と、そのようなアプリケーションを作成したことがない開発者の両方をチーム メンバに入れます。複数の視点を持つことが重要になります。
  2. 見積もり対象の作業概要を説明します。作業内容に最も詳しい作業者が、チーム メンバに、要件に重点を置いて確定済みの作業概要を説明します。
  3. 各メンバが、個別に見積もりを出します。メンバは、各自で問題点を考えて仮説を立てます。各自で考えた理想的な作業時間を見積もりとして情報カードに書き、裏を向けてテーブルの上に置きます。
  4. カードを表に向けて、メンバが提示した見積もりについて話し合います。進行役のメンバが匿名の見積もりを他のメンバに提示します。ここでは、どのような現象が起こると思いますか。十分な情報がない状態で私の机の上にあるスーパーボールの数を推測してもらったときと同様、この作業の結果はバラバラでした。見積もりについての議論は、この後に行う見積もりに反映されます。
  5. メンバの見積もり結果がある程度収束するまで手順 3. ～ 5. を繰り返します。各メンバは、他のメンバから得た情報に基づいて、見積もりを修正し、新たな見積もりを提示します。見積もりは、1 つの結論へ収束されていきます。各メンバが提示する見積もり結果がある程度収束するまで、この手順を繰り返します。

この技法を使用すると、どうして正確な見積もりを出せるのでしょうか。広域デルファイ法では、他のメンバの経験を取り入れ、フィードバックに基づいて見積もりを出します。また、1 回目の見積もりを匿名で出せることで、見積もりをするメンバは、最初の見積もりを直感で出すことができます。このようなプロセスにより、見積もりで他のメンバの意見を吸収することができます。このプロセスでは、無効または的外れな見積もりをすばやく見極め、大まかな要件とデザインを詰めることができます。チーム メンバが見積もり時間が妥当であると同意したら、その見積もりを文書化します。このように、複数人で見積もりすることで、論理的根拠のある見積もりを出すことができます。

広域デルファイ法を使用すると、見積もりの精度が大幅に向上するだけでなく、複数人で発生し得る問題を考えるためプロジェクトのリスクを緩和できます。たとえば、私のチームでは、優先順位を付けた要件の一覧を作成し、この一覧から 30 日サイクルで開発するサブセットを選択しています。また、この一覧に要件を追加する際に作業項目の規模を予測したり、プロジェクトのある 1 つの開発サイクルの詳細な見積もりを出したりするときにも広域デルファイ法を使用します。この手法を使用することで、ソフトウェア開発をスケジュールどおりに進め、仕事と生活のバランスを保つことができます。

広域デルファイ法と他の見積もり技法の詳細については、Steve McConnell 氏の著書『Software Estimation: Demystifying the Black Art』 (英語) をご覧になることをお勧めします。この書籍は、ソフトウェア プロジェクトをスケジュールどおりに開発するのに役立ちます。少なくとも、私の机の上にあるスーパー ボールの数をより的確に推測するのに役立つことは間違いありません。

James Waletzky は、Microsoft Engineering Excellence グループのソフトウェア設計エンジニアです。James はトレーニング、コンサルティング、およびツールによって、マイクロソフトにおけるエンジニアリングの手法の改善に尽力していますが、継続的な改善が必要であることを強く主張しています。