: MSDN Magazine May 2007

May 2007
MAY 2007 ボリューム 21 番号 5

• JavaScript: オブジェクト指向の手法によって高度な Web アプリケーションを作成する
Ray Djajadinata

• ゲームの勝敗は? XNA Game Studio Express で想像力を解き放つ
Charles Cox および Michael Klucher

• WPF: Windows Presentation Foundation のコントロールをカスタマイズする
Shawn Wildermuth

• 移行: JLCA を使用して Java Web アプリケーションを ASP.NET に変換する
Brian Jimerson

• BizTalk Server: BizTalk プログラミングを向上させる 8 つのヒントと秘訣
Marty Wasznicky および Scott Zimmerman

• 共同作業: Web サービスと Groove 2007 がチーム間の共同作業を支援する
John C. Hancock

Code Download (661 KB) HTMLHelp Format
コラム

• CLR の完全理解: 再利用可能な 9 つの並列データ構造とアルゴリズム
Joe Duffy

• Office Space: SharePoint の機能
Ted Pattison

• Cutting Edge: ASP.NET ページのサブクラス化とオーバーライドを行う - 第 II 部
Dino Esposito

• テストの実行: Windows PowerShell を使用した軽量テスト
Dr. James McCaffrey

• セキュリティに関するブリーフィング: Windows Vista のイベント
Keith Brown

• 基礎: WCF トランザクションの伝達
Juval Lowy

• { End Bracket }: アジャイルを実践する
James Waletzky

各出版物

• 編集者のメモ: 夏時間
Joshua Trupin

• ツールボックス: コードの検証、SQL クエリ、その他
Scott Mitchell


JavaScript: オブジェクト指向の手法によって高度な Web アプリケーションを作成する

JavaScript

オブジェクト指向の手法によって高度な Web アプリケーションを作成する

Ray Djajadinata
この記事の内容 : :
  • JavaScript はプロトタイプ ベ―スの言語
  • JavaScript によるオブジェクト指向プログラミング
  • JavaScript コーディングの秘訣
  • JavaScript の未来
この記事は次のテクノロジを使用しています:
JavaScript
コンテンツ
JavaScript オブジェクトはディクショナリである
JavaScript の関数は優秀である
クラスはなくてもコンストラクタ関数がある
プロトタイプ
静的なプロパティとメソッド
クロージャ
プライベート プロパティをシミュレートする
クラスの継承
名前空間をシミュレートする
JavaScript ではこのようにコーディングが必要なのか
大局的に見る

最近、5 年間 Web アプリケーションの開発に携わっているというあるソフトウェア開発者に話を聞く機会がありました。彼女は 4 年半にわたり JavaScript を使用しており、JavaScript については高いスキルを持っていると自認していました。しかし、すぐにわかったのですが、実際には JavaScript の知識がほとんどなかったのです。とはいっても、それは彼女の責任ではありません。それだけ JavaScript が変わった言語だということです。C、C++、C# を知っているから、あるいは以前プログラミングの経験があるから、という理由だけで多くの人が得意だと思い込んでいる (私も最近までそうでした) のが、この JavaScript という言語です。

ある意味、このような思い込みはまったく根拠がないものでもありません。JavaScript で単純なことをするのは簡単です。きわめて敷居が低く、取っ付きやすい言語であり、詳しい知識がなくてもコーディングを始めることができます。JavaScript であれば、プログラマでない人でもホームページの便利なスクリプトをおそらく数時間で作成できてしまいます。

実際、私も最近まで、 JavaScript に関するなけなしの知識と MSDN® の DHTML リファレンスや C++ と C# の使用経験だけで何とかやってくることができました。現実の AJAX アプリケーションにかかわるようになって初めて、JavaScript の知識がいかに乏しいかを思い知らされました。この新世代の Web アプリケーションに特有の複雑さと対話性によって、JavaScript コードを書くには、従来とはまったく異なるアプローチが必要とされます。AJAX アプリケーションは本格的な JavaScript アプリケーションなのです。これまでの使い捨てのスクリプトを書くような方法ではとても対応できません。

コードベースの管理性と保守性を高める方法として多くの JavaScript ライブラリに採用されている一般的なアプローチの 1 つが、オブジェクト指向プログラミング (OOP) です。JavaScript は OOP をサポートしていますが、その内容は C++、C#、Visual Basic® などの Microsoft® .NET Framework 準拠の言語でのサポートとは大きく異なっています。そのため、これらの言語を使い慣れた開発者が JavaScript で OOP を行うと、初めのうちは違和感を覚え、戸惑うことがあります。この記事では、JavaScript がどのようにオブジェクト指向プログラミングをサポートし、それによっていかに効果的なオブジェクト指向開発を行うことができるかについて詳しく説明します。まず、オブジェクトの話から始めます (それ以外に何かあるでしょうか)。

JavaScript オブジェクトはディクショナリである

C++ や C# では、オブジェクトというとクラスや構造体のインスタンスを意味します。オブジェクトにはプロパティやメソッドが含まれていますが、それらはオブジェクトがどのテンプレート (クラス) からインスタンス化されたかによって異なります。このようなことは JavaScript オブジェクトにはありません。JavaScript では、オブジェクトは名前と値のペアの集まりに過ぎません。したがって、JavaScript オブジェクトは文字列キーが集まったディクショナリと考えることができます。オブジェクトのプロパティを取得し、設定するには、使い慣れた "." (ドット) 演算子、またはディクショナリの操作で一般に使用される "[]" 演算子を使用します。次のコードを参照してください。

var userObject = new Object();
userObject.lastLoginTime = new Date();
alert(userObject.lastLoginTime);
このコードは次のコードとまったく同じ処理を実行します。

var userObject = {}; // equivalent to new Object()
userObject[“lastLoginTime”] = new Date();
alert(userObject[“lastLoginTime”]);
lastLoginTime プロパティは、次のように userObject の定義内に直接定義することもできます。

var userObject = { “lastLoginTime”: new Date() };
alert(userObject.lastLoginTime);

これは C# 3.0 のオブジェクト初期化コードとよく似ています。また、Python に詳しい人であれば、2 つ目および 3 つ目のコードで userObject をインスタンス化する方法が Python でディクショナリを指定する方法と同じであることに気付くはずです。異なる点は、JavaScript オブジェクト (ディクショナリ) が文字列キーだけを扱い、Python ディクショナリのようにハッシュ可能なオブジェクトは扱わないことだけです。

これらのコードを見ると、JavaScript オブジェクトの方が C++ オブジェクトや C# オブジェクトよりはるかに柔軟性が高いこともわかります。lastLoginTime プロパティは事前に宣言する必要がありません。userObject に lastLoginTime という名前のプロパティがなければ、lastLoginTime プロパティを宣言なしで userObject に追加することができます。JavaScript オブジェクトがディクショナリであることを考えると、これは驚くほどのことではありません。どのような場合でも、新しいキー (およびその値) をディクショナリに追加するだけだからです。

これで、オブジェクトのプロパティを作成できました。オブジェクトのメソッドについてはどうでしょうか。この点でも JavaScript は C++ や C# とは異なります。オブジェクトのメソッドについて理解するためには、まず JavaScript 関数について詳しく見ていく必要があります。

JavaScript の関数は優秀である

多くのプログラミング言語では、通常、関数とオブジェクトは異なるものとして扱われます。JavaScript では、この区別があいまいです。JavaScript 関数は実際には実行可能コードが関連付けられたオブジェクトです。次のような一般的な関数について考えてみます。

function func(x) {
    alert(x);
}
func(“blah”);
JavaScript では通常、関数をこのように定義します。ただし、この関数を次のように定義することもできます。この場合は、無名関数オブジェクトを作成し、それを変数 func に割り当てます。

var func = function(x) {
    alert(x);
};
func(“blah2”);
また、Function コンストラクタを使用して次のように定義することもできます。

var func = new Function(“x”, “alert(x);”);
func(“blah3”);

この例を見ると、関数が実際は関数呼び出しをサポートするオブジェクトに過ぎないことがわかります。この最後の Function コンストラクタを使用した関数定義方法は広く使用されているわけではありませんが、注目すべき可能性を秘めています。ご覧のとおり、この関数には Function コンストラクタへの String パラメータしか含まれていません。これは、実行時に任意の関数を構築できることを意味します。

JavaScript オブジェクトと同じように関数にもプロパティを設定または追加できることも、関数がオブジェクトであることの証拠です。

function sayHi(x) {
    alert(“Hi, “ + x + “!”);
}
sayHi.text = “Hello World!”;
sayHi[“text2”] = “Hello World... again.”;

alert(sayHi[“text”]); // displays “Hello World!”
alert(sayHi.text2); // displays “Hello World... again.”

関数についても、オブジェクトと同じように、変数に割り当てることができ、他の関数に引数として渡したり、他の関数の値として返したりすることができます。さらに、関数をオブジェクトのプロパティや配列の要素として保存することも可能です。その実例を図 1 に示します。

Figure 1 JavaScript の関数は優秀 (Close Figure 1)
// assign an anonymous function to a variable
var greet = function(x) {
    alert(“Hello, “ + x);
};
greet(“MSDN readers”);

// passing a function as an argument to another
function square(x) {
    return x * x;
}
function operateOn(num, func) {
    return func(num);
}
// displays 256
alert(operateOn(16, square));

// functions as return values
function makeIncrementer() {
    return function(x) { return x + 1; };
}
var inc = makeIncrementer();
// displays 8
alert(inc(7));

// functions stored as array elements
var arr = [];
arr[0] = function(x) { return x * x; };
arr[1] = arr[0](2);
arr[2] = arr[0](arr[1]);
arr[3] = arr[0](arr[2]);
// displays 256
alert(arr[3]);

// functions as object properties
var obj = { “toString” : function() { return “This is an object.”; } };
// calls obj.toString()
alert(obj);

したがって、名前を選択し、その名前に関数を割り当てるだけで、簡単にメソッドをオブジェクトに追加することができます。たとえば、オブジェクトに 3 つのメソッドを定義するには、次のようにそれぞれのメソッド名に無名関数を割り当てます。

var myDog = {
    “name” : “Spot”,
    “bark” : function() { alert(“Woof!”); },
    “displayFullName” : function() {
        alert(this.name + “ The Alpha Dog”);
    },
    “chaseMrPostman” : function() {
        // implementation beyond the scope of this article
    }
};
myDog.displayFullName();
myDog.bark(); // Woof!

関数 displayFullName 内でキーワード "this" がどのような働きをするかは、C++ や C# を使用する開発者であればだれもが知っています。このキーワードはメソッドの呼び出し元のオブジェクトを参照します (これは Visual Basic を使用する開発者にも馴染み深いものだと思います。Visual Basic には同じ働きをするキーワード "Me" があります)。上の例にある displayFullName で使用されている "this" の値は myDog オブジェクトです。ただし、"this" の値は静的ではありません。別のオブジェクトから呼び出すと、図 2 に示すように、"this" の値もそのオブジェクトに変わります。

Figure 2 “this” の値はオブジェクトによって変わる (Close Figure 2)
function displayQuote() {
    // the value of “this” will change; depends on
    // which object it is called through
    alert(this.memorableQuote);
}

var williamShakespeare = {
    “memorableQuote”: “It is a wise father that knows his own child.”,
    “sayIt” : displayQuote
};

var markTwain = {
    “memorableQuote”: “Golf is a good walk spoiled.”,
    “sayIt” : displayQuote
};

var oscarWilde = {
    “memorableQuote”: “True friends stab you in the front.”
    // we can call the function displayQuote
    // as a method of oscarWilde without assigning it
    // as oscarWilde’s method.
    //”sayIt” : displayQuote
};

williamShakespeare.sayIt(); // true, true
markTwain.sayIt(); // he didn’t know where to play golf

// watch this, each function has a method call()
// that allows the function to be called as a
// method of the object passed to call() as an
// argument.
// this line below is equivalent to assigning
// displayQuote to sayIt, and calling oscarWilde.sayIt().
displayQuote.call(oscarWilde); // ouch!

図 2 の最後の行は、関数をオブジェクトのメソッドとして呼び出すもう 1 つの方法を示しています。先ほど説明したとおり、JavaScript の関数はオブジェクトです。どの関数オブジェクトにも関数を 1 つ目の引数のメソッドとして呼び出す call というメソッドがあります。つまり、関数呼び出しの 1 つ目の引数として渡したオブジェクトが、その呼び出しの "this" の値となります。この手法は基本クラス コンストラクタを呼び出すときにも利用できます。この点については後ほど説明します。

1 つだけ注意が必要な点があります。所有オブジェクトが存在しない場合には "this" を含む関数を呼び出してはならないことです。そうすると、グローバル名前空間に損傷を与えることになります。そのような呼び出しでは、"this" が Global オブジェクトを参照するため、アプリケーションに深刻な悪影響を及ぼす可能性があるからです。たとえば、次のようなスクリプトは JavaScript のグローバル関数 isNaN の動作を変更してしまいます。このような "this" の使い方は避けてください。

alert(“NaN is NaN: “ + isNaN(NaN));

function x() {
    this.isNaN = function() {
        return “not anymore!”;
    };
}
// alert!!! trampling the Global object!!!
x();

alert(“NaN is NaN: “ + isNaN(NaN));

ここまでオブジェクトの作成方法について説明してきましたが、その締めくくりとしてプロパティとメソッドについて考えます。これまで挙げたコードを見るとわかるとおり、プロパティとメソッドはオブジェクト定義内にハードコードされています。オブジェクトの作成について、より柔軟な操作が必要な場合は、どうすればよいのでしょうか。たとえば、オブジェクトのプロパティ値をパラメータに基づいて計算することが必要な場合もあります。ときにはオブジェクトのプロパティを実行後でなければ取得できない値に初期化することも必要になります。オブジェクトのインスタンスを複数作成しなければならない場合もあり、しかもこれは多くの場面で必要となります。

C# では、クラスを使用してオブジェクトをインスタンス化します。しかし、JavaScript は違います。クラスがないからです。次のセクションで説明しますが、"new" 演算子と組み合わせることにより関数がコンストラクタの役割を果たすことを利用します。

クラスはなくてもコンストラクタ関数がある

JavaScript OOP に関して最も奇妙に思える点は、前にも指摘したとおり、C# や C++ とは異なり、JavaScript にはクラスがないことです。C# で次のような処理を実行したとします。

Dog spot = new Dog();
この場合、オブジェクトが返されますが、それは Dog クラスのインスタンスです。ところが JavaScript にはそもそもクラスがありません。クラスと同じような効果を得るためには、次のようなコンストラクタ関数を定義します。

function DogConstructor(name) {
    this.name = name;
    this.respondTo = function(name) {
        if(this.name == name) {
            alert(“Woof”);
        }
    };
}

var spot = new DogConstructor(“Spot”);
spot.respondTo(“Rover”); // nope
spot.respondTo(“Spot”); // yeah!
これで何が起きると思いますか。DogConstructor 関数定義は後回しにして、まず次のコードから見ていきます。

var spot = new DogConstructor(“Spot”);

"new" 演算子が行うことはいたって単純です。まず、新しい空のオブジェクトを作成します。次に、その直後の関数呼び出しが実行され、その関数の "this" に新たに作成された空のオブジェクトが設定されます。つまり、"new" 演算子の上の行は次の 2 行に似たものと考えることができます。

// create an empty object
var spot = {};
// call the function as a method of the empty object
DogConstructor.call(spot, “Spot”);
DogConstructor の内容を見るとわかるとおり、この関数を呼び出すと、その呼び出しにおいてキーワード “this” が参照するオブジェクトが初期化されます。これによって、オブジェクトのテンプレートを作成することができます。類似のオブジェクトを作成することが必要なときには、“new” を指定してコンストラクタ関数を呼び出すことで、完全に初期化されたオブジェクトを取得できます。クラスの働きとよく似ていると思いませんか。実際、JavaScript では、コンストラクタ関数を作成する場合、それに相当するクラスの名前を付けるのが一般的です。つまり、上の例のコンストラクタ関数には次のように Dog という名前を付けることができます。

// Think of this as class Dog
function Dog(name) {
    // instance variable
    this.name = name;
    // instance method? Hmmm...
    this.respondTo = function(name) {
        if(this.name == name) {
            alert(“Woof”);
        }
    };
}

var spot = new Dog(“Spot”);

この関数 Dog の定義では、name というインスタンス変数を定義しています。コンストラクタ関数として Dog を使用して作成したオブジェクトには、必ずインスタンス変数 name のコピーが含まれています (この変数は、前に指摘したとおり、オブジェクトのディクショナリへのエントリに過ぎません)。この点は想定どおりです。オブジェクトの状態を保持するためには、個々のオブジェクトにインスタンス変数の固有のコピーが必要になります。しかし、次の行を見ると、Dog のインスタンスもそれぞれが respondTo メソッドの固有のコピーを持っており、これでは無駄です。複数の Dog インスタンスで共有すれば、respondTo のインスタンスは 1 つしか必要ありません。この問題を解決するには、次のように respondTo の定義を Dog の定義の外側に置きます。

function respondTo() {
    // respondTo definition
}

function Dog(name) {
    this.name = name;
    // attached this function as a method of the object
    this.respondTo = respondTo;
}

これによって、Dog のインスタンス (つまりコンストラクタ関数 Dog によって作成されたインスタンス) は、すべて respondTo メソッドの 1 つのインスタンスを共有します。ただし、メソッドの数が増えるに従って管理が困難になっていきます。いずれコードベースはグローバル関数で一杯になります。クラスが増えるにつれて事態は悪化するばかりで、特に同じような名前のメソッドが多い場合は収拾がつかなくなります。このような問題を回避するのに効果的なのがプロトタイプ オブジェクトです。次のセクションでは、この点について説明します。

プロトタイプ

プロトタイプ オブジェクトは JavaScript によるオブジェクト指向プログラミングの中心的な概念の 1 つです。プロトタイプ オブジェクトという名前は、JavaScript ではオブジェクトを既存のオブジェクト (プロトタイプ) のコピーとして作成されるという考え方に由来します。プロトタイプ オブジェクトのプロパティとメソッドは、そのプロトタイプのコンストラクタによって作成されたオブジェクトのプロパティとメソッドとなります。つまり、オブジェクトはプロトタイプのプロパティとメソッドを継承するということができます。次のような Dog オブジェクトを作成するとします。

var buddy = new Dog(“Buddy“);
buddy が参照するオブジェクトはプロトタイプのプロパティとメソッドを継承します。とはいっても、どのように継承するのかは、このプロトタイプを作成する 1 行だけでは、はっきりとはわかりません。オブジェクト buddy のプロトタイプはコンストラクタ関数 (ここでは関数 Dog) のプロパティから派生します。

JavaScript では、すべての関数に "prototype" というプロパティがあり、このプロパティがプロトタイプ オブジェクトを参照します。一方、プロトタイプ オブジェクトには "constructor" というプロパティがあり、このプロパティは関数を逆参照します。このように一種の循環参照を形成します。この循環関係をわかりやすく示したのが図 3 です。

図 3 どの関数のプロトタイプにも constructor プロパティがある
図 3 どの関数のプロトタイプにも constructor プロパティがある

ここで、関数 (上の例では Dog) を使用して "new" 演算子を持つオブジェクトを作成すると、そのオブジェクトは Dog.prototype のプロパティを継承します。図 3 を見ると、Dog.prototype オブジェクトに constructor プロパティがあり、関数 Dog を逆参照していることがわかります。そこから推すと、Dog.prototype から派生した Dog オブジェクトにも関数 Dog を逆参照する constructor プロパティがあるように思えます。この点は図 4 に示すコードで確認できます。コンストラクタ関数、プロトタイプ オブジェクト、それらによって作成されたオブジェクトの関係を図 5 に示します。

Figure 4 オブジェクトにはそのプロトタイプのプロパティが定義されているように見える (Close Figure 4)
var spot = new Dog(“Spot”);

// Dog.prototype is the prototype of spot
alert(Dog.prototype.isPrototypeOf(spot));

// spot inherits the constructor property
// from Dog.prototype
alert(spot.constructor == Dog.prototype.constructor);
alert(spot.constructor == Dog);

// But constructor property doesn’t belong
// to spot. The line below displays “false”
alert(spot.hasOwnProperty(“constructor”));

// The constructor property belongs to Dog.prototype
// The line below displays “true”
alert(Dog.prototype.hasOwnProperty(“constructor”));
図 5 インスタンスはプロトタイプを継承する
図 5 インスタンスはプロトタイプを継承する

図 4 で hasOwnProperty メソッドと isPrototypeOf メソッドを呼び出していることに気付いた方もいるかもしれません。これらのメソッドはどこから来たのでしょうか。Dog.prototype のメソッドではありません。事実、Dog.prototype や Dog のインスタンスでは他にも toString、toLocaleString、valueOf などのメソッドを呼び出すことができますが、これらはいずれも Dog.prototype のメソッドではありません。実は、.NET Framework にすべてのクラスの基本クラスである System.Object があるように、JavaScript にもすべてのプロトタイプの基本プロトタイプとなる Object.prototype があります (Object.prototype のプロトタイプは null です)。

次の例では、Dog.prototype が 1 つのオブジェクトであることに注意してください。このオブジェクトは Object コンストラクタ関数の呼び出しによって作成されますが、その処理は表面には現れません。

Dog.prototype = new Object();

Dog のインスタンスが Dog.prototype を継承するのとまったく同じように、Dog.prototype も Object.prototype を継承します。それによって、Dog のすべてのインスタンスが Object.prototype のメソッドとプロパティも継承することになります。

どの JavaScript オブジェクトもプロトタイプのチェーンを継承し、すべてのプロトタイプが Object.prototype に帰着します。ただし、これまで見てきた継承はライブ オブジェクト間の継承です。一般に考えられているような、クラスの宣言によって形成されるクラス間の継承とは異なります。したがって、JavaScript の継承はクラス間の継承よりはるかに動的なものとなります。JavaScript の継承は次のような単純なアルゴリズムによって確立されます。あるオブジェクトのプロパティまたはメソッドにアクセスすると、JavaScript がそのオブジェクトに該当するプロパティまたはメソッドが定義されているかどうかをチェックします。定義されていない場合は、そのオブジェクトのプロトタイプをチェックします。プロトタイプにも定義されていない場合は、そのプロトタイプのプロトタイプをチェックします。このようにして、最終的に Object.prototype までチェックが進みます。図 6 に、この解決プロセスを示します。

図 6 プロトタイプ チェーンで toString() メソッドを解決する
図 6 プロトタイプ チェーンで toString() メソッドを解決する(画像を縮小するには、ここをクリックします)
図 6 プロトタイプ チェーンで toString() メソッドを解決する
図 6 プロトタイプ チェーンで toString() メソッドを解決する (画像を拡大するには、ここをクリックします)

このように JavaScript はプロパティへのアクセスやメソッドの呼び出しを動的に解決するため、次のような影響をもたらします。

  • プロトタイプ オブジェクトに加えた変更は、直ちにそのプロトタイプを継承するオブジェクトで認識可能になります。オブジェクトの作成後に行われた変更についても同様です。
  • あるオブジェクトにプロパティまたはメソッド X を定義すると、そのオブジェクトのプロトタイプでは同じ名前のプロパティまたはメソッドが無効になります。たとえば、Dog.prototype に toString メソッドを定義することによって、Object.prototype の toString をオーバーライドできます。
  • 変更は、プロトタイプから派生オブジェクトへの 1 方向のみに伝達され、その逆の方向には伝達されません。

これらの影響を図 7 に示します。図 7 には、不要なメソッド インスタンスが発生する問題を早期に解決する方法も示してあります。オブジェクトごとに関数オブジェクトのインスタンスを作成する代わりに、メソッドをプロトタイプの内部に置くことによって複数のオブジェクトで共有することができます。この例では、spot の toString メソッドをオーバーライドするまで、rover と spot が getBreed メソッドを共有します。toString メソッドをオーバーライドした後は、spot には getBreed メソッドの固有のバージョンがありますが、rover オブジェクトとそれ以降に新しい GreatDane で作成されたオブジェクトは依然として GreatDane.prototype オブジェクトに定義された getBreed メソッドのインスタンスを共有します。

Figure 7 プロトタイプの継承 (Close Figure 7)
function GreatDane() { }

var rover = new GreatDane();
var spot = new GreatDane();

GreatDane.prototype.getBreed = function() {
    return “Great Dane”;
};

// Works, even though at this point
// rover and spot are already created.
alert(rover.getBreed());

// this hides getBreed() in GreatDane.prototype
spot.getBreed = function() {
    return “Little Great Dane”;
};
alert(spot.getBreed());

// but of course, the change to getBreed
// doesn’t propagate back to GreatDane.prototype
// and other objects inheriting from it,
// it only happens in the spot object
alert(rover.getBreed());

静的なプロパティとメソッド

インスタンスではなくクラスに関連付けられたプロパティやメソッド、つまり静的なプロパティやメソッドが必要になることがあります。このような場合、JavaScript では簡単に対応できます。関数がオブジェクトであり、そのプロパティとメソッドを必要に応じて設定できるからです。JavaScript ではコンストラクタ関数がクラスに相当するため、次のようにコンストラクタ関数にメソッドとプロパティを設定するだけで、静的なメソッドとプロパティを追加することができます。

    function DateTime() { }

    // set static method now()
    DateTime.now = function() {
        return new Date();
    };

    alert(DateTime.now());

JavaScript で静的なメソッドを呼び出す構文は、C# の場合と実質的に同じです。これは驚くほどのことではありません。事実上、コンストラクタ関数とクラスの名前は一致するからです。したがって、クラスもあれば、パブリックなプロパティやメソッドもあり、さらに静的なプロパティやメソッドまであるということです。これ以外に必要なものはあるでしょうか。もちろん、プライベート メンバは必要です。しかし、JavaScript にはプライベート メンバのネイティブ サポートは含まれていません (さらに言うと、プロテクト メンバもサポートしていません)。オブジェクトのすべてのプロパティとメソッドは、だれもがアクセス可能です。クラスにプライベート メンバを定義することは可能です。ただし、そのためにはクロージャについて知っておく必要があります。

クロージャ

私は自分から進んで JavaScript を勉強しようと思ったわけではありません。JavaScript が使用できなくては本格的な AJAX アプリケーションに取り組むうえで準備不足であるとわかり、急いで習得せざるを得なかったのです。初めのうちは、プログラミングのレベルを少し落としたような印象でした (何と言っても JavaScript です。C++ を使っている友人だったら何と言うでしょうか)。しかし、当初あった抵抗感を克服すると、JavaScript が実は強力で表現力に富むコンパクトな言語であることがわかってきました。JavaScript には他のよく使われる言語がサポートし始めたばかりの機能も含まれています。

JavaScript の高度な機能の 1 つにクロージャのサポートがあります。C# 2.0 は匿名メソッドを介してクロージャをサポートしています。クロージャとは、内側の関数 (C# では内側の匿名メソッド) が外側の関数のローカル変数にバインドされたときに発生する実行時の現象をいいます。当然ながら、内側の関数が何らかの形で外側の関数の外部でアクセス可能にならなければ、この現象はあまり意味がありません。例を使うとわかりやすくなります。

100 を超える数だけを取得し、他の数は除外するという、単純な基準のフィルタ処理を一連の数に適用する必要があるとします。この場合、図 8 のような関数を定義できます。

Figure 8 要素に対して述語に基づくフィルタ処理を適用する (Close Figure 8)
function filter(pred, arr) {
    var len = arr.length;
    var filtered = []; // shorter version of new Array();
    // iterate through every element in the array...
    for(var i = 0; i < len; i++) {
        var val = arr[i];
        // if the element satisfies the predicate let it through
        if(pred(val)) {
            filtered.push(val);
        }
    }
    return filtered;
}

var someRandomNumbers = [12, 32, 1, 3, 2, 2, 234, 236, 632,7, 8];
var numbersGreaterThan100 = filter(
    function(x) { return (x > 100) ? true : false; },
    someRandomNumbers);

// displays 234, 236, 632
alert(numbersGreaterThan100);

その後、別のフィルタ基準を作成する必要が生じたとします。たとえば、300 より大きな数だけを取得する場合は、次のようなコードを作成します。

var greaterThan300 = filter(
    function(x) { return (x > 300) ? true : false; },
    someRandomNumbers);

さらに、50 より大きな数、25 より大きな数、10 より大きな数、600 より大きな数などの取得が必要になることも考えられます。ここで、勘の良い人であれば、どの基準も述語は同じ "より大きな数" であることに気付きます。異なるのは数だけです。このような場合は、次のような関数によって数を変数化することができます。

function makeGreaterThanPredicate(lowerBound) {
    return function(numberToCheck) {
        return (numberToCheck > lowerBound) ? true : false;
    };
}
これによって、次のようなことが可能になります。

var greaterThan10 = makeGreaterThanPredicate(10);
var greaterThan100 = makeGreaterThanPredicate(100);
alert(filter(greaterThan10, someRandomNumbers));
alert(filter(greaterThan100, someRandomNumbers));

関数 makeGreaterThanPredicate が返す内側の無名関数をよく見てください。この内側の無名関数は、makeGreaterThanPredicate に渡された引数 lowerBound を使用します。通常のスコープ ルールでは、lowerBound は makeGreaterThanPredicate が終了するとスコープ外となります。ところが、この例では、内側の無名関数は makeGreaterThanPredicate の終了後も lowerBound を保持します。これがクロージャと呼ばれる現象で、内側の関数がその定義された環境 (外側の関数の引数とローカル変数) を閉じ込めることを意味します。

クロージャは、初めのうちはそれほど役に立つとは思えないかもしれません。しかし、適切な方法で使用すると、アイデアをコードとして具体化する際に注目すべき新たな可能性をもたらします。JavaScript でクロージャを使用する場合、その最も面白い用途の 1 つとして、クラスのプライベート変数をシミュレートすることがあります。

プライベート プロパティをシミュレートする

ではここで、プライベート メンバをシミュレートするうえでクロージャがどのように役立つのかを説明します。関数のローカル変数は、通常はその関数の外側からアクセスすることはできません。関数が終了すると、その関数のローカル変数はあらゆる意味で効力を失います。ただし、内側の関数のクロージャがローカル変数をキャプチャすると、変数はその後も存続できます。この点が JavaScript でプライベート プロパティをシミュレートするときの鍵となります。次の Person クラスについて考えてみましょう。

function Person(name, age) {
    this.getName = function() { return name; };
    this.setName = function(newName) { name = newName; };
    this.getAge = function() { return age; };
    this.setAge = function(newAge) { age = newAge; };
}

引数の name と age は、コンストラクタ関数 Person のローカル変数です。Person が終了すると、name と age は通常であれば効力を失いますが、Person のインスタンスのメソッドとして割り当てられた 4 つの内側の関数がキャプチャすることにより、それ以降も使用可能になります。ただし、name と age にアクセスできるのは、これら 4 つのメソッドだけです。これによって、次のような処理が可能です。

var ray = new Person(“Ray”, 31);
alert(ray.getName());
alert(ray.getAge());
ray.setName(“Younger Ray”);
// Instant rejuvenation!
ray.setAge(22);
alert(ray.getName() + “ is now “ + ray.getAge() +
      “ years old.”);

コンストラクタで初期化されないプライベート メンバは、次のようにコンストラクタ関数のローカル変数として定義することができます。

function Person(name, age) {
    var occupation;
    this.getOccupation = function() { return occupation; };
    this.setOccupation = function(newOcc) { occupation =
                         newOcc; };

    // accessors for name and age
}
これらのプライベート メンバは、C# のプライベート メンバとは働きが少し異なります。C# では、クラスのパブリック メソッドが同じクラスのプライベート メンバにアクセスできます。それに対し、JavaScript では、プライベート メンバにアクセスできるのは、クロージャ内にそれらのプライベート メンバがあるメソッドだけです (このようなメソッドは、通常のパブリック メソッドとは異なるため、特権メソッドと呼ばれます)。したがって、Person のパブリック メソッド内でプライベート メンバにアクセスするためには、次のように特権アクセサ メソッドを使用する必要があります。

Person.prototype.somePublicMethod = function() {
    // doesn’t work!
    // alert(this.name);
    // this one below works
    alert(this.getName());
};

Douglas Crockford は、クロージャを使用してプライベート メンバをシミュレートする手法を発見 (あるいは発表) した最初の人物として知られています。彼の Web サイト javascript.crockford.com には JavaScript に関する情報が大量に掲載されています。JavaScript に興味のある開発者の方はぜひチェックしてみてください。

クラスの継承

コンストラクタ関数とプロトタイプ オブジェクトを使用すると JavaScript でクラスをシミュレートできることについては、既に説明しました。プロトタイプ チェーンによって、すべてのオブジェクトで Object.prototype の共通のメソッドを使用できることも説明しました。クロージャによってプライベート メンバをシミュレートする方法も紹介しました。しかし、何かが抜けています。派生クラスを作成する方法には触れていませんでした。C# では頻繁に行う作業です。クラスを継承することは、C# ではコロンを 1 つ入力するような単純な作業ですが、残念ながら JavaScript ではそれほど簡単ではありません。その一方で、JavaScript は柔軟性に富んでおり、多くのクラス継承方法が存在します。

たとえば、図 9 のように、基本クラス Pet とその派生クラス Dog があるとします。このようなクラスを JavaScript ではどのように定義するのでしょうか。Pet クラスは簡単です。前に説明したとおり、次のように定義できます。

図 9 クラス
図 9 クラス

// class Pet
function Pet(name) {
    this.getName = function() { return name; };
    this.setName = function(newName) { name = newName; };
}

Pet.prototype.toString = function() {
    return “This pet’s name is: “ + this.getName();
};
// end of class Pet

var parrotty = new Pet(“Parrotty the Parrot”);
alert(parrotty);

では、Pet の派生クラス Dog はどのように作成するのでしょうか。図 9 を見るとわかるとおり、Dog には Pet にないプロパティ breed があり、Pet の toString メソッドをオーバーライドしています (メソッド名とプロパティ名については、C# では Pascal 形式の使用が推奨されていますが、JavaScript ではキャメル形式を使用するように規定されています)。これをどのように定義するのかを図 10 に示します。

Figure 10 Pet クラスの派生クラスを作成する (Close Figure 10)
// class Dog : Pet
// public Dog(string name, string breed)
function Dog(name, breed) {
    // think Dog : base(name)
    Pet.call(this, name);
    this.getBreed = function() { return breed; };
    // Breed doesn’t change, obviously! It’s read only.
    // this.setBreed = function(newBreed) { name = newName; };
}

// this makes Dog.prototype inherits
// from Pet.prototype
Dog.prototype = new Pet();

// remember that Pet.prototype.constructor
// points to Pet. We want our Dog instances’
// constructor to point to Dog.
Dog.prototype.constructor = Dog;

// Now we override Pet.prototype.toString
Dog.prototype.toString = function() {
    return “This dog’s name is: “ + this.getName() +
        “, and its breed is: “ + this.getBreed();
};
// end of class Dog

var dog = new Dog(“Buddy”, “Great Dane”);
// test the new toString()
alert(dog);

// Testing instanceof (similar to the is operator)
// (dog is Dog)? yes
alert(dog instanceof Dog);
// (dog is Pet)? yes
alert(dog instanceof Pet);
// (dog is Object)? yes
alert(dog instanceof Object);

ここで使用されているプロトタイプ置換手法によってプロトタイプ チェーンが正しく設定されるため、instanceof によるテストが C# を使用した場合と同じように実行されます。また、特権メソッドも期待どおりに動作します。

名前空間をシミュレートする

C++ や C# では、名前の競合が発生する可能性を最小限に抑えるために名前空間を使用します。.NET Framework では、たとえば Microsoft.Build.Task.Message クラスと System.Messaging.Message クラスを区別するような場合に名前空間を利用できます。JavaScript には、名前空間をサポートする固有の言語機能はありませんが、オブジェクトを使用することによって簡単に名前空間をシミュレートすることができます。ここで、JavaScript ライブラリの作成を例にとって考えます。グローバルな関数とクラスを定義する代わりに、それらを次のように名前空間にラップすることができます。

var MSDNMagNS = {};

MSDNMagNS.Pet = function(name) { // code here };
MSDNMagNS.Pet.prototype.toString = function() { // code };

var pet = new MSDNMagNS.Pet(“Yammer”);

名前空間の 1 レベルは一意でなくてもよいため、次のような入れ子になった名前空間を作成できます。

var MSDNMagNS = {};
// nested namespace “Examples”
MSDNMagNS.Examples = {};

MSDNMagNS.Examples.Pet = function(name) { // code };
MSDNMagNS.Examples.Pet.prototype.toString = function() { // code };

var pet = new MSDNMagNS.Examples.Pet(“Yammer”);
ご想像どおり、このような入れ子になった長い名前空間をいくつも入力していたら、すぐに嫌気がさしてきます。さいわい、ライブラリのユーザーは名前空間に次のような短い別名を簡単に付けることができます。

// MSDNMagNS.Examples and Pet definition...

// think “using Eg = MSDNMagNS.Examples;”
var Eg = MSDNMagNS.Examples;
var pet = new Eg.Pet(“Yammer”);
alert(pet);

Microsoft AJAX Library のソース コードを参照すると、ライブラリの作成者が同じような手法で名前空間を実装していることがわかります (静的メソッド Type.registerNamespace の実装を探してみてください)。詳細については、「OOP と ASP.NET AJAX」のリンクを参照してください。

JavaScript ではこのようにコーディングが必要なのか

JavaScript がオブジェクト指向プログラミングを適切にサポートしていることについては既に説明しました。JavaScript はプロトタイプ ベースの言語として設計されていますが、柔軟性が高く強力であるため、他のよく使われる言語で一般的なクラス ベースのプログラミング スタイルにも対応できます。ただし、ここで疑問が生じます。JavaScript でも、そのようなスタイルでコーディングを行ってよいのでしょうか。JavaScript でも C# や C++ の場合と同じコーディング スタイルを使用し、JavaScript にはない機能をシミュレートする巧妙な方法を編み出していけばよいのでしょうか。プログラミング言語はそれぞれに違いがあり、ある言語のベスト プラクティスが別の言語でもベスト プラクティスであるとは限りません。

既に説明したとおり、JavaScript では (クラスがクラスを継承するのではなく) オブジェクトがオブジェクトを継承します。したがって、静的な継承階層を使用して多くのクラスを作成することが JavaScript のやり方ではない可能性があります。おそらく、Douglas Crockford が自分の記事「JavaScript におけるプロトタイプ的継承」で述べているように、JavaScript のプログラミング手法は、プロトタイプ オブジェクトを作成し、次のような簡単なオブジェクト関数を使用して新しいオブジェクトを作成するというものです。つまり、新しいオブジェクトは元のオブジェクトを継承します。

    function object(o) {
        function F() {}
        F.prototype = o;
        return new F();
    }
JavaScript のオブジェクトは柔軟性に富んでいるため、作成後に新たなフィールドやメソッドを追加し、簡単にオブジェクトを拡張することができます。

このこと自体はまったく問題がないとしても、世界的に見て開発者の大部分がクラス ベースのプログラミングの方に慣れ親しんでいることは否定できない事実です。実際、クラス ベースのプログラミングは JavaScript にも浸透してきています。近々発表される ECMA-262 (JavaScript の公式仕様) の第 4 版によると、JavaScript 2.0 には正式なクラスが導入されるようです。このことから、JavaScript もクラス ベースの言語に移行しつつあると言えます。ただし、JavaScript 2.0 が普及するまでには、おそらく数年かかるでしょう。それまでの間は、現行の JavaScript について、プロトタイプ ベースとクラス ベースの両方のスタイルで JavaScript のコードを読み書きできるだけの知識を身に付けることが大切です。

大局的に見る

対話的なクライアント集約型 AJAX アプリケーションが急増したことにより、.NET 開発者の間では利用価値の高いツールの 1 つとして JavaScript が急浮上してきました。ただし、そのプロトタイプ ベースという特性は、C++、C#、Visual Basic などの言語に慣れている開発者を初めのうちは驚かせることになったようです。私にとって JavaScript の習得は実りあるものでしたが、その過程で嫌気がさしたことがまったくなかったわけではありません。皆さんが JavaScript を習得するうえで、この記事が役に立ってくれればさいわいです。それが私の目指したことだからです。

OOP と ASP.NET AJAX

ASP.NET AJAX に実装されている OOP は、ここで説明した標準的な実装とは多少異なります。その主な理由は 2 つあります。1 つはリフレクションの点で ASP.NET AJAX バージョンの方が多くの可能性を持っていることで (リフレクションは XML-Script などの宣言構文やパラメータ検証などに必要です)、もう 1 つは ASP.NET AJAX がプロパティ、イベント、列挙、インターフェイスなどの .NET 開発者に馴染み深い構文を JavaScript に変換することを目的としていることです。

JavaScript の現行バージョンでは .NET 開発者が慣れ親しんでいる OOP の主要な概念がいくつか欠如していますが、そのほとんどは ASP.NET AJAX でエミュレートされています。

クラスには、名前付け規則に基づくプロパティ アクセサ (後で例を示します) や .NET が提供するパターンを忠実に再現するパターンに基づくマルチキャスト イベントを定義できます。プライベート変数は、アンダースコアで始まるメンバはプライベート メンバであるという規則に基づいています。真のプライベート変数が必要となることはめったになく、この規則によってプライベート変数をデバッガで検査することができます。通常のアヒル型定義 (アヒルのように歩き、アヒルのように鳴くものはアヒルであり、そうでないとしてもアヒルとして扱うことができるという考え方) を越えた型チェック シナリオを可能にするインターフェイスも導入されています。

クラスとリフレクション

JavaScript には、関数の名前を調べる機能はありません。関数の名前を調べることができたとしても、ほとんどの場合、役には立ちません。クラスを作成するには、一般に名前空間で定義された変数に無名関数を割り当てるからです。実際に型名を構成しているのは、この変数の完全修飾名です。この変数はどこからもアクセスできず、そのコンストラクタ関数は何の情報も持っていません。この問題を解決し、ASP.NET AJAX が JavaScript クラスに対する充実したリフレクション機能を提供するためには、型名を登録する必要があります。

ASP.NET AJAX のリフレクション API は、組み込み型、クラス、インターフェイス、名前空間、列挙など、どのような型にも適用でき、isInstanceOfType や inheritsFrom など、実行時にクラス階層を検査する .NET Framework 型の関数を備えています。ASP.NET AJAX にはデバッグ モードで実行できる型チェック機能もあり、バグの早期発見に役立ちます。

クラス階層の登録と基本クラスの呼び出し

ASP.NET AJAX でクラスを定義するためには、クラスのコンストラクタを変数に割り当てる必要があります (コンストラクタが基本クラスを呼び出す方法に注目してください)。

MyNamespace.MyClass = function() {
    MyNamespace.MyClass.initializeBase(this);
    this._myProperty = null;
}
Then, you need to define the class members itself in its prototype:

MyNamespace.MyClass.prototype = {
    get_myProperty: function() { return this._myProperty;},
    set_myProperty: function(value) { this._myProperty = value; },
    doSomething: function() {
        MyNamespace.MyClass.callBaseMethod(this, “doSomething”);
        /* do something more */
    }
}
最後にクラスを登録します。

MyNamespace.MyClass.registerClass(
    “MyNamespace.MyClass “, MyNamespace.BaseClass);
コンストラクタやプロトタイプ階層を管理する必要はありません。その役割は registerClass 関数が担っています。

Bertrand Le Roy は ASP.NET AJAX チームのソフトウェア設計エンジニア II です。

Ray Djajadinataは Barclays Capital Singapore で AJAX アプリケーションの開発に夢中で取り組んでいます。Ray の連絡先は、ray.djajadinata@gmail.com です。

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


ゲームの勝敗は?: XNA Game Studio Express で想像力を解き放つ

ゲームの勝敗は?

XNA Game Studio Express で想像力を解き放つ

Charles Cox and Michael Klucher
この記事の内容 : :
  • ゲーム開発の基本
  • Spacewar Starter Kit
  • Spacewar に機能を追加する
  • ゲームのグラフィックをカスタマイズする
この記事は次のテクノロジを使用しています:
XNA Game Studio Express、Xbox 360、C#
コードのダウンロード : : XNA2007_05.exe (155KB)
コンテンツ
ゲーム開発の基本
Spacewar Starter Kit
ゲーム ループを理解する
シールドを装備する
細部をレンダリングする
シールドにパワーを与える
おまけ : 宇宙空間の変更
発射する

ビデオ ゲームをプレイしていて、「こんなふうに自分でゲームを作れたらいいのに」と思ったことはありませんか。XNA™ Game Studio Express は、主に学生、趣味での利用者、フリーのゲーム開発者を対象にした、Microsoft® Windows® と Xbox 360™ の新しいゲーム開発ソリューションです。

Visual C#® 2005 Express Edition を使用した XNA Game Studio Express は、XNA Framework を基に構築された、ゲーム開発用のマネージ コード クラス ライブラリです。このフレームワークでは、Xbox 360 と Windows XP SP2 というまったく異なる 2 つのプラットフォーム向けのコードの作成手順が簡素化されています。作成するコードの多くは、一切の変更なしに両方のシステムで動作します。

Windows 向けのゲームの作成は、XNA Game Studio Express を MSDN® の Web サイト (msdn2.microsoft.com/xna) からダウンロードすれば、すぐに始めることができます。Xbox 360 コンソールでゲームを開発するには、XNA Creators Club の会員になる必要があります。詳細については、msdn2.microsoft.com/bb219592.aspx を参照してください。会員になると、ゲーム資産や XNA Creators Club 専用バージョンのスタート キットを利用できるようになります。

XNA Game Studio Express についての説明を開始する前に、XNA Game Studio Express をダウンロードしてインストールする必要があります。Xbox 360 コンソール上にセットアップする場合は、ビデオによる手順説明を msdn.com/xna/videos で参照できます。"XNA Creators Club 入門" というタイトルのビデオを探してください。

ゲーム開発の基本

ゲーム開発にはいくつかの特有の課題があります。ゲームには、デザイン面だけでなく、技術的にもユーザーを惹きつける魅力が必要です。つまり、目を奪われるようなグラフィックを使うと同時に適切なフレーム レートを維持する必要があります。ユーザーが直観的にゲームを操作できるような入力方法や、高品質なオーディオ出力も検討すべき点です。

XNA Game Studio Express が登場する前は、ゲーム開発者は DirectX® や OpenGL などのテクノロジを使用していました。これらの API にはハードウェアへの基本的なアクセス機能はありますが、直観的ではありません。そのため、多くの場合、開発者はグラフィック、オーディオ、およびエンジンと呼ばれるフレームワークへの入力などのコア コンポーネントをアセンブルし、フレームワークの上の層に固有のゲーム ロジックを記述していました。この抽象化を作成および使用するには費用と手間がかかります。

また、ゲーム開発の使用言語である C++ でのメモリおよびロジックの管理は技術的に難しく、ゲームに使用する数多くのアート コンテンツの管理も容易ではありません。ゲーム開発がアマチュア開発者にとって格別ハードルの高いチャレンジであるというのも無理からぬ話です。さらに、Xbox 360 などの次世代ビデオ ゲーム コンソールの開発に求められる技術要件およびビジネス要件の複雑さもゲームの開発に及び腰になる一因となります。

Visual C# 2005 Express Edition を基に構築された XNA Game Studio Express を使ってみてください。XNA Game Studio Express は趣味の利用者や学生などのゲーム開発愛好家向けに設計され、C# および Visual C# 2005 Express Edition 統合開発環境を使用して Windows および Xbox 360 上で固有のゲームを作成する機能を提供します。

XNA Game Studio Express には、Windows と Xbox 360 向けのクロス プラットフォーム開発 API である XNA Framework があります。XNA Framework を使用すれば、グラフィック、オーディオ、入力、ストレージはもちろんのこと、すべてのゲームで使用される基本的なタイミングと描画ループも大幅に簡素化されます。

また、XNA Game Studio Express では、XNA Framework Content Pipeline が Visual C# 2005 Express Edition の構築環境に組み込まれています。Content Pipeline を使用すれば、3D モデル、グラフィック テクスチャ、シェーダー、オーディオなどのアート コンテンツを IDE のソリューションに追加することが可能であり、そのコンテンツはコード内でアクセス可能なランタイム オブジェクトに組み込まれます。これにより、アート コンテンツに伴うオーバーヘッドが削減されます。

以前のゲーム開発ソリューションでは、一から開発を始めるのが一般的でした。つまり、タイミングやレンダリング ループを手動で指定して時間の間隔を管理し、ゲームの動作が適切に処理されるようにしていました。XNA Game Studio Express では、タイミングおよびレンダリング ループは自動的に設定されます。固有のタイミングおよびレンダリングを処理する Game クラスが用意されており、このクラスでは、オブジェクトを追加して各フレームを更新する Update メソッドや、オブジェクトを追加して画面に描画する Draw メソッドが公開されています。

また、アプリケーション モデルと呼ばれるゲーム フレームワークを使用すれば、適切なグラフィック デバイスが自動的に設定されるため、複雑なデバイス列挙体のコードを記述する必要がありません。このコードを配置した XNA Game Studio Express プロジェクトをコンパイルして実行すれば、すぐに適切なタイミングおよびレンダリング ループで動作します。

Spacewar Starter Kit

XNA Framework コード テンプレートのほかに、XNA Game Studio Express には Spacewar Starter Kit が付属しています。このキットには、完全な機能を備えたゲームとユーザーの手で変更可能なソース コードおよびアート資産が含まれています。Spacewar Starter Kit は、1962 年に Stephen Russell、Peter Samson、Dan Edwards の各氏の手によって DEC PDP-1 向けに開発された元祖 Spacewar の実装に基づいて作られた 2 人用のトップダウン型スペース シューティング ゲームです。スタート キットに収録されているゲームには 2 種類のバージョンがあります。Retro (レトロ) バージョンにはオリジナルのゲームとグラフィックが保存され、Evolved (現代風) バージョンには改善された 3D グラフィックおよびサウンドが含まれています。

では、これからスタート キットを開け、Evolved (現代風) バージョンを使ってみて、ゲームの内容を確認してから、新しい機能として各プレイヤーが一時的に敵の射撃を阻止するシールドを追加します。

まず、XNA Game Studio Express を起動し、[File] (ファイル) メニューをクリックしてから [New Project] (新しいプロジェクト) をクリックします。[New Project] (新しいプロジェクト) ウィンドウが開いたら、[Spacewar Windows Starter Kit] をクリックし、[OK] をクリックします (図 1 を参照)。Creators Club に登録し、XNA Game Studio Express から入手できるように Xbox 360 をセットアップしている場合は、[Spacewar Windows Starter Kit] ではなく、[Spacewar Xbox 360 Starter Kit] をクリックして [OK] をクリックします。

図 1 Spacewar Starter Kit を開く
図 1 Spacewar Starter Kit を開く(画像を縮小するには、ここをクリックします)
図 1 Spacewar Starter Kit を開く
図 1 Spacewar Starter Kit を開く (画像を拡大するには、ここをクリックします)

Spacewar Starter Kit の読み込みには多少の時間がかかります。読み込みが完了すると、コード ファイルがソリューション エクスプローラに表示されます。また、ゲームのプレイ方法を示す手順も表示されます。最適な状態で使用するため、2 つの Xbox 360 コントローラを接続します。キーボードを使うこともできます。Spacewar Starter Kit の読み込みが完了すると、手順ページにコントロールが表示されます。

F5 キーを押して Spacewar アプリケーションをコンパイルし、実行します。ゲームが開始したら、Evolved (現代風) モードを選択して、しばらくゲームをプレイしてみてください。

ゲーム ループを理解する

XNA の重要な機能の 1 つにゲーム ループがあります。このループでは、(ユーザー入力、ゲーム内の状態などの適用可能な状態に基づいた) ゲームの状態を継続的に更新し、レンダリング (画面への描画、適切なオーディオの再生、コントローラの振動などのユーザーへの出力に関する処理) を行います。Spacewar のゲーム ループの処理を理解しておけば、この記事でこれからスタート キットに加える変更を理解するのに役立ちます。

ゲームの実行は、他の .NET アプリケーションと同様に Main メソッドで開始します。Main メソッドで行う処理は、次のように、SpacewarGame クラスのインスタンスを作成して基本クラスの Run メソッドを呼び出すだけです。

static void Main(string[] args)
{
    using (SpacewarGame game = new SpacewarGame())
    {
        game.Run();
    }
}

SpacewarGame は Microsoft.Xna.Framework 名前空間の Game クラスから派生します。このクラスでは、ゲームのライフサイクルに関する主なメソッド、プロパティ、およびイベントが公開されています。次に、Game のメソッドをオーバーライドして、自作のゲームに固有の機能を指定します。たとえば、SpacewarGame で BeginRun メソッド (Run から呼び出されます) をオーバーライドして、ゲームのタイトル音楽を再生し、内部ステート マシンをロゴ画面の段階に変更して、プレイヤーに対してすべてが表示される初期カメラを作成します。

SpacewarGame の ChangeState メソッドは、BeginRun メソッドから呼び出されると、ゲームで使用するオブジェクトの多くを作成します。BeginRun メソッドから GameState.LogoSplash を引数に指定して ChangeState を呼び出すと、TitleScreen が作成されます。TitleScreen は FullScreenSplash から派生し、FullScreenSplash は Spacewar プロジェクトで定義された抽象 Screen クラスから派生しています。Screen クラスは、スプラッシュ スクリーン、選択スクリーンや実際のゲーム レベルなど、ゲームのレンダリングの単位を表します。このクラスは複数の仮想メソッドを公開し、その派生型でオーバーライドして画面固有の機能を提供します。TitleScreen は Screen.Update メソッドをオーバーライドして、ユーザーが A、B、または X を押したかどうかを確認し、次に進むゲーム モード (Evolved (現代風)、Retro (レトロ)、または Information (情報)) を指定します。TitleScreen は Spacewar で使われる Screen 派生型の 1 つにすぎません。その他に SelectionScreen (宇宙船選択の場合)、EvolvedScreen (ゲームの "現代風" バージョンをプレイする場合)、VictoryScreen (プレイヤーが勝った場合) などがあります。

各 Screen には、画面の機能に必要な状態と、画面に固有の更新およびレンダリングの動作がカプセル化されています。EvolvedScreen と RetroScreen は、各ゲーム タイプの主なゲーム プレイを表し、SpacewarScreen から派生しています。

public class SpacewarScreen : Screen
{
    protected Projectiles bullets;
    protected Ship ship1;
    protected Ship ship2;
    protected SceneItem sun;
    protected Particles particles;
    protected SceneItem backdrop;

    protected bool paused = true;
    protected int player1Score;
    protected int player2Score;

    ...
}

SpacewarScreen には、両対戦者の宇宙船 (SceneItem 基本クラスから派生した SpacewarSceneItem から派生している Ship として実装されます) の SceneItems、レンダリング対象となるその他のすべてのパーティクルやプロジェクタイル、ゲームの背景となる太陽など、画面上のレンダリング項目が格納されます。また、プレイヤーのスコアやゲームが一時停止されているかどうかなどの状態も保持されています。

Game の作成時 (この場合は SpacewarGame) には、ゲームを収容するホストが作成され、アプリケーション レベルのサービス (アクティブ化、非アクティブ化、終了、アイドル状態などのイベントの通知) がゲームに提供されます。後者の機能は特に重要です。Game.Tick メソッドがこのアイドル ハンドラで呼び出されると、Tick メソッドは Game.Update メソッドと Game.DrawFrame メソッドを呼び出します。Game.Update メソッドは SpacewarGame.Update でオーバーライドされ、ChangeState メソッドで設定されている現在のシーンの Update を呼び出します。Game.DrawFrame から呼び出される Game.Draw は SpacewarGame でオーバーライドされ、現在のシーンで Render を呼び出します。

シールドを装備する

Spacewar に新しい機能を追加するのは簡単です。では、エネルギー シールドを追加しましょう。このシールドは、シーン内の他の項目から被害を受けないように宇宙船を保護します。それには Ship.cs および SpacewarScreen.cs の 2 つのコード ファイルの編集が必要です。これらのファイルは msdn.microsoft.com/msdnmag/code07.aspx からダウンロードまたは参照できます。

Ship.cs ファイルにコードの大部分を記述します。Spacewar では、Ship.cs ファイルは Ship オブジェクトを定義します。Ship オブジェクトには、プレイヤーが制御する宇宙船オブジェクトを定義するメソッドやデータが含まれます。ソリューション エクスプローラで Ship.cs を探し、ダブルクリックしてファイルを開いてください。

まず、シールドのロジックを制御する変数を定義する必要があります。シールドが上がっている時間は 5 秒間で、1 ラウンドに 1 回だけ使用できます。Ship.cs の 40 行目に次のコードを追加します (宇宙船が現在 invulnerable、inHyperspace、または inRecovery のいずれの状態であるかなどの情報を保持する他のプライベート メンバの間に追加します)。

private bool inShield;
private bool shieldUsed;
private double exitShieldTime;
private const double shieldTime = 5.0;
これで、シールドがアクティブであるかどうか、シールドが既に使用されているかどうか、およびシールドのタイマーの残り時間を追跡する変数が設定されます。

宇宙船のシールドがアクティブかどうかを簡単に確認できるプロパティを使うことにしましょう。このプロパティは、後で SpacewarScreen.cs を変更するときに使用します。Ship.cs の 188 行目に移動して (前述のスニペットを追加したために 188 行目になりました)、次のコードを追加します。

public bool Shield { get { return inShield; } }

ここまでで複数の変数やアクセサを準備しました。これらの変数をゲームの動作に関連付けます。そのために Ship.cs の Update メソッドを変更します。前述したように、Ship の Update メソッドは、XNA Framework アプリケーション モデルから 1 フレームに 1 回ずつ (ゲームの実行速度によってはより頻繁に) 自動的に呼び出されるゲーム ループの Update の呼び出しで呼び出されます。Update では、ゲーム オブジェクトを動かし、オブジェクト間のやりとりを処理し、ユーザー入力を取得します。これらの処理をこの Update メソッドで行います。

次に、Ship の Update メソッドを変更し、ユーザーからの入力に反応して新しいシールドが有効になるようにします。Ship.cs の 248 行目 (Update メソッド内の "if (inHyperspace)" のチェックの直前) に移動し、次のコードを追加します。

if (XInputHelper.GamePads[player].XPressed && !shieldUsed)
{
    inShield = true;
    shieldUsed = true;
    exitShieldTime = shieldTime;
    Sound.PlayCue( Sounds.PhaseActivate );
}
キーボード操作やコントローラのボタンが押されたときのチェックを行うための入力の取得は、条件ステートメントで示されている 1 行で完了です。Spacewar Starter Kit に付属の XInputHelper には、入力を簡素化するために Xbox 360 コントローラとキーボード入力のラッパーが用意されています。XInputHelper を使用した場合、この if ステートメントで、宇宙船が関連付けられているプレイヤーが X ボタンを押したかどうかがチェックされます。キーボードを使用している場合は F キーがプレイヤー 1、Page Up キーがプレイヤー 2 に相当します。

これらのキーボード マッピングは、Spacewar プロジェクトの settings.xml ファイルを変更することで簡単に変えられます。Settings.xml は Spacewar に固有の設定 (SpacewarGame の Game.Initialize メソッドのオーバーライドで読み込まれます) ですが、一般に、ゲーム データには XML データ ファイルを使用した方が良いでしょう。XNA Game Studio Express は .NET Framework を基にして構築されているため、Xml 名前空間がゲームに公開されます。

条件ステートメント内のコードでは、シールドを上げ、このラウンドでシールドが使用されていることを通知し、シールドのタイマーを起動しています。また、XNA Framework のオーディオ システムを使用してキューを再生し、そのキューでユーザーのサウンドを再生します。Spacewar では、再生するオーディオ キューを識別するために、Sounds.PhaseActivate などの列挙値を使用して XNA Framework のオーディオ層を抽象化しています。通常、キューの呼び出しには文字列値を使用します。Spacewar のように、ゲームで使用するキューの数が限られている場合には (特に、開発中に名前が変更される場合は)、列挙体が有効です。

ゲームにサウンドを追加するには、XNA Game Studio Express に付属の Microsoft Cross-Platform Audio Creation Tool (XACT) を使います。すべてのオーディオ ファイルおよびそのファイルに関連付けられたキューが XACT プロジェクトに保持され、プロジェクトにサウンドをドラッグ アンド ドロップする機能を持つグラフィカル インターフェイスに表示されます。ゲームを作成するとき、XNA Framework Content Pipeline が XACT プロジェクトを読み込んで必要なオーディオ ファイルを作成します。Spacewar にサウンドを追加する場合は、キュー名配列にキュー名を追加し、この配列に Sounds インデックスの列挙値を指定する処理を追加する必要があります。この Sounds 列挙体を使ってサウンドを呼び出すことができます。

Update の呼び出しで、シールドがアクティブな場合にシールドのタイマーを実行するロジックを追加します。348 行目 ("if (!Paused)" ブロックの最後) に移動して次のコードを追加します。

if (inShield)
{
    exitShieldTime -= elapsedTime.TotalSeconds;
    if (exitShieldTime <= 0)
    {
        inShield = false;
        Sound.PlayCue( Sounds.PhaseExpire );
    }
}
このコードでは、単純にシールドの残り時間から経過時間を差し引いています。経過時間は Update が呼び出されてから次に Update が呼び出されるまでに経過した時間です。この時間はゲームの動作やタイマーなどのロジックに適用され、時間の経過をシミュレーションして、XNA Framework によって Update に渡されます。シールドは時間切れになると非アクティブ化され、適切なサウンドが再生されます。

細部をレンダリングする

入力やサウンドは簡単に処理できても、シールドの外観をどうするかという問題が残っています。今度は Render メソッドにコードを追加する必要があります。そこで、XNA Framework に用意された数値演算機能と描画機能を使って宇宙船の 3D の位置にスプライト (透明な 2D テクスチャ) を重ねて描画します。

ソリューション エクスプローラのソリューション ツリーで Content フォルダを探し、このフォルダを開いて、その下にある Textures フォルダを開きます。各種の .tga ファイルが表示されます。これらのファイルのうちの 1 つをシールドのテクスチャとして使います。.tga ファイルは 2D グラフィカル イメージ (スプライト) を表します。

Ship.cs の 433 行目に移動し、その直後 ("if (showThrust && evolved)" ブロックの後) に新しい行を 1 行作成します。この新しい行に図 2 のコードを追加します。

Figure 2 スプライトをレンダリングする (Close Figure 2)
if (inShield && evolved)
{
    Texture2D shieldtexture =
        SpacewarGame.ContentManager.Load<Texture2D>(
        SpacewarGame.Settings.MediaPath + @”textures\circle” );

    batch.Begin( SpriteBlendMode.AlphaBlend );

    // Move into screen space.
    Vector3 location = device.Viewport.Project(
        position, SpacewarGame.Camera.Projection,
        SpacewarGame.Camera.View, Matrix.Identity );

    batch.Draw(
        shieldtexture,
        new Vector2( location.X - shieldtexture.Width / 2.0f,
                     location.Y - shieldtexture.Height / 2.0f ),
        new Rectangle( 0, 0, shieldtexture.Width, shieldtexture.Height ),
        new Color( new Vector4( 0.0f, 1.0f, 0.0f, 0.5f ) ), 0.0f,
        new Vector2( shieldtexture.Width / 2.0f,
                     shieldtexture.Height / 2.0f ),
        (3.0f * (float)(Math.Abs( Math.Cos( exitShieldTime ) ))) + 2.0f,
        SpriteEffects.None, 0.0f);

    batch.End();
}

このコードを順番に見てみましょう。まず、XNA Framework と XNA Framework Content Pipeline を使用して、ContentManager クラスで Content\Textures フォルダから circle という名前のテクスチャを読み込みます。ContentManager クラスは、実行時に Content Pipeline からコンテンツを読み込む XNA Framework クラスです。このクラスは、新規の各 XNA Game Studio Express プロジェクトで事前設定されたアプリケーション モデル コードの一部です。"circle" という名前が Content\Textures フォルダ内の circle.tga ファイルに関連付けられています。XNA Framework Content Pipeline は各コンテンツ ファイルにフレンドリ名 (わかりやすい名前) を割り当て、このファイルを使用して実行時にコンテンツを読み込みます。

次に、テクスチャが XNA Framework の Texture2D クラスに読み込まれます。SpriteBatch (このコード ブロックの名前付きバッチ) を使用して画面に Texture2D オブジェクトが描画されます。SpriteBatch は、すぐに画面に描画されるテクスチャの集まりを表します。

これからスプライトの描画が開始することを示す batch.Begin を呼び出します。アルファ ブレンディングを使用することに注意してください。これにより、円形のテクスチャの枠の周囲を透明にし、全体を部分的に透過にすることができるため、シールドの下にある宇宙船が見えます。

描画する前に、画面上の描画位置を知る必要があります。Spacewar Evolved は 3D ゲームであるため、オブジェクトは 3 次元で移動し、描画するときだけ 2D 平面上に投影されます。シールドの描画位置を知るには、3D の宇宙船の 2D 座標を求める必要があります。

XNA Framework には、この情報をグラフィックス デバイスから取得する機能があります。それには、device.Viewport.Project を呼び出して、宇宙船の 3D 座標と、表示および投影マトリックスを渡します。表示マトリックスは各 3D オブジェクトに適用する線形代数変換を定義して、プレイヤーの視点の適切な相対位置にオブジェクトを配置します。投影マトリックスは、オブジェクトを画面に描画するために各オブジェクトの 3D 座標を 2D 画面座標に変換します。多くの場合、これらのマトリックスは事前に計算され、保存されています。Spacewar では、これらのマトリックスを、プレイヤーの視点も定義している Camera クラスに保存します。これらの値を使って device.Viewport.Project を呼び出すと、宇宙船の位置を 2D 座標に変換するシミュレーションが行われますが、これらの座標に実際に描画はしません。この呼び出しで、シールドの描画に使う 2D 描画座標を保持するベクトルが返されます。

次に、以下のパラメータを渡して batch.Draw を呼び出します。

  • Texture2D オブジェクト。
  • 投影された座標からスプライトの幅と高さの半分を引いた値。これでスプライトを宇宙船の中央に配置します。
  • テクスチャのソース サイズ。
  • スプライトの既存の色を掛ける色。ここでは Vector4 として設定します。赤、緑、青を表す 3 つの要素を指定し、4 つ目にアルファまたは不透明度を指定します。これらの値は部分的に透明な緑を定義します。透明の設定によってシールドの下にある宇宙船が見えるようになります。
  • 倍率。シールドの残り時間の余弦を取得するところがポイントです。これでシールドのサイズを時間の経過とともに変化させます。視覚効果を高めるためだけの処理です。
  • スプライトの効果フラグ。None に設定しましたが、このフラグを使ってスプライトを水平または垂直に反転することができます。
  • 画面深度。値を 0 に設定すると、スプライトが一番上に描画され、スプライトが宇宙船の下ではなく、上にレンダリングされることが保証されます。

シールドにパワーを与える

この時点でも、プレイヤーがシールドを上げて視覚化し、適切な時間が経過したら非表示にするという機能はあります。ただし、まだシールドに保護の機能がありません。保護機能を付加するには、SpacewarScreen.cs を開いて、2 つの単純なコード ブロック (各プレイヤーについて 1 ブロックずつ) を追加します。SpacewarScreen.cs の 94 行目に移動して、HitPlayer2 の冒頭に次のコードを追加します。

if (ship2.Shield)
{
    // Make a bouncy sound rather than an explosion.
    Sound.PlayCue( Sounds.WeaponPickup );
    return;
}
また、122 行目に移動して、HitPlayer1 の冒頭に次のコードを追加します。

if (ship1.Shield)
{
    // Make a bouncy sound rather than an
    // explosion.
    Sound.PlayCue( Sounds.WeaponPickup );
    return;
}

前述のように、SpacewarScreen.cs は Spacewar ゲーム ループの更新ロジックを表しています。Spacewar を含む多くのゲームでは、メイン メニュー、武器の選択、実際のゲームなど、ゲームのさまざまな状態ごとに独立したゲーム ロジック ループを利用しています。プレイヤーがゲームを進めていくうちに、制御は他の状態に渡されます。Spacewar では、これらの状態を画面と呼び、Screen クラスで表します。SpacewarScreen は Screen から派生し、実際のゲームプレイの状態を処理します。この中で、2 人のプレイヤーが宇宙船を制御して対戦します。

SpacewarScreen クラスでは、画面中を動き回る宇宙船、武器、小惑星などのゲーム オブジェクトを Update ループで管理します。これらのオブジェクトが宇宙船と衝突すると、ロジックが HitPlayer1 または HitPlayer2 を呼び出し、宇宙船に与えるダメージを計算します。

このコードでは、HitPlayer1 および HitPlayer2 のゲーム ロジックを変更し、宇宙船がダメージを受けたら、シールドをチェックしてからダメージを解決するようにしています。シールドが上がっている場合、防御サウンドが再生され、それ以上の処理を行わずにダメージ関数は終了します。

これだけです。F5 キーを押してコンパイルと実行を行い、ゲームをプレイしてみてください。メイン メニューから [Evolved] (現代風) ゲーム モードを選択します。ゲームを開始したら、図 3 のように、X ボタンまたはキーボードの相当するキー (プレイヤー 1 の場合は F、プレイヤー 2 の場合は Page Up) を押してシールドを上げます。プレイヤーがシールドを使用できるのは 1 ラウンドに 1 回限りですが、シールドはきわめて強力です。あらゆる種類の弾丸を吸収し、小惑星が当たってもダメージを受けません。太陽さえダメージを与えることができないのです。

図 3 パワーを与えられたシールド
図 3 パワーを与えられたシールド(画像を縮小するには、ここをクリックします)
図 3 パワーを与えられたシールド
図 3 パワーを与えられたシールド (画像を拡大するには、ここをクリックします)

シールドを使って障害物の周囲を旋回してください。ただし、シールドが時間切れになったら太陽に近づきすぎないように注意しましょう。シールドが上がっている時間はわずか 5 秒です。

さらにゲームの内容を発展させるアイデアをいくつかご紹介しましょう。ベクトル演算を使って、弾丸がシールドに吸収されるのではなく、シールドで跳ね返されるようにするというのも一案です。自分の撃った弾が跳ね返ってきたら敵は慌てるでしょう。また、ダメージ システムを変更して、シールドで保護された宇宙船が敵に打撃を与えたときは敵のダメージを倍にするという方法もあります。猛スピードの攻撃です。

ゲーム作成の醍醐味は実験にあります。XNA Game Studio Express を使用すれば、よい材料を手に入れて自力でゲームを作成するのは簡単です。

おまけ : 宇宙空間の変更

XNA Game Studio Express が役に立つのは、ゲームに新機能を追加するために最善のコードを作成しようと頭を悩ませているときばかりではありません。前述したように、XNA Framework Content Pipeline と呼ばれる機能セットも組み込まれています。XNA Framework Content Pipeline は、XNA Framework をベースにしたゲームの 2D、3D、およびオーディオ コンテンツの作成にも役立ちます。

XNA Framework Content Pipeline では、コンテンツの作成およびコンパイルのプロセスが Visual C# Express に統合されています。これにより、一定の種類のコンテンツを直接 Visual C# Express から起動できるというメリットが得られます。コンテンツを編集して再コンパイルすれば、ゲームの変更内容を確認できます。コードのコンパイル時にコンテンツもコンパイルされるため、ゲームを実行する前にコンテンツのエラーを見つけることができます。

XNA Framework Content Pipeline は各種の 2D グラフィック ファイル形式 (.bmp、.jpg、.png、.tga、.dxt など) をサポートしています。また、2 種類の 3D ファイル形式 (.x および .fbx) もサポートしています。どちらの 3D 形式にも、数多くの無償および有償 3D グラフィック作成ツール用のファイル形式エクスポータがあります。XNA Framework Content Pipeline は拡張性もきわめて高く、オンラインで検索したファイルのサポートを追加することが可能です。

Spacewar を使って XNA Framework Content Pipeline を試してみましょう。XNA Game Studio Express に戻り、ソリューション エクスプローラに移動します。Content フォルダを開いて、Textures フォルダを開きます。Spacewar で使用する多くのテクスチャが表示されます。B1_nebula01.tga ファイルをクリックします。このファイルはゲーム中に使われる背景の 1 つです。ソリューション エクスプローラの下部にあるプロパティ グリッドに、関連するプロパティが表示されます (図 4 を参照)。プロパティ グリッドが表示されない場合は B1_nebula01.tga ファイルを右クリックし、[プロパティ] をクリックします。

図 4 テクスチャのプロパティ
図 4 テクスチャのプロパティ

プロパティ グリッドに、XNA Framework Content Pipeline 専用の固有プロパティが表示されています。ここでは、[Content Importer] (コンテンツ インポータ) プロパティを使用しています。インポータはソース データ (この場合は .tga ファイル) を取得し、XNA Framework コンテンツの DOM に変換する機能を果たします。コンテンツ DOM にはインポータで変換および正規化されたデータが保存され、これによってプロセッサの動作はファイル形式に依存しなくなります。次のプロパティは [Content Processor] (コンテンツ プロセッサ) プロパティです。このプロセッサは、コンテンツ DOM からのデータを実行時に使用するのに適したオブジェクト (この場合はスプライト) に変換する機能を果たします。[Asset Name] (アセット名) プロパティは実行時のアセットの読み込みに使用します。アセットの処理をオフにするには、[XNA Framework Content] (XNA Framework コンテンツ) プロパティを false に設定します。

B1_nebula01.tga グラフィック ファイルを編集して、Spacewar がどのように変更されたかを確認してみましょう。そのためには、.tga ファイルを操作する機能を持つイメージ エディタが必要です。エディタがない場合は、getpaint.net から Paint.NET をダウンロードできます。

B1_nebula01.tga ファイルをダブルクリックして、このファイルに関連付けられたエディタを起動します。次に、ファイルを少し編集します (図 5 を参照)。ここでは簡単に、Paint.NET の [Adjustments] (調整) メニューをクリックし、[Hue/Saturation] (色相/彩度) をクリックして、色相と彩度を変更してみましょう。スライダを移動してグラフィックの色相、彩度、明度を変更し、[OK] をクリックして変更をコミットします。ファイルの変更が完了したら、保存して XNA Game Studio Express に戻ります。F5 キーを押して Spacewar プロジェクトをコンパイルおよび実行します。XNA Game Studio Express はファイルの変更を検出し、新しいアセットを再構築および再展開します (Xbox 360 コンソールでも同様です)。現代風モードでゲームを起動すると、変更後の背景が表示されます。

図 5 Paint.NET でテクスチャを編集する
図 5 Paint.NET でテクスチャを編集する(画像を縮小するには、ここをクリックします)
図 5 Paint.NET でテクスチャを編集する
図 5 Paint.NET でテクスチャを編集する (画像を拡大するには、ここをクリックします)

発射する

以上の単純な例では、XNA Game Studio Express の能力のほんの一部しか現れていません。XNA Framework と XNA Framework Content Pipeline というクロスプラットフォームの組み合わせにより、ゲーム開発の真の醍醐味である優れたコンテンツと創意に富むゲームプレイに集中できます。

また、ゲーム開発スキルを向上し、レベルアップするためも XNA Creators Club に加入することをお勧めします。このクラブのメンバになれば、Xbox 360 プラットフォーム上で自作のゲームを開発する道が開かれるだけでなく、会員専用のコンテンツ、サンプル、スタート キットなど、あなたの想像をゲーム上で実現するうえで役立つ数多くのリソースを利用することができます。

Charles Coxは、マイクロソフト ゲーム プラットフォーム ドキュメント グループの開発者教育に携わっています。以前は DirectX および Microsoft Game Studios のテクニカル ライターでもあった Charles は、現在、XNA Game Studio Express のチュートリアルと教育ワークショップの開発および実施を担当しています。

Michael Klucherは、マイクロソフト XNA Game Studio チームのプログラム マネージャとして、Xbox 360 コンソールの XNA Framework Content Pipeline および XNA Game Launcher に取り組んでいます。klucher.com でブログを公開しています。


WPF: Windows Presentation Foundation のコントロールをカスタマイズする

WPF

Windows Presentation Foundation のコントロールをカスタマイズする

Shawn Wildermuth
この記事の内容 : :
  • 複合コントロール
  • スタイルおよびテンプレート
  • カスタム プロパティ
  • カスタム コントロール
この記事は次のテクノロジを使用しています:
Windows Presentation Foundation
コンテンツ
複合コントロールを使用する
スタイルを使用する
テンプレートを使用する
コントロールを作成する
カスタム プロパティ
まとめ

Windows Presentation Foundation のコントロール モデルの多様性には驚くばかりですが、それでも必要なすべてのコントロールを揃えることは不可能です。そこでこの救済策としてコントロール作成機能が用意されています。この記事では、Windows® Presentation Foundation を使用して既存のコントロールをカスタマイズする方法およびプロジェクトで使用するコントロールまたは要素を新規に作成する方法を説明します。

カスタム コントロールを開発する前に、本当にそのコントロールが必要なのかどうかを自問してみる必要があります。Windows Presentation Foundation では、複合コントロール、スタイル、およびテンプレートを使用して、これまでのテクノロジでは想像もできないレベルまで既存のコントロールをカスタマイズすることができます。新しいコントロールの作成を決断する前に、コントロールのカスタマイズに使用する以下の 3 つのメソッドについて簡単におさらいしてみましょう。

複合コントロールを使用する

一般的な要件として、複数のコントロールで構成される複合コントロールの作成があります。再生ボタンを押してビデオを再生する場合を考えてみましょう。そのための XAML およびコントロールを図 1 に示します。

Figure 1 簡単な再生コントロール (Close Figure 1)
<StackPanel>
  <Button Height=”50” Width=”50” Content=”Play” />
  <Polygon HorizontalAlignment=”Center”
           Points=”0,0 0,26 17,13”
           Fill=”Black” />
</StackPanel>

それには、再生アイコンを取得してボタン上に配置する必要があります。複合コントロールを使用して、XAML 要素を別の XAML 要素に埋め込むことができます。たとえば、XAML を変更して、ラベルおよびアイコンをボタンのコンテンツとして作成することが可能です。これらの要素をボタン上のコンテナ (この場合は StackPanel) 内に配置すると、図 2 のように Button クラスの Content プロパティに割り当てられます。これで、自作のコンテンツを実行するということ以外は他のボタンと同様の機能を果たすボタンが作成されます。

Figure 2 ボタン内のすべての内容 (Close Figure 2)
<StackPanel>
  <Button Height=”50” Width=”50”>
    <StackPanel>
      <TextBlock>Play</TextBlock>
      <Polygon Points=”0,0 0,26 17,13”
               Fill=”Black” />
    </StackPanel>
  </Button>
</StackPanel>

複合コントロールを使用すれば、コントロールの作成はこのように単純です。Windows フォーム、Visual Basic® 6.0、MFC などのプレゼンテーション テクノロジと異なり、多くのコントロールは他のコントロールのコンテナです。複合コントロールのみが必要な場合、カスタム コントロールを作成する必要はありません。

スタイルを使用する

コントロールの外観だけを変更するにはどうすればよいでしょう。答えはスタイルを使用することです。ボタンに赤い境界線を付けるには、次のようなスタイルを作成します。

<StackPanel>
  <StackPanel.Resources>
    <Style TargetType=”Button” x:Key=”RedButton”>
      <Setter Property=”BorderBrush” Value=”Red” />
    </Style>
  </StackPanel.Resources>
  ...
</StackPanel>

特定のボタンの境界線を変更するには、図 3 のように対象のボタンにスタイルを割り当てます。最初のボタンは標準的な外観をしていますが、2 番目のボタンは共有スタイルにバインドされています。

Figure 3 ボタンにスタイルを適用する (Close Figure 3)

図 3
<Button Height=”50” Width=”50”>
  <StackPanel>
    <TextBlock>Play</TextBlock>
    <Polygon Points=”0,0 0,26 17,13”
             Fill=”Black” />
  </StackPanel>
</Button>
<Button Height=”50” Width=”50” Style=”{StaticResource RedButton}”>
  <StackPanel>
    <TextBlock>Play</TextBlock>
    <Polygon Points=”0,0 0,26 17,13”
             Fill=”Black” />
  </StackPanel>
</Button>

スタイルを使用して、コンテナ内の特定の種類の XAML 要素のすべてのインスタンスの外観を変更することもできます。たとえば、再使用可能なスタイルを作成してボタンを変更するのではなく、図 4 に示すようにすべてのボタンの外観を指定するスタイルを作成することもできます。この例ではすべてのボタンの背景色をグレー/緑/グレーのグラデーションに設定しています。この例のスタイルではスタイルのキーは省略しています。このようにすれば、TargetType 属性に指定されたすべての要素が変更されます。

Figure 4 コンテナ全体にスタイルを適用する (Close Figure 4)

図 4
<StackPanel>
  <StackPanel.Resources>

    <Style TargetType=”Button”>
      <Setter Property=”Background”>
        <Setter.Value>
          <LinearGradientBrush>
            <GradientStop Color=”#DDDDDD” Offset=”0” />
            <GradientStop Color=”#88FF88” Offset=”.6” />
            <GradientStop Color=”#EEEEEE” Offset=”1” />
          </LinearGradientBrush>
        </Setter.Value>
      </Setter>
    </Style>

  </StackPanel.Resources>

  <Button Height=”50” Width=”50”>
    <StackPanel>
      <TextBlock>Play</TextBlock>
      <Polygon Points=”0,0 0,26 17,13”
               Fill=”Black” />
    </StackPanel>
  </Button>

  <Button Height=”50” Width=”50”>
    <StackPanel>
      <TextBlock>Play</TextBlock>
      <Polygon Points=”0,0 0,26 17,13”
               Fill=”Black” />
    </StackPanel>
  </Button>

</StackPanel>

テンプレートを使用する

スタイルは XAML 要素の既定のプロパティの設定のみに制限されます。たとえば、前述の例で BorderBrush を設定したとき、ブラシは指定できましたが、境界線の幅は指定できませんでした。コントロールの外観を完全に自由に指定するには、テンプレートを使用する必要があります。それには、スタイルを作成して Template プロパティを指定します (図 5 を参照)。Template プロパティの値はコントロールの作成方法を指定する ControlTemplate 要素になります。この例では、中央に再生アイコンが配置された円形のボタンを指定しています。この外観を実現するために、再生アイコンを Ellipse 要素に重ねました。新しいテンプレート ボタンが標準のボタンの横に表示されます。

Figure 5 テンプレートを使用する (Close Figure 5)

図 5
<StackPanel>
  <StackPanel.Resources>

    <Style TargetType=”{x:Type Button}” x:Key=”PlayButton” >
      <Setter Property=”Template”>
        <Setter.Value>
          <ControlTemplate TargetType=”{x:Type Button}”>
            <Grid>
              <Ellipse Width=”{TemplateBinding Width}”
                       Height=”{TemplateBinding Height}”
                       Stroke=”DarkGray”
                       VerticalAlignment=”Top”
                       HorizontalAlignment=”Left”
                       Fill=”LightGray” />
              <Polygon Points=”18,12 18,38 35,25”
                       Fill=”Black” />
            </Grid>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

  </StackPanel.Resources>

  <Button Height=”50” Width=”50”>Normal Button</Button>
  <Button Height=”50” Width=”50” Style=”{StaticResource PlayButton}” />

</StackPanel>

スタイルやテンプレートで変更できるのはコントロールの外観のみです。ボタンに動作などの機能を追加するには、カスタム コントロールを作成する必要があります。

コントロールを作成する

自作のコントロールを作成するには、まずコントロールの作成に使用するメソッドを選択する必要があります。Windows Presentation Foundation でコントロールを作成する主な方法には、ユーザー コントロールとカスタム コントロールの 2 つがあります。それぞれの方法に利点があります。

ユーザー コントロールの場合、Windows Presentation Foundation アプリケーション開発と同様の単純な開発モデルがあります。ユーザー コントロールは、既存のコンポーネントからコントロールを作成する必要があり、テンプレートやスタイルを使用したときのような複雑なカスタマイズが不要な場合に適しています。カスタム コントロールは、外観を完全に制御する場合、特殊なレンダリングをサポートする必要がある場合、コントロールを他のコントロールのコンテナにする場合に適しています。

どちらのコントロールを使用したらよいか判断がつかない場合はユーザー コントロールを選択してください。ユーザー コントロールの機能で不足があれば、後からカスタム コントロールに切り替えてもたいした手間はかかりません。

ユーザー コントロールを作成するには、まず、プロジェクトに新しい項目を追加します。プロジェクトを右クリックして [追加] をクリックしたら、コンテキスト メニューで [ユーザー コントロール] を選択したくなるかもしれませんが、これは新しい Windows フォームのユーザー コントロールを作成する機能です。代わりに、[新しい項目の追加] を選択してください。[新しい項目の追加] ダイアログ ボックスでユーザー コントロールの項目 (WPF) を選択します。

新しいユーザー コントロールを作成すると、新しい XAML ファイルとコード ファイルが作成されます。XAML ファイルは新しい Windows Presentation Foundation プロジェクトの作成時に作成されるメイン ファイルに類似しています。違いは、新しい XAML ファイルのルート要素は UserControl 要素であるという点です。UserControl 要素の内部に、自作のコントロールのコンテンツを作成します。

この例では、前述と同じ XAML を使用して PlayButton コントロールのテンプレートを作成します。この新しいコントロールを MediaElement に関連付けて、デジタル メディアの再生および停止を制御します。図 6 に、PlayButton の XAML を示します。

Figure 6 PlayButton の XAML (Close Figure 6)
図 7 WPF ウィンドウの PlayButton
図 7 WPF ウィンドウの PlayButton
<!-- PlayButton.xaml -->
<UserControl x:Class=”CustomWPF.PlayButton”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>

  <Grid>

    <Ellipse Width=”50” Height=”50”
             Stroke=”DarkGray”
             VerticalAlignment=”Top”
             HorizontalAlignment=”Left”
             Name=”ButtonBack”
             Fill=”LightGray” />
    <Path Name=”PlayIcon” Fill=”Black”
          Data=”M18,12 18,38 35,25”/>
    <Path Name=”PauseIcon” Fill=”Black” Opacity=”0”
          Data=”M15,12 15,38 23,38 23,12z M27,12 27,38 35,38 35,12” />

  </Grid>

</UserControl>

テンプレートの例に新しい Path、PauseIcon を追加しました。メディアの停止ボタンのアイコンは Polygon では表せないので、分離コードの PlayIcon を Path に変更して、各アイコンを Path オブジェクトとして処理する方が簡単です。では、ボタンをクリックしたときにメディアが停止または再生され、その動作に応じてボタンが適切なアイコンに切り替わるように MediaElement 要素を制御することにします。

このロジックを追加する前に、ボタンが正しくウィンドウに表示されることを確認します。ここでは、新しいコントロールをウィンドウに表示します。XAML ドキュメントでカスタム要素 (ユーザー コントロールまたはカスタム コントロール) を使用する前に、カスタム要素への参照を作成する必要があります。カスタム要素を XAML ドキュメントの他の部分と同じプロジェクト内に配置する場合、XML 名前空間の宣言を追加するだけで参照できます。次のコードでは、コントロールが存在する共通言語ランタイム (CLR) 名前空間を指定する名前空間の宣言 (xmlns:cust) を作成しています。

<!-- MainWindow.xaml -->
<Window x:Class=”Tester.MainWindow”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    xmlns:cust=”clr-namespace:CustomWPF“
    Title=”Control Viewer”
    Height=”100” Width=”200”>

  <!-- ... -->

</Window>

XML 名前空間の宣言に指定されている clr-namespace (CustomWPF) は、コントロールの実際の CLR 名前空間 (CustomWPF) と一致しています。使用するコントロールが別のアセンブリに存在する場合は、名前空間の宣言にアセンブリ名も指定する必要があります。XML 名前空間を宣言しても、アセンブリはプロジェクトに自動的にはインポートされません。アセンブリへの参照を手動でプロジェクトに追加する必要があります。

<!-- MainWindow.xaml -->
<Window x:Class=”Tester.MainWindow”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    xmlns:cust=”clr-namespace:CustomWPF;assembly=CustomWPF”
    Title=”Control Viewer”
    Height=”100” Width=”200”>

  <!-- ... -->

</Window>

XML 名前空間への参照を追加したら、その参照を使用して新しいユーザー コントロールのインスタンスを作成できます。そのためには、名前空間宣言の名前を使用します。この場合の名前は CLR 名前空間ではなく、XML 名前空間の別名です。XML 名前空間の別名の後にコントロールの実際の名前を指定します。これで、XAML ファイルで使われている名前とクラス名が一致します。PlayButton クラスのインスタンスを XAML に追加するには、要素名に cust:PlayButton を指定します。

<StackPanel>
  <TextBlock HorizontalAlignment=”Center”>User Control:</TextBlock>
  <cust:PlayButton />
</StackPanel>

これで、一般的な Windows Presentation Foundation ウィンドウでホストされている図 7 のような PlayButton コントロールができあがります。

カスタム プロパティ

コントロールを作成するときには、コントロールの外観および実行時の動作を管理するプロパティを実装する必要があります。たとえば、PlayButton コントロールにはアイコンの色を取得および設定する機能が必要です。図 8 のような単純な CLR プロパティを作成すれば、この機能を実装できます (この記事のダウンロード ファイルに Visual Basic のサンプル コードがあります)。

Figure 8 アイコンの色を取得する (Close Figure 8)
// PlayButton.xaml.cs
public partial class PlayButton : System.Windows.Controls.UserControl
{
  // ...

  Brush _iconColor = Brushes.Black;

  public Brush IconColor
  {
    get {return _iconColor; }
    set
    {
      _iconColor = value;
      PlayIcon.Fill = _iconColor;
      PauseIcon.Fill = _iconColor;
    }
  }
}

IconColor プロパティなどの単純な CLR プロパティで十分に機能を果たせます。プロパティ名だけで XAML に設定できます。

<cust:PlayButton IconColor=”Black” />

ところが、単純なプロパティはデータ バインドやアニメーションなどの高度な機能をサポートしていないため、多くのコントロールでは機能不足です。たとえば、図 9 のように、コントロールの IconColor を XAML の四角形の塗りつぶしの色にバインドすることによって指定しようとしてもうまくいきません。

Figure 9 このデータ バインドは機能しない (Close Figure 9)
<!-- MainWindow.xaml -->
<Window x:Class=”Tester.MainWindow”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    xmlns:cust=”clr-namespace:CustomWPF;assembly=CustomWPF”
    Title=”Control Viewer”
    Height=”100” Width=”200”>

  <StackPanel>
    <Rectangle Name=”theRect” Fill=”Red” />
    <TextBlock>User Control:</TextBlock>

    <!-- Simple Assignment Works -->
    <cust:PlayButton IconColor=”Blue” />

    <!£Data Binding Does Not -->
    <cust:PlayButton
      IconColor=”{Binding ElementName=theRect, Path=Fill}” />

  </StackPanel>
</Window>

すべての機能を自在に使用できるようにするには、単純な CLR プロパティではなく、依存関係プロパティを使う必要があります。依存関係プロパティを使用すれば、要素の値をアニメーションやデータ バインドなどの多様な手段で設定できます。依存関係プロパティを実装するには、DependencyProperty.Register メソッドを呼び出して、コントロールに静的な (Visual Basic で共有された) DependencyProperty フィールドを作成します。このメソッドは、プロパティを登録し、作成した DependencyProperty のインスタンスを返します。

DependencyProperty フィールドを作成したら、コントロールの GetValue メソッドまたは SetValue メソッドを使用して、プロパティを取得または設定できます。たとえば、図 10 のように、PlayButton の IconColor プロパティを DependencyProperty に変更できます。

このクラスから Brush フィールドが削除されていることに注目してください。DependencyProperty に各インスタンスの値とそのプロパティに関するメタデータが保存されるため、プロパティに関するデータを保存するためのフィールドを PlayButton の各インスタンスで保持する必要はありません。

この DependencyProperty をデータ バインドやアニメーションで使用できます。今のところ、プロパティの変更の有無や既定値の有無を確認する良い方法はありません。プロパティの値が設定されたという事実がパブリック プロパティから送られてこないため (CLR プロパティは DependencyProperty のラッパーですが、DependencyProperty は CLR プロパティのラッパーではありません)、値が変更されたとき、単純に set アクセサでアイコンの色を変更することはできません。プロパティの変更時に呼び出されるイベントを追加する必要があります。それには、DependencyProperty の登録時に、プロパティが変更されたら静的メソッドまたは共有メソッドがコールバックされるように指定します。次のように、登録を修正して、既定値と変更のコールバックを指定する FrameworkPropertyMetadata オブジェクトを追加します。

public static readonly DependencyProperty IconColorProperty =
        DependencyProperty.Register(
            “IconColor”,
            typeof(Brush),
    typeof(PlayButton),
    new FrameworkPropertyMetadata(Brushes.Black,
    new PropertyChangedCallback(OnIconColorChanged
    )));

最後に、コールバックを実装する必要があります。このコールバックは、変更されたオブジェクトと、古い値と新しい値を指定する引数を取得するコントロールの静的 (または共有) メソッドです。通常は、変更されたオブジェクト内でメソッドを呼び出して更新します。たとえば、IconColor が変更されたら 2 つのアイコンの Fill を設定するという処理が考えられます。この処理を行うコールバック メソッドと更新メソッドを次に示します。

private static void OnIconColorChanged(DependencyObject obj,
    DependencyPropertyChangedEventArgs args)
{
  // When the color changes, set the icon color
  PlayButton control = (PlayButton)obj;
  control.PlayIcon.Fill = control.IconColor;
  control.PauseIcon.Fill = control.IconColor;
}
コントロールを完成させるには、DependencyProperty でコントロールに MediaElement を割り当てることができるようにします (図 11 を参照)。

このプロパティを使用して XAML に MediaElement を追加し、この要素を新しい MediaPlayer プロパティにデータ バインドすることができます。MediaElement を XAML に追加する場合、LoadedBehavior を Manual に設定して、再生を手動で制御できるようにする必要があります。新しい XAML を図 12 に示します。

Figure 12 修正後の MediaElement XAML (Close Figure 12)
<!-- MainWindow.xaml -->
<Window x:Class=”Tester.MainWindow”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
    xmlns:cust=”clr-namespace:CustomWPF;assembly=CustomWPF”
    Title=”Control Viewer”
    Height=”100” Width=”200”>
  <StackPanel>

    <MediaElement Width=”150” Height=”100”
         Name=”theMedia”
         Source=”http://download.microsoft.com/.../ctorrec9billg.wmv”
         LoadedBehavior=”Manual” />

    <TextBlock>User Control:</TextBlock>

    <cust:PlayButton MediaPlayer=”{Binding ElementName=theMedia}” />

  </StackPanel>
</Window>

次に、PlayButton ユーザー コントロールにクリック イベントを実装します。最初に、コントロールのメイン Grid の MouseLeftButtonUp イベントを追加します。

<!-- PlayButton.xaml -->
<UserControl x:Class=”CustomWPF.PlayButton”
    xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
    xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
  <Grid MouseLeftButtonUp=”PlayButton_Clicked”>
    <!-- ... -->
  </Grid>
</UserControl>
これで、コントロールの動作およびアイコンの変更を実装できます。このイベント ハンドラの実装を図 13 に示します。また、完成したコントロールおよびビデオを図 14 に示します。

図 14 完成したユーザー コントロール
図 14 完成したユーザー コントロール
カスタム コントロール

ここまで作成した PlayButton コントロールでもそれなりの機能を果たしますが、テンプレートとテーマのサポートが不完全です。テンプレートとテーマを完全にサポートするには、コントロールをカスタム コントロールとして作成する必要があります。カスタム コントロールはコントロール階層の他のクラスから派生します。たとえば、PlayButton コントロールを再利用しやすい MediaButton コントロールにリファクタリングすることができます。

MediaButton コントロールを作成するには、まず、Visual Studio の [新しい項目の追加] ダイアログ ボックスでカスタム コントロール (WPF) を選択します。新しいカスタム コントロール クラス (MediaButton.cs) ファイルがプロジェクトに追加され、generic.xaml ファイルとテーマ フォルダも追加されます。generic.xaml ファイルには新しいコントロール クラスのテンプレートが含まれています。この XAML ファイルを使用してコントロールにさまざまなテーマを指定できます。generic.xaml ファイルはフォールバックとして使用されます。多くのコントロールでは、作成する必要があるテーマ ファイルはこのファイルのみです。現在のテーマに応じて外観が変わるコントロールを作成する場合、このディレクトリにテーマ ファイルを作成できます。標準的なテーマ ファイルとテーマ ファイルを使用するタイミングを図 15 に示します。

複数のテーマを指定することにより、ユーザーが選択したテーマに応じてコントロールの外観を変更することができます。サンプル コントロールの汎用テーマを作成するには、ユーザー コントロールの XAML ファイルを取得して ControlTemplate タグ内に配置するという方法があります。XAML をテンプレート内に配置すれば、テンプレート バインドを使用して、コントロールのプロパティに基づいて XAML のプロパティを設定することができます。今まで、PlayButton の幅と高さを常に 50 論理単位に設定していました。単純なコントロールの場合はこれでも問題はありませんが、コントロールを再利用可能にするには、テンプレートのサイズを可変にする必要があります。それには、テンプレートの高さと幅に {TemplateBinding Height} および {TemplateBinding Width} を指定して、テンプレートの高さと幅をコントロールの高さと幅に置換します。

PlayButton では、再生または停止のどちらを表示するかに応じて 2 つのアイコンの Opacity (不透明度) を変更するだけです。テンプレートを操作して Opacity を変更してもかまいませんが、あまり気の利いた方法ではありません。それよりも、1 つのアイコンを表す Path オブジェクトを作成し、描画データを変更して適切なアイコンが描画されるようにする方法がより優れています。この方法では、ビジュアル ツリーのサイズが XAML ドキュメントのパフォーマンスに直接影響します。この機能を利用するには、新しい DependencyProperty を使って、使用する現在のアイコンを保存します。使用するアイコンを指定する列挙体を作成し、DependencyProperty として公開します。新しい Icon プロパティを使って、Icon プロパティが変更されたときのトリガをテンプレートに追加します。

このアイコンでは、Path 要素を使って、ボタンに使用する各アイコンの外観を定義します。ボタンのサイズが常に一定であれば、これで問題はありませんが、ここではボタンのサイズを可変にしているため、ボタンに使うアイコンのサイズを変更する方法を探す必要があります。その 1 つの解決策として、背景の円に重なる楕円を作成し、VisualBrush を使ってアイコンを描画する方法があります。VisualBrush には、背景のサイズをコントロールに合わせる機能があります。完成した generic.xaml テンプレートを図 16 に示します。

この MediaButton は、PlayButton と同様に MediaElement を制御します。MediaPlayer DependencyProperty を PlayButton から MediaButton にコピーします。このとき、(特に DependencyProperty の登録では) コピーしたコードの参照を PlayButton から MediaButton に変更することを忘れないでください。

PlayButton とは異なり、テンプレートのマウス クリック イベントを処理する必要はありません。代わりに、OnMouseLeftButtonUp イベントをオーバーライドしてクリックに対応させます。このメソッド内で、アイコンの変更およびメディアの再生または停止を行うことができます。最後のカスタム コントロール コードを図 17 に示します。新しいコントロールではサイズ変更やアイコンのプロパティの設定がサポートされているため、ユーザーはより柔軟にコントロールのサイズやアイコンを変更できます。さまざまなサイズおよびアイコンのコントロールを図 18 に示します。

図 18 さまざまな形およびサイズ
図 18 さまざまな形およびサイズ

このコントロールの例では直接 System.Windows.Controls.Control クラスから派生していますが、カスタム コントロールにはクラス階層のどこからでも派生できるという性質があります。カスタム コントロールを使って組み込みコントロールの動作をオーバーライドまたは変更することも、まったく固有のコントロールを最初から作成することも可能です。たとえば、FrameworkElement から派生した場合、組み込みレイアウトの機能をほとんど使用しないでコントロールを作成することができます。Panel から派生した場合、他のオブジェクトに固有の専用コンテナを作成することができます。どのクラスから派生するのが適切かを判断するのは容易ではありません。それはコントロールの要件しだいで決まります。

まとめ

特殊なコントロール機能が必要な場合、複合コントロール、スタイル、テンプレートなどの複数の選択肢があります。コンポーネント化を利用すれば、多くの場合、複合コントロールを作成することで新しいコントロールを作成する必要がなくなります。コントロールの外観だけを変更するには、スタイルを使用します。テンプレートを使用した場合は、既存のコントロールの内容を完全に制御できます。テンプレートを使用したコントロールのカスタマイズの詳細については、Charles Petzold の「Foundation に関するコラム」を参照してください。

新しいコントロールを作成する場合は、自作のウィンドウやページを作成する場合とほとんど同様に簡単なプログラミング モデルがあります。オペレーティング システム テーマごとに外観や動作が異なるコントロールのテンプレートを作成できるという点は、カスタム コントロールの特筆すべき利点です。

既存のユーザー コントロールからカスタム コントロールへの移行は特に難しくありませんが、テンプレートを使用しているため、XAML オブジェクトを直接操作するのではなく、カスタム コントロールの設計を変更する必要があります。

Windows Presentation Foundation を使用すれば、カスタム コントロールを作成する必要が生じるのは一般的なことではなく、むしろ例外的なことになります。コントロールの作成方法を研究したいというのでなければ、動作をカスタマイズする必要はありません。Windows Presentation Foundation のコントロールの詳細については、msdn2.microsoft.com/ms754130.aspx の SDK を参照してください。

Shawn Wildermuthは Microsoft MVP、MCSD.NET、MCT であり、Wildermuth Consulting Services の創立者でもあります。Shawn は『実践的 ADO.NET』 (Addison-Wesley、2002 年) の著者であり、4 つのマイクロソフト認定トレーニング キットおよび近々発行される『規範データ アーキテクチャ』の共著者でもあります。彼の Web サイトは、www.wildermuthconsulting.com からご覧いただけます。


移行: JLCA を使用して Java Web アプリケーションを ASP.NET に変換する

移行

JLCA を使用して Java Web アプリケーションを ASP.NET に変換する

Brian Jimerson
この記事の内容 : :
  • リソース検索
  • I/O ストリーム
  • ログ記録およびコレクション
  • リファクタリング
この記事は次のテクノロジを使用しています:
ASP.NET、JLCA、および C#
コードのダウンロード : : JLCA2007_05.exe (157KB)
コンテンツ
JLCA について
リソースを検索する
入力/出力 API を処理する
ログ記録
コレクション
フィルタおよび HTTP ハンドラ
ソース ツリーおよび名前付け規則
名前付け規則に対するリファクタリングの時期
ディレクトリ レイアウトおよび名前空間
プロパティ
メソッド名を Pascal 形式にする
まとめ

一般的なソフトウェア開発サイクルは、単純明快なモデルに従っています。要求の収集、アプリケーションのデザイン、コードの記述、ソフトウェアのテスト、そして展開です。しかしときには、顧客がアプリケーションを展開するプラットフォームのみに基づいて、新しい開発プロジェクトが開始される場合もあります。そのような場合は、既存のアプリケーションのコード ベースを目的のプラットフォームに変換 (移植) することができます。

この記事では、Java の Web アプリケーションを C# で実装された ASP.NET アプリケーションに変換する手順について説明します。内容は、私が実際に従事したプロジェクトに基づいています。そのプロジェクトでは、既存の Java ベースのアプリケーションがあり、顧客はその ASP.NET 版を求めていました。この記事では、まず Microsoft® Java Language Conversion Assistant (JLCA) の概要を紹介してから、両方のプラットフォームに直接相当する機能のない、次のような一般的な開発パラダイムについて説明します。

  • 入力/出力
  • リソース解決
  • ソース ツリー レイアウトおよび名前付け規則
  • 実行環境の活用

このアプリケーションは、SOAP 準拠の Web サービスとして、従来のリレーショナル データベース型永続ストアと共に実装されています。ここでは、実際の Web サービス プレゼンテーション層および SOAP インターフェイスについてではなく、それを駆動するアプリケーションについて説明します。この記事のサンプル コードは、ダウンロードして使用できます。

JLCA について

JLCA は、Java アプリケーションを C# アプリケーションに変換するために使用されるツールです。このツールは、Visual Studio® に付属しています (Visual Studio .NET 2003 以降)。現在は、JLCA の Version 3.0 が Visual Studio 2005 に付属し、JLCA ホーム ページからも無料でダウンロードできます。

Version 3.0 には、サーブレットや Java Server Pages (JSP) などの Java 要素に加えて、Swing や Abstract Windowing Toolkit (AWT) を使用したリッチ クライアント アプリケーションを変換するための拡張が含まれています。実際には、JLCA は変換の手始めとして非常に良い土台を提供しますが、それだけで変換プロセス全体が適切に完了するわけではありません。つまり、完全に自動的なツールであるとは期待しないでください。ツールを使用した後で、変換された各アイテムの手動解決が必要になります。

Visual Studio 2005 で JLCA を使用するには、[ファイル] メニューの [開く] をクリックし、[変換] をクリックして、ウィザードを起動します。ウィザードの画面は、一目瞭然です。入力する必要がある情報で重要なのは、図 1 に示すように、既存の Java プロジェクトのルート ディレクトリです。

図 1 JLCA ウィザードでルート Java ディレクトリを入力する
図 1 JLCA ウィザードでルート Java ディレクトリを入力する(画像を縮小するには、ここをクリックします)
図 1 JLCA ウィザードでルート Java ディレクトリを入力する
図 1 JLCA ウィザードでルート Java ディレクトリを入力する (画像を拡大するには、ここをクリックします)

次に JLCA は、Java ソース コードから C# への変換に進みます。このプロセスはそれほど時間はかかりません。ちなみに、約 100 個のクラス ファイルを含む私たちのコード ベースの場合、変換が完了するまでに 10 分もかかりませんでした。コーヒーを 1 杯飲むのにちょうどいい時間です。もちろん、この数字はプロジェクトおよびシステムごとに異なります。

変換プロセスが完了した後、JLCA はエラーおよび警告の HTML レポートを作成します。これらの項目は、原因となったメンバによるコメントとして、生成された C# コードにも記入されます。各項目には、問題の解決に役立つ詳細情報へのハイパーリンクも含まれています。

警告の多くは、無視しても問題のないものです。それらは単に、Java と C# との動作の違いを示したものであり、たとえば、"プリミティブ型間における型のキャストは、その動作が異なる可能性があります" といった警告がそれに当たります。ただし、そのような違いがアプリケーションに影響しないことを確認するために、各警告にはひととおり目を通してください。

変換レポートを初めて目にしたときには、膨大な数の問題が報告されているように見えるかもしれません。この例では、816 個のエラーと 16 個の警告がありました。ただし、エラーのほとんどは、3 つのカテゴリのいずれかに分類でき、ごく簡単に対処できました。3 つのカテゴリとは次のとおりです。

  • 単に C# または Microsoft .NET Framework に同等の要素のないメンバ。
  • .NET Framework に直接同等の要素のない、一般的なサードパーティ製 Java ライブラリ (Hibernate および log4j など)。
  • クラスの読み込みまたはリソース解決を処理するアイテム。

また、もう 1 つ注意が必要なこととして、インポートされたパッケージ (または using 名前空間ステートメント) が見つからない場合に、JLCA は解決を試みていないように見えます。その代わり、生成される C# コードにそれらを単純にそのまま渡しています。もしこの新しい C# アプリケーションのコンパイルを試みたら、変換レポートで示されるよりも、より多くのコンパイラ エラーが返されるでしょう。

しかし、心配する必要はありません。前に述べたように、これらのエラーのほとんどは同じ繰り返しパターンに分類され、まとめて対処できます。以降のセクションでは、これら一般的なエラーの解決方法を説明し、また、変換したアプリケーションを真の C# アプリケーションにするために必要な他のタスクも取り上げます。

リソースを検索する

Java におけるリソース検索のプロセス、つまりファイル リソースを探して読み込むプロセスは、C# でのリソース検索とは大きく異なっています。Java では、クラス ローダーを使用して、クラス定義の読み込みと解析を実行時に行います。クラス ローダーの役割には、クラスのコンテキストを管理し、読み込むクラスに対して環境的な機能を提供することが含まれます。Java では、クラス ローダーはクラスパスと呼ばれる特殊な種類の環境変数を使用してリソースを見つけます。クラスパスは、クラス ローダーが他のクラスやリソースを探す場所を定義するという点で、パス環境変数に似ています。他のクラスまたはリソースを読み込む必要があるアプリケーションは、クラスパスを基準としたそのファイルの相対位置をクラス ローダーに伝えることで、それを読み込むことができます。

Java におけるリソース解決で特に一般的なアプローチの 1 つは、接続またはホスト情報、他のリソースへのパス、ローカライズ文字列、認証に使用する資格情報など、構成可能な情報について、プロパティ ファイルと呼ばれる特殊な種類のファイルを使用することです。プロパティ ファイルには、"名前=値" のペアが複数含まれ、それぞれが改行で区切られています。この形式は、INI ファイルとよく似ていますが、セクションはありません。

さらに、リソース検索は、常にファイル システム表記を使用して実行され、リソースが実際にファイル システムに存在するかどうかが確認されます。これにより、開発者は、アプリケーションがどのように展開されているかを知らなくても済みます。画像やバイナリ ファイルなど、他の種類のリソースも、同じ方法で検索され、読み込まれます。Java でプロパティ ファイルを検索して使用する方法の例を次に示します。

InputStream is = this.getClass().getResourceAsStream(
    “/application.properties”);
Properties properties = new Properties();
properties.load(is);
String myValue = properties.getProperty(“myKey”);
対照的に、.NET Framework 内のリソースは、2 つの異なる方法で展開し読み込むことができます。アセンブリを伴う埋め込みバイナリ リソースとして、またはローカル ファイル システム内のファイルとしてです。

リソースにアクセスするための適切な技法は、リソースの場所によって異なります。たとえば、ファイル システム リソースの場合は、次のようなコードを実行できます。

string fileName = Path.Combine(Path.GetFullPath(
    @”..\config\”), “properties.xml”);
Stream fileStream = File.Open(fileName, FileMode.Open);
//Do something with the stream
一方、リソースがアセンブリに埋め込まれている場合は、次のようなアプローチになります。

Assembly assembly = Assembly.GetExecutingAssembly();
Stream fileStream = assembly.GetManifestResourceStream(
    GetType(), “properties.xml”);
//Do something with the stream

私のチームのアプリケーションは、リソースがどのように展開されているかを知らなくてもリソースを解決できる、という前提で記述されました。アプリケーション全体にわたって多くのリソースが読み込まれるため、読み込まれる各リソースを分析し、リソースの展開方法を確認してから、適切に読み込むためにコードを変更するのでは、作業量が膨大になっていたでしょう。そこで私たちは代わりに、ResourceLocator と呼ばれるユーティリティ クラスを作成しました。

ResourceLocator は、クラスパスに基づいてリソースを解決する Java クラス ローダーの機能を模してデザインされています。リソースを読み込むための呼び出しがすべてこのアプローチで記述されているため、最も煩雑さの少ない変換方法と思われました。ResourceLocator を記述した後で行う必要があったのは、呼び出しを Class.getResourceAsStream から ResourceLocator.LocateResource に変更することだけでした。これは、Visual Studio では単純な検索と置換で実行できました。

基本的に、ResourceLocator は検索するリソースの名前と相対パスを受け取り、使用可能なアセンブリおよびローカル ファイル システム全体からリソースを見つけようとします。領域を検索する順序を指定したり、アセンブリまたはファイル システムのどちらかのみを検索するよう選択したりなど、よりきめ細かい制御を提供するオーバーロードされたメソッドもあります。ResourceLocator のソースは、MSDN® Magazine Web サイトのコード サンプルに含まれています。

リソースを見つけるためにすべての場所を検索するのは非常にコストが高いと考えるかもしれませんが、確かにそのとおりです。ただし、見つかってアプリケーションに読み込まれたリソースは、すべてキャッシュされます。つまり、各リソースはアプリケーションの実行中に 1 回しか読み込まれないため、このオーバーヘッドは緩和されます (ただし、メモリの消費量が増えるため、読み込まれるリソースが多数ある場合には問題となる可能性があります)。したがって、私たちは、行わなくても済むコード変更の数を考慮し、このトレードオフは受け入れることができると判断しました。このような種類の変更は時間をかけて行うこともできます。迅速にポートを動作させるための単純なソリューションから始めて、.NET ベースのアプリケーションのデザインと実装のガイドラインに少しずつ近づけながら、ゆっくり、しかし確実に実装を変更してゆくのです。

入力/出力 API を処理する

Java の I/O API と .NET の I/O API には、変換後に処理する必要のあるいくつかの違いがあります。大きな違いの 1 つは、Java の I/O ストリームが単方向であるのに対して、.NET の I/O ストリームは双方向であることです。これは、.NET プログラミングでは理論的に、同じストリームに対して読み取りと書き込みの両方を行えることを意味します。しかし Java では、ストリームから読み取るかストリームに書き込むかのいずれかであり、同じストリームに両方を行うことはできません。これは拡張的な違いであり、.NET ストリームは Java ストリームと少なくとも同等以上の機能を提供するため、この違いは変換時に大きな問題とはなりません。

単方向ストリームの安全性を保持する場合は、.NET I/O リーダーおよびライタを利用できます。それにより、基になるストリームがラップされて読み取りまたは書き込みの機能が提供されます。その結果、Java の I/O 動作とプログラム的に同等な I/O 動作が行われます。

私たちのアプリケーションでは、ストリームへの直接アクセスで十分です。JLCA は I/O 動作を適切に変換するため、コンパイルのために必要な変更はありません。ただし、これらの低レベル動作では論理エラーが発生する機会が多くあるため、私たちは変換後のコードを注意深く見直しました。

ログ記録

ログ記録は、コード内の実行の特定の時点でメッセージを出力先に書き込む能力に関係します。メッセージには、捕捉された例外、ロジック内でデバッグ情報が役立つ可能性のある領域、および読み込まれる構成情報などが含まれます。Java と .NET Framework はいずれも、情報のログ記録のための強力なフレームワークを備えていますが、それぞれのデザインおよび実装は大きく異なっています。

Java では、アプリケーションのログ記録は通常、Apache Software Foundation (ASF) の log4j フレームワーク、または Sun Microsystems Java Development Kit (JDK) の最新バージョンに付属する Java Logging API によって実現されます。Java Logging API は、実行に関して log4j とよく似ているため、このドキュメントの目的においては、この 2 つのフレームワークをどちらでも入れ替えて適用できます。.NET ベースのアプリケーションの場合は、Microsoft Enterprise Library で、Logging Application Block と呼ばれるログ記録用の堅牢なアプリケーション ブロックが提供されています (Microsoft Logging Application Block ホームページを参照)。

Java および .NET の標準のログ記録フレームワークは、それぞれデザイン時構成のサポートや多様なログ記録先 (データベース、ファイル、電子メール受信者など) といった強力な機能を備えています。ただし、API デザインは異なるため、変換時に何らかの手動による調整が必要となることに注意してください。

API の違いをより良く理解するために、図 2 に示す 2 つのコード例を考えます。これらのコードは、Java および C# で実行される一般的なログ記録のシナリオを示しています。log4j を使用したコードは、getLog ファクトリ メソッドから Log オブジェクトの静的インスタンスを作成し、それを MyClass カテゴリに割り当てます。次に、デバッグ レベルでいくつかの情報を Log に出力します。Logging Application Block を使用する C# のコードは、LogEntry クラスの新しいインスタンスを作成します。これは、記録先のログ内のエントリを表します。次に、このコードはログ エントリに優先度 2 を割り当て、カテゴリを Debug に設定して、メッセージを提供します。そしてメッセージが Logger クラスに書き込まれます。

Figure 2 Java および C# でのログ記録 (Close Figure 2)

log4j を使用した Java
private static final Log log = Logger.getLog(MyClass.class);
...
//Somewhere else in the class
log.debug(“Printing some debug information.”);
Logging Application Block を使用した C#
Logger.Write(”Printing some debug information.”, “Debug”);

いずれのプラットフォームでもログ記録が外部構成によって制御されることに注意してください。ログの記録先、ログ メッセージの選択的なフィルタリング、ログ エントリの形式などの情報が、この構成に含まれています。この構成はここでの説明には直接関係しないため、コード例からは意図的に省いてあります。

この 2 つの例を見ると、非常によく似た機能が異なる方法で実現されていることがわかります。log4j の例では、カテゴリを使用して、メッセージがログに記録されるかどうか (レベルに基づく) およびログの記録先を決定しています。一方、Logging Application Block では、優先度のフィルタとカテゴリを組み合わせて、ログの対象を決定しています。

Logger クラスは、いくつかのオーバーロードされた Write メソッドを提供し、各メソッドは異なるレベルの柔軟性を持っています (情報の記録方法の "微調整つまみ" を多数備えた LogEntry インスタンスを提供するオーバーロードもあります)。ただし、図 2 で使用されているような単純なオーバーロードでも、正規表現との組み合わせにより、検索と置換を使用して大半の変換を実現できました。

ログ記録は非常に多くのリソースを消費するプロセスであり、アプリケーションのパフォーマンスに影響を与えないように注意する必要があります。結局、アプリケーション内でログ記録の実行箇所を検索して置換するのにかかった時間は、わずか数時間でした。

コレクション

.NET Framework と Java は両方とも、強力なコレクション API を備えています。どちらも非常に拡張性が高く、コレクションを使用する際に遭遇する可能性のあるほとんどのシナリオに対応しています。ここでは、よく使用される 2 種類のコレクションについて詳しく見てみましょう。それは、リストと辞書です。

リストは、インデックスによってアクセスできるコレクションです。これは順序の付けられたコレクションであり、1 次元配列の一種と考えることができます。一方、辞書は、名前と値のペアを含むコレクションです。名前は、コレクション内の値にアクセスするために使用されるキーです。辞書には必ずしも保証された順序がないことに注意してください。

図 3 に、リストおよび辞書に対する C# と Java での同等の実装を示します。JLCA は、これらの Java コレクション クラスを .NET の同等のクラスに非常にうまく変換することができ、変換後に必要な作業はそれほど多くありません。変換の時点で、これらの一般的なコレクションの動作が 2 つのプラットフォーム間で異なるという警告が出力されるので、変換結果の検証が必要です。ただし、変換プロセスの大部分は適切に実行されます。

Figure 3 .NET と Java の同等のコレクション クラス (Close Figure 3)

 | .NET | Java
リスト インターフェイス | IList、IList<T> | List
一般的なリスト クラス | ArrayList、List<T> | ArrayList、Vector
辞書インターフェイス | IDictionary、IDictionary<TKey,TValue> | Map
一般的な辞書クラス | Hashtable、Dictionary<TKey,TValue> | HashMap、HashTable

ところが、元の Java アプリケーションで特別なコレクションが使用されていた箇所で、コレクションの変換に問題が発生しました。問題となった 1 つの例は、Java の LinkedHashSet クラスの使用です。Java API のドキュメントによれば、LinkedHashSet クラスは、他の順序付けられた辞書に起因するオーバーヘッドなしで、HashSet (辞書の一種) 内のエントリについて一貫した順序を保証するものです。LinkedHashSet の定義は単純明快ですが、アプリケーションにおけるその使用の意図は明確ではありませんでした (そのコードを書いた人は既にチームを離れており、使用理由を明記したドキュメントもありませんでした)。さらに、使用されていたコンテキストからも何もわからなかったため、このクラスが問題を修正するために使用されたのか、それとも何か他の理由で使用されたのかは不明でした。

コードをひととおり見ても、このクラスの使用の意図は把握できず、私たちには次の 3 つの選択肢が残されました。元のアプリケーションで実際には必要なかったと仮定するか、.NET アプリケーション用に独自の実装を記述するか、または .NET Framework で最も近いコレクションを使用するかです。順序付けられた名前と値のペアが他の場所で使用されている形跡がなかったので、私たちはこの LinkedHashSet の特別な実装が不要であると見なしました。そこで、私たちは基本の .NET Hashtable クラスを代わりに使用し、既存の単体テストおよび統合テストにより動作が正しいことを検証しました。ただし、もしパフォーマンス上または機能上の問題が発見された場合には、.NET Framework 2.0 の SortedDictionary<TKey, TValue> クラスを代わりに使用することができました。このクラスは、キーによって並べ替えられたキー/値のペアのコレクションを表します。その実装は、内部でレッドブラック ツリー データ構造に基づいたセットを使用しています。

このプロジェクトで特別な Java コレクション クラスが使用されていたのは 4 か所だけであり、それらはすべて似た状況で使用されていました。それらのクラスで提供される追加機能は利用されておらず、より汎用的な .NET のクラスを使用した方が同じタスクを容易に実現できました。

私たちの Java アプリケーションは、Java Version 1.4 で記述されていました。コレクション メンバの厳密な型指定を可能にするジェネリック コレクションは、Version 1.5 で初めて Java に導入されたものです。したがって、私たちはジェネリックの変換について詳しく検討する必要はありませんでした。おそらく、Java Version 1.5 におけるコレクションの変換では、コレクションを変換するだけでなく、型指定されたエントリも変換する必要があるため、変換プロセスの複雑さが少し増すでしょう。

フィルタおよび HTTP ハンドラ

フィルタは、前処理および後処理を実行するために要求や応答を選択的に遮断する方法として、J2EE Web アプリケーションで使用されている一般的なパラダイムです。フィルタの一般的な用途には、ログ記録、使用監査、セキュリティなどがあります。

Java フィルタは、特定のライフサイクル イベントを定義する Filter インターフェイスを実装します。フィルタは、アプリケーション サーバーによって、Filter クラスを完全なまたは部分的な URL にマップする URL マッピングを使用して起動されます。一致時に、アプリケーション サーバーはマップされたフィルタのライフサイクル イベントを実装して、要求および応答にハンドルを渡します。

ASP.NET は、同じ機能を IHttpHandler と呼ばれるインターフェイスの形で提供します。フィルタおよび HTTP ハンドラはいずれも、非常に単純なインターフェイスを備えながら、非常に強力な機能を提供します。私たちのアプリケーションには、2 つの異なる種類のフィルタがありました。GZip 圧縮を使用して応答を圧縮するフィルタと、要求をいったん遮断して、既知のユーザー エージェントからの要求かどうかを調べるフィルタです。

圧縮フィルタは、Java アプリケーションでパフォーマンスを向上させるために実装されていました。このフィルタはすべての応答ストリームを受け取り、クライアントが GZip 圧縮をサポートしているかどうかを調べて、サポートしている場合は応答を圧縮します。現在の HTTP サーバーのほとんどはこの機能をカスタム コードなしで提供しているので、このフィルタは一般には不要です。ただし、私たちのアプリケーションは、必ずしも HTTP サーバーを必要としないサーブレット アプリケーションとして使用されていたため、圧縮機能は付加価値モジュールの 1 つでした。

ユーザー エージェント ヘッダー値に基づいて要求を拒否する 2 番目のフィルタの方が、より興味深いものです。私たちの顧客の多くは、HTTP ユーザー エージェント ヘッダーを許可されたエージェントのリストと比較するというレベルの認証を実装するよう求めていました。受信した要求のユーザー エージェント値が、許可されたユーザー エージェントでない場合は、要求を拒否する必要があります (通常、HTTP の未認証戻り値を返します)。

この種類の要求/応答フィルタは、さまざまな方法で実現できます。しかし、ほとんどのソリューションでは、大量のコードや属性の挿入が必要となります。さいわい、J2EE と .NET はいずれも、要求および応答をアプリケーション レベルで遮断、変更、および調整するための非常に単純なメカニズムを提供しています。さらに、そのような HTTP インターセプタはコード浸透型ではありません。つまり、すべてのクラスにコードを追加するような必要はありません。それらはアプリケーション サーバーを介して管理および挿入される別個のクラスであるため、グローバルな検索/置換を行うよりもずっと簡単に機能を変更できます。

JLCA 3.0 では、Java から ASP.NET アプリケーションへのフィルタの移行を助けるようなヘルパ クラスが提供されます。図 4 には、サーバー処理のタイミングを制御する Java のサンプル フィルタを示しています。また、JLCA がこのフィルタを ASP.NET 用にどのようにして変換するかも示しています。理想的には、.NET 実装に移行するにはフィルタを HTTP ハンドラとして一から書き直す必要があるかもしれませんが、JLCA には再実装のほとんどの過程を処理する SupportClass.ServetFilter というサポート クラスが用意されています。ServletFilter は、Java が提供するライフ サイクルをエミュレートします。すべての問題が解決されるわけではありませんが、移行の実装は容易になります。

Figure 4 ServletFilter を使用した JLCA 変換 (Close Figure 4)

Java
import javax.servlet.*;
import java.io.*;

public final class TimerFilter implements Filter
{

    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
        throws IOException, ServletException
    {

        long startTime = System.currentTimeMillis();
        chain.doFilter(request, response);
        long stopTime = System.currentTimeMillis();
        System.out.println(“Time to execute request: “ +
            (stopTime - startTime) + “ milliseconds”);
    }

    public void destroy() {}

    public void init(FilterConfig fc) {}
}
C#
using System;
using System.Web;

// UPGRADE_TODO: Verify list of registered servlet filters.
public sealed class TimerFilter : SupportClass.ServletFilter
{
    public override void DoFilter(HttpRequest request,
        HttpResponse response, SupportClass.ServletFilterChain chain)
    {

        long startTime =
            (System.DateTime.Now.Ticks - 621355968000000000) / 10000;
        chain.doFilter(request, response);
        long stopTime =
            (System.DateTime.Now.Ticks - 621355968000000000) / 10000;
        Console.Out.WriteLine(“Time to execute request: “ +
            (stopTime - startTime) + “ milliseconds”);
      }

      public void  destroy() {}

      // UPGRADE_ISSUE: Interface ‘javax.servlet.FilterConfig’
      // was not converted.
      public void  init() {}
}

しかし、私たちのチームは、Java のフィルタ機能を HTTP ハンドラに手動で変換するという方法を取りました。これを詳しく説明するために、まず 2 つのインターフェイスを比較してみます。J2EE フィルタには、init、destroy、doFilter という 3 つのメソッドがあります。init および destroy メソッドは、ライフサイクル メソッドであり、ある程度の重要性を持っていますが、ここでの話題には直接関係ありません。一方、doFilter メソッドは、フィルタにおける最も重要な作業を実行します。

私たちのシナリオでは、doFilter メソッドは受信された要求からユーザー エージェント ヘッダー変数を取得し、構成可能な既知のユーザー エージェントのリストと照合します。変換で最も難しい問題は、動作可能なメソッドに引数として渡されるオブジェクトの違いです。

Java の Filter.doFilter メソッドには、3 つの引数が渡されます。要求オブジェクト、応答オブジェクト、およびフィルタ チェーン オブジェクトです。一方、IHttpHandler クラスの ProcessRequest メソッドには、HttpContext 変数という 1 つの引数しかありません。HttpContext 変数は HttpRequest および HttpResponse オブジェクトを参照し、同じ機能のほとんどが、HttpContext のメンバにアクセスすることによって実現できます。

Java の FilterChain オブジェクトは、興味深いオブジェクトです。このオブジェクトは、HTTP 要求内のフィルタのチェーンを表します。フィルタは、要求をチェーン内の次のフィルタに渡すことができ、それによりフィルタの役割を次々に委任していくことができます。FilterChain はそれほど頻繁に使用されるわけではありませんが、使用されている場合は、IHttpModule の実装によって同様な動作を実現できます。

FilterChain を除けば、Java フィルタを .NET の IHttpHandler に変換する作業は単純明快です。最初に、IHttpHandler の実装を作成する必要があります。次に、フィルタの doFilter メソッド内のロジックを、作成した実装の ProcessRequest メソッドに再構築する必要があります。このとき、渡される要求オブジェクトおよび応答オブジェクトの代わりに HttpContext のメンバを利用します。最後に、実装内に IsReusable メソッドを定義する必要があります。単純化のために、これは単に false を返すものとします (パフォーマンスを向上させるために、後でコードを追加して、以降の要求で同じハンドラ インスタンスを実際に再利用できるかどうかを決定するようにもできます)。

ソース ツリーおよび名前付け規則

Java と C# 言語は似ている一方で、辞書式順序、ソース ツリー レイアウト、および名前付け規則に違いがあります。ここでは、それらの違いのいくつかについて詳しく説明します。

Java のパッケージ (C# の名前空間に相当) は、反転したドメイン名の後にアプリケーション名またはモジュール名、さらに機能が続くという規則に従います。これらのパッケージは通常、深く入れ子になっています (一般に、4 または 5 レベル)。一方、C# の名前空間は通常、機能を表す名前によってグループ化され、一般に入れ子は浅くなっています (通常は 1 ～ 4 レベル)。また、Java のパッケージは通常、小文字またはキャメル形式で表記されますが、C# の名前空間は通常 Pascal 形式で表記されます。同様に、Java のメソッドは通常、キャメル形式で表記されますが、C# のメソッドおよびプロパティは通常 Pascal 形式で表記されます。

C# のインターフェイスは通常大文字の I で始まり、インターフェイスであることを表します (これは純粋に習慣的なものであり、機能上必須ではありません)。古い Java の慣習では、インターフェイス名の終わりに "able" を付けて、何かを実行可能であることを示すよう求められていました。この慣習はあまり従われなくなり、現在では通常、インターフェイスの名前とクラスの名前に区別はありません。

C# では、プロパティを使用してプライベート メンバのアクセスや変更を実装します。プロパティは、get および set アクセサ メソッド用のメタデータ ラッパーです。しかし、Java にはプロパティはありません。一般に、プライベート メンバのアクセサおよびミューテータは、getter または setter と呼ばれるメソッドとして実装されます。つまり、"name" という名前のプライベート フィールドに対するアクセサは、getName となります。

最後に、Java のクラスは、ソース ファイルのルート (またはクラスパス) を基準として、宣言されたパッケージと一致するディレクトリ内に存在する必要があります。C# には、この制限はありません。

名前付け規則に対するリファクタリングの時期

変換後の C# アプリケーションは、これらの違いに対処しなくてもコンパイルして実行できますが、名前付け規則に従うことは常に最良の習慣です。名前付け規則に従うように変換後のコードのリファクタリングをどの時点で行うかは、判断の難しいところです。しかし、遅いよりは早い方が良いことを示す理由が 2 つあります。

名前付け規則に従うようにコードをリファクタリングする作業は、時間がかかることはもとより、アプリケーションが機能するために絶対に必要なことではないため、プロジェクトの終わりの方になると不要と見なされる可能性があります。さまざまな要因により、これは低優先度のタスクとなる場合があり、まったく行われなくなるリスクもあります。ただし、プロジェクトに携わっている開発者のチームにとって、コードをリファクタリングしてわかりやすくすることは、チームの効率および生産性の向上に役立つ場合があります。こうした作業の重要性を過小評価しないでください。この種のリファクタリングは、変換プロセスに関わる他の作業と同様に、適切な結果を確実に得るために重要なものです。

ディレクトリ レイアウトおよび名前空間

名前付けとディレクトリの規則は、主観的なプロセスですが、一般的なガイドラインがあります。私たちのプロジェクトにおいて、カスタム XML 解析用のクラスがあると仮定し、そのクラスが com.mycompany.myapplication.xml.util 名前空間 (および変換後はディレクトリ) に含まれるとします。C# の規則では、このクラスは Xml.Util 名前空間に含まれることになります。リファクタリングの前に、私たちのディレクトリ ツリーは図 5 のようになっています。Visual Studio でファイルをドラッグ アンド ドロップすることで、ファイルを物理的に移動でき、ディレクトリ ツリーは図 6 のようになります。

図 5 Java ソース ツリー
図 5 Java ソース ツリー
図 6 C# ソース ツリー
図 6 C# ソース ツリー

ただし、C# では、ファイルのディレクトリ位置が、宣言された名前空間と一致することは求められていません。したがって、クラスの名前空間は、ファイル システムでの位置と一致するように更新されてはいません。複数のクラスを Visual Studio の異なる名前空間に自動的に移動する方法はありません。私の知る範囲で最善の方法は、図 7 に示すように、ソリューション全体に対して検索および置換を実行することです。これはもちろん、1 つの名前空間内のすべてのクラスを同じ移動先に移動すると仮定しています。

図 7 名前空間宣言の検索/置換
図 7 名前空間宣言の検索/置換(画像を縮小するには、ここをクリックします)
図 7 名前空間宣言の検索/置換
図 7 名前空間宣言の検索/置換 (画像を拡大するには、ここをクリックします)

プロパティ

C# におけるプロパティの構築は、Java とは大きく異なります。プロパティは、クラスの状態を格納するパブリック フィールド、または UML の用語で言えば、クラスの属性と考えることができます。しかし、Java にはプロパティ構造はありません。代わりに、プロパティは getter および setter と呼ばれるメソッドによって表されます。

"name" というインスタンス変数を持つクラスを考えてください。この変数を変更するとすべての制御が失われるため、この変数をパブリックにするのは望ましくありません。Java でこの変数にアクセスするための標準的な方法は、getter および setter を使用することです。これらは、変数名の先頭に "get" または "set" を付けてメソッド名とすることから名付けられています。したがって、name 変数が文字列である場合、Java の getter および setter は次のようになります。

public String getName() {
    return this.name;
}

protected void setName(String name) {
    this.name = name;
}

C# では、同じことを実現するためにプロパティ構造が提供されています。プロパティはアプリケーション ロジックに対しても使用できますが、本来の目的は、上記のようにプライベートな実装の詳細に対して保護されたアクセスを提供することです。したがって、同じアクセスの C# における実装は次のようになります。

public String Name
{
    get {return this.name;}
    protected set {this.name = value;}
}

実現する目的は同じですが、私は C# のプロパティの方がより明確であると考えます。Java では、クラスの状態を変更しないメソッドが get または set で始まる場合もあり、あいまいさにつながります。したがって、Java の getter および setter は C# のプロパティにリファクタリングすることをお勧めします。

とは言うものの、Java の getter および setter を C# のプロパティに変換する簡単な方法はありません。正規表現による検索/置換はきわめて複雑であり、メソッドがクラスの状態を変更しているのか、それとも何か他の機能を実行しているのかを知る方法がないため、JLCA は Java の getter および setter メソッドを単純にそのまま移行します。この問題に対する私たちのアプローチは、もっと実際的な解決策を用いることでした。Visual Studio には、プライベート メンバをプロパティでカプセル化するためのウィザードがあります。これにより、変換された Java の getter および setter を削除することなしに、目的のプロパティを生成できます。

他の理由でコードを編集していたときには、Visual Studio で C# プロパティを生成し、対応する getter および setter メソッドを削除することもありました。この手順の後、開発者は Visual Studio 2005 の "参照の検索" 機能を使用して、特定のメソッドに対するすべての呼び出しサイトのリストを表示します。これにより、古い getter および setter メソッドへの参照を確認し、簡単に参照先を新しいプロパティに変更できます。これは洗練された解決策とは言えませんが、驚くほどうまくいきました。

プロパティは C# および .NET の本質に関わる核心部分であり、私たちの目標は C# アプリケーションを作成することであったため、この作業は移行プロセスにおいて非常に重要な手順と見なされたことを強調しておきます (このアプリケーションを最終的にサポートするチームが想定していたのは C# アプリケーションであったことを思い出してください)。

メソッド名を Pascal 形式にする

既に述べたとおり、Java のメソッド名はキャメル形式で表記されるのが慣例です。つまり、これらの名前は小文字で始まり、以降の単語間の境界は大文字で表記されます。C# の規則では、メソッドおよび他のメンバを Pascal 形式で表記します。Pascal 形式はキャメル形式と似ていますが、名前の先頭文字も大文字にします。

これは何を意味するのでしょうか。おそらく、すべてのメソッド名を、小文字ではなく大文字で始まるように変更する必要があるでしょう。たとえば、次の Java メソッドを考えます。

public void getResponseCode()
これは、次のような C# メソッドにリファクタリングする必要があります。

public String GetResponseCode()

getter および setter をプロパティに変換する場合と同様に、変換後のすべてのコードを検索して C# の名前付け規則に従うようメンバを更新するための、簡単な方法はありません。ただし、変換されたメンバは C# のメンバには見えず、顧客はコードを正しい C# アプリケーションにするよう求めていたので、私たちはこれを非常に重要なタスクと考えました。

理論的には、このタスクを実行するコード コンバータを書くこともできましたが、私たちは自分たちの手でそれを行おうと考えました。ただし、プロパティのときと同じアプローチを採用し、メンバ名を少しずつ更新してゆくことにしました。手動によるアプローチを採用することには、いくつかの理由がありました。

まず、私たちの開発者は既に、getter や setter など他の要素を更新するためにすべてのコードを分析していました。そして、メンバ名を変更しなくてもコンパイルや機能には影響しないため、プロセスは段階的に行うことができました。さらに、何らかの理由で 1 つか 2 つのメンバを見逃したとしても、アプリケーションが壊れる心配はありませんでした。このプロセスは、皆さんが考えるほど時間はかからず、主な問題は、作業が単調で退屈だということでした。

Visual Studio 2005 には、リファクタリングのサポートが組み込まれています。リファクタリング サポートの一部として、メンバの名前を安全に変更する機能があります。メンバの名前を変更すると、そのメンバへのすべての参照が更新されます。更新する必要があるすべてのメンバに対して変更作業を行うのは多少時間がかかりますが、これは有効な解決策です。

まとめ

ここでは、Java の Web アプリケーションを ASP.NET に変換する実際の事例について紹介しました。大変な作業の多くは JLCA がやってくれましたが、手動による作業が必要な事項もいくつかありました。とは言え、JLCA は強力なツールであり、そのおかげで実稼動品質の .NET アプリケーションへの迅速な移行が可能になりました。

本稿における私の目的は、Java アプリケーションから .NET への変換が十分実現可能であることを示し、JLCA ではカバーしきれずに対処する必要のある一般的な問題をいくつか指摘することでした。アプリケーションの移行にはそれぞれ固有の問題があり、ここではそのすべてを取り上げることはできませんでした。しかし、遭遇するどのような問題についても役立つと思われる、いくつかの手法およびアプローチを紹介しました。

Brian Jimersonは、オハイオ州クリーブランドに本社を置くカスタム ソリューション プロバイダ Avantia, Inc. (www.avantia-inc.com) のシニア テクニカル アーキテクトです。彼は最近、Java と .NET Framework の両方について、インフラストラクチャやアーキテクチャのソリューションを顧客が定義および実装する際の手助けを行っています。


BizTalk Server: BizTalk プログラミングを向上させる 8 つのヒントと秘訣

BizTalk Server

BizTalk プログラミングを向上させる 8 つのヒントと秘訣

Marty Wasznicky and Scott Zimmerman
この記事の内容 : :
  • マルチパート メッセージ
  • 直接バインドされたポート
  • Web サービスを作成する
  • XSLT をデバッグする
この記事は次のテクノロジを使用しています:
BizTalk Server 2006、Visual Studio
コンテンツ
1. 常にマルチパート メッセージの種類を使用する
2. 常に直接バインドされたポートを使用してオーケストレーションをデザインするよう試みる
3. 常に別個の内部スキーマと外部スキーマを使用する
4. 内部スキーマを WSDL 内に直接公開しない
5. 常に BizTalk レジストリを Web サービス用に最適化する
6. 常にアセンブリ キー ファイルを相対パスで設定する
7. 無料のサンプル コードを見逃さない
8. Visual Studio で XSLT をデバッグする
まとめ

以前、主要な顧客に対して概念実証を行う予定だった Marty は、そのわずか半日前に、複雑な BizTalk® ソリューションの修正を依頼されました。メインのコンポーネントは、いくつかのバックエンド システムを統合したオーケストレーションであり、各システムへ複数の呼び出しを行っていました。彼が見つめたオーケストレーション デザイナの画面は、40 を超える受信および送信ポートに何十個もの受信図形と送信図形を接続している黒い線で埋め尽くされ、ほとんどデバッグ不可能なデザインでした。そこで彼は、次のような解決策を立てました。マルチパート メッセージの種類および直接バインドされたポート (後述のヒント #1 および #2) を使用して初めからやり直し、随時単体テストを実行しながら進めるという方法です。そして、どうなったと思われますか。何と、見事にうまくいったのです。

Marty の成功は、少なくとも部分的には、BizTalk Server のデザインのおかげと考えることができます。BizTalk Server は、接続されたシステムのプログラミングに固有である特別な困難を、ときにはコードを書く必要すらなしに、解決できるよう作られています。しかし、ドラッグ アンド ドロップによるこぎれいなフローチャートや、一見してコードが使用されてないことなどに、だまされないでください。BizTalk は、熟練するまでに何年もかかる幅広く強力な製品です。BizTalk のエキスパートへと至る道筋では、BizTalk に組み込まれた洗練されたメッセージ処理を補完するために、Microsoft® .NET Framework に基づくコードの記述が必要となる場面に多く遭遇します。

最近まで、BizTalk の初心者が、熟練者から秘訣を学んで習熟度を深める機会はほとんどありませんでした (いくつかのリソースについては、「BizTalk Server 2006 のプログラミングについて学習する」を参照してください)。私は、Marty が講師を務めた 1 週間の BizTalk トレーニング クラスに参加することで、BizTalk への最初の集中的な手ほどきを受けました。このクラスで Marty は、私たちに対して、システムを適切にデザインすること、そしてデザインを切り上げる前に、現実的な負荷条件の下で (PerfMon を使用して) システムをテストすることの重要性を、何度も熱心に説いていました。これらの講義から、私たちは、より有能な開発者になるために役立つたくさんのヒントを学びました。この記事の目的は、それらの有益なヒントを読者の皆さんと共有することです。それでは、始めましょう。

1. 常にマルチパート メッセージの種類を使用する

BizTalk の本質はメッセージです。ところで、それは電子メールに限ったことではありません。ドキュメント、InfoPath フォーム、大きなバイナリ ファイル、SQL レコード、フラット ファイル、および任意の XML をメッセージとして処理でき、ユーザーは非同期通信の恩恵を受けます。いったん慣れてしまうと、メッセージ指向のプログラミングは、オブジェクト指向プログラミングと同じぐらい洗練され便利なパラダイムです。

BizTalk のメッセージはデータであり、各メッセージはそれぞれ選択された種類のメッセージである必要があります。BizTalk で最も一般的なメッセージの種類はスキーマであり、メッセージ内のレコードおよびフィールド構造を指定する .XSD ファイルに基づいたメッセージです。BizTalk のスキーマおよびマップの詳細については、msdn2.microsoft.com/en-us/library/ms942182.aspx を参照してください。

いつか皆さんが BizTalk でプログラミングをするようになったら、間違いなく、メッセージが基づいているスキーマを変更したいと思うことでしょう。問題は、そのメッセージを既に送信図形または受信図形に対して選択していて、それをオーケストレーション ポートに接続済みである場合には、メッセージの種類を変更しようとすると、次のエラーが発生することです。

Property value is not valid: One or more Send or Receive actions are
 connected to Ports and are using this Message. Please disconnect the
 actions before changing the Message Type.

あわててエラー メッセージの指示に従う前に、その意味についてちょっと考えてみましょう。第 1 に、すべての受信図形と送信図形を検査して、それらの図形が、変更するスキーマに関連付けられた (そのメッセージの種類が設定された) メッセージ変数を使用しているかどうかを確認する必要があります。現在、BizTalk には、このような監査を行ったり依存関係マップを生成したりする機能はありません。そのような機能を必要とするほど多くの受信/送信図形を持つ単一のオーケストレーションを作成することは、あまり感心できません。最初に例に挙げた恐ろしいオーケストレーションでは、受信図形と送信図形のほとんどが異なるメッセージ変数を使用しながら、すべてが同じスキーマ定義 (同じメッセージの種類) を参照していたのです。

第 2 に、すべての受信/送信図形を確認した後で、それらのポート接続を削除する必要があります。第 3 に、"メッセージの種類" プロパティが新しいスキーマに設定されるようにメッセージ変数を変更してから、メッセージ変数をそれぞれの受信/送信図形と再度関連付ける必要があります。

そして (まだ終わりではありません) 第 4 に、受信/送信図形から切り離されたポートに関連付けられているすべてのポートの種類を見つけて、それらの "操作" の "メッセージの種類" プロパティをリセットする必要もあります。最初に受信図形または送信図形をポートに関連付けるときに、BizTalk はポートの種類の "メッセージの種類" プロパティを、その受信/送信図形のメッセージ変数のメッセージの種類と一致するよう自動的に設定します。ポートと受信/送信図形の間の接続を削除すると、BizTalk はそのポートのメッセージの種類を失います。

さいわいにも、これよりも良い方法があります。コンピュータ科学の格言の 1 つとして、多くの問題は間接レベルを追加することで解決できると言われています。それはこの場合にもやはり当てはまります。マルチパート メッセージの種類を使用して、基になるスキーマをラップすればよいのです。最初に必要な作業はやや増えますが、ずっと柔軟性が高くなり、長い目で見れば時間の節約になります。その方法を次に示します。

[オーケストレーションの種類] タブで [メッセージ] を右クリックして新しいメッセージを作成する場合、"メッセージの種類" プロパティには 4 つのオプションがあります (図 1 の右下を参照)。メッセージの種類にはスキーマを選択するのが一般的ですが、ここでは何か根本的に異なるものを試してみましょう。+ 記号をクリックして、マルチパート メッセージの種類を展開し、[新しいマルチパート メッセージの種類の作成] をクリックします。マルチパート メッセージの種類に名前を付けてから、+ 記号をクリックしてそれを展開すると、その種類の MessagePart_1 メンバ (新しいマルチパート メッセージの種類の最初の部分に自動的に付けられる名前) が表示されます。ここで、"識別子" プロパティを "MessagePart_1" から何かもっと良い名前、たとえば "Body" などに変更します (小文字の "body" は予約語なので使用しないでください)。"メッセージのボディ部" プロパティを True に設定することを忘れないでください (それにより、通常のメッセージと同じように動作します)。

図 1 [オーケストレーションの種類] で &quot;メッセージの種類&quot; プロパティを設定する
図 1 [オーケストレーションの種類] で &quot;メッセージの種類&quot; プロパティを設定する(画像を縮小するには、ここをクリックします)
図 1 [オーケストレーションの種類] で &quot;メッセージの種類&quot; プロパティを設定する
図 1 [オーケストレーションの種類] で &quot;メッセージの種類&quot; プロパティを設定する (画像を拡大するには、ここをクリックします)

これで、追加の作業は終わりです。最後に、新しいメッセージの部分 ("Body" など) の "種類" プロパティを、通常のスキーマ ベースのメッセージの種類に使用するのと同じスキーマに設定します。これで、受信/送信図形の論理ポートでマルチパート メッセージの種類が使用され、将来、ポートを受信図形から切り離すことなしに、基になるスキーマを簡単に変更できるようになります。

2. 常に直接バインドされたポートを使用してオーケストレーションをデザインするよう試みる

図 2 には、オーケストレーション デザイナでポートを構成するときに選択できるオプションを示しています。ポート構成ウィザードでの作業は、この図の上から下に向かって進みます。図中には 3 つの直接オプションがあります。BizTalk の UI にはポートの種類として "直接" と示されていますが、ここでは一般的な用語を使用して、これを "直接バインドされた" ポートと呼ぶことにします。

図 2 オーケストレーション デザイナのポート構成ウィザードのポート オプション
図 2 オーケストレーション デザイナのポート構成ウィザードのポート オプション(画像を縮小するには、ここをクリックします)
図 2 オーケストレーション デザイナのポート構成ウィザードのポート オプション
図 2 オーケストレーション デザイナのポート構成ウィザードのポート オプション (画像を拡大するには、ここをクリックします)

BizTalk のポート用語を理解するための鍵は、論理ポート (オーケストレーション ポートとも呼ばれます) と物理ポートという概念を理解することです。ごく単純に言うと、これはポートをオーケストレーション デザイナで作成するか (論理)、BizTalk エクスプローラまたは BizTalk 管理コンソールを使用するか (物理) の違いです。

開発者がオーケストレーション デザイナを使用して [後で指定する] オプションでポートを作成すると、論理ポートを構成することになり、対応する物理ポートのプロパティは後で BizTalk 管理者によって構成されます。実稼動環境で物理ポートを構成できる柔軟性を管理者に与えることが、[後で指定する] が最もよく使用されるオプションである主な理由です。

[今指定する] オプションを選択すると、開発者が論理構成および物理構成をすべて行うことになり、ソリューションは固定化されたものになります。手早く実験を行う場合を除き、これはお勧めしません。

"動的" ポートの場合、物理ポートのプロパティの最終的な構成は、やはり管理者によって行われます。ただし、この場合は、開発者もコードを記述して、特定の送信メッセージ プロパティが実行時に設定されるようにできます。これは、オーケストレーションのロジックに次のようなステートメントを使用した式図形が含まれる、SMTP または HTTP の場合に有用です。

DynamicSendPort(Microsoft.XLANGs.BaseTypes.Address)=
    “mailto:john@contoso.com”;

図 2 には多くのオプションがありますが、図ではオーケストレーション デザイナのオプションだけが示されています。静的ポートはどこに含まれるのでしょうか。この用語は、BizTalk エクスプローラまたは BizTalk 管理コンソールで物理ポートを作成するときに出現します。開発者がオーケストレーション デザイナで "後で指定する" ポートまたは "動的" ポート (論理ポート) を作成した場合、管理者は BizTalk エクスプローラまたは BizTalk 管理コンソールで対応する物理ポートを作成するときに、図 3 に示される 4 つのオプションから選択できます。

Figure 3 物理ポートおよび論理ポートの種類 (Close Figure 3)

作成する物理ポートの種類 | 対応する論理ポートの種類
静的、一方向 | 一方向/受信または送信/後で指定する
静的、送信請求 - 応答 | 要求 - 応答/送信ポート/後で指定する
動的、一方向 | 一方向/送信ポート/動的
動的、送信請求 - 応答 | 要求 - 応答/送信ポート/動的
BizTalk Server 2006 のプログラミングについて学習する

BizTalk プログラミングの学習を始めたばかりの方であれば、MSDN Magazine の 2005 年 11 月号の Aaron Skonnard による Service Station に関するコラム、および 2005 年 12 月号のコラムが非常に参考になると思います。BizTalk Server Developer Center には、無料の Webcast が多数用意されています。

中級レベルの方は、Mark Beckner 著『BizTalk 2006 のレシピ : 問題解決のアプローチ』(Apress 発行) をお読みください。また、MSDN Magazine の 2006 年 12 月号では、BizTalk アダプタについての Alex Starykh によるアダプタ プログラミングのトピックも参考になります。

中級から上級レベルの参考資料としては、George Dunphy 著 (Marty 共著)『プロの BizTalk 2006』(Apress 発行)、および Darren Jefford 著『プロフェッショナル BizTalk Server 2006』(Wrox 発行) が既に発行されているはずですのでご覧ください。タイトル中のバージョン番号は古いですが、Scott Woodgate 著『Microsoft BizTalk Server 2004』(Sams 発行) もまだ十分参考になります。これらの書籍には、C# コードを使用して BizTalk のレベルを高めるいくつかの例が含まれています。

統合システムの原理およびデザインに精通したいならば、Gregor Hohpe 著『エンタープライズ統合パターン』(Addison-Wesley 発行) を読む必要があります。一見したところ、この本は BizTalk の学習には役立たないように感じるかもしれませんが、BizTalk Server 2006 がこれらのパターンのいくつかを暗黙に実装しているのを知ると興味深いでしょう。ヒント #1 については、Kurt Guenther と Charles Young による以下のブログも参考になります。

  • www.infusionblogs.com/blogs/894.aspx
  • geekswithblogs.net/cyoung/articles/4634.aspx

ヒント #4 については、Brian Loesgen と Kevin Lam による以下の専門的なブログを参照してください。

  • geekswithblogs.net/bloesgen/archive/2005/10/27/58364.aspx
  • blogs.msdn.com/578572.aspx

BizTalk に関するその他のヒントについては、"7 つの習慣" に関する Alan Smith の記事、および BizTalk の名前付け規則に関する Scott Colestock の役立つ記事を参照してください。

  • geekswithblogs.com/asmith/articles/17333.aspx
  • traceofthought.net/misc/BizTalk%20Naming%20Conventions.htm

動的な物理ポートは (当然ながら) 動的な論理ポートに対応しますが、静的な物理ポートは "後で指定する" 論理ポートに対応することを覚えておいてください。また、"送信請求 - 応答" ポートと "要求 - 応答" ポートとの用語の不一致にも注意してください。これは、仕方のないことです。

前置きはこれぐらいにして、直接バインドされたポートの利点について説明しましょう。直接バインドされたポートは、BizTalk から BizTalk へメッセージを送信するときに役立ちます。BizTalk が Web ポートに対して [直接] オプションを持つことに意味がないのは、そのためです。Web ポートは BizTalk から Web への送信のためのものであり、BizTalk から BizTalk への送信に使用されるものではありません。

直接バインドされたポートは、柔軟性やパフォーマンスを損なうことなく、自動的に構成されます。直接バインドされたポートの便利な長所は、開発者も管理者も BizTalk エクスプローラや BizTalk 管理コンソールで対応する物理ポートを作成する必要がないことです。そのため、直接バインドされたポートを使用すると、より自己完結的なオーケストレーションが得られ、したがって、オーケストレーションを再利用したり、独立して再展開を行ったりしやすくなります。

直接バインドされたポートには、3 つの種類があります。メッセージ ボックス ルーティング、自己関連付け、およびオーケストレーション間 (パートナー ポートとも呼ばれます) です。オーケストレーション デザイナの 3 つのラジオ ボタンには、実際はもっと長いテキストが表示されていますが、ここで使用する簡略名をそれぞれ UI に対応付けることは簡単です。

この 3 つのポートの種類のうち、メッセージ ボックスは最も頻繁に使用されるものです。これを使用すると、オーケストレーションを他のオーケストレーションから完全に独立させることができますが、オーケストレーションを自分自身と通信させる必要があるかもしれません。メッセージ ボックスは、無限ループを引き起こす可能性を含んでいます。これについては、すぐ後で説明します。

"アクティブ化" プロパティが True に設定された受信図形をメッセージ ボックス ルーティング用に構成すると、指定されたサブスクリプション フィルタに一致するメッセージがメッセージ ボックスに到着したときに、オーケストレーションの新しいインスタンスが起動されます。これは、メッセージの発信場所や送信者には関係ありません。これを利用して、ユーティリティ ライブラリ内の汎用関数のような、再利用可能なオーケストレーションを構築できます。Marty は、この手法を用いて洗練されたメッセージ ベースの例外処理システムを構築しましたが、それはまた別の話題です。

オーケストレーションを 2 つの条件で起動するとしましょう。1 つは、オペレータが静的な一方向の FILE (物理) ポートでフォルダに "キックオフ" メッセージを配置した場合であり、もう 1 つは、オーケストレーションが自分自身にキックオフ メッセージを送信して別のインスタンスを起動した場合です。まず考えることは、(オーケストレーションに対する OR 演算子のような) 待ち受け図形を使用して、その下に 2 つの受信図形 (FILE ポート用の "後で指定する" とメッセージ ボックス用の "直接") を配置することかもしれません。しかし、直接バインドされたメッセージ ボックス ポートの良い所は、メッセージの発信場所を問わない点です。したがって、このオーケストレーションを開始するには、1 つの受信図形だけで十分です。

自己関連付けを行う直接バインドされたポートは、通常、1 つのオーケストレーションから次のオーケストレーションにメッセージを非同期で送信する場合に使用します。たとえば、2 つのオーケストレーション A と B があるとします。オーケストレーション B について、[パラメータ] セクションに論理送信ポートを作成し、そのポートがオーケストレーション A で作成されたポートの種類を使用するように設定します。これにより、オーケストレーション B 内のそのポートから送信する任意のメッセージは、オーケストレーション A で作成した論理受信ポートに直接ルーティングされます。ここでは基本的に、BizTalk のパブリッシュ/サブスクライブの特性を利用して、メッセージを転送するだけではなく、隠れた固有の関連付けトークンをメッセージに付加することで "自己関連付け" を行います。この場合、2 つのオーケストレーション間でポートの種類を共有しているため、各オーケストレーションはそれぞれ相手について多少の知識を持っています。このポートの種類に対して [操作] の [メッセージの種類] を System.Xml.XmlDocument に設定することで、依存性をいくらか小さくできます。これは、ある種のブローカーが他のオーケストレーションから肯定応答や否定応答を受信できるようにしながら、各オーケストレーションを非同期で実行することができる、便利な方法です。

"オーケストレーション間" の直接バインドされた (パートナー) ポートは、メッセージを非同期で送信するために使用されます。この場合、メッセージ ボックス型の直接バインドされたポートに比べて、サブスクリプションに加えられる制限はより幅広いものになります。これは、パートナー オーケストレーションからの任意のメッセージを受け入れることを意味します。一方のオーケストレーションは直接バインドされたポートを指定し、もう一方のオーケストレーション (パートナー) は自分自身を指定します。直接バインドされたポートを参照するのは、送信側でも受信側でもかまいません。ここでのポイントは、2 つの既知のソリューションを結び付けて通信可能にすることです。メッセージ ボックス型の直接バインドされたポートの場合は、送信側と受信側はより緩く結合されています。

ここで、興味深いことをお話しましょう。直接バインドされたポート (特にメッセージ ボックス) でよくある落とし穴は、無限ループを作り出してしまうことです。"アクティブ化" が True である受信図形 (もちろん、直接バインド) と、単にメッセージを FILE ポートに転送するだけの送信図形という、2 つの図形だけからなる単純なオーケストレーションを考えます。このオーケストレーションがメッセージを送信すると、メッセージはどこに行くでしょうか。通常どおり、最初はメッセージ ボックスに行きます。メッセージ ボックスにメッセージが到着すると、BizTalk は一致するサブスクリプションがあるかどうかを検索します。そして、このメッセージは、最初にオーケストレーションをアクティブ化したメッセージとどのように異なるでしょうか。いいえ、異なりません。したがって、BizTalk は、メッセージを処理するためにオーケストレーションの別のインスタンスを起動します。これが、メモリがなくなるまで繰り返されます。この問題を修正する方法の 1 つは、受信メッセージを新しく構築された送信メッセージにコピーし、少なくとも 1 つの昇格されたプロパティの値を変更して、受信図形に対するサブスクリプション フィルタが新しいメッセージと一致しないようにすることです。管理コンソールを起動し、[グループ ハブ]、[新しいクエリ]、[クエリを開く] の順にクリックします。次の手順は、BizTalk\SDK\Utilities フォルダの BTSSubscriptionViewer.btq を開くことです。受信図形のメッセージの種類を設定すると、BizTalk によって自動的に述語が作成されるのがわかります。オーケストレーション デザイナで、"フィルタ式" プロパティに述語を追加して、サブスクリプションの全体を次のようにします。

BTS.MessageType == “http://MyInternalSchemas.MyProject#MyRootNode”
AND inbound_message(status) != 1

または、スキーマを変更してこのようなカスタム フィールドを追加したくない場合は、受信図形のサブスクリプション フィルタに句を追加して、BTS.Operation 名がオーケストレーション内の名前と一致しないことをテストするようにもできます。

私たちが BizTalk の本質はメッセージであると言ったのを覚えていますでしょうか。それは取り消します。実際には、BizTalk の本質はサブスクリプションです。送信ポートまたはオーケストレーションにおけるサブスクリプションがなければ、BizTalk のメッセージには何も起こりません。

3. 常に別個の内部スキーマと外部スキーマを使用する

受信したメッセージは常に、スキーマがどれだけ単純であるか、または送信元がだれであるかに関係なく、独自の標準スキーマに変換する必要があります。

この少しの処理で、送信者がスキーマを変更した場合に大きな柔軟性を得る、または適切な処理を行うことができます。オーケストレーションのロジックが、他のだれかによって制御されるスキーマ内のプロパティやフィールドに依存するのは望ましくありません。送信側がスキーマを変更した場合は、単にマップを変更すればよく、オーケストレーションを変更する必要はありません。ここでもまた、間接性についての格言が当てはまっています。

さらに、マップ、オーケストレーション、内部スキーマ、および外部スキーマに対してそれぞれ別の Visual Studio プロジェクトを作成するというベスト プラクティスに従えば、更新したソリューションの再展開が容易になります。これは、BizTalk Web サービス公開ウィザードによって生成された Web サービスについては、特に重要となります。

もしも面倒であれば、新しいフィールド名を持つ独自のスキーマを作り出さなくてもかまいません。単に、与えられた受信スキーマをコピーして、ソース スキーマからのフィールドをすべてそのままマップします。ただし、少なくともそのコピーを内部スキーマ プロジェクトに保存し、独自のスキーマとして扱ってください。

これには、必要なマップの数を減らす効果もあります。たとえば、3 種類の受信メッセージを 3 種類の送信メッセージにマップする必要があるとします (現在はないとしても、将来はあるかもしれません)。この手法を適用し、各受信スキーマに対して独自の標準スキーマへのマップを作成してから、標準スキーマから各送信スキーマへのマップを作成します。これにより、構築して保持する必要のあるマップは、9 個ではなく 6 個で済みます。

避けた方がよいこと

BizTalk Server 2006 は大規模なアプリケーションであり、さいわいにも、多くの場合、同じことを成し遂げるのにいくつか異なる方法が用意されています。しかし、そのうちのいくつかは、ほとんどの熟練者がさまざまな理由で避けることを学んできた落とし穴です。

  1. BizTalk Server 2006 の BizTalk エクスプローラを使用するのは避けてください。この機能は、BizTalk Server 2004 では必要不可欠であったため、Microsoft はこれを BizTalk Server 2006 からも外さないことに決めましたが、新しいアプリケーション パッケージ化機能と再設計された BizTalk 管理コンソールにより、冗長なものとなっています。残念ながら、BizTalk エクスプローラを使用すると混乱が生じる場合がいくつかあります。
  2. Visual Studio® 2005 のソリューション エクスプローラでは、プロジェクト レベルで [配置] をクリックしないでください。ソリューション内で独立してプロジェクトをビルドするのはかまいませんが、[配置] はソリューション レベルでのみクリックしてください。なぜなら、Visual Studio は自動的に依存関係を追跡しようとしますが、アセンブリを個別に BizTalk に配置すると、依存関係を見失う場合があるためです。
  3. 1 つのプロジェクトから別のプロジェクトにスキーマ ファイル (.XSD) をコピーする場合は、名前空間属性が一致するように注意深く編集する必要があります。それを行わないと、頭痛を抱えることになります。
  4. BizTalk のプロフェッショナルは、[Quick Promote] (クイック昇格) を決して使用しません。クイック昇格で与えられた種類を修正するために、必要な手順を実行できる (それを忘れないと仮定して) ならば、それらを自分ですばやく明示的に作成することもできるはずです。
  5. 複数の受信メッセージを 1 つのメッセージにマップするか、または、既存のメッセージの変更された (マップされた) 内容をベースとして、新しいメッセージを生成する必要がある場合以外は、マップをオーケストレーションには配置しないようにします。展開を単純にするためには、マップを受信ポートおよび送信ポートに配置するのが適切です。ビジネス パートナーがスキーマを変更した場合、または、新しいマップを必要とする新しいパートナーを追加する場合に、スキーマとオーケストレーションの両方を更新する羽目になるのは好ましくありません。

4. 内部スキーマを WSDL 内に直接公開しない

これは、BizTalk Web サービス公開ウィザードの最初のラジオ ボタンを決してクリックしないことを意味します。常に 2 番目のボタンをクリックしてください (図 4 を参照)。最初のラジオ ボタンは、オーケストレーションの公開用にデザインされ、2 番目のボタンは純粋なメッセージング ソリューション用にデザインされています。しかし、オーケストレーションにも 2 番目のオプションを使用することをお勧めします。

図 4 オーケストレーションではなく、外部スキーマを公開する
図 4 オーケストレーションではなく、外部スキーマを公開する(画像を縮小するには、ここをクリックします)
図 4 オーケストレーションではなく、外部スキーマを公開する
図 4 オーケストレーションではなく、外部スキーマを公開する (画像を拡大するには、ここをクリックします)

確かに、これはより複雑なので、なぜそうする必要があるのかの適切な理由を知りたいことでしょう。一言で言えば、インターフェイスです。または、より冗長な表現を好むならば、ゆるやかな結びつき (疎結合) です。これにより、呼び出し元を切断せずにオーケストレーションを変更するための自由度が高くなります。これは、ヒント #3 の概念を特化したものと考えてください。

どのように実行するか見てみましょう。ウィザードについて、1 つ注意することがあります。手順 2 の [Web サービス] ダイアログ (図 5) に進んだら、事実上すべてのものを右クリックする必要があります。これにより、Web サービスと、作成するそのメソッドに名前を付けます。

図 5 [Web サービス] ダイアログで WSDL を編集する
図 5 [Web サービス] ダイアログで WSDL を編集する

アセンブリを参照できるようにするために、Web サービス公開ウィザードでこの手順に達する前に、BizTalk でスキーマ プロジェクトをコンパイルしておく必要があります。実際には、後で見るように、ウィザードを実行する前にソリューションを展開するのが合理的です。もう 1 つ付け加えると、オーケストレーションを Web サービスとして公開するときには、ビルドして展開する前に、オーケストレーションのポートの種類の "型修飾子" プロパティを、"内部" から "パブリック" に変更する必要があります。ポートを最初に作成したときにこの変更を行っていない場合は、これを見つけるのに多少手間取るかもしれません ([オーケストレーションの種類] の [種類] ペインでポートの種類をハイライト表示します。ポート画面でポートをクリックするのではありません。そうすると、異なるプロパティのセットが表示されます)。しかし、スキーマを Web サービスとして公開することのもう 1 つの利点は、この作業を以降は行わなくて済むことです。

さて、ウィザードに戻ることにしましょう。図 5 をご覧ください。要求および応答のノードを右クリックして、Web メッセージの基になるスキーマを選択する必要があります。一般に、公開するオーケストレーションには、Web サービスの呼び出し元と通信するための論理受信図形および送信図形があります。これらの図形にはメッセージが関連付けられ、それらのメッセージにはスキーマの種類が関連付けられます (マルチパート メッセージの種類は必須ではありませんが、お勧めします)。したがって、ウィザードで外部に面したスキーマを使用することを指定する必要があります (ヒント #3 を思い出してください)。次に、受信ポートのマップ (受信マップおよび送信マップ) を使用して、Web サービスの要求および応答スキーマを、オーケストレーションで期待される内部スキーマに変換します。顧客がこの Web サービスへの参照を追加すると、外部に面したスキーマの種類を含むプロキシ クラスが顧客に提供されます。顧客はその種類を使用して、Web サービスに送信する要求メッセージの作成と値の格納を行います。そして、Web サービスからの応答を、図 5 で指定する外部応答スキーマに基づくオブジェクトに格納します。

また、受信場所で XML パイプラインを選択することも忘れないでください。選択しないと、BizTalk は、BTS メッセージ コンテキストのプロパティを作成しません。このプロパティの 1 つが、受信 Web サービス メッセージの "メッセージの種類" です。BizTalk は、パイプライン内で XML 逆アセンブラを使用してメッセージの種類を解析し、プロパティを昇格しているので、メッセージの種類を知っており、その種類を使用して、要求をオーケストレーションが期待する種類に変換するためのマップを実行します。

特に重要なのは、ウィザードで選択する操作名が、オーケストレーションの論理ポートで指定する操作の名前と正確に一致する必要があることです。単にすべてを既定値のままにするだけでは、うまくいきません。なぜなら、オーケストレーション デザイナでの既定の操作名は "Operation_1" ですが、ウィザードでの既定名は "WebMethod1" であるためです (図 5 を参照)。それらの名前を、動詞と名詞で構成された意味のある名前 (UpdateInventory など) に設定すると良いでしょう。

受信 Web サービス要求メッセージも含め、メッセージはサブスクリプションに基づいてオーケストレーションに送信されることに注意してください。"要求 - 応答" ポートを Web サービスとして公開する場合 (スキーマを介して公開する場合も含む) の要件として、論理ポートを物理ポートにバインドすると、BizTalk は自動的にサブスクリプションを作成します。BizTalk 管理コンソールからサブスクリプションを表示するには、クエリ ウィンドウで値として "Subscriptions" を選択します。SOAP に対するサブスクリプションは、他の直接バインドではない論理/物理ポートに対して通常作成されるものとは若干異なっています。オーケストレーションに対して BizTalk が作成する標準的なサブスクリプションは、次のようになります。

http://schemas.microsoft.com/BizTalk/2003/
system-properties.ReceivePortID == {some Guid}
AND
http://schemas.microsoft.com/BizTalk/2003/system-properties.MessageType ==
http://schemas.someschema#Root
AND
http://schemas.microsoft.com/BizTalk/2003/
system-properties.InboundTransportType != SOAP
OR
http://schemas.microsoft.com/BizTalk/2003/
system-properties.ReceivePortID == {some Guid}
AND
http://schemas.microsoft.com/BizTalk/2003/soap-properties.MethodName ==
 {Inbound Operation Name on Logical Port}

ウィザードに MethodName として入力する名前は、オーケストレーション内の論理ポートの操作名に一致する必要があります。一致しないと、マップされた Web 要求メッセージがオーケストレーションに送られません。

ウィザードで [Web サービス プロジェクト] ダイアログに進んだら、Web サービスの URL (http://localhost/MyBizTalkWebService など) を入力します。また、BizTalk でアプリケーション内に受信場所を自動的に作成するようにもできます。アプリケーションは既に展開済みであるため、ここではドロップダウン リストから選択できます。これらすべての詳細については、BizTalk ヘルプ ファイルで「BizTalk Web サービス公開ウィザードを使用してスキーマを Web サービスとして公開する方法」という記事を必ず参照してください。

BizTalk の本質はサブスクリプションであると言いましたが、それはまだ最終的な答えではありません。最後の 2 つのヒントからわかりますが、実際には、本質はスキーマなのです。すべてのメッセージおよびマップは、1 つ (または 2 つ) のスキーマに依存するため、BizTalk に習熟するためには、スキーマについてマスターすることが肝要です。

5. 常に BizTalk レジストリを Web サービス用に最適化する

BizTalk オーケストレーションを Web サービスとして公開する場合、BizTalk で使用される既定の ASP.NET パラメータのいくつかを調整したくなるのは確実です。実際、これらのパラメータは、XLANG を含む CLR を使用したすべての BizTalk アーティファクトに影響します。BizTalk は自動的に、使用している CPU の個数をこれらの推奨値に乗算します。次に手順を示します。

1. レジストリを変更する前に、レジストリのバックアップを取ります。

2. メモ帳で、次のコードを含むファイルを作成します。

Windows Registry Editor version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BTSSvc$BTSHOST\CLR Hosting]
“MaxIOThreads”=dword:00000064
“MaxWorkerThreads”=dword:00000064
“MinIOThreads”=dword:00000019
“MinWorkerThreads”=dword:00000019

このようにファイルを作成することで、BizTalk が動作する他のコンピュータでファイルを簡単に再利用できるようになります。DWORD 値は 16 進数です (レジストリ スクリプトでは、16 進値に標準的な 0x プレフィックスを付けません)。

3. 文字列 "BTSHOST" を、使用しているコンピュータの BizTalk ホストの名前で置き換えます。最初に RegEdit を実行して、コンピュータ上の実際のキー名を調べることもできます。

4. このファイルに .REG という拡張子を付けて保存すると、BizTalk 上でダブルクリックして実行できます。

5. RegEdit を実行し、CLR Hosting キーに移動して、値が正しく追加されていることを確認します。

この手順および他の最適化の詳細については、msdn2.microsoft.com/en-us/library/aa561380.aspx を参照してください。また、低遅延メッセージングのためのパフォーマンス調整に関する優れた記事が msdn2.microsoft.com/en-us/library/aa475435.aspx にあります。

6. 常にアセンブリ キー ファイルを相対パスで設定する

このヒントは、バージョン管理からソリューションをチェック アウトし、別のコンピュータ上でそれを構築する際に役立ちます。チーム内の開発者の何人かが環境を異なるドライブ文字で構成している場合もあれば、自分も将来は同じように変更する場合もあります。これは実際には単純ですが、"これらのドットは何を意味しているのか" と頭をひねることがあるかもしれません。

お勧めするのは、ソリューションの最初の開発者が、Visual Studio ソリューション ファイル (.sln) と同じフォルダに厳密な名前のキー ファイルを作成することです。そして、そのソリューション フォルダのサブフォルダを作成し、マップ、パイプライン、オーケストレーション、内部スキーマ、外部スキーマ、およびコンパイルする任意の .NET コンポーネントなど、各 Visual Studio プロジェクトをアーティファクトの種類別に保持します。ところで、このアドバイスは何度も聞いたかもしれませんが、これを厳密なルールと考える必要はありません。特定のアーティファクトが将来一緒に修正される可能性が高い (たとえば、マップとそのスキーマなど) ことがわかっている場合は、それらを同じプロジェクトに含めることで、再展開する必要のあるコンポーネントの数を減らします。開発中には、ソリューション内のプロジェクト間、および開発者間で、同じキー ファイルを共有できます (実稼動用に安全性の高いキー ファイルを作成することは、また別の問題です)。単に、ソリューション内の他のアーティファクトと共に、キー ファイルをバージョン管理にチェック インしてください。ここで、ソリューション内の各アセンブリに対するキー ファイルのパスを示します。BizTalk プロジェクトに対しては、図 6 に示すように、"..\..\..\Key.snk" と記述します。これが機能するのは、BizTalk がバイナリを [ソリューション名]\[プロジェクト名]\bin\Development のような名前のサブフォルダに構築するからです。したがって、3 つの親ディレクトリをさかのぼることで、キー ファイルを含むソリューション フォルダに戻ります。

図 6 BizTalk プロジェクトに対してアセンブリ キー ファイルを構成する
図 6 BizTalk プロジェクトに対してアセンブリ キー ファイルを構成する(画像を縮小するには、ここをクリックします)
図 6 BizTalk プロジェクトに対してアセンブリ キー ファイルを構成する
図 6 BizTalk プロジェクトに対してアセンブリ キー ファイルを構成する (画像を拡大するには、ここをクリックします)

ソリューションに .NET Framework クラス ライブラリ プロジェクトを追加する場合、それらのプロジェクトは BizTalk プロジェクトとは少し異なるディレクトリ構造でコンパイルされることに注意してください。また、キー ファイル パスを設定する Visual Studio の UI は、実際には相対パスの入力を許可しません。1 つの解決策は、.csproj (C# の場合) または .vbproj (Visual Basic® の場合) をテキスト ファイルとして編集することです。Visual Studio のプロジェクト プロパティ UI で参照したキー ファイルの名前を検索し、「..\Key.snk」のように 1 つの親ディレクトリだけをさかのぼる相対パスに置き換えます。各ビルド後に、.NET Framework アセンブリを GAC に追加することを忘れないでください。これは、Visual Studio のビルド後の手順で自動的に行うことができます。次に示すように、完全パスを使用して、スペースを含む文字列は引用符で囲む必要があります (すべて 1 行に記述します)。

“C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe” /i “$(TargetPath)” /F

7. 無料のサンプル コードを見逃さない

ここでは、非常に価値のある簡単なヒントを提供します。BizTalk ヘルプ ファイルには、学習し、適用し、再利用することのできるサンプル アプリケーションおよびスクリプトが 50 以上用意されています (SDK\Samples フォルダにインストールされます)。また、msdn2.microsoft.com/en-us/biztalk/aa937647.aspx にも、30 以上の便利な BizTalk アプリケーションが掲載されています。最後に、GotDotNet.com にある「BizTalk ブロガーズ ガイド」には、400 以上の技術記事が集められ、多くの有用なコードおよびサンプルが含まれています。私は、この「ブロガーズ ガイド」へのショートカットをデスクトップに置いていて、BizTalk の新しい領域に踏み込むときには最初に参照する場所の 1 つです。

もし既にこれをご存知でしたら、つまらないヒントだと思われるかもしれません。しかし、そうしないと専門的な BizTalk のサンプル コードを見つけるのは困難であるため、これらの宝庫がすぐ手の届くところにあることを、あえてお知らせしたいと考えました。実のところ、これらのサンプルは、BizTalk を使い始めるときに有用なだけでなく、何度も何度も繰り返し役に立つものです。

ところで、BizTalk ヘルプ ファイルは定期的に更新されるので、MSDN でチェックすることを忘れないでください。ファイルは PDF または圧縮された .chm 形式でダウンロードするか、またはオンラインで閲覧することができます。

8. Visual Studio で XSLT をデバッグする

これは Visual Studio で最も知られていない機能かもしれません。この機能は、BizTalk をインストールしていなくても使用できます。XSLT デバッガを使用すると、変換の実行中にその結果を表示したり、ブレークポイントを設定したり、ローカル変数やコール スタックを調べたりできます。C# または Visual Basic から XSLT スクリプトにステップ インすることもできます。

図 7 は、変換の実行中の例を示しています 空白のソリューションを作成して、HTML に変換するこの小さな XML データ ファイルを読み込んだところです。

図 7 Visual Studio の XSLT デバッガ
図 7 Visual Studio の XSLT デバッガ(画像を縮小するには、ここをクリックします)
図 7 Visual Studio の XSLT デバッガ
図 7 Visual Studio の XSLT デバッガ (画像を拡大するには、ここをクリックします)

<?xml version=”1.0” encoding=”utf-8”?>
<node attr=”debug” />
次に、図 7 の左側のペインに示されている XSLT ファイルを入力しました。これにより、Hello World ドキュメントが作成され、また、ノード要素の attr 属性が "debug" に等しい場合は、それが出力されます。

ここで、カーソルが XSLT ファイル内にある状態で (これは重要です)、"入力" プロパティを、変換する XML データ ファイルのパスに設定します。ソリューション エクスプローラ ウィンドウで XSLT ファイル名自体を右クリックすると、異なるプロパティのセットが表示されることに注意してください。ドキュメントのプロパティではなく、ファイルのプロパティが表示されます。もっと直観的であればよいとは思いますが、いったん覚えてしまえば大丈夫です。そして、もしもプロフェッショナルと平均的なユーザーとを隔てるものがあるとすれば、それは探究心です。"出力" プロパティを設定することもできますし、中央のペインで変換が実行される様子を眺めるのもよいでしょう。

Visual Studio 内のアクティブ ドキュメントが XSLT ファイルである場合、[XML] ドロップダウン メニュー (メイン メニューバー) に [Debug XSLT] (XSLT のデバッグ) コマンドが表示されます。これをクリックすると、変換コード内をステップ実行でき、サブルーチンにステップ インすることもできます。図 7 では、プログラムが xsl:if テストを実行し終えたところで、[ウォッチ] ウィンドウに @attr の値が表示されています。中央のウィンドウの出力からわかるように、テストの結果は true であり、<h1> ノードが出力されています。

まとめ

今日、さまざまな組織で開発されるアプリケーションの数を考えると、稼動するソリューションをできるだけ早く作り出すために、優れたツールや技法を使用することは重要です。BizTalk は、概念から稼動するプロトタイプへと効率的に移行するために役立ちます。ここに示したヒントによって、実際の作業で費やされる時間が少しでも節約できればさいわいです。

Marty Wasznicky(MCSE、MCSD、MCDBA、MCTS) は、Microsoft の Connected System Division で BizTalk Server の地域プログラム マネージャを務めています。彼の職務は、顧客や Microsoft のパートナーが、この領域で Microsoft のソリューションを採用、導入する際に、それを支援することです。現在担当しているのは、Software Oriented Architecture テクノロジおよび Enterprise Service Bus テクノロジです。現在、夫人と共にロサンゼルスで暮らしています。

Scott Zimmerman(MCSD、MCTS) は、Microsoft で BizTalk および .NET を専門とするシニア アプリケーション開発コンサルタントを務めています。Web サービス デザインに関して 2 つの賞を受賞しているほか、フリスビーの世界チャンピオンに 8 度輝いた実績を持ちます。現在、Vera 夫人と共にワシントン D.C. で暮らしています。


共同作業: Web サービスと Groove 2007 がチーム間の共同作業を支援する

共同作業

Web サービスと Groove 2007 がチーム間の共同作業を支援する

John C. Hancock
この記事の内容 : :
  • Groove 2007 の概要
  • Groove フォームを使用してデータを収集する
  • Groove Web サービス API
この記事は次のテクノロジを使用しています:
Microsoft Office, .NET Framework
コードのダウンロード : : Groove2007_05.exe (524KB)
コンテンツ
Groove でソリューションを構築する
Groove 2007 のアーキテクチャ
Groove フォームを使用してデータを収集する
アプリケーションから Groove データにアクセスする
Groove Web サービス API を使用する
Groove Web サービスのレジストリ設定
アカウントと ID
ワークスペースとツールのリストを取得する
Groove フォームからデータをフェッチする
フォーム データをデータベースに読み込む
まとめ

現実の世界では、外出していて企業ネットワークにアクセスできない状況で情報を収集しなければならない場面が多々あります。たとえば、消費者リサーチ会社であれば、従業員が街頭へ出て通行人にアンケートを依頼し、回答を手動でデータベースに入力することがあります。このプロセスを自動化するカスタム アプリケーションを作成し、屋外で使用できるようにするためには、ローカル データ ストアの管理やサーバー データベースとの同期などの問題に対応する必要があります。

この記事では、Microsoft® Office Groove™ 2007 の独特な共同作業機能を Microsoft .NET Framework ベースのカスタム アプリケーションに組み込むことにより、これらの問題にどのように対処できるかについて説明します。ここでは、Groove を共同作業によるデータ収集やチーム メンバ間での安全なデータ同期を実現するリッチ クライアントとして利用し、そのデータをカスタム .NET Framework アプリケーションを使って企業データベースに送信します。

Groove が提供する共同作業環境によって、チーム メンバはネットワークに接続していなくても組織の境界を越えてデータを共有することができます。つまり、参加者がすべて同じネットワークに属していなくてもよいため、IT 部門の協力なしで従業員、パートナー、および顧客と共同作業を行う必要がある場合に、Groove が絶好のツールとなります。また、モバイル環境において、オフラインにした後でも共有データにアクセスして更新し、オンラインに戻したときにデータが自動的に同期される必要がある場合にも、Groove は大きな力を発揮します。

Groove 2007 は 2007 Microsoft Office System の Ultimate エディションおよび Enterprise エディションに含まれていますが、単体でも購入できます。microsoft.com/downloads で Groove の無料評価版をインストールし、Groove SDK をダウンロードすることもできます。

Groove でソリューションを構築する

ファイル共有やディスカッションなど、Groove の機能は設定なしですぐに使用できるので、関連するツールを集めたワークスペースを作成し、再利用可能なテンプレートとして保存するだけで、共同作業のカスタム ソリューションを構築することができます。さらなる柔軟性が必要な場合は、Web サービス ベースの API を介して Groove の安全な共同作業機能を使用するカスタム アプリケーションを構築することもできます。

ここで紹介する消費者リサーチ アプリケーションでは、調査員が商店街で集めた調査結果を Tablet PC で入力します。このソリューションは、オフラインでも使用でき、複数の調査員から集めた情報を簡単に集約できることが必要です。実際には、調査員が公共インターネット接続の設備があるカフェに入り、他の調査員との間で自動的かつ安全に情報を同期できれば、申し分ありません。

このようなソリューションを設計するには、まずリサーチ コーディネータが Groove で新しいワークスペースを作成します。どのワークスペースにもそれぞれの要件に応じた複数のツールを組み込むことができます。たとえば、ファイル ツールでは参加者がドキュメントを共有でき、ディスカッション ツールでは参加者どうしで質疑応答をすることができます。

ワークスペースを作成したら、リサーチ コーディネータは各調査員にワークスペースへの参加を求める招待状を送信します。この招待状は各参加者のコンピュータに通知として表示され、通知を受けた参加者はワークスペースをダウンロードすることができます。これで、すべての調査員が共同作業に参加できます。ある参加者が自分のワークスペースでファイルやディスカッション トピックの追加などを行うと、その変更は他の参加者がネットワークに接続したときにそれぞれのワークスペースに自動的かつ安全な方法で反映されます。

Groove 2007 のアーキテクチャ

Groove には革新的なアーキテクチャが採用されており、そこから他に類を見ない機能が生み出されています。まず、Groove ではすべてのデータを中央のサーバーではなく各参加者のコンピュータに保存します。したがって、すべての参加者が 1 つの共有リソースにアクセスするための安全な方法を設計する必要はありません。つまり、調査員が調査会社の従業員であろうと、独立したユーザーであろうと、リサーチ コーディネータは簡単に共有ワークスペースを作成し、すべての調査員に参加を求めることができます。

また、データは、ディスク上のローカル ワークスペースにあるときにも、ネットワーク上で送信されるときにも、常に暗号化された状態にあります。これには、調査員が公共インターネット接続を介して企業ネットワークに接続するだけで変更を同期できるという利点があります。データが自動同期の段階で既に暗号化されているため、すべての参加者が仮想プライベート ネットワーク (VPN) によって企業リソースにアクセスする必要はありません。

Groove は大部分がクライアント ベースですが、サーバー コンポーネントもいくつか含まれています。Groove では、中継サーバーがストア アンド フォワード サービスの役割を果たすため、クライアントでは標準プロトコルによってファイアウォールを越えて通信を行うことができます。また、中継サーバーには、ユーザーがオフラインでワークスペースに加えた変更の一時コピーが暗号化した状態で保存されます。さらに、管理サーバーもあるため、管理者はすべてのユーザーのライセンスとポリシー設定を管理することができます。

スタンドアロン版の Groove では、ユーザーがクライアントのインストール時にアカウントを作成できます。インストールしたクライアントは自動的に Microsoft がホストする中継サーバーを使用します。多数の Groove ユーザーがいる大規模企業では、インフラストラクチャのコンポーネントとして Office Groove Server 2007 を展開すると、ライセンス管理が容易になり、Active Directory® との統合なども可能になります。中小規模の企業では、Office Groove Enterprise Services のサブスクリプションを購入することにより、ホストされている管理ツールにアクセスできるため、社内にインフラストラクチャを展開しなくても一元管理を実現できます。

Groove フォームを使用してデータを収集する

ここで紹介するソリューションでは、調査員が消費者に依頼したアンケートの回答を記入する調査フォームをワークスペースに追加する必要があります。Groove にはフォーム ツールがあるので、カスタム フォームを作成し、データ型やキャプションなどのプロパティを指定することができます。それによって、図 1 のように参加者がワークスペースにデータを追加することができます。

図 1 Groove ワークスペースのフォーム ツール
図 1 Groove ワークスペースのフォーム ツール(画像を縮小するには、ここをクリックします)
図 1 Groove ワークスペースのフォーム ツール
図 1 Groove ワークスペースのフォーム ツール (画像を拡大するには、ここをクリックします)

フォームに JavaScript や VBScript を追加してカスタム ソリューションの構築機能を拡張することもできます。さらに、フォームには DHTML と CSS が使用されているため、UI スタイルや書式設定についても柔軟な操作が可能になっています。既に InfoPath® フォームを導入している場合は、Groove InfoPath のフォーム ツールを使用することによって、Groove の組み込みフォームを使用する代わりに XML ベースの InfoPath フォーム定義をインポートすることができます。

調査員は、カスタム フォームが組み込まれたワークスペースを Tablet PC にダウンロードすることにより、屋外でもオフラインの状態でフォームにレコードを追加することができます。ネットワークに接続すると、オフラインで追加したレコードが他の参加者によって追加されたレコードと安全に同期されます。リサーチ コーディネータは、Groove が表示するポップアップ通知によってワークスペースに未読の新規データがあることを把握できます。

このソリューションは調査員からデータを収集するだけで終わりではありません。リサーチ コーディネータは、個々の調査結果のデ―タを同期した後で、すべてのデータを企業のデータベースに読み込み、分析できる状態にする必要があります。企業のアナリストは、SQL Server™ データベース、Analysis Services キューブ、および Excel® のビジネス インテリジェンス機能を利用して、調査結果を分析し、提言を行います。

アプリケーションから Groove データにアクセスする

Groove ワークスペースからデータを取得し、企業データベースに読み込むには、Groove のフォーム ツールからデータを CSV (カンマ区切り値) などの形式で手動によりエクスポートし、データベースにインポートします。ただし、調査データが届くたびにデータベースに読み込む必要があり、一括処理ができない場合は、この手動プロセスではすぐに行き詰まってしまいます。

この点を解決するには、調査員が入力したデータを Groove Web サービス ベースの API によって読み込む .NET Framework ベースのカスタム アプリケーションを構築します。このアプリケーションをコンピュータにインストールしたリサーチ コーディネータは、公共インターネット接続しか利用できない街頭の調査員とは異なり、図 2 に示すように企業ネットワークに接続されるため、Groove ワークスペースと SQL Server データベースの両方にアクセスできます。

図 2 Groove ワークスペース ユーザーと SQL Server データベース
図 2 Groove ワークスペース ユーザーと SQL Server データベース(画像を縮小するには、ここをクリックします)
図 2 Groove ワークスペース ユーザーと SQL Server データベース
図 2 Groove ワークスペース ユーザーと SQL Server データベース (画像を拡大するには、ここをクリックします)

このアプローチは Groove を Windows® SharePoint® Services (WSS) 3.0 と統合するという方法と似ています。この方法では、チームで 1 つの Groove ワークスペースを使用し、共同でドキュメントを作成して、完成したドキュメントを SharePoint ドキュメント ライブラリに公開することにより、企業内のすべての人が検索やワークフローなどの SharePoint 機能を利用できます。この場合、Groove と SharePoint サイトの両方のアクセス権を持った者が SharePoint ファイル ツールをワークスペースに追加し、そのワークスペースを SharePoint ドキュメント ライブラリに接続します。これによって、ワークスペースのすべての参加者に各ドキュメントのコピーが割り当てられ、ネットワークに接続したユーザーは必要なときに SharePoint ドキュメント ライブラリとの同期をとることができます。

Groove Web サービス API を使用する

ここではクライアント アプリケーションを開発しますが、Groove のプログラミング モデルではマネージ API ではなく Web サービスを使用しています。クライアント アプリケーションは実際には同じコンピュータ上で動作する Web サービスを呼び出すため、このプログラミング方法は少し奇妙なものに思えます。このような Web サービスはコンピュータをネットワークに接続していないときにも使用でき、要求には Groove クライアントが直接応答します。規模の大きいエンタープライズ ソリューションであれば、同様のプログラミング モデルに基づくサーバー ベースの製品である Groove Server Data Bridge を使用することもできますが、このソリューションではクライアント側アプローチを採用します。

図 3 のようなクライアント アプリケーションを作成するには、まずアクセスする各 Groove サービスの Web 参照をプロジェクトに追加します。Groove SDK に各サービスの WSDL ファイルが含まれているので、Web 参照を追加する場合は、対応する .wsdl ファイルのパスを指定し、Web 参照の名前を既定の "Web Reference" からわかりやすい名前に変更し、[Add Reference] (参照の追加) をクリックします。このアプリケーションでは、GrooveAccounts、GrooveForms2、GrooveSpaces、GrooveTools という 4 つのサービスを使用します。

図 3 Groove データを SQL Server データベースに読み込む
図 3 Groove データを SQL Server データベースに読み込む(画像を縮小するには、ここをクリックします)
図 3 Groove データを SQL Server データベースに読み込む
図 3 Groove データを SQL Server データベースに読み込む (画像を拡大するには、ここをクリックします)

Groove Web サービスのレジストリ設定

Web サービスを呼び出すためには、Groove がどのポートを使用するかを把握している必要があります。既定のポートは 9080 ですが、これを現在のユーザーの GrooveLocalHTTPPort レジストリ設定から取得することが必要です。さらに、Web サービスが呼び出し元の認証に使用する要求キーと呼ばれる値をそれぞれの呼び出しに割り当てることも必要です。このキーは図 4 に示すように LocalRequestKey レジストリ設定から取得できます。要求キーは Groove を再起動するたびにランダムに生成されます。したがって、前回の確認以降に再生成されている可能性があるため、呼び出しごとにレジストリから取得する必要があります。

Figure 4 Groove Web サービスのレジストリ設定 (Close Figure 4)
using (RegistryKey grooveSettingsRegKey = Registry.CurrentUser.OpenSubKey(
    “Software\\Microsoft\\Office\\12.0\\Groove”))
{
    // Get the URL for web services including the port
    string grooveHost = string.Format(“http://localhost:{0}”,
       grooveSettingsRegKey.GetValue(“GrooveLocalHTTPPort”, 9080));

    // Get the LocalRequestKey for authentication
    string localRequestKey = (string)grooveSettingsRegKey.OpenSubKey(
        “WebServices”).GetValue(“LocalRequestKey”);
    ...
}

Web サービス認証には、応答キーと呼ばれるレジストリ設定も使用されます。Groove Web サービスの呼び出しに対する応答に含まれるキーを LocalResponseKey レジストリ設定と照合することによって、両方が一致するかどうかを確認できます。

アカウントと ID

Groove ユーザーには Groove を初めて起動したときに作成されるアカウントが割り当てられます。このアカウントは各ユーザーのコンピュータ (または複数のコンピュータ) にファイルとして保存されます。アカウントには暗号化キーや連絡先リストなどの情報が含まれています。各アカウントには最低 1 つの ID があり、各 ID には他の Groove ユーザーと共有される連絡先情報 (電子メール アドレスや電話番号など) が含まれています。必要に応じて 1 つのアカウントに複数の ID を作成できるので、1 つを仕事に使い、もう 1 つを私用で友人とのファイル共有などに使うということも可能です。

コンピュータ上のアカウントと ID のリストを取得するには、GrooveAccounts サービスを使用します。図 5 に示すように要求キーとポートを指定して Web サービス呼び出しを作成した後で、GrooveAccounts サービスの Read2 メソッドを使用することにより、Account2 オブジェクトの配列を返すことができます。各アカウントに ID のコレクションがあるため、ID のリストを表示することによって、図 3 に示すようにユーザーが消費者リサーチ ワークスペースのある ID を選択できるようになります (ほとんどの場合、各ユーザーのアカウントは 1 つで、ID も 1 つだけです)。

Figure 5 全 Groove アカウントのリストを作成する (Close Figure 5)
// Set up the web service call for GrooveAccounts
GrooveAccounts.GrooveAccounts svc = new GrooveAccounts.GrooveAccounts();
svc.GrooveRequestHeaderValue = new GrooveAccounts.GrooveRequestHeader();
svc.GrooveRequestHeaderValue.GrooveRequestKey = localRequestKey;
svc.Url = grooveHost + “/GWS/Groove/2.0/Accounts/”;

// Get a list of all the accounts
GrooveAccounts.Account2[] accounts = svc.Read2();

// Display all identities
foreach (GrooveAccounts.Account2 account in accounts)
    foreach (GrooveAccounts.Identity2 identity in account.Identities)
        Console.WriteLine(account.Name + “, “ + identity.Name);

ワークスペースとツールのリストを取得する

対象となる ID がわかれば、GrooveSpaces サービスの Read メソッドを使用して、そのユーザーのワークスペースのリストを取得できます。ID オブジェクトにはその ID に固有の URI プロパティがあり、図 6 に示すように、ほとんどの Groove Web サービス呼び出しで使用できます。また、そのユーザーのワークスペースの URL が含まれる Spaces プロパティもあります。

Figure 6 全 Groove ワークスペースのリストを作成する (Close Figure 6)
private void LoadSpaces(GrooveAccounts.Identity2 identity)
{
    // Set up the web service call for GrooveSpaces
    GrooveSpaces.GrooveSpaces svc = new GrooveSpaces.GrooveSpaces();
    svc.GrooveRequestHeaderValue =
        new GrooveSpaces.GrooveRequestHeader();
    svc.GrooveRequestHeaderValue.GrooveRequestKey =
        GrooveSettings.RequestKey;
    svc.GrooveRequestHeaderValue.GrooveIdentityURL = identity.URI;
    svc.Url = GrooveSettings.GrooveHost + identity.Spaces;

    // Get the list of spaces
    _spaces = svc.Read(“”);

    // Load the list into a combobox
    comboWorkspaces.DisplayMember = “Name”;
    comboWorkspaces.DataSource = _spaces;
}

Groove ワークスペースを作成したときに説明したとおり、1 つのワークスペースに、ファイル、ディスカッション、フォームなどの複数のツールを追加することができます。ユーザーがクライアント アプリケーションでワークスペースを選択すると、GrooveTools サービスの Read メソッドを使用してマーケット リサーチ データが含まれるフォーム ツールを検索できます。このワークスペースにはフォーム ツールが 1 つしかないため、"Forms" という名前のツールを検索するだけで済みます。ただし、より高度な機能を提供するために、該当する種類のツールをすべて表示し、ユーザーが適切なツールを選択できるようにすることも可能です。

Groove フォームからデータをフェッチする

レジストリから Groove 設定を取得し、アカウント、ID、ワークスペース、およびツールのリストを取得するコードが完成したので、Groove ツールからデータを取得し、分析できるように SQL Server データベースに読み込むという当初の要件に取り組むことができます。実は、データをフェッチすることはいたって簡単です。次のように、RecordQuery オブジェクトを使用してフェッチするレコードを指定したクエリを作成し、そのクエリを GrooveForms2 サービスの QueryRecords メソッドに渡します。

// Query ALL records in the ALL view:
GrooveForms2.RecordQuery query = new GrooveForms2.RecordQuery();
query.ViewURI = “”;
query.UnreadRecordsOnly = false;
query.QueryMetadataOnly = false;
query.IncludeFileAttachmentContent = false;

// Issue the query
GrooveForms2.Forms2RecordDataSet recordDataSet =
    svc.QueryRecords(query);

QueryRecords が返す Forms2RecordDataSet オブジェクトに関しては、なかなか楽しいことが待っています。Forms2RecordDataSet にはフォームのスキーマとデータの両方が XML 形式で含まれているので、このオブジェクトを十分に利用するためには何らかの処理を加える必要があります。フォームのデータを操作するための最も簡単な方法は、Forms2RecordDataSet オブジェクトを使い慣れた .NET DataSet オブジェクトに変換することです。

Groove SDK のドキュメントにはこの問題を扱った章があり、この記事のサンプル ダウンロードにはコードが含まれているので、ここでは概要を説明するにとどめます (しかも、このコードはだれもが一度は書いたことがあり、何度も再利用しているようなものなので)。基本的には、新しい DataSet を作成し、ReadXmlSchema メソッドを使用してテーブルと列定義を作成します。このとき、スキーマの文字列リーダーを作成し、文字列の先頭に適切な XML バージョン宣言を追加します。スキーマが完成したら、ReadXml メソッドを使用して実際のデータを読み込みます。

小さな問題ですが、日時の列に関して検討しなければならないことがあります。Groove は Forms2RecordDataSet 内の日時フィールドを世界時で返すのに対し、.NET DataSet では日時フィールドが既定で現地時刻として保存されます。この問題を解決するには、DataSet 内に作成されるすべての列に対して ReadXmlSchema メソッドによるループ処理を実行し、列の DateTimeMode を Utc に設定してからデータを読み込みます。

フォーム データをデータベースに読み込む

これで、DataSet に対してループ処理を実行し、Groove フォームの新しいレコードのそれぞれに対応する行を SQL Server データベースに追加することができます。ユーザーはこのプロセスを何度も実行する可能性があるため、重複したレコードがデータベースに追加されることを防ぐメカニズムを組み込む必要があります。

Groove フォーム内のレコードには _RecordID と呼ばれるシステム生成のフィールドがあり、各レコードを一意に識別します。レコードをデータベースに読み込むときに、このフィールドを含めると、これによってレコードが既に追加されているかどうかを確認でき、追加されている場合は重複する行を挿入せずに既存のデータベース行を更新することができます。これによって、このプロセスの前回の実行後に変更された調査レコードが SQL Server データベースに反映されます。

まとめ

Groove は、一から作成すると手間がかかる分散チーム向けソリューションを構築するのに絶好の環境を提供します。ファイル共有やフォームなどの設定なしで使用できる機能だけを使用する場合でも、API を基礎としてまったく新しいアプリケーションを開発する場合でも、Groove は強力な新兵器となります。

John C. Hancockは Microsoft のシニア コンサルタントで、ビジネス インテリジェンスと .NET Framework の開発を専門としています。『SQL Server 2005 を使った実践的ビジネス インテリジェンス』 (Addison-Wesley Professional、2006 年) の共著者です。www.johnchancock.net で著作の情報およびブログを公開しています。


編集者のメモ: 夏時間

編集者のメモ
夏時間

新しい年が始まってから、 しばらく注目されていた問題への関心がにわかに高まりました。2007 年に、夏時間の開始が数週間繰り上げられました。

どうしてこうなったのでしょうか。DST (夏時間) は、まったくわけのわからない慣行です。私たちは 90 年間にわたって、数か月の間、日の出と日の入りの時刻を 1 時間ずつ遅らせて、日照時間を有効に利用してきました。科学的根拠は私たちの理解を超えていますが、宇宙に入念に設置された一連のレバーが関与しているように思えます。

科学技術の進歩に伴い、米国では夏時間を早めに開始できるようになりました。2005 年エネルギー政策法により DST が変更になり、適用期間が 3 月中旬から 11 月の第 1 週までになりました。これは 2 つの大きな影響をもたらしました。第 1 に、新しい開始日が迫ってきた 2 月下旬に、歯ぎしりをして悔しがりました。第 2 に、ハロウィーンに日の出ている時間が 1 時間延びました。

マスコミが急に、夏時間の話題を取り上げるようになりました。数日間、この話題は主要な関心事となりました。「2000 年問題 (Y2K) と同じことが起こるのだろうか。」「午前 2 時に、すべてのコンピュータが故障し、空から飛行機が落下するのだろうか。」「調査すべき重要なことが他にないだろうか。」

当然ながら、DST と Y2K には、1 つの大きな違いがあります。1999 年には、ほとんどすべての種類のコンピューティング システムが何らかの影響を受けました。金融システム、旅行の問題、電力の供給など、ありとあらゆるものについて心配しました。2000 年の最悪の事態を想定した、うんざりするような TV 映画が制作されました。全国のキャンプ用品店では、ボトル入りミネラル ウォーター、防寒用の毛布、念のために、ゾンビ対策用のお守りなどを盛り込んだサバイバル キットが販売されました。

DST の変更によるマイナス面は、これとは程遠いものでした。3 週間、予定表に 1 時間のずれが生じて、いくつかの会議に出席できなかった可能性はありますが、地元の事務用品店では、ペンやカレンダーが入った特別な DST サバイバル キットなどは販売されませんでした。ほとんどのシステムは、Windows Update を通じて暗黙的に修正プログラムが適用され、それでおしまいです。

ここには、一般的なプログラミング開発との類似点があることにお気付きになったことでしょう。7 年前の Y2K の世界的なパニックには共感できるものがあります。ずっと前に誰かが作成したコードには、常に何らかの問題が存在し、いざというときには他の人がその問題の解決を任されることになります。一般に、これらのバグは実際に及ぼす悪影響よりも大きなパニックになることが多く、最優先の課題となります。

DST の変更は、大きなパニックというよりは、むしろ毎日のプログミングの中で発生する問題に類似しています。DST は、世界共通の概念ではありません。各国 (および米国内の各地域) は、異なった DST の規則に従います。多くのプログラムには、独自の DST コードがそもそも含まれていません。独自のコードを作成するのではなく、基本オペレーティング システムに依存しています。実際、日照時間のためにプログラムのコードを変更することは、"将来のバージョンで保証されていない" 動作について計画を立てるのとほぼ同じことです。

DST の大きな影響の 1 つは、日中の時間が 1 時間長くなって、MSDN Magazine のブログを毎晩読めることです。blogs.msdn.com/msdnmagazine にアクセスすると、MSDN Magazine の最新のニュースを読み、編集者とチャットし、今月の「編集者のメモ」について討論できます。今後は毎月これを実施しますので、ぜひお立ち寄りになり、新しい DST 政策が仕事に与えた影響についてお知らせください。 £J.T.

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、David Callahan、Dave Detlefs、Matt Gibbs、Mike Harder、Bertrand Le Roy、Lawrence Liu、Val Menn、Kevin Moore、Chris Norman、Hugh Pyle、Gaurav Seth、Matt Shannon、Jim Truher、Molly Yen です。


ツールボックス: コードの検証、SQL クエリ、その他

ツールボックス
コードの検証、SQL クエリ、その他
Scott Mitchell

コンテンツ
アプリケーションのアーキテクチャを分析およびグラフィカルに検証する
SQL クエリ エディタをアップグレードする
スクリーンショットをすばやく簡単に撮る
.NET アプリケーションを開発する

アプリケーションのアーキテクチャを分析およびグラフィカルに検証する

大規模な Microsoft .NET Framework アプリケーションには、数百または数千のクラスが含まれていることがあります。これらのクラスは、アプリケーションのアーキテクチャの個々の層を構成するさまざまなアセンブリに編成されています。ほとんどの開発者はアーキテクチャの特定のセクションのみを作業しますが、上級開発者とシステム アーキテクトは、一般にアーキテクチャのさまざまな部分とその相互接続に影響するコードおよび設計の意思決定を担当します。これらのコード変更とアーキテクチャ上の意思決定のために、上級開発者およびシステム アーキテクトはアーキテクチャの構造と相互依存関係をよく理解しておくことが重要です。通常、開発者はソース コードを注意深く調べますが、この処理はエラーを起こしがちで時間がかかります。

NDepend はさまざまなアセンブリ間の依存関係を示すマトリックスを提供する (画像を縮小するには、ここをクリックします)

NDepend はさまざまなアセンブリ間の依存関係を示すマトリックスを提供する (画像を拡大するには、ここをクリックします)

さいわい、.NET Framework アプリケーションのアーキテクチャをより簡単に検証する方法があります。SMACCHIA.COM が提供する NDepend Version 2.0 は、アプリケーションのアーキテクチャのさまざまな部分のメトリックス、複雑性、および依存関係を検証および分析するのに役立つツールです。アプリケーションのアセンブリとソース コードが選択され、分析された後で、NDepend は多数のメトリックスと情報を検証するための豊富なユーザー インターフェイスを提供します。

インターフェイスの上部には、アーキテクチャのアセンブリのツリーマップがあります (ツリーマップは、各要素のサイズがその "重量" によって決まるグラフです)。ツリーマップは、コード行数やメソッド数などのさまざまなメトリックスを示します。これにより、アーキテクチャのコンポーネントの相対的な複雑性をすばやく判断できます。

NDepend は、さまざまなアセンブリ間の依存関係を示すマトリックスを提供します。アセンブリのクラスにドリル ダウンし、そこからクラスのメソッドにドリル ダウンできます。右クリックして、選択したアセンブリ、クラス、またはメソッドを直接的または間接的に呼び出す、または使用する名前空間、クラス、またはメソッドを確認できます。マトリックスは、循環依存関係を特定するのにも役立ちます。

NDepend の最も優れた機能は、クエリ言語 CQL です。これは SQL と構文が似ています。CQL ステートメントは、アドホック レポートに使用できます。次の CQL クエリは、最も多くの型を持つ上位 10 個の名前空間を返します。

SELECT TOP 10 NAMESPACES ORDER BY NbTypes DESC

CQL クエリは、コード品質の監査にも役立ちます。このクエリは、ソース コードが 30 行を超えるメソッド (これは、リファクタが必要な可能性の高い複雑なメソッドを示します) がある場合に警告を生成します。

WARN IF Count > 0 IN SELECT TOP 10 METHODS WHERE NbLinesOfCode > 30

UI だけでなく、NDepend はコマンド ラインを通じてアクセスできるため、自動化されたビルド プロセスに簡単に統合できます。

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

www.NDepend.com

SQL クエリ エディタをアップグレードする

Enterprise Manager と SQL Server Management Studio はデータベース関連タスクを実行するための使いやすいグラフィカル環境を提供しますが、そのネイティブ クエリ エディタは、最新機能が不足した単純なテキスト エディタであるという印象を多くの人に与えます。たとえば、IntelliSense® や統合ソース管理サポートはありません。Visual Studio® には、CREATE スクリプトやよく使用されるその他の T-SQL をグループ化するためのデータベース プロジェクトが IDE 内に用意されていますが、これらはクエリを作成、テスト、および実行するインターフェイスというよりも、データベース関連のスクリプトのリポジトリの役割を果たします。

一方、開発者が現在当然あるべきと考える機能を提供するサードパーティ製の SQL クエリ エディタが多数あります。このような製品の 1 つに、ApexSQL の ApexSQL Edit Version 2005.03 があります。ApexSQL Edit には時間を節約できる多くの機能が備えられており、最も優れた機能は IntelliSense によく似た機能を提供する Object MemberList です。"SELECT * FROM..." のようなクエリを入力すると、データベースのテーブルを一覧表示するドロップダウン リストがすぐに表示されます。この機能は、列、変数、パラメータ、プロシージャなどにも拡張されています。MemberLists を使用したクエリの作成をいったん経験すると、MemberLists なしにエディタを使用しなければならないときなど、まるで水面下に潜っているような気持ちになります。この機能だけで無数のキーストロークとタイプミスを排除できるため、ApexSQL Edit は開発者のツールボックスに入れるのに興味を引くツールとなっています。

クエリの作成、テスト、または実行 (画像を縮小するには、ここをクリックします)

クエリの作成、テスト、または実行 (画像を拡大するには、ここをクリックします)

時間を節約する別の機能として、Object Explorer があります。これは、選択したテーブル、ストアド プロシージャ、ビュー、UDF、またはその他のデータベース オブジェクトに関する詳細を一覧表示します。この情報には、オブジェクトのデータ、オブジェクトを削除して再作成するための T-SQL ステートメント、およびその権限と依存関係が含まれます。オブジェクトに関するコメントを追加できる [Notes] (コメント) タブもあります (これらのコメントは、最初にデータベースに追加する必要のあるデータベース テーブルに格納されます)。データベース テーブルについて、Object Explorer はテーブルの列とインデックスも一覧表示します。Object Explorer によって、現在クエリを作成しているテーブル、ビュー、またはその他のオブジェクトに関する情報をすばやく簡単に検索できるようになります。

ApexSQL Edit には、SQL Server Reporting Service 統合、組み込みの単体テスト サポート、強力な検索および置換機能、カスタマイズされた SQL 書式設定なども用意されています。Enterprise Manager または Management Studio に付属のクエリ エディタを使用してクエリの作成、テスト、または実行に長時間を費やしている方には、クエリ エディタをアップグレードして ApexSQL Edit を試すことをお勧めします。

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

www.apexsql.com

スクリーンショットをすばやく簡単に撮る

開発者がスクリーンショットを必要とするシナリオは多数あります。スクリーンショットはドキュメントにおいて優れた視覚的効果を提供し、テストとデバッグの際に異常やエラー メッセージをキャプチャするのに役立ちます。Windows® では、Print Screen キーで現在のウィンドウをクリップボードにコピーできます。スクリーンショットは、ドキュメントに直接貼り付けたり、イメージ エディタに貼り付けて数多くあるイメージ ファイル形式のいずれかで保存したりできます。ただし、多くのスクリーンショットを撮り、それらをファイルに保存する場合、Print Screen キーを押してスクリーンショットをイメージ エディタに貼り付け、必要に応じてイメージを切り取ってからディスクに保存するというワークフローは時間がかかり、反復的な作業となります。

この単調な作業を排除して効率を上げることのできるさまざまなフリーウェアやサードパーティ製品があります。このような製品の 1 つに、Brian Scott の Cropper Version 1.9 があります。これは C# で記述されており、ソース コードを無料で入手できます。Cropper では、ウィンドウ全体または定義済み領域をキャプチャできます。ウィンドウ全体をキャプチャするには、単純に Print Screen キーを押します。タスク トレイの Cropper アイコンをダブルクリックすると、位置と寸法でキャプチャ領域を指定できるフローティング フレームが表示されます。

Cropper で、スクリーン キャプチャをクリップボードに保存したり、プリンタに直接送信したり、BMP、PNG、または JPG ファイルとしてディスクに自動的に保存したりできます。サムネイル機能がアクティブになっている場合、Cropper はフルサイズのスクリーンショットと指定したサイズのサムネイルの両方を保存します。四角形以外のウィンドウをキャプチャする場合は、それらの透明な領域 (カーブした角の後ろにある背景など) を特定の色で塗りつぶすように Cropper を構成できます。

価格: 無料 (ソース コードを含む)

blogs.geekdojo.net/brian/articles/Cropper.aspx

.NET アプリケーションを開発する

最も人気のある市販のコンピュータ書籍では、テクノロジの非常に特定的なサブセットに焦点を当てているか、テクノロジ全体の広範な概要について記述しています。Christian Gross の『.NET のコーディング方法』(2006 年、Apress 発行) は、どちらのカテゴリにも該当しません。そのタイトルは .NET アプリケーションの作成に関する壮大な学術書であることを暗に意味していますが、そうでないことはページ数がわずか 232 ページであることからわかります。.NET の特定のサブセットに焦点を当てているのでも、全般的な概要について記述しているのでもありません。テスト駆動型開発 (TDD)、低レベルの .NET ランタイムおよびフレームワーク情報、テキスト処理のヒント、C# コーディングの秘訣という 4 つのトピックについてそれぞれ 30～50 ページを割いて説明しています。

この本は、TDD セクションから開始します。ここには無駄な言葉は書かれていません。TDD の原理の概要、NUnit の概要、およびテストの作成に関する説明のみ記載されています。この後、章の残りの部分では、TDD を使用して開発していない、継承したプロジェクトへの TDD の原理の適用など、一般的な TDD の課題に対するソリューションが検証されています。第 2 章「.NET ランタイムおよび Framework 関連のソリューション」では、低水準のトピックに関する説明、ヒント、および秘訣が記載されています。たとえば、値型と参照型の違い、デリゲートの利点を示すコード例、Object クラスの Equals および GetHashCode メソッドの概要とそのオーバーライドが必要なケースが説明されています。

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

第 3 章では、文字列の解析と検索に関する説明およびコードなど、便利なテキスト関連ソリューションが記載されています。この本の最も長い章である「C# コーディング ソリューション」では、継承、nullable 型、オーバーロード関数、インターフェイス、ファクトリ パターンなど、多くの C# 機能とパターンを考察しています。

232 ページの『.NET のコーディング方法』では、どのトピックも詳細には説明されていないため、これらのトピックを詳細に検証したい読者は対象としていません。しかしこの本では、各トピックをかじるのに十分な情報量が提供されています。『.NET のコーディング方法』には有用なヒントが多数記載されており、より高度な .NET 概念のいくつかの検証を開始するのに適しているため、ほとんどの中間 .NET 開発者にはこの書籍が役に立ちます。

価格: 29.99 ドル

www.apress.com

ご意見やご質問は、

Scott toolsmm@microsoft.com.

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


CLR 徹底解剖: 再利用可能な 9 つの並列データ構造とアルゴリズム

CLR 徹底解剖
再利用可能な 9 つの並列データ構造とアルゴリズム
Joe Duffy

コードのダウンロード : : CLRInsideOut2007_05.exe (156KB)
コンテンツ
カウントダウン ラッチ
再利用可能なスピン待ち
バリア
キューのブロッキング
制限されたバッファ
シン イベント
ロックされない LIFO スタック
ループ タイル
並列リダクション
まとめ

このコラムは、共通言語ランタイム (CLR) 機能のしくみよりは、むしろ自由に使用できる機能の効率的な使用方法に焦点を当てています。当然ながら、適切なデータ構造とアルゴリズムの選択は、プログラマが行う最も一般的かつ重要な意志決定の 1 つです。間違った選択は、成功するか失敗するかを左右し、ほとんどの場合は、高パフォーマンスと劣悪なパフォーマンスの差異が生じます。多くの場合、並列プログラミングはパフォーマンスの向上を目的としており、通常ではシリアル プログラミングより困難であることを考えると、成功するためにはこの選択は非常に重要です。

このコラムでは、再利用可能な 9 つのデータ構造とアルゴリズムについて説明します。これらは多くの並列プログラムに共通のものであり、独自の .NET ソフトウェアに簡単に適合させることができます。すべての例には、コードが付属しています。これらは完全に強化、テスト、およびチューニングされたコードではありませんが、正常に実行できる状態にあります。リストはすべてを網羅しているわけではありませんが、いくつかの一般的なパターンを表しています。多くの例は、他の例に基づいて構築されていることがおわかりになるでしょう。

最初に説明しておかなければならないことがあります。Microsoft® .NET Framework には、同時実行に関するいくつかの既存プリミティブがあります。このコラムで独自のプリミティブの構築方法を説明しますが、既存のプリミティブは、ほとんどの状況に十分に適しています。ここでは単に、代替のプリミティブも検討してみる価値があることを示したいと思います。また、これらの技法の動作を確認することで、並列プログラミングに関する全般的な知識を高めることができます。ただし、このコラムの読者は既存プリミティブについての基礎知識があることを前提にしています。詳細については、2005 年 8 月発行の MSDN® Magazine に掲載された Vance Morrison の「マルチスレッド アプリケーションについて開発者が知っておかなければならないこと」を参照してください。

ではその技法を見てみましょう。

カウントダウン ラッチ

セマフォは、さまざまな理由から、並列プログラミングでよく知られているデータ構造の 1 つです。それはセマフォが、コンピュータ科学において、1960 年代のオペレーティング システム設計までさかのぼる長い歴史があるという理由からではありません。セマフォは、カウント フィールドを持つデータ構造であり、put および take (それぞれ P および V と呼ばれることが多い) という 2 つの操作をサポートします。put 操作はセマフォのカウントを 1 ずつインクリメントし、take は 1 ずつデクリメントします。セマフォのカウントが 0 になると、後続の take 操作は、並列に実行される put によってカウントが 0 以外になるまでブロック (待機) します。put と take は、どちらもアトミックで同時実行に適した操作であり、これらの操作は互いにシリアル化されます。Windows® には優れたカーネルがあり、Win32® はセマフォ オブジェクトをサポートします (CreateSemaphore と関連 API を参照してください)。これらは、System.Threading.Semaphore クラスを使用して .NET Framework で公開されています。Mutex と Monitor がサポートするクリティカル領域は、多くの場合、0 と 1 を切り替えるカウントを持つ特殊なセマフォ (つまり、バイナリ セマフォ) として位置付けられています。

"リバース セマフォ" のようなものがあれば、非常に便利な場合があります。ときには、カウントが 0 になるまで待機するようなデータ構造が必要な場合もあります。単一の "マスタ" スレッドが n 個の "従属" スレッドの実行を制御し、全スレッドの処理が完了するまで待機するような fork と join の並列実行は、データを並列に処理するプログラミングでよく使われます。この場合、リバース セマフォが手元にあると非常に便利です。ほとんどの場合、スレッドをアクティブ化してカウントを変更するのは避けたいものです。そのため、カウントが減っていく構造のことをカウントダウン "ラッチ" と呼ぶことにします。ラッチは、一度シグナルされた状態に設定されると、その状態を維持します (これはラッチによく関連付けられるプロパティです)。残念ながら、このようなデータ構造は、Windows にも .NET Framework にも存在しません。ただし、さいわいにも、このデータ構造は簡単に作成できます。

カウントダウン ラッチを作成するには、カウンタを n に初期化し、たとえば、デクリメント操作をロックで囲むか、Interlocked.Decrement を呼び出すことによって、従属タスクがそれぞれの処理を完了したときに各タスクがアトミックに 1 ずつデクリメントするように構成します。こうすると、take 操作を使用せずにスレッドをデクリメントでき、カウンタが 0 になるのを待機できます。スレッドがアクティブになると、ラッチに n 個のシグナルが登録されていることを認識します。この条件に対する while (count != 0) などのスピン (ループ) 処理を実行するのではなく、スレッドを待機させてブロックすることが通常では得策です。ただし、この場合は、イベントを使用する必要があります。図 1 は、簡単な CountdownLatch タイプの例です。

Figure 1 CountdownLatch (Close Figure 1)
public class CountdownLatch {
    private int m_remain;
    private EventWaitHandle m_event;

    public CountdownLatch(int count) {
        m_remain = count;
        m_event = new ManualResetEvent(false);
    }

    public void Signal() {
        // The last thread to signal also sets the event.
        if (Interlocked.Decrement(ref m_remain) == 0)
            m_event.Set();
    }

    public void Wait() {
        m_event.WaitOne();
    }
}

これは非常に簡単ですが、正しく理解するにはいくらか注意が必要です。このデータ構造の使用方法については、後でいくつかの例を示します。ここに示された基本実装には、さまざまな改善の余地が残されています。たとえば、イベントをコンストラクタではなく、ゆっくりと割り当て、いくらかスピン待ちをしてからイベントに対して WaitOne を起動することもできます (このコラムの後半で示す ThinEvent の例のように、ブロックを避けるためにスピン処理で十分に対応できる場合)。また、リセット機能を追加したり、不要になった内部イベント オブジェクトをクローズできるように Dispose メソッドを提供したりすることも可能です。これらはすべて読者のために残された課題です。

再利用可能なスピン待ち

通常は、ビジー ウェイトよりもブロックの方が好まれますが、状況によっては、しばらくスピンした後に実際の待機に戻りたい場合もあります。この処理が有効であるかどうかは微妙であり、まったく無駄な処理に見えるため、多くのユーザーは最初はスピン待ちを避けます。Windows では、コンテキスト スイッチ (スレッドがカーネル イベントを待機しているときに発生します) のコストが数千ものサイクルに及び (これを c と呼びます)、スレッドが待機している条件が 2c サイクル (待機のための 1c と、起動のための 1c) より少ない時間内に発生する場合は、スピンすることによって、待機から生じるオーバーヘッドと遅延が削減され、アルゴリズムの総スループットとスケーラビリティが向上します。

スピン待ちの使用が決定したら、慎重に事を運ぶ必要があります。さまざまな問題に直面する可能性があります。Intel Hyper スレッド マシンでは、Thread.SpinWait をスピン ループ内で呼び出し、他のハードウェア スレッドに対するハードウェアの可用性を向上する必要があります。Thread.Sleep では、引数を 0 ではなく 1 に設定して、優先順位の逆転を防ぐ必要があります。わずかなバックオフを使用してランダム化を導入します。局所性を改善します (呼び出し側が共有状態を継続的に再読み取りしている場合)。また、可能な限りライブロックを防止し、当然ながら、シングル CPU マシンでは常に yield します (このような環境でのスピン処理はまったくの無駄であるため)。

SpinWait クラスは、割り当てのコストを削減するため、値型として定義されます (図 2 を参照)。これで、このアルゴリズムを使用して、前に説明した CountdownLatch アルゴリズムのブロックを回避できます。

Figure 2 SpinWait (Close Figure 2)
public struct SpinWait {
    private int m_count;
    private static readonly bool s_isSingleProc =
        (Environment.ProcessorCount == 1);
    private const int s_yieldFrequency = 4000;
    private const int s_yieldOneFrequency = 3*s_yieldFrequency;

    public int Spin() {
        int oldCount = m_count;

        // On a single-CPU machine, we ensure our counter is always
        // a multiple of ‘s_yieldFrequency’, so we yield every time.
        // Else, we just increment by one.
        m_count += (s_isSingleProc ? s_yieldFrequency : 1);

        // If not a multiple of ‘s_yieldFrequency’ spin (w/ backoff).
        int countModFrequency = m_count % s_yieldFrequency;
        if (countModFrequency > 0)
            Thread.SpinWait((int)(1 + (countModFrequency * 0.05f)));
        else
            Thread.Sleep(m_count <= s_yieldOneFrequency ? 0 : 1);

        return oldCount;
    }

    private void Yield() {
        Thread.Sleep(m_count < s_yieldOneFrequency ? 0 : 1);
    }
}

private const int s_spinCount = 4000;
public void Wait() {
    SpinWait s = new SpinWait();
    while (m_remain > 0) {
        if (s.Spin() >= s_spinCount) m_event.WaitOne();
    }
}

明らかに、yield の頻度とスピン カウントの選択は任意です。Win32 のクリティカル セクションのスピン カウントと同様に、これらの数値は、テストと経験に基づいて選択する必要があり、適切な数値はシステムごとに異なります。たとえば、MSDN ドキュメントでは、Microsoft Media Center と Windows カーネル チームからの経験に基づいてクリティカル セクションのスピン カウントとして 4,000 を推奨していますが、効果はシステムごとに異なります。完璧な数値は、その時点でイベントを待機しているスレッドの数、イベントが発生する頻度など、さまざまな要素に応じて異なります。ほとんどの場合は、ラッチの例で示したように、明示的な yield ではなく、イベントを待機する方法を選択することができます。

カウントの動的な調整を選択することも可能です。たとえば、中間的なスピン数から開始し、スピンが失敗するたびに、カウントを増加していきます。カウントが、あらかじめ決定しておいた最大値に達したら、スピン処理を停止し、直ちに WaitOne を発行します。このロジックは、次のとおりです。事前に決定しておいた最大サイクル数まで使い尽くし、それ以上は追加しません。この最大値がコンテキスト スイッチの防止に不十分であることが判明した場合は、直ちにコンテキスト スイッチを実行した方が総合的にコストが低くなります。やがて、スピン カウントが安定した値になることを期待するでしょう。

バリア

バリアは、ランデブー ポイントとも呼ばれ、別の "マスタ" スレッドによってすべてをまとめる必要なしに、スレッドが相互に調整することを可能にする並列処理のプリミティブです。各スレッドは、バリアに到達すると、アトミックに、シグナルを送信して待機します。すべての n がこれに到達したときにのみ、すべてのスレッドが次の処理に進むことができます。これは協調アルゴリズムで使用でき、科学、数学、およびグラフィックスの分野でよく使われます。バリアの使用は、さまざまな計算処理に適しています。実際、CLR のガベージ コレクタでも使用しています。バリアは、より大きな計算処理を、複数の小さな協調フェーズに分解します。次に例を示します。

const int P = ...;
Barrier barrier = new Barrier(P);
Data[] partitions = new Data[P];

// Running on ‘P’ separate threads in parallel:
public void Body(int myIndex) {
    FillMyPartition(partitions[myIndex]);
    barrier.Await();
    ReadOtherPartition(partitions[P – myIndex - 1]);
    barrier.Await();
    // ...
}

この状況にはカウントダウン ラッチの使用が可能であることにすぐにお気付きになると思います。Await を呼び出す代わりに、各スレッドが Signal に続けて Wait を呼び出すことが可能です。すべてのスレッドがバリアに到達したら、すべてのスレッドが解放されます。しかし、問題点があります。前に示したラッチは、同じオブジェクトを何度も再利用することをサポートしません。この便利なプロパティは、すべてのバリアがサポートする必要があります。実際、上の例にはこれが必要です。別々のバリア オブジェクトを使用して、これを達成することも可能ですが、それでは無駄が多すぎます。すべてのスレッドは、一度に 1 つのフェーズにしか存在しないため、複数のバリア オブジェクトは必要ではありません。

この問題を解決するには、まず、基本的な同じカウントダウン ラッチ アルゴリズムを使用して、カウンタのデクリメント、イベントのシグナル、待機などを処理し、これを拡張して再利用をサポートします。これを実行するには、いわゆるセンス リバース バリアを使用する必要があります。このバリアでは、"偶数" フェーズと "奇数" フェーズの切り替えが必要です。代替フェーズに対して別々のイベントを使用します。この種のバリア データ構造の実装例を図 3 に示します。

Figure 3 バリア (Close Figure 3)
using System;
using System.Threading;

public class Barrier {
    private volatile int m_count;
    private int m_originalCount;
    private EventWaitHandle m_oddEvent;
    private EventWaitHandle m_evenEvent;
    private volatile bool m_sense = false; // false==even, true==odd.

    public Barrier(int count) {
        m_count = count;
        m_originalCount = count;
        m_oddEvent = new ManualResetEvent(false);
        m_evenEvent = new ManualResetEvent(false);
    }

    public void Await() {
        bool sense = m_sense;

        // The last thread to signal also sets the event.
        if (m_count == 1 || Interlocked.Decrement(ref m_count) == 0) {
            m_count = m_originalCount;
            m_sense = !sense; // Reverse the sense.
            if (sense == true) { // odd
                m_evenEvent.Reset();
                m_oddEvent.Set();
             } else { // even
                m_oddEvent.Reset();
                m_evenEvent.Set();

            }
        } else {
            if (sense == true) m_oddEvent.WaitOne();
            else               m_evenEvent.WaitOne();
        }

    }
}

なぜ 2 つのイベントが必要なのか、それを理解するのはたいへん困難です。1 つのアプローチは、Await で Set の直後に Reset を実行する方法ですが、これは危険であり、2 つの理由からデッドロックが発生します。第一に、スレッドで立て続けに Set と Reset を呼び出した場合、別のスレッドでは、m_count をデクリメントしていても、まだイベントに対する WaitOne の呼び出しに到達していない可能性があります。第二に、待機中のスレッドが WaiOne の呼び出しに到達した場合でも、CLR で常時使用されるような警告可能な待機は、非同期のプロシージャ呼び出し (APC) を実行できるように、待ち行列から待機中のスレッドを一時的に削除して、待機を中断する可能性があります。待機中のスレッドは、設定された状態のイベントを確認できなくなります。どちらの場合も、イベントの見逃しおよびデッドロックが生じる可能性があります。これを回避するには、偶数フェーズと奇数フェーズで別々のイベントを使用します。

CountdownLatch の場合と同様に、思い切ってバリアにスピン処理を追加することもできます。ただし、これを試みると、問題が発生します。通常、スピン中のスレッドは、m_count が 0 になるまでスピンし続けます。ただし、上記の実装では、最後のスレッドが m_count を m_originalCount にリセットするまで、m_count が実際に 0 になることはありません。スピンに対する未熟なアプローチは、1 つ以上のスレッドが永久にスピンし、次のフェーズで他のすべてのスレッドが永久にブロックされる事態を招く可能性があります。解決策は簡単です。図 4 に示すように、スピンしてセンスが変更されるのを待ちます。

Figure 4 待機 (Close Figure 4)
public void Await() {
    bool sense = m_sense;

    // The last thread to signal also sets the event.
    if (m_count == 1 || Interlocked.Decrement(ref m_count) == 0) {
        m_count = m_originalCount;
        m_sense = !sense; // Reverse the sense.
        if (sense == true) { // odd
            m_evenEvent.Set();
            m_oddEvent.Reset();
        } else { // even
            m_oddEvent.Set();
            m_evenEvent.Reset();
        }
    } else {
        SpinWait s = new SpinWait();
        while (sense == m_sense) {
            if (s.Spin() >= s_spinCount) {
                if (sense == true) m_oddEvent.WaitOne();
                else               m_evenEvent.WaitOne();
            }
        }
    }
}

後続のフェーズが完了する前に、前のフェーズからのすべてのスレッドが Await を終了する必要があるため、すべてのスレッドがセンスの変更に気付くか、イベントを待機することになり、この方法でアクティブ化されることが保証されます。

キューのブロッキング

共有メモリ アーキテクチャでは、2 つ以上のタスク間の唯一の同期ポイントは、多くの場合、中央の共有コレクション データ構造です。1 つ以上のタスクが、1 つ以上の他のタスクが利用するための "ワーク" を生成する役割を担うことがよくあります。これをプロデューサ/コンシューマ関係といいます。通常、このようなデータ構造の単純な同期は、Monitor または ReaderWriterLock を使用すると、簡単に目的を達成できます。しかし、バッファが空になったときのタスク間の調整は、いくらか困難な作業です。通常、この問題はキューのブロッキングによって解決されます。

実際には、わずかに異なる数種のキュー ブロッキングがあります。キューが空の場合にのみコンシューマがブロックするという簡単なものから、各プロデューサが正確に 1 つのコンシューマと "ペア" になる、つまり、プロデューサは、コンシューマが到着してキューに登録されたアイテムを処理するまでブロックされ、同様に、コンシューマは、プロデューサがアイテムを提供するまでブロックされるという複雑なものまであります。FIFO (先入れ先出し) の順番は一般的ですが、必ずしも必要ではありません。後で説明するように、バッファを制限することも可能です。空のときにブロックするという簡単な動作は、後で説明する制限されたバッファに含まれているため、ここでは、ペアの動作についてのみ説明します。

これを実装するには、最上部に同期を使用して簡単な Queue<T> をラップします。これはどのような種類の同期でしょうか。スレッドが要素をキューに登録するときは、コンシューマが要素をキューから取り出すまで待機し、その後で戻ります。スレッドがキューから要素を取り出すとき、バッファが空であることを発見した場合は、新しい要素の到着を待つ必要があります。当然ながら、キューからアイテムを取り出した後、コンシューマはアイテムを受け取ったことをプロデューサにシグナルする必要があります (図 5 を参照)。

Figure 5 キューのブロッキング (Close Figure 5)
class Cell<T> {
    internal T m_obj;
    internal Cell(T obj) { m_obj = obj; }
}

public class BlockingQueue<T> {
    private Queue<Cell<T>> m_queue = new Queue<Cell<T>>();
    public void Enqueue(T obj) {
        Cell<T> c = new Cell<T>(obj);
        lock (m_queue) {
            m_queue.Enqueue(c);
            Monitor.Pulse(m_queue);
            Monitor.Wait(m_queue);
        }
    }
    public T Dequeue() {
        Cell<T> c;
        lock (m_queue) {
            while (m_queue.Count == 0)
                Monitor.Wait(m_queue);
            c = m_queue.Dequeue();
            Monitor.Pulse(m_queue);
        }
        return c.m_obj;
    }
}

Enque メソッドでは、Pulse の後に Wait を呼び出し、同様に、Dequeue メソッドでは、Wait の後に Pulse を呼び出している点に注意してください。モニタの実装方法により (モニタが解放される前に、内部イベントが設定されます)、一部のスレッド スケジュールのピンポンが生じる可能性があります。Win32 イベントを使用して、きめ細かい通知メカニズムの作成を検討することもできます。ただし、本格的な Win32 イベントをこのような形式で使用すると、特に、割り当てコストおよびカーネルの遷移で、かなりのオーバーヘッドが生じる可能性があります。そのため、代替策を検討する必要があります。CLR の ReaderWriterLock がするように、プールすることも可能ですし、ThinEvent タイプ (後述) のように、ゆっくりと割り当てることも可能です。この実装にも、新しい要素ごとにオブジェクトを割り当てるという欠点があります。代替アプローチとして、これらのオブジェクトをプールすることも可能ですが、この場合も複雑な作業が必要となります。

制限されたバッファ

ある種のキューで、リソースの消費問題が発生する可能性があります。プロデューサ タスクがアイテムを極めて高速に作成し、コンシューマがそれを処理しきれない場合、メモリ使用量が無限に増加する可能性があります。

これを理解するために、単一のプロデューサがキュー内に毎秒 50 アイテムを登録し、コンシューマの利用速度が毎秒 10 アイテムのみである場合を想像してください。第一に、システムはアンバランスになり、一対一のプロデューサとコンシューマの構成では釣り合いがとれません。わずか 1 分後には、2,400 個のアイテムがバッファ内に堆積します。これらのアイテムのメモリ使用量が、たとえば、1 個あたり 10 KB であれば、バッファだけで 24 MB のメモリを消費します。1 時間後には、1 GB を超えるでしょう。プロデューサ スレッドとコンシューマ スレッドの数を調整することは、解決策の 1 つです。この場合、プロデューサとコンシューマの比率は 1 対 5 になります。ただし、多くの場合、到着の速度は気まぐれであり、一時的にアンバランスになって重大な問題に発展する可能性があります。単純な固定された比率ではこれを解決できません。

プログラムが長時間実行され、稼働時間の良さが期待されているサーバーでは、無制限のメモリ使用によって、大混乱を引き起こす可能性があり、サーバー プロセスの定期的なリサイクルが必要になる場合があります。

制限されたバッファを使用すると、プロデューサがブロックを強要される前に、バッファの到達可能なサイズに制限を設けることができます。プロデューサをブロックすると、コンシューマは "追いつく" 機会を与えられ (コンシューマ スレッドがスケジュールのタイムスライスを受け取ることが可能となるため)、メモリの堆積問題もなくなります。私たちのアプローチは、待ち条件とイベント通知条件をそれぞれ 2 つずつ追加して、単純に Queue<T> をラップすることです。キューが一杯になると、プロデューサは空きができるまで待機し、キューが空の場合、コンシューマは空でなくなるまで待機します。プロデューサは、アイテムを提供したときに待機中のコンシューマにシグナルを送り、コンシューマは、アイテムを取り出したときにプロデューサにシグナルを送ります。これを 図 6 に示します。

Figure 6 制限されたバッファ (Close Figure 6)
public class BoundedBuffer<T> {
    private Queue<T> m_queue = new Queue<T>();
    private int m_consumersWaiting;
    private int m_producersWaiting;
    private const int s_maxBufferSize = 128;

    public void Enqueue(T obj) {
        lock (m_queue) {
            while (m_queue.Count == (s_maxBufferSize - 1)) {
                m_producersWaiting++;
                Monitor.Wait(m_queue);
                m_producersWaiting--;
            }
            m_queue.Enqueue(obj);
            if (m_consumersWaiting > 0)
                Monitor.PulseAll(m_queue);
        }
    }

    public T Dequeue() {
        T e;
        lock (m_queue) {
            while (m_queue.Count == 0) {
                m_consumersWaiting++;
                Monitor.Wait(m_queue);
                m_consumersWaiting--;
            }
            e = m_queue.Dequeue();
            if (m_producersWaiting > 0)
                Monitor.PulseAll(m_queue);
        }
        return e;
    }
}

再び、いくらか未熟なアプローチがとられました。しかし、2 つのカウンタ m_consumersWaiting と m_producersWaiting を維持し、それぞれの値が 0 以外の場合にのみシグナルを送ることで、PulseAll の呼び出しを最適化しています (この呼び出しは決して安くないため)。さらに改善の余地があります。たとえば、このように単一のイベントを共有すると、非常に多くのスレッドをアクティブ化する可能性があります。コンシューマのキュー サイズが 0 に減少し、プロデューサとコンシューマの両方が待機中である場合は、明らかに、アクティブ化するのは (少なくとも最初は) プロデューサだけで十分です。この実装では、すべての待機を FIFO の順番に従って処理します。したがって、プロデューサを実行する前に、コンシューマがアクティブになる可能性があり、この場合、キューが空であることを確認してから再び待機するだけになります。さいわいにも、プロデューサとコンシューマが同時に待機状態になることはめったに発生しませんが、制限のサイズが小さいと、定期的に起こる可能性があります。

シン イベント

Win32 イベントには、Monitor.Wait、Pulse、および PulseAll に優る大きな利点が 1 つあります。Win32 イベントは、"厄介" です。つまり、一度イベントのシグナルが送出されると、シグナルが発生する前からスレッドが待機を開始していない場合でも、その後の待機は直ちにアンブロックされます。この機能がないと、待機とシグナルがすべて厳密にクリティカルな領域内で発生するという、非効率的なコードを記述するか (非効率的である理由は、Windows スケジューラは、アクティブ化されたスレッドの優先順位を常に引き上げ、常にコンテキスト スイッチをもたらし、アクティブなスレッドはすぐさまクリティカル領域の待機に戻ることになるからです)、または、困難で、競争状態を生じやすいコードを採用する必要があります。

これらのアプローチは使用せずに、代わりに、ブロックの前に短い間スピンし、必要な場合のみ Win32 イベントをゆっくりと割り当て、それ以外の場合は手動式のリセット イベントのような動作を提供する、"シン イベント" という再利用可能なデータ構造を使用できます。言い換えると、"シン イベント" は、競争状態を生じやすい困難なコードをカプセル化して、コード ベース全体にコードが散乱することを回避します。この例は、Vance Morrison の記事で説明しているいくつかのメモリ モデルの保証に依存し、使用に際しては十分な注意が必要です (図 7 を参照)。

Figure 7 シン イベント (Close Figure 7)
public struct ThinEvent {
    private int m_state; // 0 means unset, 1 means set.
    private EventWaitHandle m_eventObj;
    private const int s_spinCount = 4000;

    public void Set() {
        m_state = 1;
        Thread.MemoryBarrier(); // required.
        if (m_eventObj != null)
            m_eventObj.Set();
    }

    public void Reset() {
        m_state = 0;
        if (m_eventObj != null)
            m_eventObj.Reset();
    }

    public void Wait() {
        SpinWait s = new SpinWait();
        while (m_state == 0) {
            if (s.Spin() >= s_spinCount) {
                if (m_eventObj == null) {
                    ManualResetEvent newEvent =
                        new ManualResetEvent(m_state == 1);
                    if (Interlocked.CompareExchange<EventWaitHandle>(
                            ref m_eventObj, newEvent, null) == null) {
                       // If someone set the flag before seeing the new
                       // event obj, we must ensure it’s been set.
                       if (m_state == 1)
                           m_eventObj.Set();
                    } else {
                        // Lost the race w/ another thread. Just use
                        // its event.
                        newEvent.Close();
                    }
                }
                m_eventObj.WaitOne();
            }
        }
    }
}

これは基本的に、m_state 変数のイベント状態をミラーします。値 0 は未設定、1 は設定済みを意味します。設定済みイベントの待機は、非常に低コストです。Wait ルーチンの入り口で m_state が 1 であれば、すぐに戻るため、カーネルの遷移は不要です。困難な状況は、イベントが設定される前にスレッドが待機する場合に発生します。待機する最初のスレッドは、新しいイベント オブジェクトを割り当て、比較を行って m_eventObj フィールドにスワップインする必要があります。CAS が失敗した場合は、別の待機中のスレッドによってイベントが初期化されたことを意味するため、単純に再利用できます。失敗しなかった場合は、最後の確認以降に m_state が変更されていないかどうかを再確認する必要があります。確認をしない場合、m_state が 1 および m_eventObj がシグナルを未送信であれば、WaitOne を呼び出したときにデッドロックが生じる可能性があります。Set を呼び出すスレッドは、最初に m_state を設定し、m_eventObj が null 以外であることを確認したら、これに対して Set を呼び出す必要があります。メモリのバリアが 2 つ必要です。m_state の 2 回目の読み取りは、これより前に移動することができません。これによって、Interlocked.CompareExchange を使用して m_eventObj が設定されることを保証します。また、Set での m_eventObj の読み取りは、m_eventObj への書き込みより前に移動できません (Thread.MemoryBarrier の明示的な呼び出しなしに、Intel と AMD プロセッサの一部、および CLR 2.0 メモリ モデルでの適正な変換に少し驚かされます)。通常、並列処理ではイベントのリセットは安全でないため、呼び出し側での追加の同期が必要です。

これで、CountdownLatch や上記のキュー例などの別の場所でも、この構造を簡単に使用できます。とりわけ、スピンを賢く使用すれば、通常ではかなりのパフォーマンスの向上が見込まれます。

上記では、困難な実装をご紹介しました。単一のフラグとモニタを使用して、自動リセットおよび手動リセットの両方の形式が実装可能であり、この例よりも非常に簡潔になります (ただし、効率的であるとは限りません)。

ロックされない LIFO スタック

上記のとおり、制限やブロックによって複雑にはなりますが、ロックを使用したスレッド セーフなコレクションの作成は非常に簡単です。ただし、簡単な LIFO スタック (最後に入れたデータが最初に取り出される) データ構造によってすべての調整が行われる場合、ロックの使用は必要以上に高価になる可能性があります。スレッドのクリティカル領域、ロックが保持される期間には、始まりと終わりがあり、その期間は多数の命令に値する時間に及ぶことがあります。ロックを保持すれば、他のスレッドが同時に読み取りおよび書き込みを実行できません。これによって、私たちに必要なシリアル化が得られますが、これは厳密には、必要以上に強力です。私たちは単に、スタックの要素をプッシュおよびポップするだけです。これは、通常の読み取りと、単一の比較とスワップによる書き込みで達成できます。この事実を利用して、スレッドを必要以上に待機させないスケーラブルでロックされないスタックを構築できます。

私たちのアルゴリズムは、次のように動作します。リンクされたリストを使用してスタックを表します。スタックのヘッド (先頭) は、スタックの最上部を表し、m_head フィールドに格納されます。スタックに新しいアイテムをプッシュする場合は、スタックにプッシュする値を使用して新しいノードを構築し、ローカルに m_head フィールドを読み取り、これを新しいノードの m_next フィールドに格納してから、アトミックな Interlocked.CompareExchange を実行してスタックの現在のヘッドを置き換えます。このシーケンスのいずれかの時点でヘッドが変更されている場合 (最初に読み取りを行って以来)、CompareExchange が失敗し、スレッドは再度ループし、すべてのシーケンスをやり直す必要があります。ポップも同様に簡単です。m_head を読み取り、ローカル コピーの m_next の参照とスワップします。スワップが失敗した場合は、引き続きスワップを試みます。これについては、図 8 を参照してください。Win32 は、同様のアルゴリズムを使用して構築された SList と呼ばれる類似したデータ構造を提供します。

Figure 8 ロックされないスタック (Close Figure 8)
public class LockFreeStack<T> {
    private volatile StackNode<T> m_head;

    public void Push(T item) {
        StackNode<T> node = new StackNode<T>(item);
        StackNode<T> head;
        do {
            head = m_head;
            node.m_next = head;
        } while (m_head != head || Interlocked.CompareExchange(
                ref m_head, node, head) != head);
    }

    public T Pop() {
        StackNode<T> head;
        SpinWait s = new SpinWait();

        while (true) {
            StackNode<T> next;
            do {
                head = m_head;
                if (head == null) goto emptySpin;
                next = head.m_next;
            } while (m_head != head || Interlocked.CompareExchange(
                    ref m_head, next, head) != head);
            break;

        emptySpin:
            s.Spin();
        }

        return head.m_value;
    }
}

class StackNode<T> {
    internal T m_value;
    internal StackNode<T> m_next;
    internal StackNode(T val) { m_value = val; }
}

これはオプティミスティックな並列処理制御である点に注意してください。他のスレッドからのデータへのアクセスをブロックせずに、競争に "勝つ" ことを期待して処理を進めています。この期待に反する結果になった場合は、ライブロックなどの生の問題が発生する可能性があります。また、この設計の選択は、信頼できる FIFO スケジュールを実行できないことを暗に示しています。システム内のすべてのスレッドは、見込みで処理を進行します。また、1 つのスレッドの失敗は、常に他のスレッドの進行を意味するため (これは "ロックされない" と呼ぶための要件です)、事実上、システム全体としては、常に進行していることになります。CompareExchange が m_head に対する膨大なメモリの競合を回避できない場合は、急激なバックオフを使用することが有効な場合もあります。

また、スタックが空になった場合については、いくらか未熟なアプローチをとりました。新しいアイテムがプッシュされるのを待機して、いつまでもスピンするだけです。Pop を待ちのない TryPop メソッドに書き換えることは簡単ですが、待機イベントの有効利用はいくらか複雑です。どちらも重要な機能であり、やる気のある読者のために課題として残されています。

私たちは Push ごとにオブジェクトを割り当てることで、いわゆる ABA 問題についての懸念から逃れています。ABA は、リストからポップ オフされたノードの内部的な再利用によって発生します。開発者は、オブジェクトの割り当て数を減らすためにノードのプールを試みる場合がありますが、これは問題になる可能性があります。これによって、m_head への多数の書き込みが介在している場合でも、アトミック操作が間違って成功と見なされる可能性があります。たとえば、ノード A がスレッド 1 によって読み取られた後、スレッド 2 によって削除され、プールに配置されます。スレッド 2 によって、B が新しいヘッドとしてプッシュされた後、A がプールからスレッド 2 に戻り、プッシュされます。このとき、スレッド 1 は CompareExchange に成功しますが、現在のヘッドにある A は、スレッド 1 が読み取ったものとは異なります。このアルゴリズムをネイティブ C/C++ で記述する場合も、類似した問題が発生します。アドレスが解放されると、メモリ割り当てによってすぐにアドレスが再利用される可能性があり、ノードをポップし、解放した場合、後からそのアドレスが新しいノード割り当てに渡される可能性があり、同じ問題が発生します。ABA の詳細については、別の場所で詳しく説明しているので、ここでは省略させていただきます。

最後に、類似のロックされない技法を使用して、FIFO キューを作成することが可能です。これは魅力的な方法です。なぜなら、上記の LockFreeStack では、同じ m_head フィールドをプッシュ側とポップ側が常に競い合っていますが、これとは対照的に、同時にプッシュとポップを実行しているスレッド同士の競合が必ずしも発生しないからです。ただし、アルゴリズムはいくらか複雑です。関心がある場合は、Maged M. Michael と Michael L. Scott の 1996 年の記事「簡単、迅速、かつ実用的な非ブロックおよびブロック方式の並列処理キュー アルゴリズム」を参照してください。

ループ タイル

ループ タイルは、ループの入力範囲またはデータをパーティション化し、各パーティションを別々のスレッドに割り当てて並列に実行することです。これは、OpenMP (Kang Su Gatlin の MSDN Magazine の記事を参照) などの一部のプログラミング モデルで並列処理を達成するための主要な技法であり、高パフォーマンス FORTRAN の専門用語の影響を受けて、すべてのループのための並列処理と呼ばれることがよくあります。範囲が単なるインデックスのセットである場合 :

for (int i = 0; i < c; i++) { ... }
データ範囲である場合 :

foreach (T e in list) { ... }
いずれの場合でも、タイル化されたループを提供するパーティションの技法を利用できます。

多数の適用可能なデータ構造固有のパーティションの技法があり、このコラムではとうてい網羅できません。ここでは、一般的な技法に的を絞って説明します。それは、連続していない要素範囲を配列から各パーティションに割り当てる技法です。ストライドを計算して、要素の数をパーティションの数で割った大まかな数値を求め、この数値を使用して連続した範囲を計算します (図 9 を参照)。こうすると、入力が値型の配列である場合の空間的局所性を簡単に得ることができます。ただし、他のアプローチが、確実に有効かつ便利であり、ときには必要になる場合もあります。

Figure 9 ループ タイル (Close Figure 9)
public static void ForAll(int from, int to, Action<int> a, int p) {
    ForAll<int>(null, from, to, null, a, p);
}
public static void ForAll<T>(IList<T> data, Action<T> a, int p) {
    ForAll<T>(data, 0, data.Count, a, null, p);
}

private static void ForAll<T>(IList<T> data, int from, int to,
        Action<T> a0, Action<int> a1, int p) {
    int size = from - to;
    int stride = (size + p - 1) / p;
    CountdownLatch latch = new CountdownLatch(p);

    for (int i = 0; i < p; i++) {
        int idx = i;
        ThreadPool.QueueUserWorkItem(delegate {
            int end = Math.Min(size, stride * (idx + 1));
            for (int j = stride * idx; j < end; j++) {
                if (data != null) a0(data[j]);
                else              a1(j);
            }

            latch.Signal();
        });
    }

    latch.Wait();
}

ここでは、ForAll の 2 つのパブリック バージョンを提供します。一方は数値の範囲を受け入れ、他方は C# の foreach ループのように IList<T> を受け入れます。どちらも同じヘルパ オーバーロードに転送され、指定されたインデックスにあるリストの要素を渡すか、インデックス自体を渡して、アクションを呼び出します。最初のオーバーロードは、通常ではループの規準を置く場所に使用できます。たとえば、次のコードがあります。

for (int i = 0; i < 10; i++) { S; }
このコードは、下記のようになります。

Parallel.ForAll(0, 10, delegate(int i){ S; },
    Environment.ProcessorCount);
また、2 番目のオーバーロードは、通常では C# の foreach ループを置く場所に使用できます。たとえば、次のコードがあります。

List<T> list = ...;
foreach (T e in list) { S; }
このコードは、下記のようになります。

Parallel.ForAll(list, delegate(T e) { S; },
    Environment.ProcessorCount);

S のステートメントが共有メモリへの書き込みを行わないように注意する必要があります。注意を怠ると、並列処理バージョンに適切な同期を追加する必要があります。当然ながら、任意の IEnumerable<T> への対応、異なる方法によるイテレーション空間のパーティションなど、さまざまな変更や追加が可能です (このコラムでは、スペースの都合により省略させていただきます)。また、この例では、n 個のサブタスクの期間、呼び出し側のスレッドが "無駄" になっています。より適切なアプローチとしては、呼び出し側のスレッドを使用していずれかのタスクを実行し、完了時にその他と結合することです。これは ForAll メソッドの拡張によって簡単に達成できます。

並列リダクション

リダクション (折りたたみまたは集約とも呼ばれる) を使用して実行できる操作カテゴリがあります。この操作では、いずれかの方法で多数の値を結合して、単一の出力を生成します。一般的なリダクションは、次のように動作します。バイナリ演算子 (つまり、2 つの引数を持つ関数) を取得し、ベクトルまたはサイズ n の要素のセットを左側から順番に計算します。j = 0 ～ n - 1 について、バイナリ演算子を呼び出し、j 番目のイテレーションへの入力として、要素 j - 1 に対する演算子の呼び出し結果を最初の引数、j 番目の要素自体を 2 番目の引数として渡します。0 番目の要素に対しては、直前の値を使用できないため、特別なシード値を最初の引数として使用します。最後に、オプションの結果セレクタを使用して、中間値を最終結果に変換します。

それでは例を見てください。バイナリ演算子が + で、入力が 5 個の要素 {1, 2, 3, 4, 5} のベクトルである場合、展開された計算式は ((((1 + 2) + 3) + 4) + 5) のようになります。この展開を関数呼び出し形式に変換すると、次のようになります (シードを 0 とした場合) : +(+(+(+(+(0, 1), 2), 3), 4), 5)。つまり、入力されたすべての数値の合計を計算するだけです。これを合計のリダクションと呼びます。この一般化アルゴリズムをシリアル アルゴリズムに簡単に変換すると、次のようになります。

delegate T Func<T>(T arg0, T arg1);

T Reduce<T>(T[] input, T seed, Func<T> r) {
    T result = seed;
    foreach (T e in input)
        result = r(result, e);
    return result;
}
この関数を呼び出すときは、次のように、数値のセットを簡単にまとめることができます (C# 3.0 の場合)。

int[] nums = ... some set of numbers ...;
int sum = Reduce(nums, 0, (x,y) => x + y;);
すべてが非常に抽象的ですが、図 10 に示すように、合計のほかに、多数の演算をリダクションとして表現できます。

Figure 10 リダクションとして表現される演算 (Close Figure 10)

 | シード | バイナリ演算子 | 結果セレクタ
カウント | 0 | (a, b) => a + 1 | 該当なし
合計 | 0 | (a, b) => a + b | 該当なし
最小値 | NaN | (a, b) => a < b ?a :b | 該当なし
最大値 | NaN | (a, b) => a > b ?a :b | 該当なし
平均 | { 0, 0 } | (a, b) => new { a[0] + b, a[1] + 1 } | (a) => a[0] / a[1]

上記の nums 配列から、私たちのリダクション ルーチンを使用して配列の最小値と最大値を求めることができます。

int min   = Reduce(nums, int.MaxValue, (x,y) => x < y ? x : y;);
int max   = Reduce(nums, int.MinValue, (x,y) => x > y ? x : y;);
(カウントを省略した理由は、結果の一部を合計する必要があり、別々のバイナリ演算子が 2 つ必要となるからです。また、平均を省略したのは、追加の手順が必要だからです。)

これと同様の技法をループのタイル化に使用して、入力データを分割し、並列処理でリダクションを実行できます。各パーティションでそれぞれの中間値を計算したら、この中間値の計算に使用したのと同じ演算子を使用して、各パーティションの中間値を単一の最終値にします。この計算が成り立つ理由は、次のとおりです。上記の演算はすべて結合演算です。初等数学を思い出してください。結合バイナリ演算子 + は、(a + b) + c = a + (b + c) であることを意味します。つまり、計算の順番に関係なく、正しく計算されます。

たとえば、合計のリダクションについて考えます。入力データ { 1, 2, 3, 4 } を、{1, 2} と {3, 4} の 2 つのパーティションに分割します。このとき、+ の結合則のため、それぞれの和を合計しても同じ結果が得られます : (1 + 2) + (3 + 4) = ((((1 + 2) + 3) + 4)。実際、入力をどのようにばらばらに分割しても、正しい結果が得られます。図 11 に、前に説明したストライド パーティション アプローチを使用して、シードとバイナリ演算子を引数として受け取る一般化された Reduce メソッドを示します。

Figure 11 Reduce メソッド (Close Figure 11)
public delegate T Func<T>(T arg0, T arg1);

public static T Reduce<T>(IList<T> data, int p, T seed, Func<T> r){
    T[] partial = new T[p];
    int stride = (data.Count + p - 1) / p;
      CountdownLatch latch = new CountdownLatch(p);

    for (int i = 0; i < p; i++) {
        int idx = i;
          ThreadPool.QueueUserWorkItem(delegate {
            // Do the ‘ith’ intermediate reduction in parallel.
            partial[idx] = seed;
            int end = Math.Min(data.Count, stride * (idx + 1));
            for (int j = stride * idx; j < end; j++)
                partial[idx] = r(partial[idx], data[j]);
            latch.Signal();
        });
    }

    latch.Wait();

    // Do the final reduction on the master thread.
     T final = seed;
    for (int i = 0; i < p; i++)
        final = r(final, partial[i]);

    return final;
}

マスタ スレッドは、p 個のワーカー スレッドのセットを fork します。各スレッドは、中間結果を計算するため、それぞれのデータ パーティションでリダクションを実行し、中間配列内の専用のスロットに値を置きます。マスタ スレッドは、すべての子が完了するのを待ち (前に説明した CountdownLatch を使用)、各子の部分的な結果を入力として使用して、最後のリダクション ステップを実行します。文字どおり、ツリーのリダクションは非常に一般的です。ツリーのリダクションでは、ツリー内の各ノードが一定の数の中間結果に対して部分リダクションを実行します。理論的には、処理するスレッドの数や Windows での同期に関連したコストの増加を伴い、よりスケーラブルなアルゴリズムになりますが、実際には、ここに示されたシリアル リダクションは、一般的な p の値およびバイナリ演算子に対してより優れた処理を行います。

ここには、最適化のための機会が豊富にあります。たとえば、いずれかのパーティションで呼び出し側のスレッドを再利用できます。しかし、この例は重要なポイントを十分に表しています。平均などの演算では、中間リダクション ステージと最終リダクション ステージとで中間リダクション演算が異なり、最終結果の選択ルーチンが必要です。このような演算をサポートするには、いくらか異なる API が必要です。これはかなり簡単な課題です。

まとめ

このコラムでは、マルチプロセッサおよびマルチコア アーキテクチャを活用したマネージ コードの作成に役立つ、低レベルの並列処理のデータ構造とアルゴリズムについて説明しました。すべてのプログラミングと同様に、抽象概念は層を成して積み重なる傾向があり、通常、パフォーマンスにとって最も重要な概念が一番下に存在します。このコラムで説明した多くの技法は、一番下の層にあり、ハイレベルの抽象やアプリケーション固有の並列処理コードの基盤となっているといえます。適切なデータ構造と基本アルゴリズムの選択は、長いプロセスの中の 1 ステップにすぎませんが、並列プログラミングの技法の理解を深めることができたと思います。これらの技法は、新しいプログラミング世界での成功に役立ちます。

ご意見やご質問は、

こちら clrinout@microsoft.com.

まで英語でお送りください。
Joe Duffy は、Microsoft で並列プログラミング モデルとインフラストラクチャを担当し、ブログを www.bluebytesoftware.com/blog で定期的に公開しています。このコラムのコード サンプルの一部は、2007 年に Addison Wesley から刊行される『Windows での並列プログラミング』に基づいています。


Office Space: SharePoint の機能

Office Space
SharePoint の機能
Ted Pattison

コードのダウンロード : : OfficeSpaceFeature2007_05.exe (190KB)
コンテンツ
“機能” とは
機能の構造
機能の展開とアクティブ化
機能を拡張する
カスタム アプリケーション ページ
まとめ

前回の Office Space コラムでは、Office オープン XML ファイル形式について説明しました。前回は、ASP.NET ページのコードを作成し、Microsoft .NET Framework 3.0 に付属のパッケージ API を使用して Microsoft® Word の .docx ファイルを生成する方法に焦点を当てました。今月号では、前回の内容を踏まえ、Office オープン XML ファイル形式の扱い方について説明します。ここでは、前回のコラムで作成したコードを Windows® SharePoint® Services (WSS) 3.0 または Microsoft Office SharePoint Server (MOSS) 2007 で展開可能なビジネス ソリューションに統合します。

今月の Office Space コラムでは、WSS と MOSS でビジネス ソリューションを作成するときに使用する基本的な構成要素に焦点を当てます。WSS の "機能" とは何を意味するのかを説明し、サイト内での新しいリストやドキュメント ライブラリの作成を自動化する WSS 機能の作成方法を示します。さらに、カスタム ビジネス ソリューションにカスタム アプリケーション ページを追加します。このアプリケーション ページには、SharePoint リスト内のデータを基に Word 文書を生成し、それを SharePoint ドキュメント ライブラリに保存するために必要な Visual Basic® コードが含まれています。

“機能” とは

WSS および MOSS 向けのカスタム ビジネス ソリューションを開発するときには、まず機能について十分に理解する必要があります (ここで言う機能とは一般的な機能セットのことではありません)。機能とは、WSS 3.0 に追加された開発者向けの革新的な機能のことです。一言で言うと、サイト要素を定義するメカニズムを提供するものであり、このメカニズムはターゲット サイトまたはターゲット サイト コレクションのコンテキスト内で自動的にアクティブ化できます。機能によって定義できる要素には、リスト インスタンス、リストの種類、メニュー コマンド、ページ テンプレート、ページ インスタンス、イベント ハンドラ、ワークフローなどがあります。ここで紹介する例では OfficeSpaceFeature という機能を使用しています。この機能は、リストとドキュメント ライブラリを作成し、WSS の標準メニューである [サイトの操作] にカスタム アプリケーション ページまで移動するためのカスタム メニュー項目を追加します。

機能は、各フロントエンド Web サーバーのファイル システムにある特殊な WSS システム ディレクトリ内に作成されるディレクトリによって構成されます。機能のディレクトリには 1 つ以上の XML ベースのファイルがあり、それらのファイルには Collaborative Application Markup Language (CAML) が含まれています。機能ディレクトリには必ず feature.xml というマニフェスト ファイルがあります。このファイルには、ID やユーザーにわかりやすいタイトルなど、機能の高レベル属性が定義されています。

通常、機能には feature.xml ファイルのほかに機能の構成要素が定義された 1 つ以上の XML ファイル (elements.xml など) も含まれています。機能ディレクトリには、リスト定義やページ テンプレートなどのファイルのほか、画像、カスケード スタイル シート、JavaScript などのリソース ファイルも追加することができます。ここで紹介する例の機能ディレクトリには、ドキュメント ライブラリのドキュメント テンプレートとして使用されるファイルも含まれています。

機能について把握するには、WSS の基本インストールに付属している機能の標準セットを調べることをお勧めします。図 1 は、WSS のインストールによって作成される FEATURES ディレクトリの例を示しています。この図からわかるとおり、各機能に固有のディレクトリがあります。MOSS をインストールしている場合は、機能が 100 を超えるため、FEATURES ディレクトリの内容がこの図とは大きく異なります。

図 1 標準の FEATURES ディレクトリ
図 1 標準の FEATURES ディレクトリ(画像を縮小するには、ここをクリックします)
図 1 標準の FEATURES ディレクトリ
図 1 標準の FEATURES ディレクトリ (画像を拡大するには、ここをクリックします)

機能を構成するすべてのファイルを収めたディレクトリを作成したら、次にその機能をファームレベルのスコープで WSS に対してインストールする必要があります。WSS に対してインストールした機能は、[サイトの設定] ページから開く管理ページを使用してサイトまたはサイト コレクションのコンテキスト内でアクティブ化できます。

このコラムのダウンロードには OfficeSpaceFeature という Visual Studio® プロジェクトが含まれており、このプロジェクトには同じ名前の機能があります。図 2 に示すソリューション エクスプローラの画面を見ると、WSS または MOSS 向けのカスタム機能を開発するために Visual Studio プロジェクトを作成したときにどのようなファイルが生成されるかがわかります。ここで紹介する例は Visual Basic DLL プロジェクトとして作成されています。もちろん、好みに応じて C# DLL プロジェクトを使用することもできます。アセンブリ DLL 内のマネージ コードは、厳密な名前でコンパイルし、グローバル アセンブリ キャッシュ (GAC) にインストールすることによって、機能のイベント ハンドラで使用できる状態にする必要があります。

図 2 OfficeSpaceFeature
図 2 OfficeSpaceFeature

機能の構造

feature.xml ファイルの内容を調べる前に、この機能のファイルを FEATURES と呼ばれる WSS システム ディレクトリ内の固有の特殊なディレクトリに展開する必要があることについて考えます。FEATURES ディレクトリは、図 1 に示すように、TEMPLATE という別の WSS システム ディレクトリ内にあります。機能を展開する必要があることを考えると、WSS 機能の開発に使用する Visual Studio プロジェクト内にも同じフォルダ階層を作成すると便利です。それによって、開発プロセス中に機能ファイルを適切な場所にコピーしてテストする作業が容易になります。

まず、TEMPLATE フォルダを現在のプロジェクトのルート ディレクトリに追加します。次に、TEMPLATE 内に FEATURES というディレクトリを作成します。最後に、FEATURES 内に機能プロジェクトと同じ名前のディレクトリをを作成します。ここでは、図 2 に示すとおり、プロジェクトとディレクトリの名前を OfficeSpaceFeature としています。

では、feature.xml ファイルの CAML について見ていきましょう。feature.xml ファイルは機能マニフェストの役割をするもので、機能の高レベル属性を定義する情報を指定します。ここで作成する機能の feature.xml ファイルの内容を図 3 に示します。

Figure 3 サンプル feature.xml ファイル (Close Figure 3)
<?xml version=”1.0” encoding=”utf-8” ?>

<Feature Id=”AAEC2E08-1CCF-4712-AE5E-A33BEA53A325”
  Title=”A Sample Office Space Feature”
  Description=
    “Demoware from Ted Pattison’s Office Space column in MSDN Magazine”
  Version=”1.0.0.0”
  Scope=”Web”
  ImageUrl=”OfficeSpace/PithHelmet.gif”
  ReceiverAssembly=”OfficeSpaceFeature, [full strong name]”
  ReceiverClass=”OfficeSpaceFeature.FeatureReceiver”
  xmlns=”http://schemas.microsoft.com/sharepoint/”>

    <ElementManifests>
      <ElementManifest Location=”elements.xml” />
    </ElementManifests>

</Feature>

この図を見ると、機能が Feature 要素によって定義され、この要素には Id、Title、Description、Version、Scope、Hidden、ImageUrl などの属性があることがわかります。機能を一意に識別できるように、Id 属性の値となる GUID を作成する必要があります。Title 属性と Description 属性には、わかりやすいテキストを入力します。これらの属性は、機能のアクティブ化および非アクティブ化に使用する WSS の管理ページに表示されます。

Scope には、機能をアクティブ化および非アクティブ化するコンテキストを定義します。ここで作成する機能のスコープは Web と同じです。これは、アクティブ化および非アクティブ化をサイトのコンテキスト内で実行できることを意味します。Scope の値を Site に設定すると、機能のアクティブ化および非アクティブ化をサイト コレクションのスコープ内で実行できます。機能を定義するスコープには、他に WebApplication と Farm があります。

ご覧のとおり、Hidden 属性の値は FALSE となっています。この場合、ファーム内にインストールした機能がリストに表示され、ユーザーが必要に応じてアクティブ化することができます。Hidden 属性を TRUE に設定すると、その機能は使用可能な機能のリストに表示されません。このリストに表示されない機能をアクティブ化するには、コマンド ラインからコマンドを実行するか、カスタム コードまたは別の機能のアクティブ化依存関係を使用する必要があります。

既にお気付きのように、ImageUrl 属性の値は WSS の基本インストールに付属のグラフィック イメージを参照しています。このイメージは、UI において機能の隣に表示されます。

図 3 に示すとおり、feature.xml ファイルの最後には ElementManifests 要素があります。この要素には、機能の構成要素を定義する他の XML ファイルを参照する内側の ElementManifest 要素が含まれています。この例には、ElementManifest 要素が 1 つだけあり、location 属性によって element.xml ファイルを参照しています。

feature.xml や elements.xml などの CAML ベースのファイルで XML を追加および変更する場合は、XML スキーマ駆動型 IntelliSense® のサポートを追加する必要があります。TEMPLATE ディレクトリには XML というディレクトリがあり、そこに wss.xsd などの XML スキーマがあります。このスキーマ ファイルを feature.xml や elements.xml などの機能ファイルに関連付けると、Visual Studio の IntelliSense を使用することにより、カスタム機能の作成が格段と容易になります。

では、ここで element.xml ファイルの内容を見てみましょう。機能をアクティブ化するたびにドキュメント ライブラリをインスタンス化するとします。図 4 のような elements.xml ファイルを作成します。

Figure 4 サンプル elements.xml ファイル (Close Figure 4)
<Elements xmlns=”http://schemas.microsoft.com/sharepoint/”>

  <ListInstance
   FeatureId=”00BFEA71-E717-4E80-AA17-D0C71B360101”
   TemplateType=”101”
   Id=”CustomerLetters”
   Description=”Letters sent to customers”
   OnQuickLaunch=”True”
   Title=”Customer Letters”
   Url=”CustomerLetters”>
  </ListInstance>

  <Module Name=”LetterTemplate” List=”101” Url=”CustomerLetters/Forms”>
    <File Url=”LetterTemplate.docx” Type=”GhostableInLibrary” />
  </Module>

  <!-- more elements to come... -->

</Elements>

この図にある 1 つ目の要素 ListInstance は、リストまたはドキュメント ライブラリのインスタンスを作成します。ListInstance には FeatureId と TemplateType があり、リストの種類とそのリストの種類が定義された機能の ID が指定されています。この例では、TemplateType の値が 101 である WSS の標準ドキュメント ライブラリを使用して、Customer Letters というタイトルのドキュメント ライブラリを作成します。ListInstance 要素には、File 要素が含まれる Module 要素もあります。File 要素を使用して、ドキュメント ライブラリのドキュメント テンプレートを準備します。より厳密に言うと、File 要素は WSS に対し、LetterTemplate.docx ファイルを機能ディレクトリから WSS サイトにコピーするよう指示します。これによって、このファイルはユーザーによるアクセスが可能になります。

elements.xml の宣言型ロジックは WSS サイト内に要素を作成するために必要なものを提供するうえで役に立ちますが、多くの場合、対応するプログラマティック ロジックが必要になります。この例では、新しいドキュメント ライブラリを作成し、ドキュメント テンプレートとなるファイルを準備しました。ただし、WSS オブジェクト モデルに対してドキュメント テンプレートをドキュメント ライブラリに関連付けるように指示するコードも作成する必要があります。

この例の feature.xml ファイルでは、Feature 要素に ReceiverAssembly と ReceiverClass という 2 つの属性があります。これらの属性は、イベント ハンドラを提供するマネージ クラスである機能レシーバを参照しています。機能レシーバ クラスに含まれるイベント ハンドラは、機能がインストールまたはアクティブ化されたとき、および機能がアンインストールまたは非アクティブ化されたときに起動します。機能レシーバを作成するには、図 5 に示すように SPFeatureReceiver クラスを継承するクラスを作成します。

Figure 5 SPFeatureReceiver を継承するクラス (Close Figure 5)
Imports Microsoft.SharePoint

Public Class FeatureReceiver : Inherits SPFeatureReceiver

  Public Overrides Sub FeatureActivated( _
      ByVal properties As SPFeatureReceiverProperties)
    ‘*** fires when feature is activated
  End Sub

  Public Overrides Sub FeatureDeactivating( _
      ByVal properties As SPFeatureReceiverProperties)
    ‘*** fires when feature is deactivated
  End Sub

  Public Overrides Sub FeatureInstalled( _
      ByVal properties As SPFeatureReceiverProperties)
    ‘*** fires when feature is installed
  End Sub

  Public Overrides Sub FeatureUninstalling( _
      ByVal properties As SPFeatureReceiverProperties)
    ‘*** fires when feature is uninstalled
  End Sub

End Class

この例では、FeatureActivated イベント ハンドラについてのみコードを作成します。このコードでは、WSS オブジェクト モデルを使用して、ドキュメント テンプレートを Customer Letters ドキュメント ライブラリに関連付けます。イベント ハンドラでは、この関連付けを行うために、まず現在のサイトへの SPWeb 参照を取得し、次にターゲット ドキュメント ライブラリへの SPDocumentLibrary 参照を取得します。これによって、次のように DocumentTemplateUrl プロパティを変更し、Update メソッドを呼び出して変更を保存することができます。

Public Overrides Sub FeatureActivated( _
    ByVal properties As SPFeatureReceiverProperties)

  Dim site As SPWeb = CType(properties.Feature.Parent, SPWeb)

  Dim libLetterTemplates As SPDocumentLibrary
  libLetterTemplates = CType(site.Lists(“Customer Letters”), _
    SPDocumentLibrary)
  Dim templateUrl As String = _
    “CustomerLetters/Forms/LetterTemplate.docx”
  libLetterTemplates.DocumentTemplateUrl = templateUrl
  libLetterTemplates.Update()

End Sub

機能の展開とアクティブ化

feature.xml ファイルと elements.xml ファイルを作成できたので、次に機能をテスト用にインストールします。機能をインストールするには、4 つの手順を実行します。まず、OfficeSpaceFeature.dll というアセンブリ ファイルを GAC にインストールします。次に、OfficeSpaceFeature ディレクトリを WSS のシステム ディレクトリ FEATURES にコピーします。さらに、Stsadm.exe を実行して機能を WSS に対してインストールします。最後に、WSS サイトのコンテキスト内で機能をアクティブ化します。

最初の 3 つの手順については、OfficeSpaceFeature プロジェクトのルート ディレクトリに install.bat というバッチ ファイルを作成し、図 6 に示すコマンド ライン命令を追加することによって、自動化することができます。必要であれば、Stsadm.exe ユーティリティで ActivateFeature 操作を実行することより、最後の手順も自動化できます。この例で最後の手順を自動化していないのは、ユーザーが WSS のユーザー インターフェイスを使用して手動で機能をアクティブ化するプロセスを示すためです。

Figure 6 インストールを自動化するコマンド (Close Figure 6)
@SET TEMPLATEDIR=”c:\Program Files\Common Files\Microsoft Shared\
                  web server extensions\12\TEMPLATE”
@SET STSADM=”c:\Program Files\Common Files\Microsoft Shared\
             web server extensions\12\bin\stsadm.exe”
@SET GACUTIL=”c:\Program Files\Microsoft Visual Studio 8\
              SDK\v2.0\Bin\gacutil.exe”

Echo Installing OfficeSpaceFeature.dll in GAC
%GACUTIL% -if bin\debug\OfficeSpaceFeature.dll

Echo Copying source files to WSS \TEMPLATE directory
xcopy /e /y TEMPLATE\* %TEMPLATEDIR%

Echo Installing feature with WSS
%STSADM% -o installfeature -filename  OfficeSpaceFeature\feature.xml -force

Echo Restarting IIS worker process
IISRESET

install.bat ファイルに命令を追加したら、OfficeSpaceFeature プロジェクトをリビルドするたびに install.bat ファイルが実行されるように Visual Studio を設定します。そのためには、[プロジェクト プロパティ] の [ビルド イベント] タブに移動し、次に示すビルド後に実行するコマンド ライン命令を追加します。

cd $(ProjectDir)
Install.bat

1 行目は、現在のディレクトリをプロジェクト ディレクトリの現在のディレクトリに変更するために必要です。2 行目は、機能ファイルを正しい場所にコピーするバッチ ファイルを実行し、コマンド ライン ユーティリティ Stsadm.exe の InstallFeature 操作によって機能をインストールします。

機能を正しくインストールできたので、サイトのコンテキスト内で機能をアクティブ化することができます。WSS ファームまたは MOSS ファームのサイト内で機能をアクティブ化するには、[サイトの設定] ページを開きます。[サイトの管理] セクションで [サイトの機能] というリンクをクリックします。図 7 のようなページが表示されます。MOSS がインストールされたファーム内で操作を行う場合は、WSS だけがインストールされたファーム内で操作を行う場合より多くの機能が表示されます。

図 7 サイト機能のアクティブ化と非アクティブ化
図 7 サイト機能のアクティブ化と非アクティブ化(画像を縮小するには、ここをクリックします)
図 7 サイト機能のアクティブ化と非アクティブ化
図 7 サイト機能のアクティブ化と非アクティブ化 (画像を拡大するには、ここをクリックします)

[サイトの機能] ページには機能のタイトルと説明が表示されるので、OfficeSpaceFeature を簡単に見つけることができます。OfficeSpaceFeature が見つかったら、[アクティブ化] ボタンをクリックします。これによって、elements.xml 内の宣言型ロジックに定義されたすべての要素が現在のサイト内に準備されます。さらに、GAC からレシーバ アセンブリが読み込まれ、レシーバ クラスのインスタンスが作成され、レシーバ クラスの FeatureActivated メソッドが実行されます。

このサンプルはきわめて単純ですが、ご想像どおり、FeatureActivated には宣言型 CAML 要素と WSS オブジェクト モデル コードを複雑に組み合わせて追加することもできます。

機能を拡張する

elements.xml ファイルの内容を見ると、ListInstance 要素がもう 1 つあることに気付きます。この ListInstance 要素では、WSS に組み込まれた連絡先リストを使用して Customers というタイトルのリストを作成します。この ListInstance を見ると、リストに既定の項目を追加して顧客データのサンプルを作成する方法もわかります。

宣言型の CAML ロジックを使用してリストとドキュメント ライブラリを作成する方法については、既に説明しました。ここで、リストやドキュメント ライブラリを作成するには、WSS オブジェクト モデルに対するカスタム コードを機能アクティブ化イベントに追加するという方法もあることを指摘しておきます。

このサンプル機能には、FeatureActivated イベント ハンドラに WSS の標準カスタム リストから Letter Templates というリストを作成するコードも含まれています。WSS の標準カスタム リストを基にリストを作成した場合は、自動的に Title という名前のフィールドが作成されます。このコードでは、Body という列も作成し、WSS オブジェクト モデルを使用して、顧客宛てのレターを作成するためのテンプレートとして使用されるデータを提供するリスト アイテムを追加します。この操作を実行するコードを図 8 に示します。

カスタム アプリケーション ページ

機能とその操作という考え方を理解することは重要ですが、WSS や MOSS 向けのソリューションの設計、開発には、もう 1 つの基本的な構成要素があります。WSS のアーキテクチャは、アプリケーション ページと呼ばれる特殊なページをサポートしています。アプリケーション ページの良い例が標準の [サイトの設定] ページ (settings.aspx) です。settings.aspx ペ―ジは、フロントエンド Web サーバーごとに展開されますが、WSS ファームのどのサイトからでもアクセスできます。

アプリケーション ページは、フロントエンド Web サーバーのファイル システムに物理ファイルとして展開されます。保存先ディレクトリのパスは次のとおりです。

%PROGRAMFILES%\common files\microsoft shared
   \web server extensions\12\TEMPLATE\LAYOUTS
物理ディレクトリ LAYOUTS は、WSS が新しい Web アプリケーションを準備するたびに、_layouts という仮想ディレクトリにマップされます。WSS ランタイムは、このマップ スキームと追加の処理ロジックを使用して、各アプリケーション ページをファーム内のどのサイトのコンテキストでもアクセスできるようにします。たとえば、WSS ファームに次の URL でアクセス可能な 3 つのサイトがあるとします。

http://Litwareinc.com
http://Litwareinc.com/sites/Vendors
http://Litwareinc.com:1001/sites/Accounting

アプリケーション ページにアクセスするには、その _layouts ディレクトリからの相対パスをサイトの URL の終わりに追加します。たとえば、[サイトの設定] ページには次の URL のいずれでもアクセスできます。

http://Litwareinc.com/_layouts/settings.aspx
http://Litwareinc.com/sites/Vendors/_layouts/settings.aspx
http://Litwareinc.com:1001/sites/Accounting/_layouts/settings.aspx
ファームをスコープとするアプリケーション ページは 1 つのバージョンしか存在しないため、1 つの DLL にコンパイルし、メモリに 1 回読み込むだけで済みます。アプリケーション ページはサイトごとにバージョンが異なるわけではないので、複数のバージョンについて考慮する必要はありません。さらに、アプリケーション ページがサイト ページのカスタマイズ権限を持つユーザーの攻撃にさらされることはありません。したがって、WSS ではアプリケーション ページにインライン コードを追加することが禁じられていません。

WSS チームではアプリケーション ページを幅広く活用しており、サイトやサイト内の要素の準備および管理を担う標準機能の多くをアプリケーション ページによって提供しています。WSS は、カスタム ビジネス ソリューション向けのカスタム アプリケーション ページもサポートしています。このコラムで紹介したサンプルには LetterGenerator.aspx というカスタム アプリケーション ページが含まれており、他のアプリケーション ページと同じように LAYOUTS ディレクトリに展開されます。

ただし、LetterGenerator.aspx は LAYOUTS ディレクトリ自体に展開されるわけではなく、LAYOUTS ディレクトリ内の OfficeSpace ディレクトリに展開されます。カスタム アプリケーション ページを使用する場合は、名前の競合を防ぐために、LAYOUTS ディレクトリ内に専用のディレクトリを作成し、そこにカスタム アプリケーション ページを保存することをお勧めします。

アプリケーション ページは特殊な ASP.NET ページであり、通常は基本クラス LayoutsPageBase の派生クラスとして作成し、WSS チームが作成した標準マスタ ページ application.master にリンクします。図 9 に、古典的な Hello World カスタム アプリケーション ページの例を示します。

Figure 9 Hello World カスタム アプリケーション ページ (Close Figure 9)
<%@ Assembly Name=”[Full assembly name for Microsoft.SharePoint]” %>
<%@ Page Language=”VB” MasterPageFile=”~/_layouts/application.master”
         Inherits=”Microsoft.SharePoint.WebControls.LayoutsPageBase” %>

<%@ Import Namespace=”Microsoft.SharePoint” %>

<script runat=”server”>
  Protected Overrides Sub OnLoad(e As EventArgs)
      MyBase.OnLoad(e)
      ‘*** get current site and web objects through WSS object model
      Dim siteCollection As SPSite = SPContext.Current.Site
      Dim site As SPWeb = SPContext.Current.Web
      ‘*** now program against WSS objects within the current site
      lblDisplay.Text = “Current site: “ & site.Title
  End Sub
</script>

<asp:Content ID=”Main” runat=”server”
             contentplaceholderid=”PlaceHolderMain” >
    <!-- ADD HTML elements and controls here for page content -->
    <asp:Label ID=”lblDisplay” runat=”server” />
</asp:Content>

ご覧のとおり、アプリケーション ページには標準 ASP.NET コントロールを含めることができ、OnLoad などの標準 ASP.NET ページ オーバーライドのコードを追加することも可能です。さらに、アセンブリ参照を追加することもできます。Microsoft.SharePoint.dll の参照を追加すると、図 9 で行ったように、SPContext クラスを介して参照を取得することにより、現在のサイト コレクションおよび現在のサイトに対してプログラムを実行することができます。

ソリューションにカスタム アプリケーション ページを追加したら、ユーザーがそのアプリケーション ページにアクセスするための手段を用意する必要があります。機能に CustomAction 要素を追加することによって、リンクまたはメニュー項目を作成できます。次に、サンプルの OfficeSpaceFeature で使用されている、[サイトの操作] ドロップダウンにメニュー項目を追加する CustomAction 要素の例を示します。

<CustomAction
  Id=”OfficeSpaceLetterGenerator”
  GroupId=”SiteActions”
  Location=”Microsoft.SharePoint.StandardMenu”
  Sequence=”1001”
  Title=”OfficeSpace Letter Generator”
  Description=”Create letter using Open XML”
  ImageUrl=”/_layouts/images/crtpage.gif” >

    <UrlAction Url=”~site/_layouts/OfficeSpace/LetterGenerator.aspx”/>

</CustomAction>

この CustomAction 要素は標準の [サイトの操作] メニューにカスタム メニュー項目を追加します。CustomAction 要素内の UrlAction 要素は、Url 属性にカスタム アプリケーション ページの相対パスを割り当てます。Url 属性に割り当てられた文字列値には ~site という形式の動的トークンが含まれています。このトークンは実行時に WSS によって現在のサイトのパスに置き換えられます。

ユーザーがこのカスタム メニュー項目を選択すると、LetterGenerator.aspx というカスタム アプリケーション ページに移動し、図 10 に示すユーザー インターフェイスが表示されます。このページの OnLoad メソッドには 2 つのリストボックスに目的の顧客とレターの種類を表示するコードがあります。このコードでは、WSS オブジェクト モデルを使用して、機能のアクティブ化プロセス中に作成された 2 つのリストからデータを取得します。また、前回の Office Space コラムで紹介したコードを使用して顧客宛てのレターを .docx ファイルとして作成する方法を選択できる 2 つのコマンド ボタンもあります。

図 10 カスタム アプリケーション ページ
図 10 カスタム アプリケーション ページ(画像を縮小するには、ここをクリックします)
図 10 カスタム アプリケーション ページ
図 10 カスタム アプリケーション ページ (画像を拡大するには、ここをクリックします)

ユーザーがいずれかのコマンド ボタンをクリックすると、イベント ハンドラが Office オープン ファイル形式でレターを作成するコードを実行します。このコードは、前回のコラムで紹介したコードとほとんど同じです。唯一異なる点は、ファイルに名前を付け、Customer Letters というタイトルで WSS ドキュメント ライブラリ内に保存するコードです。

この例では、Office オープン XML ファイル形式を使用したカスタム レターの作成を自動化する方法を示しています。また、この例から WSS とこのファイル形式の間に相乗効果があることがわかります。

新しい .docx ファイルを作成したら、そのファイルを保存する場所が必要になります。WSS には、共同作業環境においてチームでバージョンを管理し、ドキュメントにアクセスできる絶好の保存場所があります。図 11 は、OfficeSpaceFeature をアクティブ化したサイト内の Customer Letters ドキュメント ライブラリを示しています。

図 11 Customer Letters ドキュメント ライブラリ
図 11 Customer Letters ドキュメント ライブラリ(画像を縮小するには、ここをクリックします)
図 11 Customer Letters ドキュメント ライブラリ
図 11 Customer Letters ドキュメント ライブラリ (画像を拡大するには、ここをクリックします)

まとめ

今回のコラムでは、機能とカスタム アプリケーション ページについて説明しました。いずれも WSS および MOSS 向けソリューションの設計および実装に不可欠の構成要素です。これで、開発とテストにどのように着手すればよいかがおわかりいただけたと思います。次回の Office Space では、ここで紹介したようなソリューションを運用環境に展開できるようにパッケージ化する方法について説明します。

ご意見やご感想は、

Ted instinct@microsoft.com.

まで英語でお送りください。
Ted Pattisonは、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。また、Ted は Microsoft Press 発行の『インサイド Windows SharePoint Services 3.0』の著者でもあります。彼の会社 Ted Pattison Group では、企業の開発者を対象とする SharePoint の上級者向けトレーニングを実施しています。


Cutting Edge: ASP.NET ページのサブクラス化とオーバーライドを行う - 第 II 部

Cutting Edge
ASP.NET ページのサブクラス化とオーバーライドを行う - 第 II 部
Dino Esposito

コードのダウンロード : : CuttingEdge2007_05.exe (163KB)
コンテンツ
背景
元のソリューション
きわめて単純な攻撃をシミュレートする
この問題の原因
改良された TextBox コントロール
TextBox コントロールを置換する
最終的なソリューション

最近、きわめて単純な Web ユーザー コントロール、その名も ASCX ファイルをふんだんに使った比較的大規模な Web サイトを見つけました。使用しているサーバー コントロールが予期しない動作をすることに気付いた開発者たちは、この方法が必要だと考えました。

この発見の結果、開発者はサイトで使用されているこのサーバー コントロールのすべてのインスタンスを元のコントロールの修正版を使用したユーザー コントロールに置き換えました (次にどのような動作をするかが不確かであったため、その他の数多くのコントロールも置き換えました)。開発者たちは、ページやコントロール間にこの抽象層を追加したことによって信頼性が高くなったと感じました。ASP.NET アプリケーションではユーザー コントロールの置き換えが簡単で、バイナリを変更してアプリケーションを再起動する必要がないという利点もありました (バイナリを変更せざるを得ない場合もありますが、その際にもバイナリの変更を不要にする展開オプションが用意されています)。

私はこのアプリケーションのレビューを依頼されましたが、そのとき開口一番出された質問は、"サイト全体のサーバー コントロールを置き換える場合に、すべてのページを改造するような大変な手間をかけずに済むもっとよい方法はないか" というものでした。

私の 2007 年 4 月のコラムで、元のソース コードを変更せずに ASP.NET Web サイトに部分的な、場合によっては一時的な変更を加えるという問題に対するいくつかの方法について説明しています。今月は、ソース コードを変更せずに、宣言を使用してサーバー コントロールや URL を置き換える方法についてさらに詳しく説明します。

開発チームの質問に対して即答はできませんが、解決策を探す手がかりは知っています。私が ASP.NET インフラストラクチャを開発するとしたら、構成ファイルに設定を追加し、宣言を使用してコントロールにタグをマップするように設計するでしょう。これは ASP.NET では特に目新しい考え方ではありません。ASP.NET 1.x では、Web ページやユーザー コントロールの基本クラスなど、コードに関連する部分を宣言で変更することができます (ただし、これは Page ディレクティブの Inherits 句を明示的に指定しないページやユーザー コントロールの場合に限った話です)。これを、サーバー コントロールで使用しない手があるでしょうか。私の考え方に間違いはありませんでした。ASP.NET 2.0 では、この目的で <tagMapping> セクションが用意されています。

背景

背景について少しお話しておきましょう。このシナリオは、顧客企業で典型的な SQL インジェクション攻撃を受ける可能性があるアプリケーションのセキュリティ ホールが見つかった際の内部セキュリティ評価の時期に始まりました。この会社では応急パッチを適用しましたが、別の問題が発生するばかりでした。

この会社の Web サイトでは、多くのページでクエリ文字列に固定の 5 文字コードを使用できるようになっていて、このコードで SQL ステートメントが作成されます。同社では、いまだに次のようなコードを実行していました。

Dim code As String = Request.QueryString(“Code”).ToString();
Dim command As String = _
    “SELECT * FROM customers WHERE id=’” & code & “’”
このようなコードがみなさんのサイトではもう実行されていないことを切に願います。このコードは、クエリ文字列によって渡された情報を全面的に無条件に信頼し、SQL コマンドを構成する文字列に追加しています。これは重大なセキュリティの危機です。知識のあるハッカーは、無害を装ったテキストを容易に見出し、そのテキストによって元のテキストとカスタム SQL コマンドを危険な攻撃に転化する術を知っています。SQL インジェクションの詳細については、まずは Paul Litwin の記事「SQL インジェクション攻撃を未然に防ぐ」を参照してください。

パフォーマンスの点から言うと、動的に作成したコマンドを送信するのは不適切です。動的に作成されたコマンドを使用すると、送信されるたびにコードの内容が異なるため、クエリ プランを再利用できません。パラメータ化クエリまたはストアド プロシージャを使用する利点は 2 つあります。1 つは、少なくとも送信中のデータの種類は確実に自動チェックされるということ、もう 1 つは SQL Server などのデータベースでのクエリ プランのキャッシュを利用できるということです。

2007 年 3 月の初回 Cutting Edge で説明したように、クエリ文字列の検証は、コンソール ユーティリティのコマンド ラインで行う場合と同様に静的に行う必要があります。この方法によって数字、日付、ブール値のあいまいさは排除されますが、文字列が想定される場合にはあまり役立ちません。本当に問題なのは、文字列の内容です。そこで、渡された文字列を入念に検証するビジネス ロジックが必要になります。しかし、文字列の長さの検証は HTTP モジュールを使用するか、コードの一部を変更するだけで実行可能な単純なチェックです。

元のソリューション

SQL インジェクション ホールが見つかった後、顧客企業では、使用可能な引数のサイズを 5 文字 (実際のコード サイズ) に制限するのが簡単で有効な回避策だと判断しました。使用可能な文字がたった 5 文字であれば、ハッカーがデータベースを攻撃する余地は少ないだろうと期待していました。そこで、2007 年 3 月の初回 Cutting Edge で私が紹介した HTTP モジュールをインストールし、影響のあるページのクエリ文字列サイズをチェックするようにしました。確かに 5 文字以下の文字列がページに送信されました

が、このアプリケーションには新しい ASP.NET ページと改訂された古い ASP ページが混在し、一部のページではユーザーが同じコードをテキスト ボックスに入力して送信できるようになっていました。サーバーでは、ポストバック時に、指定されたコードが前述した方法で SQL ステートメントに追加されます。そのため、テキスト ボックスから送信されるテキストの長さも制限する必要がありました。これは大した問題ではないと考えた開発者は、次のようにテキスト ボックスの MaxLength 属性を目的の値に設定しました。

<asp:textbox runat=”server” id=”TextBox1” MaxLength=”5” />

これですべてが解決したように見えます。5 文字を超えるコードがサイトの中間層に潜み込む可能性はなくなりました。これでサイトがインジェクション攻撃から保護されたとは限りませんが、攻撃の可能性が制限されたことは確かです。少なくとも開発者チームはそう考えました。

きわめて単純な攻撃をシミュレートする

図 1 のようなサンプル ASP.NET ページを考えてみましょう。図 2 に、このページのソース コードを示します。このページには、MaxLength 属性を 5 に設定したテキスト ボックスと送信ボタンがあります。送信ボタンをクリックすると、ポストバックが実行され、テキスト ボックスの内容が処理されます。通常では、ブラウザに表示されたページに 6 文字以上を入力する方法はありません。6 文字以上のテキストを貼り付けると、指定されたサイズに従って切り捨てられます。

Figure 2 サンプル ASP.NET ページのソース コード (Close Figure 2)

ASPX

<%@ Page Language="VB" AutoEventWireup="false"
    CodeFile="Default.aspx.vb" 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 runat="server">
    <title>Max Length</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h3>Try specifying more than 5 characters</h3>
        <asp:TextBox ID="TextBox1" MaxLength="5"
          runat="server"></asp:TextBox><br />
        <asp:Button ID="Button1" runat="server" Text="Click" />
    </div>
    </form>
</body>
</html>
Codebehind
Partial Class _Default : Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, _
          ByVal e As System.EventArgs) Handles Me.Load
        TextBox1.Focus()
    End Sub
    Protected Sub Button1_Click(ByVal sender As Object, _
          ByVal e As System.EventArgs) Handles Button1.Click
        Response.Write("<b>Processing text: </b>" & TextBox1.Text & "<b>
   </b><hr/>")
    End Sub
End Class
図 1 サンプル ASP.NET ページ
図 1 サンプル ASP.NET ページ(画像を縮小するには、ここをクリックします)
図 1 サンプル ASP.NET ページ
図 1 サンプル ASP.NET ページ (画像を拡大するには、ここをクリックします)

これを攻撃者の側から考えてみましょう。Web ページに対する一般的な攻撃では、ページのコピーをプレーン HTML として作成し、一部の値を変更し、"有害化された" ページをポストします。悪意のあるユーザーがページの HTML を取得するために要する手間は、通常のユーザーと同様に [表示] メニューの [ソース] を選択し、ローカル HTML ファイルに保存するだけです。ただし、それには、攻撃者が物理的にページにアクセス可能である必要があります。たとえば、ページが保護されている場合、攻撃者がページを表示するためには有効な資格情報を提供しなければなりません。それでも、認証 Cookie の盗用やフィッシングなどのソーシャル エンジニアリングによって正確な情報が不正なユーザーの手に渡る可能性があります。

ASP.NET ページのマークアップをローカル マシンに保存した後、いくつかの変更が必要です。まず、攻撃者は、フォームの action 属性を変更し、同じ ASP.NET ページの絶対 URL を指定します。default.aspx ASP.NET ページのサーバー フォームの一般的なマークアップを次に示します。

<form name=”form1” method=”post” action=”Default.aspx” id=”form1”>
攻撃者はこれを次のように変更します。

<form name=”form1” method=”post”
      action=”http://targetserver/Source/Default.aspx” id=”form1”>

ASP.NET HtmlForm コントロールには action 属性がありませんが、プレーン HTML を使用する場合には任意の URL にフォームのコンテンツをポストすることができます。もう 1 つの変更は、有害化されたデータのポストに関連します。私なら、ASP.NET の TextBox サーバー コントロールから出力されるマークアップに value 属性を追加し、通常の 5 文字よりはるかに長い文字列に設定します。

<input name=”TextBox1” type=”text”
maxlength=”5” id=”TextBox1”
value=”This is a far looooonger text” />

この HTML ページを攻撃者のマシンで表示してボタンをクリックするとどうなると思いますか? その結果を図 3 に示します。左側にあるブラウザ ウィンドウのアドレス バーを見ると、表示されているページがローカル HTML ページであることがわかります。右側のブラウザ ウィンドウでは、変更後のフォームのコンテンツがリモート ASP.NET アプリケーションにポストされたことがわかります。5 文字の制限は回避されています。これで、テキスト ボックスの MaxLength の設定にかかわらず、悪意のあるユーザーには任意のサイズのページ テキストを送信する方法があるということがわかりました。

図 3 任意の長さの文字列をポストするローカル HTML ページ
図 3 任意の長さの文字列をポストするローカル HTML ページ(画像を縮小するには、ここをクリックします)
図 3 任意の長さの文字列をポストするローカル HTML ページ
図 3 任意の長さの文字列をポストするローカル HTML ページ (画像を拡大するには、ここをクリックします)

この問題の原因

どこに問題があるのかという疑問をお持ちのことだろうと思います。原因はブラウザにあるのか。ASP.NET ランタイムにあるのか。それとも TextBox コントロールにあるのか。実は、問題は TextBox コントロールにあります。

TextBox サーバー コントロールは、ポストバック後にサーバー上でこのコントロールを使用したページを再作成した場合、MaxLength をチェックしません。明らかに、セキュリティを確保するためには、Text プロパティを割り当てる前に、TextBox が MaxLength の値をチェックし、ポスト後のテキストの長さと比較する処理を行う必要があります。

改良された TextBox コントロール

TextBox は <input type=text> HTML タグのサーバー側機能に相当し、ユーザーが入力バッファに入力したテキストを取得します。TextChanged サーバー イベントを発生して、データをページ内の他のコントロールが使用できるようにするには、TextBox がこのテキストを処理する必要があります。ポストされたデータを処理する ASP.NET コントロールは、次のように IPostBackDataHandler インターフェイスを実装します。

Public Interface IPostBackDataHandler
  Function LoadPostData(ByVal postDataKey As String, _
          ByVal postCollection As NameValueCollection) As Boolean
  Sub RaisePostDataChangedEvent()
End Interface
LoadPostData メソッドは TextBox コントロールのポストバック データが以前の値と異なっているかどうかを確認し、異なっている場合はその内容を読み込み、true を返します。それ以外の場合は false を返します。

LoadPostData メソッドは、ページの Init イベントと Load イベントの間で、各コントロールのビュー ステートの内容が復元された直後に呼び出されます。postDataKey パラメータは、読み込むコンテンツを参照するポストされたコレクション内の名前を示します。postCollection パラメータは、ポストされたすべての値のコレクション (選択した HTTP 動詞に応じてクエリ文字列またはフォーム コレクションのいずれか) を保持します。

RaisePostDataChangedEvent メソッドは、ライフサイクルの後の方で呼び出され、ポストバック後にコントロールの状態が変更されたかどうかを示す、オプションのコントロール固有のイベントを起動します。実際には、RaisePostDataChangedEvent メソッドが呼び出されるのは、LoadPostData で true が返された場合のみです。

System.Web.UI.WebControls.TextBox コントロールでの LoadPostData の実装を表す擬似コードを図 4 に示します。基本的には、このメソッドは、ビュー ステートから読み取った Text プロパティの値とポストされた値を比較します。2 つの値が異なる場合はポストされた値で現在の値が置換され、コントロールの Text プロパティの新しい値になります。

Figure 4 TextBox コントロールの LoadPostData メソッド (Close Figure 4)
Protected Overridable Function LoadPostData(ByVal postDataKey As?String, _
      ByVal postCollection As NameValueCollection) As Boolean
   MyBase.ValidateEvent(postDataKey)

   Dim oldText As String = Me.Text
   Dim newText As String = postCollection.Item(postDataKey)

   If (Not Me.ReadOnly AndAlso Not oldText.Equals( _
         newText, StringComparison.Ordinal)) Then
      Me.Text = newText
      Return True
   End If

   Return False
End Function

これまでの説明でおわかりのように、ポストされた値は文字列の長さを考慮せず、無条件に Text プロパティに割り当てられます。各コントロールは LoadPostData メソッド内でいくつでもプロパティを更新することが可能で、テストする意味があるプロパティどうしを照合することもできます。図 4 に示すように、LoadPostData の TextBox の実装では、検証はコントロールが読み取り専用でないことを確認するのみにとどまり、次に古いテキストと新しいテキストの比較が開始されます。

LoadPostData メソッドの実装を若干変えた新しい TextBox コントロールを図 5 に示します。オーバーライドされたメソッドは、ポストされたテキストを単純に最大長で切り捨ててからテキストの照合を開始します。図 6 に示すように、最大長を超えるテキストは自動的に切り捨てられ、以降のポストバック中の結果作成には使用されません。この処理は、クライアント ブラウザの機能にかかわらず行われます。

Figure 5 修正後の TextBox の LoadPostData メソッド (Close Figure 5)
Public Class TextBox : Inherits System.Web.UI.WebControls.TextBox

    Protected Overrides Function LoadPostData( _
            ByVal postDataKey As String, _
            ByVal postCollection As NameValueCollection) As Boolean
        Page.ClientScript.ValidateEvent(Me.UniqueID, String.Empty)

        Dim oldText As String = Me.Text
        Dim newText As String = postCollection.Item(postDataKey)

        If (newText.Length > Me.MaxLength) Then
            newText = newText.Substring(0, Me.MaxLength)
        End If

        If (Not Me.ReadOnly AndAlso Not oldText.Equals( _
                newText, StringComparison.Ordinal)) Then
            Me.Text = newText
            Return True
        End If

        Return False
    End Function

End Class
図 6 最大長を超えるテキストの切り捨て
図 6 最大長を超えるテキストの切り捨て(画像を縮小するには、ここをクリックします)
図 6 最大長を超えるテキストの切り捨て
図 6 最大長を超えるテキストの切り捨て (画像を拡大するには、ここをクリックします)

図 4 と図 5 の LoadPostData メソッドのソース コードを比較すると、若干の相違があることがわかります。図 4 では、メソッドから基本クラスである System.Web.UI.Control クラスの ValidateEvent を呼び出しています。図 5 では、図 4 のコードが変更され、ClientScriptManager オブジェクトの ValidateEvent を呼び出すようになっています。

Page.ClientScript.ValidateEvent(Me.UniqueID, String.Empty)

ValidateEvent メソッドは Control 基本クラスの C# コード内で Friend 宣言されているため、System.Web.dll アセンブリの外部で定義されたクラスから呼び出すことはできません。Control 基本クラス内の ValidateEvent メソッドのコール スタックで ClientScriptManager オブジェクトの ValidateEvent メソッドが呼び出され、Page クラスの ClientScript プロパティを使用して ClientScriptManager オブジェクトのインスタンスが公開されます。

ValidateEvent は ASP.NET 2.0 でイベント検証を実装するためのツールです。イベント検証は、ページがそのページまたは登録されたコントロールで固有に生成されたイベント以外のイベント (およびイベント引数) を処理しないようにするための組み込み機能です。

TextBox コントロールを置換する

これで新しい TextBox コントロールができました。Text プロパティに設定された最大長を超えるテキストは検出され、削除されます。このコントロールを各自の ASP.NET ページで使ってみてはいかがでしょう。各ページにコントロールを登録し、元のテキスト ボックスのすべてのインスタンスを登録したコントロールに置き換えるだけです。

ASP.NET 2.0 では、web.config ファイルの <pages> ブロックの <controls> セクションに次の構成スクリプトを追加することで、かなりの手間を省くことができます。

<add tagPrefix=”x” namespace=”Samples” assembly=”TextBox” />
これにより、指定したタグおよびコントロールが web.config ファイルのスコープ内のすべてのページで自動的に登録されます。

それでもまだ大きな問題が 1 つ残っています。それは、古いテキスト ボックスと新しいテキスト ボックスとを切り替える方法です。さいわい、ASP.NET 2.0 の構成ファイルには <tagMapping> セクションがあります。

<pages>
   <tagMapping>
     <add tagType=”System.Web.UI.WebControls.TextBox”
          mappedTagType=”Dino.Samples.TextBox” />
   </tagMapping>
</pages>

<tagMapping> セクションでは、コンパイル時にコントロールの型を別の型のコントロールに再マップすることができます。この再マップにより、構成ファイルのスコープ内のすべてのページおよびユーザー コントロールで元の型の代わりにマップされた型が使用されます。前述のコードの場合、システム TextBox が参照されているすべての箇所で Dino.Samples.TextBox が使われるようになります。後は、新しいコントロールを作成して web.config ファイルを編集するだけです。驚くほど簡単で、しかも有効な方法です。

言うまでもなく、再マップする型は元の型を継承したクラスでなければなりません。ASP.NET チームではこの機能を ASP.NET AJAX Extensions 1.0 の RTM 以前のビルドで使用して、元の検証コントロールを UpdatePanel コントロールに適した新しい検証コントロールに置き換えたということもお伝えしておきます。

最終的なソリューション

顧客企業は、ASP.NET の TextBox コントロールと、このコントロールでのポストされたデータの処理方法の問題を的確に診断し、新しい TextBox コントロールを作成して問題を適切に修正しました。開発チームはサイト全体のコントロールを置換する、より優れた、できれば宣言を使用した方法を知らなかったため、すべてのインスタンスを手動で置換してユーザー コントロールでラップしました。これは、将来の変更の影響を小さくするための処理でした。

tagMapping 機能を使用したため、このソリューションはきわめて簡単でした。tagMapping を使用する方法は整然としており、問題のあるコントロールを置換したり、既存コントロールに新しい機能を追加する場合に利用できます。ただし、再マップ後のコントロールに新しいプロパティまたはメソッドがある場合、これらのプロパティやメソッドを使用するにはソース コードを変更する必要があります。

宣言を使用したマッピングに関する機能としては、ASP.NET 2.0 の <urlMappings> セクションもあります。このセクションは <configuration> の直接の子です。<urlMappings> セクションは、HttpContext オブジェクトの RewritePath メソッドに相当する ASP.NET 2.0 の宣言部です。

要は、MaxLength が設定されている場合、元の ASP.NET の TextBox コントロールはポストされた Text プロパティの値を切り捨てないということに注意が必要だということです。しかし、このコラムで説明した、この制限を回避するためのコントロールの修正法をご覧いただけば、問題の解決に役立つことと思います。web.config ファイルに簡単な 1 行を追加してアプリケーションに宣言を追加することができます。

ご意見やご感想は、

Dino cutting@microsoft.com.

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


テストの実行: Windows PowerShell を使用した軽量テスト

テストの実行
Windows PowerShell を使用した軽量テスト
Dr. James McCaffrey

コードのダウンロード : : TestRun2007_05.exe (155KB)
コンテンツ
テスト対象のモジュール
アドホックな対話型モジュール テスト
軽量モジュール テストの自動化
まとめ

Windows PowerShell は、古い cmd.exe コマンド シェルと関連 .bat ファイルに対する劇的なアップグレードと考えることができます。システム管理タスクを念頭に置いて設計されていますが、Windows PowerShell™ には軽量テスト タスクに理想的な機能もあります。今月、筆者は Microsoft® .NET Framework ベースのコード モジュールを Windows PowerShell コマンド ラインと軽量の Windows PowerShell スクリプトの両方からテストしました。

図 1 および図 2 に示すスクリーンショットを見てみましょう。図 1 は、MyPointLib という名前の .NET DLL モジュールのアドホックな対話型テストを実行している Windows PowerShell を示しています。Windows PowerShell には、オブジェクトを作成し、実行中のシェル プロセスにライブラリを動的に読み込める構文が用意されています。テスト対象の MyPointLib ライブラリを読み込み、Point オブジェクトをインスタンス化し、オブジェクトの使用可能メソッドを調べ、Distance メソッドを呼び出し、結果が正しいことを検証します。

図 1 コマンド ラインで Windows PowerShell を使用したアドホック テスト
図 1 コマンド ラインで Windows PowerShell を使用したアドホック テスト(画像を縮小するには、ここをクリックします)
図 1 コマンド ラインで Windows PowerShell を使用したアドホック テスト
図 1 コマンド ラインで Windows PowerShell を使用したアドホック テスト (画像を拡大するには、ここをクリックします)

図 2 は、Windows PowerShell スクリプトの出力を示しています。ここでは、従来のモジュール テストを実行していることがわかります。つまり、MyPointLib ライブラリの Point オブジェクトをインスタンス化し、Distance メソッドのインスタンス バージョンと静的バージョンを呼び出し、実際の結果を予想される結果と比較してテスト ケースの合格/不合格を判断しています。

図 2 スクリプトを使用した従来のモジュール テスト
図 2 スクリプトを使用した従来のモジュール テスト(画像を縮小するには、ここをクリックします)
図 2 スクリプトを使用した従来のモジュール テスト
図 2 スクリプトを使用した従来のモジュール テスト (画像を拡大するには、ここをクリックします)

このコラムでは、最初にテスト対象のクラス ライブラリについて説明してから、図 1 に示した Windows PowerShell コマンドを取り上げます。.NET ベースのモジュールを対話的にテストするための Windows PowerShell 手法についても指摘します。次に、図 2 の出力を生成したスクリプトについて説明します。テスト ハーネスのソース コードとテスト対象のライブラリはダウンロードに含まれています。

次に、ここで示した概念を適用して自身のテスト ニーズを満たす方法を説明してから、Windows PowerShell が NUnit のような単体テスト フレームワークなどよりも適しているシナリオを説明します。Windows PowerShell を追加することでソフトウェア テスト ツールボックスが強化されることがおわかりいただけるものと信じています。

テスト対象のモジュール

デモンストレーションの目的で、MyPointLib ライブラリはかなり単純にしてあります (図 3 を参照) (経験豊かな .NET 開発者は、次のセクションに進んでかまいません)。例外処理など、本番での実装に必要なコードは含めませんでした。

Figure 3 MyPointLib.cs テスト ライブラリ (Close Figure 3)
using System;
namespace MyPointLib
{
  public class Point
  {
    private int x;
    private int y;

    public enum DistanceKind { Euclidean, CityBlock };

    public Point() { }

    public Point(int x, int y)
    {
      this.x = x; this.y = y;
    }

    public int X { get { return this.x; } set { this.x = value; } }

    public int Y { get { return this.y; } set { this.y = value; } }

    public override string ToString()
    {
      return “(“ + this.x + “,” + this.y + “)”;
    }

    public double Distance(Point p)
    {
      return Distance(this, p);
    }

    public static double Distance(Point p1, Point p2)
    {
      return Distance(p1, p2, DistanceKind.Euclidean);
    }

    public static double Distance(Point p1, Point p2, DistanceKind dk)
    {
      switch(dk)
      {
        case DistanceKind.CityBlock:
          return Math.Abs(p1.x - p2.x) + Math.Abs(p1.y - p2.y);
        case DistanceKind.Euclidean:
          return Math.Sqrt((Math.Pow(((double)(p1.x - p2.x)), 2)) +
            (Math.Pow(((double)(p1.y - p2.y)), 2)));
        default:
          throw new ArgumentException(“dk”);
      }
    }
  }
}

MyPointLib には C# を使用しましたが、実装言語にかかわらず Windows PowerShell を使用して .NET ライブラリを検証およびテストできます。また、Windows PowerShell を使用して従来の COM ライブラリをテストすることもできます。

MyPointLib ライブラリは、四角形 (x,y) 形式で 2 次元 Point オブジェクトを実装します。パブリックに参照可能な列挙型 DistanceKind を宣言していることにも注意してください。

private int x;
private int y;

public enum DistanceKind { Euclidean, CityBlock };
2 点間の距離を計算する方法はいくつかあります。ユークリッド距離 (おそらく高校で習った式です) が最も一般的です。別のアプローチである都市ブロック手法では、整数値軸ラインを左右にのみ移動できます (都市ブロック距離は、タクシー距離と呼ばれることもあります)。

Windows PowerShell を使用して .NET ライブラリのカスタム列挙型にアクセスするには、後で簡単に説明する特殊構文が必要です。既定のコンストラクタと、2 つの引数を受け取る補助コンストラクタを実装します。

public Point() {}

public Point(int x, int y)
{
  this.x = x; this.y = y;
}
既定のコンストラクタは座標 (0,0) のポイントをインスタンス化します。ライブラリ モジュールを Windows PowerShell でテストする場合は、両方の種類のコンストラクタ (引数なしと引数あり) の呼び出し方法を知っておく必要があります。この x フィールドと y フィールドはプライベートであるため、X および Y プロパティを実装してそれらの値を取得および設定します。

public int X { get { return this.x; } set { this.x = value; } }
ここでは大文字と小文字が区別される C# を使用しているため、フィールド名には小文字の x を使用し、プロパティ名には大文字の X を使用できます (同様に y と Y を使用します)。あいまいさは発生しません。クラス プロパティにアクセスする機能は、Windows PowerShell を使用したモジュール テストの基本です。

MyPointLib ライブラリは ToString メソッドもオーバーライドします。

public override string ToString()
{
  return “(“ + this.x + “,” + this.y + “)”;
}
この ToString メソッドは、Windows PowerShell を使用してテストする際にアクセスする、非常に典型的なパラメータなしメソッドです。

Point クラスには、3 つの異なる Distance メソッドが含まれます。1 つ目はインスタンス メソッドです。

public double Distance(Point p)
{
  return Distance(this, p);
}
インスタンス メソッドは、Windows PowerShell を使用して .NET ライブラリ モジュールを検証およびテストする際に最もよく見られるメソッドの形式です。Distance メソッドは、暗黙の this コンテキストの Point オブジェクトと、パラメータとして渡された Point オブジェクト P の間のユークリッド距離を「距離 = sqrt( (x1 - x2)2 + (y1 - y2)2 )」の式に従って返します。式を明示的には実装せず、静的な 2 つ目の Distance メソッドにデリゲートします。

public static double Distance(Point p1, Point p2)
{
  return Distance(p1, p2, DistanceKind.Euclidean);
}
静的 Distance は、インスタンス バージョンと同様にユークリッド距離を返します。違いは、2 つのメソッドの呼び出し方法だけです。次に示すように、.NET ライブラリ モジュール内の静的メソッドを Windows PowerShell で呼び出すには、インスタンス メソッドの呼び出しとは異なる構文が必要です。このメソッドは、3 つ目の Distance メソッドに依存して実際の計算を行います。3 つ目の Distance メソッドは、距離の計算時に使用する距離式の種類 (Euclidean または CityBlock) を示すパラメータを受け取ります。

public static double Distance(Point p1, Point p2, DistanceKind dk)
{
  switch(dk)
  {
    case DistanceKind.CityBlock:
      return Math.Abs(p1.x - p2.x) + Math.Abs(p1.y - p2.y);
    case DistanceKind.Euclidean:
      return Math.Sqrt((Math.Pow(((double)(p1.x - p2.x)), 2)) +
        (Math.Pow(((double)(p1.y - p2.y)), 2)));
    default:
      throw new ArgumentException(“dk”);
  }
}

図 4 の図では、ポイント (0,0) と (3,4) の間のユークリッド距離と都市ブロック距離の違いを示しています。このバージョンの Distance の真の目的は、Windows PowerShell で列挙型を使用する方法を示すことです。ご覧のように、ユークリッド距離はポイント間の直線ですが、都市ブロック距離はグリッドラインに沿うため、より長くなります。

図 4 ユークリッド距離と都市ブロック距離
図 4 ユークリッド距離と都市ブロック距離(画像を縮小するには、ここをクリックします)
図 4 ユークリッド距離と都市ブロック距離
図 4 ユークリッド距離と都市ブロック距離 (画像を拡大するには、ここをクリックします)

MyPointLib ライブラリは、必要に応じて Windows PowerShell コマンド ラインからビルドできます。たとえば、MyPointLib クラス ライブラリのソース コードの名前が MyPointLib.cs で、C:\ModTestWithPS\AppLibs ディレクトリにある場合は、次の単純なコマンドを入力することでライブラリを C# コンパイラでビルドできます。

PS C:\ModTestWithPS\AppLibs> csc.exe /target:library MyPointLib.cs
これにより、ライブラリ MyPointLib.dll がディレクトリ C:\ModTestWithPS\AppLibs に作成されます。この例では、Windows PowerShell パス環境変数に csc.exe プログラムの場所が含まれていることを前提としています。パス変数の設定方法については次のセクションで説明します。

Point クラスには、モジュール テストの実行時に最もよく見る可能性の高いフィールドのタイプが含まれています。それは、既定コンストラクタおよび補助コンストラクタ、get および set プロパティ、引数を受け取らないオーバーライドされた ToString メソッド、列挙型、および引数を受け取るインスタンス メソッドと静的メソッドです。

アドホックな対話型モジュール テスト

Windows PowerShell を使用して .NET ライブラリを対話的に検証およびテストできる方法をいくつか見てみましょう。図 1 に示した出力の最初の部分は、Windows PowerShell を使用していることを示しています。

Windows PowerShell
Copyright (C) 2006 Microsoft Corporation. All rights reserved.
PS C:\> _

Windows PowerShell の新規インスタンスを起動した場合、特殊なプロファイル ファイルに格納されている任意のコマンドが実行されます。Windows PowerShell プロンプトで「notepad.exe $profile」と入力することにより、プロファイル ファイルを表示できます。図 1 に示す例では、プロファイル ファイルに次の 4 つのステートメントが含まれています。

set-location \
# write-host “Adding location of csc.exe and ildasm.exe to path”
$env:path += ‘;C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727’
$env:path += ‘;C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin’
最初のステートメントは、set-location cmdlet を使用して現在の作業ディレクトリをルート C:\ ドライブに変更しています。Windows PowerShell を初めて使用する方のために、cmdlet をよく調べてみましょう。Windows PowerShell には、130 の cmdlet ("コマンドレット" と発音します) が含まれていますが、実際にはこれらのうちの 1 ダース程度のみを使用して日常的なファイル システム タスクの大半を実行できます。set-location cmdlet は、"sl" として省略できます。実際に、ほとんどの cmdlet には、古い cmd.exe シェル コマンドにマッピングされる明白なエイリアスがあります。この場合は "cd" です。get コマンドを入力することですべての cmdlet のリストを取得でき、ターゲット cmdlet の名前と共に get-help cmdlet を使用して (たとえば、get-help set-location)、特定の cmdlet のヘルプを表示することもできます。Windows PowerShell では、大文字と小文字が区別されません。

プロファイル ファイル内の 2 つ目のステートメントは、コメント アウトされている print ステートメントです。# コメント文字が削除された場合は、二重引用符で囲まれたメッセージが起動時にシェルに出力されます。3 行目は、csc.exe プログラムへのパスをシェル パス環境変数に追加します。4 行目は、ildasm.exe プログラムへのパスをシェル パス変数に追加します。

(セキュリティ上の理由から、既定の Windows PowerShell ポリシーではスクリプトの実行が許可されません。set-executionpolicy cmdlet を使用してこれを変更できますが、プロファイルの外部で行う必要があります。実行ポリシーによってスクリプトの実行が無効になっている場合、プロファイルは起動時に実行されないため、プロファイル内の実行ポリシーを変更しようとしても意味がありません)。

Windows PowerShell が初期化された後で、set-location cmdlet を使用して現在の作業ディレクトリを変更します ("cd" は set-location の標準のエイリアスであるため、代わりにこれを使用できます)。

PS C:\> set-location .\ModTestWithPS\AppLibs
PS C:\ModTestWithPS\AppLibs> _
次に、現在のディレクトリにある拡張子が .dll のファイルをすべて表示します。

PS C:\ModTestWithPS\AppLibs> gci *.dll

  Directory: Microsoft.PowerShell.Core\FileSystem::C:\ModTestWithPS\AppLibs

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        12/12/2006  10:31 AM       3072 MyMathLib.dll
-a---        12/12/2006  10:02 AM       4096 MyPointLib.dll
get-childitem の短縮形である gci を使用します (dir と入力することもできます)。

ここまでは、Windows PowerShell に関する特殊な点を示していません。次のコマンドからは、Windows PowerShell の背後にある強力な機能が示されます。

PS C:\ModTestWithPS\AppLibs> [Reflection.Assembly]::LoadFile(‘C:\ModTestWithPS\AppLibs\MyPointLib.dll’)

GAC    Version        Location
---    -------        --------
False  v2.0.50727     C:\ModTestWithPS\AppLibs\MyPointLib.dll
ここで、.NET Framework を直接呼び出し、System.Reflection 名前空間の Assembly タイプに対して LoadFile メソッドを使用してクラス ライブラリを読み込みます。構文 [...] は、Windows PowerShell が角かっこ内でそのタイプのインスタンスを探す必要があることを意味します。2 つのコロン "::" は、Windows PowerShell が静的なプロパティまたはメソッドを探す必要があることを示します。いかかでしょう。Windows PowerShell は .NET Framework をベースとしており、これによって .NET Framework に完全にアクセスできます。

クラス ライブラリが読み込まれたので、ライブラリからオブジェクトをインスタンス化できます。

PS C:\ModTestWithPS\AppLibs> $p1 = new-object MyPointLib.Point
Windows PowerShell はオブジェクト指向であり、cmd.exe や UNIX ベースの bash などの古いシェルと比較した場合にいくつかの利点があります。$p1 は Windows PowerShell 変数です。すべての変数には先頭に $ 文字が付いており、スクリプト内で簡単に認識できます。new-object cmdlet を使用して、既定の引数なし Point コンストラクタを呼び出します。既定の Point コンストラクタは、ポイントの座標を (0,0) に設定することを思い出してください。実際にメソッドを呼び出すわけではないため、new-object の呼び出しにはかっこを使用しません。代わりに、コンストラクタで使用される引数を提供しています。これは些細ですが重要な違いです。cmdlet はコマンドライン ツールであり、そのように動作します。

この例では、ライブラリ ソース コードにアクセスできるため、MyPointLib ライブラリに Point クラスが含まれていることがわかっています。しかし、DLL をテストしており、ソース コードにアクセスできない場合はどうすればよいでしょうか。.NET ベース DLL に収容されているクラスを判断する簡単な方法の 1 つは、ildasm.exe ツールを使用することです。

PS C:\ModTestWithPS\AppLibs> ildasm.exe MyPointLib.dll /text /classlist

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Class Point                  (public) (auto) (ansi)
// Class DistanceKind           (auto) (ansi) (sealed) (nested public)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(etc.)
/text スイッチ パラメータは、ildasm.exe に対して、既定の GUI 表示ではなくシェルに出力を送信するよう指示します。また、/classlist パラメータは、ildasm.exe に対して、出力にクラス情報を含めるよう指示します。

アセンブリで使用可能なクラスを判断する方法として、Windows PowerShell を使用することもできます。次の小さなスクリプトでは、MyPointLib アセンブリから型を取得できます。

PS> $asms=[AppDomain]::CurrentDomain.GetAssemblies()
PS> $asm=$asms|where {$_.fullname -match “MyPointLib”}
PS> $assembly.gettypes()
IsPublic IsSerial Name         BaseType
-------- -------- ----         --------
True     False    Point        System.Object
False    True     DistanceKind System.Enum
1 行目では、現在の AppDomain にあるすべての読み込み済みアセンブリを収集します。2 行目では、フルネームが "MyPointLib" と一致するアセンブリを探します。最後に、アセンブリから型を取得します。

次に、get-member cmdlet を使用して、Point オブジェクトに属するインスタンス メソッドを厳密に判断します。

PS C:\ModTestWithPS\AppLibs> $p1 | get-member -membertype method

Name        MemberType Definition
----        ---------- ----------
Distance    Method     System.Double Distance(Point P)
Equals      Method     System.Boolean Equals(Object obj)
GetHashCode Method     System.Int32 GetHashCode()
GetType     Method     System.Type GetType()
(etc.)
| 文字はパイプ演算子であり、Windows PowerShell では重要です。上記のコマンドは、"$p1 Point オブジェクトを取得し、get-member cmdlet に送信する" と解釈できます。この場合は、すべてのパブリック インスタンス メソッドを要求します。-static スイッチを指定して静的メソッドを表示することもできます。次に、ToString メソッドを呼び出します。

PS C:\ModTestWithPS\AppLibs> $p1.tostring()
(0,0)
この例は、引数を受け取らないメソッドを呼び出す方法を示しています。Windows PowerShell では大文字と小文字が区別されないため、「$p1.tostring()」と入力することも、「$p1.ToString()」と入力することもできます。より長く「write-host $p1.tostring()」と入力することもできますが、Windows PowerShell はユーザーが意味するところを推測し、適切な動作を実行しようとします。

次に、2 つ目の Point オブジェクトをインスタンス化します。

PS C:\ModTestWithPS\AppLibs> $p2 = new-object MyPointLib.Point(1,1)
より一般的には、これは 1 つ以上の引数を受け取るオブジェクト コンストラクタの呼び出し方法を示しています。図 1 のスクリーンショットでは Point クラスの 2 つのプロパティのいずれも呼び出しませんでしたが、これを次のように行うことができます。

PS C:\ModTestWithPS\AppLibs> $p2.X
1
プロパティを呼び出すには、推測されるように、通常のドット表記法を使用します。

2 つの Point オブジェクト (0,0) および (1,1) が作成されたところで、Distance メソッドの 1 つを呼び出すことができます。

PS C:\ModTestWithPS\AppLibs> $d = $p1.Distance($p2) | out-host
1.4142135623731
この場合は、Distance メソッドのインスタンス バージョンを呼び出し、$d という名前の変数に結果を格納します。結果が Windows PowerShell シェルに表示されるように、操作を out-host cmdlet にパイプします。パイプがない場合、距離は暗黙的に $d に格納されます (次のセクションでは、Distance メソッドの静的バージョンを呼び出す方法を示します)。

次に、Distance メソッドの戻り値を予想される値と比較チェックします。

PS C:\ModTestWithPS\AppLibs> $check = [System.Math]::Sqrt(2)
PS C:\ModTestWithPS\AppLibs> $check
1.4142135623731
ここで、System.Math 型の Sqrt メソッドを呼び出して 2 の平方根を計算し、それを $check という名前の変数に格納します。Windows PowerShell インタープリタは、変数 $check が Sqrt メソッドから返される値であるため double 型であると自動的に判断します。これは動的な型付けと呼ばれ、ほとんどのスクリプト言語に共通です。次のようにして Sqrt を呼び出すことができます。

PS C:\ModTestWithPS\AppLibs> [double] $check = [System.Math]::Sqrt(2)

Windows PowerShell に固有の機能は、変数のデータ型を明示的に指定できることです。[Int32] のような任意の基本的な .NET データ型 (または [int] のような C# の同等のもの) を使用できます。データ型を指定すると、ランタイム エラー チェックを行うことができます。このようにして [double] を使用すると、その変数に double 以外の値を格納しようとした場合にエラーを受け取ることができます。

まとめると、Windows PowerShell には .NET ベースのクラス ライブラリの対話的なアドホック調査およびテストを実行する機能があります。Windows PowerShell では .NET Framework でメソッドを直接呼び出せるため、Assembly タイプに対して LoadFile メソッドを使用して、.NET ベースの DLL を読み込むことができます。Windows PowerShell シェル内部で /classlist スイッチを指定して既存の ildasm.exe ツールを使用し、使用可能なクラスを判断し (または、Windows PowerShell を使用して型が公開されているターゲット Assembly を直接クエリし)、Windows PowerShell new-object cmdlet を使用してオブジェクトをインスタンス化できます。

オブジェクトをインスタンス化した後で、Windows PowerShell get-member cmdlet を使用して、使用可能なメソッドとプロパティを判断できます。通常のドット表記法を使用して、テスト対象のライブラリ内のインスタンス メソッドを呼び出すこともできます。

軽量モジュール テストの自動化

ここでは、軽量の Windows PowerShell スクリプトを作成して .NET ベースのクラス ライブラリをテストする方法を説明します。図 2 に示した出力を生成したスクリプトについて詳細に説明します。スクリプト全体を、図 5 に示します。

Figure 5 ファイル Harness.ps1 (Close Figure 5)
# file: harness.ps1
# test MyPointLib.dll using data in testCases.txt

write-host “`nBegin MyMathLib.dll module test run using PowerShell`n”

$ep = get-executionpolicy
if ($ep -eq ‘unrestricted’) {
  write-warning “: harness running in unrestricted mode`n”
}

write-host “Loading library MyPointLib.dll `n”
[Reflection.Assembly]::LoadFile(‘C:\ModTestWithPS\AppLibs\MyPointLib.dll’) | out-null

$f = get-content testCases.txt
foreach ($line in $f)
{
  write-host “--------------”
  $tokens = $line.split(‘:’)

  $caseID = $tokens[0]
  $method = $tokens[1]
  $inp = $tokens[2]
  $expected = $tokens[3]
  # The above lines can be written as
  # $caseID,$method,$inp,$expected = $line.split(“:”)

  $coords = $inp.split(‘,’)
  [int] $p1x = $coords[0]; [int] $p1y = $coords[1]
  [int] $p2x = $coords[2]; [int] $p2y = $coords[3]
  # the above lines can be written as:
  # $p1x,$p1y,$p2x,$p2y = $inp.split(“,”)

  write-host “Case ID: $caseID  “ -nonewline
  write-host “Inputs: ($p1x,$p1y) ($p2x,$p2y)”
  write-host “Method: $method “ -nonewline
  write-host “Expected: $expected”

  $p1 = new-object MyPointLib.Point($p1x,$p1y)
  $p2 = new-object MyPointLib.Point($p2x,$p2y)

  if ($method -eq “Instance Distance “) {
    $actual = $p1.Distance($p2)
    $a = “{0:f2}” -f $actual
  }
  elseif ($method -eq “Static Distance   “) {
    $actual = [MyPointLib.Point]::Distance($p1,$p2)
    $a = “{0:f2}” -f $actual
  }
  else {
    write-host “Not yet implemented”
    continue
  }

  write-host “Actual: “$actual
  write-host “Result: “ -nonewline

  if ($a -eq $expected) {
    write-host “ Pass “ -backgroundcolor green -foregroundcolor darkblue
  }
  else {
    write-host “*FAIL*” -backgroundcolor red
  }

  trap {
    write-host “Fatal error trapped at case “ $caseID
    continue
  }

} # main loop

write-host “--------------”
write-host “`nEnd test run`n”

# end script

Windows PowerShell テスト ハーネス スクリプトはいくつかのコメントから開始します。

# file: harness.ps1
# test MyPointLib.dll using data in testCases.txt
# 記号は、Windows PowerShell スクリプトのコメント文字です。Visual Basic® と同様に、Windows PowerShell は行ベースであるため、コメントは行の終わりまで続きます。

テスト ハーネスでは、テスト ケース データを testCases.txt という外部ファイルから読み取ります。

001:Instance Distance :0,0,3,4:5.00
002:Instance Distance :1,1,4,4:4.24
003:Chebyshev Distance:0,0,3,4:5.00
004:Chebyshev Distance:1,1,4,4:4.24
005:Static Distance   :0,0,3,4:6.00:deliberate fail
006:Static Distance   :0,3,3,0:4.24
各行は単一のテスト ケースを表し、各フィールドはコロン文字で区切られています。最初のフィールドはテスト ケース ID です。2 つ目のフィールドは、テストする Distance メソッドをテスト ハーネスに指示します (末尾のスペースは読みやすくするために追加されています)。3 つ目のフィールドは、(x,y) 座標を表すカンマ区切りの 4 つの整数のセットです。4 つ目のフィールドは予想された距離値です。5 つ目のフィールドはオプションのフィールドであり、テスト ケース コメントです。テスト ケース 005 は意図的なエラーを生成することに注意してください。(0,0) と (3,4) 間の距離は 6.00 ではなく 5.00 です。

次に、Windows PowerShell シェルにメッセージを表示します。

write-host “`nBegin MyMathLib.dll module test run using PowerShell`n”
write-host cmdlet を使用します。これは、コンソールに出力を送信する最も柔軟な方法です。代わりに、write-output cmdlet および out-host cmdlet を使用することや、cmdlet を使用せずにメッセージを単純に配置することもできます。

Windows PowerShell の文字列リテラルは、一重引用符または二重引用符で区切ることができます。一重引用符で囲まれた文字列は入力したとおりに表示されますが、二重引用符で囲まれた文字列では文字列内の特定のエスケープ文字が評価されます。この例では、"n" の後のバックティック文字は埋め込み改行のエスケープ シーケンスです。

テスト ハーネス スクリプトでは、次に現在のシェル実行ポリシーを検証します。

$ep = get-executionpolicy
if ($ep -eq ‘unrestricted’) {
  write-warning “: harness running in unresticted mode`n”
}
ご覧のように、get-executionpolicy cmdlet を呼び出し、変数 $ep に結果を格納します。Windows PowerShell が無制限実行モードの場合は、write-warning cmdlet を使用して図 2 に示すようにテキスト "WARNING" に続けて黒い背景に黄色のテキストのメッセージを表示します。

次に、テスト対象のライブラリを読み込みます。

write-host “Loading library MyPointLib.dll `n”
[Reflection.Assembly]::LoadFile(
    ‘C:\ModTestWithPS\AppLibs\MyPointLib.dll’) | out-null
LoadFile メソッドを Windows PowerShell コマンド ラインから呼び出すプロセスについては既に説明しました。Windows PowerShell スクリプトの内部から呼び出す場合、メカニズムはまったく同じです。ここでは、出力を out-null cmdlet にパイプして、進捗メッセージを抑制します。

ここで、テスト ケース データ ファイルからメモリにテスト ケースの内容全体を読み取り、各行を反復処理します。

$f = get-content ‘testCases.txt’
foreach ($line in $f)
{
  # process each test case here
}
get-content cmdlet を使用して、ファイル testCases.txt の内容全体を $f という名前のオブジェクトに読み取ります。次に、Windows PowerShell foreach ループ構造を使用して、ファイルの内容を一度に 1 行ずつ反復し、結果を任意の名前の変数 $line に格納します。

メイン処理ループの内部では、各テスト ケース フィールドを解析します。

$tokens = $line.split(‘:’)
$caseID = $tokens[0]
$method = $tokens[1]
$inp = $tokens[2]
$expected = $tokens[3]
Split メソッドを使用して、各行をコロン文字で分割し、行の各部分を $tokens という名前の配列に格納します。読みやすくするために、$tokens の各セルの内容を説明的な名前の変数 ($caseID、$method、$inp、および $expected) に転送します ($input は特殊な Windows PowerShell 予約変数であるため、$inp を使用しています)。

次に、入力を解析します。

$coords = $inp.split(‘,’)
[int] $p1x = $coords[0]; [int] $p1y = $coords[1]
[int] $p2x = $coords[2]; [int] $p2y = $coords[3]
Windows PowerShell の明示的な型付け機能を使用して、入力が int 型であることをスクリプトに通知しています。

メッセージをシェルに送信すると、2 つの Point オブジェクトをインスタンス化する準備ができます。

$p1 = new-object MyPointLib.Point($p1x,$p1y)
$p2 = new-object MyPointLib.Point($p2x,$p2y)
前のセクションでは new-object cmdlet について説明しました。new-object cmdlet は、-com スイッチを追加すれば従来の COM オブジェクトをインスタンス化することもできます。

ここで、テスト対象の my Distance メソッドを呼び出すことができます。

if ($method -eq “Instance Distance “) {
  $actual = $p1.Distance($p2)
  $a = “{0:f2}” -f $actual
}
elseif ($method -eq “Static Distance   “) {
  $actual = [MyPointLib.Point]::Distance($p1,$p2)
  $a = “{0:f2}” -f $actual
}
else {
  write-host “Not yet implemented”
  continue
}

if/elseif/else 制御構造を使用して、テスト ケース入力から読み取った $method 変数の値で分岐します。Windows PowerShell には、スイッチ ステートメントを含む完全な制御構造セットがあります。Windows PowerShell で少々奇抜なのは、構文的なトークン (==、<、> など) ではなく Boolean 演算子 (-eq、-lt、-gt など) を使用することです。これは主にスタイルの問題ですが、筆者は Windows PowerShell のアプローチを本当に気に入っています。Windows PowerShell の設計者は、ファイル リダイレクションを理由にこのアプローチを選択しました。たとえば、次のような処理を行うとします。

get-childitem > output.txt
この場合、> 演算子は Boolean の比較ではなくファイル リダイレクションを意図しています。このため、bash および Perl で使用可能な演算子が採用されました。

テスト対象の Distance メソッドのインスタンス バージョンの呼び出し方法については、前のセクションで説明しました。静的メソッドを呼び出すには、メソッド名の前に角かっこで囲んだ名前空間と、それに続けてコロン文字のペアを付けます (C++ の場合と同様です)。

[MyPointLib.Point]::Distance($p1,$p2)
インスタンスおよび静的 Distance メソッドの結果は double 型であるため、実際の結果を予想される結果と比較する前に、実際の結果を小数点以下 2 桁に書式設定します。

$a = “{0:f2}” -f $actual
基本的に、これは "変数 $a に変数 $actual の値を格納し、小数点以下 2 桁に書式設定する" ことを意味します。

ここで、実際の結果を予想される結果と比較し、テスト結果を表示します。

if ($a -eq $expected) {
  write-host “ Pass “ -backgroundcolor green -foregroundcolor darkblue
}
else {
  write-host “*FAIL*” -backgroundcolor red
}
write-host cmdlet の -backgroundcolor および -foregroundcolor スイッチ パラメータの使用方法に注意してください。これらのパラメータを使用すると、出力を色付きテキストで簡単に生成できます。

Windows PowerShell の非常に便利な機能は、言語のエラー処理機能です。Windows PowerShell trap キーワードを使用して、スクリプトでスローされた例外を処理しました。

trap {
  write-host “Fatal error trapped at case “ $caseID
  continue
}
ここでは、メイン処理ループの内部で trap ブロックを使用して、メッセージを単純に出力してから、continue キーワードを使用して次のループ反復の実行を継続するようスクリプトに指示しています。

このテスト ハーネスでは Point クラスの 3 つ目の Distance メソッドをテストしていません。このメソッドは列挙型を使用します。その署名を見てみましょう。

public enum DistanceKind { Euclidean, CityBlock };

public static double Distance(Point p1, Point p2, DistanceKind dk)
このバージョンの Distance 関数の呼び出しは次のようになります。

$dk = [MyPointLib.Point+DistanceKind]::Euclidean
$dist  = [MyPointLib.Point]::Distance($p1,$p2,$dk)

まとめ

Windows PowerShell には、効果的で軽量のテスト自動化スクリプトの作成に必要な機能がすべて揃っています。これらの機能には、制御構造の完全なセット、変数を明示的に型指定する機能、エラー処理機能、および単純な cmdlet を呼び出して、通常は数行のコードを必要とする機能を生成する機能が含まれます。

Windows PowerShell は無料で、microsoft.com/powershell から別途インストールできます。Windows® XP、Windows Vista™、Windows Server® 2003、およびコードネーム "Longhorn" という次のバージョンの Windows Server を実行しているマシンにインストールできます。

「だけど、James」とあなたは言います。「他の方法でもモジュール テストは実行できるよ。Windows PowerShell を使用する利点は何かな」と。他の方法が時代遅れだというわけではありませんが、Windows PowerShell の方が適切な場合があります。コマンド ラインからアドホックな対話型 .NET ライブラリ テストを行うのに特に役立ちます。

Windows PowerShell が登場する前は、.NET ベース ライブラリをテストするための最も実用的なオプションは Visual Studio® を起動することでした。しかし、ホスト マシンに Visual Studio がインストールされていない場合や、テスト対象の .NET ライブラリでメソッドをすばやく呼び出す方法が必要な場合もありました。この 2 つの状況は、Windows PowerShell に適しています。

筆者の経験から、Windows PowerShell は非常に軽量なテスト自動化スクリプトに対しても意味があると言えます。Visual Studio には、統合された .NET Framework ヘルプおよび洗練されたテキスト エディタという利点があります。Windows PowerShell には、ほとんどのケースで非常にオーバーヘッドが少なく、より対話的であるという利点があります。

「だけど、James、別の言語を習得するにはどれぐらい時間がかかるかな」。Windows PowerShell は直感的で学習しやすいように設計されています。早期採用者から、Windows PowerShell は非常に簡単に使用できるという報告がありました。

ご意見やご質問は、

James testrun@microsoft.com.

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


セキュリティの概要: Windows Vista のイベント

セキュリティの概要
Windows Vista のイベント
Keith Brown

コンテンツ
イベントと XML
イベント マニフェスト
テンプレートとイベント
リソース DLL をビルドおよび登録する
実行時にイベントを生成する
まとめ

先月のコラムでは、Windows NT® 以降に登場したイベント システムを詳しく検討し、Microsoft® .NET Framework 2.0 でそれを使用する方法を示しました。このコラムでは、新しいイベント システムである Windows® Eventing 6.0 について説明します。Windows Vista™ にはこの機能が備えられているため、すぐにでもお試しいただけます。

図 1 に、Windows Vista の新しいイベント ビューアを示します。以前のバージョンからの変更点のうち、最も大きなものの 1 つにログの数があります。[Windows ログ] フォルダに、従来のイベント ログが保存されています。アプリケーション、セキュリティ、システムの他に、アプリケーション セットアップ プログラム用に設計されたセットアップ ログなどの新しいログがいくつかあります。特定のアプリケーションとサービス用に作成されたログのフォルダもあります。ここに多くのログがあるようには見えないかもしれませんが、[Microsoft] サブフォルダを開き、[Windows] サブフォルダを開くと、Windows Vista オペレーティング システムのさまざまなコンポーネントに固有のログが 50 個以上あるのがわかります。たとえば、バックアップ サービス固有のログや、WinLogon サービスに固有のログなどがあります。

図 1 Windows Vista イベント ビューア
図 1 Windows Vista イベント ビューア(画像を縮小するには、ここをクリックします)
図 1 Windows Vista イベント ビューア
図 1 Windows Vista イベント ビューア (画像を拡大するには、ここをクリックします)

ログの数が非常に多いため、最初、管理者が何かを探すにはどうすればよいのだろうかと疑問に思いました。しかし、[概要] ページをしばらく見た後で、この新しいイベント ビューアでは、必ずしも同じログにはないイベントを 1 つのビューに収集できることがわかりました。エラー イベントのリストを展開することにより、アプリケーション、システム、および仮想サーバーのログなど、その他の特定のログからのイベントが表示されます。

旧イベント ビューアでは、イベント レベル (情報、エラーなど) やイベント ソース (イベント プロバイダとも呼ばれる) などの基準に基づいてログをフィルタ処理できました。新しいイベント ビューアでは同様のフィルタ処理が可能ですが、それぞれ任意の数のログからのイベントを集計できる保存済みビューも作成できます。管理者が関連メッセージを簡単に検索できるように、アプリケーション開発者はアプリケーションと共に独自のカスタム ビューを出荷できるようになりました。たとえば、筆者は [カスタム ビュー] というフォルダに、過去 1 時間に発生したすべてのログのすべてのエラー イベントのビューを作成しました。また、このアプリケーションのサンプル コードのソースから発生したすべてのログの管理イベントまたは操作イベントを検索する 2 つ目のカスタム ビューを作成しました。

イベントと XML

カスタム ビューのフィルタを作成する場合、実際には、ビューのイベントを選択するためにイベント システムで使用される一連の XPath クエリを作成します。次に、筆者のカスタム ビューの 1 つのビュー定義を示します。読みやすくするために空白を追加し、一部のテキストを省略してありますが、何が行われているかはおわかりいただけると思います。

<QueryList>
  <Query Id=”0” Path=”Application”>
    <Select Path=”Application”>
      *[System[Provider[@Name=’...’]
      and (Level=1 or Level=2)]]
    </Select>
    <Select Path=”MyCompany-...”>
      *[System[Provider[@Name=’MyCompany-...’]
      and (Level=1 or Level=2)]]
    </Select>
  </Query>
</QueryList>

各 Select 要素には Path があることに注意してください。これは、XPath クエリの実行対象のログを示します。つまり、1 つのビューに複数のログからのイベントをホストできることは明らかです。次に、クエリ自体を見てみましょう。このクエリは、System 要素の Provider 子要素に特定の Name 属性および 1 または 2 の Level 属性を持つイベントをすべて選択します。イベントが致命的、エラー、情報、またはその他のタイプであるかどうかは、Level の数値によって表されます。

待ってください。イベントが XML として表現されるようになったのかと思っていませんか。多くの開発者と同様に筆者は既に XML に大きな投資をしたため、イベントの処理が XML の処理と同じぐらい簡単であればすばらしいと思いました。もちろん、これを見たときに最初に心配したのはログのサイズでした。XML は、最も簡潔なデータ表現ではありません。しかし、Windows Eventing の本質をもう少し深く掘り下げてみるとわかるように、イベント データは実際にはバイナリ形式で格納され、オンデマンドでのみ XML としてレンダリングされます。また、筆者の前回のコラムを読んだ読者は、より冗長なデータ (イベントを人間が判読できるようにする文字列) に作り変えたメッセージ ファイルについて学びました。これらの文字列は、イベントのデータの大半を構成することが多く、それらをメッセージ ファイルに作り変えることは、ログのスペースの節約とローカライゼーションのために、新しいイベント システムでも依然として重要な機能です。

図 2 に、イベントが XML でどのように見えるかを示します。これは失敗したログオン監査のイベントであり、明らかに筆者は最近自分のパスワードをタイプミスしました。ここでも、読みやすくするために若干省略してありますが、通常のイベントがどのように見えるかをご覧いただけると思います。

Figure 2 イベント (Close Figure 2)
<Event xmlns=”http://schemas.microsoft.com...”>
 <System>
   <Provider Name=”...-Auditing” Guid=”...” />
   <EventID>4625</EventID>
   <Version>0</Version>
   <Level>0</Level>
   <Task>12544</Task>
   <Keywords>0x8010000000000000</Keywords>
   <TimeCreated SystemTime=”2007-01-15...” />
   <EventRecordID>22</EventRecordID>
   <Correlation />
   <Execution ProcessID=”588” ThreadID=”5736” />
   <Computer>mario</Computer>
 </System>
 <EventData>
   <Data Name=”SubjectUserSid”>S-1-5-18</Data>
   <Data Name=”SubjectUserName”>MARIO$</Data>
   <Data Name=”SubjectDomainName”>WORKGROUP</Data>
   <Data Name=”SubjectLogonId”>0x3e7</Data>
   <Data Name=”TargetUserSid”>S-1-0-0</Data>
   <Data Name=”TargetUserName”>Keith</Data>
   <Data Name=”TargetDomainName”>MARIO</Data>
   ...
  </EventData>
</Event>

このイベントには、2 つの主要要素があります。最初の部分である System には、イベント自体に関する情報だけでなく、イベントが生成された環境に関する情報も含まれています。プロセス ID とスレッド ID がコンピュータ名 (Provider 要素の Name 属性) と共に記録されていることに注意してください。2 番目の部分である EventData には、イベントのパラメータがあります。これは、イベントを生成したプロセス (この場合はローカル セキュリティ機関サブシステム) によって挿入された実際のデータです。

イベント ビューアのフィルタが XPath クエリを使用してビューに表示するイベントを判断する方法を思い出してください。前に示したクエリは System 要素のみを参照していましたが、ここではクエリが EventData 要素をも掘り下げて、フィルタが以前よりもはるかに正確になっている様子がわかります。

ここでも、図 2 のイベントを検証するとわかるように、イベントに通常関連付けるメッセージのわかりやすくローカライズされたテキストは、いずれも実際にはイベント自体に格納されないことに注意してください。このテキストは、プロバイダのメッセージ ファイルに格納されています。また、イベント自体は XML に格納されず、バイナリ レコードに格納されます。このため、参照している XML とイベントのメッセージ テキストはすべて、筆者がイベント ビューアでイベントを検証したときに取り出された、つまりレンダリングされたものです。EvtRender API を呼び出すことで、同じ処理をプログラムで行うことができます。

新しいイベント システムが管理者の視点からどのように見えるかが見えてきたところで、さらに掘り下げて、Windows Vista と Windows Server® コードネーム "Longhorn" でこれらの新しいイベントを生成する処理を見てみましょう。

イベント マニフェスト

従来のメッセージ ファイルは、適切に文書化された形式に準拠し、メッセージのローカライズに効果的です。一方、メッセージ ファイルに含まれる情報は、Windows Vista に導入されたすべての新しい属性では特に、イベントを記述するのに不十分です。また、.mc ファイルを読み取る際に、どの文字列がイベントに対応し、どの文字列が対応していないかを知ることはできません。

さいわい、Windows Vista には、メッセージとイベントを定義するための新しい形式が導入されています。この新しい形式によってイベントとそのすべての属性を完全に記述できるだけでなく、XML をベースとしているため、非常に簡単に解析できます。これを使用して、前回のコラムで手作業で作成したようなイベントのヘルパ クラスを生成できます。いずれにしても、この種のコードは手作業で記述したくありません。

一連の Windows イベントを定義するには、マニフェストを作成する必要があります。マニフェストは、生成するイベントの記述など、一連のメタデータを含む XML ファイルです。図 3 では、このコラムに付属のサンプル コード用に作成したマニフェストの例を示します。

Figure 3 サンプル マニフェスト ファイル (Close Figure 3)
<instrumentationManifest
  xmlns=”http://schemas.microsoft.com/win/2004/08/events”>

  <instrumentation xmlns:xs=”http://www.w3.org/2001/XMLSchema”
    xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
    xmlns:win=”http://manifests.microsoft.com/win/2004/08/windows/events”>

    <events>

      <!-- Publisher Info -->
      <provider name=”MyCompany-MyApp-MyComponent”
        guid=”{559b3019-e149-441f-b59b-d5ac6b87c7c3}”
        symbol=”MY_PUBLISHER” resourceFileName=”...\events.dll”
        messageFileName=”...\events.dll”>

        <channels>

          <!--  import the Application log defined in winmeta.xml -->
          <importChannel chid=”admin” name=”Application” />

          <!--  define my own channels -->
          <channel chid=”op”
            name=”MyCompany-MyApp-MyComponent/Operational”
            type=”Operational” isolation=”Application” enabled=”true”/>
        </channels>

        <!-- Event Templates -->
        <templates>
          <template tid=”dbConnFailed”>
            <data name=”DatabaseName” inType=”win:UnicodeString” />
            <data name=”ConnectionString”
                  inType=”win:UnicodeString” />
            <UserData>
              <DatabaseConnectionFailure xmlns=”http://example.org”>
                <name>%1</name>
                <connectionString>%2</connectionString>
              </DatabaseConnectionFailure>
            </UserData>
          </template>
        </templates>

        <!-- All the Events that can be published by this Publisher -->
        <events>
          <event value=”1001” level=”win:Informational” channel=”op”
            symbol=”MYEVENT_APP_STARTED”
            message=”$(string.msgAppStarted)” />
          <event value=”1002” level=”win:Informational” channel=”op”
            symbol=”MYEVENT_APP_STOPPED”
            message=”$(string.msgAppStopped)” />
          <event value=”1003” level=”win:Critical” channel=”admin”
            symbol=”MYEVENT_DB_CONN_FAILED” template=”dbConnFailed”
            message=”$(string.msgDatabaseConnectionFailed)” />
          <event value=”1004” level=”win:Critical” channel=”op”
            symbol=”MYEVENT_DB_CONN_FAILED_DETAIL”
            template=”dbConnFailedDetail”
            message=”$(string.msgDatabaseConnectionFailed)” />
        </events>

      </provider>
    </events>
  </instrumentation>
  <localization>
    <resources culture=”en-US”>
      <stringTable>

      <!-- This is how event data can be used as part of
           Message String -->
        <string id=”msgAppStarted” value=”MyApp started.” />
        <string id=”msgAppStopped” value=”MyApp stopped.” />
        <string id=”msgDatabaseConnectionFailed” value=”Failed to connect
         to the %1 database using the following connection string: %2.
         Check to see that the database is available, or update web.config
         with a new connection string in the appSettings section.” />

      </stringTable>
    </resources>
  </localization>
</instrumentationManifest>

マニフェストのさまざまなセクションを見てみましょう。最初のセクションでは、プロバイダを定義します。このプロバイダには、管理者が認識できる一意の GUID と名前を割り当てる必要があります。発行元名の標準形式は [CompanyName]-[ApplicationName]-[ComponentName] です。ご覧のように、発行元名には、メッセージ定義を含む実行可能ファイルへの完全パスを提供する必要のある属性がいくつかあります。一方、イベント ログ API は環境変数を展開するため、"%ProgramFiles%\MyApp\events.dll" のような文字列を使用できます。これらのパスは、ユーザーがプログラムをインストールするために選択した場所によって異なる場合があるため、セットアップ プログラムでマニフェストを修正する必要があります。

次のセクションでは、一連のチャネルを定義します。チャネルはアプリケーションがイベントを送信する場所ですが、マニフェストでは、送信に使用するチャネルが各イベントで事前に定義されるため、実行時にチャネルについて考える必要はありません。イベントを起動するだけで、Windows Eventing プラミングによって、イベントが指定されたチャネルにルーティングされます。チャネルのもう一方の側には何があるでしょう。ほとんどの場合、チャネルのタイプに応じてイベント ログまたは高パフォーマンス トレース ログがあります。Windows Eventing についてさらに学習していくと、チャネルとログの違いをぼかしている資料が見つかる場合がありますが、驚かないでください。イベント チャネルの概念は Windows にとって新しいものであり、将来はアーキテクチャ的に重要になる可能性があります。

インポートすることでアプリケーション チャネルを参照したことに注意してください。これは、図 1 に見られるグローバル アプリケーション イベント ログです。次に、新しいチャネルを作成し、最終的に新しいイベント ログ ファイルを作成します。これは、前回のコラムで作成したカスタム イベント ログとよく似ており、グローバル アプリケーション ログを乱雑にしかねない操作イベントの格納に使用されます。

チャネルには 4 つの種類があります。管理チャネルと操作チャネルは従来のイベント ログを指し、サブスクリプションおよびイベント転送サービスで処理されることがあります。管理ログと操作ログの違いは、主にその使用方法にあります。管理イベントは不定期に発生し、対処可能です。管理者が注目する必要のある問題がある場合に発生します。操作イベントは、より平凡な My app 起動/My app 停止タイプのイベントです。

他の 2 種類のチャネルである分析およびデバッグは、多くのイベントを生成すると想定されるため、イベント ログにダンプされない高パフォーマンス チャネルです。これらのチャネルは、Event Tracing for Windows (ETW) という高パフォーマンスのイベント トレース形式で使用されます。イベント ビューアでチャネルを有効にすることでトレースを開始できます。これにより、ビューアでイベントを表示することもできます ([表示] メニューで [Show Analytic and Debug channels] (分析およびデバッグ チャネルの表示) を選択する必要があります)。詳細については、2007 年 4 月発行の MSDN® Magazine で「ETW でのデバッグおよびパフォーマンス チューニングの改善」を参照してください。

テンプレートとイベント

次に、マニフェストにはテンプレート セクションがあります。このセクションでは、イベントの <EventData> セクションの構造を定義します。イベント構造をテンプレートに作り変えることで、そのテンプレートをさまざまなイベントに再利用できます。dbConnFailed テンプレートをどのように定義したかを見てみましょう (図 3 を参照)。最初に、2 つのデータ要素があります。これは、イベントの形式的な引数リストのようなものです。アプリケーションがイベントの生成時に提供すると予想されるデータ セットを順序どおりに一覧表示します。

形式的な引数リストの後に、<UserData> というオプションのセクションがあります。UserData 要素内に挿入する XML はイベントの EventData セクションに表示されるため、このセクションではマニフェストは終了し、創造性が生かされる場となります。このテンプレートはデータベース接続エラーに対するものであるため、上位レベルのイベント データ タグを DatabaseConnectionFail-ure と呼びました。このタグはマニフェスト ファイルのスキーマの既知の部分ではなく拡張であるため、独自の XML 名前空間にも挿入します。独自のテンプレートでこの処理を忘れた場合は、メッセージ コンパイラがエラーで停止します。データが入るプレースホルダがわかりますか。%1 と %2 は、それぞれ DatabaseName 引数と ConnectionString 引数を示します。

テンプレート セクションを離れる前に、形式的なパラメータ リストが厳密に型指定されていることを指摘しておきます。図 4 では、既製の型のリストを示しました。これらの型とその他一連のメタデータは、winmeta.xml で定義されています。このファイルは、Windows SDK for Windows Vista および Windows Server "Longhorn" で提供されています。このファイルは SDK の Include ディレクトリにあり、メッセージ コンパイラはマニフェストの処理にこのファイルを必要とするため、マニフェストをコンパイルしているディレクトリにコピーするか、-W フラグを使用して winmeta.xml の完全パスを指定する必要があります。この処理を忘れた場合、MC.EXE によって指摘されます。

Figure 4 定義済み入力型 (Close Figure 4)

型 | 説明
win:UnicodeString | NULL で終了する UNICODE 文字列。
win:AnsiString | NULL で終了する ANSI 文字列。
win:Int8 | 符号付き 8 ビット整数。
win:UInt8 | 符号なし 8 ビット整数。
win:Int16 | 符号付き 16 ビット整数。
win:UInt16 | 符号なし 16 ビット整数。
win:Int32 | 符号付き 32 ビット整数。
win:UInt32 | 符号なし 32 ビット整数。
win:Int64 | 符号付き 64 ビット整数。
win:UInt64 | 符号なし 64 ビット整数。
win:Float | IEEE 4 バイト浮動小数点数値。
win:Double | IEEE 8 バイト浮動小数点数値。
win:Boolean | 0 が false、1 が true の 32 ビット値。
win:Binary | 可変サイズのバイナリ データ。
win:GUID | DCE 準拠の 128 ビット UUID。XML 仕様:{12345678-1234-4667-1234-123456789abc}
win:Pointer | 現在のプラットフォーム (32 ビットまたは 64 ビット) にサイズ設定されたポインタ。
win:FILETIME | Windows FILETIME 構造体。
win:SYSTEMTIME | Windows SYSTEMTIME 構造体。
win:SID | 自己相対 Windows SID 構造体。XML 仕様:S-1-0-0。
win:HexInt32 | 符号なし 32 ビット整数の 16 進数表現。
win:HexInt64 | 符号なし 64 ビット整数の 16 進数表現。

メッセージ コンパイラについては、Windows Vista および Windows Server "Longhorn" SDK に付属の MC.EXE バージョンを使用していることを確認する必要があります。実験中に簡単に確認できます。ここでは、Visual Studio コマンド プロンプトではなく Windows SDK コマンド プロンプトからメッセージ コンパイラを実行することのみを確認します。

イベント セクションに進みましょう。ここには、定義済みの 4 つのイベントがあり、それぞれに一意の数値があります。これはイベント ID であり、定義するイベントごとに異なります。次に、イベントの重要度を決定するレベルがあります。winmeta.xml には、従来の Windows イベント ログの処理に携わってきた人ならよく知っている一連の定義済みレベルがあります。次に、前に定義したチャネルの 1 つを表すチャネルがあります。これは、イベントが移動する初期の方向を定義します。イベント転送やサブスクリプションなど、このコラムの説明対象外の機能でイベントの到着場所を最終的に判断する場合がありますが、ほとんどの場合、イベントは単純にチャネル定義で指定されたログに記録されます (イベントは、いずれにしてもログに書き込まれ、転送ではイベントのコピーが使用されます)。

各イベントには、そのイベントを説明する判読可能な文字列も含まれます。テキストをメッセージ自体にハードコーディングする代わりに、マニフェストの最後のセクションに見られる文字列テーブルを参照します。これにより、文字列をアプリケーションでサポートされる各国語にローカライズできます。また、データベース接続エラー メッセージに、テンプレートにあったようなプレースホルダがどのように含まれているかに注意してください。このようにして、同じパラメータが実行時にメッセージにプラグインされます。

リソース DLL をビルドおよび登録する

従来のイベントに関する筆者の 2007 年 4 月のコラムを読んだ方には、メッセージを含むリソース DLL のビルドが Windows Vista でも同様のプロセスであることがおわかりいただけると思います。次に、サンプル コードのリソース DLL をビルドするバッチ ファイルを示します。

REM buildEventDll.cmd
mc MyManifest.xml
rc /fo events.res MyManifest.rc
link /DLL /NOENTRY /SUBSYSTEM:WINDOWS /MACHINE:X86 events.res

メッセージ コンパイラを実行する際の副作用として、C ヘッダー ファイルがマニフェストで指定したすべての記号で作成されます。使用されるこれらの記号の一部を図 5 に示します。

Figure 5 イベントを生成する (Close Figure 5)
#include <windows.h>
#include <winevt.h>
#include <evntprov.h>
# pragma comment(lib, “wevtapi.lib”)

// this header file generated by MC.EXE
#include “MyManifest.h”

void wmain() {
    REGHANDLE pub;
    EventRegister(&MY_PUBLISHER, 0, 0, &pub);
    EventWrite(pub, &MYEVENT_APP_STARTED, 0, 0);
    EventWrite(pub, &MYEVENT_APP_STOPPED, 0, 0);
    EventUnregister(pub);
}

DLL をビルドした後で、マニフェストが DLL を正しく指していることを確認します (発行元属性にはリソース DLL を指す完全パスがあることを思い出してください)。次に、wevtutil.exe という新しいツールを実行して、新しいイベント システムにアプリケーションを登録できます。この処理は、Windows SDK コマンド プロンプトから実行します。

wevtutil im MyManifest.xml
アプリケーションの登録解除もできます。

wevtutil um MyManifest.xml

マニフェストのインストール時にこのツールで行われる処理の 1 つは、リソース DLL のパスを示すレジストリ キーの書き込みです。このコラムの執筆時点では、これらのパスが正しくない場合、wevtutil.exe はエラーを生成せず、意図しない動作が発生します。不正な登録の副作用として、ログにエラー イベントが含まれたり、イベントは正しくてもメッセージが存在しなかったりすることがあります。

マニフェストを登録すると、最初のイベントを生成する前であっても、そのすべてのメタデータが管理者から使用可能になります。Windows Vista でわかったことは、新しいチャネルを定義することで作成したカスタム イベント ログは、そのチャネルをターゲットとするイベントを実際に生成するまで表示されないということです。

実行時にイベントを生成する

現在、.NET Framework には、Windows Eventing API 用のラッパーがないため、ここでいくつかの C++ コードを示します。Microsoft の人員は読者が使用できるサンプル ラッパーを鋭意作成中です。.NET Framework の次のリリース (コードネーム "Orcas") ではこの機能が直接公開される予定ですので、もうしばらくお待ちください。

イベントの生成を開始する前に知っておく必要のある API はほんのわずかであり、図 5 のコードでその単純な例を示します。MSDN Magazine のこのコラムで使用可能なサンプル コードは、一歩先に進んで、イベントの引数を渡すだけでなく、これらの API で返されるエラーのチェックも行います。

コードの最初の行は、イベント システムと、前に wevtutil.exe を使用して登録したメタデータに接続しています。アプリケーションを停止する前に、必ず登録解除してください。このコードは非常に簡単に作成できます。時間の大半は、マニフェストの作成に費やされますが、それが本来あるべき姿です。

まとめ

古いイベント手法を使用するアプリケーションについてはどうでしょうか。Windows Vista と Windows Server "Longhorn" でも機能するでしょうか。はい、機能します。イベントの構造はあまり制御できませんが、アプリケーション イベント ログに記述したすべてのイベントは、新しいグローバル アプリケーション イベント ログに正常に表示され、従来のイベント API と .NET クラスはすべてこれらのプラットフォームで動作します。

Windows Server "Longhorn" をターゲットとする新しいコードを作成している場合は、新しい API を使用して、Windows Eventing 6.0 の利点をフルに活用することを真剣に検討する必要があります。

ご質問やご意見は

Keith briefs@microsoft.com.

まで英語でお送りください。
Keith Brownは、Microsoft .NET の第一のトレーニング プロバイダである Pluralsight の共同設立者の 1 人です。Keith は、Pluralsight の Applied .NET Security コースの作成者で、『Windows セキュリティに関する .NET 開発者ガイド』(印刷物および Web で提供) をはじめとする数冊の書籍の著者でもあります。詳細については、www.pluralsight.com/keith を参照してください。


Foundation: WCF トランザクションの伝達

Foundation
WCF トランザクションの伝達
Juval Lowy

コードのダウンロード : : Transactions2007_05.exe (211KB)
コンテンツ
トランザクション フローの構成
トランザクション フローと操作コントラクト
アンビエント トランザクション
トランザクション サービスのプログラミング
トランザクションの伝達モード
クライアント トランザクションとサービス トランザクション
投票と完了
まとめ

トランザクションは、堅牢で品質の高いサービス指向アプリケーションを構築するための重要な部分を成します。Windows® Communication Foundation を使用すると、サービス開発者は、簡潔な宣言型のトランザクションを使用して、トランザクション フローや投票などのパラメータを完全にサービス スコープの外部で構成できます。また、Windows Communication Foundation では、クライアント アプリケーションでトランザクションを作成し、そのトランザクションをサービスの境界を越えて伝達できます。このコラムでは、Windows Communication Foundation でのトランザクションの伝達、投票の構成、およびこれによって生じるプログラミング モデルについて説明します。

トランザクション フローの構成

Windows Communication Foundation では、サービスの境界を越えてトランザクションを伝達できます。これにより、サービスがクライアントのトランザクションに参加し、クライアントが複数のサービスの操作呼び出しを同一トランザクション内に含めることが可能になります。クライアントは、Windows Communication Foundation サービスである場合と、そうでない場合があります。クライアント トランザクションをサービスに伝達するかどうかの決定は、バインディングと操作コントラクトの構成により制御されます。

このコラムでは、クライアントのトランザクションをサービスに伝達できるバインディングのことを、トランザクション対応のバインディングと呼びます (伝達可能として構成されている場合)。具体的には、NetTcpBinding、NetNamedPipeBinding、WSHttpBinding、WSDualHttpBinding、および WSFederationHttpBinding のみがトランザクション対応のバインディングです。既定では、トランザクション対応のバイディングは、トランザクションを伝達しません。Windows Communication Foundation の他のほとんどの設定と同様に、トランザクションの伝達を許可するかどうかは、選択可能な設定となっています。

組織やビジネスの境界を越えて受信する可能性があるトランザクションの承諾には、サービスのホストまたは管理者による明示的な同意が必要です。トランザクションを伝達するには、サービスのホスト側とクライアント側の両方のバインディングで、トランザクションを明示的に有効にする必要があります。次に示すように、すべてのトランザクション対応のバインディングでは、ブール型プロパティの TransactionFlow が指定され、既定で false に設定されています。

public class NetTcpBinding : Binding,...
{
   public bool TransactionFlow {get;set;}
   //More members
}
伝達を可能にするには、プログラムまたはホストの構成ファイルで、このプロパティを true に設定します。たとえば、構成ファイルを使用する場合は、次のコードでプロパティを設定します。

<bindings>
   <netTcpBinding>
      <binding name = “TransactionalTCP” transactionFlow = “true” />
   </netTcpBinding>
</bindings>

TransactionFlow プロパティの値は、サービス メタデータに公開されない点に注意してください。Visual Studio® 2005 を使用してクライアントの構成ファイルを生成する場合は、手動でトランザクション フローを有効にする必要があります。

トランザクション フローと操作コントラクト

トランザクション対応のバインディングを使用し、トランザクション フローを有効にしても、サービスのすべての操作でクライアントのトランザクションが使用されるかどうか、また、伝達するトランザクションがクライアント側にあるかどうかも明確ではありません。このような決定には、コントラクトでのクライアントとサービスの同意が必要です。この同意のために、Windows Communication Foundation では、クライアントのトランザクション フローをサービスに伝達するかどうか、および伝達する場合はそのタイミングを制御する TransactionFlowAttribute メソッド属性を提供しています。

public enum TransactionFlowOption
{
   Allowed,
   NotAllowed,
   Mandatory
}

[AttributeUsage(AttributeTargets.Method)]
public sealed class TransactionFlowAttribute :
   Attribute,IOperationBehavior
{
   public TransactionFlowAttribute(TransactionFlowOption flowOption);
}
Windows Communication Foundation では、トランザクション フローについての決定を操作ごとに行う必要があるため、TransactionFlow 属性はメソッド レベルの属性であることに注意してください。

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void MyMethod(...);
}
これは意図的なものであり、クライアントのトランザクションを使用するメソッドもあれば、そうでないメソッドもあるという細かい制御を可能にするためです。

TransactionFlow 属性の値は、公開されるサービスのメタデータに含まれます。コントラクト定義をインポートすると、構成済みの値がインポートした定義に保存されています。これらの属性は、TransactionFlowOption.NotAllowed、TransactionFlowOption.Allowed、および TransactionFlowOption.Mandatory です。

属性 TransactionFlowOption.NotAllowed を使用して、操作がトランザクション フローを禁止するように構成されている場合は、クライアントのトランザクションをサービスに伝達できません。バインディングでトランザクション フローが有効化され、クライアントにトランザクションがある場合でも、トランザクションは暗黙的に無視され、サービスに伝達されません。この結果、サービスではクライアントのトランザクションが使用されず、サービスとクライアントは任意のバインディングと構成を選択できます。TransactionFlowOption.NotAllowed は、TransactionFlow 属性の既定の TransactionFlowOption 値です。

TransactionFlowOption.Allowed を使用して、操作がトランザクション フローを許可するように構成されている場合、クライアントにトランザクションがあると、サービスの境界を越えてクライアントのトランザクションが伝達されます。ただし、トランザクションが伝達されても、サービスでは、クライアントのトランザクションを使用することも、使用しないこともあります。TransactionFlowOption.Allowed を選択した場合、トランザクションに対応したバインディングかどうかにかかわらず、サービスは任意のバインディング構成を使用できますが、クライアントとサービスのバインディング構成は一致している必要があります。サービスの操作でトランザクション フローが許可されていても、バインディングでトランザクション フローが禁止されている場合は、クライアント側でもバインディングでトランザクション フローを禁止する必要があります。クライアント トランザクションの伝達を試みると、サービスがメッセージのトランザクション情報を理解できないため、エラーが発生します。ただし、サービス側のバインディング構成がトランザクション フローを許可するように設定されていても、クライアントは、クライアント側での伝達を許可する場合と、許可しない場合があります。このため、クライアントは、バインディングでのサービスの TransactionFlow が true に設定されている場合でも、クライアントの TrasactionFlow を false に設定できます。

操作が TransactionFlowOption.Mandatory として構成されている場合、サービスとクライアントでは、トランザクションに対応したバインディングを使用して、トランザクション フローを有効にする必要があります。Windows Communication Foundation は、サービスの読み込み時にこの要件を検証し、サービスのいずれかのエンドポイントが不適合である場合、InvalidOperationException をスローします。TransactionFlowOption.Mandatory では、クライアントのトランザクションをサービスに伝達する必要があります。トランザクションなしにサービスの呼び出しを試みると、クライアントで例外が発生します。トランザクションの伝達が必須の場合、クライアントのトランザクションは常にサービスに伝達されます。この場合も、サービスでは、クライアントのトランザクションを使用することも、使用しないこともあります。

クライアント トランザクションをサービスに伝達するには、その性質上、サービスがクライアント トランザクションを中止できるようにする必要があります。これは、一方向の操作呼び出しには応答メッセージがないため、一方向の操作によってクライアント トランザクションをサービスに伝達できないことを暗に示しています。Windows Communication Foundation では、サービスの読み込み時に検証を行い、一方向の操作が TransactionFlowOption.NotAllowed 以外のいずれかの値に設定されていると、例外が発生します。

//Invalid definition:
[ServiceContract]
interface IMyContract
{
   [OperationContract(IsOneWay = true)]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void MyMethod(...);
}

アンビエント トランザクション

Microsoft® .NET Framework 2.0 で導入され、System.Transactions 名前空間に定義されている Transaction クラスは、Windows Communication Foundation のトランザクションを表すために使用されます。

[Serializable]
public class Transaction : IDisposable,ISerializable
{
   public static Transaction Current {get;set;}
   public TransactionInformation TransactionInformation {get;}
   public void Dispose();

   //More members
}

開発者と Transaction クラスの直接の対話は、めったに必要ではありません。.NET Framework 2.0 では、この対話の代わりに、アンビエント トランザクションという概念を定義します。アンビエント トランザクションは、現在実行中のコードに使用されるトランザクションです。アンビエント トランザクションへの参照を取得するには、Transaction の静的な Current プロパティを呼び出します。

Transaction ambientTransaction = Transaction.Current;
アンビエント トランザクションが存在しない場合、Current は null を返します。すべてのコードは、クライアントであれサービスであれ、常にアンビエント トランザクションを取得できます。アンビエント トランザクション オブジェクトは、スレッド ローカル記憶域に格納されます。この結果、スレッドが同一呼び出しチェーン上の複数のオブジェクトやメソッドを処理するとき、すべてのオブジェクトおよびメソッドがアンビエント トランザクションにアクセスできます。

Windows Communication Foundation のコンテキストでは、アンビエント トランザクションは非常に重要です。アンビエント トランザクションが存在する場合、すべての Windows Communication Foundation リソース マネージャ (SQL Server™ や揮発性リソース マネージャなど) は、アンビエント トランザクションに自動的に参加します。クライアントが Windows Communication Foundation サービスを呼び出したとき、クライアントのアンビエント トランザクションが存在し、なおかつバインディングとコントラクトがトランザクション フローを許可するように構成されている場合、アンビエント トランザクションがサービスに伝達されます。

Transaction クラスは、ローカル トランザクションと分散トランザクションの両方で使用されます。分散トランザクションをサポートする必要がない場合、Windows Communication Foundation はローカル トランザクション マネージャを使用します。クライアントがサービスへのトランザクションの伝達を試みた場合、または複数の持続性リソースが関与している場合、トランザクションは分散トランザクションに昇格し、分散トランザクション管理 (DTC) によって管理されます。各トランザクションには、ローカル トランザクションと分散トランザクションを識別するために、2 つの識別子があります。トランザクションの識別子を取得するには、Transaction クラスの TransactionInformation プロパティにアクセスします。TransactionInformation は、TransactionInformation 型であり、次のように定義されます。

public class TransactionInformation
{
   public Guid DistributedIdentifier {get;}
   public string LocalIdentifier {get;}
   //More members
}

LocalIdentifier は、ローカル トランザクションを表し、常に null 以外の値を持ちます。DistributedIdentifier は、トランザクションが分散トランザクションに昇格していなければ、Guid.Empty である場合もあります。これらの識別子は、ログ、トレース、および分析で主に使用されます。このコラムでは、構成の結果として生じるトランザクション フローを示すための便利な方法として、これらの識別子がコード内で使用されています。

トランザクション サービスのプログラミング

Windows Communication Foundation は、簡潔で洗練された宣言プログラミング モデルをサービスに対して提供します。既定では、サービス クラスとその操作には、アンビエント トランザクションがありません。この状況は、クライアント トランザクションがサービスに伝達される場合にも該当します。

図 1 に示されたサービスについて考えます。必須のトランザクション フローによって、クライアント トランザクションの伝達が保証されている場合でも、サービスのアンビエント トランザクションは null です。サービスでアンビエント トランザクションを持つためには、次のコードで示すように、各コントラクト メソッドで、OperationBehavior 属性の TransactionScopeRequired プロパティを使用して、Windows Communication Foundation がトランザクションのスコープでメソッドの本体を指定する必要があります。

Figure 1 簡単なサービス コントラクト (Close Figure 1)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Mandatory)]
   void MyMethod(...);
}

class MyService : IMyContract
{
   public void MyMethod(...)
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction == null);
   }
}

[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationBehaviorAttribute : Attribute,...
{
   public bool TransactionScopeRequired {get;set;}
   //More members
}
TransactionScopeRequired の既定値は false です。このため、既定では、サービスにはアンビエント トランザクションがありません。TransactionScopeRequired を true に設定すると、操作にアンビエント トランザクションが提供されます。

class MyService : IMyContract
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod()
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction != null);
   }
}

クライアント トランザクションをサービスに伝達する場合、Windows Communication Foundation はクライアント トランザクションを操作のアンビエント トランザクションとして設定します。それ以外の場合は、Windows Communication Foundation がこの操作のために新しいトランザクションを作成し、新しいトランザクションをアンビエント トランザクションとして設定します。

図 2 は、Windows Communication Foundation サービスがバインディング構成、コントラクト操作、およびローカル操作の behavior 属性に対して使用するトランザクションを示しています。

図 2 では、非トランザクション クライアントがサービス 1 を呼び出しています。操作コントラクトは、TransactionFlowOption.Allowed により構成されています。バインディングでトランザクション フローが有効になっている場合でも、クライアントのトランザクションがないと、トランザクションは伝達されません。サービス 1 の操作ビヘイビアは、トランザクション スコープを必要とする構成になっています。この結果、Windows Communication Foundation は、サービス 1 のために新しいトランザクションであるトランザクション A を作成します。次に、サービス 1 は、構成の異なる他の 3 つのサービスを呼び出します。

図 2 トランザクションの伝達
図 2 トランザクションの伝達(画像を縮小するには、ここをクリックします)
図 2 トランザクションの伝達
図 2 トランザクションの伝達 (画像を拡大するには、ここをクリックします)

サービス 2 では、バインディングでトランザクション フローが有効化され、操作コントラクトでクライアント トランザクションの伝達が必須となっています。操作ビヘイビアは、トランザクション スコープを必要とする構成になっているため、Windows Communication Foundation は、トランザクション A をサービス 2 のアンビエント トランザクションに設定します。サービス 3 の呼び出しでは、バインディングと操作コントラクトでトランザクション フローが禁止されています。ただし、サービス 3 の操作ビヘイビアは、トランザクション スコープを必要とする構成となっているため、Windows Communication Foundation は、サービス 3 のために新しいトランザクション (トランザクション B) を作成し、これをサービス 3 のアンビエント トランザクションとして設定します。サービス 3 と同様に、サービス 4 の呼び出しでは、バインディングと操作コントラクトでトランザクション フローが禁止されています。サービス 4 にはトランザクション スコープが必要でないため、このサービスにはアンビエント トランザクションがありません。

トランザクションの伝達モード

どのトランザクションがサービスで使用されるかは、バインディングのフロー プロパティ (2 つの値)、操作コントラクトのフロー オプション (3 つの値)、操作ビヘイビアのトランザクション スコープ プロパティ値 (2 つの値) によって決まります。これらを合わせると、12 とおりの構成の設定が可能になります。12 とおりの構成のうち、4 つの構成は、一貫性がないために Windows Communication Foundation で除外される構成 (バインディングでフローが無効であるが、操作コントラクトでフローが必須になっている場合など)、または単純に実現不可能な構成です。図 3 は、残りの 8 とおりの組み合わせを示しています。

Figure 3 トランザクション伝達モード (Close Figure 3)

トランザクション フローのバインディング | トランザクション - FlowOption | トランザクション - ScopeRequired | トランザクション モード
False | Allowed | False | なし
False | Allowed | True | サービス
False | NotAllowed | False | なし
False | NotAllowed | True | サービス
True | Allowed | False | なし
True | Allowed | True | クライアント/サービス
True | Mandatory | False | なし
True | Mandatory | True | クライアント

これらの 8 とおりの組み合わせから、4 つのトランザクション伝達モードが生じます。このコラムでは、この 4 つのモードを "クライアント/サービス"、"クライアント"、"サービス"、および "なし" と呼びます。図 3 では、各モードの推奨構成が緑色のフォントで示されています。アプリケーションを設計する際には、各モードをそれぞれふさわしい場所に配置する必要があります。適切なモードの選択方法を理解すれば、トランザクション サポートに対する配慮や構成に時間を費やす必要がなくなります。

"クライアント/サービス" モードは、その名前のとおり、可能であればクライアント トランザクションを使用し、クライアントのトランザクションがない場合はサービス側のトランザクションを使用します。"クライアント/サービス" モードでは、サービスがクライアントの処理内容に関する想定を最小限に抑えるため、最も分離された構成となっています。クライアントにトランザクション フローがあれば、サービスはクライアント トランザクションに参加します。クライアント トランザクションへの参加は、システム全体の一貫性を保つのに効果的です。サービスが、クライアントとは別々のトランザクションを持つ場合を想像してみましょう。この場合、2 つのトランザクションのいずれかがコミットされ、他のトランザクションが中止されると、システムの一貫性が損なわれます。

サービスがクライアント トランザクションに参加する場合、クライアントとサービスによって実行されるすべての処理は (クライアントが呼び出す可能性があるその他のサービスを含む)、1 つのアトミック操作としてコミットまたは中止されます。クライアントのトランザクションがない場合でも、サービスにはトランザクションの保護が必要であるため、このモードでは、サービスを新しいトランザクションのルートにすることにより、サービスに対して臨時のトランザクションが提供されます。ルートになったサービスは、トランザクションの有効期間を制御します。図 4 は、"クライアント/サービス" トランザクション用に構成されたサービスです。サービスは、常にトランザクションがあるとアサートしている点に注意してください。サービスは、トランザクションがクライアント トランザクションなのか、ローカルに作成されたトランザクションかどうかについて推測もアサートもできません。

Figure 4 クライアント/サービス モードの構成 (Close Figure 4)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void MyMethod(...);
}

class MyService : IMyContract
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod(...)
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction != null);
   }
}

"クライアント/サービス" モードは、サービスをスタンドアロンか、より大きなトランザクションの一部として使用できる場合に適用可能です。このモードを選択する場合は、デッドロックが起きる可能性について考慮する必要があります。具体的には、結果として生じたトランザクションがサービス側のトランザクションである場合、トランザクションごとにリソースへのアクセスが分離され、サービス側のトランザクションが新しいトランザクションとなるため、同じリソースへのアクセスを試みている他のトランザクションとのデッドロックが生じる可能性があります。"クライアント/サービス" モードを使用する場合、サービスはトランザクションのルートとなる場合と、そうでない場合がありますが、サービスがルートである場合と、クライアントのトランザクションに参加する場合とで、異なった動作が生じないようにする必要があります。

クライアント トランザクションとサービス トランザクション

"クライアント" モードでは、クライアントのトランザクションのみがサービスで使用されます。"クライアント" トランザクション モードは、設計上、サービスでクライアントのトランザクションを使用する必要があり、サービスをスタンドアロンでは使用できない場合に選択します。このモードを選択する主な動機は、デッドロックを回避し、システム全体の一貫性を最大限に保つことです。サービスがクライアントのトランザクションを共有することにより、アクセスされるすべてのリソースが同じトランザクションに参加するため、同一リソースにアクセスする他のトランザクションとの競合やそれに基づくリソースのロックがなくなり、デッドロックが生じる可能性が低くなります。単一のトランザクションであれば、トランザクションが 1 つのアトミック操作としてコミットまたは中止されるため、一貫性を最大限に保つことができます。図 5 は、"クライアント" トランザクション モードのサービス構成を示しています。

Figure 5 クライアント トランザクション モードの構成 (Close Figure 5)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Mandatory)]
   void MyMethod(...);
}

class MyService : IMyContract
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod(...)
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction.TransactionInformation.
                   DistributedIdentifier != Guid.Empty);
   }
}

メソッドは、アンビエント トランザクションが分散トランザクション、つまり、クライアントからのトランザクションであることをアサートしている点に注意してください。"サービス" モードでは、クライアントのトランザクションがあるかどうかにかかわらず、クライアントのトランザクションとは別のサービス トランザクションが常に存在します。サービスは、常に新しいトランザクションのルートになります。

"サービス" トランザクション モードは、クライアントのトランザクション スコープ外でトランザクション処理を実行する必要がある場合に選択します。たとえば、クライアント トランザクションがコミットまたは中止されたかどうかにかかわらず、ログ操作、監査操作、サブスクライバへのイベントのパブリッシュなどを実行する場合です。データベースにエラーを記録するログ登録サービスについて考えます。クライアント側でエラーが発生すると、クライアントはログ登録サービスを使用してエラーやその他のエントリを記録します。エラーが発生した場合は、エラーが記録された後、クライアント側のエラーによりクライアントのトランザクションが中止されます。サービスでクライアント トランザクションを使用しているときにクライアント トランザクションが中止されると、記録されたエラーがデータベースから破棄されるため、エラーの記録がなくなり、ログの本来の目的が失われます。

サービスが独自のトランザクションを持つように構成されていると、クライアント トランザクションが中止された場合でも、エラーは記録されます。この弱点は、当然ながら、クライアント トランザクションがコミットされてもサービス トランザクションが中止される可能性があるため、システムの一貫性が損なわれる可能性があることです。このモードの選択は、サービス トランザクションが、クライアントのトランザクションと比較して、成功およびコミットされる可能性が非常に高いという経験に基づいて行われます。ログ サービスの例は、これに該当します。さまざまな理由で失敗する可能性があるビジネス トランザクションとは反対に、確定的なログを配置すれば、多くの場合は適切に動作します。通常、"サービス" トランザクション モードを使用する場合は十分に注意を払い、2 つのトランザクション (クライアントのトランザクションとサービスのトランザクション) の一方が中止され、他方がコミットされた場合でも、システムの一貫性が損なわれないことを確認する必要があります。一般的に、このモードはログ サービスと監査サービスに適しています。

図 6 のコードは、"サービス" トランザクション モードのサービス構成を示しています。実際にローカル トランザクションを所持していることをサービスがアサートしている点に注意してください。

Figure 6 サービス トランザクション モードの構成 (Close Figure 6)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(...);
}

class MyService : IMyContract
{
   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod(...)
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction.TransactionInformation.
                   DistributedIdentifier == Guid.Empty);
   }
}

"なし" トランザクション モードは、サービスが常にトランザクションを持たないことを意味します。このモードは、サービスによって実行される処理が、あれば便利だが必ずしも必要ではなく、この処理の失敗によってクライアントのトランザクションが中止されてはならない場合に有効です。たとえば、送金の領収書を印刷するサービスでは、プリンタが用紙切れになっても、クライアント トランザクションを中止することはできません。また、カスタム動作が必要な場合にも、"なし" モードを採用することがあります。たとえば、レガシー トランザクション コードの呼び出しなど、プログラムで独自のトランザクションをサポートしたり、手動でリソースを登録する必要が生じる場合もあります。

明らかに、"なし" モードはシステムの一貫性を損なう可能性があるため、このモードの使用には危険が伴います。たとえば、呼び出し側クライアントにトランザクションがあり、"なし" として構成されたサービスを呼び出した後、クライアントがトランザクションを中止した場合、サービスがシステム状態に加えた変更はロールバックしません。このモードのもう一つの欠点は、"なし" として構成されたサービスが、別の "クライアント" トランザクション用に構成されたサービスを呼び出した場合です。呼び出し側サービスには伝達するトランザクションがないため、このような呼び出しは失敗します。

図 7 は、"なし" トランザクション モードとして構成されたサービスを示しています。サービスにはアンビエント トランザクションがないことをアサートしている点に注意してください。

Figure 7 &quot;なし&quot; トランザクション モードの構成 (Close Figure 7)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}

class MyService : IMyContract
{
   public void MyMethod()
   {
      Transaction transaction = Transaction.Current;
      Debug.Assert(transaction == null);
   }
}

"なし" モードを使用すると、トランザクション クライアントから非トランザクション サービスを呼び出すことができます。前に説明したように、"なし" モードは、ほとんどの場合、あると便利な操作のために構成されます。問題は、"なし" サービスによって例外が発生すると、呼び出し側クライアントのトランザクションが中止されることです。あると便利な操作のために、クライアントのトランザクションが中止されることは回避すべきです。これを解決するには、"なし" サービスに関連するすべての例外をクライアントがキャッチして、クライアント トランザクションへの混入を回避します。

4 つのモードのうち、"サービス" モードと "なし" モードは、少し難解なモードです。特定のシナリオでは役立ちますが、システムの一貫性が損なわれる可能性があることを忘れないでください。"クライアント/サービス" または "クライアント" トランザクション モードを使用し、また、デッドロックの可能性や一貫性への作用を考慮して、サービスをスタンドアロンとして使用できるかどうかに基づいてモードを選択してください。

投票と完了

Windows Communication Foundation は、トランザクションの伝達に関するすべての側面を担当し、リソース間でのトランザクションのコミットと中止を総合的に管理しますが、トランザクションをコミットするのか、中止する必要があるかどうかについては認識していません。Windows Communication Foundation は、システム状態に加えられた変更が、一貫性のある、理にかなった変更であるかどうかを知ることができません。参加するすべてのサービスが、トランザクションをコミットするか中止するかに対して投票する必要があります。さらに、Windows Communication Foundation は、どの時点でトランザクションが終了し、どの時点ですべてのサービスの処理が完了するのかを認識していません。これはルート サービスが示す必要があります。

Windows Communication Foundation は、サービスに代わってトランザクションのコミットまたは中止に対する投票を自動的に行うことができます。自動投票は、OperationBehavior 属性のブール型の TransactionAutoComplete プロパティによって制御されます。

[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationBehaviorAttribute : Attribute,...
{
   public bool TransactionAutoComplete {get;set;}
   //More members
}
TransactionAutoComplete プロパティの既定値は true です。true に設定すると、操作中に未処理の例外がなければ、Windows Communication Foundation はトランザクションをコミットするための票を自動的に投じます。未処理の例外がある場合、Windows Communication Foundation はトランザクションを中止するための票を投じます。セッションを持つサービスのみが TransactionAutoComplete を false に設定できます。これはプログラミング モデルに非常に大きな影響を与えますが、このコラムでカバーすべき範囲を超えているので、ここでは取り扱いません。

トランザクションは、トランザクションの開始者によって終了します。トランザクションがないクライアント、またはトランザクションをサービスに伝達しないクライアントが、TransactionScopeRequired を true に設定したサービス操作を呼び出す場合を考えます。このサービス操作は、トランザクションのルートになります。ルート サービスは、他のサービスを呼び出してトランザクションを伝達できます。トランザクションは、ルート操作のトランザクションが完了したときに終了します。このような理由で、TransactionAutoComplete という名前が付けられています。TransactionAutoComplete は、投票するだけでなく、ルート サービスのためにトランザクションを完了し、トランザクションを終了します。ルート操作から呼び出された下流のサービスは、トランザクションに対する投票はできますが、完了することはできない点に留意してください。トランザクションの投票とトランザクションの完了を両方とも実行できるのはルートだけです。事実上、非サービス クライアントが複数のサービスの呼び出しを単一のトランザクションにまとめるには、トランザクション スコープを使用する以外に方法がありません。これを図 8 に示します。

図 8 トランザクション スコープの使用
図 8 トランザクション スコープの使用(画像を縮小するには、ここをクリックします)
図 8 トランザクション スコープの使用
図 8 トランザクション スコープの使用 (画像を拡大するには、ここをクリックします)

非サービス クライアントがトランザクションを開始した場合、そのトランザクションは、使用するトランザクション オブジェクトをクライアントが破棄したとき (通常では TransactionScope オブジェクトを破棄したとき) に終了します。ルート トランザクション スコープを作成するためのオプションにより、クライアントはトランザクションをサービスに伝達し、総合的なサービスの結果に基づいてトランザクションを管理およびコミットすることが可能になります。

using(MyContractClient proxy1 = new MyContractClient())
using(MyOtherContractClient proxy2 = new MyOtherContractClient())
using(TransactionScope scope = new TransactionScope())
{
   proxy1.MyMethod(...);
   proxy2.MyOtherMethod(...);
   scope.Complete();
}

まとめ

トランザクションは、エラー後のシステムの適切な回復を確約し、多くのアプリケーションで役立ちます。技術およびサービスの境界を越えて、さまざまなベンダからのさまざまなサービスを単一のアトミック トランザクションとしてグループ化できることは、Windows Communication Foundation とこれがサポートする標準の重要な機能です。すべての開発者は、いくつかの属性を設定し、バインディングでフローを有効化して、適切な論理トランザクション伝達モードを選択するだけで済みます。これは Windows Communication Foundation の簡潔で有能なプログラミング モデルであることを証明しています。特に、同期やインスタンスの管理に関して、Windows Communication Foundation のトランザクション サポートには、ここでは説明できなかったさまざまなことがあります。これらの側面については、今後のコラムで説明していきます。

トランザクションおよび Windows Communication Foundation の詳細については、「.NET Framework 2.0 における System.Transactions の紹介」および 2005 年 12 月発行の MSDN Magazine の「.NET の揮発性リソース マネージャがトランザクションに共通の型をもたらす」を参照してください。

ご意見やご質問は、

こちら mmnet30@microsoft.com.

まで英語でお送りください。
Juval Lowyは、WCF トレーニングと WCF アーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最近の著作としては、『WCF サービスのプログラミング』 (O'Reilly、2007) があります。彼はまた、シリコン バレー地域の Microsoft Regional Director も務めています。Juval の連絡先は、www.idesign.net です。


{ End Bracket }: アジャイルを実践する

{ End Bracket }
アジャイルを実践する
James Waletzky

運動は、人生の避け難い現実です。 好きか嫌いかを問わず、健康な体を維持するには運動が必要です。ただし、健康を維持するために何を行うかということより、運動の基本原則に従うこと、すなわち、柔軟体操、エアロビクス、アナエロビック運動を取り入れて実践していくことが重要です。ソフトウェア開発プロセスについても同じことが言えます。定められた開発プロセスに厳密に従うことより、プロセスの原則を貫くことが重要です。この理念は、とりわけアジャイル開発に当てはまります。

"アジャイル" と見なされているソフトウェア開発プロセスは多数あります。いくつかの例を挙げると、スクラム (プロジェクト管理)、エクストリーム プログラミング (XP)、フィーチャ駆動型開発 (FDD) などがあります。各プロセスの定められた手順に従うことは、初心者にとっては有効ですが、実際には、まったく同じソフトウェア プロジェクトやソフトウェア チームは二つと存在しないため、通常はカスタマイズが必要になります。アジャイル開発の基本となるインフラストラクチャを侵害しない限り、ニーズに合わせてプロセスを変更することが推奨されています。変更への対応は、アジャイル開発の指針の 1 つです。

アジャイル ソフトウェア開発の原則、または本質とは何でしょうか。アジャイルは、次のことに重点を置いています。

単純化 アジャイル開発プロセスは単純なだけでなく、単純なソフトウェア設計を奨励しています。単純なソフトウェア設計によって保守がしやすい状態を保ち、過剰なエンジニアリングが必要なくなります。XP のテスト駆動型開発 (TDD) のプラクティスでは、テストに合格するための最小限のコードを記述し、それ以外は記述しないことを奨励しています。このプラクティスと継続的なリファクタリング (つまり、機能を変えずに既存コードの設計を改善すること) を合わせると、ソフトウェアの複雑さを最小限に抑えることができます。

変更の受け入れ ソフトウェア開発には、確実なことが 2 つあります。それは、人が間違いをする (バグを作成する) ことと、要件が変更されることです。アジャイル開発者は変更を恐れず、常に変更を受け入れる態勢にあります。たとえば、多くの場合、ユーザーは開発サイクルの終盤になってから機能を要求してきます。アジャイル開発者はユーザーの要求を聞き入れ、TDD、継続的な統合、リファクタリングなどのプラクティスを使用して、面倒な作業を最小限に抑えて変更を取り入れます。

インクリメンタルな反復 小さな出荷可能な機能単位を頻繁に提供し、顧客の評価を頻繁に取り入れると、開発を順調に進めることができます。また、"完了" の意味を明確に定義したうえで、小さな機能単位の開発に専念することで、品質に重点が置かれます。スクラムでは、通常、30 日間サイクルで反復し、これを "スプリント" と呼びます。各スプリントの成果物は、高品質を維持する必要があります。FDD では、通常、2 週間ごとに具体的な作業結果を提供します。

継続的なフィードバックと改善 アジャイル開発では、開発技術の改善を目的として、現在の成果を頻繁に評価することを奨励しています。スクラムでは、1 回のスプリントが終わると、ミーティングを開催して見直しを行います。XP では、内部設計の向上と簡素化のために継続的なコードのリファクタリングを奨励しています。毎日のスクラム ミーティングで見積りを更新するなど、フィードバックに基づいたメカニズムによって、知識を補強し、常に正確なプロジェクトの実態を提供します。

共同作業 アジャイル開発では、チーム内、チーム間、および顧客とのオープンなコミュニケーション チャネルを重視します。プロジェクト管理者、開発者、テスト担当者、および顧客と定期的に連絡を取ることで、すべての関係者が同じ方向に進み、開発を阻害する問題に迅速に対処できます。デイリー スクラム ミーティングと XP のペア プログラミングは、チームの内外の共同作業を促進する方法として好適な例を示しています。

無駄を排除する 無駄を排除するという構想は、リーン ソフトウェア開発 (www.poppendieck.com) から借用しています。多くの場合、リーン ソフトウェア開発は、アジャイル開発と組み合わせて使用されます。"無駄" とは、顧客の要求やニーズの達成に大きく貢献することがないタスクや動作 (機能など) のことです。たとえば、最初に詳細な要件分析や大掛かりな設計を行うと、開発サイクルの終盤で変更が発生したときに作業の多くが無駄になることがよくあります。ハイレベルな状態を維持し、先々で詳細を発見することが重要です。

実践する手法が、スクラム、XP、FDD、または他のアジャイル プロセスであるかどうかを問わず、これらのアジャイル開発の原則により、ユーザーと高品質に焦点を当てた適切な意志決定を行うことが可能になります。運動についても同じことが言えます。庭での作業や友人の引越しの手伝いなども含め、さまざまな活動に定期的に取り組むことで、成果を上げることができます。単純に原則に従い、実践するのみです。

原則の詳細については、www.agilemanifesto.org/principles.html を参照してください。それでは、外に出てアジャイルでいてください。原則に従って、体とソフトウェア プロジェクトを好ましい状態に維持してください。

James Waletzkyは、Microsoft Engineering Excellence グループのシニア ソフトウェア設計エンジニアです。James はトレーニング、コンサルティング、およびツールによって、マイクロソフトにおけるエンジニアリングの手法の改善に尽力していますが、継続的な改善が必要であることを強く主張しています。