: MSDN Magazine July 2007

July 2007
JULY 2007 ボリューム 22 番号 07

• モビリティ: WPF アプリケーションで電源状態を認識する
Andre Michaud

• コードを共有する: モバイル アプリケーションとデスクトップ アプリケーションの両方に対してコードを 1 回のみ記述する
Daniel Moth

• NTFS: ファイル システム トランザクションを使用してアプリケーションを強化する
Jason Olson

• セキュリティ: Windows Vista の CNG API を使用して暗号化を行う
Kenny Kerr

• 音声認識: Text Services Framework によるディクテーションのサポート
Eric Brown

Code Download (3461 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: IDisposable について
Shawn Farkas

• データ ポイント: ADO.NET Entity Framework の概要
John Papa

• Cutting Edge: AJAX を使用した状況によって異なるフィードバック
Dino Esposito

• サービス ステーション: WCF バインドの詳細
Aaron Skonnard

• Bugslayer: Wait Chain Traversal
John Robbins

• セキュリティに関するブリーフィング: Active Directory キャッシュの依存関係
Keith Brown

• 基礎: WPF アニメーション クラスを拡張する
Charles Petzold

• { End Bracket }: Photosynth を使って写真を編集する
Richard Szeliski

各出版物

• 編集者のメモ: モビリティとは
Joshua Trupin

• ツールボックス: ボトルネックの特定、コードの類似部分、その他
Scott Mitchell


モビリティ: WPF アプリケーションで電源状態を認識する

モビリティ

WPF アプリケーションで電源状態を認識する

Andre Michaud
この記事の内容 : :
  • 電源状態認識アプリケーションを作成する
  • Windows XP での電源のサポート
  • Windows Vista での電源のサポート
  • カスタム WPF 依存関係プロパティとルーティング イベント
この記事は次のテクノロジを使用しています:
.NET Framework 3.0、Windows Vista、Windows XP
コードのダウンロード : : PowerAware2007_07.exe (360KB)
コンテンツ
既存の作業
コードの概要
システムの電源状態変更の通知
PowerModeChanged イベントとハンドラ
WM_POWERBROADCAST ウィンドウ メッセージをトラップする
Windows Vista で通知を登録する
オペレーティング システムを検出する
カスタム電源依存関係プロパティ
カスタム電源イベント
Windows XP の電源構造体
Windows XP で依存関係プロパティを初期化する
Windows Vista の電源構造体
メッセージが処理されるしくみ
依存関係プロパティとイベントを使用する
アニメーションを制御する
まとめ

モバイル コンピュータがますます普及してくるにつれて、これまで以上にバッテリ寿命が重要になっています。

ハードウェアの製造元は、既にこのことに気付いているため、バッテリの消費を念頭に置いてモバイル デバイスを設計しています。しかし、ソフトウェアにも同じことが言えるでしょうか。そうとは言えないまでも、電源状態を認識するソフトウェアのコーディングについて考え始める必要はあるでしょう。Windows® XP と Windows Vista™ はどちらもアプリケーションが電源状態を認識できるようにシステム情報を提供します。この記事では、開発者が Windows Presentation Foundation アプリケーションでの電源状態の認識について理解するための開始点を提供します。

最初に理解する必要があるのは、Windows Presentation Foundation 自体によって電源状態認識アプリケーションで電源の通知が使用できるようになるわけではないということです。ここで使用するサンプルでは、この機能は Windows XP または Windows Vista のいずれかのオペレーティング システム (Windows Presentation Foundation を実行できるように設計されているオペレーティング システム) に依存します。

この情報を Windows Presentation Foundation アプリケーションに取得する方法を紹介する前に、Windows Vista の電源通知システムが Windows XP のものよりも進化していることをお伝えしておきます。Windows Vista には Windows XP の電源通知システムのすべての機能が含まれているうえ、Windows XP の機能よりもかなり難しいことを実行できます。通知システムとは何でしょうか、この 2 つのオペレーティング システムの通知システムにはどのような違いがあるのでしょうか、また正常な電源状態認識アプリケーションを設計するにはそれらの差異をどのように管理すればよいのでしょうか。Windows Presentation Foundation アプリケーションが Windows XP と Windows Vista の両方で実行することを目的としているという事実から見ると、これらは特に重要な質問といえます。

既存の作業

Windows XP と Windows Vista の電源通知システムのサンプル コードを見て実際に経験したい人向けに 2 つのすばらしい実践ラボが用意されています。2 つのラボとは、Windows XP 用と Windows Vista 用です。これらのラボでは、マネージ コードで使用するのと同様の電源通知システムを扱っています。ただし、Windows Presentation Foundation で電源通知システムを使用する方法について特別に示しているわけではありません。それでも、これらのラボはすばらしい開始点となり、通知システム全体についてこの記事よりもはるかに細かい点までカバーしています。そのため、この記事と併せてラボを使用することをお勧めします。

もう 1 つの重要な点は、すべてのアプリケーションが電力消費に関してそれぞれ固有の不安を抱えているということです。システムが中断状態になるときに、すべてのアプリケーションで 3D アニメーションの実行やディスクへのデータの書き込みが行われるわけではありません。そのため、1 つのサンプルまたは実践ラボで、あらゆるアプリケーションの電力に関するすべてのニーズを解決することは現実的にはできません。したがって、開発者は、使用するアプリケーションのニーズの枠の中で、それぞれの通知システムの詳細とこの記事で扱っている方法を確認することをお勧めします。

コードの概要

この記事では、Windows XP と Windows Vista の電源通知システムを Windows Presentation Foundation アプリケーションで使用するために、これら 2 つのオペレーティング システムの電源通知システムの違いを見ていきます。このシステムの動作の大半は、PowerAwareWindow というカスタム クラス内で実行されます。このクラスは Window クラス (多くのアプリケーションを含む要素) から派生し、電源関連のシステム情報と通知の多くを Windows Presentation Foundation に特有の形式で (カスタム依存関係プロパティとイベントを使用して) 公開します。このカスタム クラスの定義は、PowerAwareWindow.cs ファイルに存在します (この記事のすべてのコードは MSDN® Magazine Web サイトからダウンロードできます)。

オペレーティング システムからの電源の通知を可能にする関数、変数、および関連付けられた構造は、ネイティブ コードに存在します。これらのネイティブ要素は、PowerAwareWindow クラスで行われる動作をよりわかりやすくするために、NativeMethods.cs という別のコード ファイルに配置されています。

PowerAwareWindow を使用するサンプル アプリケーションは、Window1.xaml ファイルと Window1.xaml.cs ファイル内に存在します。アプリケーションは、標準の Window 内でホストされるのではなく、PowerAwareWindow 内でホストされます。つまり、カスタム イベントにフックし、システムの電源の状態に対する変更を受け取ったりそれに応答したりするために、PowerAwareWindow 内で宣言されているカスタム依存関係プロパティを使用します。

ダウンロードに含まれているサンプル コードは、ここで示すコードとは少し異なることに注意してください。サンプルに含まれているネイティブ要素の多くは、NativeMethods.cs ファイルに存在します。そのため、サンプル コードでネイティブ要素を参照すると、NativeMethods というプレフィックスが付いています。たとえば、ダウンロード可能なコードではオペレーティング システムのバージョンを次のように参照しています。

NativeMethods.VISTA_OS_VERSION

しかし、この記事では、わかりやすいように、単純に次のように参照しています。

VISTA_OS_VERSION

また、読みやすいように書式を変えてあったり、コメントが違っていたりします。その他、ダウンロードに含まれているコードには、Debug.Assert の呼び出しがいくつか含まれています。これらの呼び出しは追加の予防措置として含まれているもので、コードの一部のセクションでは、アプリケーションが Windows Vista で実行されることを想定していて、その他のセクションでは Windows XP で実行されることを想定しています。

システムの電源状態変更の通知

最初のステップでは、電源関連のシステムの変更が通知されます。既に説明したように、この記事では Windows XP と Windows Vista の通知システムの完全な概要は示しません。この機能のサブセットを、Windows Presentation Foundation で使用する場合に特化して説明します (通知システムの完全な概要については、「その他のリソース」リンクの「モバイル PC 開発ガイド」をお読みください)。

システムの電源状態変更に関する通知を取得できる 2 つの方法について説明します。1 つは、System.dll の Microsoft.Win32 名前空間に含まれている SystemEvents クラスの PowerModeChanged イベントを使用するもので、もう 1 つは WM_POWERBROADCAST ウィンドウ メッセージをトラップすることによるものです。

PowerModeChanged イベントは実際には WM_POWERBROADCAST メッセージの上に構築されますが、このイベントは電源関連の新しい機能が Windows Vista に追加される前に構築されたものです。このため、Windows Vista では後者のアプローチの方が優れています。たとえば、PowerModeChanged イベントのみを使用する場合、アプリケーションは Windows Vista ユーザーが定義した電源プランについてまったく認識しません (電源プランについては後で簡単に説明します)。

PowerModeChanged イベントとハンドラ

PowerModeChanged イベントは PowerModes 列挙体を登録済みのイベント ハンドラに渡します。この列挙体には、Resume、StatusChange、および Suspend という 3 つの値があります。このイベントを使用すると、アプリケーションは次の場合に通知を受け取ります。

  • OS が中断状態から再開する場合 (Resume)
  • 電源が AC とバッテリ間で切り替えられたり、システムの電源状態にその他何らかの変更が生じた場合 (StatusChange)
  • オペレーティング システムが中断される場合 (Suspend)

PowerAwareWindow では、このイベントは通常 Window.Loaded イベント ハンドラにフックされ、Window.Closed イベント ハンドラでアンフックされます (図 1 参照)。もちろん、これは Window.Loaded イベントと Window.Closed イベントがフックされた後のことです。このイベントは、アプリケーションが求めている電源関連の情報をほとんどカバーします。そのため、電源状態認識アプリケーションの最適な選択肢となる場合があります。アプリケーションがこの情報のみを求めている場合、電源イベントを受け取るこの方法を使用する方が、ウィンドウ メッセージをトラップする方法 (後で少しだけ説明します) を使用するよりもはるかに簡単です。

Figure 1 PowerModeChanged イベントのフック (Close Figure 1)
protected override void OnInitialized(EventArgs e)
{
    // Hook up the Window.Loaded and Window.Closed events
    this.Loaded += new RoutedEventHandler(PowerAwareWindow_Loaded);
    this.Closed += new EventHandler(PowerAwareWindow_Closed);
    base.OnInitialized(e);
}

void PowerAwareWindow_Loaded(object sender, RoutedEventArgs e)
{
    Microsoft.Win32.SystemEvents.PowerModeChanged +=
        new PowerModeChangedEventHandler(
            XP_SystemEvents_PowerModeChanged);
}

void PowerAwareWindow_Closed (object sender, RoutedEventArgs e)
{
    Microsoft.Win32.SystemEvents.PowerModeChanged -=
        new PowerModeChangedEventHandler(
            XP_SystemEvents_PowerModeChanged);
}

XP_SystemEvents_PowerModeChanged 関数はダウンロードできるコードに実装されているので、それを参照できます。サンプルでは、イベントをフックするコードはコメント アウトされていて、説明のためだけに含まれているので、使用できません。

WM_POWERBROADCAST ウィンドウ メッセージをトラップする

オペレーティング システムでは WM_POWERBROADCAST ウィンドウ メッセージを送信して、リッスンしているアプリケーションに電源関連の状態や設定への変更を通知します。ただし、これらの通知の一部を受け取るためには、アプリケーションはまず通知を登録する必要があります。この記事に付属するサンプルは、綿密なものにするために、電源通知に (PowerModeChanged イベントではなく) ウィンドウ メッセージを使用しています。

WM_POWERBROADCAST を使用すると、アプリケーションはシステムの電源変更通知をすべて受け取ることができます。Windows Presentation Foundation アプリケーションでウィンドウ メッセージを受け取るためには、まず、次に示すように、ウィンドウ メッセージを受け取るようにイベント ハンドラを宣言しておく必要があります。

void PowerAwareWindow_Loaded(object sender, RoutedEventArgs e)
{
    WindowInteropHelper helper = new WindowInteropHelper(this);
    HwndSource source = HwndSource.FromHwnd(helper.Handle);
    source.AddHook(new HwndSourceHook(this.MessageProc));
    ...
}

このコードでは MessageProc 関数をウィンドウ メッセージ イベント ハンドラとして宣言しています。この関数は WM_POWERBROADCAST メッセージを確認し (図 2 を参照)、それに応じた動作を行います。

Figure 2 WM_POWERBROADCAST メッセージ (Close Figure 2)
const int WM_POWERBROADCAST = 0x0218;

IntPtr MessageProc(
    IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case WM_POWERBROADCAST:
            // Handle the message here...
            break;

       // Handle other window messages (if the application needs to) here
    }
}

イベントに関する情報は、ウィンドウ メッセージの wParam に含まれます。wParam では、さまざまな電源関連のシステム イベントを示すために多くの形式を使用できます。たとえば、次の値は、電源が切り替わったか、バッテリ残量の割合が変わったかを示します。

const int PBT_APMPOWERSTATUSCHANGE = 0x000A;

以下は、システムが中断状態から抜け出すプロセスに入ろうとしているか、そのプロセス内にいることを示します。

const int PBT_APMSUSPEND = 0x0004;
const int PBT_APMRESUMESUSPEND = 0x0007;

いくつかの値は一部のプラットフォームでは使用できません。たとえば、以下に示す値は、Windows Vista から使用可能になったもので、電源、電源プラン、バッテリ残量の割合、またはモニタの状態のいずれかが変わったことを示します。

const int PBT_POWERSETTINGCHANGE = 0x8013;

このようなイベントの完全なリストは、電源管理関係のドキュメントに記載されています。ちなみに、PBT_APMPOWERSTATUSCHANGE、PBT_APMSUSPEND、および PBT_APMRESUMESUSPEND wParam はそれぞれ PowerModeChange イベントの StatusChange、Suspend、および Resume PowerModes に似ています。

PBT_APMPOWERSTATUSCHANGE と PBT_ POWERSETTINGCHANGE の wParam 値の違いに注意することが重要です。後者は PBT_APMPOWERSTATUSCHANGE の Windows Vista のみのスーパーセットと理解でき、PBT_APMPOWERSTATUSCHANGE が示す電源関係のすべての変更を示すだけでなく、電源プランやモニタの状態など、Windows Vista のみに固有のその他の情報も示します。これらの wParam は両方とも存在し、Windows Vista では WM_POWERBROADCAST ウィンドウ メッセージと共に渡されますが、このサンプルで両方を処理すると Windows Vista で実行しているときに冗長になります。

ただし、PBT_POWERSETTINGCHANGE の wParam は、Windows Vista アプリケーションが電源設定通知を登録するまで、Windows Vista アプリケーションに送信されません (PBT_APMPOWERSTATUSCHANGE は登録しなくても受信されます)。Windows Vista に固有の電源通知を受信するためには、これが非常に重要なステップとなります。

Windows Vista で通知を登録する

Windows Vista で電源設定通知を登録するための最初のステップは、アプリケーションで RegisterPowerSettingNotification 関数を使用できるようにすることです。このステップは、次のような P/Invoke 関数の定義を使用して実現できます。

[DllImport(@”User32.dll”, SetLastError=true,
    CallingConvention=CallingConvention.StdCall)]
static extern IntPtr RegisterPowerSettingNotification(
    IntPtr hRecipient, ref Guid PowerSettingGuid, Int32 Flags);

この関数は、アプリケーションのシャットダウン時にイベントの登録を解除するために使用されるハンドルを返します。

この関数に渡される Flags は、アプリケーションがどのようにして通知を受け取るのかをオペレーティング システムに示します。このサンプルでは、次のように宣言しています。

const int DEVICE_NOTIFY_WINDOW_HANDLE = 0x00000000;

これは、通知が PBT_POWERSETTINGCHANGE の wParam と共に WM_POWERBROADCAST ウィンドウ メッセージを使用してアプリケーションに送信されることを、登録関数に示します。Flags の他のオプションは、OS のサービスに関連していて、前述の「RegisterPowerSettingNotification」に示されています。

登録関数に渡される PowerSettingGuid 値は、アプリケーションが求めている通知をオペレーティング システムに示します。図 3 は、最も必要であると考えられる 4 つの PowerSettingGuid 値を示しています。

Figure 3 4 種類の PowerSettingGuid (Close Figure 3)
// notify application when percentage of battery remaining changes
static Guid GUID_BATTERY_PERCENTAGE_REMAINING =
    new Guid(“A7AD8041-B45A-4CAE-87A3-EECBB468A9E1”);

// notify application when the state of the monitor changes
static Guid GUID_MONITOR_POWER_ON =
    new Guid(“02731015-4510-4526-99E6-E5A17EBD1AEA”);

// notify application when the system power source changes
static Guid GUID_ACDC_POWER_SOURCE =
    new Guid(“5D3E9A59-E9D5-4B00-A6BD-FF34FF516548”);

// notify application when the user-defined power plan changes
static Guid GUID_POWERSCHEME_PERSONALITY =
    new Guid(“245D8541-3943-4422-B025-13A784F679B7”);

他の通知 GUID もあり、システムが退席モードになったときや退席モードから復帰したとき、またはシステムがもうすぐアイドル状態になりそうなときや現在の時間帯がバックグラウンド タスクやアイドル タスクを実行するのに適している場合などにイベントを通知するために使用できます。

サンプルでは、Windows Vista のすべての電源登録を 1 つの関数にグループ化しています (図 4 参照)。同様に、すべての登録解除も 1 つの関数にグループ化しています。登録は、PowerAwareWindow が読み込まれたときに行われます (Window.Loaded イベントは PowerAwareWindow_Loaded 関数によって処理されます)。また、登録解除は PowerAwareWindow の終了時に行われます (Window.Closed は PowerAwareWindow_Closed によって処理されます)。

Figure 4 Windows Vista での電源の登録 (Close Figure 4)
IntPtr _hBattCapacity;
IntPtr _hMonitorOn;
IntPtr _hPowerScheme;
IntPtr _hPowerSrc;

void RegisterForVistaPowerNotifications(IntPtr hWnd)
{
    _hPowerSrc = RegisterPowerSettingNotification(
        hWnd, ref GUID_ACDC_POWER_SOURCE, DEVICE_NOTIFY_WINDOW_HANDLE);
    _hBattCapacity = RegisterPowerSettingNotification(
        hWnd, ref GUID_BATTERY_PERCENTAGE_REMAINING,
        DEVICE_NOTIFY_WINDOW_HANDLE);
    _hMonitorOn = RegisterPowerSettingNotification(
        hWnd, ref GUID_MONITOR_POWER_ON, DEVICE_NOTIFY_WINDOW_HANDLE);
    _hPowerScheme = RegisterPowerSettingNotification(
        hWnd, ref GUID_POWERSCHEME_PERSONALITY,
        DEVICE_NOTIFY_WINDOW_HANDLE);
}

void UnregisterForVistaPowerNotifications()
{
    UnregisterPowerSettingNotification(_hBattCapacity);
    UnregisterPowerSettingNotification(_hMonitorOn);
    UnregisterPowerSettingNotification(_hPowerScheme);
    UnregisterPowerSettingNotification(_hPowerSrc);
}

登録では、このサンプルで示す主要な問題の 1 つが発生します。この問題とは、1 つの Windows Presentation Foundation アプリケーションにおいて電源状態の認識に対して一貫したアプローチをとることで、Windows XP と Windows Vista の電源通知に関する違いが明らかになるというものです。User32.dll からエクスポートされた RegisterPowerSettingNotification 関数と UnregisterPowerSettingNotification 関数は、Windows Vista より前のオペレーティング システムでは使用できません。そのため、コードでこれらの関数を使用するには、先に関数が使用できることを確認する必要があります。

オペレーティング システムを検出する

アプリケーションがオペレーティング システムを検出して、それに応じた動作を行うことは重要です。この目的を達成するため、サンプルでは、この情報を格納するためにプライベート変数を読み取り専用に設定しています。

readonly int _osVersion = Environment.OSVersion.Version.Major;

Windows Vista および Windows XP のオペレーティング システムの主要なバージョンは次のとおりです。

const int VISTA_OS_VERSION = 6;
const int XP_OS_VERSION = 5;

この時点では、PowerAwareWindow_Loaded 関数と PowerAwareWindow_Closed 関数は、図 5 に似ているはずです。この時点では、アプリケーションは電源通知を MessageProc 関数のウィンドウ メッセージを使用して受け取ります。電源通知を受け取ったら、アプリケーションは適切に応答する必要があります。

Figure 5 PowerAwareWindow_Loaded 関数および _Closed 関数 (Close Figure 5)
void PowerAwareWindow_Loaded(object sender, RoutedEventArgs e)
{
    // Optional: If the PowerModeChanged event contains the desired power-
    // notification functionality for an application, no OS detection is
    // necessary as PowerModeChanged will work with both
    SystemEvents.PowerModeChanged += new PowerModeChangedEventHandler(
        XP_SystemEvents_PowerModeChanged);

    // Use the helper to hook up our Windows Presentation Foundation window
    // to receive window messages.
    WindowInteropHelper helper = new WindowInteropHelper(this);
    HwndSource source = HwndSource.FromHwnd(helper.Handle);
    source.AddHook(new HwndSourceHook(this.MessageProc));

    if (_osVersion >= VISTA_OS_VERSION)
    {
        RegisterForVistaPowerNotifications(source.Handle);

        // There’s no need to initialize the dependency properties on
        // Windows Vista because this application will get notified of the
        // power status when it registers for the events.
    }
    else
    {
        // Application power initialization should be done here (otherwise
        // the application is left waiting for a WM_POWERBROADCAST window
        // message that might not come right away).
    }
}

void PowerAwareWindow_Closed(object sender, EventArgs e)
{
    // Optional: Unregister for PowerModeChanged if registration for it
    // was done in PowerAwareWindow_Loaded
    SystemEvents.PowerModeChanged -= new PowerModeChangedEventHandler(
        XP_SystemEvents_PowerModeChanged);

    if (_osVersion >= VISTA_OS_VERSION)
    {
        UnregisterForVistaPowerNotifications();
    }
    else
    {
        // Do Windows XP-specific application cleanup here
    }
}

カスタム電源依存関係プロパティ

多くの場合、電源状態認識アプリケーションでは、このサンプルで宣言されているものとは別に、カスタム依存関係プロパティを宣言して使用する必要があります。カスタム依存関係プロパティは高度なトピックであることに注意してください。このセクションを読む前にもっと背景を知りたい方は、「カスタム依存関係プロパティ」をご覧ください。

これらのカスタム依存関係プロパティは一見複雑ですが、PowerAwareWindow を使用するアプリケーションに多くの機能と柔軟性を提供します。サンプルで宣言されているカスタム依存関係プロパティは、PowerPlan、MonitorOn、RemainingBattery、および RunningOnBattery です。

PowerPlan は現在のユーザー指定の電源プランを公開します。この情報は Windows Vista に固有のものです。Windows XP では、常に既定値 (Automatic) になります。関連付けられた PowerPlanChanged イベントは Windows XP では発生しません。Windows XP には電源プランという概念がないためです。

PowerPlan 列挙体は次のように定義されます。

public enum PowerPlan
{
    Automatic,
    HighPerformance,
    PowerSaver,
};

Windows Vista UI では、それぞれ "バランス"、"高パフォーマンス"、および "省電力" と呼ばれています。依存関係プロパティの登録は、図 6 に示すコードのようになります。

Figure 6 PowerPlan 依存関係プロパティの登録 (Close Figure 6)
private static readonly DependencyPropertyKey PowerPlanPropertyKey =
    DependencyProperty.RegisterReadOnly(
        “PowerPlan”, typeof(PowerPlan), typeof(UIElement),
        new FrameworkPropertyMetadata(PowerPlan.Automatic,
            FrameworkPropertyMetadataOptions.Inherits |
            FrameworkPropertyMetadataOptions.Journal |
            FrameworkPropertyMetadataOptions.
                OverridesInheritanceBehavior,
            new PropertyChangedCallback(OnPowerPlanChanged)));

public static readonly DependencyProperty PowerPlanProperty =
    PowerPlanPropertyKey.DependencyProperty;

private static void OnPowerPlanChanged(
    DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    PowerAwareWindow myPowerAwareWindow = d as PowerAwareWindow;
    if (myPowerAwareWindow != null &&
        myPowerAwareWindow.PowerPlanChanged != null)
    {
        myPowerAwareWindow.PowerPlanChanged(myPowerAwareWindow, e);
    }
}

public event DependencyPropertyChangedEventHandler PowerPlanChanged;

public PowerPlan PowerPlan
{
    get { return (PowerPlan)GetValue(PowerPlanProperty); }
    private set
    {
        SetValue(PowerAwareWindow.PowerPlanPropertyKey, value);
    }
}

MonitorOn はモニタ (ディスプレイ) の現在の状態 (オンまたはオフ) を公開します。この情報は Windows Vista に固有のものです。Windows XP では、常に既定値 (True) になります。関連付けられた MonitorOnChanged イベントは Windows XP では発生しません。Windows XP にはモニタの状態という概念がないためです。

依存関係プロパティの登録は、PowerPlan 用に説明したものとよく似ています。ただし、プロパティが Boolean 型であること、プロパティが MonitorOn という名前であること、および対応するイベントが MonitorOnChanged である点が異なります。これと、残りの依存関係プロパティの完全な実装は、コード サンプルに含まれています。

RemainingBattery は、現在のバッテリ残量の割合を公開します。範囲は 0 ～ 100 パーセントです。この場合、依存関係プロパティの登録は、前の 2 つの依存関係プロパティの登録とよく似ています。

RunningOnBattery は、システムがバッテリで実行されている場合は True で、AC 電源で実行されている場合は False です。

カスタム電源イベント

電源状態認識アプリケーションでは、カスタム依存関係プロパティと同様に、カスタム イベントを宣言して使用することが必要になる場合もよくあります (カスタム イベントに関する背景をもっと知りたい方は、「ルーティングされるイベントの概要」をご覧ください)。サンプルに含まれているカスタム イベントは、PreviewSystemResuming、SystemResuming、PreviewSystemSuspending、および SystemSuspending です。

PreviewSystemResuming は、システムが中断状態から再開するときに発生します。イベントの Preview 部分は、イベントのルーティング方法が、Direct や Bubble ではなく Tunnel であることを示します。イベントの登録は、図 7 に示すコードのようになります。

Figure 7 PreviewSystemResuming イベントの登録 (Close Figure 7)
static readonly RoutedEvent PreviewSystemResumingEvent =
    EventManager.RegisterRoutedEvent(“PreviewSystemResumingEvent”,
        RoutingStrategy.Tunnel, typeof(RoutedEventHandler),
        typeof(PowerAwareWindow));

public event RoutedEventHandler PreviewSystemResuming
{
    add { AddHandler(PreviewSystemResumingEvent, value); }
    remove { RemoveHandler(PreviewSystemResumingEvent, value); }
}

//  This is called when the PowerAware window wants to raise the event
void RaisePreviewSystemResumingEvent()
{
    RaiseEvent(new RoutedEventArgs(
        PowerAwareWindow.PreviewSystemResumingEvent, this));
}

SystemResuming は、システムが中断状態から再開するときに発生します。これは、PreviewSystemResuming イベントの Bubble ルーティング方法のバージョンを使用します。イベントの登録は、図 7 に示すコードのようになります。

Figure 8 SystemResuming イベントの登録 (Close Figure 8)
static readonly RoutedEvent SystemResumingEvent =
    EventManager.RegisterRoutedEvent(“SystemResumingEvent”,
        RoutingStrategy.Bubble, typeof(RoutedEventHandler),
        typeof(PowerAwareWindow));

public event RoutedEventHandler SystemResuming
{
    add { AddHandler(SystemResumingEvent, value); }
    remove { RemoveHandler(SystemResumingEvent, value); }
}

//  This is called when the PowerAware window wants to raise the event
void RaiseSystemResumingEvent()
{
    RaiseEvent(new RoutedEventArgs(
        PowerAwareWindow.SystemResumingEvent, this);
}

PreviewSystemSuspending は、システムが中断状態になるときに発生します。イベントの Preview 部分は、イベントのルーティング方法が、PreviewSystemResuming と同様に、Tunnel であることを示します。

SystemSuspending は、システムが中断状態になるときに発生します。SystemResuming と同様に、Bubble ルーティング方法を使用します。

Windows XP の電源構造体

カスタム依存関係プロパティは、最新のシステム情報を含んでいない限り、役に立ちません。同様に、カスタム電源イベントが適切に発生する必要があります。これから、このことについて見ていくと同時に、Windows XP と Windows Vista の両方で実行する 1 つのアプリケーションに対して Windows Presentation Foundation による電源状態の認識を適切に機能させることに伴う、さらに複雑なことについても説明します。

まずは、Windows XP の重要な電源構造体に目を向けましょう。最初に、SYSTEM_POWER_STATUS があります (図 9 参照)。ご覧のとおり、このサンプルでは ACLineStatus 列挙体と BatteryFlag 列挙体を使用しています。

Figure 9 SYSTEM_POWER_STATUS (Close Figure 9)
class SYSTEM_POWER_STATUS
{
    public ACLineStatus ACLineStatus;
    public BatteryFlag BatteryFlag;

    // can be a value in the range 0 to 100, or 255 if status is unknown
    public byte BatteryLifePercent;
    public byte Reserved1;            // reserved; must be zero

    // number of seconds of battery life remaining,
    // or –1 if remaining seconds are unknown
    public Int32 BatteryLifeTime;

    // number of seconds of battery life when at full charge,
    // or –1 if full battery lifetime is unknown
    public Int32 BatteryFullLifeTime;
}

SYSTEM_POWER_STATUS が GetSystemPowerStatus 関数から返され、次のような Import を使用してアプリケーションで使用できるようになります。

[DllImport(“Kernel32.DLL”, CharSet=CharSet.Auto, SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetSystemPowerStatus(
    [In, Out] SYSTEM_POWER_STATUS SystemPowerStatus);

この関数は WM_POWERBROADCAST メッセージが PBT_APMPOWERSTATUSCHANGE の wParam で受信されると必ず呼び出されます。この wParam はシステムの電源状態に何らかの変更が加えられたことを示し、GetSystemPowerStatus 関数はアプリケーションに対してその情報へのアクセス権を与えます。その情報は渡される SYSTEM_POWER_STATUS の内部に返されます。

Windows XP で依存関係プロパティを初期化する

「オペレーティング システムを検出する」で説明したように、依存関係プロパティは、アプリケーションの読み込み時に最新の状態にするために初期化する必要があります (図 5 参照)。この初期化は Windows Vista では必要ありません。Windows Vista ではアプリケーションが電源の通知を登録した場合、アプリケーションがシステムの電源状態をウィンドウ メッセージ経由で認識するためです。ただし、これは Windows XP では行われないので、初期化する必要があります。この初期化はオペレーティング システムが検出されたときに PowerAwareWindow_Loaded 関数で行われます。また、次のようなコードが使用されます。

void InitializeDependencyPropertiesForXP()
{
    if (GetSystemPowerStatus(this._xpPowerStatus))
    {
        this.RunningOnBattery =
            (ACLineStatus.Battery == _xpPowerStatus.ACLineStatus);
        this.RemainingBattery = _xpPowerStatus.BatteryLifePercent;
    }

    this.MonitorOn = true;
    this.PowerPlan = PowerPlan.Automatic;
}

この時点では、Windows XP モニタや、Windows Vista の通知に似ている電源プラン通知がないことを指摘することが重要です。そのため、Windows XP アプリケーションがこれらの値を取得しようとすると、既定値が返されます。また、アプリケーションが MonitorOnChanged イベントまたは PowerPlanChanged イベントをフックする場合でも、初期化後にイベントの発生がトリガされません。

Windows Vista の電源構造体

次の話題は、Windows Vista の電源構造体です。POWERBROADCAST_SETTING は Windows Vista の主要な電源構造体です。

struct POWERBROADCAST_SETTING
{
    public Guid PowerSetting;
    public UInt32 DataLength;
}

この構造体の公式の定義では、Data という 3 番目のメンバが存在します。構造体に示されているように、このサイズは GUID か DWORD かで異なります。したがって、アプリケーション内で宣言されるのは、これらのデータ型のいずれかを保持できるこの構造体のカスタム バリエーションです (この構造体は元のネイティブ構造のレイアウトを模倣しないので、マーシャリングには使用できないことに注意してください。この構造体を使用するのは、厳密に型指定された形式でアプリケーション内に引き続き値を渡すことができるようにするためです)。

struct my_POWERBROADCAST_SETTING
{
    public IntPtr wParam;
    public Guid PowerSetting;
    public int PowerChangeData;
    public Guid PowerPlanGuid;
}

この構造体には wParam も含まれるので、このカスタム バリエーションを使用して、すべての関連データをウィンドウ メッセージ ハンドラから 1 つの構造体に含まれているアプリケーションの Windows Vista メッセージ ハンドラ (後で定義します) に渡すことができます。

そのため、Windows Vista で PBT_POWERSETTINGCHANGE の wParam と共に WM_POWERBROADCAST ウィンドウ メッセージを受信すると、メッセージに関連付けられた lParam がシステムの電源状態の変更に関する情報を含む POWERBROADCAST_SETTING 構造体へのポインタとなります。

最後にもう 1 つ重要なちょっとした情報として、Windows Vista がどのようにして電源プランを定義するのかを考えてみます。最大限の省電力には、次のように、バッテリ寿命を延ばすのに役立つ非常に強力な省電力方法が関連しています。

static Guid GUID_MAX_POWER_SAVINGS =
    new Guid(“a1841308-3541-4fab-bc81-f71556f20b4a”);

省電力では、次のような省電力方法はほとんど使用しません。

static Guid GUID_MIN_POWER_SAVINGS =
    new Guid(“8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c”);

最後に、通常の省電力電源プランでは、次のように比較的強力な省電力方法を使用します。

static Guid GUID_TYPICAL_POWER_SAVINGS =
    new Guid(“381b4222-f694-41f0-9685-ff5bb260df2e”);

これらは、それぞれ PowerPlan.PowerSaver、PowerPlan.HighPerformance、および PowerPlan.Automatic に対応します。既に説明したように、Windows Vista UI では、それぞれが "省電力"、"高パフォーマンス"、および "バランス" として表示されます。

メッセージが処理されるしくみ

図 10 のコードは、MessageProc 関数を大まかに示しています。ここでの説明には 2 つの重要なポイントがあります。最初のポイントは、作業が各ハンドラに送信されるしくみです。2 つ目のポイントは、Windows Vista の WM_POWERBROADCAST メッセージが発生したときにデータが取得されるしくみ (および取得されるタイミング) です。

Figure 10 MessageProc 関数 (Close Figure 10)
IntPtr MessageProc(
    IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case WM_POWERBROADCAST:
            if (_osVersion >= VISTA_OS_VERSION)
            {
                // Grab the relevant power data from the message here.

                // Then send the data and work to the Windows Vista
                // handler here.
            }
            else
            {
                //  Send work to the Windows XP handler here.
            }
            break;

        // Other (non-power) window messages in which the
        // application is interested would be processed here
        // (if the application needs to do so).
    }

    return IntPtr.Zero;
}

通常、各ハンドラに作業を送信するのは、サンプル コードで定義された関数を呼び出すのと同じくらい単純です。アプリケーションが Windows XP または Windows Vista のいずれかで実行されている場合、これらの関数によってメッセージが適切に処理されます。しかしこの場合、システムがハングするのを防ぐために、もう少し多くの予防措置が取られています。たとえば、メッセージを処理する関数が複雑でプロセッサを集中的に使用する場合、ウィンドウ メッセージ ハンドルにコントロールがすぐに返されないことが原因で、目立ったシステム ハングが発生することがあります。このようなハングを避けるため、作業が Dispatcher スレッドへの投稿を通して Windows Vista および Windows XP のハンドラに送信されます (図 11 参照)。

Figure 11 非同期の呼び出し (Close Figure 11)
delegate void VoidXPDelegate(/*power data arguments*/ );
delegate void VoidVistaDelegate(/*power data arguments*/ );

// A call to the Windows XP handler would look like this:
this.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle,
    new VoidXPDelegate(/*XP handler function here*/),
    /*power data here*/);

// A call to the Windows Vista handler would look like this:
this.Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle,
    new VoidVistaDelegate(/*Vista handler function here*/),
    /* power data here*/);

メッセージは Dispatcher が関連付けられているスレッドで指定の優先順位で非同期に処理されます。つまり、コントロールは作業が Dispatcher に投稿された直後に MessageProc 関数に返されます。Dispatcher と Windows Presentation Foundation のスレッド化の詳細は、「WPF Fundamentals: スレッド モデル」に記載されています。

ここで、Windows Vista の電源イベントでのデータの取得について説明します。WM_POWERBROADCAST ウィンドウ メッセージの wParam 部分は、既に説明したように、メッセージの性質を理解していることが重要です。Windows XP では、電源メッセージを正常に処理するときに必要なのはこのことだけです。しかし、Windows Vista では、この wParam はメッセージ内の lParam がメッセージの性質を判断するために必要なデータへのポインタであることを示します。図 12 は、lParam ポインタからそのデータをコピーする方法を示しています。図には、これらの変更を含んだ MessageProc 関数が示してあります。

Figure 12 lParam ポインタからのデータのコピー (Close Figure 12)
// First grab the wParam, that’s important information to the
// Windows Vista handler no matter what
my_POWERBROADCAST_SETTING myPBS = new my_POWERBROADCAST_SETTING();
myPBS.wParam = wParam;

if (wParam == (IntPtr)PBT_POWERSETTINGCHANGE)
{
    // Grab the power change information from the pointer passed
    // in the window message
    POWERBROADCAST_SETTING ps = (POWERBROADCAST_SETTING)
        Marshal.PtrToStructure(lParam, typeof(POWERBROADCAST_SETTING));

    // Copy the PowerSetting in the POWERBROADCAST_SETTING to
    // our own structure
    myPBS.PowerSetting = ps.PowerSetting;

    // lParam points to the POWERBROADCAST_SETTING, but we need to
    // calculate a pointer to where the data about the setting lives.
    IntPtr pData = (IntPtr)((int)lParam + Marshal.SizeOf(ps));

    // Use the pointer calculated to grab the relevant data. Note that
    // the relevant data can be either a DWORD or a GUID, depending on
    // the datalength indicated in the POWERBROADCAST_SETTING struct.
    if (ps.DataLength == Marshal.SizeOf(typeof(Int32)))
    {
        myPBS.PowerChangeData = (Int32)Marshal.PtrToStructure(
            pData, typeof(Int32));
    }
    else if (ps.DataLength == Marshal.SizeOf(typeof(Guid)))
    {
        myPBS.PowerPlanGuid = (Guid)Marshal.PtrToStructure(
            pData, typeof(Guid));
    }
}

// At this point ‘myPBS’ contains all the data the Windows Vista handler
// needs for this application to process the system power change

メッセージに関連データがある場合、関連するプロパティを更新してから関連するイベントを発生させる必要があります。通知を受け取りカスタム依存関係プロパティを設定するためにこれまでに行ってきたすべての作業を考えると、このステップは実際には非常に簡単です。必要なのは、オペレーティング システムから提供されたデータを適切なアプリケーション変数に割り当てるか、適切なカスタム イベントを発生させることだけです。

依存関係プロパティとイベントを使用する

では、PowerAwareWindow 内でホストされるアプリケーションを見てみましょう。このレベルでは、電源状態の認識は Windows Presentation Foundation モデルにぴったりと適合するので、Windows XP と Windows Vista の両方で実行する電源状態認識アプリケーションを作成するのはとても簡単です。

最初に調査すべきなのはユーザー インターフェイスです。これは、Window1.xaml で定義されています。アプリケーションが、次のように、PowerAwareWindow 内に存在することに注意してください。

<local:PowerAwareWindow x:Class=”WPFPower_Aware.Window1”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    xmlns:local=”clr-namespace:WPFPower_Aware”
    x:Name=”MainWindow”
    Title=”WPF Power Aware Application” Height=”480” Width=”550”
    Loaded=”Window1_Loaded”
>
    ...
</local:PowerAwareWindow>

その結果、XAML ファイルではデータ バインドにカスタム依存関係プロパティを使用できます。たとえば、ここで示すサンプルには、ウィンドウの下部にステータス バーを含めてあります。ステータス バーの中には、電源関連のデータを示す 2 つの部分があります。1 つは、現在のシステムの電源を示すテキスト ブロックで、もう 1 つは、バッテリ残量の現在の割合を示す進行状況バーです。これらに割り当てられる値は、対応する依存関係プロパティにデータ バインドされます。

データ バインドは非常に単純なのです。たとえば、進行状況バーについて考えてみてください。

<ProgressBar IsIndeterminate=”False” x:Name=”myBatteryProgressBar”
    Width=”200” Height=”20”
    Value=”{Binding ElementName=MainWindow,
            Path=RemainingBattery, Mode=OneWay}” />

このようなほんの少しのマークアップで、進行状況バーは常に最新の状態に維持されるので、分離コードの仲介が必要ありません。また、バッテリ メーターのような独自のカスタム コントロールにデータバインドして、この情報を表示することもできます。

電源テキスト ブロックは、Boolean 値 (RunningOnBattery) からテキスト ブロック用の文字列に変換する必要があるので、やや複雑です。これについて詳しく理解したい場合は、サンプル コードをご覧ください。

アニメーションを制御する

サンプル アプリケーションを実行すると、2 つのアニメーション要素が表示されます。左側の要素は分離コードで制御されていて、もう 1 つの要素は XAML で制御されています。XAML によって制御されるアニメーションは、RunningOnBattery プロパティが False および True になったときにそれぞれ開始および停止されます。

図 13 のコードは、XAML で使用してカスタム依存関係プロパティの電源を例示するアニメーション用です。この XAML を使用すると、アニメーションの停止と開始にコードの仲介が必要ありません。これからが本番です。XAML でアニメーションとトリガを使用して実行できることはいろいろあります。詳細については、「Windows Presentation Foundation: アニメーションの概要」と「Trigger クラス」を参照してください。

Figure 13 XAML のカスタム依存関係プロパティ (Close Figure 13)
<Rectangle Name=”rect” Width=”140” Height=”90” Fill=”Blue” Stroke=”Red”>
  <Rectangle.RenderTransform>
    <ScaleTransform/>
  </Rectangle.RenderTransform>
  <Rectangle.Style>
    <Style>
      <Style.Triggers>
        <Trigger Property=”local:PowerAwareWindow.RunningOnBattery”
  Value=”False”>
          <Trigger.EnterActions>
            <BeginStoryboard Name=”myStoryboard”>
              <Storyboard>
                <DoubleAnimation Storyboard.TargetProperty=
                  “(Rectangle.RenderTransform).(ScaleTransform.ScaleY)”
                  From=”1” To=”1.5” RepeatBehavior=”Forever”
                  AutoReverse=”True”/>
              </Storyboard>
            </BeginStoryboard>
          </Trigger.EnterActions>
          <Trigger.ExitActions>
            <StopStoryboard BeginStoryboardName=”myStoryboard”/>
          </Trigger.ExitActions>
        </Trigger>
      </Style.Triggers>
    </Style>
  </Rectangle.Style>
</Rectangle>

既に説明したように、サンプル アプリケーションの左側の要素には分離コードで制御されるアニメーションが含まれています。関連するシステムの電源イベントが発生すると、電源を節約するためにアニメーションを一時停止する必要があるかどうかを判断するために、システムの現在の電源の状態が使用されます。まず、アニメーションが設定されてから、カスタム イベントがフックされます。このために必要なコードは、Window1 .xaml.cs にあります (図 14 参照)。

Figure 14 Window1.xaml.cs のコード (Close Figure 14)
DoubleAnimation myDoubleAnimation;
Storyboard myStoryboard;

void Window1_Loaded(object sender, RoutedEventArgs e)
{
    // Initialize the animation
    myDoubleAnimation = new DoubleAnimation();
    myDoubleAnimation.From = 0.0;
    myDoubleAnimation.To = 360.0;
    myDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(10));
    myDoubleAnimation.RepeatBehavior = RepeatBehavior.Forever;

    // Note that yRotate is an AxisAngleRotation3D declared
    // in the Window1.xaml file
    Storyboard.SetTargetName(myDoubleAnimation, “yRotate”);
    Storyboard.SetTargetProperty(myDoubleAnimation,
        new PropertyPath(AxisAngleRotation3D.AngleProperty));
    myStoryboard = new Storyboard();
    myStoryboard.Children.Add(myDoubleAnimation);

    // Begin the animation if appropriate
    if (!ShouldStopAnimation()) myStoryboard.Begin(this, true);

    // Hook up the events from the PowerAwareWindow
    this.RemainingBatteryChanged +=
        new DependencyPropertyChangedEventHandler(
            Window1_RemainingBatteryChanged);
    this.RunningOnBatteryChanged +=
        new DependencyPropertyChangedEventHandler(
            Window1_RunningOnBatteryChanged);
    this.SystemSuspending += new RoutedEventHandler(
        Window1_SystemSuspending);
    this.SystemResuming += new RoutedEventHandler(
        Window1_SystemResuming);
    this.MonitorOnChanged +=
        new DependencyPropertyChangedEventHandler(
            Window1_MonitorOnChanged);
    this.PowerPlanChanged +=
        new DependencyPropertyChangedEventHandler(
            Window1_PowerPlanChanged);
}

アプリケーションが電源を節約する措置をどのように取る必要があるか、およびそのタイミングの決定は、アプリケーションの動作とニーズによって大きく異なります。このサンプルでは、次のように決定が行われ、電源イベント ハンドラが適切なイベント ハンドラでこの関数を使用しています。

bool ShouldStopAnimation()
{
    // If the monitor is off, or if we’re running on a battery
    // and the user hasn’t explicited requested high performance,
    // stop the animation
    return
        !MainWindow.MonitorOn ||
        (MainWindow.PowerPlan != PowerPlan.HighPerformance &&
         MainWindow.RunningOnBattery);
 }

このサンプルで注目すべきことの 1 つは、電源イベントが UI のリスト ボックスに報告されることです。たとえば、以下に the RunningOnBatteryChanged イベント ハンドラを示します。

void Window1_RunningOnBatteryChanged(
    object sender, DependencyPropertyChangedEventArgs e)
{
    myEventsListBox.Items.Add(
        “RunningOnBattery changed - now it’s {0}”, (bool)e.NewValue);

    if (ShouldStopAnimation()) myStoryboard.Pause(this);
    else myStoryboard.Resume(this);
}

アプリケーションが電源を節約するために取ることができるその他の措置の詳細については、「開発者向け Mobile PC ユーザー エクスペリエンス : 電源とデバイスの認識」をご覧ください。

まとめ

この記事と付属するコードでは、Windows Presentation Foundation アプリケーションに電源状態を認識させるための開始点を提供しています。電源状態の認識と Windows Presentation Foundation には、ここでは説明しきれていないことがたくさんあります。そのため、電源状態認識アプリケーションを設計する前に、この記事全体に示した参考資料を確認することを強くお勧めします。使用しているプログラムにとって最善の解決策を導き出すのに役立つ多くの機能や技法が見つかるでしょう。

その他のリソース

電源とデバイスの管理に関する一般的な情報や、電力消費を減らすことができる方法に関するアイディアについては、以下に示す MSDN リンクの役立つドキュメントをご覧ください。

  • 開発者向け Mobile PC ユーザー エクスペリエンス ガイドライン : 電源とデバイスの認識
  • Mobile PC 開発ガイド : Windows Vista の電源管理
  • Mobile PC 開発ガイド : Windows XP の電源管理
  • Win32 および COM の開発 : 電源管理

この記事で使用している Windows Presentation Foundation 機能の概要については、以下のリンクをご覧ください。

  • ルーティングされるイベントの概要
  • カスタム依存関係プロパティ
  • WPF Fundamentals: スレッド モデル
  • Windows Presentation Foundation: データ バインドの概要
  • Windows Presentation Foundation: アニメーションの概要

この記事で使用されている特定の関数、クラス、列挙体の定義については、以下のリンクをご覧ください。

  • SystemEvents.PowerModeChanged イベント
  • WM_POWERBROADCAST
  • RegisterPowerSettingNotification
  • SYSTEM_POWER_STATUS
  • POWERBROADCAST_SETTING
  • Trigger クラス

Andre Michaud はマイクロソフトの Tablet PC プラットフォーム チームで SDET を担当しています。以前、彼は Windows Presentation Foundation の Tablet PC コンポーネントを担当した経験があります。

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


コードを共有する: モバイル アプリケーションとデスクトップ アプリケーションの両方に使えるコードを 1 回で記述する

コードを共有する

モバイル アプリケーションとデスクトップ アプリケーションの両方に使えるコードを 1 回で記述する

Daniel Moth
この記事の内容 : :
  • Windows Mobile および Windows デスクトップ開発
  • プラットフォーム間でのアプリケーションの移植
  • 再ターゲット可能なアセンブリ
  • プラットフォーム間でのコードの共有
この記事は次のテクノロジを使用しています:
.NET Framework
コードのダウンロード : : NetCfSharing2007_07.exe (246KB)
コンテンツ
プラットフォームの差異
先に進まない
再ターゲット可能
バイナリではなくコードを共有する
同じディレクトリ内のプロジェクト ファイルとコード ファイル
スタブ化する
プラットフォーム呼び出し
異なるフレームワーク実装
部分型
System.Diagnostics.Conditional
マイ コンピュータに展開する
リフレクションを使用する
拡張メソッド
まとめ

過去数年間、開発者は Windows® 用の Microsoft® .NET Framework クライアント アプリケーションを構築してきましたが、多くの開発者は同じスキルとツールセットを使用して Windows Mobile® 用のアプリケーションも作成できるとは考えていませんでした。ただし、当時 Windows Mobile は企業でそれほど広く利用されていなかったため、モバイル デバイスをターゲットにしたカスタム アプリケーションを作成する必要性はあまり大きくありませんでした。現在は大きな需要があり、多くのデスクトップ開発者がモバイル開発に着手し始めています。プラットフォーム間での .NET コードの共有は比較的簡単であるにもかかわらず、残念なことに多くの開発者はその機会を逃しています。

元のアプリケーションが Windows と Windows Mobile のどちらに対して作成されたかにかかわらず、両方のプラットフォームでビジネス ロジックを共有する理由は数多くあります。どちらから始めても、もう一方に移植できます。アプリケーションが現在フィールドのラップトップで実行されている場合、そのアプリケーションはおそらくモバイル デバイスで実行するのに適しています。その逆も同様です。また、製品が競合他社の製品に似ている場合は、新しいプラットフォームでも使用できるように拡張すると有利になります。さらに、デスクトップで実行するコードを作成することにより、スマート デバイス プロジェクトでは動作しない開発者ツールを利用できるという利点もあります。

この記事で説明する手法と原則は、デバイスをまたがるコードを作成する場合に利用できます。デバイスをまたがるコードとは、異なるフォーム ファクタ (異なる画面サイズ、方向、タッチ スクリーンなど) を持つ Windows デバイスを対象にしたアプリケーションです。

プラットフォームの差異

.NET Framework について考える場合、筆者はツール、言語、ライブラリ、およびランタイム エンジンについて考えます。この 4 つの要素の差異は、クロスプラットフォーム コード作成の目標に影響します。さいわい、デスクトップ アプリケーションとモバイル アプリケーションに使用するツールはどちらも同じ Visual Studio® です。Visual Studio の各バージョンでは、Visual Studio for Devices チームが継続的にデバイス開発のための新機能を追加し、重要な新しいデスクトップ開発機能がモバイル デバイス開発で期待されるとおりに動作することを確認しています (デバイス開発には Visual Studio 2005 Standard 以降を使用する必要があります)。

構文の観点からは、Visual Basic® と C# はどちらも両方のプラットフォームで完全にサポートされています。ただし、Visual Basic の使用を計画している場合は、Microsoft.VisualBasic.Compatibility アセンブリと暗黙の遅延バインディングがサポートされないことに注意してください。Microsoft.VisualBasic.Compatibility は、Visual Basic 6.0 から移行されたデスクトップ プロジェクトで使用するアセンブリであり、暗黙の遅延バインディングは、Visual Basic プロジェクトで Option Strict On を有効にすることでベスト プラクティスに従っている場合には問題になりません。また、マネージ C++ もサポートされないことに注意してください (ただし、ネイティブ デバイス プロジェクトに同じ IDE を使用することはできます)。サードパーティの言語は、適切なアセンブリを参照すること、また .NET Compact Framework 共通言語ランタイム (CLR) でサポートされていない IL オペレーション コードがコンパイラで生成されないことを確認することによって、.NET Compact Framework のサポートを追加できます。問題を引き起こす IL オペレーション コードは、jmp、calli、refanyval、refanytype、mkrefany、arglist、localloc、unaligned、および tail です。

.NET Compact Framework でのガベージ コレクタとジャスト イン タイム (JIT) コンパイラの実装は、.NET Framework での実装とは異なりますが、それぞれ同じ目的を果たし、マネージ コードに同じサービスを提供する点に注目してください。

他の最新プログラミング フレームワークと同様に、.NET Framework にはクラス ライブラリの豊富なセットが付属しています。.NET Compact Framework は、.NET Framework にある関連機能の 80% をわずか 20% のフットプリントで実現します。コンパクト バージョンにはない .NET Framework の主要領域は、ASP.NET、CLR ホスティング、コード アクセス セキュリティ (CAS)、バイナリ シリアル化、Reflection.Emit、および CodeDOM です。

Windows Presentation Foundation と Windows Workflow Foundation はすぐにはサポートされませんが、.NET Compact Framework のバージョン 3.5 には Windows Communication Foundation のコンパクト バージョンが用意される予定です。バージョン 3.5 では、LINQ to Objects、LINQ to XML、および LINQ to DataSet もサポートされますが、その他の種類の LINQ はサポートされません。

ただし、ほとんどの場合に、問題の原因になるのはこれらの大きな不足分野ではありません。通常、問題が発生する原因は、サポートされる名前空間の一部に期待するすべてのクラスが揃っていないことです。さらに重要なのは、期待するすべてのメンバが揃っていないクラスがあることです。対処方法については後で示します。

また、逆のことも言えます。つまり、.NET Compact Framework は .NET Framework の厳密なサブセットではありません。実際にはいくつかの独自のメンバが追加されています。System.Data.SqlServerCe、System.Net.IrDA、Microsoft.WindowsMobile.DirectX、Microsoft.WindowsCE.Forms という 4 つのデバイス固有のアセンブリがあります。

SqlServerCe には、デバイス上のメモリ内データベースを処理するクラスが含まれています。IrDA には、赤外線に対するプログラミング用のクラスが含まれています。DirectX® は、主にゲーム プログラミング用の多彩なグラフィックスに使用されます。Forms には、デバイスに固有の多くの GUI クラスがあります。これらのデバイス固有のアセンブリに加えて、Mobile プラットフォームに付属する Windows Mobile 5.0 以降用のデバイス固有の Windows Mobile API もいくつかあります。後で、一方のフレームワークに存在し、もう一方には存在しない型の処理方法を示します。

もちろん、デスクトップ デバイスとモバイル デバイスでは入力方法が異なるため、モバイル デバイスではキーボード ショートカットやツールヒントなどについて心配する必要はありません。また、モバイル フォームはサイズ変更不可で、常に全画面表示になります。これはアプリケーションのフォーム間の移動方法にも影響します。そして、最も明白な違いは、モバイル デバイスの画面サイズの方がはるかに小さいことです。したがって、この 2 つのプラットフォーム間ではアプリケーションの UI 層を共有せずに、プラットフォーム固有のユーザー インターフェイスを作成し、ビジネス ロジックのみ共有するのが最適です。

先に進まない

デスクトップ開発者がアプリケーションをモバイル デバイスに移植しようとする際に通常最初に行うのは、デスクトップ フレームワークに対してビルドされたアセンブリを取り出し、デバイス プロジェクトから参照することです。これは動作せず、まったくサポートされていません。デスクトップ プロジェクトで、.NET Compact Framework でも使用可能な型とメンバのみを慎重に使用した場合でも、実行時に TypeLoadException 例外および MissingMethodException 例外が発生します。このため、非スマートデバイス プロジェクト テンプレートを選択して Visual Studio プロジェクトを作成した場合、そのプロジェクトの出力は、Windows CE プラットフォーム (当然、Windows Mobile が構築されているプラットフォーム) で実行されません。

あいにく、前述の状況を検出するのは困難です。スマートデバイス プロジェクトからアセンブリを参照する場合、それがデスクトップ アセンブリかどうかのチェックは行われないため、デスクトップ プロジェクト テンプレートからビルドされたクラス ライブラリを使用している可能性があります。この状況は、サードパーティ製の DLL を使用している場合によく起こります。このような場合には、実行時にエラーが検出されるだけです。

この問題が発生していると疑われる場合は、探すべき手がかりがいくつかあります。Visual Studio を通じてターゲットにプロジェクトを展開する際 (デバッグでプロジェクトを実行する際など) に、Visual Studio 出力ウィンドウで、どのアセンブリがデバイスに展開されているかを示す診断メッセージに注目します (図 1 を参照)。展開プロセスに長時間かかる場合は、誤って参照したデスクトップ アセンブリへの依存関係として検出された .NET Framework アセンブリのフル セットが Visual Studio によってターゲットにコピーされている可能性があります。

図 1 Visual Studio の出力ウィンドウ
図 1 Visual Studio の出力ウィンドウ(画像を縮小するには、ここをクリックします)
図 1 Visual Studio の出力ウィンドウ
図 1 Visual Studio の出力ウィンドウ (画像を拡大するには、ここをクリックします)

運がよければ、展開プロセスは十分なディスク容量がないことを通知するエラーで失敗します。デスクトップの依存関係が限定されている場合は、展開プロセスが成功します。その場合は、すべてのファイルの種類を示すリモート ファイル ビューアを使用して、アプリケーションが展開されたターゲット上のフォルダを調べます。展開されたすべての .NET Framework アセンブリが見つかります。.NET Compact Framework アセンブリの公開キー トークンは 9 から開始しますが、デスクトップの同等のトークンは B から開始します。この情報を使用して、展開または参照しているものがデスクトップとデバイスのどちらのフレームワーク アセンブリであるかを迅速に判断できます。その情報は、.NET Compact Framework ローダー ログにも表示されます。このログには、これらの問題によってアプリケーションが実行時に失敗した場合のエラーが含まれます。

再ターゲット可能

デスクトップ アセンブリはデバイス上で実行できませんが、その逆は可能です。スマート デバイス プロジェクトを作成し、それをビルドした場合、出力 (EXE または DLL) はデスクトップ上で実行でき、デスクトップ プロジェクトから参照できます。これが動作するのは、.NET Compact Framework アセンブリが再ターゲット可能であるためです。ILDASM でその 1 つを開いた場合、System.Reflection.AssemblyNameFlags.Retargetable 属性があることがわかります (図 2 を参照)。つまり、.NET Compact Framework アセンブリに対するすべての参照は、実行時にデスクトップの同等のターゲットを対象とするように変更されます。このため、明確にするために、アセンブリでは .NET Framework のデバイス実装ではなくデスクトップ実装を使用します。

図 2 ILDASM の Retargetable フラグ
図 2 ILDASM の Retargetable フラグ(画像を縮小するには、ここをクリックします)
図 2 ILDASM の Retargetable フラグ
図 2 ILDASM の Retargetable フラグ (画像を拡大するには、ここをクリックします)

自宅で試すことのできる例を見てみましょう。任意の Windows Mobile プラットフォームをターゲットとする新しいスマート デバイス アプリケーションを作成します。いくつかのコントロールをメイン フォームに追加します。たとえば、TabControl、ProgressBar、および MainMenu コントロールに対する 4 つの MenuItem を追加します (図 3 を参照)。各 MenuItem の Click イベント ハンドラ メソッドを生成し、図 4 のコードをこれらのイベント ハンドラ メソッドに追加します。

Figure 4 Click イベント ハンドラ メソッド (Close Figure 4)
private void mnuFormShow_Click(object sender, EventArgs e)
{
  // TODO later
}

private void mnuMbox_Click(object sender, EventArgs e)
{
  MessageBox.Show(“some text”, “random caption”);
}

private void mnuDateTime_Click(object sender, EventArgs e)
{
  this.Text = DateTime.Now.TimeOfDay.ToString();
}

private void mnuCalc_Click(object sender, EventArgs e)
{
  string pth = @”\Windows\calc.exe”;
  Process.Start(pth, null);
}
図 3 MainMenu のデザイン
図 3 MainMenu のデザイン

エミュレータまたは独自のデバイスをターゲットにしたプロジェクトを実行し、各メニュー項目をクリックしたときに期待どおりに動作することを確認します。デスクトップに戻り、ビルド フォルダ (bin/Debug) に移動して、実行可能ファイルをダブルクリックするとそのファイルがデスクトップ マシンでどのように実行されるかを確認します (図 5 を参照)。

図 5 デバイス プロジェクト
図 5 デバイス プロジェクト

コントロールは自身をデスクトップ スタイルでレンダリングします。たとえば、TabControl には上部にタブがあり、メニューは上部に表示され、ProgressBar の外観は若干異なります。ここで、MessageBox を表示する MenuItem をクリックすると、期待どおりに動作します。フォームのキャプションを変更する MenuItem をクリックし、ミリ秒単位の追加で精度がどのように向上するかに注目します。これは、若干異なるデスクトップ実装を現在使用していることの手がかりになります。ここでの教訓は、再ターゲット可能性に完全には依存せずに、アプリケーションを徹底的にテストして、実装のわずかな違いが重要ではないという確信を得ることです。最後に、電卓を起動する MenuItem をクリックし、アプリケーションが「System.ComponentModel.Win32Exception: 指定されたファイルが見つかりません」という例外でクラッシュすることを確認します。明らかに、原因は、デスクトップ上の電卓のパスがデバイス上の電卓のパスと異なっていることです。このようなプラットフォーム固有の問題に対処し、プラットフォームをまたがって動作する単一の実装を提供するには、使用しているプラットフォームが何であるかを実行時に検出し、それに応じてコードを分岐する必要があります。たとえば、イベント ハンドラに対する次の変更を参照してください。

private void mnuCalc_Click(
  object sender, EventArgs e)
{
  string pth;
  if (Environment.OSVersion.Platform == PlatformID.WinCE)
  {
    pth = @”\Windows\calc.exe”;
  }
  else
  {
    pth = @”c:\Windows\system32\calc.exe”;
  }
  Process.Start(pth, null);
}

これで、Windows Mobile と Windows デスクトップのどちらでもプロジェクトを実行できるようになり、プラットフォームに対応する電卓アプリケーションが正しく起動されます。

デスクトップには存在しない、デバイス固有のアセンブリがあることを思い出してください。このようなアセンブリを使用した場合にどうなるかを見てみましょう。新しいフォーム (Form2) をスマート デバイス プロジェクトに追加し、そのプロジェクトで InputPanel コントロールを削除してから、2 つの Button コントロールと 1 つの TextBox を追加します。コントロールのイベント ハンドラを生成して、図 6 に示すようなコードを追加します。

Figure 6 textBox イベント ハンドラ (Close Figure 6)
private void textBox1_GotFocus(object sender, EventArgs e)
{
  inputPanel1.Enabled = true;
}

private void textBox1_LostFocus(object sender, EventArgs e)
{
  inputPanel1.Enabled = false;
}

private void button1_Click(object sender, EventArgs e)
{
  Cursor.Current = Cursors.WaitCursor;
}

private void button2_Click(object sender, EventArgs e)
{
  Cursor.Current = Cursors.Default;
}

ここで、最初のフォームの空の MenuItem Click イベント ハンドラ メソッドに戻り、Form2 を表示するコードを次のように追加します。

private void mnuFormShow_Click(object sender, EventArgs e)
{
  Form2 f = new Form2();
  f.ShowDialog();
}

デバイス上でプロジェクトを実行した場合、Form2 を Form1 のメニュー項目から開いたときに、Form2 のボタンをクリックしてデバイス上のビジー カーソルを表示または非表示できることがわかります。Textbox にフォーカスを置くと Soft Input Panel (SIP) が表示され、フォーカスを外すと非表示になります。デスクトップに戻り、前と同様にビルド ディレクトリに移動し、デスクトップ上で実行可能ファイルを実行します。メニュー項目をクリックして Form2 を表示すると、次のような例外が発生します。

System.IO.FileNotFoundException: Could not load file or assembly
 ‘Microsoft.WindowsCE.Forms, Version=2.0.0.0, Culture=neutral,

PublicKeyToken=969db8053d3322ac’ or one of its dependencies.
The system cannot find the file specified. File name: ‘Microsoft.WindowsCE.Forms,
 Version=2.0.0.0, Culture=neutral, PublicKeyToken=969db8053d3322ac’

この例外は、InputPanel コントロールをプロジェクトに追加したときに、Microsoft.WindowsCE.Forms への参照が自動的に追加されたために発生します。これはデバイス専用のアセンブリであり、デスクトップ上には再ターゲット先の対象がないため、フォームが作成されて Microsoft.WindowsCE.Forms.InputPanel をそのアセンブリから作成しようとしたときに例外がスローされます。これをもっとわかりやすくするために変更を加えてみましょう。まず、InputPanel コントロールをフォームのデザイナ ビューから削除し、図 7 に示すように textBox イベント ハンドラを変更します。

Figure 7 プラットフォームをチェックする (Close Figure 7)
private void textBox1_GotFocus(object sender, EventArgs e)
{
  if (Environment.OSVersion.Platform == PlatformID.WinCE) //naive
  {
    Microsoft.WindowsCE.Forms.InputPanel inputPanel1 =
                new Microsoft.WindowsCE.Forms.InputPanel();
    inputPanel1.Enabled = true;
  }
}

private void textBox1_LostFocus(object sender, EventArgs e)
{
  // in the interest of space, let’s ignore this method
  //inputPanel1.Enabled = true;
}

これにより、InputPanel の使用が 1 つのメソッドに分離され、実行時にプラットフォームをチェックすることで例外の単純な修正も試行されます。このプロジェクトをデバイス上で実行した場合、依然として期待どおりに動作しますが、デスクトップで実行した場合は、前と同じ例外で失敗します。フォームが表示された直後ではなく、textBox1 にフォーカスを移動したときにのみ失敗するのですが、いずれにしても失敗します。

JIT コンパイルが原因で、このケースではランタイム チェックが不十分です。実行時に、メソッド呼び出しが行われると、CLR は呼び出されるメソッド (textBox1_GotFocus) に対してネイティブ コードが存在するかどうかを確認します。存在しない場合、JIT コンパイラは、メソッド エントリが指す IL コードからネイティブ オペレーション コードを生成します。その時点で、JITer は、実行時にどのパスが実行されたかに "注意" せず、メソッドで使用されるすべての型を単純に検索する必要があります。このため、Microsoft.WindowsCE.Forms.InputPanel 型および対応するアセンブリを読み込もうとしますが、アセンブリがデスクトップに存在しないため読み込むことができません。そして、例外がスローされます。InputPanel の使用を別のメソッドに抽出し、呼び出し側メソッドからのランタイム チェックで、抽出されたメソッドが呼び出されないことを確認できますが、これはほとんどの開発者が考えるよりも面倒な作業です。この問題を解決する適切な方法は、プラットフォーム間でコードを共有するための代替アプローチを探すことです (前述のアプローチの追跡に関心がある場合は、msdn2.microsoft.com/ aa480686 の「プラットフォーム検出」のセクションを参照してください。この文書には、ここで示したのと同様のファクタリング アプローチの例が記載されています)。

バイナリではなくコードを共有する

前のセクションでは、スマート デバイス プロジェクトを作成し、再ターゲットによってデスクトップで出力バイナリを使用する方法を調べました。ただし、再ターゲット可能アプローチには 2 つの制限があります。まず、コードではデスクトップ専用の型またはメソッドを使用できません。これらは単純にコンパイルされません。次に、デスクトップでデバイス固有のアセンブリを読み込まないように注意する必要があります。これらのアセンブリはランタイム例外で失敗するからです。

どちらの問題も、実行時にではなくコンパイル時にコードの条件分岐を使用すること、また 2 つの異なる出力を生成してプラットフォーム固有のアセンブリを実現する 2 つのプロジェクトを作成することによって解決できます。この 2 つのプロジェクトは、どちらもソース コード ファイルの単一セットを使用します。最終的な目標は、ビジネス ロジックに対するすべての変更を、エラーを起こしやすいコピー/貼り付けのオーバーヘッドなしに両方のプラットフォームに同時に反映することであるため、2 つのプロジェクトを作成することが重要なポイントとなります。別のアプローチを試してみましょう。

最初に、新しいデスクトップ Windows プロジェクトを作成します。既定で追加されるコード ファイル (Form1 ファイルと Program ファイル) を削除します。プロジェクト メニューで、[既存項目の追加...] を選択し、コード ファイルが存在する、前に作成したスマート デバイス プロジェクトのフォルダを参照します。最後に、すべてのコード ファイルのみ ( *.cs) を選択し、これらのファイルをプロジェクトにリンクとして追加します。ダイアログの既定の [追加] オプションを選択した場合は、ファイルへのリンクではなくコピーが作成されるため (図 8 を参照)、リンクとして追加することが重要です。

図 8 既存のファイルを追加する
図 8 既存のファイルを追加する

プロジェクトをビルドし、以前は実行時のみに見られたエラーがコンパイル時にも見られることに注目します (図 9 を参照)。もちろん、この場合はデバイス固有のアセンブリへの参照を追加しても意味がないため、ここで条件付きコンパイルを導入します。

図 9 コンパイル時に問題を探す
図 9 コンパイル時に問題を探す(画像を縮小するには、ここをクリックします)
図 9 コンパイル時に問題を探す
図 9 コンパイル時に問題を探す (画像を拡大するには、ここをクリックします)

デスクトップ プロジェクトのプロパティに移動し、図 10 に示すように、[ビルド] タブで FULL_FRAME ("full framework" の短縮形) という名前の新しい "条件付きコンパイル シンボル" を追加します。

図 10 条件付きコンパイル定数
図 10 条件付きコンパイル定数(画像を縮小するには、ここをクリックします)
図 10 条件付きコンパイル定数
図 10 条件付きコンパイル定数 (画像を拡大するには、ここをクリックします)

次に、新たに導入したコンパイル定数を使用して、Form2.cs のコンパイル エラーを修正します。問題の原因となっているメソッドを次のように変更し、デスクトップ プロジェクトを再ビルドします。

private void textBox1_GotFocus(object sender, EventArgs e)
{
#if !FULL_FRAME
    Microsoft.WindowsCE.Forms.InputPanel inputPanel1 =
          new Microsoft.WindowsCE.Forms.InputPanel();
    inputPanel1.Enabled = true;
#endif
}

これで、この (デスクトップ) プロジェクトがビルドされるので、このプロジェクトを実行し、すべてが期待どおりに動作することを確認できます。デバイス プロジェクトに切り替えた場合に、ソース ファイルが Visual Studio のインスタンスの外部で変更されたことがどのように警告されるか、および再読み込みの確認がどのように行われるかに注目します。[はい] をクリックします。

これは条件付きコンパイル定数の別のセットを使用する別のプロジェクトであるため、プロジェクトはデバイス上で正常にコンパイルおよび実行されます。デスクトップではこのプロジェクトの出力をこれ以上実行しません。つまり、以前は電卓の起動時に導入されたランタイム条件をコンパイル時の条件に変更できるため、メソッドのサイズが削減されます。

これで、条件付きコンパイルの使用のしくみがわかったので、利点について考えます。2 つの異なるプロジェクトがあるということは、それぞれに異なるプロジェクト参照のセットを使用できることを意味します。一方のプロジェクトにコード ファイルを含め、もう一方には含めないこともできます。デスクトップ側では、デスクトップ ユーザーの操作性を高めるために完全フレームワークのメンバを利用できます。これらのデスクトップ メンバの使用が #if プリプロセッサ ディレクティブで囲まれているか、デスクトップ プロジェクトにのみ存在するソース ファイルに分離されているかを確認するだけです。完全フレームワーク メンバを使用して、デスクトップ側での操作性を強化できる例は多数あるため、その 1 つを見てみましょう。

Program.cs ファイルで、Main 関数を次のように変更します。

static void Main()
{
#if FULL_FRAME
  Application.EnableVisualStyles();
#endif
  Application.Run(new Form1());
}

ここで、プロジェクトを実行し、元のコントロールの外観を再ターゲット可能アプローチの結果と比較します (図 11 を参照)。

カーソルの表示と非表示を行うコードがデスクトップでは動作しないことがわかります (ただし、コンパイル エラーやランタイム エラーは生成されません)。また、Visual Studio 2005 からは、デバイス プロジェクトに特定の条件付きコンパイル定数 (PocketPC、Smartphone、Windows CE) が既に定義されていることにも注意してください。これらの組み込み定数のみ使用するか、導入された FULL_FRAME などの独自の定数を使用するか、または両方を組み合わせて使用するかを選択できます。どれを選択するかは重要ではありません。

図 11 元のコントロールと再ターゲット後のコントロール
図 11 元のコントロールと再ターゲット後のコントロール(画像を縮小するには、ここをクリックします)
図 11 元のコントロールと再ターゲット後のコントロール
図 11 元のコントロールと再ターゲット後のコントロール (画像を拡大するには、ここをクリックします)

プラットフォーム間でのコード資産の共有に対する両方のアプローチを理解したところで、いくつかのヒントとコツについて見てみましょう。これらのヒントとコツには、再ターゲット可能アプローチに適用するもの、条件付きコンパイルに適用するもの、および両方に適用するものがあります。

同じディレクトリ内のプロジェクト ファイルとコード ファイル

コードを共有するときに、筆者は 2 つの異なるプロジェクトを作成し、最初のソース コード ファイルから 2 つ目のソース コード ファイルへのリンクとして追加しました。筆者は、すべてのソース コード ファイルをプロジェクト ファイルと同じディレクトリに置いて、Visual Studio のソリューション エクスプローラで必要に応じてファイルを非常に簡単に含めたり除外したりできるようにすることを好みます。これを実現するには、デバイス プロジェクトを作成した後で、Windows デスクトップ プロジェクトを作成し、前と同じようにコード ファイルを削除します。他のファイルを追加する代わりに、Visual Studio を終了し、デスクトップ プロジェクト ファイルが作成されたディレクトリに移動します。プロジェクト ファイルを切り取り、デバイス プロジェクト ファイルが置かれているのと同じディレクトリに貼り付けます。たとえば、デバイス プロジェクトが "C:\Temp\" にあった場合、デスクトップ プロジェクト ファイル (csproj) を切り取ってそのフォルダに貼り付けます (デスクトップ プロジェクトとデバイス プロジェクトの名前が異なることを確認します)。

ここで、デスクトップ プロジェクトを開き、ソリューション エクスプローラで "すべてのファイルを表示" します。すべてのデバイス ソース コード ファイルが表示され、適切だと思われるものをプロジェクトに含めることができます。また、デバイス プロジェクトを開いた場合は逆の操作も実行できます (図 12 を参照)。

図 12 ファイルを選択する
図 12 ファイルを選択する

このアプローチでは、プロジェクトの 1 つに新規ファイルを追加した場合、他の種類のプロジェクトを開いたときに特定するのが非常に簡単になり、必要に応じてファイルを含める方が簡単です。

プロジェクト名と出力アセンブリ名は異なりますが、名前空間と型名は同じままです。両方のプラットフォームに同じアセンブリ名を使用することを好む場合は、プロジェクトのプロパティで、ビルド フォルダの名前を binCF や binFF などに変更する必要があります。また、一度に 1 つのプロジェクトのみ開くことをお勧めします。これにより、混乱が回避され、ソース制御が非常にうまく機能するためです。

スタブ化する

多くの場合、条件付きコンパイルでコードの乱雑化を防ぎ、プラットフォームに適用できないコードの空のダミー スタブを追加することが最適です。たとえば、あるクラスをプロジェクト全体で何度も使用し、そのクラスが他のプラットフォームに適用されない場合は、同じ (ただし、空の) インターフェイスを持つクラスを追加できます。InputPanel クラスの使用例を次に示します。

namespace Microsoft.WindowsCE.Forms
{
    public class InputPanel
    {
        public bool Enabled;
    }
}

これで、InputPanel が使用される場合に、条件分岐を使用しないでコードを同じにすることができます。単純にデスクトップ プラットフォームには影響しません。

プラットフォーム呼び出し

多くの .NET Compact Framework アプリケーションは、P/Invoke サービスを使用してネイティブ DLL に存在する機能にアクセスすることにより、追加の機能を実現します。通常、これらの DLL の名前は Windows CE と Windows のデスクトップ バージョンとで異なります。以前にどちらのアプローチを選択したかに応じて、これに対処する方法がいくつかあります。条件付きコンパイルを使用している場合は、図 13 のコードに、クロスプラットフォーム P/Invoke 宣言を作成する方法を示します。図 14 の手法は、再ターゲット可能アプローチを選択した場合に役立ちます。

Figure 14 実行時の P/Invoke の決定 (Close Figure 14)
class PInvokesRuntime
{
    [DllImport(“coredll.dll”, EntryPoint = “QueryPerformanceCounter”)]
    internal static extern int QueryPerformanceCounterCE(
        out Int64 perfCounter);

    [DllImport(“kernel32.dll”, EntryPoint = “QueryPerformanceCounter”)]
    internal static extern int QueryPerformanceCounterFull(
        out Int64 perfCounter);

    private void UseIt()
    {
        long p;
        int i;
        if (Environment.OSVersion.Platform == PlatformID.WinCE)
        {
            i = QueryPerformanceCounterCE(out p);
        }
        else
        {
            i = QueryPerformanceCounterFull(out p);
        }
    }
}
Figure 13 コンパイル時の P/Invoke の決定 (Close Figure 13)
class PInvokesCompileTime
{
#if FULL_FRAME
    private const string DllName = “kernel32.dll”;
#else
    private const string DllName = “coredll.dll”;
#endif

    [DllImport(DllName)]
    internal static extern int QueryPerformanceCounter(
        out Int64 perfCounter);

    private void UseIt()
    {
        long p;
        int i;
        i = QueryPerformanceCounter(out p);
    }
}

異なるフレームワーク実装

前に、使用しているフレームワークに応じて System.DateTime.TimeOfDay が異なる精度の値をどのように返すかを説明しました。このため、テスト後に、これをコードでどのように扱うかを必要に応じて決定できます。メソッドが異なる動作をする例は他にもあります。特に、一方のフレームワーク上のメソッドが、もう一方のフレームワーク上のメソッドとは異なる例外をスローするケースがいくつかあります。次に例を示します。

private void button1_Click(object sender, EventArgs e)
{
    Type t = this.GetType();
    MethodInfo m = t.GetMethod(“DoIt”);
    m.Invoke(this, null); //throws
}

public void DoIt()
{
    throw new InvalidOperationException(“my message”);
}

このコードでは、両方のプラットフォームで、MethodInfo を呼び出す行で例外が発生します。デバイスでは、スローされる例外は InvalidOperationException です。一方、デスクトップでは、スローされる例外は TargetInvocationException です (この例外には、InnerException プロパティとして設定された元の InvalidOperationException があります)。したがって、テスト中に例外ケースをテストし、両方の種類の例外に対する catch ブロックがあることを確認する必要があります。

異なるフレームワーク実装の別の例は、前にビジー カーソルの表示で説明しました。デバイスで動作するものはデスクトップに影響せず、デスクトップで必要なものはデバイスでサポートされていません。両方のプラットフォームで一貫した動作が必要な場合は、単一のメソッドを抽出し、カーソルの表示をカプセル化する条件付き動作を次のように分離できます。

public static class TheCursor
{
    public static void CursorCurrent(Control c, Cursor defaultOrWait)
    {
#if FULL_FRAME
        if (c != null) c.Cursor = defaultOrWait;
#else
        Cursor.Current = defaultOrWait;
#endif
    }
}

すると、呼び出し側コードは、デバイス プラットフォームとデスクトップ プラットフォームの両方で同じままにすることができます。

private void button1_Click(object sender, EventArgs e)
{
    TheCursor.CursorCurrent(this, Cursors.WaitCursor);
}

private void button2_Click(object sender, EventArgs e)
{
    TheCursor.CursorCurrent(this, Cursors.Default);
}

部分型

ソース コード ファイルにプラットフォーム固有の機能が含まれている場合には、そのソース コード ファイルを一方のプロジェクトに含め、もう一方のプロジェクトには含めないことができると前に述べました。クラスが両方のプラットフォームに適用されても、そのクラスの一部のメンバが適用されない場合は、同様のシナリオが発生します。クラスの大きなセクションを #if ディレクティブでラップする代わりに、Visual Studio 2005 で導入された部分クラス機能を使用できます。つまり、部分キーワードを使用してクラスを 2 つのファイルに分割し、一方のプラットフォームにのみ適用される機能を別のファイルに分離できます。適用されないプロジェクトには、単純にそのファイルを含めません。

System.Diagnostics.Conditional

注意すべき重要なヒントは、実行時に呼び出さないメソッドに System.Diagnostics.Conditional 属性を使用することです。この属性をメソッドに適用した結果、そのメソッドの呼び出しはコンパイルされたバイナリから除外されます。もちろん、適用される属性を持つメソッドは、引き続きコンパイル可能である必要があります。

[Conditional(“FULL_FRAME”)]
public void SomeDesktopOnlyMethod()
{
    //TODO code that compiles in both projects
}

メソッド全体を #if で囲んだ場合は、メソッドが呼び出されたすべての場所を検索し、呼び出しを #if でラップする必要もあります。その代わりここでは単純に、不要なメソッドに属性を適用し、すべての呼び出しサイトを無視します。これらはバイナリにコンパイルされません。

マイ コンピュータに展開する

再ターゲット可能アプローチを選択した場合は、デスクトップ プラットフォーム エラーは実行時にしか発見できません。スマート デバイス プロジェクトはエミュレータまたは物理デバイスにのみ展開されるため、デスクトップ上でアプリケーションをデバッグする方法について問題が発生します。使用できるアプローチはいくつかあります。

最も簡単ですが最も面倒なアプローチは、ファイル システムからアプリケーションを実行し、Visual Studio の [ツール] メニューの [プロセスにアタッチ] を使用してそのアプリケーションにデバッガをアタッチする方法です (図 15 を参照)。ランチャーとして機能するデスクトップ プロジェクトを作成することにより、このプロセスを半自動化することもできます。デスクトップ プロジェクトをスマート デバイス プロジェクトのあるソリューションに追加し、それをスタートアップ プロジェクトとして設定し、スマート デバイス プロジェクトを参照します。デスクトップ プロジェクトに、単一の静的 Main 関数を持つ単一の Program ファイルがあることを確認し、そこから Application.Run メソッドにスマート デバイス プロジェクトのスタートアップ フォームの新規インスタンスを渡します。

図 15 [プロセスにアタッチ] ダイアログ
図 15 [プロセスにアタッチ] ダイアログ(画像を縮小するには、ここをクリックします)
図 15 [プロセスにアタッチ] ダイアログ
図 15 [プロセスにアタッチ] ダイアログ (画像を拡大するには、ここをクリックします)

スマート デバイス プロジェクトの展開オプションに [Deploy to My Computer] (マイ コンピュータに展開する) の選択肢があるのが理想です。実際に、Visual Studio 2005 の早期 Community Technology Preview 中には、そのオプションが存在していました (ただし、最終製品からは削除され、このオプションは Visual Studio の "Orcas" リリースにもありませんでした)。

筆者のブログで説明している手順に従って [Deploy to My Computer] (マイ コンピュータに展開する) 機能を再び有効にできますが、Microsoft では推奨またはサポートされていないことに注意してください。

リフレクションを使用する

再ターゲット可能アプローチを選択した場合、デスクトップ専用のメソッドはデバイス プロジェクトでコンパイルされないため、このメソッドは使用できません。ただし、どうしても使用する必要がある場合は、リフレクションを使用してこのようなメソッドを呼び出すことができます。前に示した、アプリケーションのテーマを有効にする例に戻り、次のようにしてこれを実現できました。

static void Main()
{
    //Application.EnableVisualStyles();
    if (Environment.OSVersion.Platform != PlatformID.WinCE)
    {
        Type t = typeof(Application);
        MethodInfo m = t.GetMethod(“EnableVisualStyles”);
        m.Invoke(null, null);
    }

    // As before
    Application.Run(new Form1());
}

拡張メソッド

.NET Compact Framework クラスを使用している場合、特定のクラスに完全 Framework と比較して欠落しているメンバがあることがよく起こります。通常、開発者は欠落している機能を自身で作成する必要があり、可能な場合には継承を使用して欠落メンバを追加するか、ユーティリティ ヘルパ クラスに追加するかを決定します。たとえば、デスクトップで開始したデバイス プロジェクトで、TextBox に対して Cut 操作を実行するとします。Cut メソッドは、.NET Compact Framework の TextBox クラスにはありません。この記事で説明した内容に基づいて、図 16 のようなコード ファイルをデバイス プロジェクトに追加できます。

Figure 16 TextBoxExtensions (Close Figure 16)
using System;
using System.Windows.Forms;
using Microsoft.WindowsCE.Forms;

static class TextBoxExtensions
{
    const int WM_CUT = 0x0300;
    const int WM_COPY = 0x0301;
    const int WM_PASTE = 0x0302;

    public static void Cut(TextBox t)
    {
        Message msg = Message.Create(
            t.Handle, WM_CUT, IntPtr.Zero, IntPtr.Zero);
        MessageWindow.SendMessage(ref msg);
    }
}

図 16 のファイルは、デバイス プロジェクトにのみ存在するため、条件の設定は不要です。さらに、次のような呼び出し側コードを作成できます (これは両方のプロジェクトに存在します)。

private void SomeMethod()
{
#if FULL_FRAME
    textBox1.Cut();
#else
    TextBoxExtensions.Cut(textBox1);
#endif
}

C# 3.0 および次期バージョンの Visual Basic には、拡張メソッドという新機能があります。ここで機能を詳細に説明する余裕はないため、この機能をよく知らない場合は、筆者のブログにアクセスするか、2007 年 6 月発行の 『MSDN® Magazine』で Anson Horton の記事を参照してください。この機能は、コードネーム "Orcas" の Visual Studio に付属の .NET Compact Framework 3.5 でもサポートされ、上記の状況に対処するのに役立ちます。

デバイス プロジェクトで、System.Core.dll への参照を追加してから、ヘルパ メソッドの最初のパラメータの前に次のキーワードを追加してデバイス専用のファイルを変更し、拡張メソッドにします。

public static void Cut(this TextBox t)

この変更を行うと、条件を使用しないで呼び出し側コードを同じくクロスプラットフォームにすることができます。

private void SomeMethod()
{
  textBox1.Cut();
}

デスクトップではフレームワーク メソッドが呼び出され、デバイスでは拡張メソッドが呼び出されます。将来 .NET Compact Framework チームがメソッドを追加した場合、コードは実際の .NET Framework メソッドをシームレスに呼び出し (インスタンス メソッドは拡張メソッドよりも優先されるため)、ある時点で余分なコード ファイルを削除できます。

まとめ

この記事では、デスクトップの開発と Windows Mobile の開発の違いを調べ、違いの多くは Framework ライブラリにあるという結論に達しました。プラットフォーム間でコード資産を共有するための 2 つのアプローチの例を提供し、筆者が条件付きコンパイル アプローチを好むことを示し、いくつかの便利なヒントを提示しました。個人的にはこの知識を使用してクラス ライブラリに含まれるビジネス ロジックを共有することをお勧めしますが、アプリケーションのターゲットが Windows Mobile ではなく Windows デスクトップを実行している Ultra Mobile PC (UMPC) やその他のタッチスクリーン デバイスなどの場合には特に、GUI を共有することもできます。

Daniel Moth は、英国 Microsoft の開発者およびプラットフォーム グループに勤務しています。Daniel は『Microsoft Mobile Development Handbook』 (Microsoft Press、2007 年) の共著者であり、彼のブログ (www.danielmoth.com/Blog) を通して連絡できます。


NTFS: ファイル システム トランザクションを使用してアプリケーションを強化する

NTFS

ファイル システム トランザクションを使用してアプリケーションを強化する

Jason Olson
この記事の内容 : :
  • カーネル トランザクション マネージャ
  • 分散トランザクション コーディネータ
  • ネイティブ コードからトランザクション NTFS を使用する
  • マネージ コードからトランザクション NTFS を使用する
この記事は次のテクノロジを使用しています:
Windows Vista、.NET Framework
コードのダウンロード : : TxF2007_07.exe (159KB)
コンテンツ
トランザクション NTFS の利点
トランザクション NTFS を使用しないほうがよい状況
TxF を使用する
セカンダリ リソース マネージャ
管理
トランザクション コマンド ラインの変更点
まとめ

これは、Windows® ベースのアプリケーションにとって、画期的な一歩です。

また、今までより堅牢なアプリケーションの開発にも役立ちます。何の話かというと、トランザクション NTFS (別名 TxF) の話です。この記事では、トランザクション NTFS の概要、トランザクション NTFS が開発者に与える影響、およびこの新しいテクノロジをアプリケーション内で使用する方法について説明します。

TxF とは何でしょうか。概念的には非常に単純であり、トランザクション NTFS = トランザクション + NTFS という論理的な等式で表すことができます。Windows Vista™ と Windows Server® の次期バージョンであるコードネーム "Longhorn" では、TxF によって、トランザクション ファイル操作という概念が導入されます。

トランザクション ファイル操作とは、開発者に直接公開できる、完全に ACID セマンティクスに基づいたトランザクションのことを指します。つまり、完全にアトミックで (atomic)、一貫性があり (consistent)、独立していて (isolated)、永続性がある (durable) 操作を、ファイル操作にも直接適用できるようになったということです。データベース開発者は、長年にわたってこのような ACID 機能の恩恵を受けてきたため、待望の機能がファイル操作に追加されたと言えます。

これらのトランザクション機能がすべて NTFS に組み込まれた場合、トランザクション NTFS によってどの程度のパフォーマンス オーバーヘッドが発生するか、気になると思います。TxF は、使用した分だけ負荷がかかります。トランザクション ファイル操作を使用しない場合、オーバーヘッドは発生しません。また、トランザクション ファイル操作を使用した場合でも、発生するオーバーヘッドは非常にわずか (1 ～ 2%) です。I/O 関連の処理を頻繁に実行しないアプリケーションの場合、この程度のパフォーマンスへの影響をユーザーが感じることはほとんどありません。

もう 1 つパフォーマンスに関する特徴があります。トランザクション NTFS は、コミット用に最適化されています。トランザクション内でディスクに新しいページを書き込むと、それらのページは直接ディスクに書き込まれ、古いページは、他のリーダーが必要に応じて取得できるように保存されます。この最適化により、トランザクションがコミットされたときには、変更後のページが既にディスクに反映されています。その結果、トランザクションのコミットによって発生するオーバーヘッドが最小限に抑えられます。

トランザクション NTFS の利点

理論的に考えると、トランザクション NTFS は魅力的です。しかし、開発者としては、なぜ TxF を使用する必要があるのでしょうか。これには多くの明確な理由があります。具体的には、TxF により、強力なアプリケーションの開発に役立つ、次の 3 つの目的が達成されるからです。

  • アプリケーションの安定性を強化する
  • プラットフォームの安定性を強化する
  • 革新性を高める

それでは、TxF がどのようにしてこれらの目的を達成するのか、詳しく見ていきましょう。

トランザクション NTFS を使用すると、特定のアプリケーション用に記述および管理する必要があるエラー処理コードが減少するか一切なくなるため、アプリケーションの安定性を向上させることができます。これにより、最終的にはアプリケーションの複雑さが低下し、アプリケーションのテストが容易になります。たとえば、ドキュメント管理システムを開発していて、そのシステムで使用する SQL データ ソースと、ディスク上のファイル ストアとの間の一貫性を維持する必要があるとします。非トランザクション システムの場合、一貫性を維持する作業は、複雑で難しい作業です。トランザクション ファイル操作を使用しなかった場合、処理中のあらゆる時点で発生する可能性がある障害 (オペレーティング システムのクラッシュなどの大きな障害を含む) を考慮することはほとんど不可能です。

以前は、この一貫性の問題に対処するために、ファイルの新しいバージョンに一時的に別の名前を付けて保存し、新しいデータを SQL データベースに書き込んだ後、SQL トランザクションをコミットするときに、その一時的なファイル名を元のファイル名に変更したりしていました。しかし、SQL データベースへのコミットを実行した直後、ファイル名が変更される前に、アプリケーションのクラッシュや停電が発生した場合のことを考えてみてください。データ セットの一貫性が損なわれるだけでなく、不完全なデータがファイル システム上に残るため、後から削除する必要があります。例によって、処理が失敗する可能性があるさまざまな状況を細かく把握することは、きわめて困難です。

ソリューションによっては、全体で 2 段階のコミットを行うプロトコルを独自に実装し、データベースとファイル システムへの変更を同時にコミットする場合もあるでしょう。これにより、アプリケーションの安定という目的には近付きますが、独自に作成するアプリケーションに、カスタムの 2 段階コミット システムを実装することは非常に困難です。また、それらのコードは、独自に記述、テスト、および維持する必要があります。

トランザクション管理の動作を必要とする開発者が、この動作を独自に実装するよりも、この動作がプラットフォームに組み込まれていたほうが便利ではないでしょうか。Windows Vista と Windows Server "Longhorn" では TxF が導入され、これらの機能が直接プラットフォームに組み込まれました。また、トランザクション NTFS はシステム自体に組み込まれているため、他の方法では実現できないレベルの統合が、アプリケーションに提供されます。

トランザクション NTFS の最も優れた特徴の 1 つは、他の多くのトランザクション テクノロジと連携できるということです。TxF は、新しいカーネル トランザクション マネージャ (KTM) の機能を使用します。この新しい KTM は、Microsoft® 分散トランザクション コーディネータ (DTC) と直接連携できるため、DTC をトランザクション コーディネータとして使用できるテクノロジは、1 つのトランザクション内でトランザクション ファイル操作を使用できます。つまり、トランザクション ファイル操作は、SQL 操作、WS-AtomicTransaction を経由した Web サービス呼び出し、OleTransactionProtocol を経由した Windows Communication Foundation サービス、さらにはトランザクション MSMQ 操作と同じトランザクション内に参加させることもできるということです。また、DTC は XA-Transactions とも連携できるため、XA-Transactions を使用する他のテクノロジにトランザクション ファイル操作を直接参加させることもできます。簡単に言えば、DTC と連携できるすべてのテクノロジに、トランザクション ファイル操作を組み込むことができるということです。

ドキュメント管理の例の場合、TxF を使用して、ドキュメント管理アプリケーション内の一貫性をより簡単に維持するにはどうすればよいでしょうか? ここで DTC が役立ちます。SQL データベースとファイル ストアとの間の一貫性が確実に維持されるようにするには、トランザクションを開始し、その同一のトランザクション内で SQL ステートメントとファイル操作を実行した後、完了したトランザクションを、その結果に基づいてコミットまたはロールバックします。SQL 呼び出しが失敗した場合、ファイルは書き込まれません。また、ファイル システム呼び出しが失敗した場合、SQL はロールバックされます。すべてのデータの一貫性は損なわれず、この一貫性を維持する処理も、すべてプラットフォームによって自動的に行われます。これは、すべての操作が 1 つのトランザクションに参加しているからです。

この方法を、独自のアプリケーションで使用すれば、発生する可能性があるすべての例外を把握したり、その処理を考えたりする必要がなくなります。単に同一のトランザクション内に両方の操作を組み込めば、例外がスローされたときに、プラットフォームによって問題の操作がロールバックされます。コードの量が減り、さらにアプリケーションも強化できます。すばらしいですね。

残る目的は 2 つあります。プラットフォームの安定性は、マイクロソフト テクノロジの中で TxF を使用することによって実現されます。Windows Vista と Windows Server "Longhorn" では、3 つの主要な機能にトランザクション NTFS が使用されます。これらは、Windows Update、システムの復元、およびタスク スケジューラです。これらの機能は TxF を使用して、1 つのトランザクション スコープ内でファイル システムにファイルを書き込むため、停電によるシステムの再起動などの例外が発生した場合に、ロールバックやコミットを実行できます。Windows Update の実行中に停電を経験したことがある皆さんは、これによってシステムが停止し、新規インストールが必要になる場合があることをご存知でしょう。しかし、Windows Update で TxF が使用されるようになったことにより、Windows Vista システム上で同じ状況が発生しても、システムの再起動時にトランザクションをロールバックすれば、コンピュータを復旧させることができます。マイクロソフト テクノロジの中で TxF を使用することにより、マイクロソフトのオペレーティング システムの安定性は、さらに向上しました。

最後に、TxF は、SQL 呼び出し以外の操作でトランザクションを使用するためのフレームワークを提供することによって、革新性を高めます。最終的には、トランザクション NTFS によって、アプリケーションの記述方法が根本的に変化するため、開発者がより強力なコードを作成できるようになります。トランザクションを設計に組み込むことによって、発生する可能性がある障害をすべて把握する必要なく、コードを記述できるようになります。そういった面白味のないことには、オペレーティング システムが対処してくれます。

トランザクション NTFS を使用しないほうがよい状況

TxF には多くの長所がありますが、TxF を使用しないほうがよいシナリオもあります。これらのシナリオは、従来のデータベース トランザクション システムを使用しないほうがよいシナリオと同じです。

トランザクションの実行時間が長いシステムがある場合は、TxF を使用しないことをお勧めします。ここでの "長い" という間隔は、相対的に決定されます。実行時間が長いトランザクションとは、同一のログ内に記録された他の多くのトランザクションよりも長い時間実行されたトランザクションのことを指します。つまり、何千ものトランザクションが、それぞれ開始してから非常に短い時間で完了する場合は、実行時間が数秒のトランザクションでも、実行時間が長いと言えます。一方、システムで実行されるトランザクションの数が非常に少ない場合は、1 日や 2 日でも実行時間が長いとは言えません。

同一のファイルに対して複数のライタを使用する場合も、注意して TxF を使用する必要があります。トランザクション ファイル操作で同時に使用できるライタ (共有ライタも含む) は 1 つのみです。つまり、あるライタがファイルを開いている場合、別のライタは、そのライタと同一のトランザクションに参加していても、既に開かれているファイルを開くことはできません。このため、1 つのファイルに対して複数のライタを使用するシステムで TxF を使用すると、競合が発生する場合があります。

最後に、自動的に開始されるサービスと、混在したトランザクション リソースに注意する必要があります。サービスがトランザクション NTFS と SQL を使用している場合、このサービスがアクセスする必要があるファイルは、復旧が完了するまでロックされたままになります (このサービスは DTC と SQL に直接依存しているため、これらのサービスを最初に開始する必要があります。このため、DTC と SQL の復旧が完了するまで、ファイルを完全に復旧できない場合があります)。

TxF を使用する

では、TxF の使用方法について見ていきましょう。既存のファイル I/O Win32® API の知識がある場合、トランザクション NTFS API は簡単に使用することができます。新しいトランザクション NTFS API は、トランザクション用の特別なパラメータがいくつか付加されたという点以外は、非トランザクション API と同じです。図 1 は、Win32 ファイル API と、対応するトランザクション API の例を示しています。一覧が非常に大きくなるため、すべては紹介しませんが、Win32 のほとんどすべてのファイル API には、対応するトランザクション API があります。

Figure 1 Win32 API とトランザクション API の対応関係 (Close Figure 1)

非トランザクション API | トランザクション API
CreateFile | CreateFileTransacted
CopyFileEx | CopyFileTransacted
MoveFileWithProgress | MoveFileTransacted
DeleteFile | DeleteFileTransacted
CreateHardLink | CreateHardLinkTransacted
CreateSymbolicLink | CreateSymbolicLinkTransacted
CreateDirectoryEx | CreateDirectoryTransacted
RemoveDirectory | RemoveDirectoryTransacted

TxF 対応のアプリケーションを開発する場合、アプリケーションの観点からは、主に 3 つの機能を使用できます。1 つ目は、新しい KTM を直接トランザクション コーディネータとして使用する方法です。この方法を使用すると、トランザクション内で TxF ファイル操作のみを使用するアプリケーションを開発できます。

2 つ目は、DTC をトランザクション コーディネータとして使用する方法です。この方法を使用する場合、DTC を使用してカーネル レベルのトランザクション オブジェクトのハンドルを取得し、トランザクション API に渡す必要があります。

3 つ目は、2 つ目の方法を拡張したもので、マネージ コードから System.Transactions を使用することによって、DTC (単一の永続的な参加のみの場合は Lightweight Transaction Manager) を使用する方法です。この後すぐ、この方法の問題点と、そのことがマネージ開発者に与える影響について説明します。

では、これら 3 つの方法について詳しく見ていきましょう。図 2 は、Windows Vista と Windows Server "Longhorn" のトランザクション プラットフォームにおける主な可動部分をすべて示しています。

図 2 トランザクション プラットフォーム
図 2 トランザクション プラットフォーム(画像を縮小するには、ここをクリックします)
図 2 トランザクション プラットフォーム
図 2 トランザクション プラットフォーム (画像を拡大するには、ここをクリックします)

API レベルでは、主に 3 つの機能を介して TxF を使用できます。1 つ目は、新しい ktmw32.h ヘッダー ファイルから直接 KTM を使用することにより、TxF を使用する方法です。最も基本的なレベルでは、KTM をトランザクション コーディネータとして使用する Win32 呼び出しの CreateTransaction、CommitTransaction、RollbackTransaction などがあります。この方法では、これらの呼び出しを直接使用することにより、KTM をトランザクション コーディネータとして使用します (図 3 を参照)。この方法は、1 つのトランザクション内のみでトランザクション ファイル操作を実行する場合に使用します。

Figure 3 ネイティブ コードから KTM を使用することによって TxF を使用する例 (Close Figure 3)
hTx = CreateTransaction(NULL, NULL, 0, 0, 0, 0, L”MyTxn”);
hFile = CreateFileTransacted(myFileName,
            GENERIC_READ | GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL,
            hTx, NULL, NULL);

... // work with the file using its file handle hFile

CommitTransaction(hTx);

2 つ目は、DTC を使用することによって、TxF を使用する方法です。この方法では、DTC をトランザクション コーディネータとして使用します。DTC を介して TxF を使用するには、COM と QueryInterface を使用して、DTC の ITransaction インターフェイスから IKernelTransaction インターフェイスを直接取得します。新しい IKernelTransaction COM インターフェイスへの参照を取得したら、このインターフェイスの GetHandle メソッドを呼び出して、カーネル レベルのトランザクション ハンドルを取得し、さまざまな Win32 トランザクション API に渡すことができます (図 4 を参照)。

Figure 4 ネイティブ コードから DTC を使用することによって TxF を使用する例 (Close Figure 4)
hr = DtcGetTransactionManagerEx( NULL, NULL,
         IID_ITransactionDispenser, OLE_TM_FLAG_NONE,
         NULL, (void**) &pITransactionDispenser);
hr = pITransactionDispenser->BeginTransaction(
         NULL, ISOLATIONLEVEL_READCOMMITTED,
         ISOFLAG_RETAIN_BOTH, NULL, &pITransaction);
hr = pITransaction->QueryInterface(
         IID_IKernelTransaction,
         (void**) &pKernelTransaction);
hr = pKernelTransaction->GetHandle(&hTransactionHandle);
hAppend = CreateFileTransacted(TEXT(“test.txt”),
              FILE_APPEND_DATA, FILE_SHARE_READ,
              NULL, OPEN_ALWAYS,
              FILE_ATTRIBUTE_NORMAL, NULL,
              hTransactionHandle,    NULL, NULL);

... // work with the file using its file handle hAppend

hr = pITransaction->Commit(FALSE, XACTTC_SYNC_PHASEONE, 0);

マネージ開発者にとって、この方法は優れているとは言えません。現在、マネージ コードから TxF を使用するには、P/Invoke と COM 相互運用機能を使用する必要があります。しかし、心配しないでください。多くの場合、P/Invoke と COM 相互運用機能を使用したコードは、それほど悪い選択肢ではありません。まだ心配な場合は、MSDN® Magazine Web サイトのサンプル コードを利用できます。このサンプルでは、独自のアプリケーションで使用できる、トランザクション NTFS の単純なマネージ ラッパーが提供されます (このサンプル コードを試すと共に、スクリーンキャストも参照してください。このスクリーンキャストでは、単純に KTM と共に TxF を使用したり、SQL や Windows Communication Foundation と共に TxF を使用したりする方法が紹介されます)。短期的な情報としては、このラッパーのより強力なバージョンが、Windows SDK の一部として近日リリースされる、Windows VistaBridge サンプルに含まれます。長期的な情報としては、製品チームが、これらの API を直接 Microsoft .NET Framework に統合するという計画に取り組んでいます。

マネージ コードから TxF を使用する場合も、System.Transactions 名前空間の機能を使用できます。TxF まで辿り着くには、基本的に、ネイティブ コードで DTC を使用するときに記述したコード (図 4 を参照) を、対応するマネージ コードに置き換えていきます。オブジェクト レベルでは、図 5 のようになります。

図 5 マネージ コードからの TxF の使用
図 5 マネージ コードからの TxF の使用

まず、現在のトランザクションから IDtcTransaction COM インターフェイスを取得します (これは、System.Transactions の TransactionInterop ヘルパ クラスを使用して行います)。IDtcTransaction を取得したら、このインターフェイスを IKernelTransaction COM インターフェイスにキャストできます。これにより、内部では IKernelTransaction COM インターフェイスに対して QueryInterface が呼び出されます。現在実行されているカーネル レベルのトランザクションへの参照を IKernelTransaction によって取得したら、このインターフェイスの GetHandle を呼び出してハンドルを取得し、さまざまな新しいトランザクション Win32 API に渡すことができます。たとえば、このトランザクション ハンドルを CreateFileTransacted に渡して、新しいファイルの SafeFileHandle を取得できます。取得した SafeFileHandle は、その後ファイルへの書き込みに使用する StreamWriter クラスのコンストラクタに渡すことができます (図 6 を参照)。

Figure 6 マネージ コードから System.Transactions を使用することによって TxF を使用する例 (Close Figure 6)

NativeMethods.cs
    // IKernelTransaction COM Interface
[Guid(&quot;79427A2B-F895-40e0-BE79-B57DC82ED231&quot;)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IKernelTransaction
{
    int GetHandle(out IntPtr pHandle);
}

[DllImport(KERNEL32,
   EntryPoint = &quot;CreateFileTransacted&quot;,
   CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern SafeFileHandle CreateFileTransacted(
   [In] string lpFileName,
   [In] NativeMethods.FileAccess dwDesiredAccess,
   [In] NativeMethods.FileShare dwShareMode,
   [In] IntPtr lpSecurityAttributes,
   [In] NativeMethods.FileMode dwCreationDisposition,
   [In] int dwFlagsAndAttributes,
   [In] IntPtr hTemplateFile,
   [In] KtmTransactionHandle hTransaction,
   [In] IntPtr pusMiniVersion,
   [In] IntPtr pExtendedParameter);
...

Program.cs
...
using (TransactionScope scope = new TransactionScope())
{
   // Grab Kernel level transaction handle
   IDtcTransaction dtcTransaction =
      TransactionInterop.GetDtcTransaction(managedTransaction);
   IKernelTransaction ktmInterface = (IKernelTransaction)dtcTransaction;

   IntPtr ktmTxHandle;
   ktmInterface.GetHandle(out ktmTxHandle);

   // Grab transacted file handle
   SafeFileHandle hFile = NativeMethods.CreateFileTransacted(
      path, internalAccess, internalShare, IntPtr.Zero,
      internalMode, 0, IntPtr.Zero, ktmTxHandle,
      IntPtr.Zero, IntPtr.Zero);

   ... // Work with file (e.g. passing hFile to StreamWriter constructor)

   // Close handles
}

TxF では、コミットされた読み取りによって一貫性が維持されることも知っておく必要があります。この機能は、トランザクション内で実行するファイルの書き込みと読み取りに影響を与えます。あるファイルへの書き込みを実行する場合、そのファイルに対して同時に書き込みを実行できるトランザクション (ライタではなくトランザクションの話です、注意してください) の数は 1 つだけです。非トランザクション ライタは、共有書き込みアクセスで開かれた場合でも、トランザクション ライタによってブロックされます。また、あるファイルに対して非トランザクション ライタが開かれた場合、そのファイルに対してトランザクション リーダーやトランザクション ライタを開くことはできません。

あるファイルの読み取りをトランザクション内で実行する場合、取得したファイル ハンドルが有効な間は、そのファイルの表示は分離されます。この分離されたファイル表示は、ファイル ハンドルが作成されたときの最新のファイルの内容に基づいています。あるトランザクション ライタがファイルを変更している場合、それらの変更は、実際にコミットされるまで、新しいトランザクション リーダーによって読み取られることはありません。また、トランザクション NTFS では、コミットされた読み取りによって一貫性が維持されるため、あるトランザクション リーダーの分離された表示は、トランザクション ライタがファイルに変更を加え、それらの変更をコミットしても、そのトランザクション リーダーが有効な間は変更されません。これらのトランザクション リーダーは、取得したファイル ハンドルが開いている間のみ、非トランザクション ライタをブロックします。また、トランザクションが有効な間は、そのファイルへの変更をブロックしません。トランザクションが有効な間、同一の表示を維持する必要がある場合は、トランザクション リーダーのファイル ハンドルを開いたままにしておく必要があります。

最後に、開発者は、トランザクション ファイル操作のファイル ハンドルを使用する場合、トランザクション ファイル操作と非トランザクション ファイル操作との間に、ファイル ハンドルに関する大きな違いがあることを知っておく必要があります。いずれかの新しいトランザクション API からファイル ハンドルを取得した場合、そのファイル ハンドルは、トランザクションのコミットやロールバックが行われた後など、いつでも無効になる可能性があります。

セカンダリ リソース マネージャ

アプリケーションは、セカンダリ リソース マネージャを使用することによって、トランザクション内で実行している独自のリソース (特に独自のトランザクション ログ) を管理できるだけでなく、そのアプリケーションに固有のディレクトリ (およびすべてのサブディレクトリ) 内で発生するファイル操作も管理できるようになります。

この記事でセカンダリ リソース マネージャについて説明するのには、きちんとした理由があります。TxF が開発されたとき、重要視されたのは、一貫性よりも可用性でした。現在は、トランザクション内で、起動に必要なファイルを変更できるようになったため、オペレーティング システムは、これらのファイルを常に使用可能な状態にしておく必要があります。これらのファイルを常に使用可能な状態にするには、システム ドライブのトランザクション マネージャをトランザクション コーディネータとして使用する必要があります。このため、DTC など、他のトランザクション コーディネータを、これらのファイルの "ルート" トランザクション コーディネータまたは優先的なトランザクション コーディネータとして使用することはできません。したがって、DTC を他のリソース マネージャ (SQL など) と共に使用する必要があるトランザクションは、システム ドライブに TxF を使用できません。つまり、SQL Server™ などに対する操作と同一のトランザクション内で、システム ドライブに対してトランザクション NTFS を使用する場合は、他の方法を使用する必要があります。

この制限を回避するための最も簡単な方法は、すべてのデータを別のパーティションに配置することです。データをシステム パーティション以外のパーティションに配置できない場合、または配置できない可能性がある場合は、セカンダリ リソース マネージャを使用します。セカンダリ リソース マネージャを使用して、特定のディレクトリ (およびすべてのサブディレクトリ) のトランザクション ファイル操作を管理する場合、KTM には、そのディレクトリ内にあるすべてのファイルのリソース管理が、そのセカンダリ リソース マネージャを使用するアプリケーションによって制御されることが通知されます。これにより、システム ボリューム上の既定のリソース マネージャが引き続き使用可能な状態になるため、KTM 以外のトランザクション コーディネータを、優先的なトランザクション コーディネータとして使用できるようになります。

セカンダリ リソース マネージャは、コマンド ライン上で、FSUtil ツールと図 7 のようなコマンドを使用することによって、作成および管理できます。ただし、セカンダリ リソース マネージャを使用する必要があるアプリケーションの場合は、プログラムからセカンダリ リソース マネージャを制御することをお勧めします。これを行うには、DeviceIoControl Win32 API と、図 8 に記載されている特定の dwIoControlCode を使用します。

Figure 8 プログラムからのセカンダリ リソース マネージャの管理 (Close Figure 8)

制御コード | 目的
FSCTL_TXFS_LIST_TRANSACTIONS | 指定したリソース マネージャに現在関連しているすべてのトランザクションを列挙します。
FSCTL_TXFS_LIST_TRANSACTION_LOCKED_FILES | 指定したトランザクションによって現在ロックされているすべてのファイルを列挙します。
FSCTL_TXFS_GET_METADATA_INFO | ファイルのトランザクション NTFS メタデータと、ファイルをロックしているトランザクションの ID を取得します。
Figure 7 コマンド ラインからのセカンダリ リソース マネージャの管理 (Close Figure 7)

コマンド | 目的
fsutil resource create | セカンダリ トランザクション リソース マネージャを作成します。
fsutil resource info | トランザクション リソース マネージャに関する情報を表示します。
fsutil resource setautoreset | トランザクション リソース マネージャが次回の起動時にトランザクション メタデータを消去するかどうかを設定します。
fsutil resource setlog | 実行中のトランザクション リソース マネージャの特性を変更します。
fsutil resource start | トランザクション リソース マネージャを起動します。
fsutil resource stop | トランザクション リソース マネージャを停止します。

セカンダリ リソース マネージャを使用する場合、リソース マネージャの起動と復旧は、アプリケーションで行う必要があります。リソース マネージャを起動する操作によって、トランザクションを管理するための KTM オブジェクトが作成され、ローカル ディレクトリに格納されている共通ログ ファイル システム (CLFS) ログとメタデータが開かれます。また、リソース マネージャを復旧する操作によって、セカンダリ リソース マネージャ内のすべてのファイルが、一貫性がある状態に復旧されます。セカンダリ リソース マネージャは、セカンダリ リソース マネージャのルートに対する最後のハンドルが無効になったときに停止します。このため、セカンダリ リソース マネージャ内のファイルは、システムが起動したときに、自動的に一貫性がある状態にはなりません。この一貫性の復旧は、アプリケーションでセカンダリ リソース マネージャを起動および復旧したときに実行されます。また、ボリューム シャドウ コピー サービス (VSS) を使用するソフトウェアでシステムをバックアップしても、VSS ライタを提供しない限り、ファイルは自動的に一貫性がある状態にはなりません。もちろん、一貫性がないセカンダリ リソース マネージャを復元してから起動および復旧した場合、ファイルは一貫性がある状態になります。使用しているバックアップ ソフトウェアで、dwIoControlCode の FSCTL_TXFS_READ_BACKUP_INFORMATION と FSCTL_TXFS_WRITE_BACKUP_INFORMATION を使用して TxF メタデータをバックアップしているかどうかも確認してください。

既に説明したとおり、セカンダリ リソース マネージャは、トランザクションに参加しているファイルがシステム ドライブ上にあるときに、KTM 以外のテクノロジを優先的なトランザクション コーディネータとして使用する必要がある場合や、リソース マネージャの一貫性と可用性を制御する必要がある場合のみ必要です。このため、アプリケーション内でリソース マネージャの動作をさらに直接的に制御する必要がある場合は、セカンダリ リソース マネージャが非常に役立ちます。操作するファイルがシステム ドライブ以外のドライブ上にある場合、セカンダリ リソース マネージャを使用する必要はありません。現在私たちは、Windows Server "Longhorn" におけるこの制限を緩和し、この要件が適用される対象を、システム ドライブの中で Windows の起動にどうしても必要な部分のみに抑え、システム ドライブ全体に及ばないようにすることを目指しています。

管理

運用環境で使用するどんなアプリケーションに関しても、それらの管理方法を検討することは重要です。このため、TxF 自体の監視方法を知っておく必要があります。図 9 は、この作業を実行するために DeviceIoControl と共に使用できる制御コードを示しています。

上記でリソース マネージャに関する情報を入手した場合と同様に、FSUtil コマンド ライン ツールを使用すると、これらの制御コードに対応する情報を入手できます。これらのコマンドを次に示します。

  • fsutil transaction list
  • fsutil transaction fileinfo
  • fsutil transaction query

トランザクション コマンド ラインの変更点

Windows Vista の初期のベータ版をテストしたことがある皆さんは、コマンド ラインからトランザクション コマンドを呼び出すことができた記憶があるでしょう。管理者は、この機能を使用して、コマンド ラインからトランザクションを開始し、アプリケーションを実行して、それ以降にアプリケーション内で実行するファイル操作を、暗黙的にすべてトランザクション ファイル操作として実行できました。

しかし、Windows Vista でのアプリケーションの互換性をテストした段階で、この暗黙的なモデルでは、COM+ 開発者やマネージ開発者が安全に使用することは難しいことがわかりました。そのため、この混乱を回避するために、選択的かつ明示的なモデルに変更しました。この変更の欠点は、アプリケーション内で TxF を使用する必要がある場合、コードを変更して新しい API を呼び出す必要があるということです。

まとめ

トランザクション NTFS は、非常に革新的なテクノロジです。開発者は、トランザクション NTFS を使用することにより、さらに強力なプラットフォームで、信頼性の高いアプリケーションを作成することができます。また、アトミックなファイル操作を実行してファイル データの一貫性を維持したり、テストするエラー状態の数を減らしてテストの負担を軽減したり、ACID トランザクション セマンティクスによる分離を提供してマルチ ユーザー環境をサポートしたりすることができます。Windows Vista と Windows Server "Longhorn" を対象としたアプリケーションを開発する際は、TxF の使用を検討することをお勧めします。詳細については、「トランザクションの関連資料」を参照してください。

この記事の執筆に力を貸してくれた、Dana Groff、Christian Allred、および Jon Cargille に感謝します。

トランザクションの関連資料

トランザクション NTFS の関連資料を、次の一覧に示します。新しい資料が公開されたら、随時この一覧を更新します。私のブログにも、この更新情報を掲載します。

Web サイト

  • トランザクション プログラミング フォーラム
  • Windows のトランザクション管理
  • トランザクション NTFS (TxF)

ブログ

  • Jim Johnson
  • Florin Lazar
  • Jason Olson

ブログの記事

  • MSDTC のプロキシ拡張 (MLTM)
  • System.Transactions と TxF の統合
  • TxF の紹介、TxF に注目する理由

メディア

  • トランザクションに対応した Vista: KTM とその関連機能
  • Vista のトランザクション ファイル システム
  • 革新的な Vista の TxF インフラストラクチャ
  • トランザクション NTFS を使用した Windows Vista 向けのアプリケーションの開発
  • 開発者向けのサーバー技術、第 1 回 - トランザクション NTFS
  • 開発者向けのサーバー技術、第 2 回 - トランザクション NTFS と WCF

Jason Olsonは、マイクロソフトの開発者およびプラットフォーム エバンジェリズム部門で、Windows Server "Longhorn" に関する業務に従事するテクニカル エバンジェリストです。プライベートでは、家庭を大事にする優しい父親であり、ゲーム開発愛好家でもあります。また、ブログ サイト (managed-world.com) での執筆も行っています。Jason の連絡先は jason.olson@microsoft.com (英語のみ) です。


セキュリティ: Windows Vista の CNG API を使用して暗号化を行う

セキュリティ

Windows Vista の CNG API を使用して暗号化を行う

Kenny Kerr
この記事の内容 : :
  • CryptoAPI と CNG の違い
  • アルゴリズム プロバイダ
  • 乱数、暗号化、署名、および検証
  • .NET アプリケーションとの相互運用
この記事は次のテクノロジを使用しています:
Windows Vista
コードのダウンロード : : CNG2007_07.exe (158KB)
コンテンツ
アルゴリズム プロバイダ
乱数の生成
ハッシュ関数
対称暗号化
非対称暗号化
署名と検証
.NET との相互運用
今後の展望

Windows Vista™ では、これまでの CryptoAPI に代わる新しい暗号化 API が導入されます。CryptoAPI は、初期バージョンの Windows® NT や Windows 95 を起源とする暗号化 API です。Cryptography Next Generation (CNG) は、CryptoAPI を今後長期にわたって置き換え、CryptoAPI が提供してきたすべての暗号化プリミティブの代用となることを目的としています。CNG は、CryptoAPI が提供するアルゴリズムをすべてサポートしますが、これをさらに進化させ、多数の新しいアルゴリズムや柔軟性に優れたデザインが導入されています。そのため、開発者は、暗号化操作を実行する方法や、アルゴリズムを組み合わせてさまざまな操作を実行する方法をこれまでより細かく制御できるようになります。

図 1 は、CNG のデザイン概要を示しています。この記事は BCrypt を中心に説明しています。BCrypt とは、暗号化プリミティブ (乱数の生成、ハッシュ関数、署名、暗号化キーなど) を提供する CNG のサブセットです。それに対して、NCrypt とは、非対称キーの保存をサポートするキー記憶域機能やスマートカードなどのハードウェアを提供するサブセットです。BCrypt や NCrypt の名前をあまり深く考えないでください。BCrypt は、単に、CNG の基本サービスを提供するヘッダー ファイルと DLL の名前です。この場合 "B" は "base" (基本) を表します。同様に、NCrypt は、単に、高レベルのキー記憶域機能を提供するヘッダー ファイルと DLL の名前です。"N" は "new" (新規) を表します。

図 1 CNG のアーキテクチャ
図 1 CNG のアーキテクチャ

BCrypt が提供する暗号化プリミティブはカーネル モードで直接使用できるため、ユーザー モードとカーネル モードのアプリケーションの両方に提供される初めての共通暗号化フレームワークです。ただし、NCrypt が提供するキー記憶域機能は、ユーザー モードのアプリケーションでしか使用できません。

CNG が提供する暗号化プリミティブには、主に 2 つの考え方があります。1 つは一連の論理オブジェクトという考え方です。つまり、呼び出し可能なメソッドと、照会可能な (場合によっては変更可能な) プロパティを備えたオブジェクトです。このようなオブジェクトには、アルゴリズム プロバイダ、ハッシュ関数、キー、秘密協定があります。乱数ジェネレータ、署名、およびさまざまな種類のキーはどこに含まれるのでしょう。結局のところ、乱数生成はアルゴリズム プロバイダによって直接処理され、他のほとんどはキーによって提供されます。キーは対称キーまたは非対称キーとして表現でき、ハッシュ署名の署名や検証に使用されます。ハッシュ オブジェクトとキー オブジェクトは、アルゴリズム プロバイダから派生され、秘密協定は、キー オブジェクトのペアから派生されます。キー オブジェクトは、セキュリティが確保された通信を目的として、あるプリンシパルでは公開キーとなり、別のプリンシパルでは秘密キーとなります。

CNG のもう 1 つの考え方は、暗号化操作用のソフトウェア ルーターまたは中間層としての考え方です。CNG API は、一連の論理暗号化インターフェイスを基に構築されています。たとえば、ハッシュ インターフェイスを使用すれば、特定のハッシュ アルゴリズムの実装をハードコーディングする必要がなくなります。ほとんどの暗号化 API はアルゴリズム主体のアプローチを使用しますが、CNG はインターフェイス主体のアプローチを採用しています。このアプローチにより、出荷後にアプリケーションで使用しているアルゴリズムの不備に気付き、交換の必要がある場合、開発者もアプリケーション管理者も対応の敏捷性が向上します。図 2 に、このインターフェイス主体の CNG の概要を示します。

図 2 CNG インターフェイス
図 2 CNG インターフェイス(画像を縮小するには、ここをクリックします)
図 2 CNG インターフェイス
図 2 CNG インターフェイス (画像を拡大するには、ここをクリックします)

CNG 構成システムについては、別途記事を用意すべきですが、現状では、CNG を使用すれば、開発者はアルゴリズム プロバイダを指定する必要なくアルゴリズムを要求できることを知っておけば十分です。アルゴリズムの実装はプロバイダが担当します。別のアルゴリズム実装を使用する場合、CNG のこうした側面により、管理者がアプリケーションで宣言するか、システム ポリシーを使用して、アプリケーションを再構成するだけです。

アルゴリズム プロバイダ

概要はこれぐらいにして、CNG を使用して一般的なさまざまな暗号化操作を行う方法を見ていきましょう。最初に必要なのがアルゴリズム プロバイダです。BCrypt で定義されているすべての CNG オブジェクトは、BCRYPT_HANDLE で特定されます。アルゴリズム プロバイダも例外ではありません。BCryptOpenAlgorithmProvider 関数は、選択したアルゴリズムとオプションの実装に基づいてアルゴリズム プロバイダを読み込み、その後 CNG 関数の呼び出し時に使用するハンドルを返します。また、BCrypt は、ユーザー モードとカーネル モードのどちらでコードを記述しているかに関係なく、Windows Driver Kit (WDK) の NTSTATUS 型を使用して、エラー情報を示します。アルゴリズム プロバイダをメモリに読み込む方法を次に示します。

BCRYPT_HANDLE algorithmProvider = 0;

NTSTATUS status = ::BCryptOpenAlgorithmProvider(
                      &algorithmProvider, algorithmName,
                      implementation, flags);

if (NT_SUCCESS(status))
{
    // Use algorithm provider
}

ほとんどの場合、implementation パラメータと flags パラメータの両方に 0 を渡します。implementation に 0 を渡すと、algorithmName パラメータで指定される特定のアルゴリズム用に既定のアルゴリズム プロバイダが読み込まれます。これまでに WDK を使用したことがあれば、NT_SUCCESS マクロには馴染みがあるでしょう。これは、COM 開発者が使用する SUCCEEDED に似たマクロで、状態値が成功または失敗のどちらを表すかを示します。

アルゴリズム プロバイダは終了時にアンロードする必要があります。これには、次に示すように、BCryptOpenAlgorithmProvider から返されたハンドルを BCryptCloseAlgorithmProvider 関数に渡します。

  status = ::BCryptCloseAlgorithmProvider(
    algorithmProvider, flags);
  ASSERT(NT_SUCCESS(status));

現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。当然、アサーションはなくてもかまいませんが、アプリケーションのバグを除去する際に役立ちます。

この記事の残りの部分では、NT_VERIFY マクロを使用して CNG 関数の結果を検証します。このマクロは、返された状態コードを確認する必要があることを忘れないようにする単なるプレースホルダです。つまり、次のようにマクロを定義できます。

#define NT_VERIFY(x) ASSERT(NT_SUCCESS(x))

運用環境で実行するコードでは、必ず、プロジェクトのエラー処理方針に従ったエラー処理を使用してください。

アルゴリズム プロバイダの読み込み操作は負荷が高いため、アプリケーションでのプロバイダの使用パターンを検討し、適度に再利用することをお勧めします。次に、アルゴリズム プロバイダを使用してさまざまな目標を実現します。しっかりついてきてください。

乱数の生成

乱数の生成は、BCryptGenRandom 関数によって行われます。この処理ではバッファ全体に乱数を設定できるため、ランダム バッファ生成と呼ばれることもあります。たとえば、10 個の乱数を生成する必要があるとします。図 3 にその方法を示します。

Figure 3 乱数 (Close Figure 3)
BCRYPT_HANDLE algorithmProvider = 0;

NT_VERIFY(::BCryptOpenAlgorithmProvider(
    &algorithmProvider,
    BCRYPT_RNG_ALGORITHM,
    0,                     // implementation,
    0));                   // flags

for (int i = 0; i < 10; ++i)
{
    UINT random = 0;

    NT_VERIFY(::BCryptGenRandom(
        algorithmProvider,
        reinterpret_cast<PUCHAR>(&random),
        sizeof(UINT),
        0));

    cout << random << endl;
}

この例では、BCRYPT_RNG_ALGORITHM アルゴリズム識別子を指定して、既定の乱数生成アルゴリズムを要求しています。デジタル署名アルゴリズム (DSA) に使用する Federal Information Processing Standards (FIPS) 準拠のアルゴリズムが必要であれば、BCRYPT_RNG_FIPS186_DSA_ALGORITHM アルゴリズム識別子も使用できます。

図 3 の例からわかるように、BCryptGenRandom 関数には、実用性の高い void (任意の型) のポインタではなく、バッファを特定する UCHAR へのポインタが必要です。さいわい、C++ のおかげで、タイプセーフな方法でこれを簡単に解決できます。次の関数テンプレートを見てください。

template <typename T>
__checkReturn
NTSTATUS GenRandom(BCRYPT_HANDLE algorithmProvider,
                   T& buffer, ULONG flags)
{
    ASSERT(0 != algorithmProvider);

    return ::BCryptGenRandom(algorithmProvider,
                             reinterpret_cast<PUCHAR>(&buffer),
                             sizeof(T), flags);
}

Standard Annotation Language (SAL) に馴染みのない読者のために説明すると、__checkReturn マクロは、関数の呼び出し側が関数の結果をチェックすることを示す、分析ツールへのヒントにすぎません。

この関数テンプレートは、あらゆる場合に適しているとは限りませんが (コレクション クラス内にラップされているバッファに設定する場合など)、キャストが関数テンプレート内に分離されるため、ほとんどの場合、乱数の生成をさらに実行しやすくします。また、テンプレートでは、バッファのサイズも自動的に決まります。10 個のランダムな GUID 値を生成する次の例を考えてみます。

for (int i = 0; i < 10; ++i)
{
    GUID random = { 0 };

    NT_VERIFY(GenRandom(algorithmProvider, random, 0));

    // Use ‘random’ value
}

BCryptGenRandom 関数は、オプションのフラグをサポートします。このフラグにより、乱数生成アルゴリズムにエントロピーを追加できます。BCRYPT_RNG_USE_ENTROPY_IN_BUFFER フラグは、関数に渡されたバッファ内の値をアルゴリズムで使用する必要があることを示します。この値は、乱数を計算し、後で同じバッファに返す際に追加のエントロピーとして使用されます。次に例を示します。

UINT random = 0;

for (int i = 0; i < 10; ++i)
{
    NT_VERIFY(GenRandom(algorithmProvider, random,
                        BCRYPT_RNG_USE_ENTROPY_IN_BUFFER));

    // Use ‘random’ value
}

乱数変数の定義をループの外側に移動したため、前の乱数値が次に生成される数値の追加のエントロピーになることに注意してください。

ハッシュ関数

乱数の生成と同様に、現代のコンピューティングの多くの側面でセキュリティ対策や機能を実現する際に、ハッシュ関数が重要な役割を果たします。ハッシュ関数は CNG ではオブジェクトとして公開されますが、API はカーネル モードのコード (大部分は C で記述されています) からアクセスできる必要があるため、オブジェクトを戻すにはあらかじめ作業が必要です。最初に説明したように、CNG では、アルゴリズム プロバイダとハッシュ関数がオブジェクトで表されます。これらのオブジェクトは BCrypt に含まれるため、BCryptGetProperty 関数と BCryptSetProperty 関数を使用すれば、特定のオブジェクトの名前付きプロパティの照会と設定が可能です。オブジェクトのプロパティを処理するために、NCrypt にも対応する関数が用意されています。

BCryptCreateHash 関数では、新しいハッシュ オブジェクトを作成します。ただし、その関数を呼び出す前に、ハッシュ関数の処理に使用するバッファを割り当てておく必要があります。ここで、1 つの API でユーザー モードとカーネル モードの両方をサポートすることによる悪影響を 1 つ確認できます。カーネル モードでは、メモリの割り当て場所 (特に、ページ メモリと非ページ メモリのどちらから割り当てが行われるか) に細心の注意が必要です。ユーザーは、バッファ以外に、ハッシュ オブジェクトのハンドル リソースとハッシュ テーブル リソースの管理を担当します。

ハッシュ オブジェクトの作成に必要なバッファのサイズは、アルゴリズム プロバイダのプロパティで、このために照会するプロパティの名前は BCRYPT_OBJECT_LENGTH 識別子で指定します。図 4 は、SHA-256 ハッシュ アルゴリズム用アルゴリズム プロバイダを読み込み、ハッシュ バッファ サイズを照会します。

Figure 4 アルゴリズム プロバイダを使用する (Close Figure 4)
BCRYPT_HANDLE algorithmProvider = 0;

NT_VERIFY(::BCryptOpenAlgorithmProvider(
    &algorithmProvider,
    BCRYPT_SHA256_ALGORITHM,
    0,                      // implementation,
    0));                    // flags

ULONG hashBufferSize = 0;
ULONG bytesCopied = 0;

NT_VERIFY(::BCryptGetProperty(
    algorithmProvider,
    BCRYPT_OBJECT_LENGTH,
    reinterpret_cast<PUCHAR>(&hashBufferSize),
    sizeof(ULONG),
    &bytesCopied, 0));

ASSERT(sizeof(ULONG) == bytesCopied);

BCryptGetProperty の最初のパラメータは、照会するオブジェクトを示します。2 番目のパラメータはプロパティ名を示します。3 番目と 4 番目のパラメータは、プロパティ値が格納されるバッファとバッファのサイズを指定します。bytesCopied パラメータは、予測されるバッファ サイズが前もってわからない場合に便利です。現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。

ハッシュ バッファのサイズが決まったら、ハッシュ オブジェクト用にメモリ チャンクをコミットする必要があります。カーネル モードを以外では、このバッファの割り当て先の場所はそれほど問題になりません。また、安定してさえいれば、どのような記憶域でもほぼ自由に使用できます (つまり、ピン設定が解除されたマネージ バイト配列は使用できません)。図 5 に、単純なユーザー モード バッファ割り当てに使用できる簡単な Buffer クラスを示します。このクラスは暗号化キー オブジェクトの作成時に再度使用します。そのため、ここで何らかの Buffer クラスを用意しておくことは価値があります。

前提条件がすべてそろったら、最後に、次に示すように、BCryptCreateHash 関数を使用してハッシュ オブジェクトを作成できます。

Buffer hashBuffer;
NT_VERIFY(hashBuffer.Create(hashBufferSize));

BCRYPT_HANDLE hash = 0;

NT_VERIFY(::BCryptCreateHash(algorithmProvider, &hash,
                             hashBuffer.GetData(),
                             hashBuffer.GetSize(),
                             0,   // secret
                             0,   // secret size
                             0)); // flags

BCryptCreateHash の最初のパラメータは、ハッシュ インターフェイスを実装するアルゴリズム プロバイダを示します。2 番目のパラメータは、ハッシュ オブジェクトへのハンドルを受け取ります。3 番目と 4 番目のパラメータは、ハッシュ バッファとそのサイズを指定します。キー付きハッシュ アルゴリズムでは、5 番目と 6 番目のシークレット パラメータを使用して秘密キーを指定します。現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。ハッシュ オブジェクトの操作が完了したら、BCryptDestroyHash 関数を使用してオブジェクトを破棄し、ハッシュ オブジェクトのバッファを解放する必要があります。

ハッシュ オブジェクトの作成方法と破棄方法がわかったところで、実際に、ハッシュ オブジェクトを使って何か役立つことをしてみましょう。ハッシュ オブジェクトが作成されたら、BCryptHashData 関数を呼び出し、バッファの一方向ハッシュを実行できます。この関数を繰り返し呼び出して、追加のバッファをハッシュにまとめることができます。ハッシュ値のサイズはアルゴリズム プロバイダによって固定および決定されるため、BCryptHashData を何度呼び出しても、生成されるハッシュ値は常に同じサイズになることに注意してください。

この例では、COM IStream インターフェイスを使用してストリームから読み取ったデータをハッシュする方法を示します。

BYTE buffer[256] = { 0 };
ULONG bytesRead = 0;

while (SUCCEEDED(stream->Read(buffer, _countof(buffer),
                              &bytesRead)) && 0 < bytesRead)
{
    NT_VERIFY(::BCryptHashData(hash, buffer, bytesRead, 0));
}

BCryptHashData の最初のパラメータは、BCryptCreateHash 関数から返されたハンドルです。2 番目と 3 番目のパラメータは、ハッシュするデータを含むバッファと、そのバッファのサイズを示します。ストリームから実際に読み取ったデータのみが含まれるように、バッファのサイズに bytesRead を渡すようにしたことに注目してください。最後に、BCryptHashData では flags がまだ定義されていないため、最後のパラメータには 0 を渡す必要があります。

すべてのデータをハッシュ関数に渡せば、BCryptFinishHash 関数を呼び出して、結果のハッシュ値を取得できます。結果のハッシュ値のサイズは、使用する特定のハッシュ アルゴリズムによって異なります。このサイズがわからない場合は、BCRYPT_HASH_LENGTH 識別子を使用してハッシュ オブジェクトに照会できます。次の例では、ハッシュ値のサイズを照会して、そのバッファを作成し、最後に、BCryptFinishHash を使用してハッシュ値をバッファにコピーします。

ULONG hashValueSize = 0;
NT_VERIFY(::BCryptGetProperty(hash, BCRYPT_HASH_LENGTH, ...))

Buffer hashValue;
NT_VERIFY(hashValue.Create(hashValueSize));

NT_VERIFY(::BCryptFinishHash(hash,
                             hashValue.GetData(),
                             hashValue.GetSize(),
                             0)); // flags

このパターンは、おそらくよく知っているでしょう。BCryptFinishHash の最初のパラメータは、ハッシュ オブジェクトのハンドルです。2 番目と 3 番目のパラメータは、ハッシュ値を受け取るバッファと、そのバッファのサイズを示します。最後のパラメータは flags を示します。これは、現在存在しません。

最後に、ハッシュ オブジェクトは複製できます。これは、何らかの共通データに基づいて複数のハッシュ値を作成する場合に役立ちます。最初に、1 つのハッシュ オブジェクトを作成して共通データをハッシュした後、1 回以上そのオブジェクトを複製して、複製するオブジェクトに差分を追加することができます。ハッシュ オブジェクトが複製されると、2 つのハッシュ オブジェクトは同じ状態を保持しますが、相互に結び付きはありません。そのため、各ハッシュ オブジェクトに一意なデータを追加したり、ハッシュ値を生成したり、一方のオブジェクトに影響を与えることなくもう一方のオブジェクトを破棄したりできます。

BCryptDuplicateHash 関数が、ハッシュ オブジェクトの複製を処理します。必要なのは、複製するハッシュ オブジェクトへのハンドルと、処理に使用する新しいバッファを用意することだけです。

Buffer newHashBuffer;
NT_VERIFY(newHashBuffer.Create(hashBufferSize));
BCRYPT_HANDLE newHash = 0;

NT_VERIFY(::BCryptDuplicateHash(hash,
                                &newHash,
                                newHashBuffer.GetData(),
                                newHashBuffer.GetSize(),
                                0));

対称暗号化

対称暗号化には、対称キーとそのオブジェクト バッファの作成と準備が必要です。この点は、ハッシュ関数とほぼ同様です。おなじみの手順として、アルゴリズム プロバイダの作成、プロバイダの BCRYPT_OBJECT_LENGTH プロパティの照会によるキー オブジェクト バッファ サイズの決定、そのサイズのバッファの割り当てを実行します。キーを初期化するシークレットを指定して BCryptGenerateSymmetricKey 関数を呼び出すと、対称キーが作成されます。図 6 に、このプロセスを示します。

図 6 対称キー オブジェクトを作成する
図 6 対称キー オブジェクトを作成する

ここまでにアルゴリズム プロバイダの開始とバッファの作成については既に説明したので、先に進み、対称キーの生成に注目します。次のコードは、通常の BCryptGenerateSymmetricKey の呼び出しの例を示しています。

BCRYPT_KEY_HANDLE key = 0;

NT_VERIFY(::BCryptGenerateSymmetricKey(algorithmProvider,
                                       &key,
                                       keyBuffer.GetData(),
                                       keyBuffer.GetSize(),
                                       secret.GetData(),
                                       secret.GetSize(),
                                       0)); // flags

最初のパラメータは、対称暗号化アルゴリズムを実装するアルゴリズム プロバイダを示します。2 番目のパラメータは、キー オブジェクトへのハンドルを受け取ります。3 番目と 4 番目のパラメータは、キー バッファとそのサイズを指定します。その次の 2 つのパラメータは、送信者と受信者が共有する秘密キーを含むバッファを示します。これはバイト配列にできます。空にすることもできますが、通常は、何らかのパスワードのハッシュを指定します。現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。

キー オブジェクトの操作が完了したら、BCryptDestroyKey 関数を使用してオブジェクトを破棄し、キー オブジェクト バッファを解放する必要があります。

キー オブジェクトを作成すると、実際にデータの暗号化や暗号化解除を実行したくなってきたでしょう。では、早速取りかかりましょう。わかりやすい名前の BCryptEncrypt 関数と BCryptDecrypt 関数を使用して、対称キーと非対称キー両方のデータの暗号化と暗号化解除を行います。パディング方式と初期化ベクタ以外は、使用中のキーの種類に関係なく、関数は同じように使用します。これらの関数を初めて見ると、パラメータ リストが長くて脅威を感じるかもしれませんが、データの暗号化解除を成功させるために一致させる必要がある、一連の共通パラメータを受け取ることだけを注意してください。対称暗号化操作の送信者と受信者は、共通プロパティをいくつか共有する必要があります。両者には、同じシークレットを使用して作成されたキーと、対応するプロパティ値が必要です。また、初期化ベクタも、パディング方式も同じである必要があります。

対称暗号化は、驚くほど簡単に使用できます。このようなプロパティすべてを当事者間で共有することを検討すると、問題が発生します。確認が必要な最初の情報として、アルゴリズムに対するデータ ブロックのサイズがあります。

ULONG blockSize = 0;
NT_VERIFY(::BCryptGetProperty(key, BCRYPT_BLOCK_LENGTH, ...))

最も一般的な形式の対称アルゴリズムであるブロック暗号化アルゴリズムを使用するときは、さまざまな理由からブロックのサイズが役に立ちます。ブロック暗号化では、固定サイズのプレーンテキスト ブロックを同じサイズの暗号化テキスト ブロックに暗号化します。初期ベクタを使用する場合、このブロックのサイズは初期化ベクタのサイズも示します。

暗号化するメッセージと初期化ベクタの準備ができたら、BCryptEncrypt 関数を呼び出して、プレーンテキスト メッセージを暗号化することができます。当然、暗号化テキストを受け取るバッファのサイズを決める必要があります。これを行うには、output パラメータと output size パラメータの両方に 0 を渡して、BCryptEncrypt を呼び出すだけです。その結果、BCryptEncrypt から必要なサイズが返されます。

ULONG ciphertextSize = 0;

NT_VERIFY(::BCryptEncrypt(key,
                          message.GetData(),
                          message.GetSize(),
                          0,   // padding info
                          iv.GetData(),
                          iv.GetSize(),
                          0,   // output
                          0,   // output size,
                          &ciphertextSize,
                          0)); // flags

最初のパラメータは、暗号化に使用するキーを示します。2 番目と 3 番目のパラメータは、暗号化するメッセージを指定します。4 番目のパラメータは、非対称暗号化の追加パディング情報を指定します。対称アルゴリズムではこのパラメータが使用されないため、0 を渡す必要があります。次の 2 つのパラメータは、使用する初期化ベクタを含むバッファを示します。最後から 2 番目のパラメータは、予想される暗号化テキストのサイズを受け取り、最後のパラメータは省略可能なパラメータを受け取ります。対称アルゴリズムでは BCRYPT_BLOCK_PADDING フラグのみが使用されます。このフラグは、特定のメッセージをブロック サイズの倍数になるようにパディングすることを考える必要がない場合に便利です。

次に、暗号化テキスト バッファを作成して、BCryptEncrypt を再度呼び出すことができます。このとき、暗号化テキストを受け取るバッファを指定します。

Buffer ciphertext;
NT_VERIFY(ciphertext.Create(ciphertextSize));

NT_VERIFY(::BCryptEncrypt(key,
                          message.GetData(),
                          message.GetSize(),
                          0,   // padding info
                          iv.GetData(),
                          iv.GetSize(),
                          ciphertext.GetData(),
                          ciphertext.GetSize(),
                          &ciphertextSize,
                          0)); // flags

暗号化解除のプロセスもほぼ同じです。プレーンテキストのサイズがわからない場合は、暗号化テキストのサイズを決定するために、上記とほぼ同様に、CryptDecrypt 関数を呼び出します。最後に、暗号化テキスト、初期化ベクタ、およびプレーンテキスト バッファを指定して BCryptDecrypt を呼び出し、結果として暗号化解除されたメッセージを受け取ります。

Buffer plaintext;
NT_VERIFY(plaintext.Create(plaintextSize));

NT_VERIFY(::BCryptDecrypt(newKey,
                          ciphertext.GetData(),
                          ciphertext.GetSize(),
                          0,   // padding info
                          iv.GetData(),
                          iv.GetSize(),
                          plaintext.GetData(),
                          plaintext.GetSize(),
                          &plaintextSize,
                          0)); // flags

メッセージの暗号化と同様に、メッセージの暗号化解除時に必ず同じフラグを指定します。

非対称暗号化

非対称暗号化では、両当事者が秘密キーと公開キーを保持する公開キー アプローチを使用して、シークレットや初期化ベクタの共有に関する問題を解決します。公開キーはだれでも使用できます。公開キーで暗号化されたデータの暗号解読には秘密キーしか使用できないように、キーどうしが関連付けられています。当然、この追加機能にはコストがかかります。また、非対称暗号化は、対称暗号化に比べて、計算に相当高いコストがかかることがわかります。それにもかかわらず、通信の確立時に、当事者が安全に対称暗号化キー情報を共有するためのメカニズムを提供するということだけでも、非常に重要です。図 7 に、非対称キーの作成プロセスを示します。

図 7 非対称キーを作成する
図 7 非対称キーを作成する

BCryptGenerateKeyPair 関数を呼び出して公開キーと秘密キーのペアを生成する例を次に示します。

NT_VERIFY(::BCryptGenerateKeyPair(algorithmProvider,
                                  &key, keySize,
                                  0)); // flags

最初のパラメータは、非対称暗号化アルゴリズムを実装するアルゴリズム プロバイダを示します。2 番目のパラメータは、キー オブジェクトへのハンドルを受け取ります。3 番目のパラメータは、アルゴリズムで使用するキーのサイズを示します。この値は、バイト単位ではなくビット単位で表され、アルゴリズムによって異なります。たとえば、RSA アルゴリズムは、64 の倍数である 512 ビットから 16384 ビットまでのキー サイズをサポートします。一般に、キー サイズはアルゴリズムのパフォーマンスに直接影響しますが、もっと重要なのは、ブルート フォース攻撃を使用して暗号を解読する際のコストに影響することです。それほど重要ではありませんが、キー サイズは、アルゴリズムで使用するブロックのサイズも示します。ブロック サイズを決定するには、キーのサイズを 8 で割るだけです。

BCryptGenerateKeyPair を使用してキー ペアを生成したら、BCryptSetProperty 関数を使用して、アルゴリズム固有のさまざまなキー プロパティの設定が必要になる場合があります。ただし、キー ペアを使用する前に、次のように、BCryptFinalizeKeyPair 関数を呼び出して、キー オブジェクトの作成を終了しておく必要があります。

   NT_VERIFY(::BCryptFinalizeKeyPair(key, 0));

この時点で、さいわいにも、既に説明した BCryptEncrypt 関数と BCryptDecrypt 関数を使用して、データの暗号化と暗号化解除を行うことができます。この場合、非対称キーでは初期化ベクタが無視される点だけがやや異なります。また、パディング方式は、選択するフラグによっては少し複雑になる場合があります。暗号化されるメッセージが、ブロック サイズの倍数のサイズのバッファで指定されているとすると、パディングの必要はないため、BCRYPT_PAD_NONE フラグを指定するだけです。一方、メッセージを暗号化する場合に、最初にブロック サイズの倍数になるようにパディングする必要がないときは、いくつかのオプションがあります。最も簡単なオプションは BCRYPT_PAD_PKCS1 フラグを指定することです。これにより、アルゴリズム プロバイダは、PKCS-1 標準に基づく乱数を使用して、入力バッファがブロック サイズの倍数になるようにパディングするよう指示されます。

当然ながら、キー ペアの公開部分を共有できない場合は、非対称暗号化はほぼ役に立ちません。BCryptExportKey 関数と BCryptImportKeyPair 関数は、キーのエクスポートとインポートに使用できます。ついでながら、対称キーのエクスポートには BCryptExportKey も使用できますが、バッファをキー オブジェクトに関連付ける必要があるため、対称キーのインポートには BCryptImportKey 関数を使用する必要があります。BCryptExportKey と BCryptImportKeyPair は、公開キーと秘密キーのペアまたは公開キーのみをエクスポートする際に使用できます。たとえば、キー ペアをエクスポートして後で使用するために保存したり、公開キーを別個にエクスポートして通信相手に提供したりすることが考えられます。

図 8 のコードは、公開キーのみを含むバイナリ BLOB を作成する方法を示しています。ご覧のとおり、BCryptExportKey は、既に説明したパターンに従い、最初に BLOB のサイズを決定するために呼び出し、次に実際に公開キー情報を受け取るために再度呼び出します。この関数の 3 番目のパラメータは注目する必要があり、アルゴリズムとエクスポートされる正確な内容の両方を示します。

Figure 8 公開キー バイナリ BLOB を作成する (Close Figure 8)
ULONG publicKeyBlobSize = 0;

NT_VERIFY(::BCryptExportKey(key,
                            0,                      // reserved
                            BCRYPT_RSAPUBLIC_BLOB,
                            0,                      // output,
                            0,                      // output size
                            &publicKeyBlobSize,
                            0));                    // flags

Buffer publicKeyBlob;
NT_VERIFY(publicKeyBlob.Create(publicKeyBlobSize));

NT_VERIFY(::BCryptExportKey(key,
                            0,                      // reserved
                            BCRYPT_RSAPUBLIC_BLOB,
                            publicKeyBlob.GetData(),
                            publicKeyBlob.GetSize(),
                            &publicKeyBlobSize,
                            0));                    // flags

この例では、BCRYPT_RSAPUBLIC_BLOB を使用して、RSA キー ペアの公開キー部分のみをエクスポートする方法を示しました。公開キーと秘密キーの両方をエクスポートする場合は、代わりに BCRYPT_RSAPRIVATE_BLOB を使用するだけです。名前に惑わされないでください。秘密キーと公開キーの両方が含まれています。暗号化には公開キーのみが使用されているので、これを把握することが重要です。そのため、秘密キーをエクスポートしてからインポートした場合、メッセージを暗号化することはできますが、暗号化には秘密キーではなく、公開キーが使用されます。秘密キーは、暗号化解除のみに使用されます。

BLOB からキーをインポートするのは簡単です。

BCRYPT_HANDLE publicKey = 0;

NT_VERIFY(::BCryptImportKeyPair(algorithmProvider,
                                0,   // reserved
                                BCRYPT_RSAPUBLIC_BLOB,
                                &publicKey,
                                publicKeyBlob.GetData(),
                                publicKeyBlob.GetSize(),
                                0)); // flags

3 番目のパラメータに再び注目する必要があり、インポートされる BLOB の詳細を示します。アルゴリズム プロバイダにインポートしたアルゴリズムを実装するだけで、正常に機能します。その後、結果として生成される公開キー ハンドルを使用すると、秘密キーの所有者だけが解読できるように、メッセージを安全に暗号化できます。

署名と検証

非対称暗号化の一般的な使用方法として、デジタル署名の作成があります。デジタル署名は、Windows や Microsoft® .NET Framework でよく使用され、メッセージ、実行可能ファイル、およびアセンブリの正当性を証明します。

既に説明した非対称暗号化や暗号化解除のプロセスとは異なり、署名は、作成に秘密キーを、検証に公開キーを使用します。そのため、公開キーを保持していれば、秘密キーの所有者が生成した特定の署名を検証できます。その秘密キーにアクセスする必要ありません。

基本は既に説明したため、署名を作成または検証するプロセスは非常に簡単です。最初に、署名するデータを特定した後、上記のハッシュ関数の説明に従ってデータをハッシュします。署名は、ハッシュ値とデジタル署名アルゴリズム用の秘密キーを基に、BCryptSignHash 関数で計算されます。図 9 に簡単な例を示します。

Figure 9 署名を計算する (Close Figure 9)
ULONG signatureSize = 0;

NT_VERIFY(::BCryptSignHash(keyPair,
                           0,                  // padding info
                           hashValue.GetData(),
                           hashValue.GetSize(),
                           0,                  // output
                           0,                  // output size
                           &signatureSize,
                           0));                // flags

Buffer signature;
NT_VERIFY(signature.Create(signatureSize));

NT_VERIFY(::BCryptSignHash(keyPair,
                           0,                  // padding info
                           hashValue.GetData(),
                           hashValue.GetSize(),
                           signature.GetData(),
                           signature.GetSize(),
                           &signatureSize,
                           0));                // flags

通常、最初の呼び出しで、作成した署名のサイズが計算され、次の呼び出しで、実際に署名が算出されます。ハッシュ値のサイズと使用するアルゴリズムによっては、パディング情報の追加指定が必要になる場合があります。

署名の検証はさらに簡単です。つまり、ハッシュを別個に計算してから、このハッシュ値、署名者の公開キー、および受け取った署名を BCryptVerifySignature 関数に渡して検証するという考え方です。BCryptVerifySignature は、署名がハッシュ値と一致する場合に STATUS_SUCCESS を返し、一致しない場合に STATUS_INVALID_SIGNATURE を返します。図 10 にこの考え方の例を示します。

Figure 10 署名を検証する (Close Figure 10)
NTSTATUS status = ::BCryptVerifySignature(publicKey,
                                          0,              // padding info
                                          hashValue.GetData(),
                                          hashValue.GetSize(),
                                          signature.GetData(),
                                          signature.GetSize(),
                                          0);             // flags
switch (status)
{
    case STATUS_SUCCESS:
    {
        // The signature matches the hash.
        break;
    }
    case STATUS_INVALID_SIGNATURE:
    {
        // The signature does not match the hash.
        break;
    }
    default:
    {
        // An error occurred.
    }
}

.NET との相互運用

.NET Framework 2.0 および 3.0 に付属する暗号化ライブラリでは、CryptoAPI からのネイティブ実装のラッパーだけでなく、さまざまなアルゴリズムのマネージ実装を提供しています。多くの場合、問題なく CNG データと相互運用できますが、.NET Framework 暗号化クラスの中には、不適切なアルゴリズム プロパティを既定のプロパティとして使用しているものもあります。これらのプロパティによって、トラブルシューティングを余儀なくされることもあります。このような場合の秘訣として、各キーのプロパティが CNG とマネージ実装との間で等しくなるように一致させると、正常に機能するようになります。

いくつかの点では、マネージ暗号化クラスよりも CNG を使用する方がかなり簡単です。これは、考慮すべき抽象型や継承される既定値が少ないためです。効果的な暗号化には、すべての詳細に詳しいプログラマが頼りです。

対称暗号化の場合、CNG と .NET Framework との間で暗号化テキストを交換できるように、.NET Framework SymmetricAlgorithm クラスのプロパティである BlockSize、IV、Key、KeySize、Mode、および Padding に注意してください。これらのうち 1 つを少しでも間違えると、エラーになります。CNG と .NET Framework で使用される既定値は必ずしも同じものとは限りませんが、通常、実装間でブロック サイズやパディング方式などに互換性のある共通の値を見つけることができます。

アルゴリズムによって使用するプロパティがさまざまなので、実装間で非対称暗号化を機能させるには手間がかかります。そのため、一致する共通プロパティ リストを見つけるだけでは、非対称暗号化を機能させることはできません。代わりに、さまざまな実装でキー情報をインポートおよびエクスポートする方法に重点を置き、自身で必要な変換を用意する必要があります。実装間で公開キーを通信するプロセスを示す具体的な例を見てみましょう。

RSA アルゴリズムでは、一般的なパラメータとして、公開キーと秘密キーの指数、モジュール (法)、1 組の素数、もう 1 組の指数、および係数を受け取ります。暗号学者や数学者であればこれに興味があるかもしれませんが、ほとんどの開発者が知りたいのは、キー ペアの公開部分を共有する方法だけです。RSA アルゴリズムの場合は、公開キーの指数とモジュールの値だけを共有する必要があります。

CNG が提供する BCryptExportKey 関数は、必要に応じて保持または共有できるバイナリ BLOB に暗号化キーをエクスポートする際に役立ちます。この関数も繰り返し説明してきたのと同じパターンに従っていることに注意してください。ここでは、最初に BLOB のサイズを決定するために呼び出してから、エクスポートされたキー情報を受け取れるように、新しく割り当てたバッファを指定して再度呼び出します。説明が必要なのは、次に示す 3 番目のパラメータのみです。

NT_VERIFY(::BCryptExportKey(key,
                            0,   // reserved
                            BCRYPT_RSAPUBLIC_BLOB,
                            blob.GetData(),
                            blob.GetSize(),
                            &blobSize,
                            0)); // flags

このパラメータは、生成する BLOB 型を示します。この例の BCRYPT_RSAPUBLIC_BLOB 識別子は、関数が RSA キーの公開キー部分のみをエクスポートすることを示しています。

ここからが面白い部分です。.NET Framework RSA クラスがキー情報のインポートに別の形式を使用するため、その BLOB を開いてその内容を確認する必要があります。実際の BLOB は、ヘッダーとして先頭に BCRYPT_RSAKEY_BLOB 構造体を備えた単純な形式です。このヘッダーでは、BLOB の型だけでなく、さまざまなキー パラメータの領域を示します。図 11 に、公開キー BLOB のメモリ レイアウトを示します。

図 11 公開キー BLOB のメモリ レイアウト
図 11 公開キー BLOB のメモリ レイアウト

この情報がわかれば、必要なメモリ範囲をコピーして、RSAParameters 構造体に設定するだけです。この設定は、キー情報をインポートする場合に .NET Framework RSA クラスで必要になります。

図 12 に示すように、C++/CLI を使用して、ネイティブ RSA キー BLOB をマネージ RSAParameters 構造体に直接変換しました。この構造体の指数フィールドとモジュール フィールドには、新しく作成したマネージ バイト配列が割り当てられます。その後、最初に値をピン設定してから memcpy_s 関数を使用すると、値がこれらのバイト配列に直接コピーされます。たとえば、C# でバイナリ BLOB を受け取ったとすると、.NET Framework BinaryReader クラスを使用して同じ結果を得ることができます。残りのプロセスは簡単です。

Figure 12 RSA キー BLOB を .NET 構造体に変換する (Close Figure 12)
BCRYPT_RSAKEY_BLOB* rsaBlob =
    reinterpret_cast<BCRYPT_RSAKEY_BLOB*>(blob.GetData());
RSAParameters rsaParams;

rsaParams.Exponent = gcnew array<BYTE>(rsaBlob->cbPublicExp);
{
    pin_ptr<BYTE> destination = &rsaParams.Exponent[0];
    const BYTE* source = reinterpret_cast<BYTE*>(rsaBlob);

    memcpy_s(destination,
             rsaParams.Exponent->Length,
             source + sizeof(BCRYPT_RSAKEY_BLOB),
             rsaBlob->cbPublicExp);
}

rsaParams.Modulus = gcnew array<BYTE>(rsaBlob->cbModulus);
{
    pin_ptr<BYTE> destination = &rsaParams.Modulus[0];
    const BYTE* source = reinterpret_cast<BYTE*>(rsaBlob);

    memcpy_s(destination,
             rsaParams.Modulus->Length,
             source + sizeof(BCRYPT_RSAKEY_BLOB) + rsaBlob->cbPublicExp,
             rsaBlob->cbModulus);
}

RSACryptoServiceProvider rsa;
rsa.ImportParameters(rsaParams);
array<BYTE>^ ciphertext = rsa.Encrypt(plaintext, false);

RSACryptoServiceProvider クラスは、CryptoAPI に基づいて作成された .NET Framework RSA アルゴリズムの実装です。ImportParameters メソッドでは、準備した RSAParameters 構造体がインポートされます。Encrypt メソッドでは、先に進み、公開キーを使用してメッセージを暗号化します。このメッセージは、秘密キーを使用しないと、暗号化解除できません。

今後の展望

この記事の執筆時点では、マイクロソフトは Visual Studio の次期バージョン (コードネーム "Orcas") の開発に取り組んでいます。また、そのリリースにあわせて、.NET Framework の新しいバージョンについても見ていく予定です。.NET Framework 3.5 では、CNG に基づいた新しいアルゴリズムの実装が数多く導入されます。.NET Framework 3.5 がリリースされると、特に暗号化の分野で、ネイティブ コードとマネージ コード間の相互運用性がさらに向上することも予想できます。CNG が Windows プラットフォームの主要な暗号化プロバイダになるため、フレームワークに追加されたこうした新機能は、既存の実装の多くに置き換わります。以前 .NET Framework では見られなかった新しいアルゴリズムの実装もいくつか導入されています。このような実装は、CNG のキー記憶域機能によって提供される楕円曲線アルゴリズムの一部に対応しています。こうした実装は、この記事の冒頭部分で触れた NCrypt として知られている、CNG のサブセットに分類されます。この詳細については、「Cryptographic Next Generation のリソース」を参照してください。

Cryptographic Next Generation のリソース

  • Microsoft Windows Cryptographic Next Generation Software Development Kit for Windows Vista
    Windows Vista を対象とする暗号化アプリケーションやライブラリを開発するためのドキュメント、コード、およびツール。
  • 暗号化 API: 次世代
    CNG の操作に役立つドキュメントおよびリファレンス。
  • Channel 9: Tomas Palmer と Tolga Acar - Windows Vista における暗号化
    Tomas Palmer と Tolga Acar が Joe Smith を交えて興味深い対談を行っています。
  • MSDN バーチャル ラボ : セキュリティ
    安全なコードの記述方法について説明します。
  • TechNet バーチャル ラボ : Windows Vista
    Windows Vista で強化されたセキュリティ、管理、および生産性について説明します。

Kenny Kerr は、Windows のソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr (英語) です。


音声認識: Text Services Framework によるディクテーションのサポート

音声認識

Text Services Framework によるディクテーションのサポート

Eric Brown
この記事の内容 : :
  • Windows 音声認識とテキスト サービス
  • TSF 対応アプリケーションを作成する
  • テキスト ストアとドキュメント ロック
  • TSF 対応コントロールを作成する
この記事は次のテクノロジを使用しています:
Windows Vista
コードのダウンロード : : Dictation2007_07.exe (1560KB)
コンテンツ
TSF 対応アプリケーションを作成する
テキスト ストアとドキュメント ロック
TSF に接続する
実装の詳細
Scintilla と TSF との対話
高度なトピック
まとめ

Windows Vista™ の新機能の中で筆者が気に入っているものの 1 つに、テキストを電子メール メッセージやその他のドキュメントに音声入力 (ディクテーション) を行なうなど、音声のみでコンピュータを操作することができる Windows® 音声認識があります。Windows 音声認識では、Text Services Framework (TSF) を使用して、ディクテーションされたテキストを挿入、選択、および修正します。TSF は先進のテキスト入力技術を提供するスケーラブルなフレームワークです。TSF は、音声認識、手書き認識、スペル チェッカー、日本語 IME (Input Method Editor) など、アプリケーションやテキスト コントロールと通信するためのテキスト サービスの標準化された手法を提供します。TSF の特筆すべき機能に、アプリケーションやテキスト サービスとの双方向通信機能があります。この機能により、テキスト サービスをアプリケーションのドキュメントに読み取りおよび書き込みすることが可能であり、アプリケーションからテキスト サービスを要求してテキストの修正などの操作を実行することもできます。

Windows Vista は、Win32® 標準の編集コントロール、RichEdit コントロール、HTML エディタ コントロール、Windows フォームの TextBox コントロールおよび RichTextBox コントロール、Windows Presentation Framework の TextBox コントロールまたは RichTextBox コントロールの TSF をサポートします。Win32 内部の編集コントロール、Windows フォームの ComboBox、ListBox、ListView コントロール、および TreeView コントロールも最初からサポートされています。Microsoft® Word や Publisher などの多くの Microsoft アプリケーションでも TSF をサポートしています。

アプリケーションまたはテキスト コントロールで TSF が完全にサポートされていない場合でも、Windows 音声認識の Dictation Everywhere 機能でディクテーションがサポートされます。Dictation Everywhere を使う場合にアプリケーションやコントロールの機能を使用する必要はありませんが、可能な操作はテキストの挿入のみであり、挿入されたテキストの選択や修正を行うことはできません。不正なテキストが挿入されるのを防ぐため、ディクテーションされた内容に間違いがないことをユーザーが確認する必要があります。

TSF はキーボード レイアウトの管理、キーボード レイアウトの切り替えに使用する言語バーの表示、アクティブな IME の管理および切り替えを行います。また、今回のトピックで特に重要な機能である、テキスト サービスがアプリケーションのドキュメント テキストに継続的にアクセスするための抽象層を提供します。

ただし、テキスト サービスでアプリケーションのドキュメント テキストを読み取るには、アプリケーションまたはコントロールに COM インターフェイスを実装する必要があります。TSF、テキスト サービス、アプリケーションまたはコントロール間の関係を図 1 に示します。

図 1 TSF とテキスト サービス
図 1 TSF とテキスト サービス(画像を縮小するには、ここをクリックします)
図 1 TSF とテキスト サービス
図 1 TSF とテキスト サービス (画像を拡大するには、ここをクリックします)

TSF は 100 以上のインターフェイスで構成されます。ディクテーションのサポートに必要なインターフェイスはこのうちの 4 つ (ITfThreadMgr、ITfDocumentMgr、ITfContext、ITextStoreACPSink) のみであり、コントロールに実装する必要があるインターフェイスは ITextStoreACP のみです。これらのインターフェイスの詳細を図 2 に示します。

Figure 2 ディクテーション関連の TSF インターフェイス (Close Figure 2)

インターフェイス名 | 実装者 | 機能
ITfThreadMgr | Text Services Framework | スレッド マネージャとも呼ばれる TSF の主なインターフェイスです。TSF 対応のアプリケーションまたはコントロールで作成する必要があります。フォーカスを管理し、ITfDocumentMgr オブジェクトを作成します。
ITfDocumentMgr | Text Services Framework | ドキュメント マネージャとも呼ばれます。このインターフェイスは 1 つまたは場合によっては 2 つの ITfContext オブジェクトのスタックを管理します。
ITfContext | Text Services Framework | このインターフェイスには ITextStoreACP のインスタンスが含まれます。このインターフェイスはテキスト サービスとテキスト ストアとの通信を管理します。
ITextStoreACPSink | Text Services Framework | テキスト ストア イベント ハンドラとも呼ばれます。このインターフェイスはアプリケーションからの変更通知を処理します。
ITextStoreACP | 開発者 | テキスト ストアとも呼ばれます。このインターフェイスはドキュメントのテキストおよびプロパティを取得し、ドキュメントのテキストを設定します。

TSF 対応アプリケーションを作成する

TSF 対応のアプリケーションを作成するには、TSF 対応のテキスト コントロールのみを使用するか、TSF のサポートをアプリケーションに直接統合します。通常、Microsoft Publisher や Microsoft Word などのように、テキストの表示や編集を直接行うアプリケーションの場合は、TSF のサポートを直接追加します。アプリケーションでコントロールを使ってテキストの表示および編集を行う場合は、対象コントロールに TSF のサポートが実装されていなければ、そのコントロールに TSF のサポートを実装することができます。

アプリケーションでプレーン編集コントロール、RichEdit コントロール、HTML エディタ コントロールを変更せずにそのまま使う (またはサブクラス化する) 場合、または Windows Presentation Foundation を基にする場合、アプリケーションは既に TSF 対応になっているので、アプリケーションを変更しなくてもディクテーションを使用できます。その他のコントロールをアプリケーションで使用する場合も、コントロールを TSF 対応にすることは簡単です。

Rich Edit コントロールをスーパークラス化し、Rich Edit 4.1 以降を使用するコントロールの場合は、コントロールを生成した後、コントロールのウィンドウ ハンドルで次のメソッドを呼び出します。

SendMessage(hwnd, EM_SETEDITSTYLE, SES_USECTF, SES_USECTF);

プレーン編集コントロールをスーパークラス化したコントロール、または Rich Edit 4.1 より古い Rich Edit コントロールをスーパークラス化したコントロールの場合、次のレジストリ エントリに DWORD 値を追加することによってコントロールの互換性が確保されます。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\CTF\KnownClasses\<RealClassName> = <flags>

<RealClassName> は、作成するコントロールのクラス名です。<flags> は、プレーン編集コントロールをスーパークラス化したコントロールの場合は 1、Rich Edit コントロールをスーパークラス化したコントロールの場合は 2 です。

アプリケーションで Rich Edit コントロールまたはプレーン編集コントロール以外のテキスト コントロールを使用している場合、またはアプリケーション内部でテキストのレンダリングを行う場合は、テキスト コントロールまたはアプリケーションに TSF のサポートを手動で実装する必要があります。この場合は多少余計な手間がかかりますが、それでも実装は簡単です。

テキスト ストアとドキュメント ロック

TSF は、テキスト ストアがオフセット 0 から始まる 16 ビット Unicode 文字の統一されたストリームであることを前提にしています。ドキュメントがこのモデルに対応していない場合の処理方法には、いくつかのオプションがあります。まず、ストリーム内の一部の文字を非表示にするという方法があります。たとえば、ドキュメント モデルで HTML マークアップなどの埋め込み書式設定文字が使用されている場合、これらの文字に非表示のタグを設定すれば、TSF はすべてのテキスト サービスでこれらの文字を非表示にします。

1 つの論理ドキュメント (Word ドキュメントなど) に複数のストリームが存在するドキュメント モデルの場合、ストリームごとに 1 つのテキスト ストアを作成することができます。ドキュメント モデルに異なる編集領域 (テーブルなど) が存在する場合、各領域を領域マーカーで区別することができます。アプリケーションは、ドキュメントのテキスト、選択内容、またはレイアウトが変更されるたびに TSF マネージャに通知する必要があります。詳細については、この記事の後半で詳しく説明します。

TSF のサポートを追加すると、テキスト サービスで ITextStoreACP メソッドを使ってドキュメントを変更できるようになります。これらのメソッドは随時呼び出し可能で、キーボードやマウスからのユーザー入力がいつでも受け付けられるようになっているため、ドキュメントの一貫した状態を保つには、何らかのドキュメント ロック機能が必要です。

テキスト サービスがドキュメントから読み取りまたは書き込みを行うとき、TSF マネージャは ITextStoreACP::RequestLock を呼び出します。たとえば、Microsoft グローバル IME はドキュメントを読み取って、挿入コンテキストを調べ、その新しいテキストを挿入するためにドキュメントに書き込みます。ロックが許可されている間は ITextStoreACPSink メソッドを呼び出すことはできません。ロック中に呼び出すと、TSF コードが混乱します。

また、ロックが許可されている間は、外部イベントによってドキュメントが変更されないようにする必要があります。コントロールに COM インターフェイスが実装されている場合、ドキュメントが変更されるような COM の呼び出しは禁止または延期する必要があります。

ITextStoreACP::RequestLock には 2 つのパラメータがあります。第 1 パラメータ (dwLockFlags) にはロック要求が指定されます。このパラメータは、TSF が要求するロックの種類が読み取り専用 (TS_LF_READ) ロックであるか、読み取り/書き込み (TS_LF_READWRITE) ロックであるかを示します。また、ロック要求をすぐに許可する (TS_LF_SYNC) か、後になってから許可するかを示します。第 2 パラメータ (phrSession) には、ロック要求の戻りコードが設定されます。

アプリケーションでロックを許可するには、ITextStoreACP::AdviseSink から取得し、保存されているインターフェイス ポインタを使用して、ITextStoreACPSink::OnLockGranted を呼び出します。ロックの要求では同期 (この RequestLock の呼び出しで ITextStoreACPSink::OnLockGranted を呼び出す) または非同期 (後から ITextStoreACPSink::OnLockGranted を呼び出す) を指定することができます。

アプリケーションが同期ロック要求を許可する必要があるケースは限られています。アプリケーションが同期ロックを許可する必要があるのは、アプリケーション (またはコントロール) でテキスト サービスにテキストの修正などの処理を要求する場合です (テキストの修正には ITfFunctionProvider を使いますが、このメソッドの使い方はこの記事の説明の対象外です)。ITfKeystrokeMgr インターフェイスを使ってメソッドを呼び出すときにもアプリケーションで同期ロックを許可する必要があります。また、ITfKeystrokeMgr を使用しない場合でも、ロック要求が GetMessage または PeekMessage 内で行われている場合には、アプリケーションで同期ロックを許可する必要があります。

これらの例外を除けば、アプリケーションで同期ロック要求を許可する必要はなく、非同期ロック要求で十分です。非同期ロック要求を同期ロック要求として処理してもかまいません。

MSDN® のドキュメントでは、phrSession の意味についての説明が多少紛らわしくなっています。図 3 を見ると、ITextStoreACP::RequestLock から返される *phrSession の内容がよくわかります。

Figure 3 *phrSession の戻り値 (Close Figure 3)

要求されたロックの種類 | サポートされるロックの種類 | *phrSession の内容
同期 | 同期 | ITextStoreACPSink::OnLockGranted からの戻り値
同期 | 非同期 | TS_E_SYNCHRONOUS
非同期 | 非同期 | TS_S_ASYNC
非同期 | 同期 | ITextStoreACPSink::OnLockGranted からの戻り値

コントロールまたはアプリケーションで複数のスレッドをサポートしている場合、またはメッセージ ポンプを再入力する場合 (たとえば、メッセージの処理中に COM メソッドを呼び出す場合など) は、TSF のロックを非同期に処理する必要があります。ロック要求の処理方法として、ITextStoreACP::RequestLock 内でメッセージをメッセージ キューにポストし、ドキュメントが安定してからロックを許可する (ITextStoreACPSink::OnLockGranted を呼び出す) ことも可能です。それ以外では、多くの場合、ドキュメントの非同期ロックをサポートする方が簡単です。

TSF に接続する

TSF のさまざまなメソッドの関連性を示すため、scintilla.org から入手可能な Scintilla テキスト エディタ コンポーネントを使った実用的な例を示します。Scintilla に加えた変更は赤で示しています。

まず、コードを TSF のフレームワークに登録する必要があります。登録は、スレッドごとに少なくとも 1 回行います。複数のスレッド上で作成可能なコントロールの場合は、コントロールのインスタンスごとに 1 回ずつ登録した方がよいでしょう。それには、CoCreateInstance を使って ITfThreadMgr オブジェクトを作成し、作成したオブジェクトをアクティブにします。スレッド マネージャをアクティブにすると、コントロールが TSF 対応としてマークされ、コンテキスト オブジェクトの作成に必要なクライアント ID が返されます。

スレッド マネージャを使用する場合は、ITfDocumentMgr オブジェクトおよび ITfContext オブジェクトも作成する必要があります。アクティブなドキュメントごとに各オブジェクトのインスタンスが 1 つずつ必要です。Scintilla コンポーネントはウィンドウ ベースであるため、例では、Initialise メソッドでスレッド マネージャ、ドキュメント マネージャ、およびコンテキストを作成しています。Initialise メソッドは WM_CREATE ハンドラで呼び出されます (図 4 を参照)。

Figure 4 TSF のコンポーネントの初期化 (Close Figure 4)
void ScintillaWin::Initialise()
{
    // Initialize COM.  If the app has already done this it will have
    // no effect.  If the app hasn’t, we really shouldn’t ask them to
    // call it just so this internal feature works.
    ::OleInitialize(NULL);
    // Register with TSF...
    HRESULT hr = CoCreateInstance (CLSID_TF_ThreadMgr, NULL,
        CLSCTX_INPROC_SERVER, IID_ITfThreadMgr, (void**)&cpThreadMgr);
    if (SUCCEEDED(hr))
    {
        hr = cpThreadMgr->Activate(&tid);
    }
    // TSF: create context for this window
    if (SUCCEEDED(hr))
    {
        hr = cpThreadMgr->CreateDocumentMgr(&cpDocMgr);
    }
    if (SUCCEEDED(hr))
    {
        hr = cpDocMgr->CreateContext(tid, 0,
            reinterpret_cast<ITextStoreACP *>(&acp),
            &cpContext, &ecTextStore);
    }
    if (SUCCEEDED(hr))
    {
        hr = cpDocMgr->Push(cpContext);
    }
}

ウィンドウを破棄するときは、スレッド マネージャ、ドキュメント マネージャ、コンテキストなどのオブジェクトを解放する必要があります。例では、オブジェクトを破棄する直前に呼び出される Finalise メソッドでオブジェクトを解放しています (図 5 を参照)。

Figure 5 TSF オブジェクトの解放 (Close Figure 5)
void ScintillaWin::Finalise()
{
    ScintillaBase::Finalise();
    SetTicking(false);
    SetIdle(false);
    DestroySystemCaret();
    ::RevokeDragDrop(MainHWND());
    // TSF: Cleanup
    cpDocMgr->Pop(TF_POPF_ALL);
    cpDocMgr = NULL;
    cpThreadMgr->Deactivate();
    cpThreadMgr = NULL;
    cpContext = NULL;
    cpSinkUnk = NULL;
    cpTextStoreACPSink = NULL;
    ::OleUninitialize();
}

実装の詳細

筆者は、Scintilla に ITextStoreACP を実装するうえで、いくつかの重要な設計上の選択を行いました。まず、表示属性をサポートしないことにしました。ディクテーションでは表示属性を使用しないため、これによってディクテーションに影響なくコードを簡素化できます。また、Scintilla は埋め込みオブジェクトをサポートしていないため、当然のように埋め込みオブジェクトについてもサポートしないことにしました。

最後の選択は、1 文字が 8 ビットである Scintilla のドキュメント ストリームと、1 文字が 16 ビットであるストリームを想定する ITextStoreACP との不一致に対する対処方法です。この問題は、16 ビットの文字ストリームの末尾の文字 (DBCS の末尾バイトまたは UTF-8 の末尾バイト) を非表示の空白として処理することで解決しました。この処理によって ITextStoreACP::GetText の実装は複雑になりますが、Unicode 文字と DBCS 文字の 1 対 1 のマッピングが維持されるため、その他のルーチンの実装が大幅に簡素化されます。

以上の点が決まれば、その他の ITextStoreACP メソッドの実装はとても簡単です。この実装内容は、この記事のソース コードのダウンロードに含まれています。

Scintilla には、TSF で通知が必要になるたびに何らかのイベントを発生させる通知メカニズムが既に用意されているため、図 6 のように、Scintilla の通知ルーチンに適切な TSF の通知を追加するだけで済みました。

Figure 6 TSF 通知ハンドラの追加 (Close Figure 6)
void ScintillaWin::NotifyFocus(bool focus)
{
    ::SendMessage(::GetParent(MainHWND()), WM_COMMAND,
        MAKELONG(GetCtrlID(), focus ? SCEN_SETFOCUS : SCEN_KILLFOCUS),
        reinterpret_cast<LPARAM>(MainHWND()));
    //TSF:  Update focus
    if (!!cpThreadMgr)
    {
        cpThreadMgr->SetFocus(focus ? cpDocMgr : NULL);
    }
}
void ScintillaWin::ClaimSelection()
{
    //TSF:  The selection has changed - notify the sink,
    //      if there’s no lock held.
    if (!!cpTextStoreACPSink && dwLock == NONE)
    {
        cpTextStoreACPSink->OnSelectionChange();
    }
}

sptr_t ScintillaWin::WndProc(unsigned int iMessage,
    uptr_t wParam, sptr_t lParam)
{
    ... // lots of existing code not shown

    case WM_WINDOWPOSCHANGED:
       // TSF: Update position
       if (!!cpTextStoreACPSink && dwLock == NONE)
       {
           cpTextStoreACPSink->OnLayoutChange(TS_LC_CHANGE, 0);
       }

    ... // lots more existing code not shown
}

void ScintillaWin::NotifyParent(SCNotification scn)
{
    scn.nmhdr.hwndFrom = MainHWND();
    scn.nmhdr.idFrom = GetCtrlID();
    ::SendMessage(::GetParent(MainHWND()), WM_NOTIFY,
        GetCtrlID(), reinterpret_cast<LPARAM>(&scn));
    // TSF: update sinks
    if (!!cpTextStoreACPSink && dwLock == NONE)
    {
        if (scn.nmhdr.code == SCN_MODIFIED)
        {
            bool fNotify(false);
            static TS_TEXTCHANGE chg;
            // Lots of notifications get routed here.
            // We’re only interested in insert/delete
            // insert operations have before/after
            // notifications, so we save up info
            if (scn.modificationType & SC_MOD_BEFOREINSERT)
            {
                chg.acpStart = scn.position;
            }
            if (scn.modificationType & SC_MOD_INSERTTEXT)
            {
                fNotify = true;
                chg.acpOldEnd = scn.position;
                chg.acpNewEnd = scn.position + scn.length;
            }
            if (scn.modificationType & SC_MOD_BEFOREDELETE)
            {
                chg.acpStart = scn.position;
            }
            if (scn.modificationType & SC_MOD_DELETETEXT)
            {
                fNotify = true;
                chg.acpOldEnd = scn.position + scn.length;
                chg.acpNewEnd = scn.position;
            }
            if (fNotify)
            {
                cpTextStoreACPSink->OnTextChange(0, &chg);
            }
        }
        else if (scn.nmhdr.code == SCN_UPDATEUI)
        {
            cpTextStoreACPSink->OnLayoutChange(TS_LC_CHANGE, 0);
        }
    }
}

最後に、ドキュメント ロックを検討する必要があります。ユーザーが使用中のドキュメントを TSF が変更しないようにすることは簡単だということがわかりました。Scintilla はシングル スレッド アパートメントで動作し、メッセージのディスパッチはアトミックです (Scintilla は 1 つのメッセージの処理中にメッセージをポンプしません)。ということは、ロック要求を同期処理してもかまわないということです。他のメッセージの処理中に TSF ロック要求を取得する方法はありませんでした。

TSF 要求の適切なロックも簡単であることを確認します。ロックは ITextStoreACP へのすべてのエントリ ポイントでチェックすることが可能です。最も難しい点は、テキスト サービスがロックを保持している間はユーザーがドキュメントを変更できないということです。その対策として、このロックの実装では、ロックが保持されている間は受信 Windows メッセージをキューに入れておき、ロックが解放された後にキューを排出することにしました。

ロックが保持されている間に Scintilla が生成する通知もキューに入れる必要があります。通知は、ロックの保持中にホスト アプリケーションに送信された場合、正常に動作しない可能性があります。

Scintilla と TSF との対話

Windows 音声認識の Speech Text Service (音声テキスト サービス) (テキストのディクテーションなど) のイベントによって Scintilla がどのように変更されるか、または Scintilla のイベントによって Speech Text Service (音声テキスト サービス) がどのように変更されるかがまだわかりにくいかもしれません。次に、Windows 音声認識の Speech Text Service (音声テキスト サービス)、TSF、および Scintilla 間の通信について説明します。図 7 は、Scintilla が TSF オブジェクトを作成する方法、および Scintilla にテキスト ストアが存在することを Speech Text Service (音声テキスト サービス) に認識させる方法を示しています。

図 7 TSF オブジェクトの作成
図 7 TSF オブジェクトの作成(画像を縮小するには、ここをクリックします)
図 7 TSF オブジェクトの作成
図 7 TSF オブジェクトの作成 (画像を拡大するには、ここをクリックします)

Speech Text Service (音声テキスト サービス) によって Scintilla ドキュメントにテキストを挿入する方法を図 8 に示します。この図には、Scintilla に関連するオブジェクトのみが示されており、TSF で管理されるその他のオブジェクトは示されていません。ここで重要なことは、TSF がアプリケーションとテキスト サービスを切り離しているという点です。アプリケーションはロック要求の許可による変更を制御し、テキスト サービスは関連する変更が行われたことを示す通知を受け取ります。

図 8 ドキュメントへのテキストの挿入
図 8 ドキュメントへのテキストの挿入(画像を縮小するには、ここをクリックします)
図 8 ドキュメントへのテキストの挿入
図 8 ドキュメントへのテキストの挿入 (画像を拡大するには、ここをクリックします)

ユーザーが Scintilla ドキュメントにテキストを入力したときの、Scintilla と TSF との通信のしくみを図 9 に示します。TSF は、テキストが変更された後にドキュメントをロックしています。これは、TSF が内部データ構造をアトミックに更新可能であることを保証するためです。ここでも、TSF はテキスト サービスをアプリケーションから切り離していますが、テキスト サービスはドキュメントの変更を認識することはできます。

図 9 入力時のドキュメントのロック
図 9 入力時のドキュメントのロック(画像を縮小するには、ここをクリックします)
図 9 入力時のドキュメントのロック
図 9 入力時のドキュメントのロック (画像を拡大するには、ここをクリックします)

高度なトピック

コントロールのソースを持っていなくても、そのコントロールを TSF のサポートでラップすることが可能です。その際、特に注意を要する点が 4 つあります。1 番目の注意点として、適切なタイミングで ITextStoreACPSink の通知を送信する機能が必要であることが挙げられます。コントロールにこのような通知機能が存在しない場合、TSF のサポートを追加するのははるかに難しくなります。2 番目の注意点は、ITextStoreACP::GetACPFromPoint を実装するには、コントロールがテキストをどのようにレイアウトするかを知っている必要があるということです。これは、ソースがなければ難しい問題です。3 番目の注意点は、ドキュメントのロックにも課題があるということです。最善の方法は、サードパーティ製コントロールを抽象層にラップして、コントロールがロックされている間は変更要求の受信を延期または拒否できるようにすることです。4 番目の注意点は、コントロールが非同期の変更 (URL からの読み込みなど) をサポートしている場合、コントロールを TSF のサポートでラップするのはかなり難しいということです。

P/Invoke を使用して適切なメソッドを呼び出すことによって、ITextStoreACP をマネージ コードに実装することが可能です。ただし、呼び出すメソッドはアンマネージ コードであるため、呼び出すためには完全な信頼 (または、少なくとも SecurityPermissionFlag.UnmanagedCode) が必要です。

テキスト サービスのデバッグを 1 台のマシンで行うには困難が伴います。プログラムをデバッガで実行中にブレークポイントに達すると、Text Services Manager は状態通知のためにアプリケーションへの呼び出しを試行します。アプリケーションは停止しているため、Text Services Manager も停止し、結果としてシステム全体が停止します。テキスト ストアの実装をデバッグする場合は、別のマシンでリモート デバッグすることをお勧めします。

まとめ

ディクテーションのサポートをアプリケーションに追加するのは、完全な TSF のサポートを追加する場合であっても、最初に思っていたほどには難しくはありません。複雑な作業は、Windows Vista の Windows 音声認識と TSF が処理してくれます。

この記事のダウンロードには、完全な編集済みコードが含まれています。この記事の例で使用した Scintilla コントロールの詳細については、www.scintilla.org を参照してください。その他の詳細については、MSDN® Web サイト (msdn2.microsoft.com/ms629032.aspx) で TSF に関する信頼性の高い参考資料を参照してください。

Eric Brown は、20 年以上にわたってソフトウェア開発に携わっており、音声認識とディクテーションに情熱を注いでいます。彼のブログは、blogs.msdn.com/tsfaware で公開されています。


編集者のメモ: モビリティとは

編集者のメモ
モビリティとは
Joshua Trupin

何を思い付きますか "モビリティ" という言葉を聞いたとき、何を思い付きますか。これは世代によって異なります。ある人は原動機付きスクーターを思い出します。またある人は自分の車について思案し、車のおかげで安全で信頼できる廉価な大量輸送の恐怖から解放された経緯について考えます。特定の年代の変わった人は、"ポータブル" だった Kaypro コンピュータを思い出します。ただし、これはハンドルのある軽量ブロックのようなものでした。現在では、モバイルの定義は狭まると同時に広がっています。

頭をかきむしりながら「しかし、そんなことが可能だろうか」と思われるかもしれません。本当に可能です。これは両方の側のコンシューマの期待の問題です。

もはや、モビリティは単なる場所の問題ではありません。デバイスをオフィスの外で使用できるからと言って、常に豊かで満足がいくとは限りません。たとえば、Pocket PC を見てみましょう。確かに、これらは電子メールや関連タスクに使用できますが、常にオンラインでない限り、拠点にいるときにこれらを意図的に同期する必要があります。ここで、Treo などの Windows Mobile フォンを見てみましょう。確かに見た目は同じですが、このデバイスは自動的に接続し、ユーザーがどこにいても電子メールを最新に保ちます。真のモバイル コンピューティングの定義は狭まっています。

同時に、このカテゴリに分類されるデバイスの数は非常に拡大しました。スマートフォンは今では標準です。信じられない場合は、電車や店にいる人、そして隣の車に乗っている人が何を使用しているかを見てください。このカテゴリの成長により、ここ数年で、モバイル デバイスは最終的にニッチからメインストリームに移行しました。これらの道具の 1 つを持つと、これがなくて生活できるだろうかと思うようになる可能性があります。隣の部屋の人にテキスト メッセージを送信することから、ドライブ中にフリーセルで遊ぶことまで、無類のユーティリティが提供されます。

Ultra-Mobile PC により、現在の市場にはさらに継続性があります。自分に適したサイズのデバイスを選択できます。ただし、これらすべてのマシンでは、モバイル デバイス用に設計するか、通常のデスクトップ マシン用に設計するかにかかわらず、アプリケーションを作成するときに考える必要のある事項が多数あります。

このため、今回発行する『MSDN マガジン』には、最小のスマートフォンから最もかさばるラップトップまで、あらゆるフォーム ファクタにサービスを提供するプログラムの設計に役立つ記事を 2 つ載せています。まず、Andre Michaud による「WPF アプリケーションで電源の状態を認識する」です。Andre は、Windows XP と Windows Vista の両方で Windows Presentation Foundation を採用するアプリケーションの電源管理サポートと、ポータブル デバイスのバッテリを長持ちさせるためにアプリケーションでこの機能を利用する方法について説明しています。

Daniel Moth は、「モバイル アプリケーションとデスクトップ アプリケーションの両方に使えるコードを 1 回だけ記述する」という彼の記事の中で .NET Compact Framework を詳細に検証しています。具体的には、アプリケーションの標準デスクトップ バージョンと、スマート デバイスで実行されるバージョンの間で、コードを共有する方法を検証しています。デスクトップ アプリケーションはモバイル デバイス用に変更できるでしょうか。モバイル アプリケーションはデスクトップ マシンで実行できるでしょうか。プラットフォーム間でコードベースを再利用できるでしょうか。これらの質問にはっとした場合は、まず落ち着いてから衝撃的な回答を読んでください。

最新のデバイスと既存のフォームの迅速な改良により、現在モバイル コンピューティングは成功しています。これはどのように役立つでしょうか。『MSDN Magazine』のブログ のディスカッションに参加してご意見をお聞かせください。あなたとチャットすることを楽しみにしています。 £J.T.

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Christian Allred、Claudio Caldato、Jon Cargille、Stephan Doll、Shawn Farkas、Donovan Follette、Matt Gibbs、Dushyant Gill、Richard Greenberg、Dana Groff、Brian Grunkemeyer、Solom Heddaya、Elisa Johnson、Ladi Prosek、Travis Querec、Rob Relyea、および Andrew Tucker です。


ツールボックス: ボトルネックやコードの類似性などを発見する

ツールボックス
ボトルネックやコードの類似性などを発見する
Scott Mitchell

コンテンツ
パフォーマンスおよびメモリのボトルネックを発見する
複数ファイルの類似コードをすばやく見つける
単純で手軽なカラー ピッカー
セキュリティの要点を学習する

パフォーマンスおよびメモリのボトルネックを発見する

今年の夏、筆者は妻と連れ立って、3 週間にわたり、215 マイルの距離を移動するバックパック旅行に出かけました。最初に携行品リストを作成した後、数日かけて持ち物の評価を行い、荷物をなるべく軽くするためにリストの項目を削っていきました。効率よく作業するため、テント、寝袋、ベアー キャニスタなどの重い物から手を付け、軽い物はほぼそのまま残しました。このようなケースは 80 対 20 の法則の典型例で、総重量の 80 パーセントは 20 パーセントの品物が占めています。

アプリケーションのパフォーマンスを最適化する場合にも、これと同じ方法を採用することをお勧めします。アプリケーションのコードが膨大でも、パフォーマンスに大きな影響を及ぼしているのはごく一部のコードのみである可能性があります。速度やメモリのボトルネックの所在を特定し、そこに注意を集中することが重要です。コード プロファイラを利用すれば、コードのどの部分にボトルネックが存在するかを特定することは簡単です。コード プロファイラは、他のアプリケーションで使用されている時間リソースやメモリ リソースを監視および記録する専用アプリケーションです。

パフォーマンスおよびメモリのボトルネックを特定する ANTS Profiler
パフォーマンスおよびメモリのボトルネックを特定する ANTS Profiler (画像を縮小するには、ここをクリックします)
パフォーマンスおよびメモリのボトルネックを特定する ANTS Profiler
パフォーマンスおよびメモリのボトルネックを特定する ANTS Profiler (画像を拡大するには、ここをクリックします)

このようなサードパーティ製コード プロファイラの 1 つに、Red Gate Software の ANTS Profiler 2.7 があります。ANTS Profiler は使い方が簡単です。起動すると、Profiler Project Wizard に 3 ステップのセットアップ手順が示されます。まず、パフォーマンスまたはメモリのプロファイルを作成するかどうかを選択するよう要求されます。パフォーマンスのプロファイルには、各コード行の実行回数と各行の実行にかかった総実行時間が表示されます。目立って低速なコード行やメソッドも示されます。メモリのプロファイルは、ヒープ、メモリ内のオブジェクト、総メモリ フットプリントなどのメモリ関連メトリックスのスナップショットをプログラムのさまざまな実行時点で定期的に取得することによって機能します。

次に、プロファイルの作成対象 (Microsoft® .NET Framework デスクトップ アプリケーション、ASP.NET Web アプリケーション、.NET Windows® サービス、または COM+ サーバー アプリケーション) を選択するよう要求されます。ANTS Profiler では、.NET Framework アプリケーション バージョン 1.0、1.1、および 2.0 のプロファイルを作成可能です。最後に、プロファイルを作成するアプリケーションの場所または名前を指定するよう要求されます。ここで指定した時点でプロファイルの作成対象となるアプリケーションが起動します。プログラムの実行中に、ANTS Profiler は実行またはメモリの使用状況に関する情報を自動的にキャプチャします。

プロファイルを取得したアプリケーションを閉じると、ANTS Profiler レポートに高度な概要と詳細なメトリックスが表示されます。パフォーマンスのプロファイルでは、ソース コードと共に、各コード行の実行回数と総実行時間を示す列に表示されます。実行時間が異常に長いコード行にはフラグが付けられます。メモリのプロファイルでは、すべてのオブジェクトを並べ替え、フィルタしてグリッド表示する詳細レポートが表示されます。特定のオブジェクトを選択すると、そのオブジェクトが存在するコード ツリー上の場所が示されます。

アプリケーションの最適化では、パフォーマンスに最大の影響を与えるコードに的を絞ることが重要です。コード プロファイラを使用すれば、問題のある箇所を経験的証拠に基づいて特定することができます。操作が簡単で機能豊富な ANTS Profiler をお試しください。

価格 : 295 ドル (標準エディション)、495 ドル (プロフェッショナル エディション)

www.red-gate.com/products/ants_profiler

複数ファイルの類似コードをすばやく見つける

大きいプロジェクト、特に複数の開発者がかかわっているプロジェクトには、コードの反復という問題がつきものです。たとえば、Bob は出力の書式設定、入力の解析、および計算をシステムの複数の箇所で統一する必要があるとします。このような類似コードはメソッドにリファクタリングし、そのメソッドを複数の箇所から呼び出すようにするのが理想的ですが、Bob は急いでいたので、この問題は後日修正することにしようと考えるかもしれません。ところが、Bob がコードをリファクタリングする前に、Alice がそのコードを見直し、バグを見つけて修正したとしたらどうなるでしょう。Bob のコードが他の箇所で重複していることを Alice が知らなければ、両方の箇所のバグを修正する可能性はありません。要するに、コードが重複していると、アプリケーションの保守性と更新容易性が損なわれます。

そこで、コードの反復がすぐに見つかるツールが用意されています。Red Hill Consulting の Simian 2.2 は、複数のファイルから重複する構文を検索する類似性アナライザです。このプログラムは、C#、T-SQL、JavaScript、Visual Basic® などの各種プログラミング言語の構文を認識し、プレーン テキスト ファイル内の反復も検出します。さまざまな構成オプションにより、ユーザーは重複コードを検出するためのルールを調整できます。たとえば、しきい値の設定により、同一コード行が何行あったら重複と判断するかを指定することが可能です。

Simian は重複コードを効率的に検出する手軽なツールです。グラフィカル インターフェイスはありませんが、コマンド ラインまたはプログラムでのアクセスによって操作できます。結果はプレーン テキストで出力されますが、XML など、組み込み形式のいずれかを通してフォーマットすることも可能です。Simian はインターフェイスが軽く、レポート作成機能も限定的であるため、習得する手間がかからず、特化した効率的な機能を提供します。

Simian では、プロジェクトの大小を問わず重複コードを検索することができます。たとえば、Red Hill Consulting の報告によれば、4,136 個のファイルに分散している Java Development Kit の 130 万行の生ソース コードを Simian で照合したところ、約 25 秒で完了したということです。筆者が行ったテストでは、200 個程度のファイルに約 15,000 行の生ソース コードが存在するプロジェクトの解析が 1 秒足らずで完了しました。

重複コードはプロジェクトの保守性と更新容易性に悪影響を及ぼします。Simian を使えば、複数ファイルの重複コードをすばやく検出することができます。Simian はコマンド ラインから実行可能であるため、ビルド プロセスにプラグインして、警告を出したり、反復コードが検出された時点でビルドを中止することも可能です。

価格 : 99 ドル (パーソナル ライセンス)、499 ドル (プロジェクト ライセンス)

www.redhillconsulting.com.au/products/simian

単純で手軽なカラー ピッカー

Web サイトのフロントエンドを設計したり、市販デスクトップ アプリケーションのユーザー インターフェイスを作成したりするには、多くの場合、画面上の個々の色について特定の RGB 設定に関する知識が必要です。フォームの背景色を会社のロゴと同じ色にしたり、Web ページのテキストの色を PDF ドキュメントに表示される色と同じにしなければならない場合もあるでしょう。正確な色仕様が不明な場合は、カラー ピッカーで目的の色を見つけることができます。カラー ピッカーは、特定の色 (通常は現在マウス ポインタが配置されている色) の RGB の組み合わせを示します。

グラフィック編集プログラムで作業しているのでない場合は、カラー ピッカー プログラムを単独で使用することもできます。画面上の特定の色の仕様を確認するためだけにイメージ ファイルを開いたり、スクリーンショットを取得してグラフィック編集プログラムに貼り付けるのは面倒な作業です。

詳細な色情報を表示する Pixie の単純なインターフェイス
詳細な色情報を表示する Pixie の単純なインターフェイス (画像を縮小するには、ここをクリックします)
詳細な色情報を表示する Pixie の単純なインターフェイス
詳細な色情報を表示する Pixie の単純なインターフェイス (画像を拡大するには、ここをクリックします)

筆者がこれまで出合った最高のフリーウェア カラー ピッカーの 1 つに Nattyware の Pixie 1.0 があります。Pixie を起動すると、デスクトップの中央に表示された小さい正方形の画面に、現在マウス ポインタが配置されている色が 16 進数、HTML、RGB などの形式で示され、現在のマウス ポインタの X 座標と Y 座標も示されます。Pixie は、ユーザー インターフェイス画面を表示するだけでなく、最小化してタスク トレイに表示することもできます。

Pixie では、いくつかの操作をキーボード ショートカットに割り当てることができます。マウス ポインタが置かれている箇所を拡大するには、単純に Ctrl + Alt + Z キーを押します。Windows の標準カラー パレットを表示するには、Ctrl + Alt + X キーを押します。色の情報をクリップボードにコピーするには、マウス ポインタを目的の色の上に配置して、Ctrl + Alt + C キーを押します。

Pixie の真価は、単純さと簡素なインターフェイスにあります。画面上の色の仕様を確認する必要があれば、Pixie をお試しください。

価格 : 無料、ソース コードは 25 ドル

www.nattyware.com/pixie.html

セキュリティの要点を学習する

セキュリティには、計画とドメインに関する知識が必要であり、成果の測定は容易ではありません。また、セキュリティは物理レベル、技術レベル、ポリシー レベルにわたる課題であるため、開発者は必要なセキュリティ上の予防策の実装を自ら手がけないで、他者に任せてしまいがちです。しかし、このような姿勢が災難を招きます。セキュリティはあらゆるアプリケーションの重要な側面であり、すべての開発者が、少なくとも、重要なセキュリティ上の概念、潜在的な攻撃ベクトル、一般的な脅威に対する保護手法の概要を把握しておく必要があります。

『Foundations of Security: What Every Programmer Needs to Know』(Apress、2007 年 － Neil Daswani、Christoph Kern、Anita Kesavan 著) は、セキュリティに関する知識が少ない、またはまったくない開発者に適したレベルでセキュリティの基礎に関する凝縮した概要を提供しています。この本は、セキュリティ設計の原則に関する 3 つの章から始まり、認証、承認、暗号化、メッセージ整合性などの概念をカバーしています。

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

この本の核心は、セキュリティ上安全なプログラミング手法について説明している 2 番目のセクションです。このセクションでは、バッファ オーバーフロー、クライアント状態の操作、SQL 挿入攻撃、パスワードの安全な保存方法、クロスサイト スクリプト (XSS) 攻撃などの一般的な攻撃ベクトルについて説明し、これらの攻撃を阻止する方法を示しています。このセクションのコード サンプルは Java で記述されていますが、主に擬似コードのような参考資料として掲載されているので、Java を使用しない開発者でも一読に値します。

最後のセクションでは、対称暗号化、非対称暗号化、鍵管理、メッセージ認証コード (MAC)、署名に関する章で暗号化について説明しています。

あらゆるプロジェクトの重要な一部であるセキュリティを怠ったことにより、ユーザーの個人データや企業の収益が犠牲になっている例が数多く見られます。『Foundations of Security: What Every Programmer Needs to Know』は、すべての開発者にお勧めのわかりやすいセキュリティ入門書です。

価格 : $39.99.

ご意見やご質問は、

Scott toolsmm@microsoft.com.

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


CLR 徹底解剖: IDisposable について

CLR 徹底解剖
IDisposable について
Shawn Farkas

コンテンツ
破棄可能なオブジェクト
Disposable パターン
マネージ リソースとネイティブ リソース
マネージ リソースのクリーンアップ
Disposable 型からの派生
Dispose とセキュリティ
SafeHandles
まとめ

共通言語ランタイム (CLR) がマネージ コードの開発者に提供している生産性向上のための機能の 1 つに、マネージ ヒープに割り当てられたメモリが不要になるとガベージ コレクタ (GC) によってクリーンアップされるという機能があります。 これにより、開発者は、メモリのリークや、解放されたメモリを使おうとしたり、メモリを二重解放したりすることによって生じる困難な問題のデバッグに膨大な時間を費やす必要がなくなりました。 しかし、ガベージ コレクタはメモリのリークを防ぐのには有効ですが、解放する必要がある他のリソースについての情報は保持していません。 たとえば、ガベージ コレクタは、ファイル ハンドルを閉じる方法や、CoAllocTaskMem などの API を使用してマネージ ヒープの外部に割り当てられたメモリを解放する方法を知りません。

そのような種類のリソースを管理するオブジェクトは、リソースが不要になったときに確実に解放されるようにする必要があります。 そのためには、System.Object の Finalize メソッドをオーバーライドします。これにより、オブジェクトが自身のクリーンアップを実行しようとしていることをガベージ コレクタに通知できます (C# では、このメソッドを直接オーバーライドするのではなく、C++ のデストラクタ構文である ~MyObject を使用します)。 クラスにファイナライザがある場合、ガベージ コレクタは、その型のオブジェクトを収集する前にオブジェクトのファイナライザを呼び出して、オブジェクトが保持しているすべてのリソースのクリーンアップを実行できるようにします。

このシステムにおける問題の 1 つは、ガベージ コレクタが確定的に実行されないことであり、その結果として、オブジェクトへの最後の参照がなくなった後も長時間にわたってオブジェクトのファイナライズが実行されない可能性があることです。オブジェクトが高価または希少なリソース (データベース接続など) を保持する場合、これは許容できない問題になります。たとえば、利用可能な開いている接続が 10 個しかなく、そのうちの 1 つをオブジェクトが保持している場合、オブジェクトは、ガベージ コレクタがファイナライズ メソッドを呼び出すのを待たずにできるだけ早くその接続を解放する必要があります。

破棄可能なオブジェクト

不確定な期間にわたってガベージ コレクタの実行を待機する必要をなくすには、リソースを所有する型が IDisposable インターフェイスを実装する必要があります。これにより、その型のコンシューマは適切なタイミングでリソースを解放できるようになります。 IDisposable の実装は、オブジェクトの操作が終了した直後に Dispose を呼び出す必要があることをオブジェクトのコンシューマに示し、不要になったリソースをオブジェクトが直ちに解放できるようにします。 C# と Visual Basic® には、このプロセスをできるだけ簡単にするための "using" キーワードが用意されています。 C# では、IDisposable を実装している MyClass クラスがある場合、次のコードによってコンパイラに図 1 のようなコードを生成させることができます。

Figure 1 生成されるコード (Close Figure 1)
MyClass myClass = GetMyClass();
try
{
    myClass.DoSomething();
}
finally
{
    IDisposable disposable = myClass as IDisposable;
    if (disposable != null) disposable.Dispose();
}

using (MyClass myClass = GetMyClass())
{
    myClass.DoSomething();
}

try/finally ブロックを使用すると、using ブロック内のコードで例外が発生した場合でも、MyClass の Dispose メソッドはオブジェクトのクリーンアップを確実に実行できます。 コンパイラによって生成されたコードは、Dispose メソッドを呼び出す前に 2 つの興味深い処理を行います。 まず、破棄可能なオブジェクトが null でないことを確認します。そのため、変数に割り当てられた式が null と評価されても、アプリケーションの using ブロックでは NullReferenceException が発生しません。 また、このコードは IDisposable への参照を経由して Dispose を呼び出すことにより、IDisposable を明示的に実装している型が正しくクリーンアップされるようにします。

using ブロックは IDisposable の明示的な実装を持つクラスと連動しますが、このような方法でクラスに IDisposable インターフェイスを実装することはお勧めできません。 IDisposable を明示的に実装した場合、Visual Studio® の IntelliSense® を使用してオブジェクト モデルを探索している開発者は、オブジェクトに Dispose メソッドがあることに気付かず、したがって早期のクリーンアップを利用しません。その結果、オブジェクトはガベージ コレクタがファイナライザを実行するまでリソースを保持することになります。

ガベージ コレクタの観点から見ると IDisposable は 1 つのインターフェイスにすぎないという点に注意してください。 ガベージ コレクタは、破棄可能なオブジェクトを IDisposable の実装を持たないオブジェクトと同じように扱います。具体的には、ガベージ コレクタは Dispose メソッドを自動的に呼び出しません。 ガベージ コレクタが呼び出すクリーンアップ メソッドは、オブジェクトのファイナライザだけです。Dispose メソッドを明示的に呼び出すコードを記述していなければ、Dispose メソッドは呼び出されません。

Disposable パターン

既に説明したように、C# と Visual Basic の "using" コンストラクトを使用すると、破棄可能なオブジェクトを簡単にクリーンアップできます。 ただし、そのためには開発者が破棄可能な型を作成する必要があります。 IDisposable インターフェイスは 1 つのメソッドしか定義していないため、破棄可能な型の開発者はこのインターフェイスをすばやく簡単に実装できるように思われます。 しかし、Joe Duffy の IDisposable guidelines で述べられているように、本当に強固な破棄可能な型を実装するにはさまざまな細かい配慮が必要です。

Dispose メソッドの主な特徴の 1 つに、このメソッドが行うクリーンアップが、オブジェクトのファイナライザによるクリーンアップとよく似ているという点があります。 コードの重複を避けることは常に望ましいため、2 つの場所の間でこのクリーンアップ コードをコピーして貼り付けるよりも、共有する方が適切です。 この方法には、型のクリーンアップ コードを 1 か所にまとめることができるという利点もあります。 このコードを共有する明白な方法は、すべてのクリーンアップ コードを Dispose メソッドに含めて、オブジェクトのファイナライザが単純に Dispose を呼び出すようにすることです。

public class MyClass : IDisposable
{
    ~MyClass()
    {
        Dispose();
    }

    public void Dispose()
    {
        // Cleanup
    }
}

この場合、すべてのクリーンアップ コードを 1 か所にまとめるという目標は確かに達成できますが、ファイナライザの実装に問題が生じます。 一部のクリーンアップ コードは Dispose とファイナライザの間で共有できますが、Dispose メソッドでは解放したいがファイナライズでは解放したくない一連のリソースも存在するからです。

マネージ リソースとネイティブ リソース

ファイナライザは確定的な順序で実行されないため、オブジェクトが参照する他のオブジェクトが、ファイナライザが実行される時点でまだファイナライズされていないことは保証できません。 たとえば、次のクラスについて考えてみましょう。

public class Database : IDisposable
{
    private NetworkConnection connection;
    private IntPtr fileHandle;

    // ...
}

public class NetworkConnection : IDisposable
{
    private IntPtr connectionHandle;

    // ...
}

Database クラスには、NetworkConnection 型と、ファイル ハンドルを表す IntPtr 型の 2 つのリソースが含まれています。 Database 型のオブジェクトへのアクティブな参照が残っている間は、ガベージ コレクタは connection メンバ変数によって参照される NetworkConnection オブジェクトを収集しません。 しかし、ガベージ コレクタが Database オブジェクトのファイナライザを呼び出している場合は、このオブジェクトへのライブ参照が残っておらず、オブジェクトが収集できる状態になったことがわかります。

NetworkConnection オブジェクトへのライブ参照が残っていない場合、データベースのファイナライザを実行する前に NetworkConnection オブジェクトがファイナライズされたという可能性もあります。 既にファイナライズされているオブジェクトを使用することは、オブジェクトが使用可能な状態になる可能性が低いためお勧めできません。ファイナライズの順序に関しては保証がないので、Database のファイナライザが connection フィールドを参照しないようにする必要があります。

しかし、Dispose メソッドの呼び出しの結果としてクリーンアップを実行している場合は、Dispose のオブジェクトへのライブ参照がないと Dispose を呼び出せないことから、ファイナライズが行われていないことがわかります。 Database オブジェクトはまだ終了しておらず、connection フィールドによって参照される NetworkConnection の GC ルートとして機能しているため、NetworkConnection オブジェクトはファイナライズされません。 したがって、Dispose メソッドでは、ファイナライズ可能なメンバ変数に安全にアクセスできます。

ただし、fileHandle フィールドについては事情が異なります。 このハンドル フィールドは単なる IntPtr で、ガベージ コレクタは IntPtr をファイナライズしません。 つまり、ファイナライザと Dispose メソッドの両方で、fileHandle フィールドに安全にアクセスできます。

この原則を一般化すると、Dispose メソッドでは、オブジェクトが保持しているすべてのリソースを、マネージ オブジェクトかネイティブ リソースかに関わらず、安全にクリーンアップできるということになります。 しかし、ファイナライザではファイナライズ可能でないオブジェクトだけを安全にクリーンアップでき、一般的にはファイナライザがネイティブ リソースだけを解放するようにする必要があります。

この例で興味深いのは、connection フィールドが明らかにネイティブな接続ハンドルを含んでいるにもかかわらず、ネイティブ リソースと見なされないという点です。 NetworkConnection クラスの内部実装は確かにネイティブ リソースを含んでいますが、NetworkConnection 型はそれ自身のリソースの有効期間を管理するため、Database クラスからはマネージ リソースと見なされます。 しかし、NetworkConnection クラス内では、connectionHandle はコードで明示的に記述されていない限りクリーンアップされないため、ネイティブ リソースと見なされます。

破棄ではすべてのリソースをクリーンアップする必要があり、ファイナライズではこれらのリソースのサブセットをクリーンアップする必要があることから、ファイナライザと Dispose メソッドの両方から呼び出されるメソッドにクリーンアップ コードを含めることができます。 この新しいメソッドでは、破棄を実行する場合はすべてのリソースがクリーンアップされ、オブジェクトをファイナライズする場合はネイティブ リソースだけがクリーンアップされるようにします。 そのためには、図 2 のパターンに従った Disposable の実装を作成する必要があります。

Figure 2 基本的な IDisposable パターン (Close Figure 2)
public class DisposableClass : IDisposable
{
    ~DisposableClass()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Clean up all managed resources
        }

        // Clean up all native resources
    }
}

この場合も、すべてのクリーンアップ コードが Dispose メソッドのオーバーライドに含まれているため、コードの重複を避けてクリーンアップ コードを 1 つの場所にまとめるという当初の目標は達成されています。 他の Dispose メソッドとの混同を避けるため、この記事の残りの部分ではこのメソッドを "クリーンアップ メソッド" と呼ぶことにします。 クリーンアップ メソッドは、現在のパスが破棄コード パスとファイナライズ コード パスのどちらであるかを示すブール型パラメータを受け取ります。 両方のコード パスで、クラスが保持しているネイティブ リソースをクリーンアップする必要があります。 さらに、Dispose コード パスでは、マネージ リソースもクリーンアップする必要があります。

クラスのコンシューマがクリーンアップ メソッドを直接呼び出せるようにはしないので、クリーンアップ メソッドは public である必要はありません。 その代わりに、コンシューマは IDisposable インターフェイス経由でクリーンアップ ロジックにアクセスできます。 クリーンアップ メソッドは、クラスのコンシューマから直接呼び出す必要はありませんが、サブクラスからは呼び出す必要があるため、private ではなく protected としてマークされています (破棄可能な型のサブクラス化については後で説明します)。

Dispose はオブジェクトが保持しているすべてのリソースを解放するため、ガベージ コレクタがオブジェクトをファイナライズする必要はありません。 ファイナライザがある場合でもオブジェクトのファイナライズを実行する必要がないことをガベージ コレクタに知らせるには、Dispose メソッドでクリーンアップ コードを実行した後に GC.SuppressFinalize を呼び出します。

実際には、型がネイティブ リソースを所有していないと、ファイナライザによって実行されるコードは何の操作も行いません。 そのような場合には、クラスでファイナライザ メソッドを定義しないでください。

Database の例では、新しいクリーンアップ メソッドは図 3 のようになります。 Database オブジェクトの破棄では、connection マネージ リソースと fileHandle ネイティブ リソースの両方がクリーンアップされます。 しかし、オブジェクトのファイナライズでは、fileHandle リソースだけがクリーンアップされます。 このクリーンアップ コードは、複数回にわたって安全に呼び出すことができます。 Dispose メソッドの最初の呼び出しでは、Database オブジェクトが保持しているすべてのリソースが解放されます。それ以降の Dispose の呼び出しでは、リソースが既に解放されていることが検出され、解放を試みずに呼び出しが終了します。 追加の呼び出しは不要ですが、複数回呼び出されても成功するように Dispose の実装をコーディングしてください。 オブジェクトの他のメソッドは、オブジェクトが破棄された後に呼び出されると ObjectDisposedException をスローすることがあります。

Figure 3 Database のクリーンアップ メソッドの例 (Close Figure 3)
protected virtual void Dispose(bool disposing)
{
    if (disposing)
    {
        if (connection != null)
            connection.Dispose();
    }

    if (fileHandle != IntPtr.Zero)
    {
        CloseHandle(fileHandle);
        fileHandle = IntPtr.Zero;
    }
}

また、興味深いのは、クリーンアップ コードが例外をスローしないことです。 通常、開発者は、例外を発生させずにオブジェクトをクリーンアップできるようにしたいと考えます。 これは、クリーンアップ コード パスの途中で例外から回復する適切な方法がないからです。 クリーンアップ中にオブジェクトが失敗した場合、オブジェクトのコンシューマは、再試行して 2 回目のクリーンアップが成功するよう祈るか、一部のリソースが正しく解放されなくてもあきらめるしかありません。

クリーンアップ コードが例外をスローしないようにするには、クリーンアップ メソッドでアクセスするすべてのオブジェクト参照が null でないことを確認して、失敗しないメソッドだけを呼び出すようにする必要があります。 このことは、オブジェクトがファイナライズされるときに非常に重要になります。 Dispose の呼び出し中に例外が発生しても開発者の立場が悪くなるだけですが、ファイナライザ スレッドで処理不能な例外が発生すると、既定では CLR のバージョン 2.0 のプロセス全体が破壊されます。

ファイナライザ スレッドを実行していなくても、クリーンアップ中に例外がスローされると、他のリソースが期待どおりにクリーンアップされないおそれがあります。 たとえば、図 4 を見てください。 ここで、DisposableClass には Foo 型と Bar 型のリソースが含まれています。 Foo.Dispose が実行中に例外をスローした場合、この DisposableClass は Bar オブジェクトを破棄できなくなります。

Figure 4 Dispose における例外の危険性 (Close Figure 4)
public class Disposableclass : IDisposable
{
    Foo a;
    Bar b;

    // ...

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (a != null) a.Dispose();
            if (b != null) b.Dispose();
        }
    }
}

マネージ リソースのクリーンアップ

オブジェクトをファイナライズするときにマネージ リソースがクリーンアップされないならば、どのようにしてマネージ リソースのリークを防止できるのでしょうか。 クリーンアップの必要があるリソースを含んでいる場合、オブジェクトは IDisposable パターンに従う必要があります。 オブジェクトのファイナライザはマネージ リソースが既にファイナライズされているかどうかを確認できないため、マネージ リソースを直接クリーンアップできません。 しかし、これらの型が解放する必要のあるリソースを含んでいる場合は、ガベージ コレクタが実行できるファイナライザも持っている必要があります。マネージ リソースだけを含んでいる場合は、ファイナライザは必要ありません。

例を見てみましょう。 マネージ コントロールがブラウザ セッション中に状態を維持するために使用する Cookie クラスがあるとします。 Cookie クラスは IsolatedStorage を使用して実装でき、IsolatedStorage 自体は FileStream を使用して実装できます。 FileStream は Win32® ファイル ハンドルを含むため、最終的なクラスは図 5 に示すコードのようになります。

Figure 5 クラス階層での IDisposable の実装 (Close Figure 5)
public class DisposableBase : IDisposable
{
    ~DisposableBase()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        // ...
    }
}

public class DisposableSubclass : DisposableBase
{
    protected override void Dispose(bool disposing)
    {
        try
        {
            if (disposing)
            {
                // Clean up managed resources
            }

            // Clean up native resources
        }
        finally
        {
            base.Dispose(disposing);
        }
    }
}

コントロールが Cookie オブジェクトを破棄し忘れた場合、Cookie は IsolatedStorageFileStream をクリーンアップするためのファイナライザを持ちません。これは、IsolatedStorageFileStream がマネージ リソースであるからです。 ガベージ コレクタはストリーム オブジェクトを自動的に回収するので、それで問題ありません。 さらに、IsolatedStorageFileStream はマネージ リソースだけを含んでいるため、ファイナライザを必要としません。 FileStream オブジェクトはネイティブ リソースを所有しており、ファイナライザを含むため、これも問題ありません。 ガベージ コレクタは、FileStream をファイナライズするときにハンドルを閉じます。 誰もこのオブジェクト グラフのルート オブジェクトを破棄しなくても、関係するすべてのクラスが Disposable パターンに従っているため、リソースのリークは発生しません。

Disposable 型からの派生

破棄可能な型からの派生を行う場合に、派生型が新しいリソースを含んでいなければ、特別な処理は不要です。 IDisposable 基本型の実装がリソースのクリーンアップを行うため、サブクラスが詳細を意識する必要はまったくありません。 ただし、サブクラスにはクリーンアップを必要とする新しいリソースが含まれているのが一般的です。 その場合、クラスはそれ自身のリソースを解放すると同時に、基本型のリソースが確実に開放されるようにする必要があります。 そのためには、クリーンアップ メソッドをオーバーライドし、リソースを解放した後で、図 6 に示すように基本型を呼び出してそのリソースをクリーンアップします。

Figure 6 Dispose のオーバーライド (Close Figure 6)
public class FileStream : IDisposable
{
    private IntPtr fileHandle;

    // ...

    protected void Dispose(bool disposing)
    {
        if (fileHandle != IntPtr.Zero)
        {
            CloseHandle(fileHandle);
            fileHandle = IntPtr.Zero;
        }
    }
}

public class IsolatedStorageFileStream : IDisposable
{
    private FileStream file;

    // ...

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (file != null)
                file.Dispose();
        }
    }
}

public class Cookie : IDisposable
{
    private IsolatedStorageFileStream isolatedStore;

    // ...

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (isolatedStore != null)
                isolatedStore.Dispose();
        }
    }
}

継承したバージョンの public な Dispose メソッドとファイナライザは目的どおりに動作するので、派生クラスがクリーンアップ メソッドだけをオーバーライドする必要がある点に注意してください。 この原則の例外は、基本型がネイティブ リソースを所有せず、派生型がたまたまネイティブ リソースを所有しているために、基本型がファイナライザを持たない場合です。 その場合は、派生型にファイナライザを追加する必要があります。

派生型では、基本型のクリーンアップ メソッドを呼び出す前にリソースをクリーンアップしてください。 これによって、オブジェクトが構築されたのと逆の順序で分解されます。 基本型のクリーンアップ メソッドを最初に呼び出した場合は、クリーンアップ メソッドを完了するために必要なオブジェクトの一部が、基本型によって破壊された状態になる恐れがあります。

Dispose とセキュリティ

クリーンアップ コードの記述における微妙な問題の 1 つに、コードを実行できるセキュリティ コンテキストがあります。クリーンアップ コードはすべて、実行中のスレッドがどの ID を偽装していても実行できるようにする必要があります。 ガベージ コレクタは専用スレッドでファイナライザを実行するので、破棄可能なオブジェクトをインスタンス化したスレッドで有効になっている偽装があっても、ファイナライザが実行されるときには有効になりません。 また、型を使用するコードがオブジェクトの構築と Dispose 呼び出しの間にスレッドの偽装を元に戻したり、別の ID を共に偽装したりしていないことを確認する方法はありません。

偽装を正しく処理しないクラスの 1 つの例として、Microsoft® .NET Framework 2.0 の RSACryptoServiceProvider クラスがあります。RSACryptoServiceProvider オブジェクトが使用する短期キーを作成する場合、オブジェクトのファイナライザはそのキーを削除しようとします。このキーは、キーを作成するスレッドを実行しているユーザーのプロファイルに保存されます。 スレッドがユーザーを偽装している間に RSACryptoServiceProvider オブジェクトが作成されると問題が発生し、オブジェクトが破棄されません。 その場合、ファイナライザはオブジェクトのコンストラクタとは別のユーザーのコンテキストで実行されます。 ファイナライザがキーを削除しようとすると、ファイナライザ スレッドを実行しているユーザーがキーの保存されているユーザー プロファイルにアクセスできないため、例外が発生します。

ファイナライザが別スレッドで実行されるということは、Dispose メソッドのコール スタックのアクセス許可チェックを信頼できないということも意味します。 クリーンアップ メソッドがオブジェクトのファイナライザから呼び出されている場合、コール スタックの上位にコードがないため、Code Access Security のチェックが無意味になります (一般に、セキュリティ要求は例外を発生させる可能性があるので、クリーンアップ コードでは避ける必要があります)。

SafeHandles

.NET Framework 2.0 では、リソース管理を支援するために SafeHandle クラスが導入されています。 SafeHandle はネイティブ リソースをラップし、IntPtr を使用してリソースを保持する場合に比べていくつかの利点があります。 SafeHandle の完全な詳細についてはこの記事では説明しませんが、詳細については、基本クラス ライブラリ (BCL) に関する Brian Grunkemeyer の秀逸な記事 team blog を参照してください。

重要なのは、SafeHandle を導入すると、SafeHandle から派生していない型が生のネイティブ リソースを含む必要がなくなるという点です。 その代わりに、オブジェクトにはマネージ リソース (使用するネイティブ リソースの SafeHandle のサブクラスなど) だけを含める必要があります。

このパターンに従うことにはいくつかの利点があります。 第一に、SafeHandles がネイティブ リソースを所有するため、SafeHandles 以外のクラスがファイナライザを持つ必要はありません。 第二に、各 SafeHandle がネイティブ リソースを 1 つだけ所有する簡潔なモデルを作成でき、マネージ オブジェクトが必要に応じてさまざまな SafeHandle を構成できます。

まとめ

CLR のガベージ コレクタのおかげで、管理者はマネージ ヒープに割り当てられたメモリの管理に煩わされずに済むようになりました。しかし、その他の種類のリソースに対しては依然としてクリーンアップを実行する必要があります。 ガベージ コレクタがオブジェクトのファイナライズを実行する前にコンシューマが高価なリソースを解放できるようにするには、マネージ クラスに IDisposable インターフェイスを使用します。 Disposable パターンに従い、注意する必要がある問題を認識することにより、クラスのすべてのリソースを正しくクリーンアップでき、Dispose を呼び出してクリーンアップ コードを直接実行する場合とファイナライザ スレッドを使用してクリーンアップ コードを実行する場合の両方で問題の発生を防ぐことができます。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Shawn Farkasは、Microsoft の CLR セキュリティ チームのソフトウェア設計エンジニアで、セキュリティ ポリシー、暗号化、および ClickOnce に携わっています。 彼のブログは、blogs.msdn.com/shawnfa で公開されています。


データ ポイント: ADO.NET Entity Framework の概要

データ ポイント
ADO.NET Entity Framework の概要
John Papa

コードのダウンロード : : DataPoints2007_07.exe (1590KB)
コンテンツ
Entity Framework のコンポーネント
Entity Data Model
Entity Data Model を生成する
CSDL を詳細に分析する
ストアにマップする
継承を定義する
複数テーブルにマップする
EntityClient を使用する
Object Services を使用する
LINQ to Entities を使用する
ラッピングする

Visual Studio® の次期リリース (コードネーム "Orcas") の ADO.NET には、新機能 Entity Framework が搭載されます。 Entity Framework を使えば、開発者は、論理/リレーショナル データ モデルではなく、オブジェクト モデルによるデータに集中できます。 Entity Framework は論理データ スキーマを概念モデルに抽象化するのに役立ち、Object Services および EntityClient と呼ばれる新しいデータ プロバイダを通じて概念モデルとやりとりする複数の方法を提供します。 今月のコラムでは、Entity Framework とは何か、Entity Framework をアプリケーションに組み込む方法、および Entity Framework の設計およびプログラミング方法について説明します。

Entity Framework は概念層、マッピング層、および論理層を使用して論理データベース構造を抽象化します。 このコラムでは、これらの各層の目的を説明します。 また、EntityClient を紹介し、概念層の Entity Data Model (EDM) を処理できる新しい言語、Entity SQL についても説明します。

EntityClient の代替機能に Object Services があります。 特に、Entity Framework の Object Services を使用すると、開発者が記述しなければならないデータ アクセス コードの量が少なくなります。 このコラムでは、Entity SQL での Object Services の使い方、および LINQ to Entities を使って EDM の概念エンティティにアクセスし、概念エンティティを取得する方法についても説明します。

この記事の例では、すべて "Orcas" の Beta 1 に存在する ADO.NET と Entity Framework を使用しています。

Entity Framework のコンポーネント

Entity Framework を使用すると、開発者が記述するデータ アクセス コードの量が減り、保守が軽減され、データ構造を実務により近い方法で (より少ない正規化の方法で) 抽象化することが可能になり、データの永続性も確保しやすくなります。 LINQ to Entities (後述) を使用すれば、概念モデルから厳密に型指定されたクラスを生成するため、コンパイル時のエラー数も少なくなります。

Entity Framework は、開発者がコードを記述することが可能な概念モデルを生成します。 このモデルでは、新しいデータ プロバイダ EntityClient と、T-SQL に類似した新しい言語 Entity SQL を使って、直接通信を行うことができます。 EntityClient には、見慣れた ADO.NET オブジェクトに類似したモデルがあり、EntityConnection オブジェクトと EntityCommand オブジェクトを使用して DbDataReader を返します。 別のオプションとして、開発者は Object Services を使うことも可能です。この場合、Entity SQL で ObjectQuery オブジェクトを使用するか、または LINQ to Entities を使用します。 Object Services を使用すると、開発者は概念モデルから生成したクラスを利用できます。このクラスには、厳密に型指定されたオブジェクトや永続性などの機能があります (図 1 を参照)。

図 1 Entity Framework の概要
図 1 Entity Framework の概要

これらのデータ アクセス手法を利用して、開発者は EDM の概念エンティティにアクセスすることができます。 EDM の層は XML ファイルとして存在します。この時点で、コマンド ライン ツール (EDMGEN.EXE) を手動で実行するか、Visual Studio のウィザードを使って EDM を生成できます。

Entity Data Model

Entity Framework の中心にあるのはそのモデルです。 Entity Framework は、データベースからのリレーショナル スキーマを表す論理ストア モデルをサポートします。 リレーショナル データベースのデータは、多くの場合、そのデータを使用するアプリケーションとは別に保存されます。 そのため、通常、開発者は、データを、データを含むデータベースと同じ構造で取得する必要があります。 多くの場合、開発者は、ビジネス ルールの処理により適したビジネス エンティティにデータを送ります。 次の例では、リレーショナル データベースからのスキーマを論理モデルで表し、ビジネス エンティティを概念モデルで表しています。 Entity Framework では、この 2 つのモデルの中間にマッピング層を設けます。 つまり、Entity Framework のモデルでは 3 層がアクティブになります。

  • 概念層
  • マッピング層
  • 論理層

この 3 層構造では、データをリレーショナル データベースから、よりオブジェクト指向の強いビジネス モデルにマップすることができます。 Entity Framework には、XML ファイルを使ってこれらの層を定義する手段があります。 また、Entity Framework は、概念モデルのスキーマに基づいて一連のクラスを生成します。 これらのクラスをプログラミングして、データに直接アクセスすることができます。 このような一定レベルの抽象化により、開発者はリレーショナル モデルではなく、概念モデルでプログラミングすることが可能です。 Entity Framework は、概念モデルでコーディングされたすべてのコマンドを論理モデルにマップします (図 2 を参照)。

図 2 Entity Data Model の設計
図 2 Entity Data Model の設計(画像を縮小するには、ここをクリックします)
図 2 Entity Data Model の設計
図 2 Entity Data Model の設計 (画像を拡大するには、ここをクリックします)

概念モデルは、概念スキーマ定義言語 (CSDL: Conceptual Schema Definition Language) を使用して XML ファイルに定義されます。 CSDL は、エンティティおよびリレーションシップを、それらを認識しているアプリケーションのビジネス層として定義します。 データベース スキーマを表す論理モデルは、ストア スキーマ定義言語 (SSDL: Store Schema Definition Language) を使って XML ファイルに定義します。 たとえば、データベースの複数テーブルから実際にデータを派生する概念モデル内にエンティティがあるとします。 概念モデルおよび論理モデルはエンティティを 1 対 1 で関連付けることができますが、 EDM の真価は、1 対 1 にリンクさせる必要がないという点にあります。 マッピング層は、マッピング スキーマ言語 (MSL : Mapping Schema Language) を使用して定義され、概念層と論理層を関連付けます。 このマッピングにより、開発者は概念モデルでコードを記述し、その命令を論理モデルにマップすることができます。

Entity Data Model を生成する

データベースを開始点として使って EDM を生成することができます。 その後、XML を手動で (または Visual Studio の今後のリリースで提供される可能性があるモデリング ツールを使用して) 変更することもできます。 ADO.NET EDM をプロジェクトに追加すると、ウィザードに EDM の作成手順が示されます。

"Orcas" に付属のサンプル Northwind データベースから EDM を生成することを選択した後、図 3 のように、モデル化が可能なオブジェクトの一覧から選択するように求められます。 筆者はすべてのテーブルのモデル化を選択し、ウィザードで Northwind のすべてのテーブルの CSDL、SSDL、および MSL ファイルを生成することにしました。 もちろん、これはテーブルとエンティティの 1 対 1 のマッピングです。 このマッピングをビジネス要件に合わせて変更するには、複数エンティティの組み合わせや継承を利用します。

図 3 EDM ウィザード
図 3 EDM ウィザード(画像を縮小するには、ここをクリックします)
図 3 EDM ウィザード
図 3 EDM ウィザード (画像を拡大するには、ここをクリックします)

ウィザードは、ドメイン モデルを表す CSDL からも一連のクラスを生成します。 これらのクラスの一部を [クラス ビュー] ウィンドウに表示したものが図 4 です。 最初に 1 対 1 でマッピングされていたため、すべてのテーブルにクラスが存在します。 EDM ウィザードは、データベースの Customers と Orders とのリレーションシップについて、対応する関連付けを概念モデル用に作成しました。 そのため、Customers クラスには Orders というナビゲーション プロパティが存在し、このプロパティを使って、開発者は Customers のインスタンスを特定の顧客に関連付けられた任意の Orders インスタンスに移動することができます。

図 4 クラス
図 4 クラス

CSDL を詳細に分析する

CSDL ファイル内のメタデータには、EntityType 要素で表されるエンティティ リストおよび Association 要素で表されるリレーション リストが含まれます (これは Beta 1 の名前付けスキームには反してしていることに注意してください)。 先に筆者が生成した CSDL ファイルの一部分を図 5 に示します。 エンティティには、エンティティを定義するスカラ プロパティ リストが含まれます。 Key 属性は、どのプロパティがエンティティのキーであるかを示します。 複合キーは、各プロパティ名をスペースで区切って示します。 エンティティに NavigationProperty と呼ばれる特殊なプロパティが存在する場合もあります。 このプロパティは、関連付けに従った他のエンティティへの移動方法を定義します。

Figure 5 CSDL の EntityType の定義 (Close Figure 5)
<EntityType Name=”Customers” Key=”CustomerID”>
    <Property Name=”CustomerID” Type=”String” Nullable=”false”
MaxLength=”4000” FixedLength=”true” />
    <Property Name=”CompanyName” Type=”String” Nullable=”false”
MaxLength=”4000” />
    <Property Name=”ContactName” Type=”String” MaxLength=”4000” />
    <Property Name=”ContactTitle” Type=”String” MaxLength=”4000” />
    <Property Name=”Address” Type=”String” MaxLength=”4000” />
    <Property Name=”City” Type=”String” MaxLength=”4000” />
    <Property Name=”Region” Type=”String” MaxLength=”4000” />
    <Property Name=”PostalCode” Type=”String” MaxLength=”4000” />
    <Property Name=”Country” Type=”String” MaxLength=”4000” />
    <Property Name=”Phone” Type=”String” MaxLength=”4000” />
    <Property Name=”Fax” Type=”String” MaxLength=”4000” />
    <NavigationProperty Name=”Orders”
Relationship=”NorthwindModel.FK_Orders_Customers”
FromRole=”Customers” ToRole=”Orders” />
  </EntityType>

Customer と Orders との AssociationType は、次の CSDL フラグメントで定義します。

  <Association Name=”FK_Orders_Customers”>
     <End Role=”Customers” Type=
         ”NorthwindModel.Customers”
         Multiplicity=”0..1” />
     <End Role=”Orders” Type=
         ”NorthwindModel.Orders” Multiplicity=”*” />
  </Association>

AssociationType の End 要素は関連付けの参加者を示します。 この例では、Customers エンティティは Orders エンティティに関連付けられています。 Customers エンティティを Multiplicity の定義によって任意の数の Orders エンティティに関連付けることもできます。

EntityType 要素および AssociationType 要素がドメイン エンティティの種類を定義するのに対して、EntitySet 要素および AssociationSet 要素はドメイン エンティティのスコープを定義します。 論理的にグループ化する必要があるすべての "セット" が EntityContainer 要素に含まれます (CSDL の詳細については、付属のダウンロード ファイルに含まれている NorthwindEntities.csdl ファイルを参照してください)。

次の CSDL フラグメントは、EntityContainer とそのコンテンツの一部を示しています。

<EntityContainer Name=”NorthwindEntities”>
    <EntitySet Name=”Customers”
        EntityType=”NorthwindModel.Customers” />
    <EntitySet Name=”Orders”
        EntityType=”NorthwindModel.Orders” />
    <AssociationSet Name=”FK_Orders_Customers”
            Association=”NorthwindModel.FK_Orders_Customers”>
        <End Role=”Customers” EntitySet=”Customers” />
        <End Role=”Orders” EntitySet=”Orders” />
    </AssociationSet>

</EntityContainer>

このフラグメントは、EntityType が Customers および Orders の EntitySet を示しています。 ここで、FK_Orders_Customers という名前の AssociationSet も宣言されています。 このフラグメントでは、Customers エンティティおよび Orders エンティティと、2 つのエンティティ間のリレーションシップを定義しています。

ストアにマップする

SSDL ファイルはデータベース内のリレーショナル データの構造を定義します。 このファイルでは EntityType 要素と AssociationType XML 要素も使用され、この場合はそれぞれテーブルの構造とデータベースの外部キーを宣言します。 SSDL ファイルの名前空間は EDM で使われているデータベース名に基づいて設定されますが、EntityContainer 要素にはデータベース スキーマの名前が付けられます。 EntityContainer には一連の EntitySet 要素および AssociationSet 要素が含まれます。これらの要素は、テーブルのインスタンスと、EntityType および AssociationType で表されるリレーションシップを宣言します。 SSDL ファイルの各 EntitySet に対応するデータベース テーブルが 1 つずつ存在します。

データベースから EDM を生成し、何も変更しないで CSDL ファイルおよび SSDL ファイルをそのまま開くと、2 つのファイルはきわめて類似していることがわかります。 これは、モデルを直接データベースから生成したことにより、概念モデルが論理ストアに直接マップされたためです。 MSL ファイルには、CSDL から SSDL への直接マップが含まれます。 EDM に対して記述されたすべてのクエリは、生成済み SQL コマンドに変換されます。 Entity Framework では、生成済み SQL クエリの代わりにストアド プロシージャを使用することもサポートされています。

EntityContainerMapping 要素を使って、モデル (CSDL) をストア (SSDL) にマップします。 StorageEntityContainer 属性は、ストア内の EntityContainer の名前を示し、EdmEntityContainer 属性は、モデル内の対応する EntityContainer を示します。 モデルの EntitySet をストアの EntitySet にマップするには、EntitySetMapping 要素を使用する必要があります。 Name 属性はモデルの EntitySet の名前を定義し、TableName 属性はストア内の対応する EntitySet の名前を定義します。 モデルの各プロパティは、ScalarProperty 要素によってストアにマップされます。 図 6 に MSL フラグメントを示します。

Figure 6 MSL での継承されたエンティティのマッピング (Close Figure 6)
<cs:EntitySetMapping cs:Name=”Products”>
  <cs:EntityTypeMapping cs:TypeName=”NorthwindModel.Products”>
    <cs:TableMappingFragment cs:TableName=”Products”>
      <cs:ScalarProperty cs:Name=”ProductID” cs:ColumnName=”ProductID” />
      <cs:ScalarProperty cs:Name=”ProductName”
          cs:ColumnName=”ProductName” />
      <cs:ScalarProperty cs:Name=”QuantityPerUnit”
          cs:ColumnName=”QuantityPerUnit” />
      <cs:ScalarProperty cs:Name=”UnitPrice” cs:ColumnName=”UnitPrice” />
      <cs:ScalarProperty cs:Name=”UnitsInStock”
          cs:ColumnName=”UnitsInStock” />
      <cs:ScalarProperty cs:Name=”UnitsOnOrder”
          cs:ColumnName=”UnitsOnOrder” />
      <cs:ScalarProperty cs:Name=”ReorderLevel”
          cs:ColumnName=”ReorderLevel” />
      <!--<cs:ScalarProperty cs:Name=”Discontinued”
          cs:ColumnName=”Discontinued” />-->
      <cs:Condition cs:ColumnName=”Discontinued” cs:Value=”0”/>
    </cs:TableMappingFragment>
  </cs:EntityTypeMapping>
<!--</cs:EntitySetMapping>
<cs:EntitySetMapping cs:Name=”DiscontinuedProducts”>-->
  <cs:EntityTypeMapping cs:TypeName=
        ”NorthwindModel.DiscontinuedProducts”>
    <cs:TableMappingFragment cs:TableName=”Products”>
      <cs:ScalarProperty cs:Name=”ProductID” cs:ColumnName=”ProductID” />
      <cs:ScalarProperty cs:Name=”ProductName”
          cs:ColumnName=”ProductName” />
      <cs:ScalarProperty cs:Name=”QuantityPerUnit”
          cs:ColumnName=”QuantityPerUnit” />
      <cs:ScalarProperty cs:Name=”UnitPrice” cs:ColumnName=”UnitPrice” />
      <cs:ScalarProperty cs:Name=”UnitsInStock”
          cs:ColumnName=”UnitsInStock” />
      <cs:ScalarProperty cs:Name=”UnitsOnOrder”
          cs:ColumnName=”UnitsOnOrder” />
      <cs:ScalarProperty cs:Name=”ReorderLevel”
          cs:ColumnName=”ReorderLevel” />
      <cs:Condition cs:ColumnName=”Discontinued” cs:Value=”1”/>
    </cs:TableMappingFragment>
  </cs:EntityTypeMapping>
</cs:EntitySetMapping>

継承を定義する

EDM は、データベースと完全に 1 対 1 対応していないモデルもサポートします。 たとえば、Northwind データベースを使用して、Products クラスのすべてのプロパティを継承し、Discontinued 列が 1 の製品のみを含む DiscontinuedProducts クラスを作成することができます (DiscontinuedProducts クラスにプロパティを追加することも可能です)。これは簡単な継承のシナリオですが、EDM を使って継承を実装する方法がわかります。

概念モデルに DiscontinuedProducts クラスを作成するには、まず、CSDL ファイルを開いて、DiscontinuedProducts という名前で新しい EntityType を作成し、BaseType 属性を NorthwindModel.Products (スキーマ名とベース EntityType 名) に設定します。 派生 EntityType は、Keys を含む Products EntityType のプロパティを継承します。 そのため、例では、新しい派生 EntityType の Key 属性またはプロパティを指定していません。 また、Products の EntityType の Discontinued プロパティはコメント アウトしてあります。 この操作を行うには、CSDL ファイルに次のようなコードを追加する必要があります。

<EntityType Name=”DiscontinuedProducts”
    BaseType=”NorthwindModel.Products”/>

次に、MSL ファイルを開き、Products の EntitySetMapping 要素を見つけて TypeName 属性と TableName 属性を削除します。 これらの属性は各 EntityType ごとに設定されます。 次に、EntityTypeMapping 子要素を作成して、TypeName を NorthwindModel.Products に設定します。 ベース EntityType を継承している各 EntityType の EntitySetMapping に EntityTypeMapping 要素を含める必要があります。 EntityTypeMapping 要素の子を TableMappingFragment という名前で作成し、TableName 属性を Products に設定します。 基本的には、これらの手順でマッピングを EntitySetMapping 要素から、より詳細なレベルに移動していきます。

Discontinued プロパティのマッピングをコメント アウトし、Discontinued が 0 のレコードのみを対象とすることを示す Condition 要素を追加します。 EntityTypeMapping XML フラグメント全体をコピーし、Name 属性を DiscontinuedProducts に変更して、Condition の値を 1 に変更します。MSL ファイルの新しいフラグメントを図 6 に示します。

複数テーブルにマップする

EDM が純粋な 1 対 1 モデルをストア マッピングに変えることができるもう 1 つの方法として、モデル内の 1 つのエンティティをストア内の複数のテーブルにマップするという方法があります。 サンプル Northwind データベースの Contacts テーブルと ContactNameSplit テーブルとの間には 1 対 1 のリレーションシップがあり、モデル内の 1 つのエンティティに結合することができます。 この例では、Contacts テーブルのすべての列と、ContactNameSplit テーブルの Title 列および Name 列を含むモデルにエンティティを作成することにします。

まず、CSDL ファイルの Contacts の EntityType を変更して、 Name プロパティと Title プロパティを追加します。

次の変更はより詳細になります。 今度は、モデル内のこれらの新しい 2 つのプロパティを MSL ファイルが存在するストアにマップする必要があります。 Contacts の EntitySet の EntitySetMapping 要素を変更して、複数テーブルへのマッピングを表すようにする必要があります。 この例では、TableName 属性と TypeName 属性を削除することによって、Contacts の EntitySet の既存の EntitySetMapping タグを変更しています。 モデルの EntitySet がストアの EntitySet と 1 対 1 でマップされている場合、これらの属性は EntitySetMapping 要素のみで宣言されます。

モデルの EntitySet からストアの Contacts の EntitySet へのマッピングは削除されているため、これに代わるものを作成する必要があります。 その代替要素が EntityTypeMapping と呼ばれる子要素です。 これらの要素を 2 つ作成し、それぞれストアの Contacts と ContactNameSplit を表すようにする必要があります。 EntityTypeMapping 要素は各 EntitySets の TypeName 属性を定義します。

各 EntityTypeMapping 要素の内部には、TableMappingFragment と呼ばれる子要素が存在します。 この要素には、ストアの EntitySet に相当する TableName 属性が含まれます。 TableMappingFragment には、モデルのプロパティをストアにマップするすべての ScalarProperty 要素が定義されます。 図 7 は、ストアの Contacts テーブルと ContactSplitName テーブルをモデルの 1 つの EntitySet にマップするように変更した Contacts の EntitySetMapping を示しています。

Figure 7 テーブルをエンティティに組み合わせる (Close Figure 7)
<cs:EntitySetMapping cs:Name=”Contacts”>
  <cs:EntityTypeMapping cs:TypeName=”NorthwindModel.Contacts”>
    <cs:TableMappingFragment cs:TableName=”Contacts”>
      <cs:ScalarProperty cs:Name=”ContactID”
          cs:ColumnName=”ContactID” />
      <cs:ScalarProperty cs:Name=”ContactType”
          cs:ColumnName=”ContactType” />
      <cs:ScalarProperty cs:Name=”CompanyName”
          cs:ColumnName=”CompanyName” />
      <cs:ScalarProperty cs:Name=”ContactName”
          cs:ColumnName=”ContactName” />
      <cs:ScalarProperty cs:Name=”ContactTitle”
          cs:ColumnName=”ContactTitle” />
      <cs:ScalarProperty cs:Name=”Address” cs:ColumnName=”Address” />
      <cs:ScalarProperty cs:Name=”City” cs:ColumnName=”City” />
      <cs:ScalarProperty cs:Name=”Region” cs:ColumnName=”Region” />
      <cs:ScalarProperty cs:Name=”PostalCode”
          cs:ColumnName=”PostalCode” />
      <cs:ScalarProperty cs:Name=”Country” cs:ColumnName=”Country” />
      <cs:ScalarProperty cs:Name=”Phone” cs:ColumnName=”Phone” />
      <cs:ScalarProperty cs:Name=”Extension”
          cs:ColumnName=”Extension” />
      <cs:ScalarProperty cs:Name=”Fax” cs:ColumnName=”Fax” />
      <cs:ScalarProperty cs:Name=”PhotoPath”
          cs:ColumnName=”PhotoPath” />
    </cs:TableMappingFragment>
  </cs:EntityTypeMapping>
  <cs:EntityTypeMapping cs:TypeName=”ContactNameSplit”>
    <cs:TableMappingFragment cs:TableName=”ContactNameSplit”>
      <cs:ScalarProperty cs:Name=”ContactID” cs:ColumnName=”ID” />
      <cs:ScalarProperty cs:Name=”Name” cs:ColumnName=”Name” />
      <cs:ScalarProperty cs:Name=”Title” cs:ColumnName=”Title” />
    </cs:TableMappingFragment>
  </cs:EntityTypeMapping>

EntityClient を使用する

Entity Framework の概念モデルへのアクセスは、3 つの異なるメカニズムのいずれかを通して行います (図 1 を参照)。 ここでは、EntityClient という新しい .NET データ プロバイダを導入します。

EntityClient は、概念モデルと通信を行うときに、Entity SQL という固有のテキスト ベース言語を使用して論理ストアから抽象化されます。 EntityClient を使用して実行されるすべての Entity SQL クエリは、ストアに送信するコマンド ツリーにコンパイルされます。 概念モデルによる Entity SQL からストアへのクエリの変換処理は Entity Framework が行います。

EntityClient のクラスは、一般的な ADO.NET プロバイダのクラスに似ています。 たとえば、EntityClient のクエリの実行には EntityCommand オブジェクトを使いますが、このオブジェクトを EDM に接続するには、EntityConnection オブジェクトが必要です。 EntityClient は EDM のエンティティにアクセスしたとき、エンティティのインスタンスを返すのではなく、DbDataReader オブジェクトのすべての結果を返します。 EntityClient では、行と列の標準セットを返すことも、DbDataReader を使ってより複雑な階層データを返すことも可能です。

図 8 は、EntityClient を使って概念モデルに接続し、ロンドンから Customers (顧客) リストを取得する例を示しています。 EntityConnection には、概念層の完全な接続文字列または App.Config ファイルの接続文字列名を指定できます。 接続文字列には、メタデータ ファイル (CSDL、MSL、および SSDL ファイル) のリストと、ストアに関するデータベース固有の接続文字列情報が含まれます。 図 8 の完全な接続文字列の例を次に示します。

Figure 8 Entity SQL で EDM を取得する EntityClient (Close Figure 8)
string city = “London”;
using (EntityConnection cn =
    new EntityConnection(“Name=NorthwindEntities”))
{
    cn.Open();
    EntityCommand cmd = cn.CreateCommand();
    cmd.CommandText =
         “SELECT VALUE c FROM NorthwindEntities.Customers “ +
         “AS c WHERE c.City = @city”;
    cmd.Parameters.AddWithValue(“city”, city);
    DbDataReader rdr = cmd.ExecuteReader(
        CommandBehavior.SequentialAccess);
    while (rdr.Read())
        Console.WriteLine(rdr[“CompanyName”].ToString());
    rdr.Close();
}

“metadata=.\NorthwindEntities.csdl|.\NorthwindEntities.ssdl|.
\NorthwindEntities.msl;provider=System.Data.SqlClient;provider connection string=’Data Source=DDVPC01
\SQLEXPRESS;Initial Catalog=Northwind;
Integrated Security=True’”

図 8 のコードは、EntityConnection オブジェクトを作成し、このオブジェクトに対して EntityCommand を実行する方法を示しています。 Entity SQL で記述されたクエリは、EDM の Customers EntitySet を参照しています。 構文は、意図的に T-SQL と同様にしてあります (Entity SQL の構文に関する参考資料が必要な場合は、"Orcas" の Beta 1 リリースに関する MSDN® のドキュメントを参照してください)。

図 8 のように、EntityParameter オブジェクトを追加することもできます。 Beta 1 の時点では、EntityClient は DML クエリをサポートしていませんが、将来的にはサポートされる予定です。 LINQ to Entities など、他の手段で DML を実行することも可能です。

Object Services を使用する

EDM で表されるデータにアクセスするには、Object Services を使用するという方法もあります。 Object Services には、オブジェクトを読み込み、EDM に定義されたリレーションシップを操作する方法が用意されています。 図 1 に示すように、Object Services は EntityClient を使ってデータを取得します。 Object Services は ID 解決を追加します。DataSet を使用する場合、この処理は手動になります。 また、Object Services にはオブジェクトの永続性および変更追跡の機能があり、イベントを使って明示的な読み込みおよび保存を行うことが可能です。 これによりサーバーへのラウンドトリップが減少します。

Object Services にはオブジェクト リストを直接返す機能があり、投影または定義済みエンティティを返すことができます。 たとえば、Object Services を使って、EDM に定義されている List<Customers> を取得することができます。 Customers オブジェクトを確認し、値を変更して、データをデータベースに保存することも可能です。

Object Services で投影を使った場合、返されるデータは更新可能オブジェクトです。 投影で返されるのはエンティティ全体ではなく、エンティティの特定のプロパティであるため、投影されたデータの更新を Object Services でデータベースに保存することはできません。 データを更新する場合は、投影を使用するのではなく、エンティティ全体を返した方が便利です。

Object Services では、Entity SQL を使ったクエリを実行したり、LINQ to Entities を使ってクエリを作成することができます。 次の例は、Object Services と Entity SQL を使ったクエリで Customers のリストを取得する方法を示しています。

string city = “London”;
ObjectQuery<Customers> query = northwindContext.CreateQuery<Customers>(
    “SELECT VALUE c FROM Customers AS c WHERE c.City = @city”,
     new ObjectParameter(“city”, city));
foreach (Customers c in query) Console.WriteLine(c.CompanyName);

EDM では、EntityContainer は ObjectContext を継承したクラス (この例では northwindContext) によって表されます。 ObjectContext クラスは、ObjectQuery<T> インターフェイスの実装により、Entity SQL または LINQ を使ってクエリを作成することができます。

CreateQuery メソッドは、Customers エンティティのリストを取得するクエリを定義する、パラメータ化された Entity SQL ステートメントを受け取ります。 データベースを検索する実際の SQL ステートメントは、foreach ステートメントで ObjectQuery<Customers> の反復処理を行うときに実行されます。

LINQ to Entities を使用する

動的クエリを Entity SQL で記述し、Object Services を使用して EDM エンティティと対話することができます。 ただし、Entity Framework では、LINQ to Entities を使用して、厳密に型指定された EDM クラスを使うことも可能です。 たとえば、サンプルで示したように、Object Services と Entity SQL を使用したクエリを次のように変更して、LINQ to Entities で実行することができます。

string city = “London”;
var query = from c in northwindContext.Customers
            where c.City == city
            select c;
foreach (Customers c in query) Console.WriteLine(c.CompanyName);

このコード サンプルは、Entity SQL のすべてのテキスト ベースの構文を、C# 3.0 でサポートされる厳密に型指定された LINQ 構文に置き換えます。LINQ の詳細、および C# と Visual Basic® での LINQ のサポートの詳細については、2007 年 6 月の MSDN Magazine (msdn.microsoft.com/msdnmag/issues/07/06) (英語) を参照してください。

先ほど、Products を継承した DiscontinuedProducts という名前の EntityType を EDM に作成したことを思い出してください。 EDM の継承機能と LINQ および Object Services を使用して、生産中止になった製品のリストを取得することができます。 次の例では、where 句に Discontinued = 1 を指定していません。その代わりに、製品エンティティの種類を派生 EntityType である DiscontinuedProducts と照合し、先ほどマッピング (MSL ファイル) に作成した条件を使用して、生産中止になった製品のみを取得する適切な SQL ステートメントを生成します。

var query = from p in northwindContext.Products
            where p is DiscontinuedProducts
            select p;
foreach (Products p in query) Console.WriteLine(p.ProductName);

AssociationSet によって定義される EDM の組み込みリレーションシップを利用してクエリを記述することもできます。 たとえば、次の LINQ クエリ式で、ロンドンの Customers (顧客) の Orders (注文) リストを取得することが可能です。

var query = from o in northwindContext.Orders
            where o.Customers.City == “London”
            select o;
foreach (Orders o in query) Console.WriteLine(o.OrderID);

このコード サンプルは Orders エンティティから始まり、このエンティティのナビゲーション プロパティ Customers を使って City プロパティを確認します。 ロンドン以外の顧客からの注文はすべてフィルタされ、取得対象から除外されます。 Orders エンティティのリストが返されるため、エンティティを変更し、その変更をデータベースに保存することも可能です。 変更をデータベースに保存するには、SaveChanges メソッドを使います。

次の例では、London の顧客のリストを取得し、各顧客の Country プロパティを "United Kingdom" に設定します。 変更は StateManager に永続化されますが、SaveChanges メソッドを実行しない限り、データベースには書き込まれません。

var query = from c in northwindContext.Customers
            where c.City == “London”
            select c;
foreach (Customers c in query) c.Country = “United Kingdom”;
northwindContext.SaveChanges();

エンティティの新しいインスタンスを作成し、ObjectContext の AddObject メソッドを使って EDM に追加することもできます。 次の例は、新しいカテゴリをデータベースの Categories テーブルに追加する方法を示しています。

Categories newCat = new Categories();
newCat.CategoryName = “Other”;
northwindContext.AddObject(newCat);
northwindContext.SaveChanges();
int newCatID = newCat.CategoryID;

まず、Categories エンティティのインスタンスを作成し、CategoryName プロパティを設定します。 次に、AddObject メソッドを使って、新しいカテゴリを EDM に追加します。 SaveChanges メソッドを呼び出すと、新しい Category をデータベースに挿入し、新しい行の CategoryID を返す SQL ステートメントが生成されます。

エンティティに他のエンティティとのリレーションシップがある場合、新しいエンティティを既存のエンティティに関連付けることができます。 たとえば、新しい Orders エンティティを作成し、そのエンティティの Customers プロパティを既存の Customers エンティティに設定することができます。 データベースの Orders テーブルには CustomerID 列がありますが、EDM は、関連付けられた Customers エンティティに移動するために、Customers プロパティを使用してリレーションシップをオブジェクト指向の手法で表します。

Orders newOrder = new Orders();
newOrder.OrderDate = DateTime.Today;
Customers cust = northwindContext.Customers.Where(
    “it.CustomerID = ‘ALFKI’”).First();
newOrder.Customers = cust;
northwindContext.AddObject(newOrder);
northwindContext.SaveChanges();

ラッピングする

Entity Framework では、開発者は論理/リレーショナル データ モデルではなく、オブジェクト モデルを用いることにより、データに集中することができます。 EDM の設計およびリレーショナル ストアへのマップが完了したら、EntityClient、ObjectServices、LINQ などのさまざまな方法でオブジェクトにアクセスすることができます。

DataSet、DataAdapter、DbConnection、DbCommand などの従来のオブジェクトは Visual Studio "Orcas" で提供される ADO.NET の次期リリースでも引き続きサポートされますが、同時に、ADO.NET に新たな素晴らしい可能性を切り開く重要な機能が Entity Framework に追加されます。

John の連絡先は

mmdata@microsoft.com.

です。
John Papaは、ASPSOFT (aspsoft.com) で働く .NET シニア コンサルタントです。野球を熱狂的に愛し、夏の夜のほとんどを家族および忠犬 Kadi (カーディ) と共にヤンキースの応援に費やします。 C# MVP でもある John は、ADO、XML、および SQL Server に関する何冊かの書籍の執筆を担当しました。また、主に業界のカンファレンス (VSLive など) での講演やブログ (codebetter.com/blogs/john.papa または www.johnpapa.net) などで活躍しています。


Cutting Edge: AJAX を使用した状況によって異なるフィードバック

Cutting Edge
AJAX を使用した状況によって異なるフィードバック
Dino Esposito

コードのダウンロード : : CuttingEdge2007_07.exe (170KB)
コンテンツ
更新の進行状況を把握するための組み込みのツール
UpdateProgress の制限事項
進行状況のインジケータ パターン
進行状況モニタ サーバーの API
サンプル ページを作成する
進行状況モニタのクライアント API
クライアント呼び出し用のイベント シンク
サンプル ページを完成する
UpdatePanel コントロールのヒント
まとめ

コンピュータ アプリケーションのユーザーが時間のかかる可能性がある操作を開始した場合、その動作が進行中であること、および動作結果をしばらく取得できない場合があることを示すように、ユーザー インターフェイスを更新するのが最適です。これは、デスクトップ アプリケーションでは簡単に実現できますが、Web シナリオでは難しい場合があります。Web アプリケーションでは、操作開始直前に "しばらくお待ちください" などのテキストを表示するのは簡単ですが、操作完了までの推定時間や作業完了度などの役立つフィードバックを提供する必要がある場合は、実現するのが少し困難です。これは、Web アプリケーションの時間のかかるタスクの多くはサーバー上で実行されるのに、状態情報をクライアントにプッシュする組み込みのメカニズムがないためです。

2006 年 9 月と 10 月 の Cutting Edge のコラムで、ASP.NET Web アプリケーションの進行状況バーについて説明しました。最初のコラムでは、ASP.NET スクリプト コールバックを使用した純粋な ASP.NET 2.0 ソリューションに注目しました。2 番目のコラムでは、ASP.NET AJAX Extensions に基づいたソリューションを構築しました。コラムを執筆した時点では、ASP.NET AJAX はベータ以前の段階でした。おそらくご存知だと思いますが、そのときから多くのことが変わっています。今月のコラムでは、同じトピックをもう一度取り上げ、サーバーとクライアントの ASP.NET AJAX インフラストラクチャを構築して時間のかかる可能性があるサーバー側のタスクを開始および制御します。

更新の進行状況を把握するための組み込みのツール

ASP.NET AJAX Extensions では 2 つの異なるプログラミング モデルをサポートしています。各モデルは、"部分的なレンダリング" と "直接的なリモート メソッド呼び出し" です。部分的なレンダリングは、ASP.NET の従来のコントロール ベースのプログラミング モデルが進化したものです。このモデルでは、相互に依存せずに更新できるページ内の領域やページ全体の領域を定義できます。特定の領域または領域のサブセットに関連するポストバック イベントが、領域内のコントロールのマークアップの生成をトリガします。その結果、ページのほんの一部だけが更新されます。これらのコントロールが更新されても (別のページに明示的に移動しない限り) 新しいページは表示されませんが、現在表示されているページのドキュメント オブジェクト モデル (DOM) が更新されます。

部分的なレンダリングでは、依然として分離コード メソッドとサーバー側のイベント ハンドラを使用して、ページの期待する動作を定義します。ページの部分的な更新は ASP.NET AJAX ランタイム環境によって完全に管理されます。いくつかのクライアント イベントはパブリックに公開されますが、部分的なレンダリングは基本的にはブラックボックス プロセスです。ユーザーがボタンをクリックすると、システムはポストバックが必要であると判断し、クライアント側の AJAX インフラストラクチャを使用して非同期要求を行います。データの準備が完了すると、現在のページの DOM が自動的に更新されます。

サーバー側のポストバック イベントは完了するのに時間がかかる場合があるので、ASP.NET AJAX にはユーザーへの通知を維持するための UpdateProgress コントロールが用意されています。図 1 は、部分的なレンダリングを使用して進行状況画面を表示する ASP.NET AJAX のサンプル ページを示しています。

Figure 1 UpdatePanel コントロールと UpdateProgress コントロールを含むページ (Close Figure 1)
<%@ Page CodeFile=”LongUpdates.aspx.cs” Inherits=”LongTask” %>

<html>
<head runat=”server”>
    <title>Lengthy Operations and Updates</title>
    <style type=”text/css”>
        #UpdateProgress1
        {
            width: 30%; background-color: #FFC080; height:15%;
            top: 40%; left: 35%; position: absolute;
            border: solid 1px black;
        }
        #ProgressTemplate1
        {
            font-weight: bold; font-size: 12pt; color: navy;
            font-family: Verdana; background-color: #ffff99;
        }
    </style>
</head>

<body>
<form id=”form1” runat=”server”>
        <asp:ScriptManager ID=”ScriptManager1” runat=”server” />

        <asp:UpdatePanel ID=”UpdatePanel1” runat=”server”
                         UpdateMode=”Conditional”>
            <ContentTemplate>
                <asp:Button runat=”server” ID=”Button1”
                  Text=”Start Task ...” onclick=”Button1_Click” />
                <hr />
                <asp:Label runat=”server” ID=”Label1” /><br />
            </ContentTemplate>
        </asp:UpdatePanel>

        <hr />

        <asp:UpdateProgress runat=”server” ID=”UpdateProgress1”>
            <ProgressTemplate>
                <div ID=”ProgressTemplate1”>
                   <img alt=”” src=”Images/indicator.gif” />
                   <span id=”Msg”>Please, wait ... </span>
                </div>
                <br /><br />
            </ProgressTemplate>
        </asp:UpdateProgress>
    </form>
</body>
</html>

UpdateProgress コントロールは、1 つ以上の UpdatePanel コントロールの内容が更新されたときに、ブラウザ ウィンドウに視覚的なフィードバックを自動的に提供するように設計されています。ページの開発者は、コントロールを明示的に表示および非表示にすることに関して心配する必要がありません。必要なのは、ユーザーに表示する視覚的なテンプレートと、そのスタイルと位置を定義することだけです。ASP.NET AJAX クライアント インフラストラクチャがその他すべての処理を引き受け、テンプレートを必要に応じて表示したり非表示にしたりします (図 2 参照)。

図 2 実行中の UpdateProgress コントロール
図 2 実行中の UpdateProgress コントロール(画像を縮小するには、ここをクリックします)
図 2 実行中の UpdateProgress コントロール
図 2 実行中の UpdateProgress コントロール (画像を拡大するには、ここをクリックします)

UpdateProgress コントロールは使いやすく、特にほんの数秒で完了する比較的速い操作に適しています。数秒では完了しないような時間のかかる操作の場合、もっと包括的なソリューションを見つける必要があります。

UpdateProgress の制限事項

UpdateProgress は多数の便利なプロパティを持つ優れたコントロールですが、UpdateProgress コントロールと実行されるコード間の対話がサポートされていないため、サーバー プロセス情報を取得することができません。そのうえ、UpdateProgress はサーバー操作の完了度を示す進行状況バーやサーバー操作の現在の状態を示すメッセージの一覧を表示できません。

他の場所に目を向ける別の理由は、UpdateProgress コントロールが部分的なレンダリング エンジンと UpdatePanel コントロール自体の両方と密接に連携していることにあります。そのため、このコントロールは、ASP.NET AJAX ページを使用してローカルの Web サービスまたはページ メソッド経由でサーバー メソッドを呼び出すと動作しません。ただし、同一の視覚的なテンプレートを再利用して、スクリプト コード経由で表示および非表示にすることができます。たとえば、図 1 の進行状況テンプレートは以下に示すように単純な <div> タグになります。

<div id=”UpdateProgress1” style=”display:none”>
    <div id=”ProgressTemplate1”>
        <img alt=”” src=”images/indicator.gif” />
        <span id=”Msg”>Please, wait ... </span>
    </div>
</div>

図 3 は、サーバー メソッドへのリモート呼び出しが行われたときに進行状況テンプレートを管理するのに必要な JavaScript コードを示しています。部分的な更新とリモート メソッドの呼び出しを実行するときに、タイムリーかつ有益なフィードバックを返す、クライアントとサーバーのインフラストラクチャを構築する方法を見てみましょう。

Figure 3 視覚的な進行状況テンプレートの表示と非表示 (Close Figure 3)
<script type=”text/javascript”>
function startTask()
{
    // Display the progress screen
    enableProgress(true);

    // Invoke the remote method (ExecuteTask in codebehind class)
    PageMethods.ExecuteTask(taskCompleted, taskFailed);
}

function taskCompleted(results, context, methodName)
{
    // Hide the progress screen and update the UI
    enableProgress(false);
    $get(“Label1”).innerHTML = results;
}

function taskFailed(results, context, methodName)
{
    // Hide the progress screen
    enableProgress(false);
}

function enableProgress(shouldShow)
{
    var template = $get(“UpdateProgress1”);
    if (shouldShow)
        template.style.display = “”;
    else
        template.style.display = “none”;
}
</script>

進行状況のインジケータ パターン

ページの部分的な更新を実行するときにどのアプローチを使用しても、操作中はクライアントに情報がプッシュされません。そのため、クライアントが状態情報を取得して進行状況バーを更新できる簡単な方法がありません。ただし、現在の状態が進んだときにその状態を既知の場所に書き込めるカスタム サーバー操作を作成することができます。その後、2 番目の操作として、その場所から定期的に状態情報を読み取って、クライアントに報告します。

たとえば、多数の個別のステップを踏むサーバー操作を想像してください。各ステップの最初に、サーバー コードによって、永続的なストアがタスク関連の情報の一部を使用して更新されます。それと同時に、クライアント制御の監視サービスがテキストを読み取り、読み取ったテキストを表示するためにクライアントに返します。この動作は AJAX の最も普及しているパターンである進行状況のインジケータ パターンに関連付けられています (一般的な AJAX デザイン パターンの詳細については、www.ajaxpatterns.org を参照してください)。

検討すべき一般的なシナリオは他にもあります。たとえば、ユーザーが一連のレコードをループするサーバー操作をトリガするとします。1 つの大規模なセット ベースのトランザクションを使用するストアド プロシージャですべての作業を行う場合、1 つのデータベース要求のみを発行するので、優れたパフォーマンスになります。ただし、ユーザーにフィードバックを提供する必要がある場合は、元のトランザクションを中間層で制御される複数のステップに分割することもできます。そうすると、トランザクションの状態が監視サービスに適切に公開されてから各ステップが処理されるので、システムの作業完了度 (35 パーセントなど) をユーザーに通知できます。図 4 は、進行状況のインジケータ パターンをグラフィカルに示しています。

図 4 進行状況のインジケータ パターン
図 4 進行状況のインジケータ パターン(画像を縮小するには、ここをクリックします)
図 4 進行状況のインジケータ パターン
図 4 進行状況のインジケータ パターン (画像を拡大するには、ここをクリックします)

ASP.NET AJAX の進行状況のインジケータ パターンを実装するプロセスは、3 つのステップから成ります。まず、データベース、ディスク ファイル、メモリの共有ブロックなどの永続的なストアから状態情報を読み書きする API を定義します。次に、クライアントが XMLHttpRequest を使用してサーバーに接続して現在の状態を読み取るためのイベント シンクを提供します。最後に、進行状況を把握するためにサーバーに定期的に接続する監視サービスを表す、クライアントの JavaScript API を設定します。ページの部分的な更新を開始するときに使用する技法が UpdatePanel コントロール、ページ、Web サービス メソッドのどれであっても動作するパターンを実装する必要があることに注意してください。

進行状況モニタ サーバーの API

既知のコンテナとの間で状態情報の読み書きを行うには、サーバー側のコントラクト ベースの API が必要です。図 5 は、特定のインターフェイスを実装する ProgressMonitor クラスのソース コードを示しています。このインターフェイスでは、タスクの状態を読み書きするために GetStatus と SetStatus という 2 つのメソッドが機能します。どのデータ コンテナを選択するかは、最終的にはクラスの作成者にお任せします。図 5 の ProgressMonitor クラスは、データを ASP.NET キャッシュに保存します。SetStatus メソッドが新しい項目を追加し、5 分という期限ポリシーを設定します。この値はニーズに合わせて変更できます。また、外部パラメータを使用して、構成ファイルから項目のキャッシュ期間をそのクラスが読み取れるようにすることもできます。期限を明示的に設定しておくと、ASP.NET キャッシュが不要な項目でいっぱいになるのを防ぐのに役立ちます。

Figure 5 ProgressMonitor サーバー クラス (Close Figure 5)
namespace Samples.Server
{
    public interface IProgressMonitor
    {
        void SetStatus(int taskID, object message);
        string GetStatus(int taskID);
    }

    public class ProgressMonitor : IProgressMonitor
    {
        // Sets the current status of the task
        public void SetStatus(int taskID, object message)
        {
            HttpContext.Current.Cache.Insert(
                taskID.ToString(),
                message,
                null,
                DateTime.Now.AddMinutes(5),
                Cache.NoSlidingExpiration);
        }

        // Reads the current status of the task
        public string GetStatus(int taskID)
        {
            object o = HttpContext.Current.Cache[taskID.ToString()];
            if (o == null)
                return String.Empty;

            return (string) o;
        }
    }
}

ProgressMonitor クラスは、時間のかかるタスクを実行および制御する ASP.NET サーバー側コードに明記しておく必要があります。以下のコードは、JavaScript クライアントに公開される ASP.NET AJAX ページ メソッドを示しています。このメソッドを呼び出すと、サーバー側の進行状況モニタが起動され、サーバー データ コンテナ (この場合は ASP.NET キャッシュ) に状態が格納されます。

[WebMethod]
public static string ExecuteTask(int taskID)
{
   ProgressMonitor progMonitor = new ProgressMonitor();
   progMonitor.SetStatus(taskID, “5% done”);
   DoStep1();
   progMonitor.SetStatus(taskID, “25% done”);
   DoStep2();
   ...
   progMonitor.SetStatus(taskID, “100% done”);
}

直接呼出しのためにクライアントに公開される ASP.NET AJAX Web サービス メソッドの本文にも同様のコードが必要です。別の例として、以下に、UpdatePanel コントロールがポストバックとその他のページの部分的な更新をトラップするために使用されるサーバー イベント ハンドラを示します。

protected void Button1_Click(object sender, EventArgs e)
{
    ProgressMonitor progMonitor = new ProgressMonitor();
    int taskID = RetrieveTaskID();

    progMonitor.SetStatus(taskID, “15% done”);
    DoStep1();
    ...
}

SetStatus メソッドは値を受け取って、内部データ コンテナに格納します。その値としては、文字列と数値が考えられます。一般的には、Microsoft® .NET Framework オブジェクトを使用して、タスクの状態を表すことができます。ただし、プリミティブ型以外を使用する場合は、JavaScript オブジェクトに適切に変換できることを保証する必要があります。つまり、状態情報は JavaScript Object Notation (JSON) ストリームにシリアル化する必要があり、同様の構造を持つ JavaScript クラスがクライアントで利用できる必要があります。最終的には、返される JSON 文字列をあらかじめ処理して、そのクラスのインスタンスを適切に初期化する必要があります。

2 つの個別のソフトウェア エンティティ (サーバー側の進行状況モニタとクライアント側の監視サービス) が同じタスクの状態で動作するため、タスクの一意識別子が必要です。クライアント上でランダムな番号のタスク ID が生成され、リモート メソッドに追加の引数として渡されます。タスク ID は、どのデータ コンテナを使用することにした場合でも状態情報を設定して取得するためのキーとして使用されます。

サンプル ページを作成する

どのように動作するのかを理解するため、1 つのボタンが含まれているサンプル ASP.NET AJAX ページを見てみましょう。ユーザーがボタンをクリックすると、リモート メソッド呼び出しが行われます。進行状況を監視するには、ProgressMonitor クラスを使用してクライアントからタスク ID を受け取るサーバー メソッドが必要です。以下に、リモート メソッドを呼び出す JavaScript コードを示します。

function startTask() {
    // Start the remote task using a page method
    var taskID = progressManager.getTaskID();
    PageMethods.ExecuteTask(taskID, taskCompleted, taskFailed);

    // Enable the monitoring service
    updateUI(true);
    progressManager.startMonitor(taskID, 2000, updateProgress,
        updateProgressCompleted);
}

ExecuteTask はクライアントから呼び出す ASP.NET AJAX ページ メソッドの名前です。このメソッドは、必要なすべての入力パラメータや、成功と失敗を示す通常のコールバックだけでなく、タスク ID を受け取ります。上記のコードでは、updateUI という JavaScript 関数が進行状況画面を表示するローカル関数です。

リスト内の progressManager オブジェクトは JavaScript クラスとして実装され、Microsoft AJAX ライブラリに基づいています。このオブジェクトはクライアント側の監視サービスを表しています。getTaskID メソッドは開始されているタスクの ID を返します。タスク ID は JavaScript ヘルパ クラスを使用して生成されるランダムな数値です。startMonitor メソッドはリモート呼び出しをサーバー ページに定期的に送信するタイマを設定します。サーバー コードはタスク ID を受け取ってデータ コンテナ (ASP.NET キャッシュ) に接続し、指定されたタスクの現在の状態を読み取ります。その後、見つかった値を返します。状態情報がクライアントにダウンロードされたら、updateProgress コールバックが呼び出されてクライアントの UI が更新されます。updateProgressCompleted コールバックは、タスクが進行状況画面のクリアを完了したときに呼び出されます。

progressManager オブジェクトは、ASP.NET AJAX ページが読み込まれるときに初期化されます。pageLoad という JavaScript 関数を使用して、Microsoft AJAX ライブラリが完全に初期化され設定されたときに読み込むページ上で実行するコードを定義します。

var progressManager = null;
function pageLoad() {
   progressManager = new Samples.Progress();
}

読み込み時に ASP.NET AJAX 固有のタスクを実行する必要がある場合は、本文の onload イベントよりも pageLoad メソッドの方が有用です。Samples.Progress クラスは、クライアント側の監視サービスで、タスクの状態を定期的にチェックしてユーザー インターフェイスを更新します。

進行状況モニタのクライアント API

Samples.Progress クラスは、2 つの JavaScript ファイル (progress.js および random.js) で定義されます。以下に示すように、これら両方のファイルは、<script> タグまたは ScriptManager コントロールのいずれかを使用してページにバインドされます。

<asp:ScriptManager ID=”ScriptManager1” runat=”server”
     EnablePageMethods=”true”>
  <Scripts>
      <asp:ScriptReference path=”random.js” />
      <asp:ScriptReference path=”progress.js” />
  </Scripts>
</asp:ScriptManager>

両方のスクリプト ファイルは Microsoft AJAX クライアント ライブラリを使用します。クラスは同じ名前空間で定義され、Microsoft AJAX クライアント ライブラリから提供されるオブジェクト指向の機能の拡張セットを使用して構築されます。クラスはプロトタイプ モデルを使用し、明示的なコンストラクタを必要とします (プロトタイプ モデルの詳細については、MSDN® Magazine 2007 年 5 月号に含まれている、Ray Djajadinata の JavaScript に関する記事を参照してください)。random.js ファイルでは Samples.Random クラスを定義しています。このクラスは 1 つのメソッドのみを所有します。所有するメソッドは、以下のように、JavaScript のネイティブ Math オブジェクトを使用してランダムな数を生成するものです。

Type.registerNamespace(‘Samples’);
Samples.Random = function Samples$Random() {
    Samples.Random.initializeBase(this);
}
function Samples$Random$getNumber(minNumber, maxNumber) {
    var num = minNumber + Math.floor(Math.random() * maxNumber);
    return num;
}
Samples.Random.prototype = {
    getNumber:  Samples$Random$getNumber
}
Samples.Random.registerClass(‘Samples.Random’);

これに対して、Samples.Progress クラスはもっと多くのことを行います (図 6 参照)。このクラスには基本的なパブリック メソッドが 3 つあります。それぞれは、getTaskID、startMonitor、および stopMonitor です。これに加えて、4 つ目のメソッドがあります。このメソッドは、時間のかかる可能性があるタスクを開始するときに UpdatePanel コントロールが使用される場合だけ呼び出されます。既に説明したように、getTaskID はタスクを一意に識別するために使用される、ランダムに生成される数値を返します。JavaScript の Math オブジェクトを信頼していない場合は、ページ メソッドまたは Web サービスへのサーバー側呼び出しを追加で配置して、GUID を要求することができます。

Figure 6 Samples.Progress JavaScript クラス (Close Figure 6)
//
// Samples.Progress ::
// Client-side monitoring service
//

// Define the namespace of the class
Type.registerNamespace(‘Samples’);

// Constructor
Samples.Progress = function Samples$Progress()
{
    Samples.Progress.initializeBase(this);
    this._timerID = null;
    this._taskID = null;
    this._msInterval = null;
    this._progressCallback = null;
    this._callback = null;
}

// Start the timer to periodically check the status of the ongoing task
function Samples$Progress$startMonitor(taskID, msInterval,
    progressCallback, progressCompletedCallback) {
    if (arguments.length !== 4) throw Error.parameterCount();

    // Update internal members
    _taskID = taskID;
    _msInterval = msInterval;
    _progressCallback = progressCallback;
    _progressCompletedCallback = progressCompletedCallback;

    this._startTimer();
}

// Stop the timer
function Samples$Progress$stopMonitor() {
    window.clearTimeout(_timerID);
    if (_progressCompletedCallback !== null)
        _progressCompletedCallback();
}

// Get task ID
function Samples$Progress$getTaskID(taskID) {
    return Samples.Random.getNumber(0, 10000000);
}

// Start the timer to control progress
function Samples$Progress$_startTimer() {
    this._callback = Function.createDelegate(this, this._checkProgress);
    _timerID = window.setTimeout(this._callback, _msInterval);
}

// Modify the request to add the task ID to a hidden field
//(for UpdatePanel pages)
function Samples$Progress$modifyRequestForTaskId(request, taskID,
    hiddenField) {
    var body = request.get_body();
    var token = “&” + hiddenField + “=”;
    body = body.replace(token, token + taskID);
    request.set_body(body);

    return request;
}

// Timer function(s)
function Samples$Progress$_checkProgress() {
    PageMethods.GetCurrentStatus(_taskID, this._onFeedbackReceived,
        this._onFeedbackFailed, this);
}

function Samples$Progress$_onFeedbackReceived(results, context) {
    context._startTimer();

    if (_progressCallback !== null)
        _progressCallback(results);
}

function Samples$Progress$_onFeedbackFailed(results) {
}

// Class prototype
Samples.Progress.prototype =
{
    getTaskID:              Samples$Progress$getTaskID,
    startMonitor:           Samples$Progress$startMonitor,
    stopMonitor:            Samples$Progress$stopMonitor,
    modifyRequestForTaskId: Samples$Progress$modifyRequestForTaskId,
    _startTimer:            Samples$Progress$_startTimer,
    _checkProgress:         Samples$Progress$_checkProgress,
    _onFeedbackReceived:    Samples$Progress$_onFeedbackReceived,
    _onFeedbackFailed:      Samples$Progress$_onFeedbackFailed
}

// Register the new class
Samples.Progress.registerClass(‘Samples.Progress’);

startMonitor メソッドは監視するタスクの ID と、必要な間隔 (ミリ秒単位) を受け取ります。さらに、このメソッドは 2 つのコールバックも受け取ります。1 つは状態情報を使用してユーザー インターフェイスを更新するためのもので、もう 1 つは操作終了時にユーザー インターフェイスをリセットするためものです。

ウィンドウの setTimeout 関数を使用して実装されたタイマは、その間隔の経過後に内部メソッドをコールバックします。メソッドは _checkProgress という名前で、次のようになります。

function Samples$Progress$_checkProgress() {
    PageMethods.GetCurrentStatus(_taskID,
           this._onFeedbackReceived,
           this._onFeedbackFailed,
           this);
}

このメソッドが行う主要なことの 1 つは、サーバーでタスクの現在の状態を受け取るイベント シンクを呼び出すことです。イベント シンクは、パブリックに公開されているメソッドで、JavaScript クライアントから呼び出すことができます。今回の実装では、GetCurrentStatus というページ メソッドを想定しています。イベント シンクへの呼び出しは、従来の ASP.NET AJAX リモート メソッドの呼び出しです。そのため、成功か失敗かどうかを示すコールバックが必要なので、オプションでコンテキスト オブジェクトを実行することもできます。このとき、失敗の場合に必要な特別な操作はありません。ユーザー インターフェイスが更新されないだけです。そうではなく、重要な状態情報がクライアントにダウンロードされた場合は、以下に示すように、その都度ユーザー インターフェイスを更新し、次回の更新に備えてタイマを再起動する必要があります。

function Samples$Progress$_onFeedbackReceived(results, context) {
    context._startTimer();

    if (_progressCallback !== null)
        _progressCallback(results);
}

成功のコールバックでは、コンテキスト パラメータを使用して Samples.Progress オブジェクトを受け取り、タイマを再起動します。その後は、ページで定義されたコールバックを呼び出して、進行状況バーを更新するだけです。ASP.NET ページでは、ユーザーが操作を開始するためにクリックしたときに監視サービスを開始し、操作完了後に実行するコールバック (成功または失敗) からサービスを停止します。

クライアント呼び出し用のイベント シンク

リモート操作と同様、監視サービスは定期的に繰り返されるリモート呼び出しに基づいています。時間のかかる可能性がある操作のエンドポイントに加えて、サーバー上に、状態情報を取得するために JavaScript から呼び出せる 2 つ目のパブリック メソッドも必要です。ご存知のとおり、ASP.NET AJAX ページには、クライアントから呼び出し可能なエンドポイントを公開するための 3 つの方法があります。各方法は、ローカル Web サービス、ページ メソッド、および UpdatePanel コントロールです。これらの技法のいずれかを使用して、監視するタスクと監視サービスを実装できます。

ただし、リモート タスクの実行に別の UpdatePanel を既に使用している場合、Timer コントロールにバインドされた UpdatePanel を使用して、進行状況バーを定期的に更新することができません。仕様では、UpdatePanel の更新が同じページ要求マネージャ オブジェクトの制御下で一度に一回実行されます。新しい要求が投稿されると、この管理オブジェクトによって保留中の要求が中止されます。このため、UpdatePanel コントロールから時間のかかるタスクを呼び出し、別のタイマ ベースの UpdatePanel を使用してフィードバックを提供する場合、更新された状態に対する最初の要求によって監視対象の操作が中止されます。

さらに、2 つの UpdatePanel の更新は、同時に実行するとビュー ステートが不適切に更新されることがあるので、同時に実行することができません。したがって、ページを一貫した状態に保つには、UpdatePanels を経由した呼び出しをシリアル化する必要があります。

監視対象のタスクを開始する場合には UpdatePanel を使用することを検討し、その状態を同時に読み取るにはページ固有のメソッドを使用することをお勧めします。時間のかかる可能性がある操作を含む各ページにクライアントから呼び出し可能なページ メソッドを配置する必要はありません。以下に示すように、同じ基本クラスから監視可能なページを派生すれば十分です。

public class UpdateProgressPage : System.Web.UI.Page
{
   static ProgressMonitor _progMonitor = new ProgressMonitor();

   [WebMethod]
   public static string GetCurrentStatus(int taskID)
   {
      return _progMonitor.GetStatus(taskID);
   }
}

GetCurrentStatus というパブリック メソッドが progress.js ファイルにハードコードされていることに注意してください。UpdateProgressPage クラスのインターフェイスを変更すると、progress.js スクリプトのソース コードの編集も必要になります。

サンプル ページを完成する

それでは、ASP.NET AJAX ページに監視機能を追加するためのおおまかな手順を説明しましょう。たとえば、タスクがページの分離コードまたは進行状況モニタ サーバーの API にアクセスできるある種の中間層から制御されているとします。

まずは、パブリック ページ メソッドまたはリンクされている Web サービス メソッドとしてページに実行時間の長いメソッドを追加します。次に、<script> タグまたは ScriptManager コントロールを使用して、progress.js ファイルと random.js ファイルをページに追加します。ページ クラスは UpdateProgressPage から継承するか、次のプロトタイプを使用して GetCurrentStatus 静的 Web メソッドを公開する必要があります。

public string GetCurrentStatus(int taskID)

この時点で、操作を開始および監視するスクリプト コードを追加します。重要な要素は、現在の状態でユーザー インターフェイスを更新するために startMonitor に渡すコールバック関数です。この関数の実行内容は、進行状況の表示に使用されるページのマークアップによって異なります。メッセージを表示するために <span> タグだけがある場合、コールバックの適切な例は次のようになります。

function updateProgress(perc) {
    $get(“Msg”).innerHTML = perc + “ done.”;
}

コールバック関数が、進行状況を示す値を受け取ります。受け取る値は、進行状況モニタ サーバーの API で指定する文字列、割合などの形式になります。次のコードを使用して進行状況を通知するとします。

progMonitor.SetStatus(taskID, “5”);

updateProgress コールバック関数が "5" という値を受け取ります。この場合、作業完了の割合を渡します。別の方法として、現在の状態を説明するテキストを渡すこともできます。

updateProgress コールバックは、ユーザー インターフェイスの更新に全責任があります。従来の進行状況メーターを表示する場合、この場所にそのコードを記述する必要があります。以下に、これを行うための 1 つの方法を示します。

function updateProgress(perc) {
   var table = “<table width=100%><tr><td>{2}%</td></tr>” +
               “<tr><td bgcolor=blue width=’{0}%’>&nbsp;</td>” +
               “<td width=’{1}%’></td></tr></table>”;
   table = String.format(table, perc, 100-perc, perc);
   $get(“Msg”).innerHTML = table;
}

動的な <table> タグを構築して、その行の 1 つを 2 つのセルに分割します。左側のセルは、作業の完了度に応じて幅が増えていき、別の色で描画されます。図 7 は最終結果を示しています。ブラウザ ウィンドウの下部にある Web Development Helper ツールは、ブラウザのすべての要求をログに記録します (このツールは Nikhil Kothari 氏のブログ (www .nikhilk.net) から入手できます)。ご覧のとおり、監視サービスは 2 秒ごとにフィードバックの呼び出しを発行します (このことは、Timestamp 列をご覧になればわかります)。各要求の応答は、作業の完了度を示す数値で構成されます。この数値は updateProgress に渡されて、動的 HTML テーブルの構築に使用されます。

図 7 状況によって異なる、実行中の AJAX 進行状況バー
図 7 状況によって異なる、実行中の AJAX 進行状況バー(画像を縮小するには、ここをクリックします)
図 7 状況によって異なる、実行中の AJAX 進行状況バー
図 7 状況によって異なる、実行中の AJAX 進行状況バー (画像を拡大するには、ここをクリックします)

このコラムの完全なソース コードは、MSDN Magazine Web サイトから入手できます。このプロジェクトでは、3 つの ASP.NET AJAX ページが含まれていて、各ページでは異なるシナリオが採用されています。ページ メソッドとして実装した時間のかかる操作はローカル Web サービスのメソッドとして表示され、部分的な更新操作内でトリガされます。どの場合でも、監視サービスはページの基本クラスから継承されたページ メソッドに依存します。

UpdatePanel コントロールのヒント

今月のコラムを仕上げるため、UpdatePanel コントロールを使用してリモート タスクを開始および監視する場合に発生する問題を指摘しておきます。スクリプトを使用して操作を開始する場合は、必ずタスク ID を明示的に生成して渡します。サーバー コードはタスク ID を引数として受け取って続行します。UpdatePanel コントロールが使用されている場合、ユーザーはボタンなどのポストバック コントロールをクリックするだけでタスクを開始します。それでは、タスク ID をサーバーに渡すには、どうすればよいのでしょうか。クライアントからサーバーにカスタム データを送信するときは、常に非表示フィールドを使用するのが適しています。非表示フィールドを追加するには、非表示フィールドをページ マークアップに明示的に追加するか、以下のようにプログラムを使用します。

void Page_Load(object sender, EventArgs e)
{
    ScriptManager.RegisterHiddenField(UpdatePanel1,
          “HiddenField1”, “”);
}

通常の ASP.NET 2.0 ページのように ClientScript.RegisterHiddenField を呼び出す代わりに、このコードを使用するのはなぜですか。ScriptManager クラスのメソッドを使用すると、部分的なポストバックの間でも非表示フィールドが維持されるようになります。サーバーでは、次のコードを使用してタスク ID を取得します。

int taskID;
Int32.TryParse(Request[“HiddenField1”], out taskID);

しかし、監視を開始するには、どうすればよいのでしょうか。さいわい、部分的な更新を行う ASP.NET AJAX クライアント コンポーネントには、クライアント イベント モデルがあります。部分的な更新の開始時と終了時に、beginRequest および endRequest という 2 つの JavaScript イベントが発生します。

function pageLoad() {
   progressManager = new Samples.Progress();
   reqManager = Sys.WebForms.PageRequestManager.getInstance();
   reqManager.add_beginRequest(onBeginRequest);
   reqManager.add_endRequest(onEndRequest);
}

ページの部分的な更新要求が送信される直前に onBeginRequest に関連付けれたコードが実行されます。以下に、サンプル ハンドラを示します。

function onBeginRequest(sender, args) {
    // Get the task ID
    taskID = progressManager.getTaskID();

    // Must pass the taskID via the hidden field.
    progressManager.modifyRequestForTaskId(args.get_request(),
         taskID, “HiddenField1”);

    // Too late for this to work
    progressManager.startMonitor(taskID, 2000,
         updateProgress, updateProgressCompleted);
}

beginRequest イベントが発生したときに、非表示フィールドの内容をタスク ID に設定するのでは遅すぎます。より正確に説明すると、イベントが発生したときに、要求の本文は既に準備されているので、それ以上更新されません。そのため、この時点で非表示フィールドを設定しても役に立ちません。非表示フィールドを使用してタスク ID 値を送信するには、要求の本文を変更する必要があります。

function modifyRequestForTaskId(request, taskID, hiddenField) {
    var body = request.get_body();
    var token = “&” + hiddenField + “=”;
    body = body.replace(token, token + taskID);
    request.set_body(body);

    return request;
}

要求の本文は、&HiddenField= 形式のトークンを含むプレーンな文字列です。このトークンをタスク ID &HiddenField=xyz を含む別のトークンに置き換えます。最後に、endRequest イベント ハンドラで、Samples.Progress クラスの stopMonitor メソッドを呼び出します。

まとめ

進行中のタスクを監視することは、Windows® アプリケーションでは比較的容易ですが、Web アプリケーションではそうはいきません。しかし、AJAX プログラミング モデルを使用すれば、このタスクを非常に容易に実現できます。ASP.NET AJAX Extensions ではクライアント API とサーバー API を使用して必要なインフラストラクチャを構築するための既存のツールが提供されているためです。

ご意見やご感想は、

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Espositoは、Solid Quality Learning の指導者であり、『Microsoft ASP.NET 2.0 AJAX Extensions の概要』(Microsoft Press、2007 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。cutting@microsoft.com (英語のみ) で連絡を取るか、weblogs.asp.net/despos のブログに参加してください。このトピックに関するその他のヒントについては、最近再開された彼のブログを定期的にチェックしてください。


サービス ステーション: WCF バインドの詳細

サービス ステーション
WCF バインドの詳細
Aaron Skonnard

コードのダウンロード : : ServiceStation2007_07.exe (162KB)
コンテンツ
バインドとは
組み込みのバインド
バインドを構成する
カスタム バインドを定義する
カスタム バインドを簡単に使用できるようにする
バインド記述を共有する
まとめ

Windows® Communication Foundation プログラミング モデルによって、開発者はさまざまな通信形式およびメッセージ プロトコルでサービスを簡単に構成できます。 バインドは、この単純さの裏にある隠れた面白味です。 今月は、組み込みバインドの動作、その構成方法、およびカスタム バインドを自分で定義する方法を詳細に示します。

バインドとは

結局のところ、Windows Communication Foundation は XML メッセージを処理するサービスを構築するためのフレームワークです。 Windows Communication Foundation では、各種トランスポート プロトコル (HTTP、TCP、MSMQ など) を使用し、さまざまな XML 表現 (テキスト、バイナリ、または Windows Communication Foundation では一般にメッセージ エンコーディングと呼ばれる MTOM) を使用して、メッセージを転送できます。 また、セキュリティ、信頼性のあるメッセージング、およびトランザクションに関連する各種 WS-* 仕様などの SOAP プロトコル一式で特定のメッセージング相互作用を拡張できます。 この 3 つの通信概念 (トランスポート、メッセージ エンコーディング、およびプロトコル一式) は、すべて実行時の通信の中核となります。

Windows Communication Foundation では、これらの通信の詳細がすべてチャネル スタックで処理されます (図 1 を参照)。 名前が示すように、チャネル スタックはチャネル コンポーネントのスタックであり、ランタイム処理中にすべてのメッセージがチャネル スタックを通じて渡されます。 一番下のコンポーネントは、トランスポート チャネルです。 このチャネルは、特定のトランスポート プロトコルを実装し、外部からの受信メッセージを読み取ります。 トランスポート チャネルは、メッセージ エンコーダを使用して、受信バイトをさらに処理するために論理的な Message オブジェクトに読み取ります。

図 1 バインドとチャネル スタック
図 1 バインドとチャネル スタック(画像を縮小するには、ここをクリックします)
図 1 バインドとチャネル スタック
図 1 バインドとチャネル スタック (画像を拡大するには、ここをクリックします)

その後、メッセージは残りのチャネル スタックを通じてバブルアップされ、最終的に最上部に到達し、Windows Communication Foundation が最後のメッセージをサービス実装にディスパッチするまで、各プロトコル チャネルに処理の機会が与えられます。 メッセージには、途中で大量の変換が行われます。

Windows Communication Foundation チャネル アーキテクチャでは一貫した柔軟なメッセージング基盤が提供されますが、ほとんどの開発者にとってこれを直接処理するのは非常に手間がかかります。 チャネル スタックを構築する際には、コンポーネントの順序と、コンポーネント相互に互換性があるかどうかを慎重に考慮する必要があります。 このため、Windows Communication Foundation が代わりに、基礎となるチャネル スタックを間接的に構成するためのより単純な抽象化 (エンドポイントの概念) を提供します。

つまり、さまざまな通信シナリオに適応するために、1 つ以上のエンドポイントでサービスを構成します。 各エンドポイントについて、アドレス、バインド、およびコントラクトを指定します。 アドレスは、メッセージをリッスンするネットワーク アドレスを単純に指定し、コンストラクトは、指定されたアドレスに到着するメッセージに何が含まれている必要があるかを指定します。 一方バインドは、メッセージを正しく処理するために必要なチャネル スタックを構築するためのレシピを提供します。 サービスを読み込むと、Windows Communication Foundation はバインド記述で概説されている手順に従って各チャネル スタックを作成します。 バインドは、途中にあるチャネル スタックを通じてサービス実装を通信にバインドします (図 1 を参照)。

この通信モデルでは、必要なメッセージング機能について単純な用語で考えることができ、Windows Communication Foundation ランタイム環境で実際に発生する複雑さを無視することもできます。

メッセージ処理の基礎の詳細な背景については、WCF メッセージングの基礎に関する 2007 年 4 月のコラムを参照してください。

組み込みのバインド

Windows Communication Foundation プログラミング モデルでは、バインドは System.ServiceModel.Channels.Binding クラスによって表現されます。 すべてのバインド クラスは基本クラスから派生する必要があります。 図 2 に、Windows Communication Foundation に付属する組み込みのバインド クラスをまとめます。 各クラスは Binding から派生し、実装を通じて異なるチャネル スタック構成を定義します。 これらの組み込みバインドは、現時点で起こりうる最も一般的なメッセージング シナリオに対処します。 既定では、トランザクション フローは常に無効となっています。トランザクション フローを有効にすることを選択した場合に既定で使用されるプロトコルを表に示しています。 また、MsmqIntegrationBinding は Windows Communication Foundation メッセージ エンコーディングを使わずに、Windows Communication Foundation より前のシリアル化形式を選択できることにも注目してください。

Figure 2 Windows Communication Foundation の組み込みバインディング (Close Figure 2)

バインドのクラス名 | トランスポート | メッセージング エンコーディング | メッセージ バージョン | セキュリティ モード | 信頼性のあるメッセージング | トランザクション フロー (既定では無効)
BasicHttpBinding | HTTP | テキスト | SOAP 1.1 | なし | サポートされていません | サポートされていません
WSHttpBinding | HTTP | テキスト | SOAP 1.2 WS-Addressing 1.0 | メッセージ | 無効 | WS-AtomicTransactions
WSDualHttpBinding | HTTP | テキスト | SOAP 1.2 WS-Addressing 1.0 | メッセージ | 有効 | WS-AtomicTransactions
WSFederationHttpBinding | HTTP | テキスト | SOAP 1.2 WS-Addressing 1.0 | メッセージ | 無効 | WS-AtomicTransactions
NetTcpBinding | TCP | バイナリ | SOAP 1.2 | トランスポート | 無効 | OleTransactions
NetPeerTcpBinding | P2P | バイナリ | SOAP 1.2 | トランスポート | サポートされていません | サポートされていません
NetNamedPipesBinding | 名前付きパイプ | バイナリ | SOAP 1.2 | トランスポート | サポートされていません | OleTransactions
NetMsmqBinding | MSMQ | バイナリ | SOAP 1.2 | メッセージ | サポートされていません | サポートされていません
MsmqIntegrationBinding | MSMQ | サポートされていません (WCF より前のシリアル化形式を使用) | サポートされていません | トランスポート | サポートされていません | サポートされていません
CustomBinding | ユーザーが決定 | ユーザーが決定 | ユーザーが決定 | ユーザーが決定 | ユーザーが決定 | ユーザーが決定

たとえば、BasicHttpBinding は、相互運用性が最重要事項であるシナリオ用に設計されました。 このため、BasicHttpBinding はトランスポートに HTTP を使用し、メッセージ エンコーディングにテキストを使用します。 メッセージ エンコーディングの重要な側面は、予想されるメッセージング バージョンです。BasicHttpBinding ではこれは SOAP 1.1 です。 追加のプロトコルについては、BasicHttpBinding はトランスポートまたはメッセージ セキュリティを使用できますが、どちらも既定では無効になっています。 その他の WS-* プロトコルは、このバインドではサポートされていません。 その結果、このバインドによって、他の基本的な Web サービス実装との相互運用が可能な単純なチャネル スタックが生成されます。これは、正常に動作させることが第 1 の優先順位である場合に適した選択肢です。

次に、WSHttpBinding があります。 これも、セキュリティ、信頼性のあるメッセージング、およびトランザクション用のより豊富な Web サービス プロトコルを組み込む一方で相互運用性を実現するように設計されました。 このため、WSHttpBinding もトランスポートに HTTP を使用し、メッセージング エンコーディングにテキストを使用しますが、メッセージ バージョンには SOAP 1.2 を WS-Addressing 1.0 と共に使用します。これらは、追加の Web サービス プロトコル ヘッダを伝送するために必要です。 バインドにより、(WS-Security とその仲間を通じた) メッセージベースのセキュリティが可能になり、有効にすることを選択した場合は WS-ReliableMessaging と WS-AtomicTransactions をサポートできます。 WSHttpBinding は、より高度なチャネル スタックを生成し、そのほとんどがフレームワークとプラットフォーム間の統合が要求されるエンタープライズ シナリオに制約されます。

また、NetTcpBinding も提供されます。 2 つの HTTP バインドとは異なり、各種 "Net" バインドは相互運用性を実現するようには設計されていません。 実際に、Microsoft® .NET Framework 3.0 が両方の側にインストールされていると想定できる場合に、それぞれ異なる通信シナリオを最適化するように設計されました (これにより、バインド名の先頭に "Net" が付いている理由が説明されます)。

NetTcpBinding は、トランスポートに TCP を使用し、メッセージ エンコーディングにバイナリを使用し、メッセージ バージョンに SOAP 1.2 を使用します。 これにより、トランスポート セキュリティが既定で有効になり、トランスポート セキュリティが有効になっているとトランザクションがサポートされます。 このように、バインドの構成は Windows 環境でより適切に実行されるチャネル スタックの作成に重点を置いており、各種 COM+ および .NET Remoting への投資を置換するための優れたオプションが提供されます。

ユーザーは、通信ニーズに基づいて特定のバインドを選択するだけでよく (図 2 を参照)、適切なチャネル スタックの生成は Windows Communication Foundation で行われます。 ServiceHost.AddServiceHost の呼び出しに目的のバインド クラスのインスタンスを提供することにより、バインドをプログラムで選択します。

using (ServiceHost host = new ServiceHost(typeof(ChatService)))
{
    host.AddServiceEndpoint(typeof(IChat),
        new BasicHttpBinding(), “http://localhost:8080/chat”);
    host.AddServiceEndpoint(typeof(IChat),
        new WSHttpBinding(), “http://localhost:8080/chat/secure”);
    host.AddServiceEndpoint(typeof(IChat),
        new NetTcpBinding(), “net.tcp://localhost:8081/chat”);

    host.Open();

    ... // remaining code ommitted for brevity
}

または、ホストの構成ファイルでエンドポイントを定義する際に、バインド名を指定できます。 構成を通じてエンドポイントを指定する場合、バインド名はキャメル形式で記述する必要があります (たとえば、basicHttpBinding、wsHttpBinding、netTcpBinding など)。 図 3 に、ホストの構成ファイルに同じ 3 つのエンドポイントを構成する方法を示します。

Figure 3 ホストの構成ファイルでエンドポイントを構成する (Close Figure 3)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”ChatService”>
        <endpoint
           address=”http://localhost:8080/chat”
           binding=”basicHttpBinding”
           contract=”IChat”/>
        <endpoint
           address=”http://localhost:8080/chat/secure”
           binding=”wsHttpBinding”
           contract=”IChat”/>
        <endpoint
           address=”net.tcp://localhost:8081/chat”
           binding=”netTcpBinding”
           contract=”IChat”/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

図 2 の残りのバインドは、他の通信シナリオを対象としています。 たとえば、HTTP で双方向通信を行う必要がある場合は、WSDualHttpBinding を使用できます。また、非同期の永続メッセージングを実装する場合は、NetMsmqBinding を使用できます。 豊富なクライアント アプリケーションにピア ツー ピア機能が必要な場合は、NetPeerTcpBinding を利用できます。

これらの組み込みバインドのいずれにも対応していない通信シナリオを思い付くのは困難です。 一方、特定の統合の問題に対応するには、多くの場合組み込みバインドの 1 つを構成する必要があります。

バインドを構成する

クラスにある各種コンストラクタとプロパティを利用して、組み込みバインドを構成できます。 バインド オブジェクトをインスタンス化した後で、オブジェクトを ServiceHost.AddServiceEndpoint に渡す前に書き込み可能なパブリック プロパティを変更できます。

次の例は、MTOM およびトランスポート セキュリティ (HTTPS) を使用するように BasicHttpBinding オブジェクトを構成する方法を示しています。

BasicHttpBinding basicHttpBinding = new BasicHttpBinding();
basicHttpBinding.MessageEncoding = WSMessageEncoding.Mtom;
basicHttpBinding.Security.Mode = BasicHttpSecurityMode.Transport;

host.AddServiceEndpoint(
    typeof(IChatService),
    basicHttpBinding,
    “http://localhost:8080/chat”);
...

<bindings> 構成セクションを使用して、構成で同じことを完全に実現できます。 このメカニズムにより、任意の組み込みバインド クラスに対して多くの名前付きバインド構成を定義できます。 さらに、<endpoint> の bindingConfiguration 属性を通じて特定のバインド構成をエンドポイントに適用できます。 図 4 に、basicConfig という名前のバインド構成の作成方法を示します。この構成は、MTOM とトランスポート セキュリティを使用するようにも設定されます。

Figure 4 バインド構成を作成する (Close Figure 4)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”ChatService”>
        <endpoint address=”http://localhost:8080/chat”
          binding=”basicHttpBinding”
          bindingConfiguration=”basicConfig”
          contract=”ChatLibrary.IChat” />
        ...
      </service>
    </services>
    <bindings>
      <basicHttpBinding>
        <binding name=”basicConfig” messageEncoding=”Mtom”>
          <security mode=”Transport”/>
        </binding>
      </basicHttpBinding>
      ...
    </bindings>
  </system.serviceModel>
</configuration>

<endpoint> 定義がバインド名の basicHttpBinding とバインド構成の basicConfig をどのように参照するかに注目してください。 複数のエンドポイントで異なるバインド構成を使用する必要がある場合は、既知の各バインド クラスに複数のバインド構成を定義できます。 例については、図 5 を参照してください。 この構成ファイルでは、BasicHttpBinding に対して 2 つのバインド構成を作成しました。どちらもトランスポート セキュリティを有効にしますが、一方はテキストを使用し、もう一方は MTOM を使用します。 最初のエンドポイントは basicConfig1 を使用するように構成され、2 つ目のエンドポイントは basicConfig2 を使用するように構成されます。両方のエンドポイントは同じバインド クラスを使用しますが、2 つの異なる構成を使用します。

Figure 5 バインド構成の例 (Close Figure 5)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”ChatService”>
        <endpoint address=”text”
           binding=”basicHttpBinding”
           bindingConfiguration=”basicConfig1”
           contract=”IChat” />
        <endpoint address=”mtom”
           binding=”basicHttpBinding”
           bindingConfiguration=”basicConfig2”
           contract=”IChat” />
        <endpoint address=”secure”
           binding=”wsHttpBinding”
           bindingConfiguration=”wsConfig”
           contract=”IChat” />
        <endpoint address=””
           binding=”netTcpBinding”
           bindingConfiguration=”tcpConfig”
           contract=”IChat” />
      </service>
    </services>
    <bindings>
      <basicHttpBinding>
        <binding name=”basicConfig1” messageEncoding=”Text”>
          <security mode=”Transport”/>
        </binding>
        <binding name=”basicConfig2” messageEncoding=”Mtom”>
          <security mode=”Transport”/>
        </binding>
      </basicHttpBinding>
      <wsHttpBinding>
        <binding name=”wsConfig” transactionFlow=”true”>
          <security mode=”TransportWithMessageCredential”>
            <message clientCredentialType=”UserName”/>
          </security>
          <reliableSession enabled=”true” ordered=”true”/>
        </binding>
      </wsHttpBinding>
      <netTcpBinding>
        <binding name=”tcpConfig” transactionFlow=”true”
            transactionProtocol=”WSAtomicTransactionOctober2004”>
          <security mode=”None”/>
          <reliableSession enabled=”true” />
        </binding>
      </netTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

図 5 には、いくつかの処理を行う wsConfig という名前の WSHttpBinding 用の構成も含まれています。 この構成により、トランザクション フローと順序付きの信頼性のあるメッセージングが有効になります。 また、セキュリティ モードを TransportWithMessageCredential に変更し、クライアント資格情報の種類として UserName を指定します。したがって、チャネル スタックはトランスポート セキュリティ (HTTPS) を使用するように構成されますが、受信メッセージには WS-Security <UsernameToken> 要素が含まれていることが期待されます。

図 5 に示す最終的なバインド構成は、NetTcpBinding の既定を変更します。 この例では、tcpConfig という名前のバインド構成がトランザクション フローを有効にし、トランザクション プロトコルを OleTransactions (既定) から 2004 年 10 月バージョンの WS-AtomicTransaction に変更します。 また、既定ではオンになるトランスポート セキュリティを無効にし、WS-ReliableMessaging を有効にします。

Windows Communication Foundation バインド構成メカニズムは、ほとんどの統合シナリオに適応できます。 ただし、解決できない不明確なシナリオの処理で行き詰まった場合でも、ニーズを満たすカスタム バインドを常に定義できます。

カスタム バインドを定義する

カスタム バインドは、System.ServiceModel.Channels.Binding からクラスを派生させることで定義します。 実装には、1 つの主要な責務があります。 それは、BindingElement オブジェクト (BindingElementCollection 型のオブジェクト) の順序付きコレクションを生成することです。 バインド インスタンスは、実行時に CreateBindingElements メソッドを呼び出すことでこれを行うよう求められます。 この時点で、バインド インスタンスは現在の構成を参照し、必要なバインド要素オブジェクトを作成し、コレクション内で正しく並べ替えてからそれを返す必要があります。

Windows Communication Foundation ランタイムは、バインド要素コレクションを調べ、これを使用して基礎となるチャネル スタックを作成します (これは筆者が基本的に "レシピ" と呼んでいるものです)。 バインド要素と、実行時にチャネル スタックに入るオブジェクトの間には、1 対 1 のマッピングがあります。

各組み込みバインド クラスは、Binding から派生し、CreateBindingElements の一意の実装を提供します。 .NET Reflector (aisto.com/roeder/dotnet で入手できます) を取り出し、各クラスの実装を表示する必要があります。 それぞれが使用するバインド要素と、コレクション内でのそれらの要素の順序を正確に理解します。 カスタム バインドの作成時にまったく同じことを行うため、これは重要です。

ただし、カスタム バインドを作成する前に、使用可能なバインド要素クラスをよく理解する必要があります。 バインド要素は、System.ServiceModel.Channels.BindingElement から派生するクラスです。 Windows Communication Foundation には、すぐに使用できる BindingElement クラスが多数付属しています。

図 6 ～ 9 で、現在 Windows Communication Foundation にある組み込み BindingElement クラスの大部分を説明します。 図 6 に、Windows Communication Foundation でサポートされているさまざまなトランスポート プロトコルを表すクラスの一覧を示します。 図 7 では、各タイプのトランスポート セキュリティを表すクラスについて説明します。 図 8 では、各メッセージ エンコーディングを表すクラスについて説明します。 最後に、図 9 では、セキュリティ、信頼性のあるメッセージング、およびトランザクション用の階層化プロトコルを表すクラスについて説明します。

Figure 9 プロトコル バインド要素 (Close Figure 9)

プロトコル | クラス | 要素
トランザクション フロー | TransactionFlowBindingElement | <transactionFlow/>
信頼性のあるメッセージング | ReliableSessionBindingElement | <reliableSession/>
セキュリティ | SecurityBindingElement | <security/>
Figure 8 メッセージ エンコーディング バインドの要素 (Close Figure 8)

メッセージ エンコーディング | クラス | 要素
テキスト | TextMessageEncodingBindingElement | <textMessageEncoding/>
MTOM | MtomMessageEncodingBindingElement | <mtomMessageEncoding/>
バイナリ | BinaryMessageEncodingBindingElement | <binaryMessageEncoding/>
Figure 7 トランスポート セキュリティ バインドの要素 (Close Figure 7)

トランスポート セキュリティ | クラス | 要素
Windows | WindowsStreamSecurityBindingElement | <windowsStreamSecurity/>
SSL | SslStreamSecurityBindingElement | <sslStreamSecurity/>
Figure 6 トランスポート バインドの要素 (Close Figure 6)

トランスポート | クラス | 要素
HTTP | HttpTransportBindingElement | <httpTransport/>
HTTPS | HttpsTransportBindingElement | <httpsTransport/>
TCP | TcpTransportBindingElement | <tcpTransport/>
名前付きパイプ | NamedPipeTransportBindingElement | <namedPipeTransport/>
MSMQ | MsmqTransportBindingElement | <msmqTransport/>
MSMQ | MsmqIntegrationBindingElement | <msmqIntegration/>
P2P | PeerTransportBindingElement | <peerTransport/>

これらの図に示されていない別の BindingElement があります。 これは CompositeDuplexBindingElement (<compositeDuplex/>) と呼ばれ、これらのグループのいずれにも分類されない特殊なケースです。 これは、二重通信をサポートするバインドを作成する必要がある場合に使用します。

CustomBinding をインスタンス化し、目的の BindingElement オブジェクトを Elements コレクションに追加することで、カスタム バインドを作成します。 ただし、これらのオブジェクトを追加する順序はとても重要です。 順序の詳細は次のとおりです。

  • トランザクション フロー (必須ではない)
  • 信頼性のあるメッセージング (必須ではない)
  • メッセージ セキュリティ (必須ではない)
  • コンポジット デュプレックス (必須ではない)
  • メッセージ エンコーディング (必須)
  • トランスポート セキュリティ (必須ではない)
  • トランスポート (必須)

コレクションは上から順に追加します。 カスタム バインドを定義する際に正式に必要となるのは、トランスポート バインド要素のみです。 メッセージ エンコーディングは各バインドに必要ですが、これを指定しない場合は、Windows Communication Foundation によって既定のエンコーディングが追加されます。 HTTP(S) の既定のエンコーディングはテキストで、その他すべてのトランスポートではバイナリです。

最もシンプルなカスタム バインドの 1 つを作成および使用する例を次に示します。

...
CustomBinding myHttpBinding = new CustomBinding();
myHttpBinding.Name = “myHttpBinding”;
myHttpBinding.Elements.Add(new HttpTransportBindingElement());

host.AddServiceEndpoint(typeof(IChat), myHttpBinding,
    “http://localhost:8080/chat/custom”);
...

追加した BindingElement オブジェクトは、HttpTransportBindingElement のインスタンスのみです。 この場合、Windows Communication Foundation は、チャネル スタックを構築する際にテキスト メッセージ エンコーディングの使用を選択します。これが HTTP の既定のエンコーディングであるためです。 この例では、組み込み BasicHttpBinding と同一になるバインドが生成されます。

これで、基礎となる BindingElement オブジェクトへの直接アクセスが可能になります。これにより、結果として生成されるスタック構成の柔軟性と制御が向上します。 組み込みバインド クラスの 1 つを使用する場合は、基礎となる BindingElementCollection を提供するファサードに限定されます。

図 10 に示す例について考えます。 この例では、個々の BindingElement オブジェクトをコレクションに追加する前に、これらのオブジェクトを構成します。 結果として生成されるバインドでは、標準の SOAP 1.1 以外のメッセージ バージョンが、2004 年 8 月バージョンの WS-Addressing と共に使用されます。この WS-Addressing は、古い Web サービス フレームワークと統合する必要がある場合に役立ちます。 また、カスタマイズされた HTTP トランスポート インスタンスも使用します。

Figure 10 個々の BindingElement オブジェクトを構成する (Close Figure 10)
...
// instantiate message encoding element and configure
TextMessageEncodingBindingElement text =
    new TextMessageEncodingBindingElement();
text.MessageVersion = MessageVersion.Soap11WSAddressingAugust2004;

// instantiate transport element and configure
HttpTransportBindingElement http = new HttpTransportBindingElement();
http.TransferMode = TransferMode.Streamed;
http.UseDefaultWebProxy = true;

CustomBinding myHttpBinding = new CustomBinding();
myHttpBinding.Name = “myHttpBinding”;
myHttpBinding.Elements.Add(text);
myHttpBinding.Elements.Add(http);

host.AddServiceEndpoint(typeof(IChat), myHttpBinding,
    “http://localhost:8080/chat/custom”);
...

もちろん、図 11 に示すように <customBinding> の構成を作成することで、構成内で同じことを完全に実現できます。 この手法は、他の組み込みバインドの構成と同じように動作します。 customBinding の構成を定義してから、エンドポイントに適用します。 異なるのは、この場合は構成内でバインド定義全体を定義している点です。 CustomBinding を使用する際に無償で手に入るものはありません。

Figure 11 <customBinding> の構成を作成する (Close Figure 11)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”ChatService”>
        <endpoint
          address=”custom”
          binding=”customBinding”
          bindingConfiguration=”myBasicHttpBindingConfiguration”
          contract=”IChat” />
      </service>
    </services>
    <bindings>
      <customBinding>
        <binding name=”myHttpBindingConfiguration”>
          <textMessageEncoding
            messageVersion=”Soap11WSAddressingAugust2004”/>
          <httpTransport
            useDefaultWebProxy=”true” transferMode=”Streamed”/>
        </binding>
      </customBinding>
    </bindings>
  </system.serviceModel>
</configuration>

図 12 に、図に示されている他の BindingElement クラスのいくつかを使用するカスタム バインドの例をいくつか示します。 myWSHttpBindingConfiguration 構成は組み込み WSHttpBinding と同様ですが、バイナリ メッセージ エンコーディングを使用し、トランザクション フローと順序付きの信頼性のあるメッセージングを有効にする点が異なります。 myNetTcpBindingConfiguration 構成は NetTcpBinding と同様ですが、テキスト メッセージ エンコーディングを使用し、トランザクション フローを有効にする点が異なります。

Figure 12 カスタム バインド構成の例 (Close Figure 12)
<configuration>
  <system.serviceModel>
    ...
    <bindings>
      <customBinding>
        <binding name=”myBasicHttpBindingConfiguration”>
          <textMessageEncoding
            messageVersion=”Soap11WSAddressingAugust2004”/>
          <httpTransport allowCookies=”true”
            useDefaultWebProxy=”true” transferMode=”Streamed”/>
        </binding>
        <binding name=”myWSHttpBindingConfiguration”>
          <transactionFlow/>
          <reliableSession ordered=”true”/>
          <security authenticationMode=”SspiNegotiated”/>
          <binaryMessageEncoding/>
          <httpTransport/>
        </binding>
        <binding name=”myNetTcpBindingConfiguration”>
          <transactionFlow/>
          <textMessageEncoding/>
          <windowsStreamSecurity/>
          <tcpTransport/>
        </binding>
      </customBinding>
    </bindings>
</configuration>

カスタム バインドを簡単に使用できるようにする

CustomBinding は 1 回限りのカスタマイズには便利ですが、それを簡単に再利用する必要がある場合はカスタム Binding クラスを定義する必要があります。 Binding から派生させた場合は、CreateBindingElements および Scheme という 2 つの抽象メンバをオーバーライドする必要があります。 CreateBindingElements の実装で、BindingElement オブジェクトのコレクションを作成して返します。

常にバイナリ メッセージ エンコーディングを使用する NetHttpBinding という名前のカスタム HTTP バインドを作成し、このバインドのユーザーが基礎となる HttpTransportBindingElement インスタンスに対して 2 つのプロパティ TransferMode および UseDefaultWebProxy を構成するとします。 図 13 のコードは、これを実現する方法を示しています。

Figure 13 カスタム HTTP バインド (Close Figure 13)
public class NetHttpBinding : Binding
{
    private BinaryMessageEncodingBindingElement binary =
        new BinaryMessageEncodingBindingElement();
    private HttpTransportBindingElement http =
        new HttpTransportBindingElement();

    public override BindingElementCollection CreateBindingElements()
    {
        return new BindingElementCollection(
            new BindingElement[] { binary, http });
    }

    public TransferMode TransferMode
    {
        get { return http.TransferMode; }
        set { http.TransferMode = value; }
    }

    public bool UseDefaultWebProxy
    {
        get { return http.UseDefaultWebProxy; }
        set { http.UseDefaultWebProxy = value; }
    }

    public override string Scheme { get { return “http”; } }
}

このコードにより、開発者は次に示すように NetHttpBinding クラスを単純にインスタンス化し、インスタンスをエンドポイントに適用できます。

...
NetHttpBinding netHttp = new NetHttpBinding();
netHttp.TransferMode = TransferMode.Streamed;
netHttp.UseDefaultWebProxy = true;

host.AddServiceEndpoint(typeof(IChat), netHttp,
    “http://localhost:8080/chat/nethttp”);
...

これは、CustomBinding を扱うよりもはるかに簡単です。 ただし、さらに簡単にする必要がある場合は、Windows Communication Foundation 構成セクションを介して NetHttpBinding を構成できます。 この操作を行うには、他にいくつかの構成関連クラスを実装する必要があります。 図 14 に、必要になるクラスの詳細な例を示します。

Figure 14 NetHttpBinding のカスタム構成クラス (Close Figure 14)
public class NetHttpBindingConfigurationElement : StandardBindingElement
{
    public NetHttpBindingConfigurationElement(string configurationName)
        : base(configurationName) { }

    public NetHttpBindingConfigurationElement()
        : this(null) { }

    protected override Type BindingElementType
    {
        get { return typeof(NetHttpBinding); }
    }

    [ConfigurationProperty(“transferMode”,
         DefaultValue = TransferMode.Buffered)]
    public TransferMode TransferMode
    {
        get { return ((TransferMode)(base[“transferMode”])); }
        set { base[“transferMode”] = value; }
    }

    [ConfigurationProperty(“useDefaultWebProxy”, DefaultValue = false)]
    public bool UseDefaultWebProxy
    {
        get { return ((bool)(base[“useDefaultWebProxy”])); }
        set { base[“useDefaultWebProxy”] = value; }
    }

    protected override ConfigurationPropertyCollection Properties
    {
        get
        {
            ConfigurationPropertyCollection properties = base.Properties;
            properties.Add(new ConfigurationProperty(“transferMode”,
                typeof(TransferMode), TransferMode.Buffered));
            properties.Add(new ConfigurationProperty(“useDefaultWebProxy”,
                typeof(bool), true));
            return properties;
        }
    }

    protected override void InitializeFrom(Binding binding)
    {
        base.InitializeFrom(binding);
        NetHttpBinding netHttpBinding = ((NetHttpBinding)(binding));
        this.TransferMode = netHttpBinding.TransferMode;
        this.UseDefaultWebProxy = netHttpBinding.UseDefaultWebProxy;
    }

    protected override void OnApplyConfiguration(Binding binding)
    {
        if (binding == null)
        {
            throw new System.ArgumentNullException(“binding”);
        }
        if (binding.GetType() != typeof(NetHttpBinding))
        {
            throw new System.ArgumentException(
                “Invalid binding type – expected NetHttpBinding”);
        }
        NetHttpBinding netHttpBinding = ((NetHttpBinding)(binding));
        netHttpBinding.TransferMode = this.TransferMode;
        netHttpBinding.UseDefaultWebProxy = this.UseDefaultWebProxy;
    }
}

public class NetHttpBindingSection :
    StandardBindingCollectionElement<NetHttpBinding,
        NetHttpBindingConfigurationElement>
{
}

これらのクラスがあると、構成ファイルにバインド拡張マッピングを追加できるため、Windows Communication Foundation は、netHttpBinding が図 14 に示す構成クラスにあることを認識します。 ここで、バインド名に netHttpBinding を単純に指定し、図 15 に示すように他のバインドと同様に <bindings> セクション内で構成できます (msdn2.microsoft.com/aa967561.aspx で参照できる MSDN® ドキュメントには、より詳細なカスタム バインドの例が記載されています)。

Figure 15 netHttpBinding を構成する (Close Figure 15)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”ChatService”>
        <endpoint
          address=”nethttp”
          binding=”netHttpBinding”
          bindingConfiguration=”myNetHttpBindingConfiguration”
          contract=”ChatLibrary.IChat” />
      </service>
    </services>
    <bindings>
      <netHttpBinding>
        <binding
          name=”myNetHttpBindingConfiguration”
          transferMode=”Streamed”
          useDefaultWebProxy=”true”/>
      </netHttpBinding>
    </bindings>
    <extensions>
      <bindingExtensions>
        <add name=”netHttpBinding”
          type=”NetHttpBindingSection, NetHttpBindingLibrary” />
      </bindingExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

カスタム バインドの実装に関連する主要なオプションとコンポーネントについてはすべて説明しました。 まだ説明していないのは、カスタム BindingElement クラスの作成方法だけです。このクラスは、ニーズに厳密に適合するクラスが見つからない場合に必要になることがあります。 たとえば、カスタム トランスポートを実装する必要があるとします。 このような場合は、トランスポート チャネル (これは重要です) および対応する BindingElement クラスを実装して、バインド内で簡単に使用できるようにする必要があります。 カスタム チャネル コンポーネントとバインド要素の実装は、今後のコラムのために残しておく高度な拡張性のトピックです。

バインド記述を共有する

バインドの本質は、通信で行われる処理です。 特定のバインドを選択する場合、バインドを特別な方法で構成する場合、またはカスタム バインドを定義する場合は、コンシューマが正確なバインド構成を検出してエンドポイントと正しく統合できることが重要です。 サービスは、Web Services Description Language (WSDL) および WS-Policy の形式でメタデータを公開することにより、バインド構成をコンシューマと共有できます。

<serviceMetadata> ビヘイビアを介して Windows Communication Foundation サービスでメタデータを有効にできます。 これにより、開発者は Web ブラウザを使用してサービス記述を参照できます。 別のエンドポイントで MEX (IMetadataExchange) を有効にすることにより、メタデータをプログラムで使用可能にすることもできます。

メタデータを有効にすると、Windows Communication Foundation は、要求されたときにサービスに対して適切な WSDL 定義を自動的に生成します。 各エンドポイントのバインド構成は WS-Policy ステートメントに変換され、WSDL 定義内に埋め込まれて、正確な構成を指定します。 Windows Communication Foundation で生成された WSDL 定義を参照すると、ファイルの上部にこれらのポリシー ステートメントが見つかります。

クライアントは、WS-Policy ステートメントに見つかったすべての処理を実行できる限り、必要なクライアント側の構成を生成できます。 Windows Communication Foundation をクライアント側で使用する場合、svcutil.exe は WSDL 定義に見つかったポリシー ステートメントを調べ、クライアント構成ファイル内の対応するバインド構成セクションに変換します (図 16 を参照)。 したがって、クライアントは最終的に送信メッセージのディスパッチ用の同等のチャネル スタックになります。

図 16 メタデータを介してバインド構成を共有する
図 16 メタデータを介してバインド構成を共有する(画像を縮小するには、ここをクリックします)
図 16 メタデータを介してバインド構成を共有する
図 16 メタデータを介してバインド構成を共有する (画像を拡大するには、ここをクリックします)

次のコードは、Windows Communication Foundation クライアントが実行時に MEX を介してサービスにより公開されるすべてのエンドポイントに自動的にアクセスする方法を示しています。

...
ServiceEndpointCollection endpoints =
    MetadataResolver.Resolve(typeof(IChat),
    new EndpointAddress(“http://localhost:8080/chat/mex”));
foreach (ServiceEndpoint se in endpoints)
{
    ChannelFactory<IChat> cf = new ChannelFactory<IChat>(
        se.Binding, se.Address);
    IChat client = cf.CreateChannel();
    client.SendMessage(msg);
}
...

このコードがクライアントで使用可能な場合は、それぞれ一意のバインド構成を持つ新規エンドポイントをサービスに追加できます。 クライアントを実行するたびに、適切な構成を使用してすべてのエンドポイントが呼び出されます。 これは、クライアントが各エンドポイントのバインド構成を自動的に検出できることを示しています。

両方で Windows Communication Foundation を使用している場合でも相互運用性が損なわれる可能性がある状況の 1 つに、クライアントがアクセス権を持たないカスタム BindingElement (カスタム トランスポートなど) がサービスで採用されている場合があります。 明らかに、任意の種類のカスタム チャネル コンポーネントを使用するには、クライアントとサービス間の合意が必要であり、両方に適切な実装が必要です。

これにより、最後に一言注意が必要になります。 適切な理由がなくバインド構成を変則的にしすぎると、構成の相互運用性が保たれにくくなります。 何を有効にしているかと、その理由を理解してください。 適切な理由がない場合は、既定のままにしてください。

たとえば、既定を使用している場合に相互運用に失敗する状況が発生した場合は、最初に何が障害になっているかを調べてから、とりあえず問題に対処するバインドを構成 (またはカスタム バインドを作成) できます。

まとめ

Windows Communication Foundation のバインド アーキテクチャでは、通信で行われることを構成するための洗練されたモデルが提供されます。 バインドを指定すると、Windows Communication Foundation は実行時にチャネル スタックを構築するためのレシピとしてそれを使用します。

Windows Communication Foundation には、現在の最も一般的な通信ニーズに対処するように事前構成されている多数の組み込みバインドが付属しています。 ただし、組み込みバインドが目的に合わない場合は、それらのバインドを構成するか、自身でカスタム バインドを作成して、発生した困難な統合シナリオに適応できます。

ご意見やご質問は、

Aaron sstation@microsoft.com.

まで英語でお送りください。
Aaron Skonnardは、Microsoft .NET のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。 Aaron は、Pluralsight が提供する Applied Web Services 2.0、Applied BizTalk Server 2006、および Applied Windows Communication Foundation の各コースの著者です。 Aaron はこれまで何年もの間、コースの開発、会議での講演、およびプロ開発者の指導に従事してきました。 連絡先は pluralsight.com/aaron です。


Bugslayer: Wait Chain Traversal

Bugslayer
Wait Chain Traversal
John Robbins

コードのダウンロード : : Bugslayer2007_07.exe (175KB)
コンテンツ
Wait Chain Traversal の概要
WAITCHAIN_NODE_INFO 構造体の問題点
Unsafe
LockWatcher
x64 での混合デバッグ
まとめ

長年にわたり開発ツールの作成に携わってきたので、新しいオペレーティング システムがリリースされると、必ず、開発上の難問の解決に使用できる新しいものがないかという目で見てしまいます。たとえば、Windows® XP ではベクトル化例外処理が導入され、ログ出力や診断目的に例外処理チェーンをインターセプトすることができるようになりました。2006 年 11 月号の MSDN® Magazine では、この機能を使用して Microsoft® .NET ベースのアプリケーション用に例外ログ出力プログラムを記述する試みを説明しました。

マイクロソフトは、Windows Vista™ でも期待を裏切りませんでした。ちなみに、Windows Vista では、Wait Chain Traversal (WCT) という、非常に興味深い新しい API が導入され、プロセスのデッドロックがいつ、どのような理由で発生するかを突き止めることができるようになります。これは実に魅力的に思えたので、当然ながら、すぐに夢中になってしまいました。WCT が優れているのは、具体的にどの同期オブジェクトでデッドロックが発生しているかがレポートされる点です。残念なのは、レポートされる同期プリミティブが限られている点です。しかし、このような制限があったとしても、WCT は非常に便利な API であり、デバッグ ツールキットに入れておきたいものになるでしょう。

このコラムでは、WCT API と、その使用法および制限事項について説明します。このコラム執筆の一環として、WCT でサポートされるすべてのデッドロックを正確に特定するツールを用意しました。ツールを .NET で記述することにこだわったため、相互運用機能を使用する上で苦労したこと、および .NET Framework から WCT API を機能させる方法を示すことにもなりました。

Wait Chain Traversal の概要

図 1 に、WCT API がデッドロックのレポートをサポートする同期プリミティブを示します。デッドロックが発生する可能性のある同期プリミティブは他にもイベントなど、いくつかありますが、ほとんどのアプリケーションで問題となる領域の大半がこの一覧に網羅されています。.NET に取り組んでいれば、sync ブロックがサポートされるかどうか、気になるかもしれません。これまで sync ブロックを耳にしたことがないとしたら、それは、直接使用しないためでしょう。sync ブロックは、lock キーワードを Monitor クラスと共に実装するのに使用されている内部構造です。sync ブロックの実装は共通言語ランタイム (CLR) から提供されるので、ネイティブの WCT API では認識されません。

Figure 1 WCT でサポートされる同期プリミティブ (Close Figure 1)

同期プリミティブ | 説明
ALPC | リモート プロシージャ コールに使用される、Windows の内部メカニズム
COM | スレッド間やプロセス間の COM 呼び出し
クリティカル セクション | 軽量で、プロセス単位の、ネイティブの同期タイプ
ミューテックス | 通常、複数のプロセスの同期を調整するのに使用される、ハンドルベースの同期タイプ
SendMessage | メッセージが処理されるまで、別のスレッド ブロックやウィンドウ ブロックへのメッセージ送信
プロセス ハンドルおよびスレッド ハンドル | プロセスおよびスレッドの待機

WCT API は、ブロッキング呼び出し内でスレッドが待機しているかどうかを調べることで機能します。スレッドがブロッキング呼び出しにあれば、そのスレッドがブロックしているオブジェクトを調べ、必要に応じて、そのオブジェクトの名前、およびそのオブジェクトを所有しているスレッドまたはプロセスを調べます。たとえば、プロセス B の実行完了をスレッド A が待機しているときに、プロセス B がプロセス A に対して SendMessage を呼び出すとデッドロックが発生します。このデッドロックは、WCT での検出とレポートが可能です。

WCT API に関して唯一問題と思われるのは、WaitForMultipleObjects を使用しているときにデッドロックがレポートされないことです。これにより、多くのアプリケーションでこの API の有効性が弱まります。たとえば、.NET 同期では、すべての同期の調整に WaitForMultipleObjects が使用されます。WCT では、スレッドがデッドロック状態になっていることはレポートされませんが、スレッドがブロックされていることはレポートされます。さいわい、WCT API では待機時間をレポートできるので、LockWatcher (このコラムのコード ダウンロードの一部) などのツールを使用して手動で検査や比較を行うことにより、特定のスレッドがデッドロック状態になっているかどうかを判断できます。

WCT は典型的な Win32® API で、非透過ハンドルを取得し、データを記録するメソッドにそのハンドルを渡して、完了時に CloseHandle 形式のメソッドを呼び出す必要があります。Platform SDK ドキュメントでは、WCT API の初期化と使用に必要な情報がすべて紹介されています。初期化にはちょっとした問題が 1 つあります。それは、WCT が COM 所有権情報をレポートできるように、COM 関数の CoGetCallState や CoGetActivationState を登録するため、必ず RegisterWaitChainCOMCallback 関数を呼び出すことになる点です。これらの関数はドキュメントに記載されておらず、ole32.dll に含まれている GetProcAddress を使用して取得する必要があります。個人的には、この簡単な処理は、COM のデッドロックを探すための WCT API の初期化時に自動的に行われるべきだと思います。

WCT を初期化するには、以下のように、advapi32.dll からエクスポートされた OpenThreadWaitChainSession 関数を呼び出します。

HWCT WINAPI OpenThreadWaitChainSession(
    DWORD Flags, PWAITCHAINCALLBACK callback
);

初期化が完了したら、アプリケーション内でブロックされているスレッドを特定するために、いつでも GetThreadWaitChain を呼び出すことができます。

BOOL WINAPI GetThreadWaitChain(
  HWCT WctHandle, DWORD_PTR Context,
  DWORD Flags, DWORD ThreadId,
  LPDWORD NodeCount, PWAITCHAIN_NODE_INFO NodeInfoArray,
  LPBOOL IsCycle
);

WCT API の初期化では、すべての操作を非同期に行うように設定することもできますが、WCT API を使用するほぼ全員が同期バージョンを使用することになるでしょう。GetThreadWaitChain に渡すスレッド ID と Flags パラメータによって、受け取るデータの種類が決まります。Flags パラメータの既定値では、指定したプロセス内のスレッドを調べるだけです。他のプロセスから情報を取得する場合は、WCT_OUT_OF_PROC_COM_FLAG、WCT_OUT_OF_PROC_CS_FLAG、WCT_OUT_OF_PROC_FLAG という 3 つのフラグを指定する必要があります。これら 3 つを論理和で結合したフラグ WCTP_GETINFO_ALL_FLAGS を使用すれば、簡単に指定できます。

GetThreadWaitChain が返す主な情報は、スレッドの状態とスレッドのブロックの現状を示す、WAITCHAIN_NODE_INFO 構造体の配列です。また、GetThreadWaitChain は、WCT コード内の分析エンジンに基づいて、スレッドがデッドロック状態にあるかどうかも返します。WCT API は概ね比較的簡潔で、すべてのデッドロックをレポートするわけではありませんが、レポートするデッドロックについては正確な情報を提供します。

スレッドがブロックされていると、WAITCHAIN_NODE_INFO 構造体の配列で、そのスレッドとデッドロックの原因がレポートされます。たとえば、クリティカル セクション X を所有するスレッド A がクリティカル セクション Y を取得しようとし、クリティカル セクション Y を所有するスレッド B がクリティカル セクション X を取得しようとしている、などデッドロックが発生している場合、デッドロックは配列内で以下のようにレポートされます。

Index 1: Thread A’s ID
Index 2: Waiting on a critical section
Index 3: Thread B’s ID
Index 4: Waiting on a critical section
Index 5: Thread A’s ID

クリティカル セクションのアドレスはレポートされていないことに注意してください。ただし、ミューテックスなど、その他のオブジェクトの場合、名前があればその名前がレポートされます。

WCT API を最大限に活用するには、デッドロックの発生原因を正確に把握できるように、常にハンドルに名前を付けるようにします。もちろん、名前を付けたハンドルはプロセス外から参照可能になり、同期の深刻な問題につながることがあります。たとえば、ミューテックスに MyHappyMutex のような名前を付けたとします。ここで、実行中の 2 つのプロセス A と B がどちらも同じ名前のミューテックスを待機しています。プロセス A がミューテックスを解放すれば、プロセス A もプロセス B もこのミューテックスを獲得できるようになります。そのため、デバッグ時に深刻な問題が発生する可能性があります。ハンドルに名前を付け、そのハンドルをプロセス固有のものにする場合は、プロセス ID など、プロセス固有であることが保証されている文字列を名前の後ろに付けます。

ハンドルに名前を付ければ、確かにデバッグには役立ちますが、そのハンドルをグローバルにすると、別のプロセスがハンドルを開くことができるようになり、セキュリティ攻撃の懸念が生じます。シナリオによっては、この懸念が現実になることもあります。攻撃を受ける可能性がある場合は、デバッグ ビルドだけでハンドルに名前を付けることを検討します。

WAITCHAIN_NODE_INFO 構造体の問題点

WCT API はネイティブ コード向けにデザインされていますが、ここではいくつか追加分析の構想があったので、これを .NET コードから使用するツールを記述することを計画しました。また、.NET アプリケーションは、メモリのことをそれほど心配する必要がなく、記述が比較的簡単なので、すぐに実装できると考えていました。相互運用機能について本当に頭をかきむしりたくなるような問題が待ち受けていようとは、思ってもみませんでした。

図 2 に、ネイティブ WAITCHAIN_NODE_INFO 構造体を示します。この構造体をよく見ると、2 つの共用体が重なり合う部分があることがわかります。P/Invoke は、ほとんどの構造体では問題になりませんが、C++ の共用体が存在すると必ず、深刻な苦労を味わうことになります。Windows の構造体や API は、ほぼすべて pinvoke.net Web サイトで定義されていますが、この厄介なネイティブ構造体に関しては自力で何とかしなければならないという悪い予感がしました。

Figure 2 ネイティブ WAITCHAIN_NODE_INFO 構造体 (Close Figure 2)
typedef struct _WAITCHAIN_NODE_INFO
{
    WCT_OBJECT_TYPE ObjectType;
    WCT_OBJECT_STATUS ObjectStatus;

    union {
        struct {
            WCHAR ObjectName[WCT_OBJNAME_LENGTH];
            LARGE_INTEGER Timeout;    // Not implemented in v1
            BOOL Alertable;           // Not implemented in v1
        } LockObject;

        struct {
            DWORD ProcessId;
            DWORD ThreadId;
            DWORD WaitTime;
            DWORD ContextSwitches;
        } ThreadObject;
    };

} WAITCHAIN_NODE_INFO, *PWAITCHAIN_NODE_INFO;

当初は、.NET 側で 2 つの構造体を別々に定義すれば何とかなるかもしれないと期待しました。他のプロジェクトでは、.NET 側の各構造体宣言に対して DllImportAttribute P/Invoke メソッドを個別に宣言することで、構造体を宣言する作業を容易にできました。WCT コードでは、内部実装の詳細には苦労することは想定していましたが、.NET 構造体の定義はささいなことだと思っていました。デッドロックのサンプルに対してネイティブの Wait Chain Traversal サンプル (msdn2.microsoft.com/ms681418.aspx) を実行したところ、返されたデッドロック情報に大きな違いがあったので、この当初の試みは解決策として不十分であることがすぐにわかりました。

これは、LayoutKind.Explicit を使用して構造体内部の個々のオフセットを定義する必要があることを意味していました。WAITCHAIN_NODE_INFO 構造体のサイズを返す簡単なネイティブ サンプルを実行したところ、32 ビット Windows 上でも 64 ビット Windows 上でもサイズは 280 バイトであることがわかりました。そのため、別々の構造体を宣言し、オペレーティング システムに応じてどちらを使用するかをプログラムで判断する必要はなくなりました。

構造体のオフセットを簡単に知るのに使った秘密の裏技は、Debugging Tools for Windows パッケージに含まれている WinDBG を使用することです。このパッケージは microsoft.com/whdc/devtools/debugging からダウンロードできます。WinDBG はネイティブ専用のデバッガなので、ネイティブ C++ アプリケーションの中で問題の構造体を使用しているアプリケーションをデバッグする必要があります。型の表示には DT コマンドを使用します。都合の良いことに、既定ですべてのフィールドのオフセットが表示されます。WAITCHAIN_NODE_INFO の場合は、DT コマンドに –r 1 オプションと –v オプションを渡します。–r 1 オプションは構造体を再帰的に 1 レベル拡張するよう WinDBG に指示し、–v オプションは構造体についてのすべての情報をダンプします。図 3 は、Windows Vista x64 でのコマンドの実行結果を示しています。

Figure 3 WinDBG の DT コマンドの結果 (Close Figure 3)
0:000> dt -v -r 1 NodeInfoArray[0]
Local var [AddrFlags 90  AddrOff 0000000000000050  Reg/Val rsp (7)] @ 0x12d970
[0] [16] struct _WAITCHAIN_NODE_INFO, 4 elements, 0x118 bytes
   +0x000 ObjectType       : Enum _WCT_OBJECT_TYPE,  11 total enums
8 ( WctThreadType )
   +0x004 ObjectStatus     : Enum _WCT_OBJECT_STATUS,  11 total enums
3 ( WctStatusBlocked )
   +0x008 LockObject       : struct _WAITCHAIN_NODE_INFO::<unnamed-tag>::<unnamed-tag>,
                             3 elements, 0x110 bytes
      +0x000 ObjectName       : [128]  “۰”
      +0x100 Timeout          : union _LARGE_INTEGER, 4 elements, 0x8 bytes
 0x0
         +0x000 LowPart          : 0
         +0x004 HighPart         : 0
         +0x000 u                : struct <unnamed-tag>, 2 elements, 0x8 bytes
         +0x000 QuadPart         : 0
      +0x108 Alertable        : 0
   +0x008 ThreadObject     : struct _WAITCHAIN_NODE_INFO::<unnamed-tag>::<unnamed-tag>,
                             4 elements, 0x10 bytes
      +0x000 ProcessId        : 0x6f0
      +0x004 ThreadId         : 0x6f4
      +0x008 WaitTime         : 0x3005ce
      +0x00c ContextSwitches  : 0x2fb

最初の興味深い情報は、図 3 の出力結果の 3 行目にある 0x118 です。これが構造体のサイズです。お察しのとおり、最初の 2 つの列挙値はオフセット 0x0 と 0x4 にあります。LockObject、ThreadObject という 2 つの共用体は、オフセット 0x8 から始まります。LockObject 共用体を例にとると、Unicode 文字配列のオフセットは 0x8 から始まり、サイズは 0x100 バイトになります。その結果、Timeout の下位パートは 0x108 から始まり、上位パートは 0x110 から始まることになります。最後の Alertable フィールドは 0x114 からになります。パターンを理解してしまえば、ThreadObject 共用体のオフセットも簡単にわかります。

FieldOffset 属性を使用して .NET バージョンの WAITCHAIN_NODE_INFO 構造体をコーディングした後、最初の大きな問題にぶつかりました。LockObject 共用体の最初の項目は、要素数 128 の Unicode 文字配列 ObjectName です。どのような方法でこのフィールドを定義しても、コードから定義した構造体にアクセスするたびに TypeLoadException が発生しました。タイプセーフにするため、およびガベージ コレクションに関する問題により、参照型を指すフィールドを他のフィールドと重ねることができません。配列と文字列はどちらも参照型なので、この状況では使用できません。

Unsafe

あきらめかけていたときに、unsafe キーワードと、.NET Framework 2.0 で導入された、C# の fixed キーワードの新しい使い方を思い出しました。構造体を unsafe で定義すれば、ObjectName フィールドに固定長の文字配列の定義を使用できます。その結果、構造体内で unsafe インライン配列が可能になります。これは構造体内では配列のように見えますが、実際のフィールドへのアクセスにはポインタが使用されます。そのため、文字ポインタを受け取る String コンストラクタを使用すれば、実際の値にアクセスできます。

構造体を定義し、/unsafe コンパイラ スイッチを追加したところ、自作のテスト アプリケーションで、ネイティブのテスト サンプルとまったく同じようにクリティカル セクションのデッドロックがレポートされるようになり、非常に嬉しくなりました。テスト アプリケーションを拡張して、1 つのスレッドがクリティカル セクションを取得し、同じクリティカル セクションを待機している別のスレッドのウィンドウに対して SendMessage を呼び出すことも機能しました。エクスプローラ プロセスをブロックするデッドロックのテストを追加したところ、これもうまくいきました。

すべてが順調だと思いながら、名前付きミューテックスでデッドロックを発生させるテストを追加しました。Platform SDK の WCT サンプルでは、ミューテックスの名前は次のように表示されました。

\Sessions\1\BaseNamedObjects\Deadlock Mutex A

\Session\1 は対話型ログイン セッションを示しており、BaseNamedObjects はカーネル オブジェクト テーブルです。テスト アプリケーションでは、ミューテックスに Deadlock Mutex A という名前を付けました。しかし、自作の構造体の P/Invoke では、名前はただの "\Sessi" としか表示されませんでした。

構造体のサイズが不適切なのだと考え、Marshal.SizeOf を使用してチェックを行ったところ、.NET 側のレポートでは構造体のサイズは 280 バイトと適切に認識されていました。また、構造体のサイズが不適切だったとしたら、ネイティブ GetThreadWaitChain 関数は .NET メモリ全体を配列の処理対象にしていたでしょうから、今までのクリティカル セクションに関するもテストはまったく機能しなかったはずだと気づきました。さらに、これまでデッドロックに関して配列の中身を確認してきたときに、C# コードでは配列内の正しいデータは表示されなかったでしょう。

ここで、また少し混乱しました。クリティカル セクションのデッドロック テストによると、作成した構造体のサイズは適切であるにもかかわらず、名前付きミューテックスの例からは、構造体が小さすぎると考えられます。まず、ネイティブの GetThreadWaitChain 関数で何が行われているかを確認する必要があると考えました。そこで、WinDBG を使用して、自作の .NET アプリケーションを名前付きミューテックスのデッドロックのシナリオで実行しました。ADVAPI32!GetThreadWaitChain にブレークポイントを設定し、配列パラメータを見つけて、関数からステップ アウトしたところ、ネイティブ側ではミューテックスの完全な名前を配列にコピーしていることがわかりました。しかし、そのデータは確かに、自作の C# コードには戻ってきていなかったのです。

残念なことに、さらに、.NET がネイティブ コードの配列に何を行っているかを確認する必要が生じました。今回は Windows Vista の x64 バージョンを実行しているので、もちろん、混合デバッグ (アプリケーションのマネージ側とネイティブ側を同時にシングル ステップ実行できるデバッグ) はサポートされていません。バイナリが 1 つしかない単純なケースでは、プラットフォームを x86 に設定します。こうすると、アプリケーションが 32 ビットになり、64 ビット Windows Vista で混合デバッグを行えるようになります。ただし、多くのアセンブリから成る実際のアプリケーションでは、本当の 32 ビット コンピュータでビルドを変更せずに混合デバッグを行う方がはるかに高速です。このコラムの執筆中に、x64 システムでの混合デバッグに関する質問がいくつか送られてきていましたが、それはある程度可能であるとお伝えできます。このコラムの最後に、完全な x64 .NET Framework コードを実行している場合に、ネイティブ側とマネージ側の両方を確認するのに使用してきた手法の一部を説明します。

アセンブリ言語についてあまりにも膨大な調査を始めた後で、ある考えが浮かびました。"\Sessi" は固定長配列内でしか確認しなかったので、同じ場所に他のフィールドが存在しているときに、.NET 相互運用機能コードでデータのマーシャリングが行われただけではないかと考えました。共用体のスレッド部分 (12 バイト長) によって名前部分の長さが決まるのではないかとさえ思えてきました。自説を検証するため、スレッド共用体用に Int32 フィールドを追加し、構造体レイアウトで、このフィールドが 0x18 から始まるようにしました。アプリケーションを実行し、"\Session" という文字列が返ってきたときのショックは察していただけるでしょう。

結局のところ、マーシャラを文字配列の作成に使用する場合、C# コンパイラによって fixed キーワード用に生成されたコードとマーシャラがやり取りする方法にバグがあるのです (このバグは Visual Studio "Orcas" のリリースでは修正されているでしょう)。この問題に対処するには、構造体が blittable になるように変更する必要がありました。これは、構造体を構成する型がすべて、マネージとネイティブで同じ表現でなければならないことを意味します。blittable 型には、Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、UInt64、IntPtr、UintPtr、Single、Double などがあります。しかし、明らかに Char が含まれていないことがわかります。実際、Char はネイティブ コードでは (1 バイトでも 2 バイトでも) 複数の表現が可能なので、blittable ではありません。そこで問題を回避するために、固定長配列のフィールド型を char ではなく ushort に変更しました。続いて、この配列を String に変換するコードの中で、まず、結果の ushort* から、String コンストラクタが想定する char* にキャストしました。図 4 に、正しく機能する最終的な構造体を示します。

Figure 4 最終的な WAITCHAIN_NODE_INFO 定義 (Close Figure 4)
[StructLayout ( LayoutKind.Explicit, Size=280 )]
internal unsafe struct WAITCHAIN_NODE_INFO
{
    [FieldOffset ( 0x0 )]
    public WCT_OBJECT_TYPE ObjectType;
    [FieldOffset ( 0x4 )]
    public WCT_OBJECT_STATUS ObjectStatus;

    // The name union.
    [FieldOffset ( 0x8 )]
    private fixed ushort RealObjectName [ WCT_OBJNAME_LENGTH ];
    [FieldOffset ( 0x108 )]
    public Int32 TimeOutLowPart;
    [FieldOffset ( 0x10C )]
    public Int32 TimeOutHiPart;
    [FieldOffset ( 0x110 )]
    public Int32 Alertable;

    // The thread union.
    [FieldOffset ( 0x8 )]
    public Int32 ProcessId;
    [FieldOffset ( 0xC )]
    public Int32 ThreadId;
    [FieldOffset ( 0x10 )]
    public Int32 WaitTime;
    [FieldOffset ( 0x14 )]
    public Int32 ContextSwitches;

    // Does the work to get the ObjectName field.
    public String ObjectName ( )
    {
        fixed ( WAITCHAIN_NODE_INFO* p = &this )
        {
            return (p->RealObjectName [ 0 ] != ‘\0’) ?
                new String ( (char*)p->RealObjectName ) :
                String.Empty;
        }
    }
}

LockWatcher

WAITCHAIN_NODE_INFO 構造体を理解したら、LockWatcher アプリケーションを完成させることができました。これを使用して、アプリケーション内のデッドロックを見つけることができます。1 つのプロセス内または特定のプロセスのグループ内のデッドロックを探すには、コマンド ラインでプロセス ID 値を渡すだけです。プロセス ID を 1 つも指定しないと、コンピュータ上のすべてのプロセスについてレポートされます。すべてのプロセスを調べる場合は、他の昇格したプロセス内のデータを検査できる完全な権限が必要になるため、権限を昇格したコマンド ウィンドウから LockWatcher を起動します。

既定では、LockWatcher は、ブロックされているスレッドと、WCT API からレポートされたブロックされているアイテムを示すだけです。待機時間やコンテキスト スイッチの情報を表示するには、–a コマンド ライン オプションを渡します。LockWatcher の役立つ機能の 1 つは、–t <seconds> オプションです。このオプションを使用すると、指定した時間継続してプロセスのデッドロックが検査されます。

LockWatcher からデッドロックがレポートされると、以下のような出力結果が得られます。これはクリティカル セクションのデッドロックの例です。

Process : DEAD.EXE, PID : 5828
TID: 5392
**Following thread is DEADLOCKED!
TID:  424
   CriticalSection Status: Owned
      TID: 5492
         CriticalSection Status: Owned
            TID:  424
**Following thread is DEADLOCKED!
TID: 5492
   CriticalSection Status: Owned
      TID:  424
         CriticalSection Status: Owned
            TID: 5492

2 つ目のスレッドの場合、スレッド 424 が所有するクリティカル セクションをスレッド 5492 が待機していることを出力から読み取ることができます。スレッド 424 はクリティカル セクションを待機しています。

既に説明したように、WaitForMultipleObjects を使用している場合、WCT API ではデッドロックがレポートされません。以下に示すのは、Deadlock Mutex A を所有するスレッド 5652 が、WaitForMultipleObjects を呼び出して、他の 2 つのスレッド ハンドルを待機しているというデッドロックの出力です。

Process : DEAD.EXE, PID : 5796
TID: 5652
TID: 4776
   Mutex Status: Owned Name: \Sessions\1\BaseNamedObjects\Deadlock Mutex A
      TID: 5652
TID: 4920
   Mutex Status: Owned Name: \Sessions\1\BaseNamedObjects\Deadlock Mutex A
      TID: 5652

出力を見ると、デッドロックの一部をつなぎ合わせてある程度のことを把握できますが、全貌を知ることはできません。LockWatcher の目的は、WCT API から返されたデータをうまく処理するアルゴリズムを構築し、WaitForMultipleObjects を使用している場合にデッドロックを特定することでした。前の例に取り組んでみて、必要な作業は簡単にわかりました。次に行ったのは、Mutex クラスの 2 つのインスタンスでデッドロックが発生する簡単な .NET サンプルを作成することでした。このサンプルでは、以下の出力結果が得られました。

Process : DEADDOTNET.EXE, PID : 3644
TID: 4976
TID: 6120
TID: 2152
TID: 5852

ご覧のとおり、役に立つことは何もレポートされていないので、コードの挿入、スタック ウォーク、およびパラメータの解読を行わない限り、できることはあまりありません。WCT API には限界がありますが、使用しない場合と比べれば、はるかに多くのデッドロックを検出することができます。さまざまなデッドロックが LockWatcher によってどのようにレポートされるかを確認するために、DEAD という意味深長な名前を付けたプログラムをテスト プログラムとして含めました。このプログラムは、ネイティブ C++ で記述しました。

x64 での混合デバッグ

アプリケーションを x86 プラットフォーム用に再コンパイルし、マネージとネイティブの混合デバッグを可能にする方法については既に説明しました。しかし、WAITCHAIN_NODE_INFO 構造体に関して直面した P/Invoke の問題についての調査の一環として、x64 バイナリとして実行されている .NET アプリケーションの混合デバッグを少し行いました。マイクロソフトでサポートされているシナリオではありませんが、支障なくマネージとネイティブの両側を確認できるようにするために使用した手法の一部を紹介しようと思います。

マネージ デバッグでは、標準の Windows デバッグ API (msdn2.microsoft.com/ms679276.aspx) は使用せず、ベクトル化例外処理に依存した手法を使用します。CLR デバッグ API (msdn2.microsoft.com/ms404520.aspx) で行われている内容を確認してみると、標準のデバッガと同様に、通常のシングル ステップおよびブレークポイント例外が使用されていることがわかります。マネージ デバッグで Windows デバッグ API が使用されないため、既にデバッガで実行中のアプリケーションにネイティブ デバッガをアタッチした場合に発生するエラーは発生しません。したがって、Visual Studio® でマネージ アプリケーションのデバッグを開始したら、マネージ アプリケーションにネイティブ デバッガをアタッチすることができます。

Visual Studio の 2 番目のインスタンスをネイティブ デバッガとして使おうとすることは可能ですが、マネージ デバッグ API によって実行される 1 ステップごとにブレークが発生するので、ほとんど何もすることができません。Visual Studio では、例外発生時に自動的に続行する方法はないので、頼みの綱は WinDBG を使用することです。

WinDBG は使いにくいですが、便利なイベント フィルタリングを使用すると、CLR デバッグ API が絶えず発生させるシングル ステップの例外を無視することができます。WinDBG をマネージ アプリケーションにアタッチした後、すぐに最初のブレークポイント (ローダー ブレークポイント) で停止します。

最初に使用するコマンドを以下に示します。

sxd –c “gn” sse

SXD (Set Exception Disabled : 例外を無効に設定) コマンドを使用して、SSE (Single Step Exception : シングル ステップの例外) を無効に設定しています。これは、シングル ステップの例外が発生したときに停止しないことを意味します。CLR デバッグ API がシングル ステップに関するすべてを管理するので、WinDBG は自動的に続行され、デバッグ対象にシングル ステップの例外を未処理の例外として渡すように、初回コマンドの設定を行いました。64 ビット ネイティブ部分で停止させるときは、このコマンドを設定したことを思い出してください。この設定のままでは、それ以降、シングル ステップ実行できないためです。必要なときにシングル ステップ実行を有効に戻すには、以下のコマンドを実行します。

sxe -c “” sse

シングル ステップ実行を管理できるようになったら、CLR デバッグ API のブレークポイントとネイティブ コード内で設定するブレークポイントの取り扱い方法について検討する必要があります。P/Invoke の問題で何が起こっているのかを解明しようとしていたときに、GetThreadWaitChain (advapi32.dll に含まれています) へのパラメータを確認する必要がありました。そのため、次のコマンドを使用してそのアドレスにブレークポイントを設定する必要がありました。

bu ADVAPI32!GetThreadWaitChain

CLR デバッグ API によって設定されるブレークポイントをすべて無視するという大技の実現が必要です。さいわい、WinDBG ではブレークポイント例外をその他の例外と同様に扱うので、SX (Set Exception : 例外を設定) ファミリのコマンドを引き続き使用して、ブレークポイント例外が発生するたびに WinDBG によってコマンドが実行されるようにできます。さらにさいわいなことに、WinDBG では、デバッガ コマンド プログラムと呼ばれる基本制御フロー コマンドがサポートされています。.if コマンドと .else コマンドを使用すると、デバッガ コマンドで条件付きロジックを実行することができます。

自分でブレークポイントを設定した箇所以外のすべてのブレークポイント例外を無視するには、x64 コンピュータの場合は次のコマンドを使用します。

sxe -c “.if (rip==ADVAPI32!GetThreadWaitChain){.echo STOPPED AT YOUR BU!}.else{gn}” bpe

すべての BPE (Break Point Exception : ブレーク ポイント例外) に対して、初回例外に関するコマンドを実行するように、SXE (Set Exception Enabled : 例外を有効に設定) で指定しています。このコマンドでは、登録の命令 (RIP) によって GetThreadWaitChain の最初の命令が実行されているかどうかをチェックしています (32 ビット コンピュータの場合は、登録の命令として RIP の代わりに EIP を使用します)。実行されている場合は、画面にテキストが繰り返し出力され、停止がレポートされます。実行されていない場合は、CLR デバッグ API が混乱しないように、デバッグ対象で例外を発生させるために GN (Go Not Handled : 処理せずに継続) コマンドが実行されます。

少し複雑ですが、これらの手順によって、x64 システムで混合デバッグを少しは行えるようになります。行っている内容に関して細心の注意を払っていないとマネージ アプリケーションは実に簡単に大混乱に陥ることに注意してください。ここで紹介した手法がマイクロソフトによってサポートされる可能性は (または、容認される可能性さえ) ほとんどありません。しかし、x64 システムでアプリケーションのネイティブ側とマネージ側の両方を確認する必要がある場合に、少なくとも作業を進めるヒントにはなります。

まとめ

このコラムは何と長く奇妙な道のりとなったことでしょう。一見簡単だと思って始めたことが、P/Invoke や x64 での混合デバッグを扱う長い道のりとなり、ようやくアプリケーションのデッドロックの一部を検出することにたどり着きました。マイクロソフトが長年にわたって行ってきた API の強化の実績を考え、WCT の今後の Service Pack で WaitForMultipleObjects がサポートされるようになることを願っています。実際にサポートされるようになったら、それをサポートするように LockWatcher を更新するつもりです。

ヒント 78 ASP.NET 開発を行っているとしたら、送受信される生の HTTP 情報を調べざるを得ない場面もあることをご存知でしょう。そのような場合は、Fiddler (www.fiddler2.com) が役に立ちます。すべてのバイトを確認できるだけでなく、実にすばらしいスクリプト API を使用してデータの変更や微調整を行うことができます。また、最新バージョンの Fiddler 2 では HTTPS インターセプトがサポートされており、ますますツールキットになくてはならないものとなっています。

ヒント 79 Gregg Miskelly は、自身のブログですばらしいデバッグ手法を打ち出しました。マネージ コードでは、オブジェクトのアドレスがわからないので、インスタンスごとにブレークポイントを設定するのはほぼ不可能です。しかし Gregg は、インスタンスに対して、オブジェクト Id の作成というすばらしい手法を実行すると、条件付きブレークポイントを this == 1# に設定することにより、インスタンスごとにブレークポイントを設定することができることを指摘しています。筆者もこの手法を使うのが適切だったことはこれまで 100 万回はあったと思われます。

ご意見やご質問は、

John slayer@microsoft.com.

まで英語でお送りください。
John Robbinsは、.NET と Windows の両方を専門とするソフトウェアのコンサルティング、教育、開発を行う Wintellect 社の共同設立者です。最新の著書は『Debugging Microsoft .NET 2.0 Applications』(Microsoft Press 刊、2006 年) です。連絡先は www.wintellect.com です。


セキュリティに関するブリーフィング: Active Directory キャッシュの依存関係

セキュリティに関するブリーフィング
Active Directory キャッシュの依存関係
Keith Brown

コードのダウンロード : : SecurityBriefs2007_07.exe (182KB)
コンテンツ
LDAP クエリと SQL クエリ
プロパティとオブジェクトをキャッシュする
SqlDependency と優れたキャッシュ処理
DirSync コントロール
System.DirectoryServices.Protocols
ADDependency の使用モデル
詳しく調べる
詳細情報

企業内の随所で使用できるアプリケーションをビルド中であれば、Active Directory® と統合することでメリットが得られる可能性があります。Active Directory には大量の情報が含まれています。ほんの少しの時間を割いて、Active Directory がサポートする多くの API の 1 つに対してプログラミングを行う方法を学習する意思があれば、そこから多くの情報を得ることができます。

たとえば、クライアントのドメイン ID を検証するために、既に統合 Windows® 認証を使用しているアプリケーションがあるとします。この場合は、Active Directory との統合に向けての (ほぼ間違いなく最も大きな) 第一歩を既に踏み出しています。プライベートなユーザー データベースを別に作成しなくても、Active Directory 内にあるユーザーの既存の資格情報を使用できます。ユーザーは企業内で実行されるアプリケーション用に特別な資格情報を用意する必要がなくなり、既存の資格情報を簡単に使用できるため、これは優れた選択肢となります。

ユーザーの特権レベルを判断するために Active Directory 内のグループを使用する、ロール ベースのセキュリティのような手法も利用できます。WindowsPrincipal クラスの IsInRole メソッドを呼び出せば、特定のグループが存在するかどうかをテストできます。これは、ごく一般的に行われていることです。

では、さらに一歩進めて、実に興味深い点に踏み込んでみましょう。アプリケーションがドメイン アカウントで実行されている場合は、そのアカウントの特権が低くても、Active Directory に照会して、そのユーザーについての詳細情報を問題なく入手できます。認識していないかもしれませんが、ラボ内のドメイン コンピュータ上で Network Service アカウントを使用してアプリケーションを実行している場合でも、Active Directory への呼び出しなど、ネットワーク上で行われるセキュリティが確保された呼び出しに統合認証を使用しているときは、そのアプリケーションのプロセスはそのコンピュータのドメイン資格情報で表されます。

そのため、アプリケーションでクライアントの電子メール アドレスや電話番号を取得する場合は、IsInRole を呼び出すと同時に、図 1 に示すようなコードを追加することを検討できます。ASP.NET アプリケーションから Windows Communication Foundation サービスや Windows フォーム デスクトップ アプリケーションに至るすべてのマネージ アプリケーションが、Active Directory 統合の恩恵を受けることができます。

Figure 1 WindowsIdentity から電子メールを検索 (Close Figure 1)
string email = LookupEmail((WindowsIdentity)User.Identity)

...

public string LookupEmail(WindowsIdentity id)
{
    string path = string.Format(“LDAP://<SID={0}>”, id.User);
    using (DirectoryEntry user = new DirectoryEntry(
        path, null, null, SECURE_BINDING))
    {
        return (string)user.Properties[“mail”].Value;
    }
}

const AuthenticationTypes SECURE_BINDING =
    AuthenticationTypes.Secure | AuthenticationTypes.Signing |
    AuthenticationTypes.Sealing;

アプリケーションを企業内部でよりシームレスに機能させるために使用できる多くの有益な属性 (mail など) があります。誰かに友好的な電子メールを送るため、ユーザーのファースト ネームでアドレスを指定したいと考えたことはありませんか。このような場合は givenName 属性があります。ユーザーの個人アシスタントの電子メール アドレスを入手したいと考えたことはありますか。このような場合は、ユーザーのアシスタント オブジェクトにバインドし、その mail 属性を確認するだけです。

LDAP クエリと SQL クエリ

図 1 のコード サンプルでは、System.DirectoryServices という名前空間を使用します。これは、Active Directory との統合を始める最も簡単な方法の 1 つです。このマネージ ライブラリは、実は Active Directory サービス インターフェイス (ADSI) というCOM ライブラリを使用しています。その下に、LDAP API というネイティブ C ライブラリがあります。LDAP はライトウェイト ディレクトリ アクセス プロトコル (Lightweight Directory Access Protocol) の略称です。ライトウェイトと呼ばれる理由の 1 つは LDAP が TCP/IP 上で実行されるためですが、この話はまた別の日にしましょう。図 2 にこれらのライブラリの関係を示します (コラムの後半でもこの図を参照します)。

図 2 ディレクトリ サービス スタック
図 2 ディレクトリ サービス スタック

Active Directory と通信するときは、最終的には LDAP を使用することになります。LDAP は、ネットワーク プロトコルでもあり、独自のクエリ言語を備えたプログラミング言語でもあります。図 1 に示すような単純なクエリの実装は、非常に簡単です。

組織図の作成などを行おうとすると、必ずしも簡単に実装できるとは限りませんが、Active Directory によりユーザー間の関係を追跡できるため実装は可能です。このような場合は、たとえば、Active Directory 内のユーザー オブジェクトの manager 属性、directReports 属性、department 属性、title 属性などを使用します。ここで、この種のデータが SQL Server™ に格納されていたとすると、データベースとのごくわずかなやり取りで、組織図を生成できるようになります。データベースへの問い合わせでは SQL JOIN 演算子を使用して、レポートを構築し、ユーザーに返すことができます。

LDAP には JOIN 演算子がありません。また、Active Directory には、サーバー側でこのようなカスタム レポートをまとめるために記述できるストアド プロシージャのようなものもありません。そのため、必要なデータをまとめるために、クライアント側で手動での結合を行おうとすると、LDAP クエリが複雑になっていくことがわかります。その結果、ドメイン コントローラとのやり取りが多くなります。この点に注意を払わないと、アプリケーションでネットワーク帯域幅が浪費されると同時に、ユーザーがコンソールの前でいらいらしながら応答を待機することになります。

プロパティとオブジェクトをキャッシュする

では、解決策はあるのでしょうか。読み取り操作が頻繁に行われ、書込み操作がめったに行われないことを想定してディレクトリがデザインされていれば、一時的なクライアント側キャッシュが、複雑な LDAP クエリのオーバーヘッドの削減に役立つ論理的手法になります。このような理由から、実際、ADSI には既にプロパティ キャッシュが実装されています。図 1 のコードで、mail という LDAP 属性を問い合わせると、実際には、DirectoryEntry クラスが user オブジェクトの属性をすべてダウンロードし、ローカルに使用できるようにします (このような動作を疑問に思う場合は、関心のある属性を正確に選択するために、DirectoryEntry クラスの RefreshCache メソッドを使用して、この動作を制御できます)。

1 つのオブジェクトのプロパティをキャッシュする場合は、プロパティ キャッシュが役に立ちます。しかし、組織図の例のように多くのオブジェクトを扱う場合は、プロパティだけでなく、このようなオブジェクトもキャッシュする必要があります。ここで疑問が生じます。「このようなオブジェクトのキャッシュがある場合、どの程度の期間このキャッシュを保持できるのでしょうか」。すぐにデータが再度必要になる場合は、この情報をすぐに破棄してしまうのはもったいないような気がします。

ASP.NET には、ディレクトリ オブジェクトのローカル キャッシュの管理に有効活用できるキャッシュ インフラストラクチャがあります。キャッシュ内の情報を削除するのは非常に容易で、キャッシュが無効になるまでの時間を指定します。必要な情報をキャッシュ内で見つけることができない場合は、キャッシュの有効期限が切れたと判断し、ディレクトリに再度アクセスして、キャッシュを最新の情報に更新できます。次に基本的なパターンを示します。

List<DirectoryEntry> users = Cache[“users”];
if (null == users)
{
    // refresh cache from Active Directory
    users = loadUserListFromAD();
    Cache.Add(“users”, users, null, DateTime.Now.AddMinutes(5),
        Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
}
displayOrgChart(users);

こうした組織図に多くの要求がある場合は、このようなキャッシュが救いになり得ます。

SqlDependency と優れたキャッシュ処理

ADO.NET 2.0 と SQL Server 2005 のリリースにより、ローカル データ キャッシュの管理に役立つ新しいクラスが導入されました。これは System.Data.SqlClient 名前空間にある SqlDependency クラスで、クエリ通知という SQL Server 2005 の機能を使用します。マネージ コードでは、SqlDependency クラスのインスタンスを作成し、そのインスタンスにクエリと共に SqlCommand を提供できます。その後、クエリによって表されるデータが変化すると、通知が行われます。データベースの負荷を制限するために、監視するクエリにはいくつか制限事項がありますが、監視するクエリとデータの取得に使用したクエリは必ずしも正確に一致する必要がないことに注意してください。依存関係として使用するクエリを実際に実行するクエリのスーパーセットにできるため、依然として、この機能からのメリットを得ることができます。

このことから、もしも、これに相当する ADDependency クラスがあったならば、と考えました。LDAP データのローカル キャッシュが非常に一般的になったため、データが変化したときに Active Directory から通知を受け取る方法を考案しようと、以前に調査を行ったことがありました。残念ながら、あまり多くの解決策は見つかりませんでした。しかし、最近 Joe Kaplan (Microsoft MVP で Directory Services のリーダー) とチャットしたときに、DirSync コントロールについての説明を受けました。翌日、これまでずっと欲しかった ADDependency クラスをビルドし、テストを始めていました。

DirSync コントロール

LDAP バージョン 3.0 には、ディレクトリ サービスが拡張機能を公開できるコントロールが含まれています。Microsoft® .NET Framework を使用しているプログラマのほとんどは、LDAP コントロールという用語から、おそらく、Windows フォーム コントロールや ASP.NET サーバー側コントロールを思い浮かべたでしょう。しかし、実際には LDAP コントロールは、動作方法を制御するための LDAP コマンドと共に追加メタデータを送信する方法のみから成ります。

Active Directory は、いくつか専用の LDAP コントロールを公開します。ここでは DirSync というコントロールを使用します。このコントロールは、その名前が示すように、Active Directory 内のデータと他のデータ ストアを同期するだけです。

Active Directory が機能する方法について考えると、当然、データの同期についてもデザインされています。ディレクトリの最も重要な機能の 1 つは、ドメイン コントローラ (DC) 間でデータを効率的に複製する機能です。Active Directory のオブジェクトやプロパティには、64 ビットの更新シーケンス番号 (USN) が付いています。話を大幅に単純にすると、DC では現在の USN を管理し、何らかの変更が行われるたびにこの USN を 1 つ増加して現在の USN とし、変更が行われたオブジェクトや属性に新しい現在の USN を設定すると考えてください。パートナーが別の DC からレプリケーションを要求するたびに、DC はそのパートナーから最後に受け取った USN を送信する必要があるだけです。その結果、その特定のパートナーと最後にレプリケーションを行った以降に変更されたオブジェクトや属性が受信側でわかります。

Active Directory 実装の主な目標の 1 つは、ネットワークがレプリケーション トラフィックであふれかえらないようにすることです。このため、Active Directory はデータのバージョン番号の追跡に多くの注意を払います。Active Directory のレプリケーションについて少し調査すれば、そのしくみはかなり明らかになります。ただし、msdn2.microsoft.com/ms677626.aspx にも、DirSync を使用して変更をポーリングすることに関するドキュメントがあります。

DirSync コントロールは検索要求と連携して使用します。要求を行うときに、同期 Cookie を含む追加メタデータ (DirSync コントロール データ) も同時に送信します。Active Directory に対して DirSync コントロールを含む要求を最初に行うときは、空白の Cookie が送信され、Active Directory は検索結果をすべて含む応答を Cookie 値と共に返します。Cookie 値はバイトの配列です。最後にクエリを行って以降何らかの変更が行われたかどうかを調べる場合は、次回の検索要求の DirSync コントロール メタデータに前回と同じ Cookie を含めて送信するだけです。

Cookie 自体は不明確ですが、そこに含まれている情報はかなり明確です。特に、ある種のバージョン番号、おそらくはレプリケーションに使用された USN が含まれていると想像できます。では、2 回目の要求では何が返されるのでしょうか。そう、最初の要求以降に変更が加えられたオブジェクトと属性のみが返されます。DirSync コントロールを使用すれば、ディレクトリから差分のみが送信されるため、実際に SqlDependency を使うよりも効率的にキャッシュすることができ、ネットワークへの影響を最小限に抑えて、キャッシュされたバージョンを最新状態に保つことができるのがわかります。同期する場合、またはこの例のように多くのデータをキャッシュする場合には非常に優れています。

System.DirectoryServices.Protocols

.NET Framework 2.0 以降を使用して Active Directory のプログラミングを行う場合、実際には 2 つの主な技法を使用できることがわかります。System.DirectoryServices と図 2 に示す ADSI スタックを使用する場合は、図 1 で使用したようなコードを使用できます。しかし、Framework のバージョン 2.0 では、LDAP ディレクトリと対話する新しい API が導入されました。この新しいインターフェイスは完全に管理され、ADSI とはまったく切り離されるため、多くの方法に新風を吹き込みます。ADSI では、LDAP の上位にプロパティ キャッシュや暗黙の接続管理など、一連の独自の抽象概念が追加されます。このような概念は便利ですが、実際に LDAP ネットワーク要求を実行するときに混乱の原因になり、やや不明確になることがあります。

新しい System.DirectoryServices.Protocols 名前空間には、こうした ADSI 関連の概念は含まれていません。これは WLDAP32.DLL のネイティブ LDAP API のすぐ上に位置する低レベルのインターフェイスです。このインターフェイスが好ましい点は、その明確な性質にあります。LDAP 用に TCP/IP 接続を開く場合、ユーザーが LdapConnection.Bind を呼び出して開くことになるため、接続が開かれるタイミングがわかります。要求を行う場合は、LdapConnection.SendRequest を呼び出して、実現を希望する内容にかかわるさまざまなクラスのメッセージを送信するだけなので、ネットワーク トラフィックが発生するタイミングが正確にわかります。非同期処理を行う場合に必要な BeginSendRequest と EndSendRequest もあります。

この新しい名前空間に重点を置いたサンプルがほとんどないため、ここで最初に扱い、このモデルを使用する ADDependency をビルドすることに決めました。このモデルが、いかに容易かつ自然に使用できるかを知り、非常に驚きました。ただし、前もってお伝えしておきます。この新しい API に関するドキュメントは、この記事の執筆時点ではほとんど存在していません。そのため、Active Directory に不慣れな場合は、まず、System.DirectoryServices からの従来の ADSI モデルを使用して基礎を理解してから、この新しい API を調べることをお勧めします。

ADDependency の使用モデル

SqlDependency に基づいて ADDependency をモデル化しました。一方を使用したことがあれば、もう一方を使用することはそれほど難しくはありません。図 3 に、ADDependency へのパブリック インターフェイスを示します。次のように、インスタンスを構築し、少なくとも検索のルートと LDAP フィルタを指定することから始めます。

Figure 3 ADDependency のパブリック インターフェイス (Close Figure 3)
public ADDependency(string dnSearchRoot, string ldapFilter,
    params string[] attrsToWatch) :
        this(“localhost”, dnSearchRoot, ldapFilter,
            DirectorySynchronizationOptions.ObjectSecurity, attrsToWatch)
{
}

public ADDependency(string adServer, string dnSearchRoot,
    string ldapFilter, DirectorySynchronizationOptions dirSyncOptions,
    params string[] attrsToWatch)
{
    this.adServer = adServer;
    this.dnSearchRoot = dnSearchRoot;
    this.ldapFilter = ldapFilter;
    this.attrsToWatch = attrsToWatch;
    this.dirSyncOptions = dirSyncOptions;
}

public void Start()
{
    if (null != conn) throw new InvalidOperationException(
        “You may only call Start one time.”);
    conn = new LdapConnection(new LdapDirectoryIdentifier(adServer),
        null, AuthType.Negotiate);
    conn.Bind();

    timer = new Timer(timerCallback, null,
        TimeSpan.FromSeconds(0), pollingInterval);
}

public void Stop()
{
    timer.Dispose();
    conn.Dispose();
}

public event ChangedEventHander Changed;

public delegate void ChangedEventHander(object sender,
    ChangedEventArgs args);

public class ChangedEventArgs : EventArgs
{
    public ChangedEventArgs(SearchResponse response)
    {
        this.Response = response;
    }
    public readonly SearchResponse Response;
}

ADDependency dep = new ADDependency(
    “dc=contoso,dc=com”, “(objectclass=user)”);

依存関係のインスタンスを作成したら、データを同期またはキャッシュしている間は保持しておく必要があります。つまり、ADDependency では Cookie と、サーバーへの基になる LDAP 接続を管理することになります。ここで、次のように、変更通知を受け取るために Changed イベントを構成する必要があります。

dep.Changed += OnChange;
...
void OnChange(object sender, ChangedEventArgs args)
{
    foreach (SearchResultEntry e in args.Response.Entries)
    {
        Console.WriteLine(e.DistinguishedName);
    }
}

ここに示した簡単なハンドラでは、最近変更されたオブジェクトの名前をすべて出力しているだけです。実際の実装では、コレクション内の各オブジェクトの属性を使ってキャッシュ内のデータを更新するか、キャッシュを無効にして、ディレクトリからデータを再度読み取って、最終的にデータを同期して再設定します。

接続を開き、クエリを開始する準備が整ったら、単純に Start を呼び出します。

dep.Start();

これにより、System.Threading.Timer のインスタンスが起動され、変更が行われたかどうかを確認するために、既定では N 秒ごとに Active Directory にポーリングを行います。N は定数で、ポーリング操作の負荷とキャッシュの一貫性のバランスが取れるように調整する必要があります。いったん実行されてしまえば、何か変更が行われない限り、要求の負荷はそれほど高くなりません。何も変更されていないことを確認するために、サーバーとのやり取りが発生するだけです。ただし、初回起動時には、キャッシュを初期化できるように、すべてのオブジェクトのリストが返されることに注意してください。

処理が完了し、接続を閉じるときは、Stop メソッドを呼び出します。現在のデザインでは、いったん停止したインスタンスを再起動することはできません (これを行うと例外が発生します)。

ADDependency と SqlDependency を比較すると、その動作方法に大きな違いがあることに気付きます。ADDependency ではディレクトリに対して N 秒ごとにポーリングが行われ、毎回の要求で差分を受け取るため、ローカル キャッシュの増分更新を実行できます。一方 SqlDependency ではポーリングは行われず、SQL Server 2005 から発行される通知を受け取ります。呼び出し側は差分を取得するのではなく、ディレクトリに戻り、実際のデータを取得しなければなりません。

ポーリングは悪いことでしょうか。あまり頻繁に行うと、問題になる可能性があります。ディレクトリ データがほとんど変化しないならば、ポーリング間隔の頻度をあまり高くすべきではありません。他の多くの決定事項と同様に、アプリケーションが使用するデータ アクセス パターンを想定し、ポーリング間隔が妥当であるかどうかを測定して、決定するのが最適です。

詳しく調べる

図 3 を詳しく調べてみると、ADDependency のコンストラクタではこれまで示してきたことよりも多くのことが行われているのがわかるでしょう。たとえば、mail、title、manager など、関心のある LDAP 属性のリストを渡すことができます (渡す必要があります)。これにより、受信する初期データのサイズと、その後の差分データのサイズが制限されます。また、DC の負荷も軽減されます。

コンストラクタで次に興味深い点は、特定の DC を指定する必要があることです (図 1 のコンストラクタは、DC 上で実行され、localhost に接続すると想定されています。これはラボの設定では正しく機能しますが、企業内では正しく機能する可能性が低くなります)。特定の DC を指定し、そのまま使用し続けることが重要です。これは、特定のインスタンスでは、やがて 2 台の DC 間で特定のオブジェクトに関する現状が必ずしも一致しなくなる可能性があるためです。Active Directory での更新は SQL データベースに比べればそれほど頻繁には行われませんが、時折更新が行われることがあり、そのレプリケーションにはある程度の時間がかかることを思い出してください。さらに、ある DC が発行した DirSync Cookie が異なる DC に提供された場合に意味をなすかどうかはドキュメントに記載されていません。ただし、アプリケーションで Cookie の名前をハードコーディングするよりは、DC ごとに構成可能にしておくことをお勧めします。これを行うには、グループ ポリシーやアプリケーション構成ファイルを使用するか、System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain のようなメソッドを使用します。このメソッドは、そのユーザーの現在のドキュメント情報を取得する場合に使用します。

SqlDependency で使用するクエリとまったく同様に、ADDependency で実行できるクエリにもいくつか制限事項があります。検索ルートはパーティションのルートにする必要があります。今回の例で、検索ルートを dc=contoso,dc=com に設定している方法に注意してください。これは、contoso.com でホストされているディレクトリのアプリケーション パーティションのルートです。ただし、cn=users,dc=contoso, dc=com を使用して検索範囲を狭めようとすると、Start メソッドで例外がスローされます。これは、単なる DirSync コントロールの制限事項です。監視するデータを絞り込む場合は、代わりに、ldapFilter 引数と attrsToWatch 引数を使用する必要があります。

この作業はすべて DirSyncRequestControl クラスと DirSyncResponseControl クラスで行われます (図 4 参照)。これがサーバーとの間で同期 Cookie (および、一連の同期オプション) を受け渡す方法です。ここで最も重要なオプションは ObjectSecurity です。このオプションは、Windows Server® 2003 でのみサポートされます。このオプションがなければ、DirSync コントロールを使用するのに高い特権が必要になります。このオプションにより、特権の低いアカウントを使用でき、オブジェクトへの変更を監視することのみ可能になります。このような監視では、通常、読み取りを許可するだけで十分です。

Figure 4 ADDependency の実装 (Close Figure 4)
private void timerCallback(object o)
{
    beginPollDirectory();
}

private void beginPollDirectory()
{
    SearchRequest request = new SearchRequest(dnSearchRoot,
        ldapFilter, SearchScope.Subtree, attrsToWatch);
    request.Controls.Add(
        new DirSyncRequestControl(cookie, dirSyncOptions));
    conn.BeginSendRequest(request, requestTimeout,
        PartialResultProcessing.NoPartialResultSupport,
        endPollDirectory, null);
}

private void endPollDirectory(IAsyncResult asyncResult)
{
    // harvest results
    SearchResponse response =
        SearchResponse)conn.EndSendRequest(asyncResult);

    // grab the current search cookie
    foreach (DirectoryControl control in response.Controls)
    {
        DirSyncResponseControl dsrc = control as DirSyncResponseControl;
        if (null != dsrc) cookie = dsrc.Cookie;
    }

    // if anything changed, notify any listeners
    if (response.Entries.Count > 0 && null != Changed)
    {
        Changed(this, new ChangedEventArgs(response));
    }
}

イベントのシーケンスに注意します。まず、timerCallback を呼び出すことで、タイマが起動されます。このメソッドにより、LdapConnection.BeginSendRequest を使用して非同期要求が DC に送信されることになります。ADDependency の endPollDirectory メソッドで結果が収集され、その後 Changed イベントが発生します。このイベントのイベント引数に検索応答がパッケージ化されます。

ADDependency の完全な実装については、この記事に関連付けられたサンプル コードをダウンロードしてください。このサンプル コードに含まれる例には、ディレクトリ内のユーザー オブジェクトを監視し、変更が加えられている場合に出力を行うコンソール アプリケーション、ADCacheDependency というカスタム CacheDependency を実装する ASP.NET アプリケーション、および N 秒ごとにユーザーの役職をランダムに交換する、変更のランダム エージェントとして機能するコンソール アプリケーションがあります (かつて、このような会社に勤務したことはありますか)。ADCacheDependency では、ディレクトリの変更を検出した時点で ASP.NET のキャッシュ項目を無効にします。この動作方法は SqlCacheDependency と同じです。

記事を終える前に、DirectoryNotification LDAP コントロールに触れておきます。このコントロールは、今後調査するのにふさわしい分野です。このコントロールを使用した例はこれまで 1 つも見たことはありません。ただし、検索で何か関心のある事項 (変更など) を返す場合に、このコントロールを使用すれば、非同期検索を構成して結果を受け取ることができるように思えます。キャッシュされたオブジェクトをディレクトリからすべて再読み取りする計画がある場合 (ASP.NET で SqlCacheDependency が機能するのとまったく同じ方法です)、変更検出のかなり軽量な手法になると考えられます。

詳細情報

このコラムを読んで、ディレクトリ サービスのプログラミングに関心をお持ちになった読者にヒントを差し上げましょう。Joe Kaplan および Ryan Dunn 共著の『The .NET Developer’s Guide to Directory Service Programming』(英語) のコピーを入手してください。マネージ コードからの Active Directory のプログラミングを扱った書籍はあまり多くありませんが、この書籍は、私がこの種の作業を行う場合に必ず机上に用意する 1 冊です。

もう、迷うことはありません。いくつかコードを記述して、ローカル ディレクトリに何が見つかるかを探してください。

ご質問やご意見は、

Keith briefs@microsoft.com.

まで英語でお送りください。
Keith Brownは、Microsoft .NET の第一のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Keith は、Pluralsight の Applied .NET Security コースの作成者で、『The .NET Developer's Guide to Windows Security』 (印刷物および Web で提供) をはじめとする数冊の書籍の著者でもあります。詳細については、www.pluralsight.com/keith (英語) を参照してください。


基礎: WPF アニメーション クラスを拡張する

基礎
WPF アニメーション クラスを拡張する
Charles Petzold

コードのダウンロード : : Foundations2007_07.exe (200KB)
コンテンツ
補間値
クラスの構造
フリーズ アウト
テキストのモーフィング
キーフレーム アニメーション
3D ポイントのアニメーション処理

Microsoft® Windows® Presentation Foundation のアニメーションのサポートは、System.Windows.Media.Animation 名前空間にほぼ集約されています。 これは、22 個の特定のデータ型のアニメーション プロパティ用の 164 個のクラスを定義する大規模な名前空間です。 これらの 22 個のデータ型は、byte、int、double などの基本型から Matrix、Point3D、Quaternion などの珍しい型まで多岐にわたります。

Windows Presentation Foundation のアニメーション機能は確かに優れていますが、これらの 22 個のデータ型であらゆるアプリケーションに対応できると考えるのは、プログラマ以外のユーザーだけでしょう。 私自身は、オブジェクトのコレクション全体、特に座標点のコレクションをアニメーション処理したいと思うアプリケーションに何度も出会いました。 既存の API には、個々の 2D 座標または 3D 座標をアニメーション処理する PointAnimation や Point3DAnimation などのクラスが用意されています。しかし私は、2 つのコレクションの対応するメンバの間を補間することによって点のコレクション全体をアニメーション処理する PointCollectionAnimation クラスや Point3DCollectionAnimation クラスが欲しかったのです。

Microsoft .NET Framework、特に Windows Presentation Foundation では、オブジェクト自体とほとんど同じくらい簡単にオブジェクトのコレクションを定義して使用できます。 Windows Presentation Foundation では PointCollection クラスと Point3DCollection クラスが既に定義されており、XAML でこれらのコレクションを使用するのはささいなことです。 XAML では、Point 型のプロパティを割り当てることと PointCollection 型のプロパティを割り当てることの違いは、「100 50」と入力するか「100 50, 40 25, 35 80, 50 100」と入力するかの違いです。それなら、これらの型のプロパティをアニメーション処理するのも簡単だろうと私は考えました。

架空の PointCollectionAnimation クラスは、PointCollection 型のプロパティをアニメーション処理します。 Windows Presentation Foundation には、この型のプロパティを持つ 5 つのクラス、つまり Polyline と Polygon (いずれも Shape から派生)、PolyLineSegment、PolyBezierSegment、および PolyQuadraticBezierSegment (PathSegment から派生) が組み込まれており、5 つのクラスすべてでこのプロパティは Points という名前になっています。 この Points プロパティをアニメーション処理することにより、1 つのアニメーションを使用するだけでグラフィック図形の形を変化させることができます。

私の知る限り、Point3DCollection を含むクラスは 1 つだけですが、それは Windows Presentation Foundation の 3D グラフィック システムの中心に位置する MeshGeometry3D クラスです。 Point3DCollection をアニメーション処理できれば、一般的にはかなり高度な 3D プログラミング タスクと考えられている 3D オブジェクトの変形も簡単に行うことができます。

コンピュータ画面上で 2D 図形や 3D 図形の入れ替え、モーフィング、変形を行いたいという思いが、私が Windows Presentation Foundation のアニメーション クラスを拡張するきっかけになりました。

補間値

Windows Presentation Foundation では、アニメーション処理は基本的に補間であり、通常は線形補間です。 新しいアニメーション クラスのコーディングで難しいのは、補間自体ではなく、補間する正確な値を決定することです。

Point 値のアニメーション処理に関する簡単な例を見てみましょう。 次のように、塗りつぶした円を XAML で描画できます。

<Path Fill=”Blue”>
    <Path.Data>
        <EllipseGeometry x:Name=”ellipse”
            RadiusX=”10” RadiusY=”10” />
    </Path.Data>
</Path>

このマークアップでは、Point 型のプロパティである、EllipseGeometry の Center プロパティが指定されていません。 この Center プロパティを、次のように PointAnimation 要素を使用してアニメーション処理できます。

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                From=”10 10” To=”100 100”
                Duration=”0:0:3” />

Center プロパティは、3 秒間で点 (10, 10) から点 (100, 100) までアニメーション処理されます。つまり、PointAnimation クラスは、経過時間に基づいてこれらの 2 つの値の間の線形補間を実行します。

EllipseGeometry マークアップで Center プロパティを次のように指定することもできます。

<EllipseGeometry x:Name=”ellipse” Center=”50 50” ...

この値に基づいてアニメーションを実行できるため、この値をベース値と呼びます。 次に示すマークアップのように、PointAnimation マークアップの From 属性または To 属性を省略することにより、このベース値を利用できます。

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                To=”100 100”
                Duration=”0:0:3” />

この場合、アニメーションは EllipseGeometry マークアップで指定された中心点 (50, 50) で開始され、点 (100, 100) で終了します。 また、次の構文も使用できます。

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                From=”100 100”
                Duration=”0:0:3” />

今度は、点 (100, 100) でアニメーションが開始され、点 (50, 50) で終了します。 From 属性または To 属性が指定されておらず、EllipseGeometry マークアップで Center 属性が指定されていない場合は、Center プロパティの既定値である (0, 0) がアニメーションに使用されます。

処理はより煩雑になります。 To プロパティの代わりに By プロパティを使用しますが、これは、開始点に加算する値を示します。

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                From=”100 100” By=”125 125”
                Duration=”0:0:3” />

このアニメーションは点 (100, 100) で開始され、点 (225, 225) で終了します。 しかし、From 属性を省略することもできます。

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                By=”125 125”
                Duration=”0:0:3” />

この場合、アニメーションは EllipseGeometry マークアップで指定した点 (50, 50) で開始され、点 (175, 175) で終了します。

アニメーションのベース値は、多くの場合、アニメーション処理されるプロパティに対して元から定義された値ですが、それ以外の値も可能です。 次の 2 つの連続する PointAnimation オブジェクトについて考えてみましょう。

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                To=”100 100” Duration=”0:0:3” />

<PointAnimation Storyboard.TargetName=”ellipse”
                Storyboard.TargetProperty=”Center”
                BeginTime=”0:0:5” Duration=”0:0:3” />

最初の 3 秒間、1 番目のアニメーションによって Center プロパティの値がベース値 (50, 50) から To 値 (100, 100) まで変化し、2 番目のアニメーションが開始されるまでの 2 秒間は値がそのまま維持されます。2 番目のアニメーションでは、From 属性、To 属性、および By 属性がいずれも指定されていません。 このアニメーションはベース値 (100, 100) で開始され、EllipseGeometry マークアップで指定された値 (50, 50) で終了します。

次のセクションで説明するように、PointAnimation などのクラスにはアニメーションの開始と終了のための 2 つの既定値が用意されており、特定のアニメーション オブジェクトに対して From プロパティ、To プロパティ、および By プロパティが定義されていない場合は、これらの既定値が使用されます。 このようなクラスでは、アニメーション処理される値をベース値に加算する IsAdditive と、アニメーションが繰り返される場合にアニメーション処理される値を累積する IsCumulative という 2 つのブール型プロパティも定義します。 これらのプロパティによってアニメーション処理のロジックが単純化されることはありません。

クラスの構造

さまざまなアニメーション クラスを、DoubleAnimationBase、PointAnimationBase などの 22 個のクラスを示す <Type>AnimationBase のような名前でまとめて呼ぶことがよくあります。 しかし、このように便利な表記方法があるからといって、アニメーション クラスをジェネリックとして実装できるわけではありません。それぞれのクラスが特定のデータ型に固有の計算を実行する必要があるため、それは不可能です。 図 1 に、アニメーション クラスの一般的なクラス階層を示します (ただし、この図にはアニメーション処理できる 22 個の型のバリエーションがいくつか含まれています)。

図 1 アニメーション クラスの階層
図 1 アニメーション クラスの階層(画像を縮小するには、ここをクリックします)
図 1 アニメーション クラスの階層
図 1 アニメーション クラスの階層 (画像を拡大するには、ここをクリックします)

カスタム型をアニメーション処理するクラスを作成する場合は、既存のアニメーション クラスの構造を模倣することができます。 一般的には、まず <Type>AnimationBase 抽象クラスを定義し、このクラスから、線形補間に基づいてアニメーションを実行する通常の <Type>Animation クラスを派生させます。 私は PointCollection 型のプロパティをアニメーション処理しようとしているので、まず PointCollectionAnimationBase というクラスを定義しました。 このクラスは AnimationTimeline から派生させ、抽象クラスとして定義します。 読み取り専用の TargetPropertyType をオーバーライドし、typeof(PointCollection) を返すようにする必要もあります。

新しい <Type>AnimationBase クラスでは、AnimationTimeline によって定義された GetCurrentValue メソッドもオーバーライドします。

public override object GetCurrentValue(
    object defaultOriginValue,
    object defaultDestinationValue,
    AnimationClock animationClock)
{
    ...
}

新しいクラスは、このメソッドでアニメーション処理用の補間値を計算する必要があります。 パラメータ名は、ドキュメントで使用されているパラメータ名です。 最初の 2 つのパラメータは、アニメーションの開始位置と終了位置になる既定のベース値を示します。 From、To、および By が明示的に設定されていない場合は、これらのパラメータが使用されます。 AnimationClock オブジェクトには、0 ～ 1 の範囲の値を持つ CurrentProgress という名前の double 型のプロパティが含まれています。この値が補間に使用されます。

TargetPropertyType プロパティをオーバーライドしたため、WPF はアニメーション処理しようとしているオブジェクトの型を認識します。 新しいクラスが PointCollection 型のオブジェクトをアニメーション処理する場合、defaultOriginValue パラメータと defaultDestinationValue パラメータは PointCollection 型になり、WPF はこのメソッドが PointCollection 型のオブジェクトを返すと想定します。

既存のアニメーション クラスをエミュレートする場合、GetCurrentValue は単純にそのパラメータを適切な型にキャストし、クラスで定義された別の GetCurrentValue メソッドを呼び出すことができます。呼び出された別の GetCurrentValue メソッドは、GetCurrentValueCore というメソッドを呼び出します。 PointCollection オブジェクトをアニメーション処理するためのコードは次のようになります。

public PointCollection GetCurrentValue(
    PointCollection defaultOriginValue,
    PointCollection defaultDestinationValue,
            AnimationClock animationClock)
{
    return GetCurrentValueCore(defaultOriginValue,
        defaultDestinationValue,
        animationClock);
}

次に、GetCurrentValueCore メソッドをプロテクトされた抽象メソッドとして定義します。

protected abstract PointCollection GetCurrentValueCore(
    PointCollection defaultOriginValue,
    PointCollection defaultDestinationValue,
    AnimationClock animationClock);

ここで、この GetCurrentValueCore メソッドをオーバーライドし、補間用の実際のコードを提供するために、PointCollectionAnimationBase から派生させた非抽象クラスが必要になります。

このコラムでダウンロードできるコードは、5 つのプロジェクトを含む 1 つの Visual Studio® ソリューションで構成されています。 これらのプロジェクトの 1 つである Petzold.AnimationExtensions という名前の DLL には、この記事で説明した、PointCollection 型と Point3DCollection 型のオブジェクトをアニメーション処理するクラスが含まれています。 この Visual Studio ソリューションの他の 4 つのプロジェクトは、その DLL を使用するデモンストレーション プログラムです。 PointCollectionAnimationBase クラスはこの DLL に含まれ、PointCollectionAnimationBase.cs というファイルに実装されています。 同様に、この DLL に含まれる他のクラスとインターフェイスはすべて、そのクラスまたはインターフェイスにちなんだ名前のファイルに含まれています。

フリーズ アウト

すべての <Type>AnimationBase クラスは、Animatable と Timeline を使用して Freezable クラスから派生させた AnimationTimeline を継承します。 実際、Petzold.AnimationExtensions DLL に含まれるすべてのクラスは何らかの方法で Freezable から派生させたクラスであり、そのことがいくつかの重要な影響をもたらします。

Freezable から派生したクラスには、非常に特殊な要件がいくつかあります。 これらの要件を無視する場合は、自己責任において行ってください。 その場合は、原因不明の動作不良や、実際の問題に関する手がかりがまったく得られない予想外の例外が発生することがあります。

Freezable から派生した非抽象クラスはすべて、CreateInstanceCore メソッドをオーバーライドする必要があります。 通常、このメソッドが行う必要があるのは、クラスのコンストラクタの呼び出しだけです。 Freezable はこのメソッドを抽象メソッドとして定義するため、このメソッドを含めないとコンパイラから警告が出されます。 ただし、このメソッドがないことをコンパイラが通知するのは、非抽象クラスが Freezable より下の階層で 1 番目の非抽象クラスである場合のみです。 非抽象クラスから派生させる場合、CreateInstanceCore を忘れがちです。

また、Freezable から派生したクラスは、そのすべてのプロパティを依存関係プロパティでサポートする必要があります。 これは必須条件ではありませんが、これを行わない場合は、他の 5 つのメソッドをオーバーライドし、複製とフリーズ処理について詳しく学ぶ必要があります。

一般的な線形補間アニメーションを実行するには、<Type>AnimationBase から派生させた <Type>Animation という形式のクラスを作成する必要があります (この例では PointCollectionAnimation クラスであり、2 つの PointCollection オブジェクト間の対応する点の間で補間を実行します)。このクラスは CreateInstanceCore メソッドをオーバーライドし、依存関係プロパティでサポートされた From、To、および By という名前のプロパティを定義する必要があります。 さらに、IsAdditive プロパティと IsCumulative プロパティも定義する必要があります。これらの 2 つのプロパティの依存関係プロパティは、AnimationTimeline によって既に定義されています。

<Type>Animation の最大の任務は、GetCurrentValueCore のオーバーライドです。 最後の引数は AnimationClock 型のオブジェクトで、CurrentProgress プロパティはアニメーションの進行状況を示す 0 ～ 1 の範囲の数値です。 この値は、Timeline クラスによって定義されて <Type>Animation に継承された AccelerationRatio、DecelerationRatio、AutoReverse、SpeedRatio などのプロパティを考慮します。 ただし、GetCurrentValueCore は、どの値の間を補間するかを最初に判断する必要があります。 たとえば、From プロパティは設定されているが To プロパティまたは By プロパティが設定されていない場合は、From とメソッドの defaultDestinationValue パラメータの間の補間が実行されます。

そのため、コードまたはマークアップで From、To、および By が設定されているか、または設定されていないかを GetCurrentValueCore が認識することが重要です。 既存の Windows Presentation Foundation アニメーションのほとんどは、Double 構造体や Point 構造体などの値型に基づいています。 From プロパティが既定値の 0 なのか特別に 0 に設定されたのかを、DoubleAnimation クラスはどのような方法で判断できるのでしょうか。

その方法は簡単です。 <Type> が値型であるすべての <Type>Animation クラスでは、From プロパティ、To プロパティ、および By プロパティが null 許容型として定義されます (私はこれらのプロパティを設定する以外の操作を行う機会がなかったので、今までずっとこのことに気付きませんでした)。依存関係プロパティの定義によって既定値が null に設定されるため、GetCurrentValueCore はプロパティが設定されているかどうかを簡単に判断できます。

同様に、PointCollectionAnimation などのクラスではアニメーション処理されるオブジェクトが参照型になるため、From プロパティ、To プロパティ、および By プロパティが設定されていない場合、これらのプロパティは null に設定されます。 ただし、参照型のアニメーション処理には他のもっと複雑な問題が伴います。 特に、GetCurrentValueCore を呼び出すたびにメモリ割り当てが行われるような事態は避けたいものです。 そこで、クラスはアニメーション処理される型のオブジェクトをフィールドとして作成し、GetCurrentValueCore メソッドからそのオブジェクトを返すようにする必要があります。

実際、PointCollectionAnimation を研究しているときに、私は GetCurrentValueCore の連続呼び出し中に単に同じオブジェクトを返すだけでは不十分であることに気付きました。 GetCurrentValue の受信側のユーザーが誰であっても、別のオブジェクトが必要です。 そこで私は、2 つの PointCollection オブジェクトをフィールドとして作成し (補間先であることから、フィールドの名前は ptsDst1 と ptsDst2 にしました) 、GetCurrentValueCore の連続呼び出し中に flip という名前のブール型フィールドに基づいてこれらの 2 つのオブジェクトを交互に切り替えることにしました。 また、メソッドの補間先の点のコレクションを格納するフィールドとして、ptsTo という PointCollection オブジェクトも作成しました。 このコレクションが別個に必要なのは By プロパティのためで、このコレクションは 2 つのコレクションをまとめたものになります。

PointCollectionAnimation における GetCurrentValueCore の実装を図 2 に示します。

Figure 2 PointCollectionAnimation の GetCurrentValueCore (Close Figure 2)
protected override PointCollection GetCurrentValueCore(
    PointCollection defaultOriginValue,
    PointCollection defaultDestinationValue,
    AnimationClock animationClock)
{
    // Let’s hope this doesn’t happen too often
    if (animationClock.CurrentProgress == null) return null;

    double progress = animationClock.CurrentProgress.Value;
    int count;

    // Set ptsFrom from From or defaultOriginValue
    PointCollection ptsFrom = From ?? defaultOriginValue;

    // Set ptsTo from To, By, or defaultOriginValue
    ptsTo.Clear();

    if (To != null)
    {
        foreach (Point pt in To) ptsTo.Add(pt);
    }
    else if (By != null)
    {
        count = Math.Min(ptsFrom.Count, By.Count);

        for (int i = 0; i < count; i++)
            ptsTo.Add(new Point(ptsFrom[i].X + By[i].X,
                                ptsFrom[i].Y + By[i].Y));
    }
    else
    {
        foreach (Point pt in defaultDestinationValue) ptsTo.Add(pt);
    }

    // Choose which destination collection to use
    PointCollection ptsDst = flip ? ptsDst1 : ptsDst2;
    flip = !flip;
    ptsDst.Clear();

    // Interpolate the points
    count = Math.Min(ptsFrom.Count, ptsTo.Count);

    for (int i = 0; i < count; i++)
    {
        ptsDst.Add(new Point((1 - progress) * ptsFrom[i].X +
                                  progress  * ptsTo[i].X,
                             (1 - progress) * ptsFrom[i].Y +
                                  progress  * ptsTo[i].Y));
    }

    // If IsAdditive, add the base values to ptsDst
    if (IsAdditive && From != null && (To != null || By != null))
    {
        count = Math.Min(ptsDst.Count, defaultOriginValue.Count);

        for (int i = 0; i < count; i++)
        {
            Point pt = ptsDst[i];
            pt.X += defaultOriginValue[i].X;
            pt.Y += defaultOriginValue[i].Y;
            ptsDst[i] = pt;
        }
    }

    // Take account of IsCumulative
    if (IsCumulative && animationClock.CurrentIteration != null)
    {
        int iter = animationClock.CurrentIteration.Value;

        for (int i = 0; i < ptsDst.Count; i++)
        {
            Point pt = ptsDst[i];
            pt.X += (iter - 1) * (ptsTo[i].X - ptsFrom[i].X);
            pt.Y += (iter - 1) * (ptsTo[i].Y - ptsFrom[i].Y);
            ptsDst[i] = pt;
        }
    }

    // Return the PointCollection
    return ptsDst;
}

特定のアニメーション処理では、PointCollection のフィールドがすべての点を格納するのに十分なサイズになるように再割り当てが行われるため、GetCurrentValueCore の最初の 2 回の呼び出しでメモリ割り当てが行われる可能性があります。 しかし、いったんフィールドが適切なサイズに設定された後は、それ以上のメモリ割り当てを行うことなくアニメーションが続行されます。

点のセットを単純に変換、拡大縮小、または回転する場合は、通常の変換クラスでこれらの操作を実行できるため、PointCollectionAnimation を使用する意味はありません。 このクラスは、通常以外の非線形変換に最も役立ちます。たとえば、図 3 の SquaringTheCircle.xaml ファイルは、正方形の面積を変えずに図を円に変形し、また正方形に戻す PointCollection のアニメーション処理を行うことにより、ユークリッドの時代から西洋文明を悩ませてきた幾何学の問題を解決しています。

Figure 3 SquaringTheCircle.xaml (Close Figure 3)
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
        xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
        xmlns:pae=”clr-namespace:Petzold.AnimationExtensions;
                   assembly=Petzold.AnimationExtensions”
        x:Class=”SquaringTheCircle.SquaringTheCircle”
        Title=”Squaring the Circle”>
  <Canvas RenderTransform=”2 0 0 -2 300 300”>
    <Path StrokeThickness=”3” Stroke=”Blue” Fill=”AliceBlue”>
      <Path.Data>
        <PathGeometry>
          <PathFigure x:Name=”fig” IsClosed=”True”>
            <PolyBezierSegment x:Name=”seg” />
          </PathFigure>
          <PathGeometry.Transform>
            <RotateTransform Angle=”45” />
          </PathGeometry.Transform>
        </PathGeometry>
      </Path.Data>
    </Path>
  </Canvas>

  <Window.Triggers>
    <EventTrigger RoutedEvent=”Window.Loaded”>
      <BeginStoryboard>
        <Storyboard RepeatBehavior=”Forever” AutoReverse=”True”>
          <PointAnimation Storyboard.TargetName=”fig”
                          Storyboard.TargetProperty=”StartPoint”
                          From=”0 100” To=”0 125” />

          <pae:PointCollectionAnimation
            Storyboard.TargetName=”seg”
            Storyboard.TargetProperty=”Points”
            From=”  55  100,  100   55,  100    0,
                   100  -55,   55 -100,    0 -100,
                   -55 -100, -100  -55, -100    0,
                  -100   55,  -55  100,    0  100”
            To=” 62.5  62.5,  62.5  62.5,  125    0,
                 62.5 -62.5,  62.5 -62.5,    0 -125,
                -62.5 -62.5, -62.5 -62.5, -125    0,
                -62.5  62.5, -62.5  62.5,    0  125” />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Window.Triggers>
</Window>

WPF に関する私の著書で紹介している機能的に同等なプログラムでは、個別の Point アニメーション処理を 13 も実行する必要がありました。 もちろん私は、洗練されたこの新しいバージョンの方が好きです。

テキストのモーフィング

TextMorphCheckBox プロジェクトは、CheckBox のコンテンツ (存在する場合) にテンプレート リソースとして定義された 2 つの PointCollection オブジェクトのいずれかを参照する Polyline 要素を重ね合わせる CheckBox コントロールのテンプレートを定義しています。 CheckBox をクリックすると、その PointCollection オブジェクトともう 1 つの PointCollection オブジェクトの間でアニメーションが実行されます。 ControlTemplate マークアップのかなりの部分を図 4 に示します。

Figure 4 TextMorphCheckBox.xaml の抜粋 (Close Figure 4)
<ControlTemplate x:Key=”templateTextMorph”
                 TargetType=”{x:Type CheckBox}”>
  <ControlTemplate.Resources>
    <PointCollection x:Key=”textYes”>
       5 11,  3 10,  2  8,  2  7,  3  5,  5  4,  6  4,  8  5,
       9  7,  9  9,  8 13,  7 16,  6 20,  6 22,  7 24,  8 25,
      10 25, 12 24, 14 22, 16 19, 17 17, 19 11, 21  4, 19 11,
      16 21, 14 27, 12 32, 10 36,  8 37,  7 36,  7 34,  8 31,
      10 28, 13 26, 17 25, 23 23, 25 22, 26 21, 27 19, 27 17,
      26 16, 25 16, 23 17, 22 19, 22 22, 23 24, 25 25, 27 25,
      29 24, 30 23, 32 20, 34 17, 35 15, 35 17, 37 20, 38 22,
      38 24, 36 25, 32 24, 34 25, 38 25, 40 24, 41 23, 43 20
    </PointCollection>

    <PointCollection x:Key=”textNo”>
       5 11,  3 10,  2  8,  2  7,  3  5,  5  4,  6  4,  8  5,
       9  7,  9  8,  9  9,  8 14,  7 18,  5 25,  7 18, 10 10,
      11  8, 12  6, 13  5, 15  4, 17  4, 19  5, 20  7, 20  8,
      20  9, 19 14, 17 21, 17 24, 18 25, 19 25, 21 24, 22 23,
      24 20, 25 18, 26 17, 28 16, 29 16, 30 16, 29 16, 28 16,
      26 17, 25 18, 24 20, 24 21, 24 22, 25 24, 27 25, 28 25,
      29 25, 31 24, 32 23, 33 21, 33 20, 33 19, 32 17, 30 16,
      29 17, 29 18, 29 19, 30 21, 32 22, 35 22, 37 21, 38 20
    </PointCollection>
  </ControlTemplate.Resources>
  ...
  <!-- This Border displays the text -->
  <Border>
    <Border.Background>
      <VisualBrush Stretch=”Uniform”>
        <VisualBrush.Visual>
          <Polyline Name=”polyline”
                    Stroke=”{TemplateBinding Foreground}”
                    StrokeThickness=”2”
                    StrokeStartLineCap=”Round”
                    StrokeEndLineCap=”Round”
                    StrokeLineJoin=”Round”
                    Points=”{StaticResource textNo}” />
        </VisualBrush.Visual>
      </VisualBrush>
    </Border.Background>
  </Border>
  ...
  <!-- Triggers convert text from No to Yes and back -->
  <ControlTemplate.Triggers>
    <EventTrigger RoutedEvent=”CheckBox.Checked”>
      <BeginStoryboard>
        <Storyboard TargetName=”polyline”
                    TargetProperty=”Points”>
          <pae:PointCollectionAnimation
                    To=”{StaticResource textYes}” />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>

    <EventTrigger RoutedEvent=”CheckBox.Unchecked”>
      <BeginStoryboard>
        <Storyboard TargetName=”polyline”
                    TargetProperty=”Points”>
          <pae:PointCollectionAnimation
                    To=”{StaticResource textNo}” />
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  ...
  </ControlTemplate.Triggers>
</ControlTemplate>

このアニメーションで使用される 2 つの PointCollection オブジェクトには、スクリプト フォントの No と Yes という単語の定義が含まれています。 これらの 2 つのコレクションの間でアニメーション処理を実行することにより、図 5 に示すように、1 秒の間にテキストを一方の単語からもう一方の単語にモーフィングさせることができます。

図 5a テキストをモーフィングする CheckBox
図 5a テキストをモーフィングする CheckBox

図 5b

図 5c

図 5d

図 5e

図 5f

テキストのモーフィングの途中で CheckBox をクリックすると、アニメーションは逆に実行されます。このことは、GetCurrentValueCore メソッドに渡されたパラメータを PointCollectionAnimation クラスが正しく処理していることを示しています。

ここで、TextMorphCheckBox テンプレートの 2 つのポリラインの入手元について説明しておきましょう。 その昔、"Rock Me Amadeus" がヒットしていた頃に Windows を使って仕事していた方は、Windows に原始的なベクタ フォントが含まれていたことをおそらく覚えているでしょう。 これらのフォントは今でも Windows Vista™ に残っており、Modern.fon ファイル、Roman.fon ファイル、および Script.fon ファイルに含まれています。 これらのフォントの文字は単純に直線の線分で定義され、多くの場合、プロッタで使用されていました。 TrueType フォントのような曲線、ヒント付け、塗りつぶしはありません。 Notepad ではこれらの古いベクタ フォントを選択できますが、Microsoft Word の美しい文書にこの見栄えの悪いフォントを挿入しようとしたら Microsoft Word が機能しなくなってしまうでしょう。

フォント ファイル形式に関する知識が少しあれば、これらのフォント ファイルを簡単に解読して、各文字を定義するポリラインを取り出すことができます。 この作業の後で、私は個々の文字の座標を手作業で調整して、No と Yes という単語を示す単一の連続するポリラインを作成しました。さらに、両方の PointCollection オブジェクトが同じポイント数になるように微調整を加えました。 いくつかの制御点を挿入して座標を一連の二次ベジエ曲線に変換する心の準備もしていましたが、その必要はありませんでした。 ちょうど良い太さのペンによって、この風変わりな目的にぴったりなポリラインが得られました。

キーフレーム アニメーション

Windows Presentation Foundation animation を扱ってきたプログラマは、<Type>Animation クラスが、より多機能な <Type>AnimationUsingKeyFrames に代わる簡単なクラスであることを知っています。 キーフレーム アニメーションでは、一連の不連続なジャンプ、線形アニメーション、および加速したり減速したりするアニメーションをベジエ曲線の線分に基づいて組み合わせることができます。 各キーフレームは、時間と値で構成されます。 このアニメーション処理では、ベース値 (通常は前のキーフレームの終了値) とキーフレーム値の間を補間します。

私の予想どおり、PointCollectionAnimationUsingKeyFrames クラスの実装はやりがいのある作業でした。 最初に、Freezable を継承し IKeyFrame インターフェイスを実装する PointCollectionKeyFrame 抽象クラスを派生させました。 これには、依存関係プロパティによってサポートする KeyTime プロパティと Value プロパティの定義が必要です。 私は、PointCollectionKeyFrame で InterpolateValue というパブリック メソッドを定義することによって、既存のキーフレーム アニメーション クラスを模倣しました。

public PointCollection InterpolateValue(
    PointCollection baseValue,
    double keyFrameProgress)
{
    return InterpolateValueCore(baseValue, keyFrameProgress);
}

また、InterpolateValueCore というプロテクトされた抽象メソッドも定義しました。 2 番目のパラメータは進行状況を表す 0 ～ 1 の範囲の値ですが、これは特定のキーフレームの進行状況を表す値であり、アニメーション全体についての値ではありません。

次に、PointCollectionKeyFrame から、DiscretePointCollectionKeyFrame、LinearPointCollectionKeyFrame、および SplinePointCollectionKeyFrame という 3 つのクラスを派生させました。 ベース Value パラメータと Value プロパティの間の補間を実行するために、これらのクラスで InterpolateValueCore メソッドをオーバーライドします。 SplinePointCollectionKeyFrame では KeySpline というプロパティも定義します。 ここでも、ptsDst1 と ptsDst2 という 2 つのフィールドと結果用の flip というブール型フィールドを定義しました。 図 6 に示す InterpolateValueCore メソッドを含む PointCollectionKeyFrame から NonDiscretePointCollectionKeyFrame 抽象クラスを最初に派生させることにより、繰り返しのコードを避けられることに私は気付きました。

Figure 6 InterpolateValueCore メソッド (Close Figure 6)
protected override PointCollection InterpolateValueCore(
    PointCollection baseValue, double keyFrameProgress)
{
    // Choose which destination collection to use.
    PointCollection ptsDst = flip ? ptsDst1 : ptsDst2;
    flip = !flip;
    ptsDst.Clear();

    int num = Math.Min(baseValue.Count, Value.Count);
    double progress = GetProgress(keyFrameProgress);

    for (int i = 0; i < num; i++)
        ptsDst.Add(new Point(
            (1 - progress) * baseValue[i].X +
                 progress  * Value[i].X,
            (1 - progress) * baseValue[i].Y +
                 progress  * Value[i].Y));
    return ptsDst;
}

このメソッドは、NonDiscretePointCollectionKeyFrame で抽象メソッドとして定義されている GetProgress というメソッドを呼び出します。 LinearPointCollectionKeyFrame と SplinePointCollectionKeyFrame の両方が、このメソッドをオーバーライドします。 LinearPointCollectionKeyFrame は単純にパラメータを返します。SplinePointCollectionKeyFrame は、KeySpline 構造体で定義された、直線の進行状況の値をベジエ曲線に基づく進行状況の値に変換するメソッドによって都合良く提供される値を返します。

次に、<Type>KeyFrameCollection という形式のクラスが必要になります。これは、<Type>KeyFrame オブジェクトのフリーズ可能なコレクションです。 私の例では、このコレクション クラスは PointCollectionKeyFrameCollection という紛らわしい名前で、PointCollectionKeyFrame オブジェクトのコレクションです。 このコレクション クラスを FreezableCollection<T> ジェネリック クラスを継承するクラスとして定義することで、かなりの手間を省くことができます。 ただし、FreezableCollection<T> からの派生を行うときは CreateInstanceCore をオーバーライドする必要があることを忘れないでください。

public class PointCollectionKeyFrameCollection :
    FreezableCollection<PointCollectionKeyFrame>
{
    // CreateInstanceCore override required when
    // deriving from Freezable
    protected override Freezable CreateInstanceCore()
    {
        return new PointCollectionKeyFrameCollection();
    }
}

最後の手順は最も難しい部分で、<Type>KeyFrameCollection 型の KeyFrames プロパティ (および依存関係プロパティ) を定義することにより、<Type>AnimationBase から派生し IKeyFrameAnimation インターフェイスを実装する <Type>AnimationUsingKeyFrames クラスを定義します。 <Type>AnimationUsingKeyFrames クラスは GetCurrentValueCore メソッドをオーバーライドしますが、KeyFrames コレクションのどのオブジェクトが実際に補間を実行する必要があるかを判断する必要があります。

このクラスに取り組んでいる間に、私は IsCumulative プロパティがあまり役に立たないということと、1 つの点に基づくアニメーションを集めたら面白いユーティリティができるということに気付きました。 そこで、AccumulationPoint という新しいプロパティ (および依存関係プロパティ) を定義しました。これは、繰り返し処理が行われるたびにコレクションに追加される 1 つの点です。 これを使用して作成したのが、StickFigureWalking という名前のアニメーションです。 これは 5 つの PointCollection オブジェクト、5 つのキーフレーム、および 1 つの集積点に基づいており、図 7 に示すように棒のような人物が画面上を歩いて横切ります。

図 7 歩く
図 7 歩く

3D ポイントのアニメーション処理

独自のアニメーション クラスを作成するときは、PointCollection アニメーションで行ったように既存のクラスを模倣するか、そのような例を利用せずに自分で作成することができます。 MeshGeometry3D の Positions プロパティのデータ型である Point3DCollection 型のオブジェクトをアニメーション処理するときに私がとったのは、後者の方法でした。 まず通常の Point3DCollectionAnimationBase クラスを定義しましたが、その後、Point3DCollectionAnimationUsingDeformer というクラスを使用する別の方法を実行したのです。 Point3DCollectionAnimationUsingDeformer は IDeformer 型の Deform という 1 つのプロパティだけを定義します。これは、1 つのメソッドだけを定義するインターフェイスです。

Point3DCollection Deform(Point3DCollection points, double progress);

Deform メソッドは、3D ポイントのコレクションと進捗状況を表す 0 ～ 1 の範囲の値を受け取り、3D ポイントのコレクションを返します。 目的は、通常のクラスでは実行できない非線形変換を可能にすることです。 実際、想像力と数学力さえあれば、点のセットを Deform によって別の点のセットに変換する方法はいくらでもあります。

3D オブジェクトをひねる Twister というクラスを例にとってみましょう。 ひねり処理は 3D プログラマが使用する標準の AxisAngleRotation3D 変換に似ており、軸を中心とする回転を行いますが、特定の点の回転角度が、中心点からその点までの距離に応じて変化するという点だけが異なります。

Twister クラスは IDeformer インターフェイスを実装し、独自のプロパティをいくつか定義します。 Axis プロパティと Center プロパティは、ベクトルとして表現される回転軸と、回転の中心を示します。 MinAngle プロパティと MaxAngle プロパティは、進行状況を表す値が 0 のときと 1 のときの回転角度をそれぞれ指定します。 これらの角度は、回転軸に沿った単位あたりの距離です。 たとえば、Axis ベクトルが Center 点を通るとします。 その Center 点を含む、Axis に対して垂直な点の平面はまったく回転しません。 Center 点から 1 単位の距離にある平面は、MinAngle と MaxAngle の間の角度で回転します。 Center 点から 2 単位の距離にある平面は、MinAngle の 2 倍と MaxAngle の 2 倍の間の角度で回転します。 Center 点の反対側では、点は反対方向に回転します。

図 8 に、Twister クラスの Deform メソッドを示します。 このメソッドが対象のコレクションの 2 つの作成済み Point3DCollection クラス間を切り替え、xform という名前の作成済み RotateTransform3D オブジェクトと axisRotate という名前の AxisAngleRotation3D オブジェクトを使用することに注意してください。

Figure 8 Twister の Deform メソッド (Close Figure 8)
public Point3DCollection Deform(Point3DCollection pointsSrc,
    double progress)
{
    // Clear destination collection
    Point3DCollection pointsDst = flip ? pointsDst1 : pointsDst2;
    flip = !flip;
    pointsDst.Clear();

    // Prepare RotateTransform3D object using AxisAngleRotation3D
    xform.CenterX = Center.X;
    xform.CenterY = Center.Y;
    xform.CenterZ = Center.Z;

    // Prepare AxisAngleRotation3D object
    axisRotate.Axis = Axis;
    Vector3D axisNormalized = Axis;
    axisNormalized.Normalize();
    double angleAttenuated = MinAngle + progress * (MaxAngle - MinAngle);

    // Transform each point based on its distance from the center
    foreach (Point3D point in pointsSrc)
    {
        axisRotate.Angle = angleAttenuated *
            Vector3D.DotProduct(axisNormalized, point - Center);

        pointsDst.Add(xform.Transform(point));
    }

    // Return the points
    return pointsDst;
}

本当に面白いのは、ひねり処理を加えるオブジェクトを見つけることです。 TeapotTwister プロジェクトには、多くの 3D デモンストレーションに使用された有名な "Iowa Teapot" の MeshGeometry3D オブジェクトを生成する TeapotMesh.cs ファイルが含まれています。 私は DirectX® 9.0 の Mesh.Teapot 静的メソッドで入手できるデータからこのファイルを作成しました。図 9 に、TeapotMesh クラス、Point3DCollectionAnimationUsingDeformer クラス、および Twister を参照する完全な XAML ファイルを示します。 ひねり処理を加えたティーポットを図 10 に示します。

Figure 9 TeapotTwister.xaml (Close Figure 9)
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
        xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
        xmlns:pae=”clr-namespace:Petzold.AnimationExtensions;
                   assembly=Petzold.AnimationExtensions”
        xmlns:src=”clr-namespace:TeapotTwister”
        x:Class=”TeapotTwister.TeapotTwister”
        Title=”Teapot Twister (3D Deformation)”>
  <Window.Resources>
    <src:TeapotMesh x:Key=”teapot” />
  </Window.Resources>

  <Viewport3D>
    <ModelVisual3D>
      <ModelVisual3D.Content>
        <Model3DGroup>

          <!-- 3D teapot geometry and materials -->
          <GeometryModel3D x:Name=”geomod”
            Geometry=”{Binding Source={StaticResource teapot},
                       Path=Geometry}”>
            <GeometryModel3D.Material>
              <DiffuseMaterial Brush=”Cyan” />
            </GeometryModel3D.Material>

            <GeometryModel3D.BackMaterial>
              <DiffuseMaterial Brush=”Blue” />
            </GeometryModel3D.BackMaterial>
          </GeometryModel3D>

          <!-- Light sources -->
          <AmbientLight Color=”#404040” />
          <DirectionalLight Color=”#C0C0C0” Direction=”2 -3 1” />
        </Model3DGroup>
      </ModelVisual3D.Content>
    </ModelVisual3D>

    <!-- Camera -->
    <Viewport3D.Camera>
      <PerspectiveCamera Position=”0 0 6”
                         LookDirection=”0 0 -1”
                         UpDirection=”0 1 0”
                         FieldOfView=”45” />
    </Viewport3D.Camera>
  </Viewport3D>

  <!-- Animation using Twister class -->
  <Window.Triggers>
    <EventTrigger RoutedEvent=”Window.Loaded”>
      <BeginStoryboard>
        <Storyboard TargetName=”geomod”
                    TargetProperty=”Geometry.Positions”>
          <pae:Point3DCollectionAnimationUsingDeformer
                    Duration=”0:0:5” AutoReverse=”true”
                    RepeatBehavior=”Forever”>
            <pae:Point3DCollectionAnimationUsingDeformer.Deformer>
              <pae:Twister Axis=”1 0 0” />
            </pae:Point3DCollectionAnimationUsingDeformer.Deformer>
          </pae:Point3DCollectionAnimationUsingDeformer>
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Window.Triggers>
</Window>
図 10 ひねり処理を加えたティーポット
図 10 ひねり処理を加えたティーポット

この XAML ファイルでは Twister クラスが Point3DCollectionAnimationUsingDeformer のプロパティ要素で参照されていますが、次のように Twister クラスをリソースとして定義することもできます。

   <pae:Twister x:Key=”twister” Axis=”1 0 0” />

次に、Point3DCollectionAnimationUsingDeformer タグの属性を使用してこのクラスを参照します。

Deformer=”{StaticResource twister}”

MeshGeometry3D の Positions コレクションを動的に変更するのは強力な手法ですが、理論上はあまり適切ではありません。 MeshGeometry3D の Positions コレクションの Point3D オブジェクトに実行する非線形変換は、Normals コレクションの Vector3D オブジェクトにも適用される必要があります。 Point3D と Vector3D の間には明示的なキャストが定義されていますが、これらのオブジェクトのコレクションについては、そのようなキャストが存在しません。 このことは、Vector3DCollection オブジェクトをアニメーション処理するために、クラスの並列構造全体を作成する必要があることを暗示しているように思われます。

そのような機能強化がなくても、ここで紹介した新しいアニメーション クラスは、今まで見たことがないような種類の画像 (テキストのモーフィングやティーポットのひねり処理など) を自分のコンピュータ上で見たいという、プログラミングでの私の基本的な動機を満たしてくれました。

ご意見やご質問は、

こちら mmnet30@microsoft.com.

まで英語でお送りください。
Charles Petzold は、MSDN Magazine の寄稿者で、『アプリケーション = コード + マークアップ : Microsoft Windows Presentation Foundation』(Microsoft Press 発行、2006 年) の著者です。


{ End Bracket }: Photosynth を使って写真を編集する

{ End Bracket }
Photosynth を使って写真を編集する
Richard Szeliski

デジタル写真ブーム により、ここ数年、Web 上で共有される写真の数が劇的に増加しました。 Microsoft Research の Interactive Visual Media Group は、このデジタル写真現象に応えるため、写真の編集および表示のための革新的な製品を開発しました。 Microsoft Research は、完全に自動化された画像編集ソフトウェアをいち早く製品化し、360 度のビデオ ベースのウォークスルーの開発や、リアルタイムにナビゲーション可能な 3D ビデオの作成を行ってきました (research.microsoft.com/IVM を参照)。

この分野の最大の課題の 1 つは、写真集から写実的な 3D モデルを作成するための完全に自動化されたシステムを開発することでした。 Virtual Earth™ 3D のテクスチャ マッピングした 3D 作成モデルなどに見られるように、この分野の最近の進歩にはめざましいものがあります (maps.live.com を参照) が、結果として確立されたモデルは、元の写真集の持つ豊かな表現や多様性を完全には再現していませんでした。

このような限界を克服するため、ワシントン大学の Noah Snavely および Steve Seitz 両氏の協力を得て、3D 写真ブラウザ システム Photo Tourism を開発しました (phototour.cs.washington.edu を参照)。 このシステムでは、コンピュータビジョン技術を利用して、撮影されたシーンの 3D モデルの一部や、写真集の各画像の 3D の位置や方向を再構成します。 この処理を行うために、まず、各画像の特徴点を抽出し、それらを写真集全体と照合して、制約付き非線形最適化問題を解くことによって、3D カメラおよびシーン ジオメトリを徐々に再構成します。 ユーザーは、対象領域を選択するか、左への移動、右への移動などを行う直感的なコマンドを使って、画像間を移動できます。

画像間の移動をスムーズにし、3D 動作の感覚を出すため、このシステムでは 3D カメラの動きをシミュレーションし、画像を平面の "インポスタ" (プロキシ) に射影します。これは、コンピュータ ゲームやモデル特有のジオメトリによく使用される手法です。 点群、線分、低解像度の "水彩ウォッシュ" で構成される、シーンの 3D モデルのスケッチも使われます。 できあがった 3D 画像ブラウザは、従来のスライドショーの写実性および美しさと、ビデオ ゲームの 3D インタラクティブ機能を兼ね備えています。

研究室の環境から一般的な展開に移行するためには、研究用プロトタイプを作成後、画像のリアルタイムのマルチ解像度ストリーミングをインターネット上でサポートするためにシステムをスケールアップする必要がありました。 折りよく、Microsoft® Live Labs が新興企業 Seadragon を買収したところでした。同社は、さまざまな解像度で画像をストリームし、アニメーション化して 3D 配置で表示する技術を既に持っていました。 Seadragon のエンジンは、元の画像を、一連のマルチ解像度の重ね合わせた傾斜画像の断片に変換します。これらの断片を必要に応じてストリームし、クライアント側で結合して、視覚的にシームレスなズーム機能と、段階的な画像のリファイン機能を提供し、数千の画像の表示を同時にサポートします。

少人数の設計者、プログラム マネージャー、研究者、開発者の複数の機能にまたがるグループが協力して Photosynth™ の設計、構築、展開に携わりました。Photosynth は、3D 画像の配置およびナビゲーション技術と、基本となる Seadragon エンジンを統合したシステムです。 初期のユーザー インターフェイスでは、複数のサムネイル ペインとアイコンでナビゲーションを制御していましたが、このインターフェイスの設計も見直しを重ねました。 最終的な設計では、発光する四角形でユーザーのマウス ポインタの近くに追加画像が存在することを示します。 四角形をクリックすると、新しい画像にシームレスに切り替わります。 画像を順番に移動したり、ナビゲーション履歴に従って移動するフィルムストリップ コントロールのほか、シーンの 3D の概要や、2D で画像を拡大/縮小してサムネール表示する機能も追加されました (labs.live.com/Photosynth を参照)。

Photosynth の今後のバージョンでは、3D 再構成アルゴリズムを改良して、より大きいデータ セットを処理できるようにスケールアップしたり、コンシューマ レベルのオーサリング機能を搭載したり、ビューアを拡張してプラットフォームを追加する予定です。 現在、Photosynth を通常のスライドショーや写真共有サイトと統合する方法、3D オブジェクトをスムーズに巡回する方法、および Virtual Earth などの 3D 都市モデルと統合する方法を検討中です。 また、教育 (インタラクティブな博物館ツアー、時系列のインターバル撮影など)、不動産販売、大規模画像コレクションのインデックス作成など、旅行以外の Photosynth の追加アプリケーションについても検討しています。 私たちは、Photosynth が従来の写真の美しく豊かな表現力と、ゲームや 3D の世界が持つインタラクティブな閲覧機能を兼ね備えたビジュアル メディアとして定着することを願っています。

Richard Szeliski は、Microsoft Research Interactive Visual Media Group のリーダーとして、個人的な記憶をキャプチャ、閲覧、および共有する新しい方法の開拓に携わっています。