: MSDN Magazine June 2007

June 2007
JUNE 2007 ボリューム 22 番号 06

• C# 3.0: LINQ の進化の過程と C# のデザインへの影響
Anson Horton

• VISUAL BASIC: Visual Studio "Orcas" でのクエリの性能を最大限に引き出す
Ting Liang & Kit George

• C++: 次世代の Visual C++ の内側
Tarek Madkour

• OFFICE UI: Word と Outlook のカスタマイズに使用できる VSTO の新機能
Steve Fox & Paul Stubbs

• SILVERLIGHT: Web 上でより豊かな体験を作る
Laurence Moroney

• 同時実行: Windows Vista の新しい同期プリミティブ
Robert Saccone & Alexander Taskov

Code Download (288 KB) HTMLHelp Format
コラム

• CLR 徹底解剖: リフレクションについての考察
Mike Repass

• データ ポイント: SQL Server 管理オブジェクト
John Papa

• Advanced Basics: Visual Basic から RibbonX を使用する
Ken Getz

• Cutting Edge: トランザクション ワークフロー
Dino Esposito

• サービス ステーション: WCF アドレス指定の詳細
Aaron Skonnard

• Wicked Code: UpdatePanel のヒントと秘訣
Jeff Prosise

• 基礎: ワークフローにおける ActivityExecutionContext
Matt Milner

• 同時実行の問題: 非同期のデバイス操作
Jeffrey Richter

• Netting C++: EEK! の概要
Stanley B. Lippman

• .NET の問題: コンソール アプリケーションでメッセージを処理する
Stephen Toub

• { End Bracket }: 開発者の誇り
Adam Barr

各出版物

• 編集者のメモ: 10 周年を迎えた Visual Studio
Joshua Trupin

• ツールボックス: XML コメント、オンライン支払、その他
Scott Mitchell


C# 3.0: LINQ の進化の過程と C# のデザインへの影響

C# 3.0

LINQ の進化の過程と C# のデザインへの影響

Anson Horton
この記事は、Visual Studio のプレリリース バージョン (コードネーム "Orcas") に基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • C# と LINQ
  • LINQ の進化
  • コードからの SQL クエリ
この記事は次のテクノロジを使用しています:
LINQ、C#
コンテンツ
ラムダ式
拡張メソッド
匿名型
暗黙的に型指定されたローカル変数
オブジェクト初期化子
クエリ式

私は、Discovery Channel で放映されていた James Burke 氏の Connections シリーズの大ファンでした。同シリーズの根底には、一見無関係と思われる発見がいかに他の発見に影響を及ぼし、現代の利便性に帰結したかというテーマがありました。そこから教訓を引き出すとすれば、進歩は単独では生まれないということになるでしょう。当然のことながら、LINQ (Language Integrated Query) についても同じことが言えます。

簡単に言うと、LINQ はタイプセーフなデータ クエリをサポートする一連の言語拡張で、次期バージョンの Visual Studio (コードネーム "Orcas") と共にリリースされる予定になっています。XML (LINQ to XML)、データベース (LINQ to SQL、LINQ to Dataset、LINQ to Entities などの LINQ 対応 ADO.NET)、オブジェクト (LINQ to Objects) などの形式のデータをクエリの対象とすることができます。LINQ のアーキテクチャを図 1 に示します。

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

それではコードを見てみましょう。次期 "Orcas" バージョンの C# における LINQ クエリのサンプルを次に示します。

var overdrawnQuery = from account in db.Accounts
                     where account.Balance < 0
                     select new { account.Name, account.Address };

このクエリの結果を foreach で反復処理すると、返される要素はそれぞれ残高が 0 未満の口座の名前と住所で構成されることになります。

上記のサンプルを一見してわかるように、構文は SQL に似ています。数年前、Anders Hejlsberg (C# のチーフ デザイナ) と Peter Golde は、データ クエリの統合強化のため C# を拡張しようと思い立ちました。当時 C# コンパイラ開発のリーダーであった Peter は、C# コンパイラに拡張性を持たせる方法を模索しており、特に SQL のようなドメイン固有言語の構文確認が可能なアドインをサポートすることを目指していました。一方の Anders は、より深く詳細な統合の構想を練っていました。彼が考えていたのは、IEnumerable を実装したコレクションの操作や、IQueryable を実装した型に対するリモート クエリの操作を行う一連の "シーケンス演算子" でした。結局、このシーケンス演算子のアイデアは最も支持を集めることとなり、Anders は 2004 年の初めにこのアイデアに関するレポートを Bill Gates の Thinkweek に提出しました。Thinkweek での評価はきわめて肯定的なものでした。デザインの初期段階では、単純なクエリの構文は次のようになっていました。

sequence<Customer> locals = customers.where(ZipCode == 98112);

この場合、sequence は IEnumerable<T> のエイリアスを表し、"where" という単語はコンパイラで認識される特殊な演算子を表しています。where 演算子は、Predicate デリゲート (bool Pred<T>(T item) の形式のデリゲート) をとる通常の C# の静的メソッドとして実装されていました。演算子に関する特殊な情報をコンパイラに持たせることによって、コンパイラで該当する静的メソッドを正しく呼び出し、デリゲートを式にフックするコードを作成できるようにするというのがその考え方でした。

上記の例が C# のクエリの理想の構文であるとします。このクエリを C# 2.0 で言語拡張なしで記述すると、どのようになるでしょうか。

IEnumerable<Customer> locals = EnumerableExtensions.Where(customers,
                                                    delegate(Customer c)
        {
            return c.ZipCode == 98112;
        });

このコードはおそろしく冗長で、さらに悪いことに、奥深くまで入り込まなければ関係するフィルタ (ZipCode == 98112) にたどりつけません。しかも、これは単純な例なのです。フィルタや射影などが複数ある場合はどれほど読みにくくなるか想像してみてください。この冗長性の原因になっているのは、匿名メソッドに必要な構文です。理想のクエリでは、式さえあれば、その式を評価することができます。ZipCode が実際に Customer に定義されている ZipCode かどうかなど、コンテキストの推測はコンパイラ側で行われます。この問題を解決するにはどうすればよいでしょうか。特定の演算子の情報を言語にハードコードする方法は言語デザイン チームにはしっくりこなかったため、匿名メソッドに適した別の構文を探ることになりました。きわめて簡潔でありながら、匿名メソッドを処理するためにコンパイラ側で持つべき情報が必ずしも多くならない構文が求められました。こうしてラムダ式が考案されるに至りました。

ラムダ式

ラムダ式は、匿名メソッドと多くの類似点を持つ言語機能です。実際のところ、ラムダ式のほうが先に言語に取り入れられていれば、匿名メソッドは必要なかったでしょう。基本的な考え方は、コードをデータとして扱えるようにするというものです。C# 1.0 では一般に、文字列、整数、参照型などをメソッドに渡し、それらの値に基づいてメソッドで処理を行います。匿名メソッドとラムダ式の場合は、この値の範囲にコード ブロックも含まれます。これは、関数型プログラミングでおなじみの概念です。

上記の例で、匿名メソッドをラムダ式に置き換えてみましょう。

IEnumerable<Customer> locals =
    EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

注目すべき点はいくつかあります。まず、ラムダ式が簡潔である要因を何点か挙げることができます。第 1 に、コンストラクタの導入に delegate キーワードが使用されていません。代わりに新しい演算子 => が使用され、通常の式ではないことをコンパイラに伝えています。第 2 に、Customer の型が使用方法から推測されています。この場合、Where メソッドの署名は次のようになっています。

public static IEnumerable<T> Where<T>(
    IEnumerable<T> items, Func<T, bool> predicate)

Where メソッドの最初のパラメータが IEnumerable<Customer> であり、T に当たるのが Customer であると判断できるため、コンパイラは "c" が顧客を表していると推測できます。コンパイラはさらに、この情報を使用して Customer に ZipCode メンバが存在することを確認します。最後に挙げられるのは、return キーワードが指定されていない点です。構文の形式上、return メンバが省略されていますが、これは単に構文の都合によるものです。この場合も、式の結果は戻り値と見なされます。

ラムダ式では、匿名メソッドと同様に変数のキャプチャがサポートされます。たとえば、ラムダ式が含まれるメソッドのパラメータまたはローカル変数を、ラムダ式の本体で参照することができます。

public IEnumerable<Customer> LocalCusts(
    IEnumerable<Customer> customers, int zipCode)
{
    return EnumerableExtensions.Where(customers,
        c => c.ZipCode == zipCode);
}

最後に、ラムダ式ではより詳細な構文がサポートされ、型を明示的に指定したり複数のステートメントを実行したりできるようになっています。次に例を示します。

return EnumerableExtensions.Where(customers,
    (Customer c) => { int zip = zipCode; return c.ZipCode == zip; });

さいわい、当初のレポートで提案された理想の構文に大幅に近づくことができ、しかもクエリ演算子以外の一般用途にも役立つ言語機能でこれを実現することができました。改めて現状を確認してみましょう。

IEnumerable<Customer> locals =
    EnumerableExtensions.Where(customers, c => c.ZipCode == 91822);

ここには明らかな問題点があります。Customer に対して実行できる操作について考える代わりに、現時点ではコンシューマはこの EnumerableExtensions クラスに関する情報を得る必要があります。さらに、演算子が複数の場合には、正しい構文を記述するために考えを逆転させなくてはなりません。次に例を示します。

IEnumerable<string> locals =
    EnumerableExtensions.Select(
        EnumerableExtensions.Where(customers, c => c.ZipCode == 91822),
        c => c.Name);

Select は、Where メソッドの結果を操作するものであるにもかかわらず、外部メソッドになっています。これが次のような構文になると理想的です。

sequence<Customer> locals =
    customers.where(ZipCode == 98112).select(Name);

それでは、別の言語機能を使って理想の構文に近づけることができるでしょうか。

拡張メソッド

検討の結果、拡張メソッドと呼ばれる言語機能により、はるかに優れた構文を実現できることがわかりました。拡張メソッドは、基本的にはインスタンス構文で呼び出せる静的メソッドです。上記のクエリの問題点は、IEnumerable<T> にメソッドを追加しようとしたことに起因しています。しかしながら、Where や Select などの演算子を追加するとすれば、実装の大多数は不変であるにもかかわらず、現在も将来も実装ごとに該当するメソッドを実装しなければならなくなります。C# で "インターフェイスの実装" を共有するには、静的メソッドを使用する以外に方法はありません。これは、以前使用した EnumerableExtensions クラスで行っていた方法です。

代わりに、Where メソッドを拡張メソッドとして記述するとします。この場合、クエリは次のように書き換えることができます。

IEnumerable<Customer> locals =
    customers.Where(c => c.ZipCode == 91822);

この単純なクエリに関して言えば、この構文は限りなく理想に近い形になっています。しかし、Where メソッドを拡張メソッドとして記述することは、正確には何を意味しているのでしょうか。答えはとても簡単です。基本的に、静的メソッドの署名が変わり、最初のパラメータに "this" 修飾子が追加された形になります。

public static IEnumerable<T> Where<T>(
    this IEnumerable<T> items, Func<T, bool> predicate)

さらに、メソッドは静的クラスの内部で宣言する必要があります。静的クラスとは、静的メンバのみを格納するクラスで、クラス宣言の static 修飾子によって示されます。拡張メソッドとして記述することの意味はそれだけです。この宣言は、コンパイラに対し、IEnumerable<T> を実装する型についてインスタンス メソッドと同じ構文で Where の呼び出しを許可するよう指示します。ただし、現在のスコープから Where メソッドにアクセスできることが条件になります。含まれている型がスコープ内にあれば、そのメソッドはスコープ内にあることになります。したがって、Using ディレクティブを使用して拡張メソッドをスコープに含めることができます (詳細については、「拡張メソッド」のリンクを参照してください)。

拡張メソッド

拡張メソッドが クエリの例を簡略化するうえで役立つことは明らかになりましたが、それ以外のシナリオでも一般に役立つ言語機能と言えるでしょうか。結論を言えば、拡張メソッドには多くの使い道があります。最も一般的な用途の 1 つに、共有インターフェイスの実装手段としての使い方があります。たとえば、次のようなインターフェイスがあるとします。

interface IDog
{
    // Barks for 2 seconds
    void Bark();
    void Bark(int seconds);
}

このインターフェイスを実装するには、必ず両方のオーバーロードの実装を記述する必要があります。"Orcas" バージョンの C# では、次のように単純にインターフェイスを記述できます。

interface IDog
{
    void Bark(int seconds);
}

拡張メソッドを別のクラスに追加できます。

static class DogExtensions
{
    // Barks for 2 seconds
    public static void Bark(this IDog dog)
    {
        dog.Bark(2);
    }
}

このインターフェイスの場合は、実装側で 1 つのメソッドを実装するだけで、インターフェイスのクライアントはどちらのオーバーロードも自由に呼び出すことができます。

ここまででフィルタ句の構文は限りなく理想に近いところまで来ましたが、"Orcas" バージョンの C# でできることはそれだけでしょうか。そうではありません。顧客オブジェクト全体ではなく顧客の名前だけを射影して、もう少し例を広げてみましょう。前述のとおり、理想の構文は次のような形になります。

sequence<string> locals =
    customers.where(ZipCode == 98112).select(Name);

先ほど挙げた言語拡張 (ラムダ式と拡張メソッド) のみを使用して書き換えると、次のようになります。

IEnumerable<string> locals =
    customers.Where(c => c.ZipCode == 91822).Select(c => c.Name);

このクエリでは戻り値の型が異なっており、IEnumerable<Customer> ではなく IEnumerable<string> になっていることに注目してください。これは、Select ステートメントで顧客の名前のみが返されるようにしているためです。

この処理は、射影するフィールドが 1 つだけの場合は非常に有効に機能します。これに対し、顧客の名前だけでなく、顧客の住所も返されるようにする場合を考えてみましょう。理想の構文は次のようになります。

locals = customers.where(ZipCode == 98112).select(Name, Address);

匿名型

既存の構文のままで戻り値を名前と住所にしようとすると、Name と Address のみを格納する型が存在しないという問題にすぐに行き当たります。とはいえ、該当する型を導入することによってこのクエリを記述することができます。

class CustomerTuple
{
    public string Name;
    public string Address;

    public CustomerTuple(string name, string address)
    {
        this.Name = name;
        this.Address = address;
    }
}

続いて、その型 (ここでは CustomerTuple) を使用してクエリの結果を構築します。

IEnumerable<CustomerTuple> locals =
    customers.Where(c => c.ZipCode == 91822)
                 .Select(c => new CustomerTuple(c.Name, c.Address));

フィールドのサブセットを射影するためにこれだけの定型コードを記述するのは間違いなく大変です。また、このような型をどう名付ければよいのか明確でないことも多々あります。CustomerTuple は実際のところ適切な名前と言えるでしょうか。代わりに Name と Age を射影した場合はどうなるでしょうか。その場合も CustomerTuple という名前になる可能性があります。つまり、定型コードがあり、かつ作成する型に適した名前が見当たらない点が問題になります。さらに、さまざまな型が必要になり、その管理がにわかに煩雑になることも考えられます。

匿名型は、まさにこのような場合に適しています。この機能を使用すると、基本的に名前を指定せずに構造型を作成することができます。匿名型を使用して上記のクエリを書き換えると、次のようになります。

locals = customers.Where(c => c.ZipCode == 91822)
                       .Select(c => new { c.Name, c.Address });

このコードでは、Name フィールドと Address フィールドを持つ型が暗黙的に作成されます。

class
{
    public string Name;
    public string Address;
}

この型には名前がないため、名前で参照することはできません。フィールドの名前は、匿名型の作成時に明示的に宣言できます。たとえば、作成されるフィールドが複雑な式から派生する場合や、設定される名前が単に望ましくない場合などに、名前を変更することができます。

locals = customers.Where(c => c.ZipCode == 91822)
    .Select(c => new { FullName = c.FirstName + “ “ + c.LastName,
                       HomeAddress = c.Address });

この場合、生成される型のフィールドの名前は FullName と HomeAddress になります。

これでさらに理想に近づきましたが、1 つ問題点があります。匿名型を使用した箇所では意図的にローカル変数の型を省略しているのがわかると思います。当然ながら匿名型の名前は記述できません。それでは、どのように使用すればよいのでしょうか。

暗黙的に型指定されたローカル変数

ここまでに紹介した言語機能の他に、暗黙的に型指定されたローカル変数 (略して var) と呼ばれる言語機能があります。これは、ローカル変数の型を推測するようコンパイラに指示する機能です。次に例を示します。

var integer = 1;

この場合、整数は int 型になっています。ここではまだ厳密に型指定されている点を理解しておくことが重要です。動的言語では、整数の型は後で変更される場合があります。その実例を挙げると、次のコードはコンパイルされません。

var integer = 1;
integer = “hello”;

C# コンパイラは 2 行目でエラーを報告し、string を暗黙的に int に変更することはできないというメッセージが表示されます。

上記のクエリの場合は、代入式全体を次のように記述することができます。

var locals =
   customers
       .Where(c => c.ZipCode == 91822)
       .Select(c => new { FullName = c.FirstName + “ “ + c.LastName,
                          HomeAddress = c.Address });

結果として、ローカル変数の型は IEnumerable<?> になります。ここで "?" は、(匿名であるため) 記述できない型の名前を表します。

暗黙的に型指定されたローカル変数でできるのはそこまでで、機能範囲はメソッド内に限られます。メソッド、プロパティ、インデクサやその他のブロックの境界を越えることはできません。なぜなら、型を明示的に記述できず、フィールドやパラメータ型に対して "var" は有効でないからです。

暗黙的に型指定されたローカル変数は、クエリのコンテキスト以外でも便利に使用できます。たとえば、複雑なジェネリック インスタンスの簡略化に役立ちます。

var customerListLookup = new Dictionary<string, List<Customer>>();

クエリの首尾は上々です。理想の構文に近づき、しかも汎用的な言語機能でこれを実現することができました。

興味深いことに、この構文を扱う人が増えるにつれて、メソッドの境界を越えた射影がしばしば必要になることが判明しました。先に説明したように、これは Select 内で該当するコンストラクタを呼び出すことによってオブジェクトを構築することで実現できます。しかし、設定する必要のある値にぴったりのコンストラクタが存在しない場合はどうなるでしょうか。

オブジェクト初期化子

このような場合を想定して、次期 "Orcas" バージョンの C# にはオブジェクト初期化子という言語機能が用意されています。オブジェクト初期化子を使用すると、基本的に、複数のプロパティやフィールドを 1 つの式に割り当てることができます。たとえば、オブジェクト作成の一般的なパターンは次のようになります。

Customer customer = new Customer();
customer.Name = “Roger”;
customer.Address = “1 Wilco Way”;

この場合、名前と住所をとる Customer のコンストラクタは存在しませんが、Name と Address という 2 つのプロパティをインスタンスの作成後に設定することができます。オブジェクト初期化子を使用すると、同じ作成処理を次の構文で実行することができます。

Customer customer = new Customer()
    { Name = “Roger”, Address = “1 Wilco Way” };

前述の CustomerTuple の例では、CustomerTuple クラスをそのコンストラクタの呼び出しによって作成しました。これと同じ結果をオブジェクト初期化子によって達成することができます。

var locals =
    customers
        .Where(c => c.ZipCode == 91822)
        .Select(c =>
             new CustomerTuple { Name = c.Name, Address = c.Address });

オブジェクト初期化子を使用することで、コンストラクタのかっこが不要になったことに注目してください。さらに、フィールドと設定可能なプロパティをいずれもオブジェクト初期化子の本体の中で割り当てられるようになっています。

こうして、C# でクエリを作成するための簡潔な構文が出来上がりました。それだけでなく、拡張メソッドを単体でも役立つ一連の言語機能と組み合わせることにより、新しい演算子 (Distinct、OrderBy、Sum など) を拡張的に追加することもできるようになりました。

言語デザイン チームの手元には、フィードバックを得る材料となるプロトタイプがいくつか揃いました。そこで私たちは、C# と SQL の両方の使用経験がある多数の開発者の参加を得てユーザビリティ調査を実施しました。フィードバックはほぼ例外なく肯定的なものでしたが、何かが足りないことは明らかでした。特に、私たちが理想的と考えた構文が開発者の専門知識とうまく結び付かず、SQL の知識を適用しにくい面がありました。

クエリ式

そこで言語デザイン チームは、より SQL に近い、クエリ式と呼ばれる構文をデザインしました。たとえば、先ほどの例にクエリ式を適用すると次のようになります。

var locals = from c in customers
             where c.ZipCode == 91822
             select new { FullName = c.FirstName + “ “ +
                          c.LastName, HomeAddress = c.Address };

クエリ式は、前述の言語機能を基礎としています。クエリ式は文字どおり構文的に解釈され、ここまでに見てきた基本構文に変換されます。たとえば、上記のクエリは直接次のように変換されます。

var locals =
   customers
       .Where(c => c.ZipCode == 91822)
       .Select(c => new { FullName = c.FirstName + “ “ + c.LastName,
                          HomeAddress = c.Address });

クエリ式では、from、where、select、orderby、group by、let、join などのさまざまな "句" がサポートされます。これらの句は同等の演算子呼び出しに変換され、拡張メソッドによって実装されます。クエリ句は演算子を実装する拡張メソッドと緊密に結び付いているため、必要な演算子の句がクエリ構文でサポートされていない場合は両者を簡単に組み合わせることができます。次に例を示します。

var locals = (from c in customers
              where c.ZipCode == 91822
              select new { FullName = c.FirstName + “ “ +
                          c.LastName, HomeAddress = c.Address})
             .Count();

この場合、クエリで返されるのは郵便番号が 91822 の地域に住む顧客の数になります。

こうしてほぼ出発点に戻ったところで今回の記事は終わりです (この終わり方は私にとって常に満足の行くものです)。次期バージョンの C# の構文は、いくつかの新しい言語機能によって過去数年の間に進化を遂げ、最終的には 2004 年の冬に提案された当初の構文にきわめて近いところまでたどり着きました。次期バージョンの C# の他の言語機能を基盤としてさらにクエリ式が追加されたことにより、クエリ シナリオの多くは SQL のバックグラウンドを持つ開発者にとって読みやすく理解しやすいものとなっています。

Anson Hortonは、Microsoft で約 6 年間にわたりプログラム マネージャを務めています。C++ チームを経て C# チームに移り、チーム創設当初からのメンバとして取り組みを続けています。これまでに、C# 言語とコンパイラ、C# プロジェクト システム、C# IDE (IntelliSense)、および C# 式エバリュエータとデバッガのデザインに携わってきました。Anson のブログは blogs.msdn.com/ansonh (英語) にあり、不定期に更新されています。

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


VISUAL BASIC: Visual Studio &quot;Orcas&quot; でのクエリの性能を最大限に引き出す

VISUAL BASIC

Visual Studio "Orcas" でのクエリの性能を最大限に引き出す

Ting Liang and Kit George
この記事は、Visual Studio のプレリリース バージョン (コードネーム "Orcas") に基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • Visual Basic とクエリの統合
  • サポートされている句 (Where、OrderBy、Select など)
  • クエリ結果を理解し、使用する
この記事は次のテクノロジを使用しています:
Visual Basic、LINQ
コンテンツ
LINQ の構文
From 句
Select 句
クエリの結果を使用する
Where 句を使ってフィルタ処理をする
Order By を使って並べ替える
その他の句
まとめ

コード内で SQL クエリと同じようなデータ クエリの技法を使用できるようになることは、長年にわたる開発者の望みでした。リリース予定の Microsoft® Visual Studio® (コードネーム "Orcas") では、この機能が現実になろうとしています。Visual Studio の次期バージョンには、LINQ (Language Integrated Query) と総称される一連の新しい言語機能が搭載されます。LINQ によって、Visual Basic® と Visual C#® でデータ クエリ機能を使用できるようになります。

LINQ が搭載されることにより、データ アクセス コードを Visual Basic で直接記述できるようになります。コンパイル時の構文チェックとスキーマ チェックのほか、クエリの作成を支援する優れたツール (IntelliSense® など) が提供されます。もうクエリを文字列で記述する必要はなく、クエリの形式が適切かどうかが実行時までわからないということもなくなります。

LINQ では、データ アクセス コードが基になるストレージ機能から分離されます。つまり、同じコード構造を使ってリレーショナル データベース、XML、およびメモリ内オブジェクトをクエリでき、さまざまなソース ドメイン間で容易に情報を結合できます。たとえば、XML とデータベース テーブルにそれぞれ格納されている顧客名のリスト間で共用体 (ユニオン) を作成できます。また、LINQ では遅延実行が可能であるため、クエリを論理的な部分に分割でき、かつ最終結果は一度しか実行されないのでパフォーマンス上の利点も得られます。

さらに、Visual Basic と XML の統合によって、作成するコードと表現または操作しようとするドキュメントの間の概念的な障壁が事実上解消されます。では、これからクエリの記述方法を説明していきます。

LINQ の構文

LINQ を使用すると、Microsoft .NET Framework のオブジェクト コレクション、リレーショナル データベース、あるいは XML データ ソースを含む各種ソースに対してクエリを実行できます。Visual Basic での LINQ クエリの一般的な形式は次のようになります。

From ... < Where ... Order By ... Select ... >

SQL プログラマの方は、クエリが Select ではなく From で始まっていることに疑問を感じるでしょう。理由は単純です。IntelliSense のためです。データ ソースを最初に示すことで、IntelliSense によってコレクション内のオブジェクトに該当する型メンバの一覧が示されます。これは Select 句を入力する際に重要となります。

この後のセクションでは、標準的な LINQ の句をいくつか紹介し、それらが互いにどのように依存して堅牢で柔軟なクエリの作成を可能にしているかを説明します。From 句、Select 句、クエリの結果の使用、Where 句、Order By 句の各項目を順に説明していきます。

次の各例では、顧客 (Customer) または注文 (Order) を参照している箇所があります。これらの例では、この 2 つが図 1 のように定義付けされていると考えてください。各 Customer は、その顧客の注文のコレクションを Orders プロパティを通じて参照します。これにより、2 つのエンティティの間に単純な関係が成立します。この関連付けは、各 Order に CustomerID 外部キーが存在することで形成されます。これらの型定義に加えて、customers 変数に格納される Customer のコレクションも存在します。

Figure 1 Customers と Orders (Close Figure 1)
Public customers As List(Of Customer)

Public Class Customer
    Public Property CustomerID As Integer
    Public Property FirstName As String
    Public Property LastName As String
    Public Property City As String
    Public Property Orders As List(Of Order)
End Class

Public Class Order
    Public Property OrderID As Integer
    Public Property CustomerID As Integer
    Public Property OrderDate As DateTime
End Class

From 句

すべてのクエリに必要な From 句は、LINQ の中核です。クエリは常に From 句で始める必要があり、この句だけは明示的に指定しなければなりません。From 句の基本的なパターンは次のとおりです。

From <query variable> In <datasource>

基本的に、From 句はあるデータ セットを順に処理することを表します。そのために、ソース内の各データ アイテムに対応する変数を必要とします。これは、For Each ステートメントで使用される繰り返し変数によく似ています。From ステートメントに続く句では、クエリ変数を参照してフィルタ処理、並べ替え、またはその他のデータ操作を実行できます。

次に示すのは From 句のごく単純な例です。

Dim numbers As Integer = {1, 7, 4, 2, 91, 12, 23, 37, 42}
Dim allNumbers = From number In numbers
For Each num In allNumbers
    Console.Write(num & “ “)
Next

この例では、クエリが IEnumerable(Of Integer) として型指定されたコレクションを返します。このクエリには他にもいくつか注意すべき点があります。まず Select が暗黙に指定されています。この例に Select がないのはそのためです。Select ステートメントは省略可能です。指定しない場合は、スコープ内の一連の変数が返されます。次に注意すべき点は、このクエリが実際にはあまり役に立たないことです。このクエリを使わなくても、単純に For Each ループで数値変数を繰り返し処理できます。これでは LINQ の本当の性能はまだわかりません。しかし次のクエリを見てください。

From <query variable> As Type In <datasource>

Dim checkboxes = From checkbox As Checkbox In groupJobType.Controls

この別形態の From ステートメントでは、クエリ変数に続いて型ステートメントを指定できます。これは、特定のソースから型を取得し、その際に具象性のより高い (またはより低い) 型を再指定する場合に便利です。上の例では、groupJobType GroupBox 内のすべてのコントロールが Checkbox コントロールであることがわかっているため、取得時にそれらのコントロールを Checkbox に型指定しても安全です。

From 句には、From キーワードを 2 回使用する形態もあります。

From <query variable> In <datasource> From <let variable> = <value>

From <query variable> In <datasource>, <let variable> = <value>

Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0)
Dim evens = From Number In numbers, IsEven = (Number Mod 2 = 0)

このクエリ構文は実際には 1 つの From 句であり、後半部分は From の "Let" 部分と呼ばれます。基本的には、2 番目の変数を定義し、それを指定した値に代入しています。この場合、2 番目の値は "IsEven" という結果セット上で定義しています。これはブール値であり、選択した数値が偶数かどうかの結果を示します。これはもちろん、返される内容が変更されることを意味します。つまり、返される列挙可能なオブジェクトの各アイテムに数値とブール値の両方が含まれるため、整数型の IEnumerable ではなくなります。クエリの結果の使い方については、この後で説明します。ここで示した一般的な構文表現では、2 番目の From キーワードを省略し、代わりにコンマを使用できることに注意してください。つまり上の 2 つの例は等価です。

Let 変数と標準のクエリ変数の違いは、Let 変数では常に = を使用して値を代入する一方で、クエリ変数ではその後に In キーワードを付加する点です。ただし、Visual Basic チームでは Let 変数を公開する方法を積極的に検討しているので、製品の最終版がリリースされた時点で、サポートされる Let の構文が現在と異なっている可能性は十分にあります。

From <queryVar1> In <datasource1>, <queryVar2> In <datasource2>, ...

Dim customerOrders = From cust In customers, ord In cust.Orders

この構文では、複数のデータ ソースを指定できます。上の例では顧客と注文のコレクションを取得していますが、本質的にはこれは 2 つのソースに対する暗黙の Join です。ほとんどの場合、ソースがそれほど簡単に相関することはない (最初のクエリ変数上に 2 番目のクエリ変数のコレクションがあることはない) ため、Where 句で両者を関連付ける必要があります。ただし今回は、Orders コレクションが実際に各 cust インスタンス上にあるため、指定の顧客に該当する注文のみを取得しています。

Select 句

ここまではデータ ソースを指定する方法を見てきました。すべての例で Select キーワードが省略されていたことに注意してください。これは、Select の既定の動作を使用していたことを意味します。つまり、指定されたすべてのクエリまたは Let 変数を選択していました (論理的には Select * に相当します)。ただし、Select 句を指定して、高い柔軟性を得ることもできます。

Select <varA>, <varB>, ...

Dim allNumbers = From number In numbers _
                 Select number

Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0) _
            Select Number, IsEven

Dim customerOrders = From cust In customers, ord In cust.Orders _
                     Select cust, ord

これは Select の最も単純な形式です。前述のクエリを、ここで示す From セクションの同等のクエリと比較してみてください。Select 句を明示的に指定した 3 つの例のクエリは、いずれも Select 句を暗黙に指定した前述のクエリと同じように動作します。

ただし、このテーマで各種の形態を試す場合は、Select を使用する方が面白くなります。

Dim customerInfos1 = From cust In customers _
                     Select cust.FirstName, cust.LastName

Dim customerInfos2 = From cust In customers, ord In cust.Orders _
                     Select cust.LastName, ord.OrderID, ord.OrderDate

まず、新たな一般構文が指定されていないことに注意してください (例の上にコメントがありません)。これは、これらの例が一見して前のコードと同じスタイルであるためです。つまり、FirstName と LastName は単なる変数です。ただし前の例とは違って、この例ではクエリ変数を逆参照し、数個のプロパティのみを返します。これはきわめて強力な機能です。

LINQ のもう 1 つの優れた特徴は、フィールド名を変更する必要がない場合に、Visual Basic がプロパティの名前を基に名前を推測することです。ただし、場合によっては、フィールド名の変更や次のようなデータのマージが必要になります。

Select <aliasA> = <varA>, <aliasB> = <varB>, ...

Dim customerInfos1 = From cust In customers, ord In cust.Orders _
                     Select Date = ord.OrderDate, _
                            Name = cust.FirstName & cust.LastName

あるシナリオでは (この例の Date = ord.OrderDate など)、個人的な好みでこの処理を行います。別のシナリオでは (この例の Name = cust.FirstName & cust.LastName など)、フィールドを組み合わせて新しいデータを作成する場合もあります。エイリアスを使用するもう 1 つのシナリオもあります。別々のソースから選択しようとする 2 つのフィールドがたまたま同じ名前であったとします。名前が推測されると、これらの変数に結果の型で同じプロパティ名が割り当てられ、そのために競合が生じてコンパイルが失敗します。競合するフィールド名の一方 (または両方) にエイリアスを使用すれば、この競合は解消されます。

Select 句に関して最後に注意すべきポイントは、Select 句が変数のスコープを変更するために、クエリ内のすべての句が参照できるのは Select によって公開されるものに限られる点です。これについては、Where 句のセクションでさらに詳しく説明します。

クエリの結果を使用する

では、クエリの結果の使い方を見ていきましょう。そのためには、クエリが実行されると何が生成されるかを理解する必要があります。

Dim allNumbers = From number In numbers _
                 Select number

どのクエリでも結果は常に IEnumerable(Of T) です。これは、クエリの最も頼もしく強力な側面の 1 つです。なぜなら、常にデータを繰り返し処理できることを意味するからです。T の型は、わかっている場合もあれば、匿名型のようにわからない場合もあります。

匿名型は LINQ が提供する新機能で、複数のフィールド列を返すクエリの結果を表します。匿名型は、コード内で直接命名したり参照したりできない型ですが、コンパイラはこの型を他の名前付きの型と同じように扱います (この型にはコンパイラが名前を付けますが、ユーザーにはその名前がわからず、また知る必要もありません)。匿名型のフィールド名は、コード内で直接指定するか、またはコンパイラがクエリ式から自動的に推測します。たとえば、匿名型を使用すると、前述のようなコードが可能となります。つまり、{Num, IsEven} および {cust, ord} の組み合わせが、それぞれに 2 つのデータを持つ実質的に新しい型となります (Num、IsEven および Cust、Ord をそれぞれのアクセス可能なフィールド名とします)。

Dim customerInfos1 = From cust In customers _
                     Select cust.FirstName, cust.LastName

Dim customerInfos2 = From cust In customers, ord In cust.Orders _
                     Select cust.LastName, ord.OrderID, ord.OrderDate

ご覧のように、匿名型を使用することで、特定のフィールドの組み合わせを正確に表す型を事前に指定しなくても、Select 句でさまざまなフィールドを指定できます。

各クエリは IEnumerable(Of T) であると説明しましたが、どの例でもクエリを表す変数の型が実際には指定されていないことに気付いた方がいるかもしれません。また、変数が System.Object として型指定されることや、その過程で厳密な型指定がサポートされなくなることに不安を感じた方もいるでしょう。

そんなことはありません。型推測と呼ばれる新しいコンパイラ機能によって、コンパイラは型指定子なしで宣言された変数の型をその初期値に基づいて推測できます (コードの最上部に Option Infer On ステートメントを記述した場合)。たとえば、次の変数は初期値に基づいて静的に型指定されます。

Dim x = 5             ‘ x is typed as an Integer
Dim y = DateTime.Now  ‘ x is typed as a DateTime
Dim z = GetCustomer() ‘ assuming GetCustomer returns a customer
                      ‘ type, z is typed as a Customer

x = “hello”           ‘ COMPILE-TIME ERROR: x can only store Integers

同様に、クエリを表す変数は厳密に型指定されます。これにより、その変数のメンバが遅延バインディングされる心配なしに、変数上で直接メンバを使用できます。さらに、IntelliSense のサポートを始めとする厳密な型指定のその他の利点も引き続き確保されます。型指定子なしで新しい変数が宣言されると、For Each ステートメントでも同じような型推測が行われます。

 ‘ assuming allNumbers is a collection of Integers,
 ‘ the type of num is inferred to be an Integer
For Each num In allNumbers

一般に、クエリを使用する際に容易に知ることができないのが、返されるオブジェクトの具体的な種類です。この点についてはクエリを信頼しなければなりません。見えないところで、さまざまなオブジェクトを使ってクエリ結果が格納されている場合があります。認識しておくべき重要な点は、結果が常に IEnumerable(Of T) となるため、For Each で繰り返し処理したり、IEnumerable オブジェクトを受け取る API に渡せるということです。

ただし、返される内容を知るために役立つ経験則もあります。この法則は重要です (その理由はすぐにわかります)。返される結果が名前付きの型 1 つである場合は、クエリから IEnumerable(Of <your type>) が返されます。返される結果が複数の型である場合は、クエリから IEnumerable(Of <anonymous type>) が返されます。したがって、整数の配列から 1 つの整数を返す上の例では、IEnumerable(Of Integer) が返されます。

クエリ結果が単一の型の IEnumerable であれば、その結果は簡単に使用できます。For Each 句を使えばよいのです。

For Each num In allNumbers
    Console.WriteLine(num)
Next

ここでは IEnumerable が単一の型なので、意外なことは何もありません。一方、次に匿名型を含むシナリオを見てみます。

Dim evens = From Number In numbers From IsEven = (Number Mod 2 = 0) _
            Select Number, IsEven

For Each numInfo In evens
    Console.WriteLine(“Is “ & numInfo.Number & “ even? “ & _
                      numInfo.IsEven)
Next

このクエリは 2 つのデータを持つオブジェクトを返すため、変数 "evens" は IEnumerable(Of <anonymous type>) です。取り出すエンティティそれぞれに Num および IsEven というフィールドが含まれているため、evens コレクションを繰り返し処理する際に、これらのデータを取得するために各データを逆参照する必要があります (numInfo.Num など)。ここでいくつかの点に注目してください。まず、繰り返し変数に numInfo という名前を付けました。匿名型のコレクションを繰り返し処理する場合は、複数のデータが含まれていることを示す一般的な名前を使用するとよいでしょう。次に、例ではクエリ変数を大文字で記述しました。そのため、For Each ループ内でそれらの変数を逆参照して使用すると、その変数が標準プロパティのように見えます。前の例では、匿名型が導入されていなかったため、"number" に対してこの処理を行う必要がありませんでした。

この処理がいかに簡単であるかを示すために、もう 1 つ例を挙げます。

Dim customerInfos = From cust In customers, ord In cust.Orders _
                    Select Name = cust.FirstName & cust.LastName, _
                           Date = ord.OrderDate

For Each custInfo In customerInfos
    Console.WriteLine(“Customer Name: “ & custInfo.Name & _
                      “ Order Date: “ & custInfo.OrderDate)
Next

実はそれほど簡単になっていません。しかし、姓と名のフィールドを別々に取得したい場合もあります。次に示す上の例の別形態では、すべてのデータが Customer と Order (他のフィールドは後で使用) に保持されますが、この特定の繰り返しについては同じ結果が得られます。

Dim customerInfos = From cust In customers, ord In cust.Orders _
                    Select Customer = cust, Order = ord

For Each custInfo In customerInfos
    Console.WriteLine(“Customer Name: “ & _
                       custInfo.Customer.LastName & “, “ & _
                       custInfo.Customer.FirstName)
    Console.WriteLine(“Order Date: “ & custInfo.Order.OrderDate)
Next

ここでも意外なことは何もありません。各オブジェクト全体を結果に取り込むことで、オブジェクトを逆参照して使用する特定のフィールドに到達する必要があります。姓と名を For Each ループ内で結合していることに注意してください。

もう 1 つヒントを示しておきましょう。クエリで使用できるメンバに注目すると、なかなか気の利いたことができます。たとえば、次のコードを考えてみてください。

Dim customerInfos = From cust In customers, ord In cust.Orders _
                    Select Customer = cust, Order = ord
Console.WriteLine(“There are “ & customerInfos.Count & _
                  “ customer orders”)

この例では、クエリの結果に対して直接 Count を使用して、含まれているアイテムの数を確認しています。コレクション上のすべてのメンバについては触れませんが、結果セットに対して実行できる操作は忘れずに確認しておいてください。これらのメンバは、クエリの結果であるすべてのコレクション上で使用できることにも注意してください。これらのメンバが IEnumerable(Of T) 上になければならないと推測したのであればそれは正解です。そのメンバの実装も IEnumerable(Of T) 上にある必要があります。これは、拡張メソッドと呼ばれる新しい機能です。

クエリの実行方法に関して理解しておく必要がある概念に、遅延実行があります。遅延実行では、データが実際に使用されるときまでクエリがそのデータを取得しようとしません。この機能が重要である理由は、複数の異なる部分でクエリを記述しても基になるデータ ソースが複数回呼び出されることがなく、特にデータベースの情報にアクセスする際に (この状況では複数回の呼び出しを特に避ける必要があります) 多大なパフォーマンス上の利点を得られるからです。

つまり、クエリを宣言しても実際にはデータが取得されず、データを必要とするアクションを実行することで初めてデータが取得されます。おそらく最も頻度の高いアクションは、クエリの繰り返し処理です。それ以外のアクションとしては、データの表示や、クエリ内のエントリ数を数える Count などのメソッドの使用などが考えられます。図 2 は、データの取得がどこで実行されるかを示しています。

Figure 2 For Each 句で取得されたデータ (Close Figure 2)
Dim seattleCustomers = From cust In customers _
                       Where cust.City = “Seattle”

Dim recentOrders = From ord In orders _
                   Where ord.OrderDate.Year = 2007

Dim custInfos = From cust In seattleCustomers, ord in recentOrders _
                Where cust.CustomerID = ord.OrderID _
                Select Name = cust.LastName & “, “ & cust.FirstName, _
                              cust.CustomerID, ord.OrderDate _
                Order By OrderDate Descending, Name

‘ None of the above queries are run until this point.
‘ This means that the information is only retrieved once.
For Each custInfo In custInfos
    ‘ take action on the data here
Next

Where 句を使ってフィルタ処理をする

Where 句では、特定の条件でフィルタ処理を行うことで、クエリの性能を高めます。Where 句は If ステートメントと同じように動作します。つまり、If ステートメントで実行できることはすべて Where 句で実行できます。

Where <condition>

Dim evens = From num In numbers _
            Where num Mod 2 = 0 _
            Select num

このコレクションの名前は、前の例と同じ "evens" になっています。ただし前の例とは異なり、このコレクションは偶数だけで構成されます。結果を繰り返し処理すると、偶数だけが結果として得られます。

Dim mids = From num In numbers _
           Where num > 10 And num < 50 _
           Select num

Where 句の優れた点は、その簡潔さです。<condition> には任意の条件を指定できます。もう少し複雑なクエリでその動作を確認してみましょう。

Dim seattleCustomers = From cust In customers, ord In cust.Orders _
                       Where cust.City = “Seattle” _
                       Select Customer = cust, Order = ord

Dim seattle2003Orders = From cust In customers, ord In cust.Orders _
                        Where cust.City = “Seattle” _
                        And ord.OrderDate.Year = 2003 _
                        Select Customer = cust, Order = ord

あるフィールドでフィルタ処理を行い、別のフィールド (1 つまたは複数) を Select で指定する場合があります。これは非常に単純です。

Dim dallas2005Orders = From cust In customers, ord In cust.Orders _
                        Where cust.City = “Dallas” _
                        And ord.OrderDate.Year = 2005 _
                        Select cust.CustomerID, ord.OrderID

From 句を一度宣言すれば、その他の句は自由な順序で使用できます。

Dim seattle2003Orders = From cust In customers, ord In cust.Orders _
                        Select Customer = cust, Order = ord _
                        Where Customer.City = “Seattle” _
                              And Order.OrderDate.Year = 2003

この例を、同じ名前の上の例と比較してください。Select 句を Where 句の前に移動しています。このこと自体はまったく問題ありません。ところが、Where 句が若干変更され、cust と ord ではなく Customer と Order が参照されています。Select 句はスコープを変更する句であるため、Select 句の後のすべての句は Select が公開するものしか参照できないことに十分注意してください。つまり、このコードは無効になります。

‘ This won’t compile
Dim dallas2005Orders = From cust In customers, ord In cust.Orders _
                       Select cust.CustomerID, ord.OrderID _
                       Where cust.City = “Dallas” _
                              And ord.OrderDate.Year = 2005

ord と cust を Where 句の内部で参照していますが、Where 句が Select の後にあるために、この 2 つの変数は消失しています。Select の後に残されるのは、CustomerID と OrderID という 2 つのプロパティを持つ匿名型だけになります。このコードは次のように変更できます。

Dim dallas2005Orders = From cust In customers, ord In cust.Orders _
                        Select cust.CustomerID, ord.OrderID, _
                               cust.City, ord.OrderDate _
                        Where City = “Dallas” _
                              And OrderDate.Year = 2005

Where や Order By を始めとする多くの句は、使用できる変数のスコープを変更しない点に注意してください。

Order By を使って並べ替える

Order By は、あらゆるクエリで使用される基本的な句の 1 つです。Order By は、指定された 1 つまたは複数のフィールドでクエリの結果を並べ替えます。

Order By <query variable>

これは Order By の最も単純な形式です。並べ替えの順序を指定しない場合は、結果を昇順で並べ替えるものと見なされます。次に実際に動作する句を示します。

Dim nums = From num In numbers _
           Order By num _
           Select num

次に示す別形態の Order By は、最初のものとほぼ同じですが、変数自体ではなく、変数内のフィールドで並べ替えるものと見なされます。

Order By <field1>, <field2>, ...

Dim customerInfos1 = From cust In customers, ord In cust.Orders _
                     Order By cust.City _
                     Select Customer = cust, Order = ord

Dim customerInfos2 = From cust In customers, ord In cust.Orders _
                     Order By cust.City, ord.OrderDate _
                     Select Customer = cust, Order = ord

選択するフィールドは 1 つでも複数でもかまいません。また、Order By では使用できる変数のスコープが変更されないため、後続の Select 句では前のスコープからどの変数でも使用できます。

Order By <var1> [Ascending/Descending], _
         <var2> [Ascending/Descending], ...

Dim customerInfos = From cust In customers, ord In cust.Orders _
                    Order By cust.City Descending, ord.OrderDate _
                    Select Customer = cust, Order = ord

この例では Order By が大いに活用されています。2 番目の列では順序の指定を省略していますが、1 番目の列では指定しています。これは、まず city (都市名) で降順に並べ替えた後に、order date (注文日) で昇順に並べ替えることを意味します。

もちろん、Order By を Where と組み合わせることも可能です。最後にいくつか例を紹介しましょう。

Dim orderedEvens = From num In numbers _
                   Where num Mod 2 = 0 _
                   Order By num _
                   Select num

Dim seattleCustomers = From cust In customers, ord In cust.Orders _
                       Order By cust.City Descending, ord.OrderDate _
                       Where City = “Dallas” _
                                     And OrderDate.Year = 2005 _
                       Select Name = cust.LastName & “, “ _
                                     cust.FirstName, ord.OrderDate

その他の句

その他の使用可能な句に関する説明は別の記事に任せますが、標準のクエリ機能の多くは、一般にこの記事で取り上げたような句としてサポートされています。たとえば、結合、集計、グループ化などがサポートされています。暗黙の結合については上の例で説明しましたが、明示的な結合は Join キーワードを使って実行できます。情報の集計は Aggregate 句でサポートされ、グループ化は Group By 句に Sum や Average などの事前に用意された集計メソッドを組み合わせることでサポートされます (ただし集計メソッドを独自に作成して使用することもできます)。さらに、挿入、更新、削除などの標準のクエリ機能もサポートされます (ただしクエリ構文で直接的にはサポートされません)。

まとめ

LINQ は、ソースの指定 (From)、返すデータの識別 (Select)、フィルタ処理 (Where)、並べ替え (Order By) を始めとする基本的なクエリ機能を提供します。データ セットのグループ化、結合、集計を行う高度なクエリ機能も利用可能です。これらの機能が匿名型や型推測などの他の機能によってサポートされることで、SQL と同様のクエリを Visual Basic コードで直接記述するために必要なものがすべて揃います。

Ting Liangは、Microsoft Visual Basic コンパイラ チームのソフトウェア設計エンジニアで、Visual Basic LINQ プロジェクトのプロトタイプ作成と実装に広くかかわってきました。

Kit Georgeは、Microsoft Visual Basic チームのプログラム マネージャで、Visual Basic LINQ の諸機能の設計と実現に協力しました。


C++: 次世代の Visual C++ の内側

C++

次世代の Visual C++ の内側

Tarek Madkour
この記事は、Visual Studio のプレリリース バージョン (コードネーム "Orcas") に基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • Windows Vista をターゲットとするアプリケーションを開発する
  • MFC の拡張
  • UAC 用にアプリケーションを準備する
  • ネイティブ型および CLR 型をマーシャリングする
この記事は次のテクノロジを使用しています:
Visual C++、Visual Studio
コンテンツ
Windows Vista 用にアプリケーションを準備する
新しいコモン コントロールのサポートを追加する
MFC のその他の拡張
ユーザー アカウント制御用に準備する
マネージ コードと相互運用する
マーシャリング ライブラリを使用して単純化する
CLR 型で STL/CLR を使用する
コードをより迅速にビルドする
まとめ

Windows Vista™ では、Windows® プラットフォームを拡張する新しいネイティブ API が多数公開されています。これらの API を利用することで、開発者は、スタイリッシュなユーザー インターフェイスの作成、応答性の高い I/O 操作の作成、スレッドのより効果的な管理など、さまざまな機能を実現できます。 次期バージョンの Visual Studio® (コードネーム "Orcas") を使用することにより、C++ の開発者は Windows Vista で提供される新機能を最大限に利用できるようになります。これは、すべての新しいネイティブ API に対する組み込みサポートと、MFC とダイアログ エディタの新しいコントロールおよびコモン ダイアログ ボックスに対する追加のサポートを提供することにより、実現します。あらゆる面においてネイティブ開発環境が向上したのに加えて、Visual Studio "Orcas" では、ネイティブ コードとマネージ コードのギャップを埋めるために設計された新しいライブラリを提供して、ネイティブ コンポーネントを Microsoft® .NET Framework のマネージ コンポーネントと相互運用できるように、C++ 開発者が利用できるテクノロジを拡張しています。この記事では、リリース予定の Visual Studio "Orcas" で利用可能なこれらの新しいテクノロジについて詳しく説明します。

Windows Vista 用にアプリケーションを準備する

Visual Studio "Orcas" は、Windows 2000 から Windows Vista までの Windows の全バージョンをターゲットとするのに必要なヘッダーおよびライブラリをすべて備えています。これにより、開発者は、C/C++ アプリケーションで使用するオペレーティング システムおよびそのコンポーネントで直接公開されている低レベルの API にアクセスできるようになります。C レベルの API へのアクセスを提供するだけでなく、Visual Studio "Orcas" では必要に応じて Windows Vista の新しい API を使用できるように MFC を拡張し、MFC ライブラリのコンテキスト内の API のサブセットをカプセル化して公開します。

MFC に対する拡張のほとんどは、UI に焦点を絞っています。MFC アプリケーションは Visual Studio "Orcas" を使用して再コンパイルされると、Windows Vista 用に自動的にモデルチェンジされます。すなわち、コモン ダイアログが強化され、ツール バーが Windows Vista テーマに従い、タイトル バーで Aero™ の透明効果を利用できるようになります。このモデルチェンジは、コードを変更しなくても、アプリケーションを再コンパイルするだけで実現されます。 MFC アプリケーションを新しいバージョンの Visual Studio に移植すれば、もちろん、開発者は Windows Vista で公開されている新しいコントロールおよびスタイルにアクセスできます。

また、Windows Vista のコモン ダイアログについては、使用に必要なすべての作業が更新された MFC によって実行されるため、簡単に利用できるようになります。Visual Studio "Orcas" を使用してコンパイルされた、次の MFC コードについて考えてみましょう。

    CFileDialog dlgFile(TRUE);
    dlgFile.DoModal();

図 1 Windows Vista 上の MFC コモン ファイル ダイアログ
図 1 Windows Vista 上の MFC コモン ファイル ダイアログ(画像を縮小するには、ここをクリックします)
図 1 Windows Vista 上の MFC コモン ファイル ダイアログ
図 1 Windows Vista 上の MFC コモン ファイル ダイアログ (画像を拡大するには、ここをクリックします)

Windows Vista 上で実行した場合、図 1 のように見た目の良い、新しいコモン ファイル ダイアログが表示されます。しかし、Windows 2000 などの下位のプラットフォームでは、MFC のフォールバック メカニズムが働き、図 2 のように古いコモン ファイル ダイアログが表示されます。

図 2 下位のプラットフォーム上の MFC コモン ファイル ダイアログ
図 2 下位のプラットフォーム上の MFC コモン ファイル ダイアログ(画像を縮小するには、ここをクリックします)
図 2 下位のプラットフォーム上の MFC コモン ファイル ダイアログ
図 2 下位のプラットフォーム上の MFC コモン ファイル ダイアログ (画像を拡大するには、ここをクリックします)

以前のリリースの Visual Studio と MFC を使用して同じコードをコンパイルした場合は、Windows Vista 上であっても、常に、図 2 に示すような従来のコモン ファイル ダイアログが表示される点に注意してください。必要に応じて、MFC の新しい動作をオーバーライドして、既定で常に非 Windows Vista の古いコモン ダイアログを表示することもできます。これは、bVistaStyle フラグを FALSE に設定するために追加のパラメータを CFileDialog クラスのコンストラクタに渡すことで行います。

また、更新された MFC クラスを使用すると、開発者は、コモン ダイアログのカスタマイズなどのより複雑なタスクを実行するために、Windows Vista のコモン ファイル ダイアログの基になる COM 実装にフォールバックできます。たとえば、更新された CFileDialog クラスには、IFileDialogCustomize インターフェイスへのアクセスを可能にする getter があり、これを使用することによって、開発者はダイアログ オブジェクトにコントロールを追加できます。

新しいコモン コントロールのサポートを追加する

Windows Vista で利用可能な新しいコントロールおよびスタイルがサポートされたことで、開発者は、優れた外観の、真の Windows Vista アプリケーションを作成できるようになります。ただし、Visual Studio "Orcas" では、以前のリリースの Visual Studio で実装されなかった Windows XP の一部のコントロールおよびスタイルに対してもサポートを追加しました。Visual Studio "Orcas" に追加されたコントロールの概要を次に示します。

コマンド リンク コントロール 図 3 に示すコマンド リンクは、Windows Vista で導入された新しいスタイルのボタンです。基本的には、BS_COMMANDLINK スタイルのフラグが設定されたボタン コントロールです。コマンド リンクを使用すると、実際に、ボタンで実行されるアクションがそのアクションの簡単な説明と共に示されるようになり、ユーザー インターフェイスを向上できます。このコントロールは、Windows Vista のタスク ダイアログでよく使用されます。

図 3 コマンド リンク
図 3 コマンド リンク

分割ボタン コントロール Figure 4 に示す分割ボタンは、既定のアクションだけでなく、多くのアクションを持つドロップダウン メニューを表示する複雑な UI ジェスチャによってアクセス可能なその他のアクションも含む Windows Vista のボタンです。分割ボタン コントロールは、基本的には、BS_SPLITBUTTON スタイルのフラグが設定されたボタン コントロールです。

図 4 分割ボタン
図 4 分割ボタン

ネットワーク アドレス コントロール 図 5 のコントロールは、入力を IPv4 アドレス、IPv6 アドレス、またはわかりやすい DNS 名に制限するために入力内容を検証する Windows Vista の単純な編集ボックスです。

図 5 ネットワーク アドレス
図 5 ネットワーク アドレス

Sys Link コントロール これは、以前のリリースの Visual Studio でサポートされなかった Windows XP のコントロールです。図 6 に示すように、ウィンドウにハイパーテキスト リンクを埋め込むのに便利です。

図 6 Sys Link
図 6 Sys Link

MFC では、新たにサポートされた各コントロールに対して、クラスおよび通知も提供します。また、MFC の新しいクラスおよびコントロールを使用できるように、IDE のリソース エディタが更新されました。[ツールボックス] ウィンドウには、すべての新しいコントロールが MFC ダイアログ エディタで利用可能なネイティブ コントロールとして表示されます。コントロールが MFC ダイアログにドロップされると、Windows Vista のホストでは適切に描画されます。下位プラットフォームの場合は、このコントロールが Windows Vista 以外のプラットフォームでは描画できないことを示すメッセージを含むプレースホルダが表示されます (図 7 は Windows Vista 以前のダイアログ ボックスを示し、図 8 はより見た目の良い Windows Vista ダイアログ ボックスを示しています)。また、[プロパティ] ページも新しいコントロールで使用可能なスタイルとイベントを表示するように更新されました。最後に、コントロールのクラスおよびメンバを追加できるように MFC ウィザードのサポートが追加されました。

図 7 Windows XP でのダイアログ ボックス
図 7 Windows XP でのダイアログ ボックス

Visual Studio "Orcas" の MFC は、Windows Vista で拡張された既存のコントロールについてもサポートを強化しています。次のコントロールについてサポートが強化されました。TreeView、MonthCalendar、DateTimePicker、静的、スライダ (TrackBar)、および進捗状況の各コントロールです。また、Windows Vista では、拡張されたスタイルを使用して他にも多くのコントロールを強化し、より洗練された UI 表示と豊富なユーザー機能を提供しています。これらのコントロールのスタイルは、各コントロールにメッセージを送信することで直接設定できます。たとえば、アイコンとテキストによってコンテンツをカスタム描画できる TreeView コントロールで詳細なヒントを表示するには、このコントロールに TVM_SETEXTENDEDSTYLE メッセージを送信して TVS_EX_RICHTOOLTIP 拡張スタイルを設定します。

図 8 Windows Vista コントロールを使用して更新されたダイアログ
図 8 Windows Vista コントロールを使用して更新されたダイアログ(画像を縮小するには、ここをクリックします)
図 8 Windows Vista コントロールを使用して更新されたダイアログ
図 8 Windows Vista コントロールを使用して更新されたダイアログ (画像を拡大するには、ここをクリックします)

アプリケーションを Windows Vista の UI のガイドラインに準拠させる場合は、より魅力的かつ機能的なユーザー インターフェイスを作成するために Windows Vista で使用可能な新しいコントロールを活用することをお勧めします。

MFC のその他の拡張

アプリケーションのメニュー バーを非表示にするオプションを既定として設定するなどの Windows Vista の新しい UI 手法は、Visual Studio "Orcas" においても利用されます。この例の場合、メニュー バーは Alt キーを使用してアクティブ化される場合にのみ表示され、それ以外の場合は表示されません。同じ手法が、Windows Vista 上で実行している Internet Explorer® 7 など、Microsoft のさまざまなアプリケーションで使用されます。この動作を有効にするには、開発者は CFrameWnd::SetMenuBarVisibility (AFX_MBV_DISPLAYONFOCUS) を呼び出して CFrameWnd に適切なフラグを設定する必要があります。また、MFC では Aero ウィザードも公開されます。これにより、CPropertySheet コンストラクタで m_psh.dwFlags |= PSH_AEROWIZARD を設定することにより、Aero ウィザード フレームワークを使用するように MFC ウィザードを更新できるようになります。

同様に、リソース エディタが Windows Vista の新しい UI ガイドラインをサポートするように更新されました。既定では、追加された新しいダイアログは、コントロール配置、コントロール サイズ、および使用するフォントの観点から UI ガイドラインに準拠します。また、リソース エディタでは、Windows Vista ベースのアプリケーションで使用されるアイコンを表示するための拡張機能も提供されます。32 ビット カラー イメージ、PNG イメージ、256×56 の大型アイコン、PNG アイコンがサポートされるようになりました (図 9 を参照)。さらに、複数のアイコン イメージを含むアイコン ファイルの向上したプレビュー モードについてもサポートされています。

図 9 リソース エディタでのアイコンのサポート
図 9 リソース エディタでのアイコンのサポート(画像を縮小するには、ここをクリックします)
図 9 リソース エディタでのアイコンのサポート
図 9 リソース エディタでのアイコンのサポート (画像を拡大するには、ここをクリックします)

最後に、Spy++ アプリケーションが、Windows Vista で導入された新しいメッセージを監視するように更新されました。また、Spy++ では、Visual Studio "Orcas" での 64 ビット アプリケーションに対するサポートが追加されました。

ユーザー アカウント制御用に準備する

Windows Vista の新しいセキュリティ機能の代表的なものの 1 つに、ユーザー アカウント制御 (UAC) があります。UAC では、ユーザーは最低限の特権で実行し、管理者権限については明示的に付与する必要があります。この機能は、非常に重要な意味を持っています。Windows Vista の既定の機能である UAC で実行するときに、アプリケーションがどのように実行されるかを開発者が認識することは、現在、重要になっています。また、管理者はアプリケーションで必要なセキュリティ レベルを指定することで、デスクトップ セキュリティの強化に積極的な役割を果たすことが推奨されます。Visual Studio "Orcas" は、UAC で必要な XML マニフェストを手動で生成する負担をなくし、開発者が UAC 用にアプリケーションを準備するのに役立ちます。明示的に管理者特権で実行されていないことを示す UAC マニフェストを持たないアプリケーションは、互換性の仮想化モードで実行されます。このモードでは、レジストリへの書き込みやユーザー ディスク以外の場所への書き込みなど、グローバルな影響を及ぼす書き込みはすべてユーザーごとの場所にリダイレクトされます。ただし、互換性モードはアプリケーションに微妙な欠陥をもたらす可能性があるため、あくまでも応急処置と考えてください。Windows Vista 用に設計されたすべてのアプリケーションについては UAC でのテストを実行し、適切な UAC マニフェストを利用することをお勧めします。

UAC マニフェストには、3 つの呼び出しモード As-Invoker、Highest-Available、および Require-Administrator のいずれかを含めることができます。

As-Invoker この特権レベルでは、アプリケーションを親プロセスと同じトークンで実行できます。これが既定値であり、Windows Vista のすべてのアプリケーションの UAC 設定でこのレベルを指定することをお勧めします。

Require-Administrator この特権レベルでは、アプリケーションを完全な管理者特権で実行する必要があります。ローカル マシン上の管理者グループのメンバのみが、この設定のアプリケーションを実行できます。この特権レベルが設定されたアプリケーションを呼び出すと、このアクセス許可レベルでの実行を要求する UAC ダイアログが表示されます。

Highest-Available この特権レベルでは、現在のユーザーが使用可能な最も高い特権でアプリケーションを実行する必要があります。大半のユーザーが管理者特権で実行するため、これは、多くの場合、Require-Administrator 特権と解釈されます。

では、UAC マニフェストをどのようにして生成するのでしょうか。Visual Studio "Orcas" では、UAC XML マニフェストを部分的に自動生成することができる LINK.EXE の新しいスイッチを公開します。新しいスイッチ /MANIFESTUAC の形式は次のとおりです。

/MANIFESTUAC[:{NO|UAC fragment}]

このスイッチでは、既定で As-Invoker 特権が設定されたマニフェストが生成されます。スイッチに No を指定すると、マニフェストは生成されず、アプリケーションは互換性モードで実行されます。requireAdministrator または highestAvailable を指定すると、生成された UAC マニフェストにはそれぞれの特権が設定されます。

また、Visual Studio "Orcas" では、図 10 に示すように、プロジェクトのプロパティのページでも同じオプションが表示されます。

図 10 Visual C++ プロジェクトのユーザー アカウント制御オプション
図 10 Visual C++ プロジェクトのユーザー アカウント制御オプション(画像を縮小するには、ここをクリックします)
図 10 Visual C++ プロジェクトのユーザー アカウント制御オプション
図 10 Visual C++ プロジェクトのユーザー アカウント制御オプション (画像を拡大するには、ここをクリックします)

既定では、Visual Studio "Orcas" で作成する新しいアプリケーションには、すべて As-Invoker が設定された UAC マニフェストが生成されます。また、以前のバージョンの Visual Studio からアップグレードされたすべてのアプリケーションで、UAC が自動的に有効になります。

また、Internet Explorer 7 でも Windows Vista の UAC 機能を使用して、より安全な保護モードが提供されています。このモード (既定値) では、基本的に、Internet Explorer 内のコンポーネントが標準よりも低いユーザー特権によって実行されます。これは、新しく作成される、低い権限の Internet Explorer のインスタンス内でコンポーネントを起動することにより実現されます。したがって、ActiveX® コントロールなど、Internet Explorer 7 のコンテキスト内で実行する必要があるコンポーネントについては、Internet Explorer 7 の保護モードでのテストを行う必要があります。

Visual Studio "Orcas" では、C++ プロジェクトに対して Web ブラウザでアプリケーションのデバッグを有効にする新しいオプションを追加します。 現在、使用可能なデバッガの一覧に、Web ブラウザ デバッガが含まれています。このデバッガでも Internet Explorer 7 の保護モードが認識され、新しく作成された低い特権のプロセスにアタッチするために必要な作業がすべて実行されます。

マネージ コードと相互運用する

Visual Studio "Orcas" では、C++ 開発者のためにネイティブ ツールに特に重点を置いたことに加えて、ネイティブの C++ コンポーネントと、マネージ コードを使用して開発された .NET コンポーネントとのギャップを埋めるテクノロジのサポートも強化しています。.NET Framework の導入以来、業界レベルのアプリケーションでは、マネージ コードが有益かつ適切な分野において、ネイティブの C++ アプリケーションに多くのマネージ コンポーネントが組み込まれるようになりました。

Visual Studio "Orcas" では、これらの .NET コンポーネントを C++ の既存および新規のネイティブ コードに簡単に統合できるようになります。Visual Studio では、ネイティブの手法をマネージ コードで利用できるライブラリを提供しており、ネイティブとマネージの境界にまたがるコードを効率的に記述できます。

マーシャリング ライブラリを使用して単純化する

ネイティブ型と共通言語ランタイム (CLR) 型を混在させる場合の最も基本的な要件は、両方の型システムの間でデータをマーシャリングできることです。さいわいなことに、Visual Studio "Orcas" ではこれを解決しました。新しいマーシャリング ライブラリを装備して、ネイティブ環境と CLR の間で型をマーシャリングするためのコードを記述する煩雑さを軽減しました。マーシャリング ライブラリの操作は既存の .NET Framework ルーチンに基づいていますが、マーシャリング ルーチンを拡張可能なテンプレート ライブラリのメンバとして公開することによって、これらのクラスの操作の複雑さを軽減しています。

新しいマーシャリング ライブラリには、C++ アプリケーションで使用される共通文字列型 ATL CString、BSTR、CComBSTR、char *、wchar *、std::string、std::wstring、System::String^ に対するサポートが組み込まれています。開発者は、購入時にサポートされる変換だけでなく、新規に変換を追加できます。以下に、マーシャリング ライブラリを使用して System::String を char * に変換する簡単な例を紹介します。

#include <msclr/marshal.h>

void myfunc (String^ s)
{
    msclr::interop::marshal_context ctx;
    char *s2 = ctx.marshal_as<char*> (s);
    //...
}

CLR 型で STL/CLR を使用する

標準テンプレート ライブラリ (STL) のコンテナ - 反復子 - アルゴリズム構造は、C++ 開発を簡素化し、現在では、多くの C++ 開発者が広く利用しています。Visual Studio "Orcas" では、STL によってネイティブ型で有効になる手法を、STL/CLR を使用することによって CLR 型の環境でも利用できるようにします。この新しい機能は、ネイティブ環境とマネージ環境の両方で同じ手法を使用可能にすることで、C++ 開発者の生産性を大幅に向上できます。また、この機能を使用すると、STL コンテナと .NET コレクション間のデータ交換が容易になります。

STL/CLR では、STL がネイティブ型に提供するのと同じコンテナを CLR 型に提供します。新しいコンテナは、cliext 名前空間に含まれます。STL/CLR でも STL と同様にアルゴリズムの複雑性が維持されます。STL/CLR コンテナには、ガベージ コレクトされた CLR 参照型および値型と、CLR とネイティブの両方の組み込み型を保持できます。純粋なネイティブ型、またはネイティブ型へのポインタは保持できません。

また、STL/CLR ではコンテナで使用される反復子も公開します。STL/CLR の反復子では STL と同じ階層が維持され、STL/CLR の反復子へのアクセス方法もまったく同じです。STL/CLR では STL と同じアルゴリズムのセットも公開します。これらのアルゴリズムは、STL/CLR および STL のコンテナに対して操作できます。次のサンプルは、実行中の STL/CLR のコンテナおよび反復子を示しています。

#include <cliext/hash_map>

void myfunc ()
{
    cliext::hash_map<Int32, String^> m;
    m.insert (cliext::make_pair(5, L”five”));

    cliext::hash_map<Int32, String^>::iterator i = m.find(5);
    Console::WriteLine(“map[{0}] == {1}”, i->first, i->second);
}

コードをより迅速にビルドする

CLR をターゲットとする C++ コード開発で改善された点には、この他に、増分ビルドにかかる時間が大幅に短縮されたことが挙げられます。C++ コンポーネントを含む大規模な CLR アプリケーションの開発の短所の 1 つは、これまで、コア コンポーネントに実装の変更があるだけで、ユーザー インターフェイスには何も変更がない場合でも、依存関係を再ビルドする必要があったことです。これは、CLR アセンブリでコンポーネントのメタデータと実装の両方が 1 つのバイナリにパッケージ化されることが原因でした。その結果、CLR アセンブリに依存する大規模な C++ プロジェクトは、対象の CLR アセンブリが一部でも変更された場合には、再ビルドする必要があります。

リリース予定の Visual Studio では、インターフェイスをキャプチャするメタデータのみのアセンブリを作成し、インターフェイス アセンブリが変更された場合には、依存する C++ プロジェクトのみを再ビルドする新しいツールを追加しています。アセンブリの実装を変更しても、依存アセンブリの再ビルドはトリガされません。これにより、増分ビルド シナリオにおけるビルド時間を大幅に短縮できるため、開発者の生産性が向上します。

ビルドのスループットという点で、Visual Studio "Orcas" の好ましい機能の 1 つに、ソース ファイルを並列にコンパイルできる機能があります。この機能は、ネイティブ コードとマネージ コードの両方で使用可能です。この動作を有効にするために、CL.EXE で新しいスイッチ /MP が提供されます。新しいスイッチの構文は、次のとおりです。

/MP[n] use up to ‘n’ processes for compilation

この場合、n には、基本的に、使用するプロセッサの数を指定します。プロセッサの数を指定しない場合、コンパイラではコンピュータで使用可能な論理プロセッサの数が使用されます。数字を指定すると、ちょうど n 個のプロセスがコンパイル用に作成されます。

並列コンパイル機能は、異なるプロセスの異なる翻訳単位をコンパイルする機能です。1 つの翻訳単位を複数のプロセスでコンパイルすることはできません。たとえば、4 つのプロセッサを持つコンピュータで実行したときの以下のコンパイラ呼び出しの結果について考えてみましょう。

CL /c /MP a.cpp b.cpp c.cpp d.cpp e.cpp f.cpp

このコンパイラでは、4 つのプロセッサの 1 つが解放されるとすぐに新しい翻訳単位がコンパイルされ、4 つの翻訳単位が並列にコンパイルされます。

次に、2 つのプロセッサを持つコンピュータで実行したときの以下のコンパイラ呼び出しの結果について考えてみましょう。

CL /c /MP a.cpp

このコンパイラでは、1 つの翻訳単位内で並列コンパイルは実行されないため、受け取った 1 つの翻訳単位の順次コンパイルが実行されます。

まとめ

リリース予定の Visual Studio "Orcas" は、C++ の開発者が Windows Vista で提供された新しい機能を最大限に利用できるように、大幅に機能拡張したツールを提供します。今回のリリースで、Windows Vista の多くの新しい API を、Win32® 開発者が利用できるようになります。また、MFC も拡張されているため、MFC の開発者は Windows Vista の多くの機能をすぐに利用できます。IDE では、Windows Vista の新しいコントロールを表示できるようにリソース エディタが拡張されました。また、UAC 対応のアプリケーションの作成に対するサポートが追加されています。

Visual Studio "Orcas" は、ネイティブ C++ コードと CLR 間のギャップを埋めることが必要になる開発者に対しても機能拡張を提供します。これは、開発者の生産性を向上させる新しいライブラリおよびツールの提供によって実現します。一方、新バージョンの Visual Studio は並列コンパイルなどの便利な新機能を備えて、開発環境をさらに強化します。

Tarek Madkourは、Microsoft の Visual C++ チームのリード プロジェクト マネージャです。彼は、この 6 年間 Visual C++ の IDE やコンパイラに関するさまざまな分野に取り組んできました。


OFFICE UI: Word と Outlook のカスタマイズに使用できる VSTO の新機能

OFFICE UI

Word と Outlook のカスタマイズに使用できる VSTO の新機能

Steve Fox and Paul Stubbs
この記事は、Microsoft Visual Studio Tools for the Microsoft Office System のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • VSTO "Orcas" Beta 1 の新機能
  • Office ドキュメント用リボン デザイナを使用する
  • XML でリボンをカスタマイズする
  • Word コンテンツ コントロールをデータにバインドする
  • VSTO を使用してカスタム Outlook フォーム領域を作成する
この記事は次のテクノロジを使用しています:
VSTO、Office、Visual Studio
コンテンツ
VSTO の概要
リボン デザイナで Office UI をカスタマイズする
XML を使用して高度なリボンをカスタマイズする
VSTO の Word コンテンツ コントロール
Outlook フォーム領域
まとめ

2007 Microsoft® Office system は、さまざまな Office Business Application (OBA) の構築および展開に使用できる堅牢なビジネス アプリケーション プラットフォームへと進化を遂げました。OBA は、実際のビジネスの重要な問題に対処する新しい種類のアプリケーションで、既存の Office ソフトウェア/サービス (Microsoft Office Word、Excel® Services、および SharePoint® Server) と基幹業務 (LOB) システム (SAP など) をシームレスに統合して構築できます。この Office system のパワーと拡張性が、今年後半にリリースされる Visual Studio® (コードネーム "Orcas") と組み合わさると、きわめて強力なテクノロジの融合が実現されます。この記事では、プロの開発者を対象に、Microsoft Visual Studio Tools for the Microsoft Office System (VSTO) を使用して 2007 Microsoft Office system の強力なカスタム アプリケーションを作成する方法を説明します。

VSTO の概要

VSTO は開発者用の強力なツールで、Microsoft Office アプリケーションを拡張してカスタマイズするための機能を備えています。使用言語は、Visual Basic® および C# です。"Orcas" リリースの VSTO では、開発者のための機能がさらに強化されています。マネージ コード開発が独創的かつエキサイティングな形で Microsoft Office や Microsoft Office SharePoint と統合されているほか、Windows® フォームに似た単純なプログラミング モデルによる Office リボンのカスタマイズやプログラミングを実現するリボンのビジュアル デザイナも用意されています。

さらに、Microsoft Office Outlook® のフォームをビジュアル デザイナを使って簡単に拡張できます。このビジュアル デザイナでは、Windows フォーム コントロールや Windows Presentation Foundation コントロールを使用できるほか、Outlook の組み込みコントロールで構成されたフォームをインポートすることもできます。その他、C# や Visual Basic の SharePoint ワークフローで Visual Studio の高速アプリケーション開発 (RAD) を利用できるようになりました。視覚的なワークフロー デザイナが含まれているほか、SharePoint サーバー上のカスタム ワークフローのすばやい展開とデバッグが新たにサポートされており、15 のステップと数日の時間を要する作業負荷の高いプロセスが、ウィザードの数回のクリックにまで縮小されます。

VSTO には、Visual Studio IDE 内に表示される Word 2007 および Excel 2007 用のビジュアル デザイナも用意されています。これにより、ドキュメントをビジュアル デザイン画面のように使用して、ドキュメント ベースの強力なソリューションを簡単に作成できます。新しい Word コンテンツ コントロールを使用して Word 2007 文書を作成する機能も、ADO.NET スタイルのデータ バインド、データ ソース ウィンドウからのドラッグ アンド ドロップ、プロパティ グリッドなどの完全なサポートを含め、すべて VSTO でも利用できます。

この記事では、これらの機能の一部に焦点を絞ります。具体的には、Office リボンのカスタマイズ、Microsoft Office Word 2007 への Word コンテンツ コントロールの追加、カスタム リボンとコンテンツ コントロールの統合、および Outlook フォーム領域の作成の方法を説明します。これらは Visual Studio "Orcas" の機能のほんの一部にすぎません。ここでこれらの機能を取り上げるのは、これらが Beta 1 リリースに含まれるからです。Beta 1 リリースは、皆さんがこの記事を読む頃には公開されているはずです。

リボン デザイナで Office UI をカスタマイズする

VSTO "Orcas" では、Office リボンを 2 とおりの方法でカスタマイズできます。1 つ目の方法は、Microsoft Visual Studio 2005 Tools for Office Second Edition (VSTO 2005 SE) と同様の方法です。この方法では、XML のクラスと定義を使用してリボン要素を定義し、そのリボン要素をイベント ハンドラに (コールバック コードを使用して) フックします。この方法は既にご存じでしょうから、ここでは、VSTO "Orcas" で Office リボンをカスタマイズするためのもう 1 つの方法となるリボン デザイナを重点的に取り上げます (VSTO 2005 SE でのリボンのカスタマイズについては、今月号の MSDN® Magazine の Ken Getz の Advanced Basics コラムを参照してください)。

リボン デザイナにはビジュアル デザイナと機能拡張オブジェクト モデルが含まれており、リボン UI の作成、構成、およびデバッグを簡単に行えるようになっています。他のビジュアル デザイナと同様に、コントロールをデザイン画面にドラッグ アンド ドロップして、プロパティ ウィンドウでプロパティを設定できます。また、コントロールをダブルクリックして新しいリボン項目のイベント ハンドラを簡単に作成できます。Office のコールバックは VSTO のリボン オブジェクトのイベントにマップされているため、開発者はコールバック メソッドの代わりにイベント ハンドラを作成します。では、例として Word 2007 用のサンプル リボンをカスタマイズしてみましょう。

まず、C# または Visual Basic の [Office] ノードで新しい Word 2007 プロジェクトを作成して、WordRibbonExample という名前を付けます。プロジェクトを作成すると、いくつかのプロジェクト リソース ファイルが自動的に作成されます。ルート プロジェクト ファイル (WordExample.cs) を右クリックし、[新しい項目の追加] をポイントして、[Ribbon (Visual Designer)] (リボン (ビジュアル デザイナ)) をクリックします。リボンに名前を付けるか、既定の名前をそのまま使用します。これで、リボン要素を追加できる既定のカスタム リボンが作成されます。

リボン デザイナはドラッグ アンド ドロップに対応しているため、ツールボックスの [Office Ribbon Controls] (Office リボン コントロール) タブからコントロールをドラッグできます。VSTO "Orcas" の Beta 1 リリースでサポートされているリボン コントロールは、タブ、グループ、およびボタンです。サポートされている Office アプリケーションは、Word 2007、Excel 2007、PowerPoint® 2007、および Outlook 2007 です。

次の例では、[My Custom Tab] (カスタム タブ) というリボン タブに 3 つのグループが追加され (グループはリボン コントロールのコンテナとして必要です)、各グループにボタンとイメージが追加されています。図 1 は、コントロールのプロパティを編集する方法を示しています。

図 1 リボン デザイナ
図 1 リボン デザイナ(画像を縮小するには、ここをクリックします)
図 1 リボン デザイナ
図 1 リボン デザイナ (画像を拡大するには、ここをクリックします)

一般的な VSTO のリボンのカスタマイズは、1 つ以上のリボン オブジェクト、その中に含まれる 1 つ以上のタブ オブジェクト、さらにその中に含まれる 1 つ以上のグループ オブジェクトなどで構成されます。したがって、VSTO のカスタマイズは、リボン オブジェクトの特定の階層構造の中に作成されます。たとえば、図 2 の概念図からは、最上位のコントロールとしてタブが必要なことがわかります。タブには 1 つ以上のグループ コントロールを、グループには 1 つ以上のボタン、トグル ボタン、およびメニューを追加できます。ただし、メニューの階層は 5 つまでに制限されています。

図 2 コントロール階層
図 2 コントロール階層

カスタム リボンを作成できたら、リボン要素にイベント ハンドラを追加することができます。個々のリボン コントロールにイベント ハンドラを追加する方法は、基本的に他のコントロールの場合と同じです。コントロールをダブルクリックするとクリック イベントが自動的に追加されて、イベント ハンドラ コードを入力できるようにソース コード ビューが開きます。たとえば testButtonOne というボタンがあった場合に、デザイナでそのボタンをダブルクリックすると、次のような testButtonOne_click イベントがコード ビューに表示されて、イベント処理コードを入力できるようになります。

private void testButtonOne_click(
    object sender, RibbonControlEventArgs e)
{
    // Simple message box to illustrate click event.
    testButtonOne.label = “New Label”;
    MessageBox.Show(testButtonOne.label);
}

XML を使用して高度なリボンをカスタマイズする

VSTO 2005 SE では、XML ファイルを使用して Office リボンのカスタマイズが行われます。プロジェクトに新しい項目を追加するときに [Ribbon サポート] を選択すると、カスタム UI を読み込むために使用されるリボン クラスと、カスタム UI を定義するリボン XML ファイルが自動的に作成されます。図 3 は、[Ribbon サポート] 項目の既定の XML コードです。

Figure 3 既定の XML リボン定義 (Close Figure 3)
<customUI xmlns=”http://schemas.microsoft.com/office/2006/01/customui” onLoad=”OnLoad”>
  <ribbon>
    <tabs>
      <tab idMso=”TabAddIns”>
        <group id=”MyGroup”
               label=”My Group”>
          <toggleButton id=”toggleButton1”
                        size=”large”
                        label=”My Button”
                        screentip=”My Button Screentip”
                        onAction=”OnToggleButton1”
                        imageMso=”HappyFace” />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

その他、トグル ボタン (ユーザーによってクリックされるとメッセージ ボックスを呼び出します)、コールバック コード (カスタム UI のイベント ハンドラの作成方法を示します)、ヘルパ コードなど、UI のカスタマイズを支援する既定の要素もプロジェクトに追加されます。

新しいリボン デザイナは、リボンのカスタマイズの一般的なタスクには対応していますが、高度なシナリオ (組み込みコマンドの再利用や、あまり使用されない種類のコントロールの使用など) はサポートしていません。既定の Office UI をカスタマイズしたり、デザイナでは使用できない種類のコントロールを使用したりする場合は、以前のようにリボン XML を使用してカスタマイズを行うことができます。最初からリボン (XML) 項目で作業することも、最初はデザイナで作業して、リボン XML にエクスポートした後、XML を使用してカスタム UI の定義を続行することもできます (エクスポートできるのは 1 回だけであることに注意してください)。エクスポートした場合は、手動でコールバックのシグネチャを定義して、デザイナで作成したイベント ハンドラのコードを移植する必要があります。

VSTO の Word コンテンツ コントロール

リボンのカスタマイズでは、アプリケーション レベルでカスタム UI を作成してラップできるのに対し、VSTO の Word コンテンツ コントロールでは、ドキュメントやテンプレートを非常に簡単にカスタマイズして、よりスマートで構造化されたドキュメントを作成することができます。ドラッグ アンド ドロップ機能を使用して Word 2007 テンプレートにコントロールを追加して、それらのコントロールにビジネス ロジックを組み込んだり、ユーザーが編集したり削除したりできないように保護したり、ローカル データベースや企業データベースへの単純なデータ バインドを追加したりできます。VSTO を通じて強力なカスタマイズ機能 (タスク ペインのカスタマイズなど) を利用できるため、文書の画面が企業データへのインターフェイスになり、タスク ペイン (またはカスタム リボン) が、Word 2007 文書内のコンテンツ コントロールにユーザーがデータを入力するための手段になるようなシナリオも考えられます。

ここでは、コンテンツ コントロールを使用して、顧客データベースのデータを使って自動的に構築できる単純な Word テンプレートを作成し、エンド ユーザーがデータベース内のデータを参照するために使用できるカスタム リボンを追加してみます。結果の文書は図 4 のようになります。

図 4 招待状のサンプル
図 4 招待状のサンプル

まず最初にソリューションを作成して、エンド ユーザーが顧客データベースのデータに基づく定義済み領域のコンテンツを選択して Word テンプレートをカスタマイズできるようにする必要があります。Visual Studio で Visual Basic または C# の新しいプロジェクトを作成し、[Office] を選択して、[Word 2007 Document template] (Word 2007 文書テンプレート) を選択します。Visual Studio IDE に空の Word 文書が表示され、図 5 のようなコンテンツ コントロールがツールボックスに表示されます。

図 5 Word コントロール
図 5 Word コントロール

プロジェクトのシェルができたら、文書の画面にコントロールを追加することができます。コントロールを追加するだけでなく、文書に構造を与える必要もあります。この文書テンプレートを作成するには、Visual Studio 環境内でテンプレートを作成するか、定義済みのテンプレートを読み込みます。この例では、まず、招待状テンプレートとして機能する既定のテキストを追加し、次に、5 つのコンテンツ コントロールを (ツールボックスから文書の画面にコントロールをドラッグ アンド ドロップして) 追加しています。追加したコントロールは、会社の名前、会社の住所、および招待状の宛名の RichTextContentControl と、日付および返信日付の DatePickerContentControl です。コントロールを文書の画面に追加すると、コントロールのプロパティを (Visual Studio の他のコントロールの場合と同じように) プロパティ ウィンドウで調整できるようになります。コントロールがどのように追加されているかは先ほどの図 4 で確認できます。これらのコントロールをデータ参照イベント ハンドラにフックすると、ユーザーが顧客データベースを参照できるようになります。

以上の流れを把握できたら、F5 キーを押して、ソリューションをビルドして実行します。まだコントロールが既存のデータベースにフックされていないので、フィールドにデータは表示されません。そのまま Word 文書を保存せずに閉じて、Visual Studio 環境に戻ります。

文書にコントロールが配置されたので、特定のデータ ソースを参照してコントロールをバインドすることができます。このサンプルでは、CustomerDatabase という Microsoft Office Access™ のごく単純な顧客データベースを使用します。このデータベースは 1 つのテーブルで構成されており、テーブルには、ID、CompanyName、CompanyAddress、ContactName、CurrentDate、および ResponseDate の 6 つの列があります。参照を作成するのはとても簡単です。データ ソース構成ウィザードを使用して、コントロールを顧客データベース内の特定のレコードにバインドするだけです。

まず、[データ] メニューの [新しいデータ ソースの追加] をクリックします。データ ソース構成ウィザードの選択の画面が表示されます。[データベース] を選択し、[次へ] をクリックします。次のダイアログで [新しい接続] をクリックし、[接続の追加] ダイアログで既定のデータ ソース (Access) をそのまま使用して、使用する Access データベースを参照します。その後、プロジェクトに関連付けるテーブルとビューを選択するように求められます。この例と同じデータベースを使用する場合は、CustomerInfo テーブルを選択し、[ビュー] チェック ボックスはオフのままにします。このダイアログを図 6 に示します。

図 6 [データベース オブジェクトの選択] ダイアログ
図 6 [データベース オブジェクトの選択] ダイアログ(画像を縮小するには、ここをクリックします)
図 6 [データベース オブジェクトの選択] ダイアログ
図 6 [データベース オブジェクトの選択] ダイアログ (画像を拡大するには、ここをクリックします)

この手順が完了したら、[完了] をクリックします。指定した情報に基づいて、データ セット、テーブル アダプタ、およびバインド ソースが自動的に作成されます。これらは、コントロールをデータにバインドしてデータベースのレコードをプログラムで操作するために必要な要素です。この例では、これらの要素に対して、customerDatabaseDataSet、customerInfoTableAdapter、および customerInfoBindingSource という名前がそれぞれ自動的に割り当てられます。

データベースがプロジェクトに接続されたので、今度は、データを特定のコントロールにバインドする必要があります。そのためには、プロパティ ウィンドウのデータ バインディング プロパティを使用します (図 7)。Text を選択し、各コンテンツ コントロールに関連付ける特定のデータベース レコードを選択します (特定のデータ要素をツールボックスからバインド先のコントロールにドラッグ アンド ドロップすることもできます)。

図 7 データ バインディング プロパティ
図 7 データ バインディング プロパティ(画像を縮小するには、ここをクリックします)
図 7 データ バインディング プロパティ
図 7 データ バインディング プロパティ (画像を拡大するには、ここをクリックします)

各コンテンツ コントロールに顧客データベースの特定のレコードが接続されたので、今度はリボンをカスタマイズし、新しいカスタム ボタンにイベント ハンドラを追加して、エンド ユーザーが顧客データベースのデータを参照できるようにします。リボン デザイナを使用して 2 つのボタン ([次へ] ボタンと [前へ] ボタン) を作成し、プロパティ ウィンドウを使用してタブ、ボタン グループ、およびボタンのプロパティを編集します。ボタンにビットマップ形式のイメージを割り当てることもできます。このカスタム リボンは図 8 のようになります。

図 8 データ参照ボタン
図 8 データ参照ボタン

最後に、各ボタンにイベント ハンドラを追加して、エンド ユーザーが顧客データベースのデータを参照できるようにします。そのためには、各ボタンをダブルクリックして、ユーザーが前または後ろのデータに移動できるようにするためのコードを追加します。ボタンをダブルクリックすると、そのリボン クラスのコード ビューが開いて、イベント ハンドラのコードを追加できるようになります。次のコードでは、文書にアクセスするための Globals オブジェクト、顧客データベースのバインド ソース、および MoveNext メソッドと MovePrevious メソッドが使用されています。

private void button1_OnAction(
  object sender, ControlEventArgs e)
{
  Globals.ThisDocument.customerInfoBindingSource.MoveNext();
}

private void button2_OnAction(
  object sender, ControlEventArgs e)
{
  Globals.ThisDocument.customerInfoBindingSource.MovePrevious();
}

これでカスタム ボタンがコンテンツ コントロールにフックされたので、F5 キーを押してソリューションをビルドして実行し、カスタム リボンとコンテンツ コントロールの統合をテストします。ソリューションを実行して、[Customer Data] (顧客データ) タブをクリックすると、先ほど追加したカスタム リボン ボタンが表示されます。これらのボタン (図 9 を参照) を文書の右上でクリックすると、文書のコンテンツ コントロールに特定のレコードが読み込まれて、データを参照できます。

図 9 招待状で顧客データを参照する
図 9 招待状で顧客データを参照する

このシナリオは単純ですが実用的です。このように、新しいリボン デザイナと Word コンテンツ コントロールを使用すれば、特定の顧客向けにカスタマイズされた招待状を読み込んで作成できる自動アプリケーションを作成することができます。VSTO を使用した 2007 Office system 開発についてさらに学習すれば、このシナリオを拡張して、より洗練されたカスタマイズ アプリケーションを作成できるようになります。では、次に Outlook フォーム領域について見てみましょう。

Outlook フォーム領域

Outlook 2007 では、フォーム領域を作成して、メール メッセージや連絡先などの Outlook アイテムに関する情報をユーザーに表示することができます。たとえば、顧客から電子メールを受信するとシステムでその顧客が検索されて顧客の履歴が表示されるようにしたり、連絡先アイテムを開くと連絡先の住所の地図が表示されるようにしたりすることができます。ここでは、メール メッセージに関するメモを追加できるフォーム領域を作成します。メモの入力や表示はメッセージの下のフォーム領域で行われ、作成されたメモはメッセージの非表示の添付ファイルとして保持されます (図 10)。

図 10 Outlook 2007 のフォーム領域
図 10 Outlook 2007 のフォーム領域(画像を縮小するには、ここをクリックします)
図 10 Outlook 2007 のフォーム領域
図 10 Outlook 2007 のフォーム領域 (画像を拡大するには、ここをクリックします)

Outlook 2007 のフォーム領域を作成するには、先ほどと同じように、まず Visual Basic または C# のプロジェクトを作成します。フォーム領域は、新しい [Outlook Form Region] (Outlook フォーム領域) 項目を追加するだけで簡単に追加できます。プロジェクトを右クリックし、コンテキスト メニューの [新しい項目の追加] をクリックします。[新しい項目の追加] ダイアログで [Outlook Form Region] (Outlook フォーム領域) を選択し、名前を NotesFormRegion.cs として設定します。[追加] をクリックすると、Outlook フォーム領域ウィザードが起動します。このウィザードに従ってフォーム領域を追加できます。まず最初に、作成するフォーム領域の種類を選択する必要があります。[Separate] (分離)、[Adjoining] (結合)、[Replacement] (置換)、および [すべて置換] の 4 種類から選択できます。

ここでは [Adjoining] (結合) を選択します。この場合、フォーム領域はインスペクタの下部に表示されます。[次へ] をクリックして続行します。名前を VSTO Notes に設定します。種類が [Adjoining] (結合) のフォーム領域では、タイトルや説明は設定できません。その他に、作成モード、閲覧モード、閲覧ウィンドウでのフォーム領域の表示方法を構成することもできます。すべての既定値をオンのままにして、[次へ] をクリックして続行します。

フォーム領域を結合する Outlook インスペクタを選択します。この例ではメール メッセージを使用して、メール アイテムが表示されたときにだけフォーム領域が表示されるようにします。以上で、ウィザードでフォーム領域を作成するために必要な情報をすべて指定できました。

[完了] をクリックすると、新しい NotesFormRegion.cs 項目がプロジェクトに追加され、フォーム領域デザイナが開きます。後は、Windows フォームをデザインするときと同じように、ツールボックスからデザイン画面にコントロールをドラッグしてフォーム領域をデザインできます。この例で追加するのは、メールのメモを表示するために使用する 1 つのリッチ テキスト ボックスだけです。ツールボックスからフォーム領域のデザイン画面に RichTextBox コントロールをドラッグします。コントロールを親コンテナの中にドッキングするように設定して、フォーム領域全体に表示されるようにします。メモらしく見えるように背景色を黄色に設定します。この時点で、完全に機能する Outlook フォーム領域ができあがります。F5 キーを押してビルドすると、Outlook が起動します。メール メッセージを開くと、メッセージの下部に黄色のテキスト ボックスが表示されます。

次にコードを記述して、フォーム領域でメモを入力できるようにします。フォーム領域デザイナを右クリックし、[コードの表示] をクリックします。コード ビューを見ると、フォーム領域が Microsoft.Office.Tools.Outlook.FormRegionControl クラスから派生していることがわかります。FormRegionControl 基本クラスには、コントロールを Outlook フォーム領域に埋め込むためのすべての基礎コードが含まれています。その他に 3 つのイベント ハンドラがあることもわかります。FormRegionInitializing イベントは、フォーム領域が最初に作成されるときに呼び出されます。このイベントを使用すると、フォーム領域の作成をキャンセルしたり、フォーム領域の結合先となる Outlook アイテムへの参照を受け取ったりすることができます。残りの 2 つのイベントは、FormRegionShowing と FormRegionClosed です。これらは、フォーム領域が表示されるときと閉じるときに呼び出されます。ここでは、これらのイベントにコードを追加します。フォーム領域が閉じるときには、リッチ テキスト ボックスのテキストをメッセージの非表示の添付ファイルとして保存する必要があります。テキストを添付するには、まず一時ファイルに保存する必要があります。フォーム領域が表示されるときには、非表示の添付ファイルからテキストを読み取って、リッチ テキスト ボックスに表示する必要があります。この場合も、最初に添付ファイルを一時ファイルに書き出す必要があります。図 11 にこのプロセスを示します。

Figure 11 Outlook 2007 のフォーム領域でテキストを編集できるようにする (Close Figure 11)
Outlook.MailItem mailItem;
string NotesPath = “C:\\VSTO Note.txt”;
string NoteDisplayName = “VSTO Note”;

private void NotesFormRegion_FormRegionShowing(
    object sender, System.EventArgs e)
{
    mailItem = (Outlook.MailItem)this.OutlookItem;
    foreach (Outlook.Attachment attachment in mailItem.Attachments)
    {
        if (attachment.DisplayName == NoteDisplayName)
        {
            attachment.SaveAsFile(NotesPath);
            richTextBox1.Text = System.IO.File.ReadAllText(NotesPath);
        }
    }
}

private void NotesFormRegion_FormRegionClosed(object sender,
    System.EventArgs e)
{
    if (richTextBox1.Modified)
    {
        System.IO.File.WriteAllText(NotesPath, richTextBox1.Text);
        foreach (Outlook.Attachment attachment in mailItem.Attachments)
        {
            if (attachment.DisplayName == NoteDisplayName)
            {
                attachment.Delete();
            }
        }
        if (richTextBox1.TextLength > 0)
        {
            long position = 0;
            mailItem.Attachments.Add(NotesPath,
                Outlook.OlAttachmentType.olEmbeddeditem, position,
                NoteDisplayName);
        }
        mailItem.Save();
    }
}

上の手順がすべて完了したら、F5 キーを押してプロジェクトを実行します。プロジェクトがビルドされて、Outlook 2007 が起動します。Outlook が起動したら、受信トレイのメッセージを開いてテキストを入力してみてください。

まとめ

この記事では、Office リボンのカスタマイズ、Word 文書へのコンテンツ コントロールの追加、カスタム リボンと Word 文書の統合、および Outlook フォーム領域の作成の方法を説明しました。これらは、Visual Studio "Orcas" Beta 1 リリースの VSTO で利用できる Office 開発機能のほんの一部にすぎません。

Steve Foxは、Visual Studio Tools for Office (VSTO) チームのプログラム マネージャです。VSTO のトレーニングや会議のセッションなどの VSTO の顧客との社外での活動と、VSTO 製品の進化に向けての開発チームとの社内での活動の両方に時間を割いています。

Paul Stubbsは、Visual Studio Tools for Office および Visual Tools for Applications 担当チームのプログラム マネージャです。『VSTO for Mere Mortals』(英語)(Addison-Wesley、2006 年) を Kathleen McGrath と共同で執筆しました。また、TechEd や TechReady では講演を行い、マイクロソフトのフォーラムでは開発者コミュニティに参加しています。彼のブログは、blogs.msdn.com/pstubbs (英語) で公開されています。


SILVERLIGHT: Web 上でより豊かな体験を作る

SILVERLIGHT

Web 上でより豊かな体験を作る

Laurence Moroney
この記事は、Silverlight のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
この記事の内容 : :
  • Silverlight と XAML の概要
  • 簡単な Silverlight アプリケーションを構築する
  • サーバー上で動的に Silverlight アプリケーションを生成する
  • XAML サービスを作成する
この記事は次のテクノロジを使用しています:
Silverlight、XAML、JavaScript
コードのダウンロード : : Silverlight2007_06.exe (300KB)
コンテンツ
Silverlight について
XAML の概要
XAML の内容
変形、メディア、アニメーション
簡単な Silverlight アプリケーション
Silverlight を使用したサーバー アプリケーション
天気予報アプリケーションを構築する
フェードイン アニメーションを定義する
XAML サービスを作成する
XAML を Silverlight フロントエンドに配信する
まとめ

次世代の Web 開発に向けた Microsoft の戦略における重要な要素が、Microsoft® Silverlight™ という新しいテクノロジです (以前のコードネームは "WPF/E")。Windows® Presentation Foundation の体験を Web で実現するために設計された Silverlight は、ASP.NET AJAX を含めた他の Web 開発環境にシームレスに適応する多彩で魅力的なコンテンツを提供します。

Web コミュニティ全体に行き渡るためには、広く使われているオペレーティング システム上の最も普及しているブラウザで Silverlight が正常に動作する必要があります。このため、最初のリリースでは、Mac OS X 上で動作する Firefox と Safari、および Windows 上で動作する Firefox と Internet Explorer® をサポートします。製品の進化と共に、より多くのオペレーティング システムとブラウザがサポートされるようになるはずです。これらの機能に加え、Silverlight は自己完結型で他の製品に依存しません。たとえば、ビデオ再生を Windows Media® Player に依存したり、XAML の解析を Microsoft .NET Framework 3.0 に依存したりすることはありません。

この記事では、Silverlight のアーキテクチャについて概説し、いくつかの Silverlight アプリケーションを実際に構築します。手始めに基本的な Hello World アプリケーションを作成し、その後に、簡単なメディア プレーヤーを構築する手順を説明します。また、クライアント側のテクノロジである Silverlight が、より大規模なサーバー指向の環境にも適応することを、PHP や Java を実行するサーバーでも使用できる点を含めて説明します。

Silverlight について

Silverlight は、根本的に XAML をレンダリングするブラウザ プラグインであり、その内部ドキュメント オブジェクト モデル (DOM) およびイベント モデルを、スクリプト形式でブラウザに公開します。つまり、設計者は、グラフィックス、アニメーション、およびタイムラインを含んだ XAML ドキュメントを作成でき、開発者はそれらをページ内でコードにフックして機能を実装できます。XAML は XML に基づいており、クライアントにダウンロードされる UI を定義するドキュメントはテキストベースであることから、検索エンジンやファイアウォールとの相性が優れています。さらに、XAML は実行時にサーバー アプリケーションで構築して発行できるので、多彩でグラフィカルな体験だけでなく、カスタマイズ性に富んだダイナミックな体験も実現します。

静的な XAML ファイルで UI を定義し、イベント処理に JavaScript を使用する、簡単な Silverlight アプリケーションのしくみを図 1 に示します。ブラウザは、プラグインをインスタンス化し、そのプロセスの中で XAML ファイルを読み込みます。このファイルに含まれるボタンのクリックなどのイベントは、ブラウザがキャプチャし、JavaScript が処理します。Silverlight コンテンツの DOM は公開されているので、JavaScript コードで Silverlight コンテンツを動的に更新して、レンダリングされたコンテンツの状態を変更することもできます。

図 1 サンプル アプリケーション
図 1 サンプル アプリケーション

Silverlight アプリケーションをサポートするアーキテクチャを図 2 に示します。メインのプログラミング インターフェイスは JavaScript DOM API です。このインターフェイスでは、Silverlight XAML 内で発生したイベントに応答できます (たとえば、コンテンツの読み込みが終了した場合やアニメーションが終了した場合など)。メソッドを呼び出してプレゼンテーションを操作することもできます (アニメーションの開始やビデオ再生の一時停止など)。このインターフェイスの下にあるのが XAML 解析エンジンです。パーサーは、XAML によって定義されたグラフィックスとアニメーションのレンダリングを処理するプレゼンテーションの中核で使用される XAML DOM を、メモリ内に作成します。さらに、ランタイムには WMV、WMA、MP3 の各マルチメディア コンテンツの再生に必要なコーデックが含まれています。

図 2 Silverlight のアーキテクチャ
図 2 Silverlight のアーキテクチャ

最後に、ランタイムにはレンダリング プロセスを管理するプレゼンテーションの中核が含まれています。このプレゼンテーション ランタイムが組み込まれているブラウザ プラグインは、前述のようにいくつかのブラウザを使用する Windows および Mac OS X のさまざまな特色をサポートします。その結果、自己完結型のグラフィックスおよびメディア レンダリング エンジンとして、ブラウザにプラグインし、JavaScript でスクリプト化することができます。

XAML の概要

XAML は、グラフィカル資産、ユーザー インターフェイス、動作、アニメーションなどの定義に使用できる XML ベースの言語です。マイクロソフトでは、.NET Framework 3.0 に組み込まれたデスクトップ指向のテクノロジである Windows Presentation Foundation で使用するマークアップ言語として XAML を導入しました。XAML は、アプリケーション作成時に設計者と開発者の共同作業を支援するように設計されています。

従来は、アプリケーションを作成する際に設計者と開発者が別々のツール スイートやリソースを使用していました。使用するツールセットが一致しないことで、作成されたアプリケーションに悪影響を与える可能性がありました。マイクロソフトが新たに導入した Expression ツール スイート、特に Microsoft Expression® Design と Microsoft Expression Blend では、設計担当者がそれぞれ作成したグラフィカル アーティファクトとユーザー インターフェイスを 1 つにまとめて、最終結果を XAML で出力し、開発者がこの XAML を使用してアプリケーションを構築できます。

Silverlight の最初のリリースで使用される XAML は、デスクトップで使用できる完全な XAML の Web 指向のサブセットであるという点で、Windows Presentation Foundation で使用される XAML と異なっています。このため、Windows Presentation Foundation の XAML に詳しいユーザーは、<Window> タグ、ページ リソース、データ バインド、リッチ コントロール モデルといった一部の機能がないことに気付くでしょう。

XAML では、XML タグを使用して要素を定義します。すべての Silverlight ドキュメントのルート レベルにあるのは Canvas タグです。このタグは、UI 要素が描画される領域を定義します。Canvas には、Silverlight で必要な XML 名前空間宣言が含まれている必要があります。

<Canvas
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
</Canvas>

Canvas は 1 つ以上の子を持つことができ、子 Canvas もそれぞれに子を作成できます。Canvas の子の相対位置は、ルート Canvas ではなくそれぞれの親 Canvas が基準となります。次に示すのは Rectangle を含んでいる Canvas の例です。この四角形は、その親の左上隅から 25 ピクセルの位置に配置されます。

<Canvas Width=”250” Height=”200”>
  <Rectangle Canvas.Top=”25” Canvas.Left=”25”
       Width=”200” Height=”150” Fill=”Black” />
</Canvas>

XAML の内容

Silverlight の XAML では多数の図形がサポートされており、それらを組み合わせて複雑なオブジェクトを作成できます。サポートされている基本図形は、Rectangle (四角形)、Ellipse (楕円)、Line (直線)、Polygon (多角形)、PolyLine (ポリライン)、および Path (パス) です。そのほとんどは説明不要でしょう。PolyLine では、一連の連結された線セグメントを定義できます。Path では、Canvas 上で非線形パス (走り書きのようなもの) を定義できます。

Brush (ブラシ) は、画面上でオブジェクトがどのように描画されるかを決定します。Brush の中身は Fill (塗りつぶし) を使用して描画され、輪郭は Stroke (ストローク) を使って描画されます。ブラシには、単色ブラシ、グラデーション ブラシ、およびイメージ ブラシがあります。単色ブラシは、塗りつぶし属性の固定色 (前の例の Fill="Black" など) を使用して実装するか、または次のように SolidColorBrush プロパティを付加して実装します。

   <Rectangle Canvas.Top=”25” Canvas.Left=”25”
          Width=”200” Height=”150”>
      <Rectangle.Fill>
         <SolidColorBrush Color=”Black” />
      </Rectangle.Fill>
   <Rectangle>

色は名前で指定するか (141 色の名前がサポートされています) または 16 進 RGB 定義で指定できます。

グラデーション ブラシは、正規化された空間で 1 つのグラデーション範囲と複数のグラデーション境界を定義することで実装されます。たとえば、右から左に移動しながらグレー シェードを経て黒から白へと変化する線形グラデーションを作成するとします。この場合、0 (正規化された線の始点) で黒となるグラデーション境界を指定し、1 (正規化された線の終点) で白となるグラデーション境界を指定します。後は Silverlight がグラデーションを描画します。グラデーションは 2 次元空間でも描画できます。その場合、正規化された四角形で空間を定義します (0,0 は左上、1,1 は右下)。したがって、左上を赤、右下を黒、およびその間を滑らかなグラデーションで塗りつぶす 2 次元グラデーションの四角形を定義するには、次のような XAML を使用します。

<Rectangle Width=”200” Height=”150” >
  <Rectangle.Fill>
    <LinearGradientBrush StartPoint=”0,0” EndPoint=”1,1”>
      <LinearGradientBrush.GradientStops>
        <GradientStop Color=”Red” Offset=”0” />
        <GradientStop Color=”Black” Offset=”1” />
      </LinearGradientBrush.GradientStops>
    </LinearGradientBrush>
  </Rectangle.Fill>
</Rectangle>

オブジェクトは ImageBrush を使用して描画することもできます。イメージは必要に応じて切り捨てられたり引き伸ばされたりします。したがって、たとえば ImageBrush を使用して楕円の塗りつぶしを指定する場合は、次のような XAML を使用します。

<Ellipse Width=”200” Height=”75” >
  <Ellipse.Fill>
    <ImageBrush ImageSource=”http://.../logo.jpg” />
  </Ellipse.Fill>
</Ellipse>

テキストを XAML でレンダリングする場合は、TextBlock タグを使用します。このタグで、テキストの内容、フォント、サイズ、折り返しなどの各要素を制御できます。次に例を示します。

<TextBlock>Hello</TextBlock>

<TextBlock FontSize=”18”>Hello</TextBlock>

<TextBlock FontFamily=”Courier New”>Hello</TextBlock>

<TextBlock TextWrapping=”Wrap” Width=”100”>
  Hello there, how are you?
</TextBlock>

さらに、Silverlight はテキスト入力を実装するためのキーボード イベントをサポートしています。ルート要素でキーボード イベント (KeyDown や KeyUp) を定義し、イベント引数を使用して押下されたキーを取得することができます。

変形、メディア、アニメーション

XAML では、オブジェクトに対して複数の変形を定義できます。変形には、指定の角度で要素を回転させる RotationTransform、オブジェクトを拡大縮小する ScaleTransform、指定の方向に指定の量だけオブジェクトを傾斜させる SkewTransform、指定のベクトルに従ってオブジェクトを移動する TranslateTransform、およびこれらすべてを組み合わせた MatrixTransform があります。

複数の変形を 1 つにまとめてグループの一部として定義することで、オブジェクトに複数の変形を設定できます。図 3 に例を示します。この例では、Canvas に 3 つの楕円が配置されています。変形が Canvas レベルで定義されているため、3 つの楕円それぞれが回転し、拡大縮小されます。

Figure 3 複数の変形 (Close Figure 3)
<Canvas xmlns=”...” xmlns:x=”...”>
  <Canvas.RenderTransform>
    <TransformGroup>
      <RotateTransform Angle=”-45” CenterX=”50” CenterY=”50”/>
      <ScaleTransform ScaleX=”1.5” ScaleY=”2” />
    </TransformGroup>
  </Canvas.RenderTransform>
  <Ellipse Width=”100” Height=”100” Fill=”Yellow” />
  <Ellipse Canvas.Top=”25” Canvas.Left=”25” Width=”10” Height=”10”
           Fill=”Black” />
  <Ellipse Canvas.Top=”25” Canvas.Left=”65” Width=”10” Height=”10”
           Fill=”Black” />
</Canvas>

オーディオおよびビデオ コンテンツは、MediaElement タグを使って制御します。このタグには、再生されるメディアを指すソース属性を指定します。このタグを使用して定義されるオブジェクトは、メディア再生を制御する多数のメソッドとイベントを提供します。この記事の後半で、XAML を使用して構築され、JavaScript でユーザーとの対話 (再生、一時停止、停止など) を実現する簡単なメディア プレーヤーの例を紹介します。MediaElement の定義は簡単です。

<Canvas xmlns=”...” xmlns:x=”...”>
   <MediaElement Source=”xbox.wmv” />
</Canvas>

XAML でのアニメーションは、プロパティの経時的変化をタイムラインで定義することで実装されます。アニメーションの定義は、Storyboard に含まれています。アニメーションには、数値プロパティを変更する DoubleAnimation、色とブラシを変更する ColorAnimation、2 次元値を変更する PointAnimation などのさまざまな種類があります。これらのアニメーションは、線形ベースまたはキーフレーム ベースです。線形アニメーションの場合は、アニメーションが定義されたタイムラインに沿って滑らかに変化します。キーフレーム ベースのアニメーションでは、途中の離散値間をアニメーションが移動します。

図 4 は、円の幅が徐々に変化する単純なアニメーションの例です。AutoReverse プロパティを True に設定することで、アニメーションを途中で反転させながら継続するようエンジンに指示しています。つまり、円の幅を縮小する変形を繰り返すのではなく、円の幅が縮小した後に拡大されるようにしています。

Figure 4 単純なアニメーション (Close Figure 4)
<Canvas xmlns=”...” xmlns:x=”...”>
  <Canvas.Triggers>
    <EventTrigger RoutedEvent=”Canvas.Loaded”>
      <BeginStoryboard>
        <Storyboard>
          <DoubleAnimation
            Storyboard.TargetName=”theCircle”
            Storyboard.TargetProperty=”Width”
            From=”200” To=”1” Duration=”0:0:2”
            AutoReverse=”True”/>
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Canvas.Triggers>
  <Ellipse x:Name=”theCircle” Width=”100” Height=”100” Fill=”Yellow” />
</Canvas>

簡単な Silverlight アプリケーション

Visual Studio 2005 では、Silverlight 用のアプリケーション テンプレート (Silverlight SDK ダウンロード付属) を使用して、Silverlight 対応アプリケーションを簡単に構築できます。この記事では Visual Web Developer™ Express アプリケーションを使用しました。これは MSDN® からダウンロードして入手できます。

この例は、WMV ビデオ コンテンツを再生するごく簡単なメディア プレーヤーです。この例では Channel 9 のビデオ (channel9.msdn.com) を使用しますが、再生するビデオは自由に変更できます。XAML はきわめて簡単で、図 5 にそのすべてが示されています。簡素化のために、[再生]、[停止]、[一時停止] の各ボタンには TextBlock 要素を使用しています。

Figure 5 簡単なメディア プレーヤー (Close Figure 5)
<Canvas
    xmlns=”http://schemas.microsoft.com/client/2007”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    Loaded=”javascript:root_Loaded”>

    <MediaElement
        x:Name=”mPlayer” Width=”640” Height=”400”
        Source=”http://download.microsoft.com/download/6/5/2/6527a090-aef4-4ed2-8d7e-8946e94257a0/WM_IN_Sumit_Chauhan.wmv” />

    <TextBlock Canvas.Top =”0” Text=”Play”
        MouseLeftButtonDown=”javascript:PlayVideo”>
    </TextBlock>
    <TextBlock Canvas.Top =”16” Text=”Stop”
        MouseLeftButtonDown=”javascript:StopVideo”>
    </TextBlock>
    <TextBlock Canvas.Top =”32” Text=”Pause”
        MouseLeftButtonDown=”javascript:PauseVideo”>
    </TextBlock>
</Canvas>

ビデオの定義には MediaElement タグを使用しています。これに mPlayer という名前を指定し、大きさを 640×400 に設定しています。Source タグで指している WMV ビデオは、マイクロソフトの Web サイトでダウンロードとしてホストされているものです。

次に 3 つの TextBlock 要素を使って [再生]、[停止]、[一時停止] の各ボタンを定義しています。これらのボタンは、キャンバスの左上に間隔をおいて縦に配置されます。Canvas.Left は設定されておらず、その既定値は 0 であることに注意してください。これらの TextBlock 要素では、クリックされたとき (MouseLeftButtonDown) に起動する JavaScript イベント ハンドラを定義しています。これらのボタン上でマウス ボタンが押されると、Silverlight からブラウザにイベントが渡され、そこでイベントがキャプチャされて、JavaScript によって処理されます。

では、この XAML を使用し JavaScript イベント ハンドラを実装するアプリケーションを Visual Web Developer Express で作成してみましょう。これを行うには silverlight.js ファイルが必要です。このファイルは、Silverlight 用 SDK ダウンロードまたは MSDN のサンプル ダウンロード サイトにあるいずれかの Silverlight サンプルから入手できます。

Visual Web Developer Express を使用して新しい Web サイトを作成します。このサイト内で、\js フォルダを作成し、このフォルダに silverlight.js ファイルを追加します。\js フォルダを右クリックし、[新しい項目の追加] をクリックします。[new file](新しいファイル) ダイアログ ボックスで [JScript ファイル] を選択し、eventhandlers.js という名前を付けます。

次に、ソリューション エクスプローラでプロジェクトを右クリックし、[新しい項目の追加] をクリックして、videoplayer.xaml という新しい XML ファイルを作成します。このファイルに図 5 に示した XAML を入力します。

最後に、新しい HTML ページをプロジェクトに追加し、Default.htm という名前を付けます。この HTML ページで JavaScript ライブラリを読み込み、それらのライブラリを使って XAML コンテンツをレンダリングする Silverlight プレーヤーのインスタンスを作成するように、HTML ページを編集します。図 6 に、Default.htm のソース コード全体を示します。

Figure 6 Default.htm によるメディア プレーヤーの読み込み (Close Figure 6)
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
    “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>

<html xmlns=”http://www.w3.org/1999/xhtml” >
<head>
    <title>Untitled Page</title>
    <script type=”text/javascript” src=”js/silverlight.js”></script>
    <script type=”text/javascript” src=”js/eventhandlers.js”></script>
</head>
<body>
    <form>
         <div id=”AgControl1Host” style=”background:#FFFFFF”>
            <script type=”text/javascript”>
                Sys.Silverlight.createObjectEx(
                    {source: ‘videoplayer.xml’,
                     parentElement:’AgControl1Host’,
                     Id:’AgControl1’,
                     properties:{
                         width:’1024’,
                         height:’530’,
                         background:’white’,
                         isWindowless:’false’,
                         framerate:’24’,
                         version:’0.8.5’},
                     events:{onError:null, onLoad:null,
                         onNotInstalled:null},
                     context:null});
            </script>
         </div>
    </form>
</body>
</html>

ページ最上部のスクリプト タグで、JavaScript ライブラリ silverlight.js および eventhandlers.js がインポートされます。silverlight.js ライブラリは、Silverlight プラグインのダウンロードとインスタンス化を管理します。このプラグインにはブラウザおよび OS の抽象化が含まれているので、それについて心配する必要はありません。後は Sys.Silverlight.createObjectEx 関数で新しい Silverlight コントロールを実装するだけです。Silverlight プラグイン自体は、ID を割り当てられた DIV ブロックに含まれている必要があります。この例では、プラグインを含んでいる DIV の名前が AgControl1Host となっています。この ID は、Silverlight コンテンツをインスタンス化する際に使用します。

Silverlight プラグインをインスタンス化するには、構築に必要な一連のパラメータを Silverlight コントロールに渡してそのコントロールの新しいインスタンスを作成します。これらのパラメータの詳細については、Silverlight SDK を参照してください。

最後の手順では、[再生]、[停止]、[一時停止] の各ボタンがクリックされると発生するイベントを処理する JavaScript 関数を実装します。次のコードを eventhandlers.js に挿入します。

function PlayVideo(sender,args)
{
   var mPlayer = sender.findName(“mPlayer”);
   mPlayer.Play();
}
function PauseVideo(sender,args)
{
   var mPlayer = sender.findName(“mPlayer”);
   mPlayer.Pause();
}
function StopVideo(sender,args)
{
   var mPlayer = sender.findName(“mPlayer”);
   mPlayer.Stop();
}

XAML では、[停止] ボタンの TextBlock で、JavaScript:StopVideo を指す MouseLeftButtonDown 属性が定義されていました。この関数を実装するために、sender パラメータと args パラメータを受け取る同じ名前の JavaScript 関数を作成します。Silverlight の findName 関数を使用すると、指定したパラメータと一致する要素をその DOM 内で検索できます。したがって、ビデオの再生を停止するには、findName API を使用して MediaElement を検索します。XAML では、この要素の名前が mPlayer となっていました。findName API から返されるこのオブジェクトへの参照を、JavaScript 変数に読み込むことができます。参照を取得した後は、その Stop メソッドを呼び出すだけでビデオ再生を停止できます。[再生] ボタンと [一時停止] ボタンの機能も同様です。

これで作業は完了です。簡単なビデオ プレーヤーが Silverlight であっという間に作成されました。アプリケーションを実行すると、図 7 のような画面が表示されます。

図 7 簡単なビデオ プレーヤー
図 7 簡単なビデオ プレーヤー(画像を縮小するには、ここをクリックします)
図 7 簡単なビデオ プレーヤー
図 7 簡単なビデオ プレーヤー (画像を拡大するには、ここをクリックします)

この例は、ビデオの URI を XAML にハードコードする場合は何の問題もありませんが、実現性の高いユース ケースとは言えません。では、これを静的な HTML ページから、ビデオをパラメータとして受け取ることのできるアクティブな Web フォームに変更してみましょう。

まず、メディア ファイルへの参照を XAML から削除します。XAML の MediaElement から Source 属性を削除します。この作業が完了すると、次のようなコードになります。

<MediaElement x:Name=”mPlayer” Width=”640” Height=”400”/>

次に、アプリケーション内に VideoPlayer.aspx という新しい Web フォーム (ASPX) を作成します。このフォームでは、図 6 で行ったように、videoplayer.xaml に記述した Silverlight コントロールをインスタンス化する必要があります。さらに、次の JavaScript コードを含んでいる必要があります。

<script type=”text/javascript”>
  function root_Loaded(sender,args) {
    var mPlayer = sender.findName(“mPlayer”);
    mPlayer.Source =
      “<% Response.Write(Request.Params[“VideoURI”]); %>”;
    mPlayer.Play();
}
</script>

XAML には、Canvas が読み込まれたときに起動されるイベントのイベント宣言が含まれています。このイベントは、JavaScript の root_Loaded イベント ハンドラによってキャプチャされます。この関数は、ASP.NET を使用してサーバー上の HTTP 要求から VideoURI を取り出し、その VideoURI を使用して JavaScript 関数に情報を提供することで、ビデオ ソースを設定します。ページが実行されると、VideoURI パラメータで指定されたビデオが Silverlight によってレンダリングされます。このごく簡単な例では、エラー処理やフィルタ処理を一切行っていないことに注意してください。アプリケーションでこの種の機能が必要な場合は、これらの処理を追加する必要があります。

次のような URI でこのページを呼び出す場合は注意してください。

http://localhost/MSDN1/
VideoPlayer.aspx?videouri=xbox.wmv

ASP.NET は、次のような JavaScript ブロックを含んだページを出力します。

   <script type=”text/javascript”>
     function root_Loaded(sender,args)
     {
       var mPlayer =
         sender.findName(“mPlayer”);
       mPlayer.Source = “xbox.wmv”;
       mPlayer.Play();
     }
   </script>

この JavaScript が xbox.wmv ファイルを MediaElement に読み込み、Silverlight がビデオを再生します。つまり、サーバー側のアクティブ ページ (ASPX、PHP、または Java) を使用した方が、Silverlight エンド ユーザーの体験をはるかに柔軟に操作でき、体験の質を大きく高めることができます。

Silverlight を使用したサーバー アプリケーション

Silverlight を、単にブラウザにリッチ コンテンツを埋め込むためのクライアント テクノロジとして捕らえるのは正しくありません。このようなリッチ コンテンツは、Java アプレット、ActiveX® コントロール、あるいは Flash アプリケーションとして構築された閉鎖的なプラグインを使用することで既に実現可能です。これに対し Silverlight は、UI がテキストベースの XAML で定義され、JavaScript を使用してプログラミング可能であるという点で、オープンなテクノロジです。これにより開発者は、バックエンド サーバーと対話するアプリケーションを容易に構築できます。

たとえば、天気予報アプリケーションについて考えてみましょう。クライアント側で天気予報アプリケーションを作成する場合は、Web サービスを利用する Java アプレット、ActiveX コントロール、または Flash アプリケーションを構築し、そのアプレットをクライアントに展開します。しかしこれではクライアントとサーバー間の通信ニーズが増大します。このデータ ソースが有料加入サービスだとどうなるでしょう。アプリケーションを展開する担当者は、データ サービスにアクセスするすべてのクライアントのライセンスを認証する作業に追われ、ドメイン固有のビジネス ロジックを構築する時間が取られてしまいます。

しかし、アプリケーションをサーバー側で作成し、すべてのデータをクライアントに返すようにすれば、この作業は軽減されます。Expression Blend などのツールを使用して、アプリケーションのユーザー インターフェイスのテンプレートを作成し、XAML として表現できます。開発者は、実行時に関連データを受け取ってテンプレートに挿入し、XAML をレンダリングするクライアントに完成した XAML を返します。クライアント側ではロジックや接続が不要となり (処理を行うサーバーとの最初の接続は当然必要です)、クライアントの展開と管理も比較的簡単になります。

天気予報アプリケーションを構築する

ここでは、天気予報アプリケーションを実装する単純な XAML ファイルを作成する方法を説明します (図 8 を参照)。完成した天気予報アプリケーションは、ブラウザでアドレス内のパラメータとしてアプリケーションに渡された特定の郵便番号に対する 3 日分の気温予想を表示します。また、グラフィカルに表された気象情報、正しい日付、郵便番号で特定された地域の名称、および気象データのアニメーションを表示します。この天気予報アプリケーションの完全な XAML は、MSDN Magazine Web サイトから入手できます。

図 8 Silverlight 天気予報アプリケーション
図 8 Silverlight 天気予報アプリケーション(画像を縮小するには、ここをクリックします)
図 8 Silverlight 天気予報アプリケーション
図 8 Silverlight 天気予報アプリケーション (画像を拡大するには、ここをクリックします)

ドキュメントの最上部にあるのは開始 Canvas タグです。これはオブジェクトを描画する場所となる基本コンテナです。Canvas は複数作成できるので、Canvas を使って関連する要素をグループ化すると合理的です。たとえば、日ごとに含まれているグラフィックス、最高気温、最低気温、および各種のラベルが、すべて 1 つの XAML テンプレートに集められます。図 9 は、ある特定の日の例を示しています。この例では、Canvas に多数の TextBlock と 1 つの Image コントロールが含まれています。このキャンバスは、cnv2 という名前を付けられ、不透明度 0 (最初は表示されないことを意味します) で画面上に配置されます。画面上の特定の位置に配置するために、その高さ、幅、および左と上の位置が設定されます。

Figure 9 ある 1 日について収集される要素 (Close Figure 9)
<Canvas x:Name=”cnv2” Width=”624” Height=”101” Opacity=”0”
        Canvas.Left=”8” Canvas.Top=”165”>

<TextBlock x:Name=”lblDate2” Width=”513” Height=”26” Canvas.Left=”97” FontFamily=”Tahoma” FontSize=”20” FontWeight=”Bold” Foreground=”#FF24AB46” Text=”Wednesday, December 18, 2006” TextWrapping=”Wrap”/>

<TextBlock x:Name=”lblHigh2” Width=”70” Height=”59” Canvas.Left=”112” Canvas.Top=”30” FontFamily=”Tahoma” FontSize=”48” FontWeight=”Bold” Foreground=”#FFF64F12” Text=”27” TextWrapping=”Wrap”/>

<TextBlock x:Name=”lblDegHigh2” Width=”62” Height=”59” FontFamily=”Tahoma” FontSize=”48” FontWeight=”Bold” Foreground=”#FFF64F12” Text=”°F” TextWrapping=”Wrap” Canvas.Left=”172” Canvas.Top=”30”/>

<TextBlock x:Name=”lblLow2” Width=”71” Height=”59” FontFamily=”Tahoma” FontSize=”48” FontWeight=”Bold” Foreground=”#FF444DB5” Text=”27” TextWrapping=”Wrap” Canvas.Left=”265” Canvas.Top=”30”/>

<TextBlock x:Name=”lblDegLow2” Width=”67” Height=”59” FontFamily=”Tahoma” FontSize=”48” FontWeight=”Bold” Foreground=”#FF444DB5” Text=”°F” TextWrapping=”Wrap” Canvas.Left=”327” Canvas.Top=”30”/>

<Image x:Name=”imgWeather2” Width=”97” Height=”87” Canvas.Top=”4” Source=”http://www.nws.noaa.gov/weather/images/fcicons/ra80.jpg” Stretch=”Uniform”/>

</Canvas>

TextBlock は、Silverlight でテキストをレンダリングするために使用されます。この例では多数の TextBlock が使用されており、その一部は "°F" ラベルなどのハードコードされたラベルとして使用され、lblDegHigh2 および lblDegLow2 という TextBlock に格納されます。名前は x:Name 属性で確認してください。

lblDate2、lblHigh2、lblLow2 の各 TextBlock には、プレースホルダ情報が含まれています。現時点では特定の日付と気温がハードコードされていますが、後から ASP.NET コードによって指定の郵便番号に応じた実際の日付と気温で上書きされます。

フェードイン アニメーションを定義する

Canvas ノードは、イベントに応じてアクションを起動するトリガをサポートします。この例では、次に示すように、ページが読み込まれたときに発生する Canvas.Triggers が指定されています。

<Canvas.Triggers>
  <EventTrigger RoutedEvent=”FrameworkElement.Loaded”>
    <BeginStoryboard>
...

こうして、ページが読み込まれたときにトリガが起動され、アニメーション Storyboard が開始されます。アニメーション Storyboard は、アニメーションの動作を定義します。次に示すのは、cnv1 という Canvas (テキスト プレースホルダと特定の日の気象情報のグラフィックスを含む) で使用されるアニメーションです。

<DoubleAnimationUsingKeyFrames BeginTime=”00:00:00”
    Storyboard.TargetName=”cnv1”
    Storyboard.TargetProperty=”(UIElement.Opacity)”>
  <SplineDoubleKeyFrame KeyTime=”00:00:00” Value=”0”/>
  <SplineDoubleKeyFrame KeyTime=”00:00:01” Value=”1”/>
</DoubleAnimationUsingKeyFrames>

アニメーションの種類は、DoubleAnimationUsingKeyFrames です。Canvas とその子の不透明度 (UIElement.Opacity で定義) を 0 から 1 に修正して、一定の時間を掛けてアニメーションがフェードインするようにします。この時間は、アニメーションに関連付けられたキーフレームで定義されます。

XAML がレンダリングを行うと、Storyboard が開始され、レンダリングの開始時点 (0 秒) から 1 秒後の間に、最初の要素が不透明度 0 から 1 へとフェードインします。XAML を見ると、他のキャンバスがそれぞれ 1 秒から 2 秒の間および 2 秒から 3 秒の間にフェードインして、各キャンバスが順番に表示されるようになっていることがわかります。

XAML は一種の有効な XML であるため、要素に属性で名前を指定でき、その名前によってそれらの要素を処理できるようになります。たとえば、1 日目の最高気温に対するプレースホルダ要素は次のようになります。

<TextBlock x:Name=”lblHigh1” Width=”67” Height=”59”
        Canvas.Left=”112” Canvas.Top=”30” FontFamily=”Tahoma”
        FontSize=”48” FontWeight=”Bold” Foreground=”#FFF64F12”
        Text=”27” TextWrapping=”Wrap”/>

この TextBlock には lblHigh1 という名前が付けられています。サービス (次の手順で説明) は XMLDocument を使用してテンプレート XAML を読み込み、この名前のノードを検索して、次のような C# コードでそのテキスト プロパティを変更します。

string xpath = “//d:TextBlock[@x:Name=’lblHigh1’]”;
xNode = xmlDoc.SelectSingleNode(xpath, mng);
string high = myResults[n - 1].MaxTemperatureF;
xNode.Attributes[“Text”].Value = high;

XAML サービスを作成する

Web ページで Silverlight を使用する際にコントロールが受け取るパラメータの 1 つが、レンダリングする XAML の場所です。この例では、XAML が動的であり、天気予報が要求されている郵便番号に基づいて変化します。このため、この XAML を生成して返す Web アプリケーションを作成し、その XAML をレンダリングするページはこのページを参照することになります。これがこの Silverlight アプリケーションを構築するための最初の手順です。

Visual Studio® を使用して新しい Web プロジェクトを作成します。前の例のビデオ プレーヤーのときと同様に、silverlight.js を含んだ \js フォルダを追加します。プロジェクトに新しい Web 参照を追加して、次の場所で定義されている Web サービスを参照します。

http://www.webservicex.net/WeatherForecast.asmx?WSDL

このサービスに weatherservice という名前を付けます。プロジェクトに新しい Web フォームを追加し、XAMLSource.aspx という名前を付けます。[Add Code as a Separate Page](コードを別ページとして追加) オプションを選択します。コード ページの最上部に、次の一連の using ディレクティブがあることを確認してください。

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Xml.XPath;

この後は、XAMLSource.aspx 上の Page_Load イベント ハンドラにコードを追加します。まず、テンプレート (Scene.xaml) を XMLDocument オブジェクトに読み込みます。

XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(Server.MapPath(“Scene.xaml”));

次に読み取りパラメータを設定します。ページを呼び出す際に、必要な郵便番号を指定するクエリ文字列パラメータを使用します。

http://server/XamlSource.aspx?ZIP=<something>

パラメータを取得するには、次のコードを使用します。

string strZip = “98052”;
if (Request.Params[“ZIP”] != null)
    strZip = Request.Params[“ZIP”];

これによりパラメータ化された ZIP (郵便番号) をローカルに保持する文字列が作成され、ZIP が HTTP 要求の一部として渡されない場合はこれが既定値に設定されます。次に、気象情報を提供する Web サービスへの呼び出しを作成します。

weatherservice.WeatherForecast myWeather = new
    weatherservice.WeatherForecast();

weatherservice.WeatherForecasts myForecast =
    myWeather.GetWeatherByZipCode(strZip);

weatherservice.WeatherData[] myResults = myForecast.Details;

このコードによって、Web サービス プロキシのインスタンスが作成され (サービスの WSDL への参照を追加するときに Visual Studio が自動的に作成)、myWeather という名前が付けられます。この Web サービスは WeatherForecasts データ構造体にデータを返します。つまり、サービスに対して GetWeatherByZipCode メソッドを呼び出すことで、このデータ構造体のインスタンス (myForecast) が作成され、そのインスタンスに文字列が渡されます。予報オブジェクトの Details メンバは、WeatherData 型の配列であり、変数 myResults はこの配列のインスタンスとして設定されます。

次の手順では、XPath を使用して検索を行うために XMLDocument を初期化します。XML では XAML スキーマを一部の要素の名前空間として使用するため (x:Name のように x をプレフィックスとして付加)、ページ内のすべての要素 (プレフィックスのない要素も含む) に対して 1 つの名前空間を定義する必要があります。たとえば、XPath の //TextBlock と一致するノードは検索できませんが、既定の名前空間に新しいプレフィックス (ここではダミーを表す d) を付加するように指定し、このプレフィックスを使用して既定のノードを検索できます。つまり、//d:TextBlock は検索可能です。このコードを次に示します。

NameTable myn = new NameTable();
XmlNamespaceManager mng = new XmlNamespaceManager(new NameTable());
mng.AddNamespace(“d”,
    “http://schemas.microsoft.com/winfx/2006/xaml/presentation”);
mng.AddNamespace(“x”, “http://schemas.microsoft.com/winfx/2006/xaml”);

XPath を使用できれば、都市名を含んだノードを検索し、その Text プロパティをサービスが郵便番号から特定した都市名に変更できます。

XmlNode xNode = xmlDoc.SelectSingleNode(
    “//d:TextBlock[@x:Name=’lblTownName’]”, mng);

xNode.Attributes[“Text”].Value =
    myForecast.PlaceName + “,” + myForecast.StateCode;

同様に、次の 3 日間をループ処理し、その値の目的の位置を含んだプレースホルダ ノードを検索して、さらに正しい値を XML に取り込みます。この処理を行うコードを図 10 に示します。

Figure 10 気象情報の更新 (Close Figure 10)
string strXPath = “”;
string strDate = “”;
string strHigh = “”;
string strLow = “”;
string strWeatherURL = “”;

for (int n = 1; n <= 3; n++)
{
  strXPath = “//d:TextBlock[@x:Name=’lblDate” + n.ToString() + “’]”;
  xNode = xmlDoc.SelectSingleNode(strXPath, mng);
  strDate = myResults[n - 1].Day;
  xNode.Attributes[“Text”].Value = strDate;

  strXPath = “//d:TextBlock[@x:Name=’lblHigh” + n.ToString() + “’]”;
  xNode = xmlDoc.SelectSingleNode(strXPath, mng);
  strHigh = myResults[n - 1].MaxTemperatureF;
  xNode.Attributes[“Text”].Value = strHigh;

  strXPath = “//d:TextBlock[@x:Name=’lblLow” + n.ToString() + “’]”;
  xNode = xmlDoc.SelectSingleNode(strXPath, mng);
  strLow = myResults[n - 1].MinTemperatureF;
  xNode.Attributes[“Text”].Value = strLow;

  strXPath = “//d:Image[@x:Name=’imgWeather” + n.ToString() + “’]”;
  xNode = xmlDoc.SelectSingleNode(strXPath, mng);
  strWeatherURL = myResults[n - 1].WeatherImage;
  xNode.Attributes[“Source”].Value = strWeatherURL;
}

これで XML ドキュメントへのテンプレートと正しいデータの挿入が完了しました。最後の手順では、この XML ドキュメントを呼び出し元に返します。これを行うには、MIME の種類を設定し、XMLDocument オブジェクトの内容を次のように応答ストリームに直接書き込みます。

Response.ContentType = “text/xml”;
Response.Write(xmlDoc.InnerXml);

次に、Visual Studio によって ASPX ページに設定されたプレースホルダ HTML を削除します。1 行目を除くすべての内容を ASPX ファイルから削除します。作業が完了すると次のようなコードになります。

<%@ Page Language=”C#” AutoEventWireup=”true”
         CodeFile=”XamlSource.aspx.cs” Inherits=”_Default” %>

最後に、新しい XML ファイルをソリューションに追加し、Scene.xaml という名前を付けます。このファイルは前に示したコードで参照されていたものです。天気予報 XAML 全体を取得し、このファイルに貼り付けます。これでページを実行できるようになり、XAMLSource.aspx ページから返された XAML がブラウザに表示されます。

XAML を Silverlight フロントエンドに配信する

前の手順では、テンプレート XAML を構築し、さらにこの XAML を受け取り、Web サービスを利用し、Web サービス呼び出しの結果を XAML の正しい場所に挿入する Web フォームを構築しました。次の手順では、Silverlight コントロールを含み、そのコントロールをこの XAML サービスに向けるページを提供します。

前のセクションで使用した Web サイトを開き、Default.aspx という新しい Web フォームを追加します。Default.aspx 上の HTML コードを図 11 のように変更します。

Figure 11 Default.aspx に対する追加の変更 (Close Figure 11)
<%@ Page Language=”C#” AutoEventWireup=”true”
    CodeFile=”Default.aspx.cs” Inherits=”_Default” %>

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
 “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>

<html xmlns=”http://www.w3.org/1999/xhtml”>
<head>
    <title>Weather</title>
    <meta http-equiv=’Content-Type’
          content=”text/html; charset=windows-1252”/>
    <script type=”text/javascript” src=”js/silverlight.js”></script>
</head>
<body>
<div id=”AgControl1Host” style=”background:#FFFFFF”>
    <script type=”text/javascript”>
        Sys.Silverlight.createObjectEx(
            {source: ‘WeatherSite/XamlSource.aspx?ZIP=<%
                        if (Request.Params[“ZIP”] == null)
                            Response.Write(“98052”);
                        else
                           Response.Write(Request.Params[“ZIP”]);
              %>’, parentElement:’AgControl1Host’, Id:’AgControl1’,
              properties:{width:’1024’, height:’530’,
                  background:’white’, isWindowless:’false’,
                  framerate:’24’, version:’0.8.5’},
              events:{onError:null, onLoad:null, onNotInstalled:null},
                  context:null});
    </script>
</div>
</body>
</html>

このコードの重要な部分は Silverlight.createObjectEx の呼び出しです。ここでは、静的な videoplayer.xaml が XAML ソース アプリケーションの呼び出しで置換され、その際にこの Default.aspx ページが郵便番号に関して受け取ったパラメータが渡されます。

“WeatherSite/XamlSource.aspx?ZIP=<%
    if (Request.Params[“ZIP”] == null)
        Response.Write(“98052”);
    else
        Response.Write(Request.Params[“ZIP”]);
%>”

これにより、このページに対する要求パラメータが URL に追加され、最後の手順で作成した XamlSource ページが呼び出されます。ここからさらに XamlSource が呼び出され、郵便番号が渡され、XAML ドキュメントが返されて Silverlight によってレンダリングされます。Default.aspx?ZIP=90210 の実行結果は、図 8 で確認できます。

まとめ

この記事では、Silverlight について詳しく紹介しました。また、"Hello, World" の表示よりも高度な処理を行うアプリケーションをいくつか紹介する中で、XAML を使用してユーザー インターフェイスを構築する方法や、JavaScript と連携して対話処理を実現する方法、純粋なクライアント パラダイムからサーバーベースのパラダイムに移行してアプリケーションの柔軟性を高める方法を見てきました。この方法論を使用して、ビデオ ストリームの URI を受け取ってそのストリームを再生する、ブログなどのシナリオに適した簡単なビデオ プレーヤーを作成する方法も説明しました。

さらに、ごく単純なクライアント配信モデルを備えた気象モニタの形式でデータ認識アプリケーションを構築する方法を学習しました。Silverlight コンポーネントでサーバーを呼び出して正しい XAML を取得し、それをレンダリングすることで、リッチなシン クライアントを構成することができました。

このような Web アプリケーションを構築する際に非常に高い生産性を実現するプラットフォームは ASP.NET と Microsoft スタックですが、この記事のダウンロードにはこのアプリケーションの Java バージョンも含まれています。このバージョンは ASP.NET バージョンと同じ機能を提供します。つまり、Web サービスを利用してデータを取得し、XAML を XML ドキュメント DOM に読み込みます。さらに、テンプレート データの適切なノードを検索し、その値を Web サービスから取得した値で置換して、完成した XAML を URI として書き込みます。その後に、このサーブレットの URL を参照する JSP (未掲載) が構築されて、Silverlight を使用して XAML がレンダリングされます。

Web での Silverlight の利用はまだ始まったばかりです。XAML レンダリング エンジンの豊富な機能と、標準の Web 開発テクノロジを使用できる柔軟性を兼ね備えた Silverlight は、開発者用ツールボックスに加えるだけの価値があります。詳細については、MSDN Silverlight デベロッパー センターをご覧ください。

Laurence Moroneyは、Microsoft Silverlight テクノロジのエバンジェリストです。これまで執筆したコンピューティング関連書籍は 9 冊、技術記事は 100 本以上に上り、その中の『Programming Web.NEXT』が間もなく Microsoft Press から出版されます。記事やコードを書いていないときは、たいてい Silverlight における機能サポートについて製品チームを質問攻めにしています。


同時実行: Windows Vista の新しい同期プリミティブ

同時実行

Windows Vista の新しい同期プリミティブ

Robert Saccone and Alexander Taskov
コードのダウンロード : : VistaSynchronization2007_06.exe (174KB)
この記事の内容 : :
  • 条件変数
  • slim reader/writer ロック
  • one-time initialization
  • ダブルチェック ロッキング
この記事は次のテクノロジを使用しています:
Windows Vista、C++
コンテンツ
条件変数
slim reader/writer ロック
one-time initialization
まとめ

Windows Vista には、Windows® Presentation Foundation、Windows Communication Foundation、Windows Workflow Foundation など、開発者のための新しいエキサイティングなテクノロジが数多く盛り込まれています。実際、あまりに多くの .NET 対応テクノロジが導入されているために、ネイティブ C/C++ 開発者がアプリケーションの開発に利用できる新しい機能の数々が見過ごされがちです。

この記事では、ネイティブ C/C++ 開発者にが利用すべき Windows Vista のいくつかの新機能について説明します。具体的には、条件変数、slim reader/writer ロック、one-time initialization など、Windows Vista で導入された新しいスレッド同期機能を取り上げます。

条件変数

条件変数は、他のスレッド ライブラリには以前から含まれていましたが、残念ながら Windows SDK にはこれまで含まれていませんでした。条件変数は基本的に、なんらかの条件テストの結果に基づいてスレッドのグループを同期させるために使用されます。この動作は既存の同期構造の組み合わせによっても実現できますが、条件変数では、取得したロックを解放してスリープ状態に入るタスクを 1 つのアトミック操作で行うことができます。また、目的の動作をよりわかりやすく間違えにくい方法で実装できます。

Windows SDK for Windows Vista (ここからダウンロードできます) では、条件変数が CONDITION_VARIABLE 構造体によって公開されています。この構造体は、InitializeConditionVariable 関数を使用して設定できます。CONDITION_VARIABLE 構造体をクリーンアップしたり破棄したりするための関数はありません。これは、基になる実装で必要とされないためです。

SleepConditionVariableCS 関数 (クリティカル セクションが使用されている場合) や SleepConditionVariableSRW 関数 (slim reader/writer ロックが使用されている場合) を使用すると、スレッドに条件変数を待機させることができます。こうしてスリープ状態になったスレッドは、別のスレッドで WakeConditionVariable または WakeAllConditionVariable が呼び出されると解放されます。呼び出し元のスレッドが 1 つのスレッドを解放する場合は WakeConditionVariable が呼び出され、その条件変数を待機しているすべてのスレッドを解放する場合は WakeAllConditionVariable が呼び出されます。

条件変数が役に立つ典型的なシナリオとなるのが、一般的なプロデューサ/コンシューマの問題です。この古典的な例は、プロデューサがデータを生成してバッファに配置し、コンシューマがバッファからデータを取得して処理するシナリオを表します。ここで問題となるのは、プロデューサがいっぱいになったバッファにデータを追加したり、コンシューマが空のバッファからデータを取得したりしないようにする必要があるということです。これから、このシナリオを使って、条件変数がどのように役に立つのかを示します。

この例のために、数値データを共有キューに投稿する 1 つのプロデューサ スレッドと、5 つのコンシューマ スレッドを作成します。各コンシューマ スレッドは、キューからアイテムを削除して処理します。現在のデータの処理が完了するとループして、プロセスが無限に繰り返されます。

以前のバージョンの Windows でこのプロデューサ/コンシューマの問題を解決するには、Win32 イベントとクリティカル セクションの組み合わせを使用します。クリティカル セクションによって共有リソースへの同時アクセスが防止され、コンシューマがリソースを利用できるようになるとイベントがシグナル状態になります。

この問題を解決するための最初の取り組みでは、整数の標準テンプレート ライブラリ (STL) リストを共有リソースとして使用します。このリストは動的に拡張されるため、リストがいっぱいでない場合にシグナル状態になるイベントは必要ありません (共有キューに固定サイズの配列を使用する場合は、バッファの境界が上書きされないようにするために not-full イベントが必要になります)。ここで必要となるのは、リストが空でない場合に、処理するデータがあることがコンシューマにわかるようにすることだけです。したがって、CRITICAL_SECTION オブジェクトと、リストが空でない場合に通知するための自動リセット イベントを宣言して初期化します。

図 1 のプロデューサ スレッドは、最初にクリティカル セクションを取得しようとします。取得に成功すると、共有リストの最後に整数値を挿入します。その後、クリティカル セクションを解放して、not-empty イベントを設定します。ここでは自動リセット イベントを使用しているため、このイベントを待機しているスレッドが 1 つだけ解放されます。図 1 のコンシューマ スレッドは、まず、キューが空かどうかを確認します。キューが空でない場合は、アイテムを削除し、クリティカル セクションを解放します。キューが空の場合は、スリープ状態に戻り、not-empty イベントを待機します。プロデューサは、最初のコンシューマ スレッドがキューから削除したアイテムを処理している間に、別のコンシューマ スレッドをアクティブ化して次の作業が行われるようにすることができます。これにより、キューを停止させずに済みます。

Figure 1 条件変数を使用しないソリューション (Close Figure 1)

プロデューサ
unsigned _stdcall ProducerFunc(void *pParam)
{
    for (unsigned int i = 0;i < g_uiIterations;i++)
    {
        EnterCriticalSection(&g_csLock);
        // Produce work
        g_listWork.push_back(i++);
        LeaveCriticalSection(&g_csLock);
        SetEvent(g_hNotEmpty);
        Sleep(g_uiProducerDelay); // Simulating work
    }
    return 0;
}
コンシューマ
while (true)
{
    EnterCriticalSection(&g_csLock);

    if (g_listWork.empty())
    {
        LeaveCriticalSection(&g_csLock);
        WaitForSingleObject(g_hNotEmpty,INFINITE);
    }
    else
    {
        i = g_listWork.front();
        g_listWork.pop_front();

        LeaveCriticalSection(&g_csLock);

        wcout << L"Thread " << iThread << L" Consumed: " << i << endl;
        Sleep(g_uiConsumerDelay); // Simulating work
    }
}

通常はこのソリューションを使用できますが、この実装にはいくつかの制限があります。まず、同期オブジェクトを正しく初期化するにはいろいろと注意が必要です。たとえば、データをリストに挿入したときに、プロデューサがコンシューマ スレッドを 1 つだけアクティブにするか、すべてのコンシューマ スレッドをアクティブにするかを決定しなければなりません。これは、イベントを初期化する方法によって決まります。自動リセット イベントを使用した場合はスレッドが 1 つだけ解放されます。すべてのスレッドをアクティブにする場合は、手動リセット イベントを使用しますが、適切な時点で ResetEvent を呼び出す必要があります。

キューが空の場合にコンシューマ スレッドがクリティカル セクションを解放してから not-empty イベントを待機するように注意する必要があります。また、プロデューサ スレッドで not-empty イベントをシグナル状態にするときの PulseEvent の使い方にも注意が必要です。間違った使い方をすると競合状態が発生します。この問題は、コンシューマ スレッドがクリティカル セクションを解放した直後、WaitForSingleObject を呼び出す前に横取りされて、プロデューサ スレッドが PulseEvent を呼び出した場合に発生します。PulseEvent は sticky ではないため、現在イベントを待機しているスレッドのみを解放します。横取りされたスレッドが再開されてもイベントはもうシグナル状態にならないため、そのスレッドはアクティブ化されません。

条件変数を使用すると、この問題を簡単に解決できます。この場合もクリティカル セクションを使用しますが、not-empty イベントの代わりに条件変数を使用します。main 関数で、CONDITION_VARIABLE 構造体へのポインタを使用して InitializeConditionVariable を呼び出して、条件変数を初期化します。

図 2 のコンシューマ スレッドは、クリティカル セクションに入ると、キューが空かどうかを確認します。キューが空の場合は SleepConditionVariableCS が呼び出されます。この関数は、クリティカル セクションを解放してスレッドをスリープ状態にするタスクを 1 つのアトミック操作で行います。これにより、この 2 つのタスクの間にコンシューマが横取りされることによって競合状態が発生する可能性がなくなります。SleepConditionVariableCS 関数ではタイムアウトのパラメータも指定できるため、待機時間が長すぎる場合に別の処理を行うこともできます。

Figure 2 条件変数を使用するソリューション (Close Figure 2)

プロデューサ

for (unsigned int i = 0;i < g_uiIterations;i++)
{
    EnterCriticalSection(&g_csLock);

    // Produce work
    g_listWork.push_back(i);

    LeaveCriticalSection(&g_csLock);

    WakeConditionVariable(&g_condNotEmpty);

    Sleep(g_uiProducerDelay);
}
コンシューマ
while (true)
{
    EnterCriticalSection(&g_csLock);

    while (g_listWork.empty())
    {
        SleepConditionVariableCS(&g_condNotEmpty,&g_csLock,INFINITE);
    }

    i = g_listWork.front();
    g_listWork.pop_front();

    LeaveCriticalSection(&g_csLock);

    wcout << L"Thread " << iThread << L" Consumed: " << i << endl;
    Sleep(g_uiConsumerDelay); // Simulating work
}

条件変数がシグナル状態になると、SleepConditionVariableCS 関数が戻る前にスレッドがアクティブ化されて、クリティカル セクションが再び自動的にロックされます。その後、コンシューマ スレッドがループして、キューが空かどうかをもう一度確認します。このように、スレッドが解放されたら条件をもう一度テストすることが非常に重要です。これは、コンシューマが解放される直前に別のスレッドによって状態が変更されている可能性があるからです。さらに、条件変数は間違ってアクティブ化されることがあるため、条件が変更される前に実行される可能性もあります。キューが空でなかった場合は、作業アイテムをキューから削除して、クリティカル セクションを解放します。

図 2 のプロデューサ スレッドは、最初にクリティカル セクションに入ります。ロックを取得すると、新しい作業アイテムをキューに入れて、クリティカル セクションを解放します。その後、WakeConditionVariable を呼び出してコンシューマ スレッドを解放できます。この関数は、条件変数を使用しないソリューションと同じように、スレッドを 1 つだけ解放します。すべてのコンシューマ スレッドを解放する場合は、WakeAllConditionVariable 関数を呼び出します。条件変数の構文では、関数の名前に処理の内容が示されているため、スレッドがどのようにディスパッチされるかが明確になります。イベントを使用する場合は、目的の動作をイベント シグナル関数で指定するのではなくイベントの初期化時に指定するため、間違いが起こりやすくなります。

プロデューサ スレッドがクリティカル セクションを解放した直後、Wake 関数を呼び出す前に横取りされると、コンシューマ スレッドが解放される前に別のスレッドによってキューの状態が変更される可能性が生じます。先ほど、コンシューマ スレッドが解放されたら条件をもう一度チェックする必要があると言ったのはこのためです。

条件変数によってさらなる効率化がもたらされる場合もあります。SleepConditionVariableCS API と SleepConditionVariableSRW API は、可能な場合にカーネル モードへの遷移を回避しようとします。一方、条件変数を使用しない例では、少なくとも WaitForSingleObject を呼び出すときに 1 回、必ずカーネル モードへの遷移が発生します。

このように、ネイティブの条件変数の実装には独自の実装に比べて数多くのメリットがあります。柔軟性が損なわれることもありません。作成したコードはより単純で読みやすく、パフォーマンスが向上する場合もあります。これにより、見つけるのがきわめて困難な数多くの小さなバグの混入の可能性を大幅に低減できます。

slim reader/writer ロック

読み取り/書き込みロックは、複数のリーダーによる同時アクセスを許可するが、データの保護のために更新時にはライタの排他アクセスのみを許可する場合に使用されます。通常、これらのロックは、データが頻繁に読み込まれるが更新はあまり行われないシナリオに最も適しています。適切な状況で読み取り/書き込みロックを使用するとスケーラビリティの向上につながります。

読み取り/書き込みロックは以前から使用されていますが、Windows Vista がリリースされるまでは、Windows のネイティブ ユーザー モード コードで読み取り/書き込みロックが必要な場合は独自に作成するか、教科書の実装を応用するしかありませんでした。Windows Vista には、slim reader/writer (SRW) ロックと呼ばれる読み取り/書き込みロックが含まれています。ここでは、この新しい同期プリミティブの機能セットを紹介し、それを使用するための API を確認して、Win32® 標準のクリティカル セクション、セマフォ、およびイベントを使用して作成した別の実装とパフォーマンスを比較します。

SRW ロックは高速かつ (その名のとおり) 軽量に作られており、使用時にはリソース効率が維持されます。ベースになっているメカニズムは、Windows カーネルのキー付きイベントです。このメカニズムは、アプリケーションで多数の CRITICAL_SECTION オブジェクトが使用される場合に発生する可能性があるリソースの枯渇の問題を解決するために導入されました。読み取り/書き込みロックには、ライタよりリーダーが優先されるものや、逆にライタが優先されるものもありますが、SRW ロックではどちらも優先されません。したがって、アプリケーションでデータの更新をデータの読み取りより優先する必要がある場合は、書き込みを優先する別の読み取り/書き込みロックの使用を検討することもできます。しかし、独自のロックを作成する前に、SRW ロックの動作をアプリケーションのコンテキストで実際に確認してみてください。

読み取り/書き込みロックでは、そのほかに 2 つの機能がサポートされる場合もあります。1 つは、ロックを再帰的に取得する機能、もう 1 つは、既にロックを許可されたアクセスをアップグレード (またはダウングレード) する機能です。まず、再帰的な取得について見てみましょう。同期オブジェクトを再帰的に取得する必要があるロック ポリシーをアプリケーションのために作成すると、おそらくロック ポリシーを見直してその再帰的な取得を取り除くように通知する赤いフラグが表示されます。これは、ロックの取得と解放のコードを何度も実行するとオーバーヘッドが増加すること、および、おそらくはより重要な点として、ロックの解放と取得の回数が間違っていないことを確認するのは困難な場合が多いことに基づくアドバイスです。

SRW ロックでは、再帰的な取得はサポートされていません。再帰的な取得をサポートすると、正しい状態を維持するためにスレッドごとの数を追跡しなければならなくなるため、オーバーヘッドが増加します。また、共有アクセスから排他アクセスへのアップグレードや、排他アクセスから共有アクセスへのダウングレード (あまり一般的ではありません) もサポートされていません。アップグレードの機能をサポートすると、複雑になりすぎたり、余分なオーバーヘッドが発生したりして、ロックの共有取得や排他取得のコードの一般的なケースにまで影響が及ぶ可能性があるからです。また、待機中のリーダー、待機中のライタ、およびアップグレードを待機中のリーダーの 3 つをどのようにして選択するかについて、ポリシーを定義する必要も出てきます。これは、特定のロックを優先しないという基本的な設計目標に反することになります。

slim reader/writer ロックを使用するには、まず最初に SRWLOCK 構造体を宣言し、InitializeSRWLock を使用して初期化します。

VOID WINAPI InitializeSRWLock(PSRWLOCK SRWLock);

SRW ロックでは、各オブジェクトに初期化とクリーンアップの関数がある従来の Win32 のパターンとは異なり、終了時にクリーンアップ関数を呼び出す必要はありません。

初期化が完了したら、以下の基本的な API を使用できます。

VOID WINAPI AcquireSRWLockExclusive(PSRWLOCK SRWLock);

VOID WINAPI ReleaseSRWLockExclusive(PSRWLOCK SRWLock);

VOID WINAPI AcquireSRWLockShared(PSRWLOCK SRWLock);

VOID WINAPI ReleaseSRWLockShared(PSRWLOCK SRWLock);

AcquireSRWLockExclusive 関数では、その名のとおり、呼び出し元がロックを排他的に取得できます。いったん排他アクセスが許可されると、対応する ReleaseSRWLockExclusive 関数を使用してロックが解放されるまで、他のスレッドはすべて、要求するアクセスの種類に関係なくブロックされます。一方、AcquireSRWLockShared では、ロックが共有アクセスで取得されます。この場合、同じように共有アクセスを要求する他のスレッドは、ロックを所有していなかったり、既に別のスレッドによってロックが共有アクセスで取得されていたりしても、待機する必要はありません。

SleepConditionVariableSRW 関数を使用すると、slim reader/writer ロックを条件変数と共に使用できます。

BOOL WINAPI SleepConditionVariableSRW(
  PCONDITION_VARIABLE ConditionVariable,
  PSRWLOCK SRWLock,
  DWORD dwMilliseconds,
  ULONG Flags
);

SleepConditionVariableSRW は、SRW ロックを解放して指定の条件変数を待機するタスクを 1 つのアトミック操作で行います。この関数は、条件変数がシグナル状態になるか、dwMilliseconds で指定したタイムアウト期間が経過するまで戻りません。dwMilliseconds が INFINITE の場合はタイムアウトになりません。Flags パラメータで CONDITION_VARIABLE_LOCKMODE_SHARED を指定した場合は、SRW ロックが共有アクセスで保持されていると見なされます。それ以外の場合は排他アクセスと見なされます。関数が正しく戻ったときには、指定したアクセスで再度ロックが取得されています。

これらの API を試している間に、コーディング時に注意する必要がある点がいくつか見つかりました。ロックの取得や解放の API は、いずれも結果を返すように指定されていません。SRW ロックがスレッドによって所有されていない場合に ReleaseSRWLockShared や ReleaseSRWLockExclusive への呼び出しが行われると、STATUS_RESOURCE_NOT_OWNED 構造化例外がスローされます。この場合は、エラーが開発者にわかるように示されるため、問題はありません。

既に述べたように、SRW ロックでは再帰的な取得はサポートされていません。排他アクセスをもう一度再帰的に取得しようとした場合、2 回目の AcquireSRWExclusive の呼び出しは戻りません。これは、ロックが解放されるのを待機して内部でブロックされているスレッドと自己デッドロックの状態になるためです。この状況を調べて何が起こっているのかを把握するには、おそらくプロセスにデバッガをアタッチする必要があります。既に共有アクセスを取得しているスレッドから AcquireSRWShared を呼び出した場合も、AcquireSRWShared の 2 つの呼び出しの間に別のスレッドがロックを排他的に取得しようとすると、デッドロックが発生します。

取得と解放の関数を正しい組み合わせで使用することに対しても細心の注意を払う必要があります。間違って AcquireSRWLockExclusive を ReleaseSRWLockShared と組み合わせて (あるいはその逆の組み合わせで) 使用しても 、例外は発生しません。これらのエラーのシナリオで例外が発生してくれると助かりますが、エラーの検出のためにリソースとパフォーマンスの余分なオーバーヘッドが発生することになります。

この記事のサンプル コードには、さまざまな読み取り/書き込みロックの実装を実験できる ReaderWriterExample というプログラムのソースが含まれています。このプログラムでサポートされているロックの種類は、SRW ロック、独自に実装した読み取り/書き込みロックの 2 つのバリエーション、およびクリティカル セクションです。カスタムの読み取り/書き込みロックはどちらも、ロックのデータを保護するための Win32 クリティカル セクション、待機しているリーダーのためのイベント、および待機しているライタのためのセマフォを使用して作成されています。この 2 つのカスタム ロックの違いは、一方はライタを優先するのに対し、もう一方はリーダーもライタも優先しない点にあります。

ここで紹介するテストはすべて、デュアルコア Intel Xeon 3.20 GHz プロセッサを 2 基搭載したシステムで 64 ビット バージョンの Windows Vista を使用して行われています。テストのため、システムのハイパースレッドは無効にしてあります。

テスト 1 は、各スレッドの 2,000,000 回の繰り返しで構成されています。ロックが保持されている間に行われる作業は最小限に抑えられているため、ロックの取得と解放の実際のコストが結果に反映されます。その結果をまとめたのが図 3 です。

Figure 3 2,000,000 回の繰り返しのテスト (結果は秒単位) (Close Figure 3)

ロックの種類 | 1 リーダー | 1 ライタ | 2 リーダー/ 2 ライタ | 3 リーダー/1 ライタ | 4 リーダー | 4 ライタ
SRW ロック | 0.126 | 0.119 | 0.589 | 0.667 | 0.871 | 0.517
カスタム R/W ロック - 優先なし | 1.238 | 0.257 | 27.095 | 4.076 | 2.466 | 53.567
カスタム R/W ロック - ライタ優先 | 1.228 | 0.260 | 31.306 | 6.083 | 2.307 | 53.567
クリティカル セクション | 0.134 | 0.133 | 1.084 | 1.021 | 1.036 | 1.009

ここからは、興味深いことがいくつかわかります。まず、リーダー スレッドまたはライタ スレッドを 1 つだけ使用した場合 (結果の最初の 2 つの列) は、ロックの競合がないため、SRW ロックとクリティカル セクションのパフォーマンスがきわめて近くなっています。一方、すべてのスレッドがロックへの排他アクセスを取得しようとする 4 つのライタ スレッドのケースでは、SRW ロックはクリティカル セクションの半分ほどの時間しかかかっていません。ここから、SRW ロックを排他モード ロックとして使用した場合、クリティカル セクションよりパフォーマンスが高くなるため、SRW ロックの使用を検討する価値があることがわかります。"2 リーダー/2 ライタ" と "3 リーダー/1 ライタ" というラベルの 2 つの列では、SRW ロックのパフォーマンスがクリティカル セクションに比べて格段に高くなっています。この結果は、データ リーダーが並行して動作できる読み取り/書き込みロックのメリットを実証しています。

では、独自に作成した読み取り/書き込みロックについてはどうでしょうか。これらは、Windows に組み込まれている 2 つのロックに比べてかなりパフォーマンスが劣るようです。リーダーとライタの両方を含む競合の負荷が大きくなると、特にそれが顕著になります。ただし、"2 リーダー/2 ライタ" と "3 リーダー/1 ライタ" の結果は注目に値します。ここからは、ロックのポリシーが全体的な動作とパフォーマンスに影響することがわかります。ライタを優先するロックは、リーダーもライタも優先しないロックよりパフォーマンスが低くなっています。これは、ライタが更新を待機している場合に、ライタを優先するために並列実行が制限されるからです。もちろん、リーダーが最新のデータを読み取れるようにすることが目標となる場合も多いため、そのような場合には、書き込みを優先するロックが正しい選択になります。

独自に作成したロックのもう 1 つの問題は、4 つのライタがロックを取得しようとする場合に見られます。排他アクセスの競合が多い場合にこれほどまでにパフォーマンスが低下するのはなぜでしょうか。問題は、内部データを保護するクリティカル セクションへのアクセスを各スレッドが取り合うことにあります。クリティカル セクションで競合が発生すると、カーネル モードへの遷移が発生して、スレッドがイベントを待ってスリープ状態になります。いったんスレッドがクリティカル セクションに入ると、ロックの状態が検証されて、共有アクセスを保持しているリーダーや、既に排他アクセスを取得している別のライタがいないかどうかが確認されます。そのようなリーダーやライタがいた場合は、排他アクセスを取得しようとしているスレッドはクリティカル セクションから出てセマフォを待機しなければなりません。セマフォは、スレッドが排他アクセスを取得して実行を続けられるようになるとシグナル状態になります。テストの結果からわかるように、排他アクセスを取得するために複数の同期オブジェクトを頻繁に待機しなければならなくなると、パフォーマンスが大幅に低下します。

ReaderWriterExample アプリケーションには多数のコマンド ライン スイッチが含まれています。これにより、アプリケーションの動作をカスタマイズして、さまざまな種類の読み取り/書き込みロックを使用したシナリオを数多く実験することができます。ロックによって保護されるデータは単純な LONG 型です。ただし、ロックを保持している間に行う追加の作業の量を指定して、より複雑なデータ構造へのアクセスや更新をシミュレートすることもできます。この追加作業のパラメータは、リーダーとライタにそれぞれ個別に指定できるため、読み取りより更新のオーバーヘッドが大きいデータ構造をシミュレートすることもできます。そのほか、リーダー/ライタがロックにアクセスするまでの間に行う作業の量を指定するパラメータもあります。作業は、計算を実行するループによってシミュレートされます。

この最初の例からは、独自に作成したロックでは、リーダーとライタの両方の競合が数多く発生して負荷が大きくなるとパフォーマンスが低下することや、SRW ロックがすべてのケースで高いパフォーマンスを示すことがわかりました。しかし、SRW ロックを利用できるようになった今となっては独自の読み取り/書き込みロックを作成することはなくなるかというと、必ずしもそうとは言えません。アプリケーションでこれらのロックが使用される、より一般的なシナリオではどうなるかを見てみましょう。

これらのシナリオでは、リーダーによる共有アクセスの数が 1,000,000 であるのに対し、ライタによる排他アクセスの数は 150,000 になっています。これは、読み取り/書き込みロックは排他的更新に対して共有アクセスの割合が高い場合に適しているという先ほどの説明と一致するシナリオです。また、リーダーでは読み取り要求をシミュレートするために 2000 作業単位にわたってロックが保持されるのに対し、ライタでは、データの更新に伴う追加のコストをシミュレートするために 3000 作業単位にわたってロックが保持されます。さらに、リーダー スレッドでは、アクセスの間隔を短くするために、ロックにアクセスするまでの間に 100 作業単位を実行し、ライタでは、更新の間隔を長くするために、排他アクセスを試行するまでに 10,000 作業単位を実行します。これにより、全体的なロックの競合が少なくなります。

私たちはこのテストを 2 リーダー/2 ライタと 3 リーダー/1 ライタで実行しました。その結果をまとめたのが図 4 と 5 です。表の数字は、各スレッドが作業を完了するのにかかった時間 (単位は秒) です。リーダー スレッドの 2 つ目の数字は、リーダー スレッドで観測されたデータの更新の数です。

Figure 5 更新より読み取りが多い 3 リーダー/1 ライタのシナリオのテスト (結果は秒単位) (Close Figure 5)

3 リーダー/1 ライタのシナリオ | Slim R/W | クリティカル セクション | カスタム R/W ロック (優先なし) | カスタム R/W ロック (ライタ優先)
リーダー スレッド 1 | 2.345 (761 の更新) | 7.650 (21,266 の更新) | 6.313 (97,997 の更新) | 8.365 (132,290 の更新)
リーダー スレッド 2 | 2.282 (631 の更新) | 7.486 (11,466 の更新) | 6.267 (102,102 の更新) | 8.170 (140,144 の更新)
リーダー スレッド 3 | 2.324 (633 の更新) | 7.581 (20,013 の更新) | 6.321 (98,100 の更新) | 8.134 (126,568 の更新)
ライタ スレッド 1 | 7.970 | 11.990 | 8.010 | 8.446
総実行時間 | 7.970 | 11.990 | 8.010 | 8.446
Figure 4 更新より読み取りが多い 2 リーダー/2 ライタのシナリオのテスト (結果は秒単位) (Close Figure 4)

2 リーダー/2 ライタのシナリオ | Slim R/W | クリティカル セクション | カスタム R/W ロック (優先なし) | カスタム R/W ロック (ライタ優先)
リーダー スレッド 1 | 1.892 (1,185 の更新) | 5.044 (19,689 の更新) | 1.868 (7,728 の更新) | 5.664 (133,215 の更新)
リーダー スレッド 2 | 1.920 (1,177 の更新) | 3.906 (16,223 の更新) | 1.861 (7,402 の更新) | 5.559 (139,283 の更新)
ライタ スレッド 1 | 7.575 | 9.996 | 7.372 | 7.321
ライタ スレッド 2 | 7.574 | 10.250 | 7.378 | 7.317
総実行時間 | 7.575 | 10.250 | 7.378 | 7.321

これらの結果では、独自に作成したロックと SRW ロックのパフォーマンスが先ほどより拮抗しています。ポリシーの違いが結果に与える影響に注意してください。ライタを優先するロックでは、ライタがアクセスを許可されている間、リーダーは待機させられます。つまり、更新が優先されるため、リーダーがより多くの更新を読み取れるようになります。更新の優先はアプリケーションで不可欠になる場合があるため、ロックのポリシーと、その予想される負荷でのパフォーマンス特性を理解することが重要です。

Windows プラットフォームの新しい同期プリミティブとして SRW ロックが導入されたのはすばらしいことです。これにより、Windows で初めて、ネイティブ システム プログラマが組み込みの読み取り/書き込みロックを使用できるようになりました。SRW ロックはさまざまな状況で高いパフォーマンスを発揮するため、読み取り/書き込みロックを使用する際にはまず最初に試してみるべきです。アプリケーションによっては別のロック ポリシーが役に立つ場合もありますが、ここで示したように、さまざまなシナリオで高いパフォーマンスを発揮するカスタムの読み取り/書き込みロックを作成するのは簡単ではありません。

one-time initialization

複数のスレッドによって共有されるオブジェクトやリソースが正しく初期化されるようにするにはどうすればよいか。この問題は、マルチスレッド システムの開発において繰り返し発生する問題です。C および C++ では、言語の標準でマルチスレッドのサポートが謳われていないため、この問題の解決に役立つものは用意されていません。たとえば、メッセージをログに記録するために使用される Logger オブジェクトのインスタンスを 1 つ、プログラムの実行が開始されるときではなく、必要になったときに作成する必要があったとします。図 6 のシングル スレッドの初期化コードは、GetLogger 関数内で同時に実行されて Logger オブジェクトにアクセスする複数のスレッドがシステムに存在すると、正しく実行されません。

Figure 6 初期化 (Close Figure 6)

シングル スレッドの初期化
Logger* GetLogger()
{
    // C++ will execute the Logger’s constructor
    // only the first time this method is called.
    static Logger logger;

    return &logger;
}
スレッド セーフな初期化
Logger* GetLogger()
{
    static Logger* pLogger = 0;

    EnterCriticalSection(&cs);

    if (pLogger == 0)
    {
        try
        {
            pLogger = new Logger();
        }
        catch (...)
        {
            // Something went wrong.
        }
    }

    LeaveCriticalSection(&cs);

    return pLogger;
}

静的 Logger オブジェクトの初期化は、実際には複数回行われる可能性があります。これは、オブジェクトの作成時の同期がコンパイラによって考慮されていないためです。結果として、Logger オブジェクトが破損する可能性があります。その場合、運が良ければ使用時に例外が生成されますが、エラーが明らかになる保証はなく、誰も問題に気付かないままシステムが長時間実行される可能性もあります。この問題を修正するには、関数を書き直して、複数のスレッドが存在する場合でも正しく機能するように同期を導入するという方法があります。このスレッド セーフな初期化コードを図 6 に示します。

この場合、GetLogger 関数に入るスレッドは、それぞれクリティカル セクションに入ろうとします。つまり、保護されたコード ブロックに入れるのは一度に 1 つのスレッドだけになります。クリティカル セクションで実行されるスレッドは、pLogger の値をチェックして、値が NULL だった場合にのみ Logger オブジェクトのインスタンスを作成します。このインスタンスの作成が行われるのは、最初のスレッドがクリティカル セクションに入ったときだけです。その後にクリティカル セクションに入る他のスレッドはすべて、pLogger の値が NULL でないことを確認すると、それ以上何もしないままクリティカル セクションを出ます。return ステートメントに到達するまでには pLogger の値が NULL ではなくなっているため、呼び出し元に値を返すことができます。

このスレッド セーフな初期化コードは、一見、特に問題はないように見えます。しかし、このソリューションにはマイナス面があります。多数のスレッドが同時に GetLogger 関数を呼び出すことになる場合にはそれが特に深刻になります。いったん最初のスレッドによって pLogger ポインタの割り当て、作成、および設定が行われたら、その後のスレッドがクリティカル セクション オブジェクトに入る必要はありません。そのポインタは常に有効になります。この認識から生まれたのが、ダブルチェック ロッキング パターンと呼ばれる C++ プログラミングのデザイン パターンです。

図 7 は、ダブルチェック ロッキング パターンを使用した GetLogger の実装を示しています。このパターンでは、pLogger 変数が最大で 2 回テストされます。最初のチェックは、クリティカル セクションの外部で行われます。そこで pLogger が NULL だった場合は、スレッドがクリティカル セクションに入ります。クリティカル セクションに入ったスレッドは、pLogger 変数をインスタンス化して設定する前に、もう一度 pLogger が NULL かどうかをテストします。これは、別のスレッドとの競合によって、クリティカル セクションを待機しているときにブロックされていた可能性があるからです。

Figure 7 ダブルチェック ロッキング (Close Figure 7)
Logger* GetLogger()
{
    volatile static Logger* pLogger = 0;

    if (pLogger == NULL)
    {
        EnterCriticalSection(&cs);

        if (pLogger == NULL)
        {
            try
            {
                pLogger = new Logger();
            }
            catch (...)
            {
                // Something went wrong.
            }
        }

        LeaveCriticalSection(&cs);
    }

    return pLogger;
}

このダブルチェック ロッキング バージョンはベストなソリューションのように見えます。強制的に同期されるのは、オブジェクトがインスタンス化される前に GetLogger に入る少数のスレッドだけで、その後のスレッドはクリティカル セクションに入る必要もありません。これ以上何が必要でしょう。

ダブルチェック ロッキング パターンは、概念的には単純ですが、正しくコーディングするのが非常に困難であることがこれまでにわかっています。その原因は、C++ 標準でスレッド モデルが定義されていないことにあります。C++ では、実行のスレッドは 1 つだけと想定されているため、相対的な命令の順序に対する制約を開発者が表現するための方法が定義されていません。そのため、メモリの読み取りや書き込みがコンパイラによって並べ替えられてしまう可能性があります。マルチスレッド環境では、この並べ替えにより、スレッドでメモリへの書き込みが、ソース コードでそれより前にあるすべてのステートメントより先に実行される可能性があります。ダブルチェック ロッキング コードのケースでは、pLogger 変数が、Logger コンストラクタが実行される前に、Logger オブジェクトに割り当てられたメモリの場所のアドレスで更新される可能性があります。その場合、2 番目のスレッドは、値が NULL でないことを確認すると、クリティカル セクションには入らずに、まだ完全に作成されていないオブジェクトのアドレスを返します (Vance Morrison の記事「マルチスレッド アプリケーションにおける低ロック テクニックの影響について」では、マネージ コードにおける同様の問題が取り上げられています)。Visual Studio® C++ コンパイラの過去のリリースでは、変数 pLogger に対して volatile 修飾子を使用するだけではマルチスレッドのシナリオで正しい動作を確保できませんでした。ただし、Visual Studio 2005 のリリースにより、変数 pLogger がキーワード volatile で修飾されている限り、Windows プラットフォームでダブルチェック ロッキング パターンの実行の信頼性が確保されるようになりました。

Visual Studio 2005 ではダブルチェック ロッキング パターンを正しく実装できるようになりましたが、それでも、インスタンス ポインタを保持する変数に volatile 修飾子を付け忘れたり、クリティカル セクション内でのチェックを怠ったりするだけで、正しく実装されなくなってしまいます。その場合もプログラムはコンパイルされ、機能するように見えます。使用できるメカニズムが C++ に追加されるのが理想ですが、Windows Vista では、標準化団体による新たな改訂を待たずに、one-time initialization と呼ばれる機能が導入されています。one-time initialization は、Windows を使用しているハードウェア プラットフォームに関係なく動作することが保証されており、同期と非同期の両方の初期化をサポートしています。まず同期初期化について見てみましょう。

同期初期化は、概念的にはダブルチェック ロッキング パターンと同じように動作します。同時に初期化を実行しようとする最初の n 個のスレッドのうち実際にリソースをインスタンス化するのは 1 つだけで、残りのスレッドは初期化が完了するまでブロックされます。初期化が完了すると、リソースにアクセスしようとするその後のスレッドはブロックされなくなり、格納されたリソースがそのまま返されます。

まず最初に、INIT_ONCE 構造体のインスタンスを宣言して初期化する必要があります。初期化には InitOnceInitialize 関数を使用します。この作業は同期と非同期の両方の one-time initialization で必要で、この構造体を他の one-time initialization 関数で使用する前に行う必要があります。この定義を以下に示します。

VOID WINAPI InitOnceInitialize(
  PINIT_ONCE InitOnce
);

一方、同期 one-time initialization を実行するには次の関数を使用します。

BOOL WINAPI InitOnceExecuteOnce(
  PINIT_ONCE InitOnce,
  PINIT_ONCE_FN InitFn,
  PVOID Parameter,
  LPVOID* Context
);

最初のパラメータである InitOnce は、INIT_ONCE 構造体のインスタンスへのポインタです。同じ INIT_STRUCTURE のアドレスを渡すすべてのスレッドが、初期化が行われている間、互いに同期されます。2 番目のパラメータである InitFn は、開発者が作成する関数へのポインタです。この関数が実際の初期化を実行します。3 番目のパラメータである Parameter は、InitFn を呼び出すときに渡されるオプションの値です。ここに、初期化の実行に必要な情報を指定できます。最後のパラメータである Context は、void ポインタへのポインタです。関数が正しく実行された場合は、初期化されたオブジェクトのアドレスがここに格納されます。ここで、one-time initialization の結果を "オブジェクト" と呼んだことに注意してください。one-time initialization の結果は、C++ オブジェクトへのポインタである必要はありません。実行に使用されているプラットフォームのポインタのサイズに収まる値であれば何でもかまいません。InitOnceExecuteOnce は、成功した場合には TRUE を、成功しなかった場合には FALSE を返します。

次に、InitFn が指す関数を見てみましょう。この関数の定義には次のシグネチャがあります。

BOOL CALLBACK InitOnceCallback(
  PINIT_ONCE InitOnce,
  PVOID Parameter,
  PVOID* Context
);

パラメータは InitOnceExecuteOnce 関数のパラメータと同じです。InitOnceCallback 関数は、初期化されたオブジェクトのアドレスを Context に格納します。図 8 には、同期 one-time initialization を使用するように書き直した GetLogger 関数が含まれています。コールバック関数が失敗した場合に FALSE が返されることに注意してください。コールバックが FALSE を返すことにより、初期化を実行しようとしている別のスレッドがコールバック関数を実行できるようになります。初期化を実行しようとするスレッドがなくなるか、コールバックが TRUE を返すまで、これが繰り返されます。

Figure 8 同期 one-time initialization (Close Figure 8)
BOOL WINAPI InitLoggerFunction(PINIT_ONCE intOncePtr,
                               PVOID Parameter,
                               PVOID* contextPtr)
{
    try
    {
        Logger* pLogger = new Logger();
        *contextPtr = pLogger;
        return TRUE;
    }
    catch (...)
    {
        // Something went wrong.
        return FALSE;
    }
}

Logger* GetLogger()
{
    static INIT_ONCE initOnce;

    PVOID contextPtr;
    BOOL status;

    status = InitOnceExecuteOnce(&initOnce,
                                 InitLoggerFunction,
                                 NULL,
                                 &contextPtr);

    if (status)
    {
        return (Logger*)contextPtr;
    }

    return NULL;
}

非同期 one-time initialization は同期 one-time initialization よりやや複雑ですが、非同期 one-time initialization を使用すると、初期化を実行しているスレッドが、初期化の完了を待つ間ブロックされなくなります。したがって、初期化が完了するまでの間、初期化を実行しようとしているすべてのスレッドを同時に実行できるようになります。それらのスレッドは、それぞれ初期化関数でオブジェクトの専用のコピーを初期化して、そのオブジェクトを初期化された唯一のオブジェクトとして登録しようとします。このオブジェクトの登録のいわば勝者となるスレッドは 1 つだけで、残りのスレッド (敗者) は、それぞれの専用のオブジェクト インスタンスを破棄して、システムに勝者のオブジェクトを問い合わせなければなりません。

非同期 one-time initialization に使用される API は次の 2 つです。

BOOL WINAPI InitOnceBeginInitialize(
  LPINIT_ONCE InitOnce,
  DWORD dwFlags,
  PBOOL fPending,
  LPVOID* Context
);

BOOL WINAPI InitOnceComplete(
  LPINIT_ONCE lpInitOnce,
  DWORD dwFlags,
  LPVOID lpContext
);

図 9 は、上で使用したのと同じ GetLogger 関数を、非同期 one-time initialization を使用するように書き直したものです。このルーチンをステップごとに見ていきましょう。スレッドは、まず最初に InitOnceBeginInitialize を呼び出します。その際には、使用する INIT_ONCE 構造体へのポインタを指定する必要があります。また、dwFlags パラメータを INIT_ONCE_ASYNC に設定して、非同期 one-time initialization を開始しようとしていることを示す必要もあります。初期化の状態は、残りの 2 つのパラメータ (fPending と Context) を使用して呼び出し元に伝えられます。これらのパラメータは、関数が更新できるようにポインタとして指定されます。InitOnceBeginInitialize は、成功した場合は TRUE を返します。それ以外の場合は FALSE を返して、なんらかの問題で初期化を続行できないことを示します。この場合は、fPending パラメータを調べて、既に別のスレッドによって初期化が完了されていないかどうかを確認する必要があります。fPending が FALSE の場合、初期化は既に完了しており、初期化されたオブジェクトが Context パラメータに格納されています。この場合、GetLogger が行うべき残りの作業は、Context を Logger* に変換して呼び出し元に返すことだけです。

Figure 9 非同期 one-time initialization (Close Figure 9)
Logger* GetLogger()
{
    static INIT_ONCE initOnce;

    PVOID contextPtr;
    BOOL    fStatus;
    BOOL    fPending;

    fStatus = InitOnceBeginInitialize(&initOnce,INIT_ONCE_ASYNC,
        &fPending, &contextPtr);

    // Failed?
    if (!fStatus)
    {
        return NULL;
    }

    // Initialization already completed?
    if (!fPending)
    {
        // Pointer to the logger is contained context pointer.
        return (Logger*)contextPtr;
    }

    // Reaching this point means that the logger needs to be created.

    try
    {
        Logger* pLogger = new Logger();

        fStatus = InitOnceComplete(
            &initOnce,INIT_ONCE_ASYNC,(PVOID)pLogger);

        if (fStatus)
        {
            // The Logger that was created was successfully
            // set as the logger instance to be used.
            return pLogger;
        }

        // Reaching this point means fStatus is FALSE and the object this
        // thread created is not the sole instance.
        // Clean up the object this thread created.
        delete pLogger;
    }
    catch (...)
    {
        // Instantiating the logger failed.
        // Fall through and see if any of
        // the other threads were successful.
    }

    // Call again but this time ask only for
    // the result of one-time initialization.
    fStatus = InitOnceBeginInitialize(&initOnce,INIT_ONCE_CHECK_ONLY,
        &fPending,contextPtr);

    // Function succeeded and initialization is complete.
    if (fStatus)
    {
        // The pointer to the logger is in the contextPtr.
        return (Logger*) contextPtr;
    }

    // InitOnceBeginInitialize failed, return NULL.
    return NULL;
}

fPending の値が TRUE だった場合は、もっと面白いことになります。この場合は、初期化を続行する必要があることになります。また、呼び出し元のスレッドと同時に初期化コードを実行している他のスレッドがある可能性があることもわかります。図 9 では、結果として新しい Logger オブジェクトが作成され、そのインスタンスを、非同期 one-time initialization の結果となる唯一のインスタンスとして設定しようとしています。このことは、new ステートメントの後に行われている InitOnceComplete の呼び出しからわかります。最初のパラメータには、前に使用したのと同じ INIT_ONCE 構造体へのポインタが指定されています。2 番目のパラメータとして INIT_ONCE_ASYNC フラグが渡され、3 番目のパラメータには Logger インスタンスへのポインタが指定されています。InitOnceComplete が TRUE を返した場合は、GetLogger のすべての呼び出し元に対してこのインスタンスが返されるようになります。

InitOnceComplete が FALSE を返した場合は、別のスレッドによって作成された Logger が格納されています。この場合、呼び出し元は、作成したインスタンスを破棄して、使用しているリソースを解放する必要があります。その後、もう一度 InitOnceBeginInitialize が呼び出されます。ただし今度は、INIT_ONCE_ASYNC フラグの代わりに INIT_ONCE_CHECK_ONLY フラグが使用されます。INIT_ONCE_CHECK_ONLY フラグは、初期化が完了しているかどうかを確認するために使用されます。初期化が完了していた場合は、格納された初期値が、指定した Context ポインタ パラメータにコピーされます。有効な初期値が返された場合、関数は TRUE を返し、fPending パラメータが FALSE に設定されます。

では、同期と非同期の one-time initialization をどのように使い分ければよいのでしょうか。ユニプロセッサ システムでは、一度に実行できるスレッドは 1 つだけなので、当然、同期 one-time initialization を使用することになります。マルチプロセッサ システムやマルチコア システムでは、複数のオブジェクト インスタンスが作成される場合に発生する可能性があるコストを定量化する必要があります。コストには、時間、メモリ (または不足する可能性があるその他のリソース)、オブジェクトの初期化が成功する前に初期化コードを同時に実行する典型的なスレッドの数などが含まれます。オブジェクトの作成にかかる時間を無視できない場合や、オブジェクトが大量のメモリ (オブジェクトごとの使用量、または同時に作成される可能性があるすべてのオブジェクトの使用量の合計) を使用する場合は、同期初期化を選択することをお勧めします。また、オブジェクトの作成のプロセスによって、初期化を実行しているスレッドがブロックされる可能性がある場合は、スレッドを同時実行できる機会が減り、非同期初期化を行うメリットがほとんどなくなると考えられるため、同期初期化をお勧めします。

この記事のソース コードには、OneTimeInit.exe というサンプルが含まれています。このサンプルは、共有されたイベント ハンドルの同期および非同期の one-time initialization のデモになっています。このプログラムは、実行するスレッドの数と、同期または非同期のどちらの初期化を実行するかを示すフラグを受け取ります。指定した数のスレッドが作成され、各スレッドが、スレッドの終了のシグナルとして使用される Win32 イベント オブジェクトへのハンドルを取得しようとします。各スレッドの進行状況は stdout に送られるため、初期化の進行状況を正確に確認できます。各スレッドでは、メイン スレッドが終了イベントをシグナル状態にするまでなんらかの作業が行われます。プログラムの使用法は、パラメータを指定せずにプログラムを実行するだけで確認できます。

one-time initialization については、言い残したことが 2 点あります。同期初期化では成功するまでコールバック関数の呼び出しが続けられると説明しましたが、アプリケーションのシナリオで、初期化を試行するのは 1 回だけで、失敗した場合にはそれ以上行われないようにする必要がある場合にはどうすればよいでしょうか。まず、コールバック関数でそのまま TRUE を返し、lpContext に特別な値を設定して、他のスレッドがその値で初期化が失敗したかどうかを確認できるようにする方法があります。また、InitOnceExecuteOnce API の代わりに InitiOnceBeginInitialize/InitOnceComplete API を使用して同期初期化を行う方法もあります。そのためには、dwFlags パラメータから INIT_ONCE_ASYNC フラグを削除し、初期化コードを、独立したコールバック関数ではなく、InitOnceBeginInitialize API と InitOnceComplete API の呼び出しの間に配置します。失敗した場合は、InitOnceComplete API で INIT_ONCE_INIT_FAILED フラグを使用して、それ以上初期化を試行しないことを示します。lpContext を使用してオプションの値を設定して、初期化が失敗したかどうかを他のスレッドが確認できるようにすることもできます。

もう 1 つ言い残したことは、one-time initialization では、作成したオブジェクトのクリーンアップが行われないということです。この問題に対処する方法はいくつかありますが、すべての状況に当てはまる 1 つのソリューションはありません。プロセスの終了後にオペレーティング システムによってクリーンアップされるオブジェクトの場合は、リークさせることを検討します。ただし、プログラムのリソース リークを監視するツールを使用する場合は問題として示される可能性があるので注意してください。オブジェクトのクリーンアップ関数が呼び出されないと正しい動作を確保できない場合は、オブジェクトを使用しているスレッドがすべて終了した後にその関数が呼び出されるようにする以外にほとんど選択肢はありません。ただし、サンプルで使用されているのと同じような手法を使用できる場合もあります。サンプルでは、初期化された HANDLE を静的オブジェクトを使用して登録して、すべてのスレッドが終了した後、プログラムの実行の最後に自動的に破棄されるようにしています。

まとめ

この記事では、Windows Vista でネイティブ C/C++ 開発者のために導入されたスレッド同期プリミティブの数々の重要な強化について説明しました。これらの強化により、スレッドの同期の問題に従来より簡単に対処できるようになりました。ただし、この記事では表面をなぞったにすぎません。ここで紹介したものやその他の変更について、さらに詳しく調べてみることをお勧めします。まずは、スレッド プール API の強化について調べてみてください。残念ながらここで説明することはできませんが、これは、詳細に検討する価値がある重要な追加機能です。

私たちの質問に答え、洞察に満ちたフィードバックを与えてくれた Neill Clift と Arun Kishan に感謝します。また、記事の内容をレビューして貴重なフィードバックを提供してくれた Jerry Albro にも感謝します。

Robert Sacconeは、Forefront Server Security グループのアーキテクトです。大規模ソフトウェアの設計、分散システム、オペレーティング システムの実装などの分野に関心を持っています。

Alexander Taskovは、Microsoft の Forefront Server Security チームのシニア ソフトウェア開発エンジニアです。Forefront Server Security 製品のセキュリティ機能の設計および開発を担当しています。


編集者のメモ: 10 周年を迎えた Visual Studio

編集者のメモ
10 周年を迎えた Visual Studio
Joshua Trupin

1997 年にはさまざまなことがありました。 最初の Web ブームが訪れたのはこの年です。だれもがたまごっち (一時期人気を博した仮想ペット) を持ち歩き、スパイス ガールズがラジオを席巻し、オースティン パワーズがまだ新鮮だったこの年に、マイクロソフトは Visual Studio をリリースしました。

強力な開発ツールを 1 つにまとめ、プログラマが生産性を維持することを容易にした Visual Studio は、今月で誕生から 10 周年を迎えます。

Microsoft Systems Journal (MSDN Magazine の前身) の 1997 年 5 月号で、私たちは Visual Studio を初めて紹介しました。

Microsoft Visual Studio 97 は、開発プロジェクトの完遂を支援する充実したツールボックスを提供するために大きな役割を果たします。Visual Studio は、動的な Web ベース アプリケーションまたはその他の分散アプリケーションを作成する開発者およびチームを対象としています。Visual Studio は、マイクロソフトのすべての開発ツールを包含し、Web コンテンツを作成するための新しいツール群を加え、それらすべてを包括的なドキュメントによって結び付けています。分散アプリケーションを作成していない場合でも、マイクロソフトの開発者向け製品を 2 つ以上使用していれば、Visual Studio はきっと役に立つはずです。

1997 年以来、Visual Studio はさまざまな方向に進化してきました。私たちは、Visual Studio が .NET ベース開発のための最適なツールとなるのを目の当たりにし、それを文章で取り上げてきました。最近では、Visual Studio Team System が、そのアプリケーションのライフサイクル管理のための包括的なソリューションにより、市場で確固たる地位を確立するのを目撃しました。

ここ数年で私たちが目にした Visual Studio の大きな進歩とは何でしょうか。信じられないかもしれませんが、1 つは完全なオーバーホールが行われたことであり、もう 1つはグラニュラリティに関連することです。

2000 年に初期プレリリースとして初めて登場した Visual Studio .NET は、Visual Basic ユーザーにとっても C++ ユーザーにとってもアクセスしやすいようにパッケージ化された、まったく新しいパラダイム (共通言語ランタイム (CLR) によるクロス言語プログラミング) でした。さらに、新しい言語 (C#) が開発者、特に MSDN Magazine の執筆者や寄稿者の間でまたたく間に人気を集めました。でもこれは私たちが仕組んだことではありません。長年執筆しているライターは、C# がすばらしい言語であることにすぐに気付きました。その大きな理由は、Visual Studio .NET によって C# へのアクセスがきわめて容易になっていたからです。

数年後、Visual Studio 2005 がリリースされました。今では、無償の Express エディションを使用して、異なる言語を試すことができるようになりました。軽量ながらも多機能な IDE を無料でダウンロードしてコーディングを始められるというのは、すばらしく魅力的です。

Visual Studio 環境の Express バージョンが登場したことは、本誌の内容が読者にとってより身近なものになったことも意味します。SQL Server をインストールしていないからという理由で、データ関連の記事を飛ばし読みする時代は終わりました。今や Express バージョンをダウンロード (35 ～ 70 MB) で入手できるのです。

MSDN Magazine では、すべての人は人生のちょっとした問題を最善の方法で、つまりプログラムを用いることによって解決すべきであると考えています。IDE のまっさらなプログラミング空間に勝るものはありません。今や Visual Studio は、この体験をインターネットに接続できるすべての人に提供します。仮想ペットが生き返るわけではありませんが、毎日 1 時間ほどコーディングを行えば、以後 10 年以上にわたって頭を研ぎ澄ましておくことができます。

注 : 印刷のミスにより、5 月号の「CLR 徹底解剖」の最後のページが抜け落ちていました。読者を大切にするという観点から、この記事の完全版を 24 ページ以降に再掲載しています。 £J.T.

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Luca Bolognese、Neill Clift、Nikola Dudar、Joe Duffy、Eric Faller、Matt Gibbs、Mike Harsh、Anders Hejlsberg、Luke Hoban、Arun Kishan、Karen Liu、Marian Luparu、Bogdan Ionut Mihalcea、George Mileka、Raj Pai、Doug Purdy、Mark Rideout、Rachel Falzone Schaw、Pete Sheill、Amanda Silver、Weitao Su、Deepu Thomas、Scott Wisniewski、および Michiel Wories です。

Joshua Trupin


ツールボックス: XML コメント、オンライン支払、その他

ツールボックス
XML コメント、オンライン支払、その他
Scott Mitchell

コンテンツ
Visual Studio プロジェクトに XML コメントをすばやく追加する
クレジット カードのオンライン支払を受け付ける
Visual Studio のプログラミング エクスペリエンスを強化する
データドリブン アプリケーション開発のガイド

Visual Studio プロジェクトに XML コメントをすばやく追加する

Microsoft® .NET Framework では、クラス、メソッド、プロパティ、およびイベントのドキュメント化のタスクが XML コメントによって単純化されています。XML コメントは、ソース コード コメント内に埋め込まれる特殊な XML タグで、クラスやそのメンバに関するメタデータを提供します。このメタデータは、コンパイル時に Visual Studio® によって独立した XML ファイルに変換されます。その後、その XML ファイルを、Microsoft Sandcastle project のようなツールを使用してヘルプ ファイルに変換できます。

XML コメントを自動的に生成する GhostDoc
XML コメントを自動的に生成する GhostDoc (画像を縮小するには、ここをクリックします)
XML コメントを自動的に生成する GhostDoc
XML コメントを自動的に生成する GhostDoc (画像を拡大するには、ここをクリックします)

このように、XML コメントを使用すると、ソース コード コメントをリッチなヘルプ ドキュメントに自動的に変換できますが、コメント テキストを書かなければならないことに変わりはありません。これは、多くの開発者が負担に感じている作業です。これらのコメントの作成を自動化によって迅速化できないものでしょうか。まさにそれを実現しようとしているのが、GhostDoc 1.9.5 です。

GhostDoc は、Roland Weigelt によって作成された無償の Visual Studio アドインで、XML コメントの作成を支援します。GhostDoc をインストールすると、ポイント アンド クリックの簡単な操作で XML コメントを自動生成できるようになります。たとえばメソッドに XML コメントを追加するには、メソッド内を右クリックして、コンテキスト メニューの [Document this] (ドキュメント化) をクリックするだけです。これで、メソッドの型、パラメータ、名前、およびその他のコンテキスト情報に基づいて、そのメソッドの XML コメント テキストが自動的に生成されます。

.NET Framework の型を使用するプロパティやメソッドのドキュメントを生成する場合は、既に Microsoft によって記述されているドキュメントが使用されます。GhostDoc は、Pascal 形式か camel 形式が使用されていると想定して名前を単語に分割し、個々の単語を分析してドキュメントを生成します。ドキュメントのロジックはすべて GhostDoc の生成ルールによって処理されます。組み込みのルールをカスタマイズすることも、新しいルールを追加することもできます。ルールをエクスポートして他のコンピュータで使用することもできます。

ごく単純なメンバを除いては、GhostDoc で XML コメントを完全に作成できると考えることはできません。GhostDoc によって自動生成されるコメントはあくまで候補であり、開発者による見直しが必要です。それでも、ドキュメント作成の第 1 歩として便利であることに変わりはなく、GhostDoc を使用することで時間を大幅に節約できます。

価格 : 無料

www.roland-weigelt.de/ghostdoc

クレジット カードのオンライン支払を受け付ける

筆者は、開発者としてのキャリアの中で、作成したアプリケーションやコンポーネントを他の開発者に販売しようと考えたことが何度もありましたが、その度に、オンライン支払をどのように処理するかという問題に悩まされてきました。

今日では、ほとんどすべてのオンライン取引の支払いがクレジット カードやデビット カードで行われています。この種の支払いを受け付けるには、必要なものが 2 つあります。1 つは金融機関 (最寄りの銀行や PayPal のようなオンライン サービスなど) のマーチャント アカウント、もう 1 つは支払ゲートウェイです。マーチャント アカウントを取得すると、別の銀行口座やクレジット カードから支払いを回収できるようになります。一方、支払ゲートウェイは、支払いを電子的に処理するシステムです。

支払ゲートウェイのサービスは多くの企業によって提供されています。筆者が見つけた中で最も簡単に利用できるものの 1 つが Authorize.NET です。Authorize.NET で口座を作成すると、Authorize.NET の Web サイトか自分の Web サイトで顧客の支払情報を収集できるようになります。1 つ目の方法では、注文の最後のステップで顧客が Authorize .NET の Web サイトにリダイレクトされます。そこで顧客が支払情報を入力すると、所定の金額が請求されます。その後、顧客は再び元の Web サイトにリダイレクトされます。

2 つ目の方法では、自分の Web サイトで顧客が直接支払情報を入力します。入力された支払情報は、セキュリティで保護された HTTP 要求を使用して、XML ペイロードとしてバックグラウンドで Authorize.NET のサーバーに送信されます。この XML データのやり取りは、ASP.NET ページの数行のコードでプログラムによって処理できます。Authorize.NET をサポートしているさまざまなサードパーティのショッピング カート製品を利用することもできます。

顧客の支払情報の収集方法に関係なく、Authorize.NET は、ユーザーのマーチャント アカウントと顧客のクレジット カード口座 (またはデビット カード口座) を仲介して、顧客に代金を請求し、その代金をマーチャント アカウントに入金します。驚くことに、通常はプロセス全体にわずか数秒しかかかりません。

Authorize.NET では、クレジット カード詐欺防止のために住所とカード コードの両方の確認を実施しています。定期循環払いを扱うように構成することもできます。

Authorize.NET が提供するのは支払ゲートウェイだけであることに注意してください。その他にマーチャント アカウントが必要で、その開設料、月々の使用料、および取引手数料がかかります。たいていは、開設料が 100 ドルくらい、月々の使用料が 30 ドルくらい、取引手数料が 2.0 ～ 3.0% + 0.05 ～ 0.50 ドルくらいです。

価格 : 149.00 ドル (開設料)、20.00 ドル (月々の使用料)、0.10 ドル (取引手数料)

www.authorize.net

Visual Studio のプログラミング エクスペリエンスを強化する

Visual Studio では、新しいバージョンが出るたびに、開発者の生産性や作業効果を高めるウィザード、機能、およびツールが追加されています。しかし、活発な Visual Studio アドイン市場からも明らかなように、各バージョンに組み込まれるこうした機能強化のほかにも生産性向上のために利用できるツールはたくさんあります。こうしたアドインにより、コードの記述、リファクタリング、およびテストの支援のためのさらに多くの機能が提供されています。

そうした Visual Studio アドインの一例となるのが JetBrains の ReSharper 2.5 です。このアドインには、quick-fix オプションを含むインテリジェントなエラー検出機能、リファクタリングの拡張サポート、カスタマイズ可能なコード書式設定、単体テスト、NAnt および MSBuild のスクリプト編集、Visual Studio プロジェクトの型やファイルをすばやく検索して移動するためのさまざまなツールなどの機能が用意されています。筆者が最も便利に感じた機能は、quick-fix と、移動と検索のオプションです。quick-fix は、警告やエラーが検出されたコード行の横に電球のアイコンとして表示されます。たとえば、型キャストを実行するのを忘れると、適切なキャストを自動的に挿入できる quick-fix オプションが電球のアイコンによって通知されます。

電球のアイコンで quick-fix を通知する ReSharper
電球のアイコンで quick-fix を通知する ReSharper (画像を縮小するには、ここをクリックします)
電球のアイコンで quick-fix を通知する ReSharper
電球のアイコンで quick-fix を通知する ReSharper (画像を拡大するには、ここをクリックします)

ReSharper の移動と検索の機能は、多数の型やファイルを含む大規模なプロジェクトにとって理想的な機能です。[Navigate from Here] 機能を使用すると、変数や型の宣言や基本クラスにすばやくジャンプできます。さらに、すべての継承メンバの一覧や、変数や型がプロジェクト内で使用されているすべての場所の一覧を表示することもできます。[Find Results] (検索結果) ウィンドウでは、プロジェクトで特定の型を簡単に検索できます。[File Structure] (ファイル構造) ウィンドウには、現在のファイルの型とメンバが表示されます。そこから、ファイル内の型やメンバを並べ替えたり、ファイル内の特定のメンバにすばやくジャンプしたりできます。

Visual Studio .NET 2003 バージョンの ReSharper は C# コードでしか使用できないので注意してください。Visual Studio 2005 バージョンには Visual Basic® の機能も含まれています (C# 開発者と同じ機能をすべて利用できるわけではありません)。その他、ASP.NET Web ページを編集するための機能もいくつか含まれています。

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

www.jetbrains.com/resharper

データドリブン アプリケーション開発のガイド

筆者が 1990 年代後半にデータドリブン Visual Basic アプリケーションのプログラミングを始めたときに同僚が勧めてくれたのが、William R. Vaughn の『Hitchhiker’s Guide to Visual Basic and SQL Server』でした。これは、"ドア ストッパー" と呼ばれるほどの重さと厚さを持つ大著です。このガイドは人気を博し、90 年代の間に 6 版を重ねました。その後 9 年の中断を経て、第 7 版が Peter Blackburn との共著として出版されました。タイトルも、『Hitchhiker’s Guide to Visual Studio and SQL Server』 (Addison-Wesley, 2007 年) と改められました。

第 7 版では、これまでの版と同様に、SQL Server™ 2005 のインストールから、T-SQL の基本や、.NET アプリケーションを作成してデータを操作する方法まで、重要なトピックが詳しく解説されています。まず始めにアプリケーション アーキテクチャについて説明し、さまざまなデータ アクセス テクノロジを紹介して、SQL Server 2005 の各バージョンを比較対照しています。次に、SQL Server 2005 のインストール手順を説明し、セキュリティと管理の問題について検討しています。その後には、SQL ステートメント (SELECT、INSERT、UPDATE、および DELETE)、ビュー、UDF、TRIGGER、ストアド プロシージャ、データベースの設計、正規化、データとリレーションシップの整合性など、SQL の基本についての説明が続きます。

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

『Hitchhiker’s Guide to Visual Studio and SQL Server』では、SQL Server 固有のトピックと、Visual Studio を使用したデータ操作のトピックが均等に配分されています。Visual Studio 関連のトピックには、IDE からのデータベース アクセス、プログラムによるデータ操作 (ADO.NET を使用)、型指定されたデータセットの作成、データベース エンティティをアプリケーション アーキテクチャ内のオブジェクトとしてモデリングする手法などがあります。

著者である Vaughn の豊富な知識と読みやすい文体により、この本は、データドリブン アプリケーションを作成する .NET 開発者にとって必携の書となっています。著者のこれまでの本と同様に、コード例はすべて Visual Basic で書かれていますが、それを C# 開発者が気にする必要はありません。この本の真価は、コード サンプルの再利用ではなく、コンセプトとアーキテクチャ上のアドバイスにあります。

価格 : $59.99.

www.aw-bc.com

ご意見やご質問は、

Scott toolsmm@microsoft.com.

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


CLR 徹底解剖: リフレクションについての考察

CLR 徹底解剖
リフレクションについての考察
Mike Repass

コードのダウンロード : : CLRInsideOut2007_06.exe (163KB)
コンテンツ
.NET のリフレクション
型およびメンバを検査する
コードを動的に呼び出す
検査と呼び出しのまとめ
シリアル化の効率的な型処理
仕上げ

ライブラリ間で共有する必要のある型情報が多すぎることで、簡潔なコンポーネント化が妨げられていませんか。厳密に型指定されたデータ ストレージの効率性が必要なものの、オブジェクト モデルが進化するたびにデータベース スキーマを更新するのはコストがかかるため、実行時に型スキーマを推測しようと考えていませんか。任意のユーザー オブジェクトを受け取り、型インテリジェントな方法でそのオブジェクトを処理するコンポーネントを出荷する必要はありませんか。プログラムによって、ライブラリの呼び出し元でその型を示すことができればと思いませんか。

厳密に型指定されたデータ構造を維持しながらもランタイムの柔軟性を最大化しようと苦心している場合は、リフレクションについて、またそれがどの程度ソフトウェアを改善するかについて検討することをお勧めします。このコラムでは、Microsoft® .NET Framework の System.Reflection 名前空間とそれが開発作業にどのようなメリットをもたらすかについて説明します。簡単な例について順に説明して、最後にシリアル化の実際のシナリオに取り組みます。そこでは、リフレクションと CodeDom がどのように連携してランタイム データを効率的に処理できるかを示します。

System.Reflection について説明する前に、少し戻って、一般的なプログラムによるリフレクションについて考えてみましょう。まず、リフレクションは、前もって ID や正規構造を把握していなくてもプログラマがコード エンティティを検査して操作できるようにする、プログラミング システムによって提供される任意の機能と定義できます。わかりにくいので、1 つ 1 つ分析していきます。

まず、リフレクションによって何ができるのでしょうか。何のために使用するのでしょうか。一般的なリフレクション中心のタスクは、次の 2 つのカテゴリに分類するのが好ましいです。1 つは検査で、もう 1 つは操作です。検査では、オブジェクトと型を分析して、その定義と動作についての構造化された情報を収集する必要があります。通常、基本的な条件以外は、ほとんどまたはまったくの予備知識なしにこれを行います (たとえば、.NET Framework では、System.Object からすべて継承され、オブジェクト型指定の参照がリフレクションの通常の開始点です)。

操作では、検査を通して取得した情報を使用して、動的にコードを呼び出したり、見つけた型の新しいインスタンスを作成したり、実行時に型とオブジェクトを再構築します。ここで重要なのは、ほとんどのシステムでは、実行時に型とオブジェクトを操作すると、同等の処理をソース コードで静的に実行する場合と比較してパフォーマンスが低下する点です。これは、リフレクションの動的な性質との必要なトレードオフですが、リフレクションのパフォーマンスの最適化に使用できる手法やベスト プラクティスはいくつもあります (リフレクションの使用の最適化の詳細については、msdn.microsoft.com/msdnmag/issues/05/07/Reflection を参照してください)。

では、リフレクションのターゲットは何でしょうか。プログラマは、実際に何を検査したり、操作するのでしょうか。リフレクションの定義で "コード エンティティ" という新しい用語を使用しましたが、これは、プログラマの視点として、リフレクション テクノロジはオブジェクトと型の従来の区別をあいまいにすることがあるという事実を強調するためです。たとえば、一般的なリフレクション中心のタスクは次のようになります。

  1. まずオブジェクト O へのハンドルを開始し、リフレクションを使用して関連付けられている定義である型 T へのハンドルを取得します。
  2. 型 T を検査して、そのメソッド M へのハンドルを取得します。
  3. 別のオブジェクト O’ でメソッド M を呼び出します (型 T の)。

インスタンスからその基になる型へ、その型からメソッドへと進み、次にそのメソッドへのハンドルを使用して別のインスタンスでそのメソッドを呼び出すことに注意してください。明らかに、これはソース コードでの従来の C# プログラミング技法ではできません。このシナリオについては、.NET Framework の System.Reflection を説明した後で、具体例を示して再び説明します。

プログラミング言語によっては、その構文でリフレクションをネイティブに提供しますが、他のプラットフォームやフレームワーク (.NET Framework など) では、システム ライブラリとしてリフレクションを使用できます。公開方法に関係なく、特定のシナリオでリフレクション テクノロジを使用する可能性は、少し複雑です。プログラミング システムのリフレクションを提供する機能は、次のような多くの要素によって決まります。プログラマは、プログラミング言語の機能を使用して概念を適切に表現したか。コンパイラは、後で解釈できるように、十分な構造情報 (メタデータ) を出力に埋め込むか。そのメタデータの利用に使用できるランタイム サブシステムまたはホスト インタープリタはあるか。プラットフォーム ライブラリは、プログラマにとって有用な方法でこの解釈の結果を公開するか、などです。

複雑なオブジェクト指向型システムを想像では維持しつつも、正規のデータ構造のない簡単な C スタイル関数でコードを表現する場合、ある変数 v1 のポインタがある型 T のオブジェクト インスタンスを指すことをプログラムで動的に推測することは明らかに不可能です。なぜなら、結局、型 T は、明示的なプログラミング ステートメントでは捕らえることのできない想像の概念だからです。ただし、より柔軟なオブジェクト指向言語 (C# など) を使用してプログラムの抽象的な構造を表現し、型 T の概念を直接導入すれば、コンパイラは、共通言語ランタイム (CLR) または動的言語インタープリタで提供されるように、後で適切なロジックで理解できる形式に構想を変換します。

リフレクションは、動的なランタイム テクノロジに限定されるのでしょうか。簡潔に言うと、違います。開発と実行のサイクルを通して、開発者にとってリフレクションが利用可能で役に立つときは何度もあります。プログラミング言語には、高レベル コードをコンピュータが使用できる命令に直接変換するスタンドアロン コンパイラで実装されるものもあります。その出力ファイルは変換された入力だけで構成され、不透明なオブジェクトを取得してその定義を動的に分析するのに、実行時に使用できるサポート ロジックはありません。これは、従来の多くの C コンパイラのシナリオそのものです。ターゲットの実行可能ファイルで使用できるサポート ロジックはほとんどないので、動的なリフレクションはできませんが、コンパイラは静的なリフレクションをよく提供します。たとえば、一般的に使用される typeof 演算子で、プログラマはコンパイル時に型 ID を確認できます。

この対極には、常にホスト プロセスで実行されるインタープリタ プログラミング言語があります (スクリプト言語は、多くの場合このカテゴリに分類されます)。プログラムの完全な定義が使用可能で (入力ソース コードとして)、完全な言語実装に統合されるので (インタープリタ自体として)、必要なテクノロジは自己分析のサポートだけです。そのような動的言語は、網羅的なリフレクション機能をよく提供するので、プログラムを動的に分析して操作するための豊富なツールが使用できます。

.NET Framework CLR と C# などのホスト言語は、この中間に分類されます。コンパイラは、ソース コードを IL (中間言語) とメタデータに変換するのに使用されます。IL とメタデータは、元のソースより低レベルで "論理的" ではないものの、抽象的な構造情報と型情報を多く含んでいます。CLR によりプログラムが起動されてホストされると、基本クラス ライブラリ (BCL) の System.Reflection ライブラリはこの情報を使用して、オブジェクトの型、型のメンバ、メンバのシグネチャなどについての情報を返すことができます。さらに、遅延バインディング呼び出しなどの呼び出しもサポートできます。

.NET のリフレクション

.NET Framework でのプログラミングの際にリフレクションを利用するには、System.Reflection 名前空間を使用します。この名前空間は、アセンブリ、モジュール、型、メソッド、コンストラクタ、フィールド、プロパティなどランタイムのさまざまな概念をカプセル化するクラスを提供します。図 1 の表は、System.Reflection のクラスが、その概念のランタイム クラスにどのように対応しているかを示しています。

Figure 1 System.Reflection クラス (Close Figure 1)

言語コンポーネント | 対応する .NET クラス
アセンブリ | System.Reflection.Assembly
モジュール | System.Reflection.Module
抽象メンバ | System.Reflection.MemberInfo (下にあるすべての基本クラス)
型 | System.Type
プロパティ | System.Reflection.PropertyInfo
フィールド | System.Reflection.FieldInfo
イベント | System.Reflection.EventInfo
抽象メソッド | System.Reflection.MethodBase (下にあるすべての基本クラス)
メソッド | System.Reflection.MethodInfo
コンストラクタ | System.Reflection.ConstructorInfo

とても重要なことですが、System.Reflection.Assembly と System.Reflection.Module は、主に新しいコードを検索してランタイムに読み込むのに役立ちます。このコラムでは、この 2 つは置いておき、すべての関連コードは既に読み込まれているものとします。

読み込まれたコードの検査と操作では、System.Type に重点を置くのが一般的なパターンです。通常、目的のランタイム型の System.Type インスタンスを取得することから始めます (Object.GetType を使用)。次に、System.Type のさまざまなメソッドを使用して、型の定義を調べて System.Reflection 内の別のクラスのインスタンスを取得します。たとえば、特定の Method を目的とする場合、その Method の System.Reflection.MethodInfo のインスタンスを取得します (場合により Type.GetMethod を使用)。同様に、特定のフィールドを目的とする場合は、その Field の System.Reflection.FieldInfo のインスタンスを取得します (場合により Type.GetField を使用)。

必要なリフレクション インスタンス オブジェクトをすべて取得したら、必要に応じて、検査または操作の方針に従って進めます。検査では、リフレクション クラスのさまざまな説明的なプロパティを使用して、必要な情報を取得します (ジェネリック型かどうか、インスタンス メソッドかどうか)。操作では、メソッドの実行を動的に呼び出したり、コンストラクタを呼び出して新しいオブジェクトを作成したりできます。

型およびメンバを検査する

コードを使用して、検査のための基本的なリフレクションの使用を説明しましょう。ここでは型分析に重点を置きます。オブジェクトから始めて、その型を取得して目的のメンバをいくつか調べます (図 2 を参照)。

Figure 2 オブジェクトの型とメンバの取得 (Close Figure 2)

コード

using System;
using System.Reflection;

// use Reflection to enumerate some basic properties of a type...

namespace Example1
{
    class MyClass
    {
        private int MyField = 0;
        public void MyMethod1() { return; }
        public int MyMethod2(int i) { return i; }
        public int MyProperty { get { return MyField; } }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Reflection Demo Example 1");

            MyClass mc = new MyClass();
            Type t = mc.GetType();
            Console.WriteLine("Type Name: {0}", t.Name);

            foreach(MethodInfo m in t.GetMethods())
                Console.WriteLine("Method Name: {0}", m.Name);

            foreach (PropertyInfo p in t.GetProperties())
                Console.WriteLine("Property Name: {0}", p.Name);
        }
    }
}
出力

Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Method Name: GetType
Method Name: ToString
Method Name: Equals
Method Name: GetHashCode
Property Name: MyProperty

まず気付くのは、クラス定義を最初に見たうえでの予測よりもメソッドを記述する行が多いことです。この追加のメソッドはどこから来たのでしょうか。.NET Framework のオブジェクト階層に十分詳しければ、Object 自体の汎用的な基本クラスから継承されたメソッドだとわかるはずです (実際に、最初に Object.GetType を使用して型を取得しました)。また、プロパティの getter 関数が確認できます。では、MyClass 自体で明示的に定義されるそのような関数が単に必要な場合はどうでしょうか。つまり、継承した関数をどのように隠すのでしょうか。または、明示的に定義されるインスタンス関数が必要なだけでしょうか。

MSDN® オンラインを少し見ると、BindingFlags パラメータを受け取る GetMethods の 2 番目のオーバーロードを使用する必要があることがわかります。BindingFlags 列挙体のさまざまな値を組み合わせることで、メソッドの必要なサブセットを単に返すように関数に指示できます。GetMethods の呼び出しを次の呼び出しで置き換えます。

GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |
           BindingFlags.Public)

その結果、次の出力が得られます (静的なヘルパ関数と System.Object から継承された関数がないことに注目してください)。

Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Property Name: MyProperty

型とメンバの名前 (完全修飾) があらかじめわかっている場合はどうでしょうか。型列挙から型検索へどのように変更するのでしょうか。図 3 の例は、型情報を記述する文字列リテラルを使用して、Object.GetType と Type.GetMethod で実際のコードの文字列リテラルを取得する方法を示しています。最初の 2 つの例のコードの取得により、基本クラスのブラウザを実装するのに必要な基本コンポーネントがあります。名前を使用してランタイム エンティティを見つけて、そのさまざまな関連プロパティを列挙することができます。

Figure 3 文字列を使用した Type と MethodInfo の取得 (Close Figure 3)
using System;
using System.Reflection;

// use Reflection to retrieve references to a type and method via name

namespace Example2
{
    class MyClass
    {
        public void MyMethod1() { return; }
        public void MyMethod2() { return; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(“Reflection Demo Example 2”);

            // note that we must use the fully qualified name...
            Type t = Type.GetType(“Example2.MyClass”);
            MethodInfo m = t.GetMethod(“MyMethod1”);

            Console.WriteLine(“Type Name: {0}”, t.Name);
            Console.WriteLine(“Method Name: {0}”, m.Name);
        }
    }
}

コードを動的に呼び出す

ここまでは、名前の出力のようなわかりやすい目的のためだけに、ランタイム オブジェクト (型やメソッドなど) へのハンドルを取得しました。しかし、それ以上はどのように行うのでしょうか。メソッドは実際にどのように呼び出すのでしょうか。図 4 は、型のメンバの MethodInfo を取得し、MethodInfo.Invoke を使用して実際にメソッドを動的に呼び出す方法を示しています。

Figure 4 メソッドの動的呼び出し (Close Figure 4)
using System;
using System.Reflection;

// use Reflection to retrieve a MethodInfo for an
// instance method and invoke it upon many object instances

namespace Example3
{
    class MyClass
    {
        private int id = -1;

        public MyClass(int id) { this.id = id; }

        public void MyMethod2(object p)
        {
            Console.WriteLine(
                “MyMethod2 is being invoked on object with “ +
                “id {0} with parameter {1}...”,
                    id.ToString(), p.ToString());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(“Reflection Demo Example 3”);

            MyClass mc1 = new MyClass(1);
            MyClass mc2 = new MyClass(2);

            Type t = mc1.GetType();
            MethodInfo method = t.GetMethod(“MyMethod2”);

            for(int i = 1; i <= 5; i++)
                method.Invoke(mc2, new object[]{i});
        }
    }
}

重要なポイントがこの例にはいくつかあります。まず、MyClass (mc1) のインスタンスから System.Type インスタンスを取得します。次に、その Type から MethodInfo インスタンスを取得します。最後に、MethodInfo を呼び出す際に、それを Invoke の最初のパラメータとして渡すことで、別の MyClass (mc2) のインスタンスにバインドします。

既に説明したように、この例は、一般的なソース コードから考えられる型とオブジェクトの使用方法の境界をあいまいにします。論理的に言うと、メソッドへのハンドルを取得して、そのメソッドが別のオブジェクトに属しているかのように呼び出します。これは関数型プログラミング言語の経験があるプログラマには簡単かもしれませんが、C# のみに慣れているプログラマにとっては、オブジェクトのインスタンス化からオブジェクト実装を切り離すのはわかりにくいかもしれません。

検査と呼び出しのまとめ

検査と呼び出しの基本を説明したので、具体例を使用してこれらについてまとめます。オブジェクトを処理する必要がある静的なヘルパ関数をライブラリに含めるとします。ただし、デザイン時、オブジェクトの型については何もわかりません。オブジェクトから意味のある情報を抽出する方法について指示するかどうかは、ヘルパ関数の呼び出し元が決定します。ヘルパ関数は、オブジェクトのコレクションとメソッドの文字列記述子を受け取ります。ヘルパ関数は、コレクションを繰り返し処理して、各オブジェクトでメソッドを呼び出し、関数により戻り値を集約します (図 5 を参照)。

Figure 5 オブジェクトからの情報の抽出 (Close Figure 5)
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Example4
{
    class Program
    {
        static void Main(string[] args)
        {
            // prepare some objects for our function to process
            object[] objs = new object[] {
                new IntReturner(1), new IntReturner(2),
                new IntReturner(3), new SonOfIntReturner(4),
                new SonOfIntReturner(5), new SonOfIntReturner(6),
            };

            Console.WriteLine(
                “Attempting to compute average, “ +
                “passing in array with {0} elements.”, objs.Length);

            int average = ComputeAverage(objs, “GetInteger”);

            Console.WriteLine(“Found an average of {0}!”, average);
        }

        public static int ComputeAverage(
            IEnumerable<object> objs, string methodname)
        {
            int sum = 0, count = 0;

            Type firstType = null;
            MethodInfo firstMethod = null;

            foreach (object o in objs)
            {
                if (firstMethod == null)
                {
                    firstType = o.GetType();
                    firstMethod = firstType.GetMethod(methodname);
                }

                sum += (int)firstMethod.Invoke(o, null);
                count++;
            }

            // note that we use integer division here (not floating point)
            if (count == 0) return 0;
            return sum / count;
        }
    }

    class IntReturner
    {
        protected int value = -1;
        public IntReturner(int i) { value = i; }
        public virtual int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of IntReturner, “
                “I’m returning {0}!”, value);
            return value;
        }
    }

    class SonOfIntReturner : IntReturner
    {
        public SonOfIntReturner(int i) : base(i) { }
        public override int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of SonOfIntReturner, “
                “I’m returning {0}!”, this.value);
            return value;
        }
    }

    class EnemyOfIntReturner
    {
        protected int value = -1;
        public EnemyOfIntReturner(int i) { value = i; }
        public virtual int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of EnemyOfIntReturner, “
                “I’m returning {0}!”, value);
            return value;
        }
    }
}

この例の目的のため、制約についていくつか説明します。まず、文字列パラメータで記述されるメソッドは (必ず各オブジェクトの基になる型で実装)、パラメータを受け取らず、整数を返します。コードは、オブジェクトのコレクションを繰り返し処理して、指定されたメソッドを呼び出し、すべての値の平均を順次算出します。最後に、これは実稼働環境で実行するコードではないので、合計の算出時、パラメータの検証や整数オーバーフローについては気にしません。

サンプル コードを見れば、Main 関数と静的ヘルパ ComputeAverage の間のプロトコルは、Object 自体の汎用的な基本クラス以外の型情報には依存しないことがわかります。つまり、型と順に渡すオブジェクトの構造をまったく違うものにする場合もありますが、文字列を使用して整数を返すメソッドを記述できる限りは、ComputeAverage 関数は機能します。

この最新の例に含まれている MethodInfo (およびリフレクション全般) に関して、注意する必要のある問題があります。ComputeAverage の foreach ループ内で、コードは、単にコレクションの最初のオブジェクトから MethodInfo を取得して、後続の各オブジェクトの呼び出しへバインドすることに注意してください。コーディングのとおり、これはうまく機能します。これは、MethodInfo キャッシュの簡単な例です。ただし、ここには基本的な制限があります。MethodInfo インスタンスは、取得元のオブジェクトと同じ階層の型のインスタンスでのみ呼び出し可能です。IntReturner と SonOfIntReturner (IntReturner から継承) の両方のインスタンスを渡すので、これは動作でわかります。

サンプル コードには、EnemyOfIntReturner というクラスを組み込んでいます。このクラスは、残りの 2 つのクラスと同じ基本プロトコルを実装しますが、共通の共有型は共有しません。つまり、インターフェイスは論理的に同等ですが、型階層での重なり合いはありません。このシナリオでの MethodInfo の使用を調べるには、"new EnemyOfIntReturner(10)" で取得したインスタンスであるコレクションに別のオブジェクトを追加してから、例を再び実行します。指定された Object の Invoke に MethodInfo は使用できないことを説明する例外が発生しますが、これは、MethodInfo の取得元の元の型とはまったく関係がないためです (メソッド名と基本プロトコルが同等であっても)。コードを実稼働可能な品質にするには、このシナリオに対応する必要があります。

解決策としては、受け取るすべてのオブジェクトの型を独自に分析して、その共有の型階層の解釈を維持する (存在する場合) ことが考えられます。次のオブジェクトが任意の既知の型階層とは異なる階層の型の場合、新しい MethodInfo を取得して格納する必要があります。別の解決策は、TargetException をキャッチして、MethodInfo インスタンスを単に再取得することです。ここで説明した解決策には、両方とも長所と短所があります。MethodInfo キャッシュとリフレクションのパフォーマンスに関して、Joel Pobar が書いた 2007 年 5 月号の MSDN Magazine の優れた記事を参照することを強くお勧めします。

この例は、アプリケーションまたはフレームワークへのリフレクションの追加によって、後のカスタマイズのための柔軟性や拡張性をさらに向上できることを示していると思います。確かに、リフレクションの使用は、皆さんが主に使用するプログラミング言語での同等のロジックと比較した場合、多少厄介かもしれません。リフレクション ベースの遅延バインディングをコードに追加するのは、皆さんや皆さんの顧客にとって問題が多いと感じる場合は (結局、フレームワークに何らかの形で型とコードを記述する必要があるため)、柔軟性の適切な程度とバランスを取ることもできます。

シリアル化の効率的な型処理

いくつかの例を使用して .NET Reflection の基本を説明してきたので、次は実際のシナリオに取り組みましょう。ソフトウェアが Web サービスか別のアウトプロセスのリモート テクノロジを使用して他のシステムと通信する場合、シリアル化に直面していると考えられます。シリアル化とは、基本的には、ライブのメモリ内オブジェクトをネットワーク経由の転送またはディスクへの保存に適したデータ形式に変換することです。

.NET Framework の System.Xml.Serialization 名前空間は、XmlSerializer で強力なシリアル化エンジンを提供します。XmlSerializer は、任意のマネージ オブジェクトを使用して、それを XML に変換できます。その XML データを型指定されたオブジェクト インスタンスに後から変換して戻すことも可能で、この処理は逆シリアル化と呼ばれます。XmlSerializer クラスは、強力な企業向けソフトウェアで、プロジェクトでシリアル化に直面した場合の一番の選択肢になるはずです。ただし、学習のため、シリアル化 (または、他の同様のランタイム型のインスタンス処理) がどのように実現されるのかを見てみましょう。

シナリオ : 任意のユーザー型のオブジェクト インスタンスを使用して、それをインテリジェントなデータ形式に変換する必要があるフレームワークを出荷します。たとえば、次のような型 Address のメモリ内オブジェクトがあるとします。

(pseudocode)
class Address
{
    AddressID id;
    String Street, City;
    StateType State;
    ZipCodeType ZipCode;
}

後で使用するための適切なデータ表現をどのように生成するのでしょうか。単なる簡単なテキストのレンダリングは、次のように実行します。

Address: 123
    Street: 1 Microsoft Way
    City: Redmond
    State: WA
    Zip: 98052

コードの記述時などに前もって変換する必要がある正規のデータ型を十分に把握している場合は、次のように非常に簡単になります。

foreach(Address a in AddressList)
{
    Console.WriteLine(“Address:{0}”, a.ID);
    Console.WriteLine(“\tStreet:{0}”, a.Street);
    ... // and so on
}

ただし、実行時に対話する必要がある型について事前の知識がない場合は、面白さが増します。このような一般的なフレームワーク コードをどのように作成するのでしょうか。

MyFramework.TranslateObject(object input, MyOutputWriter output)

まず、どの型メンバがシリアル化の対象であるかを決める必要があります。可能性としては、プリミティブなシステム型など特定の型のメンバを単に取得するか、またはどのメンバをシリアル化する必要があるのかを型の作成者が記述できるメカニズムを提供する (型メンバのマークアップとしてカスタム属性を使用することによってなど) ことが考えられます。プリミティブなシステム型など特定の型のメンバを単に取得することも可能で、型の作成者は、どのメンバをシリアル化する必要があるのかを記述できます (型メンバのマークアップとしてカスタム属性を使用することによって)。

どのデータ構造メンバを変換するかを記述したら、それを列挙して、受け取るオブジェクトから取得できるロジックをコーディングする必要があります。データ構造とデータ値の両方にクエリを実行できるようにすることで、リフレクションは、複雑な作業を行います。

単純化のため、簡易変換エンジンを設計しましょう。このエンジンは、オブジェクトを取得し、そのすべてのパブリック プロパティの値を取得して、ToString を直接呼び出すことでそれを文字列に変換してから、値をシリアル化します。input という名前の特定のオブジェクトでは、このアルゴリズムは、次のようになります。

  1. input.GetType を呼び出して、input の基になる構造を記述する System.Type インスタンスを取得します。
  2. 適切な BindingFlags パラメータを指定した Type.GetProperties を使用して、PropertyInfo インスタンスとしてパブリック プロパティを取得します。
  3. PropertyInfo.Name と PropertyInfo.GetValue を使用して、キー値のペアとしてプロパティを取得します。
  4. 各値で Object.ToString を呼び出して、その値を文字列形式に (プリミティブに) 変換します。
  5. オブジェクト型の名前をプロパティ名と文字列値と共に正しいシリアル化の形式にパッケージ化します。

このアルゴリズムは、問題を大幅に単純化していますが、ランタイム データ構造を取得して自己記述型データに変換するのに必要とされる要点を押さえています。ただし、問題点があります。それはパフォーマンスです。既に説明したように、リフレクションは、型処理と値取得のためにパフォーマンスに影響します。この例では、指定された型のインスタンスごとに完全な型分析を実行しています。

型の構造についての解釈を得て保持できることにより、後でそれを単に取得してその型の新しいインスタンスを効率的に処理できる場合はどうでしょうか。つまり、サンプル アルゴリズムの手順 3 にスキップするとしたらどうでしょうか。さいわい、.NET Framework の機能を使用すれば、これは十分に可能です。型のデータ構造を把握したら、CodeDom を使用することで、そのデータ構造にバインドされるコードを実行時に生成できます。ヘルパ クラスとヘルパ メソッドを含むヘルパ アセンブリを生成します。これは、受け取る型を参照してそのプロパティに直接アクセスするので (マネージ コード内での任意の他の参照と同様)、型検査によるパフォーマンスへの影響が一度で済みます。

では、アルゴリズムを修正しましょう。新しい型では、次のようになります。

  1. その型に対応する System.Type インスタンスを取得します。
  2. System.Type のさまざまなアクセサを使用して、プロパティ名、フィールド名などのスキーマ (または少なくともシリアル化の対象であるスキーマのサブセット) を取得します。
  3. スキーマ情報を使用してヘルパ アセンブリを生成します (CodeDom を使用)。このアセンブリは、新しい型に対してリンクされ、インスタンスを効率的に処理します。
  4. ヘルパ アセンブリのコードを使用して、インスタンスのデータを抽出します。
  5. データを適切な形式でシリアル化します。

指定された型の受け取るインスタンスのすべてで手順 4 にスキップできるので、各インスタンスを明示的に検査する場合と比べて、パフォーマンスは格段に向上します。

SimpleSerialization という、リフレクションと CodeDom と共にこのアルゴリズムを実装する基本的なシリアル化ライブラリを開発しました (これは、このコラムに関するダウンロードの一部として利用可能です)。メイン コンポーネントは SimpleSerializer というクラスで、System.Type インスタンスを使用して構築します。コンストラクタ内で、SimpleSerializer の新しいインスタンスは、指定された Type を分析してヘルパ クラスを含む一時アセンブリを生成します。このヘルパ クラスは、指定されたデータ型に緊密にバインドされ、型について前もって完全に把握した状態でコードを独自に記述したかのようにインスタンスを処理できます。

SimpleSerializer クラスは、次のようなレイアウトです。

class SimpleSerializer
{
    public class SimpleSerializer(Type dataType);

    public void Serialize(object input, SimpleDataWriter writer);
}

驚くほど簡単です。コンストラクタは、次の複雑な処理の大部分を行います。リフレクションを使用して型の構造を分析し、CodeDom でヘルパ アセンブリを生成します。SimpleDataWriter クラスは、共通のシリアル化のパターンを示すための単なるデータ シンクです。

簡単な Address クラスのインスタンスのシリアル化は、次の疑似コードで実行します。

SimpleSerializer mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriter writer = new SimpleDataWriter();
mySerializer.Serialize(addressInstance, writer);

仕上げ

サンプル コード、特に SimpleSerialization ライブラリを実際に使用してみることを強くお勧めします。SimpleSerializer の興味深い部分にはすべてコメントを加えましたが、それが有益となればと思います。もちろん、実稼働環境で実行するコードで本格的なシリアル化を行う必要が生じた場合は、.NET Framework で提供されるテクノロジ (XmlSerializer など) が間違いなく役立ちます。ただし、実行時に任意の型を使用してそれを効率的に処理する必要があるとわかった場合、使用するシナリオで、この SimpleSerialization ライブラリを採用できることを期待しています。

ガイダンスとフィードバックを寄せていただいた CLR 開発者の Weitao Su (リフレクション) と Pete Sheill (CodeDom) に感謝の意を表します。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Mike Repassは、.NET Framework CLR チームのプログラム マネージャです。彼は、リフレクション、CodeDom など実行エンジンのさまざまな部分に携わっています。連絡先は、blogs.msdn.com/mrepass にある本人のブログです。


データ ポイント: SQL Server 管理オブジェクト

データ ポイント
SQL Server 管理オブジェクト
John Papa

コードのダウンロード : : DataPoints2007_06.exe (173KB)
コンテンツ
SMO の基本事項
サーバーの一覧を取得する
サーバーに接続する
データベースの一覧を取得する
DDL または DML コマンドを発行する
オブジェクトの一覧を繰り返し処理する
データベースをバックアップする
データベースを復元する
データベースのバックアップを確認する
まとめ

データベース開発者はデータを照会して操作するだけでなく、定期的に管理タスクを実行する必要もあります。SQL Server™ 管理オブジェクト (SMO) は、開発者がデータベースのバックアップと復元、データ定義言語 (DDL) コマンドの発行などを行うための堅牢なツールセットを提供します。また、SQL SMO を使用して、SQL Server に接続したり、データベース オブジェクトのコレクションを繰り返し処理したり、さまざまなタスクを実行したりできます。

このコラムでは、SMO を利用してデータベースのオブジェクトを調べ、さまざまな管理タスクを実行する方法について説明します。具体的には、SMO を使用するプロジェクトを設計してサーバーに接続する方法などを取り上げます。また、データベースに対して DDL コマンドやデータ操作言語 (DML) コマンドを発行するサンプル アプリケーションについても見ていきます。最後に、SMO でデータベースのバックアップ、復元、バックアップの検証を実行する方法を示します。

SMO の基本事項

SMO は Microsoft® .NET Framework のオブジェクト ライブラリで、1 つ以上のデータベース サーバーの管理に利用できます。SMO は、SQL Server 2005 で導入された新しい機能で動作するよう設計されていますが、従来の機能をそれぞれサポートする SQL Server 2000 データベースにも接続できます。ただし、一部の SMO クラス、プロパティおよびメソッドは、SQL Service Broker やスナップショット分離などの新機能をサポートする SQL Server 2005 でしか動作しません。

COM ベースの SQL 分散管理オブジェクト (SQL-DMO) は現在も SQL Server 2005 に付属していますが、SMO を持つ最新の SQL Server 2005 機能への直接アクセスを組み込む更新はされていません。SQL Server データベースを管理する .NET アプリケーションの設計時には、COM の DMO との 相互運用機能を使用するのではなく、SQL SMO を選択することをお勧めします。現在 DMO を使用しているアプリケーションの SMO へのアップグレードも検討する必要があります。

SMO は、Microsoft® SQL Server 2005 のインストール時に、クライアント ツールをインストールするオプションをオンにすることによってインストールできます。その後、SMO を使用するプロジェクトを作成するときに、最初に適切なアセンブリを参照します。参照する必要のある主な 2 つの DLL は Microsoft.SqlServer.Smo.dll と Microsoft.SqlServer.ConnectionInfo.dll です。前者にはコア SMO クラス (Database、Server、Backup、および Restore など) があり、後者にはSQL Server オブジェクトへの接続の管理を支援するクラス ライブラリがあります。また、多くの場合、Microsoft.SqlServer.SqlEnum.dll (通常有用な列挙を含む) や Microsoft.SqlServer.SmoEnum.dll (Microsoft SQL Server オブジェクトを一意に識別する Uniform Resource Name (URN) アドレスにプログラムからアクセスする Urn クラスを含む) も参照する必要があります。

サーバーの一覧を取得する

SMO を使用して、ネットワーク上にインストールされた有効な SQL Server の一覧を取得できます。引数を指定して、ネットワークで SQL Server を検索するか、ローカルのみで検索するかを制御できます。SmoApplication クラスでは EnumAvailableSqlServers という名前の静的メソッドを公開します。true のブール値を渡すと、メソッドはローカル サーバーのみをすべて検索して返します。このメソッドに false のブール値を渡すと、ネットワーク上の有効なサーバーをすべて検索します。次のコードは、ネットワーク上で有効な SQL Server インスタンスの一覧を取得し、ADO.NET DataTable に返します。

DataTable dtServers = SmoApplication.EnumAvailableSqlServers(false);
foreach (DataRow row in dtServers.Rows)
{
  string sqlServerName = row[“Server”].ToString();
  if (row[“Instance”] != null && row[“Instance”].ToString().Length > 0)
      sqlServerName += @”\” + row[“Instance”].ToString();
}

次に、DataTable の行を繰り返し処理し、SQL Server とそのインスタンスの名前の形式を整えます (インスタンスが存在する場合)。残念ながら、このメソッドはタイムアウトの設定された UDP ブロードキャストに依存するため信頼性が低く、一貫性のない結果または不完全な結果を得る可能性があります。

サーバーに接続する

次に挙げる例はいずれも、MSDN® Magazine Web サイト上でダウンロードや閲覧が可能なアプリケーションの一部です。SMO でアプリケーションを構築する際、最初に行う手順の 1 つとして SQL Server インスタンスへの接続があります。これは通常どおりに行います。接続を確立するには、SQL Server の名前 (複数インスタンスが存在する場合はそのインスタンス名も) とログイン資格情報を渡します。Windows® 認証で接続する場合は、SMO を使用してこれを指定できます。ただし、SQL 認証で接続する場合は、有効なログイン名とパスワードを渡す必要があります。

図 1 は、ユーザーが SQL Server インスタンスの名前と資格情報を入力できるサンプル アプリケーションを示しています。この図では、インスタンス化された SQL Server の名前を入力し、Windows 認証を使用してサーバーに接続することを指定しています。接続ボタンをクリックすると、データベースの一覧が設定されます。

図 1 SMO 経由でサーバーに接続する
図 1 SMO 経由でサーバーに接続する(画像を縮小するには、ここをクリックします)
図 1 SMO 経由でサーバーに接続する
図 1 SMO 経由でサーバーに接続する (画像を拡大するには、ここをクリックします)

サンプル アプリケーションに、アプリケーションで使用するいくつかの共通ルーチンを備えた SMOHelper というクラスを作成しました。SMOHelper クラスのインスタンスは、単一の SQL Server との対話に使用できます。これには、Windows 認証または SQL Server 認証のいずれかによってサーバーに接続するために必要な情報を受け入れるコンストラクタがあります。図 2 は、適切なクラス プロパティを取得して設定する SMOHelper コンストラクタを示しています。クラスのメソッドはこのコンストラクタを使用して、データベース操作を実行します。

Figure 2 サーバー接続情報を受信する (Close Figure 2)
public SMOHelper(string serverAndInstanceName,
      string userName, string password,
      bool useWindowsAuthentication)
{
  this.server = new Server();
  int slashPos = serverAndInstanceName.IndexOf(‘\\’);
  if (slashPos >= 0)
  {
    this.serverName = serverAndInstanceName.Substring(0, slashPos);
    this.instance = serverAndInstanceName.Substring(slashPos + 1);
  }
  else
  {
    this.serverName = serverAndInstanceName;
    this.instance = string.Empty;
  }
  this.userName = userName;
  this.password = password;
  this.useWindowsAuthentication = useWindowsAuthentication;
}

サーバーへの接続は、Server クラスのインスタンスを作成して ConnectionContext クラスのプロパティを設定することによって実行されます。図 3 のコードは、ユーザーがフォーム上で選択した認証の種類に基づいて接続を設定します。ConnectionContext の Connect メソッドは明示的に呼び出すことができますが、SMO はプールから自動的に接続を取得して解放するため、実際にはその必要はありません。詳細については、blogs.msdn.com/mwories/archive/2005/05/02/smoconnections.aspx を参照してください。

Figure 3 サーバーに接続する (Close Figure 3)
public void Connect()
{
    server.ConnectionContext.ServerInstance = ServerAndInstanceName;
    server.ConnectionContext.LoginSecure = useWindowsAuthentication;
    if (!this.useWindowsAuthentication)
    {
        server.ConnectionContext.Login = this.userName;
        server.ConnectionContext.Password = this.password;
    }
    // server.ConnectionContext.Connect();
}

データベースの一覧を取得する

サーバーに接続すると、そのサーバー上のデータベースの一覧が、Server オブジェクトの Databases プロパティを通じて取得できます。このサンプル アプリケーションでは、サーバー上のデータベースの一覧をコンボ ボックス コントロールにバインドするデータ バインドを使用しました。DatabaseCollection に繰り返し処理を行い、データベースの名前を含む List<string> を作成しました。次にこの一覧をコンボ ボックス コントロールにバインドします。ユーザーはコンボ ボックス コントロールでデータベースを選択してアクションを実行できます。次のコードでは、データベースを繰り返し処理し、名前の文字列の一覧を作成します。

public List<string> GetDatabaseNameList()
{
    List<string> dbList = new List<string>();
    foreach (Database db in server.Databases)
        dbList.Add(db.Name);
    return dbList;
}

上のコードでは、データベース名のみの一覧を作成するため、List<string> と指定しました。しかし、コンボ ボックス コントロールは、通常多くの SMO メソッドから返される豊富な機能のオブジェクトをラップするカスタム クラス (List<MyDatabase> など) の一覧にも同じように簡単にバインドすることができます。

DDL または DML コマンドを発行する

ADO.NET と同様に、SMO でデータベースに対するコマンドを実行し、標準の DML を使用してデータを取得または変更できます。SMO は複雑な DML を処理するよう最適化されていませんが、単純な DML ステートメントの実行には使用できます。

図 1 のフォームは、ユーザーが DML または DDL ステートメントを入力して実行できるテキスト ボックスを示しています。クエリで行セットを取り出す (SELECT ステートメント) 場合、SMO では ExecuteWithResults メソッドによって行セットを取り出します。それ以外の、コマンドが行セットを返さない (INSERT、UPDATE、または DELETE) 場合、SMO は ExecuteNonQuery メソッドを使用するコマンドを実行します。次のコード サンプルでは、クエリを実行して ADO.NET DataSet に行セットを返します。DataSet は次にフォーム上の DataGridView にバインドされます。

  DataSet ds = db.ExecuteWithResults(txtSQL.Text);
  dgvResults.DataSource = ds.Tables[0];

次のコードでは、データベース オブジェクトに対して結果を返さないコマンドを実行します。

  db.ExecuteNonQuery(sql);

実行するコマンドが、DML コマンドである必要はありません。DDL コマンドも使用できます。たとえば、ExecuteNonQuery メソッドで、ALTER TABLE、CREATE PROCEDURE、RESTORE または DBCC CHECKDBB コマンドも実行できます。たとえば、次のコマンドでは Person テーブルを作成できます。

    string sql = “CREATE TABLE Person “ +
       “(PersonID INT NOT NULL PRIMARY KEY,
       FullName VARCHAR(100))”;
    db.ExecuteNonQuery(sql);

オブジェクトの一覧を繰り返し処理する

SMO を使用して、特定のデータベース オブジェクトを検索したり、状況によって異なる操作を実行したりすることもできます。SMO の Database クラスでは、含まれるオブジェクトそれぞれの一連のコレクションを公開します。図 4 は、Database クラスが公開するさまざまなコレクションの一覧の一部を示しています。

Figure 4 コレクション (Close Figure 4)

既定値
FileGroups
Roles
Rules
Schemas
StoredProcedures
Tables
Triggers
UserDefinedFunctions
Users
Views

図 5 は、開発用コンピュータ上にある Northwind というデータベースのテーブル、ストアド プロシージャおよびユーザーをすべて表示するサンプル アプリケーションを示しています。特定のデータベース内にあるテーブルの一覧を取得するのは非常に簡単です。Server オブジェクトがあれば、Server オブジェクトから必要な Database オブジェクトを取得し、Tables プロパティを通じて TablesCollection にアクセスできます。図 6 は、データベースのオブジェクトのコレクションにアクセスしてループ処理し、独自のカスタム オブジェクトを構築する方法を示しています。このコードでは、単純にオブジェクトの名前を取得して List<string> に設定し、SmoLists フォームのリスト ボックス コントロールにバインドします (図 5 を参照)。

Figure 6 リストを取得する (Close Figure 6)
public List<string> GetTableNameList(Database db)
{
    List<string> tableList = new List<string>();
    foreach (Table table in db.Tables) tableList.Add(table.Name);
    return tableList;
}

public List<string> GetStoredProcedureNameList(Database db)
{
    List<string> storedProcedureNameList = new List<string>();
    foreach (StoredProcedure storedProcedure in db.StoredProcedures)
        storedProcedureNameList.Add(storedProcedure.Name);
    return storedProcedureNameList;
}

public List<string> GetUserNameList(Database db)
{
    List<string> userNameList = new List<string>();
    foreach (User user in db.Users) userNameList.Add(user.Name);
    return userNameList;
}
図 5 データベース一覧
図 5 データベース一覧(画像を縮小するには、ここをクリックします)
図 5 データベース一覧
図 5 データベース一覧 (画像を拡大するには、ここをクリックします)

もちろん、これらのオブジェクト内には Name プロパティ以外にさらに多くのプロパティとメソッドがあります。たとえば、テーブルの権限を設定する Revoke または Grant などのメソッドを Table オブジェクトに対して実行することもできます。

データベースをバックアップする

開発者が SMO を使用して行う一般的なタスクとして、データベースのバックアップが挙げられます。SMO では、Backup クラスを使用してデータベースのバックアップを比較的簡単に実行できます。図 7 は、データベースをバックアップして復元できるサンプル フォームを示しています。

図 7 バックアップと復元
図 7 バックアップと復元(画像を縮小するには、ここをクリックします)
図 7 バックアップと復元
図 7 バックアップと復元 (画像を拡大するには、ここをクリックします)

アプリケーションを実行して [バックアップ] ボタンをクリックすると、進行状況バーが表示されます。バックアップまたは復元の操作の状態をユーザーにフィードバックするために、ProgressBar コントロールをフォームに追加しました。SMO Backup クラスは、バックアップの進行状況を追跡できるイベントを公開します (これらのイベントを確認してみます)。

ユーザーが [バックアップ] ボタンをクリックすると、次に示す btnBackup_Click イベント ハンドラが実行されます。

private void btnBackup_Click(
   object sender, EventArgs e)
{
   pbDBAction.Maximum = 100;
   pbDBAction.Style = ProgressBarStyle.Blocks;
   pbDBAction.Value = 0;
   pbDBAction.Step = 10;
   string databaseName = cboDatabase.SelectedValue.ToString();
   smoHelper.BackupDatabase(databaseName);
}

まず、Progressbar コントロールのプロパティを設定し、Progressbar がブロックのセットとして表示され、空の状態で開始されるように設定します。次に、コンボ ボックス コントロールの選択内容からデータベースの名前を取得し、図 8 の SMOHelper クラスで作成した BackupDatabase メソッドに渡します。

Figure 8 データベースをバックアップする (Close Figure 8)
public void BackupDatabase(string databaseName)
{
    Console.WriteLine(“*** Backing up ***”);

    Backup backup = new Backup();
    backup.Action = BackupActionType.Database;
    backup.Database = databaseName;
    backup.Incremental = false;
    backup.Initialize = true;
    backup.LogTruncation = BackupTruncateLogType.Truncate;

    string fileName = string.Format(“{0}\\{1}.bak”,
      testFolder, databaseName);
    BackupDeviceItem backupItemDevice =
      new BackupDeviceItem(fileName, DeviceType.File);
    backup.Devices.Add(backupItemDevice);
    backup.PercentCompleteNotification = 10;
    backup.PercentComplete += backup_PercentComplete;
    backup.Complete += backup_Complete;

    backup.SqlBackup(server);
}

BackupDatabase では、まず、Backup クラスのインスタンスを作成します。次に Action プロパティを BackupActionType.Database 列挙値に設定します (Logs と Files もバックアップできます)。当然、ここでバックアップするデータベースの名前を設定し、他のいくつかのプロパティを設定します。データベースの完全バックアップを行うので、Incremental プロパティを false に設定して増分バックアップをオフにします。

データベースをバックアップする際に、バックアップ ファイルの作成先のファイル パスの場所とファイル名を指定する必要があります。サンプル コードでは、簡略化するためすべてのバックアップを同じフォルダに入れています (testFolder 変数を参照してください)。バックアップ ファイルを作成する場所を選択したら、BackupDeviceItem を作成し、Backup クラスの Devices リストに追加します。ここではバックアップをファイルとして保存するよう選択しましたが、LogicalDevice や Tape など、他の形態でバックアップを保存することもできます。

バックアップ処理が呼び出し側のフォームとその ProgressBar コントロールに通知するように、イベント ハンドラを PercentComplete イベントに追加しました。このイベントは、PercentCompleteNotification プロパティによって定義される間隔で発生します。サンプル アプリケーションでは、このイベントで ProgressBar コントロールに進行状況を表示します。また、バックアップ処理が完了したときにフォームに通知できるように、イベント ハンドラを Complete イベントにも追加しました (イベントをまとめてフックする方法の詳細については、サンプル アプリケーションを参照してください)。

最後に、実際にデータベースのバックアップ タスクを実行する SqlBackup メソッドを発行します。データベースのバックアップは、データベースのサイズや内容など、要因の数によっては予想よりも長時間かかる可能性があるので注意してください。

データベースを復元する

SMO は、ファイルまたは別のバックアップ デバイスからのデータベースの復元に使用できる Restore クラスを公開します。サンプル アプリケーションでは、先ほどバックアップしたデータベースを復元します。処理はバックアップする場合と似ています。バックアップ元であるデータベースには復元せずに、データベースを新しいファイル名と新しい論理名で復元していることに注意してください。実際は、バックアップのメソッドによってデータベースのコピーが作成されます。もちろん、既存のデータベースにデータベースを復元するよう選択することもできますが、この処理は便利です。

図 9 のコードは、データベースを復元するサンプル アプリケーションで使用する RestoreDB メソッドを表しています。最初に Restore クラスのインスタンスを作成し、復元するバックアップ ファイルの名前を設定します。

Figure 9 データベースを復元する (Close Figure 9)
public void RestoreDB(string databaseName)
{
    Restore restore = new Restore();
    string fileName = string.Format(“{0}\\{1}.bak”,
        testFolder, databaseName);
    restore.Devices.Add(new BackupDeviceItem(fileName, DeviceType.File));

    // Just give it a new name
    string destinationDatabaseName =
        string.Format(“{0}_newly_restored”, databaseName);

    // Go grab the current database’s logical names for the data
    // and log files. For this example, we assume there are 1 for each.
    Database currentDatabase = server.Databases[databaseName];
    string currentLogicalData =
        currentDatabase.FileGroups[0].Files[0].Name;
    string currentLogicalLog = currentDatabase.LogFiles[0].Name;

    // Now relocate the data and log files
    RelocateFile reloData =
        new RelocateFile(currentLogicalData,
        string.Format(@”{0}\{1}.mdf”, testFolder,
            destinationDatabaseName));
    RelocateFile reloLog =
        new RelocateFile(currentLogicalLog,
        string.Format(@”{0}\{1}_Log.ldf”, testFolder,
            destinationDatabaseName));
    restore.RelocateFiles.Add(reloData);
    restore.RelocateFiles.Add(reloLog);

    restore.Database = destinationDatabaseName;
    restore.ReplaceDatabase = true;

    restore.PercentCompleteNotification = 10;
    restore.PercentComplete += restore_PercentComplete;
    restore.Complete += restore_Complete;
    restore.SqlRestore(server);
}

次に、復元先の新しい名前のデータベース (Northwind_newly_restored) を作成し、FileGroups コレクションと Files コレクションから論理データとログ ファイルの名前を取得します。これらの名前は、バックアップの物理ファイルを新しい名前に再配置するために必要です (データベースを別の新しいデータベースに復元するため)。次に、復元するオブジェクトの RelocateFiles コレクションに RelocateFile クラスのインスタンスを (再配置するファイルごとに 1 つ) 追加します。

復元するデータベースの名前を設定し、データベースが既に存在する場合に同じ名前で既存のデータベースを置き換えるよう SMO に指示します。ProgressBar に復元操作の進行状況を示すため、PercentComplete イベント ハンドラと Complete イベント ハンドラを適宜設定します。

データベースのバックアップを確認する

データベースのバックアップが破損していたら、あまり役に立ちません。バックアップを確認することをお勧めします。これは、Restore クラスのインスタンスを作成して、DeviceType を適切なメディアに設定し、デバイスを追加して SqlVerify メソッドを実行することで、簡単に実施できます。この結果、バックアップが確認されたかどうかを示すブール値が返されます。次のコードは、この確認を実行する方法を示しています。

Restore restore = new Restore();
restore.DeviceType = DeviceType.File;
restore.Devices.Add(@”c:\MySMOTest.bak”);
boolean verified = restore.SqlVerify(server,out msg);

まとめ

SMO では、.NET コードでデータベースの管理操作を簡単に実行できます。このようなタスクはすべて、データベースに対して直接 T-SQL を使用して、または SQL Server Management Studio を通じて実行できます。さらに、SMO が使用できることにより、.NET アプリケーションのこれらの操作が追加で利用できるなど、柔軟性が高まります。実際に、SQL Server Management Studio は、管理タスクの大部分を SMO に依存します。

ご意見やご質問は、

John mmdata@microsoft.com.

まで英語でお送りください。
John Papaは、プロのコンサルタントで、マイクロソフトの .NET テクノロジを使用する教育、指導に従事しています。Microsoft C# の MVP である John は、アーキテクチャと ADO.NET、XML、SQL Server などのデータ アクセス テクノロジに関する記事や書籍をいくつか執筆しています。また、VSLive などの業界のカンファレンスでの講演も頻繁に行っています。連絡先は、www.johnpapa.net です。


Advanced Basics: Visual Basic から RibbonX を使用する

Advanced Basics
Visual Basic から RibbonX を使用する
Ken Getz

コンテンツ
準備作業
サンプル アドインを作成する
カスタム色ギャラリーを作成する
GetEnabled を作成する
GetItemCount を作成する
GetItemHeight および GetItemWidth を作成する
イメージを作成する
ヒントを作成し、アドインをテストする
アクション コールバックを追加する
リボン コントロールの対話操作を追加する

MSDN Magazine の 2007 年 2 月号で Eric Faller は、RibbonX API と XML マークアップを使用して 2007 Microsoft® Office system の拡張機能を作成する方法を説明しました (msdn.microsoft.com/msdnmag/issues/07/02/RibbonX を参照)。同じ号で Paul Stubbs は、2007 Office system アプリケーション (特に PowerPoint® 2007) 用のマネージ アドインを紹介し、それらを Visual Studio® 2005 Tools for Office Second Edition を使用して作成する方法について説明しました。これらのわかりやすい説明に引き続き、このコラムでは、RibbonX を使用したり独自のリボンをカスタマイズする際によく発生する疑問点について、あらかじめ説明します。

2007 Office system も Visual Studio 2005 も、本当の意味でリボンのデザイナではないため、リボンのカスタマイズには追加の作業が多少必要です。リボンをカスタマイズするための便利なユーザー インターフェイスは存在せず、作業はすべて XML と、.NET 互換コードまたは Visual Basic® for Applications (VBA) コード内で行います。Visual Studio Tools for Office の次のリリースでは、リボンのデザイナが提供されます。詳細については、Steve Fox と Paul Stubbs による今月号の記事を参照してください。また、2007 Office system のリボンの詳細については、msdn.microsoft.com/office/tool/ribbon を参照してください。ここでは、2007 Office system のリボンの使用およびカスタマイズに関して、Microsoft からの最新情報を入手できます。

準備作業

リボンのさまざまなコントロールを使用するには、リボンの各要素に対応する識別子 (msoID 属性) を知る必要があります。"2007 Office System ドキュメント : コントロール ID の一覧" をダウンロードしてください。このダウンロード (Excel® 2003 および Excel 2007 形式で入手可能) 内の一連のスプレッドシートには、リボンの各要素の全容が記載されています。この情報がなければ、既存のリボンを変更することはまず不可能です。なお、[Options] (オプション) ダイアログ ボックスの [Customization] (カスタマイズ) ペインで、任意のコントロール上にマウスを重ねることによっても、その ID を知ることができます。

開発用コンピュータ上に、RibbonX を記述する XML スキーマのコピーがあることを確認してください。このスキーマ (customUI.xsd) が存在することによって、リボンのカスタマイズのマークアップを作成する際に、Visual Studio 2005 XML エディタで IntelliSense® を利用できるようになります。"2007 Office System : XML スキーマ リファレンス" をダウンロードできます。このダウンロードには、customUI.xsd ファイルと、2007 Office system で使用するこれらのスキーマに関する参考資料が含まれています。customUI.xsd をダウンロードして展開したら、%Program Files%\Microsoft Visual Studio 8\Xml\Schemas フォルダにコピーします (このパスの %Program Files% は、使用しているコンピュータの Program Files フォルダの場所に置き換えます)。このフォルダにスキーマ ファイルが格納されていると、Visual Studio 2005 でリボンのカスタマイズ ファイルを新規作成する際にスキーマにアクセスできます。

Word 2007、Excel 2007、または PowerPoint 2007 のドキュメントの一部としてリボンのカスタマイズを保存し、リボンのコールバックを VBA コードで処理する場合は、通常、ZIP ファイル処理ツールを使用してドキュメントを開き、カスタマイズ部分を作成し、この部分への関連付けを必要に応じて設定する必要があります。これは手間のかかる作業です。この手間を省くため、OpenXMLDeveloper.org から Custom UI Editor Tool をダウンロードします。このツールは、リボンのカスタマイズの作成には役立ちませんが、いくつもの重要な点で有用です。Word、Excel、または PowerPoint のドキュメントやテンプレートにカスタマイズ部分を挿入し、必要な部分と関連付けを設定します。アイコンを挿入してドキュメントをカスタマイズすることができます。RibbonX マークアップを customUI.xsd に照らして検証します。また、コールバック プロシージャの正確な形式を検索しなくても済むように、VBA スタイルのコールバック ハンドラ スタブを生成することができます。Visual Studio Tools for Office (VSTO) のアドインや、Visual Studio COM の共有アドインを作成する場合でも、最後の 2 つの機能が役立つことがあります。

もちろんドキュメントも必要です。MSDN® からオンラインで参照できる、Frank Rice と私が共同執筆した 3 つの連続記事には、例、参照情報、およびよく寄せられる質問への回答が掲載されています。RibbonX の完全なヘルプ ファイルが Microsoft からまとめて提供されるようになるまでは、まずこれらの記事を参照することをお勧めします。これらの記事については、Part 1、Part 2、および Part 3 をそれぞれ参照してください。

最後に、カスタマイズしたリボンの格納場所を考慮する必要があります。3 つのうちの最初の記事でも述べたとおり、これには複数の方法があります。UI のカスタム部分を作成し、この部分への関連付けを設定することにより、Word、Excel、または PowerPoint のドキュメントに RibbonX マークアップを直接格納できます。カスタム UI をドキュメントに挿入する際には、前述の CustomUI Editor Tool が役立ちます。Access™ 2007 のリボンに対するカスタマイズの場合、XML マークアップをテーブルに格納できます。このカスタマイズは、コードを使用して取得およびアクティブ化することができます。また、このカスタマイズを USysRibbons テーブルに格納することもできます。この場合、Access ではデータベースで読み込むようにカスタマイズを読み込むことができます。これらの方法では、ドキュメント ベースまたはテンプレート ベースのカスタマイズが作成されます。

アプリケーション ベースのカスタマイズを作成する場合は、2007 Office system アプリケーション用の COM アドインの作成を検討する必要があります。このアドインは、Visual Studio 2005 の共有アドイン テンプレートを使用するか、Visual Studio 2005 Tools for Office Second Edition を使用して (この方が簡単で堅牢な方法です) 作成できます。これらのテクニックの使用方法についても、最初の MSDN 記事に記載されています。

ツールとマニュアルがすべて揃ったので、独自の RibbonX カスタマイズを作成する準備ができました。このコラムの作業でこれらすべてを使用するとは限りませんが、独自のカスタマイズを作成する際にきっと役立ちます。

XML マークアップをどこに格納しても内容は変わらないため、リボンのコールバックをマネージ コードで処理する、最も単純な方法を選びました。つまり、Excel 2007 用のアドインを VSTO 2005 SE を使用して作成します (VSTO 2005 SE がインストールされていない場合は、製品のポータル msdn2.microsoft.com/aa905543.aspx からダウンロードすることも詳細情報を入手することもできます)。

サンプル アドインを作成する

最初に、VSTO 2005 SE で単純なアドインを作成してテストする必要があります。ここでは、前に述べた 3 つのうちの最初の MSDN 記事に記載されている手順を簡単に示しながら、Word ではなく Excel のアドインを作成します。詳細については、元の記事を参照してください。

まず、Visual Studio 2005 で新しいプロジェクトを作成します。[新しいプロジェクト] ダイアログ ボックスの [プロジェクトの種類] ペインで、[Office] ノードを展開し、[2007 アドイン] をクリックします。[テンプレート] ペインで、[Excel アドイン] をクリックします。新しいアドインに RibbonAddin という名前を付け、[OK] をクリックしてこれを作成します。(アドイン テンプレートでは、アドイン プロジェクトとセットアップ プロジェクトの両方が作成されます。ここでは、セットアップ プロジェクトは無視します。これはアドインの配置を容易にしますが、この説明では必要ありません。)

[プロジェクト] メニューの [新しい項目の追加] をクリックし、ダイアログ ボックスで [Ribbon サポート] をクリックします。[追加] をクリックして、既定の名前 Ribbon1.vb をそのまま使用します。テンプレートによって、Ribbon1.vb ファイルと、Ribbon1.xml という XML ファイルが追加されます。この Ribbon1.xml は後で変更します。[プロジェクト] メニューの [RibbonAddin Properties] (RibbonAddin のプロパティ) をクリックします。[リソース] タブをクリックし、ソリューション エクスプローラから Ribbon1.xml を [リソース] デザイン画面にドラッグします。[リソース] ウィンドウを閉じて、リソースを保存します。

ソリューション エクスプローラで、Ribbon1.xml をダブルクリックして内容を表示します。既定のマークアップ (図 1 を参照) では、[AddIns] (アドイン) タブに [マイ グループ] という新しいグループが作成されます。このグループには "マイ ボタン" というトグル ボタンが 1 つ含まれます。このボタンをオンにすると、OnToggleButton1 コールバック プロシージャが呼び出されます。

Figure 1 Ribbon1.xml (Close Figure 1)
<customUI
  xmlns=http://schemas.microsoft.com/office/2006/01/customui
  onLoad=”OnLoad”>
  <ribbon>
    <tabs>
      <tab idMso=”TabAddIns”>
        <group id=”MyGroup”
               label=”My Group”>
          <toggleButton id=”toggleButton1”
                        size=”large”
                        label=”My Button”
                        screentip=”My Button Screentip”
                        onAction=”OnToggleButton1”
                        imageMso=”HappyFace” />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

ソリューション エクスプローラで、Ribbon1.vb ファイル内の ThisAddIn 部分クラスのコメントを外します。Ribbon1 クラスで、GetCustomUI プロシージャが新しいリソースの値を返すよう、次のように変更します。

Return My.Resources.Ribbon1

提供されている GetResourceText ヘルパ プロシージャは、XML コンテンツを手動で開き、リーダーを使用してテキストを取得します。この時点で、このプロシージャを削除してもかまいません。

多くの場合、カスタマイズしたリボンの内部からホスト アプリケーションを参照できる必要があるため、Ribbon1 クラスに次の宣言を追加します。

Public Application As Excel.Application

この変数を初期化する必要もあります。そのため、ThisAddIn クラスの RequestService メソッドで、Ribbon1 クラスのインスタンスを作成するプロシージャの直後に次のコード行を追加します。

ribbon.Application = Application

コードの Ribbon Callbacks 領域を展開し、OnLoad コールバック プロシージャを確認します。

Public Sub OnLoad(ByVal ribbonUI As Office.IRibbonUI)
    Me.ribbon = ribbonUI
End Sub

このプロシージャは、Ribbon1 インスタンスに渡された Office.IRibbonUI 値を受け取り、これを格納してその後のコールバックもリボンと対話できるようにします。これが行われないと、項目のコンテンツを変更した後のリボンの更新など、重要な操作を実行できません。

また、XML マークアップによって作成されたトグル ボタンのオン/オフを処理する、サンプル コールバック プロシージャもあります。これは単純に、新規作成されたトグル ボタンのオン/オフを切り替えるたびに、それに応じた警告を表示します。

Public Sub OnToggleButton1( _
  ByVal control As Office.IRibbonControl, _
  ByVal isPressed As Boolean)
    If isPressed Then
        MessageBox.Show(“Pressed”)
    Else
        MessageBox.Show(“Released”)
    End If
End Sub

この説明では手順をわずかに変更していますが、元の記事でさらに詳細な説明を参照することもできます。ここでプロジェクトを保存および実行し、Visual Studio によって Excel 2007 が起動されることを確認します。[アドイン] タブをクリックし、[マイ ボタン] トグル ボタンをオンにしてからオフにします。どちらの場合にもアドインによって警告が表示されることを確認できます。

このコラムの以降の説明では、Ribbon1.xml のコンテンツを変更し、さらに複雑なコールバック プロシージャを Ribbon1 クラスに追加します。リボンでできることはおそらく無限にありますが、スペースに限りがあるため、この API のおもしろい使用法を 2 つだけ紹介します。

まず、使用可能な色のサブセットから成る独自のギャラリーを表示して、ユーザーがセルの背景を塗りつぶすことができるようにします。このために、独自の色ギャラリーを作成します。図 2 にその外観を示します。次に、図 2 の左上隅にあるようなチェック ボックスを使用して、独自の色ギャラリーを使用できるかどうかを制御する方法について説明します。

図 2 RibbonX を使用して独自の色ギャラリーを作成する
図 2 RibbonX を使用して独自の色ギャラリーを作成する

カスタム色ギャラリーを作成する

図 3 に示すように、ユーザーは Excel の FillColor ギャラリーから塗りつぶしの色を選択できますが、選択肢を一部の色セットに限定する必要がある場合もあります。これを行うには、独自の色ギャラリーを作成します。ところが、Excel の FillColor ギャラリーを見ると、図 2 のギャラリーよりも簡単にアクセスできることがわかります。Excel のギャラリーが SplitButton コントロールのクリック時に表示されるのに対し、独自に作成するギャラリーはメニューから表示する必要があります。分割ボタンの子として直接ギャラリーを作成できるとよいのですが、customui.xsd スキーマに基づくと、SplitButton コントロールに含めることができるのは、メニューとボタンまたはトグル ボタンだけです。作成するサンプルでは、単純なメニューからギャラリー コントロールを表示します。

図 3 Excel の FillColor ギャラリー
図 3 Excel の FillColor ギャラリー

作成したアドインの使用を開始するには、Ribbon1.xml のコンテンツを図 4 のマークアップに置き換えます。このコードを、このコラムのオンライン版からコピーせずに、実際に最初から入力してみることもためになります。RibbonX マークアップを作成する際に IntelliSense がどのように役立つかを知ることができます。このコードは、作成済みの onLoad プロシージャを起動時に呼び出すカスタマイズを作成します。さらに、このマークアップでは、"リボンのカスタマイズ" というテキストを表示する新しいタブ (demoTab) が作成されます。タブ内には "gallery Control" というグループが作成され、このグループ内に、ボタンとメニューが含まれた splitButton コントロールが作成されます。ボタンは、組み込みの CellFillColorPicker ID を使用してイメージを提供します。このコントロールの実際の ID は、既に説明した Microsoft サイトからダウンロードした、Excel のリボン コントロール ID が含まれているブックで調べました。ここで、アドインを保存および実行して、動作を確認します。

Figure 4 Ribbon1.xml の新しいコンテンツ (Close Figure 4)
<?xml version=”1.0” encoding=”utf-8” ?>
<customUI
   xmlns=http://schemas.microsoft.com/office/2006/01/customui
   onLoad=”onLoad”>
  <ribbon>
    <tabs>
      <tab id=”demoTab” label=”Ribbon Customization” >
        <group id=”galleryGroup” label=”gallery Control”>
            <splitButton id=”highlightSplitButton” >
              <button id=”highlightButton”
                imageMso=”CellFillColorPicker”/>
              <menu id=”highlightMenu” >
              </menu>
            </splitButton>
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

図 5 の要素を <menu> 要素の本体に挿入して、マークアップを引き続き変更します。このマークアップでは、ギャラリー コントロールを作成して、コントロールで使用可能なほぼすべてのコールバック プロシージャを使用します。まず、ラベルを指定し、コントロールで項目のラベルを表示しないよう指定します (この例では色だけを表示し、ラベルは表示しません)。また、ギャラリー内に表示する行と列の数を指定します。コールバック プロシージャを指定して、ギャラリーの残りのプロパティを指定します。Excel 2007 でリボンのカスタマイズをレンダリングする際、データを取得する必要に応じて、ここに記述されている各プロシージャが呼び出されます (たとえば、項目の幅を決定する必要が生じると GetItemWidth メソッドが呼び出されます)。さらに、ギャラリーの下部にボタンを表示するよう指定します。このボタンはいくつでも指定できますが、この例で必要なのは 1 つだけです。

Figure 5 ギャラリー コントロール (Close Figure 5)
<gallery id=”highlightGallery”
  label=”Select Color”
  showItemLabel=”false”
  columns=”5” rows=”3”
  onAction=”InsertFillColor”
  getEnabled=”GetEnabled”
  getItemCount=”GetItemCount”
  getItemHeight=”GetItemHeight”
  getItemWidth=”GetItemWidth”
  getItemImage=”GetItemImage”
  getItemScreentip=”GetItemScreenTip”>
  <button id=”noColorButton”
    label=”No color”
    onAction=”RemoveFillColor”
    screentip=”Remove color”
    supertip=”Remove any applied color.”/>
</gallery>

ギャラリー コントロールの動作を実装するため、ここで、マークアップ内に記述されている各コールバック プロシージャを作成する必要があります。そのためには、コード エディタ ウィンドウで Ribbon1.vb を開き、Ribbon Callbacks 領域を探します。コードはこの領域に配置しなくてもかまいませんが、こうすると読みやすくなります。ここからは、必要な各コールバック プロシージャの作成方法について説明します。

これらのコールバック プロシージャを必ずしも使う必要はありません。代わりに、ギャラリー コントロールのさまざまなプロパティに静的な値をそれぞれ指定することもできます。たとえば、マークアップで getItemHeight 属性を指定する代わりに、itemHeight 属性を固定値と共に指定できます。この例では、ギャラリーのレイアウトを動的に変更できるよう、これらのプロパティのそれぞれにコードを使用しています。

ギャラリーを表示するには、色の一覧が必要です。そこで、Ribbon1 クラスに次のコードを挿入します。

Private image As New Bitmap(25, 25)
Private colors As Color() = _
  {Color.Yellow, Color.GreenYellow, Color.Turquoise, _
   Color.Magenta, Color.Blue, Color.Red, Color.DarkBlue, _
   Color.Teal, Color.Green, Color.Violet, Color.DarkRed, _
   Color.YellowGreen, Color.Gray, Color.DarkGray, Color.Black}

このコードによって、クラス レベルの Bitmap インスタンスが作成されます。このインスタンスを使用して、さまざまなイメージを作成したり、さまざまな塗りつぶしの色を表す Color 値の配列を作成します。このコードおよび以降に示すコードは、ThisAddIn 部分クラスではなく、Ribbon1 クラスに挿入してください。

GetEnabled を作成する

Ribbon1.vb クラスに、次のコールバック プロシージャを追加します。このプロシージャは、この特定のコールバックに必要な署名を照合します。コールバックが正しい署名に一致しない場合、2007 Office system アプリケーションでこのプロシージャを呼び出すことはできません。

Public Function GetEnabled( _
       ByVal control As Office.IRibbonControl) As _
           Boolean

     Dim returnValue As Boolean = False
     Select Case control.Id
       Case “highlightGallery”
         returnValue = True
     End Select
     Return returnValue
   End Function

このコールバック プロシージャ (および他のすべてのコールバック プロシージャ) 用の署名を調べるには、前述の 3 つのうちの 3 番目の記事を参照してください。

2007 Office system は、コールバックをトリガしたコントロールへの参照を、パラメータとして GetEnabled メソッドに渡します。サンプル コードでは、その情報を使用し、呼び出し元の Id プロパティに基づいて Select Case ステートメントを作成します。このようなデザイン パターンを使用すると、同じコールバック プロシージャ署名を共有する任意のリボン コントロールで使用できる、単一の GetEnabled コールバック プロシージャを作成できます。ここでは、プロシージャは単に True を返します。これは、ギャラリーが有効にされることを示します。

GetItemCount を作成する

getItemCount コールバックを使用すると、ギャラリーに表示する項目の数を計算できます。表示する項目の数を指定する場合は、作成するギャラリーごとにこのコールバック プロシージャを指定する必要があります。アドインの Ribbon1 クラスに、次のプロシージャを挿入します。

Public Function GetItemCount( _
 ByVal control As Office.IRibbonControl) As Integer
  Dim returnValue As Integer = 0

  Select Case control.Id
    Case “highlightGallery”
      returnValue = 15
  End Select
  Return returnValue
End Function

先ほどと同様に、このコールバックは、トリガ元のコントロールへの参照を受け取り、ギャラリーに表示する項目の数を示すスカラ値を返します。

コールバックごとに Select Case ブロックを含め、呼び出し元コントロールの Id プロパティでプロシージャの動作を決定する手法が好ましくない場合、コールバックを共有する各コントロールに対して個別にコールバック プロシージャ作成することも、同じくらい簡単にできます。私はこのように動作を一元管理するのを好みますが、これは必須ではありません。

GetItemHeight および GetItemWidth を作成する

ギャラリー内の各項目の高さと幅をコードで指定します。各項目のサイズは同一でなければならず、このサイズは、作成済みの Bitmap インスタンスによって制御されます。したがって、正しいサイズを Excel に返すため、図 6 のコールバック プロシージャを Ribbon1 クラスに追加します。

Figure 6 GetItemHeight および GetItemWidth (Close Figure 6)
Public Function GetItemHeight( _
 ByVal control As Office.IRibbonControl) As Integer

  Dim returnValue As Integer = 0
  Select Case control.Id
    Case “highlightGallery”
      returnValue = image.Height
  End Select
  Return returnValue
End Function

Public Function GetItemWidth( _
 ByVal control As Office.IRibbonControl) As Integer

  Dim returnValue As Integer = 0
  Select Case control.Id
    Case “highlightGallery”
      returnValue = image.Width
  End Select
  Return returnValue
End Function

イメージを作成する

ディスクやリソース ファイルのイメージをギャラリーの項目ごとに読み込むことも簡単ですが、この例で必要とするのは単に色のブロックです。このため、GetItemImage コールバック プロシージャでは、ギャラリーの各項目に対して適切な純色のブロックを描画する必要があります。GetItemImage コールバック プロシージャは、コールバックをトリガしたコントロールへの参照だけでなく、返すイメージを示すインデックスも受け取ります。Ribbon1 クラスに、図 7 のコールバック プロシージャを挿入します。

Figure 7 GetItemImage (Close Figure 7)
Public Function GetItemImage( _
 ByVal control As Office.IRibbonControl, _
 ByVal index As Integer) As Object

  Select Case control.Id
    Case “highlightGallery”
      Using g As Graphics = Graphics.FromImage(image)
        Using brush As New SolidBrush(colors(index))
          g.DrawRectangle(Pens.Gray, _
            New Rectangle(0, 0, image.Width - 1, image.Height - 1))
          g.FillRectangle(brush, _
            New Rectangle(1, 1, image.Width - 2, image.Height - 2))
        End Using
      End Using
  End Select
  Return image
End Function

GetItemImage プロシージャは、作成済みの Bitmap から Graphics オブジェクトを作成します。また、色の配列のインデックスとして指定された値を使用して、新しい純色ブラシを作成します。コードにより、ビットマップよりも少し小さい灰色の四角形が描画され、その内側が適切な色で塗りつぶされます。

ヒントを作成し、アドインをテストする

Excel でイメージの上にマウスを重ねると、色の名前が表示されるようにします。各イメージに表示するテキストが GetItemScreenTip コールバック プロシージャによって要求されるため、必要なのは、それぞれの色の名前を返すことだけです。ここでは簡単に GetItemScreenTip を使用することにしましたが、実際は GetItemLabel を使用した方が、視覚障害を持つユーザーがギャラリー項目のラベルを取得できるようになります。ただし、showItemLabel 属性ではラベルが視覚的に表示されません。項目のヒントを明示的に指定しないと、ラベルが自動的にヒントとして使用されます。この動作を追加するため、Ribbon1 クラスに次のプロシージャを追加します。

Public Function GetItemScreenTip( _
 ByVal control As Office.IRibbonControl, _
 ByVal index As Integer) As String

  Dim returnValue As String = String.Empty
  Select Case (control.Id)
    Case “highlightGallery”
      returnValue = colors(index).Name
  End Select
  Return returnValue
End Function

これで、ギャラリーを表示するために必要なコールバック プロシージャの指定がすべて完了しました。プロジェクトを保存して実行します。Visual Studio によって Excel 2007 が読み込まれ、リボンの [リボンのカスタマイズ] タブをクリックすると splitButton コントロールが表示されます。ドロップダウン矢印をクリックし、メニュー項目をクリックすると、ギャラリーが表示されます。しかし、ギャラリー上のどのボタンを押してもエラーが返されます。これは、ギャラリーのボタン クリックを処理するコールバック プロシージャをまだ指定していないためです。アドインのこの部分を完成させるため、Excel を終了して Visual Studio に戻ります。

アクション コールバックを追加する

このカスタマイズのマークアップには、"アクション" コールバックが 2 つ含まれます。ギャラリー自体のための InsertFillColor と、ギャラリー下部のボタンのための RemoveFillColor です。ギャラリー上のボタンやギャラリー下部のボタンをクリックしたときに発生するエラーを回避するため、Ribbon1 クラスに次のコールバック プロシージャを追加します。

Public Sub InsertFillColor( _
 ByVal control As Office.IRibbonControl, _
 ByVal selectedId As String, ByVal index As Integer)

  CType(Application.Selection, Excel.Range).Interior.Color = _
        ColorTranslator.ToOle(colors(index))
End Sub

Public Sub RemoveFillColor( _
 ByVal control As Office.IRibbonControl)

  CType(Application.Selection, Excel.Range). _
       Interior.Color = Excel.Constants.xlNone
End Sub

これら 2 つのプロシージャは、異なる署名を使用します。最初のプロシージャはギャラリー コントロールの OnAction コールバックを処理し、2 番目のプロシージャはボタン コントロールの OnAction コールバックを処理します。ギャラリーの OnAction コールバックは、コントロールへの参照と、ギャラリー内で選択した項目のインデックスに加えて、ID が含まれた文字列も提供します。この selectedId パラメータは選択した項目の ID であり、コントロールの Id プロパティはギャラリー自体のプロパティです。

InsertFillColor と RemoveFillColor は、このギャラリーに固有のコールバック プロシージャです。そのため、これらのプロシージャ内ではあえて Select Case ブロックを使用していません。また、これら 2 つのプロシージャでは、Application.Selection 値が Excel.Range に変換され、Interior.Color プロパティには、選択した色または色なしのいずれかが設定されます。ColorTranslator.ToOle メソッドでは、System.Drawing.Color マネージ型の色が、VBA および Excel で認識可能な色に変換されます。

アドインをテストするため、もう一度プロジェクトを保存して実行します。ギャラリーの項目を選択すると、アドインにより、選択した色で選択範囲が塗りつぶされます。ギャラリー下部のボタンをクリックすると、選択範囲からすべての色が削除されます。

これで、使用できるさまざまなコールバックを利用した、単純なリボンのカスタマイズが作成できました。さて、ユーザー入力に基づいてギャラリーを有効または無効にするには、どうしたらよいでしょうか。たとえば、リボン上のチェック ボックス コントロールで、ギャラリーが使用できるかどうかを制御するには、どうしたらよいでしょうか。次は、やや複雑なこの機能を追加します。

リボン コントロールの対話操作を追加する

Windows アプリケーションを作成する場合、チェック ボックスで、他のコントロールの有効な状態を変更することは簡単です。適切なイベント ハンドラからコントロールの Enabled プロパティを設定すると、詳細はランタイム環境によって処理されます。ASP.NET を使用して Web アプリケーションを構築するときでも、コントロールの有効な状態を実際に変更する方法を細かく考慮する必要はありません。プロパティを設定するだけで、次にページの再描画を強制したときには、コントロールのレンダリングが ASP.NET ランタイムによって管理されます。

しかし、他の多くの環境とは異なり、リボン コントロールのプログラミングではコントロールのプロパティと直接対話することはできません。代わりに、ローカル変数の状態を設定し、プロパティを変更するコントロールを無効にしてから、コントロールのコールバック プロシージャにコントロールのレンダリングを要求することは可能です。

このような動作を実現するため、リボンのカスタマイズにチェック ボックス コントロールを追加し、チェック ボックスのオン/オフ状態でギャラリー コントロールの有効な状態が決定されるようにします。Ribbon1.xml ファイルで、既存の splitButton 要素の下にある既存のグループ要素内に、次のマークアップを追加します。

<checkBox id=”enableColorsCheckBox” label=”Enable Color Selection?”
  screentip=”Enable color selection”
  onAction=”HandlePressedAction”
  supertip=”Select to enable or disable color selection.”
  getPressed=”GetPressed”/>

onAction 属性では、ユーザーがチェック ボックスのオン/オフを切り替えたときに呼び出されるコールバック プロシージャを指定します。getPressed 属性では、リボンでチェック ボックスの値が初期化されたときに呼び出されるコールバックを指定します。

Ribbon1 クラスには、チェック ボックスの現在のオン/オフ状態を保持できる変数を追加します。

Private enableColors As Boolean = False

メソッドが単に True を返すのではなく enableColors 変数の値を返すように、既存の GetEnabled プロシージャを変更します。

returnValue = enableColors

enableColors 変数の値がチェック ボックス コントロールの状態に確実に反映されるよう、次のコールバック プロシージャを追加します。2007 Office system のリボンがチェック ボックス コントロールの状態を初期化するとき、このコードが呼び出されます。

Public Function GetPressed( _
 ByVal control As Office.IRibbonControl) As Boolean

  Dim returnValue As Boolean = True
  Select Case control.Id
    Case “enableColorsCheckBox”
      returnValue = enableColors
  End Select
  Return returnValue
End Function

最後に、ユーザーがチェック ボックスのオン/オフを切り替えたときの動作を処理する方法が必要です。enableColors 変数の値を設定すると共に、リボンでギャラリー コントロールを強制的に無効にします。コントロールを無効にすることにより、再初期化が強制され、コントロールの有効な状態が適切に設定されます。

Public Sub HandlePressedAction( _
 ByVal control As Office.IRibbonControl, _
 ByVal pressed As Boolean)

  Select Case control.Id
    Case “enableColorsCheckBox”
      enableColors = pressed
      ribbon.InvalidateControl(“highlightGallery”)
  End Select
End Sub

ギャラリー コントロールを無効にするコード行には、リボンの自動化に関する重要な点が 2 つ示されています。まず、アドインの初期化時に渡されたリボンへの参照が格納されていないと、以後のプロシージャでリボンとの対話操作を行うことができません。このため、リボンのコードで OnLoad コールバックを処理し、クラス レベルの変数に渡したリボンへの参照を格納することが重要になります。さらに、サンプル コードでは、IRibbonUI インターフェイスの InvalidateControl メソッドを使用しています。InvalidateControl メソッドを呼び出して、無効にする特定のコントロールを渡すことも、Invalidate メソッドを呼び出す (これによりリボン全体を無効にする) こともできます。どちらの場合も、コントロールのいずれかまたはすべてを再描画する必要がある事実をリボンに伝えるためには、これらのプロシージャのいずれかを呼び出す必要があります。

先ほどと同様に、プロジェクトを保存して実行します。Excel 2007 でリボンの [リボンのカスタマイズ] タブをクリックすると、既存の splitbutton コントロールに加えて、新しいチェック ボックスが表示されます。ここでギャラリーを表示しようとすると、メニュー項目が無効になっていることがわかります。チェック ボックスをオンにし、ギャラリーの表示をもう一度試みると、今度は使用できます。

リボン自体のアーキテクチャに基づいているため、ご覧のとおり、リボンの複数コントロール間の対話には想像以上の作業が必要です。リボンにその他のコントロールを追加する予定がなければ、コード サイズを小さくすることができます。コールバック プロシージャごとに Select Case ステートメントを使用することは、後でコードを拡張する際に役立ちますが、必須ではありません。

この手法は、リボン関連のさまざまなシナリオに使用できます。任意のコントロールのレイアウト、動作、またはコンテンツを、別のコントロールの "イベント" に基づいて変更する必要がある場合は、クラス レベルの変数を使用して状態を保持してから、コントロールまたはリボン全体を無効にします。

新しいユーザー インターフェイスは気に入らないと思っていましたが、自分が間違っていたことに気付きました。ユーザーに複数の選択肢を提示するために、リボンは最適の方法です。また、宣言モデルは、Office の従来の CommandBar モデルよりもはるかにプログラムが容易です。

ご意見やご質問は、

Ken basics@microsoft.com.

までお送りください。
Ken Getzは、MCW Technologies のシニア コンサルタントであり、AppDev のコースウェアの作成者でもあります。Ken は、『ASP.NET Developers Jumpstart』(英語) (Addison-Wesley、2002 年)、『Access Developer’s Handbook』(英語) (Sybex、2002 年)、および『VBA Developer’s Handbook, 2nd Edition』(英語) (Sybex、2001 年) の共著者です。連絡先は keng@mcwtech.com です。


Cutting Edge: トランザクション ワークフロー

Cutting Edge
トランザクション ワークフロー
Dino Esposito

コードのダウンロード : : CuttingEdge2007_06.exe (182KB)
コンテンツ
トランザクション タスクの分類
ワークフローにおける ACID トランザクション
TransactionScope アクティビティについて
例外用のトランザクション ワークフローの構成
Windows Workflow Foundation によるビジネス プロセスのモデル化
補正について
パフォーマンスとワークフロー トランザクション
まとめ

Microsoft® .NET Framework 3.0 の柱である Windows® Workflow Foundation は、.NET ベースのアプリケーションに組み込んだり、対象となる任意のクライアントにサービスとして公開したりすることができるワークフローを開発するためのプログラミング モデル、ランタイム エンジン、およびツールを提供します。では、カスタム ソリューションでワークフローが必要なのは、どのような場合でしょうか。

ワークフローは、ビジネス ロジックおよびルールの頻繁な変更を余儀なくされる場合に間違いなく役立ちます。このようなケースが生じるのは、同じアプリケーションをさまざまな顧客のニーズに合わせてカスタマイズしたり、単純にこの種の柔軟性を顧客から求められたりするからです。また、ワークフローをビジネス プロセス管理 (BPM) サーバーの背後に置き、分散型の複合アプリケーションのモジュールに公開されるビジネス サービスを統制することもできます。さらにワークフローは、企業にまたがるビジネス ロジックの、長期で任意な形式での実装をサポートするためにも役立ちます。

開発者から見た場合、ワークフローはその全体で目的の動作を表す、一連のアクティビティです。現実世界のビジネス ロジックをモデル化するためにワークフローを使用する場合は、必然的にトランザクション タスクを扱うことになります。では、Windows Workflow Foundation でトランザクション セマンティクスをコーディングするには、どうすればよいのでしょうか。

ほとんどのエンタープライズ システムは、特に複雑な問題に対処するために、内部にワークフローを保持しています。また、ほとんどの複雑な問題にはトランザクションが含まれます。処理するトランザクションは大きく分けて 2 種類あります。1 つは、従来的な短期のトランザクションであり、一般に ACID トランザクションと呼ばれます。もう 1 つはグローバルと称される、長期的な、ビジネス全体にまたがるトランザクションです。この種のトランザクションは、ビジネス プロセスのコンポーネントであることが多く、複数の ACID トランザクションで構成されることがあります。これらの ACID トランザクションの成功または失敗は、ワークフローによって表現される、より大きなビジネス プロセスの全体的な結果に影響します。

Windows Workflow Foundation は、ACID トランザクションとビジネス全体にまたがるトランザクションの実装を効果的にするだけでなく、比較的簡単にコーディングおよび更新できるようにする、アドホック アクティビティを提供します。このコラムでは、Windows Workflow Foundation ワークフローに組み込むことができるあらゆる種類のトランザクション タスクを提供する、一連のアクティビティについて取り上げます。

トランザクション タスクの分類

Windows Workflow Foundation ワークフローには、アトミック タスクとして認識されるアクティビティのブロックを 1 つ以上含めることができます。そのようなアクティビティを最も外側のトランザクション ブロックで一度ラップすると、Windows Workflow Foundation ランタイムにより、包含されているすべてのアクティビティが成功、失敗のどちらかであることが保証されます。そのようなトランザクションは、アトミックで (atomic)、一貫性があり (consistent)、独立していて (isolated)、永続性がある (durable) と想定されるため、ACID トランザクションと呼ばれます (図 1)。

Figure 1 ACID トランザクションの動作 (Close Figure 1)

機能 | 説明
アトミック | トランザクションのすべての操作が適切に完了するか、どの操作も完了しないかのいずれかです。
一貫性 | トランザクションに含まれるリソースは、トランザクションの開始時と終了時に適正な状態である必要があります。トランザクションは、整合性の制約やビジネス ルールに違反することはできません。
独立性 | トランザクション内の操作は、他のすべての操作から独立します。トランザクション外部からの操作では、データの中間的な状態を確認することはできません。
永続性 | トランザクションが適切に完了すると、その効果は永続化され、取り消すことはできません。

Windows Workflow Foundation ワークフロー全体を、1 つの長期トランザクション (LRT) として扱うこともできます。この場合は、ワークフロー セマンティクスを使用して、複数のサービス、異なるソフトウェア プラットフォーム、およびさまざまな企業が含まれるビジネス プロセスまたはその一部を記述します。LRT トランザクションは、結果がわかるまでに数分、数日、または数週間かかることがあります。プロセスは複数の手順からなる操作を実装し、複数の企業の情報システムにまたがることがあります。通常、LRT トランザクションは、個別に成功または失敗する 1 つ以上の ACID トランザクションで構成されます。しかし、プロセスが長くて複雑なため、プロセス内の後ろの手順が、前の ACID トランザクションの結果と矛盾する状態を引き起こすことがあります。その場合、既にコミットされたトランザクションの結果をロールバックするには遅すぎますが、何らかの方法で影響を補正する必要があります。

"トランザクション" という用語は、一般に ACID プロセスと長期プロセスの両方を表すために使用されますが、これら 2 つの間には明確な違いがあります。トランザクションという用語は、通常は単一の作業単位として処理され扱われる、連続した操作を表します。この操作は通常は数秒で終了し、中断したり、ユーザー入力や人間によるその他の操作を漠然と長時間待機したりすることはありません。この操作は持続性を必要とせず、オール オア ナッシングの操作と見なされます。後にわかるように、典型的なトランザクション構成についての説明は、基本的に ACID トランザクションについての説明です。

では、長期トランザクションとはどのようなものでしょうか。それは長期にわたって続行し、要求に応じて中断したり、再開したりすることができる ACID トランザクションでしょうか。長期トランザクションは、トランザクションという用語をはるかに広義に解釈したトランザクションです。LRT は、ゆるやかに結合した独立システムを含む、1 つにまとまったアクションのセットです。

トランザクションによっては、長期にわたって実行される性質のために、ACID トランザクションとして管理するのは困難か、完全に非現実的な場合があります。ACID トランザクションでは、往々にして何らかの重要なデータをトランザクションの実行中にロックする必要があります。これは、数秒しかかからないトランザクションの場合は問題になりません。しかし、特定のビジネス トランザクションに含まれるリソースのいずれかを全プロセスの終了までロックしておくことができない場合、ACID パターンは適切ではありません。アクションと結果を統制するには、さまざまな操作を周辺のロジックと共に独立した操作として完了する必要があり、また必要に応じてエラーや障害を補正する必要があります。統制ロジックは、BMP サーバーとして機能する Windows Workflow Foundation ワークフローに組み込むか、最も外側のワークフローで使用される Windows Workflow Foundation 複合アクティビティで構築することができます。

ワークフローにおける ACID トランザクション

ACID トランザクションは、コミットまたはロールバックの決定が通常は数秒以内に行われる、短期のトランザクションです。ロールバック ロジックでは、実行中の操作と、その操作を構成するすべての中間手順をキャンセルできる必要があり、また前の操作の影響を補正する必要があります。ユーザーから見ると、何も起こらなかったかのように見えます。ACID トランザクションの完璧な例として、2 つ以上のテーブルに対してアトミックに機能するストアド プロシージャにコーディングされた典型的なデータベース トランザクションがあります。

ACID トランザクションは、ローカルにすることも、分散型にすることもできます。ローカル トランザクションは、接続しているデータベースなどの単一のリソースを含みます。分散トランザクションは、複数の異種リソースにまたがり、トランザクション処理モニタを必要とします。分散トランザクション コーディネータ (DTC) は、Microsoft Windows 2000 およびそれ以降にリリースされた Windows 用のトランザクション処理モニタです。

ワークフロー内にトランザクション セグメントを組み込むには、TransactionScope アクティビティを使用します (図 2)。トランザクションのスコープ内で作成されるすべてのアクティビティは、標準の ACID スキーマを実現する作業単位を形成します。すべての子アクティビティが正常に完了した場合、トランザクションはコミットされ、ワークフローは次へ進みます。いずれかの子タスクから例外がスローされると、TransactionScope アクティビティはロールバック操作を実行します。

図 2 Visual Studio 2005 の TransactionScope アクティビティ
図 2 Visual Studio 2005 の TransactionScope アクティビティ(画像を縮小するには、ここをクリックします)
図 2 Visual Studio 2005 の TransactionScope アクティビティ
図 2 Visual Studio 2005 の TransactionScope アクティビティ (画像を拡大するには、ここをクリックします)

図 2 に示すサンプル ワークフローは、注文番号を受け取り、支払いに進みます。支払金額がまず顧客の口座から引き出され、ベンダの口座に追加されます。この操作はアトミック、つまり両方の手順が正常に完了するか、両方とも失敗するかのどちらかである必要があります。いずれの場合も、関連リソースは、いかなる制約にも違反せず、かつデータの整合性が保証される、一貫した状態に保たれる必要があります。

内部の手順は、開発者とエンド ユーザーの双方にとって透過的です。状態が確定するまでは、どんな外部ソフトウェア コンポーネントも、トランザクションに関連するリソースの状態にアクセスしないことが理想的です (この制約は、パフォーマンス上の理由で、修正という相応な代償により緩和されることがあります)。したがって分離を実現するには、シリアル化可能な基盤トランザクションが必要です。これはデータベースおよび同様なリソース マネージャがサポートするトランザクションの中で、最も負荷の高いトランザクション タイプです。

TransactionScope アクティビティについて

Windows Workflow Foundation では、逐次的に実行され、ACID セマンティクスを完全に満たす子アクティビティのコンテナとして、TransactionScope アクティビティを使用します。トランザクション スコープ内で Prallel アクティビティを使用する場合、アクティビティはそのアクティビティ内では同時に実行されますが、スコープのその他の部分に対しては逐次的に実行されます。

TransactionScope アクティビティは、トランザクションを構成するために宣言的に使用できる 2 つのプロパティを公開します。それが IsolationLevel と TimeoutDuration です。IsolationLevel プロパティは、System.Transactions 名前空間で定義される列挙体である IsolationLevel 型です。Iトランザクションの分離レベルは、内部データに対して他のトランザクションが有するアクセスのレベルを、トランザクションが完了する前に決定します。図 3 に、最も一般的に使用されるレベルでの動作について詳しく示します。

Figure 3 TransactionScope によってサポートされる分離レベル (Close Figure 3)

レベル | 説明
READ UNCOMMITTED | 最も制約が緩いこのレベルは、他のトランザクションによって設定されたロックを無視します。他のトランザクションによってまだコミットされていない変更済みデータを読み取ることができます (ダーティ読み取り)。
READ COMMITTED | SQL Server の既定の分離レベルであるこのレベルは、ダーティ読み取りを防止します。しかし、トランザクションが現在のトランザクション内でデータを変更、挿入、または削除することは許容します。これにより、反復不可能な読み取りやファントム行が生じることがあります。
REPEATABLE READ | このレベルは、ダーティ読み取りを防ぎ、他のトランザクションが現在のトランザクションによって読み取られたデータを変更または削除できないようにします。新しいデータの挿入は可能です。
SERIALIZABLE | 最も制約が厳しいこのレベルは、トランザクションが完了するまでロックを保持します。トランザクションが完了するまで、読み取りや更新を行うことはできません。
SNAPSHOT | SQL Server 2005 のみで使用できるこのレベルは、トランザクション内で読み取られたデータが、他の同時トランザクションによって行われた変更を反映しないことを指定します。
CHAOS | このレベルの場合、より高度な分離レベルのトランザクションが保留している変更を上書きすることはできません。

IsolationLevel の既定値は Serializable です。この値は、最も安全で、かつ最も負荷の高いオプションであり、すべてのケースで厳密に要求されるわけではありません。シリアル化可能なトランザクションは、トランザクションの継続中にデータをロックし、同時に実行されている他のトランザクションが同じデータにアクセスできないようにします。しかし、TransactionScope アクティビティは、30 秒のタイムアウトを設定することにより、このオプションを緩和します。同時実行に関する問題に不安があり、なおかつトランザクションを SQL Server™ 2005 で実行する必要がある場合は、新しい Snapshot 分離レベルを選択できます。他の分離レベルと異なり、Snapshot はプログラムによる設定のみに制限されているわけではありません。この分離レベルは、次のステートメントを使用して管理的な構成を行うことにより、SQL Server 2005 データベースに適用することができます。

ALTER DATABASE <name> SET ALLOW_SNAPSHOT_ISOLATION ON

TransactionScope アクティビティは、.NET Framework 2.0 の TransactionScope クラスのインスタンスを中心として構築されるラッパーです。TransactionScope クラスは、これまでに設計されたクラスの中で最もプログラマにとってフレンドリーなクラスの 1 つです。すべてのものを TransactionScope オブジェクトにラップすれば、作業はほぼ終了です。後はオブジェクトがすべてを引き受けて処理します。このオブジェクトは、ローカル トランザクションと分散トランザクションのどちらが必要かを判断し、必要な分散リソースを確保し、さもなければローカル処理に進みます。コードがローカルでは実行できないポイントに達すると、必要に応じて DTC にエスカレーションします。

ITransaction インターフェイスを実装するトランザクションに任意のオブジェクトを参加させることができます。これには、すべての標準 ADO.NET 2.0 データ プロバイダと、Microsoft メッセージ キュー (MSMQ) が含まれます。

何らかのコードが TransactionScope オブジェクトの Complete メソッドを呼び出すとき、それはトランザクションのスコープ内ですべての操作が適切に完了したことを意味します。このメソッドは分散トランザクションを物理的に終了しないことに注意してください。TransactionScope の破棄でコミット操作がまだ発生するためです。ただし、Complete メソッドを呼び出した後は、分散トランザクションを使用することはできなくなります。

TransactionScope アクティビティを使用すると、Visual Studio® 2005 デザイナでアクティビティをグループ化するだけで、簡単にトランザクション タスクを作成できます。図 2 に示すアクティビティのコードを考えてみてください。重要なコードを図 4 に示します。

Figure 4 アクティビティのコード (Close Figure 4)
private void WithdrawFunds_ExecuteCode(object sender, EventArgs e)
{
    Console.WriteLine(“Funds withdrawn to process order: #” +
        orderNo.ToString());

    // Execute a SQL operation on ClientAccount
    SqlHelper.ExecuteNonQuery(conn,
        System.Data.CommandType.Text,
        “use bank update ClientAccount set Balance = Balance - 100”);
}

private void AddFunds_ToVendor_ExecuteCode(object sender, EventArgs e)
{
    Console.WriteLine(“Funds added to the vendor’s account for order #” +
        orderNo.ToString());

    // Execute a SQL operation on VendorAccount
    SqlHelper.ExecuteNonQuery(conn,
        System.Data.CommandType.Text,
        “use bank update VendorAccount set Balance = Balance + 100”);
}

このトランザクション コードには、3 つの Code アクティビティが含まれています。最初の Code アクティビティは、データベース コマンドを実行し、指定された金額を差し引くことによってクライアントの口座の残高を更新します。次に、同じ金額をベンダの口座の残高に追加します。基になる TransactionScope オブジェクトの性質により、データベースを容易に分散させることができます。

さらに重要なのは、資金の転送はアトミック操作である必要があるということです。実行フローが TransactionScope アクティビティの末尾に達すると、トランザクションは適切に完了してコミットされます。いずれかのポイントで例外が発生すると、トランザクションはすべての作業を自動的にロールバックします。これらすべての処理は、ワークフロー開発者には完全に透過的です。ワークフロー開発者は、補正やエラー処理について考える必要はありません。

サンプル コードは、図 2 の最後のアクティビティのコードが示しているように、注文番号が奇数の場合に例外をスローします。

void CheckConsistency_ExecuteCode(object sender, EventArgs e)
{
    if (orderNo % 2 > 0)
        throw new DiscontinuedProductException();
}

スローされる例外は、アプリケーションの特定の状態を記述するカスタム例外オブジェクトです。これだけで、基になる .NET TransactionScope オブジェクトのロールバック メカニズムがトリガされます。図 5 に、実行中のアプリケーションと、ロールバックの場合に返されるメッセージを示します。図 6 に、コミットされた成功トランザクションを示します。

図 6 コミットされた送金
図 6 コミットされた送金(画像を縮小するには、ここをクリックします)
図 6 コミットされた送金
図 6 コミットされた送金 (画像を拡大するには、ここをクリックします)
図 5 キャンセルされた送金
図 5 キャンセルされた送金(画像を縮小するには、ここをクリックします)
図 5 キャンセルされた送金
図 5 キャンセルされた送金 (画像を拡大するには、ここをクリックします)

ワークフローの周辺、特に TransactionScope アクティビティの周辺で発生した例外をキャッチする必要がある場合は、エラー ハンドラ ビューに切り替え、FaultHandler アクティビティを追加します。これにより、特定の例外タイプをキャッチするアクティビティを構成し、そのスコープに、例外を処理するために必要なアクティビティをいくつでも追加することが可能になります。これは ACID シナリオではあまり意味がありませんが、一般に、例外の処理中に別のトランザクションを実行することができます。エラー ハンドラの目的は、例外が発生したアクティビティの部分的かつ失敗した処理を取り消すことです。ただし、トランザクション スコープに関する限り、ロールバックは自動的に行われ、アドホック SQL コマンドを記述して以前のデータベース操作の影響を取り消す必要はありません。

例外用のトランザクション ワークフローの構成

TransactionScope アクティビティの使用に関しては、注意する必要のある制約がいくつかあります。まず、Suspend アクティビティを使用して、トランザクション内からワークフローを中断することはできません。さらに、TransactionScope アクティビティを別の TransactionScope アクティビティや ICompensatableActivity インターフェイスを実装する任意のアクティビティ内に入れ子にすることはできません。ICompensatableActivity インターフェイスを実装するアクティビティの例として、ツールボックス内の CompensatableTransactionScope アクティビティや CompensatableSequence アクティビティがあります。

トランザクション ワークフローには永続化サービスが必要です。そのようなサービスが使用できなければ、例外がスローされます。永続化サービスは、クライアント アプリケーションのワークフロー ランタイムで次のようにして登録します。

string conn = “...”;
workflowRuntime.AddService(
    new SqlWorkflowPersistenceService(conn));

SqlWorkflowPersistenceService クラスは既定の永続化サービスであり、SQL Server データベースに基づいています。永続化は、ACID トランザクションが完了したとき、またはワークフロー インスタンスがアイドル状態になるかプログラムによってアンロードされたときに発生します。ワークフロー ランタイム エンジンは、永続化サービスのメソッドを呼び出して、ワークフロー インスタンスの状態を保存します。ワークフロー ランタイム エンジンは、永続化をいつ、どのようにして実行するかを判断します。永続化サービスは、選択されたデータ ストアとの間で、実際にワークフローの状態を保存したり読み込んだりする処理を行います。

SqlWorkflowPersistenceService クラスによって使用されるデータベースは、インストール時には作成されませんが、後で使用できるようにスクリプトがクライアント マシン上にコピーされます。スクリプトのパスは次のとおりです。

%WINDOWS%\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL

データベースの既定の名前は変更可能です。しかし、子テーブルの構造を変更することはできません。別のデータベース レイアウトを使用するには、カスタム永続化サービスが必要です。カスタム永続化サービスの中心となるのは、WorkflowPersistenceService を継承するクラスです。

Windows Workflow Foundation によるビジネス プロセスのモデル化

ACID トランザクションは、現実世界のワークフローのごく一部にすぎません。通常は、1 つのワークフロー内で複数の ACID トランザクションを組み合わせて、特定のビジネス プロセスを表現します。そのため、あるトランザクション アクティビティが適切に完了しているのに、後で、同じワークフロー内の別なアクティビティで例外がスローされることがあります。この場合、自動ロールバックを行うことはできません。ロールバックはトランザクションのコンテキストで可能です。操作を追跡し、必要に応じてキャンセルするリソース マネージャの存在が背後にあるためです。しかし、ワークフローはトランザクション セマンティクスを完全にはサポートせず、トランザクション アクティビティをサポートするだけです。

Windows Workflow Foundation を使用してビジネス プロセスをモデル化する際、ワークフロー全体にさまざまなトランザクション ブロックを散りばめることがあります。Windows Workflow Foundation では、2 種類のトランザクション アクティビティを使用できます。それが ACID トランザクションと補正可能トランザクションです。ACID トランザクションは、TransactionScope アクティビティによって完全に表現されます。ACID トランザクションは、コミットされるかロールバックされるかのいずれかであり、結果を永続化します。しかし、データを長時間にわたってロックすることができず、過去のコミットをロールバックできるようにする必要がある大きなプロセスのコンテキストで、トランザクション タスクを実装する必要がある場合は、どうすればよいでしょうか。この場合は、CompensatableTransactionScope アクティビティという特殊な種類のトランザクション アクティビティが必要です。

CompensatableTransactionScope は補正メカニズムをサポートします。補正とは、以前の操作の影響を取り消したり、緩和したり、補正したりするために、何らかの時点で実行するロジックです。重要なのは、補正可能トランザクションには、いったんコミットされたらロールバックすることができない子 ACID トランザクションが含まれている可能性があるということです。しかし、以降にエラーが発生した場合は、それらのトランザクションの影響を何らかの方法で補正する必要があります。補正はロールバックに似ていますが、実行された作業を補正するために使用されるコードを開発者が記述する必要があります。すべてのトランザクション タスクを補正可能にする必要はありません。たとえば、ACID トランザクションで効果的に表現できるタスクには、補正は不要です。実際、TransactionScope アクティビティは補正をサポートしていません。

図 7 に、2 つの補正可能トランザクション スコープを使用するサンプル ワークフローを示します。このビジネス プロセスは、注文のライフサイクルをモデル化しています。注文が行われると、顧客のクレジット カードへの課金が行われ (Scope_ChargeCreditCard という名前のアクティビティ)、お金がベンダの銀行口座に送金されます (Scope_PayOrder という名前のアクティビティ)。どちらのアクティビティもトランザクションと補正に対応しています。たとえば、注文された商品が入手できなくなったために、ワークフロー内で後に例外が発生したとします。この時点で、ビジネス例外が発生し、それまでに行われた作業をすべて取り消す必要が生じます。コミットされたすべての ACID トランザクションと、トランザクションに対応していない一連のアクティビティの影響を補正する必要があります。それぞれの補正可能トランザクションまたは連続アクティビティには、作業のすべてまたは一部を取り消すことだけを目的とするロジックが含まれています。

図 7 補正可能トランザクション
図 7 補正可能トランザクション

Windows Workflow Foundation では、アクティビティのビューを補正ハンドラ ビューに切り替えることにより、各補正可能アクティビティの取り消しコードを定義します (図 8)。補正ハンドラ ビューでは、操作の影響を取り消すために実行する必要があるすべてのアクティビティを一覧表示します。

図 8 ワークフロー シーケンスへの 補正ロジックの追加
図 8 ワークフロー シーケンスへの 補正ロジックの追加(画像を縮小するには、ここをクリックします)
図 8 ワークフロー シーケンスへの 補正ロジックの追加
図 8 ワークフロー シーケンスへの 補正ロジックの追加 (画像を拡大するには、ここをクリックします)

補正の概念は、ロールバックの概念に似ています。しかし、ロールバックが純粋にトランザクション操作であるのに対し、補正はトランザクション操作と非トランザクション操作、どちらの一連の操作にも適用されます。図 7 と図 8 を比較すると、2 つの補正可能トランザクション スコープには、若干のユーザー インターフェイスの違いがあることがわかります。図 8 には、直接コードの代わりに補正コードがあります。

補正について

ここでの疑問は、なぜわざわざ補正を行うのかということです。自動ロールバックを備えた大規模な ACID トランザクションで十分なのではないでしょうか。ACID トランザクションが最も適しているのは、同じデータベース内または同じ情報システム内で操作が発生する場合です。また、ACID トランザクションは、操作がすばやく終了する場合にほぼ適しています。異なる企業やサービスが含まれる場合、ACID セマンティクスの観点でプロセスを定義するのは、往々にして困難です。そのようなプロセスを独立的で永続的なものにするには、タスクが継続している間、さまざまな企業のすべてのリソースをロックし続ける必要があります。これは、特にタスクが長期である場合、かなり非現実的です。プロセスを一貫したアトミックなものにするには、アドホック補正コードが必要です。

もう 1 つの疑問は、何によって補正コードを起動するかということです。エラー処理では、ワークフローによって発生した 1 つ以上の例外を処理することになります。そのような例外のハンドラは、Compensate アクティビティという名前の、もう 1 つの特別なアクティビティです (図 9)。Compensate アクティビティは、ビジネス例外によってリスクを伴うようになった制約およびビジネス ルールを修正するコードを起動します。Compensate アクティビティは、ワークフロー内の補正可能アクティビティにバインドする必要があります。それには、Visual Studio 2005 Workflow Extensions デザイナで TargetActivityName プロパティを使用します。TargetActivityName プロパティを特定のトランザクションまたはシーケンスの名前に設定すると、そのトランザクションまたはシーケンスだけが補正されます。複数の Compensate アクティビティを追加して、補正が発生する順序と精度を決めることができます。従来的な ACID トランザクションのロールバック ロジックを模した補正ロジックを実行するだけの場合は、Compensate アクティビティをワークフロー全体にバインドします。この場合、補正は例外が発生したポイントからワークフローのルートに向かって、ボトムアップ式に進行します。

図 9 Compensate アクティビティ
図 9 Compensate アクティビティ

ワークフローの実行結果のバランスをとるために、追加の ACID トランザクションが補正コードによって開始されることがあります。さらに、補正が失敗することもあります。したがって、安全を期すためには、補正コード用の適切な例外ハンドラも用意する必要があります。

パフォーマンスとワークフロー トランザクション

Windows Workflow Foundation ワークフローのパフォーマンスに最も大きな影響を与える要素の 1 つは、永続性です。ワークフローは、複数の永続ポイントを持つことができます。プログラマによって明示的に定義されるものや、組み込みのアクティビティとカスタム アクティビティによって暗黙的に要求されるものがあります。永続化サービスは、ワークフローがアイドル状態になったとき、ワークフロー インスタンスで Unload メソッドが呼び出されたとき、または PersistOnClose 属性で修飾されたアクティビティが作業を完了したときに、自動的に呼び出されます。Windows Workflow Foundation で定義されている組み込みのトランザクション アクティビティ (TransactionScope および CompensatableTransactionScope) は、いずれも終了時に永続化を必要とします。したがって、効果的な永続化サービスと、それ以上に、そのための効果的なランタイム環境が、パフォーマンスにとって重要です。

トランザクション ワークフローでは、ランタイム マシンに関して 2 つの要件があります。それは、永続化とトランザクションのサポートです。既定では、.NET TransactionScope クラスを介してトランザクションを管理するために、DefaultWorkflowCommitWorkBatchService サービス クラスが使用されます。この方法では、必要な場合にのみ、DTC への動的なエスカレーションが暗黙的に提供されます。しかし、パフォーマンス上の理由により、既定のサービスの使用を避けるのが望ましい特定のシナリオがあります。

既製の永続化サービスを使用するときは、適切なデータベース (SQL Server 2005 または SQL Server 2000) を作成する必要があります。標準のデータベースには、標準の追跡サービスである SqlTrackingService クラス用のテーブルとストアド プロシージャも含まれています。両方のサービスが有効になっており、同じデータベースを使用する、つまりまったく同じ接続文字列を使用するシナリオを考えてみてください。データの永続化と追跡は、常に同じトランザクション内に記述されます。しかし、データベースが SQL Server 2000 の場合は、これらのサービスが同じ接続オブジェクトを共有するようにしない限り、DTC へのエスカレーションが発生します。したがって、データの永続化と追跡を同じ SQL Server 2000 データベースに対して実行するときは、既定のサービスではなく、SharedConnectionWorkflowCommitWorkBatchService を使用する必要があります。これらのサービスをワークフロー ランタイムに追加するには、次のようにします。

workflowRuntime.AddService(
    new SqlWorkflowPersistenceService(connString));
workflowRuntime.AddService(
    new SqlTrackingService(connString));
workflowRuntime.AddService(
    new SharedConnectionWorkflowCommitWorkBatchService(connString));

SharedConnectionWorkflowCommitWorkBatchService サービスは、この特定のシナリオに限って、ワークフローのパフォーマンスを最適化します。これにより、データベースおよび DTC トランザクションに追加の接続を行うオーバーヘッドが回避されます。トランザクション ワークフローで追跡が不要な場合や、追跡と永続化に個別のデータベースを使用する場合は、トランザクション サポートの既定サービスを利用するほうが無難です。

補足になりますが、追跡サービスと永続化サービスは、常に同じトランザクション内で動作します。追跡サービスには、IsTransactional という名前のブール型プロパティがあります。このプロパティを false に設定しても、追跡サービスがトランザクションに対応しなくなるわけではありません。より端的に言うと、追跡しているデータは、対応するメソッドが呼び出されたときに保存されるようになります。IsTransactional が true の場合 (既定の設定)、サービスの TrackData メソッドは、データを作業バッチに追加します。作業バッチは、ワークフロー内の次の永続ポイントでフラッシュされます。

まとめ

Windows Workflow Foundation でのトランザクション タスクのモデル化は、実際には非常に簡単です。応答をすばやく生成する ACID トランザクション (アトミックで、一貫していて、独立していて、永続的なトランザクション) が必要な場合は、TransactionScope アクティビティを使用することをお勧めします。このアクティビティ内に、基になるアンビエント トランザクションに参加するオブジェクトにアクセスする、他のアクティビティを作成します。一方、多数の疎結合サービスを統制して完了に時間のかかるタスクをモデル化する必要がある場合は、CompensatableTransactionScope を使用することをお勧めします。

また、トランザクションは永続化とメモリの必要量という要件により、固有の代償をもたらすということを覚えておく必要があります。補正のみが必要な場合は、軽量な CompensatableSequence アクティビティを選択することをお勧めします。

ご意見やご感想は、

Dino cutting@microsoft.com.

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


サービス ステーション: WCF アドレス指定の詳細

サービス ステーション
WCF アドレス指定の詳細
Aaron Skonnard

コンテンツ
エンドポイントとアドレス
アドレス指定の基本
IIS アドレス指定の考慮事項
複数のエンドポイントと一意のアドレス
物理アドレスと論理アドレス
アドレス指定ヘッダー
メッセージ フィルタ
ホスト名の比較
まとめ

Windows Communication Foundation には、サービス エンドポイントを公開し、さまざまな通信プロトコルを介してそれらのエンドポイントと通信するための柔軟なモデルが用意されています。今月は、エンドポイント通信にかかわるアドレス指定の詳細について説明します。これらの詳細の多くは、より高度なメッセージング シナリオを可能にします。

エンドポイントとアドレス

Windows® Communication Foundation のアーキテクチャでは、物理的通信の詳細が基になるサービスの実装から明確に分離されています。開発者は、使い慣れた Microsoft® .NET Framework コードを使用して、サービス コントラクトの実装およびサービス ビヘイビアの定義を行うことに多くの時間を費やしています。実装が完了したら、サービスをホストする方法とサービスをコンシューマに公開する方法を決定できます。

特定のサービスをホストするには、ServiceHost インスタンスを作成し、サービス エンドポイントのコレクションを定義します。サービス エンドポイントでは、通信に使用するアドレス、バインド、およびコントラクトを指定します。この情報は、Windows Communication Foundation が必要なメッセージング ランタイム (チャネル スタックとも呼ばれます) を構築し、外部のコンシューマの要求に応じてメタデータを Web Services Description Language (WSDL) 形式で提供する際に必要になります。

サービス エンドポイントを ServiceHost インスタンスに提供するには、コードを作成するか (ServiceEndpoint クラスを参照)、または <system.serviceModel> 構成セクションにエントリを追加します (<endpoint> 要素を参照)。コードで実現できることは構成でも実現できます。その逆も同様です。ただし、構成を使用してエンドポイントを提供すると、一般に開発後の変更作業の柔軟性が向上します。

ServiceHost を開く前に、少なくと 1 つのサービス エンドポイントを ServiceHost に含める必要があります。エンドポイントを 1 つも含まないサービスはあまり役立ちません。サービスと対話するための手段がないからです。ただし、サービスでは、複数のエンドポイントを公開することができます。これにより、機能または制約が大幅に異なる複数のコントラクトやさまざまな種類のコンシューマに対応することができます。

エンドポイントに関する説明では、一般にバインドやコントラクトに重点が置かれ、エンドポイント アドレスやそれにかかわるさまざまな問題については見落とされがちです。今月の目標は、アドレスを指定する際に使用できるさまざまな手法を明らかにすることにより、こうした傾向に反旗をひるがえすことです。

Windows Communication Foundation のプログラミング モデルの詳細、およびコードまたはさまざまな構成要素でエンドポイントを操作する方法については、MSDN® Magazine の 2006 年 2 月号の私の記事「Windows Communication Foundation プログラミングの ABC」を参照してください。

アドレス指定の基本

サービス エンドポイントを定義する場合、さまざまなアドレス指定オプションを使用できます。IP アドレスやホスト名を指定できます。また、ポート番号を指定したり、トランスポート用の既定のポートを使用したりできます。さらに、特定のパスをアドレス (host/port の後のアドレス部分) に含めることももちろん可能です。

Windows Communication Foundation でアドレスを指定する場合、2 つの基本的な手法を使用できます。各エンドポイントの絶対アドレスを指定するか、または ServiceHost にベース アドレスを提供してから各エンドポイントの相対パスを指定できます。理解しやすいのは絶対アドレスを指定する方法ですが、ベース アドレスを使用すると管理が容易になります。

絶対アドレスを指定するには、図 1 に示すように、完全修飾アドレスをエンドポイント定義で指定します。この例では、3 つの絶対アドレスを指定しています。ただし、2 つの HTTP アドレスは、http://localhost:8080/calcservice という同一のベース アドレスを共有します。この状況では、ホストにベース アドレスを提供してから、相対アドレスを使用して個々のエンドポイントを定義することもできます。

Figure 1 エンドポイントを構成する (Close Figure 1)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”>
        <endpoint address=”http://localhost:8080/calcservice”
                  binding=”basicHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”http://localhost:8080/calcservice/secure”
                  binding=”wsHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”net.tcp://localhost:8081/calcservice”
                  binding=”netTcpBinding”
                  contract=”ISimpleMath”/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

ServiceHost オブジェクトを作成する場合、次に示すように、トランスポートごとに 1 つのベース アドレスを指定できます。

ServiceHost host = new ServiceHost(typeof(CalculatorService),
    // base HTTP address
    new Uri(“http://localhost:8080/calcservice”),
    // base TCP address
    new Uri(“net.tcp://localhost:8081/calcservice”));

ベース アドレスとエンドポイント自体を構成ファイルで指定することもできます。これを行うには、次に示すように、各 <service> の <host> 要素内にベース アドレスの一覧を記述します。

<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”>
        <host>
          <baseAddresses>
            <add baseAddress=”http://localhost:8080/calcservice”/>
            <add baseAddress=”net.tcp://localhost:8081/calcservice”/>
          </baseAddresses>
        </host>
        <!-- endpoint definitions follow -->
        ...

コードで指定しても構成で指定しても同じ結果が得られます。つまり、使用中の各トランスポートのベース アドレスを持つ ServiceHost オブジェクトが構成されます。このようにして ServiceHost を構成した場合、図 2 に示すように、エンドポイントのアドレスを定義する際に相対 URI を使用できます。ServiceHost を開くと、各エンドポイントに使用する絶対アドレスが ServiceHost によって決定されます。ServiceHost は、対応するベース アドレス (バインドのトランスポートに一致するアドレス) に対して相対アドレスを解決することで絶対アドレスを決定します。

Figure 2 相対 URI を使用する (Close Figure 2)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”>
        <endpoint binding=”basicHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”secure”
                  binding=”wsHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint binding=”netTcpBinding”
                  contract=”ISimpleMath”/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

最初のエンドポイントと最後のエンドポイントの "address" 属性が省略されていることに注意してください。これは、"address" 属性を空にすることと同じです。そのため、これらの各エンドポイントでは、対応するベース アドレスがエンドポイント アドレスとして使用されます。最初のエンドポイントではベースの HTTP アドレスが使用され、最後のエンドポイントではベースの TCP アドレスが使用されます。2 番目のエンドポイントでは、"secure" の相対アドレスが使用されます。このアドレスは、ベースの HTTP アドレスに対して解決されます (http://localhost:8080/calcservice/secure になります)。

ベース アドレスを使用する場合でも、必要に応じてエンドポイントに絶対アドレスを指定することができます。この絶対アドレスは、対応するベース アドレスと一致する必要はまったくありません。各エンドポイントでは、必要に応じてまったく異なる場所を指定できます。

ベース アドレス手法は、エンドポイントの場所を変更する際に変更箇所を減らすための便利な手段として主に使用されます。また、GET 取得が有効になっている場合、Windows Communication Foundation では、既定でベース HTTP アドレスを使用して (<serviceMetadata> ビヘイビアを介して) メタデータが公開されます。ただし、取得場所は、このビヘイビアの httpGetUrl プロパティで変更できます。

クライアントは、サービスのベース アドレスを認識しません。また、同様のアドレスをクライアント側でサポートする必要もありません。そのため、クライアント側のオブジェクト モデルや <client> 構成セクションには、ベース アドレスに関連する項目がありません。クライアントは、常に絶対アドレスで構成される特定のエンドポイントを選択するだけです。このエンドポイントの絶対アドレスにより、クライアントが送信時に使用するアドレスが決定されます。

これらのアドレス指定の基本は、Windows Communication Foundation サービスを自己ホストする (ServiceHost インスタンスを自分で作成および管理する) 場合に適用されます。ただし、サービスを IIS でホストする場合は、注意すべき特別な考慮事項がいくつかあります。

IIS アドレス指定の考慮事項

IIS でホストする場合、ServiceHost インスタンスの作成または管理について考慮する必要はありません。ServiceHost インスタンスの作成および管理は、IIS によって自動的に行われます。開発者が行う必要があるのは、.svc エンドポイントをサービス クラスにマップし、サービスのエンドポイントとビヘイビアを web.config で定義して、Windows Communication Foundation が実行時に ServiceHost インスタンスの作成および構成を管理できるようにすることだけです。

このホスト シナリオでは、サービスを格納する IIS 仮想ディレクトリにより、ベース HTTP アドレスと .svc ファイルの名前が決定されます。ベース アドレスは、IIS のアドレス指定スキームによって完全に制御されます。したがって、ベース アドレスに関して開発者が行うことは何もありません。そのため、このシナリオでは、開発者が web.config で指定したベース アドレスは、Windows Communication Foundation によって適宜無視されます。エンドポイントのベース アドレスを変更する場合は、サービスを別の IIS 仮想ディレクトリに移動する必要があります。

IIS は、ベース アドレスを制御するだけでなく、(自己ホストとは異なり) すべてのエンドポイントが同じベース アドレスを使用するように強制します。つまり、特定のエンドポイントに絶対アドレスを指定する場合、その絶対アドレスは仮想ディレクトリに対応するベース アドレスで始める必要があります。そうしないと例外が発生します。したがって、IIS でホストする場合は、必ず相対アドレスを指定する必要があります。特定の .svc サービスに対して複数のエンドポイントを公開する場合は、相対アドレスを使用する必要があります (複数のエンドポイントの公開が必要になる理由については後で簡単に説明します)。

例を見てみましょう。calc.svc という名前のファイルがあり、http://localhost:8080/calcservice に対応する仮想ディレクトリにそのファイルが格納されているとします。このサービスのベース アドレスは http://localhost:8080/calcservice/calc.svc になります。HTTP GET メタデータ取得を有効にしている場合、ヘルプ ページがこれと同じアドレスで公開されます。このヘルプ ページは、Internet Explorer で簡単に閲覧できます。

次に、仮想ディレクトリの web.config ファイルに記述されているエンドポイント構成 (図 3) について考えてみましょう。この場合、最初のエンドポイントのアドレスはベース アドレス (http://localhost:8080/calcservice/calc.svc) と同じになります。これは、エンドポイントのアドレスを空にしたためです。2 番目のエンドポイントのアドレスは、ベース アドレスに "secure" を追加したもの (http://localhost:8080/calcservice/calc.svc/secure) になります。"mex" エンドポイントのアドレスは http://localhost:8080/calcservice/calc.svc/mex になります。アドレスの相対部分がファイル名の右側に追加されていることを不思議に思う人もいるかもしれませんが、calc.svc がベース アドレスの一部であるためにこのような形式になるということを覚えておいてください。

Figure 3 IIS でホストされたサービス用の構成のサンプル (Close Figure 3)
<configuration>
  <system.serviceModel>
    <services>
        <service name=”CalculatorService”>
          <!-- base address determined by IIS virtual directory -->
          <endpoint binding=”basicHttpBinding”
                    contract=”ISimpleMath”/>
          <endpoint address=”secure”
                    binding=”wsHttpBinding”
                    contract=”ISimpleMath”/>
          <endpoint address=”mex”
                    binding=”mexHttpBinding”
                    contract=”IMetadataExchange”/>
        </service>
        ...

ここでのテーマは IIS ホストであるため、IIS 5.0 および IIS 6.0 では、各種の HTTP バインドのみをエンドポイントで使用できるということも指摘しておきます。つまり、IIS 5.0 または IIS 6.0 でホストされているサービスでは、TCP エンドポイントをホストできません。サポートされるのは HTTP エンドポイントのみです。この制限は、Windows Server (コードネーム "Longhorn") に付属している Windows Process Activation Services (WAS) によって解決されます。WAS では、上記の .svc 手法を用いて HTTP 以外のエンドポイントを IIS 内にホストすることができます。

複数のエンドポイントと一意のアドレス

いくつかの理由により、特定のサービスで複数のエンドポイントを公開する必要が生じることがあります。その 1 つは、いくつかの異なるバインドを使用して同じコントラクトを公開する場合です。たとえば、WS-I Basic Profile 1.1 準拠のサービスのみを処理できるコンシューマ (1 つめのバインド) や、完全な標準セットを処理できるコンシューマ (2 つめのバインド) が存在する場合があります。または、パフォーマンス上の理由のため、TCP バイナリ送信を要求するコンシューマ (3 つめのバインド) が企業内に存在する場合もあります。異なるバインドを使用して同じコントラクトを公開する機能により、これらすべてのコンシューマに同時に対応することができます。

異なるバインドを使用して複数のエンドポイントを公開する場合、各エンドポイント アドレスは一意である必要があります。これは、各エンドポイントで別々のトランスポート リスナとチャネル スタックが必要になるためです。図 4 のサービス構成について考えてみましょう。この例では、すべてのエンドポイントで同じコントラクト (IsimpleMath) が公開されています。ただし、各エンドポイントで別々のバインドが使用されているため、各アドレスは一意である必要があります。他のいずれかのエンドポイントと同じアドレスを使用するようにエンドポイントを変更した場合、Windows Communication Foundation は、ServiceHost を開いているときに例外をスローします。

Figure 4 一意のアドレスを使用した構成のサンプル (Close Figure 4)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”>
        <endpoint address=”http://localhost:8080/calcservice”
                  binding=”basicHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”http://localhost:8080/calcservice/secure”
                  binding=”wsHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”net.tcp://localhost:8081/calcservice”
                  binding=”netTcpBinding”
                  contract=”ISimpleMath”/>
      </service>
 ...

ただし、複数のエンドポイントで同じバインドを共有する場合は、これらのエンドポイントで同じアドレスを使用することができます。このアプローチは、同じバインドを使用して複数のコントラクトを公開する必要がある場合 (これは、単一のサービスで複数のエンドポイントが必要になるもう 1 つの大きな理由です) に非常に役立ちます。次の例は、同じアドレスを共有し、別々のコントラクトを公開している 2 つの定義済みエンドポイントを示しています。

<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”>
        <endpoint address=”http://localhost:8080/calcservice”
                  binding=”wsHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”http://localhost:8080/calcservice”
                  binding=”wsHttpBinding”
                  contract=”IScientific”/>
</service>
 ...

この場合、これらのエンドポイントは同じトランスポート リスナとチャネル スタックを共有することになります (別々のメソッドにディスパッチされます)。両方のエンドポイントのバインドに "wsHttpBinding" が指定されているため、Windows Communication Foundation は、これらのエンドポイントが共有する単一のバインド インスタンスを作成および使用できることを認識しています。

複数のエンドポイントをコードに手動で追加して、これらが同じアドレスを共有するようにする場合は、AddServiceEndpoint の呼び出し時に同じバインド インスタンスを使用する必要があります。次のコード例ではそうしていません。

ServiceHost host = new ServiceHost(typeof(CalculatorService));
host.AddServiceEndpoint(
    typeof(ISimpleMath), new WSHttpBinding(),
    “http://localhost:8080/calc”);
host.AddServiceEndpoint(
    typeof(IScientific), new WSHttpBinding(),
    “http://localhost:8080/calc”);
host.Open();
...

両方のエンドポイントで同じ種類のバインドが使用されるにもかかわらず、AddServiceEndpoint の呼び出し時に 2 つの異なるインスタンスが指定されています。このコードを実行しようとすると、AddServiceEndpoint の 2 回目の呼び出し時に、バインド インスタンスが特定のエンドポイント アドレスに既に関連付けられていることを示す例外が Windows Communication Foundation によってスローされます。代わりに、次に示すように、単一の WSHttpBinding インスタンスを作成して、両方の AddServiceEndpoint 呼び出しに渡します。

ServiceHost host = new ServiceHost(typeof(CalculatorService));
WSHttpBinding wsbinding = new WSHttpBinding();
host.AddServiceEndpoint(
    typeof(ISimpleMath), wsbinding, “http://localhost:8080/calc”);
host.AddServiceEndpoint(
    typeof(IScientific), wsbinding, “http://localhost:8080/calc”);
host.Open();
...

クライアントは、これらのエンドポイントが同じアドレスを共有していることについて留意する必要はありません。2 つの異なるエンドポイントが引き続きクライアントに提供され、クライアントはこれらのいずれかを選択する必要があります。この例では、svcutil.exe は、実際には各エンドポイント用に 2 つのプロキシ クラスを生成します。これは、各エンドポイントが異なるコントラクトを公開するためです。両方のプロキシ クラスのターゲットが同じアドレスになっているのはたまたまです。

物理アドレスと論理アドレス

Windows Communication Foundation では、実際には、各サービス エンドポイントに 2 つのアドレスが関連付けられます。1 つは論理アドレスで、もう 1 つは物理アドレスです。論理アドレスと物理アドレスの違いは、WS-Addressing における "To" と "Via" の違いと同じです。論理アドレス ("To") は、SOAP メッセージの送信先アドレスです。一方、物理アドレス ("Via") は、Windows Communication Foundation がメッセージの到着をリッスンする、実際のトランスポート固有のネットワーク アドレスです。オブジェクト モデル全体を通じて、Windows Communication Foundation では、論理アドレスは "Address" または "Endpoint Address" と呼ばれ、物理アドレスは "ListenUri" と呼ばれます。

Windows Communication Foundation チャネル インフラストラクチャは、特定の場所で特定の通信プロトコルを使用してメッセージを受信するものであるため、物理アドレスを中心に展開されています。一方、Windows Communication Foundation ディスパッチャには、このようなネットワーク詳細が隠蔽されます。代わりに、ディスパッチャは、受信メッセージをエンドポイントと (最終的には) メソッド呼び出しにマッピングすることに集中します。

これまでの説明に従ってエンドポイント アドレスを指定する場合、実際には論理アドレスを指定することになります。ただし、物理アドレスが指定されていない場合、この論理アドレスは物理アドレスとしても使用されます。次の例は、論理アドレスと物理アドレスの両方を含むさまざまなエンドポイント詳細を出力する方法を示しています。

...
foreach (ServiceEndpoint se in host.Description.Endpoints)
{
    Console.WriteLine(“Endpoint details:”);
    Console.WriteLine(“Logical address: \t{0}”, se.Address);
    Console.WriteLine(“Physical address: \t{0}”, se.ListenUri);
    Console.WriteLine(“Binding: \t{0}”, se.Binding.Name);
    Console.WriteLine(“Contract: \t{0}”, se.Contract.Name);
    Console.WriteLine();
}
...

このコードを上記の例に対して実行すると、両方のアドレスについて同じ値が出力されます。ほとんどの場合、論理アドレスと物理アドレスを異なるものにする必要はありません。ただし、異なる物理アドレスを指定する必要が生じることもあります。ListenUri は、エンドポイントを定義する際にコードまたは構成のいずれかで指定できます。ListenUri をコードで指定する方法を次に示します。

host.AddServiceEndpoint(
    typeof(ISimpleMath), wsbinding,
    “urn:calcservice:simplemath”, // logical
    new Uri(“http://localhost:8080/calc”)); // physical (listenUri)
host.AddServiceEndpoint(
    typeof(IScientific), wsbinding,
    “urn:calcservice:scientific”, // logical
    new Uri(“http://localhost:8080/calc”)); // physical (listenUri)

図 5 に示すように、listenUri 属性を使用して、構成で ListenUri を指定することもできます。このコードを実行してエンドポイント詳細を出力すると、論理アドレスの値と物理アドレスの値が異なっていることがわかります。この例では、両方のエンドポイントは同じ物理アドレス (この場合は http://localhost:8080/calcservice) を共有します。ただし、各エンドポイントには、URN 形式の一意の論理アドレスがあります。

Figure 5 ListenUri を指定する (Close Figure 5)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”>
        <endpoint address=”urn:calcservice:simplemath”
                  listenUri=”http://localhost:8080/calcservice”
                  binding=”wsHttpBinding”
                  contract=”ISimpleMath”/>
        <endpoint address=”urn:calcservice:scientific”
                  listenUri=”http://localhost:8080/calcservice”
                  binding=”wsHttpBinding”
                  contract=”IScientific”/>
      </service>
      ...

コンピュータの ListenUri が必ず一意になるようにするには、ListenUriMode プロパティの値を ListenUriMode.Unique に設定します。ListenUriMode プロパティの値を ListenUriMode.Unique に設定した場合、チャネル インフラストラクチャは、一意のポート番号を選択するか (ポート共有が無効になっている TCP の場合)、または ListenUri アドレスを使用する前に GUID を ListenUri アドレスの末尾に追加します。

ListenUri を指定する際には、相対アドレスも使用できるという点に注目する必要があります。相対アドレスを指定した場合、Windows Communication Foundation は、論理エンドポイント アドレスと同様に、対応するベース アドレスに対して相対アドレスを解決します。

ここで、異なる ListenUri アドレスで構成されたサービスとクライアントがどのようにして対話するのか疑問に思われるかもしれません。クライアントには ListenUri という概念がありません。クライアントは、エンドポイントごとに 1 つのアドレスを認識します。WSDL 定義には、各エンドポイントの論理アドレスが含まれています。そのため、svcutil.exe は、論理アドレスをクライアント構成に埋め込み、送信時に既定で使用します。

異なる物理アドレスを使用してサービスを構成した場合は、使用する物理アドレスをクライアントに通知するための帯域外メカニズムが必要になります。その後、クライアントは、メッセージを物理アドレス経由で送信するよう Windows Communication Foundation に指示できます。この場合、物理アドレスは、ルーターなどの中継機器と同様の役割を果たすことになります。ClientViaBehavior を使用すると、この処理を簡単に実現することができます。次に例を示します。

SimpleMathClient client = new SimpleMathClient(
    “WSHttpBinding_ISimpleMath”);
client.Endpoint.Behaviors.Add(new ClientViaBehavior(
        new Uri(“http://localhost:8080/calcservice”)));
double sum = client.Add(3, 4);

また、図 6 に示すように、クライアントの構成ファイル内で ClientViaBehavior を適用することもできます。クライアントは、"urn:calcservice:simplemath" にメッセージを送信する代わりに、サービス エンドポイントが使用する物理アドレスを介してメッセージを送信するようになります。メッセージが ListenUri に到着したら、ディスパッチャは、論理アドレス (URN) を調べて、使用するエンドポイントを決定できます。

Figure 6 構成で ClientViaBehavior を適用する (Close Figure 6)
<configuration>
  <system.serviceModel>
    <client>
      <endpoint address=”urn:calcservice:simplemath”
                binding=”wsHttpBinding”
                behaviorConfiguration=”Via”
                contract=”Client.localhost.ISimpleMath”
                name=”WSHttpBinding_ISimpleMath”/>
      ...
    </client>
    <behaviors>
      <endpointBehaviors>
        <behavior name=”Via”>
          <clientVia viaUri=”http://localhost:8080/calcservice”/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    ...

これらの手法の使用目的は何でしょうか。その答えは簡単で、ルーティングを行うためです。中継手段を介してメッセージを最終送信先に送信する場合は、Address と ListenUri の違いを理解し、これらを適切に使用する必要があります。ルーティングは、中央の "ゲートウェイ" を使用してすべての展開済みサービスに対する共通タスク (セキュリティ、ログの記録、統計など) を処理する特定の企業シナリオで頻繁に使用されます。

TcpTrace は、まさにこの種の中継手段です。唯一の違いは、開発者が診断目的で使用するということです。TcpTrace を使用して、サービスへの送信メッセージを途中ですべてインターセプトするとします。ただし、サービスのコードは変更しないものとします。TcpTrace のリッスン アドレスを指す ClientViaBehavior を使用してクライアントを構成することで、これを簡単に実現できます。

クライアントで ClientViaBehavior を使用せずに同じことを実現できる別の方法もあります。サービス エンドポイントを定義する際に、TcpTrace がリッスンするアドレスを論理アドレスとして使用できます (このアドレスは WSDL に表示されるため、クライアントが TcpTrace のリッスン アドレスにメッセージを送信するようになります)。その後、エンドポイントに別の物理アドレス (ListenUri) を指定して、そのアドレスに転送するよう TcpTrace を構成します。

この例では、TcpTrace は、他の中継手段と同様に、単純なルーティングを実行します。より高度なカスタム ルーティング シナリオも簡単に思い浮かぶでしょう。

アドレス指定ヘッダー

より高度なルーティングとディスパッチのロジックに対応する必要がある場合は、カスタム アドレス指定ヘッダーを SOAP メッセージに追加できます。カスタム アドレス指定ヘッダーは、従来の WS-Addressing ヘッダーと共に、SOAP メッセージの一部として送信されます。たとえば、受信メッセージに <premium> メンバシップ ヘッダーが含まれる場合は、豊富な機能を備えたエンドポイントにメッセージをディスパッチします。受信メッセージに <basic> ヘッダーが含まれる場合は、基本的な機能を備えたエンドポイントにメッセージをディスパッチします。

この情報をサービス コントラクトの一部として指定し、メッセージ本文に情報が表示されるようにしたほうが良いと思われるかもしれません。問題は、コンシューマが必要な情報を認識しない、または持たない場合があるということです。代わりに、途中の中継手段からエンドポイントに情報を提供する必要があります。たとえば、中継手段で、指定されたメンバ ID を検索し、メンバシップ レベルを確認して、適切なヘッダーを送信メッセージに追加します。

カスタム アドレス指定ヘッダーをエンドポイントに関連付けることで、追加のディスパッチ条件を定義できます。追加のディスパッチ条件を定義した場合、必要なアドレス指定ヘッダーを含むメッセージのみが所定のエンドポイントにディスパッチされます。

クライアントが必要なヘッダーを認識する必要がある場合は、サービスの WSDL 定義にこれらも含める必要があります。通常、アドレス指定ヘッダーは、<EndpointReference> 要素内に参照パラメータとして記述します (<EndpointReference> は WSDL の <port> 要素内に配置します)。

これは少し複雑に思えるかもしれませんが、Windows Communication Foundation では、アドレス指定ヘッダーを使用して簡単に行うことができます。図 7 のサービス構成について考えてみましょう。この例では、同じ論理/物理アドレスを共有し、別々のコントラクトを公開する 2 つのエンドポイントが定義されています。また、各エンドポイントには、特定のアドレス指定ヘッダーが必要です。最初のエンドポイントは <basic> ヘッダーを必要とし、2 番目のエンドポイントは <premium> ヘッダーを必要とします。これらのアドレス指定ヘッダーのいずれかを含む受信メッセージのみが、これらのエンドポイントのいずれかにディスパッチされます。

Figure 7 カスタム アドレス指定ヘッダーが指定されたエンドポイント (Close Figure 7)
<configuration>
  <system.serviceModel>
    <services>
      <service name=”CalculatorService”
               behaviorConfiguration=”metadata”>
        <host>
          <baseAddresses>
            <add baseAddress=”http://localhost:8080/calcservice”/>
          </baseAddresses>
        </host>
        <endpoint binding=”wsHttpBinding” contract=”ISimpleMath”>
          <headers>
            <basic xmlns=”http://example.org/level”/>
          </headers>
        </endpoint>
        <endpoint binding=”wsHttpBinding” contract=”IScientific”>
          <headers>
            <premium xmlns=”http://example.org/level”/>
          </headers>
        </endpoint>
      </service>
 ...

アドレス指定ヘッダーはコードで指定することもできます。これを行うには、AddressHeader オブジェクトを手動で作成して、EndpointAddress オブジェクトに提供します。その後、EndpointAddress を基に新しい ServiceEndpoint を作成して、ホストの Endpoint コレクションに追加できます。次に例を示します。

ServiceHost host = new ServiceHost(typeof(CalculatorService));
AddressHeader header =
    AddressHeader.CreateAddressHeader(“basic”,
       “http://example.org/level”, null);
EndpointAddress ea = new EndpointAddress(
    new Uri(“http://localhost:8080/calcservice/foobar”), header);

host.Description.Endpoints.Add(
    new ServiceEndpoint(
        ContractDescription.GetContract(typeof(ISimpleMath)),
        new WSHttpBinding(), ea));
...

WSDL でこのエンドポイント構成を調べると、<service> 要素内に詳細が見つかります (図 8 を参照)。<port> 要素には <wsa:EndpointReference> 要素が含まれており、<wsa:EndpointReference> 要素には必要なヘッダーが参照パラメータとして含まれています。svcutil.exe を使用して WSDL を実行すると、同じ <headers> 要素を含む、類似したクライアント側構成が生成されます。その後でこのエンドポイントを使用すると、図 9 に示すように、Windows Communication Foundation によって必要な <basic> ヘッダーが送信メッセージに自動的に追加されます。

Figure 9 SOAP で送信されるカスタム アドレス指定ヘッダー (Close Figure 9)
<s:Envelope xmlns:s=”http://www.w3.org/2003/05/soap-envelope”
            xmlns:a=”http://www.w3.org/2005/08/addressing”>
  <s:Header>
    <a:Action s:mustUnderstand=”1”
     >http://example.org/calc/Add</a:Action>
    <a:MessageID
     >urn:uuid:db1ba7a6-ca2e-494b-b390-9f621e8b90f4</a:MessageID>
    <a:ReplyTo>
      <a:Address
       >http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <a:To s:mustUnderstand=”1”>http://localhost:8080/calcservice</a:To>
    <basic a:IsReferenceParameter=”true”
      xmlns=”http://example.org/level”/>
  </s:Header>
  <s:Body>
  ...
Figure 8 WSDL 内のカスタム アドレス指定ヘッダー (Close Figure 8)
...
<wsdl:service name=”CalculatorService”>
  <wsdl:port name=”WSHttpBinding_ISimpleMath”
             binding=”tns:WSHttpBinding_ISimpleMath”>
    <soap12:address location=”http://localhost:8080/calcservice”/>
    <wsa10:EndpointReference>
      <wsa10:Address>http://localhost:8080/calcservice</wsa10:Address>
      <wsa10:ReferenceParameters>
        <basic xmlns=”http://example.org/level”/>
      </wsa10:ReferenceParameters>
      ...
    </wsa10:EndpointReference>
  </wsdl:port>
  ...
</wsdl:service>
...

ディスパッチャが、生成されたクライアント構成から <headers> 要素を削除することで <basic> 要素を検索し、サービスを再度呼び出していることが確認できます。この時点でディスパッチが失敗します。

メッセージ フィルタ

メッセージ フィルタを利用すると、ディスパッチ プロセスをより詳細にカスタマイズできます。Windows Communication Foundation では、受信メッセージをディスパッチする際に、メッセージ フィルタを使用して一致するエンドポイント (存在する場合) を判断します。使用するメッセージ フィルタを選択するか、または独自のメッセージ フィルタを指定できます。こうした柔軟性により、Windows Communication Foundation を使用して従来の SOAP 以外のサービスを実装する際に、従来のディスパッチ モデルに縛られることがなくなります。たとえば、ここで説明する手法を使用すると、Windows Communication Foundation のメッセージング基盤に REST/POX スタイルのサービスを実装することができます。

各エンドポイントには、アドレス フィルタとコントラクト フィルタの 2 つのフィルタが関連付けられています。アドレス フィルタは、受信メッセージがエンドポイントの "To" アドレスや必要なアドレス ヘッダーに一致するかどうかを判断します。一方、コントラクト フィルタは、受信メッセージがエンドポイントのコントラクトに一致するかどうかを判断します。ディスパッチャでは、これら両方のフィルタを使用して送信先エンドポイントを判断します。

フィルタは、MessageFilter から派生した単なるクラスであり、いくつかの Match 抽象メソッドを定義します。EndpointAddressMessageFilter や ActionMessageFilter など、組み込みの MessageFilter がいくつか実装されています。EndpointAddressMessageFilter は既定のアドレス フィルタとして使用され、ActionMessageFilter は既定のコントラクト フィルタとして使用されます。EndpointAddressMessageFilter は、"To" アドレスとエンドポイント アドレスを単純に比較し、完全に一致するものを探します。また、受信メッセージのアドレス指定ヘッダーと、エンドポイントで必要な一連のアドレス指定ヘッダーについても比較を行います。ActionMessageFilter は、受信メッセージの "Action" の値とコントラクトのアクションを比較します。ActionMessageFilter もまた、完全に一致するものを探します。

アドレス指定フィルタは他にもあり、それぞれ動作が異なります。たとえば、PrefixEndpointAddressMessageFilter は、受信メッセージの "To" アドレスとエンドポイント アドレスが同じアドレス プレフィックスで始まる場合に、そのアドレスを抽出します (より緩やかな一致)。また、指定のエンドポイントに対応するものとしてすべてのメッセージを抽出する MatchAllMessageFilter もあります。

[ServiceBehavior] セクションの AddressFilterMode プロパティを使用すると、サービスで使用するメッセージ フィルタを簡単に変更することができます。AddressFilterMode には、Any、Exact、および Prefix の 3 つの値があります。これらの各値は、対応する MessageFilter 実装にマップされます。次の例は、PrefixEndpointAddressMessageFilter を使用するようにサービスを構成する方法を示しています。

[ServiceBehavior(AddressFilterMode=AddressFilterMode.Prefix)]
public class CalculatorService : ISimpleMath, IScientific
{
   ...

コードを記述して、各エンドポイントのフィルタを変更することもできます。独自の MessageFilter 実装を記述して、カスタマイズしたマッチング ロジックを組み込むこともできます。Windows Communication Foundation には XPathMessageFilter が実装されているため、マッチング プロセス中に受信メッセージに対して任意の XPath 式を評価することができ、コンテンツ主導のシナリオを単独で処理できます。

ホスト名の比較

アドレスとディスパッチに関する注意事項として、最後にホスト名の比較について説明します。一般的には、エンドポイントの定義時に特定のホスト名を物理アドレス内にハードコードします。しかし、特定のコンピュータにバインドされたすべてのホスト名 (localhost、contoso、www.contoso.com など) をリッスンするようにエンドポイントを構成する場合はどうでしょうか。また、特定のホスト名と一致するものだけにエンドポイントを制限する場合はどうでしょうか。

このような場合、ほとんどのバインドでプロパティとして使用できる HostNameComparisonMode を使用して詳細に制御することができます。HostNameComparisonMode には、StrongWildcard (既定)、Exact、および WeakWildcard の 3 つのうちいずれかを設定できます。

StrongWildcard と WeakWildcard は、任意のホスト名とポートの組み合わせがエンドポイント アドレス (IP アドレスなど) に一致することを意味します。両者の違いは、StrongWildcard の方が WeakWildcard よりも優先順位が高いことだけです。Exact は、ホスト名とポートがエンドポイント アドレスと完全に一致する (大文字と小文字は区別されません) ことを意味します。

既定のモードは StrongWildcard であるため、localhost とコンピュータ名の違いはエンドポイントで自動的に処理されます。したがって、この機能は、マッチング プロセスをさらに制限する場合にのみ使用する必要があります。

この記事で説明したアドレス指定の手法の多くは、SOAP 1.2 と WS-Addressing を使用するように構成したバインドと組み合わせて使用する場合にのみ機能します。うまく機能しない場合は、WSHttpBinding などの最新のバインドを使用していることを確認してください。バインドを有効にして WS-Addressing を使用する方法の詳細については、先月のコラム「WCF メッセージングの基礎」を参照してください。

まとめ

Windows Communication Foundation のメッセージング フレームワークは、開発者にさまざまなアドレス指定機能を提供します。これらの機能は、一般的なホスティングや通信のシナリオを簡素化し、高度なルーティングとディスパッチのロジックをより柔軟に使用できるように設計されています。これらのアドレス指定手法を習得することより、Windows Communication Foundation を自在に使用できるスキルが向上するだけでなく、将来の生産性も向上します。

ご意見やご質問は、

Aaron sstation@microsoft.com.

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


Wicked Code: UpdatePanel のヒントと秘訣

Wicked Code
UpdatePanel のヒントと秘訣
Jeff Prosise

コンテンツ
更新を強調表示する
UpdatePanel の更新をキャンセルする
複数の UpdatePanel
UpdatePanel を使用せずに更新する
SOAP や XML ではない Web サービス

良くも悪くも、UpdatePanel コントロールは ASP.NET AJAX コミュニティの人気者です。"良くも" というのは、部分的なページ レンダリングが UpdatePanel によって驚くほど簡単になることを指しており、"悪くも" というのは、その単純さと使いやすさを実現するために、効率と (皮肉なことに) 帯域幅が犠牲になることを指しています。

UpdatePanel を使用すると、一般的な Web ページで AJAX の便利さを実現できますが、AJAX に付きものの効率性は得られません。たとえば、UpdatePanel コントロールが、コンテンツの更新のためにサーバーへの非同期 AJAX コールバックを行うとき、その要求には、ビュー ステートも含め、従来の ASP.NET ポストバックの内容すべてが含まれます。ほとんどの開発者は、AJAX ではビュー ステートを使用しないものと思っています。しかし、UpdatePanel ブランドの AJAX の場合は異なります。

UpdatePanel コントロールを使用する場合、その性質を理解しておく必要があります。アプリケーション パフォーマンスの観点からは、UpdatePanel の代わりに、WebMethod またはページ メソッドに対する非同期呼び出しを使用した方がよい場合が少なくありません。その方が、送受信データ量を大幅に削減できます。ただし、UI の更新を、ページ内の JavaScript で開発者が明示的に行う必要がある点で、根本的に異なります。

さらに、ASP.NET AJAX のディスカッション フォーラムでは既に、UpdatePanel のカスタマイズに関する質問が続出しています。UpdatePanel をクライアント側でサポートする、Microsoft® AJAX Library の JavaScript クラスである PageRequestManager について知っていれば、こうした質問の多くが簡単に解決します。

ASP.NET AJAX が公開されたのですから、UpdatePanel のカスタマイズと最適化の方法、さらにはこれを使用せずに済ませる方法について、詳細に検討したいと思います。このコラムでは、まさにこれらの点を説明します。

更新を強調表示する

ときどき、Microsoft の開発者たちを気の毒に思わずにはいられません。仕事のできが良くなければ世間から非難を浴びます。ところが、仕事のできが良すぎても非難されることがあるのです。たとえば、私が最近受け取った顧客からの電子メールに、ASP.NET AJAX の UpdatePanel の機能が少々良すぎるという苦情がありました。

UpdatePanel を使用すると、ASP.NET ページがサーバーにポストバックするときに頻繁に発生する点滅やちらつきをなくし、スムーズでちらつきのない更新を行うことができます。UpdatePanel では、ポストバックを非同期コールバック (XML-HTTP 要求) に変換し、クライアント上で JavaScript を使用することにより、UpdatePanel コントロールによってカプセル化されたページの一部を更新します。ポストバック時に行われるページの再描画が、ブラウザによって行われないため、点滅やちらつきがなくなります。

顧客の苦情は、ページの一部が新しいコンテンツに更新されたことにユーザーが気付かない場合がある、というものでした。彼の質問は簡単でした。"ASP.NET AJAX チームの担当者たちに、UpdatePanel がほんの少しだけちらつくようにしてもらえないだろうか。そうすれば、重要な更新が見逃されることもなくなるのだが。"

残念ながら ASP.NET AJAX チームには、UpdatePanel がちらつくようにするつもりはないと思われます。そもそも、ちらつきをなくすために UpdatePanel が作られたのですから。さいわい、ブラウザで AJAX の小技を使って、更新された UpdatePanel に注意を引くことができます。その秘密は、ASP.NET AJAX のクライアント側を構成する JavaScript クラスのライブラリである Microsoft AJAX Library の、Sys.WebForms.PageRequestManager クラスにあります。PageRequestManager は、UpdatePanel によって起動された非同期コールバックを管理します。また、非同期コールバックの完了時には、UpdatePanel 内のコンテンツを更新します。

PageRequestManager は、更新発生の前後にブラウザでイベントを起動します。これらのイベントを JavaScript でフックし、更新されたコンテンツにユーザーの注意を引くためのコードを実行できます。重要なイベントは pageLoaded という名前です。このイベントは、ページがブラウザに読み込まれるたびに発生します (ASP.NET における Page_Load に類似しています)。また、UpdatePanel コントロールのために起動された非同期コールバックが完了し、UpdatePanel 内のコンテンツが更新されるたびにも、これが発生します。次の 2 行のコード (1 行に結合することもできます) を使用すると、pageLoaded イベントの JavaScript ハンドラを登録できます。

var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_pageLoaded(pageLoaded);

最初の行で、ページの PageRequestManager オブジェクトへの参照を取得します。次の行では、pageLoaded という JavaScript 関数を、pageLoaded イベントのハンドラとして登録します。

pageLoaded イベント ハンドラが呼び出されると、Sys.WebForms.PageLoadedEventArgs 型の引数を受け取ります。Sys.WebForms.PageLoadedEventArgs も、Microsoft AJAX Library のクラスです。PageLoadedEventArgs に含まれている get_panelsUpdated メソッドを呼び出すと、直前にコンテンツが更新された UpdatePanel が存在する場合に、それらすべてを列挙できます。既定では、UpdatePanel は単にクライアント側の DIV にすぎません。そこで、JavaScript を使用して DIV の点滅、強調表示など、ユーザーの注意を引くためのあらゆることができます。

図 1 のコードは、pageLoaded イベントを使用して更新の強調表示を行う方法の 1 つを示しています。更新が発生するたびに、この JavaScript で、更新された UpdatePanel を表すドキュメント オブジェクト モデル (DOM) 要素の表示/非表示をすばやく 3 回繰り返すことにより、これらの要素を点滅させます。点滅は flashPanels というヘルパ関数によって実行されます。flashPanels は入力パラメータとして点滅回数を受け取ります。

Figure 1 更新を点滅させる (Close Figure 1)
<script type=”text/javascript”>

var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_pageLoaded(pageLoaded);

var _panels, _count;

function pageLoaded(sender, args)
{
    if (_panels != undefined && _panels.length > 0)
    {
        for (i=0; i < _panels.length; i++)
            _panels[i].dispose();
    }

    var panels = args.get_panelsUpdated();

    if (panels.length > 0)
    {
        _panels = new Array(panels.length);

        for (i=0; i < panels.length; i++)
            _panels[i] = new Sys.UI.Control(panels[i]);

        flashPanels(3);
    }
}

function flashPanels(count)
{
    _count = (count << 1) + 1;

    for (i=0; i < _panels.length; i++)
        _panels[i].set_visible(false);

    window.setTimeout(toggleVisibility, 50);
}

function toggleVisibility()
{
    for (i=0; i < _panels.length; i++)
        _panels[i].set_visible(!_panels[i].get_visible());

    if (--_count > 0)
        window.setTimeout(toggleVisibility, 50);
}
</script>

更新された UpdatePanel の可視性のオン/オフを切り替えることで、点滅効果が生まれています。このコードでは、DOM 要素を直接処理せず、UpdatePanel を表す DOM 要素を Sys.UI.Control オブジェクトでラップします。次に、Sys.UI.Control の set_visible メソッドと get_visible メソッドを使用して、可視性を切り替えます。

_panels[i].set_visible(!_panels[i].get_visible());

Sys.UI.Control は、Microsoft AJAX Library (具体的には MicrosoftAjax.js) にある JavaScript クラスです。このようにして可視性を切り替える利点は、この動作がブラウザに依存しないことです。この動作は、ASP.NET AJAX をサポートするすべてのブラウザ (実質的には現在のすべてのブラウザ) で同様に機能します。一方、ブラウザの DOM を直接処理する JavaScript コードの場合、異なる種類のブラウザで使用するためには微調整が必要となります。

UpdatePanel の更新をキャンセルする

pageLoaded イベントは、UpdatePanel がサーバーに戻ってコンテンツを更新するときに、PageRequestManager クラスによって起動される複数のイベントのうちの 1 つです。PageRequestManager によって起動されるもう一つの重要なイベントは、非同期コールバックの発生前に起動される initializeRequest です。

最近ある人から、AsyncPostBackTrigger で UpdatePanel の更新をトリガできるかどうかを、実行時に決定することは可能かとたずねられました。答えはイエスです。initializeRequest イベントを処理することによって、これが可能となります。

initializeRequest ハンドラに渡される 2 番目のパラメータは、initializeRequestEventArgs 型のオブジェクトです。そのオブジェクトには、更新をトリガしたボタンなどの要素を識別する get_postBackElement メソッドが含まれています。また、コールバックを発生前にキャンセルできる set_cancel メソッドも含まれています。set_cancel メソッドの使用例を次に示します。

<script type=”text/javascript”>

var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_initializeRequest(initializeRequest);

function initializeRequest(sender, args)
{
    args.set_cancel(!confirm(‘Are you sure?’));
}
</script>

この例では、コールバックの実行前に、更新を続行するかどうかをユーザーに確認するダイアログ ボックスが、intializeRequest ハンドラによって表示されます。確認用ダイアログ ボックスで [キャンセル] をクリックすると、set_cancel に true が渡され、コールバックがトラック内で停止します。実際には、更新の続行前にユーザーに確認を求めるケースは少ないと思われますが、条件に応じて更新をキャンセルする機能がアプリケーションで役立つ場合もあります。

ちなみに、非同期コールバックを実行した後、それが完了するまでの間にキャンセルすることも可能です。PageRequestManager では、これを行う abortPostBack メソッドが提供されています。また、非同期コールバックが保留されているかどうかを判断する get_isInAsyncPostBack メソッドも提供されています。これらのメソッドは、多くの場合 UpdateProgress コントロールと組み合わせて、キャンセル用の UI に使用します。

複数の UpdatePanel

1 つのページで複数の UpdatePanel がホストされていることがあります。既定では、ページ内のいずれかの UpdatePanel が更新されるとき、同じページ内の他の UpdatePanel も更新されます。この動作が望ましいこともありますが、多くの場合、すべての UpdatePanel を連動して更新する必要はありません。

ページ内の各 UpdatePanel コントロールの UpdateMode プロパティを [Conditional] (条件付き) に設定すると、どの UpdatePanel インスタンスをいつ更新するかを選択できます。その後、いずれかの UpdatePanel が更新されてサーバー側のイベント ハンドラが呼び出されるときに、更新する他のパネルで UpdatePanel.Update を呼び出します。こうすると、レンダリングするコントロールの数が抑えられることにより、サーバーの負荷が軽くなります。また、更新されない UpdatePanel からはデータが追加されないため、応答内のデータ量も減少します。

UpdatePanel を使用せずに更新する

AJAX は、より良いユーザー エクスペリエンスを実現するだけでなく、通信効率を向上させるためのツールでもあります。従来の ASP.NET ポストバックの発生時には、ビュー ステートも含め、Web フォーム内のすべてのデータがポストバックでサーバーまで送信されます。ASP.NET ページ、特に DataGrid コントロールや GridView コントロールを使用するページの反応が遅く感じられる一因は、ビュー ステートにあります。ページに含まれるビュー ステートの数が多すぎると、パフォーマンスが低下します。そして、ASP.NET アプリケーションでは、このようなページに頻繁に遭遇します。

ASP.NET ポストバックの代わりに AJAX コールバックを使用する利点の 1 つは、AJAX コールバックが適切に実行されると、必要なデータだけが転送されることです。つまり、転送にビュー ステートを含める必要がありません。

ページでちらつきのない更新を実現するために UpdatePanel を使用する場合、効率の良い構築を行っているものと考えがちです。何しろ、UpdatePanel は AJAX を使用しているのですからね。ところが残念なことに、UpdatePanel 更新時の通信トラフィックを調べると、まったく (少なくとも送信方向では) 節約になっていないことがわかります。通常のポストバック時にサーバーに転送されるビュー ステート データ (およびその他のデータ) は、UpdatePanel のコールバック時にも転送されます。実際、UpdatePanel からの非同期 XML-HTTP 要求で送信される内容は、標準の ASP .NET ポストバックの場合とほぼ同じです。ASP.NET AJAX の隠された秘密をお教えしましょう。UpdatePanel は、通信効率ではなく使い勝手のためのツールなのです。

UpdatePanel を効率化する余地はほとんどありません。しかし、UpdatePanel の代わりに ASP.NET AJAX の他の機能を使用すると、同じくらいスムーズに、かつはるかに効率的に、ページ コンテンツを更新することができます。追加の作業が少々必要ですが、多くの場合、それに見合った結果が得られます。クライアントとサーバー間の送受信データ量を大幅に削減できるためです。

サーバーの負荷を軽減する効果もあります。UpdatePanel がサーバーにコールバックすると、コールバック対象のページでライフサイクルのほぼ全体が実行されます。つまり、ページがインスタンス化され、ページ内のコントロールがインスタンス化され、UpdatePanel 内のコントロールについて通常のレンダリング サイクルが実行されます。ページの一部を更新するだけのために、随分と大げさな作業が発生しています。

図 2 のページ フラグメントを例に取ります。ここで作成する単純な UI では、ユーザーが郵便番号を入力してボタンをクリックすると、市区町村と都道府県のフィールドが初期化されます (図 3 を参照)。すべてのコントロールが 1 つの UpdatePanel でホストされているため、ボタン コントロールのポストバックは非同期コールバックに変換され、このコールバック内のサーバーでイベント ハンドラ (GetCityAndState) が呼び出されます。GetCityAndState (コードは省略) は、ZIP Code TextBox から郵便番号を読み取り、それを市区町村と都道府県に変換し、これに従って、市区町村と都道府県を表示する TextBox と DropDownList を初期化します。これらすべてが 1 つの UpdatePanel 内で行われるため、更新はスムーズに行われ、ちらつきもありません。

Figure 2 UpdatePanel を使用して市区町村と都道府県のフィールドにデータを挿入する (Close Figure 2)
<asp:UpdatePanel ID=”UpdatePanel1” runat=”server”>
  <ContentTemplate>
    City:<br />
    <asp:TextBox ID=”City” runat=”server” />
    <br /><br />
    State:<br />
    <asp:DropDownList ID=”Region” runat=”server”>
        <asp:ListItem Value=”AL”>Alabama</asp:ListItem>
        <asp:ListItem Value=”AK”>Alaska</asp:ListItem>
        <asp:ListItem Value=”AZ”>Arizona</asp:ListItem>
          ...
        <asp:ListItem Value=”WV”>West Virginia</asp:ListItem>
        <asp:ListItem Value=”WI”>Wisconsin</asp:ListItem>
        <asp:ListItem Value=”WY”>Wyoming</asp:ListItem>
    </asp:DropDownList>
    <br /><br />
    Zip Code:<br />
    <asp:TextBox ID=”ZipCode” runat=”server” />&nbsp;
    <asp:Button ID=”AutofillButton” Text=”Autofill”
      OnClick=”GetCityAndState” runat=”server” />
  </ContentTemplate>
</asp:UpdatePanel>
図 3 市区町村、都道府県、および郵便番号の UI
図 3 市区町村、都道府県、および郵便番号の UI(画像を縮小するには、ここをクリックします)
図 3 市区町村、都道府県、および郵便番号の UI
図 3 市区町村、都道府県、および郵便番号の UI (画像を拡大するには、ここをクリックします)

しかし、次のような問題があります。このように UpdatePanel を使用した場合、ユーザー エクスペリエンスは向上しますが、送受信されるデータ量の削減にはほとんどつながりません。UpdatePanel では、サーバーの負荷もほとんど軽減されません。UpdatePanel 内のコントロールがレンダリングされる時点までにサーバーで実行される処理は、完全なポストバックの場合とほぼ同じです。これは仕方がありません。UpdatePanel コントロールの利点の 1 つは、サーバー側のイベント ハンドラ (GetCityAndState など) が、非同期コールバック内でも従来のポストバックの場合と同じように機能することだからです。このため、ページ内のコントロールは、インスタンス化および初期化される必要があり、ビュー ステートにアクセスできなければなりません。

図 4 に、これと同じ機能を UpdatePanel コントロールを使用せずに実装する方法を示します。今度は、GetCityAndState という ASMX Web メソッドへの非同期 XML-HTTP 要求を起動する JavaScript に、Autofill ボタンが関連付けられています。呼び出しは ZipCodeService という JavaScript プロキシ経由で行われます。ZipCodeService は ScriptManager コントロールのサービス参照によって生成されます。GetCityAndState は入力として郵便番号の文字列を受け取り、対応する市区町村と都道府県が含まれた文字列配列を返します。完了関数 onGetCityAndStateCompleted は配列からこれらの項目を取得し、市区町村と都道府県のフィールドに挿入します。外から見た結果は同じですが、内部の動作はかなり違います。

Figure 4 UpdatePanel を使用せずに市区町村と都道府県のフィールドにデータを挿入する (Close Figure 4)
<asp:ScriptManager ID=”ScriptManager1” runat=”server”>
    <Services>
        <asp:ServiceReference Path=”ZipCodeService.asmx” />
    </Services>
    <Scripts>
      <asp:ScriptReference Name=”PreviewScript.js”
Assembly=”Microsoft.Web.Preview” />
    </Scripts>
</asp:ScriptManager>

City:<br />
<asp:TextBox ID=”City” runat=”server” />
<br /><br />
State:<br />
<asp:DropDownList ID=”Region” runat=”server”>
    <asp:ListItem Value=”AL”>Alabama</asp:ListItem>
    <asp:ListItem Value=”AK”>Alaska</asp:ListItem>
    <asp:ListItem Value=”AZ”>Arizona</asp:ListItem>
      ...
    <asp:ListItem Value=”WV”>West Virginia</asp:ListItem>
    <asp:ListItem Value=”WI”>Wisconsin</asp:ListItem>
    <asp:ListItem Value=”WY”>Wyoming</asp:ListItem>
</asp:DropDownList>
<br /><br />
Zip Code:<br />
<asp:TextBox ID=”ZipCode” runat=”server” />&nbsp;
<asp:Button ID=”AutofillButton” Text=”Autofill”
  OnClientClick=”autoFill(); return false;” runat=”server” />

<script type=”text/javascript”>
function autoFill()
{
    var tb = new Sys.Preview.UI.TextBox ($get(‘ZipCode’));
    var zip = tb.get_text();

    if (zip.length == 5)
        ZipCodeService.GetCityAndState (zip,
            onGetCityAndStateCompleted);
}

function onGetCityAndStateCompleted(result)
{
    if (result != null)
    {
        var tb = new Sys.Preview.UI.TextBox ($get(‘City’));
        tb.set_text(result[0]);

        var select =
            new Sys.Preview.UI.Selector ($get(‘Region’));
        select.set_selectedValue(result[1]);
    }
}
</script>

JavaScript プロキシ経由で呼び出される ASMX Web メソッドの実装は次のとおりです。

[ScriptService]
public class ZipCodeService : System.Web.Services.WebService
{
    [WebMethod]
    public string[] GetCityAndState(string zip)
    {
      ...
    }
}

これは、ほぼすべての面において標準的な Web メソッドですが、唯一異なるのは、所属するクラスの属性が WebService ではなく ScriptService となる点です。ScriptService は WebService に似ていますが、Web サービスの WebMethod をクライアント側のスクリプトから呼び出すことができます。

ASP.NET AJAX では、従来の WebMethod を XML-HTTP 要求の対象とすることができるほか、ページ メソッドと呼ばれる特殊な Web メソッドもサポートしています。ページ メソッドとは、Web ページに (つまり、ASMX ファイルにではなく、ASPX ファイルまたは分離コード ファイルに) 実装された WebMethod です。開発者がページ メソッドを使用すると、専用の Web サービスを構築しなくても、XML-HTTP コールバックのエンドポイントを提供できます。

ページ メソッドは、パブリックな静的メソッドである必要があります。また、WebMethod と同様、WebMethod 属性で修飾されている必要があります (WebMethod およびページ メソッドは ScriptMethod 属性で修飾されることもあります。その場合、送信内容を制御できるようになります)。クライアント上で、ページ メソッドは JavaScript から特別の PageMethods プロキシ経由で呼び出されます。

Web サービスとは異なり、ページ メソッドはサービス参照を必要としません。ただし、ScriptManager コントロールの EnablePageMethods プロパティに次のように true を設定し、ページ メソッドを有効にする必要があります。

<asp:ScriptManager ID=”ScriptManager1” runat=”server”
  EnablePageMethods=”true” />

実際のところ、ページ メソッドは WebMethod と同様の効率性を提供します。ページ メソッドの呼び出し時には、ビュー ステートおよび他の入力はサーバーに転送されません。また、ページ メソッドは静的であるため、ページ オブジェクトをインスタンス化することなく呼び出すことができます。ページ メソッドへの呼び出しは、従来の ASP.NET 要求でトリガされるページ ライフサイクルを呼び出すことがありません。

SOAP や XML ではない Web サービス

ASP.NET AJAX の最も重要な機能の 1 つに、ブラウザ クライアントからの非同期 XML-HTTP 要求を使用して、WebMethod とページ メソッドをサーバー上で呼び出す機能があります。それでも、この機能について話すのは少々ためらわれます。

Web サービスといえば、ほとんどの人が SOAP と XML を思い浮かべます。どちらのテクノロジも、"効率的" という言葉と共に語られることはまれです。ASP.NET AJAX を使用して WebMethod を JavaScript から呼び出すことはできます。しかし、ASP.NET AJAX は SOAP や XML を使用しません。

図 5 に、図 4 の Web メソッド呼び出しが実行されたときに送受信される内容を示します。HTTP ヘッダーを除くと、この要求によって転送されるデータは、ユーザーが入力した郵便番号だけであり、応答によって返されるデータは、市区町村と都道府県を表す 1 組の文字列だけです。SOAP も XML も (さらにはビュー ステートも) 見当たりません。その代わり、入力と出力のエンコードには JavaScript Object Notation (JSON) が使用されます。これは XML よりもはるかに簡潔で、処理も簡単です。要求と応答では、SOAP の代わりに単純なプロトコルを使用しますが、これは基本的に HTTP にすぎません。HTTP と JSON を組み合わせて使用するため、WebMethod とページ メソッドへの ASP.NET AJAX 呼び出しは、従来の Web サービスの呼び出しよりもはるかに効率的です。

Figure 5 JSON でエンコードされた入力と出力 (Close Figure 5)

要求

POST /Ajax/ZipCodeService.asmx/GetCityAndState HTTP/1.1
Accept: */*
Accept-Language: en-us
Referer: http://localhost:1997/Ajax/ZipCodePage.aspx
UA-CPU: x86
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; ...)
Host: localhost:1997
Content-Length: 15
Connection: Keep-Alive
Cache-Control: no-cache

{"zip":"98052"}
応答

HTTP/1.1 200 OK
Server: ASP.NET Development Server/8.0.0.0
Date: Fri, 29 Dec 2006 21:06:17 GMT
X-AspNet-Version: 2.0.50727
Cache-Control: private, max-age=0
Content-Type: application/json; charset=utf-8
Content-Length: 16
Connection: Close

{"REDMOND", "WA"}

JSON は、業界標準のシリアル化形式として注目されています。また、ASP.NET AJAX のネイティブ形式としても採用されています。JSON のシリアル化と逆シリアル化は、クライアント側では、Microsoft AJAX Library の Sys.Serialization.JavaScriptSerializer クラスによってサポートされます。サーバー側では、System.Web.Script.Serialization.JavaScriptSerializer クラスによってサポートされます。

すべての型に JSON との互換性があるとは限りません。たとえば、JSON では、循環参照を持つオブジェクトを処理できません。JSON との互換性がない複雑なデータ型を返す必要がある場合、実際には ASP.NET AJAX の ScriptMethod 属性を使用して、戻り値の型を XML にシリアル化できます。この方法は、次のように XML データを返すメソッドの場合にも役立ちます。

[ScriptMethod (ResponseFormat=ResponseFormat.Xml)]
public XmlDocument GetData()
{
  ...
}

また、カスタム JSON コンバータを構築および登録して、通常は JSON との互換性がない型をシリアル化および逆シリアル化するようにもできます。ASP.NET AJAX January Futures CTP には、このようなコンバータが 3 つ含まれています。DataSet 用、DataTable 用、および DataRow 用です。

ご意見やご質問は、

Jeff wicked@microsoft.com.

まで英語でお送りください。
Jeff Prosiseは MSDN Magazine の寄稿編集者で、その著書には、『Programming Microsoft .NET』 (Microsoft Press、2002 年) などがあります。また、ソフトウェアのコンサルティングおよび Microsoft .NET 専門の教育機関である Wintellect (www.wintellect.com) の共同創立者でもあります。


基礎: ワークフローにおける ActivityExecutionContext

基礎
ワークフローにおける ActivityExecutionContext
Matt Milner

コードのダウンロード : : Foundations2007_06.exe (179KB)
コンテンツ
ActivityExecutionContext の必要性
ワークフロー開発
ルールと条件
カスタム アクティビティ
まとめ

今月は、Windows® Workflow Foundation における ActivityExecutionContext (AEC) について説明します。この特別なクラスは、ワークフローの実行、保存、および補正やワークフローのアクティビティにおいて不可欠なコンポーネントであるにもかかわらず、ドキュメントやサンプルで細部まで注意が払われていないことがよくあります。ワークフローおよびアクティビティを構築する際に開発者が共通して直面する問題点の多くは、ActivityExecutionContext がどのように機能するかに関連しています。While アクティビティの使用、ステート マシン ワークフローの構築、カスタム アクティビティの記述などの操作を行うかどうかにかかわらず、AEC の使用方法を理解することは重要です。そこでこのコラムでは、ActivityExecutionContext について説明し、ワークフローでの使用方法を示します。また、カスタム アクティビティを記述する際に、どのように ActivityExecutionContext を使用できるかについても説明します。

ActivityExecutionContext の必要性

Windows Workflow Foundation には、AEC (および AEC のデザイン構築) を必要とする機能がいくつかあります。それらは、状態管理 (または保存)、アクティビティの再実行、および補正の各機能です。Windows Workflow Foundation の状態管理サービスでは、ワークフローをメモリから削除した後で復元できるように、ワークフロー全体の状態をシリアル化し、その情報を永続的なストアに格納する機能が提供されます。これをサポートするには、ワーキング セットに関する状態情報のコンテナが必要です。このコンテナには、スケジュールと実行に関連付けられているアクティビティ ツリー、通信キュー、および内部データ構造が含まれます。アクティビティ ツリーは、これがアプリケーション開発者によって作成され、ワークフローの定義に使用するアクティビティに含まれる状態であることから、一般に、アプリケーション状態と呼ばれます。それ以外の状態は、ワークフローの実行を管理する目的で、主に、Windows Workflow Foundation ランタイムによって使用されるため、ランタイム状態と呼ばれます。

AEC を必要とするその他の 2 つの機能は、密接に関連しています。ワークフローの共通のデザイン パターンとして、ステップの繰り返しがあります。何らかの条件が満たされるまで子アクティビティを繰り返し実行する While アクティビティについて考えてみましょう。この実行の基になる実装について考えた場合、これを処理するには大きく分けて 2 つの選択肢があります。1 つ目の選択肢は、各繰り返しでアクティビティ インスタンスを再利用し、それぞれの実行前にアクティビティ インスタンスを初期状態にリセットする方法です。これは While アクティビティなどでは機能するかもしれませんが、子アクティビティを並行して実行できる Replicator アクティビティではどうでしょうか。この場合、同時に 3 つまたは 4 つの異なるアクティビティ インスタンスを実行状態にする必要が生じる可能性があります。 そうなると、リセットのアプローチは機能しません。

アクティビティを複数回実行する場合の 2 つ目の選択肢は、元のアクティビティのクローンを作成し、各繰り返しで複製したアクティビティを実行する方法です。このモデルでは、アクティビティの繰り返し処理がそれぞれ同じテンプレート (つまり、アクティビティの初期状態) で開始され、複製された各インスタンスを個別に管理できます。このモデルの利点として、各クローンがランタイムで管理される独自の状態を保持できることや、その他のクローンに干渉しないことが挙げられます。このような場合、各クローンには、アクティビティのそれぞれのインスタンスの状態を管理するための独自の AEC が必要になります。

補正では、アクティビティが主な実行パスを完了した後に何らかの作業を実行することを可能にします。これにより、アクティビティの完了後に発生したプロセス エラーをそのアクティビティによって処理したり、アクティビティの状態を開始の状態に戻すことができます。つまり、既に変更されたシステムまたはデータを更新します。基本的に、補正の場合は、既にアクティビティが作業を完了した後にアクティビティで実行された一連のアクションを有効にする必要があります。このためには、その作業の状態を保存および復元できる必要があります。 任意のインスタンスではなく、特定のインスタンスによって行われた変更に対して補正を実行するには、各繰り返しでアクティビティの状態を管理できる必要があります。したがって、一連のアクティビティの各繰り返しの AEC を使用し、アクティビティが実行時に使用した正しい状態にアクセスできるようにすることによって、補正が実現されます。

AEC は、ランタイム状態およびアプリケーション状態のコンテナとして機能し、各ワークフローには、ルート アクティビティ (ワークフロー自体) 用に既定の AEC が 1 つ存在します。アクティビティで子が作成された場合、子 AEC が存在する場合もあります。 たとえば、図 1 のように、ワークフローで While アクティビティを 1 つ使用し、その While アクティビティに 1 つの Code アクティビティが含まれる場合、While アクティビティの各繰り返しで子 AEC が作成されます。

図 1 単純な While アクティビティを使用した ActivityExecutionContexts
図 1 単純な While アクティビティを使用した ActivityExecutionContexts(画像を縮小するには、ここをクリックします)
図 1 単純な While アクティビティを使用した ActivityExecutionContexts
図 1 単純な While アクティビティを使用した ActivityExecutionContexts (画像を拡大するには、ここをクリックします)

ワークフロー開発

開発者が直面する共通の問題点として、繰り返しアクティビティでアクティビティが出現する場合、イベント ハンドラ内にコードを記述することが挙げられます。たとえば、図 2 のように、While アクティビティ内で一連のアクティビティを使用し、アクティビティを別のアクティビティのイベント ハンドラから参照するとします。ここでは説明のために、Code アクティビティと、コンソールから入力を読み取るカスタム ReadLine アクティビティを使用します。ReadLine アクティビティは、コンソールで入力されたテキストを表示するプロパティを 1 つ持っています。このサンプルでは、Code アクティビティの ExecuteCode イベント ハンドラにある ReadLine の Text プロパティにアクセスします。

図 2 イベント ハンドラ
図 2 イベント ハンドラ

読者はまず、ReadLine アクティビティに直接アクセスするコードを次のように記述すると考えるのではないでしょうか。

Console.WriteLine(“Text from ReadLine2: {0}”, readLine2.Text);

しかし、このコードを使用してワークフローを実行すると、出力は図 3 のようになります。ここでの問題は、While アクティビティのこの繰り返しで複製されたインスタンスではなく、テンプレートから ReadLine アクティビティを参照していることです。この問題に対処するための一般的なアプローチとして、複製された適切なターゲット インスタンス (この場合は ReadLine アクティビティ) を参照するために、複製された現在のアクティビティ インスタンスへの参照を取得することが挙げられます。

図 3 直接参照による出力の失敗
図 3 直接参照による出力の失敗(画像を縮小するには、ここをクリックします)
図 3 直接参照による出力の失敗
図 3 直接参照による出力の失敗 (画像を拡大するには、ここをクリックします)

イベント ハンドラ内の複製されたアクティビティの適切なツリーへの参照を取得するには、イベントの sender を使用できます。これが、イベントが定義されるアクティビティになります。 サンプルでは、これは Code アクティビティです。 そこから、アクティビティ ツリー内を移動して目的のアクティビティを見つけることができます。このアプローチについては、次のコードを参照してください (前述のコードの適切な位置で次のコードを使用すると、目的の結果が得られます)。

CodeActivity thisActivityInstance = sender as
  CodeActivity;
CompositeActivity parent =
  thisActivityInstance.Parent as CompositeActivity;

MSDNMag.WFActivities.ReadLine target =
  (MSDNMag.WFActivities.ReadLine)
  parent.GetActivityByName(“readLine2”);
Console.WriteLine(
  “Text from ReadLine in this AEC: {0}”, target.Text);

この例では、使用中の Code アクティビティのインスタンスへの参照を取得し、親アクティビティを経由して適切な子アクティビティに移動します。そうすれば、その特定のインスタンスにアクセスして、このインスタンスから Text プロパティの値を取得できます。

これとは別に、アクティビティ間で共有データを使用する方法があります。この方法を使用すると、子 AEC で実行中のアクティビティを使用して、既定の AEC、つまり、ワークフロー自体からデータにアクセスできるようになります。ここに示す例では、ワークフロー クラスで文字列変数を定義し、このプロパティに ReadLine アクティビティの Text プロパティをバインドします。これにより、ReadLine が実行されると、このプロパティに適切な値が設定されます。したがって、Code アクティビティでワークフローの最新のデータを参照できるようになります。このモデルでは、ワークフローの 1 つのインスタンスだけを参照しているため、複製されたアクティビティやテンプレートの参照に関する問題が回避されます。

このアプローチは、特定の時点で 1 つの繰り返しだけがアクティブである While アクティビティについては問題なく機能しますが、Replicator、ConditionedActivityGroup、またはカスタム アクティビティを使用して、アクティビティを並行して実行開始させた場合は複雑になります。その場合は、何らかのキー付きコレクションで共有状態を管理する必要があり、キーも同様に管理する必要があります。

最もわかりやすく、私が最善と考える解決策は、カスタム アクティビティおよびアクティビティ バインディングの使用です。ここに示す簡単な例では、カスタム WriteLine アクティビティを使用し、その Text プロパティを ReadLine アクティビティの Text プロパティにバインドできます。AEC 内でアクティビティ バインディングを使用する場合、対象とするアクティビティは同じ AEC 内の複製されたインスタンスに適切に解決されます。これは、アクティビティを接続して、予想した結果を得るための単純なモデルです。

AEC を使用するための前述の 3 つのモデルは、それぞれ MSDN® Magazine Web サイトのこのコラムで使用可能なサンプル コードで示しています。 AECSamples ワークフローには 3 つの While アクティビティが含まれ、各アクティビティは予想した結果を得るための前述の 3 つの方法をそれぞれ示しています。

ステート マシン ワークフローにおいても、前の例で説明した問題に類似した問題が、同様の理由で発生することがよくあります (理由が常に明白であるとは限りませんが)。 ステート マシン ワークフローは、一連の変更する状態に基づいてプロセス実行をモデル化することを目的としています。このモデルの優れている点は、プロセス、つまりモデル化されたものが、複数回、特定の状態になることを可能にするため、シーケンシャル ワークフローよりも柔軟なモデルが提供されることです。このデザインにより、特定の State アクティビティを複数回実行できます。したがって、各繰り返しは個別の AEC で実行される必要があります。つまり、State アクティビティはそれぞれがテンプレートとして機能し、複製され、特定の状態になるたびに新しい AEC で実行されます。また、State アクティビティでは、個別の AEC 内に含まれる各 EventDriven アクティビティが実行されます。ステート マシンでも前述の解決法、つまり、アクティビティ ツリー内の移動、共有状態、およびアクティビティ バインディングによる解決法を使用できます。コード サンプルに含まれる AECStateMachine サンプル ワークフローで、具体的な使用例を確認できます。

ルールと条件

上記の問題以外にも、開発者が AEC の使用で複雑に感じる部分に、ルールまたは条件の記述があります。アクティビティ条件は、IfElse アクティビティの条件を定義する場合など、ワークフローで意思決定を行うために使用します。通常は、CodeCondition と DeclarativeRuleCondition のどちらかを選択します。CodeCondition を使用すると、イベント ハンドラ内に条件チェックの結果を示すブール値のフラグを設定するコードを記述できます。DeclarativeRuleCondition では、ルール条件ステートメントを参照し、この場合も、ブール値を使って評価します。

AEC 内で CodeCondition を使用する場合 (ステート マシンでの EventDriven アクティビティ内の IfElse アクティビティなど) は、前述の AEC でのアクティビティの参照に関する場合と同様の問題が見つかります。CodeCondition を使用して、EventDriven アクティビティ内のステップを開始する ReadLine アクティビティを直接参照しようとすると、現在実行中のアクティビティではなく、そのテンプレートにアクセスすることになります。つまり、CodeCondition では、デリゲートがアクティビティのイベント ハンドラのように使用されます。たとえば、ユーザーがコンソールで "quit" と入力したら条件を評価して、True を返すように、次のようなコードを記述する場合が考えられます。

if (readLine1.Text != null && readLine1.Text.StartsWith(“quit”))
{
    e.Result = true;
}

残念ながら、readLine1 インスタンスがテンプレートであるため Text プロパティは常に null になり、State の各実行で複製されたインスタンスが使用されます。この問題は、ワークフロー レベルで共有状態を使用するか、または sender パラメータを使用して現在のアクティビティ ツリーにアクセスし、ツリー内を上下に移動することによって解決できます。

//get the grandparent activity (the eventdriven)
CompositeActivity grandparent = (sender as Activity).Parent.Parent;

//figure the result by finding the child ReadLine activity
e.Result = ((MSDNMag.WFActivities.ReadLine)
    grandparent.GetActivityByName(
        “readLine1”)).Text.StartsWith(“quit”);

親アクティビティではなく、その親の親にアクセスしたことに注意してください。IfElse アクティビティの場合、実際には条件は IfElse アクティビティの子である IfElseBranch アクティビティによって呼び出されます。親を使用した場合は、EventDriven アクティビティではなく、IfElse アクティビティを間違って参照したことになります。この場合、GetActivityByName メソッドを使用して readLine1 アクティビティを見つけることはできません。要するに、参照解決でこのモデルを使用する場合は、アクティビティ ツリーを適切に移動できるように、どのアクティビティが sender として機能しているかを把握しておく必要があります。

子 AEC 内で DeclarativeRuleCondition の使用を試みたり、一連のルールを実行するために Policy アクティビティを使用しようとすると、さらに難しい問題が発生することがあります。ルール条件およびポリシーはワークフロー クラスに対して作成されるため、ルール内で参照するアクティビティの現在の動的インスタンスにアクセスするように、ルールを記述する必要があります。このようにルールを記述できるかどうかは、子 AEC を作成したアクティビティによって公開されている機能次第です。次に示すような宣言型ルールの一般的な条件の記述とは異なるため、勘違いしないようにしてください。

this.readLine2.Text.StartsWith(“quit”)

これは、前述の CodeCondition を使用した場合の誤ったコード例と似ています。

そこで、次のように GetActivityByName メソッドを使用して目的の現在のインスタンスを取得すれば、この問題を解決できるとお考えになるかもしれません。

((MSDNMag.WFActivities.ReadLine)this.GetActivityByName(
    “AECRulesInitialState”).GetActivityByName(
        “eventDrivenActivity1”).GetActivityByName(
            “readLine2”)).Text.StartsWith(“quit”)

残念ながら、この場合も結果は同じです。現在実行中のインスタンスではなく、まったく同じテンプレートが取得されています。アクティビティの現在実行中のクローンを取得するには、新しい AEC を作成したアクティビティによって公開されている機能に依存する必要があります。たとえば、State アクティビティが GetDynamicActivity と呼ばれるメソッドを公開している場合は、それを使用して子アクティビティの現在実行中のインスタンスに対するクエリを実行できます。このメソッドを使用すると、ReadLine の現在のインスタンスを正しく識別する宣言型ルールを記述できるようになるため、適切な意思決定を行うことができます。

((MSDNMag.WFActivities.ReadLine)
    ((System.Workflow.Activities.StateActivity)
        this.GetDynamicActivity(
            “AECRulesInitialState”)).GetDynamicActivity(
                 “eventDrivenActivity1”).GetActivityByName(
                     “readLine2”)).Text.StartsWith(“quit”)

この宣言型ルールでは、最初に GetDynamicActivity メソッドを使用して AECRulesInitialState 状態アクティビティの現在実行中のインスタンスを取得します。次に、この結果を State アクティビティにキャストします。これにより、GetDynamicActivity メソッドを再び使用して、EventDriven アクティビティの現在実行中のインスタンスにアクセスできるようになります。EventDriven アクティビティへの参照を取得したら、子 AEC を気にする必要がないため、GetActivityByName メソッドを使用して ReadLine アクティビティとその Text プロパティへの参照を取得できます。

ステート マシンおよびシーケンシャル ワークフローの他のアクティビティについても、これと同じアプローチを使用できます。たとえば、While アクティビティに DynamicActivity という名前のプロパティがある場合は、これを使用して、イベント ハンドラ内のアクティビティ テンプレートの現在のクローン、または宣言型ルール条件にアクセスできます。コード サンプルに含まれるステート マシン サンプルおよびシーケンシャル ワークフローでは、While アクティビティの使用例を示しています。

カスタム アクティビティ

ここまでは、あらゆるワークフロー開発者に関連する問題であると同時に、ワークフローの作成に必要不可欠な問題について説明してきました。ただし、複合アクティビティを構築する場合は、アクティビティの作成者も、いつ、どのように AEC を作成および管理するかについて理解しておく必要があります。

カスタム複合アクティビティで新しい AEC の作成が必要かどうかを判断するときは、アクティビティが子アクティビティを複数回実行するかどうかを自問してみてください。複数回実行する場合は、子アクティビティの各繰り返しで新しい AEC を作成する必要があります。Windows Workflow Foundation の基本アクティビティ ライブラリに含まれている Parallel アクティビティと While アクティビティについて考えてみましょう。While アクティビティは子アクティビティを繰り返し実行するため、各繰り返しを新しい AEC で実行する必要があることは既に説明しました。Parallel アクティビティでは、実行の複数の分岐を定義し、各分岐をインターリーブ方式で実行するようにスケジュール設定できます。Parallel アクティビティが子アクティビティの複数のシーケンスを実行する場合でも、各子は 1 回実行するだけです。2 つの分岐の定義が同じように見える場合でも、それらはアクティビティの種類では別々のインスタンスであり、個別に実行されます。

この種類のアクティビティに関する記述を調べるために、ForEach アクティビティを作成します。このアクティビティは、データ オブジェクトのコレクションを繰り返し処理し、コレクション内の各アイテムに対して子アクティビティを 1 回実行します。完全なサンプル コードをこのコラムでダウンロードできますが、AEC の管理に特に関連するコードについて重点的に説明します。

まず、状態およびデータを管理するために、3 つの依存関係プロパティが必要です。それらは、CurrentIteration (インデックスを管理するプライベート プロパティ)、Items (繰り返し処理されるデータのコレクション)、DataItem (コレクションから、実行される子アクティビティにアイテムを渡す添付プロパティ) です。AEC に関連するこれらのプロパティについて、特殊な点はありません。これらは、単純に、子アクティビティの複数回の繰り返しを実行するために管理する必要がある状態です。

次に、ForEach アクティビティの Execute メソッドを使用します。これにより、子アクティビティの実行が開始されます。

if (EnabledActivities.Count == 0 || Items == null || Items.Count == 0)
    return ActivityExecutionStatus.Closed;

CurrentIteration = 0;

ExecuteIteration(executionContext);

return ActivityExecutionStatus.Executing;

ここで重要なのは、子アクティビティや繰り返し処理するデータのコレクションの形式での作業があるかどうかを迅速にチェックすることです。データや子アクティビティがない場合は、アクティビティを閉じて、ワークフローの次のアクティビティに移動できます。ある場合は、繰り返し処理のインデックスが初期化され、ExecuteIteration メソッドを呼び出すと、目的の処理が開始されます。最後に、子に実行のスケジュールを設定しているため、Executing のステータスを返します。

ExecuteIteration メソッドは、子アクティビティを実行するために、新しい AEC の作成を管理する役割を果たします。

Activity act = EnabledActivities[0];

ActivityExecutionContext newContext =
    parentCtx.ExecutionContextManager.CreateExecutionContext(act);

newContext.Activity.RegisterForStatusChange(
    Activity.ClosedEvent, this);
newContext.Activity.SetValue(
    ForEach.DataItemProperty, Items[CurrentIteration]);

newContext.ExecuteActivity(newContext.Activity);

まず、ActivityExecutionContextManager (AECManager) を使用して新しい ActivityExecutionContext を作成します。AECManager は、子 AEC の作成および補正を制御するために使用する、AEC で使用可能なオブジェクトです。新しい AEC が作成されると、AECManager は渡されたアクティビティを複製して、実行時にこの新しいアクティビティを使用します。そのため、次の手順では、ステータスの変更を登録し、アクティビティが実行されるコンテキストを確認する際に、新しい AEC の Activity プロパティを使用してアクティビティを参照しています。ここで注目すべきことは、実行中の動的アクティビティでデータを使用できるように、子アクティビティの DataItemProperty をコレクションの現在のデータに設定していることです。

AEC を作成するために、AEC でルート アクティビティとして機能するアクティビティを渡していることに注意してください。これは、1 つのアクティビティに基づいて新しい AEC の作成が必要になったときに、アクティビティをデザインするうえで理解しておく必要がある重要な概念です。While アクティビティおよび Replicator アクティビティのように、新しい AEC を作成するアクティビティは、そのほとんどが子アクティビティを 1 つしか含むことができません。一見すると、これは制限のように思えますが、1 つの子としてSequence アクティビティを簡単に追加でき、その後 Sequence に必要な数だけ子を追加できます。子アクティビティを確実に 1 つだけ使用するには、2 つの手順があります。1 つはカスタム バリデータを作成することです。これにより、アクティビティには 1 つの子のみが含まれるようになります。もう 1 つは、デザイナを作成し、ForEach に複数のアクティビティを追加できないようにすることです。これらの手法は、どちらも ForEach アクティビティのサンプル コード内に含まれています。デザイナとバリデータの詳細については、2006 年 12 月号の私の記事にある「Windows ワークフロー : ワークフローの対象を拡大するためのカスタム アクティビティの構築」を参照してください。

AEC を管理する最後の手順では、コンテキストを完了するために子アクティビティの Closed イベントを処理します。このデリゲートの sender パラメータは、複合アクティビティの AEC です。これを使用して、アクティビティを制御したり、子 AEC を管理するために AECManager にアクセスすることができます。ActivityExecutionStatusChangedEventArgs には、閉じたアクティビティへの参照が含まれています。したがって、この参照を使用することにより、AECManager の GetExecutionContext メソッドでそのアクティビティの適切な AEC を取得できます。アクティビティの AEC を参照したら、AECManager を使用して AEC を閉じます。すべての子アクティビティが閉じられていること、および子 AEC がすべて完了していることが重要です。AEC が完了したら、データ項目が残っているかどうかをチェックできます。残っている場合はもう一度繰り返し処理を開始し、残っていない場合はアクティビティを閉じます。

ActivityExecutionContext ctx = sender as ActivityExecutionContext;

e.Activity.UnregisterForStatusChange(Activity.ClosedEvent, this);

ActivityExecutionContext childContext =
    ctx.ExecutionContextManager.GetExecutionContext(e.Activity);
ctx.ExecutionContextManager.CompleteExecutionContext(childContext);

if (++CurrentIteration >= Items.Count) ctx.CloseActivity();
else ExecuteIteration(ctx);

CompleteExecutionContext メソッドには、この時点でのワークフロー状態を保存する必要があるかどうかを示すブール型パラメータを取得するオーバーロードがあります。既定では、この値は False になっています。ただし ICompensatable インターフェイスを実装する子アクティビティが完了している場合は、このパラメータに渡された値に関係なく AEC が保存されます。このオーバーロードを使用すると、アクティビティのニーズに基づいて AEC を保存する必要があるかどうか、また、この状態を後で使用できるようにする必要があるかどうかに関連する情報を入力できます。

アクティビティでの最後の作業は、実行される子アクティビティの現在のインスタンスへの参照を取得できるように、他のコードの機能をサポートすることです。そのために、ForEach アクティビティに対して DynamicActivity プロパティを追加し、CompositeActivity 基本クラスから GetDynamicActivities メソッドを使用しています。このメソッドはアクティビティを受け取って、そのアクティビティの動的インスタンスの配列を返します。この場合、一度に 1 つの動的インスタンスしか存在しないため、常に配列の最初のアイテムを返すことができます。

public Activity DynamicActivity
{
    get
    {
        if (EnabledActivities.Count == 0) return null;
        Activity[] activities =
            GetDynamicActivities(EnabledActivities[0]);
        if (activities.Length > 0) return activities[0];
        return null;
    }
}

複合アクティビティ開発の他の側面でも、キャンセルやエラー処理など、AEC の管理を伴う場合があることを覚えておいてください。このような場合、適切な AEC にアクセスするためのパターンは、ここに示したものと変わりません。使用する手順はそれぞれのアクティビティによって異なります。

まとめ

ワークフロー開発者は、AEC がどのように機能するか、またどのような場合に AEC の使用を検討する必要があるかを理解することが重要です。同様に、AEC を処理するためのワークフロー、アクティビティ、およびルールのコーディング方法について理解することも重要です。単純なカスタム複合アクティビティを記述することは、他のアクティビティで AEC がどのように管理されているかを理解するための良い方法です。これを 1 つの学習方法として多くのワークフロー開発者の方々にお勧めします。

ご意見やご質問は、

こちら mmnet30@microsoft.com.

まで英語でお送りください。
Matt Milnerは、.NET、Web サービス、Windows Workflow Foundation、Windows Communication Foundation、BizTalk Server などのマイクロソフト テクノロジを専門とする独立系ソフトウェア コンサルタントです。Matt は、Pluralsight のインストラクタとして、Workflow、BizTalk Server、および Windows Communication Foundation のコースで教鞭をとっています。夫人の Kristen と 2 人の息子とミネソタ州に居を構えています。Matt の連絡先は、pluralsight.com/blogs/matt にあるブログです。


同時実行の問題: 非同期のデバイス操作

同時実行の問題
非同期のデバイス操作
Jeffrey Richter

コードのダウンロード : : ConcurrentAffairs2007_06.exe (162KB)
コンテンツ
CreateFile と DeviceIoControl
Win32 デバイス通信
マネージ コードでの同期デバイス I/O
マネージ コードでの非同期デバイス I/O
DeviceIoControl の Overlapped パラメータ
便宜的ロック
まとめ

前回のコラムでは、AsyncResultNoResult および AsyncResult<Tresult> という 2 つの基本クラスの実装方法について説明しました。これらのクラスは共に、共通言語ランタイム (CLR) の非同期プログラミング モデル (APM) の中核に位置する IAsyncResult インターフェイスを実装しています。 このコラムでは、AsyncResult<TResult> クラスを使用して APM を実装し、ハードウェア デバイスの操作を非同期的に実行できるようにします。このコラムを読めば、Microsoft® .NET Framework 自体がどのようにしてファイルやネットワークの I/O 操作を非同期的に実行するかも理解できます。このコードはすべて、wintellect.com にある私の Power Threading Library に収録されています。

CreateFile と DeviceIoControl

Win32® ベースのコーディングでファイルを操作する場合、まず Win32 CreateFile 関数を呼び出してファイルを開き、次に WriteFile や ReadFile などの関数を呼び出してファイルの操作を実行します。最終的には、ファイルの操作が完了した時点で CloseHandle 関数を呼び出してファイルを閉じます。

.NET Framework では、FileStream オブジェクトの作成により内部的に CreateFile が呼び出されます。FileStream の Read メソッドと Write メソッドの呼び出しによって Win32 の WriteFile 関数と ReadFile 関数がそれぞれ内部的に呼び出され、FileStream の Close メソッドまたは Dispose メソッドの呼び出しによって Win32 の CloseHandle 関数が内部的に呼び出されます。ファイルを操作する際、大部分のアプリケーションはファイルに対するデータの読み書き以外は必要としないため、必要な操作の大部分は FileStream クラスで実行することができます。

ただし、Windows® には、ファイルに対して実行可能な操作が他にも多数用意されています。比較的一般的な操作については、WriteFile、ReadFile、FlushFileBuffers、GetFileSize などの固有の Win32 関数が用意されています。一方、まれにしか使用されない操作については固有の Win32 関数が用意されておらず、代わりに DeviceIoControl という関数が 1 つ用意されています。この関数を使用すると、アプリケーションはファイル操作を行うデバイス ドライバ (NTFS ディスク ドライバなど) と直接通信することができます。まれにしか使用されない操作の例としては、便宜的ロック (microsoft.com/msj/0100/win32/win320100.aspx)、変更ジャーナルの操作 (microsoft.com/msj/0999/journal/journal.aspx)、ディスク ボリュームやファイルの圧縮、接合ポイントの作成、ディスクのフォーマットやパーティションの再作成、スパース ファイルの操作などがあります。

さらに、DeviceIoControl を使用すると、任意のアプリケーションと任意のハードウェア デバイス ドライバの間で通信を行うことができます。DeviceIoControl を使用すると、コンピュータのバッテリの状態の照会、LCD の明るさの照会や変更、CD チェンジャや DVD チェンジャの操作、USB デバイスの照会および取り出しなどをアプリケーションで実行できます。

Win32 デバイス通信

Win32 でアプリケーションとデバイスとの通信を実現する方法について簡単に見てみましょう。その後、この機能をマネージ コードから使用できるようにラップします。

Win32 では、CreateFile 関数を呼び出してデバイスを開きます。CreateFile の最初の引数は、開くデバイスを特定する文字列です。通常、この文字列にはパス名を指定し、CreateFile でファイルが開かれるようにします。ただし、特殊な文字列を渡すことによって CreateFile でデバイスを開くこともできます。図 1 に、使用可能な文字列の一部と、その文字列を使用した場合に CreateFile で開かれるデバイスの種類を示します。システムまたは管理者グループのメンバ以外はアクセスできないデバイスもあるため、アプリケーションの実行時のアクセス権限が要件を満たしていないとデバイスを開けない場合があります。

Figure 1 CreateFile の特殊文字列 (Close Figure 1)

CreateFile に渡す文字列 | 説明
@“\\.\PhysicalDrive1” | ハード ドライブ 1 を開いて、そのすべてのセクタにアクセスできるようにします。
@“\\.\C:” | C: ディスク ボリュームを開いて、このボリュームの変更ジャーナルを操作できるようにします。
@“\\.\Changer0” | ディスク チェンジャ 0 を開いて、ディスクの移動やその他の操作を実行できるようにします。
@“\\.\Tape0” | テープ ドライブ 0 を開いて、ファイルのバックアップや復元を行えるようにします。
@“\\.\COM2” | 通信ポート 2 を開いて、バイトを送受信できるようにします。
@“\\.\LCD” | LCD デバイスを開いて、明るさの照会や調整を行えるようにします。

以上で、デバイスを開く方法がわかりました。次に、Win32 の DeviceIoControl 関数について見てみましょう。

BOOL DeviceIoControl(
   HANDLE hDevice,           // Handle returned from CreateFile
   DWORD  dwIoControlCode,   // Operation control code
   PVOID  pInBuffer,         // Address of input buffer
   DWORD  nInBufferSize,     // Size, in bytes, of input buffer
   PVOID  pOutBuffer,        // Address of output buffer
   DWORD  nOutBufferSize,    // Size, in bytes, of output buffer
   PDWORD pBytesReturned,    // Gets number of bytes written to
                             // pOutBuffer
   POVERLAPPED pOverlapped); // For asynchronous operation

この関数を呼び出す場合、ハンドル パラメータは、CreateFile の呼び出しで取得されたファイル、ディレクトリ、またはデバイス ドライバを参照します。dwIoControlCode パラメータは、デバイスに対して実行する操作を示します。各操作は、この 32 ビットの整数コードで単純に識別されます。操作に引数が必要な場合は、引数をデータ構造体のフィールドに格納し、その構造体のアドレスを pInBuffer パラメータに渡します。構造体のサイズは nInBufferSize パラメータで渡します。同様に、操作がなんらかのデータを返す場合は、出力データをバッファに格納します。このバッファは、呼び出し元が割り当てる必要があります。このバッファのアドレスは pOutBuffer パラメータで渡します。このバッファのサイズは nOutBufferSize パラメータで渡します。DeviceIoControl は、戻り時に、実際に出力バッファに書き込まれたバイト数を pBytesReturned パラメータ (このパラメータは参照で渡します) に書き込みます。同期操作を実行する場合は、最後に pOverlapped パラメータに NULL を渡します。ただし、非同期操作を実行する場合は、OVERLAPPED 構造体のアドレスを渡す必要があります。これについては、このコラムの後半で詳しく説明します。

以上で、Win32 を使用してデバイスと通信する方法がわかりました。次に、これらの Win32 関数のマネージ ラッパーの記述を開始し、ハードウェア デバイスと直接通信するためのコードを .NET 言語で記述できるようにします。

マネージ コードでの同期デバイス I/O

まず、同期デバイス操作の実行方法に焦点を当てて説明します。その後、非同期操作の実行に必要なコードを追加します。ここでは、Win32 の CreateFile 関数および DeviceIoControl 関数用の使いやすいラッパーとして DeviceIO 静的クラスを C# で定義しています。図 2 に、このクラスのパブリック オブジェクト モデルを示します。

Figure 2 CreateFile と DeviceIoControl のラップ (Close Figure 2)
public static class DeviceIO {
   public static SafeFileHandle OpenDevice(String deviceName,
      FileAccess access, FileShare share, Boolean useAsync);

   public static void Control(SafeFileHandle device,
      DeviceControlCode deviceControlCode);

   public static void Control(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer);

   public static TResult GetObject<TResult>(SafeFileHandle device,
      DeviceControlCode deviceControlCode) where TResult: new();

   public static TResult GetObject<TResult>(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer)
          where TResult : new();

   public static TElement[] GetArray<TElement>(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer,
      Int32 maxElements) where TElement : struct;
}

マネージ アプリケーションでは、OpenDevice を呼び出して、図 1 に示したような文字列と、必要なアクセスおよび共有を渡すことができます。OpenDevice は、デバイスへのハンドルを返す CreateFile を内部的に呼び出します。このハンドルは、実際には SafeFileHandle オブジェクトでラップされて返されます。これにより、ハンドルが最終的に閉じられることが保証されると共に、すべての SafeHandle 派生クラスから得られるその他のメリットを利用できるようになっています。この SafeFileHandle オブジェクトは、DeviceIO の他の静的メソッドへの最初の引数として渡すことができます。このオブジェクトは最終的に、Win32 DeviceIoControl 関数の呼び出し時に最初の引数として渡されます。

他のすべてのメソッドへの 2 番目の引数は DeviceControlCode です。これは、デバイスに送信するコマンド コードを識別する Int32 のプライベート インスタンス フィールドを 1 つだけ含む単純な構造体 (値型) です。Int32 のコマンド コードをそれ自体の型にラップすると、コードがより読みやすく、よりタイプセーフになり、Visual Studio® の IntelliSense® のメリットを利用できるようになることがわかりました。DeviceControlCode インスタンスに含まれている Int32 値は、内部的に、Win32 DeviceIoControl 関数への 2 番目の引数として渡されます。

デバイスの制御コードを見てみると、いくつかの共通パターンがあることがわかります。そこで私は、これらの共通パターンを簡単に扱える使いやすいメソッドを提供することにしました。これらのメソッドはいずれも同期的に操作を実行します。つまり、操作が完了するまでは呼び出し元のスレッドに制御が戻りません。また、これらのメソッドを呼び出す場合は、OpenDevice の呼び出し時に useAsync 引数に false を指定する必要があります。

Control メソッドは、送信する制御コードによりデバイスでなんらかの操作が実行されるものの結果データが返されないパターンをカプセル化します。SafeFileHandle と制御コードのみを受け取る Control メソッドと、追加の Object 引数を受け取るオーバーロードがあります。追加の Object 引数を使用すると、操作に使用する追加データをデバイスに渡すことができます。Win32 SDK ドキュメントで制御コードを調べれば、各制御コードに渡す必要のある追加情報 (必要な場合) がわかります。追加データが必要な場合は、Win32 データ型と同等のマネージ型を定義してインスタンスを作成し、フィールドを初期化して、そのインスタンスへの参照を inBuffer パラメータに渡す必要があります。

GetObject<TResult> メソッドは、送信する制御コードによりデバイスからアプリケーションになんらかのデータが返されるパターンをカプセル化します。返されるデータを取得するには、Win32 データ型と同等のマネージ型を定義し、GetObject の呼び出し時にこの型をジェネリック型 TResult として指定する必要があります。内部的には、GetObject がこの型のインスタンスを作成し (このため、新しい制約がジェネリック型に適用されます)、デバイスが DeviceIoControl の呼び出しの内部でフィールドを初期化し、GetObject が初期化されたオブジェクトを返します。GetObject を呼び出す場合、2 つのオーバーロードを使用できます。これらのオーバーライドを使用すると、必要に応じて inBuffer パラメータで追加データをデバイスに渡すことができます。

GetArray<TElement> メソッドは、送信する制御コードによりデバイスからアプリケーションに要素の配列が返される最後のパターンをカプセル化します。これらの要素を取得するには、Win32 要素のデータ型と同等のマネージ型を定義し、GetArray の呼び出し時にこの型をジェネリック型 TElement として指定する必要があります。DeviceIoControl を使用するには配列のメモリが直線的に並んでいることが条件になるため、マネージ データ型は構造体 (値型) として定義する必要があります。TElement が構造体に制限されるのはこのためです。また、GetArray を呼び出す際には、デバイスから返される要素の最大数を maxElements パラメータで指定する必要があります。

内部的には、GetObject がこれらの要素の配列を作成してその配列のアドレスを DeviceIoControl に渡し、DeviceIoControl が配列の各要素を初期化します。DeviceIoControl は、戻り時に、実際に配列に格納されているバイト数を返します。GetArray は、この値を使用して配列を正しいサイズに縮小し (必要な場合)、配列内の要素の数が初期化された要素の数に等しくなるようにします。これは非常に便利です。なぜなら、(GetArray から返された) この配列を使用する任意のコードで配列の Length プロパティを照会して項目数を取得し、この値をループ内で使用して返された要素を処理できるからです。

図 3 に、Win32 DisplayBrightness 構造体と同等のマネージ構造体とそのヘルパ DisplayPowerFlags 列挙型を示します。図 4 に、DeviceIO 静的クラスを使用して LCD の明るさを絶えず調整する AdjustBrightness メソッドを示します。これは、実際に役立つアプリケーションではなく、基本的な概念を示すための単純な例に過ぎません。

Figure 4 LCD の明るさの調整 (Close Figure 4)
public static void AdjustBrightness(6) {
   // The code for setting LCD brightness; obtained by looking up
   // IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS in Platform SDK documentation
   DeviceControlCode s_SetBrightness =
      new DeviceControlCode(DeviceType.Video, 0x127,
         DeviceMethod.Buffered, DeviceAccess.Any);

   // Open the device
   using (SafeFileHandle lcd = DeviceIO.OpenDevice(@”\\.\LCD”,
      FileAccess.ReadWrite, FileShare.ReadWrite, false)) {

      for (Int32 times = 0; times < 10; times++) {
         for (Int32 dim = 0; dim <= 100; dim += 20) {
            // Initialize the equivalent of a Win32 DISPLAY_BRIGHTNESS
            // structure
            DisplayBrightness db =
               new DisplayBrightness(DisplayPowerFlags.ACDC,
                  (Byte)((times % 2 == 0) ? dim : 100 - dim));

            // Tell the LCD device to adjust its brightness
            DeviceIO.Control(lcd, s_SetBrightness, db);

            // Sleep for a bit and then adjust it again
            Thread.Sleep(150);
         }
      }
   }
}
Figure 3 マネージ DisplayBrightness 構造体 (Close Figure 3)
[Flags]
internal enum DisplayPowerFlags : byte {
   None = 0x00000000, AC = 0x00000001, DC = 0x00000002, ACDC = AC | DC
}

internal struct DisplayBrightness {
   public DisplayPowerFlags m_Power;
   public Byte m_ACBrightness; // 0-100
   public Byte m_DCBrightness; // 0-100

   public Win32DisplayBrightnessStructure(
         DisplayPowerFlags power, Byte level) {
      m_Power = power;
      m_ACBrightness = m_DCBrightness = level;
   }
}

マネージ コードでの非同期デバイス I/O

LCD の明るさの変更などのデバイス操作は、実際には I/O 操作ではありません。したがって、この種の操作を実行する場合は、DeviceIoControl を同期的に呼び出すのが合理的です。ただし、DeviceIoControl の呼び出しの大部分は、(関数名が示すとおり) 最終的には I/O になります。したがって、これらの操作は非同期的に実行するのが合理的です。ここでは、ファイル、ディスク、およびその他のハードウェア デバイスに対する非同期 I/O 操作を実行するために、マネージ コードから P/Invoke で Win32 の DeviceIoControl 関数を呼び出す方法を説明します。

DeviceIO 静的クラスには、CLR の APM を使用した非同期バージョンの Control メソッド、GetObject メソッド、および GetArray メソッドが用意されています。図 5 に、APM をサポートするこのクラスのパブリック メソッド (初出) を示します。

Figure 5 DeviceIO の非同期メソッド (Close Figure 5)
public static class DeviceIO {
   public static IAsyncResult BeginControl(SafeFileHandle device,
      DeviceControlCode deviceControlCode, Object inBuffer,
      AsyncCallback asyncCallback, Object state);

   public static void EndControl(IAsyncResult result);

   public static IAsyncResult BeginGetObject<TResult>(
      SafeFileHandle device, DeviceControlCode deviceControlCode,
      Object inBuffer, AsyncCallback asyncCallback, Object state)
      where TResult: new();

   public static TResult EndGetObject<TResult>(IAsyncResult result)
      where TResult: new();

   public static IAsyncResult BeginGetArray<TElement>(
      SafeFileHandle device, DeviceControlCode deviceControlCode,
      Object inBuffer, Int32 maxElements, AsyncCallback asyncCallback,
      Object state) where TElement: struct;

   public static TElement[] EndGetArray<TElement>(IAsyncResult result)
      where TElement: struct;
}

おわかりのように、BeginXxx メソッドはすべて、CLR の APM パターンに従います。つまり、これらのメソッドは IAsyncResult を返し、最後の 2 つのパラメータは AsyncCallback と Object になります。追加のパラメータは、各メソッドの同期バージョンのパラメータに一致します。同様に、EndXxx メソッドはすべて、1 つのパラメータ (IAsyncResult) を受け取り、各メソッドの同期バージョンと同じデータ型を返します。

これらの非同期メソッドを呼び出すには、次の 2 つの処理を実行する必要があります。第 1 に、デバイスに対して非同期操作を実行することを Windows に通知します。これを行うには、FILE_FLAG_OVERLAPPED フラグ (数値は 0x40000000) を CreateFile 関数に渡します。このフラグと同等のマネージ フラグは FileOptions.Asynchronous (当然ながら数値は同じく 0x40000000) です。第 2 に、操作完了エントリを CLR のスレッド プールに入れるようにデバイス ドライバに指示する必要があります。これを行うには、ThreadPool の BindHandle 静的メソッドを呼び出します。このメソッドは、1 つのパラメータ (SafeHandle 派生オブジェクトへの参照) を受け取ります。

DeviceIO の OpenDevice メソッドの useAsync パラメータに true を渡すと、これらの処理が両方とも実行されます。OpenDevice メソッドの実装方法を次に示します。

public static SafeFileHandle OpenDevice(String deviceName,
   FileAccess access, FileShare share, Boolean useAsync) {

   SafeFileHandle device = CreateFile(deviceName, access, share,
      IntPtr.Zero, FileMode.Open,
      useAsync ? FileOptions.Asynchronous : FileOptions.None,
      IntPtr.Zero);

   if (device.IsInvalid) throw new Win32Exception();
   if (useAsync) ThreadPool.BindHandle(device);

   return device;
}

次に、BeginGetObject および EndGetObject が内部的にどのように動作するかを見てみましょう。その他の非同期メソッドも同様に動作するため、BeginGetObject および EndGetObject について先に学んでおくと、各メソッドの動作を簡単に理解することができます。BeginXxx メソッドはいずれも非常に単純です。操作がなんらかの戻り値または要素の配列を返す場合はオブジェクトまたは配列を作成し、内部のヘルパ メソッドである AsyncControl を直ちに呼び出します。AsyncControl の実装を図 6 に示します。

Figure 6 AsyncControl (Close Figure 6)
private static DeviceAsyncResult<T> AsyncControl<T>(
    SafeFileHandle device,
   DeviceControlCode deviceControlCode, Object inBuffer, T outBuffer,
   AsyncCallback asyncCallback, Object state) {

   SafePinnedObject inDeviceBuffer = new SafePinnedObject(inBuffer);
   SafePinnedObject outDeviceBuffer = new SafePinnedObject(outBuffer);
   DeviceAsyncResult<T> asyncResult = new DeviceAsyncResult<T>(
      inDeviceBuffer, outDeviceBuffer, asyncCallback, state);

   unsafe {
      Int32 bytesReturned;
      NativeControl(device, deviceControlCode, inDeviceBuffer,
          outDeviceBuffer, out bytesReturned,
          asyncResult.GetNativeOverlapped());
   }
   return asyncResult;
}

操作を実行する場合、追加の入力データが格納されているバッファのアドレスを DeviceIoControl に渡すことができます。DeviceIoControl がデータを返す場合は、DeviceIoControl を呼び出す前にそのデータ用のメモリを割り当てて、DeviceIoControl がデータを初期化できるようにする必要があります。このデータは操作の完了時に確認されます。問題点は、操作の完了までに理論上何時間もかかる場合があり、その間にガベージ コレクションが発生してデータが移動される可能性があるということです。その場合、DeviceIoControl に渡されたアドレスが目的のバッファを参照しなくなり、メモリの破損が発生します。

オブジェクトを固定することにより、オブジェクトを移動しないよう GC に指示できます。SafePinnedObject クラスは、メモリ内のオブジェクトを固定して GC による移動を防ぐためのヘルパ クラスですが、SafeHandle から派生しているため、SafeHandle 派生型から通常得られるメリットをすべて利用できます。これには、将来のある時点で確実にオブジェクトの固定が解除されるというメリットも含まれます。SafePinnedObject クラスの実装方法を図 7 に示します (スペースの節約のためにいくつかの検証コードを取り除いています)。SafeHandle 派生型とメモリ内のオブジェクトの固定方法の詳細については、私の著書『プログラミング Microsoft .NET Framework 第 2 版』 (日経 BP ソフトプレス、2006) を参照してください。

Figure 7 SafePinnedObject を使用したメモリ内のオブジェクトの固定 (Close Figure 7)
public sealed class SafePinnedObject : SafeHandleZeroOrMinusOneIsInvalid {
   private GCHandle m_gcHandle;  // Handle of pinned object (or 0)

   public SafePinnedObject(Object obj) : base(true) {
      // If obj is null, we create this object but it pins nothing
      if (obj == null) return;

      // If specified, pin the buffer and save its memory address
      m_gcHandle = GCHandle.Alloc(obj, GCHandleType.Pinned);
      SetHandle(m_gcHandle.AddrOfPinnedObject());
   }

   protected override Boolean ReleaseHandle() {
      SetHandle(IntPtr.Zero); // Just for safety, set the address to null
      m_gcHandle.Free();      // Unpin the object
      return true;
   }

   // Returns the object of a pinned buffer or null if not specified
   public Object Target {
      get { return IsInvalid ? null : m_gcHandle.Target; }
   }

   // Returns the number of bytes in a pinned object or 0 if not specified
   public Int32 Size {
      get {
         Object target = Target;

         // If obj was null, return 0 bytes
         if (target == null) return 0;

         // If obj is not an array, return the size of it
         if (!target.GetType().IsArray) return Marshal.SizeOf(target);

         // obj is an array, return the total size of the array
         Array a = (Array)target;
         return a.Length * Marshal.SizeOf(a.GetType().GetElementType());
      }
   }
}

入力バッファと出力バッファの両方を固定した後、AsyncControl は、DeviceAsyncResult<Tresult> オブジェクトを作成し、このオブジェクトに 2 つの SafePinnedObject オブジェクト (AsyncCallback および Object) を渡します。DeviceAsyncResult<TResult> は、実装内部のもう 1 つの型です。前回のコラムで説明したように、この型は AsyncResult<Tresult>から派生しています。これは、この型が APM の IAsyncResult インターフェイスを実装していることを意味します。DeviceAsyncResult オブジェクトは、その作成時に、受け取った引数をすべてプライベート フィールドに保存して戻ります。

DeviceIoControl の Overlapped パラメータ

以上で今回の準備作業はすべて完了し、AsyncControl から DeviceIoControl を呼び出せる状態になりました。AsyncControl は、DeviceIoControl を呼び出すために NativeControl を呼び出し、デバイス ハンドル、制御コード、入力バッファ、出力バッファ、および Int32 の bytesReturned 変数への参照 (メソッドは操作の完了前に戻るため、この参照は基本的に無視されます) を渡します。この中で最も重要なのは最後の引数です。非同期操作を実行する場合、DeviceIoControl には NativeOverlapped 構造体 (Win32 OVERLAPPED 構造体と同等の構造体) のアドレスを渡す必要があります。AsyncControl は、DeviceAsyncResult クラスで定義されているヘルパ メソッド GetNativeOverlapped を呼び出してこの構造体を取得します。このヘルパ メソッドの実装を次に示します。

// Create and return a NativeOverlapped structure to be passed
// to native code
public unsafe NativeOverlapped* GetNativeOverlapped() {
   // Create a managed Overlapped structure that refers to our
   // IAsyncResult (this)
   Overlapped o = new Overlapped(0, 0, IntPtr.Zero, this);

   // Pack the managed Overlapped structure into a NativeOverlapped
   // structure
   return o.Pack(CompletionCallback,
      new Object[] { m_inBuffer.Target, m_outBuffer.Target });
}

このメソッドは、System.Threading.Overlapped クラスのインスタンスを作成して初期化します。これは、OVERLAPPED 構造体を設定および操作するためのマネージ ヘルパ クラスです。ただし、このオブジェクトへの参照をネイティブ コードに渡すことはできません。代わりに、まず Overlapped オブジェクトを NativeOverlapped オブジェクトにパックして、その NativeOverlapped オブジェクトへの参照をネイティブ コードに渡す必要があります。Pack を呼び出す際には、操作の完了時にスレッド プールのスレッドが実行するコールバック メソッドを参照するデリゲートを渡します。上記のコードでは、CompletionCallback メソッドがこれに当たります。

Overlapped クラスの Pack メソッドを呼び出すと、複数の処理が実行されます。まず、ネイティブの OVERLAPPED 構造体のメモリをマネージ ヒープから割り当てて固定することにより、GC が発生してもメモリが移動されないようにします。次に、マネージ Overlapped オブジェクト内に設定されているフィールドを使用して、NativeOverlapped 構造体のフィールドを初期化します。この処理では、Overlapped オブジェクトが参照する IAsyncResult オブジェクトの通常の GCHandle が作成されます。これにより、操作を実行している間、IAsyncResult オブジェクトが有効に保たれるようになります。

Pack は次に、コールバック メソッド (CompletionCallback) のデリゲートを固定し、その固定アドレスをネイティブ コードに渡せるようにすることで、この操作が完了したときにネイティブ コードがマネージ コードにコールバックできるようにします。Pack はさらに、その 2 番目のパラメータである Object 配列によって参照されている追加オブジェクトをすべて固定します。上記の場合、inBuffer オブジェクトと outBuffer オブジェクトは SafePinnedObject クラスでラップしたため、既に固定されています。GCHandle の AddrOfPinnedObject メソッドの呼び出しでアドレスを取得できるように、これらのオブジェクトは自分で固定する必要があります。

ただし、Pack メソッドは、これらのオブジェクトを特殊な方法で再度固定します。通常、AppDomain がアンロードされると、CLR によって自動的にオブジェクトが固定解除されてガベージ コレクションの対象になり、メモリ リークが回避されるようになっています。しかし、Pack は、非同期操作が完了するまでオブジェクトを固定します。そのため、非同期操作の開始後に AppDomain がアンロードされた場合、Pack によって固定されたオブジェクトは固定解除されません。それらのオブジェクトは操作の完了後に固定解除されてガベージ コレクションの対象になります。これにより、メモリの破損が回避されます。

Pack はさらに、Pack を呼び出した AppDomain を記録して、CompletionCallback メソッドを呼び出すスレッド プールのスレッドが同じ AppDomain 内で実行されるようにします。Pack は最後に、コード アクセス セキュリティ権限のスタックをキャプチャします。これは、コールバック メソッドの実行時に、非同期操作を開始したコードと同じセキュリティ権限でコールバック メソッドが実行されるようにするためです。セキュリティ権限のスタックが適用されないようにするには、Overlapped クラスの UnsafePack メソッドを使用します。

Pack は、固定された NativeOverlapped 構造体のアドレスを返します。このアドレスは、Win32 OVERLAPPED 構造体のアドレスを必要とする任意のネイティブ関数に渡すことができます。AsyncControl メソッドでは、NativeOverlapped 構造体のアドレスが NativeControlに渡され (図 8 を参照)、NativeControl から Win32 DeviceIoControl 関数に渡されます。

Figure 8 NativeControl (Close Figure 8)
private static unsafe void NativeControl(SafeFileHandle device,
   DeviceControlCode deviceControlCode,
   SafePinnedObject inBuffer, SafePinnedObject outBuffer,
   out Int32 bytesReturned, NativeOverlapped* nativeOverlapped) {

   Boolean ok = DeviceIoControl(device, deviceControlCode.Code,
      inBuffer, inBuffer.Size, outBuffer, outBuffer.Size,
      out bytesReturned, nativeOverlapped);
   if (ok) return;

   Int32 error = Marshal.GetLastWin32Error();
   const Int32 c_ErrorIOPending = 997;
   if (error == c_ErrorIOPending) return;
   throw new InvalidOperationException(
      String.Format(“Control failed (code={0})”, error));
}

非同期操作を実行する場合、DeviceIoControl はすぐに戻ります。最終的には、IAsyncResult インターフェイスを実装する DeviceAsyncResult オブジェクトが DeviceIO の BeginObject<Tresult> メソッドから返されます。

操作が完了すると、デバイス ドライバは、CLR のスレッド プールのキューにエントリを追加します。以前に説明したように、ドライバがこの処理の必要性を認識するのは、非同期アクセス用にデバイスが開かれた時点で DeviceIO の OpenDevice メソッドが内部的に ThreadPool の BindHandle メソッドを呼び出すためです。最終的に、スレッド プールのスレッドはキュー内のエントリを抽出し、保存済みの権限セットを採用し、正しい AppDomain にジャンプして CompletionCallback メソッドを呼び出します (図 9 を参照)。CompletionCallback メソッドは、AsyncResult<TResult> クラスから派生した DeviceAsyncResult<TResult> クラスの内部で定義されるということを忘れないでください。

Figure 9 CompletionCallback (Close Figure 9)
// Called by a thread pool thread when native overlapped I/O completes
private unsafe void CompletionCallback(UInt32 errorCode, UInt32 numBytes,
   NativeOverlapped* nativeOverlapped) {
   // Release the native OVERLAPPED structure and
   // let the IAsyncResult object (this) be collectable.
   Overlapped.Free(nativeOverlapped);

   try {
      if (errorCode != 0) {
         // An error occurred, record the Win32 error code
         base.SetAsCompleted(new Win32Exception((Int32)errorCode), false);
      } else {
         // No error occurred, the output buffer contains the result
         TResult result = (TResult)m_outBuffer.Target;

         // If the result is an array of values, resize the array
         // to the exact size so that the Length property is accurate
         if ((result != null) && result.GetType().IsArray) {
            // Only resize if the number of elements initialized in the
            // array is less than the size of the array itself
            Type elementType = result.GetType().GetElementType();
            Int64 numElements = numBytes / Marshal.SizeOf(elementType);
            Array origArray = (Array)(Object)result;
            if (numElements < origArray.Length) {
               // Create new array (size equals number of initialized
               // elements)
               Array newArray = Array.CreateInstance(
                   elementType, numElements);

               // Copy initialized elements from original array to new
               // array
               Array.Copy(origArray, newArray, numElements);
               result = (TResult)(Object)newArray;
            }
         }
         // Record result and call AsyncCallback method passed to BeginXxx
         // method
         base.SetAsCompleted(result, false);
      }
   }
   finally {
      // Make sure that the input and output buffers are unpinned
      m_inBuffer.Dispose();
      m_outBuffer.Dispose();
      m_inBuffer = m_outBuffer = null;   // Allow early GC
   }
}

CompletionCallback メソッドで最初に実行されるのは nativeOverlapped オブジェクトの解放です。この処理により、IAsyncResult オブジェクトの GCHandle が解放され、Overlapped クラスの Pack メソッドに渡されたすべてのオブジェクトの固定が解除されます。また、NativeOverlapped オブジェクト自体の固定も解除されてガベージ コレクションの対象になります。したがって、この処理は非常に重要です。NativeOverlapped オブジェクトを解放し忘れると、オブジェクトがいくつかリークすることになります。

CompletionCallback の残りのコードは非常に単純です。操作が失敗したかどうかや操作の結果を AsyncResult<T> 基本クラスの状態に記録するだけです (DeviceIO の BeginGetArray を呼び出して操作を開始した場合は、配列のサイズが調整されます)。CompletionCallback の finally ブロックには、SafePinnedObject オブジェクトを破棄するためのコードが記述されています。これにより、入力バッファと出力バッファの固定が解除されて GC で最適化できるようになり、最終的には、アプリケーションでこれらのバッファが不要になったときにそのメモリを GC で再利用できるようになります。

アプリケーション コードは、ある時点で DeviceIO の EndControl メソッド、EndGetObject メソッド、または EndGetArray メソッドを呼び出し、対応する Begin メソッドから返された DeviceAsyncResult<Tresult> オブジェクトを渡します。内部的には、これらの End メソッドはいずれも AsyncResult<Tresult> の EndInvoke メソッドを呼び出すだけです。これにより、EndInvoke メソッドに設定されている例外が CompletionCallback の内部でスローされるか、または EndInvoke メソッドに設定されている値が返されます。

便宜的ロック

便宜的ロックは、非同期デバイス操作を示す単純な例です。図 10 に示す OpLockDemo 静的クラスの Main メソッドは、FileOptions.Asynchronous フラグを使用してファイルを作成します。次に、FileStream 内に埋め込まれた SafeFileHandle が BeginFilter に渡されます。BeginFilter は、DeviceIO の BeginControl メソッドを内部的に呼び出し、要求フィルタの便宜的ロックをファイルに対して設定するコードを渡します。これ以降、ファイル システムのデバイス ドライバは、他のアプリケーションがファイルを開こうとしているかどうかを監視するようになります。他のアプリケーションがファイルを開こうとした場合、デバイス ドライバは、CLR のスレッド プールのキューにエントリを追加します。続いて、s_earlyEnd フィールドを 1 (他のアプリケーションがファイルへのアクセスを必要としていることを示します) に設定する匿名メソッドが呼び出されます。最初のアプリケーションによってファイルが閉じられ、他のアプリケーションがそのファイルにアクセスして処理を続行できるようになります。

Figure 10 便宜的ロックの例 (Close Figure 10)
internal static class OpLockDemo {
   private static Byte s_endEarly = 0;

   public static void Main() {
      String filename = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
         @”FilterOpLock.dat”);

      // Attempt to open/create the file for processing (must be
      // asynchronous)
      using (FileStream fs = new FileStream(
             filename, FileMode.OpenOrCreate,
         FileAccess.ReadWrite, FileShare.ReadWrite, 8096,
         FileOptions.Asynchronous)) {

         // Request the Filter Oplock on the file.
         // When another process attempts to access the file, the system
         // will
         //    1. Block the other process until we close the file
         //    2. Call us back notifying us that another process wants to
         // access the file
         BeginFilter(fs.SafeFileHandle,
            delegate(IAsyncResult result) {
               EndFilter(result);
               Console.WriteLine(“Another process wants to access “ +
                                “the file or the file closed”);
               Thread.VolatileWrite(ref s_endEarly, 1);  // Tell Main
                                                         // thread to end
                                                         // early
            }, null);

         // Pretend we’re accessing the file here
         for (Int32 count = 0; count < 100; count++) {
            Console.WriteLine(“Accessing the file ({0})...”, count);

            // If the user hits a key or if another application
            // wants to access the file, close the file.
            if (Console.KeyAvailable ||
               (Thread.VolatileRead(ref s_endEarly) == 1)) break;
            Thread.Sleep(150);
         }
      }  // Close the file here allows the other process to continue
         // running
   }

   // Establish a Request filter opportunistic lock
   private static IAsyncResult BeginFilter(SafeFileHandle file,
      AsyncCallback asyncCallback, Object state) {

      // See FSCTL_REQUEST_FILTER_OPLOCK in Platform SDK’s WinIoCtl.h file
      DeviceControlCode RequestFilterOpLock =
         new DeviceControlCode(DeviceType.FileSystem, 23,
            DeviceMethod.Buffered, DeviceAccess.Any);

      return DeviceIO.BeginControl(file, RequestFilterOpLock, null,
         asyncCallback, state);
   }

   private static void EndFilter(IAsyncResult result) {
      DeviceIO.EndControl(result);
   }
}

これをテストするには、OpLockDemo アプリケーションを起動します。次に、デスクトップに移動して、アプリケーションが作成した FilterOpLock.dat ファイルを削除します。これにより匿名メソッドが呼び出され、サンプル アプリケーションが終了します。

まとめ

Windows およびそのデバイス ドライバは、.NET Framework クラス ライブラリでラップされていないさまざまな機能を提供しています。しかし、さいわいなことに、.NET Framework にはしかるべきメカニズムが用意され、P/Invoke でこれらの有用な機能にアクセスできるようになっています。これらの操作を非同期的に実行できるということは、取りも直さず、これらの機能を活かして堅牢性、信頼性、拡張性を兼ね備えたアプリケーションを作成できることを意味します。

ご意見やご質問は、

Jeffrey mmsync@microsoft.com.

までお送りください。
Jeffrey Richterは、アーキテクチャのレビュー、トレーニング、およびコンサルティング サービスを提供する Wintellect 社の共同設立者です。彼の著書には『プログラミング Microsoft .NET Framework 第 2 版』 (日経 BP ソフトプレス、2006) などがあります。Jeffrey は、MSDN Magazine の寄稿編集者で、1990 年から Microsoft のコンサルティングを担当しています。


Netting C++: EEK! の概要

Netting C++
EEK! の概要
Stanley B. Lippman

前シリーズのコラムでは、Microsoft® .NET Framework に既存の ISO-C++ アプリケーションを徐々に移植して、C++/CLI の相互運用機能を説明し、C++ オブジェクト モデルと .NET オブジェクト モデルの違いを明らかにしました。このシリーズのコラムでは、完全な .NET 準拠の言語として C++/CLI を使用することを検討しましょう。理由はすぐに明らかにしますが、作成するアプリケーションのコードネームは EEK! です。現在、私は、MMOG (多人数参加型オンライン ゲーム) のサーバー側での作業にほとんどの時間を費やしているので、EEK! は、そのドメインのある状況を表しています。ただし、コラムの限られたスペースに対応するために単純化しており、かなり控えめに表現していますが。

仕事以外での私の趣味は、やはり控えめな言い方をすれば、分子生物学とその細かいレベルでのできごとが、どのように身の回りで観察される行動を引き起こすのかについて考えることです。ショウジョウバエと大腸菌 (バクテリア) にはかないませんが (私はどちらにも特に興味はありません)、ハツカネズミは、その他のあらゆる生物の中で最も広く科学によって研究されてきているでしょう。しかし、ハツカネズミは、じっくり観察してみると、妙な親しみを感じさせます。彼らは子供が巣から落ちると協力して引き上げます。彼らには個性の違いがあります。また、彼らは、個としても世代を超えた集団としても、経験を記憶してそこから学ぶようです。

米国の Dian Fossey のように、わずかな予算で、これまでネズミを水槽の中で飼い、彼らを人類学的に観察し (多少雑なのは認めます)、写真に撮ってきました。EEK! は、その環境やネズミの行動をモデル化しようとする試みです。フレームワークを EEK! というコードネームにしたのは、Disney Animation のコンピュータ グラフィックス仲間に敬意を表してのことです。彼らはその音が、自分が保菌状態になったことに気付いたときのネズミの鳴き声だと断言しています。

さて、何から始めましょうか。まず、デザインを実装する一連のクラスを示す必要があります。これらの一部は、アプリケーションの記述から抽出されます。水槽、ネズミ、時間の経過を知らせる方法、および時間の経過と共に起こる変化を表示して、場合によっては保存する方法が必要になります。このシリーズの作業では、できるだけ多くの .NET テクノロジを利用するつもりです。

デザインから抽出されるクラスは、主にアプリケーションの目的によって決定されます。EEK! を、オンライン仮想環境にして、そこにユーザーが仮想ネズミとしてログオンし、お互いや環境と対話するようにすると、これから数か月間続ける予定のデザインは、対話型のゲームプレイを円滑に進めるものではないので、最適なデザインではなくなるでしょう。それは、言い換えると、間違った詳細レベルのモデル化です。つまり、同じドメイン空間から生じるデザインは、主としてアプリケーションの目的の結果であって、ドメイン空間自体の目的の結果ではありません。

ネズミは哺乳類なので人間と数多くの特徴を共有していますが、その意図は、はるかに単純で目的もずっと少ないように見えます。では、何がネズミに特定のタイミングで特定の行動を起こさせるのでしょうか。その行動のほとんどは、まったくの無意識のうちに、分子レベルで、その環境のにおいによって引き起こされることがわかります。そのにおいは、フェロモンと呼ばれる化学物質によって排出されます。まさにこれがこのデザインで取得し、調査しようと考えていることで、やりがいのあることです。

水槽 (Tank) とネズミ (Mouse) は、より抽象的な一連のクラスの具体的なインスタンスで、シミュレーション フレームワークの階層のルートになります。図 1 を見ると、階層の概要がわかります。

Figure 1 シミュレーション フレームワークの階層 (Close Figure 1)
public ref class Environment abstract;
public ref class Entity abstract;
public ref class NonSentientEntity : Entity abstract;
public ref class SentientEntity : Entity abstract;

public ref class Mouse : SentientEntity;
public ref class Tank : Environment;

public ref class ExerciseWheel : NonSentientEntity;
public ref class WaterDispenser : NonSentientEntity;
Figure 1 シミュレーション フレームワークの階層 (Close Figure 1)
public ref class Environment abstract;
public ref class Entity abstract;
public ref class NonSentientEntity : Entity abstract;
public ref class SentientEntity : Entity abstract;

public ref class Mouse : SentientEntity;
public ref class Tank : Environment;

public ref class ExerciseWheel : NonSentientEntity;
public ref class WaterDispenser : NonSentientEntity;

感覚を持つ実体 (SentientEntity) には、環境 (Environment) を知覚する能力と体内の状態を感知して目的を持った行動で反応する能力があります。したがって、一連の行動を表して、その行動の開始、終了、および進化を可能にする必要があります。つまり、行動は動作と状態をキャプチャする必要があり、状態は記憶の形式を表してその後の行動を変更します。

感覚を持たない実体 (NonSentientEntity) とは、感覚を持つ実体が対話する可能性がある環境内のすべてのことです。感覚を持たない実体は、機能を持つ場合と、持たない場合がありますが、環境におけるその存在を表す状態を維持します。この存在に対しては、何らかの形で通信する必要があります。

ネズミは、その環境や環境内の物をどのように認識するのでしょうか。一例を挙げると、すべての実体 (Entity) が、その環境のすべての状況を認識しているとは限りません。同様に、すべての実体が、環境の状況を同じように認識するとは限りません。たとえば、ネズミはとても小さいので、その視覚能力は限られています。明暗の区別はできますが、人間のような方法での視覚には依存しません。ネズミは、人間よりはるかに鋭い嗅覚を持つことで視覚を補っていて、前述したとおり、ネズミの行動の多くはこの嗅覚によって引き起こされます。視覚、嗅覚、触覚、聴覚を、感覚様相といいますが、ここでは、これらをモデル化します。

環境は、感覚様相の集まりから構成され、感覚様相の構成は時間の経過と共に変化します。感覚様相によって実体をその環境に結び付ける方法は、標準テンプレート ライブラリ (STL) のデザインで、反復子によってコンテナをアルゴリズムに結び付ける方法と似ています。つまり、各実体は一連の流動的な感覚様相を保持し、それによって環境の様相のすべてまたは一部に情報を伝えます。たとえば、光を反射したり、においを伝えたりなどです。この一連の情報が、環境内でのその存在を表します。感覚を持つ実体は、それぞれ一連の知覚の様相を持っていて、この様相によって環境の様相をサンプリングすることで、EEK! でのその実体が存在する世界を知覚します。

感覚を持つ実体は、その知覚の様相に、一連のプログラムされた行動 (つまり、その知覚に応じて行われる動作) を結び付けています。その反応の結果を否定的または肯定的のいずれかとして記憶できる場合、その行動は、変更または強化されます。たとえば、ネズミは、食べ物として知覚した物を食べた結果、ひどく具合が悪くなった場合、次はそれを食べてはいけない物として知覚します。

ネズミは、限られた数の状態の間を移行します。初めの状態は休息 (Rest) です。体内で、またはその環境についてのある知覚によって刺激が与えられると、ネズミの状態は活動中 (Restlessness) になり、一連の行動を実行して休息の状態に戻ります。一定の時間、外部と内部のどちらの刺激もないと、ネズミの状態は睡眠 (Sleep) に移行します。また、目的の追求がうまくいかないと、その状態は疲労 (Distress) に移行することがあります。

ここでは、内部の刺激を動因 (Drive) としてモデル化します。たとえば、ネズミには代謝 (Metabolic) コンポーネントがあり、その生命力の動因となっています。このシステムはエネルギーを必要とし、そのエネルギー指数が低下すると、代謝の動因によって、ネズミの状態は食べ物を探すことを目的とする活動中になります。

外部の刺激は、実体の知覚の様相の 1 つを通して生じ、感情 (Emotion) を発生させます。これは、動因と組み合わせられることもあります。感情は、実体にとって、状態を休息から活動中に移す主な動機です。このシステムでは、強さの点で異なる 8 つの基本的な感情をサポートします。

たとえば、ネズミは、不安 (Apprehension) を感じることがあります。これは、たとえば手で水槽の周りに触れるなど、未知の物として知覚したその環境内での潜在的な脅威に対する弱い反応を意味します。私がネズミをつかもうとして手を伸ばすと、その感情は恐れ (Fear) へと強まることがあり、しっかりとつかむと、恐怖 (Terror) に転じるかもしれません。型どおりの行動は、各感情に対応します。たとえば、まさに恐怖の状態にあるネズミは、実際に噛みます。

運動 (Motor) コンポーネントは、実体が取ることのできる行動の形式を制限して決定します。たとえば、ネズミは、水槽の周りを手でおおう私の意図について聞くことはできませんし、ガラスの側面を破って逃げ出すこともできません。ネズミは、その行動の範囲に物理的に拘束されます。

これは、EEK! の 4 番目の最も興味深いコンポーネント (少なくともこのシミュレーションでは) へとつながります。このコンポーネントは、実体の認識 (Cognitive) コンポーネントで、遺伝的 (Genetic) 気質と個体の経歴 (History) から構成されます。たとえば、同じ種類の別の実体が興味 (Interest) を覚える状況に対して、不安 (Apprehension) を感じることが考えられます。経歴に基づいて、その気質は強められたり、弱められたり、または変質したりします。この認識コンポーネントにより、個々の実体は、その型どおりの行動を時間の経過と共に変更することができます。

上記の説明にある Emotion、Apprehension、Fear などの英語の単語は、このアプリケーションの抽象化クラスの候補を表しています。もちろん、1 つにまとめる必要があります。ただし、それは、後のコラムでネズミの実装を開始するときに考えましょう。

次の疑問は、どのようにシミュレーションを実行するかではないでしょうか。EEK! 内のげっ歯類の実体とは違い、私は感情に任せるわけにはいきません。代わりに、図 2 に示すように、シミュレーションのメイン ロジックによって決める必要があります。

Figure 2 シミュレーションのメイン ロジック (Close Figure 2)
void EEK::run()
{
    Init(); // initialize our environment
    while ( Time < EndTime )
    {
        Tick();    // advance time, trigger events
        Update();  // update modalities
        Poll();    // SEs perceive() and behave()
        Display(); // communicate what has happened
    }
    Terminate(); // capture final state
}
Figure 2 シミュレーションのメイン ロジック (Close Figure 2)
void EEK::run()
{
    Init(); // initialize our environment
    while ( Time < EndTime )
    {
        Tick();    // advance time, trigger events
        Update();  // update modalities
        Poll();    // SEs perceive() and behave()
        Display(); // communicate what has happened
    }
    Terminate(); // capture final state
}

複数のシミュレーション実装をサポートするため、シミュレーションをインターフェイスとして抽象化し、インスタンスとして EEK をインスタンス化します。これを図 3 に示します。

Figure 3 インスタンスとしての EEK (Close Figure 3)
public interface class ISimulation
{
    void Run();

    void Init();
    void Tick();
    void Update();
    void Poll();
    void Display();
    void Terminate();

    property int Time    { int get(); }
    property int EndTime { int get(); }
}

public ref class EEK : ISimulation { ... };
Figure 3 インスタンスとしての EEK (Close Figure 3)
public interface class ISimulation
{
    void Run();

    void Init();
    void Tick();
    void Update();
    void Poll();
    void Display();
    void Terminate();

    property int Time    { int get(); }
    property int EndTime { int get(); }
}

public ref class EEK : ISimulation { ... };

一連の XML 構成ファイルを使用して初期化をサポートし、.NET Framework の基本クラス ライブラリ (BCL) 内で、XML ドキュメント オブジェクト モデル (DOM) サポートを使用してそれを解析します。BCL データ サポートを使用して、Access® または SQL Server™ のいずれかのデータベース内にシミュレーションの状態を保持します。表示コンポーネントには、Windows Presentation Foundation を使用します。最終的には、何らかの Web 上での表示機能を提供したいのですが、しばらくは保留します。これらの各サービスを関連する Manager クラスにカプセル化します。

もちろん、プログラミングで、デザインについて話をするのは、どれだけ考えていたとしても限界があります。本当に面白くなるのは、実装を始めてからです。それについては、次のコラムで開始します。

このコラムの執筆中、保菌状態となったネズミには一切危害を加えていません。

ネズミの図

ご意見やご質問は、

Stanley purecpp@microsoft.com.

まで英語でお送りください。
Stanley B. Lippmanは、C++ の考案者である Bjarne Stroustrup と共に、1984 年からベル研究所でこの言語に携わってきました。その後、Disney と DreamWorks でフィーチャー アニメーションに携わり、『Fantasia 2000』のソフトウェア テクニカル ディレクターを務めました。その後、JPL の特別コンサルタントになり、マイクロソフトでは Visual C++ チームのアーキテクトを務めてきました。現在は、Emergent Game Technologies のシニア ソフトウェア エンジニアです。


.NET の問題: コンソール アプリケーションでメッセージを処理する

.NET の問題
コンソール アプリケーションでメッセージを処理する
Stephen Toub

コードのダウンロード : : NetMatters2007_06.exe (153KB)

Q P/Invoke 経由で Win32® API を使用しています。この API は、非同期操作をサポートしており、操作の完了時に通知するコールバック メカニズムを備えています。ただし、これまで扱ってきた同様の機能を備える他の API とは異なり、この API は、コールバックに使用する関数ポインタやデリゲートを受け取りません。代わりに、操作の完了時に、API からの特定のメッセージを受信するウィンドウ ハンドルを受け取ります。GUI を持つアプリケーションからこの API を使用している場合、これは問題にはならないでしょう。しかし、コンソール アプリケーションで使用しているので、通知メッセージを受信したり処理するためのウィンドウやメッセージ ループがありません。どうすればよいでしょうか。お手上げでしょうか。

A Microsoft® .NET Framework を使用する多くの開発者は、アプリケーションの種類が、そのアプリケーションで使用できるライブラリと結び付いていると思い込んでいます。たとえば、コンソール アプリケーションでは Windows® フォーム クラスを利用できないなどです。しかし、これらは別個の概念です。Visual Studio® で、新しいコンソール アプリケーションを作成すると、アプリケーションの出力の種類は "コンソール アプリケーション" に設定されます (csc.exe に指定する /t:exe コマンド ライン パラメータと同等)。Visual Studio で、新しい Windows フォーム アプリケーションを作成すると、アプリケーションの出力の種類は "Windows アプリケーション" に設定され (/t:winexe と同等)、System.Windows.Forms.dll への参照の追加も行われます。ただし、System.Windows.Forms.dll への参照をコンソール アプリケーションに追加できることや、それとは反対に、コンパイルする Windows フォーム プロジェクトを "コンソール アプリケーション" に変更できることはあまり知られていません。アプリケーションの種類は、実際には、プロセス用に既定のコンソール ウィンドウが起動されるかどうかを指定するだけです。

つまり、コンソール アプリケーションで System.Windows.Forms のクラスを使用して、今回の問題の解決に役立てることができます。ここでは、問題の単純化のため、Windows フォーム クラスに依存するクラスを作成します。また、winmm.dll からエクスポートされる mciSendString 関数をこのクラスのテスト ケースとして使用します。これは、この関数が、質問した方が扱いに苦労している API と同様の機能を備えているからです。これと同じソリューションによって、コンソール アプリケーションで任意の種類の Windows メッセージを処理できるようになります。たとえば、Windows Vista™ で使用できる新しい電源管理機能をいくつか利用するには、WM_POWERBROADCAST メッセージを処理することが考えられます。このコラムで提供するクラスによって、それが実行できるようになります。

まず、説明を進めているデザインから始めましょう。System.dll からエクスポートされる Microsoft.Win32.SystemEvents クラスは、システムのさまざまな事象の監視に使用される静的イベントを多数提供します。静的イベントの一部は、ブロードキャスト メッセージによって発生します。たとえば、DisplaySettingsChanging イベントと DisplaySettingsChanged イベントは、WM_DISPLAYCHANGE メッセージに応じて発生します。

ユーザー対話型のアプリケーションで SystemEvents が初期化されるとき、SystemEvents の初期化を行うアプリケーションのスレッドが STA (シングル スレッド アパートメント。Windows フォーム アプリケーションでの既定) としてマークされていると、SystemEvents が利用できるメッセージ ループをこのスレッドが持つことを SystemEvents は必要とします (したがって、誤ってコンソール アプリケーションを STA としてマークした場合、メッセージ ポンプを手動で行わないと、SystemEvents でのメッセージベースのイベントは発生しません)。ただし、初期化を行うスレッドが MTA (マルチスレッド アパートメント。コンソール アプリケーションでの既定) の場合は、SystemEvents は独自のスレッドを生成します。そのスレッドは、メッセージ ループを実行します。どちらの場合でも、SystemEvents は、メッセージ ループを実行しているスレッドに関連付けられた非表示のウィンドウを作成します。このウィンドウのウィンドウ プロシージャは、関連メッセージの処理と、関連付けられているイベントの発生を制御します。

この実装は SystemEvents ほど洗練されてはいませんが、基本設計は SystemEvents と似ており、今回の問題の解決に役立つはずです。この MessageEvents クラスは、次のとおり、3 つのパブリックな静的メンバを公開します。

public static class MessageEvents
{
    public static void WatchMessage(int message);
    public static event EventHandler<MessageReceivedEventArgs>
        MessageReceived;
    public static IntPtr WindowHandle;
    ...
}

WatchMessage メソッドは、ユーザーにとって重要なメッセージを MessageEvents クラスに通知するのに使用します (一方、SystemEvents は、その実装にメッセージの関連付けを固定します)。これらのメッセージの 1 つが受信されると、MessageReceived イベントが発生します。このイベントは MessageReceivedEventArgs インスタンスを保持し、このインスタンスは受信メッセージを表す関連の System.Windows.Forms.Message 構造体を保持します。最後に、API がメッセージの送信先である 1 つの特定のウィンドウをターゲットとするシナリオでは、MesssageEvents クラスは、イベントを受け取って処理するために、MessageEvents で使用される基になるウィンドウのハンドルを公開します (通常、通知のためにウィンドウ メッセージを使用するこのような API は、HWND_BROADCAST (つまり 0xFFFF) とも連動します。HWND_BROADCAST を指定すると、同様に、基になるウィンドウに通知メッセージが送信されます)。

基になるウィンドウは、MessageEvents によって作成されます。追加のウィンドウを作成するかどうかを適切に判断することで、作成する必要がない場合にリソースを節約する SystemEvents とは異なり、ここでは、新しいスレッドとウィンドウを常に作成する、より単純な方法を取っています。測定により、この方法がアプリケーションのパフォーマンスを低下させるとわかった場合は、必要なロジックを追加して型を適切に拡張してください。

図 1 に示されている MessageWindow は、MessageEvents 内のプライベートな入れ子になった型です。これは Form から派生しているので、Control のすべてのインスタンスで使用できる、基になる NativeWindow の機能も継承しています (MessageWindow を軽量にして、NativeWindow を直接継承するように書き換えることもできますが、それが適切だと読者の方が思った場合の課題として、これも残しておきましょう)。MessageWindow は、渡された messageID をメッセージ セット (Dictionary<int, bool> で表される) に単に格納する RegisterEventForMessage という 1 つのパブリック メンバを公開します。このクラスは、WndProc メソッドのオーバーライドも行います。このメソッドは、メッセージを受信するウィンドウに応じて実行されます。このオーバーライドでは、受信したメッセージがメッセージ セットにあるかどうか、つまり、発生したイベントにあらかじめ登録されているメッセージであるかどうかを確認します。そうである場合は、WndProc によって MessageReceived イベントが発生します。どのような場合でも、すべてのメッセージは、基本クラスの WndProc 実装に渡されます。

Figure 1 MessageWindow クラス (Close Figure 1)
public static class MessageEvents
{
    private static SynchronizationContext _context;
    public static event
        EventHandler<MessageReceivedEventArgs> MessageReceived;

    ...

    private class MessageWindow : Form
    {
        private ReaderWriterLock _lock = new ReaderWriterLock();
        private Dictionary<int, bool> _messageSet =
            new Dictionary<int, bool>();

        public void RegisterEventForMessage(int messageID)
        {
            _lock.AcquireWriterLock(Timeout.Infinite);
            _messageSet[messageID] = true;
            _lock.ReleaseWriterLock();
        }

        protected override void WndProc(ref Message m)
        {
            _lock.AcquireReaderLock(Timeout.Infinite);
            bool handleMessage = _messageSet.ContainsKey(m.Msg);
            _lock.ReleaseReaderLock();

            if (handleMessage)
            {
                MessageEvents._context.Post(delegate(object state)
                {
                        EventHandler<MessageReceivedEventArgs>
                            handler = MessageEvents.MessageReceived;
                        if (handler != null) handler(null,
                            new MessageReceivedEventArgs(
                                      (Message)state));
                }, m);
            }
            base.WndProc(ref m);
        }
    }
}

このイベントがどのように発生するかについて、興味深い点がいくつかあります。まず、MessageReceived は、MessageEvents のイベントであって、MessageWindow のイベントではありません。一般的に、あるクラスは、直接アクセスによって別のクラスでイベントを発生させることはできません。これは、C# コンパイラによる問題です。ここでは、MessageReceived イベントを宣言したとき、実際には 2 つのことを行っていました。1 つはパブリック イベントの宣言で、もう 1 つはプライベート デリゲートの宣言です。パブリック イベントは、add アクセサと remove アクセサを備えているので、任意のクラスでイベントにハンドラを登録および登録解除できます。プライベート デリゲートは、"イベントの発生" 時に実際に呼び出されるものです。ただし、C# ソース コードでは、イベントを直接呼び出しているように見えます (詳細については、msdn.microsoft.com/msdnmag/issues/06/11/NETMatters を参照)。このデリゲートはプライベートなので、別のクラスからアクセスすることはできません。ただし、MessageWindow は、MessageEvents 内の入れ子になったクラスなので、MessageEvents のプライベートな実装の詳細にアクセスできます。この場合、MessageWindow は、プライベートな MessageReceived デリゲートにアクセスできるので、デリゲートを直接呼び出すことができます。

もう 1 つ気付くのは、WndProc を実行している同じスレッドではイベントが発生しないことです。代わりに、WndProc は、初期化の際に MessageEvents が取得する SynchronizationContext のインスタンスを使用します (これについては、この後説明します)。SynchronizationContext は、アプリケーションの同期コンテキストで非同期 Post によって (同期 Send ではなく) イベントを発生させるのに使用されます。これには、アプリケーションの種類は関係ありません。コンソール アプリケーションでは、SynchronizationContext.Post は、ThreadPool スレッドでデリゲートを単純に実行します。Windows フォーム アプリケーションでは、WindowsFormsSynchronizationContext.Post は、その呼び出しを UI スレッドにマーシャリングすることでデリゲートを実行します。SynchronizationContext の詳細については、msdn.microsoft.com/msdnmag/issues/06/06/NETMatters を参照してください。

MessageEvents クラスは静的メンバだけを含んでおり、実際に、このクラス自体が静的クラスとして宣言されています (非静的メンバを追加しようとすると、コンパイル エラーが発生します)。内部的には、次のとおり、MessageEvents が初期化されるときに作成される 1 つの MessageWindow への静的な参照を格納します。

private static MessageWindow _window;
private static IntPtr _windowHandle;

MessageEvents の 2 つのパブリック メソッドの実装は、処理のほとんどが個別の初期化メソッドに作り変えられているのでとても単純です。WatchMessage メソッドは、EnsureInitialized を呼び出して、MessageWindow の RegisterEventForMessage メソッドにメッセージ ID を渡します。次のとおり、WindowHandle プロパティも、EnsureInitialized を呼び出して初期化の際に作成されたウィンドウ ハンドルを返すだけです。

public static void WatchMessage(int message) {
    EnsureInitialized();
    _window.RegisterEventForMessage(message);
}

public static IntPtr WindowHandle {
    get {
        EnsureInitialized();
        return _windowHandle;
    }
}

残りは、EnsureInitialized を実装する方法についてです。EnsureInitialized は、すべての関連メッセージを監視してそれらに応答できるように、MessageEvents クラスを起動して実行します。そのため、EnsureInitialized では、新しいスレッドを生成し、メッセージを受信するブロードキャスト ウィンドウを作成して、メッセージ ループの実行を開始する必要があります。

図 2 に示されているように、EnsureInitialized は、初期化ロジックを 1 回実行するだけですが、MessageWindow が既に作成されている場合は何もせずに終了します。初期化を実行する必要がある場合、まず、現在のスレッドに関連付けられている SynchronizationContext を取得します。これは、SynchronizationContext.Current を使用して実行することもできますが、SynchronizationContext.Current は、コンテキストが前もって構成されていない場合は null を返します。代わりに、ここでは AsyncOperationManager.SynchronizationContext を使用します。これは、まだコンテキストが存在しない場合に、まず既定の SynchronizationContext を設定する SynchronizationContext.Current の簡単なラッパーで、SynchronizationContext.Current の値を返すと、それ以降は null 値を返しません。

Figure 2 バックグラウンド メッセージ ループを開始する (Close Figure 2)
public static class MessageEvents
{
    private static object _lock = new object();
    private static MessageWindow _window;
    private static IntPtr _windowHandle;
    private static SynchronizationContext _context;

    ...

    private static void EnsureInitialized()
    {
        lock (_lock)
        {
            if (_window == null)
                   {
                   _context = AsyncOperationManager.
                         SynchronizationContext;
                   using (ManualResetEvent mre =
                         new ManualResetEvent(false))
                {
                    Thread t = new Thread((ThreadStart)delegate
                    {
                        _window = new MessageWindow();
                        _windowHandle = _window.Handle;
                        mre.Set();
                        Application.Run();
                    });
                    t.Name = “MessageEvents”;
                    t.IsBackground = true;
                    t.Start();

                    mre.WaitOne();
                }
            }
        }
    }
}

コンテキストを取得すると、EnsureInitialized は新しいスレッドを作成します。このスレッドは、MessageWindow を作成して格納し、また、ウィンドウのハンドルを別個の静的メンバに格納します。このハンドルは、前に説明したように、WindowHandle プロパティからの戻り値として使用されます。この時点で、ウィンドウは起動され実行されているので、メッセージ ループの実行を開始する必要があります。そのために、独自のメッセージ ループを記述することもできますが、.NET Framework にあらかじめ用意されているループを利用する方がずっと簡単です。Application.Run メソッドは、現在のスレッドで標準のアプリケーション メッセージ ループの実行を開始するので、このメソッドを呼び出すだけで、そのプロセスを開始することができます。

今回の MessageEvents クラスについては、これだけです。動作を確認するには、既に説明した winmm.dll の mciSendString 関数を使用します。mciSendString 関数は、Windows のメディア コントロール インターフェイス (MCI) の一部で、さまざまな種類のマルチメディア デバイスの操作に使用できます。これを次に示します。

[DllImport(“winmm.dll”, EntryPoint = “mciSendStringA”,
           CharSet = CharSet.Ansi)]
private static extern int MciSendString(
    string lpszCommand, StringBuilder lpszReturnString,
    int cchReturn, IntPtr hwndCallback);

mciSendString を使用して WAV ファイルを再生するには、通常の一連のコマンドは次のようになります。

open “C:\Windows\Media\chimes.wav” type waveaudio alias 12345
play 12345 wait
close 12345

最初のコマンドは、指定されたデバイスを開いてエイリアス (この例では 12345) を割り当てます。このデバイスは、後で、他のコマンドへの呼び出しで、このエイリアスによって参照されます。2 番目のコマンドは、デバイスの再生を開始して、コマンドの実行が完了するまで戻らないようにシステムに指示します。最後のコマンドは、デバイスを閉じるようにシステムに指示します。ただし、MciSendString のために作成した P/Invoke シグネチャは、hwndCallback IntPtr パラメータを持っていることに注意してください。これは、次の play コマンドなどさまざまなコマンドに追加できる notify フラグとの連動に使用されます。

play 12345 notify

wait フラグが指定されていないので、このコマンドは再生を開始するとすぐに戻ります。ただし、notify フラグが指定されているので、コマンドの非同期完了時、MM_MCINOTIFY メッセージがその hwndCallback プロシージャで指定された HWND に送信されます。

MciSendString(“play 12345 notify”, null,
              0, MessageEvents.WindowHandle);

MessageEvents.WindowHandle をターゲットの HWND としてどのように提供しているかに注意してください。このコマンドを実行する前に、次のとおり、コールバック メッセージを取得するように MessageEvents クラスを構成できます。

MessageEvents.WatchMessage(MM_MCINOTIFY);
MessageEvents.MessageReceived += delegate(
    object sender, MessageReceivedEventArgs e)
{
    Console.WriteLine(“Message received: “ + e.Message.Msg);
};

コードが正しく配置されていれば、通知コマンドが完了すると、作成されるメッセージに関する情報がコンソールに出力されます。もちろん、Windows フォーム アプリケーションでこれを行うのに MessageEvents クラスは必ずしも必要ではありませんが、このクラスは、独自のメッセージ ループがないコンソール アプリケーションでも、Windows フォーム アプリケーションでも、同様に適切に機能します。

ご意見やご質問は、

Stephen までお送りください。 netqa@microsoft.com.

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


{ End Bracket }: 開発者の誇り

{ End Bracket }
開発者の誇り
Adam Barr

大学 4 年のとき、 投資銀行で働くことになっていた情報科学専攻の友人とこんな話をしたことがあります。彼は、自分はソフトウェアの設計に携わることになるが、実際にソフトウェアを記述するような平凡な作業はしないと言っていました。そのような作業はプログラマに任せると言うのです。プログラマは明らかに格下であり、言い換えれば私のことでもありました。コードをハッキングする能力が最高のステータスであった大学環境からやってきた私は、なんだか侮辱されたように感じました。

しかし、彼が言っていたことには一理あります。当時、私はただのプログラマでしたから (彼のほうが優秀だったという意味ではありません)。現在は、Mike Gunderloy の著書『Coder to Developer』のタイトルにあるように、自分は単なるプログラマを卒業し、開発者になれたと考えています。私は、正しくコンパイルできるコードの書き方を知っているだけでなく、高速で信頼性が高く、十分なテストがなされ、安全で、保守しやすく、グローバル化にも対応したソフトウェアを作成することができます。ソフトウェア業界全体も、プログラマの集団から開発者の集団へと成熟してきています。

さて現在において、開発者に次なるステップについて尋ねたとしたら、アーキテクトになりたいという答えが返ってくるでしょう。アーキテクトという言葉は、チタンとすりガラスの風景を彷彿とさせ、ただの開発者を建築作業員の役割に追いやります。では、私も定石どおり次なるステップに進み、アーキテクトになりたいと思っているでしょうか。答えは、ノーです。

アーキテクトを侮辱するつもりはありません。ソフトウェア開発には、システムのコンポーネント間の対話を設計し、広い視野で考える人物が必要です。それでも、私は開発者であることに誇りを持っていますし、他のすべての開発者にも同じように感じてほしいと思っています。

Frank Gehry や Rem Koolhaas といった物理建築のアーキテクトが享受できる設計の自由を、私たちも求めたとしたらどうなるでしょうか。個人的には、そうした概念的階段に上るには、ソフトウェア エンジニアリングの分野はまだまだ未熟だと考えています。アーキテクトがビルバオ グッゲンハイム美術館やシアトル公共図書館のような建物を設計できるのは、数世紀におよぶ土木建築の知識に裏打ちされているからです。ソフトウェア業界には、すべての人がこのレベルで作業できるだけの余裕はありません。私たちのほとんどは、依然として玄関のドアをバタンと閉めても崩壊しない 1 階建ての家を建てることに苦慮しています。

マイクロソフトがバグの原因を調べると、設計のバグ (アーキテクトが設計文書を見直したときに見つかるバグ) でも、コーディングのバグ (ソース コードがプログラマの意図した処理を行わない) でもないものが大量に見つかります。この中間的な性質のバグは、ソース コードはプログラマの意図したとおりの処理を行っているものの、その意図の中に、たとえばメソッドに誤ったフラグを渡す、構成パラメータの意味を誤って理解しているといったような、局所的なミスが存在する場合に発生します。これはアーキテクトの守備範囲ではありません。私たち開発者自身が正しく行わなければならない分野です。

Fred Brooks は有名な論文「銀の弾などない」でこのことに触れ、「ソフトウェア エンティティの本質は、データ セット、データ アイテム間の関係、アルゴリズム、関数の呼び出しなどの連動概念を組み合わせたものである」と書いています。さらに続けて、「私は、ソフトウェアを構築するうえで難しいのは、この概念的な構築物を仕様化し、設計し、テストすることであり、この構築物を表現したり、その表現の忠実度をテストしたりする仕事ではないと考えている」と述べています。彼が述べているのは、アーキテクトがやることではなく、開発者がやることです。また実質的に、プログラマの仕事はそれほど難しくないが、開発者の仕事は間違いなく難しいと述べています。私たちは、自分たちがやっていることのすばらしさを認識する必要があります。

この仕事がプログラミングの華でないことはわかっています。複数のコンポーネントを、さまざまなバリエーションをスマートに包含し、将来の拡張に対応するように完璧に設計するのは、なかなかスリリングです。プログラマは正確さを好みます。そしてすべてのパーツを正確に組み合わせる作業には魅力があります。しかし、ソフトウェアの作成には、コード レビューの実行、単体テストの記述、コメントのクリーンアップなど、熟練した職人のような仕事が求められる部分が数多くあります。顧客は、私たちがお客様のプライバシーを守り、システムを攻撃から防御するために努力していることに直接気付くことはないかもしれません。でも私たちは、自分たちの仕事に価値があることを知っています。また、パフォーマンスの最適化や電源管理など、真の妖術と呼べる領域もあります。これらは高レベルのアーキテクチャではなく、現場レベルの仕事であり、私たちが技能と経験を生かして問題を解決する必要があります。

私はアーキテクトではなく、開発者であることに誇りを持っています。いつの日か、ソフトウェアのエンジニアリングに関する問題をすべて解決する日が来るでしょう。そうすれば、私たちは皆アーキテクトになることができます。でもそれまでは、良いソフトウェアを仕上げるのは私たちの仕事なのです。

Adam Barrはマイクロソフトに 13 年間勤務しています。開発者、プログラム マネージャを経て、現在は Engineering Excellence チームのナレッジ エンジニアである彼は、社内コンサルタント兼インストラクタとして、開発エンジニアリング プロセスをサポートしています。