December 2007 December 2007
マップ LINQ: Visual Basic 9.0 および WPF で動的マップを作成する
Scott Wisniewski は、Windows Presentation Foundation データ バインドを LINQ および Visual Basic の新しい XML 機能と共に使用して、人口統計データをマッピングする対話型アプリケーションを構築します。 Scott Wisniewski
VSTO: WPF、WCF、および LINQ を使用して Office ベースのソリューションを構築する
Visual Studio Tools for Office を使用すると、Windows Communication Foundation、Windows Presentation Foundation、および LINQ の機能を活用する Office ベースのサービスを容易に構築できます。 Andrew Whitechapel
コマンドレット: カスタム コマンドで Windows PowerShell を拡張する
Jim Truher は、Windows PowerShell で使用する独自のカスタム コマンドレットの作成と登録の概要を示します。 Jim Truher
相互運用: BizTalk アダプタおよび .NET を使用してメインフレーム アプリケーションに接続する
メインフレーム上で動作する従来の COBOL アプリケーションに多大な投資を行っている場合は、Host Integration Server 用の BizTalk アダプタが必要です。 Bashar “Bash” Badawi
セキュリティ: ADFS を使用して組織間でユーザーを認証する
Jack Couch は、ADFS をセットアップする方法と、ADFS をどのような場合に使用すべきかについて紹介し、外部の組織に接続してシングル サインオンを提供する方法を説明しています。 Jack Couch
Columns
編集者のメモ: 深呼吸する: 今月は、Howard Dierking が 2007 年について思い起こし、2008 年のすばらしい可能性を探ります。Howard Dierking | ツールボックス: ライブ チャット、コード変換、マルチ モニタ、その他の機能: 今月は、作業の実行に必要ないくつかのツールについて説明します。Scott Mitchell | CLR 徹底解剖: 信頼できる .NET コードを記述する: コードのエラー発生の仕組み、およびより信頼性が高く、弾力性のあるマネージ コードの作成方法について説明します。Alessandro Catorcini and Brian Grunkemeyer
データ ポイント: WPF でのデータ バインド: WPF は、.NET Framework 3.0 の最も重要な新しいテクノロジの 1 つです。今月は、John Papa がそのデータ バインド機能について紹介します。John Papa | Cutting Edge: Microsoft AJAX Library の秘密: Dino Esposito が Microsoft AJAX Library および ASP.NET AJAX 1.0 用 JavaScript ライブラリを紹介します。Dino Esposito | テストの実行: Windows PowerShell での UI テスト自動化: 今回の「テストの実行」コラムでは、Windows PowerShell を使用して超軽量の UI テスト自動化を実行する方法を説明します。Dr. James McCaffrey
サービス ステーション: WCF をカスタム動作で拡張する: WCF の拡張ポイントを使用して、メッセージの変換、書式設定、および送信のプロセスをカスタマイズすることにより、サービスをさらにカスタマイズできます。Aaron Skonnard | Netting C++: EEK! - ネズミの設計: 今月は EEK! の実際の mouse クラスの設計について紹介します。Stanley B. Lippman | .NET の問題: Windows Vista での IFileOperation: 著者が、マネージ コードから Windows Vista の新しい IFileOperations インターフェイスを使用するマネージ ラッパーを作成します。Stephen Toub
Windows と C++: Windows Template Library 8.0: Kenny Kerr が、タスク ダイアログ、Aero ウィザード、ファイルを開くダイアログなどの、Windows Vista の新しいコントロールの拡張をサポートする Windows Template Library (WTL) について検討します。Kenny Kerr | { End Bracket }: SharePoint の習得の必要性: ユーザー インターフェイス コンポーネント用フレームワークや組み込み検索エンジンなどの Microsoft SharePoint テクノロジを優先的に習得する必要がある 5 つの理由について説明します。Robert Bogue


マップ LINQ
Visual Basic 9.0 および WPF で動的マップを作成する
Scott Wisniewski
コードのダウンロード : : MapsWithVB9WPF2007_12.exe (30813 KB)
Browse the Code Online

この記事は、Visual Studio のプレリリース版を基にしています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • WPF 依存関係プロパティ
  • LINQ を使用して XML をクエリする
  • マップを描画する
  • マップ データを視覚化する
この記事は次のテクノロジを使用しています:
WPF、Visual Basic 9.0、LINQ
コンテンツ
WPF データ バインド
アプリケーション オブジェクト モデル
Map クラス
マップ データをインポートする
マップ データ形式
データをインポートする
マップを描画する
人口データを視覚化する
最後に
筆者は常々マップの描画に少しばかり魅力を感じていました。Visual Studio® 2008 と Visual Basic® 9.0 もすばらしいと思います。そのため、Visual Studio で作業する機会を得て、それに関する記事を書くことになったときに、Visual Basic を使用してマップを描画する方法に関するチュートリアルを示すのが最善であると判断しました。そうすれば、すばらしい Visual Basic 機能をいくつか紹介するだけでなく、読者自身のプログラムに類似機能を追加するための基礎として使用できる実用的なサンプルを提供できます。
筆者の作業の結果を図 1 に示します。この図は、ヒート マップを使用して米国の人口を視覚化するアプリケーションのスクリーンショットを示しています。アプリケーションは、Windows® Presentation Foundation (WPF) データ バインド インフラストラクチャを多用して構築されました。このインフラストラクチャにより、アプリケーションのドメイン固有ロジックをユーザー インターフェイスの視覚化から効果的に分離できました。
図 1 人口マッピング アプリケーション
図 1 人口マッピング アプリケーション (画像を拡大するには、ここをクリックします)
まず、次のセクションでいくつかの初歩的な WPF データ バインド トピックの概要を示し、記事の残りの部分はアプリケーションの作成方法の検証に費やします。最初に、アプリケーションの Visual Basic オブジェクト モデルについて説明してから、Visual Basic 9.0 の革新的な新しい XML 機能と LINQ を使用してデータ処理ロジックを実装する方法を説明します。最後に、WPF を使用してアプリケーションのデータを効果的に視覚化する方法を示すことで締めくくります。

WPF データ バインド
WPF データ バインド インフラストラクチャは、依存関係オブジェクトと依存関係プロパティの概念に基づいています。依存関係オブジェクトは、変更通知と、プロパティ値を動的にフェッチおよび取得する機能をサポートします。依存関係プロパティは、依存関係オブジェクトに関連付けられているプロパティです。これらの組み合わせによって、データ バインド、アニメーション、スタイルなど、WPF の多くの領域の背後にあるコア インフラストラクチャが形成されます。
依存関係オブジェクトは、DependencyObject クラスから派生する型のインスタンスによって表現されます。同様に、依存関係プロパティは、WPF プロパティ システムに登録された DependencyProperty のインスタンスによって表現されます。
アプリケーションのデータ モデルで使用される場合、依存関係プロパティはコードを大幅に簡略化します。依存関係プロパティにより、プログラマがコントロールを明示的に操作する必要なく、多くのタスクを直接実装できます。WPF の宣言型データ バインドとスタイル設定メカニズムを通じて、カスタム ロジックを必要とせずに、変更を UI に動的に伝達できます。
これにより、基礎となる WPF ユーザー インターフェイスのコードでの読み取り、書き込み、および保守がはるかに簡単になります。個々の層の間の分離を促すことにより、より明確なアプリケーション アーキテクチャも促進されます。最後に最も重要なことは、Expression BlendTM などのツールを利用して、視覚的な形式でユーザー インターフェイスをデザインでき、アプリケーション ロジックではなくスタイルの要素に集中できることです。
依存関係プロパティの単純な使用例については、図 2 を参照してください。この例では、MapRegion クラスの定義の部分を示しています。MapRegion は、DependencyObject からの継承です。主な目的は、マップ内の地理的な領域を表現することです。RegionName および IsSelected という 2 つの依存関係プロパティを宣言します。依存関係プロパティは、DependencyProperty クラスの共有 Register メソッドを呼び出し、プロパティの名前と型、およびそのプロパティを含むクラスの型 (この場合は MapRegion) を提供することで宣言されます。返される依存関係プロパティは、クラスで宣言されているパブリック、共有、読み取り専用フィールドに割り当てられます。この処理は、主に WPF 規則に準拠しています。この規則は、依存関係プロパティのメタデータを、それらを定義するクラスの共有フィールドとして公開します。
Figure 2 MapRegion クラスのスニペット
Imports System.Collections.ObjectModel

Public Class MapRegion
    Inherits DependencyObject

    Public Shared ReadOnly RegionNameProperty As DependencyProperty _
        = DependencyProperty.Register("RegionName", GetType(String), _
        GetType(MapRegion))
    Public Shared ReadOnly IsSelectedProperty As DependencyProperty _
        = DependencyProperty.Register("IsSelected", GetType(Boolean), _
        GetType(MapRegion))

    '...

    Public Property RegionName() As String
        Get
            Return CStr(GetValue(RegionNameProperty))
        End Get
        Set(ByVal value As String)
            SetValue(RegionNameProperty, value)
        End Set
    End Property

    Public Property IsSelected() As Boolean
        Get
            Return CBool(GetValue(IsSelectedProperty))
        End Get
        Set(ByVal value As Boolean)
            SetValue(IsSelectedProperty, value)
        End Set
    End Property
依存関係プロパティは、DependencyObject クラスで定義されている GetValue メソッドと SetValue メソッドを呼び出すラッパー プロパティを介してコードに公開され、適切な DependencyProperty オブジェクトをパラメータとして提供します。これにより、MapRegion クラスのコンシューマは、機能豊富な WPF の依存関係プロパティ システムを利用すると同時に、他の通常のインスタンス プロパティと同じように依存関係プロパティにアクセスできます。
実際のプロパティ データは MapRegion クラスにフィールドとして直接格納されないことにも言及しておく必要があります。代わりに、WPF は依存関係オブジェクトに添付された各依存関係プロパティのストレージを管理し、SetValue および GetValue の呼び出しを通じていつでも使用できるようにします。
また、依存関係プロパティを依存関係オブジェクトに登録できるのは一度だけであることにも注意する必要があります。2 つの依存関係プロパティを同じ型に同じ名前で登録しようとすると、例外が発生します。
ここに示す例では、DependencyProperty.Register を呼び出す明示的な初期化式で共有読み取り専用フィールドを宣言することにより、各依存関係プロパティが 1 回だけ登録されるようにしています。そのため、登録が MapRegion クラスの静的コンストラクタの一部として実行されます。これにより、共通言語ランタイム (CLR) は、(MapRegion クラスを参照しているコードが実行されるよりも前に) 必ず 1 回だけ実行されます。
図 3 に示す XAML コードは、依存関係プロパティを使用して、ドメイン モデルからアプリケーションのユーザー インターフェイスに変更を自動的に伝達する方法の例を示しています。この例で定義しているスタイルが Polygon コントロールに適用されると、関連付けられている MapRegion の IsSelected プロパティが true に設定されている場合はポリゴンが太いオレンジ色の枠線でレンダリングされ、それ以外の場合は細い黒色の枠線でレンダリングされます。これにより、アプリケーションを駆動するコードは、領域を単純に選択済みとしてマークし、それに応じて UI に応答させることができます。
Figure 3 データ トリガを利用するポリゴン スタイル
<Style x:Key="RegionPolygonStyle" TargetType="Polygon">
    <Style.Triggers>
        <DataTrigger Binding="{Binding Path=Region.IsSelected}"
            Value="True">
            <Setter Property="Stroke" Value="Orange" />
            <Setter Property="StrokeThickness" Value="20"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding Path=Region.IsSelected}"
            Value="False">
            <Setter Property="Stroke" Value="Black"/>
            <Setter Property="StrokeThickness" Value="5" />
        </DataTrigger>
    </Style.Triggers>
</Style>

アプリケーション オブジェクト モデル
WPF データ バインドはすばらしいものなので、筆者はできる限りアプリケーションで有効にします。目標は、アプリケーションを駆動するための明確で簡潔なドメイン固有のコードを作成できるようにすることと、Expression Blend を使用してインターフェイスをデザインできるようにすることでした。
これを実現するために、WPF 依存関係プロパティ システムを多用するマップ情報を表現するためのオブジェクト モデルを作成しました。このオブジェクト モデルは、Map、MapRegion、MapPolygon という 3 つの主要クラスから構成されます。これらのクラスはすべて DependencyObject から派生します。Map クラスは、オブジェクト モデルのルートであり、領域の一覧を含むマップを表現するために使用されます。MapRegion クラスは、マップ上の領域または地理エンティティを表します。このクラスにはポリゴンの一覧が含まれ、各ポリゴンが、領域を定義する連続した地理的領域の 1 つの境界を示します。MapPolygon クラスは、1 つの領域内の 1 つのポリゴンを表し、その頂点を示す Point オブジェクトのコレクションを含みます。
Map クラスには、BoundingBox、ScaleX、ScaleY、TranslateX、TranslateY という 5 つの依存関係プロパティがあります。BoundingBox プロパティは、Rect クラスのインスタンスであり、マップ上のすべての領域を含む最小の矩形領域を格納します。ScaleX、ScaleY、TranslateX、TranslateY の各プロパティは、マップ上でズームとパンをサポートするために定義されています。マップ上の点の座標は赤道と子午線の交差からのオフセットをキロメートル単位で表すため、これらのプロパティが重要です。一方、WPF では、別の座標系を使用します。その座標系は、ウィンドウの左上隅からのオフセット (論理ピクセル単位) で定義されます (1 論理ピクセルは 96 分の 1 インチです)。
そのため、ScaleX、ScaleY、TranslateX、TranslateY の各プロパティは、次の処理を行うために使用されます。
  1. マップを拡大縮小して、ウィンドウに収まるようにします。
  2. マップを上下逆になるようにひっくり返します (地球のビューでは北に向かって移動すると Y 座標が増加しますが、WPF ビューではウィンドウの下に向かって移動すると Y 座標が増加します)。
  3. マップの左上隅がマップを表示するコントロールの左上隅と一致するように、マップを移動します。
これらのプロパティを使用して、UI にズーム機能とパン機能を実装することもできます。たとえば、ScaleX 値と ScaleY 値に 1.5 を乗算すると、マップが 150% ズームし、TranslateX プロパティに 50 を加算すると、マップのビューが 50 キロメートル西に移動します。これらの値を変更しても、マップ内の座標は変化しないことに注意してください。代わりに値は、マップを表示するために必要な処理をユーザー インターフェイスに通知するために使用されます。
Map クラスは、Regions プロパティを通じて領域コレクションを公開します。ただし、クラスの他のプロパティとは異なり、Regions は依存関係プロパティではありません。ObservableCollection(of MapRegion) として型指定される読み取り専用プロパティです。コレクション全体を一度に割り当てる機能は必要ないため、依存関係プロパティとして宣言しませんでした。ただし、コレクション自体は変更可能であり、ObservableCollection クラスの Add メソッドと Remove メソッドを使用して、領域をマップに追加したり、マップから削除したりできます。また、このコレクションは ObservableCollection として公開されるため、依然として WPF データ バインド システムに関与できます。たとえば、ListBox が ObservableCollection にデータ バインドされている場合は、新しいアイテムをそのコレクションに追加するだけで、新しいアイテムが ListBox に表示されます。同様に、コレクションからアイテムを削除すると、そのアイテムが ListBox から削除されます。

Map クラス
MapRegion クラスは、BoundingBox および RegionName という 2 つの依存関係プロパティを定義します。BoundingBox プロパティは領域を含む矩形領域を定義し、RegionName は領域の名前を格納します。このクラスは、Polygons および FipsCodes という 2 つのコレクション プロパティも定義します。Polygons コレクションは、領域を定義するポリゴンを格納し、FipsCodes は、領域に関連付けられている数値 FIPS (Federal Information Processing Standard) コードのセットを格納します。アプリケーションで使用するポリゴン データは United States Census Bureau からダウンロードしたデータであり、このデータは FIPS コードを使用して米国内の地理エンティティを一意に識別するため、クラスでは FIPS コードを使用しています。Federal Information Processing Standard の詳細情報は、itl.nist.gov/fipspubs/by-num.htm からダウンロードできます。また、U.S. Census Bureau のパブリック ドメイン TIGER Geographic Database に関する技術情報には、census.gov/geo/www/cob でアクセスできます。
MapPolygon クラスは、2 つのプロパティを定義します。最初のプロパティは、Region という名前の読み取り専用依存関係プロパティであり、ポリゴンを含む領域への参照を格納します。2 つ目のプロパティは、Points という名前のコレクション プロパティであり、ポリゴンの頂点を定義する Point 構造体のセットを格納します。クラスの完全なソースを図 4 に示します。
Figure 4 MapPolygon.vb
Public Class MapPolygon
    Inherits DependencyObject

    Private Shared ReadOnly RegionPropertyKey As DependencyPropertyKey _
        = DependencyProperty.RegisterReadOnly("Region",  _
        GetType(MapRegion), GetType(MapPolygon), New PropertyMetadata())
    Public Shared ReadOnly RegionProperty As DependencyProperty

    Private m_Points As PointCollection

    Shared Sub New()
        RegionProperty = RegionPropertyKey.DependencyProperty
    End Sub

    Sub New()
        SetRegion(Nothing)
        m_Points = New PointCollection
    End Sub

    Friend Sub SetRegion(ByVal r As MapRegion)
        SetValue(RegionPropertyKey, r)
    End Sub

    Public ReadOnly Property Region() As MapRegion
        Get
            Return CType(GetValue(RegionProperty), MapRegion)
        End Get
    End Property

    Public ReadOnly Property Points() As PointCollection
        Get
            Return m_Points
        End Get
    End Property
End Class
Region プロパティは、読み取り専用依存関係プロパティとして公開されます。これにより、WPF プロパティ システムはプロパティ値に対する変更を検出できますが、WPF でその値を変更することはできません。読み取り専用依存関係プロパティは、DependencyProperty クラスの RegisterReadOnly メソッドを呼び出すことで宣言されます。Register メソッドとは異なり、RegisterReadOnly は、DependencyProperty ではなく DependencyPropertyKey インスタンスを返します。
依存関係プロパティ キーは、読み取り専用依存関係プロパティを変更できるようにするオブジェクトです。これらのオブジェクトは、プロパティ値を内部的に更新し、それらの変更が他の型によって検出されるようにする一方で値の外部的な変更は許可しないようにする必要があるクラスの実装に役立ちます。その結果、一般に DependencyPropertyKey は、それらを定義するクラスからは公開されません。そのため、RegionPropertyKey フィールドはプライベートとしてマークされます。2 つ目の共有フィールドである RegionProperty は、その読み取り専用エイリアスをパブリックに公開するために使用されます。
Region プロパティは、外部的に編集できないようにデザインされているため、読み取り専用です。代わりに、領域コレクションでポリゴンが追加または削除されると、MapRegion クラスが値を自動的に設定およびクリアします。ポリゴン コレクションの CollectionChangedEvent を処理し、影響を受けるポリゴンに適切な Region インスタンスを伝達することにより、この処理を行います。
イベント ハンドラの実装を図 5 に示します。このイベント ハンドラは、ハンドラの NotifyCollectionChangedEventArgs パラメータに格納されている NewItems コレクションと OldItems コレクションの SetRegion 拡張メソッドを呼び出すことで機能します。拡張メソッドは、MapPolygon クラスの SetRegion メソッドをターゲット コレクションの各アイテムに対して呼び出します。インスタンス メソッドは、呼び出されないように friend としてマークされます。拡張メソッドの実装は、次のようになります。
Figure 5 MapRegion.PolygonCollectionChanged メソッド
Private Sub PolygonCollectionChanged(ByVal sender As Object, ByVal e As _
    System.Collections.Specialized.NotifyCollectionChangedEventArgs) _
    Handles m_polygons.CollectionChanged
    Select Case e.Action
        Case Specialized.NotifyCollectionChangedAction.Add
            e.NewItems.Cast(Of MapPolygon).SetRegion(Me)
        Case Specialized.NotifyCollectionChangedAction.Remove
            e.OldItems.Cast(Of MapPolygon).SetRegion(Nothing)
        Case Specialized.NotifyCollectionChangedAction.Replace
            e.NewItems.Cast(Of MapPolygon).SetRegion(Me)
            e.OldItems.Cast(Of MapPolygon).SetRegion(Nothing)
    End Select
End Sub

<Extension()> _
Sub SetRegion(ByVal x As IEnumerable(Of MapPolygon), _
    ByVal r As MapRegion)
    For Each item In x
        item.SetRegion(r)
    Next
End Sub
Map クラスおよび MapRegion クラスのコレクション プロパティとは異なり、MapPolygon クラスの Points コレクションは、ObservableCollection(of T) ではありません。組み込みの WPF PointsCollection クラスを使用します。このクラスは、WPF Polygon コントロールがそのコントロールに含まれる点のセットを格納するために使用するのと同じ型です。このクラスをデータ モデルで使用することにより、Polygon コントロールを MapPolygon インスタンスに直接データ バインドできるようになります。

マップ データをインポートする
アプリケーションは、ディスクに格納されている XML ファイルからマップ データを読み込みます。未加工の ASCII 地図製作境界ファイルを U.S. Census Bureau からダウンロードすることで、これらのファイルを作成しました。この地図製作境界ファイルは、米国内のすべての州と郡 (および同等の領域) の境界を示します。州境界データは米国のマップを描画するために使用され、郡境界データはマップ上の人口データを視覚化するために使用されます。次に、ダウンロードしたデータを XML ファイルに変換して、簡単に処理できるようにしました。ファイルを XML に変換するコードは、この記事でダウンロードできます。未加工データ ファイルは、census.gov/geo/www/cob/bdy_files.html でダウンロードできます。
Visual Studio Create Schema 機能を使用して、XML ファイルのコンテンツを記述する XML スキーマを自動的に生成しました。これにより、プロジェクトに含まれるスキーマ ファイルに基づいて XML メンバ アクセス式を表示する新しい Visual Basic XML IntelliSense® 機能を使用できるようになりました (図 6 を参照)。
図 6 Visual Basic XML IntelliSense
図 6 Visual Basic XML IntelliSense (画像を拡大するには、ここをクリックします)

マップ データ形式
XML マップ ファイルを記述するスキーマは、一連の Region タグを含むルート File タグから構成される単純な形式を定義します。各 Region タグには、一連の FipsCode タグと Polygon タグが含まれ、各 Polygon タグには一連の Vertex タグと Island タグが含まれます。
Region タグは、単一の領域を記述するために使用されます。このタグは、Type および Name という 2 つの必須属性を定義します。Type 属性は、領域の型の説明を提供します。筆者のアプリケーションの目的に沿って、常に値 State または County を使用します。ただし一般には、Type 属性には任意の値を使用できます。Name 属性は、領域の名前を記述します。
領域で定義されている各 FipsCode タグは、FIPS コード エントリの 1 つを記述します。Region 内の最後の FipsCode タグは、領域の数値 ID を定義します。他の FipsCode タグは、領域の親の数値 ID を再帰的に記述します。
筆者のアプリケーションの目的に沿って、すべての領域には最大 2 つの FipsCode 値が関連付けられます。具体的には、州 (または州と同等の領域) を記述する領域は、その州に関連付けられている FIPS コードを記述する 1 つのエントリを必ず持ちます。郡 (または郡と同等の領域) を記述する領域には、必ず 2 つのエントリがあります。最初のエントリは、その郡を含む州の FIPS コードを示し、2 つ目のエントリは、郡自体の FIPS コードを示します。特定の領域の数値 ID は、直接の親のスコープ内で常に一意になることが保証されます。
領域の各 Polygon タグは、その領域に含まれる単一のポリゴンを記述します。ポリゴンは、一連の Vertex タグから構成され、各タグが単一の頂点を記述します。Vertex タグは、Ordinal、Longitude、Latitude、X、Y という 5 つの属性を定義します。これらの属性はすべて必須です。Ordinal 属性は、ポリゴンの頂点の順序を定義します。主な目的は、頂点が順序を保持しないクエリで処理される場合に、頂点を正しい順序に並べられるようにすることです。現在、筆者のアプリケーションでは使用していません。Longitude 属性と Latitude 属性は、頂点の経度と緯度の座標を記述します。同様に、X 属性と Y 属性は、矩形空間に投影されるときの頂点の座標を定義し、アプリケーションではこれらの属性を使用してマップを描画します。
X 属性と Y 属性は、図 7 に示す単純な投影を使用して計算されます。この投影は正確ではなく、一部のシナリオには適さない場合があります。その場合は、ファイルに格納されている X 値と Y 値を使用する代わりに Longitude と Latitude を使用して新しい値を直接計算できます。筆者のアプリケーションでは、地理的に高い正確度は不要なため、これで十分でした。
図 7 非常に単純なマップ投影
図 7 非常に単純なマップ投影
ポリゴン内の Island タグは、そのポリゴンを含む領域の一部ではない、ポリゴンで囲まれた穴を定義します。これには、穴の境界を記述する Vertex タグのコレクションが含まれます。これらは、主に完全を期すためにデータ ファイルに含めました。筆者のアプリケーションでは使用していません。したがって、データが視覚化されたときに若干間違った値を示す非常に小さなマップ領域がいくつか存在する可能性があります。筆者のアプリケーションの目的からは、この誤差は問題ではないため、単純に無視しました。
この点についての正確さが重要なアプリケーションがある場合、またはポリゴン アイランドが多いマッピング領域の場合は、マップがレンダリングされる順序を制御することで、この問題を簡単に解決できます。最初に、アイランドを含むポリゴンを描画してから、その上にアイランドを描画します。これで、ポリゴン アイランドを正確にレンダリングできます。多くの場合、この処理には、重なる領域を判断し、適切な順序を構築するために、追加処理が必要になることもあります。

データをインポートする
LINQ を使用すると、アプリケーションへのマップ データのインポートは比較的簡単です。この操作を実行するコードを図 8 に示します。このコードは、file および list という 2 つのパラメータを受け取るプロシージャ LoadFile を定義します。file パラメータは、ディスクから読み込む XML ファイルの完全パスを含む文字列です。list パラメータは、MapRegion インスタンスのコレクションへの参照です。プロシージャは、XML ファイルを開いて処理し、定義するすべての領域を、提供されているコレクションに挿入します。
Figure 8 XML ファイルからの MapRegion インスタンスの読み込み
Private Sub LoadFile(ByVal file As String, ByVal list As ICollection(Of _
    MapRegion))
    Dim doc = XDocument.Load(file)
    Dim q = _
        From v In doc.<File>.<Region>.<Polygon>.<Vertex> _
        Let _
            Polygon = v.Parent, _
            Region = v.Parent.Parent _
        Group By _
            Polygon, _
            Region _
        Into _
            Group, _
            MinX = MinDBL(v.@X), _
            MinY = MinDBL(v.@Y), _
            MaxX = MaxDBL(v.@X), _
            MaxY = MaxDBL(v.@Y) _
        Select _
            TheMapPolygon = (From tmp In Group Select _
            tmp.v).ToMapPolygon(), _
            Region, _
            BoundingBox = New Rect(New Point(MinX, _
            MinY), New Point(MaxX,  MaxY)) _
        Group By _
            Region _
        Into _
            Group, _
            BoundingBox = Enclose(BoundingBox) _
        Select _
            Region = New MapRegion(Region.@Name, (From item In Group _
            Select item.TheMapPolygon), (From f In _
            Region...<FipsCode> Select CInt(f.Value)), BoundingBox)

    list.AddRange(q)
End Sub
LoadFile の本体は非常に単純です。プロシージャは、最初に提供されているファイルを XDocument インスタンスに読み込み、ドキュメントを走査して MapRegion インスタンスのコレクションに変換するクエリを定義し、最後にクエリを実行してその結果を提供されている出力コレクションに挿入することで機能します。
クエリは、複数の異なる部分から構成されます。最初の部分は From 句です。この句では、XML Member アクセス式 doc.<File>.<Region>.<Polygon>.<Vertex> を使用して、ドキュメント内のすべてのポリゴンの Vertex タグをクエリの基礎として取得します。ここでは、Island タグの内部で定義されているものを含め、ドキュメント内のすべての Vertex タグを取得する doc...<Vertex> は使用しないことに注意してください。代わりに、Polygon タグの内部に直接定義されている Vertex タグのみ含めます。クエリの 2 つ目の部分は Let 句です。この句は、Polygon および Region という 2 つの新しい変数を導入します。これらの変数は、各頂点を含む Polygon タグと Region タグに割り当てられます。
クエリの 3 つ目の部分は最初の Group By 句です。この句は、各頂点が含まれるポリゴンで頂点の一覧をグループ化してから、ポリゴンの境界ボックスを計算します。ポリゴンを含む領域もグループ化キーに含まれているため、後でクエリで使用できます。ポリゴンの境界ボックスは、頂点の X 属性と Y 属性の最小値と最大値を計算することで計算されます。この計算は、カスタム集計関数 MinDBL および MaxDBL を呼び出すことで行われます。これらの集計関数は、次の 2 つの拡張メソッドで定義されています。
<Extension()> _
Function MinDBL(Of T)(ByVal x As IEnumerable(Of T), _
    ByVal y As Func(Of T, String)) As Double
    Return x.Min(Function(z) CDbl(y(z)))
End Function

<Extension()> _
Function MaxDBL(Of T)(ByVal x As IEnumerable(Of T), _
    ByVal y As Func(Of T, String)) As Double
    Return x.Max(Function(z) CDbl(y(z)))
End Function
これらの拡張メソッドは、提供された引数をまず Double 型に変換した後で、それらの最小値または最大値を計算する集計関数を単純に定義します。
Group By 句の後に、未加工の group by の結果をより使いやすい形式に変換する Select 句があります。具体的には、Group By からグループ化の結果を取得し、そこから頂点のみ投影し、ToMapPolygon 拡張メソッドを使用して結果のコレクションを MapPolygon クラスのインスタンスに変換するサブクエリを定義します。
<Extension()> _
Function ToMapPolygon(ByVal items As IEnumerable(Of XElement)) As _
    MapPolygon
    Dim ret As New MapPolygon
    ret.Points.AddRange(From item In items Select item.ToPoint())
    Return ret
End Function

<Extension()> _
Function ToPoint(ByVal x As XElement) As Point
    Return New Point(x.@X, x.@Y)
End Function
また、未加工の MinX、MinY、MaxX、MaxY の各変数を Rect クラスのインスタンスに変換します。
クエリの次の部分は、2 つ目の Group By 句です。この句は、先行する select by Region の結果を集計し、領域のすべてのポリゴンを含む境界ボックスを計算します。境界ボックスは、Enclose カスタム集計関数を使用して計算されます。その定義を図 9 に示します。クエリの最後の部分は、最後の Select 句です。この句は、2 つ目の Group By から返された各結果の MapRegion インスタンスを作成します。この処理は、2 つのサブクエリを使用して行います。一方のサブクエリは、Group の各メンバに格納されているポリゴン インスタンスを投影し、もう一方のサブクエリは、Region に格納されている FIPS コードのセットを抽出します。
Figure 9 Enlose 集計関数
<Extension()> _
Function Enclose(Of T)(ByVal collection As IEnumerable(Of T), ByVal _
    selector As Func(Of T, Rect)) As Rect
    Dim ret As Rect
    Dim first As Boolean = True

    For Each item In collection

        If first Then
            ret = selector(item)
            first = False
        Else
            ret.Enclose(selector(item))
        End If
    Next

    Return ret

End Function
<Extension()> _
Sub Enclose(ByRef theBox As Rect, ByVal otherBox As Rect)

    Dim X = Math.Min(theBox.X, otherBox.X)
    Dim Y = Math.Min(theBox.Y, otherBox.Y)
    Dim Width = Math.Max(theBox.BottomRight.X, otherBox.BottomRight.X) _
        - X + 1
    Dim Height = Math.Max(theBox.BottomRight.Y, otherBox.BottomRight.Y) _
        - Y + 1
    theBox.X = X
    theBox.Y = Y
    theBox.Width = Width
    theBox.Height = Height

End Sub

マップを描画する
マップがメモリに読み込まれた後、そのマップをウィンドウに表示することは簡単です。図 10 の XAML コードは、この処理を簡単に行う方法を示しています。ここで、Render Transform に対する 2 つの変換を含み、MapViewer という名前の 1 つの ItemsControl をコンテンツとして含む単純な Canvas コントロールを定義します。変換を使用して、マップ内の領域の座標を変換し、キャンバスに正しく表示されるようにします。ItemsControl は、マップのコンテンツを視覚的にレンダリングするために使用されます。すべてのデータ バインドは、暗黙的なデータ コンテキストを使用して行われます。MasterCanvas コントロールの場合、これは常に Map クラスのインスタンスに設定されます。
Figure 10 マップを表示するための XAML コード
<Canvas Grid.Row="2" Grid.Column="1" Name="MasterCanvas">
    <Canvas.RenderTransform>
        <TransformGroup>
            <TranslateTransform X="{Binding Path=TranslateX}"
                Y="{Binding Path=TranslateY}"/>
            <ScaleTransform ScaleX="{Binding Path=ScaleX}"
                ScaleY="{Binding Path=ScaleY}" />
        </TransformGroup>
    </Canvas.RenderTransform>

    <ItemsControl Name="MapViewer" ItemsPanel="{DynamicResource SimpleCanvasTemplate}" ItemsSource="{Binding Path=Regions}" ItemTemplate="{DynamicResource RegionTemplate}" />
</Canvas>
TranslateTransform は、TranslateX プロパティと TranslateY プロパティを Map クラスの同等のプロパティにバインドします。通常、これらの値は、マップ クラスで Map の境界ボックスの左上隅の符号反転値に設定されます。これには、マップのコンテンツを移動して、その左上隅をキャンバスの左上隅に一致させる効果があります。同様に、ScaleTransform は ScaleX プロパティと ScaleY プロパティを Map クラスの関連プロパティにバインドします。ほとんどの場合、これらのプロパティは、マップの幅に対するキャンバスの幅の比率と、マップの高さに対するキャンバスの高さの符号反転した比率に設定されます。
次のコードは、Map クラスの ScaleTo メソッドの実装を示しています。
Public Sub ScaleTo(ByVal size As Size)
    If BoundingBox.Width <> 0 AndAlso BoundingBox.Height <> 0 Then
        ScaleX = size.Width / BoundingBox.Width
        ScaleY = -size.Height / BoundingBox.Height
        TranslateX = -BoundingBox.Left
        TranslateY = -BoundingBox.Bottom
    End If
End Sub
MasterCanvas の高さと幅を含む Size 構造体が与えられると、メソッドはキャンバス内に全体が表示されるようにマップを変換します。このメソッドは、キャンバスの SizeChanged イベントに応じて呼び出されるため、キャンバスに含まれるウィンドウがサイズ変更されたときに、マップが拡大縮小されて使用可能スペースに挿入されます。
Private Sub WindowSizeChanged() Handles Me.SizeChanged
    If m_Map IsNot Nothing Then
        m_Map.ScaleTo(New Size(MasterCanvas.ActualWidth,
            MasterCanvas.ActualHeight))
    End If
End Sub
TransformGroup 内の変換の順序が重要であることに注目してください。具体的には、TranslateTransformation は ScaleTransform の前に発生する必要があります。これにより、両方の変換への引数は、UI との関連ではなくドメイン モデルとの関連で指定できます。ScaleTransform が一覧表示の最初にある場合、TranslateTransform の値は、キロメートル単位ではなくピクセル単位で指定する必要があります。これには明示的な変換が必要です。TranslateTransform を最初に指定すると、アプリケーションのオブジェクト モデルが大幅に簡略化されます。
ItemsControl は、マップのコンテンツをキャンバスにレンダリングするために使用されます。ListBox コントロールの動作に似ています。実際には、ItemsControl は ListBox の基本クラスであり、データ バインド動作の大部分を定義します。ただし、ListBox とは異なり、ItemsControl はアイテムを一覧としてレンダリングしません。このため、明らかに一覧としては表示しないマップのレンダリングには、はるかに適しています。
ItemsControl は、ItemsSource プロパティをマップの Regions コレクションにバインドし、SimpleCanvasTemplate リソースと RegionTemplate リソースを利用して、すべてのアイテムのレンダリング方法を定義します。ItemsControl の ItemsPanel プロパティに使用される SimpleCanvasTemplate は、単純に空の Canvas を作成します。その目的は、コントロールの ItemTemplate で作成された各アイテムのホスティングに使用されるコンテナを定義することです。キャンバスが使用される主な理由は、キャンバスがそのコンテンツを明示的に配置できることにあります。StackPanel などのその他の "コンテナ コントロール" は、最終的にマップを間違って描画する自動レイアウト ロジックを適用しようとします。
RegionTemplate リソースは、マップ内の各 Region に使用されるビジュアル レイアウトを定義します。入れ子になった ItemsControl を領域ごとに宣言し、その ItemSource を領域の Polygon コレクションにバインドすることによって、この処理を行います。MapViewer と同様に、RegionTemplate で定義された入れ子の ItemsControl は、ItemsPanel にキャンバスを使用します (実際には SimpleCanvasTemplate を再利用します)。ただし、MapViewer とは異なり、PolygonTemplate リソースを使用してアイテムをレンダリングします。
PolygonTemplate リソースは、MapPolygon クラスのインスタンスを与えられて WPF ポリゴン コントロールをレンダリングするデータ テンプレートを定義します。Polygon の Points コレクションを MapPolygon インスタンスの Points プロパティにバインドします。Polygon コントロールには、明示的に Transparent に設定された Fill プロパティがあることに言及しておく必要があります。このプロパティは、Polygon が多くのマウス イベントに応答できるようにするために必要です。WPF は、Fill プロパティが設定されているポリゴンと設定されていないポリゴンを区別します。Fill ブラシのないポリゴンはシェル ポリゴンと見なされ、Fill ブラシが明示的に設定されているポリゴンは塗りつぶされたポリゴンと見なされます。塗りつぶされたポリゴンに対する MouseUp、MouseEnter、MouseLeave などのマウス イベントは、マウスがポリゴンの内部にあるときに起動します。シェル ポリゴンでは、図形の内部は定義の一部と見なされません。実際に、ポリゴンの内部にマウスを置いても、マウス イベントは起動しません。ただし、ポリゴンの塗りつぶしを透明に設定することにより、マップに描画される他のデータを隠蔽することなく、マウスとの対話を有効にできるようになります。
ポリゴン テンプレートの別の興味深い点は、宣言型のトリガー ベース スタイルを使用してポリゴンの外観を決定することです。具体的には、ポリゴンをレンダリングするために、2 つの異なるスタイルを定義する RegionPolygonStyle リソースを参照します。領域に値が true の IsSelected があるポリゴンは太いオレンジ色の枠でレンダリングされ、IsSelected 値が false のポリゴンは通常の細い黒色の枠でレンダリングされます。次のイベント処理コードは、入力トラッキングのような優れた UI 機能を実装するためにこれらのスタイルを簡単に使用できる方法を示しています。
Private Sub Polygon_MouseEnter(ByVal sender As Polygon, ByVal e As _
    System.Windows.Input.MouseEventArgs)
    CType(sender.DataContext, MapPolygon).Region.IsSelected = True
End Sub

Private Sub Polygon_MouseLeave(ByVal sender As Polygon, ByVal e As _
    System.Windows.Input.MouseEventArgs)
    CType(sender.DataContext, MapPolygon).Region.IsSelected = False
End Sub
図 11 に、この例の動作を示します。
図 11 マウス イベント トリガを使用した領域の強調表示
図 11 マウス イベント トリガを使用した領域の強調表示 (画像を拡大するには、ここをクリックします)

人口データを視覚化する
人口データをマップの上に視覚化することも簡単です。この処理を行う XAML コードを図 12 に示します。DataLayer という名前の 2 つ目の ItemsControl を MasterCanvas に追加することにより、マップの描画に使用される XAML を変更します。DataLayer は MasterCanvas の内部にあるため、そのコンテンツは MapViewer の場合と同じ方法で変換されます。これにより、DataLayer にバインドされたデータを、ユーザー インターフェイスとの関連ではなく基礎となるドメイン モデルとの関連で指定することもできます。
Figure 12 ヒートマップを表示するために変更された XAML
<Canvas Grid.Row="2" Grid.Column="1" Name="MasterCanvas">
    <Canvas.RenderTransform>
        <TransformGroup>
            <TranslateTransform X="{Binding Path=TranslateX}"
                Y="{Binding Path=TranslateY}"/>
            <ScaleTransform ScaleX="{Binding Path=ScaleX}" ScaleY="{Binding Path=ScaleY}" />
        </TransformGroup>
    </Canvas.RenderTransform>

    <ItemsControl Name="MapViewer" ItemsPanel="{DynamicResource SimpleCanvasTemplate}" ItemsSource="{Binding Path=Regions}" ItemTemplate=
    "{DynamicResource RegionTemplate}" />
    <ItemsControl Name="DataLayer" ItemsPanel=
        "{DynamicResource SimpleCanvasTemplate}" ItemTemplate=
        "{DynamicResource DataLayerTemplate}"/>

</Canvas>
MapViewer とは異なり、DataLayer は、MasterCanvas からデータ コンテキストを直接継承することを考慮していません。代わりに、視覚化するデータを ItemsSource として明示的に設定するようにデザインされています。提供されるデータは、Color および Points という 2 つのプロパティを定義するオブジェクトのコレクションにする必要があります。Color プロパティは、Brush 型である必要があり、Points で記述されるポリゴンをマップ上で色付けする方法を定義します。Points プロパティは、色付けするポリゴンの頂点を定義する PointsCollection のインスタンスである必要があります。次に、DataLayer の要素のレンダリングに使用される DataTemplate を示します。
<DataTemplate x:Key="DataLayerTemplate">
        <Polygon Stroke="Transparent" Fill="{Binding Path=Color}" Points="{Binding Path=Points}" />
</DataTemplate>
これは、単純にポリゴンを作成し、Fill プロパティと Points プロパティを基礎となる要素の適切なプロパティにバインドすることで動作します。
図 13 に示す LoadData メソッドは、Map クラスのインスタンスにプライマリ マップ データを読み込み、それを MasterCanvas の DataContext として設定し、ヒート マップを作成してから DataLayer をバインドします。
Figure 13 LoadData
Public Sub LoadData()
    Dim list As New List(Of MapRegion)

    LoadFile("PolygonData\2000\states.xml", list)

    Dim q = From r In list Where Not excludedStates.Contains(r.RegionName)

    m_Map = New Map()

    Dim first As Boolean = True
    Dim bb As Rect

    For Each item In q
        m_Map.Regions.Add(item)

        If (first) Then
            bb = item.BoundingBox
            first = False
        Else
            bb.Enclose(item.BoundingBox)
        End If
    Next

    m_Map.BoundingBox = bb
    MasterCanvas.DataContext = m_Map
    m_Map.ScaleTo(New Size(MasterCanvas.ActualWidth, _
        MasterCanvas.ActualHeight))

    Dim counties = New List(Of MapRegion)

    LoadFile("PolygonData\2000\counties.xml", counties)

    Dim doc = XDocument.Load("PopulationData\CountyPopulation.xml")

    Dim q2 = _
        From _
            state In m_Map.Regions _
        Join _
            county In counties _
        On _
            state.FipsCodes(0) Equals county.FipsCodes(0) _
        Join _
            pd In doc.<CountyPopulationFile>.<CountyPopulation> _
        On _
            CInt(pd.@CountyFipsCode) Equals county.FipsCodes(1) And _
            CInt(pd.@StateFipsCode) Equals state.FipsCodes(0) _
        Where _
            CDate(pd.@Date).Year = 2006 _
        From _
            Polygon In county.Polygons _
        Select _
            Color = New SolidColorBrush(colorMapEntries.Interprolate( _
                CDbl(pd.@Population))), Points = Polygon.Points

    q2 = q2.ToList()

    DataLayer.ItemsSource = q2
    'Force the state boundaries to be on top of the county regions....
    MasterCanvas.Children.Remove(MapViewer)
    MasterCanvas.Children.Add(MapViewer)

    Dim legendInfo(colorMapEntries.Length - 2) As Object

    For i = 0 To colorMapEntries.Length - 2
        legendInfo(colorMapEntries.Length - (i + 2)) = _
        New With{.StartColor = colorMapEntries(i).FillColor, _
        .StopColor = colorMapEntries(i + 1).FillColor, .Value = _
        If(colorMapEntries(i + 1).ShowInIndex, colorMapEntries( _
        i + 1).Value.ToString("n"), "")}
    Next

    Legend.ItemsSource = legendInfo

End Sub
特に興味深いメソッドの部分は、ヒート マップの構築に使用するクエリです。このクエリは、マップに表示される州のセット、ディスクから読み込まれる郡のセット、および XML 人口データを結合することで機能します。州で定義されている郡のうち、マップに表示されない郡を除外するために、州のセットを郡のセットと結合します。両側での結合のキーは、各領域の最初の FIPS コードです。State の場合、これは数値 ID に対応します。County の場合、これは州の数値 ID を含む郡に対応します。各郡の人口値を、郡を定義している MapRegion インスタンスと関連付けるために、XML 人口データが結果と結合されます。
2006 年の人口データのみ含むように、結合の結果がフィルタ処理されます。XML 人口データには複数年のデータが含まれますが、ここではマップ上に特定の 1 年のみ表示するため、この処理が必要です。年に基づいてフィルタ処理しないと、異なる値を持つ複数の重なるポリゴンが、マップ上の同じ領域に描画されることにもなります。フィルタ処理された結果は、各郡の Polygons コレクションとクロス結合されます。これにより、郡の領域のコレクションがポリゴンのコレクションに効果的に拡張されます。
クエリの最後の部分は、Select ステートメントです。このステートメントは、各ポリゴンから点のコレクションを抽出し、関連付けられている人口値に基づいて表示する色を計算し、その色で領域をレンダリングする SolidColorBrush を作成します。色の値は、colorMapEntries 配列を使用して計算されます。この配列は、人口値と色の間のマッピングを定義します。基本的に、配列内の各エントリは、色および関連する上限人口値を定義します。郡に使用される特定の色は、その郡の人口が間に入る 2 つの色マップ エントリから補間されます。
補間は、図 14 で定義する Interpolate 拡張メソッドによって実装されます。この拡張メソッドは、colorMapEntries 配列を通じてバイナリ検索を実行し、提供された値より大きい人口値を持つ最小エントリを探すことで機能します。次に、そのエントリと前のエントリを取得し、それらのエントリを使用して適切な色値を補間します。この処理は、ColorMapEntry クラスで定義されている Interpolate インスタンス メソッドを呼び出すことで行われます。
Figure 14 Interpolate 拡張メソッド
<Extension()> _
Function Interpolate(ByVal entries() As ColorMapEntry, ByVal value As _
    Double) As Color
    Dim upperBound = entries.LeastUpperBound(value)
    Dim c As Color = Nothing

    If upperBound >= entries.Length Then
        c = entries(upperBound - 1).FillColor()
    ElseIf upperBound = 0 Then
        c = entries(0).FillColor
    Else
        c = entries(upperBound - 1).Interpolate(entries( _
            upperBound), value)

    End If

    Return c
End Function
<Extension()> _
Function LeastUpperBound(ByVal entries() As ColorMapEntry, ByVal v As _
    Double) As Integer

    Dim ret = Array.BinarySearch(entries, v, _
        New ColorMapEntryToValueComparer)
    If ret < 0 Then
        ret = ret Xor -1
    End If

    Return ret
End Function

最後に
筆者のアプリケーションで最も優れている点は、作成が簡単なことです。特に、筆者は Expression Blend を多用してインターフェイスを定義したため、見栄えのする視覚的効果を簡単に追加できます。また、アプリケーションの実行に使用されるすべてのデータの格納に XML を使用しました。これにより、Visual Basic の統合された新しい XML 機能を LINQ と共に使用して、すべてのデータ操作ロジックを簡単に実装できました。また、WPF データ バインド インフラストラクチャの上に構築されるインターフェイスをデザインしたため、機能豊富なオブジェクト モデルの上に構築された、明確に分離され、適切に設計されたアプリケーションが作成されます。
基本的に、Visual Basic、WPF、Expression Blend、および LINQ を使用して、比較的洗練された視覚化を既存のデータ本体から作成するアプリケーションを迅速かつ効果的に作成できました。このアプリケーションは、異なる年のデータを表示したり、さまざまな方法でデータを操作したりするように簡単に拡張できます。この記事のコードはすべてダウンロードできるため、自由に実験し、何が可能かを確認してください。

Scott Wisniewski は、Microsoft のソフトウェア設計エンジニアで、Visual Basic コンパイラの開発に携わっています。彼は、Visual Studio の次期リリースである "Orcas" 向けに、null 許容型、エラー修正、拡張メソッドなどの機能を開発しました。連絡先は scottwis@microsoft.com (英語) です。また、Visual Basic チームのブログ (blogs.msdn.com/vbteam) を通じて連絡を取ることもできます。


VSTO
WPF、WCF、および LINQ を使用して Office ベースのソリューションを構築する
Andrew Whitechapel
コードのダウンロード : : VSTONet2007_12.exe (419 KB)
Browse the Code Online

この記事は、Visual Studio 2008 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • VSTO を使用して Office 開発をさらに強力にする方法
  • Office ソリューションで WCF、WPF、および LINQ を使用する
  • Office アプリケーションに高度な機能を簡単に追加する
  • 簡単な方法でサービスを構築する
この記事は次のテクノロジを使用しています:
VSTO、WPF、WCF、LINQ
コンテンツ
サンプル ソリューション
WCF サービスを構築する
WPF UserControl
WPF UserControl を VSTO に接続する
VSTO ソリューションで WCF サービスを使用する
セキュリティ
VSTO で LINQ、XLinq、およびラムダ式を使用する
今後の Office ベースのアプリケーション
Visual Studio® 2008 では、幅広いタイプのカスタマ ソリューションを対象とする一連の新機能が導入されました。このことにより、WPF (Windows® Presentation Foundation)、WCF (Windows Communication Foundation)、および LINQ (Language Integrated Queries) の式 (後で説明します) を使用する VSTO (Visual Studio Tools for Office) ソリューションを構築できるようになりました。
この新技術によって、以前は実現が困難であったり、不可能であったりした動作を含むエキサイティングなソリューションの構築が可能になりました。たとえば、Microsoft® Office Excel® 2007 には、強力なグラフ作成機能がありますが、Excel の計算エンジンと WPF の 3D アニメーション化グラフを使用して拡張された UI およびデータの視覚化を組み合わせると、より一層豊富なエクスペリエンスを構築できます。
Office が真の開発プラットフォームに進化するにつれて、Office ベースのソリューションはますます高度になり、ドキュメント集中型ではなく、より柔軟に結合されるソリューションになりつつあります。Office のリッチ クライアントが強力なサーバー側の機能とリモート データに接続するサービス指向ソリューションを構築するために必要なフレームワークとデザイン時ツールセットのサポートは、WCF が適切に対応します。Visual Studio 2008 では、サービスのメタデータ、プロトコル、または XML 構成に煩わされることなく WCF サービスを利用できる簡単な GUI ウィザードを提供します。
LINQ を使用すると、データを照会するためのコードをより直感的に、大幅に簡素化して構築できます。Office 開発者にとって特に役立つ LINQ 機能の 1 つは、拡張メソッドの使用です。このメソッドは、オプションの参照パラメータまたは明示的な参照パラメータを含むメソッドの従来の Office オブジェクト モデルのパターンをサポートします。
Visual Studio 2008 では、WCF を介してリモート データやリモート サービスに接続されている WPF の高度な UI 機能を組み合わせた Office クライアント アプリケーションのネイティブ機能を組み込み、LINQ の RAD 機能を使用してデータを操作するソリューションを構築できます。

サンプル ソリューション
これから構築するソリューションは、ビジネス機能ではなく、個々のテクノロジと統合の問題に焦点を当てることができるようにあえて単純にしています。図 1 にユーザー エクスペリエンスのアイデアを示します。これには 2 つのプロセスが関与しています。それらは、コンソール アプリケーションでホストされているスタンドアロンの WCF サービスと、Word 2007 で実行されている VSTO アドインです。このアドインでは、カスタム タスク ペインを提供し、このペインには WPF カスタム コントロールが備わっています。コントロール内のボタンがクリックされると、そのアドインが反応して WCF サービスを呼び出し、XML データをフェッチします。次に、このアドインはラムダ式や式ツリーを含む LINQ や XLinq を使用して、さまざまな方法でこのデータを処理します。そして、テキストを書式設定し、アクティブなドキュメントにそのテキストを挿入します。サンプル ソリューションの内容を確認するには、msdn2.microsoft.com/vstudio/aa700831 で入手できる Visual Studio 2008 ベータ 2 リリースをダウンロード、インストールします。
図 1 サンプル ソリューションの実行時動作
図 1 サンプル ソリューションの実行時動作 (画像を拡大するには、ここをクリックします)
このサンプルの要点は、特定の実行時機能を示すことではありません。本題は、Visual Studio 2008 でのすべての新しいテクノロジ (WPF、WCF、および LINQ) が Office ソリューションとシームレスに連動することを示すことです。このサンプル ソリューションのもう 1 つの目的は、エキサイティングな新しいユーザー エクスペリエンスの構築について考える材料を提供することです。
ソリューションのアーキテクチャを図 2 に示します。VSTO アプリケーション内で WCF および LINQ を使用して、軽量なアプローチが提供されるため、Windows フォームやコンソール アプリケーション内でこれらの機能を使用する場合と変わりなく使用できます。ただし、WPF を使用するとさらに面白いということが、すぐにわかります。
図 2 サンプル ソリューションのアーキテクチャ
図 2 サンプル ソリューションのアーキテクチャ (画像を拡大するには、ここをクリックします)
VSTO の 1 つの側面は、ネイティブの Office UI ウィンドウ内でマネージ コントロールを使用するソリューションを構築する機能です。VSTO では、任意の Windows フォーム ダイアログ内、アプリケーション レベルのカスタム タスク ペイン内、ドキュメントの操作ウィンドウ内、Outlook® のカスタム フォーム領域内、および Word または Excel ドキュメントの画面上でのマネージ (Windows フォーム) コントロールの配置をサポートします。Microsoft .NET Framework 3.5 (Visual Studio 2008 に付属) には、WindowsFormsIntegration.dll が含まれており、このファイルは Windows フォーム統合 ElementHost クラスを含んでいます。このクラスは、Windows フォームと WPF 間のブリッジを提供します。このため、VSTO では Windows フォーム コントロールを配置できるすべての場所での WPF コントロールの配置もサポートしています。
これから行っていくデザイン上の決定で興味深いものの 1 つが、イベントの処理を行う場所の決定です。たとえば、WPF ボタン コントロールの 1 つがクリックされると、次のような複数のレベルでこれを処理できます。つまり、WPF UserControl 内、Windows フォーム UserControl 内、またはアドイン内です。イベントを処理する場所およびそのイベントをさらにバブルアップするかどうかは、さまざまなコントロールをどの程度再利用可能にするかによって大きく変わります。

WCF サービスを構築する
筆者はこれまでプロジェクトの構築に取り掛かる方法として、まず、ImageServiceHost という単純なコンソール アプリケーションを作成していました。次に、プロジェクトに WCF サービス アイテムを追加していました。そのプロジェクトでは ImageService を呼び出すように決めていました。理解する必要のある重要な点は、Visual Studio のプロジェクトに Windows Communication Foundation サービスを追加すると、サービス コントラクトを表すインターフェイスおよびそのインターフェイスを実装するクラスを含む、サービスのスタータ コードが自動的に生成されるということです。サービスの動作およびエンドポイントを指定する構成エントリは、標準の app.config ファイル内で指定されます。
ウィザードによって生成された DoWork メソッドを削除し、キーワード文字列を受け取って XML データの文字列を返す GetDefinition という新しいメソッドに置き換えるため、サービス コントラクトとその実装を変更する必要がありました。
[ServiceContract()]
public interface IImageService
{
    [OperationContract]
    string GetDefinition(string myValue);
}
XML データは任意の場所から取得できますが、現実的には、WCF サービスはサーバー上で実行されているため、おそらくサーバー側のデータベースからかまたは SAP、Siebel などの基幹業務 (LOB) システムからデータを取得できます。ただし、このサンプルでは、ローカル マシン上でサービスを実行しているため、静的 XML 文字列リソースからデータを取得します。
これから作成する XML データ ソースには、次のようにいくつかのアイテム要素が含まれており、各要素はキーワードの定義への単純なマッピングを表しています。
<Dictionary>
    <Item>
        <Key>Frangipani</Key>
        <Definition>Some description goes here.</Definition>
    </Item>
    <Item>
        <Key>Toucan</Key>
        <Definition>...etc</Definition>
    </Item>
</Dictionary>
この XML データ ファイルをプロジェクト リソースに追加する場合は、サービス コントラクタを実装し、リソースから XML データを読み込みます。次に、GetDefinition コントラクト メソッドを実装することにより、ユーザーが要求したキーワードに一致するアイテムを返すことができます。同時に、コンソール ウィンドウに戻り文字列をエコーします。次のサンプル コードは簡略化されているため、通常組み込まれている例外処理が含まれていないことに注意してください。
public string GetDefinition(string keyword)
{
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml(Properties.Resources.ImageData);
    String xPath = String.Format(
        "descendant::Item[Key='{0}']", keyword);
    XmlNode node = xmlDoc.DocumentElement.SelectSingleNode(xPath);
    String nodeXml = node.OuterXml;
    Console.WriteLine(nodeXml);
    return nodeXml;
}
次に、コンソール アプリケーションでは、単にサービスを開始して、停止する必要があります。
static void Main(string[] args)
{
    ServiceHost s = new ServiceHost(typeof(ImageService));
    s.Open();
    Console.WriteLine("press ENTER to stop the service");
    Console.ReadLine();
    s.Close();
}
これでサービスが定義、実装、および構成されたので、ソリューションのこの部分をビルドして、実行を開始することができます。サービスがコンソール アプリケーションで実行しており、コンソール ウィンドウで <B>Enter</B> が押されるまで、受信呼び出しを待機するように設定しました。

WPF UserControl
WPF はアプリケーション内で次の 2 とおりに活用できます。ユーザー インターフェイスを強化することと、高度なデータの視覚化を提供することです。この例では、豊富な UI 機能に焦点を当てます。ここで重要なのは、VSTO ソリューションに WPF コントロールを組み込むことがいかに簡単であるかということです。これは、ごく単純な WPF コントロール (おそらく、コントロール上にボタンを備えるグリッド) を使用して示すことができますが、WPF の高度なグラフィック機能を利用しないのは残念なので、魚眼アニメーション動作を提供するカスタム WPF UserControl を使用することにします。Microsoft Expression BlendTM の HyperBar サンプル (blogs.msdn.com/expression/articles/516599.aspx で入手可能) を基にしています。また、codeproject.com/WPF/Panels.asp で説明している Paul Tallett の FishEyePanel にも基づいています。筆者のサンプルとこれらのサンプルの主な相違点は、WPF コントロールがネイティブの Office ウィンドウでホストされていることをユーザーが確認できるように、WPF 以外のウィンドウ内に WPF コントロールを配置していることです。
まず、Windows WPF UserControl ライブラリ プロジェクトを作成します。これにより、いくつかのスタータ XAML と対応する C# 分離コードが生成されます。最初に行うことは、UserControl クラス名を UserControl1 からもっとわかりやすい FishEyeControl に変更することです。この FishEyeControl には、System.Windows.Controls.Panel から派生する FishEyePanel という名前のカスタム パネルで管理されるボタンのコレクションが含まれます。FishEyePanel では、3 つのマウス イベント (MouseMove、MouseEnter、MouseLeave) のイベント ハンドラを設定します。そのため、ユーザーがパネル上にマウスを重ねるたびに、そのパネルを無効にし、再描画されるようにすることができます。以下は、マウス イベント ハンドラの 1 つです。
public class FishEyePanel : Panel
{
    public FishEyePanel()
    {
        this.MouseMove += new
            MouseEventHandler(FishEyePanel_MouseMove);
    }

    private void FishEyePanel_MouseMove(object sender,
        MouseEventArgs e)
    {
        this.InvalidateArrange();
    }
}
FishEyePanel を FishEyeControl に接続するには、次のように、FishEyeControl で ItemsControl プロパティを設定する必要がありました。
<UserControl x:Class="WPFFishEye.FishEyeControl"
    xmlns:uc="clr-namespace:WPFFishEye">
    <Grid>
        <ItemsControl>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <uc:FishEyePanel/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</UserControl>
次に、パネルのいくつかのデータ リソースを設定します。より現実的な Web サービスまたは LOB データ ソースの設定を試行するのではなく、単にローカル ファイルをリソースとして使用します。このサンプルでは、ボタン上のイメージとして .jpg ファイルの簡単なリストを使用します。これらの JPG は外部ファイル内にあります。そのため、少しコードを作成して、単純なファイル名 (Frangipani.jpg など) を取得し、実行時に有効になる完全修飾パスに変換する必要があります。このために、IvalueConverter の実装を定義することが可能です。これは、データ バインド時にカスタム変換を実行するためによく使用される手法です。IValueConverter インターフェイスでは、2 つのメソッドを定義します。ただし、筆者はそのうちの 1 つ、つまり、Convert のみを使用します。データ バインド エンジンは、バインド ソースからバインド ターゲットに値を渡すときにこのメソッドを呼び出します。筆者の実装では、単純に現在のアセンブリの CodeBase を使用し、これをイメージ ファイル名へのパスとして、名前の前に付けています。
public class ImagePathConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
        object parameter,
        System.Globalization.CultureInfo culture)
    {
        string path =
            Assembly.GetExecutingAssembly().CodeBase.Substring
            ("file:///".Length);
        path = Path.GetDirectoryName(path) + "\\Images\\";
        return string.Format("{0}{1}", path, (string)value);
    }
}
次に、FishEyeControl.xaml に戻り、コントロールの ResourceDictionary 内の該当部分をつなげて、ItemsControl 内の ResourceDictionary (図 3 を参照) を使用します。パネル内のこのアイテムの DataTemplate は、イメージ付きのボタンに設定され、イメージはデータ バインド時に ImagePathConverter を使用します。
Figure 3 データバンド ボタン コントロール用のテンプレート
<UserControl.Resources>
    <ResourceDictionary>
        <XmlDataProvider x:Key="ItemImages" XPath="ItemImages/ItemImage">
            <x:XData>
                <ItemImages >
                    <ItemImage Image="Frangipani.jpg" Width="50"/>
                    <ItemImage Image="Toucan.jpg" Width="50"/>
                    <!-- etc -->
                </ItemImages>
            </x:XData>
        </XmlDataProvider>
        <uc:ImagePathConverter x:Key="ImagePathConverter" />
    </ResourceDictionary>
</UserControl.Resources>

<Grid>
    <ItemsControl
    DataContext="{Binding Source={StaticResource ItemImages}}"
    ItemsSource="{Binding}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <uc:FishEyePanel/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <ItemsControl.ItemContainerStyle>
            <Style TargetType="{x:Type ContentPresenter}">
                <Setter Property="ContentTemplate">
                    <DataTemplate>
                        <Button Name="b1" Click="buttonClick">
                            <Image
Source="{Binding Converter={StaticResource ImagePathConverter},
XPath=@Image}"/>
                        </Button>
                    </DataTemplate>
                </Setter>
            </Style>
        </ItemsControl.ItemContainerStyle>
    </ItemsControl>
</Grid>
また、この定義は各ボタンの buttonClick イベント ハンドラを指定します。WPF コントロール自体の中でだけでなく、ホストしている Windows フォーム コントロールやアドイン内でもイベントを処理することはできますが、筆者は WPF コントロールとアドイン内でのみイベントを処理します。ボタンのイメージ名を抽出し、イベントを再度起動することで、FishEyeControl.xaml.cs でイベント ハンドラを実装します。VSTO アドインは、イベントに反応して、イメージ名に基づいて WCF サービスを呼び出します (図 4 参照)。
Figure 4 FishEye クリック イベントの処理
public partial class FishEyeControl : System.Windows.Controls.UserControl
{
    public event FishEyeEvent FishEyeClickEvent;

    private void buttonClick(object sender, RoutedEventArgs e)
    {
        Button buttonSender = (Button)sender;
        Image buttonImage = (Image)buttonSender.Content;
        ImageSource imageSource = buttonImage.Source;
        String imageName = imageSource.ToString();
        int lastSlash = imageName.LastIndexOf('/') + 1;
        String buttonName = imageName.Substring(
            lastSlash, imageName.Length - lastSlash - ".jpg".Length);

        FishEyeEventArgs fe = new FishEyeEventArgs(buttonName);
        if (FishEyeClickEvent != null)
        {
            FishEyeClickEvent(sender, fe);
        }
    }
}
次に、イベントを再度実行するときにボタン イメージの単純な名前をリスナに送信できるように、カスタム EventArgs タイプを定義します。
public delegate void FishEyeEvent(object source, FishEyeEventArgs e);

public class FishEyeEventArgs : EventArgs
{
    public String ButtonName;

    public FishEyeEventArgs(String name)
    {
        ButtonName = name;
    }
}
ユーザーがマウスを FishEyePanel の上に重ねたときに、筆者が FishEyePanel を無効にしようとしていることを思い出してください。また、魚眼アニメーション動作を提供できるようにするために、表示動作も制御する必要があります。表示を管理するには、2 つのオーバーライドである MeasureOverride と LayoutOverride を実装する必要があります。これらは、2 パス レイアウト システムを実装します。測定パスでは、親 (FishEyeControl) が子 (FishEyePanel) を呼び出し、子に必要な領域の大きさを確認します。ここでの標準動作は、子コントロールが、自身の子に必要な領域の大きさを照会し、その結果を親に戻します。WPF では、ほとんど無制限にコントロールをネストできるモデルを使用します。そのため、このコンテキストでは、領域要件についてツリー全体が照会され、情報が最終的な親に渡されます。レイアウト パスでは、同様の再帰が実行されます。親は子のサイズを決定し、これを子に渡します。さらに、それぞれの子が情報を自分の子に渡します。これにより ArrangeOverride メソッドが呼び出されます。このメソッドでは、子にサイズを指示し、レイアウトします。
MeasureOverride の実装では、UIElement.DesiredSize プロパティを使用して、子に必要なすべての領域を割り当てるように試行しています。ArrangeOverride では、すべての子に scale transforms と translate transforms とを追加し、子が必要とする大きさを計算します。このサンプルでは、マウスのすぐ下にある子ボタンのサイズが他のボタンより大きくなるように変更しています。このボタンの両側にある 2 つのボタンの方が小さいですが、それでも他のすべてのボタンより大きくなります。残りのボタンのサイズは、残りの領域全体を等分したサイズです。詳細については、MSDN® Magazine Web サイト (msdn.microsoft.com/msdnmag/code07.aspx) にあるこの記事からダウンロードできるファイルで提供されるコードを確認することができます。最終的には、各マウスの動きにより、ボタンのサイズを調整して、おなじみの魚眼効果を提供します。

WPF UserControl を VSTO に接続する
基本の VSTO ソリューションは、WordImageSelecter と呼ばれる Word 2007 アドインです。最初のアドイン プロジェクトを作成したら、WPF UserControl プロジェクトをソリューションに追加できます。これにより、Windows フォーム プロジェクト内で WPF コントロールを統合するための Visual Studio 2008 での拡張されたデザイン時サポートを利用できます。
次に、アドイン プロジェクト内で単純な Windows フォーム UserControl を作成します。これは、最終的に、カスタムの WPF UserControl をホストします。Visual Studio 2008 では、ソリューションのすべてのプロジェクト内のすべての WPF UserControl がツールボックスに表示されます。そのため、図 5 に示すように、Windows フォーム UserControl デザイン画面に WPF UserControl を直接ドラッグ アンド ドロップできます。
図 5 WPF コントロール Windows フォーム デザイン画面
図 5 WPF コントロール Windows フォーム デザイン画面
このアクションによって、特に、ElementHost オブジェクトを使用して、Windows フォーム コントロール内の WPF コントロールをホストするために必要なすべてのコードが生成されます。ElementHost は、Windows フォーム と WPF とを統合するために設計されています。この例では、Windows フォーム UserControl は ElementHost をホストし、ElementHost は WPF UserControl をホストします。また、これはカスタム WPFFishEye.dll だけでなく、標準の PresentationCore、PresentationFramework、UIAutomationProvider、WindowsBase、および WindowsFormsIntegration DLL にも必要な参照を追加します。Windows フォーム コントロールは非常に単純です。その唯一の目的は、WPF コントロールをホストすることであるため、重要な機能を構築する必要はありません。なお、(図 6 に示すように) FishEyeControl によって公開されている FishEyeEvent をシンクできるように、ホストされている WPF FishEyeControl をプロパティとして公開していることに注意してください。
Figure 6 コントロールの初期化
private System.Windows.Forms.Integration.ElementHost elementHost;
private WPFFishEye.FishEyeControl fishEye;
public WPFFishEye.FishEyeControl FishEye
{
    get { return fishEye; }
}

private void InitializeComponent()
{
    this.elementHost =
        new System.Windows.Forms.Integration.ElementHost();
    this.fishEye = new WPFFishEye.FishEyeControl();
    this.elementHost.Dock = System.Windows.Forms.DockStyle.Fill;
    this.elementHost.Child = this.fishEyeControl;
    this.Controls.Add(this.elementHost);
}
今度は、Windows フォーム コントロールを Word 内のカスタム タスク ペインに接続する必要があります。これを実行するには、ThisAddIn_Startup メソッドを実装して、カスタム タスク ペインを作成し、Windows フォーム コントロールをカスタム FishEyeClickEvent をホストおよびシンクするコントロールとして指定します。
private WFControl wfControl;

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    Microsoft.Office.Tools.CustomTaskPane taskPane=
        this.CustomTaskPanes.Add(new WFControl(), "ImageSelecter");
    wfControl = (WFControl)taskPane.Control;
    wfControl.FishEye.FishEyeClickEvent +=
        new FishEyeEvent(FishEye_FishEyeClickEvent);
    taskPane.Visible = true;
}
FishEyeClickEvent ハンドラの実装では、とりあえず、単にメッセージ ボックスを表示することはできます。この記事に従ってコードを作成している場合、この時点でアドインをテストできます。これは、カスタム タスク ペインと WPF コントロールのコードの部分の作成が完了しているためです。
private void FishEye_FishEyeClickEvent(object source,
    FishEyeEventArgs e)
{
    System.Windows.Forms.MessageBox.Show(e.ButtonName);
}
この方法と同じ一般的な方法を使用して、任意の Windows フォーム ダイアログ内、アプリケーション レベルのカスタム タスク ペイン内、ドキュメントの操作ウィンドウ内、Outlook のカスタム フォーム領域内、および Word ドキュメントまたは Excel ドキュメントの画面上など、Windows フォーム コントロールの配置が許可されている VSTO ソリューション内の任意の箇所に WPF コントロールを配置することができます。Word または Excel ドキュメントでは、WPF コントロールを組み込む Windows フォーム コントロールを構築するための通常のデザイン時サポートを使用できますが、デザイン時のドキュメント画面にコントロールを実際に配置する前に作業をストップする必要があることに注意してください。特殊な VSTO Excel および Word デザイナではこのコントロールをサポートしないためです。
このため、ホストされている WPF コントロールを VSTO ドキュメント ソリューションにプログラムで追加することは本当にまったく役に立ちません。VSTO ランタイム インフラストラクチャは、WPF コントロールをホストするコントロールを含む任意の Windows フォーム コントロールを既にサポートしています。ランタイムは Visual Studio 2005 のときから既に装備されています。そのため、残る唯一のプログラムによる作業は、外部の Windows フォーム コントロール (ElementHost を介して WPF コントロールをホストする) をインスタンス化し、そのコントロールをソリューションに追加することです。
private void Sheet1_Startup(object sender, System.EventArgs e)
{
    string controlName = "MyWfControl";
    WFControl wfControl = new WFControl();
    wfControl.Tag = Controls.AddControl(wfControl, 50,50, 208,250,
        controlName);
    wfControl.Name = controlName;
}
VSTO ソリューション内での WPF ホスティングの性質を考慮すると、制限の 1 つとして、通常の方法で Windows Vista AeroTM Glass を使用できないということをよく理解しておく必要があります。つまり、WPF コントロールで Aero Glass を使用すると、文書の画面またはネイティブ Office ウィンドウに対してではなく、基になる Windows フォーム ホスト コントロールに対して透明になるだけであるということです。コントロールの一部として背景イメージを指定することにより、これを解消することは当然可能です。そうすれば、上部にレイアウトする Aero Glass コントロールを、その背景イメージに対して透明にすることができます。

VSTO ソリューションで WCF サービスを使用する
WCF サービスの使用に必要なクライアント側のコードを開発するには、複数の方法があります。コンパイルされたサービス アセンブリまたは実行中のサービス自体からメタデータ交換情報とクライアント プロキシ コードを生成できます。svcutil.exe を手動で実行して、コードを生成することができます。または、Visual Studio 2008 では Add Service Reference (サービス参照の追加) ウィザードを使用できます。
Visual Studio 2005 では、svcutil を手動で使用する必要があります。これを実行するには、まずサービスを開始し、次に、コマンド ウィンドウを開いて、クライアント プロジェクト フォルダに移動します。次に、svcutil を実行して、クライアント側のプロキシ コードを生成します。コマンド ラインの例を次に示します。
"C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\svcutil" /language:C#
/config:app.config http://localhost:8080/ImageServiceHost/ImageService/mex
/n:*,WordImageSelecter
このコードでは、メタデータ交換エンドポイント アドレスと共に実行サービスの URL を指定します。また、C# をターゲット言語として使用する必要があり、構成情報が app.config という名前のファイルとして出力され、その WordImageSelecter が名前空間 (このサンプルでは、これはアドイン プロジェクトの名前空間) である必要があることも指定します。
svcutil によって C# プロキシ コードと app.config が生成されたら、それらをアドイン プロジェクトに追加する必要があります。また、System.ServiceModel.dll への参照を追加する必要があります。
Visual Studio 2008 には、svcutil を手動で実行する代わりに使用できるグラフィカルなウィザードが用意されています。このウィザードを使用するには、サービスを開始して、ソリューション エクスプローラのアドイン プロジェクトを右クリックし、コンテキスト メニューの [Add Service Reference] (サービス参照の追加) オプションを選択します。[Add Service Reference] (サービス参照の追加) ダイアログで、サービス URL とターゲット名前空間を入力し、[Go] (移動) をクリックします。この操作により、実行サービスからサービス コントラクトと構成情報がフェッチされ、図 7 に示すようなサービスと操作の一覧が作成されます。[OK] をクリックしてプロキシ コードを生成します。
図 7 [Add Service Reference] (サービス参照の追加) ダイアログ
図 7 [Add Service Reference] (サービス参照の追加) ダイアログ (画像を拡大するには、ここをクリックします)
これで、アドイン クラスにプロキシ クラスのフィールドを追加し、ThisAddIn_Startup 内で初期化し、プロキシを介して WCF Service GetDefinition メソッドを呼び出すことができます。FishEyeClickEvent ハンドラでは、メッセージ ボックスを GetDefinition への呼び出しと置き換え、返された XML を解析してキーと定義を抽出し、Word 文書にこれらを挿入します。キー テキストの挿入後は、定義テキストを追加する前にその挿入の末尾に移動していることに注意してください。これは、前の選択を上書きしないようにするためです (図 8 参照)。
Figure 8 WCF サービスのクライアント側のプロキシの使用
internal ImageServiceClient serviceClient;

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    // Connect to the WCF service.
    serviceClient = new ImageServiceClient();

    // (previously discussed code omitted for brevity).
}

private void FishEye_FishEyeClickEvent(object source, FishEyeEventArgs e)
{
    //System.Windows.Forms.MessageBox.Show(e.ButtonName);
    // Invoke the WCF Service and parse the returned XML.
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml(
        Globals.ThisAddIn.serviceClient.GetDefinition(e.ButtonName));
    String keyText = xmlDoc.SelectSingleNode(
        "descendant::Item/Key").InnerText;
    String definitionText = xmlDoc.SelectSingleNode(
        "descendant::Item/Definition").InnerText;

    // Insert the Key text into the Word document.
    this.Application.Selection.InsertAfter(
        String.Format("{0}{1}{2}",
        Environment.NewLine, keyText, Environment.NewLine));
    object titleStyle = Word.WdBuiltinStyle.wdStyleTitle;
    this.Application.Selection.set_Style(ref titleStyle);

    // Move to the end of the insertion.
    object gotoItem = Word.WdGoToItem.wdGoToLine;
    object gotoDirection = Word.WdGoToDirection.wdGoToLast;
    this.Application.Selection.GoTo(
        ref gotoItem, ref gotoDirection, ref missing, ref missing);

    // Insert the Definition text into the Word document.
    Word.Range insertionStart = this.Application.Selection.Range;
    this.Application.Selection.InsertAfter(
        String.Format("{0}{1}", definitionText, Environment.NewLine));
    object headingStyle2 = Word.WdBuiltinStyle.wdStyleHeading2;
    this.Application.Selection.set_Style(ref headingStyle2);
}

セキュリティ
開発マシンでは、アドイン ソリューションを構築するときに、そのセキュリティが設定されます。ソリューションを展開するときは、セキュリティが公開操作の一部として設定されます。Office 2007 では、新規のセキュリティ機能が導入され、マクロ、ActiveX コントロール、およびアドインに対するセキュリティのしくみが合理化されました。これでインストールされているすべてのアドインが、Office Trust Center を介してアクセス可能になります。また、VSTO では Visual Studio 2008 のセキュリティ モデルに対する非常に重要な変更を行いました。モデルはバージョン固有のコード アクセス セキュリティ (CAS) リポジトリに依存しなくなり、その代わりに ClickOnce モデルに組み込まれました。また、VSTO では、ユーザーの信頼性判定用のデジタル証明書とレジストリ ベースのストアとを適切に使用するアドインと文書ソリューションの両方に対するセキュリティ層が追加されています。VSTO ソリューションは、常に、FullTrust を必要としています。これは、アンマネージ コード (Office ホスト) との相互運用が行われ、この要件がクライアント上のソリューションの一部を形成する WPF および WCF の部分までに拡張されたためです。Visual Studio 2005 と、Visual Studio 2008 での Office 2003 用のプロジェクトでは、クライアント側の WCF .config ファイル (WordImageSelecter.dll 用の app.config) 専用のセキュリティを設定する必要があります。これは、.config ファイルがコードへのエントリ ポイントであり、CAS ポリシーで明示的に信頼されている必要があるためです。Visual Studio 2008 での Office 2007 用のプロジェクトの構築では、この必要はなくなりました。これは、ソリューションの一部を形成する任意の依存アセンブリとその他の補助ファイルを含むソリューションに対して信頼が付与されているためです。この時点で、WPF と WCF の両方の部分が完成したので、再度アドインをテストすることができます。

VSTO で LINQ、XLinq、およびラムダ式を使用する
探索の最後の部分では、VSTO ソリューションのコンテキストで LINQ を使用します。Visual Studio 2008 では、ほとんどのプロジェクトが System.Core および System.Xml.Linq への参照を自動的に含みます。これには、LINQ クラスのほとんどが定義されています。LINQ は、タイプセーフな方法でデータ ソースの拡張可能な範囲に対して SQL と同様のクエリを実行することができる言語拡張のセットです。サンプル アドインでは、LINQ 作業のすべてが FishEyeClickEvent ハンドラ内で実行されます。このハンドラでは、WCF サービスから返された XML データに対して簡単な分析を実行します。例は単純ですが、機能がどれほど強力であるかを示しています。
まず、XLinq を使用して、筆者が最初使用していたもっと面倒な XPath の代わりに、XML 文字列からキーおよび定義を抽出します。
XElement item = XElement.Parse(
    Globals.ThisAddIn.serviceClient.GetDefinition(e.ButtonName));
String keyText = (string)item.Element("Key");
String definitionText = (string)item.Element("Definition");
LINQ のもう 1 つの機能は、暗黙的に型指定されたローカル変数です。型の推定は var キーワードを使用して呼び出され、その型が使用されている式から変数の型を推測するようにコンパイラに指示します。ここでは、次の手法を使用して、定義テキストの文字数を取得します。
var chars = from c in definitionText
            select c;
int charCount = chars.Count();
次に、ラムダ式を使用すると、定義テキスト内の 4 文字で構成される単語の数を取得できます。ラムダ式は匿名デリゲートと似ています。
List<string> definitionWords =
    new List<string>(definitionText.Split(new char[] { ' ' }));
var fourLetterWords = definitionWords.FindAll(
    x => (x.Length == 4));
匿名デリゲートの代わりにラムダ式を使用するだけでなく、ラムダ式から式ツリーも構築することもできます。これにより、まるでデータであるかのようにコード (ラムダ式) を処理できます。次の例では、ラムダ式 y => (y.Length == 4) を使用し、それを Expression オブジェクトにマップして、式の LISP スタイルの変換を出力できるようにします。この例では、この式により "Equal Length 4" という出力が生成されます。
Expression<Func<string, bool>> f = y => (y.Length == 4);
BinaryExpression be = (BinaryExpression)f.Body;
MemberExpression me = (MemberExpression)be.Left;
ConstantExpression ce = (ConstantExpression)be.Right;

this.Application.Selection.InsertAfter(
    String.Format("{0} chars, {1} four-letter words [{2} {3} {4}] {5}",
    charCount, fourLetterWords.Count,
    be.NodeType, me.Member.Name, ce.Value,
    Environment.NewLine));
最後に使用する機能は拡張メソッドです。ユーザーの多くは Office 用の C# でコードを開発することが苦痛であると不満をもらします。これは、Office のオブジェクト モデルの多くのメソッドが大量の省略可能なパラメータを取り、(特に Word において) 多くの場合、パラメータを参照で明示的に渡す必要があるためです。これは、Visual Basic では問題になりません。Visual Basic ではユーザーに代わって変換を処理するため、基にある複雑性が隠蔽されるからです。たとえば、前回の挿入の末尾に選択を移動するために前に示したコードでは、GoTo メソッドを呼び出す必要がありました。このメソッドは 4 つのパラメータを取り、すべてのパラメータは参照によって明示的に渡されます。これには、省略可能である 2 つのパラメータも含まれています。
この問題を解消するためには、カスタム クラス内の静的メソッドである拡張メソッドを記述することができます。そのインターフェイスを第 1 パラメータの型として指定することによって、まるで Office インターフェイスの 1 つを拡張しているようにすることができます。また、このパラメータでは this キーワードを使用する必要があります。この拡張メソッドは内部で標準の Office GoTo メソッドを使用できます。
public static class RangeExtender
{
    public static void GoTo(this Word.Application wordApplication,
        Word.WdGoToItem gotoItem, Word.WdGoToDirection gotoDirection)
    {
        object what = gotoItem;
        object which = gotoDirection;
        object missing = Type.Missing;
        wordApplication.Selection.GoTo(
            ref what, ref which, ref missing, ref missing);
    }
}
そのため、Office オブジェクト モデルによって定義されているメソッドの代わりに、この拡張メソッドを使用できます。
this.Application.GoTo(
    Word.WdGoToItem.wdGoToLine, Word.WdGoToDirection.wdGoToLast);

今後の Office ベースのアプリケーション
マネージ コードを使用して Office ベースのソリューションを構築する開発者が増えるほど、.NET Framework 内や Visual Studio の新しいリリースで導入する機能が Office コンテキスト内でシームレスに機能することがますます重要になります。この記事では、WPF、WCF、および LINQ を使用する VSTO ソリューションを構築することにより、Office アプリケーション内からサービスを活用できる方法を説明しました。サンプル ソリューションでは、単純な WCF サービスを使用して静的データを取得し、LINQ を使用してデータを解析し、WPF を使用して拡張 UI を提供しています。より一般的で現実的なソリューションでは、WCF サービスの範囲を使用して、複数のサーバー側の基幹業務 (LOB) システムと連携し、WPF を使用して、UI 要素だけなく高度なデータ視覚化を提供するでしょう。また、サーバー上、中間層内、またはクライアント上のいずれかで LINQ を使用して、データの照会や操作のための直感的で保守しやすい機能を構築することができるでしょう。
Windows フォームと WPF を統合するためのデザイナの拡張と、言語統合クエリ作成のためのコンパイラの拡張により、Visual Studio 2008 が企業の開発ツールボックスを大幅に補強することは間違いありません。

Andrew Whitechapel は、長年に渡りアーキテクト コンサルタントとして幅広い顧客のためにエンタープライズ ソリューションを構築してきました。現在、Andrew は Microsoft の VSTO チームのプログラム マネージャのテクニカル リードを務めています。


コマンドレット
カスタム コマンドで Windows PowerShell を拡張する
Jim Truher
コードのダウンロード : : PowerShell2007_12.exe (154 KB)
Browse the Code Online

この記事の内容 : :
  • Windows PowerShell SDK
  • System.IO.IsolatedStorage 名前空間を使用する
  • コマンドレットの既定の動作をオーバーライドする
  • カスタム コマンドレットをインストールおよび使用する
この記事は次のテクノロジを使用しています:
Windows PowerShell、.NET Framework、分離ストレージ
コンテンツ
コマンドレットの概要
コマンドレット属性
コマンドレット クラスの定義
パラメータ定義
共通パラメータ
ShouldProcess パラメータ
パラメータ セット
メソッド オーバーライド
結果を出力する
エラー状態をレポートする
トラブルシューティング メッセージ
コマンドレット グループ
スナップインを作成する
書式設定する
PSSnapIn のインストールと読み込みを行う
まとめ
おそらく既にご存知とは思いますが、Windows PowerShellTM は強力で柔軟性のあるツールです。ただし、独自のコマンドレットを作成して Windows PowerShell を拡張できることはご存知ないかもしれません。この記事では、IsolatedStorage との対話を可能にする 3 つのカスタム コマンドレットを作成することで、独自のコマンドレットの作成方法を示します。
IsolatedStorage に関連する他のコマンドレットは見たことがなく、その機能は役に立つと考えたため、これらのサンプルには IsolatedStorage を選択し、他のアプリケーションと競合しないデータの保存場所を提供しました。
簡単に言うと、System.IO.IsolatedStorage 名前空間では、分離ストアを作成および使用できます。信頼性の低いコードからはアクセスできないデータを読み取りおよび書き込みでき、機密性の高い情報が公開されるのを防ぐことができます。基本的にデータは、現在のユーザー、またはコードが存在するアセンブリに対してのみ使用可能になります (ドメインによって分離することもできます)。
IsolatedStorage をこれらの例で使用する場合は、キー/値のペアを文字列として保存します。IsolatedStorage は、必要な任意の型のデータを格納できますが、筆者はこの記事の目的に沿って文字列にこだわります。この記事がコマンドレットに関する記事であることを思い出してください。IsolatedStorage は作業用のサンプルを提供しているだけです。また、ここでは開始点を提供しているだけであることにも注意してください。独自のカスタム コマンドレットの作成についてさらに深い部分に進む準備ができたら、Windows PowerShell SDK を調べてください。

コマンドレットの概要
Microsoft が Windows PowerShell を作成したとき、これは、Windows PowerShell に付属のツールと同じ一貫性および信頼性を提供する他のコマンド ライン ツールを簡単に作成できるようにデザインされました。シェルがすべてのコマンドレットに対して単一のパーサーを持つことがその主な理由です。このモデルにより、開発チームは、すべての引数解析やエラー処理などを、ユーザーが行うすべての処理と同様に行うことができます。
そのため、Windows PowerShell コマンドレットと他のスタンドアロン シェル環境のコマンドの間には、かなり大きな違いがいくつかあります。たとえば、コマンドレットは Microsoft® .NET Framework クラスのインスタンスであり、スタンドアロン実行可能ファイルではありません。一般にコマンドレットでは、テキストではなくオブジェクトを出力し、出力を書式設定しません。コマンドレットは、テキストのストリームではなくオブジェクト パイプラインからの入力オブジェクトを処理します。コマンドレットでは、固有の引数を解析してはならず、エラーの表示を指定してはなりません。最後に、コマンドレットはレコード指向であり、一般には一度に 1 つのオブジェクトを処理します。
コマンドレットには特定の構造体があります。これらは特定の方法で属性を付ける必要があり、特定の基本クラスから派生する必要があります。特定のコマンドレットがパラメータをサポートする場合は、それらのパラメータにも特定の方法で属性を付ける必要があり、コマンドレットはいくつかの特定のメソッドの実装を提供する必要があります。この意味がすべてわからなくても心配しないでください。これらの各要件について、これから詳細に説明します。

コマンドレット属性
.NET クラスをコマンドレットとして宣言するには、CmdletAttribute 属性 (コマンドレットの唯一の必須属性) でクラスに属性を付ける必要があります。CmdletAttribute 属性を指定する場合は、コマンドレットの名前として使用される動詞と名詞の名前のペアを指定する必要があります。これは、コマンドレットが行う処理とコマンドレットが処理するリソースの種類を示します。
CmdletAttribute 属性自体は .NET クラスであることに注意してください。クラスのプロパティは、コマンドレットを使用する場合に使用可能なパラメータに対応します。
コマンドレット名の名詞部分により、カスタム コマンドレットを他のコマンドレットと区別できます。名詞部分では、コマンドレットが操作するリソースを指定します。コマンドレットの名前付けには、非常に具体的な名詞を使用するのが理想です。汎用的な用語を使用する場合は、その用語をコマンドレットの接尾辞として使用する必要があります。たとえば、SQL データベースの get-server コマンドレットを作成する場合は、"New-Database" ではなく "New-SQLDatabase" を使用する必要があります。特定の名詞名と動詞名の組み合わせによって、ユーザーがコマンドレットを迅速に発見し、機能を予測することが簡単になります。これらの名前は人々が認識できる単語にする必要があるため、コマンドレット名には予約された句読記号 (スラッシュ、かっこなど) やワイルドカード文字を使用しないでください。
ここでは Windows® IsolatedStorage を操作するコマンドレットを作成するため、これを名詞の基礎として使用します。少々長くなる可能性がありますが、コマンドレット名は具体的なほど良いのです。
動詞名に使用する動詞については、いくつかのかなり強力なガイドラインがあります。Windows PowerShell では一貫性が重要であるため、適切な動詞を使用してください。定義済みの動詞名の 1 つを使用することにより、カスタム コマンドレット、含まれているコマンドレット、および他のユーザーにより作成されたコマンドレットの間の一貫性が向上します。たとえば、データを取得するには、Retrieve や Acquire ではなく Get を使用します。Windows PowerShell で使用される一般的な動詞には、Add、Clear、Copy、Get、Join、Lock、Move、New、Remove、Rename、Select、Set、Split、および Unlock があります。それぞれが何の目的で使用されるかは、名前を見ればわかります。この記事では、3 つのコマンドレットを作成します。IsolatedStorage のデータ コンテンツを設定するコマンドレット、コンテンツを取得するコマンドレット、および IsolatedStorage ファイルを削除するコマンドレットです。このため、Set-IsolatedStorageData、Get-IsolatedStorageData、Remove-IsolatedStorageFile という 3 つのコマンドレットを作成します。

コマンドレット クラスの定義
ここでは、Set-IsolatedStorageData コマンドレットから開始して、コマンドレットを実装するコードを作成する必要があります。最初に、コマンドレット クラスを宣言します。
[Cmdlet(VerbsCommon.Set , "IsolatedStorage",
SupportsShouldProcess=true)]

public class SetIsolatedStorageCommand : PSCmdlet
Pascal 形式を使用し、クラスの名前にはコマンドレットの動詞名と名詞名が含まれていることに注目してください。厳密に言うとこれは必須ではありませんが、コードがはるかに読みやすくなります。
コマンドレットの属性付けは、一般的な動詞を使用していることを示します。この場合は Set です。可能であれば、コマンドレットを作成する場合には常に一般的な動詞を使用してください。True に設定された SupportsShouldProcess は、コマンドレットが ShouldProcess メソッドの呼び出しをサポートすることを示します。これにより、コマンドレットは、システムを変更する操作が実行される前にユーザーに確認を求めることができます。この属性がないか、False (既定値) に設定されている場合は、コマンドレットが ShouldProcess メソッドの呼び出しをサポートしないことを示します。
Windows PowerShell の外部のリソースを変更するすべてのコマンドレットでは、CmdletAttribute 属性を宣言するときに SupportsShouldProcess プロパティを true に設定する必要があります。これにより、コマンドレットは操作を実行する前に ShouldProcess メソッドを呼び出すことができます。ShouldProcess の呼び出しで false が返される場合は、操作が実行されません (ShouldProcess 呼び出しで生成される確認要求の詳細については、msdn2.microsoft.com/ bb204629 にある MSDN® ドキュメントを参照してください)。Confirm と WhatIf の各コマンドレット パラメータは、ShouldProcess 呼び出しをサポートするコマンドレットでのみ使用できます。
最後に、クラスを宣言し、PSCmdlet を基本クラスとして使用します。これは、Windows PowerShell コマンドレットに関連付けられている拡張された動作をすべて取得することを意味します。
Windows PowerShell では、PSCmdlet および Cmdlet という 2 つの異なる基本クラスから派生したコマンドレットがサポートされます。PSCmdlet から派生したコマンドレットでは、Windows PowerShell ランタイムにアクセスできます。これにより、他のスクリプトの呼び出しが可能になり、セッション状態を操作するために Windows PowerShell プロバイダにアクセスできます。PSCmdlet によって、Windows PowerShell ログ機能にもアクセスできます。ただし、この機能を使用するとコマンドレットが若干大きくなり、Windows PowerShell ランタイムに依存することになります。
Cmdlet クラスから派生したコマンドレットは、Windows PowerShell ランタイムへの依存が最小限です。これにはいくつかの利点があります。これらのコマンドレットは機能が少ないため若干小さく、長期的な Windows PowerShell の変化による問題が発生する可能性が低くなります。また、これらのコマンドレットは、Windows PowerShell ランタイムを使用しない他のアプリケーションに簡単に組み込むことができます。
常に Windows PowerShell 環境の一部となるコマンドレットの作成を計画している場合は、PSCmdlet を基本クラスとして使用する必要があります。一方、コードが Windows PowerShell 以外でも使用されると考える場合は、Cmdlet を基本クラスとして使用する必要があります。
ここに示す例では、PSCmdlet から派生させています。コマンドレットを作成する場合は、System.Management.Automation.dll を参照する必要があります。この DLL は、グローバル アセンブリ キャッシュ (GAC) にしかないため見つけるのが難しい場合がありますが、見つけるのに役立つ秘訣があるので後ほど紹介します。

パラメータ定義
次に、コマンドレットのパラメータを検討する必要があります。パラメータでは、ユーザーがコマンドレットに入力を提供できます。
コマンドレットのパラメータ名は、コマンドレットのデザイン全体で一貫している必要があります。Windows PowerShell SDK には、パラメータ名と、それらをコマンドレットで使用する方法についての詳細な推奨があります。この推奨は、他のコマンドレットとの一貫性を保つのに役立ちます。
コマンドレットのパラメータを宣言するには、パラメータを表すプロパティを最初に定義する必要があります。プロパティがコマンドレット パラメータであることを Windows PowerShell ランタイムに通知するには、ParameterAttribute 属性をプロパティ定義に追加します。
パラメータは明示的に public としてマークする必要があります。public としてマークされていないパラメータは、既定で internal になり、Windows PowerShell ランタイムでは見つかりません。このため、コマンドレットにあるはずのパラメータがない理由を調べようとするときに、混乱が生じる可能性があります。
ここに示す例では、すべてのコマンドレットに、IsolatedStorage ファイルの実際の名前を示す名前が必要であることがわかっています。そのため、Name パラメータのパラメータ宣言を次に示します。
private string _name = "PowerShellIsolatedStore";

/// <summary>name of store</summary>
[Parameter]
public string Name
{
    get { return _name; }
    set { _name = value; }
}
パラメータを作成する場合は、位置指定か名前指定かを選択する必要があります。位置指定パラメータでは、パラメータ名を指定する必要がありません。値だけを指定します。
PS> cd c:\windows
Position=num を属性の一部として設定することにより、そのパラメータに使用される位置を指定します。パラメータが位置指定でない場合は、Position 属性をオフにし、コマンド ラインからパラメータ名を使用して値を指定する必要があります。
ドキュメントでは、頻繁に使用するパラメータはできるだけ位置指定にすることが推奨されています。このガイダンスの唯一の問題は、多数のパラメータがある場合に、覚えておくことが多くなる可能性があることです。もちろん、パラメータが位置指定の場合でも、パラメータ名をコマンド ラインから使用できます。
コマンドレット パラメータは必須として定義できます。これは、Windows PowerShell ランタイムがコマンドレットを呼び出す前に値を割り当てる必要があることを意味します。また、省略可能として定義することもできます。既定では、すべてのパラメータが省略可能として定義されます。省略可能なパラメータを定義するには、単純に属性宣言で Mandatory プロパティを省略します。キーと値は分離ストレージに格納するため、これらの値を収集できるようにパラメータを作成する必要があります (図 1 を参照)。
Figure 1 キーと値の収集に必要なパラメータ
private string _key = null;
[Parameter(
            Mandatory=true,
            Position=1,
            ValueFromPipelineByPropertyName=true
            )]
public string Key
{
    get { return _key; }
    set { _key = value; }
}

private string _value = null;

/// <summary>the value to store</summary>
 [Parameter(
            Mandatory=true,
            Position=2,
            ValueFromPipelineByPropertyName=true
            )]
public string Value
{
    get { return _value; }
    set { _value = value; }
}
コマンドレット パラメータはエイリアスを持つこともできます。パラメータにエイリアスがあることを Windows PowerShell に示すには、AliasAttribute 属性をプロパティ定義に追加します。属性の宣言の基本構文は [Alias("alias")] です。ここに示す例では、次のように Name パラメータに適用される Filename というエイリアスを作成します。
private string _name = "PowerShellIsolatedStore";
/// <summary>name of store</summary>
[Alias("Filename")]
[Parameter]
public string Name
{
    get { return _name; }
    set { _name = value; }
}

共通パラメータ
Windows PowerShell は、共通パラメータと呼ばれるパラメータ名をいくつか予約しています。これらのパラメータ名は使用できません。共通パラメータは、WhatIf、Confirm、Verbose、Debug、ErrorAction、ErrorVariable、OutVariable、および OutBuffer です。また、これらのパラメータ名に対する vb、db、ea、ev、ov、ob というエイリアスも予約されています。

ShouldProcess パラメータ
ShouldProcess パラメータという別のパラメータのグループは、コマンドレットが CmdletAttribute 属性に SupportsShouldProcess キーワードを指定する場合にのみ存在します。
コマンドレットが ShouldProcess をサポートする場合は、実行時に Confirm パラメータと WhatIf パラメータにアクセスできます。システムを変更する操作をコマンドレットが実行する前に、ユーザー確認が必要かどうかを必ず指定してください。True は確認が必要であることを示し、false は確認が不要であることを示します。
WhatIf では、操作が実行された場合に何が変更されるかを、その操作は行わずにコマンドレットがユーザーに通知する必要があるかどうかを指定します。True は操作を行わずにユーザーに通知されることを示し、False は操作を行う必要があることを示します。
SupportsShouldProcess が指定されている場合、これらのパラメータを宣言しようとするコマンドレットは、コマンドレットを登録しようとしたときに失敗します。これらのパラメータは、コマンドレットで直接定義しないでください。代わりに、[Cmdlet(...)] 属性を使用するときに SupportsShouldProcess を含めます。

パラメータ セット
Windows PowerShell では、パラメータ セットの概念を使用します。これにより、さまざまなパラメータ セットをユーザーに公開し、ユーザーが指定したパラメータに基づいて異なる情報を返す単一のコマンドレットを作成できます。たとえば、(Windows PowerShell に組み込まれている) Get-EventLog コマンドレットは、ユーザーが List パラメータまたは LogName パラメータを指定したときに異なる情報を返します。LogName が指定されている場合、コマンドレットは特定のイベント ログ内のイベントに関する情報を返します。一方、List が指定されている場合、コマンドレットは (ログ ファイルに含まれるイベント情報ではなく) ログ ファイル自体に関する情報を返します。この場合、List および LogName は 2 つの異なるパラメータ セットを識別します。
複数のパラメータ セットが定義されている場合、コマンドレットは、Windows PowerShell が判断を行うのに十分な情報を持たないときにどのパラメータ セットを使用するかを指定できます。この場合に使用されるパラメータ セットは、既定のパラメータ セットと呼ばれ、CmdletAttribute 宣言の DefaultParameterSet キーワードを使用して指定されます。ここに示すサンプル コマンドレットでは、パラメータ セットを使用していないことに注意してください。

メソッド オーバーライド
Cmdlet クラスは、図 2 に示す、レコードの処理に使用できる仮想メソッドを提供します。これらのメソッドの 1 つ以上を、すべての派生コマンドレット クラスによってオーバーライドする必要があります。
Figure 2 仮想メソッド

メソッド | 目的
BeginProcessing | コマンドレットに対してオプションの 1 回のみの前処理機能を提供します。
ProcessRecord | コマンドレットに対してレコードごとの処理機能を提供します。この機能は、コマンドレットの入力に応じて、複数回呼び出されるか、まったく呼び出されないことがあります。
EndProcessing | コマンドレットに対してオプションの 1 回のみの後処理機能を提供します。
StopProcessing | Ctrl + C キーを押すことなどでユーザーがコマンドレットを非同期に停止したときに、処理を停止します。
筆者のコマンドレットはファイルを処理するため、図 3 に示すように、BeginProcessing メソッドを使用して、IsolatedStorage ファイルを開くために使用するコードを実装します。ここで注目すべき点がいくつかあります。まず、ファイルを作成モードで開いています。これはシステム変更であるため、ShouldProcess ブロックにコードをラップする必要があります。つまり、実行しようとしている処理を、実際に実行する前にユーザーに通知する機会があります (この通知は、ユーザーが Confirm 引数または WhatIf 引数を使用したときに行われます)。また、例外をキャッチし、それらを ThrowTerminatingError でラップしようとしていることにも注目してください。この例では、何か問題が発生した場合に、ファイルを開けないため先に進むことができません。すべての例外をキャッチするのは通常は不適切ですが、これらを ThrowTerminatingError でラップしているため、障害発生時にもう少し詳細な情報を提供できるようになります。
Figure 3 BeginProcessing メソッドの使用
protected override void BeginProcessing()
{
    try
    {
        if ( ShouldProcess( Name ))
        {
            WriteVerbose("Opening Isolated Storage: " + Name);
            isoStore =  this.GetMyStore();
            fs = new IsolatedStorageFileStream(
                Name,
                FileMode.OpenOrCreate|FileMode.Append,
                FileAccess.Write,
                isoStore
                );
            sw = new StreamWriter(fs);
            WriteDebug("Stream encoding: " + sw.Encoding);
        }
    }

    catch ( Exception e )

    {
        this.closeStreams();
        ThrowTerminatingError(
            new ErrorRecord(
                e,
                "OpenIsolatedStorage",
                ErrorCategory.NotSpecified,
                Name
                )
            );
    }
}
ここに示す実装では、ProcessRecord を使用して、分離ストレージ内にデータの新規エントリを作成する作業を行います (図 4 を参照)。エラー発生時に詳細情報を追加するために、try/catch ステートメントを使用している点に注目してください。この場合は、ThrowTerminatingError ではなく WriteError を使用しています。不正な書き込み時にパイプラインを停止する必要がないためです。
Figure 4 Set-IsolatedStorageData での ProcessRecord の使用
protected override void ProcessRecord()
{
    try
    {
        // Remember ShouldProcess may not have opened the file
        if(sw != null )
        {
            WriteVerbose("Setting " + Key + " = " + Value);
            sw.WriteLine(Key + "=" + Value);
        }
    }
    catch ( Exception e )
    {
        WriteError(
            new ErrorRecord(
                e,
                "SetIsolatedStorageValue",
                ErrorCategory.NotSpecified,
                Name
                )
            );
    }
}
分離ストレージを BeginProcessing 内で開いたため、EndProcessing メソッドを使用してファイルを閉じます。Remove-IsolatedStorageFile では分離ストレージ ファイルに対する読み取りまたは書き込みを行っていません。したがって、EndProcessing を使用してそのコマンドレットでファイルを削除します。Get-IsolatedStorageData コマンドレットと Set-IsolatedStorageData コマンドレットに対して EndProcessing コードがどのようになるかを次に示します。
protected override void EndProcessing()
{
     if (sw != null ) { sw.Close(); }
     if (fs != null ) { fs.Close(); }
     if (isoStore != null ) { isoStore.Close(); }
}
Remove-IsolatedStorageFile のコードはやや複雑です。このコマンドレットでは、適切なメソッドを使用して、IsolatedStorage オブジェクトからファイル自体を削除します。
    if(ShouldProcess("Remove Isolated Storage"))
    {
         WriteVerbose("Deleting Isolated Storage: " + Name);
         isoStore = this.GetMyStore();
         isoStore.DeleteFile(Name);
    }
ここでも ShouldProcess を使用していることに注目してください。システムに変更を加えているため、実行しようとしている処理に関する情報をユーザーが必要とする場合は、その情報をユーザーに通知する必要があります。

結果を出力する
Windows PowerShell の本質は結果ですが、これらの結果を提供する方法ではバランスをとる必要があります。バランスとは、(非常に多くのメモリを使用する、実行に長時間を要するなどによって) パフォーマンスに過度の影響を与えることなく、できるだけ多くの情報を返すことです。テキストはファイルに保存するため、テキスト自体を返すことができます。これでも問題ありませんが、もっと良い方法を示すためにキーと値を提供します。
最初に IsolatedStorage について学んだときに、ストレージに使用されている実際のファイルを見つけるのはかなり難しいことがわかったため、その情報を結果に含めます。これにより、結果がもっと便利になります。キー、値、およびデータへのパスを返すことにより、オブジェクトは次のようになります。
public class IsolatedStorageData
{
    public string Key;      // The Key
    public string Value;    // The Value
    public string FullName; // The path to the storage
}
また、オブジェクトの ToString メソッドが返す必要のある文字列の種類について検討する必要もあります。既定では、ほとんどの .NET オブジェクトは型の名前のみ返します。これはあまり役に立ちません。そのため、ToString メソッドでは、型の名前ではなく Value を返します。
FullName メンバの実際のファイル名の値を取得するには、小さなリフレクションを行う必要があります。この情報は、IsolatedStorage 情報の一部としては表面化していないためです。したがって、結果に使用するオブジェクトの実装は、図 5 に示すようになります。
Figure 5 結果に使用されるオブジェクト
public class IsolatedStorageData
{
    public string Key;      // The Key
    public string Value;    // The Value
    public string FullName; // The path to the storage
    public override string ToString() { return Value; }
    public IsolatedStorageData(
        string _key,
        string _value,
        IsolatedStorageFileStream _fs
        )
    {
        Key = _key;
        Value = _value;
        FullName = _fs.GetType() .
            GetField("m_FullPath",
                BindingFlags.Instance|BindingFlags.NonPublic ) .
                    GetValue(_fs).ToString();
    }
}
すべてのコマンドレットに一貫性を提供するために、Windows PowerShell コマンドレットはストリームにテキストを返すのではなく、パイプラインにオブジェクトを返すことをもう一度思い出してください。WriteObject は、コマンドレットが結果を出力するために使用するものです。IsolatedStorage ファイルからデータを取得した後で、このデータを IsolatedStorageData 型のインスタンスに変換し、WriteObject を使用して変換結果をパイプラインに出力します。

エラー状態をレポートする
コードを実行する場合に、目的の処理が行われず、なんらかのエラーが発生することがあります。Windows PowerShell には、このような状況に対応するかなり高度な動作があり、そのエラー レポート機能により、起きたことに対して非常に詳細な制御が可能です。
エラーが発生した場合に、そのエラーが致命的ではない場合があります。たとえば、ディレクトリ内の数千個のファイルを削除する場合は、これらのファイルのうち 1 つか 2 つを削除できなくても、他のファイルの削除をすべて無効にはしません。これらは終了しないエラーです。つまり、エラーはエラーですが、実行している操作を停止させるエラーではありません。削除可能なファイルの削除を続行できます。
一方、回復できない操作もあります。後で使用するデータを保持する一時ファイルを作成する必要があるとします。一時ファイルを作成および使用できない場合は、必要なデータを使用できなくなるため、残りの操作を続行する意味がありません。これを TerminatingError と呼びます。Windows PowerShell では、WriteError および ThrowTerminatingError という 2 つの異なるコマンドレット メソッドがあり、これらのメソッドでこの区別を行うことができます。
WriteError は、コマンドレットの操作全体にとって致命的ではない例外状況がコマンドレットの実行で発生した場合に使用されます。メソッドは、ErrorRecord のインスタンスを引数として受け取ります。この引数により、例外以外の情報 (エラーの原因) を含めることができます。
実際には、コマンドレットで例外をスローする必要はありません。代わりに ThrowTerminatingError を使用して、パイプラインの実行を停止し、例外の場合よりもはるかに多くの情報を提供できます。
筆者のコマンドレットのコードでは、BeginProcessing で ThrowTerminatingError を使用し、ProcessRecord メソッドで WriteError を使用していることがわかります。IsolatedStorage にアクセスできない場合には、全体としてコマンドレットの操作に関して実行できることはあまりないためです。一方、ファイルの読み取り中にエラーが発生した場合は、回復して続行できる可能性があります。

トラブルシューティング メッセージ
Windows PowerShell には、ユーザーとの通信方法がいくつかあります。結果やエラーではない情報を通信する場合に、ユーザーへの通知に使用できるメソッドは 3 つあります。これらのメソッドを使用する場合、送信されるメッセージをリダイレクトすることはできませんが、シェルでのユーザー設定によってメッセージを抑制できることを知っておく必要があります。これらのメソッドは、ホスティング アプリケーション (この場合は PowerShell.exe) と直接通信し、コンソール ウィンドウに書き込みます。ユーザー設定変数は、何も書き込まないことから、続行前にメッセージを書き込むかどうかを確認することまで、さまざまな動作を提供します。
これらの変数がどのように役立つかを示すために、筆者のサンプル コマンドレットでは、これらの変数を通常よりも若干多く使用します。エラーや結果以外の情報を必要とする複雑なコードがある場合は、これらの使用可能なメソッドを使用してください。System.Console.WriteLine などをコマンドレットから使用することは避けたいものです。まず、これはかなり不適切な手法です。さらに、コンソールが存在しない場合があるため、ホスティング アプリケーションに依存するべきではありません。
ユーザーに渡す帯域外情報が余分にある場合は、WriteVerbose を使用する必要があります。これは、開発者のメッセージのためのものではありません。見えない部分で行われていることをユーザーに知らせるためのものです。インターフェイスはかなり単純です。筆者の Remove-IsolatedStorageFile からの例を次に示します。ここでは、WriteVerbose を使用して、IsolatedStorage ファイルを削除しようとしていることを出力します。
if(ShouldProcess("Remove Isolated Storage"))
{
WriteVerbose("Deleting Isolated Storage: " + Name);
isoStore = this.GetMyStore();
isoStore.DeleteFile(Name);
}
開発者と通信し、コマンドレットで誤った動作をトラブルシューティングする機会を与えるには、WriteDebug を使用します。通常はシステムにデバッガを接続しますが、現場で必ず接続できるとは限りません。WriteDebug は、このような状況に役立ちます。
WriteWarning もユーザーと通信する方法であり、コマンドレットの実行に関する情報を提供します (筆者のサンプル コマンドレットでは、このメソッドを使用しませんでした)。WriteWarning を使用するのが適切な例は、時間の経過に伴いコマンドレットの動作を変更する場合です。たとえば、パラメータを廃止し、新しいパラメータを優先させて古いパラメータの使用を中止するようユーザーに通知する必要がある場合に、WriteWarning を使用できます。

コマンドレット グループ
筆者のコマンドレットは 3 つとも IsolatedStorage に関連するため、名詞はすべて IsolatedStorage という同じルートを持ちます。2 つのコマンドレットは実際のデータを処理し (Set-IsolatedStorageData と Get-IsolatedStorageData)、残りの 1 つはファイルを削除します (Remove-IsolatedStorageFile)。
これらのコマンドレットすべてに共通のパラメータ (使用する実際のファイル名) があるため、すべてのコマンドレットで同じパラメータを実装する必要はありません。代わりに、基本クラス IsolatedStorageBase を作成できます。このクラスは PSCmdlet から派生し、コマンドレットは IsolatedStorageBase から派生します (図 6 を参照)。これは、コマンドレット間の一貫性を確保するための優れた方法です。
Figure 6 IsolatedStorageBase 基本クラス
public class IsolatedStorageBase : PSCmdlet
{
    [Parameter]
    public string Name
    ...
}
[Cmdlet(VerbsCommon.Set, "IsolatedStorageData",SupportsShouldProcess = true)]
public class SetIsolatedStorageDataCommand: IsolatedStorageBase
{
...
}
[Cmdlet(VerbsCommon.Get, "IsolatedStorageData")]
public class GetIsolatedStorageDataCommand: IsolatedStorageBase
{
...
}
[Cmdlet(VerbsCommon.Remove,"IsolatedStorageFile",SupportsShouldProcess = true)]
public class RemoveIsolatedStorageFileCommand: IsolatedStorageBase
{
...
}

スナップインを作成する
これらの新しいコマンドレットを使用するには、Windows PowerShell 環境に追加する必要があります。Windows PowerShell には、スナップインを介してコマンドレットをセッションに動的に追加する機能があります。MMC スナップインとの混同を防ぐために、Windows PowerShell スナップインは PSSnapIn と呼ばれています。
PSSnapIn を作成するには、2 つのタスクを実行する短いコードを作成する必要があります。最初に、スナップインに ID を提供して、システムにインストールされている他のスナップインと区別できるようにします。次に、スナップインを正常にインストールするため、また適切なレジストリ エントリを作成して Windows PowerShell がアセンブリを見つけられるようにするための情報を提供します。
System.Management.Automation 名前空間には、PSSnapIn および CustomPSSnapIn という 2 種類の Windows PowerShell スナップインがあります。1 つのアセンブリ内のすべてのコマンドレットとプロバイダを自動的に登録するには、PSSnapIn を使用する必要があります。1 つのアセンブリ内のコマンドレットとプロバイダのサブセットを登録する場合、または異なるアセンブリにあるコマンドレットとプロバイダを登録する場合は、CustomPSSnapIn を使用する必要があります。筆者のスナップインのコードを図 7 に示します。このコードはかなり簡単です。適切なメンバをオーバーライドするだけで、作業はほぼ完了です。
Figure 7 筆者のカスタム コマンドレットのスナップイン
// This class defines the properties of a snapin
[RunInstaller(true)]
public class IsolatedStorageCmdlets : PSSnapIn
{
    /// <summary>Creates an instance of DemoSnapin class.</summary>
    public IsolatedStorageCmdlets() : base()
    {
    }
    ///<summary>The snap-in name that is used for registration</summary>
    public override string Name
    {
        get { return "IsolatedStorageCmdlets"; }
    }
    /// <summary>Gets vendor of the snap-in.</summary>
    public override string Vendor
    {
        get { return "James W. Truher";  }
    }
    /// <summary>Gets description of the snap-in. </summary>
    public override string Description
    {
        get { return "Isolated Storage Cmdlets"; }
    }
    /// <summary>The format file for the snap-in. </summary>
    private string[] _formats = { "IsolatedStorage.Format.ps1xml" };
    public override string[] Formats
    {
        get { return _formats ; }
    }
}

書式設定する
図 7 では、このスナップインの Formats メンバの値があることに注目してください。この値により、コマンドレットが出力するオブジェクトの書式設定ディレクティブを作成できます。この場合は、単純なオブジェクトを用意しましたが、既定ですべてのメンバを提示する必要はありません。役に立つ情報 (この場合はキーと値) のみ出力されるようにします。
これらの書式ファイルの作成だけで記事を 1 つ書くことができ、このトピックはこの記事の対象外です。図 8 に、関心のある 2 つの列を持つテーブル書式を作成するサンプル コードを示します。これらの列は、IsolatedStorageData オブジェクトのキーと値です。この例の目的に沿って、IsolatedStorage.Format.ps1xml という名前のファイルをアセンブリと同じディレクトリに作成できます。スナップインが読み込まれるときにエラーは表示されません。
Figure 8 サンプル書式ファイル
<Configuration>
  <ViewDefinitions>
    <View>
    <Name>IsolatedStorage</Name>
    <ViewSelectedBy>
      <TypeName>IsolatedStorageSnapin.IsolatedStorageData</TypeName>
    </ViewSelectedBy>
    <TableControl>
      <TableHeaders>
        <TableColumnHeader>
          <Label>Key</Label>
          <Width>12</Width>
      </TableColumnHeader>
      <TableColumnHeader />
        </TableHeaders>
          <TableRowEntries>
            <TableRowEntry>
              <TableColumnItems>
                <TableColumnItem>
                  <PropertyName>Key</PropertyName>
                </TableColumnItem>
                <TableColumnItem>
                  <PropertyName>Value</PropertyName>
                </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

PSSnapIn のインストールと読み込みを行う
スナップインのインストールは非常に簡単です。アセンブリのパスを指定して Installutil.exe を実行するだけです。このユーティリティを実行すると、HKLM\SOFTWARE\Microsoft\PowerShell\1\PowerShellSnapins\<snapinname> の下にいくつかのレジストリ エントリが作成されます。Windows PowerShell がスナップインを読み込む場合は、これらのエントリを使用してアセンブリを読み込み、さまざまな構成ファイルを検索します。また、Installutil.exe の操作ではアンインストールの依存関係が正しく登録されないため、このユーティリティは開発中のみ推奨されるインストール方法であることに注意する必要があります。運用環境にインストールする場合は、レジストリ エントリ ディレクトリを構成する必要があります。
スナップインの読み込みも簡単です。使用する主要なコマンドレットは、Add-PSSnapIn、Remove-PSSnapIn、および Get-PSSnapIn です。当然のことですが、add-PSSnapIn は 1 つ以上の Windows PowerShell スナップインを現在のセッションに追加します。Remove-PSSnapIn は、現在のセッションからスナップインを削除します。また、Get-PSSnapIn は、コンピュータ上の Windows PowerShell スナップインを取得します。
Remove-PSSnapIn は、実際にはアセンブリをアンロードしません。Windows PowerShell がコマンドレットの検索およびプロバイダへのアクセスに使用する一覧からコマンドレットとプロバイダを削除するだけです。

まとめ
説明したことがすべて終わると、新しいカスタム コマンドレットの使用を開始する準備ができます。当然、作業はすべてコマンド ラインから行っています。最初に、コマンドレットとスナップイン コードをコンパイルする必要があります。このコードは System.Management.Automation.dll への参照と共にコンパイルする必要があると述べたのを覚えていますか。この DLL は、SDK と GAC の両方にありますが、SDK をインストールしていなくても心配しないでください。次の小さなスクリプトで、スナップイン アセンブリを簡単に作成できます。最初に行う必要があるのは、C# コンパイラのエイリアスの作成です。これを設定した後で、System.Management.Automation.dll の場所を検索し、アセンブリをコンパイルします。
New-Alias csc "${V2Framework}\csc.exe"
$SMADLL = [PSObject].Assembly.Location
csc /target:library IsolatedStorageSnapin.cs /r:$SMADLL
アセンブリをコンパイルしたので、図 9 に示すように Installutil.exe を使用して新しいスナップインを登録できます。インストールが正常に完了すると、新しい PSSnapIn が見つかります。
図 9 スナップインの登録
図 9 スナップインの登録 (画像を拡大するには、ここをクリックします)
PS> get-PSSnapIn -reg iso*

Name        : IsolatedStorageCmdlets
PSVersion   : 1.0
Description : Isolated Storage Cmdlets
次に、スナップインをセッションに追加します。
PS> add-PSSnapIn IsolatedStorageCmdlets
これで、図 10 に示すようにコマンドレットを使用できます。
図 10 コマンドレットの使用
図 10 コマンドレットの使用 (画像を拡大するには、ここをクリックします)

Jim Truher は現在、Response Point 製品を担当する Microsoft Research Incubation のプログラム マネージャです。Microsoft Research に異動する前は Windows PowerShell チームのプログラム マネージャで、Bruce Payette と共に Windows PowerShell スクリプト言語を作成していました。


相互運用
BizTalk アダプタおよび .NET を使用してメインフレーム アプリケーションに接続する
Bashar "Bash" Badawi

この記事の内容 : :
  • ホスト システムに接続する
  • COBOL アプリケーションをエンドポイントとして公開する
  • .NET Framework の統合
  • ターミナルの画面スクレイピング
この記事は次のテクノロジを使用しています:
BizTalk Server、Host Integration Server、.NET Framework 2.0
コンテンツ
メインフレーム用の BizTalk アダプタ
接続を作成する
COBOL アプリケーションをエンドポイントとして公開する
アダプタ外での統合
まとめ
たとえば、あなたが 1970 年代に銀行の最高経営責任者 (CEO) であったとします。顧客基盤が急速に拡大し、紙ベースでの記録管理業務では、コストとリスクが高まることが次第に明らかになってきました。どうすれば記録管理を自動化できたのでしょうか。また、より効率的に記録を保管するにはどのような選択肢があったのでしょうか。
後でわかったことですが、こうした組織のほとんどは、AS/400 システムや VAX システムなどのメインフレームやミニコンピュータに投資をしていました。また、多くのプログラマがこれらのシステム上で仕事を覚えました。COBOL (Common Business Oriented Language) は、1959 年に最初に開発され、このようなシステムでの主要な開発言語となりました。
驚くかもしれませんが、COBOL は依然としてビジネス アプリケーションではよく使用されている言語です。企業のプログラミング言語の採用に関する 2001 年のガートナー レポートによると、調査対象の 60% を超える企業が COBOL を使用していました。主要な業界のほぼすべてが COBOL に依存しています。ビジネスに不可欠なアプリケーションとデータ ストレージのためにメインフレーム システムを使用するほとんどの企業では、引き続き COBOL 開発に投資しています。また、メインフレームからの移行後でさえも、引き続き投資を行っていくでしょう。ニューヨーク証券取引所が自社株購入権の決算および請求システムを Windows® ベースのサーバーに移行したのは、つい最近の 2006 年のことですが、再コンパイルされた COBOL アプリケーションが引き続き実行されています。
当初、メインフレームの接続は、同軸ケーブルを使用してコントローラ (このコントローラの接続先はフロントエンド プロセッサ (FEP)) に接続されているターミナルに限定されていました。これは、現在のネットワークによく似ています。通常、ネットワーク プロトコルは SNA (システム ネットワーク アーキテクチャ) でした。ただし、メインフレームでは、その後 TCP/IP もサポートするようになりました。ワイヤ レベルの接続によく使用されるもう 1 つの選択肢は、IP-DLC およびハイ パフォーマンス ルーティングでした。SNA のデザインでは元々ルーティングをサポートしていなかったため、特殊なルーティング ハードウェアまたはソフトウェアを使用して、FEP にパケットを配信しました。多くの COBOL アプリケーションは SNA プロトコル固有であったため、IP-DLC が代替策として開発されました。IP-DLC では、IP がメインのキャリアであり、SNA は IP 内である程度カプセル化されています。
SNA 内には、論理単位 (LU。具体的には、ネットワークと対話するエンド ユーザーまたはアプリケーション) の概念に基づくサブプロトコルがあります。LU 0 はカスタム プロトコル用に使用され、LU 2 はターミナル ベースのアプリケーションをターゲットとする 3270 データ ストリーム用、LU 3 はメインフレームからの出力用、LU 6.2 はアプリケーション間の通信用に使用されていました。LU 2 と 3270 (多くの場合、メインフレーム インターフェイスに関連付けられている緑色の画面) の統合は、画面スクレイピングとして一般的に知られています。画面スクレイピングでは、固定位置にある有意なデータを検索するためにアプリケーションが画面をスキャンします。
これらの LU プロトコルを処理するときは、多くの場合、既存のツールセットを活用するか、独自のカスタム ソリューションを開発するかのいずれかを選択します。後者を選択した場合の複雑さを考慮すると、どちらを選択すべきかは一目瞭然です。どちらを選択した場合でも、低レベルのネットワーク ハンドラを開発または使用して、OSI スタックの上位に移動し、さまざまなコード ページ、パケットのアセンブリおよび逆アセンブリを使用するバイト変換、セッション接続、変換管理、およびセキュリティを含むターゲット システム固有の要件の処理に必要な追加のロジックを提供する必要があります。これは簡単な作業ではありません。

メインフレーム用の BizTalk アダプタ
Host Integration Server 用の BizTalk® アダプタは、メインフレーム システムやミッドレンジ システムへの接続、および独自の Windows システムへのホスト アプリケーション、データ ソース、メッセージング、セキュリティの組み込みに役立ちます。これらのテクノロジを取り入れることにより、メインフレーム システム上にあるリソースは、BizTalk が管理するもう 1 つのエンドポイントになります。このエンドポイントは、たとえば、送受信の場所として使用できます。
Host Integration Server 2006 には、次の 4 つの主要なアダプタがあります。
  • ホスト アプリケーション用の BizTalk アダプタ
  • WebSphere MQ 用の BizTalk アダプタ
  • ホスト ファイル用の BizTalk アダプタ
  • DB2 用の BizTalk アダプタ
Host Integration Server が提供する BizTalk アダプタだけでなく、TCP/IP、SNA、および TN3270 用のネットワーク接続コンポーネントも用意されており、データがホストに到達するために必要なトランスポート層を供給します。
インストール後は、アダプタが登録され、図 1 に示すように、BizTalk でアダプタの位置を特定できます。また、ユーザーが BizTalk 管理コンソールでアダプタを追加できます。
図 1 管理コンソールでの BizTalk アダプタ
図 1 管理コンソールでの BizTalk アダプタ (画像を拡大するには、ここをクリックします)
Visual Studio® 内からメインフレームとの直接の統合を提供するため、アダプタ自体だけでなく、アダプタのすべての機能もインストールされます。データ アクセスのため、DB2 およびホスト ファイル用の Microsoft® .NET Framework 2.0 のマネージ プロバイダがインストールされます。これは、Visual Studio でのサーバー エクスプローラとの統合を通じて使用するか、またはプログラムで使用するために提供されています。ホスト ファイル デザイナおよび DB2 データ アクセス ウィザードも提供されており、ホスト ファイルに直接アクセスしてデザインを行うことができます。
ネットワーク接続ツール、トランザクション インテグレータ、および Host Integration Server の各プロジェクトもデザイン時エクスペリエンスの一部として提供されており、メインフレーム システムおよび AS/400 システムへの接続のためにサポートされているシナリオの大半に対応するサンプル コードがあります。また、メインフレームが自宅にない方のためのホスト シミュレータまであります。
この SimHost は、インストール フォルダの System フォルダにあります。

接続を作成する
接続して正しいプロトコルを選択するには、メインフレームの管理者から適切なパラメータとセキュリティ資格情報を入手する必要があります。DB2 データベースに接続するときは、たとえば、TCP/IP および SNA (LU 6.2) 用のオプションが表示されます。これらの設定はメインフレームと統合するすべての層全体に適用されます。
TCP/IP を使用して接続する場合はポート番号が必要です。これは、メインフレームがそのポートですべての着信要求をリッスンするためです。SNA 構成ではメインフレーム管理者がより多くの入力を行う必要があります。また、接続は図 2 に示すように SNA マネージャ MMC スナップインを使用して構成する必要があります。どちらの構成でも、メインフレーム管理者からの必要なデータの収集に役立てるために、SDK ではワークシートを提供します。
図 2 SNA 接続の構成
図 2 SNA 接続の構成 (画像を拡大するには、ここをクリックします)
DB2 データベースへのアクセスは、ホスト データ用 BizTalk アダプタを使用すると簡単です。DB2 用のアダプタを使用すると、データへの接続、読み取り、書き込みおよび DB2 に対してよく使用する SQL コマンドの実行が可能になります。Host Integration Server では、テスト用のデータ アクセス ウィザードを提供することによって、この機能をさらに簡略化します。
BizTalk アダプタの機能以外に、DB2 アダプタでも DB2 のデータベース サーバーの情報に直接アクセスするために使用できる DB2 用の .NET Framework データ プロバイダを提供します。DB2 のデータ プロバイダは、DB2 用のマイクロソフト ネットワーク クライアントに基づいており、プーリング、スキーマ生成、トランザクション、および静的 SQL を提供します。
データベースに接続すると、BizTalk ポート転送プロパティには、BizTalk ランタイムで使用可能な接続の作成に必要な文字列が格納されます。
.NET Framework マネージ プロバイダと共に DB2 用のホスト アダプタがインストールされている場合は、図 3 のコードに示すように、ホスト上の DB2 データにもプログラムでアクセスできます。TCP/IP プロトコルと SNA プロトコルの両方に対してデバッグ ツールとトレース ツールが提供されます。
Figure 3 DB2 データベースへのアクセス
using Microsoft.HostIntegration.MsDb2Client;
[WebMethod(TransactionOption=TransactionOption.RequiresNew)]

public DataSet GetAccountsFromDB2(string connString)
{
    MsDb2Connection conn = new MsDb2Connection(connString);
    MsDb2DataAdapter da = new MsDb2DataAdapter();
    DataSet ds = new DataSet();

    try
    {
        conn.ConnectionPooling = false;
        conn.Open();
        da.SelectCommand = new MsDb2Command();
        da.SelectCommand.Connection = conn;
        da.SelectCommand.CommandText = "SELECT * FROM MSDB2BANK";
        da.Fill(ds);
        ContextUtil.SetComplete();
    }

    catch(Exception e)
    {
        ContextUtil.SetAbort();
        throw e;
    }

    finally
    {
        conn.Close();
    }

    return ds;
}

COBOL アプリケーションをエンドポイントとして公開する
ホスト アプリケーション用の BizTalk アダプタは、以前は COM Transaction Initiator (COMTI) と呼ばれていたトランザクション インテグレータ (TI) に基づいています。このアダプタは TI テクノロジを拡張して、COBOL アプリケーションやトランザクションをエンドポイントとして提供し、BizTalk および .NET クライアントが管理、使用できるようにします。
ホスト アプリケーション用の BizTalk アダプタについて詳しく説明する前に、デザイン環境および実行時環境のホスト上で COBOL トランザクションを呼び出す方法にプログラミング モデルがどのように影響を与えるかを理解する必要があります。前述したように接続を実現する重要性は、BizTalk および任意の .NET アプリケーションから COBOL アプリケーションを呼び出すために使用するプログラミング モデルを決定するうえで重要な役割を果たします。
プログラミング モデルを選択する目的は、メインフレームとのインターフェイス機能を提供する方法を決定することです。ホスト アプリケーション用の BizTalk アダプタではホスト上にコンポーネントをインストールしません。そのため、メインフレームのアプリケーションとの統合のホスト側の側面を理解することは困難です。
接続用プロトコルとして TCP/IP が選択されている場合は、次の 2 つのオプションがあります。最初のオプションは、リスナ アプリケーションがホスト上にある MSLink モデルです。図 4 で示すように、リスナは受信呼び出しをインターセプトし、適切な COBOL アプリケーションにそれらをディスパッチします。
この図には脅威を感じるかもしれませんが、示している概念は複雑ではありません。簡略化する最良の方法は、類似するプロセスを借用することです。IIS を例に考えると、図のリスナ ブロックは、起動時に読み込まれ、ネットワーク アダプタにバインドされた後の TCP/IP スタックを表しています。IP スタックが読み込まれたら、ポート リストを介して IP スタックとの通信を行うように IIS が構成されます。図 4 では、これは同時実行サーバーです。IIS で最もよく使用されるポートは、明らかにポート 80 です。メインフレームの世界で最もよく使用されるポートは、3000 以降のポートです。
図 4 MSLink リスナを介してのホスト アプリケーション統合
図 4 MSLink リスナを介してのホスト アプリケーション統合
サーバー トランザクション プログラム (TP) はやや複雑ですが、簡単にするために、ASP.NET HTTP ISAPI 拡張機能について考えてみましょう。種類が ASPX または ASMX のファイルに対するページ要求を受信すると、IIS ではその要求を IIS 拡張機能に渡します。次に、その拡張機能では ASP.NET ランタイムを使用して受信要求を処理し、クライアントに対する応答などを処理します。
MSLink モデルには、3 つの重要なコンポーネントがあります。1 番目のコンポーネントはリスナで、IBM によって提供され、TCP/IP ポートおよびソケット管理に使用されます。リスナが .NET クライアントから受信要求を受け取ると、接続を受け入れ、.NET クライアントにそのデータのトランザクション要求メッセージ (TRM) を送信するように指示し、同時実行サーバーを起動します。
2 番目のコンポーネントは同時実行サーバーです。.NET クライアントからの受信要求を処理、解析し、.NET クライアントが呼び出すトランザクションを抽出し、実際の呼び出しを作成します。また、サーバー TP による応答を待機するときは接続を開いたままにします。応答が返されたら、ソケットを閉じます。
最後のコンポーネントであるサーバー TP では、クライアントが実際に呼び出しの対象としているエンド トランザクションを実行します。TP ではデータを受信し、COBOL ロジックを呼び出し、同時実行サーバーに結果を返します。これにより、.NET の呼び出し元に結果が送信されます。
同時実行サーバーには TCP ソケット固有のすべてのコードが含まれています。実際、マイクロソフトでは顧客へのガイダンスとして MSCMTICS.CBL というサンプル リスナを SDK で提供しています。MSLink リスナでは、COBOL アプリケーションが呼び出し可能である (たとえば、画面出力がないなど) と想定し、ユーザーが呼び出すことのできる COBOL アプリケーションに追加の変更を行う必要がないため、MSLink リスナの使用をお勧めします。
TCP/IP 接続での 2 番目のアプローチは、ユーザー データ モデルです。このモデルでは、呼び出し可能な各 COBOL アプリケーションに TCP ソケット コードが含まれており、図 5 で示すように .NET コード内から直接呼び出すことができます。
図 5 ユーザー データおよび .NET を使用するホスト統合
図 5 ユーザー データおよび .NET を使用するホスト統合
MSLink サンプルに基づいて構築し、一貫した類似性を維持するために、IIS のメタファを引き続き使用します。図 4 の 3 ステップから構成される MSLink プロセスと比較すると、ユーザー データ モデルはステップ 2 と 3 の組み合わせです。これは、実行中の同一のプロセスにおいてポートでリッスンし、受信要求を取得して処理し、クライアントにその呼び出しを返すための独自の IIS サーバーや Windows Communication Foundation (WCF) チャネルを開発する場合に似ています。ユーザー データ モデルでは、ユーザーが呼び出しの対象とする同時実行サーバーおよびエンド トランザクションがすべてメインフレーム上の 1 つの TP 内にあります。
重要なのは、この 2 つのアプローチの違いに注意することです。呼び出されるトランザクションに対して何も実行する必要がないため、MSLink モデルの方が簡単です。また、すべての TCP ソケット コードは汎用リスナ内で処理されます。これはユーザー データ モデルとはまったく異なります。ユーザー データ モデルではクライアントが呼び出すすべてのトランザクション内にソケット コードが存在している必要があります。注意すべきもう 1 つの重要な点は、MSLink モデルではエンド トランザクションが非ブロック方式で呼び出されますが、ユーザー データ モデルではプロセス全体が単一の反復処理内で実行されます。
SNA または IP-DLC が使用中の接続方法である場合でも、これらのプログラミング モデルが適用されます。ただし、リスナ アプリケーションは必要ありません。これは、メインフレームが CSMI (別の名前の可能性もあります) と呼ばれるリスナとして動作するネイティブ トランザクションを既定で提供するためです。図 6 に示すように、このトランザクションでは、呼び出し側のクライアントとユーザーが呼び出すエンド トランザクション間でゲートウェイとして動作する分散プログラム リンク (DPL) を使用します。DPL 以外の SNA ベースのトランザクション通信に対するこのアプローチのメリットのおかげで、DPL を使用するとメインフレームにソフトウェアをインストールする必要はありません。DPL を使用すると、.NET クライアントは CICS TP を呼び出すもう 1 つのメインフレームであるかのように装います。
図 6 リスナを含まないホスト統合
図 6 リスナを含まないホスト統合
CICS リンクが選択された場合、.NET クライアントは次の要求を送信します。その要求では、ミラー トランザクションまたは CSMI が受信要求の制御を取得し、COMMAREA を呼び出した後に TP に渡します。ビジネス ロジックが呼び出されて処理されたら、CSMI はクライアントにデータを戻します。
プログラミング モデルが特定されたら、エンドポイントおよび .NET アセンブリとして COBOL アプリケーションの公開を開始できます。そのためには、メインフレームの開発者から COBOL コピーブックを入手する必要があります。このコピーブックは、COBOL アプリケーションで想定されている入出力データを特定するという点で、C++ 構造体の定義に似ています。COBOL コピーブックは、COBOL のインポート ウィザードを使用して Visual Studio にインポートされます (図 7 を参照)。
図 7 COBOL コピーブックのインポート
図 7 COBOL コピーブックのインポート (画像を拡大するには、ここをクリックします)
インポートが成功すると、.NET アセンブリが生成され、種類の制限を Web サービス、なし、またはホスト アプリケーション用 BizTalk アダプタから選択することができます。ホスト アプリケーション用 BizTalk アダプタを選択すると、BizTalk プロジェクトにこのアダプタを追加し、BizTalk が管理するビジネス プロセスまたはオーケストレーションの COBOL トランザクションに参加するために必要なすべてのアイテムが生成されます。
このステップが完了すると、.NET アセンブリを展開して、COBOL アプリケーションの呼び出しに必要なメタデータをアダプタに提供する必要があります。メタデータでは、メインフレームとも呼ばれるリモート環境 (RE) を記述します。RE はホスト アプリケーション用の BizTalk アダプタの TI マネージャ MMC スナップインを使用して構成されます。
RE が構成されると、.NET アセンブリをターゲットの RE に関連付けることができます。これで、ポート パラメータで RE を定義できるようになりました。
これまでは、.NET 内から BizTalk エンドポイントとして COBOL アプリケーションまたはトランザクションを公開する方法を確認しました。インポートされた COBOL コピーブックはデータを公開し、RE ではビジネス ロジックが実行されるメインフレームへのインターフェイスを用意しました。そのビジネス ロジックを Windows プラットフォームに移動するために、マイクロ フォーカスや富士通などのベンダでは、COBOL コードをインポートし、Windows 上でネイティブに実行するための CLR 準拠の COBOL .NET コンパイラおよび Visual Studio プラグインを提供しています。これらのツールは、COBOL コードを C# に変換するなど、さらに一歩進んだステップを実行できます。

アダプタ外での統合
次に示すその他 2 つの重要な統合機能はアダプタとしては提供されませんが、エンドポイントとして BizTalk に公開できます。ホスト側開始処理 (HIP) とセッション インテグレータです。
HIP を使用すると、メインフレームの COBOL アプリケーションが Windows Server® プラットフォーム上にある .NET アセンブリを呼び出して BizTalk ビジネス プロセスに参加することができます。HIP には Visual Studio に統合されているデザイン時ツールがあります。このツールでは、TI MMC スナップインを使用して、HIP リスナのホストに必要なメタデータやエンドポイントのセキュリティ保護に必要なセキュリティ ポリシーを管理します。通信トランスポートは前述した同じガイドラインに準拠しており、マイクロソフトでは HIP の接続と機能をテストするためにサンプルの COBOL コードを提供しています。
セッション インテグレータでは、COBOL のターミナル アプリケーションの画面スクレイピングを可能にする一連の .NET オブジェクトを提供します。これはサーバー ベースのテクノロジであり、統合しようとしている COBOL アプリケーションが呼び出し可能でなく、ビジネス ロジックと分離されていない UI を使用して画面に直接書き込む場合に便利です。図 8 に画面スクレイピング アプリケーションのサンプルを示します。データは .NET オブジェクトを介して公開されます。これにより、画面キャンバスはデータを読み取ったり、書き込んだりできる行と列に変わります。
図 8 セッション インテグレータに基づく画面スクレイピング アプリケーション
図 8 セッション インテグレータに基づく画面スクレイピング アプリケーション (画像を拡大するには、ここをクリックします)

まとめ
Host Integration Server 用 BizTalk アダプタは、メインフレーム システムや AS/400 レガシ システムとの相互運用性および統合を実現するためにマイクロソフトが提供しています。このアダプタを使用すると、COBOL、DB2、およびその他のメインフレーム リソースを BizTalk エンドポイントとして参加させたり公開したりできます。これまで見てきたように、このアダプタを使用すると、複雑な従来のメインフレーム環境に起因する、独自の統合コンポーネントの組み込みに関する煩雑な作業が大幅に軽減されます。さらに、このアダプタはホストでのフットプリントをまったく必要としません。これは、ホスト アプリケーションの開発や管理のスキルを持つ人材が絶えず不足している IT 部門にとっては魅力的です。
レガシ ホスト統合の問題の詳細については、TechNet の Host Integration Server TechCenter (technet.microsoft.com/his) を参照してください。このサイトには、有益なホワイト ペーパー「ホスト システム用の BizTalk アダプタの紹介」(technet.microsoft.com/library/bb726925.aspx) を含むさまざまな資料があります。

Bashar "Bash" Badawi には、20 年にわたるマイクロソフト ベースのソリューション開発およびコンサルティングの経験があります。Bash は現在、顧客やパートナーのエンタープライズ ソリューションの理解と実装を支援する分野を基盤とする BizTalk/Host Integration Server グループのシニア プログラム マネージャです。


セキュリティ
ADFS を使用して組織間でユーザーを認証する
Jack Couch

この記事の一部は、Windows Live ID のプレリリース版を基にしています。ここに記載されている情報は変更される可能性があります。
この記事の内容 : :
  • 単一の組織内の SSO に ADFS を使用する
  • サーバーとクライアントで ADFS を構成する
  • 外部の組織に接続する
  • ADFS を使用して信頼をさらに拡充する
この記事は次のテクノロジを使用しています:
ADFS、ADAM
コンテンツ
単一の組織内の SSO に ADFS を使用する
登録、認証、および承認
ADAM アカウント ストアを作成する
ADFS サーバーを構成する
IIS サーバーとアプリケーションを構成する
証明書をインポートする
UnderMyControl.com を追加する
リソース パートナーを追加する
アカウント パートナーを追加する
新しいユーザーを設定する
新しいリソースを設定する
付与されたアクセス許可を照会する
Tailspin Toys に信頼を拡充する
今後の展望 - Windows Live ID の統合
まとめ
Active Directory® フェデレーション サービス (ADFS) は、標準ベースの ID 連合に参加する必要のある組織の Windows Server® 2003 に導入されました。ADFS を使用すると、他の組織の ID データの検証がさらに簡単になり、パートナーとの相互運用性がさらに向上します。この記事では、ADFS のガイド ツアーを通じて、架空のオンライン サービス プロバイダ (A. Datum Corporation) が ADFS を使用して実在のオンライン サービス プロバイダ (UnderMyControl.com) および架空の顧客 (Tailspin Toys) とやり取りするようすを見ていきます。

単一の組織内の SSO に ADFS を使用する
A. Datum Corporation は、2 つの Web アプリケーションを顧客に提供するオンライン サービス プロバイダです。1 つ目の Web アプリケーションはオンラインのドキュメント共有およびストレージを利用可能にし、2 つ目は独立系のアーティストにオンライン上で音楽の公開の場を与えます。A. Datum では、ユーザーが各アプリケーションのユーザー名とパスワードを追跡するために 2 つの異なるアカウント ストアを使用するのではなく、ADFS と ADAM (Active Directory Application Mode) によって 1 つのアカウント ストアを作成し、ユーザーが単一のユーザー名とパスワードを使用して両方のアプリケーションにアクセスできるようにしていました。
多くの場合、ADFS サーバーはアカウント パートナーまたはリソース パートナーとして機能します。アカウント パートナーとして機能する ADFS サーバーは、アカウント ストア (ADAM または Active Directory) にアクセスしてユーザーを認証するように構成されます。リソース パートナーとして機能する ADFS サーバーは、ADFS 対応のアプリケーションをサポートするように構成されます。このシナリオでは、ADFS サーバーを使用してアプリケーションとアカウント ストアの両方にアクセスできます。
ADAM を ADFS と共に使用すると、シングル サインオン (SSO) が有効になり、顧客が多数のパスワードを覚えておく必要がなくなります。図 1 は、A. Datum がこのソリューションを組織内に展開したようすを示しています。
図 1 シングル サインオン ソリューション
図 1 シングル サインオン ソリューション
注 : Active Directory サーバーが存在するのは、ADFS サーバーがドメインに参加している必要があるためです。このシナリオでは、それ以外の目的に Active Directory は使用されません。

登録、認証、および承認
新しいユーザーがファイル ストレージまたは音楽公開サービスにサインアップすると、通常の登録プロセス (ユーザー名の選択、パスワードの設定、プロファイル情報の指定) が行われます。この情報は ADAM アカウント ストアに格納され、A. Datum がホストするすべてのアプリケーションでそのユーザーが認証されます。図 2 は、ADAM に接続するための一般的なコードを示しています。
Figure 2 ADAM に接続するための一般的なコード
const long ADS_OPTION_PASSWORD_PORTNUMBER = 6;
const long ADS_OPTION_PASSWORD_METHOD = 7;

const int ADS_PASSWORD_ENCODE_REQUIRE_SSL = 0;
const int ADS_PASSWORD_ENCODE_CLEAR = 1;

System.DirectoryServices.AuthenticationTypes AuthTypes =
    AuthenticationTypes.Signing |
    AuthenticationTypes.Sealing |
    AuthenticationTypes.Secure;

// connect to ADAM store
System.DirectoryServices.DirectoryEntry objUsers = new DirectoryEntry(
    "LDAP://10.0.0.212:389/cn=Users,dc=adamstore,dc=com",
    "ADATUM\\administrator","Pa$$word!",AuthTypes);

// create user object
DirectoryEntry user = objUsers.Children.Add(
    "cn="+UsernameTB.Text, "user");

// set user properties
user.Properties["msDS-UserDontExpirePassword"].Value=true;
user.Properties["userPrincipalName"].Value = UsernameTB.Text;
user.CommitChanges();

// set port number, method, and password.
user.Invoke("SetOption", new object[] { ADS_OPTION_PASSWORD_PORTNUMBER,
    389 });
user.Invoke("SetOption", new object[] { ADS_OPTION_PASSWORD_METHOD,
    ADS_PASSWORD_ENCODE_CLEAR});
user.Invoke("SetPassword", new object[] { PasswordTB.Text });
ユーザーがアカウント作成を完了してファイル ストレージまたは音楽公開サービスにアクセスしようとすると、Web サーバーはそのユーザーを ADFS サーバーにリダイレクトします。次に、ADFS サーバーは、ユーザー名とパスワードの入力を求め、ADAM サーバーと通信してユーザーの資格情報を検証し、グループ メンバシップを確認します。さらに、セキュリティ トークンをクライアントに発行し、クライアントを Web アプリケーションにリダイレクトします。
このセキュリティ トークンには、要求と呼ばれる、ユーザーの ID とグループ メンバシップに関する情報が含まれています。アプリケーションでは、ID 情報を使用して ADFS サーバーに対するユーザーの認証が正常に行われたことを検証し、グループ メンバシップ情報を使用して承認の決定を行います。たとえば、ユーザーのセキュリティ トークンに Musicians というグループ要求がある場合、ユーザーは A. Datum の音楽アプリケーションで音楽のアップロードのみ行うことができます。
// check if the user is a Musician and therefore has write (upload)
// permission
if (!User.IsInRole("Musicians"))
{
    // if the user doesn't have permission, show an error message
    Response.Clear();
    Response.Write("You do not have permission to upload files");
    return;
}
ユーザーは、A. Datum がホストする他のアプリケーションへのアクセスが必要になった場合でも、資格情報を再入力する必要はありません。これは、両方のアプリケーションが信頼する ADFS 発行のセキュリティ トークンをユーザーが既に持っているためです。A. Datum は他の手法を利用して顧客の SSO を実現してきましたが、ADFS ではアカウント パートナーとリソース パートナーを追加するだけで信頼関係を簡単に拡充できます。

ADAM アカウント ストアを作成する
ADAM アカウント ストアを追加するには、サーバーで Windows Server 2003 R2 を実行し、ローカル管理者特権でログオンする必要があります。最初に ADAM をインストールし、次に ADAM インスタンスを作成することによってストアを作成します。ADAM のインストールはコントロール パネルから実行されます。[プログラムの追加と削除]、[Windows コンポーネントの追加と削除] の順に選択します。Active Directory サービス内の [Active Directory Application Mode (ADAM)] チェック ボックスをオンにします。ADAM をインストールしたら、[スタート] メニューの [すべてのプログラム] に [ADAM インスタンスの作成] という項目が追加されます。この項目を選択します。メッセージが表示されたら、一意のインスタンスと新しいアプリケーション ディレクトリ パーティションを選択します。ADAM インスタンスの作成が完了すると、ADAM ADSI Edit を使用してそのインスタンスに接続できます。この接続は、使用するアカウントに必要なアクセス許可を与えるために行う必要があります。

ADFS サーバーを構成する
ADFS を構成して実行するには、最初に IIS をインストールする必要があります。また、IIS 6 Resource Kit (go.microsoft.com/fwlink/?LinkId=36285) の SelfSSL ツールを使用して、自己署名された SSL サーバー認証証明書を作成してインストールする必要もあります。
次の手順は、ADFS のインストールです。そのためには、[プログラムの追加と削除] の [フェデレーション サービス] チェック ボックスをオンにします。また、ADFS が動作するのに必要であるため、ASP.NET を有効にするように求めるメッセージが表示されます。インストールが完了すると、フェデレーション サービスのエンドポイント (フェデレーション サービスの URL) と URI (フェデレーション サービスの一意の名前) を Active Directory フェデレーション サービス スナップインで変更できます。
次に、ADAM アカウント ストアを追加します。そのためには、[Account Stores] (アカウント ストア) を右クリックし、[New] (新規作成) をクリックします。ウィザードが表示され、ADAM サーバー名、URI (ADAM ストアを表す一意の名前)、検索ベースの識別名 (ユーザー アカウントの場所)、およびユーザー名に使用されるライトウェイト ディレクトリ アクセス プロトコル (LDAP) 属性 (一般的には userPrincipalName 属性が使用されます) の入力を求められます。
要求とは、ユーザーが認証された後に、ADFS がユーザーのセキュリティ トークンに格納するユーザー属性です。グループ要求は、特定のグループ内のメンバシップを示しています。ADAM アカウント ストア内のユーザー アカウントによって、ユーザーが Musicians グループのメンバであることが示される場合、A. Datum ADFS サーバーはグループ要求 Musicians をユーザーのセキュリティ トークンに配置します。Active Directory フェデレーション サービス スナップイン内で、[Organization Claims] (組織の要求) を右クリックし、[New Organization Claim] (新しい組織の要求) をクリックします。これにより、ウィザードが起動し、要求の種類 (グループ要求) を選択して名前 (Musicians) を指定できるようになります。
グループ要求の抽出を作成することにより、先ほど作成した Musicians グループ要求に ADAM Musicians グループを関連付けます。そのためには、アカウント ストアを右クリックし、[New Group Claim Extraction] (新しいグループ要求の抽出) をクリックします。ここで、グループ メンバシップを表す LDAP 属性を指定します。たとえば、属性 memberof およびグループ CN=Musicians、CN=Users、DC=adamstore、DC=com を指定できます。また、ドロップダウン リストから作成した Musicians グループ要求を選択します。最後に、認証を提供する対象のアプリケーションに ADFS サービスを認識させるために、ADFS スナップインで新しいアプリケーションを選択し、アプリケーションの URL を指定します。

IIS サーバーとアプリケーションを構成する
要求を認識するアプリケーションの場合、アプリケーションをホストしている IIS サーバーで必要な構成はわずかです。最初に、サーバー認証証明書を IIS サーバーにインストールします。ここでも、IIS 6 Resource Kit の SelfSSL を使用します。次に、[Windows コンポーネントの追加と削除] で、要求を認識するエージェントをインストールする必要があります。最後に、ADFS を認証に使用するようにアプリケーションを構成します。要求を認識するアプリケーション (ADFS を認証に使用するようにビルドされたアプリケーション) の場合、ADFS 関連のすべての構成はアプリケーションの web.config ファイルで行われます。図 3 は、A. Datum の音楽共有アプリケーションの web.config ファイルを示しています。
Figure 3 A. Datum の音楽共有アプリケーションの Web.config ファイル
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="system.web">
      <section name="websso"
        type="System.Web.Security.SingleSignOn.WebSsoConfigurationHandler,
         System.Web.Security.SingleSignOn, Version=1.0.0.0,
         Culture=neutral,
         PublicKeyToken=31bf3856ad364e35, Custom=null" />
    </sectionGroup>
  </configSections>

<system.web>
  <sessionState mode="Off" />
  <compilation defaultLanguage="c#" debug="true">
  <assemblies>
    <add assembly="System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
        PublicKeyToken=B77A5C561934E089"/>
    <add assembly="System.Design, Version=2.0.0.0, Culture=neutral,
        PublicKeyToken=B03F5F7F11D50A3A"/>
    <add assembly="System.DirectoryServices, Version=2.0.0.0,
        Culture=neutral,
        PublicKeyToken=B03F5F7F11D50A3A"/>
    <add assembly="System.Web.Security.SingleSignOn, Version=1.0.0.0,
        Culture=neutral,
        PublicKeyToken=31bf3856ad364e35, Custom=null"/>
    <add assembly="System.Web.Security.SingleSignOn.ClaimTransforms,
        Version=1.0.0.0,
        Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null"/>
  </assemblies>
  </compilation>

  <customErrors mode="Off"/>
  <authentication mode="None" />

  <httpModules>
    <add name="Identity Federation Services Application
        Authentication Module"
        type="System.Web.Security.SingleSignOn.WebSsoAuthenticationModule,
        System.Web.Security.SingleSignOn, Version=1.0.0.0,
        Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null" />
  </httpModules>

  <websso>
    <authenticationrequired />

    <urls>
      <returnurl>https://www.adatum.com/Music/</returnurl>
    </urls>

    <cookies writecookies="true">
      <path>/Music</path>
    </cookies>

    <fs>https://adatum.ad.adatum.com/adfs/fs/
        federationserverservice.asmx</fs>
  </websso>
</system.web>

<system.diagnostics>
  <switches>
    <add name="WebSsoDebugLevel" value="255" />
  </switches>

  <trace autoflush="true" indentsize="3">
    <listeners>
      <add name="LSLogListener"
        type="System.Web.Security.SingleSignOn.BoundedSizeLogFileTrace
        Listener, System.Web.Security.SingleSignOn, Version=1.0.0.0,
        Culture=neutral,
        PublicKeyToken=31bf3856ad364e35, Custom=null"
        initializeData="c:\logdir\claimapp.log" />
    </listeners>
  </trace>
</system.diagnostics>

</configuration>
図 3 にはいくつかの興味深い点があります。<configSections> ブロックに注目してください。セクション名 websso は、ASP.NET で読み込むことができるように ADFS SSO.dll を指定します。<assemblies> セクションは、ADFS が使用する SingleSignOn アセンブリと ClaimTransforms アセンブリを含む、複数の項目を示しています。
httpModules セクションで宣言されたモジュールは、リソース フェデレーション サービスによって認証が署名されていることを検証するために、すべての HTTPS 要求をインターセプトします。有効な認証 Cookie が存在しない場合、この httpModule はクライアントを認証のためにフェデレーション サービスにリダイレクトします。
<websso> セクションでは、<authenticationrequired/> タグに注目してください。このタグが存在しない場合は、サイトへの匿名アクセスが許可され、アクセス権限を付与する前にセキュリティ トークンを検証するかどうかは個々のページ イベントが決定します。HTTPmodule は認証 Cookie を検証しますが、有効な認証 Cookie が存在しない場合でも、クライアントはフェデレーション サービスにリダイレクトされません。
<urls> セクションを使用すると、クライアントが認証された後に、ADFS サーバーがそのクライアントをアプリケーションにリダイレクトするために使用する URL を指定できます。また、ADFS サーバーへのクライアントのリダイレクト時にアプリケーションを識別するためにも使用されます。このセクションの終わりにはスラッシュ (/) を付ける必要があります。
<urls>
<returnurl>https://www.adatum.com/Music/</returnurl>
</urls>
<cookies> セクションは、クライアントで ADFS サーバーが発行したセキュリティ トークンを格納するために ADFS によって使用されます。
<cookies writecookies="true">
<path>/Music</path>
</cookies>
最後に、<fs> タグは、フェデレーション サービス URL の場所を指定して、アプリケーションがユーザー要求を正常に認証できるようにします。

証明書をインポートする
A. Datum に対しては自己署名された証明書を使用しているため、使用中のすべてのクライアントおよび大部分のサーバーで信頼されているルート ストアに証明書をインストールする必要があります。インストールを行わないようにするには、信頼されているルート ストアにインストールされている証明機関が発行する証明書を使用してください。証明書 Microsoft 管理コンソール (MMC) スナップインを使用して、信頼されているルート ストアに証明書を追加します。図 4 は、この手順を使用してインストールする必要のあるクライアント、サーバー、および証明書を示しています。
Figure 4 必要な証明書とデバイス

デバイスの種類 | 必要な証明書
Web アプリケーション サーバー (IIS) | 組織の ADFS サービスのトークン署名証明書。組織の ADFS サーバーのサーバー認証証明書。
クライアント コンピュータ | すべての ADFS サービスのトークン署名証明書。すべての ADFS サーバーと Web アプリケーション サーバーのサーバー認証証明書。
リソース ADFS サーバー | すべてのアカウント パートナー ADFS サービスのトークン署名証明書。

UnderMyControl.com を追加する
A. Datum のアプリケーションのリリース 1.0 のアクセス制御の要件は非常に単純でした。音楽公開アプリケーションの場合、ユーザーはミュージシャンかファンのいずれかでした。ミュージシャンは音楽をアップロードして、すべてのユーザーがすべての音楽をダウンロードできました。また、ファイル ストレージ アプリケーションに必要なアクセス要件も単純でした。ドキュメントのアップロードとダウンロードの両方が可能であるか、ダウンロードのみ可能であるかのいずれかでした。
これらのサービスのリリース 2.0 において、A. Datum はファイル共有アプリケーションのアクセス制御機能を大幅に拡充し、ドキュメントの所有者が他のユーザーに特定のアクセス許可を割り当てることができるようにする必要がありました。たとえば、Bob があるドキュメントの所有者である場合、彼は読み取りのアクセス許可を Sue に割り当て、読み取り/書き込みのアクセス許可を Tom に割り当てることができます。通常、そのためには A. Datum が多くのアプリケーション ロジックを書き直し、多数の新しい UI 要素を作成する必要がありますが、A. Datum では既に ADFS を使用していたので、UnderMyControl.com が提供する Web サービスにアクセス制御の管理を委託して、ユーザーがリソースの随意アクセス制御リストを管理できるようにすることにしました。
図 5 に示すように、UnderMyControl.com でも ADFS を使用していますが、この場合の目的はアプリケーションへのアクセス権限の付与です。A. Datum を信頼できるアカウント パートナーとして UnderMyControl.com の ADFS サーバーに追加し、UnderMyControl.com を信頼できるリソース パートナーとして A. Datum の ADFS サーバーに追加することにより、A. Datum のユーザーは A. Datum が発行するセキュリティ トークンを使用して UnderMyControl.com にアクセスできます。つまり、個別のユーザー名とパスワードを使用して UnderMyControl.com にログインする必要はありません。
図 5 アクセス制御の管理のアウトソーシング
図 5 アクセス制御の管理のアウトソーシング

リソース パートナーを追加する
必要な信頼関係を確立するために、A. Datum では UnderMyControl.com をリソース パートナーとして追加しました。これは、ADFS MMC スナップインを使用して行われます。そのためには、[Resource Partners] (リソース パートナー) を右クリックし、[New Resource Partner] (新しいリソース パートナー) をクリックします。表示されるウィザードで、UnderMyControl.com の ADFS サービスの URL を指定し、フェデレーション Web SSO (Active Directory の信頼関係が含まれていないことを示します) を選択する必要があります。

アカウント パートナーを追加する
UnderMyControl.com をリソース パートナーとして追加して A. Datum の ADFS サーバーを構成した後、A. Datum を UnderMy Control.com の ADFS サーバーにアカウント パートナーとして追加すると、信頼関係が完成します。そのためには、[Account Partners] (アカウント パートナー) を右クリックし、[New] (新規作成)、[Account Partner] (アカウント パートナー) の順にクリックします。ここで、A. Datum の ADFS サービスの URL を指定し、フェデレーション Web SSO を選択する必要があります。また、ADFS サービスが信頼するセキュリティ トークンの作成時に、A. Datum で使用されるトークン署名証明書を指定する必要もあります。

新しいユーザーを設定する
ユーザー アカウントは A. Datum の ADAM アカウント ストアに格納されていますが、UnderMyControl.com では、他のユーザーが互いにアクセス許可を付与できるようにするために、すべてのユーザーを認識する必要があります。そのためには、Web サービスを使用して UnderMyControl.com でユーザー プロファイルを作成します。ADFS を使用して認証されるユーザーを作成するときには、パスワードを指定しないでください。
// connect to Under My Control web service
umc.Service svc = new umc.Service();
svc.CookieContainer = new System.Net.CookieContainer();
Session["CookieContainer"] = svc.CookieContainer;

// create a new UMC user
System.Guid userGuid;
svc.CreateAccount(UsernameTB.Text, "", "" , out userGuid);
UnderMyControl.com では、ユーザーにリソースのアクセス許可の管理を許可するため、アクセス権限が付与される各ユーザーと他のユーザーにアクセス権限を付与できる各ユーザーのプロファイルが必要です。ただし、A. Datum アカウント (ユーザー名とパスワード) は、A. Datum の ADAM サーバーに排他的に格納されています。

新しいリソースを設定する
図 6 に示すように、新しいドキュメントや音楽が A. Datum のいずれかのアプリケーションにアップロードされると、関連付けられたリソースが UnderMyControl.com に作成され、UnderMyControl.com の Web サービスを使用して既定のアクセス許可が設定されます。
Figure 6 関連付けられたリソースの作成
// connect to the Under My Control web service
umc.Service svc = new umc.Service();
svc.CookieContainer =
    (System.Net.CookieContainer)Session["CookieContainer"];

// guid for the Under My Control music project
Guid projectGuid = new Guid("5f2739ff-a441-44fd-8c68-545a6d654e69");

// guid for the newly created resource
Guid resourceGuid;

// create new resource with name set to uploaded file name
svc.CreateResource(FileUpload.FileName, projectGuid, out resourceGuid);

// Set the permissions for the resource
// add the user as an owner of the new resource
svc.AddOwnerToObject( (Guid)Session["User_GUID"], resourceGuid);

// UMC permission ids for this project
const int READ_PERMISSION = 13;
const int WRITE_PERMISSION = 14;

// give user read (aka download) and write permissions
svc.CreateACL( (Guid) Session["User_GUID"], resourceGuid,
    READ_PERMISSION);
svc.CreateACL( (Guid) Session["User_GUID"], resourceGuid,
    WRITE_PERMISSION);

付与されたアクセス許可を照会する
ユーザーがドキュメントや音楽にアクセスしようとすると、最初に A. Datum のアプリケーションがそのユーザーに付与されたアクセス許可を UnderMyControl サービスで照会し、該当する操作だけを許可します。図 7 は、リソースの承認の実行のしくみを示しています。
Figure 7 プログラムによる UnderMyControl への接続
// connect to the Under My Control web service
umc.Service svc = new umc.Service();
svc.CookieContainer =
    (System.Net.CookieContainer)Session["CookieContainer"];

// ID for WRITE_PERMISSION in UMC for this project
const int WRITE_PERMISSION = 14;

// check if the user has write permissions for the resource in UMC
bool hasPermission = false;
if(!svc.HasACL((Guid)Session["User_GUID"], resourceGuid,
    WRITE_PERMISSION, out hasPermission)||!hasPermission)
{
    // if the user doesn't have permission, show an error message
    Response.Clear();
    Response.Write(
        "You do not have permission to overwrite this file");
    return;
}

Tailspin Toys に信頼を拡充する
アクセス制御が拡張されたバージョン 2.0 のファイル共有アプリケーションがリリースされた後、顧客の関心は高まりましたが、同時に企業の関心も得られるようになりました。驚いたことに、A. Datum は大手玩具メーカーの Tailspin Toys のドキュメント管理を行う契約を結びました。Tailspin では、A. Datum のドキュメント ストレージ アプリケーションを使用して、従業員がドキュメントにいつでもアクセスできるようにしたり、同僚、パートナー、顧客とドキュメントを共有したりしたいと考えていました。しかし、Tailspin では各従業員に A. Datum のユーザー アカウントを作成し、別途ユーザー名とパスワードを管理してもらおうとは考えていませんでした。ここでも、ソリューションは ADFS でした。
図 8 に示すように、A. Datum では Tailspin Toys をアカウント パートナーとして追加し、従業員が Tailspin Toys のセキュリティ トークンを使用して A. Datum のアプリケーションにアクセスできるようにしました。A. Datum の ADFS サーバーと Tailspin Toys の ADFS サーバーでのアカウント パートナーとリソース パートナーの関係確立の手順は、UnderMyControl.com との信頼関係の確立の場合と同じです。
図 8 Tailspin Toys と A. Datum の実装
図 8 Tailspin Toys と A. Datum の実装
もちろん、この説明では、Tailspin Toys による展開は大幅に簡略化されています。実際の展開では、Active Directory サーバーを含む社内ネットワークと社外と接している ADFS サーバーとの中継としてアカウント フェデレーション サーバー プロキシが使用されました。アカウント フェデレーション サーバー プロキシの使用方法の詳細については、TechNet Web サイトの『ADFS の設計および展開ガイド』を参照してください。

今後の展望 - Windows Live ID の統合
Tailspin Toys は、ADFS 構成が同じ A. Datum にドキュメント管理をアウトソーシングした最初の組織となりました。しかし、A. Datum は、成長を続けるにつれて、多くの顧客が Office Live を使用して従業員のアカウントをホストする中小企業であることがわかってきました。また、個人ユーザーの多くは既に Windows LiveTM ID アカウントを持っていることもわかりました。将来的には、Windows Live ID での ADFS のサポートが予定されているので、A. Datum では、Windows Live ID を別のアカウント パートナーとして追加するだけで、これらの両方のグループのユーザーが最終的に既存のユーザー アカウントを使用できるようになることに気付きました。図 9 は、完成した設定を示しています。
図 9 A. Datum と Windows Live ID の実装
図 9 A. Datum と Windows Live ID の実装
現時点では、Windows Live ID のアカウント パートナーとしての使用はサポートされていません。Microsoft Active Directory フェデレーション サービス製品チームと Microsoft Windows Live ID チームは、ここで説明したようなさまざまなシナリオを実現するための強力な統合機能を開発中です。この統合作業は、ADFS と Windows Live ID サービスの将来のリリースの一部として使用可能になる予定です。

まとめ
ADFS のおかげで、A. Datum はさまざまな組織との信頼関係を築くことができました。これにより、A. Datum は複数のユーザー アカウントの作成と管理の問題を軽減し、さらにユーザーが使いやすいサービスを提供できるようになります。

Jack Couch は、軍事情報の分野でキャリアをスタートさせ、過去 10 年余りの間は、高度なセキュリティを必要とする政府機関や Fortune 500 の企業に情報セキュリティに関する専門知識を提供してきました。最近まで、Jack は Microsoft Windows Vista のセキュリティ リリース マネージャを担当していました。現在は、大企業のセキュリティ強化を専門とするセキュリティ コンサルティング会社 DeepIntel のマネージング パートナーを担当しています。Jack の連絡先は jack@deepintel.com (英語) です。


編集者のメモ
深呼吸する
Howard Dierking

私はこれを、 シアトル発東海岸行きの機内で書いています。ニューヨークのオフィスで数日過ごした後、テキサスでのカンファレンスに向かいます。そこから、いくつかの会議に出席するためレッドモンドに戻り、最後にバルセロナで開催される Tech•Ed EMEA に出発します。カンファレンス シーズンはまさに真最中です。2007 年も終わりに近づいているということです。どうやってこんなにも早くここまで来たのでしょうか。これは、開発者としての今年を振り返り、2008 年に何が起こるかを考える良い機会です。
2007 年はいくつかの異なるテクノロジを経験しましたが、AJAX および Microsoft .NET Framework 3.0 という 2 つの主要なテーマが出現しました。AJAX は、ASP.NET AJAX Extensions と共に最高の Web アプリケーション開発プラットフォームとして発表されました。Extensions ライブラリは、ブラウザで実行する JavaScript コードとアプリケーション サーバーで実行するビジネス ロジックの接続にかかわるほとんどの詳細の抽象化を可能にするクライアント ライブラリおよびサーバ ライブラリの両方を導入しました。また、ライブラリは、スクリプトの動作と既存のコントロールを動的に関連付ける新しいメカニズムを確立し、クライアントとサーバーの両方でローカライゼーション機能などの機能を提供しました。
.NET Framework 3.0 は、アプリケーションの構築方法を既に劇的に転換させている 3 つのテクノロジを導入しました。Windows Presentation Foundation (WPF) は、グラフィック カードの進歩を利用し、Web アプリケーション開発者によって長く愛用される、宣言的でコンテナベース モードのユーザー インターフェイス要素作成を Windows 開発者に提供して、Win32 の従来のメッセージポンプ型のクリッピング矩形アーキテクチャを裏返しに利用しました。Windows Communication Foundation (WCF) は、分散アプリケーション作成のためのさまざまなメソッドを 1 つの抽象層の下に獲得しました。さらに Windows Workflow Foundation (Windows WF) は、宣言的なワークフロー モデリング言語と実行エンジン (以前はプラットフォーム サービスとしてではなく、追加アプリケーションとしてのみ使用可能だった機能) を追加しました。
.NET Framework 3.0 と AJAX の巨大さを考えたとき、開発者として圧倒された気分になるのは疑いようがありません。Windows WF や WCF のようなテクノロジは、リッチ ドメイン パターンなどの従来のアプリケーション アーキテクチャ パターンに重大な意味をもたらす可能性があります。たとえば、これらすべてのオブジェクト指向の実践を断念し、リモート サービス間で簡単に受け渡しのできる論理サービス クラスとデータ トークン クラスのペアにクラス階層を分類するというのは、現在のベスト プラクティスでしょうか。長時間実行されるワークフローの単体テストのベスト プラクティスとはなんでしょうか。部分的なレンダリングに対し、AJAX スクリプト サービス メソッドを使用する最適な時期はいつでしょうか。
そして、もしこれらの質問があまり重大でなければ、2008 年はトレンドが加速すると考えられます。MIX 2007 以来、Silverlight は Web アプリケーション開発においてこれまでになく豊富なブラウザベースのユーザー操作を可能にし、世界を心酔させる態勢を整えています。さらに、Visual Studio 2008 の LINQ と ADO.NET Entity Framework (Visual Studio の直後にリリースされます) のリリースにより、私たちのデータ アクセスに対する考え方は、重要なパラダイム シフトを迎えることになります。また、リリースされたすべての優れたプラットフォーム テクノロジと共に、アプリケーション層で実行する SharePoint を含む 2007 Office system は機能拡張を続け、高度に統合されたビジネス向け機能アプリケーションを提供します。
もし皆さんが筆者と同じく、どこで息抜きをしたらよいか迷っている場合は、引き続き見守ってください。筆者の旅程と同様、すぐに片付くようには思えません。しかしながら、"MSDN Magazine" ではいつものように全力を尽くします。皆さんが情報を選別して最高の開発者をめざせるよう、ときには楽しみながら、手助けをしていきます。—Howard

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Derek Del Conte、Beatriz de Oliveira Costa、Chris Davis、Steve Fox、Matt Gibbs、Lee Holmes、Chris Houser、John Justice、Matt Steele、Nenad Stefanovic および Stephen Toub です。


ツールボックス
ライブ チャット、コード変換、マルチ モニタ、その他の機能
Scott Mitchell

コンテンツ
Live ヘルプを Microsoft ASP.NET Web サイトに追加する
Visual Basic と C# のコードを相互にすばやく変換する
複数のデスクトップ モニタを管理する
設計パターンを楽しく学ぶ

Live ヘルプを Microsoft ASP.NET Web サイトに追加する
最近まで、私はある商品を探すためにハイキング用品の Web サイトを見ていました。何分か探して見つからなかったときに、私は販売員とチャットができるページへのリンクがあることに気付きました。リンクをクリックすると新しいウィンドウが開き、Chad という男性の販売員とチャットできるようになりました。私が探している商品について説明すると、数分でその商品へのリンクが表示されました。このようなライブ チャット機能は、インターネット上で段々と一般的なものになってきています。チャットは、電子メールよりも便利で、電話サポートよりも費用効率の高い顧客サービスです。
ほとんどのライブ チャット システムはサービスとして販売されています。"ライブ チャット" リンクを表示するためには HTML を少しだけみなさんのサイトに追加します。顧客サービスのオペレータは、サービス プロバイダのサイトに接続し、顧客を待機します。このアプローチの利点は、みなさんのサイトへのインストールや保守が必要ないということです。ただし、通常、こうしたモデルは顧客または同時間帯に勤務するオペレータの数に応じて金額が変わります。つまり、ライブ チャットの価格はビジネスの規模によって大幅に上昇するのです。
チャット システムの実装と価格設定をさらに制御する必要がある場合は、Corporate Web Solutions の .netLIVEHELP Version 2.0 を試してみてください。.netLIVEHELP は、サービス ベースのシステムとは異なり、一連の Microsoft® ASP.NET 2.0 Web ページおよび Web サービスとして、オペレータ用のデスクトップ アプリケーションと共に販売されています。顧客やオペレータの数に関係なく、Web サイトごとに均一の価格です。.netLIVEHELP のサイトへの接続は比較的簡単です。ASP.NET ファイルや Web サービスを格納したフォルダを参照する仮想ディレクトリを作成するだけです。"ライブ チャット" ボタンをページに追加するのは、HTML を 1 行追加するのと同じくらい簡単です。
顧客がチャット セッションを開始すると、ソフトウェアは呼び出しの負荷を最小限に抑えて会話をオペレータにルーティングします。会話の内容は、顧客のブラウザ、IP アドレス、サイト上で参照された Web ページなどのメトリックスとしてログに記録されます。
私は .netLIVEHELP のオペレータと管理者インターフェイスに感銘を受けました。オペレータは、顧客が自分のコンピュータにルーティングされたら、点滅しているシステム トレイ アイコンのデスクトップ アプリケーションを使用してサインオンします。システム トレイ アイコンをクリックすると、スペル チェックやテキストの書式設定などの機能を搭載したチャット ウィンドウが開きます。オペレータは、同時に複数の顧客とチャットすることができます。また、訪問中の顧客の現在と過去のチャットおよび顧客が参照したページを追跡することもできます。さらに、外部アプリケーションのデータを統合して、たとえば、オペレータが顧客のショッピング カートを表示できるようにすることも可能です。
管理者は、オペレータと同じ機能を使用できますが、さらに統計情報を表示することもできます。この統計情報は、特定のデータ範囲について会社全体でまとめたり、オペレータ単位で調査したりすることができます。この統計情報には、過去のチャットと進行中のチャットの総数、チャットの平均時間、平均待機時間などのメトリックスが含まれます。
価格 : 1 Web サイト ライセンスあたり 395 ドル
dotnetlivehelp.com
顧客サービスをさらに便利にするライブ チャット (画像を拡大するには、ここをクリックします)

Visual Basic と C# のコードを相互にすばやく変換する
使用されているプログラミング言語に関係なく、すべての Microsoft .NET Framework アプリケーションでは共通型システムとランタイムを共有するため、.NET プログラミング言語間の違いはほとんどありません。この言語の同等性によって、開発者の学習曲線を減らし、同じ言語で作成されたかどうかにかかわらず .NET コンポーネントのシームレスな統合が可能になります。Visual Basic® や C# などのよく使われている .NET プログラミング言語は、.NET ランタイムから見ると同等ですが、どちらか 1 つの言語だけを使い慣れた開発者から見ると必ずしもそうとは言えません。そのような言語の壁が、作業時にコードや従来のアプリケーションを継承する場合、または選択した言語以外のコードについて書籍や Web で学習する場合に制限となる可能性があります。
最適なソリューションは Visual Basic と C# の両方に慣れることですが、それが必ずしも可能というわけではありません。次に最適なオプションは、Tangible Software Solution の Instant C# Version 2.8 や Instant VB Version 2.0 などの自動化されたコード コンバータを使用することです。Instant C# は Visual Basic のコードを C# に変換し、Instant VB は C# のコードを Visual Basic に変換します。これらの製品ではコード、ASP.NET ページ、指定のフォルダ内のすべてのコード、または Visual Studio プロジェクト全体を変換できます。
Visual Basic と C# を同等の中間言語にコンパイルするときは、各言語に固有の "構文糖" が存在します。Instant C# と Instant VB には、変換時にこれらの違いを処理する方法を指定するためのオプションがあります。たとえば、Visual Basic のコードを C# のコードに変換するときは、Instant C# を使用して、Visual Basic の関数 (InStr、Mid、StrComp など) を .NET Framework の関数に変換するか、または変換せずに Microsoft.VisualBasic 名前空間をインポートするかを指定できます。同様に、生成される C# のコードで、Visual Basic の vbCrLf キーワードを System.Environment.NewLine または "\r\n" に変換するかどうかを指定することもできます。また、コードの書式設定オプションを使用すると、単一行のブロック ステートメントに中かっこを付けるかどうか、また付ける場合は左中かっこをその行で使用する必要があるかを指定できます。
最も強力な構成オプションは、変換後の置換の設定です。これにより、正規表現パターンと対応する置換文字列を無制限に指定できます。Instant C# と Instant VB では、変換後のコードにこれらの置換を適用します。
Visual Basic と C# の基礎は同じですが、100% 確実な変換を妨げる、わずかですが大きな違いがあります。ただ、さいわいにも Instant C# と Instant VB では、Microsoft が提供するコード サンプルにおいて 99.9% を超える変換率を実現しています。
価格 : Instant C# と Instant VB はそれぞれ 179 ドル
tangiblesoftwaresolutions.com

複数のデスクトップ モニタを管理する
開発者の生産性を向上させるための最も費用効率の高い方法の 1 つとして、モニタをもう 1 台支給するという方法があります。多くの場合、開発者は 2 つのウィンドウの情報を同時に処理する必要のある作業に直面します。実行中のアプリケーションとデバッガ ウィンドウなどはその典型的な例です。ワークステーションに 2 台 (またはそれ以上) のモニタを用意しておくことは、そのような作業を行ううえで非常に重要です。これは、絶えず Alt キーと Tab キーを押して切り替えを行わずに同時に 2 つの画面で作業できるためです。
Windows® では長年にわたってマルチ モニタをサポートしていますが、Windows シェルは実際には複数の画面の処理に対応していませんでした。最大の難点は、タスク バーを使用できるのが 1 台のモニタに限られるため、すべてのモニタ上のすべてのアプリケーションのアイコンがそこに集められることです。その結果、アプリケーションのタスク バー アイコンとそれを実行するモニタとの間に視覚的な関係がなくなるため、タスク バーが煩雑になり、特定のアプリケーションの検索が面倒になる可能性があります。
さいわいなことに、マルチ モニタ環境での使用に特化したサードパーティ製のウィンドウ管理ツールがあります。たとえば、Realtime Soft の UltraMon Version 2.7.1 などです。UltraMon Smart Taskbar では、タスク バーをすべての画面に拡張し、アプリケーションのアイコンを適切なタスク バーに配置します。また、UltraMon では、2 つのボタン ([Maximize to desktop] (デスクトップに最大化) と [Move window] (ウィンドウの移動)) を各ウィンドウの [最小化]、[最大化]、[閉じる] のボタンの横に追加します。[Maximize to desktop] (デスクトップに最大化) は、すべてのモニタにまたがってウィンドウを最大化します。[Move window] (ウィンドウの移動) は、クリック 1 回でウィンドウを別のモニタに移動させます。
その他にも、壁紙をすべてのモニタに拡張する機能や、すべてのモニタにわたってスクリーン セーバーを起動する機能が用意されています。また、UltraMon には Windows スクリプト ホストを通じてアクセス可能な API があります。
UltraMon は、マルチ モニタの機能を大幅に強化する軽量型の手頃なプログラムです。2 台以上のモニタをご使用の場合は、ぜひ UltraMon をお試しください。
価格 : 39.95 ドル
realtimesoft.com/ultramon
タスク バーをすべての画面に拡張する UltraMon Smart Taskbar
タスク バーをすべての画面に拡張する UltraMon Smart Taskbar (画像を拡大するには、ここをクリックします)

設計パターンを楽しく学ぶ
どこの書店でも、設計パターンに関する本が並べられています。設計パターンは、ソフトウェア設計に共通の問題を解決するためにソフトウェア アーキテクトが使用する一般的かつ高度なソリューションです。問題は、ほとんどの書籍が主要なパターンを退屈にも 1 つ 1 つ採り上げていて、それぞれをいつどのように使用するかについての説明には重点を置いていないという点です。
注目すべき例外は、Eric Freeman 氏と Elisabeth Freeman 氏著作の『Head First デザイン パターン』(O'Reilly 発行、2004 年) です。この書籍では、きわめて重要なすべての設計パターンについて説明しています。退屈な文字だけの説明のページは省き、興味深いリスト、質問と回答のセクション、さまざまな図、詳しい注釈付きのコードを掲載しています。また、サンプルも通常とは異なります。『Head First デザイン パターン』では、従業員や製品関連のサンプルではなく、ピザ、鴨、チョコレート、ガムボール マシンなどが登場するおかしなアプリケーションを使用して、特定の設計パターンをいつどのように使用するかを示しています。
本のレイアウトは漫画のようですが、役立つ情報がたくさん掲載されています。『Head First デザイン パターン』では、すべてのシステム アーキテクトが直面したことのある共通の問題 (新しい機能を既存のオブジェクト階層に追加する方法など) の観点から設計パターンを紹介しています。鴨のシミュレータ アプリケーションをサンプルとして採り上げ、懸念事項や課題を見ていきます。次に、戦略パターンおよびそのパターンを実装して課題に対処する方法について詳しく説明します。後半の章では、その他の重要なパターン (デコレータ、オブザーバ、ファクトリ、シングルトン、反復子、アダプタなど) を示します。
ソフトウェアの設計時には、同じ種類の課題が繰り返し発生します。設計パターンは、これらの問題に取り組むために繰り返し使用できる実証済みの方法をアーキテクトに提供します。『Head First デザイン パターン』を読むと、最も役立ついくつかのパターンとその適用方法を楽しく簡単に学ぶことができます。
価格 : 44.95 ドル
oreilly.com

ご意見やご質問は、

Scott toolsmm@microsoft.com.

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


CLR 徹底解剖
信頼できる .NET コードを記述する
Alessandro Catorcini および Brian Grunkemeyer and Brian Grunkemeyer

コードのダウンロード : : CLRInsideOut2007_12.exe (156 KB)
Browse the Code Online
コンテンツ
ランタイム障害の検討
エラーの処理
ホスト プロセスのリサイクル
ミラー化プロセス
プロセスの一部のリサイクル
AppDomain とは
状態の破損
共有状態を検出する
エスカレーション ポリシー
エスカレーションに対する弾力性
信頼できるコードを記述する
信頼性のレベルを選択する
クリーンアップ コード
制約された実行領域
信頼性のコントラクト
問題をハードに緩和する
厳密な信頼性コントラクトを使用する場合
SafeHandle
ロックを使用する
ハードおよびソフトの OOM 状態

信頼できるということは、頼りになる、予測可能であるということを意味しますが、ソフトウェアの世界で信頼できるという場合、コードの信頼性が高いと見なされるためには、他にも主な特性が存在します。
ソフトウェアには、内部および外部に中断が生じても動作を続ける弾力性が必要です。つまり、以前に認識された一貫性のある状態に復元する方法を記憶する機能などにより、回復可能でなければなりません。また、ソフトウェアは予測可能で、適切なタイミングで予期されたとおりのサービスを提供するようになっている必要があります。さらに、ソフトウェアは中断が許されません。つまり、サービスが変更やアップグレードの影響を受けないようにする必要があります。本番稼動の準備が整っていることも重要で、バグの数が最低限に抑えられ、更新プログラムの数もわずかで済む状態になっている必要があります。これらの条件を満たしてはじめてソフトウェアは真に信頼できると認められます。
信頼できるコードの主な特性は、ソフトウェアの全体的なアーキテクチャ、ソフトウェアを実行する OS、アプリケーションの開発に使用するツール、構築に使用するフレームワークなど、各種の要因に応じて異なります。弾力性はすべての層で必要な特性であり、アプリケーションの弾力性は、そのアプリケーションの最も弱いリンクと同程度にしかなりません。
Microsoft® .NET Framework ベースのアプリケーションの場合を考えてみましょう。.NET Framework ベースのアプリケーションは、ネイティブ環境では存在しない (IL コードの JIT コンパイルなどの処理) または開発者の直接制御下にある (メモリ管理などの処理) 一定の処理をランタイムにデリゲートします。信頼性の点では、プラットフォーム自体が固有の障害点をもたらすことで、そのプラットフォーム上で実行するアプリケーションの信頼性に影響を及ぼすことがあります。このような障害が発生する可能性がある箇所、およびより信頼性の高い .NET ベース アプリケーションを作成する方法を理解することが重要です。

ランタイム障害の検討
一定の例外イベントは、いつでも、どのコード セクションでも起こり得ます。これらのイベント (非同期例外と呼ばれる) には、リソース枯渇 (メモリ不足やスタック オーバーフロー)、スレッドの中止、アクセス違反があります。マネージ コードの実行では、アクセス違反はランタイムで発生します。
アクセス違反が発生するということは、共通言語ランタイム (CLR) の実装時の重大なバグが公開されていて、修正の必要があるということを意味するため、ここで取り上げる価値はあまりありません。ただし、リソース枯渇とスレッドの中止については詳細に分析する価値があります。
理論的には、リソース枯渇はランタイムで適切に管理され、アプリケーション コードの続行には影響しないと思われます。しかし、実際はそう単純ではありません。
まずは、一般的なサーバー アプリケーションでのメモリ不足 (OOM) イベントの処理方法を検討してみましょう。ASP.NET や Exchange Server 2007 など、きわめて高い可用性が求められるサーバー アプリケーションでは、AppDomain とプロセス リサイクルによって高可用性を実現しています。オペレーティング システムは、プロセスによって使用されるメモリやその他の多くのリソースをクリーンアップするきわめて高度な機構を備え、この処理はプロセスの終了時にすべて自動的に行われます。
クライアント シナリオでは、わずかなメモリ割り当てでもエラーになるほどメモリが逼迫すると、大量のスラッシングやページングが原因でシステム全体の応答性が損なわれます。こうなると、多くの場合、ユーザーはリカバリ コードを実行するよりも、リセット ボタンやタスク マネージャに手を伸ばします。ある意味では、本来、ASP.NET や Exchange 2007 が自動的に行う処理を手動で行うというのがユーザーの最初の反応です。
OOM によって、実行中のコードに特に問題が生じない場合もあります。同じコンピュータ上で実行される別のプロセス、またはプロセスを実行する別の AppDomain が使用可能なリソース プールを占有しているためにリソースの割り当てに失敗する場合もあります。この場合、コードの実行時にいつでも起こり得て、実行中のコードの外部の、実行中のコードとは無関係の環境要因に依存するという点で、リソース枯渇は非同期であると考える必要があります。
この問題は、ランタイムがメモリを割り当てて固有の動作に関連する処理を実行するという事実によってさらに悪化します。ランタイムで発生し、制約されたリソース環境ではエラーになる可能性がある割り当ての例を以下に示します。
  • ボクシングおよびアンボクシング
  • クラスが最初に使用されるまでの遅延クラス読み込み
  • MarshalByRef オブジェクトのリモート処理
  • 一定の文字列操作
  • セキュリティ チェック
  • メソッドの JIT 処理
これは、リソースの割り当てが必要になる可能性があるランタイムの数多い内部処理のほんの一部ですが、特定の割り当てエラーの結果を予測および緩和することが現実的ではない理由を理解するのに役立つはずです。
非同期例外の 1 つであるスレッドの中止には、特殊な役割があります。スレッドの中止は、OOM や SO のようなリソース枯渇によるエラーではありませんが、いつでも起こり得るエラーです。スレッドを別のスレッドから中止した場合、中止されたスレッドのどこで例外が発生するかは完全にランダムです。
スタック オーバーフローにも特有の性質があります。スタック空間はスレッドごとに予約され、頻繁にコミットされるため、常にリソースの競合を回避できるようにしておく必要があります。ところが、これには問題があります。各アプリケーションにどの程度のスタックがあれば十分かを予測するのは推理ゲームのようなものです。OS によって 1 スレッドあたりのスタック領域は制限されます。アンワインドの問題によりスレッドのスタック領域が不足すると、Structured Exception Handling (SEH) で例外が発生するという問題もあります。また、再入や再帰が行われると、メソッドに必要なスタック領域の上限が計算できなくなることがあります。
つまり、OutOfMemoryException、StackOverflowException、および ThreadAbortException がいつ発生するかを予測することは現実的には不可能だということです。そのため、大規模なシステムで非同期例外から回復するためのバックアウト コードを記述することは現実的ではありません。
ランタイムが非同期例外に対する弾力性を保証できない場合、アプリケーションの信頼性を確保する役割はどこで担うのかという疑問が起こるかもしれません。前述した ASP.NET の例がその答えのヒントになります。アプリケーション コードは一般的な同期例外を処理しますが、非同期例外は処理できません。非同期例外はホスト プロセスで処理する必要があります。ASP.NET の場合、メモリが既知のしきい値以上に消費された場合にプロセス リサイクルをトリガするロジックが記述されています。
もう少し洗練された方法では、SQL ServerTM 2005 などのホストで CLR ホスティング API を使用して、トランザクションを実行しているマネージ スレッドを中止してロールバックするかどうか、AppDomain をアンロードするかどうか、またはサーバー上のすべてのマネージ コードの実行を中断するかどうかを判断します。アプリケーションの既定ポリシーはホスト プロセスの強制終了ですが、この方法を選択して拡張する方法、またはこの動作をオーバーライドする方法がいくつかあります。CLR ホスティング API の詳細については、2006 年 8 月号の「CLR 徹底解剖」コラム (msdn.microsoft.com/msdnmag/issues/06/08/CLRInsideOut) を参照してください。

エラーの処理
この辺で、この問題を解決する鍵が見つかったのではないでしょうか。弾力性のあるアプリケーションとは、機能を分離して、他の箇所に影響を与えずにエラー箇所を単体で収めることができるアプリケーションです。これまでに、弾力性のあるマネージ アプリケーションの作成に成功した実証済みのモデルが 3 つあります。
最初のモデルは、プロセスをエラー単位としてマネージ コードの実行を自由に終了および開始できる 1 つまたは複数のワーカー プロセスに分離するモデルです。
2 つ目は、並列で動作する 2 つの冗長プロセスで処理を行い、1 つをアクティブにし、もう 1 つを休止状態にしておくモデルです。エラーが発生した場合、休止状態のプロセスが処理を引き継ぎ、別の休止状態のプロセスを起動して、別のエラーが発生した場合のバックアップにします。
3 つ目は、AppDomain をエラー単位として、プロセスがマネージ コードまたはランタイムで発生したエラーの影響を受けないようにするモデルです。
では、これらの 3 つの方法を詳細に検討し、実装および各種設計方法のコストを分析していきましょう。

ホスト プロセスのリサイクル
まず、リソース枯渇が生じると、CLR をホストするプロセスが破損する可能性があるという事実を大前提として、処理を 1 つまたは複数の子プロセスに隔離する方法で構築されたシステムが、ワーカー プロセスの有効期間を管理する役割を果たすマスター プロセスで監視されているとしましょう。このような場合には、信頼性の高いシステムを実現する低価格で効率的なソリューションがあります。システムの弾力性には欠けますが、十分に回復可能であり、動作も予測可能です。
この方法は、ASP.NET の Web 要求などのように独立したステートレスな要求を大量に処理する場合、および処理の実行を分離する場合に最適です。非同期例外が発生すると、ワーカー プロセスは消滅し、処理中の要求にサービスが提供されなくなるため、要求を再送信する必要があります。再送信する作業が高コストで長期間にわたる場合は、コストが高くなりすぎる可能性があり、この方法は適していません。
それでも、マネージ コードを実行する信頼性の高いサービスを利用するための最も低コストな方法であることに変わりありません。ランタイムの既定の動作を有効利用する方法です。

ミラー化プロセス
IT 企業では、ローカル ディスク ドライブからサーバー全体にいたるまで、高度に冗長性を利用しています。ディスクやサーバーで障害が発生すると、1 台目と同期している 2 台目のディスクまたはサーバーが、迅速に処理を引き継ぎます。
同様の方法をプロセスにも用いることができます。同一コンピュータ上でプロセス インスタンスを 2 つ実行し、各インスタンスが同じ入力を受け取り、同じ出力を生成するソフトウェアを設計できます。プロセスの全インスタンスに影響する再現性のあるバグではなく、1 つのプロセスの一時的な障害でメイン プロセスにエラーが生じるような環境では、このモデルを選択すると、一定の弾力性が得られます。このシナリオでは、トランザクションされたストアを使用して、失敗した要求が確実にロールバックされるようにする必要があります。
米国航空宇宙局 (NASA) では、スペース シャトルのコンピュータにこのようなモデルを採用しています。生死に関わる操作をコンピュータに委ねる場合には、一定の冗長性は不可欠です。
実際、NASA は、このシナリオでコンピュータを 2 台しか使用していなかったために問題に直面したことがあります。このときは、2 台のコンピュータの結果が一致しないという問題が発生しました。果たしてどちらの結果が正しいのか。コンピュータが 2 台しかない場合、1 台で障害が発生したときにどちらが正しいかがわかりません。そこで、NASA は 3 台目のコンピュータを追加しました。これなら、2 台のコンピュータの結果が一致し、1 台の結果が不一致な場合、結果が不一致なコンピュータが故障していると判断できます。
ただし、このレベルの冗長性では、3 台のうちの 1 台が故障すると、コンピュータが 2 台しかなくて、2 台目で障害が発生した場合にどちらが正しいかわからないという状態に戻ってしまいます。そこで、NASA は 4 台目を追加し、その後、関係者を説得して 5 台目も追加しました。2 つのミラー化プロセスで非クラッシュ障害を解決するのは生易しくありません。
この方法のもう 1 つの問題は、複数のプロセスを同一コンピュータ上で実行し、1 つのプロセスがシステムレベルのリソースを使い尽くした場合、同時にもう 1 つのプロセスで同じリソースが必要になる可能性が十分にあるということです。そのために、予備プロセスで最初のプロセスと同様のエラーが発生する可能性も出てきます。同じリソースをめぐって 2 つのプロセスが競合することもあり得ます。

プロセスの一部のリサイクル
弾力性を高めるには、プロセスを破棄して再起動したり、別のプロセスにフェールオーバーするだけでは不十分です。アプリケーションのどの部分で障害が発生しているかを突き止め、その部分をリサイクルすることが重要です。そのためには、アプリケーションのプロセスのさまざまな部分をリサイクル可能なチャンクに分離する必要があります。処理はステートレスに行うか、トランザクション システムを使用して書き込みが発生しないようにする、またはバックアウトされるようにする必要があります。また、プロセスの一部をリサイクルする際、使用されているすべてのリソースを解放する必要があります。
長期的なサーバーの場合は、状態の破損と一貫性の欠如の問題を考慮します。一貫性は複数の異なる層に適用可能です。単純なリンク リストの一貫性がとれていても、複雑なデータ構造には他にも不変性が必要です。リンク リストのすべての要素をハッシュ テーブルに値として保存する必要があるという不変性がある場合、リンク リストの一貫性は、アプリケーションの一貫性を意味しません。そのため、わずかでも不変性が損なわれる可能性があれば、問題にする必要があります。非同期例外が発生した場合、状態はどの程度まで破損している可能性があるでしょうか。また、この破損に対してサーバーの弾力性をどのように確保したらよいでしょうか。
アプリケーションをリサイクル可能なチャンクに分割するには、処理を切り離す必要があります。非同期例外が発生した場合、状態の破損が生じている可能性があります。プロセス全体のリサイクルを避けるには、失敗した処理セットや関連するすべての状態情報など、プロセスの一部をカプセル化する必要があります。

AppDomain とは
アプリケーション ドメイン (略称 AppDomain) は、マネージ コードを分離するサブプロセス単位です。多くのアセンブリは AppDomain に読み込むことが可能です。AppDomain をアンロードすれば、多くの場合はアセンブリもアンロードできます。
各 AppDomain は、静的変数の固有のインスタンスを取得します。スレッドは AppDomain の境界を越えることが可能ですが、.NET Remoting またはこれと同様の技術がなければ、AppDomain 間の通信を開始することはほとんど不可能です。AppDomain には、コードを保持するには比較的堅固な境界があります。
スレッド上で非同期例外が発生した場合、どの程度の破損が存在するか、およびその破損レベルをどのような方法で回復可能であるかを判断する必要があります。

状態の破損
状態の破損には 3 つのバケットがあります。1 つはローカル状態です。ローカル状態には、特定のスレッドのみで使用されるローカル変数およびヒープ オブジェクトが含まれます。2 つ目は共有状態です。共有状態には、静的変数に格納されたオブジェクトなど、AppDomain 内のスレッド間で共有されるすべてが含まれます。キャッシュは、多くの場合にこのカテゴリに属します。3 つ目は、プロセス全体、コンピュータ全体、コンピュータ間の共有状態です。ファイル、ソケット、共有メモリ、分散ロック マネージャがこのカテゴリに属します。
非同期例外で破損し得る状態の量は、スレッドが現在変更中の状態の最大量です。スレッドがいくつかの一時オブジェクトを割り当て、他のスレッドに公開していない場合、破損する可能性があるのはその一時オブジェクトのみです。ただし、スレッドが共有状態に書き込む場合、その共有リソースは破損する可能性があり、その他のスレッドも破損状態になる可能性があります。この状態を放置しておくことはできません。この場合、AppDomain 内の他のすべてのスレッドを中止してから AppDomain をアンロードしてください。このようにして、非同期例外は AppDomain に昇格し、AppDomain をアンロードして、破損している可能性がある状態を確実に破棄します。データベースなどのトランザクションされたストアでは、この AppDomain のリサイクルはローカル状態および共有状態の破損に対して弾力性をもたらします。

共有状態を検出する
スレッドが共有状態を変更しているかどうかを確認するのはそう簡単ではありません。多くのコードでは、スタック上のローカル変数の値がローカルで初期化されているかどうか、パラメータとしてメソッドに渡されたかどうか、単純に静的変数に格納されたオブジェクトを参照しているのかどうか (または静的変数からアクセス可能であるかどうか) は明確ではありません。
共有状態の変更に対する微妙な要求は同時実行領域から発生します。静的変数を編集するたびに、コードはほぼ必ずロックを保持します。そのため、スレッドごとにロック カウントを保持すると、スレッドが共有状態を編集している可能性があるかどうかについてのエスカレーション ポリシーがわかります。共有状態からの読み取り時に状態の破損が発生しなければ、たいていロックが取得されることは事実ですが (遅延初期化が行われないことが前提)、この悲観的なヒューリスティックは私たちがこれまでに見つけた最も厳密なもので、ユーザーに対して容認できないレベルの規律を要求しません。
ユーザーは、一部の共有状態を自動編集する場合にのみ安全であるインターロック操作を安全に使用できます。操作が成功したかどうかを確認するのは簡単です。実際に操作が行われたかどうかを見ればよいのです。

エスカレーション ポリシー
CLR のエスカレーション ポリシーはもう少し高度です。私たちは、スレッドを中止するときに、ユーザー・コードでクリーンアップを行うことができないかと考えてみました。そのために、スレッドを中止するとき、および AppDomain をアンロードする前に、CLR で finally ブロックおよびファイナライザの実行を試行します。とはいえ、任意のクリーンアップ コードを実行したいというユーザーの要望とホストの可用性要件との兼ね合いの問題があります。finally ブロックおよびファイナライザの実行に一定のタイムアウトを設定し、適切な時間内に終了しなければ、単純に処理を中止します。
難しいのは、プロセス全体、コンピュータ全体、またはコンピュータ間の状態にアクセスしている最中に非同期例外が発生した場合にどのように弾力性を保持するかという問題です。これについては、後で詳しく説明します。

エスカレーションに対する弾力性
エスカレーション ポリシーは、コード (ユーザーが記述したコードとライブラリ作成者が記述したコードの両方) にも制限を課します。SQL Server では、一定の厳しい制限付きで、CLR を使ってマネージ コードにストアド プロシージャを記述できます。スケーラビリティ、信頼性、およびセキュリティを考慮して、SQL Server のユーザー コードでスレッドを起動または終了するべきではありません。また、共有状態を最小限に抑えるか、完全に回避する必要があり、一定の種類の OS リソースに対するアクセスを許可しないことも必要です。ただし、比較的信頼性が低いことが多いユーザー コードの代わりに、.NET Framework などの信頼されたライブラリには、これらのリソースに対するアクセスを許可する必要があります。
CLR はコードのアクセス セキュリティを防御の最前線に配置し、ユーザー コードに与えるアクセス許可セットを調整します。ところが、CLR には関連するすべてのリソース タイプに対するアクセス許可は指定されていません。このため、CLR は HostProtectionAttribute 属性を定義します。この属性は、スレッドを強制終了する機能を持っているなどのプログラミング モデル上の懸念があるメソッドのマーキングに使用できます。
ユーザー コードに対するこれらの制限はきわめて有効です。というのも、(その他の制限と共に) ユーザー コードからの OS リソースへの直接アクセスを制限することにより、ユーザー コードはこれらのリソースの使用状況を追跡する責任から解放されるからです。
プロセスのリサイクルでは、プロセスを強制終了するたびに、OS はプロセスが使用していたコンピュータ全体のリソースのすべてを解放します。真に弾力性のある AppDomain のリサイクルを行うには、AppDomain のアンロードにも同レベルの保証が必要です。AppDomain はプロセス内の 1 単位にすぎないため、リソースへのアクセスを提供するマネージ ライブラリは、オペレーティング システムでプロセス終了時に行われるクリーンアップ機能と AppDomain のアンロード要求とのギャップを埋める必要があります。

信頼できるコードを記述する
AppDomain のアンロードはクリーンである必要があります。これは、非同期例外に対する弾力性のあるライブラリを記述するための基本原則です。固有の一貫性を保証する SQL Server のようなトランザクションされたホストの場合、ユーザー コードおよびライブラリ コードに関する他のすべての懸念 (正確性、パフォーマンス、保守性) よりも、ホストの可用性を保証する機能が優先されます。ユーザー コードにクラッシュを引き起こす可能性があるバグが存在する場合、サーバーは、処理の続行が可能で、時間の経過によりパフォーマンスが低下しない限り、稼動を続けます。
AppDomain のクリーンなアンロードと慎重なリソース管理により、ユーザー コードは CLR のエスカレーション ポリシーに対抗することが可能になり、リソースの一貫性確保やリーク防止のために実行する機会を与える必要があるコードを識別することができます。多くの場合、非同期例外は既に発生しているか、不可避です。これらは、ユーザー コードが障害に対する弾力性を備えるために必要なツールです。ライブラリを作成するうえで最も重要なことは、SafeHandle を使用することです。また、使用可能なその他の機能を理解して、SafeHandle を使用する理由を熟知することも重要です。

信頼性のレベルを選択する
すべてのコードの重要性が同等なわけではありません。特定のコード ブロックでどの程度の弾力性が必要かを考える必要があります。ここで説明している方法を実践すると開発コストが増加する可能性があるので、優れた設計者たるためには、個々のコード ブロックにどの程度の弾力性が必要かを判断する必要があります。
まず、電源障害が発生した場合にコードでどのような処理を行うかを考えてください。手始めとして、すべてのコードが電源障害の発生後も再起動して正常に動作できるようにしておく必要があります。クライアント アプリケーションが処理に失敗し、データ破損が起こっても、最低限、バックアップを起動できる状態にしておかなければなりません。
電源障害が発生しても、メール サーバーの電子メールが消失しないように設計することは、はるかに困難な問題です。また、原子力発電所を制御するソフトウェアは、電源障害に対する耐性を備えている必要があり、基本的な生産性アプリケーションなどと比べてはるかに高い弾力性が求められます。アプリケーションがこの連続体のどこに存在するかを確認することは何も難しいことではありませんし、弾力性にどの程度投資するかを決定するよい判断材料になります。
意外にも、多くのクライアント アプリケーションにおいて、その答えは余計なことをしないということです。非同期例外を克服せよというのは、多くのクライアント アプリケーションにとって過大な要求です。たいていの場合、プロセスを強制終了して再起動すれば十分です。Windows Vista® 再起動マネージャ API を使用する場合、この方法はクライアント アプリケーションがクラッシュしたときに失われる状態を少なく抑えるのに役立ちます。Outlook® が好例です。Outlook 2007 が Windows Vista 上でクラッシュした場合、回復してすべてのウィンドウを適切な場所に再表示することができます。メッセージの作成中に Outlook がクラッシュした場合、メッセージ全体が失われるわけではなく、最後の数分に行った入力のみが失われます。
ライブラリの場合、コードを実行する最も活動的なホストによって信頼性レベルが決まります。プロセスをリサイクルするホストでライブラリが使用されている場合、信頼性の要件は AppDomain をリサイクルするホストほど高くありません。ただし、この記事で説明している方法のいずれかを使用せずに自前のライブラリでリソースにアクセスし、AppDomain をリサイクルするホストで自前のコードを使用する場合、自前のライブラリが最終的にホストに障害を引き起こす可能性があります。

クリーンアップ コード
try/finally ブロックおよびファイナライザを使用してリソースをクリーンアップする必要があります。まず、これらのツールは、状態の適度な一貫性を回復する単純な方法を開発者に提供します。Try/finally ブロック (および C# の using ステートメントのような言語固有のキーワード) を使用すると、ユーザー コード内の確定的な場所でリソースが確実にクリーンアップまたは破棄されるため、正確性とパフォーマンスが向上します。
ただし、この方法には、finally ブロックのコードが完了することを CLR で簡単に保証できないという問題があります。CLR はスレッドの中止を防ごうとしますが、finally ブロックで実行中にいつでもリソース枯渇が生じて非同期例外が発生する可能性があります。
特定のファイナライザが完了することを保証する方法もありません。また、CLR のエスカレーション ポリシーは、finally ブロックが無限ループに入った場合や、無限にブロックされた場合に、ホストに finally ブロックおよびファイナライザでスレッドを中止することを許可しています。

制約された実行領域
制約された実行領域 (CER) は、コードの一貫性を維持するために役立つ信頼性プリミティブです。これは、限定された量の処理の進行またはオブジェクトの変更を取り消す機能の保証に力を入れる場合に最適です。制約された実行領域は、ランタイムからユーザー コードを実行するベストエフォート型の試行です。CER を使用して、ランタイムは CLR から誘発された障害を予測可能な場所に浮上させます。これは、ユーザー コードが実行されることを保証するものではありません。CER を妖精の粉のようにまき散らして、ユーザー コードを魔法のごとく動かすことはできません。しかし、厳密な制約に従ってコードを記述すれば、ユーザー コードが正常に完了する確率は高くなります。
CER を公開する形式は 3 つあります。
  • ExecuteCodeWithGuaranteedCleanup を使用する。この形式を使用すると、try/finally ブロックでスタック オーバーフローが発生しません。
  • RuntimeHelpers.PrepareConstrainedRegions 呼び出しの直後の try/finally ブロックとして公開する。この場合、try ブロックは制約されませんが、この try に対応するすべての catch、finally、および fault ブロックが制約されます。
  • クリティカル ファイナライザとして公開する。この場合、CriticalFinalizerObject のすべてのサブクラスが、オブジェクトのインスタンスを割り当てる前に、事前に準備されたファイナライザを持ちます。
(特殊なケースとして、SafeHandle の ReleaseHandle メソッドがあります。これは、サブクラスを割り当てて SafeHandle のクリティカル ファイナライザから呼び出す前に事前に準備される仮想メソッドです)。
ユーザー コードが実行される機会を得るために、CLR はユーザー コードを事前に準備します。これは、ユーザー メソッドの静的に発見可能な呼び出しグラフを JIT コンパイルすることを意味します。CER 内でデリゲートを使用する場合、デリゲートの対象メソッドを事前に準備する必要があります。この処理は、なるべくメソッドの RuntimeHelpers.PrepareMethod() を呼び出して行ってください。また、ngen を使用する場合、メソッドに PrePrepareMethodAttribute を設定すれば、JIT で制御する必要がなくなります。この事前準備を行えば、CLR によってメモリ不足の例外が誘発されなくなります。
CLR は CER 上のスレッドの中止を無効にします。また、CLR のホスト (たとえば、CLR の COM ホスティング インターフェイスで CLR を起動するネイティブ アプリケーションなど) がスタック オーバーフローを克服する必要がある場合にのみ、CLR は一部のスタックを検査します。ランタイムのバグ、ヒープの破損、およびハードウェアの問題を防ぎ、CLR で誘発されるすべての非同期例外をユーザー コードから排除する必要があります。

信頼性のコントラクト
筆者が、CLR で誘発される障害とその他の障害を区別していることにみなさんはお気付きかもしれません。システムの各層で一貫性のビューが異なるため、エラーはあらゆるシステムのすべてのレベルで発生する可能性があります。項目の一覧を並べ替えた順番で維持するアプリケーションを考えてみましょう。List<T> のような並べ替えられていないコレクションを使用する場合、ユーザー コードは次のようになります。
public static void InsertItem<T>(List<T> list, T item)
{
    // List<T> isn't sorted, but the app relies on it being sorted.
    list.Add(item);
    list.Sort();
}
ここで、List<T> に対するすべてのメソッドが奇跡的に常に成功しているとします。架空の ReliableList<T> を使用してこの動作を示します。Add でリストのサイズを増やす必要がなく、Sort ルーチンが常に (ジェネリック型の T で比較演算子および CompareTo メソッドを間接的に呼び出す場合でも) 動作するとします。Add の呼び出しが完了した後、Sort を呼び出す前に InsertItem で ThreadAbortException が発生した場合、やはり一貫性の問題が生じます。
ReliableList<T> の観点から見ると、リストには完全に一貫性があります。リストの内部データ構造は、コレクションに追加項目が中途半端に追加されていないという点で完全に正常な状態にあります。そこで、何の問題もなくリストの使用を続行できます。ところが、アプリケーションの観点から見ると、リストを並べ替える必要があるため、この不変性は破壊され、メソッドはこの問題から回復するように記述されていません。
このシナリオは、一貫性がシステムのさまざまなレベルで存在することを示す一例です。これは、信頼性のコントラクト (非同期例外が発生した場合の破損の程度を示す大よその機構) によってユーザーに指示されます。この場合、メソッドでリストが破壊されます。Add メソッドの呼び出し時に OutOfMemoryException が発生する可能性を考慮するのであれば、次のように、このメソッドにエラーの可能性があることを示すマークを設定する必要があります。
[ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)]
public static void InsertItem<T>(ReliableList<T> list, T item)
信頼性のコントラクトは CER 内で呼び出されるすべてのコードで必要であり、主に、エラーの可能性が予想されるかどうかを開発者が確認するためのドキュメントの役割を果たします。ここでは、信頼性のコントラクトでパラメータの破損と this ポインタを示すもっとよい方法もありましたが、実際のところ、これらは InsertItem メソッドの作成者とメソッドを呼び出す側のユーザーとの信頼性に関する情報のやりとりを開始することを目的としています。この場合、InsertItem<T> を呼び出すユーザーは、エラーが発生した場合はインスタンスが破損する可能性があるということを知らされるため、事態の緩和策が必要だということがわかります。

問題をハードに緩和する
制約された実行領域には、InsertItem などのコードの問題を緩和する方法があります。選択肢は複雑さによって異なり、方法によっては、バージョン間でコードが大幅に変更されると機能しない場合もあります。
最も明確な手法は、問題を理解して回避することです。このサンプルでは、最初からリストに十分な容量を割り当てるなど、障害から修復可能な場所に割り当てを行うことが可能です。これにより、あらゆる割り当ての障害が浮上します。ただし、この方法を使用するには、InsertItem<T> と List<T> について、および両者の問題の発生の仕方を理解している必要があります。
また、この方法ではスレッド中止の問題は解決しません。リストは相変わらず並べ替えされない可能性があります。その場合、CER を使用して、finally ブロックが常に並べ替えられるようにすることができます。並べ替えが保証されることにより、Add でエラーが発生してもリストの一貫性は保証されます。この方法には、前述の例の架空の ReliableList<T> の Sort メソッドに対する厳密な信頼性保証が必要です。それさえあれば、図 1 のようなコードを作成して、InsertItem の信頼性コントラクトの一貫性保証をより確実にすることができます。
Figure 1 より厳密な一貫性保証
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
 public static void InsertItem<T>(ReliableList<T> list, T item)
 {
     RuntimeHelpers.PrepareConstrainedRegions();  // CER marker
     try
     {
         // Add can fail with an OutOfMemoryException.
         // That's fine, since the list didn't change.
         list.Add(item);
         // Consider a failure here, perhaps from calling a second method.
     }
     finally
     {
         // Restore consistency by guaranteeing the list is sorted.
         list.Sort();
     }
 }
ReliableList<T> は実在しないため、この例の実際の実装ではこれらの方法は機能しないということを覚えておいてください。並べ替えのアルゴリズムの性質により、このようなコードの作成は困難です。Sort およびおそらく T の CompareTo メソッドで使用する比較演算子の信頼性も確保する必要があります。
ところが、このリサイクル モデルと同様の方法が他にもあります。パフォーマンスへの影響を許容できる場合は、データをコピーして、そのコピーを変更し、一貫性のあるデータ構造を公開することができます。その 1 つの方法として、メソッド シグネチャをリスクに曝す方法を次に示します。
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public static void InsertItem<T>(ref List<T> list, T item)
{
    // Forward progress
    List<T> copy = new List<T>(list);  // expensive copy operation
    copy.Add();
    copy.Sort();

    // Expose the results
    list = copy;
}
ここでは実際の List<T> を使用しますが、データをコピーするとパフォーマンスへの大幅な影響が生じることに注意してください。また、この例のように ref パラメータを使用することは、現実の解決策としては望ましくありません。ただし、厳密な信頼性コントラクトでは、多くの場合に「状態を破損しないこと」が望ましいと指定されています。
もう 1 つの方法として、処理を進行し、エラーが発生した場合にバックアウト コードを提供することができます。これは変更を迂回してトランザクションを作成し、トランザクションの変更をロールバックするコードを指定するのと同様です。

厳密な信頼性コントラクトを使用する場合
前述の例では、厳密な信頼性コントラクトを効率的に実装するのが困難であることを示しました。CER を使用するということは、どこででも起こり得る障害に備える必要があるため、まるでロケット科学を扱うようなものです。注釈付きの try/finally ブロックとして公開された CER の場合、try ブロックは CER ではないため、複数の割り当て処理さえも非同期例外によって中断される可能性があります。そのため、多くのユーザーには CER は適しません。ホストのエスカレーション ポリシーを使用するか、クライアント アプリケーションの場合であれば、アプリケーション全体をリサイクルする方がよいでしょう。
とはいうものの、CER が特に必要になるのは、共有メモリ セグメントなど、コンピュータ全体またはプロセス全体の状態を操作する場所においてです。このような場合のビジネス要件では、多くの場合、コード内のどこで電源障害が発生しても回復するようにアプリケーションを記述することが要求されるため、CER を記述するだけではなく、アプリケーションの重要な部分全体の一貫性に十分に注意を払う必要があります。このような複雑さがあるために、夏季インターンに原子力発電所の制御ソフトウェアを作成させることが憂鬱になるのです。

SafeHandle
非同期例外が存在する環境で、メソッドから値が確実に返されるのだろうかという疑問をお持ちかもしれません。確実に値を返すことはできません。IntPtr を返す CreateFile のようなハード化されていない P/Invoke メソッドを呼び出してローカル変数に戻り値を割り当てると、2 つの異なるマシン命令が生成され、スレッドは 2 つのマシン命令間で中止される可能性があります。このような場合、IntPtr を使用して指定した OS ハンドルは根本的に信頼できません。ただし、解決策はあります。
ファイル、ソケット、イベントなどの OS リソースにアクセスすると、そのリソースのハンドルが取得されますが、最終的にはそのリソースを解放する必要があります。SafeHandle を使用すると、割り当て可能なリソースを確実に解放することができます。これを保証するために、SafeHandle は複数の CLR 機能を 1 つにまとめます。
ユーザーは、SafeHandle のサブクラスを定義し、ReleaseHandle メソッドを実装します。このメソッドは SafeHandle のクリティカル ファイナライザから呼び出される CER です。自作の ReleaseHandle メソッドが CER のすべての規則に従っていることを前提にして、ネイティブ リソースのインスタンスを正常に割り当てることができれば、ReleaseHandle メソッドは実行の機会が与えられることが保証されます。
SafeHandle には重要なセキュリティ機能もあります。まず、他のスレッドがアクティブに使用しているハンドルを別のスレッドから解放できないようにすることにより、ハンドル リサイクルの攻撃を防ぎます。また、ハンドルとそのハンドル固有のファイナライザを使用して、クラス間の微妙な競合状態を防ぎます。SafeHandle も P/Invoke マーシャリング層と統合されます。ハンドルを返すメソッドやハンドルを使用するメソッドの場合、そのハンドルを SafeHandle のサブクラスに置き換えることができます。たとえば、CreateFile は SafeFileHandle を返し、WriteFile は SafeFileHandle を第 1 パラメータとして受け取ります。

ロックを使用する
ロックは適切なオブジェクト型で取得する必要があります。Monitor.Enter(Object) の呼び出しに拡張され、finally ブロックで Monitor.Exit(Object) の前に呼び出される C# の lock キーワードを考えてみましょう。ロックには厳密な ID の検知が必要です。アンボックス値型は Object として渡すたびにボックス化されるため、適切ではありません。ただし、CLR は一定の場合に AppDomain の境界を超えて一定の型を共有します。String、Type オブジェクト、CultureInfo インスタンス、およびバイト配列のロックを取得すると、AppDomain の境界を超えたロックが取得される場合があります。また、MarshalByRefObject のサブクラスに対するロックをそのクラス実装の外部から取得すると、適切な AppDomain の実オブジェクトではなく、その透過プロキシに対するロックが取得される (つまり、適切なロックが取得されない) 場合があります。ネイティブに記述したコードの場合、非同期例外が発生すると、ロックを解放するための finally ブロックが実行されず、他のスレッドが無限にブロックされる可能性があります。
作業者がメモリ モデルおよび同時実行に精通していて、より適切なロックが要求された場合以外、固有のロックを定義しないでください。警告可能な待機や、ハイパースレッド CPU に対するスピン方法のような明白な弱点の他に、ロックは CLR のエスカレーション ポリシーと連携する必要もあります。それには、各スレッドがロックを保持しているかどうかを追跡する必要があります。Thread.BeginCriticalRegion および EndCriticalRegion は、サードパーティのロックを取得および解放する場合の CLR の識別に役立ちます。

ハードおよびソフトの OOM 状態
メモリ不足エラーに対する AppDomain のアンロード以外にも緩和方法があります。MemoryFailPoint クラスは、メモリ割り当てが失敗するかどうかの予測を試行します。X メガバイトのメモリに対する MemoryFailPoint (X は、1 つの要求を処理するために予想される追加ワーキング セットの上限) を割り当てます。次に、要求を処理し、MemoryFailPoint に対する Dispose を呼び出します。
メモリが不足している場合、コンストラクタは InsufficientMemoryException をスローします。これはソフト OOM の概念を表すために使われる別の例外タイプです。アプリケーションでこの例外を使用し、使用可能メモリに基づいて固有のパフォーマンスをスロットルすることができます。例外は、メモリが実際に割り当てられる前 (破損が発生する前) にスローされます。この時点では共有状態は破損していないため、エスカレーション ポリシーを開始する必要はありません。
MemoryFailPoint は、メモリの物理ページの予約またはコミットのためにはメモリを予約しません。そのため、この方法は確実ではありません。プロセス内の他のヒープ割り当てと競合する可能性があります。ただし、この方法では、内部のプロセス全体の予約カウントを保持して、プロセス内で MemoryFailPoint を使用するすべてのスレッドを追跡します。この方法を利用すれば、フレームワークのエスカレーション ポリシーの起動が必要になるハード OOM の頻度も軽減可能だと思われます。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Alessandro Catorcini および Brian Grunkemeyer は、CLR チームのリード プロジェクト マネージャです。Visual Studio 2005 では、ホスティング API 層および CLR の SQL Server 2005 への統合を担当しました。

Brian Grunkemeyer は、1998 年から .NET Framework のソフトウェア設計エンジニアとして Framework クラス ライブラリの大部分や、ジェネリック、マネージ コードの信頼性、バージョン管理の検討事項などの横断的な機能の実装に携わってきました。


データ ポイント
WPF でのデータ バインド
John Papa

コードのダウンロード : : DataPoints2007_12.exe (161 KB)
Browse the Code Online
コンテンツ
データ バインドの特性
単純なバインドを作成する
バインド モード
バインドのタイミング
XML にバインドする
オブジェクトのバインドと DataTemplate
データを並べ替える
今後の発展

これまでの説明で、Windows® Presentation Foundation (WPF) を使用すると堅牢なユーザー インターフェイスが簡単に設計できるということが多くの方におわかりいただけたと思います。しかし、WPF に強力なデータ バインド機能があることにはおそらく気付かれていないでしょう。WPF では、Microsoft® .NET Framework コード、XAML、またはその両方を組み合わせてデータ操作を実行できます。コントロール、パブリック プロパティ、XML、またはオブジェクトにバインドすることにより、データ バインドを今まで以上にすばやく、柔軟かつ簡単に行うことができます。それでは、選択したデータ ソースへのコントロールのバインドを開始する方法について見てみましょう。

データ バインドの特性
WPF データ バインドを使用するには、ターゲットとソースが必ず必要です。バインドのターゲットは、DependencyProperty から派生したアクセス可能な任意のプロパティまたは要素にすることができます。たとえば、TextBox コントロールの Text プロパティなどです。バインドのソースは、他のコントロールのプロパティ、共通言語ランタイム (CLR) オブジェクト、XAML 要素、ADO.NET DataSet、XML フラグメントなどの任意のパブリック プロパティにすることができます。バインドを正しく行うために、WPF には 2 つの特殊なプロバイダ (XmlDataProvider および ObjectDataProvider) が用意されています。
それでは、WPF データ バインドのしくみについて見てみましょう。また、使い方の応用例も紹介します。

単純なバインドを作成する
まず、TextBlock の Text プロパティを ListBox の選択項目にバインドする方法を示す簡単な例から始めましょう。図 1 のコードは、6 個の ListBoxItem が宣言されている ListBox を示しています。コード例の 2 番目の TextBlock には、Text というプロパティ (XML 子要素 <TextBlock.Text> を含む XAML プロパティ要素の構文で指定されます) があります。このプロパティには TextBlock のテキストが格納されます。Text プロパティは、<Binding> タグを使用して ListBox の選択項目へのバインドを宣言します。Binding タグの ElementName 属性は、TextBlock の Text プロパティのバインド先のコントロールの名前を示します。Path 属性は、バインド先の要素 (ここでは ListBox) のプロパティを示します。このコードの結果は、ListBox で色を選択すると、その色の名前が TextBlock に表示されます。
Figure 1 コントロールへの基本的かつ冗長なバインド
<StackPanel>
    <TextBlock Width="248" Height="24" Text="Colors:"
        TextWrapping="Wrap"/>
    <ListBox x:Name="lbColor" Width="248" Height="56">
        <ListBoxItem Content="Blue"/>
        <ListBoxItem Content="Green"/>
        <ListBoxItem Content="Yellow"/>
        <ListBoxItem Content="Red"/>
        <ListBoxItem Content="Purple"/>
        <ListBoxItem Content="Orange"/>
    </ListBox>
    <TextBlock Width="248" Height="24" Text="You selected color:" />
    <TextBlock Width="248" Height="24">
        <TextBlock.Text>
            <Binding ElementName="lbColor" Path="SelectedItem.Content"/>
        </TextBlock.Text>
    </TextBlock>
</StackPanel>
図 1 に示すコードを少し変更して、データ バインド用に省略した構文を使用できます。たとえば、TextBlock の <Binding> タグを次のコードに置き換えてみましょう。
<TextBlock Width="248" Height="24"
    Text="{Binding ElementName=lbColor,
    Path=SelectedItem.Content}" />
この構文 (属性構文) では、TextBlock の Text 属性の内部にデータ バインド コードが記述されています。基本的に、Binding タグは中かっこで囲まれており、その内部に属性が記述されます。

バインド モード
ここでは、前の例を利用して、TextBlock の背景色を ListBox で選択した色にバインドします。次のコードでは、Background プロパティを TextBlock に追加し、属性バインド構文を使用して ListBox で選択した項目の値にバインドします。
<TextBlock Width="248" Height="24"
Text="{Binding ElementName=lbColor, Path=SelectedItem.Content,
    Mode=OneWay}"
x:Name="tbSelectedColor"
Background="{Binding ElementName=lbColor, Path=SelectedItem.Content,
    Mode=OneWay}"/>
ユーザーが ListBox で色を選択すると、その色の名前が TextBlock に表示され、TextBlock の背景色が選択した色に変更されます (図 2 を参照)。
図 2 2 つのターゲットへのソースのバインド
図 2 2 つのターゲットへのソースのバインド
前の例で、Mode 属性を OneWay に設定するステートメントに注目してください。Mode 属性は、ソースとターゲット間のデータ フローを決定するバインド モードを定義します。OneWay 以外にも、OneTime、OneWayToSource、TwoWay の 3 つのバインド モードがあります。
上記のコードに示すように、OneWay バインドを使用すると、ソースが変更されるたびにソースからターゲットにデータが送信されます。この例では明示的に指定しましたが、OneWay バインドは TextBlock の Text プロパティの既定のバインド モードであるため、個別に指定する必要はありません。OneWay バインドと同様に、OneTime バインドではデータをソースからターゲットに送信します。ただし、データが送信されるのは、アプリケーションの起動時または DataContext が変更され、その結果、ソースで変更通知がリッスンされない場合だけです。OneWay バインドや OneTime バインドとは異なり、OneWayToSource バインドではデータをターゲットからソースに送信します。最後に、TwoWay バインドではソース データをターゲットに送信します。ターゲット プロパティの値に変更があった場合は、値がソースに返されます。
前の例では、ListBox の選択項目が変更されるたびにソース (選択された ListBoxItem) を TextBlock に送信する必要があったため、OneWay バインドを使用しました。TextBlock での変更を ListBox に戻したいわけではありません。もちろん、ユーザーが TextBlock を編集する方法はありません。TwoWay バインドについて説明する場合は、このコードに TextBox を追加し、そのテキストと背景色を ListBox にバインドして、Mode を TwoWay に設定します。ユーザーが ListBox で色を選択すると、その色が TextBox に表示され、背景色が変更されます。ユーザーが TextBox に色 (水色など) を入力すると、ListBox の色の名前が更新されます (ターゲットからソース)。続いて、ListBox が更新されたために、ListBox の SelectedItem プロパティにバインドされるすべての要素に新しい値が送信されます。つまり、TextBlock の色も更新され、そのテキスト値が新しい色に設定されます (図 3 を参照)。
図 3 実行中の TwoWay バインド
図 3 実行中の TwoWay バインド
TextBlock (OneWay) と TextBox (TwoWay) の ListBox へのバインドに使用したコードを次に示します。
<TextBlock Width="248" Height="24"
    Text="{Binding ElementName=lbColor, Path=SelectedItem.Content,
    Mode=OneWay}" x:Name="tbSelectedColor"
    Background="{Binding ElementName=lbColor, Path=SelectedItem.Content,
    Mode=OneWay}"/>

<TextBox Width="248" Height="24"
    Text="{Binding ElementName=lbColor, Path=SelectedItem.Content,
    Mode=TwoWay}" x:Name="txtSelectedColor"
    Background="{Binding ElementName=lbColor, Path=SelectedItem.Content,
    Mode=OneWay}"/>
TwoWay モードを OneWay に戻すよう変更した場合、ユーザーは TextBox の色を編集できますが、変更した値が ListBox に返されることはありません。
適切なバインド モードを選択することが重要です。読み取り専用のデータをユーザーに表示する場合は、OneWay を使用します。ユーザーがコントロールのデータを変更し、その変更をデータ ソース (DataSet、オブジェクト、XML、またはバインドされた別のコントロール) に反映できるようにする場合は、TwoWay バインドを使用します。データ ソースのデータをターゲットにバインドせずにユーザーがデータ ソースを変更できるようにする場合は、OneWayToSource が適切な選択肢です。画面の読み込み時に、読み取り専用のコントロールのデータの状態を表示する場合は、OneTime バインドを使用しました。OneTime バインドを使用すると、ユーザーはフォームと対話し、データ ソースの値が変更され、バインドされたコントロールは変更されないため、読み取り専用の一連のコントロールがデータにバインドされました。これにより、行われた変更を比較する方法がユーザーに提供されました。また、OneTime バインドはソースが INotifyPropertyChanged を実装しない場合に適しています。

バインドのタイミング
前の例では、TwoWay バインドによって、ListBox の選択した ListBoxItem に TextBox がバインドされました。この TextBox から ListBox へのデータ フローが発生するのは、TextBox がフォーカスを失った場合です。データをソースに戻すイベントを変更するために、ソースをいつ更新するかを定義するバインド プロパティである UpdateSourceTrigger の値を指定できます。UpdateSourceTrigger に設定できる値は、Explicit、LostFocus、PropertyChanged の 3 つです。
UpdateSourceTrigger を Explicit に設定すると、BindingExpression.UpdateSource メソッドがコードから呼び出されない限り、ソースは更新されません。LostFocus (TextBox コントロールの既定値) に設定すると、ターゲット コントロールがフォーカスを失ったときにソースが更新されます。値 PropertyChanged は、ターゲット コントロールのバインドされたプロパティが変更されるたびにターゲットがソースを更新することを示します。この設定は、バインドをいつ発生させるかを指定する場合に役立ちます。

XML にバインドする
XML やオブジェクトなどのデータ ソースへのバインドも便利です。図 4 は、データ ソースとして使用される色の埋め込まれた一覧を格納する XmlDataProvider の例を示しています。XmlDataProvider は、XmlDataProvider タグに埋め込まれるか、または外部の場所で参照されるファイルに格納される、XML ドキュメントやフラグメントへのバインドに使用できます。
Figure 4 XmlDataProvider
<StackPanel>
<StackPanel.Resources>
<XmlDataProvider x:Key="MoreColors" XPath="/colors">
    <x:XData>
        <colors >
            <color name="pink"/>
            <color name="white"/>
            <color name="black"/>
            <color name="cyan"/>
            <color name="gray"/>
            <color name="magenta"/>
        </colors>
    </x:XData>
</XmlDataProvider>
図 4 に示すように、埋め込まれた XML コンテンツは、XmlDataProvider の内部の <x:XData> タグ内に配置する必要があります。XmlDataProvider には x:Key 値を指定して、データ バインドのターゲットから参照できるようにしてください。XPath 属性が "/colors" に設定されることに注意してください。この属性は、データ ソースとして使用される XML コンテンツのレベルを定義します。これは、ファイルまたはデータベースに含まれる可能性のある大きな XML 構造へのバインドの場合や、バインド先のデータがルート要素ではない場合に非常に役立ちます。
XmlDataProvider は、コンテキスト固有のリソースの内部に配置可能なリソースです。図 4 に示すように、XmlDataProvider は StackPanel のコンテキスト内でリソースとして定義されます。つまり、XmlDataProvider はその StackPanel 内部のすべてのコンテンツで使用できます。リソースのコンテキストを設定すると、データ ソースの公開を適切な領域に制限できます。これにより、ページ内のコントロールとサポート対象のリソースの両方について適切に定義された自己完結型の領域を作成できるため、データが読みやすくなります。
リソースへのバインドの構文は、要素へのバインドの場合とは少し異なります。コントロールへのバインドでは、Binding の ElementName プロパティと Path プロパティを設定します。ただし、リソースにバインドする場合は Source を設定します。また、XmlDataProvider へのバインドを行うため、Binding の XPath プロパティも設定します。たとえば、次のコードでは、ListBox の項目を MoreColors リソースにバインドします。Source プロパティがリソースに設定され、MoreColors という名前の StaticResource として指定されます。XPath プロパティは、XML データ ソース内の <color> 要素の name 属性に項目がバインドされることを示します。
<ListBox x:Name="lbColor" Width="248" Height="56"
    IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding Source={StaticResource MoreColors},
    XPath=color/@name}">
</ListBox>
ここでは、XML が変更されないため、StaticResource を指定しました。データ ソースが変更される場合、その変更はターゲットに送信されません。DynamicResource の設定はその逆 (変更が送信されること) を示します。これは、システム テーマ、グローバル化の言語、またはフォントを参照する場合に役立ちます。DynamicResource を使用すると、これらの設定に動的にバインドされる UI 要素を通じてこれらの設定の種類を伝達できます。
XmlDataProvider は XML コンテンツの外部ソースを参照することもできます。この例では、ListBox のバインド先の色の一覧を含む colors.xml というファイルを使用します。2 番目の XmlDataProvider リソースを StackPanel に追加して、この XML ファイルを参照するようにすることができます。Source 属性を XML ファイルの名前に設定し、x:Key を Colors に設定していることに注意してください。
<XmlDataProvider x:Key="Colors" Source="Colors.xml" XPath="/colors"/>
両方の XmlDataProvider は、同じ StackPanel 内にリソースとして存在します。StaticResource の設定先の名前を変更することにより、自身をこの新しいリソースにバインドするように ListBox に指示することができます。
<ListBox x:Name="lbColor" Width="248" Height="56"
    IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding Source={StaticResource Colors},
    XPath=color/@name}">
</ListBox>

オブジェクトのバインドと DataTemplate
XmlDataProvider は XML には適切に機能しますが、オブジェクトまたはオブジェクトの一覧にバインドする場合は、ObjectDataProvider をリソースとして作成できます。ObjectDataProvider の ObjectType では、データ バインド ソースを提供するオブジェクトを指定します。また、MethodName は、データ取得のために呼び出されるメソッドを示します。たとえば、List<Person> を返すメソッド GetPersonList を持つ PersonService というクラスがある場合、ObjectDataProvider は次のようになります。
<StackPanel.Resources>
<ObjectDataProvider x:Key="persons"
ObjectType="{x:Type svc:PersonService}"
MethodName="GetPersonList"></ObjectDataProvider>
</StackPanel.Resources>
PersonService クラスと Person クラス、およびその他すべてのサンプル コードの詳細については、このコラムに付属のコードを参照してください。
その他にも、ObjectDataProvider で使用可能ないくつかのプロパティがあります。ConstructionParameters プロパティを使用すると、呼び出されるクラスのコンストラクタにパラメータを渡すことができます。また、MethodParameters プロパティを使用してパラメータを指定することもできます。ObjectInstance プロパティを使用すると、オブジェクトの既存のインスタンスをソースとして指定できます。
データを非同期に取得する場合は、ObjectDataProvider の IsAsynchronous プロパティを true に設定します。その後で、ユーザーは、ObjectDataProvider のソースにバインドされたターゲット コントロールにデータが表示されるのを待機しながら画面と対話できます。
ObjectDataProvider を追加する場合は、データ ソース クラスの名前空間を修飾する必要があります。ここでは、xmlns 属性を <Window> タグに追加して、svc ショートカットを修飾し、適切な名前空間を示すようにする必要があります。
xmlns:svc="clr-namespace:DataBindingWPF"
これで ObjectDataProvider を使用したデータ ソースの定義が完了したので、ListBox コントロールの項目をこのデータにバインドします。各 ListBoxItem には 2 行のテキストを表示します。1 行目には、Person インスタンスの FullName プロパティを太字で表示します。2 行目には、インスタンスの Title と City を表示します。XAML では、この処理は DataTemplate を使用するときわめて単純です。これにより、再利用可能なデータの視覚化の方法を定義できます。
図 5 は、完成した XAML を示しています。この XAML には、指定されたレイアウト I に Person 情報を表示するために DataTemplate が定義されています。DataTemplate が Person クラス型を参照することを示すように、DataTemplate の DataType プロパティを設定します。実際のバインドは ListBox に指定するため、DataTemplate には実際のバインドは指定しません。Binding Source を省略すると、スコープ内の現在の DataContext に対してバインドが行われます。
図 5 では、persons リソースにバインドするように ListBox の ItemsSource プロパティを設定して、データを ListBox にバインドできるようにしていますが、書式の設定は行っていません。ItemTemplate プロパティを personLayout リソースに設定すると、データは正しく表示されます。これは DataTemplate のキー名です。最終的には図 6 のような画面になります。
Figure 5 オブジェクトのバインド
<Window x:Class="DataBindingWPF.ObjectBinding"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:svc="clr-namespace:DataBindingWPF"
Title="DataBindingWPF" Height="300" Width="300">
<StackPanel>
    <StackPanel.Resources>
        <ObjectDataProvider x:Key="persons"
          ObjectType="{x:Type svc:PersonService}"
          MethodName="GetPersonList" ></ObjectDataProvider>
            <DataTemplate x:Key="personLayout" DataType="Person">
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{Binding Path=FullName}"
                            FontWeight="Bold" Foreground="Blue">
                    </TextBlock>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Path=Title}"></TextBlock>
                    <TextBlock Text=", "></TextBlock>
                    <TextBlock Text="{Binding Path=City}"></TextBlock>
                </StackPanel>
            </StackPanel>
        </DataTemplate>
    </StackPanel.Resources>
    <TextBlock></TextBlock>
    <ListBox x:Name="lbPersons"
            ItemsSource="{Binding Source={StaticResource persons}}"
            ItemTemplate="{DynamicResource personLayout}"
            IsSynchronizedWithCurrentItem="True"/>
</StackPanel>
</Window>
図 6 DataTemplate の使用
図 6 DataTemplate の使用

データを並べ替える
データを特定の方法で並べ替える場合は、直接 ObjectDataProvider にバインドするのではなく、CollectionViewSource にバインドします。これで、CollectionViewSource はデータ ソースになり、ObjectDataProvider のデータをインターセプトする中継手段として機能します。また、並べ替え、グループ化、およびフィルタの機能を提供し、データをターゲットに伝達します。
次に示す CollectionViewSource では、Source 属性が ObjectDataProvider (persons) のリソース名に設定されています。その後で、並べ替えの基準と方向を示すプロパティを指定してデータの並べ替え順序を定義しました。
<CollectionViewSource x:Key="personView"
    Source="{Binding Source={StaticResource persons}}">
    <CollectionViewSource.SortDescriptions>
    <ComponentModel:SortDescription

    PropertyName="City"
    Direction="Ascending" />
    <ComponentModel:SortDescription
    PropertyName="FullName"
    Direction="Descending" />
    </CollectionViewSource.SortDescriptions>
</CollectionViewSource>
DataContext は、コンテナ コントロール内のすべてのコントロールをデータ ソースにバインドするために使用されます。これは、同じバインド ソースを使用する複数のコントロールがある場合に非常に役立ちます。すべてのコントロールのバインド ソースを指定した場合は、このコードを繰り返し使用できます。代わりに、コントロールのコンテナの DataContext をバインド ソースに設定し、格納されているコントロールの Source 属性を省略することができます。たとえば、同じバインド ソースに明示的にバインドされた一連の TextBlock を次に示します。
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource personView},
    Path=FullName}"></TextBlock>
<TextBlock Text="{Binding Source={StaticResource personView},
    Path=Title}"></TextBlock>
<TextBlock Text="{Binding Source={StaticResource personView},
    Path=City}"></TextBlock>
</StackPanel>
コントロールの StackPanel コンテナを逆参照する DataContext にバインドされた 3 つの同じ TextBox を次に示します。
<StackPanel DataContext="{Binding Source={StaticResource personView}}" >
<TextBlock Text="{Binding Path=FullName}"></TextBlock>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
<TextBlock Text="{Binding Path=City}"></TextBlock>
</StackPanel>
コンテナで DataContext が定義されていない場合は、現在の DataContext が見つかるまで、引き続き入れ子になった次の外部コンテナが参照されます。

今後の発展
WPF データ バインドは柔軟性に富んでいて、バインド先のデータの型およびデータの管理と表示の方法を制御できます。皆さんも、この強力かつ多機能なデータ バインドを使用してすぐに作業に取りかかりたくなるでしょう。

ご意見やご質問は、

John mmdata@microsoft.com.

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


Cutting Edge
Microsoft AJAX Library の秘密
Dino Esposito

コードのダウンロード : : CuttingEdge2007_12.exe (152 KB)
Browse the Code Online
コンテンツ
Microsoft AJAX Library について
Microsoft AJAX Library に準拠したクラス
Microsoft AJAX Library の組み込みクラス
XMLHttpRequest が存在する場所
Web 要求の詳細
スクリプト サービスのプロキシ オブジェクト
部分的なレンダリング エンジン

AJAX (Asynchronous JavaScript and XML の略語) と JavaScript との間には不可分なつながりがあります。とはいえ、JavaScript は、数年前にオブジェクト指向プログラミングは Web ページには大げさすぎると考えられて基礎が設計された単純なプログラミング言語です。また、動的言語の原理はアカデミックすぎて、幅広く使われるプログラミング言語で完全に実装するのは難しいと考えられていました。
そこで、JavaScript にはあらゆる要素が少しずつ採り入れられました。JavaScript は、オブジェクト指向のセマンティクスを C# などのより完全なオブジェクト指向言語と同様の方法でサポートし、現在の Ruby や Python と同様の動的バインド機能を複数備えています。ところが、多くの Web ページで、JavaScript はテキスト、表、グラフィックを静的に表示するのではなく、ページをインタラクティブに表示するために使われています。
実際、よく見られる JavaScript の用法は、標準化された ECMAScript 言語のブラウザでの実装です。この言語を使用する主な目的は、ユーザーの操作をページ内の要素と連結する HTML ページに機能を埋め込むことにあります。ページ内の要素 (ボタンやテキスト ボックスなど) は、Document Object Model (DOM) に定義されたスキーマをモデルにしています。JavaScript を使って、表示されたドキュメント内の要素の名前変更、編集、追加、削除をプログラムで行ったり、そのような要素で発生したイベントを処理したりできます。また、新規ウィンドウを開いたり、ポップアップ表示するなどのブラウザ固有の操作を行うこともできます。または、AJAX 特有の機能として、よく使われる XMLHttpRequest オブジェクトを呼び出し、リモート URL の非同期呼び出しを行うことも可能です。
重要なのは、JavaScript も DOM も、もはやそれらだけでは現在のような Web 時代に必要なプログラミング能力を十分には満たせなくなっているということを認識することです。これらの言語が開発者に提供できるのは、最低限のツールボックス程度の機能です。優れた AJAX ページを作成するには、リッチ クライアント側の JavaScript ライブラリなど、他の機能も使用する必要があります。
AJAX の大きな利点は、クライアント側のサイトのコードをより多く実行できるため、ブラウザがユーザーの操作により迅速に応答できるということです。一般的な AJAX の操作は、マウスが要素の上に置かれたときにイメージを切り替えたり、ポップアップ ウィンドウにメニューやツール バーを表示するばかりではありません。AJAX の操作には、クライアント側のデータ検証や高度なユーザー インターフェイス機能 (フローティング メニュー、ドラッグ アンド ドロップ、ポップアップ パネル、高度な入力など) もあります。また、サーバーに要求をディスパッチする際に、ページ全体を置き換える従来のモデルをバイパスし、帯域外メカニズムでディスパッチする機能もあります。
どのような AJAX プラットフォームを使用する場合でも、豊富な JavaScript ライブラリで補完する必要があります。通常、JavaScript ライブラリは、最低でも、すぐに使えるクライアント側ユーザー インターフェイス ガジェットと、読み込まれた各ドキュメントのライフサイクルを定義するイベントを持つアプリケーション モデルを備えています。ASP.NET AJAX 1.0 の場合、および ASP.NET 3.5 と統合された AJAX プラットフォームの場合、最適な JavaScript ライブラリは Microsoft® AJAX Library です。

Microsoft AJAX Library について
Microsoft AJAX Library は 2 つの関連する部分 (JavaScript 言語拡張と基本クラス ライブラリ) で構成され、定義済みのサービスおよびツールを開発者に提供します。オブジェクトの概念は既に普及していますが、JavaScript 自体はオブジェクト指向プログラミングの 3 本柱 (継承、ポリモーフィズム、カプセル化) のすべてを完全かつネイティブに実装してはいないため、オブジェクト指向言語として示すことはできません。JavaScript では、オブジェクト プロトタイプを使用して一部の継承を取得可能であり、クロージャを使用した一部のカプセル化が可能です。そこで、Microsoft AJAX Library を使用することにより、JavaScript 言語のツールとしての機能が高まり、新しいクラスおよびプログラミング ツールの定義を行うことができます。
Microsoft AJAX Library は、JavaScript で記述され、いくつかの .js ファイルに保存された自己完結型のライブラリです。そのため、構成要素となるファイルが正しく参照されていれば、JavaScript に対応したあらゆるプログラミング環境で Microsoft AJAX Library を使用できます。Microsoft AJAX Library には、2 つの主なファイル (MicrosoftAjax.js と MicrosoftAjaxWebForms.js) があります。MicrosoftAjax.js には、Microsoft AJAX Library がサポートする言語拡張 (名前空間、インターフェイス、列挙子、継承など) が定義されています。MicrosoftAjaxWebForms.js には、部分的なレンダリング エンジンとネットワーク スタック全体が定義されています。
ASP.NET AJAX アプリケーションでは、この 2 つのファイルを直接参照または埋め込みする必要はありません。ScriptManager コントロールが自動的にこの処理を行います。
<asp:ScriptManager runat="server" ID="ScriptManager1" />
前述のコードを ASP.NET AJAX ページに追加すると、Microsoft AJAX Library .js ファイルの両方またはどちらか 1 つがクライアントにダウンロードされます。ブラウザの JavaScript の機能を非 ASP.NET AJAX アプリケーションのコンテキストで拡張する場合、必要な .js ファイルを明示的に参照する必要があります。
<head>
    <script src="microsoftajax.js"></script>
</head>
Microsoft AJAX Library のソース コードは ajax.asp.net/downloads から入手できます。これらのスクリプトは各自のページで変更できます。

Microsoft AJAX Library に準拠したクラス
従来の JavaScript プログラミングでは、関数を作成します。関数は非常に優れた要素であるため、関数の新しいインスタンスを作成し、構成して実行することができます。つまり、関数はオブジェクト指向言語のオブジェクトと同様に使用できるということです。これは JavaScript アーキテクチャの根幹であり、変更すると、多くのアプリケーションや多数の既存ブラウザを破損する大きな危険があります。Microsoft AJAX Library は、JavaScript 関数の外観および動作を従来のオブジェクトと同様にするということを最終目標として、一定の手法を施し、前提を設けています。
Microsoft AJAX Library クラスの作成にはプロトタイプ モデルを使用します。すべての JavaScript 関数にプロトタイプ プロパティがあります。プロトタイプ プロパティはオブジェクトの内部パブリック スキーマです。メソッドが関数のプロトタイプの一部として定義されている場合、この関数のすべてのインスタンスは同じメソッドのインスタンスを呼び出します。
例として、Timer オブジェクトを使用した場合を考えてみましょう。
MsdnMag.Timer = new function() {
  // This is a sort of constructor
  // for the function
  :
  this._interval = 5000;
}
Microsoft AJAX Library によれば、これはクラス定義です。ところが、ご覧のように、これは単なる関数定義です。前述のコードでは、コードをリンクしているページでさらにインスタンス化することが可能な関数オブジェクトを定義しています。
var timer = new MsdnMag.Timer();
擬似コンストラクタに定義したコードはここで実行されます。前述のコード例に従えば、関数をインスタンス化すると、内部変数 _interval が 5000 に設定されます。
では、タイマ オブジェクトにいくつかの開始メソッドおよび停止メソッドを追加するにはどうすればよいでしょうか。このプロトタイプ モデルでは、複数の独立したコード ブロックを使用してクラスを定義するように指定されています。C# などのオブジェクト指向プログラミング言語では、クラスのすべてのコードが 1 つのクラス定義 (言うまでもなく、部分クラスは除く) に含まれています。タイマ オブジェクトにメソッドを追加するには、次のように、一見関連がないように見えるコードを追加する必要があります。
MsdnMag.Timer = new function() {
  this._interval = 5000;
  this._timerID = null;
}
MsdnMag.T

        imer.prototype

.start = function(callback) {
   this._timerID = window.setTimeout(callback, this._interval);
}
次のコードをよくご覧ください。Microsoft AJAX Library プログラミングに関する重要なヒントが含まれています。まず、このコードを同等の C# のコードと比較してください。
namespace MsdnMag {
    public class Timer {
        public Timer() {
            _interval = 5000;
            _timerID = null;
        }
        protected int _interval;
        protected object _timerID;
        public void start(Delegate callback) {
            // Any code that can create a timer
            _timerID = CreateTimer(callback, _interval);
        }
    }
}
最初の着目点は、Microsoft AJAX Library に Timer オブジェクトの一部として _timerID および _interval を明示的に定義する必要がないということです。この 2 つのメンバにコンストラクタで既定値を割り当てるだけで済みます。ただし、オブジェクト メンバとしてマークするには、プレフィックスとして this キーワードを付ける必要があります。プレフィックスを付けないと、関数のプライベート メンバとして認識され、オブジェクトのプロトタイプにバインドされません。プレフィックスとして this キーワードを付けたメンバは、プロトタイプ宣言されたどのメソッドからでも呼び出し可能です。
MsdnMag.Timer 関数は、このコラムで定義したように、ASP.NET AJAX および Microsoft AJAX Library を使用するその他のプラットフォームで Microsoft AJAX Library の上に新しいクラスを作成するための推奨される方法を示します。図 1 を参照して、前述のコードと比較してください。
Figure 1 MsdnMag.Timer クラスのクロージャ
//
//  MsdnMag.Timer :: closure
//

MsdnMag.Timer = function(interval)
{
    var _interval = interval;
    var _timerID = null;
    var _userCallback = null;

    this.CallbackInternal() {
        this._userCallback();
        this.start();
    }

    this.start(callback) {
        _userCallback = callback;
        _timerID = window.setTimeout(CallbackInternal, _interval)
    }

    this.stop() {
        _userCallback = null;
        _timerID = null;
    }
}
ここでは、JavaScript オブジェクトはプロトタイプ宣言されたオブジェクトではなく、クロージャとして記述されています。2 つのモデルは多くのブラウザが備えている JavaScript エンジンでサポートされています。両者の違いは、クロージャ モデルでは、すべてのメンバを C# のクラスと同様の同一の全体的なコンテキストに定義するオブジェクトを提供するのに対し、プロトタイプ モデルでは、クラスに一種の分散スキーマを提供するという点にあります。オブジェクトのプロトタイプに接続されたすべてのメンバは、そのオブジェクトのすべてのインスタンスによって共有されます。Microsoft AJAX Library は完全にプロトタイプ モデルに従って記述されていますが、各自で固有のオブジェクトのモデルを使用することを強くお勧めします。Microsoft AJAX Library は JavaScript の上の単なる抽象層であり、JavaScript はクロージャとプロトタイプの両方をサポートしています。どちらのプログラミング スタイルを使用するかは各自で選択してください。
では、Microsoft AJAX Library を使用する利点はどこにあるのでしょうか。このライブラリは、名前空間、列挙型、デリゲート、厳密な型指定、継承などの新しい言語機能を提供する定義済みオブジェクトを追加することにより、JavaScript プログラミング環境を拡張しています。図 2 は、完全修飾された Microsoft AJAX Library オブジェクトとして記述された MsdnMag.Timer オブジェクトを示しています。サンプル タイマ クラスは、コンストラクタに最大 3 個の引数を受け取って、タイマの間隔と定期的に実行するコールバックを初期化します。タイマは、明示的に停止してコールバックをリセットしない限り、自動的に再起動されます。
Figure 2 MsdnMag.Timer のプロパティベース バージョン
//
//  MsdnMag.Timer :: prototype model
//

// Define the namespace the following class(es) belong to
Type.registerNamespace("MsdnMag");

// Class constructor:: Initializes internal members
//
// interval      :: interval of the timer in milliseconds
// callback      :: JS function to execute periodically
// resetCallback :: JS function to execute when the timer is stopped
MsdnMag.Timer = function(interval, callback, resetCallback)
{
    this._interval = interval;
    this._timerID = null;
    this._userCallback = callback;
    this._resetCallback = resetCallback;

    // Define a delegate to invoke the user-defined callback
    this._callback = Function.createDelegate(this, this._callbackInternal);
}

// Internal wrapper for the user-defined callback
function MsdnMag$Timer$_callbackInternal()
{
    // Invoke the user-defined callback
    if (this._userCallback !== null)
        this._userCallback();

    // Restart the timer if a reset-callback is provided
    if (typeof(this._resetCallback) !== "undefined" &&
        this._resetCallback !== null)
        this.start();
}

// Official end-point to start the timer
function MsdnMag$Timer$start()
{
    this._timerID = window.setTimeout(this._callback, this._interval)
}

// Official end-point to stop the timer
function MsdnMag$Timer$stop()
{
    window.clearTimeout(this._timerID);
    this._timerID = null;

    // Execute the user-defined clear function
    if (typeof(this._resetCallback) !== "undefined" &&
        this._resetCallback !== null)
        this._resetCallback();
}

// Define the public interface of the class
MsdnMag.Timer.prototype =
{
    start               : MsdnMag$Timer$start,
    stop                : MsdnMag$Timer$stop,
    _callbackInternal   : MsdnMag$Timer$_callbackInternal
}

// Register the class with the Microsoft AJAX Library framework
MsdnMag.Timer.registerClass("MsdnMag.Timer");
JavaScript 関数の呼び出しで宣言されているパラメータを省略すると、仮パラメータの実際の値は未定義の型として評価されます。そのため、null 値と未定義の型をチェックする必要があります。次のコードは、JavaScript のパラメータが null かどうかをチェックする確実な方法を示しています。
if (typeof(this._resetCallback) !== "undefined" &&
   this._resetCallback !== null)
{
   ...
}
新しいオブジェクトにパブリックの読み取り/書き込みプロパティを追加するにはどうすればよいでしょう。JavaScript では、get アクセサまたは set 修飾子が指定されている属性を、プロパティベースのプログラミング インターフェイス経由で使用可能にする手法は使えません。フィールドのような単なるパブリック データ コンテナではなく、プロパティとして使用するには、アドホック メソッドを使用してプロパティ値の読み取りおよび書き込みを行う必要があります。使用規則は get_XXX および set_XXX (XXX はプロパティ名) です。MsdnMag.Timer オブジェクトの Interval プロパティのサンプル実装を図 3 に示します。
Figure 3 MsdnMag.Timer へのプロパティの追加
function MsdnMag$Timer$get_Interval()
{
    if (arguments.length !== 0)
        throw Error.parameterCount();
    return this._interval;
}

function MsdnMag$Timer$set_Interval(value)
{
    var e = Function._validateParams(arguments, [{name: 'value', type:
        Number}]);
    if (e)
        throw e;

    this._interval = value;
}

// Define the public interface of the class
MsdnMag.Timer.prototype =
{
    get_Interval        : MsdnMag$Timer$get_Interval,
    set_Interval        : MsdnMag$Timer$set_Interval,
    start               : MsdnMag$Timer$start,
    stop                : MsdnMag$Timer$stop,
    _callbackInternal   : MsdnMag$Timer$_callbackInternal
}
渡された引数の数を get アクセサでチェックし、問題があればパラメータ数エラーを生成します。arguments は、関数呼び出しのパラメータ リストを返す JavaScript の内部配列です。
Microsoft AJAX Library Function オブジェクトの _validateParams メソッドは、開発者が型チェックやメソッドのパラメータ数のチェックを簡単に行えるようにするための拡張機能です。
var e = Function._validateParams(
      arguments,
      [{name: 'value', type: Number}]);
この関数は引数リストと、予想される引数リストを示す 2 番目の配列を受け取ります。前述のコードでは、メソッドは値の正式名を持つ 1 つの数値引数のみを受け取ることを想定しています。_validateParams メソッドは、1 つの数値要素が格納された引数配列を検証し、引数が異なる場合は例外を生成します。

Microsoft AJAX Library の組み込みクラス
Microsoft AJAX Library フレームワークを基礎にして JavaScript で新しいクラスを作成する方法がわかったところで、次はこのフレームワークに組み込むシステム クラスを検討します。図 4 に、Microsoft AJAX Library に定義されたすべての JavaScript クラスを示します。Boolean、Date、Number などの組み込みの JavaScript オブジェクトは拡張され、新しいメソッドや機能が追加されています。たとえば、Date オブジェクトには localeFormat という名前の新しいインスタンス メソッドと形式が追加されています。これらを使用してロケール カルチャ情報で日付をレンダリングします。
Figure 4 Microsoft AJAX Library の組み込みクラス

組み込みクラス | 説明
Array | ネイティブ Array オブジェクトを拡張して新しい検索メソッドを追加します。
Boolean | ネイティブ Boolean オブジェクトを拡張して解析メソッドを追加します。
Date | ネイティブ Date オブジェクトを拡張して書式設定メソッドを追加します。
Error | ネイティブ Error オブジェクトを拡張してマネージ例外オブジェクトと同様の処理を行います。また、静的プロパティを公開して定義済みのエラー タイプを割り当てます。
Function | ネイティブ Function オブジェクトを拡張して、メソッドのシグネチャを確認するデリゲートとユーティリティを追加します。
Object | ネイティブ Object オブジェクトを拡張して、型情報を追加します。
Number | ネイティブ Number オブジェクトを拡張して、解析および書式設定メソッドを追加します。
RegExp | ネイティブ RegExp オブジェクトの単純なラッパーです。
String | ネイティブ String オブジェクトを拡張して、書式設定メソッドを追加します。
Type | すべての OOP 拡張をグループ化した関数の別名です。
Sys.UI 名前空間 | 説明
Sys.UI.Behavior | 新規および既存の ASP.NET サーバー コントロールの機能拡張に使用する動作の基本を定義します。
Sys.UI.Bounds | top、left、bottom、right の各パラメータでページの領域を定義します。
Sys.UI.Control | Microsoft AJAX Library ユーザー インターフェイス コントロールの基盤を定義します。
Sys.UI.DomElement | ページ内でレンダリングされた DOM 要素のラッパー クラスです。
Sys.UI.DomEvent | DOM レベル イベント (キーストロークやマウス動作など) のラッパー クラスです。
Sys.UI.Point | ページ内の 1 点を (x,y) 座標で定義します。
基盤クラス | 説明
Sys.Component | Microsoft AJAX Library ユーザー インターフェイス コンポーネントの基盤を定義します。
Sys.CultureInfo | カルチャ情報を定義します。
Sys._Application | 現在のページのライフサイクルを示す内部クラスです。
Sys._Debug | デバッグ サービスを提供する内部クラスです。
Sys._ScriptLoader | 外部スクリプトの読み込みを処理する内部クラスです。
ネットワーク クラス | 説明
Sys.Net.NetworkRequestEventArgs | Web 要求イベントに関連付けられたデータを定義します。
Sys.Net._WebRequestManager | 実行プログラムやタイムアウトなどのグローバル パラメータを設定する Web 要求の集中マネージャを示す内部クラスです。
Sys.Net.WebRequest | 作成中の Web 要求を示します。
Sys.Net.WebRequestExecutor | 物理的に Web 要求を実行するオブジェクトを示します。
Sys.Net.WebServiceError | サービスの呼び出し時に発生した例外のラッパー クラスです。
Sys.Net.WebServiceProxy | リモート サービスの JavaScript プロキシ クラスの基盤を定義します。
Sys.Net.XMLHttpExecutor | XMLHttpRequest を使用する Web 要求実行プログラムを示します。
サービス ヘルパ クラス | 説明
Sys.Services._AuthenticationService | サーバー側の認証 Web サービスの内部ラッパー クラスです。
Sys.Services._ProfileService | サーバー側のユーザー プロファイル Web サービスの内部ラッパー クラスです。
Sys.Services._RoleService | サーバー側のロール Web サービスの内部ラッパー クラスです。このクラスを使用できるのは ASP.NET 3.5 の場合のみです。
Sys.Services.ProfileGroup | プロファイル グループに関する情報を格納します。
イベント データ クラス | 説明
Sys.ApplicationLoadEventArgs | pageLoad イベントに関連付けられたデータを定義します。
Sys.CancelEventArgs | 割り込み可能イベントのデータ オブジェクトの基盤を定義します。
Sys.EventArgs | あらゆるイベントのデータ オブジェクトの基盤を定義します。
Sys.EventHandlerList | Web 要求の実行に関連付けられたすべてのイベントを収集するヘルパ クラスです。
Sys.PropertyChangedEventArgs | Microsoft AJAX Library コンポーネントのプロパティ変更イベントに関連付けられたデータを定義します。
ユーティリティ | 説明
Sys.Serialization.JavaScriptSerializer | ページに挿入されたアドホック データを逆シリアル化するヘルパ クラスです。このクラスは、Sys.CultureInfo オブジェクトで使用されます。
Sys.StringBuilder | 文字列を連結します。このオブジェクトの機能はマネージ StringBuilder クラスとほぼ同じです。
部分的なレンダリング クラス | 説明
Sys.WebForms.PageRequestManager | 部分的なレンダリングの要求および処理を制御するルート オブジェクトです。
Sys.WebForms.PageLoadingEventArgs | pageLoading イベントに関連付けられたデータを定義します。
Sys.WebForms.PageLoadingEventArgs | pageLoaded イベントに関連付けられたデータを定義します。
Sys.WebForms.InitializeRequestEventArgs | initializeRequest イベントに関連付けられたデータを定義します。
Sys.WebForms.EndRequestEventArgs | endRequest イベントに関連付けられたデータを定義します。
Sys.WebForms.BeginRequestEventArgs | beginRequest イベントに関連付けられたデータを定義します。
Sys.UI._UpdateProgress | 更新の進行状況の処理を行うクライアント側クラスです。
Sys.UI._UpdateProgress | 更新の進行状況の処理を行うクライアント側クラスです。
var d = new Date();
d.localeFormat("dddd, dd MMMM yyyy, hh:mm:ss");
現在のロケール情報は次のコードで取得できます。
var localShortDate =
    Sys.CultureInfo.CurrentCulture.dateTimeFormat.ShortDatePattern;
var d = new Date();
d.localeFormat(localShortDate);
このコードでは、現在のカルチャに従って短い日付パターンを取得し、そのパターンを使用して日付形式を設定しています。
Sys.CultureInfo オブジェクトはどこからロケール情報を取得するのでしょう。現在のカルチャに関する情報は、スクリプト マネージャで EnableGlobalization プロパティを true に設定するたびにクライアント ページに出力されます。
<asp:ScriptManager ID="ScriptManager1" runat="server"
EnableScriptGlobalization="true" />
スクリプト マネージャで JavaScript コードが記述された後、このコードは JavaScriptSerializer クラスで解析され、Microsoft AJAX Library ランタイムと統合されます。Date オブジェクトには、任意のテキストから新しい日付オブジェクトを作成するカルチャ固有のメソッドもあります。
Number オブジェクトには、カルチャ固有の解析および書式設定を行う同様の機能があります。
Boolean オブジェクトには、"true" や "false" などの語句を認識する追加の静的解析メソッドがあります。
Array および String は、Microsoft AJAX Library の重要な拡張を受け取る組み込み型です。Array 型には、項目の追加、削除、検索、反復処理を行ういくつかの新しい静的メソッドがあります。String 型は、endsWith、startsWith、トリム、書式設定、文字列ビルダとの統合などのメソッドを追加することにより、なるべくマネージ String クラスと類似するように作成されています。
これで、すべてのオブジェクトが正確な型情報を返すようになりました。これにより、どのオブジェクトに対するクエリでも、type メソッドで正確な型がわかります。その処理方法を次に示します。
myTimer = new MsdnMag.Timer(2000, updateUI, timerReset);
alert(Object.getTypeName(myTimer));
図 5 は、alert 関数の結果を示しています。
図 5 AJAX での型情報の確認
図 5 AJAX での型情報の確認 (画像を拡大するには、ここをクリックします)
すべてのネイティブ JavaScript オブジェクトの中で、RegExp (正規表現の組み込みサポートに含まれるオブジェクト) だけは Microsoft AJAX Library で一切拡張されていません。Microsoft AJAX Library をインストールした後で使用する RegExp オブジェクトは、このライブラリをインストールする前とまったく同じです。

XMLHttpRequest が存在する場所
Microsoft AJAX Library は、より機能豊富でより高度にオブジェクト指向を採り入れた JavaScript プログラミング環境を趣旨としていますが、主に AJAX ライブラリとして認識されています。そこで当然のように、「XMLHttpRequest はどこにあるのか?」という疑問が出るでしょう。Microsoft AJAX Library のクラス階層をよく見ると、XMLHttpRequest が Sys.Net 名前空間のクラス内のネットワーク スタックに埋め込まれていることがわかります。
Microsoft AJAX Library では、すべての Web 要求が Web 要求マネージャ オブジェクトを介して渡されます。読み込み時に Sys.Net_WebRequestManager オブジェクトの一意のインスタンスが作成され、ページの有効期間中に使用されます。要求は非同期に発生しますが、マネージャは一度に 1 つの要求しか受け取ることができません。この点は Web ページと異なりますが、要求マネージャの同一インスタンスが複数の要求を同時に処理するよう求められた場合は、2 番目の要求に対して例外が発生します。時間をおいて 2 回目の要求を行う計画を立てるのは、開発者の役割です。
Web 要求マネージャは、イベント ハンドラのリストを管理し、要求のタイムアウトおよび実行プログラムの種類を定義します。defaultTimeout プロパティを使用してタイムアウトの既定値を設定してください。文字列型の defaultExecutorType プロパティを使用して既定の実行プログラムの種類を変更することもできます。既定では、実行プログラムは Sys.Net.XMLHttpExecutor クラスのインスタンスであり、要求を送信する直前にインスタンス化されます。
実行プログラムは、リモート URL に物理的に接続し、HTTP パケットを送信するコードです。AJAX では、ブラウザの XMLHttpRequest オブジェクトを使用して、最もわかりやすい方法でこの処理を実行します。Sys.Net.XMLHttpExecutor クラスは XMLHttpRequest オブジェクトをラップしているだけで、このオブジェクトのサービスを使用して HTTP パケットを転送先に送信します。
ご覧のように、実行プログラムは独立したモジュールであり、マネージャが実行プログラムを使用する場合は、必ず executeRequest メソッドおよび invokingRequest イベントと completedRequest イベントで構成される既知のインターフェイスを介します。では、Microsoft AJAX Library の Web 要求のしくみをもっと深く掘り下げてみましょう。

Web 要求の詳細
Sys.Net.WebRequest クラスは、サーバー URL に送信された処理対象の HTTP パケットを定義します。オブジェクトのパブリック プロパティは url、httpVerb、headers、body、timeout、userContext、executor です。timeout と executor は、未設定の場合、既定で Web 要求マネージャに定義された値になります。完了イベントと invoke メソッドで WebRequest オブジェクトのプログラミング インターフェイスを作成します。要求をトリガするには、invoke メソッドを呼び出します。ご覧のように、一度に 1 つの要求がサポートされ、要求マネージャに渡されて実行されます。
function Sys$Net$WebRequest$invoke() {
    if (arguments.length !== 0) throw Error.parameterCount();
    if (this._invokeCalled) {
        throw Error.invalidOperation(Sys.Res.invokeCalledTwice);
    }

    Sys.Net.WebRequestManager.executeRequest(this);
    this._invokeCalled = true;
}
要求マネージャは、パケット内の実行プログラムに対する明示的な要求があるかどうかを確認します。明示的な要求がなければ、既定の実行プログラムを使用します。次に、要求マネージャは invokingRequest メソッドを起動します。このイベントがユーザー コードで取り消されなければ、最後の手順として、接続を物理的に設定し、パケットを送信する処理が行われます。
実行プログラムは WebRequestExecutor 基本クラスから派生し、想定されるプログラミング インターフェイスを定義します。前述したように、既定の実行プログラムは XMLHttpExecutor です。このクラスでは、XMLHttpRequest ブラウザ オブジェクトのインスタンスを内部的に作成し、要求の実行に使用します。
実行プログラムは Sys.Net.WebRequest オブジェクトのプロパティであるため、Web 要求ごとに異なる実行プログラムを指定できます。Microsoft AJAX Library に定義される実行プログラムは XMLHttpExecutor のみですが、開発者が他の実行プログラムを作成することもできます。重要なのは、どのような場合にカスタム実行プログラムを作成するのが妥当で、どのような方法や選択肢があるかを知っておくことです。
追加の実行プログラムでは、動的に作成された、非表示の IFRAME を使用して、URL に返信された応答をダウンロードすることができます。XMLHttpRequest を使用する場合は、多くのブラウザで実装されている "同一生成元ポリシー" による制限を受けることに注意してください。この場合、ブラウザは、現在のページのドメインの外部にある URL に対してスクリプトで呼び出しを行うことを許可しません。XMLHttpRequest を使用する場合、たとえば、contoso1.com を contoso2.com から呼び出すことはできません。このような呼び出しを行うには、IFRAME を使用して応答を取得し、適切な JavaScript オブジェクトに解析します。
IFRAME ベースの実行プログラムを記述するには、Sys.Net.WebRequestExecutor から派生した Microsoft AJAX Library オブジェクトを使用し、このオブジェクトのすべての抽象メンバを実装する必要があります。応答を使用可能なメモリ オブジェクトに解析する場合、共通プログラミング インターフェイスでカスタム動作を行う、要求ごとに固有のパーサー コンポーネントを定義すると便利です。

スクリプト サービスのプロキシ オブジェクト
スクリプトで実行可能なサービスを呼び出すには、JavaScript ラッパー コードが必要です。ラッパー コードを使用すれば、元のサービス (従来の ASP.NET Web サービスまたは Windows Communication Foundation (WCF) サービス) とほぼ同様のパブリック プログラミング インターフェイスを提供するスクリプトベースのプロキシ クラスを生成できます。プロキシ クラスは、特定の URL にバインドされたスクリプト タグでページにリンクされます。
http://www.yourserver.com/service.asmx/js
プロキシ オブジェクトの名前は、完全修飾されたサービス クラスの名前と同じです。オブジェクトのメソッド セットも同一です。ただし、メソッドには、入力引数の標準セット以外に、追加パラメータを含む拡張シグネチャがあります。各メソッドには、ユーザー コードで呼び出しの成功/失敗を処理できる 2 つのコールバック関数と、呼び出しのコンテキストを示すオプションのオブジェクトがあります。プロキシ オブジェクトは、Sys.Net.WebServiceProxy Microsoft AJAX Library クラスの派生オブジェクトです。
図 6 に、サービス プロキシ クラスの一部を示します。パフォーマンス上の理由から、プロキシ クラスは各メソッドをコールバック モデルで非同期に実行するシングルトンになっています。このコードでは、_invoke メソッドで Web 要求を作成し、コンテンツ タイプを application/json に設定しています。これは、ASP.NET AJAX ランタイムでは必須の設定です。以下に、サンプル コードを示します。
Figure 6 Web サービス プロキシ クラス
Type.registerNamespace('MsdnMag');
MsdnMag.SimpleService = function()
{
   MsdnMag.SimpleService.initializeBase(this);
   this._timeout = 0;
   this._userContext = null;
   this._succeeded = null;
   this._failed = null;
}
MsdnMag.SimpleService.prototype =
{
    DoSomething : function(succeededCallback,
        failedCallback, userContext)
    {
        return this._invoke(MsdnMag.SimpleService.get_path(),
            'DoSomething', false, {}, succeededCallback,
            failedCallback, userContext);
    }
}
MsdnMag.SimpleService.registerClass(
       'MsdnMag.SimpleService', Sys.Net.WebServiceProxy);

MsdnMag.SimpleService._staticInstance =
    new MsdnMag.SimpleService();

MsdnMag.SimpleService.DoSomething =
    function(onSuccess,onFailed,userContext)
{
    MsdnMag.SimpleService._staticInstance.DoSomething(
        onSuccess, onFailed, userContext);
}

var request = new Sys.Net.WebRequest();
request.get_headers()['Content-Type'] =
'application/json; charset=utf-8';
タイムアウトは内部タイマで自動的に設定および制御されます。また、プロキシは、Web 要求の完了イベントのハンドラを登録します。イベントが発生すると、システム指定のコードで応答確認が行われます。問題がなければ、プロキシはユーザー定義の成功コールバックを呼び出します。問題があれば、プロキシはエラー コールバックを呼び出し、発生したエラーに関する情報を渡します。そのコード例を次に示します。
var error = response.getResponseHeader("jsonerror");
var errorObj = (error === "true");
if (errorObj)
    err = new Sys.Net.WebServiceError(false,
    result.Message, result.StackTrace, result.ExceptionType);
...
onFailure(err, userContext, methodName);
サービス メソッドの実行で未処理の例外が発生した場合、その例外に関連付けられたメッセージ、スタックトレース、およびメッセージ タイプがクライアントに返され、エラー コールバックが処理可能な WebServiceError JavaScript オブジェクトに組み込まれます。

部分的なレンダリング エンジン
部分レンダリングの主要オブジェクトは、Sys.WebForms 名前空間の PageRequestManager です。オブジェクトを初期化するコードが、1 つ以上の UpdatePanel コントロールを使用しているクライアント ページに透過的に挿入されます。このようなページのソースを表示すると、次のようなスクリプトが記述されています。
Sys.WebForms.PageRequestManager._initialize(
    'ScriptManager1', document.getElementById('form1'));
ScriptManager コントロールの ID およびフォームの ID は任意です。PageRequestManager の _initialize メソッドは、主に 2 つの処理を行います。1 つは、オブジェクトの一意のインスタンスを作成してページのライフサイクルに必要なすべてを提供する処理で、もう 1 つは、フォームの送信イベントのスクリプト ハンドラを登録する処理です。このような方法で、あらゆる種類のポストバック (ブラウザで行う通常のポストバック、クロスページ ポストバック、DOM のコマンドによるポストバック) をインターセプトする共通コードを用意します。
送信ハンドラの目的は、ブラウザの既定の操作を停止し、フォームの送信を既定の Web 要求実行プログラムで管理された AJAX 要求に変更することです。
送信ハンドラは要求の本文を作成することによってジョブを開始します。このジョブでは、フォームの入力フィールドのループ処理および適切な文字列の作成を行います。本文はブラウザで生成される本文とほぼ同じですが、要求の本文には追加パラメータがあります。
ScriptManager1=UpdatePanel1
ここでも、ID は任意です。重要なのは、ページのスクリプト マネージャの ID に、呼び出しをトリガする更新可能なパネルの ID を割り当てることです。
要求の準備が完了すると、PageRequestManager は、まずクライアント側の initializeRequest イベントを生成します。initializeRequest イベントを使用して、開発者は処理を取り消すことができます。処理を続行可能であれば、マネージャは保留中の処理を強制終了し、一度に 1 つの部分的なレンダリング処理だけをアクティブにできるようにします。その後、beginRequest イベントを生成し、要求の開始を通知します。次に、マネージャは、対応する WebRequest オブジェクトの invoke を呼び出します。ここで応答が返ってきたら、解析してエラー チェックを行い、問題がなければ pageLoading イベントを生成します。
pageLoading イベントの発生時には、応答は解析済みですが、ページの現在の DOM とはまだ結合されていません。次に、処理に関連するパネルが更新され、サーバーから送信されたその他のデータ (データ項目やポストバック スクリプトなど) が処理されます。最後に、pageLoaded イベントが生成された後、endRequest イベントが生成されます。各 PageRequestManager イベントには固有のクラスがあり、固有の情報が保持されます。

ご意見やご感想は、

Dino cutting@microsoft.com.

まで英語でお送りください。
Dino Esposito は、Solid Quality Learning の指導者であり、『Microsoft ASP.NET 2.0 プログラミング』(日経 BP ソフトプレス社発行、2006 年) の著者でもあります。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。cutting@microsoft.com (英語) で連絡を取るか、weblogs.asp.net/despos (英語) のブログに参加することができます。


テストの実行
Windows PowerShell での UI テスト自動化
Dr. James McCaffrey

コードのダウンロード : : TestRun2007_12.exe (164 KB)
Browse the Code Online
コンテンツ
Windows PowerShell を使用する
テスト スクリプトの概要
テスト用アプリケーション
カスタム コマンドレット ライブラリを作成する
カスタム コマンドレット ライブラリの使用
UI コマンドレット ライブラリの使用、変更、および拡張

比較的短い期間しか使用していませんが、Windows PowerShellTM は既にもう筆者のお気に入りのツールの 1 つとなっています。最近、筆者は Windows PowerShell には、超軽量な UI テスト自動化を記述できる小型のライブラリの作成に必要なすべての機能が揃っているということを発見しました。
今月のコラムでは、Windows® UI テスト自動化タスクを実行する Windows PowerShell のカスタム コマンドレットの小さなコレクションを作成する方法を説明します。これらのタスクには、アプリケーションやコントロールへのハンドルの取得、コントロールの操作、およびアプリケーションの状態のチェックが含まれます。ここでは、読者が Windows PowerShell、および C# 言語で Microsoft® .NET Framework P/Invoke メカニズムを使用して Win32® API 関数を呼び出すことに関する基礎知識があることを想定します。ただし、読者にこれらの基礎知識がない場合でも、ほんの少しの努力でこのコラムを理解することができるでしょう。

Windows PowerShell を使用する
超軽量な UI テスト自動化のための Windows PowerShell の使用方法の重要ポイントを説明する最も簡単な方法は、図 1 に示すような、スクリーンショットを使用する方法です。シェルの出力の最初の 2 ～ 3 行は、単に Windows PowerShell を使用することを示しています。それに続く数行では、カスタム コマンドレッドを登録中であることを示しています。登録処理は、Windows PowerShell シェルの新規インスタンスが起動されるたびに実行するカスタムの開始スクリプトによって実行されます (また、筆者の開始スクリプトでは、現在の作業ディレクトリを C:\UiautomationWithPowerShell に設定しています)。この例では、超軽量の UI テスト自動化用の次の 8 つのカスタム コマンドレットを作成、登録しました。それらは get-window、get-control、get-controlByIndex、send-chars、send-click、get-listBox、send-menu、および、get-textBox です。
図 1 Windows PowerShell での UI テスト自動化
図 1 Windows PowerShell での UI テスト自動化 (画像を拡大するには、ここをクリックします)
次に、以下のコマンドを発行して、現在のディレクトリ内の "t" で始まるすべてのアイテムの名前を表示します。
PS C:\UIautomationWithPowerShell> get-childitem t* | select-object Name | format-table -auto
図 1 の出力では、TheAppToTest というディレクトリと testScenario.ps1 というファイルがあったことを示しています。このファイルは、Windows PowerShell のテスト スクリプトです。
get-childitem コマンドは、Windows PowerShell のおよそ 130 個ある組み込みコマンドレットの 1 つです。また、これらのコマンドレットの多くにエイリアスがあります。たとえば、get-childitem コマンドレットにはいくつかのエイリアスがあります。"dir" (古い cmd.exe コマンド シェルと .bat ファイルに詳しいユーザー向け)、"ls" (UNIX 環境の使用経験のあるエンジニア向け)、"gci" (コマンド ラインで便利な簡単な省略形) と指定することができます。
筆者のコマンドは、get-childitem の出力を select-object コマンドレットへパイプします。このコマンドレットは、ファイル名プロパティとディレクトリ名プロパティだけに結果をフィルタ処理するために使用します。次に、-auto スイッチで format-table コマンドレットにその結果をパイプし、縮小表示を生成します。パイプを指定することなく、単に "get-childitem" (またはエイリアスの 1 つ) と入力し、コマンドのすべての既定の引数をそのまま使用することも、当然可能です。
コマンド ラインで .\testScenario.ps1 というコマンドを入力すると、テスト スクリプトを呼び出すことができます。ほとんどのスクリプト言語とは異なり、Windows PowerShell では、スクリプトが現在の作業ディレクトリ内にある場合でさえも、(ここで使用したような) 相対パスまたは完全パスのいずれかを使用してスクリプトへのパスを指定しなければならないことに注意してください。これは、セキュリティのためです。また、Windows PowerShell は、既定では、スクリプトの実行を許可しません。そのため、スクリプトを実行する場合は、実行を明示的に有効にする必要があります。コマンド "get-executionpolicy" を入力すると、現在のスクリプト実行ポリシーを確認できます。また、"set-executionpolicy" を入力して、引数 "remoteSigned" または "unrestricted" を指定すると、そのポリシーを変更できます。
設定後は、特定のシステムに対する実行ポリシーが Windows PowerShell シェルおよびユーザー セッションのすべての新規インスタンスに対して有効になります。筆者の開始スクリプトは、単なる通常の Windows PowerShell スクリプトですが、新規のシェルを起動したときに実際に実行することができました。このことは、現在の実行ポリシーではスクリプトの実行が許可されていることを示しています。

テスト スクリプトの概要
筆者の UI テスト自動化スクリプトには、多くのログ メッセージが含まれています。そのため、それほど苦労することなく、スクリプトの実行内容をたどることができるでしょう。まず、このスクリプトではテスト用のアプリケーションを起動します。図 1 からわかるように、単純な Windows フォームベース アプリケーションをテストしています。この特定のアプリケーションは .NET Framework を基に構築されていますが、これらの Windows PowerShell UI テスト自動化手法は、Win32 アプリケーションでも動作します。ただし、ここで示している手法は、Web アプリケーションで動作するようには設計されていません。
テスト用のアプリケーションが起動した後、スクリプトは、アプリケーション自身およびアプリケーションの子コントロールへのハンドルを取得します。次に、アプリケーションの [Search] ボタンへのユーザーによるクリックをシミュレートします。アプリケーションは、このボタン クリックにエラー メッセージ ボックスで応答します。スクリプトはこのメッセージ ボックスを見つけ、メッセージ ボックスの [OK] ボタンに対するクリックをシミュレートします。
テスト自動化の次の部分では、テキスト "Product Name" を ComboBox コントロールに送信し (アプリケーションが検索する方法を指定します)、"ad" を TextBox コントロールに送信します (検索対象を指定します)。スクリプトは再度 [Search] ボタンをクリックし、今度はアプリケーションがダミー データのストアを 1 件ずつ検索して、ListBox コントロールに一致項目を表示します。
最後に、スクリプトは、アプリケーションの状態をチェックします (この例では、ListBox コントロールおよび TextBox コントロールの内容を確認します)。テスト シナリオの合否を判定し、アプリケーションの [File] メニューの [Exit] アイテムに対するユーザー クリックを実行します。

テスト用アプリケーション
既に説明したように、テスト用のダミー アプリケーションは .NET に基づいています。アプリケーションには、TextBox コントロール、ComboBox コントロール、Button コントロール、2 つの Label コントロール、ListBox コントロールなどの基本的なユーザー コントロールがあります。筆者は、しばしば、このダミー アプリケーションを使用して、さまざまな UI テスト自動化手法を探索します。なお、このアプリケーションの完全なコードは、このコラムで参照しているすべての他のコードと共に、このコラムからダウンロードして入手できます。
UI テスト自動化を実行する場合は、プログラムで操作または調査するすべてのコントロールの名前またはインデックス コントロール番号を把握しておく必要があります。この概念に慣れていないかもしれませんが、少しの間辛抱してください。ユーザー コントロールはすべてウィンドウです。コントロールを操作するには、そのコントロールへのハンドルが必要です。一部のユーザー コントロールにはウィンドウ名がありますが、多くのコントロールにはありません。このダミー アプリケーションでは、アプリケーション自身のウィンドウ名は Form1 です。それに対して、TextBox コントロールにはウィンドウ名はありません。コントロールにウィンドウ名がある場合は、プログラムでコントロールへのハンドルを取得するためにその名前を使用できます。ただし、コントロールにウィンドウ名がない場合は、コントロールのインデックス番号を使用できます。
コントロールのインデックス番号は、明示ではなく暗黙に指定され、InitializeComponent メソッドでメイン フォームに追加される順序によって決まります。ダミー アプリケーションのソース コードを見ると、このことがわかります。
this.Controls.Add(this.listBox1);
this.Controls.Add(this.button1);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.comboBox1);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
親 Form ウィンドウ ("this") のコントロール インデックスは 0 です。そして、ListBox コントロールはコントロール インデックス 1、Button はインデックス 2、TextBox はインデックス 3、ComboBox はインデックス 4、Label2 はインデックス 5、および Label1 はインデックス 6 です。注意すべき重要な点は、コントロールの暗黙のコントロール インデックスの値は、コントロールのタブ順序プロパティと同じではないということです。
このシナリオでは、筆者はアプリケーションのソース コードにアクセスできるため、すべてのコントロールのインデックス値を特定できます。ソース コードを利用できない場合でも、Visual Studio® に付属の Spy++ ツールを使用してコントロール インデックスの値を特定できます。
現実的には、検索アプリケーションでは、多くの場合、データベース サーバーから検索結果を取得します。ただし、次のコードでは、理解しやすいように、単純な Product クラスのフォームでローカル データ ストアを使用します。
public class Product {
  public readonly string ID, Name, Price;
  public Product(string id, string name, string price) {
    ID = id; Name = name; Price = price;
  }
}
Product インスタンスのデータは、コレクションに格納され、メインの Form オブジェクトが読み込まれるときに渡されます。
private List<Product> list = new List<Product>();

private void Form1_Load(object sender, System.EventArgs e) {
  list.Add(new Product("111", "Widget", "$11.11"));
  list.Add(new Product("222", "Gadget", "$22.22"));
  list.Add(new Product("333", "Thingy", "$33.33"));
}
UI テスト自動化を実行しているため、データ ソースはあまり関係ありません。基になる結果データがどこから取得されたかにかかわらず、結果はアプリケーションの UI に反映されます。その UI の状態を検査することで、テスト シナリオの合否結果を判定できます。
すべてのアプリケーション ロジックは、検索ボタン コントロールのクリック メソッド内にあります。アプリケーションは、まずユーザーが検索条件および検索対象を指定したかどうかの基本的なチェックを行います。いずれかの項目が見つからない場合、アプリケーションは次のエラー メッセージと共に MessageBox ウィンドウを表示します。
if (comboBox1.Text == "" || textBox1.Text == "") {
  MessageBox.Show("Please enter search criteria and term", "Error");
}
listBox1.Items.Clear();
MessageBox ウィンドウはアプリケーションの Form オブジェクトの一部ではありません。これらの処理は、UI テスト自動化の基本的な手法です。アプリケーションは、(検索結果を保持する) ListBox コントロールをクリアした後、TextBox コントロールおよび ComboBox コントロールから検索条件や対象文字列を取得し、List データ ストアを調べて一致するものを探します。
string target = textBox1.Text, criterion = comboBox1.Text;

foreach (Product p in list) {
  if (criterion == "Product ID") {
    if (p.ID.IndexOf(target) >= 0) {
      listBox1.Items.Add(p.ID + " " + p.Name + " " + p.Price);
    }
  }
  else if (criterion == "Product Name") {
    // search for by product name similarly
  }
}
このコラムでは、短くてわかりやすいコードにするため、コーディングのすべてのベスト プラクティスに従っていません。もちろん、このような大まかなコードは、開発の初期段階にあるアプリケーションの粗削りな状態のようなもので、アプリケーションのテストに着手するときは、多くの場合このような状況にあります。

カスタム コマンドレット ライブラリを作成する
今度は、小さなカスタム コマンドレットのセットの作成手順を順番に説明します。特に、図 1 に示す 8 つの UI 関連のコマンドレットを実装します。ここで使用しているアプローチは、実施できる 7 つの設計オプションのうちの単なる 1 つです (他のオプションのいくつかについては、この後すぐに取り上げます)。
まず、Visual Studio を起動し、CustomUICmdletsLib という名前の新規の C# クラス ライブラリ プロジェクトを作成します。このプロジェクトは、既定で、同じ名前を持つ名前空間を作成します。名前空間の選択は任意ですが、Windows PowerShell のマニュアルでは、カスタム コマンドレット ライブラリを XXX.Commands と命名するように推奨しています。ここで、XXX は組織またはライブラリの機能を表します。筆者のカスタム UI コマンドレット ライブラリ全体の構造は、図 2 のようになります。
Figure 2 カスタム UI コマンドレット ライブラリの構造
using System;
using System.Management.Automation;
using System.ComponentModel;
using System.Configuration.Install;
using System.Runtime.InteropServices;

namespace CustomUICmdletsLib
{
  [Cmdlet(VerbsCommon.Get, "Window")]
  public class GetWindowCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommon.Get, "Control")]
  public class GetControlCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommon.Get, "ControlByIndex")]
  public class GetControlByIndexCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommunications.Send, "Chars")]
  public class SetCharsCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommunications.Send, "Click")]
  public class SetClickCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommon.Get, "ListBox")]
  public class GetListBoxCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommon.Get, "TextBox")]
  public class GetTextBoxCommand : Cmdlet { . . . }

  [Cmdlet(VerbsCommunications.Send, "Menu")]
  public class SetMenuCommand : Cmdlet { . . . }

  [RunInstaller(true)]
  public class LibPSSnapIn : PSSnapIn { . . . }
}
このライブラリには、System.Management.Automation.dll ファイルに含まれているアセンブリへのプロジェクト参照があります。この DLL は、Visual Studio に付属する通常の .NET Framework DLL の 1 つではありません。通常、Windows Software Development Kit (microsoft.com/downloads で入手できます) の一部として入手できます。または、Windows PowerShell のダウンロード ファイル自体に System.Management.Automation.dll が付属している場合もあります。
System.ComponentModel 名前空間および System.Runtime.InteropServices 名前空間は、既定で C# クラス ライブラリ プロジェクトで使用できます。そのため、該当する using ステートメントを追加するだけで済みます。これらの名前空間はそれぞれ、LibPSSnapIn クラスと P/Invoke コードで必要です。
System.Configuration.Install.dll アセンブリにプロジェクト参照を追加する必要があります。これは、標準の .NET Framework アセンブリ セットの一部であるため、[追加]、[プロジェクトの参照] の順に選択して表示される [.NET GUI] ダイアログ ボックスの一覧に表示されています。図 2 に 9 つのクラスがあることに気付いた方もいるでしょう。各カスタム コマンドレットごとの 1 つのクラスと、カスタム コマンドレット登録用の 1 つの特殊なスナップイン クラスです。
最初のカスタム コマンドレット get-window は、最上位レベルのウィンドウの名前 (アプリケーション Form など) をそのまま使用し、ウィンドウまたはアプリケーションへのハンドルを返します。図 3 に、そのコードを示します。
Figure 3 カスタム get-window コマンドレット
[Cmdlet(VerbsCommon.Get, "Window")]
public class GetWindowCommand : Cmdlet
{
  [DllImport("user32.dll", CharSet=CharSet.Auto)]
  static extern IntPtr FindWindow(
    string lpClassName, string lpWindowName);

  private string windowName;

  [Parameter(Position = 0)]
  public string WindowName {
    get { return windowName; }
    set { windowName = value; }
  }

  protected override void ProcessRecord()
  {
    IntPtr wh = FindWindow(null, windowName);
    WriteObject(wh);
  }
}
コードの最初の行では Windows PowerShell コマンドレットの属性を使用して、カスタム コマンドレットの名前を暗黙に指定します。Windows PowerShell コマンドレットは、動詞と名詞で構成される名前表記規則に従い、標準のリストからの動詞を使用します。そのため、このコマンドレットには get-window という名前を付けています。クラス自身は、ユーザーに代わってほとんどの機能を実行する基本の Cmdlet クラスから継承するため、カスタム コマンドレットの作成は非常に簡単です (実装クラスの名前は実装されるコマンドレットの名前に関連している必要はありませんが、一貫性のあるクラス命名方式を使用することをお勧めします)。
次に、カスタム コマンドレットに windowName という名前の単一のパラメータを指定します。FindWindow 関数の Class Name パラメータは使用しません。ウィンドウ オブジェクトの Class Name は、Windows の内部カテゴリであり、C# の "クラス" 言語機能とは無関係です。そのため、ウィンドウの特定には役立ちません。
カスタム コマンドレットの入力パラメータに get および set プロパティを指定し、必要な Parameter 属性でプロパティを修飾し、パラメータの 0 から始まるインデックスの位置を指定します (また、必須の引数を指定していくつかのエラー チェックを追加することもできます)。カスタム コマンドレットの実際のすべての処理は、特殊な ProcessRecord メソッドで実行されます。ここでは、C# FindWindow エイリアスを呼び出すだけです。これは、基になる Win32 FindWindow 関数を呼び出し、指定された名前でウィンドウのハンドルをフェッチします。Windows PowerShell ではパイプライン アーキテクチャを使用しているため、"return" キーワードを使用してウィンドウ ハンドル値を返すのではなく、特殊な WriteObject メソッドを使用してハンドル値を返します。
get-ControlByIndex コマンドレットはやや複雑です。図 4 に、このコードを示します。ここでは、Win32 関数 FindWindowEx を使用して、コントロールの暗黙のインデックス値に基づき、コントロールへのハンドルを取得します。FindWindowEx は 4 つの引数を受け取ります。2 番目の引数は、参照を開始するウィンドウ/コントロールの場所を FindWindowEx に指示するハンドルです。前の呼び出しからの戻り値を FindWindowEx に繰り返し渡すことで、図 4 の do...while ループの繰り返し処理のたびに、効率良く次のウィンドウ ハンドルに進みます。ローカル ct 変数がインデックスの値に達すると繰り返し処理を停止して、変数が引数として渡されます。
Figure 4 カスタム get-controlByIndex コマンドレット
[Cmdlet(VerbsCommon.Get, "ControlByIndex")]
public class GetControlByIndexCommand : Cmdlet
{
  [DllImport("user32.dll", CharSet = CharSet.Auto)]
  static extern IntPtr FindWindowEx(IntPtr hwndParent,
    IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

  private IntPtr handleParent;
  private int index;

  [Parameter(Position = 0)]
  public IntPtr HandleParent {
    get { return handleParent; }
    set { handleParent = value; }
  }

  [Parameter(Position = 1)]
  public int Index {
    get { return index; }
    set { index = value; }
  }

  protected override void ProcessRecord()
  {
    if (index == 0) {
      WriteObject(handleParent);
    }
    else {
      int ct = 0;
      IntPtr result = IntPtr.Zero;
      do {
        result = FindWindowEx(handleParent, result, null, null);
        if (result != IntPtr.Zero) ++ct;
      } while (ct < index && result != IntPtr.Zero);
      WriteObject(result);
    }
  }
} // class
残りの 6 つのカスタム コマンドレットは、get-window および get-controlByIndex コマンドレットと同じ一般的なデザイン パターンに従っています。図 5 に、8 つのすべてカスタム コマンドレットをまとめます (コード サンプルには、これらすべての完全なソース コードが含まれています)。
Figure 5 UI テスト自動化用のカスタム コマンドレットの要約

コマンドレット名 | 入力パラメータ | 戻り値 / 効果
get-window | windowName | 最上位ウィンドウへのハンドル
get-control | handleParent, controlName | 名前付きコントロールへのハンドル
get-controlByIndex | handleParent, index | 名前付きでないコントロールへのハンドル
send-chars | handleControl, s | コントロールへの文字列の送信
send-click | handleControl | コントロールのクリック
get-listBox | handleControl, target | ターゲットの 0 から始まる位置または -1
get-textBox | handleControl | 文字列としての TextBox のコンテンツ
send-menu | mainIndex, subIndex | アプリケーションのメニュー コマンドの起動
確かに、ここで使用しているコマンドレット名はかなり簡潔です。筆者の同僚のほとんどは、もっとそのコマンドレットを説明する名前を好みます。たとえば、get-window を get-automationWindow または get-uiAutoWindowHandle などの名前に変更してもかまいません (幸い、カスタム コマンドレットのエイリアスには長い名前を指定できます)。Windows PowerShell のすべてのカスタム コマンドレット ライブラリでは、Windows PowerShell へのカスタム ライブラリの登録を可能にする特殊なスナップイン クラスを実装する必要があります。筆者のスナップイン クラスは、次のとおりです。
[RunInstaller(true)]
public class LibPSSnapIn : PSSnapIn
{
  public LibPSSnapIn() : base() { }
  public override string Name {
    get { return "LibPSSnapIn"; }
  }
  public override string Vendor {
    get { return "James McCaffrey"; }
  }
  public override string Description {
    get { return " Provides cmdlets for lightweight UI automation"; }
  }
}
Windows PowerShell のスナップインの詳細な説明は、このコラムの範囲外です。ここでは、ユーザーはこのコードを使用し、必要に応じて、文字列の値を置き換えることができます。
カスタム コマンドレット ライブラリを記述した後、Visual Studio プロジェクトを作成します。このプロジェクトは出力ディレクトリに単一の CustomUICmdletLib.dll ファイルを作成するだけです。次に、そのライブラリを登録し、有効にする必要があります。これを実行する方法はいくつかあります。お勧めする方法は、Windows PowerShell の小さな登録関数を作成し、この関数を Windows PowerShell の新規インスタンスが起動されるたびに自動的に実行する開始スクリプトに追加することです (実際は、4 種類の Windows PowerShell 開始スクリプトがあります)。
Windows PowerShell のコマンド ラインで「notepad $profile」と入力すると、開始スクリプトの編集や作成を行うことができます。図 6 は、図 1 に表示される画面を取得するときに使用した開始スクリプトを示します。
Figure 6 カスタム コマンドレット ライブラリの登録
# file: Microsoft.PowerShell_profile.ps1

function RegisterUILib
{
  write-host "registering custom cmdlets for UI automation`n"
  $env:path = $env:path +=
    ";C:\Windows\Microsoft.NET\Framework\v2.0.50727"
  sl 'C:\UIautomationWithPowerShell\CustomUICmdletsLib\bin\Debug'
  installutil.exe CustomUICmdletsLib.dll | out-null
  add-pssnapin LibPSSnapin
  write-host "get-window, get-control, get-controlByIndex,
    send-chars, send-click"
  write-host "get-listBox, send-menu, and get-textBox custom
    cmdlets are enabled `n"
}
RegisterUILib # invoke function

set-location C:\UIautomationWithPowerShell

# end startup script
スクリプトでは、Windows PowerShell のカスタム コマンドレット登録コードをカプセル化する関数を作成します (Windows PowerShell の個別のステートメントを簡単に使用できました)。開始スクリプトには、わかりやすい write-host ステートメントを配置したいと思います。これは、筆者が、開始プロファイルと、現在のシェルで使用可能な特殊機能を正確に知らせるメッセージをしばしば変更するためです。
次に、組み込みの Windows PowerShell 変数 $env:path を使用して、installutil.exe という名前の特殊なユーティリティ ファイルの場所をシェル パス変数に追加します。次に、現在のディレクトリの場所をカスタム コマンドレットの DLL ファイルの場所に設定します。installutil.exe プログラムを呼び出し、そのコマンドを out-null にパイプして、騒々しい進行状況メッセージが表示されるのを防ぎます。
最後の主要なコマンドでは組み込みコマンドレット add-pssnapin を使用して、カスタム ライブラリ内で定義されている LibPSSnapIn クラスを呼び出します。このコードは関数を定義しているだけであることに注意してください。関数の定義後、開始スクリプトでその関数を名前で呼び出します。

カスタム コマンドレット ライブラリの使用
カスタム Windows PowerShell コマンドレットが完成すると、ライブラリの使用が極めて簡単になります。今度は、図 1 に示す出力を生成したスクリプトについて順番に説明します。テスト自動化スクリプトの最初の数行は、次のとおりです。
# testScenario.ps1
write-host "`nBegin UI automation with PowerShell test"
$pass = $true
write-host "`nLaunching application to automate"
invoke-item '.\TheAppToTest\bin\Debug\TheAppToTest.exe'
[System.Threading.Thread]::Sleep(2000)
Windows PowerShell のコメントで始まり、write-host コマンドレットを使用してシェルにメッセージを出力します。`n は Windows PowerShell の埋め込み改行文字のエスケープ シーケンスです。
次に、$pass という名前の変数を true に設定します。このロジックは、テスト シナリオが合格であることを想定しています。アプリケーションの状態が予期される値を満たしていない場合は、$pass を false に設定します。Windows PowerShell 変数名は "$" 文字で開始するため、スクリプト内で容易に区別できます。$true 変数は特殊な Windows PowerShell 組み込み変数です。
write-host を使用して進行状況メッセージを表示した後、組み込みコマンドレット invoke-item を使用して、テスト用アプリケーションを起動します。Windows PowerShell では単一引用符と二重引用符の両方を使用することに注意してください (単一引用符付きの文字列はリテラルですが、二重引用符付きの文字列は、埋め込みエスケープ シーケンスや変数として評価されます)。
次に、Sys- tem.Threading.Thread 名前空間の .NET Sleep 静的メソッドを直接呼び出します (Windows PowerShell は、.NET Framework に完全にアクセスできます。すばらしいですね)。ここで、スクリプトを 2 秒 (2000 ミリ秒) 間一時停止して、アプリケーションが起動するための時間を提供します。
これで、次のように、メイン アプリケーション Form へのハンドルと Form の 3 つの子コントロールを取得できました。
$app = get-window "Form1"
$cb = get-controlByIndex $app 4
$btn = get-control $app "Search"
$tb = get-controlByIndex $app 3
最初の行では、カスタム コマンドレット get-window を使用して、最上位ウィンドウへのハンドルを取得します。そして、カスタム コマンドレット get-controlByIndex を使用して、Window Name プロパティを持たないコントロールへのハンドル (ComboBox の $cb および TextBox の $tb) を取得します。名前付きコントロールへのハンドル (検索ボタンの $btn) を取得するには、カスタム コマンドレット get-control を使用します。
次に、ウィンドウ ハンドルを表示し、null 値や重複した値がないことを確認します。そして、カスタム コマンドレット send-click を使用して、検索ボタンのクリックをシミュレートします。
write-host "`nHandle to Application is " $app
write-host "Handle to ComboBox is" $cb
write-host "Handle to Button is " $btn
write-host "Handle to TextBox is " $tb

write-host "`nClicking on search button"
send-click $btn
(実際のシナリオでは、プログラムで無効なハンドル値を確認する必要がある場合があります)。検索ボタンの最初のクリックでは、図 7 に示すエラー メッセージが生成されます。これは、ComboBox コントロールの検索条件と TextBox コントロールの検索対象の両方が空であるためです。
図 7 アプリケーションのエラーを示す MessageBox
図 7 アプリケーションのエラーを示す MessageBox
スクリプトのその次のいくつかのコマンドでは、次のように、モーダル MessageBox ウィンドウを処理します。
write-host "Finding Error Message box"
[System.Threading.Thread]::Sleep(2000)
$mbox = get-window "Error"

write-host "Clicking Message Box away"
$ok = get-control $mbox "OK"
send-click $ok
スクリプトに 2 秒間の遅延を設定し、MessageBox が完全に使用可能になっていることを確認します。次に、カスタム コマンドレット get-window を使用してウィンドウへのハンドルを取得します (なお、MessageBox ウィンドウは別個の最上位ウィンドウであり、このウィンドウを生成したアプリケーションの子ウィンドウではありません)。get-control を使用して MessageBox 上の OK ボタン コントロールのハンドルを取得し、クリックして、MessageBox を閉じます。
その次のいくつかのステートメントでは、ComboBox コントロールと TextBox コントロールに文字列を送信します。
  write-host
    "`nSending 'Product Name' to   ComboBox"
  send-chars $cb "Product Name"
  write-host "Sending 'ad' to TextBox"
  send-chars $tb "ad"
  write-host "Re-clicking search button"
  send-click $btn
検索条件およびターゲット値を指定した後、検索ボタンを再度クリックします。テスト用のアプリケーションで、製品名に "ad" を含むすべての製品が検索され、ListBox コントロールに結果が表示されます。
2 秒間停止し、次に、ListBox コントロールを調べます。
write-host "`nChecking contents of ListBox for '222'"
[System.Threading.Thread]::Sleep(2000)
$lb = get-controlByIndex $app 1
write-host "Handle to ListBox is " $lb
$result = get-listBox $lb "222"
カスタム コマンドレット get-listBox は、対象文字列が見つからない場合は -1 を返し、見つかった場合は、ターゲットの場所の 0 から始まるインデックスを返します。そのため、次のようにして、$result 変数の値をチェックできます。
if ($result -ge 0) {
  write-host "Found '222' in ListBox!"
}
else {
  write-host "Did NOT find '222' in ListBox"
  $pass = $false
}
Windows PowerShell の多少変わっている点は、-ge ("以上") や -eq ("等しい") などの Boolean 比較演算子を使用することです。">=" や "==" などのレキシカル トークンは使用しません。
今度は、カスタム コマンドレット get-textBox を使用して、検索対象のコンテンツをフェッチします。
write-host "Checking contents of TextBox for 'ad'"
[System.Threading.Thread]::Sleep(2000)
$text = get-textBox $tb
次に、アプリケーションが TextBox コントロールだけを正しく残しているかどうかをチェックします。
if ($text -eq "ad") {
  write-host "Found 'ad' in TextBox!"
}
else {
  write-host "Did NOT find 'ad' in TextBox"
  $pass = $false
}
今度は、$pass 変数を調べて、true に設定されたままであるか、不適切なアプリケーションの状態によって false に設定されたかをチェックできます。
if ($pass) {
  write-host "`nTest scenario result = Pass" -foregroundcolor green
}
else {
  write-host "`nTest scenario result = * FAIL *" -foregroundcolor red
}
最後に、カスタム コマンドレット send-menu を呼び出すことにより、[File]、[Exit] をシミュレートしてアプリケーションを閉じます。
write-host "`nClicking File -> Exit in 5 seconds . . ."
[System.Threading.Thread]::Sleep(5000)
send-menu $app 0 0
write-host "`nEnd UI automation with PowerShell test`n"
# end script
send-menu コマンドレットの 2 つの 0 引数は、インデックス 0 のメイン メニュー項目 (File) を使用し、次に、インデックス 0 のサブ項目 (Exit) を使用することを意味します。

UI コマンドレット ライブラリの使用、変更、および拡張
ここで紹介した手法は、超軽量なテスト自動化の状況に最も適しています。すばやく簡単に UI テスト自動化を実行する必要がある場合は、Windows PowerShell と小さなカスタムの UI コマンドレット ライブラリの併用が優れたアプローチです。また、Windows PowerShell コマンド ラインから対話形式の UI テスト自動化を実行する必要がある場合もこれらの手法が適しています。ただし、比較的複雑な UI テスト自動化を記述する場合は、通常、Visual Studio を使用して P/Invoke メカニズムを使用する C# プログラムを作成することをお勧めします。
この記事で提示したカスタム コマンドレット ライブラリは、ユーザーがこのコードを簡単に変更、拡張できるように特別に作成されています。特に、コードを短く、わかりやすくするためにエラー チェックはすべて削除しています。現状でも、筆者が作成したライブラリには UI テスト自動化のかなりの共通部分を実行できるカスタム コマンドレットが揃っていますが、CheckBox コントロールや RadioBox コントロールなどその他のユーザー コントロールを操作するコマンドレットを追加することをお勧めします。ただ夢中になりすぎないようにしてください。ほとんどの UI シナリオを網羅する完全な UI テスト自動化ライブラリを作成しようとすると、非常に大きくなってしまいます。軽量の UI テスト自動化は、平凡で反復的なタスクを実行し、より面白く複雑な UI シナリオの手動によるテストに専念できるようにユーザーを解放してくれます。
また、エラー チェックや追加機能を提供できるように、コアのカスタム コマンドレットを呼び出す Windows PowerShell 関数を作成して、筆者の UI テスト自動化を拡張することができます。たとえば、サンプルではテスト用アプリケーションの起動を待機する処理に非常におおまかなアプローチを使用しています。つまり、get-window コマンドレットを呼び出す前に Thread.Sleep 呼び出しを使用しているだけです。もっと確実なアプローチとしては、get-window への戻り値が null でないかまたは試行最大回数に達するまでの短い遅延が設定されている get-window を呼び出すループを含む、短い Windows PowerShell 関数を記述することです。

ご意見やご質問は、

James testrun@microsoft.com.

まで英語でお送りください。
Dr. James McCaffrey は、Volt Information Sciences, Inc. で、マイクロソフトのソフトウェア エンジニア向けの技術トレーニングを担当しています。 これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。 また、『.NET Test Automation Recipes』(Apress、2006 年) の著者でもあります。James の連絡先は jmccaffrey@volt.com または v-jammc@microsoft.com です。


サービス ステーション
WCF をカスタム動作で拡張する
Aaron Skonnard

コードのダウンロード : : ServiceStation2007_12.exe (165 KB)
Browse the Code Online
コンテンツ
WCF の拡張性
ディスパッチャとプロキシの拡張機能
カスタム拡張機能を実装する
パラメータ インスペクタ
メッセージ インスペクタ
操作呼び出し元
動作を使用してカスタム拡張機能を適用する
ランタイムに動作を追加する
属性と構成を使用して動作を追加する
動作の検証とバインド構成
拡張機能間で状態を共有する
まとめ

Windows® Communication Foundation (WCF) には、サービスのディスパッチとクライアントのプロキシ呼び出しのランタイムの動作をカスタマイズするために開発者が使用できる、さまざまな拡張ポイントが用意されています。これらの拡張ポイントを利用するには、宣言によってサービスに適用できるカスタム動作を記述します。今月は、このプロセスのしくみについて説明します。

WCF の拡張性
前回のコラムでは、WCF サービスの各エンドポイントに対して指定できる WCF のバインドの概念に焦点を絞って説明しました。バインドは、そのエンドポイントのメッセージの詳細 (通信で行われる処理) を制御します。これは、本質的には、バイトのストリーム (通信されるメッセージ) と WCF メッセージの間の変換を行うチャネル スタックを構築するために WCF が従うレシピです。WCF のチャネル層には、無数の拡張ポイントがあります。
WCF には、アプリケーション開発者に対象を絞った、チャネル層の上に位置する上位レベルのランタイムも用意されています。これは、WCF のドキュメントでは一般にサービス モデル層と呼ばれます。この上位レベルのランタイムは、主に、サービス ホストのコンテキスト内のディスパッチャと呼ばれるコンポーネントと、クライアントのコンテキスト内のプロキシと呼ばれるコンポーネントで構成されます。
ディスパッチャとプロキシの組み合わせの主な目的は、WCF の Message オブジェクトと Microsoft® .NET Framework のメソッド呼び出しの間の変換を行うことです (図 1 を参照)。これらのコンポーネントは、適切に定義された一連の手順に従ってこのプロセスを実行し、その途中の各手順で、接続可能な拡張ポイントを提供します。これらの拡張ポイントを使用すると、メッセージやパラメータの検証、メッセージのログ記録、メッセージの変換、カスタムのシリアル化および逆シリアル化形式の使用、出力のキャッシュ、オブジェクトのプーリング、エラー処理、承認など、幅広い種類のカスタム動作を実装できます。ここでは、そのような種類のカスタム動作の実装に重点を置いて説明します。
図 1 WCF ランタイム アーキテクチャ
図 1 WCF ランタイム アーキテクチャ

ディスパッチャとプロキシの拡張機能
ディスパッチャとプロキシには、独自のコードを挿入できる多数の拡張ポイントが用意されています。このような拡張機能は、ランタイムの既定の実行動作をインターセプトできることから、一般に "インターセプタ" と呼ばれます。しかし、私は普段、"ランタイム拡張機能" と呼んでいます。
図 2 に、クライアント側のプロキシ アーキテクチャと、利用できる拡張ポイントを示します。プロキシの主な役割は、呼び出し元から渡されたオブジェクト (パラメータ) を WCF の Message オブジェクトに変換することです。その後、その Message オブジェクトを、通信のために下位のチャネル スタックに渡すことができます。
図 2 プロキシ (クライアント) の拡張機能
図 2 プロキシ (クライアント) の拡張機能
このプロセスで提供される 1 番目の拡張ポイントでは、カスタムのパラメータ検査を実行できます (手順 1 を参照)。このフックを使用すると、カスタム検証、値の変更、または特殊なフィルタ処理を実行できます。プロキシは次に、シリアライザを利用して、渡されたパラメータを WCF の Message オブジェクトに変換します (図の手順 2 を参照)。ここでカスタム フォーマッタ オブジェクトを使用すると、シリアル化プロセスをカスタマイズできます。
プロキシは、生成した Message オブジェクトをチャネル スタックに送信する前に、最後の拡張ポイントを使用して Message オブジェクトを検査します (手順 3 を参照)。図 2 に示すように、この拡張機能は、どの操作が呼び出されたかに関係なく有効となります。このフックを使用すると、メッセージのログ記録、検証、変換など、必ずしも 1 つの操作に固有ではない横断的なメッセージング機能を実装できます。
プロキシでこれらの拡張機能を構成するには、ClientOperation オブジェクトと ClientRuntime オブジェクトを使用します。サービス操作ごとに 1 つの ClientOperation オブジェクトと、プロキシ全体を構成するための単一の ClientRuntime オブジェクトがあります。ClientOperation にはパラメータ検査の拡張機能とメッセージ書式設定の拡張機能を管理するためのプロパティがあり、ClientRuntime にはメッセージ検査の拡張機能を管理するためのプロパティがあります。
図 3 に、ディスパッチャの拡張ポイントを示します。これは図 2 とよく似ていますが、拡張ポイントが逆の順序で実行されるという点と、前のクライアント側の図にはないポイントがいくつか含まれているという点が異なります。
図 3 ディスパッチャの拡張機能
図 3 ディスパッチャの拡張機能
ディスパッチャがチャネル スタックから Message オブジェクトを受け取るときに、最初に提供される拡張ポイントはメッセージ検査です。ディスパッチャは、次の処理に進む前に、呼び出す操作を選択する必要があります (手順 2)。ここに、既定の操作選択動作をオーバーライドするための拡張ポイントがあります。ディスパッチャはターゲット操作を指定した後、メッセージを、ターゲット メソッドの呼び出し時にパラメータとして渡すことができるオブジェクトに逆シリアル化します。この時点 (手順 3) で、ディスパッチャでは、メッセージ書式設定 (逆シリアル化) の拡張ポイントとパラメータ検査の拡張ポイント (手順 4) を使用できます。最後に、ディスパッチャはターゲット メソッドを呼び出し、準備したパラメータを渡します。この手順を、カスタムの操作呼び出し元オブジェクトによってオーバーライドすることもできます。
ディスパッチャでこれらの拡張機能を構成するには、図 3 に示すように、DispatchRuntime オブジェクトと DispatchOperation オブジェクトを使用します。これらのオブジェクトへのアクセス方法については後で説明しますが、まず、その実装方法を見てみましょう。

カスタム拡張機能を実装する
前述の拡張ポイントはすべて、.NET インターフェイス定義 (図 4 を参照) によってモデル化されています。同じ種類の論理拡張機能が、ディスパッチャ側とプロキシ側で異なるインターフェイスを必要とする場合もあります。では、そのようなインターフェイスのいくつかを実装する方法を詳しく見ていきましょう。
Figure 4 ディスパッチャとプロキシの拡張機能のまとめ

ステージ | インターセプタ インターフェイス | 説明
パラメータの検査 | IParameterInspector | パラメータ値の検査と変更を行うために、呼び出しの前後に呼び出されます。
メッセージの書式設定 | IDispatchMessageFormatter IClientFormatter | シリアル化と逆シリアル化を実行するために呼び出されます。
メッセージの検査 | IDispatchMessageInspector IClientMessageInspector | メッセージの内容を検査して置き換えるために、送信前と受信後に呼び出されます。
操作の選択 | IDispatchOperationSelector IClientOperationSelector | 指定したメッセージに対して呼び出す操作を選択するために呼び出されます。
操作呼び出し元 | IOperationInvoker | 操作を呼び出すために呼び出されます。
たとえば、次のようなコントラクトを使用する郵便番号検索サービスを構築するとします。
[ServiceContract]
public interface IZipCodeService
{
    [OperationContract]
    string Lookup(string zipcode);
}
Lookup メソッドは、zipcode という文字列型のパラメータを 1 つ受け取り、呼び出し元に文字列を返します。呼び出し元は郵便番号の値を指定し、サービスは場所を都市と州の形式で返します。ユーザーにとって、郵便番号を正式な ZIP + 4 (#####-####) 形式で指定する必要があるかどうかは不明確です。たとえば、私の自宅の郵便番号は 84041-1501 です。この要件を考慮すると、サービス実装は、受信した郵便番号の値をすべて検証する必要があります。

パラメータ インスペクタ
ZIP + 4 の検証ロジックを Lookup メソッド自体に実装することは難しくありませんが、それによって郵便番号を受け取るための多数の操作が発生する場合は、任意の操作に宣言的に適用できる IParameterInspector 拡張として実装する方が便利です。
これを実行するには、AfterCall と BeforeCall という 2 つのメソッドを定義する IParameterInspector を実装するクラスを作成する必要があります。その名が示すとおり、ランタイムはサービス インスタンスでターゲット メソッドを呼び出す前に BeforeCall を呼び出し、ターゲット メソッドを呼び出した後に AfterCall を呼び出します。このとき、パラメータと戻り値を検査するためのインターセプト前のポイントとインターセプト後のポイントが提供され、これらのメソッドにオブジェクトの配列として渡されます。
図 5 に、必要な ZIP + 4 の検証を実行する IParameterInspector の完全な実装を示します。ZipCodeInspector クラスは IParameterInspector を実装していますが、ここでは入力の検証のみが必要なので、BeforeCall だけを実装しました。BeforeCall は指定された郵便番号の値を ZIP + 4 の正規表現 ("\d{5}-\d{4}") に対して検証し、一致しない場合は、処理を続行して FaultException をスローします。
Figure 5 郵便番号の検証を実行するパラメータ インスペクタ
public class ZipCodeInspector : IParameterInspector
{
    int zipCodeParamIndex;
    string zipCodeFormat = @"\d{5}-\d{4}";

    public ZipCodeInspector() : this(0) { }
    public ZipCodeInspector(int zipCodeParamIndex)
    {
        this.zipCodeParamIndex = zipCodeParamIndex;
    }

    ... // AfterCall is empty

    public object BeforeCall(string operationName, object[] inputs)
    {
        string zipCodeParam = inputs[this.zipCodeParamIndex] as string;
        if (!Regex.IsMatch(
            zipCodeParam, this.zipCodeFormat, RegexOptions.None))
            throw new FaultException(
                "Invalid zip code format. Required format: #####-####");
        return null;
    }
}
ZipCodeInspector を適切に使用することにより、この検証ロジックを郵便番号の値を受け取る任意の操作に簡単に適用できます。また、この実装は通信の両側 (クライアント側とサービス側) で使用できます。このパラメータ インスペクタを接続する方法については、もう少し例を示した後で説明します。

メッセージ インスペクタ
では、パラメータを検査するのではなく、操作を問わずサービスで送受信されるメッセージを検査する必要がある場合について考えてみましょう。この場合は、メッセージ検査の拡張ポイントを使用します。パラメータ検査とは異なり、メッセージ検査のインターフェイスは、ディスパッチャとプロキシとで異なります (それぞれ IDispatchMessageInspector と IClientMessageInspector)。しかし、両側をサポートする必要がある場合は、いつでも両方のインターフェイスを実装できます。
IDispatchMessageInspector には AfterReceiveRequest と BeforeSendReply という 2 つのメソッドがあります。これは、WCF の Message オブジェクトを検査するためのインターセプト前とインターセプト後のポイントが提供されることを意味します。IClientMessageInspector には、逆のポイントを提供する AfterReceiveReply と BeforeSendRequest という 2 つのメソッドもあります。
たとえば、すべての送受信メッセージをコンソール ウィンドウに出力する診断ユーティリティを実装するとします。図 6 に、この操作を実行する完全なサンプルを示します。ConsoleMessageTracer は両方のメッセージ インスペクタ インターフェイスを実装しているので、通信の両側で使用できます。各メソッドは単純に受信メッセージをコピーし、コンソール ウィンドウに出力します。
Figure 6 コンソールにトレースを出力するメッセージ インスペクタ
public class ConsoleMessageTracer : IDispatchMessageInspector,
    IClientMessageInspector
{
    private Message TraceMessage(MessageBuffer buffer)
    {
        Message msg = buffer.CreateMessage();
        Console.WriteLine("\n{0}\n", msg);
        return buffer.CreateMessage();
    }
    public object AfterReceiveRequest(ref Message request,
        IClientChannel channel,
        InstanceContext instanceContext)
    {
        request = TraceMessage(request.CreateBufferedCopy(int.MaxValue));
        return null;
    }
    public void BeforeSendReply(ref Message reply, object
        correlationState)
    {
        reply = TraceMessage(reply.CreateBufferedCopy(int.MaxValue));
    }

    public void AfterReceiveReply(ref Message reply, object
        correlationState)
    {
        reply = TraceMessage(reply.CreateBufferedCopy(int.MaxValue));
    }
    public object BeforeSendRequest(ref Message request,
        IClientChannel channel)
    {
        request = TraceMessage(request.CreateBufferedCopy(int.MaxValue));
        return null;
    }
}
図 6 でメッセージをコピーする手法を使用した理由については、前回のコラム「WCF メッセージングの基本」(msdn.microsoft.com/msdnmag/issues/07/04/ServiceStation) の、メッセージの有効期間に関するセクションを参照してください。

操作呼び出し元
最後の例として、操作呼び出し元の拡張ポイントを見てみましょう。このフックを使用すると、既定のプロセスをカスタムの呼び出し元オブジェクトでオーバーライドできます。郵便番号の例で操作呼び出し元を使用すると、単純な出力キャッシュ機能を実装できます。特定の郵便番号に対する結果は常に同じになるため、結果をキャッシュすれば、特定の郵便番号の値に対してサービス インスタンスを複数回呼び出す必要がなくなります。サービス ロジックのコストが高い場合や処理時間が長い場合、これによってパフォーマンスと応答時間が大幅に改善される可能性があります。
図 7 に詳細な例を示します。ここで、ZipCodeCacher クラスは IOperationInvoker を実装しており、郵便番号のキャッシュ機能は Invoke メソッドにあります。Invoke メソッドはまず、キャッシュ内で郵便番号に対応する場所を探します。見つからない場合、サービス インスタンスで (既定の呼び出し元を使用して) メソッドを呼び出します。そして、新たに得られた結果を、今後の呼び出しに備えてキャッシュに保存します。この拡張機能のユーザーは、構築時にディスパッチャの既定の呼び出し元オブジェクトを指定する必要があります。これは、残りのメソッドが、ディスパッチャの既定の呼び出し元オブジェクトに処理を代行させるためです。
Figure 7 郵便番号をキャッシュする操作呼び出し元
public class ZipCodeCacher : IOperationInvoker
{
    IOperationInvoker innerOperationInvoker;
    Dictionary<string, string> zipCodeCache =
        new Dictionary<string, string>();

    public ZipCodeCacher(IOperationInvoker innerOperationInvoker)
    {
        this.innerOperationInvoker = innerOperationInvoker;
    }

    public object Invoke(object instance, object[] inputs,
        out object[] outputs)
    {
        string zipcode = inputs[0] as string;
        string value;

        if (this.zipCodeCache.TryGetValue(zipcode, out value))
        {
            outputs = new object[0];
            return value;
        }
        else
        {
            value = (string)this.innerOperationInvoker.Invoke(
                instance, inputs, out outputs);
            zipCodeCache[zipcode] = value;
            return value;
        }
    }

    ...

    // remaining methods elided
    // they simply delegate to innerOperationInvoker
}
ここまでは、カスタム拡張機能の構築の一般的な例をいくつか見てきました。他にも時間の都合で紹介できなかった例がありますが、後は各自で調べてみてください。次に、さらに重要な、これらの拡張機能をディスパッチャとプロキシで構成する方法について説明します。ここでいよいよ動作が登場します。

動作を使用してカスタム拡張機能を適用する
動作は、ServiceHost または ChannelFactory の初期化プロセス中のランタイムの動作を拡張する特殊なクラスです。動作には、サービスの動作、エンドポイントの動作、コントラクトの動作、および操作の動作の 4 種類があります。種類ごとに、拡張機能を適用できるスコープが異なります (図 8 を参照)。また、動作は種類ごとに異なるインターフェイス定義によってモデル化されていますが、どの種類にも同じメソッドのセットがあります (図 9 を参照)。ただし、これには 1 つだけ例外があります。サービスの動作はクライアントに適用できないため、IServiceBehavior には ApplyClientBehavior メソッドがありません。
Figure 9 動作インターフェイスのメソッド

メソッド | 説明
Validate | ランタイムが構築される直前に呼び出され、サービスの説明に対するカスタム検証を実行できるようにします。
AddBindingParameters | ランタイムの構築の最初のステップ (下位のチャネルが構築される前) で呼び出され、下位のチャネル スタックに影響を与えるパラメータを追加できるようにします。
ApplyClientBehavior | 動作がプロキシ (クライアント) の拡張機能を挿入できるようにします。このメソッドは、IServiceBehavior にはありません。
ApplyDispatchBehavior | 動作がディスパッチャの拡張機能を挿入できるようにします。
Figure 8 動作の種類

スコープ | インターフェイス | 潜在的影響
 |  | サービス | エンドポイント | コントラクト | 操作
サービス | IServiceBehavior | ✗ | ✗ | ✗ | ✗
エンドポイント | IEndpointBehavior |  | ✗ | ✗ | ✗
コントラクト | IContractBehavior |  |  | ✗ | ✗
操作 | IOperationBehavior |  |  |  | ✗
図 8 に示すように、サービスの動作は、サービス全体に拡張機能を適用するために使用されます。その際、拡張機能をサービス自体に適用することも、指定したエンドポイント、コントラクト、および操作に適用することもできます。これに対して、エンドポイントの動作は、特定のエンドポイント (またはそのエンドポイントのコントラクトや操作) に拡張機能を適用するために使用されます。コントラクトの動作と操作の動作は、それぞれコントラクトと操作に拡張機能を適用するために使用されます。エンドポイントの動作、コントラクトの動作、および操作の動作はサービスとクライアントの両方に適用できますが、サービスの動作はサービスにしか適用できません。
それぞれの動作インタフェースが持つメソッドは同じですが、シグネチャはまったく異なります。これらは、個々のスコープに合わせて適切なランタイム オブジェクトを提供します。ApplyDispatchBehavior と ApplyClientBehavior は、それぞれディスパッチャとプロキシにカスタム拡張機能を適用するために使用する重要なメソッドです。ランタイムがこれらのメソッドを呼び出すと、拡張機能を挿入できる DispatchRuntime、DispatchOperation、ClientRuntime、および ClientOperation の各オブジェクトが提供されます (図 2 および図 3 を参照)。
図 10 に、いくつかの操作の動作を実装する方法を示します。ZipCodeInspector 拡張機能と ZipCodeCacher 拡張機能は郵便番号を処理するメソッドだけに使用する必要があるので、これらの拡張機能を適用するときには、操作の動作を使用するのが賢明です。ご覧のように、ZipCodeValidation は、提供された DispatchOperation オブジェクトと ClientOperation オブジェクトで見つかった ParameterInspectors コレクションに ZipCodeInspector のインスタンスを追加します。ZipCodeCaching は、提供された DispatchOperation オブジェクトの Invoker プロパティにカスタム ZipCodeCacher を割り当てます。
Figure 10 操作の動作のサンプル
public class ZipCodeValidation : Attribute, IOperationBehavior
{
    public void ApplyClientBehavior(OperationDescription
        operationDescription, ClientOperation clientOperation)
    {
        ZipCodeInspector zipCodeInspector = new ZipCodeInspector();
        clientOperation.ParameterInspectors.Add(zipCodeInspector);
    }
    public void ApplyDispatchBehavior(
        OperationDescription operationDescription,
        DispatchOperation dispatchOperation)
    {
        ZipCodeInspector zipCodeInspector = new ZipCodeInspector();
        dispatchOperation.ParameterInspectors.Add(zipCodeInspector);
    }

    ... // remaining methods empty
}

public class ZipCodeCaching : Attribute, IOperationBehavior
{
    public void ApplyDispatchBehavior(
        OperationDescription operationDescription,
        DispatchOperation dispatchOperation)
    {
        dispatchOperation.Invoker =
            new ZipCodeCacher(dispatchOperation.Invoker);
    }

    ... // remaining methods empty
}
今度は、ConsoleMessageTracer メッセージ インスペクタにどの種類の動作を使用するかを決めましょう。これは、用途に応じて、コントラクトの動作、エンドポイントの動作、またはサービスの動作を使用して適用できます。ConsoleMessageTracer を適切な MessageInspectors コレクションに適用するために、サービスの動作とエンドポイントの動作の両方として機能するクラスを実装する方法の例を図 11 に示します。
Figure 11 エンドポイントの動作とサービスの動作のサンプル
public class ConsoleMessageTracing :
    Attribute, IEndpointBehavior, IServiceBehavior
{
    void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint,
        ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(new ConsoleMessageTracer());
    }
    void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint endpoint,
        EndpointDispatcher endpointDispatcher)
    {
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(
            new ConsoleMessageTracer());
    }

    ... // remaining methods empty

    void IServiceBehavior.ApplyDispatchBehavior(
        ServiceDescription desc, ServiceHostBase host)
    {
        foreach (
            ChannelDispatcher cDispatcher in host.ChannelDispatchers)
            foreach (EndpointDispatcher eDispatcher in
                cDispatcher.Endpoints)
                eDispatcher.DispatchRuntime.MessageInspectors.Add(
                    new ConsoleMessageTracer());
    }
    ... // remaining methods empty
}
ここまでは、いくつかの動作の実装方法を見てきました。次は、WCF ランタイムに動作を追加する方法について説明します。

ランタイムに動作を追加する
ServiceHost またはクライアント側の ChannelFactory を構築すると、ランタイムがサービスの種類を反映し、構成ファイルを読み取り、メモリ内のサービスの説明の作成を開始します。ServiceHost 内では、この説明を ServiceDescription 型の Description プロパティを介して利用できます。ChannelFactory 内では、これを ServiceEndpoint 型の Endpoint プロパティを介して利用できます。ただし、クライアント側の説明はターゲット エンドポイントに限定されます。
ServiceDescription には、コントラクト (ContractDescription) と操作 (OperationDescription) を含む、サービスと各エンドポイント (ServiceEndpoint) の完全な説明が含まれます。ServiceDescription には、サービスの動作のコレクションをモデル化する Behaviors プロパティ (IServiceBehavior 型のコレクション) があります。各 ServiceEndpoint にも、個々のエンドポイントの動作をモデル化する Behaviors プロパティ (IEndpointBehavior 型のコレクション) があります。同様に、ContractDescription と OperationDescription にも、それぞれに応じた Behaviors プロパティがあります。
ServiceHost と ChannelFactory の構築時には、コード内で (属性を使用して) 検出された動作または構成ファイル内で検出された動作が、これらの動作コレクションに自動的に設定されます (これについては後で説明します) 。また、構築後にこれらのコレクションに手動で動作を追加することもできます。ConsoleMessageTracing をサービスの動作としてホストに追加する方法の例を次に示します。
ServiceHost host = new ServiceHost(typeof(ZipCodeService));
host.Description.Behaviors.Add(new ConsoleMessageTracing());
すべての ServiceEndpoint オブジェクトを反復処理し、ConsoleMessageTracing をエンドポイントの動作として追加する例を次に示します。
ServiceHost host = new ServiceHost(typeof(ZipCodeService));
foreach (ServiceEndpoint se in host.Description.Endpoints)
    se.Behaviors.Add(new ConsoleMessageTracing());
このような方法はクライアント側でも同様ですが、既に述べたように説明は 1 つのエンドポイントに限定され、サービスの動作はありません。ConsoleMessageTracing をクライアント側のエンドポイントの動作として追加する方法の例を次に示します。
ZipCodeServiceClient client = new ZipCodeServiceClient();
client.ChannelFactory.Endpoint.Behaviors.Add(
    new ConsoleMessageTracing());
同様の手法で、特定のコントラクトに手動で動作を追加したり (その場合は、ServiceEndpoint.Contract.Behaviors を使用します)、コントラクトの個々の操作に手動で動作を追加したりできます (その場合は、ServiceEndpoint.Contract.Operations コレクションを反復処理し、OperationDescription の Behaviors コレクションにアクセスします)。
ICommunicationObject.Open を使用して ServiceHost または ChannelFactory を開くと、ランタイムは ServiceDescription を調べ、ApplyDispatchBehavior と ApplyClientBehavior を呼び出すことによって各動作がディスパッチャの拡張機能やプロキシの拡張機能を適用できるようにします (図 12 を参照)。このプロセスが完了すると、ランタイムに動作や拡張機能を追加できなくなります。
図 12 ランタイムへの動作の追加
図 12 ランタイムへの動作の追加 (画像を拡大するには、ここをクリックします)

属性と構成を使用して動作を追加する
ServiceHost または ChannelFactory の構築時に、ランタイムは、サービスの種類と構成ファイルを反映し、検出したすべての動作を ServiceDescription の適切な動作コレクションに追加します。
ランタイムはまず、サービス コードで、図 8 の動作インターフェイスのいずれかから派生した .NET 属性を探します。いずれかの属性が見つかるたびに、その属性が適切なコレクションに自動的に追加されます。たとえば、前に定義した動作に対応する 3 つの属性を持つサービスを次に示します。
[ServiceContract]
public interface IZipCodeService
{
    [ZipCodeCaching]
    [ZipCodeValidation]
    [OperationContract]
    string Lookup(string zipcode);
}

[ConsoleMessageTracing]
public class ZipCodeService : IZipCodeService
{
    ...
}
このような構成が可能なのは、これらの動作クラスを定義したときに、IServiceBehavior と IOperationBehavior に加えて Attribute からも派生するように定義したためです。上記の ZipCodeService クラスに対して ServiceHost を構築すると、ランタイムによって、1 つのサービスの動作 (ConsoleMessageTracing) と 2 つの操作の動作 (ZipCodeCaching と ZipCodeValidation) が ServiceDescription に自動的に追加されます。
コントラクトの動作の属性は、サービス コントラクト インターフェイスまたはサービス クラスに適用できます。サービス クラスに適用する際は、エンドポイントが特定のコントラクトを使用する場合にのみコントラクトの動作が有効になるように設定することもできます。この設定を行うには、コントラクトの動作の属性クラスに IContractBehaviorAttribute を実装し、TargetContract プロパティで目的のコントラクトを指定します。
リフレクション プロセスの完了後に、ランタイムはアプリケーションの構成ファイルも検査し、<system.serviceModel> セクションで見つかった情報を ServiceDescription に読み込みます。WCF には、サービスの動作とエンドポイントの動作を構成するための <behaviors> セクションが用意されています。このセクションで見つかったサービスの動作とエンドポイントの動作はすべて、ServiceDescription に自動的に追加されます。
この構成セクションにカスタム動作を挿入するには、まず、次のような BehaviorElementExtension から派生するクラスを作成する必要があります。
public class ConsoleMessageTracingElement : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(ConsoleMessageTracing); }
    }
    protected override object CreateBehavior()
    {
        return new ConsoleMessageTracing();
    }
}
次に、作成した BehaviorExtensionElement を <extensions> セクションに登録し、要素名にマップする必要があります。これにより、登録した要素名を <behaviors> セクションで使用して動作を構成できるようになります。図 13 に、ConsoleMessageTracing 動作の構成方法の詳細な例を示します。
Figure 13 動作の構成
<configuration>
  <system.serviceModel>
    <services>
      <service name="ZipCodeServiceLibrary.ZipCodeService"
          behaviorConfiguration="Default">
        <endpoint binding="basicHttpBinding"
          contract="ZipCodeServiceLibrary.IZipCodeService"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="Default">
          <serviceMetadata httpGetEnabled="true"/>
          <consoleMessageTracing/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <extensions>
      <behaviorExtensions>
        <add name="consoleMessageTracing" type="Extensions.
            ConsoleMessageTracingElement, Extensions, Version=1.0.0.0,
            Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
    </extensions>
  </system.serviceModel>
</configuration>
サービスの動作、コントラクトの動作、および操作の動作は属性を使用して追加できますが、エンドポイントの動作は追加できません。サービスの動作とエンドポイントの動作は構成ファイルを使用して追加できますが、コントラクトの動作と操作の動作は追加できません。すべての種類の動作は、手動で ServiceDescription に追加できます。図 14 に、各種類の相違点を示します。
Figure 14 動作の構成オプション

動作の種類 | 構成オプション
 | 属性 | 構成 | 明示的
サービス | ✗ | ✗ | ✗
エンドポイント |  | ✗ | ✗
コントラクト | ✗ |  | ✗
操作 | ✗ |  | ✗
コントラクトの動作と操作の動作はプロキシの種類に属性を適用すればクライアント側で使用できますが、構成によってクライアントに適用できる動作はエンドポイントの動作のみであることにも注意してください。

動作の検証とバインド構成
動作は、カスタムのランタイム拡張機能の追加だけでなく、カスタム検証とバインド構成という 2 つのタスクも実行できるようにデザインされています。図 9 の Validate と AddBindingParameters に注目してください。
Validate メソッドは、初期化された後、ランタイムの残りの部分の構築が完了する前に、ServiceDescription に対してカスタム検証を実行できるようにします。このときに ServiceDescription ツリー (またはクライアント側の ServiceEndpoint) をスキャンして、独自の基準に対して検証することができます。要件を満たしていないものがある場合は、例外をスローして ServiceHost または ChannelFactory が開かないようにすることができます。
ServiceDescription を検証して、BasicHttpBinding を使用しているエンドポイントがないことを確認するサービスの動作を次に示します。
public class NoBasicEndpointValidator : Attribute, IServiceBehavior
{
    #region IServiceBehavior Members
    public void Validate(ServiceDescription desc, ServiceHostBase host)
    {
        foreach (ServiceEndpoint se in desc.Endpoints)
            if (se.Binding.Name.Equals("BasicHttpBinding"))
                throw new FaultException(
                    "BasicHttpBinding is not allowed");
    }

    ... //remaining methods empty
}
この動作をサービスに適用した後は、エンドポイントの構成時に BasicHttpBinding を使用できなくなり、代わりにセキュリティで保護されたバインドを選択するよう強制されます。
AddBindingParameters を使用すると、ランタイムの初期化時にバインド パラメータを追加できます。バインド パラメータは、チャネル スタックの作成に影響を与えるために下位のチャネル層に渡されます。カスタム バインド要素を使用してこれらのバインド パラメータにアクセスしたり、これらのバインド パラメータを検索するようにカスタム バインド要素をデザインしたりできます。カスタム バインドの詳細については、2007 年 7 月号の私のコラム「WCF バインドの詳細」(msdn.microsoft.com/msdnmag/issues/07/07/ServiceStation) を参照してください。これは、これまでに説明した他の拡張ポイントに比べて使用頻度が低いと思われる、より高度な拡張ポイントです。

拡張機能間で状態を共有する
ディスパッチャまたはプロキシで複数の拡張機能を使用するようになると、拡張機能間で状態を共有する方法が必要になります。さいわい、WCF にはユーザー定義の状態を格納できる拡張機能オブジェクトが用意されています。
拡張機能オブジェクトが保持される期間は、格納先によって決まります。拡張機能オブジェクトは、グローバル レベルで ServiceHost に格納するか、InstanceContext または OperationContext に格納できます。これらのクラスには、IExtension<T> (T はコレクションに応じて ServiceHostBase、InstanceContext、または OperationContext) から派生したオブジェクトを管理する Extensions コレクションがあります。
ServiceHost の拡張機能オブジェクトは、ServiceHost の有効期間中はメモリに保持されますが、InstanceContext および OperationContext の拡張機能オブジェクトは、サービス インスタンスまたは操作呼び出しの有効期間中しかメモリに保持されません。ディスパッチャとプロキシのカスタム拡張機能は、これらのコレクションを使用して、パイプライン全体を通じてユーザー定義の状態を格納 (および検索) できます。

まとめ
WCF では、ランタイムを大幅にカスタマイズできる強力な拡張機能が提供されています。ディスパッチャとプロキシ全体を通じて、パラメータの検査、メッセージの書式設定、メッセージの検査、操作の選択、呼び出しなどのタスクを実行するための重要な拡張機能のステージがいくつか用意されています。このようなカスタム拡張機能を作成するには、適切な拡張機能インターフェイスを実装した後に、カスタム動作を使用してディスパッチャまたはプロキシにカスタム拡張機能を適用します。
ディスパッチャには、ここで取り上げることができなかった高度な拡張機能が他にもあります。それらの拡張機能は、インスタンス化、同時実行、アドレス指定、およびセキュリティに関する処理を行います。こうした領域で必要な機能の大部分は組み込みの [ServiceBehavior] 動作と [OperationBehavior] 動作に用意されていますが、足りない機能がある場合は、カスタム動作を作成することによってランタイムのその領域の機能を拡張することができます。
例を詳しく調べて動作を確認するために、このコラムに関連付けられたサンプル コードをダウンロードしてください。

ご意見やご感想は、

Aaron sstation@microsoft.com.

まで英語でお送りください。
Aaron Skonnard は、Microsoft .NET のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Aaron は、Pluralsight 社が提供する Applied Web Services 2.0、Applied BizTalk Server 2006、および Introducing Windows Communication Foundation の各コースの著者です。Aaron はこれまで何年もの間、コースの開発、会議での講演、およびプロ開発者の指導に従事してきました。連絡先は pluralsight.com/aaron (英語) です。


Netting C++
EEK! - ネズミの設計
Stanley B. Lippman

コンテンツ
設計の開始点
感覚を持つ実体

最近の一連のコラムで、私は、ネズミのソフトウェア シミュレーション (EEK!) の設計と、Microsoft® .NET Framework と C++/CLI のリソースおよび Visual Studio® 2005 に導入された .NET にバインドされている改訂済みの C++ 言語を使用するその環境について紹介してきました。前回のコラム (msdn.microsoft.com/msdnmag/issues/07/10/nettingC) では、XML 記述ファイルと System::XML and System::Data 名前空間の機能を使用して EEK! を初期化する方法について説明しました。今回は、実際の Mouse クラスの設計について紹介します。

設計の開始点
EEK! では、ネズミは感覚を持つ実体の一種です。.NET を使用する私たちは皆、is-a の関係が、作成する最初のタイプとサブタイプのクラス関係を指定する、クラス継承を使用して表現されることを認識しています。
public ref class SentientEntity abstract { ... };
public ref class Mouse : SentientEntity { ... };
ネイティブ C++ とは異なり、.NET では、クラスは設計内で予定されている使い方によって区別されます。継承は、C++/CLI ではコンテキスト キーワード ref とそれに続く予約キーワード class で示される参照クラスのみでサポートされます (プログラマが決して識別子として使用しない予約キーワードとは異なり、コンテキスト キーワードは、特定のプログラム コンテキスト内でのみキーワードとして扱われます。それ以外の場合、プログラムでは自由にコンテキスト キーワードを使用できます)。ref については、class キーワードの前にある場合のみキーワードとして扱われます。
参照クラスは、名前付きハンドルによって操作され、ガベージ コレクションの対象であり、ヒープから割り当てられる実体です。C++/CLI の予約キーワード gcnew は、.NET マネージ ヒープからのメモリの割り当ておよび参照クラスのインスタンスの作成に使用されます。
Mouse ^milkyWhite = gcnew Mouse;
ハット記号 (^) は、milkyWhite がマネージ ヒープにある Mouse 型の参照クラス オブジェクトへのハンドルであることを示す C++/CLI のトークンです。
Mouse クラスと SentientEntity クラスの間にあるこの is-a 関係が意味するのは、Mouse クラスに取り組む前に、SentientEntity 抽象基本クラスを設計する必要があるということです。まずは、シミュレーション内での SentientEntity の機能から検討します。

感覚を持つ実体
前回のコラムで説明したように、シミュレーション内のすべての実体 (感覚を持つ実体を除く) は XML ファイル内で完全に記述されています。しかし、感覚を持つ実体は異なります。シミュレーションでは、以下の 4 つの関数を使用してこれらの実体を操作します。
  1. void onInit は、シミュレート環境内の感覚を持つ実体に必要なすべての初期化 (シミュレーションでネズミが放たれた初期世界タイルにネズミの内部マップをバインドするなど) を行います。
  2. void onTick は、シミュレーションの各ティックにおいて感覚を持つ実体の実際の動作を行います。シミュレータは、onTick メソッドを順に呼び出して、環境内の感覚を持つ各実体を繰り返し処理します。
  3. void onTerminate は、シミュレーションの終わりにすべてのクリーンアップ動作 (ネズミの内部状態をデータベースに保存するなど) を行います。
  4. void onLog は、指定された詳細レベルで、感覚を持つ実体のログ サービスを処理します (既定の引数は .NET ではサポートされていません)。
これらのメソッドを一般化するために、次のように、個別のインターフェイス クラスにメソッドを組み込みます。
interface class ISimulation
{
    void onInit();
    void onTick();
    void onTerminate();
    void onLog( int levelOfDetail );
};
インターフェイスは、C++ がサポートする元のオブジェクト指向モデルを改良したものです。これにより、複数の継承に関連する複雑さが解消されます。大幅に軽量化され、管理も簡素化されます。EEK! の設計では、シミュレーションのアクティブな参加者となる任意のクラス (岩やげっ歯類の動物でも、木やシジュウカラでも) は、ISimulation インターフェイスを実装するだけで済みます。実際には、これは EEK! 内の活動の定義になります。
SentientEntity クラスは、シミュレーションの実際の実体を表現せずに、実際のすべての実体の派生元となる基本クラスを表現するため、これを抽象クラスとして宣言し、ISimulation に関連付けられているメソッドを宣言する必要があります。図 1 に、C++/CLI で記述したコードを示します。
Figure 1 SentientEntity 抽象基本クラス
public ref class SentientEntity abstract
    : ISimulation {
public:
    // ...
    virtual void onInit() abstract;
    virtual void onTick() abstract;
    virtual void onTerminate() abstract;
    virtual void onLog( int levelOfDetail ) abstract;
};
ご覧のとおり、abstract キーワードには、クラスおよび仮想メソッド指定子としての 2 つの役割があります。クラスの先頭で、クラスのインスタンスをインスタンス化できないことを示す役割を果たします。SentientEntity は、特殊な派生クラスの階層のルートとして機能する不完全なクラスです。EEK! では、この共通の抽象基本クラスを使用して、特定の種類の感覚を持つ実体 (ネコやネズミなど) を操作します。その実体の実際の性質を認識する必要はありません。これはまさに、理想的なオブジェクト指向設計の本質です。
各関数プロトタイプ内で、仮想メソッドのシグネチャの後に置かれた abstract キーワードは、純粋仮想関数であることを示します。つまり、このメソッドには実装が関連付けられておらず、SentientEntity から派生するすべての具象クラス (Mouse など) は独自の実装を用意する必要があります。
こうした abstract キーワードの使い方によって、クラス設計者の意図が、次に示す同等の ISO-C++ の宣言よりもわかりやすく強調されています。
class SentientEntity : public ISimulation {
public:
    // ...
    virtual void onInit() = 0;
    virtual void onTick()= 0;
    virtual void onTerminate()= 0;
    virtual void onLog( int levelOfDetail ) = 0;
};
Stroustrup 氏は、1980 年代後半にリリース 2.0 と呼ばれるようになった C++ の元々の設計で実際に abstract というキーワードを導入しましたが、それは cfront 2.0 (私が携わった Stroustrup 氏の C++ コンパイラ) のリリース前の最後の数週間でした。しかし、リリース日が間近に迫り過ぎていたため、大量のコードを壊す可能性のある予約キーワードは設定できないと判断されました。
コンテキスト キーワード以外のキーワードは、予約語として機能するため、クライアントのプログラム内では使用できません。 そのため、C++/CLI の ref や value などのキーワードは、予約キーワードではなくコンテキスト キーワードとなりました。
何らかの理由で、元々の ISO-C++ 標準では抽象キーワードを再導入しないことになりました。私は、現在使用されている改訂版の ISO-C++ 標準でこの問題が再び取り上げられることを期待しています。C++/CLI で抽象キーワードを使用すると、純粋仮想関数の仕様が ISO-C++ 構文よりも洗練されていることがはっきりします。
SentientEntity の実装の完成に必要な最後のメソッドは、コンストラクタおよびデストラクタと、コンストラクタの 1 つの引数へのハンドルを格納するデータ メンバ、およびそのメンバを取得するプロパティ宣言です (図 2 を参照)。
Figure 2 SentientEntity コンストラクタ
public ref class SentientEntity abstract
    : ISimulation {
public:
    ~SentientEntity();
    property Genome^ genome {
        Genome^ get(){ return _genome; }
    }
    // ... ISimulation methods from Figure 1
protected:
    SentientEntity( Genome^ );
private:
    Genome^ _genome;
};
オブジェクト指向クラス階層の基本的な設計パターンでは、ルート抽象基本クラス コンストラクタを public ではなく protected として宣言し、デストラクタを public と virtual の両方として宣言します。
EEK! では感覚を持つさまざまな実体を SentientEntity ハンドルを通じて間接的に操作するため、その実体に関連付けられたリソースを解放するには、一般的なプログラム内でデストラクタを呼び出せる必要があります。そのため、デストラクタは public である必要があります。
私のハンドルは SentientEntity のインスタンスではなく具象派生クラスのインスタンスを参照するため、不明な派生クラスのデストラクタを正しく呼び出すには、デストラクタが virtual である必要があります。もちろん、鋭い読者の皆さんは「図 2 ではデストラクタを virtual として宣言していない」と反論されるでしょう。
そのとおりですが、これは、「ref クラス デストラクタを virtual にする必要があるが、すべての ref クラスは Object から暗黙的に派生し、そのデストラクタは virtual であるため、C++/CLI 内のすべての ref クラス デストラクタは常に virtual として扱われる」ということを証明するためにこうしたのです。virtual でない ref クラス デストラクタは作成できません。
.NET と C++/CLI の両方のデストラクタについて説明しなければならないことは他にもありますが、派生した Mouse クラスを設計するまで保留にしましょう。
EEK! 内で操作する唯一のハンドルは SentientEntity ですが、SentientEntity は抽象クラスであるため、実際には SentientEntity のインスタンスの作成は許可されていません。
// illegal: SentientEntity is abstract
Genome^ getGenome( System::String^ );
SentientEntity^ mw = gcnew SentientEntity( getGenome( "Mouse" ));
したがって、そのような無効な呼び出しを防ぐため、コンストラクタを protected として宣言します。プロテクト メンバは、一般的なプログラムからはアクセスできませんが、その派生クラスには完全にアクセス可能です。SentientEntity コンストラクタが呼び出される唯一の場所は、その派生クラス コンストラクタ (Mouse 派生クラスのコンストラクタなど) の初期化リスト内です。
基本クラスのプライベート メンバがその派生クラスに直接アクセスすることはできません。genome メンバをプライベートにして、すべてのアクセスが (可能であれば) インライン プロパティ get を通じて行われるようにします (図 2 を参照)。このような手段で、アクセスをインストルメント化する場合は、適切なカプセル化を行い、すべてのコードの操作を簡単に識別できる 1 つの場所にまとめます。
C++/CLI には、get をフィールド戻り、set をフィールド値割り当てとして実装する、便利で簡単なプロパティの表記法があります。これは trivial プロパティと呼ばれます。このプロパティを次に示します。
public value class SmallInt {
public:
    // trivial property
    property int Value;
};
次に示すように、内部的には、このプロパティは単純な get/set アクセサのペアと共に、プロパティ型のアクセス不能な補助ストア フィールドに拡張されます。
public value class SmallInt {
    int backingStore;
public:
    // pseudo-internal expansion of trivial property
    property int Value{
        int get{ return backingStore; }
        void set( int value ){ backingStore = value; }
    }
};
ただし、trivial プロパティを get アクセサのみの生成に制限することはできません (これをサポートするには、初期値が指定されるように宣言の構文を拡張する必要があります。現時点で、補助ストアを初期化する値をコンパイラに対して指定する方法はありません)。そのため、set アクセサがアプリケーションに対して安全である場合のみ、trivial プロパティを使用します。この例では、安全ではありません。
最後に、コンストラクタと onInit およびデストラクタと onTerminate の関係の問題があります。onInit と onTerminate は両方とも仮想関数です。ここに挙げる問題は、コンストラクタ内部からの onInit の呼び出しが適切であるかということです。
Stroustrup 氏による最初の C++ の設計では、基本クラス コンストラクタ内で仮想関数を呼び出すと、派生クラスのインスタンスが呼び出されていました。結局のところ、これは仮想関数の動作の通常のセマンティクスを反映していました。また、実装も簡単です。彼が最初に見落としていたのは、クラスの初期化が通常のセマンティクスを反映していないということでした。もし反映されていれば、virtual コンストラクタを使用できたでしょう。
問題は、C++ ではクラスがボトムアップ式で作成されることです。SentientEntity コンストラクタの実行中は、Mouse コンストラクタがまだ呼び出されていません。そのため、SentientEntity コンストラクタ内で Mouse の onInit を呼び出すと、不定の動作が発生します。リリース 2.0 で、Stroustrup 氏は言語の動作を再定義し、基本クラス コンストラクタ (またはデストラクタ) 内で呼び出された仮想関数が、派生クラスではなく基本クラスに関連付けられたインスタンスを呼び出すようにしました。
一方、.NET では、呼び出される仮想インスタンスは派生クラスのインスタンスです。そのため、基本クラスの仮想メソッドが派生クラスのコンストラクタから呼び出される場合には、インスタンスが完全に作成される前に、つまり、仮想メソッドが依存する状態が完全に初期化される前に、仮想メソッドが実行することが起こり得ます。そのため、マイクロソフト社内の CLR チームでは、決してコンストラクタ内で仮想関数を呼び出さないことをお勧めします。
onInit とコンストラクタ (または onTerminate とデストラクタ) のもう 1 つの違いは、2 つの ISimulation メソッドがシミュレーションに対してクラス インスタンスを初期化 (または終了) するのに対して、コンストラクタ (またはデストラクタ) はクラス インスタンスの内部状態を設定または破壊するということです。
この疎結合の設計方法の利点は、操作対象の感覚を持つ実体とは無関係にシミュレーションのエンジンを実装できることです。開発者のグループにたとえると、これは設計領域の機能パーティションを表します。UI インターフェイスは、3 番目の機能パーティションを表します。

ご意見やご質問は、

Stanley purecpp@microsoft.com.

まで英語でお送りください。
Stanley B. Lippman は、C++ の考案者である Bjarne Stroustrup と共に 1980 年代から 1990 年代にかけてベル研究所で C++ に携わった実績と、現在までに 4 版を重ねている入門書『C++ Primer』(Addison-Wesley、2005 年) の著述で知られています。マイクロソフトの Visual C++ チームにおいて、このコラムの焦点である C++/CLI の考案に取り組んだ経歴の持ち主でもあります。


.NET の問題
Windows Vista での IFileOperation
Stephen Toub

コードのダウンロード : : NetMatters2007_12.exe (160 KB)
Browse the Code Online

Q バッチとして実行する多数のファイル操作があります。この操作に、進行状況を表す標準の Windows® UI を使用したいと考えています。System.IO 名前空間のクラスを使用すれば、すべてのファイル操作を順番に行えることはわかっていますが、今回はより多くの作業を行うために、進行状況を表す独自の UI を作成する必要があります。Windows Vista® には新しい IFileOperations インターフェイスがありますが、これをマネージ コードから使用する方法を示すサンプルがありません。これはどのように行えばよいでしょうか?
Q バッチとして実行する多数のファイル操作があります。この操作に、進行状況を表す標準の Windows® UI を使用したいと考えています。System.IO 名前空間のクラスを使用すれば、すべてのファイル操作を順番に行えることはわかっていますが、今回はより多くの作業を行うために、進行状況を表す独自の UI を作成する必要があります。Windows Vista® には新しい IFileOperations インターフェイスがありますが、これをマネージ コードから使用する方法を示すサンプルがありません。これはどのように行えばよいでしょうか?
A 確かに、Windows Vista にはあなたが行おうとしている作業をサポートする新しいコピー エンジンがあります。しかし、既存の機能でもあなたの要求を満たすことができる可能性があります。たとえば、個々のファイルまたはディレクトリをコピー、移動、名前変更、削除する場合は、Visual Basic® ランタイムに既にラップされている SHFileOperation (shell32.dll から公開) を利用できます。Visual Basic 2005 をお使いの場合は、次のような My 名前空間の機能を使用できます。
A 確かに、Windows Vista にはあなたが行おうとしている作業をサポートする新しいコピー エンジンがあります。しかし、既存の機能でもあなたの要求を満たすことができる可能性があります。たとえば、個々のファイルまたはディレクトリをコピー、移動、名前変更、削除する場合は、Visual Basic® ランタイムに既にラップされている SHFileOperation (shell32.dll から公開) を利用できます。Visual Basic 2005 をお使いの場合は、次のような My 名前空間の機能を使用できます。
My.Computer.FileSystem.CopyDirectory(
    sourcePath, destinationPath, UIOption.AllDialogs)
C# で同じことを行うには、いくつかの処理が必要です。具体的には、Microsoft® .NET Framework のインストール ディレクトリの Microsoft.VisualBasic.dll への参照を追加して、次のようなコードを使用します。
using Microsoft.VisualBasic.FileIO;
...
FileSystem.CopyDirectory(
    sourcePath, destinationPath, UIOption.AllDialogs);
このコードを実行すると、Windows エクスプローラでファイル操作をした場合と同様の、進行状況を示す UI が表示されます。実際は、Windows Vista で実行すると、図 1 のように、進行状況を示す Window Vista の新しい UI が自動的に取得されます。
図 1 Windows Vista の進行状況ダイアログ
図 1 Windows Vista の進行状況ダイアログ
SHFileOperation および .NET が提供するラッパーの使用は、呼び出しごとに 1 つのファイル システム オブジェクト (ディレクトリ) と 1 つの操作 (コピー、移動、名前変更、削除) に制限されます。同じ操作内で異なるファイルを混在させたり、操作したりすることはできません。そこで IFileOperation の出番です。IFileOperation は SHFileOperation の後継であり、多数の操作のバッチ処理を含む豊富な新機能が用意されています。このバッチ処理では、項目のコピー、名前変更、移動、削除、および新規作成を組み合わせることができます。また、同じディレクトリ内のファイルだけではなく、任意のファイル セットで操作を行うことができます。これらは、IFileOperation を通じて提供される便利な機能のほんの一部です。詳細については、shellrevealed.com/blogs/shellblog/archive/2007/04/16/IFileOperation-_1320_-Part-1_3A00_-Introduction.aspx を参照してください。
残念ながら、ご指摘のとおり、この記事の執筆時点では、マネージ コードからこの機能を公開するためのマネージ ラッパーはマイクロソフトから提供されていません。このため、私が 1 つ作成してみました。そのコードは MSDN® Magazine Web サイトからダウンロードできます。この回答の以降の部分では、そのコードの使い方およびコードをどのように実装したかについて説明します。私が作成したラッパーは IFileOperation のすべての機能を公開するものではありません。また、基になるすべてのビットやバイトを完全に抽象化し、標準的な .NET の設計ガイドラインに従った最適な .NET ラッパーを提供するものでもありません。単に IFileOperation の機能を簡単に公開し、Windows Vista が対象とする現在のアプリケーションの基本を使用できるようにします。
まずは、IFileOperation インターフェイス自体について説明しましょう。図 2 は、マネージ定義を示しています (これは Windows SDK の ShObjIdl.idl ファイルの IDL に基づいています)。このインターフェイスの設計では、実行するすべての操作のスケジュールをインターフェイスを使用して作成し、準備が整ったら、すべての操作を実行するようインターフェイスに指示します。この時点で、実行が開始します。操作にかなりの時間がかかる可能性のある場合は、進行状況を示す Windows Vista の標準のダイアログが表示されます。このダイアログには実行中の操作に関する説明が示されます。アクションのスケジュールを作成するには、IFileOperation のメソッドである CopyItem、MoveItem、RenameItem、DeleteItem、および NewItem を使用します (このインターフェイスは、これらのメソッドの複数形バージョンも公開しますが、必要に応じてその動作は単数形バージョンでシミュレートでき、マネージ コードからの使用もさらに難しくなるため、ここではこれ以上は説明しません)。これらの各メソッドは単に操作を発生させるという意図を表現しますが、この時点では、実際にアクションは実行されません。すべての操作のスケジュールを作成したら、PerformOperations メソッドを使用してスケジュール済みのすべての操作を実行します。操作がどのように実行されるかについての詳細は、PerformOperations を呼び出す前に、IFileOperation で複数のメソッド (SetOwnerWindow や SetOperationFlags など) を使用して構成できます。IFileOperation は、便利なコールバック通知システムもサポートします。これにより、IFileOperationProgressSink を登録し、操作前や操作後の通知、進行状況の更新情報などを含む、すべての種類のイベントに関するコールバックを受け取ることができます。IFileOperation の Advise メソッドを使用してシンクを登録できます (登録済みの場合は Unadvise を使用して登録を解除します)。これにより、すべての操作の実行にシンクが使用されます。または、単数形の各操作メソッドの最後のパラメータを使用して、個々の操作についてシンクを登録できます。IFileOperationProgressSink の私が作成したマネージ定義は、図 3 に示されています (これも Windows SDK の ShObjIdl.idl ファイルに基づいています)。
Figure 3 IFileOperationProgressSink
[ComImport]
[Guid("04b0f1a7-9490-44bc-96e1-4296a31252e2")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IFileOperationProgressSink
{
    void StartOperations();
    void FinishOperations(uint hrResult);

    void PreRenameItem(uint dwFlags, IShellItem psiItem,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);
    void PostRenameItem(uint dwFlags, IShellItem psiItem,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
        uint hrRename, IShellItem psiNewlyCreated);

    void PreMoveItem(uint dwFlags, IShellItem psiItem, IShellItem
        psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);
    void PostMoveItem(uint dwFlags, IShellItem psiItem,
        IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
        uint hrMove, IShellItem psiNewlyCreated);

    void PreCopyItem(uint dwFlags, IShellItem psiItem,
        IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);
    void PostCopyItem(uint dwFlags, IShellItem psiItem,
        IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
        uint hrCopy, IShellItem psiNewlyCreated);

    void PreDeleteItem(uint dwFlags, IShellItem psiItem);
    void PostDeleteItem(uint dwFlags, IShellItem psiItem, uint hrDelete,
        IShellItem psiNewlyCreated);

    void PreNewItem(uint dwFlags, IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);
    void PostNewItem(uint dwFlags, IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
        [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName,
        uint dwFileAttributes, uint hrNew, IShellItem psiNewItem);

    void UpdateProgress(uint iWorkTotal, uint iWorkSoFar);

    void ResetTimer();
    void PauseTimer();
    void ResumeTimer();
}
Figure 2 IFileOperation
[ComImport]
[Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IFileOperation
{
    uint Advise(IFileOperationProgressSink pfops);
    void Unadvise(uint dwCookie);

    void SetOperationFlags(FileOperationFlags dwOperationFlags);
    void SetProgressMessage(
        [MarshalAs(UnmanagedType.LPWStr)] string pszMessage);
    void SetProgressDialog(
        [MarshalAs(UnmanagedType.Interface)] object popd);
    void SetProperties(
        [MarshalAs(UnmanagedType.Interface)] object pproparray);
    void SetOwnerWindow(uint hwndParent);

    void ApplyPropertiesToItem(IShellItem psiItem);
    void ApplyPropertiesToItems(
        [MarshalAs(UnmanagedType.Interface)] object punkItems);

    void RenameItem(IShellItem psiItem,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
        IFileOperationProgressSink pfopsItem);
    void RenameItems(
        [MarshalAs(UnmanagedType.Interface)] object pUnkItems,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);

    void MoveItem(
        IShellItem psiItem,
        IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
        IFileOperationProgressSink pfopsItem);
    void MoveItems(
        [MarshalAs(UnmanagedType.Interface)] object punkItems,
        IShellItem psiDestinationFolder);

    void CopyItem(
        IShellItem psiItem,
        IShellItem psiDestinationFolder,
        [MarshalAs(UnmanagedType.LPWStr)] string pszCopyName,
        IFileOperationProgressSink pfopsItem);
    void CopyItems(
        [MarshalAs(UnmanagedType.Interface)] object punkItems,
        IShellItem psiDestinationFolder);

    void DeleteItem(
        IShellItem psiItem,
        IFileOperationProgressSink pfopsItem);
    void DeleteItems(
        [MarshalAs(UnmanagedType.Interface)] object punkItems);

    uint NewItem(
        IShellItem psiDestinationFolder,
        FileAttributes dwFileAttributes,
        [MarshalAs(UnmanagedType.LPWStr)] string pszName,
        [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName,
        IFileOperationProgressSink pfopsItem);

    void PerformOperations();

    [return: MarshalAs(UnmanagedType.Bool)]
    bool GetAnyOperationsAborted();
}
これらのインターフェイスを直接マネージ コードから使用する方法について説明する前に、私がインターフェイスに実装したラッパーを示します。私が作成した FileOperation クラスの概要を、図 4 に示します。このクラスの使用例として、2 つのファイル (test1.txt、test2.txt) と 1 つのディレクトリ (SampleDir) を含む C:\test ディレクトリについて考えてみましょう。私は、最初のファイルを copy.text にコピーし、2 つ目のファイルを削除し、ディレクトリを NewDir に移動したいと考えています。この処理は、次のようなコードを使用して行うことができます。
Figure 4 マネージ FileOperation ラッパー
class FileOperation : IDisposable
{
    public FileOperation() : this(null);
    public FileOperation(FileOperationProgressSink callbackSink) :
        this(callbackSink, null);
    public FileOperation(
        FileOperationProgressSink callbackSink, IWin32Window owner);

    public void CopyItem(
        string source, string destination, string newName);

    public void MoveItem(
        string source, string destination, string newName);

    public void RenameItem(string source, string newName);

    public void DeleteItem(string source);

    public void NewItem(
        string folderName, string name, FileAttributes attrs);

    public void PerformOperations();

    public void Dispose();
}

using(FileOperation fileOp = new FileOperation())
{
    fileOp.CopyItem(@"C:\test\test1.txt", @"C:\test", @"copy.txt");
    fileOp.DeleteItem(@"C:\test\test2.txt");
    fileOp.MoveItem(@"C:\test\SampleDir", @"C:\test", @"NewDir");
    fileOp.PerformOperations();
}
さまざまなアクションに関するコールバック通知を受け取る場合は、FileOperation のコンストラクタを通じて進行状況のシンクを登録できます (ここで使用する FileOperationProgressSink クラスは、すべてのイベントをトレースするために私が指定した IFileOperationProgressSink の既定の実装です。このクラスについては、後で詳しく説明します)。
using(FileOperation fileOp =
    new FileOperation(new FileOperationProgressSink()))
{
    ...
}
また、このクラスを Windows フォーム アプリケーションで実行していて、進行状況ウィンドウを現在のフォームに関連付ける場合は、次のようなコードを使用できます。
Form form = this;
using(FileOperation fileOp = new FileOperation(null, form))
{
    ...
}
簡単だと思いませんか。それでは、実装方法を見てみましょう。IFileOperation は、CLSID_FileOperation で識別される COM コンポーネントによって Windows Vista に実装される COM インターフェイスです。次のようなコードを使用して、そのインスタンスを作成できます。
private static readonly Guid CLSID_FileOperation =
    new Guid("3ad05575-8857-4850-9277-11b85bdb8e09");
private static readonly Type FileOperationType =
    Type.GetTypeFromCLSID(CLSID_FileOperation);

public FileOperation(
    FileOperationProgressSink callbackSink, IWin32Window owner)
{
    _fileOperation = (IFileOperation)
        Activator.CreateInstance(FileOperationType);
    ...
}
作成された IFileOperation は _fileOperation メンバ変数に格納され、クラス内のすべての操作に使用できます。たとえば、CopyItem メソッドは次のように実装されます。
public void CopyItem(string source, string destination, string newName)
{
    ThrowIfDisposed();
    using (ComReleaser<IShellItem> sourceItem =
        CreateShellItem(source))
    using (ComReleaser<IShellItem> destinationItem =
        CreateShellItem(destination))
    {
        _fileOperation.CopyItem(
            sourceItem.Item, destinationItem.Item, newName, null);
    }
}
ThrowIfDisposed と ComReleaser をしばらくの間無視して、CopyItem は最初にソース パスの IShellItem インスタンスとターゲット フォルダの IShellItem を作成し、それらを (項目の新しい名前と共に) IFileOperation の CopyItem メソッドに渡します。FileOperation のすべての操作メソッドはこのように実装され、PerformOperations は基になる IFileOperation の同じメソッドにデリゲートします。
public void PerformOperations()
{
    ThrowIfDisposed();
    _fileOperation.PerformOperations();
}
CopyItem の実装に示す ComReleaser クラスは、Marshal.FinalReleaseComObject を使用した COM オブジェクトの破棄を簡単にする小型のヘルパ クラスです (オブジェクトは最終的にはガベージ コレクタによってクリーンアップされますが、これらがクリーンアップされる可能性のある頻度を考慮して、不要になったらすぐに破棄されるようにしました)。型のコンストラクタは COM オブジェクトを受け取り、プライベート メンバ変数にキャッシュします。次に、Dispose メソッドがオブジェクトを解放します。また、このクラスはオブジェクトを返す Item プロパティを公開します。これにより、例に示すような状況でプロパティを使いやすくなります。ComReleaser を図 5 に示します。
Figure 5 COM オブジェクトを解放するためのヘルパ クラス
class ComReleaser<T> : IDisposable where T : class
{
    private T _obj;

    public ComReleaser(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        if (!obj.GetType().IsCOMObject)
            throw new ArgumentOutOfRangeException("obj");
        _obj = obj;
    }

    public T Item { get { return _obj; } }

    public void Dispose()
    {
        if (_obj != null)
        {
            Marshal.FinalReleaseComObject(_obj);
            _obj = null;
        }
    }
}
特定のファイルまたはディレクトリ パスの ComReleaser<IShellItem> を取得するために、shell32.dll から Windows Vista にエクスポートされた SHCreateItemFromParsingName 関数を使用します。この関数にファイル システム オブジェクトのパスを指定して、適切な IShellItem を返すようにすることができます。私が作成した CreateShellItem メソッドでは、この IShellItem を ComReleaser<IShellItem> で単にラップします。
private static ComReleaser<IShellItem> CreateShellItem(string path)
{
    return new ComReleaser<IShellItem>((IShellItem)
        SHCreateItemFromParsingName(path, null, ref _shellItemGuid));
}

[DllImport("shell32.dll", SetLastError=true,
           CharSet=CharSet.Unicode, PreserveSig=false)]
[return: MarshalAs(UnmanagedType.Interface)]
private static extern object SHCreateItemFromParsingName(
    [MarshalAs(UnmanagedType.LPWStr)] string pszPath,
    IBindCtx pbc, ref Guid riid);
これで、残りは FileOperationProgressSink の実装の説明だけになりました。図 3 に示すように、FileOperationProgressSink クラスは IFileOperationProgressSink インターフェイスを実装します。
public class FileOperationProgressSink : IFileOperationProgressSink
{
   ...
}
そのためには、インターフェイスの各メソッドに仮想メソッドの実装を指定します。これにより、イベントに応答するカスタム動作を指定する場合は、FileOperationProgressSink を派生させるだけでかまいません。例として、PreRenameItem は、項目の名前が変更される前に呼び出されるメソッドです。
public virtual void PreRenameItem(
    uint dwFlags, IShellItem psiItem, string pszNewName)
{
#if DEBUG
    string message = string.Format("Renaming {0} to {1}",
        item.GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY), pszNewName);
    Debug.WriteLine(message);
#endif
}
ただし、このメソッドは、独自の進行状況のシンクにおいて選択した方法でオーバーライドできます。
public class MyProgressSink : FileOperationProgressSink
{
    public virtual void PreRenameItem(
        uint dwFlags, IShellItem psiItem, string pszNewName)
    {
        Console.WriteLine("Goodbye, {0}", item.GetDisplayName(0));
    }
}
次に、FileOperation クラスをインスタンス化するときに、シンクのインスタンスを指定します。
using(FileOperation fileOp = new FileOperation(new MyProgressSink()))
{
    ...
}
全体として見ると、Windows Vista チームは、マネージ コードから使いやすい IFileOperation API の設計を実現しました。しかし、ここでは公開していない機能はたくさんあります。たとえば、操作ごとの進行状況のシンク (公開していません) だけでなく、IFileOperation::Advise の複数回の呼び出し (登録されているシンクごとに 1 回) によるすべての操作への複数のシンクの提供もサポートする Windows Vista の IFileOperation などです。さらに、IFileOperation では、ディレクトリの再帰などの動作、ダイアログに表示される項目とその方法、衝突の処理方法などを制御する未公開の多数の操作フラグをサポートします。また、私がラッパーを提供していない追加の操作 (項目プロパティの変更など) も公開します (SetProperties、ApplyPropertiesToItem、ApplyPropertiesToItems の各メソッドを通じて行います)。さらに、IFileOperation は、実行されたそれぞれの操作に関する詳細なエラー情報を提供します。このような機能が必要な場合は、MSDN Magazine Web サイトからコードをダウンロードし、必要に応じて変更できます。
FileOperation の使用に関する最後の注意事項は、IFileOperation とそのサポート対象の型 (および私が作成したマネージ FileOperation クラス) はシングル スレッド アパートメント (STA) でのみ使用可能であるということです。多くの Windows フォームではスレッドが STA である必要があるため、Windows フォーム アプリケーションで使用している場合、これは問題になりません。マルチスレッド アパートメント (MTA) で実行する必要がある場合は、引き続き SHFileOperation を使用できます。また、IFileOperation コードを実行するために新しいスレッドを適切なスレッド モデルでスピンできます。その例については、2004 年 12 月の .NET の問題の私のコラム (msdn.microsoft.com/msdnmag/issues/04/12/NETMatters) の中の ApartmentStateSwitcher クラスに関する説明を参照してください。

ご意見やご質問は、

Stephen netqa@microsoft.com.

まで英語でお送りください。
Stephen Toub は、マイクロソフトの並列コンピューティング プラットフォーム チームのシニア プログラム マネージャです。MSDN Magazine の寄稿編集者でもあります。


Windows と C++
Windows Template Library 8.0
Kenny Kerr

コードのダウンロード : : WindowsWithC++2007_12.exe (175 KB)
Browse the Code Online
コンテンツ
タスク ダイアログ
Aero ウィザード
新しいファイル ダイアログ
その他の WTL 機能の紹介

Windows® Template Library (WTL) の開発者は、Windows Vista® で導入された新しいユーザー インターフェイス機能および拡張のほとんどすべてを完全にサポートするこの優れたライブラリの新しいバージョンを最近リリースしました。今月は、この最新リリースのいくつかの新機能に焦点をあてます。WTL および WTL の Visual C++® 開発環境での利用について馴染みのない読者のために、手短な説明から始めます。
Visual C++ チームは、可能な限り少量で高速なコードを生成する一方で COM クライアントおよびサーバーを簡単に作成できるように、アクティブ テンプレート ライブラリ (ATL) を開発しました。ユーザー インターフェイス アプリケーションの開発を重視し、後になって COM のサポートが追加された MFC とは異なり、ATL は最初から COM 開発を念頭に設計され、成果を上げています。ATL では、ユーザー インターフェイス開発については基本的なサポートしかありませんが、提供されるクラスは、より洗練されたユーザー インターフェイス ライブラリの軽量な構成単位や基本クラスのように非常に便利です。
WTL は、想像し得る限りシンプルまたは複雑な、アプリケーション構築のための非常に豊富なクラス テンプレートのセットにより、ATL を拡張します。WTL は Visual C++ に付属していませんが、SourceForge Web サイト (sourceforge.net/projects/wtl) または Microsoft ダウンロード センターのいずれでも入手できます。
WTL 8.0 は、本来 Windows Vista UI サポートの目的で開発されました。そこで、以前のコラムや記事で筆者が扱ったいくつかのトピックに触れたいと思います。しかしながら、今月の重要な点は、新しく強化された WTL がこれらの新機能の導入をいかに促進し、開発者自身が作成する必要のあるコードがいかに削減されるかということです。MSDN® Magazine Web サイトから入手可能なこのコラムのダウンロードには、この記事で説明するほとんどの機能のサンプルが含まれています。

タスク ダイアログ
タスク ダイアログは、Windows Vista の筆者のお気に入りの新機能の 1 つで、シンプルで洗練されたダイアログを非常に簡単に開発できる強力な API です。
TaskDialogIndirect 関数と TASKDIALOGCONFIG 構造は非常に困難で、簡単に使用するには C++ の助けを必要とします。WTL は CTaskDialogImpl 基本クラス テンプレートを通じてタスク ダイアログのサポートを提供します。内部的には、CTaskDialogImpl クラスは CTaskDialogConfig クラスを使用して TASKDIALOGCONFIG 構造をラップします。直接のアクセスが必要な場合、この構造はパブリック変数 m_tdc を通じて公開されますが、CTaskDialogImpl クラスの優れた動作によりラップのための適切なメンバ関数が提供されるため、あまり必要性はありません。非常に単純なシナリオ用に CTaskDialogImpl から派生する CTaskDialog 具象クラスも提供されますが、通常は、単純に CTaskDialogImpl からクラスを派生してタスク ダイアログ ボックスを実装するのが最善です。
図 1 は、タスク ダイアログのいくつかの機能と、WTL からそれらの機能を利用する方法を示します。サンプルは、ウィンドウのタイトル、メイン インストラクション、およびコンテンツのテキスト キャプションを設定します。タスク ダイアログではいくつかの追加テキスト キャプションがサポートされますが、それらを設定するにはタスク ダイアログが構築されているか否かに応じて異なるメカニズムが必要になります。残念ながら CTaskDialogImpl クラスではこれらの違いが抽象化されないため、これらのテキスト キャプションを更新する場合に若干混乱することがあります。図 2 は、タスク ダイアログが構築前または構築後であるかに応じて、CTaskDialogImpl クラスを使用してさまざまなテキスト キャプションを設定する方法を要約しています。通常は、タスク ダイアログのコンストラクタの構築前メソッドと、各種イベント ハンドラの構築後メソッドを使用して、キャプションを更新します。
Figure 2 タスク ダイアログ キャプションの更新

テキスト キャプション | 構築前 | 構築後
ウィンドウ タイトル | SetWindowTitle() | ::SetWindowText()
メイン インストラクション | SetMainInstructionText() | SetElementText(TDE_MAIN_INSTRUCTION, ...
コンテンツ | SetContentText() | SetElementText(TDE_CONTENT, ...
フッター | SetFooterText() | SetElementText(TDE_FOOTER, ...
展開された情報 | SetExpandedInformationText() | SetElementText(TDE_EXPANDED_INFORMATION, ...
展開されたコントロール テキスト | SetExpandedControlText() | N/A
折りたたまれたコントロール テキスト | SetCollapsedControlText() | N/A
検証 | SetVerificationText() | N/A
Figure 1 タスク ダイアログのサンプル
enum
{
    Button_Save = 101,
    Button_DontSave,
};

class SampleDialog : public CTaskDialogImpl<SampleDialog>
{
public:

    SampleDialog()
    {
        SetWindowTitle(L"Title");
        SetMainInstructionText(L"Main instruction");
        SetContentText(L"Content");

        ModifyFlags(0, TDF_ALLOW_DIALOG_CANCELLATION |
                       TDF_USE_COMMAND_LINKS);

        static TASKDIALOG_BUTTON buttons[] =
        {
            { Button_Save, L"&Save", },
            { Button_DontSave, L"Do&n't save", },
        };

        SetButtons(buttons,
                   _countof(buttons));
    }
};
図 1 タスク ダイアログのサンプル
図 1 タスク ダイアログのサンプル
図 1 のコードでは ModifyFlags メソッドも使用されます。タスク ダイアログの外観をカスタマイズするための各種フラグが Windows SDK に文書化されています。ModifyFlags メソッドの第 1 パラメータは削除するフラグを示し、第 2 パラメータは追加するフラグを示します。タスク ダイアログに表示されるボタンを指定する SetButtons メソッドも使用されます。サンプルでは、コンストラクタの前に記憶域を確保するため静的な TASKDIALOG_BUTTON 構造の配列が宣言されます。これは、タスク ダイアログを実際に表示するため最後にすべての情報を TaskDialogIndirect 関数に渡す DoModal メソッドの呼び出しで、後からボタン定義が参照されるために不可欠です。
DoModal メソッドの呼び出しは簡単で、ユーザーが選択したボタンの ID をオプションで指定できます。図 3 は、図 1 のタスク ダイアログを表示し、さまざまなボタン ID を処理する方法の例を示します。
Figure 3 DoModal を使用したダイアログ関数の呼び出し
SampleDialog dialog;
int selectedButtonId = 0;

COM_VERIFY(dialog.DoModal(
    0,                   // parent window
    &selectedButtonId));

switch (selectedButtonId)
{
    case Button_Save:
    {
        // TODO: save
        break;
    }
    case Button_DontSave:
    {
        // TODO: don't save
        break;
    }
    case IDCANCEL:
    {
        // TODO: cancel
        break;
    }
    default:
    {
        ASSERT(false);
    }
}
または、異なるイベントを処理するためオプションで派生クラスに実装できるパブリック メソッドのセットを通じて CTaskDialogImpl クラスが公開するタスク ダイアログのコールバック機能を使用できます。たとえば、ダイアログ クラス内部のボタン クリックを処理するために、図 4 に示すパブリック メソッドを追加できます。
Figure 4 クリック イベントの処理
class SampleDialog : public CTaskDialogImpl<SampleDialog>
{
public:

...

    bool OnButtonClicked(int buttonId)
    {
        bool closeDialog = false;

        switch (buttonId)
        {
            // Do something here
        }

        return !closeDialog;
    }
};
このイベントの処理は、特定のボタン クリックを処理する必要があり、タスク ダイアログを閉じる必要がない場合に特に有効です。他の多数のイベントも、同様の方法で処理できます。図 5 は、CTaskDialogImpl クラスが提供するオーバーライドのリストを示します。これらはコンパイル時に評価される ATL 形式のオーバーライドであり、仮想関数の呼び出しを必要としません。
Figure 5 CTaskDialogImpl のオーバーライド

メソッド | 説明
void OnDialogConstructed() | タスク ダイアログは作成されているが、まだ表示されていない。ウィンドウ ハンドルが有効である。
bool OnButtonClicked(int buttonId) | ボタンまたはコマンド リンクがクリックされた。ダイアログが閉じないように true を返す。
void OnRadioButtonClicked(int buttonId) | ラジオ ボタンがクリックされた。
void OnHyperlinkClicked(PCWSTR url) | ハイパーリンクがクリックされた。
void OnExpandoButtonClicked(bool expanded) | Expando ボタンがクリックされた。
void OnVerificationClicked(bool checked) | 検証チェックボックスがオンまたはオフにされた。
void OnHelp() | F1 キーが押された。
bool OnTimer(DWORD milliseconds) | 200 ミリ秒ごとに送信される。パラメータはタイマの開始またはリセット後のミリ秒を示す。
void OnNavigated() | ナビゲーションが発生した。
void OnDestroyed() | タスク ダイアログが破棄された。ウィンドウ ハンドルが無効になった。
タスク ダイアログは、他にも多くの機能を提供します。このコラムのサンプル コードには、WTL を利用するタスク ダイアログのほとんどすべての機能を示す広範なサンプルのセットが含まれています。タスク ダイアログ API の広範な機能の詳細については、筆者のブログ投稿 "Windows Vista for Developers–Part 2–Task Dialogs in Depth" (weblogs.asp.net/kennykerr/archive/2006/07/18/Windows-Vista-for-Developers-_1320_-Part-2-_1320_-Task-Dialogs-in-Depth.aspx) を参照してください。

Aero ウィザード
Windows Vista に、共通ウィザードの最新の外観が導入されました。従来のウィザード標準と同様、AeroTM ウィザードはプロパティ シート API のバリエーションにすぎません。このため、新しいユーザー インターフェイスを利用するためのアプリケーションの更新が非常に簡単です。WTL ではすべての新機能をラップするクラス テンプレートのセットが提供されるため、さらに簡単に行えます。
CAeroWizardFrameImpl クラス テンプレートからウィザード ウィンドウ クラスを派生するだけで、新しい Aero ユーザー インターフェイスを使用するよう Windows に通知する適切なフラグが適用されます。CAeroWizardFrameImpl は、すべての共通機能を提供する CPropertySheetImpl から派生します。ウィザード ページは、新しい Aero ウィザード ボタンの制御メソッドを提供する CAeroWizardPageImpl クラスから派生します。図 6 は、単純ですが完全な、単一ページの Aero ウィザードの例を示します。
Figure 6 単純な Aero ウィザード
class SamplePage :
    public CAeroWizardPageImpl<SamplePage>
{
public:

    BEGIN_MSG_MAP(SamplePage)
        MESSAGE_HANDLER_EX(WM_INITDIALOG, OnInitDialog)
        CHAIN_MSG_MAP(__super)
    END_MSG_MAP()

    enum { IDD = IDD_SAMPLE_PAGE };

    SamplePage()
    {
        VERIFY(m_title.LoadString(IDS_PAGE_TITLE));
        SetHeaderTitle(m_title);
    }

private:

    LRESULT OnInitDialog(UINT /*message*/,
                         WPARAM /*wParam*/,
                         LPARAM /*lParam*/)
    {
        ShowWizardButtons(
            PSWIZB_BACK | PSWIZB_NEXT | PSWIZB_FINISH | PSWIZB_CANCEL,
            PSWIZB_BACK | PSWIZB_NEXT | PSWIZB_CANCEL);

        EnableWizardButtons(PSWIZB_BACK,
                            0);

        SetButtonText(PSWIZB_NEXT,
                      L"&Run");

        return TRUE;
    }

    CString m_title;
};

class SampleWizard :
    public CAeroWizardFrameImpl<SampleWizard>
{
public:

    BEGIN_MSG_MAP(SampleWizard)
        CHAIN_MSG_MAP(__super)
    END_MSG_MAP()

    SampleWizard() :
        CAeroWizardFrameImpl<SampleWizard>(IDS_WIZARD_TITLE)
    {
        VERIFY(AddPage(m_page));
    }

private:

    SamplePage m_page;
};
図 6 単純な Aero ウィザード
図 6 単純な Aero ウィザード
ご覧のとおり、新しい議論は少なく、Aero ウィザードへの移行は極めて簡単です。主なタスクは、Aero ウィザードで使用する標準ボタンの管理です。サンプルでは、ShowWizardButtons、EnableWizardButtons、SetButtonText の各メソッドを示します。ShowWizardButtons は 1 つ以上のボタンを表示または非表示にします。第 1 パラメータは操作するボタンを示し、第 2 パラメータはこれらのうち表示するボタンを示します。同様に、EnableWizardButtons の第 1 パラメータは操作するボタンを示し、第 2 パラメータはこれらのうち有効にするボタンを示します。残りのボタンは無効にされます。最後に、SetButtonText は [Next]、[Finish]、および [Cancel] の各ボタンのテキスト キャプションを更新します。

新しいファイル ダイアログ
Windows Vista が提供する新しいファイル ダイアログは、新しい Windows エクスプローラのユーザー インターフェイスと整合性のある新しい外観を持ち、何年も使用されてきた古い GetOpenFileName および GetSaveFileName 関数に代わるものとして期待されています。新しいファイル ダイアログは、COM インターフェイスのセットを通じて公開されるため、直接使用するのに特に困難はありません。ただし、ファイル ダイアログをポップアップする必要が生じるたびに同じ定型 COM コードを書くとなると、特に COM インターフェイスの実装を指定するよう要求するさまざまなファイル ダイアログ イベントに応答する必要がある場合は、面倒になります。幸い、優れた WTL により、簡単に使用できるクラスのセットにこれらのすべてがラップされています。図 7 に、ファイルを開くダイアログの実装例を示します。
Figure 7 ファイルを開くダイアログ
class SampleOpenDialog :
    public CShellFileOpenDialogImpl<SampleOpenDialog>
{
public:

    SampleOpenDialog()
    {
        COMDLG_FILTERSPEC fileTypes[] =
        {
            { L"Text Documents", L"*.txt" },
            { L"All Files", L"*.*" }
        };

        COM_VERIFY(GetPtr()->SetFileTypes(_countof(fileTypes),
                                          fileTypes));
    }
};
図 7 ファイルを開くダイアログ
図 7 ファイルを開くダイアログ (画像を拡大するには、ここをクリックします)
COMDLG_FILTERSPEC 構造は、GetOpenFileName で使われている複数文字列方式に比べ、ファイル タイプを指定するためのより管理しやすい形式を提供します。これらの構造の配列が SetFileTypes メソッドに渡されて、ダイアログがフィルタを提供するファイル タイプが指定されます。GetPtr メソッドは、ダイアログの外観と動作のカスタマイズに使用される、ダイアログの IFileOpenDialog インターフェイス ポインタを返します。
新しいファイル ダイアログでは、さまざまなイベントに応答することもできます。ただし、これらのイベントを受信するには、IFileDialogEvents インターフェイスを実装する必要があります。幸い、これらは WTL の CShellFileDialogImpl 基本クラス テンプレートを通じて実装されており、必要なイベントのハンドラを指定するだけですみます。
図 8 は、CShellFileDialogImpl クラスが提供するオーバーライドのリストを示します。たとえば、ファイルを開くダイアログを閉じる前に、選択したファイルを許容できるかどうかを検証する必要があるとします。これを簡単に行うには、ダイアログが結果を返す直前に OnFileOk イベントを呼び出し、選択したファイルが許容できないと判断した場合にダイアログが閉じないようにします。図 9 の例は、この方法を示します。
Figure 9 OnFileOk イベントの処理
class SampleOpenDialog :
    public CShellFileOpenDialogImpl<SampleOpenDialog>
{
public:

    // ...

    HRESULT OnFileOk()
    {
        CString filePath;
        COM_VERIFY(GetFilePath(filePath));

        // Validate file here...
        bool acceptable = ...

        return acceptable ? S_OK : S_FALSE;
    }
};
Figure 8 CShellFileDialogImpl のオーバーライド

メソッド | 説明
HRESULT OnFileOk() | ユーザーが選択を行った。
HRESULT OnFolderChanging(IShellItem*) | フォルダ移動の防止を有効にする。
HRESULT OnFolderChange() | ユーザーの新しいフォルダへの移動を示す OnFolderChanging の後に呼び出される。
HRESULT OnSelectionChange() | ユーザーがダイアログのビューで選択内容を変更する。
HRESULT OnShareViolation(IShellItem*, FDE_SHAREVIOLATION_RESPONSE*) | 違反の共有の処理を有効にする。
HRESULT OnOverwrite(IShellItem*, FDE_OVERWRITE_RESPONSE*) | ユーザーがファイルの上書きを選択する。
最後に、次のように、ファイルを開くダイアログを使用できます。
SampleOpenDialog dialog;

if (IDOK == dialog.DoModal())
{
    CString filePath;
    COM_VERIFY(dialog.GetFilePath(filePath));

    // Use file here...
}
新しいファイル ダイアログは、さまざまな COM インターフェイスを通じて他の多くの機能を提供します。新しいファイル ダイアログ API で提供される機能の詳細については、筆者のブログ投稿 "Windows Vista for Developers–Part 6–The New File Dialogs" (weblogs.asp.net/kennykerr/archive/2006/11/10/Windows-Vista-for-Developers-_1320_-Part-6-_1320_-The-New-File-Dialogs.aspx) を参照してください。

その他の WTL 機能の紹介
「Windows と C++」の以前のコラムで、Windows Vista の多くのコントロールの拡張を紹介しました (msdn.microsoft.com/msdnmag/issues/07/08/WindowsCPP)。現在は WTL で、新しいメッセージと通知を処理する更新されたクラスとマクロによって、ほとんどすべての新機能がラップされています。最新版の WTL には、実装からサイズ変更可能なダイアログのサポートまで、他にもすばらしい機能があり、新しいタブ ビュー クラスおよび多くの便利な追加項目や改善項目があります。数分を割いて WTL の項目を調べてください。WTL が初めての読者なら、この優れた Windows 用 C++ ライブラリの持つ強力なクラスと必要最低限のデザインに驚かれるでしょう。

ご意見やご質問は、

こちら mmwincpp@microsoft.com.

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


{ End Bracket }
SharePoint の習得の必要性
Robert Bogue

開発者にとって、 学ぶ材料は無限にあります。System.DirectoryServices.Protocols の未知の世界を探っているときも、ジェネリックの研究で充実した時間を過ごしているときも、ガベージ コレクションの高度な技術を詳しく調べているときも、開発者の興味の対象は尽きることがありません。では、なぜ Microsoft® SharePoint® テクノロジを優先的に習得する必要があるのでしょうか。それには、次のような 5 つの理由があります。
  1. SharePoint には、アプリケーションに簡単に拡張できる機能が数多くあります。ナビゲーション コントロールのセットが必要なら、SharePoint に用意されています。セキュリティ システムの構築が必要なら、SharePoint に用意されています。SharePoint には、ASP.NET に組み込まれていない多くの機能が組み込まれています。
  2. 再利用可能なソフトウェアは、常に重要な課題です。1980 年代の後半以降、再利用性はバックエンド コンポーネントの観点から論じられてきました。しかし、基本的なコモン コントロールを除いては、ユーザー インターフェイスの要素の再利用性はほとんど改善されていません。SharePoint は、さまざまな構成や順序で再構成することができ、1 つの Web パーツが多様なニーズに対応できるようにするユーザー インターフェイス コンポーネント (Web パーツ) 用のフレームワークを提供します。たとえば、SharePoint で DataView Web パーツと XML Web パーツを使用すると、各種のソースからデータを取得して表示できます。いくつかの組み込みコンポーネントを使用するだけで、多数のソリューションを作成できます。独自のコンポーネントを追加して既存の Web パーツに接続すれば、組み込みの機能を利用して、既存のシステムに接続できるさらに強力なソリューションを作成できます。
  3. Microsoft Office SharePoint Server (MOSS) には、アプリケーションの情報のインデックス作成や検索に使用できる検索エンジンが含まれています。独自の検索エンジンを作成する必要はありません。組み込みの既製のプラットフォームがあるので、それを公開するだけです。ユーザーは使いやすいユーザー インターフェイスと高い検索能力を利用でき、コンテンツが変更されたときに警告を受け取るように設定することもできます。
  4. すべてのアプリケーションは単純ですが、1 つの要件が加わることで複雑になります。請求システムは単純ですが、ドキュメントの画像を保存する必要がある場合は複雑になります。CRM システムは簡単ですが、スプレッドシートを添付する必要がある場合は複雑になります。SharePoint は、Web サイトまたは WebDAV を使用してアクセスするか、Microsoft Office クライアントから直接アクセスすることができる、ドキュメント管理のための完全なツールのセットを備えています。ドキュメントのサポートはプラットフォームに無償で含まれているので、手作業で作成する必要はありません。
  5. 今日のほとんどのアプリケーションには、何らかの形式のワークフローが含まれています。複雑な作業は SharePoint に任せることができるので、ユーザーとの対話方法を含めたワークフロー サービスのプロバイダを独自にコーディングする必要はありません。SharePoint では、ワークフローの状態をアイテムのプロパティを使用して管理するか、タスクのプロパティとして管理することができます。どちらの場合も、SharePoint の Windows® Workflow Foundation のサポートを利用することによって作業を軽減できます。
しかし、SharePoint も良い点ばかりではありません。いくつかの問題点を以下に示します。
複雑なツール F5 の実行がサポートされていない、SharePoint ソリューションをサーバー OS で開発する必要がある、XML ファイルのハンド コーディングなどの不都合な点がある SharePoint 開発は、ASP.NET 開発に比べると合理性とスマートさに欠けます。Visual Studio® Extensions for Windows SharePoint Services の機能と Visual Studio の次期バージョン (コードネーム "Orcas") に付属する VSTO の機能も、まだ含まれていません。
サイクル タイム 実行される処理が多いため、"サイクル タイム" (コードの編集からテストまでの時間) は大幅に長くなります。このことは、生産性に深刻な影響を及ぼす可能性があります。
幅広い知識の必要性 SharePoint では何でも実行できるように思われますが、ソリューションを作成するためには、かなり多くのことを学ぶ必要があります。マスタ ページの技術をまだ習得していない場合は、習得する必要があります。Web パーツやユーザー コントロールを通じてモジュール ユーザー インターフェイス全体を理解していない場合は、それも理解する必要があります。XML/XPath/XSLT が苦手な場合は、速習講座を受講する必要があります。SharePoint には、学ばなければならない新しい用語がたくさんあります。たとえば、大文字 F 機能と大文字 S ソリューションは、どちらも SharePoint では特別な意味を持ちます。単純な SharePoint ソリューションを作成するだけの場合でも、多くのことを学ぶ必要があります。
SharePoint は、アプリケーションを作成するための新しい優れた手段を提供します。これは、Windows フォームから Web フォームへの飛躍には及びませんが、それに近い進歩です。努力して習得すれば、新しいレベルのインフラストラクチャと、一連の優れた機能を使いこなせるようになります。

Robert Bogue は Microsoft Office SharePoint Server、MCSE、および MCSA セキュリティの Microsoft MVP であり、100 件を超える書籍プロジェクトとその他の多数の出版プロジェクトに携わってきました。Robert のブログは www.thorprojects.com/blog (英語) で公開されており、連絡先は Rob.Bogue@thorprojects.com (英語) です。