 MSDN Magazine February 2008

MSDN Magazine February 2008

自作室: .NET Framework 用の言語コンパイラを作成する

Joel Pobar が、コンパイラのしくみと、.NET Framework 用に独自のコンパイラを作成する方法について概説します。

Joel Pobar WinUnit: ネイティブ C++ アプリケーション用の簡略化された単体テスト

Maria Blees は、ネイティブ C++ プロジェクト用の新しい便利な単体テスト ツールである WinUnit を紹介しています。

Maria Blees Silverlight: Expression Encoder 用のカスタム公開プラグインを作成する

Laurence Moroney が、単純な FTP アップロード プラグインの構築とデバッグによって Expression Encoder プラグイン API を使用する方法を紹介します。

Laurence Moroney PIAB と WCF: Policy Injection Application Block と WCF サービスを統合する

PIAB を使用すると、コードを 1 行も変更することなく、検証、パフォーマンスの監視、承認、キャッシュなどのポリシーを使用して WCF サービスを強化できます。

Hugh Ang and David San Filippo WF の使い方: Windows Workflow Foundation でステート マシンを構築する

ステート マシン ワークフローは、プログラム ロジックを視覚化するさまざまな方法を表します。ステート マシンは、シーケンシャル ワークフローのようにアクティビティ間を遷移するのではなく、状態間を遷移します。ステート マシンの使い方、および Windows Workflow Foundation を使用したステート マシン ワークフローの設計および構築方法を説明します。

Keith Pijanowski 編集者のメモ: 優れたデザインによって言語の壁を乗り越える

今月は、選択するプログラミング言語に個性が反映される理由について、Howard Dierking が説明します。

Howard Dierking ツールボックス: 強力なテキスト編集機能、.NET の Web テスト、強化された単体テストなど

強力なテキスト編集機能、.NET の Web テスト、強化された単体テストなど

James Avery CLR 徹底解剖: 動的言語と Silverlight

DLRConsole は、Silverlight で動的言語を扱うための対話型環境を提供します。

Jimmy Schementi 基本的な本能: Visual Basic 9.0 における XML プログラミング

Visual Basic 9.0 では、開発者は XML をいっそう簡単に使用できます。このコラムでは、LINQ サポート、XML リテラル、埋め込み式、XML プロパティ、XML Schema IntelliSense など、いくつかの新機能について説明します。

Jonathan Aneja データ ポイント: エンティティ データ モデルをデザインする

開発者は、Entity Framework を使用すると、リレーショナル データ モデルではなく概念モデルを使用してデザインを行うことができるため、柔軟性が向上します。これに着手するには、エンティティ データ モデルのデザイン方法を知る必要があります。この記事では、John Papa がそのプロセスについて説明します。

John Papa Cutting Edge: AJAX エクステンダを使用してコントロールをカスタマイズする、第 2 部

今月、Dino は AJAX Control Extender について再度とりあげ、マスク編集やオートコンプリートなどのより高度な機能について補足しています。

Dino Esposito Office Space: SharePoint 2007 におけるセキュリティ プログラミング

今月、Ted Pattison は Windows SharePoint Services 3.0 のセキュリティおよびアクセス許可のプログラミングについての概要を説明します。

Ted Pattison テストの実行: Microsoft UI オートメーション ライブラリ

James McCaffrey が、新しい Microsoft UI オートメーション ライブラリを使用して UI テスト自動化を開始する方法を説明します。

Dr. James McCaffrey 基礎: Visual Studio 2008 の WCF 向け新機能

Visual Studio 2008 および .NET Framework 3.5 には、Windows Communication Foundation (WCF) を拡張する新しいツールとサポートが用意されています。また、Visual Studio 2008 では、開発者が手動で行う WCF タスクの多くが自動化されます。

Juval Lowy .NET の問題: ストリームのパイプライン処理

今月は、暗号化タスクと圧縮タスクを実行するときにデュアル プロセッサを最大限に活用する方法について、Stephen Toub が説明します。

Stephen Toub { End Bracket }: とびきり最高のものを見つける

マイクロソフトが理想とする応募者は情熱、知性、そして学習への意欲を持った人材です。Howard Dierking がマイクロソフトの人材募集における哲学の変遷について解説します。

Howard Dierking


自作室: .NET Framework 用の言語コンパイラを作成する

自作室

.NET Framework 用の言語コンパイラを作成する

Joel Pobar
この記事の内容 : :
  • 言語の定義
  • コンパイラのフェーズ
  • CLR の抽象スタック
  • IL の確認用ツール
この記事は次のテクノロジを使用しています:
.NET Framework
コードのダウンロード : : CompilerWriting2008_02.exe (158KB)
コンテンツ
言語定義
アーキテクチャの概要
スキャナ
パーサー
.NET Framework を対象としたコードを作成する
IL の確認用ツール
コード ジェネレータ
まとめ (後一息です)
動的なメソッドの呼び出し
LCG を使用して簡易遅延バインドを実行する
動的言語ランタイム

コンパイラ ハッカーは、コンピュータ サイエンスの世界ではアイドルです。Professional Developers Conference で Anders Hejlsberg のプレゼンテーションを見ましたが、彼が壇上から降りると、そこには本へのサインを求めたり、一緒に写真を撮ってくれるようお願いしたりする大勢の男女が待ち構えていました。ラムダ関数、型システム、およびアセンブリ言語を隅から隅まで学ぶために時間を捧げてきた人には、何か知的な神秘性が漂っています。皆さんも Microsoft® .NET Framework 用に独自のコンパイラを作成することで、この栄光を少しわかちあうことができます。

.NET Framework 用のコンパイラは、さまざまな言語を対象に多数提供されています。.NET CLR は、これらの言語をなんとかして同じ場に収め、正常に動作および連携させます。忍者のような開発者は、大規模なソフトウェア システムを構築し、C# と Python を少し足したいときに、この CLR を利用します。もちろん、彼らはすばらしい開発者ですが、正真正銘の達人、つまりコンパイラ ハッカーとは比べ物になりません。コンパイラ ハッカーこそ、仮想マシン、言語の設計、そしてこれらの言語やコンパイラについてありとあらゆることに精通しているのですから。

この記事では、C# で記述されたコンパイラ ("Good for Nothing" (役立たず) コンパイラというぴったりな名前が付いています) のコードを詳しく解説し、それと並行して、独自の .NET コンパイラを作成するために必要なアーキテクチャの概要、理論、および .NET Framework API を紹介します。まず言語定義について説明した後、コンパイラのアーキテクチャ、.NET アセンブリを生成するコード生成サブシステムという順で説明していきます。コンパイラ開発の基本を理解し、CLR 用のコードをさまざまな言語で効率的に記述する方法の概要をしっかりと理解していただくことが目標です。ここでは C# 4.0 や IronRuby のコードは作成しませんが、コンパイラ開発芸術への情熱に火を点けるには十分な内容になっています。

言語定義

各ソフトウェア言語は、それぞれ特定の目的を達成するために開発されました。この目的はさまざまで、表現力 (Visual Basic® など)、生産性 (コードの 1 行 1 行からできるだけ多くの成果を得ようとする Python など)、特殊化 (プロセッサの製造元が使用するハードウェア記述言語の Verilog) など、単に作者の好みに合うように開発されたものもあります (たとえば、Boo の作者は .NET Framework が好きですが、提供されている言語の中で気に入ったものがありませんでした)。

目的がはっきりしたら、言語の設計を決めます。これは言語の青写真のようなものと考えてください。コンピュータ言語は、プログラマが必要なものを間違いなく正確に表現し、さらにコンパイラが表現されたものを間違いなく正確に理解してコードを生成できるように、非常に高い精度が要求されます。コンパイラの実装中にあいまいさを排除できるように、言語の青写真を指定する必要があります。これにはメタ構文を使用します。メタ構文は、言語の構文を記述するために使用されます。世の中にはかなりの数のメタ構文があるため、好みに合わせて選ぶことができます。ここでは EBNF (Extended Backus-Naur Form) というメタ構文を使用して、Good for Nothing 言語を指定します。

EBNF の生みの親はとても有名な人物であることをお伝えしておきましょう。チューリング賞の受賞者であり、FORTRAN の主任開発者でもあった John Backus です。EBNF に関する議論はこの記事の内容からはそれますが、基本的な概念については説明します。

図 1 は、Good for Nothing の言語定義です。この言語定義によると、ステートメント (stmt) には、変数の宣言、代入、for ループ、コマンド ラインからの整数の読み取り、または画面への出力を指定できます。これらはセミコロンで区切って何回でも指定できます。式 (expr) には、文字列、整数、算術式、または識別子を指定できます。識別子 (ident) は、最初の文字にアルファベットを指定したら、2 文字目以降は文字と数字を使用して名前を付けることができます。定義はこのような感じです。基本的な算術機能、規模の小さい型システム、および単純なコンソール ベースのユーザー インターフェイスを提供する、非常に単純な言語構文を定義しました。

Figure 1 Good for Nothing の言語定義 (Close Figure 1)
<stmt> := var <ident> = <expr>
        | <ident> = <expr>
        | for <ident> = <expr> to <expr> do <stmt> end
        | read_int <ident>
        | print <expr>
        | <stmt> ; <stmt>

<expr> := <string>
        | <int>
        | <arith_expr>
        | <ident>

<arith_expr> := <expr> <arith_op> <expr>
<arith_op> := + | - | * | /

<ident> := <char> <ident_rest>*
<ident_rest> := <char> | <digit>

<int> := <digit>+
<digit> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

<string> := " <string_elem>* "
<string_elem> := <any char other than ">

この言語定義に指定されている項目が少ないことにお気付きになったかもしれません。どこまで大きな数を使用できるか (32 ビット整数よりも大きな数を使用できるか) や、負の数を使用できるかどうかさえ指定されていません。EBNF を使用した定義では、本来このような細かい部分も正確に定義しますが、ここでは簡潔さを保つために定義を単純なものにしています。

以下は、Good for Nothing 言語のサンプル プログラムです。

var ntimes = 0;
print "How much do you love this company? (1-10) ";
read_int ntimes;
var x = 0;
for x = 0 to ntimes do
   print "Developers!";
end;
print "Who said sit down?!!!!!";

この単純なプログラムを言語定義と比較すると、文法のしくみがよくわかります。比較したところで、言語定義に関する説明はおしまいです。

アーキテクチャの概要

コンパイラの仕事は、プログラマが作成した上位レベルのタスクを、コンピュータのプロセッサが理解して実行できるタスクに変換することです。つまり、Good for Nothing 言語で記述されたプログラムを .NET CLR が実行できるものに変換します。コンパイラは、一連の変換段階に従ってこの作業を行います。これらの段階では、言語を解析して大事な部分を取り出し、残りは破棄します。コンパイラの動作は、一般的なソフトウェア設計の原則に準拠しています。つまり、フェーズと呼ばれる疎結合コンポーネントを連結して、各変換段階の処理を実行します。図 2 は、コンパイラの各フェーズを実行するコンポーネント (スキャナ、パーサー、およびコード ジェネレータ) を表しています。各フェーズでは言語がさらに細かく分解され、プログラムの目的についての情報が次のフェーズに渡されます。

図 2 コンパイラのフェーズ
図 2 コンパイラのフェーズ(画像を縮小するには、ここをクリックします)
図 2 コンパイラのフェーズ
図 2 コンパイラのフェーズ (画像を拡大するには、ここをクリックします)

多くの場合、コンパイラに詳しい人は、フェーズを抽象的にフロントエンドとバックエンドに分類します。フロントエンドではスキャンと解析が実行され、バックエンドでは通常コードの生成が実行されます。フロントエンドの仕事は、プログラムの構文の構造を検出し、この構造をテキストから抽象構文ツリー (AST) と呼ばれる上位レベルのメモリ内表現に変換します (AST については、この後簡単に説明します)。バックエンドの仕事は、AST をコンピュータが実行できるものに変換することです。

通常、これら 3 つのフェーズはフロントエンドとバックエンドに分類されます。これは、スキャナとパーサーが通常一組で使用され、コード ジェネレータが対象のプラットフォームと密接に結び付いているためです。開発者はこの設計を使用すると、言語を複数のプラットフォームで使用する必要がある場合に、コード ジェネレータを別のプラットフォーム用のものに置き換えることができます。

Good for Nothing コンパイラのコードは、この記事に付属しているサンプル コードに含まれています。このため、各フェーズのコンポーネントについての解説を読みながら、実装の詳細を確認することができます。

スキャナ

スキャナの主な仕事は、テキスト (ソース ファイル内の文字ストリーム) をパーサーが使用できるブロック (トークンと呼ばれます) に分割することです。スキャナは、最終的にパーサーに渡されるトークンを決定します。したがって、コメントなどの文法で定義されていないものを破棄できます。Good for Nothing 言語の場合、スキャナによって保持されるのは、文字 (A ～ Z および一般的な記号)、数字 (0 ～ 9)、演算を定義する文字 (+、-、*、/ など)、文字列を囲む引用符、およびセミコロンです。

スキャナは関連する文字ストリームを、パーサーが処理できるトークンにグループ化します。たとえば、" h e l l o w o r l d ! " という文字ストリームは、"hello world!" という 1 つのトークンにグループ化されます。

Good for Nothing のスキャナはきわめて単純であり、インスタンス化に必要なのは System.IO.TextReader のみです。次のように、この TextReader を使用してスキャン処理を開始します。

public Scanner(TextReader input)
{
    this.result = new Collections.List<object>();
    this.Scan(input);
}

図 3 は Scan メソッドを表しています。これは単純な while ループであり、テキスト ストリーム内のすべての文字を確認し、言語定義で宣言されている認識可能な文字列を検索します。認識可能な文字または文字列が検出されるたびに、スキャナはトークンを作成して List<object> に追加します (この例ではオブジェクト型を指定しましたが、Token クラスなどを作成して、トークンについてさらに多くの情報 (行や列の数など) をカプセル化することもできます)。

Figure 3 スキャナの Scan メソッド (Close Figure 3)
private void Scan(TextReader input)
{
  while (input.Peek() != -1)
  {
    char ch = (char)input.Peek();

    // Scan individual tokens
    if (char.IsWhiteSpace(ch))
    {
      // eat the current char and skip ahead
      input.Read();
    }
    else if (char.IsLetter(ch) || ch == '_')
    {
      StringBuilder accum = new StringBuilder();

      input.Read(); // skip the '"'

      if (input.Peek() == -1)
      {
        throw new Exception("unterminated string literal");
      }

      while ((ch = (char)input.Peek()) != '"')
      {
        accum.Append(ch);
        input.Read();

        if (input.Peek() == -1)
        {
          throw new Exception("unterminated string literal");
        }
      }

      // skip the terminating "
      input.Read();
      this.result.Add(accum);
    }

    ...
  }
}

コード内の二重引用符が検出されたときに、文字列トークンがカプセル化されていると見なされることがおわかりいただけると思います。このため、文字列を StringBuilder インスタンスに格納して一覧に追加します。スキャンによってトークンの一覧が作成されると、トークンは Tokens という名前のプロパティを介してパーサー クラスに渡されます。

パーサー

パーサーは、コンパイラの最も重要なコンポーネントであり、その構造やサイズはさまざまです。Good for Nothing パーサーには多くの仕事があります。まず、ソース プログラムが言語定義に従っているかどうかを確認し、エラーがある場合はエラーを出力します。また、Good for Nothing パーサーは、プログラムの構文のメモリ内表現 (コード ジェネレータによって使用されます) を作成し、使用するランタイム型を決定します。

まず、プログラムの構文のメモリ内表現である AST について説明します。次に、このツリーをスキャナ トークンから作成するコードについて解説します。AST 形式のコードは、すばやく効率的かつ簡単に作成でき、コード ジェネレータによって何度でもスキャンできます。図 4 は、Good for Nothing コンパイラの AST を示しています。

Figure 4 Good for Nothing コンパイラの AST (Close Figure 4)
public abstract class Stmt
{
}

// var <ident> = <expr>
public class DeclareVar : Stmt
{
    public string Ident;
    public Expr Expr;
}

// print <expr>
public class Print : Stmt
{
    public Expr Expr;
}

// <ident> = <expr>
public class Assign : Stmt
{
    public string Ident;
    public Expr Expr;
}

// for <ident> = <expr> to <expr> do <stmt> end
public class ForLoop : Stmt
{
    public string Ident;
    public Expr From;
    public Expr To;
    public Stmt Body;
}

// read_int <ident>
public class ReadInt : Stmt
{
    public string Ident;
}

// <stmt> ; <stmt>
public class Sequence : Stmt
{
    public Stmt First;
    public Stmt Second;
}

/* <expr> := <string>
 *  | <int>
 *  | <arith_expr>
 *  | <ident>
 */
public abstract class Expr
{
}

// <string> := " <string_elem>* "
public class StringLiteral : Expr
{
    public string Value;
}

// <int> := <digit>+
public class IntLiteral : Expr
{
    public int Value;
}

// <ident> := <char> <ident_rest>*
// <ident_rest> := <char> | <digit>
public class Variable : Expr
{
    public string Ident;
}

// <arith_expr> := <expr> <arith_op> <expr>
public class ArithExpr : Expr
{
    public Expr Left;
    public Expr Right;
    public BinOp Op;
}

// <arith_op> := + | - | * | /
public enum ArithOp
{
    Add,
    Sub,
    Mul,
    Div
}

Good for Nothing の言語定義を簡単に確認すると、AST が EBNF の文法の言語定義ノードと部分的に一致していることがわかります。言語定義が構文をカプセル化し、抽象構文ツリーが構文の要素の構造を表すと考えるとよいでしょう。

解析用のアルゴリズムは多数ありますが、この記事ではすべてのアルゴリズムについては説明しません。一般に、トークン ストリームを解析して AST を作成する方法は、アルゴリズムごとに異なります。Good for Nothing コンパイラでは、LL (左から右、左端から導出) 下降型パーサーと呼ばれるものを使用しています。これは単純に、左から右へテキストを読み取り、次の入力トークンを基に AST を作成することを意味します。

次のように、この記事のパーサー クラスのコンストラクタは、スキャナによって作成されたトークンの一覧をそのまま受け取ります。

public Parser(IList<object> tokens)
{
    this.tokens = tokens;
    this.index = 0;
    this.result = this.ParseStmt();

    if (this.index != this.tokens.Count)
        throw new Exception("expected EOF");
}

解析の中核となる処理は ParseStmt メソッドによって実行されます (図 5 参照)。このメソッドは Stmt ノードを返します。このノードはツリーのルート ノードとして機能し、言語構文定義の最上位のノードに対応しています。パーサーは、インデックスを使用して現在の位置を追跡しながら一覧内のトークンをスキャンし、言語構文内で Stmt ノードに従属するトークン (変数宣言や代入、for ループ、read_int、および print) を識別します。トークンを識別できない場合、例外がスローされます。

Figure 5 トークンを識別する ParseStmt メソッド (Close Figure 5)
private Stmt ParseStmt()
{
    Stmt result;

    if (this.index == this.tokens.Count)
    {
        throw new Exception("expected statement, got EOF");
    }

    if (this.tokens[this.index].Equals("print"))
    {
        this.index++;
        ...
    }
    else if (this.tokens[this.index].Equals("var"))
    {
        this.index++;
        ...
    }
        else if (this.tokens[this.index].Equals("read_int"))
    {
        this.index++;
        ...
    }
    else if (this.tokens[this.index].Equals("for"))
    {
        this.index++;
        ...
    }
    else if (this.tokens[this.index] is string)
    {
        this.index++;
        ...
    }
    else
    {
        throw new Exception("parse error at token " + this.index +
            ": " + this.tokens[this.index]);
    }
    ...
}

トークンが識別されると、AST ノードが作成され、このノードが必要とする細かい解析が実行されます。print AST ノードの作成に必要なコードを次に示します。

// <stmt> := print <expr>
if (this.tokens[this.index].Equals("print"))
{
    this.index++;
    Print print = new Print();
    print.Expr = this.ParseExpr();
    result = print;
}

ここでは、2 つの処理が実行されます。1 つ目は、インデックス カウンタの値を増やして print トークンを破棄する処理です。2 つ目は、ParseExpr メソッドを呼び出して Expr ノードを取得する処理です。この処理は、言語定義で print トークンの後に式が続くことが要求されているため、この要件を満たすために実行されます。

図 6 は、ParseExpr のコードを示しています。このコードは、現在のインデックスからトークンの一覧をスキャンし、式の言語定義に準拠したトークンを識別します。この場合、メソッドは文字列、整数、および変数 (スキャナ インスタンスにより作成されたもの) を検索し、該当する式を表す適切な AST ノードを返します。

Figure 6 式ノードを解析する ParseExprs (Close Figure 6)
// <expr> := <string>
// | <int>
// | <ident>
private Expr ParseExpr()
{
  ...
  if (this.tokens[this.index] is StringBuilder)
  {
    string value = ((StringBuilder)this.tokens[this.index++]).ToString();
    StringLiteral stringLiteral = new StringLiteral();
    stringLiteral.Value = value;
    return stringLiteral;
  }
  else if (this.tokens[this.index] is int)
  {
    int intValue = (int)this.tokens[this.index++];
    IntLiteral intLiteral = new IntLiteral();
    intLiteral.Value = intValue;
    return intLiteral;
  }
  else if (this.tokens[this.index] is string)
  {
    string ident = (string)this.tokens[this.index++];
    Variable var = new Variable();
    var.Ident = ident;
    return var;
  }
  ...
}

"<stmt> ; <stmt>" 言語構文定義に準拠した文字列ステートメントには、sequence AST ノードが使用されます。この sequence ノードは、stmt ノードへの 2 つのポインタを保持し、AST ツリー構造の基盤を形成します。この sequence ノードに使用されるコードを次に示します。

if (this.index < this.tokens.Count && this.tokens[this.index] ==
    Scanner.Semi)
{
    this.index++;

    if (this.index < this.tokens.Count &&
        !this.tokens[this.index].Equals("end"))
    {
        Sequence sequence = new Sequence();
        sequence.First = result;
        sequence.Second = this.ParseStmt();
        result = sequence;
    }
}

図 7 の AST ツリーは、Good for Nothing の次のコードを実行した結果です。

図 7 helloworld.gfn AST ツリーと上位レベルのトレース
図 7 helloworld.gfn AST ツリーと上位レベルのトレース(画像を縮小するには、ここをクリックします)
図 7 helloworld.gfn AST ツリーと上位レベルのトレース
図 7 helloworld.gfn AST ツリーと上位レベルのトレース (画像を拡大するには、ここをクリックします)

var x = "hello world!";
print x;

.NET Framework を対象としたコードを作成する

コードの生成を実行するコードについて説明する前に、まずコードの対象についてお話しておいた方がよいでしょう。ここでは、スタック ベースの仮想マシン、型システム、.NET アセンブリの作成に使用されるライブラリなど、.NET CLR が提供するコンパイラ サービスについて説明します。また、コンパイラの出力中に発生するエラーの特定と診断に必要なツールについても簡単に説明します。

CLR は仮想マシンです。つまり、コンピュータ システムをエミュレートするソフトウェアです。コンピュータと同様に、CLR には、CLR が実行できる一連の下位レベルの操作、一連のメモリ サービス、および実行可能プログラムを定義するアセンブリ言語が含まれています。CLR は、抽象スタック ベースのデータ構造を使用してコードの実行をモデル化し、中間言語 (IL) と呼ばれるアセンブリ言語を使用して、スタック上で実行できる操作を定義します。

IL で定義されているコンピュータ プログラムが実行されると、CLR は指定された操作をスタックでシミュレートし、命令によって処理されるデータをプッシュおよびポップします。たとえば、IL を使用して 2 つの数字を加算するとします。以下は、10 + 20 の実行に使用されるコードです。

  ldc.i4    10
  ldc.i4    20
  add

1 行目 (ldc.i4 10) は整数 10 をスタックにプッシュします。2 行目 (ldc.i4 20) は整数 20 をスタックにプッシュします。3 行目 (add) は、この 2 つの整数をスタックからポップし、これらを加算して、結果をスタックにプッシュします。

スタック マシンのシミュレーションは、IL とスタックのセマンティクスを基盤となるプロセッサのマシン言語に変換することで実行されます。この変換処理は、ジャスト イン タイム (JIT) コンパイルによってプログラムの実行時に実行するか、ネイティブ イメージ ジェネレータ (Ngen) などのサービスによって事前に実行します。

プログラムの作成に使用できる IL 命令は、基本的な算術演算から、フロー制御、さまざまな呼び出し規則に至るまで多数あります。すべての IL 命令の詳細については、欧州電子計算機工業会 (ECMA) の仕様 (msdn2.microsoft.com/aa569283) に記載されている区分 III を参照してください。

CLR の抽象スタックは、整数以外のデータの操作も実行します。この抽象スタックでは、文字列型、整数型、ブール型、浮動小数点数型、倍精度浮動小数点型など、豊富な型システムが提供されます。この記事の言語を安全に CLR 上で実行し、.NET に準拠したその他の言語と相互運用できるように、いくつかの CLR 型システムをこの記事のプログラムに組み込んでいます。具体的には、Good for Nothing 言語では 2 つの型 (数値と文字列) を定義し、System.Int32 と System.String にマップしています。

Good for Nothing コンパイラは、System.Reflection.Emit という基本クラス ライブラリ (BCL) コンポーネントを利用して、IL コードを生成し、.NET アセンブリを作成およびパッケージ化します。これは下位レベルのライブラリであり、コードの生成の単純な抽象化を IL 言語に提供することで、機械に近いレベルで機能します。このライブラリは、System.Xml.XmlSerializer など、よく知られているその他の BCL API でも使用されます。

.NET アセンブリの作成に必要な上位クラス (図 8 参照) は、ある程度ビルダ ソフトウェアの設計パターンに準拠しており、ビルダ API を使用して各 .NET 論理メタデータを抽象化します。AssemblyBuilder クラスは、PE ファイルを作成したり、マニフェストなどの必要な .NET アセンブリのメタデータ要素を設定したりするために使用されます。ModuleBuilder クラスは、アセンブリ内のモジュールを作成するために使用されます。TypeBuilder は、型とそれに関連付けられるメタデータの作成に使用されます。MethodBuilder は型へのメソッドの追加、LocalBuilder はメソッドへのローカル変数の追加にそれぞれ使用されます。ILGenerator クラスは、OpCodes クラスを使用してメソッドの IL コードを生成するために使用されます。OpCodes クラスは、考えられるすべての IL 命令を提供する大型の列挙体です。これらの Reflection.Emit クラスはすべて、Good for Nothing のコード ジェネレータで使用されています。

図 8 .NET アセンブリのビルドに使用される Reflection.Emit ライブラリ
図 8 .NET アセンブリのビルドに使用される Reflection.Emit ライブラリ(画像を縮小するには、ここをクリックします)
図 8 .NET アセンブリのビルドに使用される Reflection.Emit ライブラリ
図 8 .NET アセンブリのビルドに使用される Reflection.Emit ライブラリ (画像を拡大するには、ここをクリックします)

IL の確認用ツール

経験豊かなコンパイラ ハッカーでも、コードの生成レベルでは失敗を犯すことがあります。最もよくあるバグは、スタック内の不整合を引き起こす不適切な IL コードです。通常、CLR は不適切な IL を検出すると、(アセンブリの信頼レベルに応じて、アセンブリの読み込み時または IL の JIT コンパイル時に) 例外をスローします。このようなエラーの診断と修復は、peverify.exe という SDK ツールを使用して簡単に実行できます。このツールは IL の検証を実行し、コードが正しいかどうか、および安全に実行できるかどうかを確認します。

たとえば、次の IL コードは数値 10 を文字列 "bad" に加算しようとしています。

ldc.i4    10
ldstr    "bad"
add

この不適切な IL を含むアセンブリに対して peverify を実行すると、次のエラーが返されます。

[IL]: Error: [C:\MSDNMagazine\Sample.exe : Sample::Main][offset 0x0000002][found ref 'System
.String'] Expected numeric type on the stack.

この例では、加算命令には 2 つの数値型が必要であるが、実際には整数型と文字列型が検出されたことが peverify によって報告されています。

ILASM (IL アセンブラ) と ILDASM (IL 逆アセンブラ) は、テキストの IL を .NET アセンブリにコンパイルする処理と、アセンブリを IL に逆コンパイルする処理にそれぞれ使用できる 2 つの SDK ツールです。ILASM を使用すると、コンパイラの出力の基盤となる IL 命令ストリームをすばやく簡単にテストできます。テキスト エディタでテスト用の IL コードを作成し、このコードを ILASM で処理するだけです。一方、ILDASM ツールを使用すると、コンパイラが生成した特定のコード パスの IL を簡単に確認できます。このコードには、C# コンパイラなどの商用コンパイラが生成した IL も含まれます。このツールは、言語間で似ているステートメントが IL コードに含まれているかどうかを探す場合に便利です。つまり、C# の for ループに対して生成された IL フロー制御コードは、同様の構造を持つ他のコンパイラで再利用できる可能性があります。

コード ジェネレータ

Good for Nothing コンパイラのコード ジェネレータは、実行可能な .NET アセンブリを生成するコードの大部分に Reflection.Emit ライブラリを使用しています。ここでは、このクラスの重要な部分を分析し、これらの部分について説明します。その他の部分は必要に応じて確認してください。

CodeGen コンストラクタ (図 9 参照) は、コードの生成に必要な Reflection.Emit インフラストラクチャを構築します。まずアセンブリ名を定義してアセンブリ ビルダに渡します。この例では、ソース ファイル名をアセンブリ名に使用しています。次は ModuleBuilder 定義です。ここではモジュール定義が 1 つであるため、アセンブリと同じ名前を使用しています。次に、アセンブリ内の唯一の型を保持するために、TypeBuilder を ModuleBuilder に定義します。Good for Nothing の言語定義には、第一級オブジェクトとして定義されている型はありませんが、起動時に実行されるメソッドを保持するには、少なくとも 1 つの型が必要です。MethodBuilder は、Good for Nothing のコードに対して生成された IL を保持するための Main メソッドを定義します。ユーザーが実行可能ファイルを実行したとき、起動時に SetEntryPoint が実行されるように、このメソッドを MethodBuilder 上で呼び出します。また、GetILGenerator メソッドを使用して、MethodBuilder からグローバルな ILGenerator (il) を作成します。

Figure 9 CodeGen コンストラクタ (Close Figure 9)
Emit.ILGenerator il = null;
Collections.Dictionary<string, Emit.LocalBuilder> symbolTable;

public CodeGen(Stmt stmt, string moduleName)
{
  if (Path.GetFileName(moduleName) != moduleName)
  {
    throw new Exception("can only output into current directory!");
  }

  AssemblyName name = new
    AssemblyName(Path.GetFileNameWithoutExtension(moduleName));
  Emit.AssemblyBuilder asmb =
    AppDomain.CurrentDomain.DefineDynamicAssembly(name,
      Emit.AssemblyBuilderAccess.Save);
  Emit.ModuleBuilder modb = asmb.DefineDynamicModule(moduleName);
  Emit.TypeBuilder typeBuilder = modb.DefineType("Foo");

  Emit.MethodBuilder methb = typeBuilder.DefineMethod("Main",
    Reflect.MethodAttributes.Static,
    typeof(void),
    System.Type.EmptyTypes);

  // CodeGenerator
  this.il = methb.GetILGenerator();
  this.symbolTable = new Dictionary<string, Emit.LocalBuilder>();

  // Go Compile
  this.GenStmt(stmt);

  il.Emit(Emit.OpCodes.Ret);
  typeBuilder.CreateType();
  modb.CreateGlobalFunctions();
  asmb.SetEntryPoint(methb);
  asmb.Save(moduleName);
  this.symbolTable = null;
  this.il = null;
}

Reflection.Emit インフラストラクチャが構築されたら、コード ジェネレータは AST の解析に使用する GenStmt メソッドを呼び出します。このメソッドを呼び出すと、グローバルな ILGenerator によって、必要な IL コードが生成されます。図 10 は、GenStmt メソッドのサブセットを示しています。このメソッドが初めて呼び出されると、AST が解析 (Sequence ノードであるかどうかの確認から始まります) され、現在の AST ノードの型が有効になります。

Figure 10 GenStmt メソッドのサブセット (Close Figure 10)
private void GenStmt(Stmt stmt)
{
    if (stmt is Sequence)
    {
        Sequence seq = (Sequence)stmt;
        this.GenStmt(seq.First);
        this.GenStmt(seq.Second);
    }

    else if (stmt is DeclareVar)
    {
        ...
    }

    else if (stmt is Assign)
    {
        ...
    }
    else if (stmt is Print)
    {
        ...
    }
}

AST ノードが DeclareVar (変数の宣言) である場合に実行されるコードを次に示します。

else if (stmt is DeclareVar)
{
    // declare a local
    DeclareVar declare = (DeclareVar)stmt;
    this.symbolTable[declare.Ident] =
        this.il.DeclareLocal(this.TypeOfExpr(declare.Expr));

    // set the initial value
    Assign assign = new Assign();
    assign.Ident = declare.Ident;
    assign.Expr = declare.Expr;
    this.GenStmt(assign);
}

ここでは、まず変数をシンボル テーブルに追加する必要があります。シンボル テーブルはコンパイラの中核となるデータ構造であり、シンボリック識別子 (この例では文字列ベースの変数名) を、プログラム内でのその変数の型、場所、およびスコープに関連付けるために使用されます。Good for Nothing のシンボル テーブルは単純であり、すべての変数が Main メソッドに対してローカルに宣言されています。したがって、単純に Dictionary<string, LocalBuilder> を使用して、シンボルを LocalBuilder に関連付けています。

シンボルをシンボル テーブルに追加したら、DeclareVar AST ノードを Assign ノードに変換して、変数を宣言するための式を変数に代入します。ここでは、次のコードを使用して代入ステートメントを生成しています。

else if (stmt is Assign)
{
    Assign assign = (Assign)stmt;
    this.GenExpr(assign.Expr, this.TypeOfExpr(assign.Expr));
    this.Store(assign.Ident, this.TypeOfExpr(assign.Expr));
}

上記のコードを実行すると、式をスタックに読み込むための IL コードが生成された後、適切な LocalBuilder に式を格納するための IL コードが生成されます。

GenExpr のコード (図 11 参照) は、Expr AST ノードを使用して、式をスタック マシンに読み込むための IL を生成しています。StringLiteral と IntLiteral は、それぞれ文字列 (ldstr) と整数 (ldc.i4) をスタックに読み込む直接的な IL 命令を提供するという点で似ています。

Figure 11 GenExpr メソッド (Close Figure 11)
private void GenExpr(Expr expr, System.Type expectedType)
{
  System.Type deliveredType;

  if (expr is StringLiteral)
  {
    deliveredType = typeof(string);
    this.il.Emit(Emit.OpCodes.Ldstr, ((StringLiteral)expr).Value);
  }
  else if (expr is IntLiteral)
  {
    deliveredType = typeof(int);
    this.il.Emit(Emit.OpCodes.Ldc_I4, ((IntLiteral)expr).Value);
  }
  else if (expr is Variable)
  {
    string ident = ((Variable)expr).Ident;
    deliveredType = this.TypeOfExpr(expr);

    if (!this.symbolTable.ContainsKey(ident))
    {
      throw new Exception("undeclared variable '" + ident + "'");
    }

    this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[ident]);
  }
  else
  {
    throw new Exception("don't know how to generate " +
      expr.GetType().Name);
  }

  if (deliveredType != expectedType)
  {
    if (deliveredType == typeof(int) &&
        expectedType == typeof(string))
    {
      this.il.Emit(Emit.OpCodes.Box, typeof(int));
      this.il.Emit(Emit.OpCodes.Callvirt,
        typeof(object).GetMethod("ToString"));
    }
    else
    {
      throw new Exception("can't coerce a " + deliveredType.Name +
        " to a " + expectedType.Name);
    }
  }
}

変数式は、ldloc を呼び出してメソッドのローカル変数をスタックに読み込み、この変数をそれぞれの LocalBuilder に渡します。図 11 のコードの最後のセクションは、式の型を必要な型に変換しています (強制型変換)。たとえば、print メソッドを正しく実行するために、このメソッドの呼び出しで整数を文字列に変換することが必要になる場合があります。

図 12 は、変数に式を代入する Store メソッドを示しています。名前はシンボル テーブルから検索され、対応する LocalBuilder が stloc IL 命令に渡されます。この処理は、単純にスタックから現在の式をポップし、ローカル変数に代入します。

Figure 12 変数への式の格納 (Close Figure 12)
private void Store(string name, Type type)
{
  if (this.symbolTable.ContainsKey(name))
  {
    Emit.LocalBuilder locb = this.symbolTable[name];

    if (locb.LocalType == type)
    {
      this.il.Emit(Emit.OpCodes.Stloc, this.symbolTable[name]);
    }
    else
    {
      throw new Exception("'" + name + "' is of type " +
        locb.LocalType.Name + " but attempted to store value of type " +
        type.Name);
    }
  }
  else
  {
    throw new Exception("undeclared variable '" + name + "'");
  }
}

Print AST ノードの IL を生成するためのコードは、BCL メソッドを呼び出しているという点で興味深いコードです。スタック上に式を生成し、IL の呼び出し命令を使用して System.Console.WriteLine メソッドを呼び出しています。また、リフレクションを使用して、呼び出し命令に WriteLine メソッドを渡すために必要なハンドルを取得しています。

else if (stmt is Print)
{
  this.GenExpr(((Print)stmt).Expr, typeof(string));
  this.il.Emit(Emit.OpCodes.Call,
    typeof(System.Console).GetMethod("WriteLine",
    new Type[] { typeof(string) }));
}

メソッドの呼び出しが実行されると、メソッドの引数がスタックから後入れ先出しの順序でポップされます。つまり、メソッドの最初の引数にはスタックの 1 番上の項目が使用され、2 番目の引数にはその次の項目が使用されます。

Good for Nothing の最も複雑なコードは、for ループの IL を生成するコードです (図 13 参照)。これは、商用コンパイラがこのようなコードを生成する場合と非常によく似ています。しかし、この for ループのコードを理解するには、生成された IL を見ていただくのが一番良いでしょう (図 14 参照)。

Figure 14 for ループの IL コード (Close Figure 14)
// for x = 0
IL_0006:  ldc.i4     0x0
IL_000b:  stloc.0

// jump to the test
IL_000c:  br         IL_0023

// execute the loop body
IL_0011:  ...

// increment the x variable by 1
IL_001b:  ldloc.0
IL_001c:  ldc.i4     0x1
IL_0021:  add
IL_0022:  stloc.0

// TEST
// load x, load 100, branch if
// x is less than 100
IL_0023:  ldloc.0
IL_0024:  ldc.i4     0x64
IL_0029:  blt        IL_0011
Figure 13 for ループのコード (Close Figure 13)
else if (stmt is ForLoop)
{
    // example:
    // var x = 0;
    // for x = 0 to 100 do
    //   print "hello";
    // end;

    // x = 0
    ForLoop forLoop = (ForLoop)stmt;
    Assign assign = new Assign();
    assign.Ident = forLoop.Ident;
    assign.Expr = forLoop.From;
    this.GenStmt(assign);
    // jump to the test
    Emit.Label test = this.il.DefineLabel();
    this.il.Emit(Emit.OpCodes.Br, test);

    // statements in the body of the for loop
    Emit.Label body = this.il.DefineLabel();
    this.il.MarkLabel(body);
    this.GenStmt(forLoop.Body);

    // to (increment the value of x)
    this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[forLoop.Ident]);
    this.il.Emit(Emit.OpCodes.Ldc_I4, 1);
    this.il.Emit(Emit.OpCodes.Add);
    this.Store(forLoop.Ident, typeof(int));

    // **test** does x equal 100? (do the test)
    this.il.MarkLabel(test);
    this.il.Emit(Emit.OpCodes.Ldloc, this.symbolTable[forLoop.Ident]);
    this.GenExpr(forLoop.To, typeof(int));
    this.il.Emit(Emit.OpCodes.Blt, body);
}

この IL コードは、まず最初の for ループのカウンタに数値を代入し、その直後に IL 命令 br (分岐) を使用して for ループのテストに移動しています。IL コードの左側に列挙されているラベルは、次の命令に分岐する場所をランタイムに示すために使用されます。テスト コードでは、blt (特定の数値未満である場合は分岐) 命令を使用して、変数 x の値が 100 未満であるかどうかを確認しています。100 未満の場合は、ループの本体が実行された後、変数 x の値が 1 増加し、再度テストが実行されます。

図 13 の for ループのコードは、カウンタ変数に値を代入して 1 ずつ増やす処理を実行するために必要なコードを生成します。また、ILGenerator の MarkLabel メソッドを使用して、分岐命令の分岐先として使用できるラベルを生成しています。

まとめ (後一息です)

簡単な .NET コンパイラのコード ベースを詳しく見ていき、基盤となる理論の一部について説明しました。この記事は、コンパイラの構築という不思議な世界の基本事項について説明することを目的としています。オンラインでも価値ある情報が提供されていますが、参考にしていただきたい書籍も数冊あります。次の書籍を一読されることをお勧めします。『Compiling for the .NET Common Language Runtime』John Gough 著 (Prentice Hall、2001)、『Inside Microsoft IL Assembler』Serge Lidin 著 (Microsoft Press®、2002)、『Programming Language Pragmatics』Michael L. Scott 著 (Morgan Kaufmann、2000)、『Compilers: Principles, Techniques, and Tools』Alfred V. Oho、Monica S. Lam、Ravi Sethi、Jeffrey Ullman 共著 (Addison Wesley、2006)。

これで、独自の言語コンパイラを作成するために理解する必要がある基本事項はほとんど説明しましたが、これで完了したとは言えません。真髄を極めたい皆さんがさらに学習を進められるよう、高度なトピックについてもいくつか触れたいと思います。

動的なメソッドの呼び出し

メソッドの呼び出しは、あらゆるコンピュータ言語の基盤となりますが、呼び出しにはさまざまな種類があります。Python のような新しい言語では、メソッドのバインドと呼び出しを必要になる直前まで遅延させることができます。これを動的呼び出しといいます。Ruby、JavaScript、Lua、Visual Basic など、よく使用される動的言語は、すべてこのパターンを使用しています。コンパイラがメソッドの呼び出しを実行するコードを生成するには、メソッド名をシンボルとして扱い、各言語のセマンティクスに従って、このシンボルをバインドおよび呼び出し操作を実行するランタイム ライブラリに渡します。

Visual Basic 8.0 コンパイラの Option Strict を無効にしたとします。メソッドの呼び出しは遅延バインドになり、Visual Basic ランタイムは実行時にバインドと呼び出しを実行します。

Visual Basic コンパイラは Method1 メソッドへの IL 呼び出し命令を生成するのではなく、CompilerServices.NewLateBinding.LateCall という Visual Basic ランタイム メソッドへの呼び出し命令を生成します。このとき、オブジェクト (obj) とメソッドのシンボル名 (Method1) をメソッドの引数と共に渡します。Visual Basic の LateCall メソッドは、リフレクションを使用してオブジェクトの Method1 メソッドを検索し、このメソッドが検出された場合、リフレクション ベースのメソッドの呼び出しを実行します。

   Option Strict Off

Dim obj
obj.Method1()

IL_0001:  ldloc.0
IL_0003:  ldstr      "Method1"
...
IL_0012:  call       object CompilerServices.NewLateBinding::LateCall(object, ... , string, ...)

LCG を使用して簡易遅延バインドを実行する

リフレクション ベースのメソッドの呼び出しは、非常に低速になる場合があります (私の執筆した記事「リフレクション : よくあるパフォーマンスの落とし穴を避けて高速なアプリケーションを作成する」(msdn.microsoft.com/msdnmag/issues/05/07/Reflection を参照してください)。メソッドのバインドもメソッドの呼び出しも、単純な IL 呼び出し命令と比べて非常に低速な多数の命令から構成されます。.NET Framework 2.0 CLR には、Lightweight Code Generation (LCG) という機能が含まれています。この機能を使用すると、実行時にコードを動的に生成し、より高速な IL 呼び出し命令を使用して呼び出しサイトからメソッドへの橋渡しを行うことができます。これにより、メソッドの呼び出しにかかる時間が大幅に短縮されます。この場合もオブジェクト上のメソッドを検索する必要がありますが、一度メソッドが検出されると、DynamicMethod ブリッジが作成およびキャッシュされるため、その後同じ呼び出しが発生したときに再利用することができます。

図 15 は、ブリッジ メソッドのコードを動的に生成する、非常に単純な遅延バインダを示しています。まず、キャッシュを検索して、呼び出しサイトが以前に参照されたかどうかを確認します。この呼び出しサイトが初めて実行される場合は、オブジェクトを返し、オブジェクト配列を引数として使用する DynamicMethod を生成します。オブジェクト配列の引数には、インスタンス オブジェクトと、最終的なメソッドの呼び出しに使用される引数が含まれます。IL コードが生成され、インスタンス オブジェクト、引数の順にスタック上に展開されます。次に呼び出し命令が生成され、この呼び出しの結果が呼び出し先に渡されます。

LCG ブリッジ メソッドの呼び出しは、デリゲートを使用して非常に短い時間で実行されます。この処理を実装するには、ブリッジ メソッドの引数をオブジェクト配列に格納して呼び出しを行うだけです。この処理が初めて実行されると、JIT コンパイラはこの動的メソッドをコンパイルし、IL を実行します。次に、この IL が最終的なメソッドを呼び出します。

このコードは、基本的に事前バインドの静的コンパイラが生成するものと同じです。このコードは、スタックにインスタンス オブジェクトと引数を順にプッシュした後、呼び出し命令を使用してメソッドを呼び出します。これは、セマンティクスをぎりぎりまで遅延させることにより、ほとんどの動的言語で使用される遅延バインド呼び出しのセマンティクスに準拠させることができる優れた方法です。

動的言語ランタイム

本格的に CLR に動的言語を実装する場合は、4 月の終わりに CLR チームから発表された動的言語ランタイム (DLR) について確認する必要があります。DLR では、高いパフォーマンスを備えた動的言語を構築し、.NET Framework や、.NET に準拠したその他の言語のエコシステムと相互運用するために必要な多数のツールやライブラリが提供されます。ライブラリには、高いパフォーマンスを備えた、一般的な動的言語の抽象化 (遅延バインドを使用した超高速メソッド呼び出し、型システムの相互運用性など) の実装や、動的型システム、共有 AST、Read Eval Print ループ (REPL) のサポートなど、動的言語の作成に必要なものがすべて揃っています。

この記事では DLR について詳しく説明しませんが、DLR が動的言語に提供しているサービスについては確認しておくことをお勧めします。DLR の詳細については、Jim Hugunin のブログ (blogs.msdn.com/hugunin) を参照してください。

Joel Pobar は、前職でマイクロソフトの CLR チームのプログラム マネージャを務めていました。現在はオーストラリアのゴールドコーストで、コンパイラや言語など、さまざまな楽しいものに取り組んでいます。.NET に関する彼の最新の話題については、callvirt.net/blog を参照してください。

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


WinUnit: ネイティブ C++ アプリケーション用の簡略化された単体テスト

WinUnit

ネイティブ C++ アプリケーション用の簡略化された単体テスト

Maria Blees
この記事の内容 : :
  • 単体テストの方針
  • WinUnit の設定
  • テスト フィクスチャの作成
  • 実装とカスタマイズ
この記事は次のテクノロジを使用しています:
C++、Visual Studio
コードのダウンロード : : WinUnit2008_02.exe (1438KB)
コンテンツ
今すぐテストを開始する
WinUnit 入門
フィクスチャ : セットアップと破棄
WinUnit を実行する
実装の詳細
さらに先に進む

最近、ネイティブ コード開発者としてあからさまな迫害を感じずにいるのが難しいことがあります。Microsoft® .NET Framework を使用している開発者は便利なツールをすべて入手しているように思えます。

筆者は優れたエンジニアリング手法に常に関心を持ってきましたが、単体テストの価値を絶賛しながら、ネイティブ コード用の推奨ツールを尋ねられても手を振ることぐらいしかできないいわゆる "エンジニアリングのプロ" には失望させられます。筆者が本当に求め、自動化ビルド システムに最も簡単に統合できると考えたものは、NUnit に相当するネイティブ コード用のツールでした。つまり、中にテストのみ含む DLL を作成できること、さらにこれらのテストを実行してレポートとログの記録を行う外部テスト実行ツールがあることを望んでいました。また、各テストは 1 回のみ宣言すればよく、テスト DLL 内の余分なコードは最小限であることを希望していました。

このため、WinUnit というネイティブ コードの単体テスト ツールを作成しました。詳細は後で説明しますが、ここでは WinUnit を使用してテスト DLL を作成および実行するのがどれほど簡単かを概説します。まず、単一の CPP ファイルを作成します。これを DummyTest.cpp と呼びましょう。

#include "WinUnit.h"

BEGIN_TEST(DummyTest)
{
   WIN_ASSERT_TRUE(3 > 4, _T("You should see this error message."));
}
END_TEST

次のコマンドを使用して、DLL にビルドします。

cl /EHsc /LD DummyTest.cpp

次に、これに対して WinUnit を実行します。

>WinUnit DummyTest.dll
Processing [DummyTest.dll]...
(DummyTest)
DummyTest.cpp(5): error : WIN_ASSERT_TRUE failed: "3 > 4". You should see this error message.
FAILED: DummyTest.
[DummyTest.dll] FAILED.  Tests run: 1; Failures: 1.

There were errors.
Tests run: 1; Failed: 1.

3 > 4 を真の式 4 > 3 に変更すると、当然エラーがなくなります。

WinUnit は、任意の数の DLL またはディレクトリを受け入れて処理し、総合的な結果をレポートし、終了コードを返します。ところで、予想もしていない場合のために述べておきますが、WinUnit は Windows® でのみ動作します。

筆者が独自のツールを作成するまで、C++ の単体テストのためのオプションが世界に 1 つもなかったわけではないことを認めておきます。既存のツールの問題は、一般に理解しやすさや、すぐに簡単に使用できることよりも、移植性を優先していることでした。CppUnitLite は移植可能なセットの中で筆者のお気に入りですが、高度なカスタマイズ性に重点を置いてデザインされているため、セットアップに予想以上のオーバーヘッドがありました。すべてが同じバイナリで行われるためテスト実行ツールとテストの間の線引きが明確でなく、実際にテスト実行機能を呼び出すコードを作成してどこかに挿入する必要があります。また、厄介なマクロ実装を同僚に説明するのが難しいため、導入が困難でした。ただし、他のオペレーティング システムへの移植性が必要な場合は、CppUnitLite は明らかに検討の価値があります。

かなり前から C++ 単体テストに使用可能だったその他のオプションは、.NET コードのテストに使用できるのと同じオプションです。その代表は NUnit と Visual Studio® Team Test 単体テスト フレームワークです。どちらも適切に属性指定された .NET アセンブリを実行するため、これらのツールはマネージ C++ (現在は C++/CLI) で作成されたテストに使用できます。このアプローチに見られる主な不都合は、ツールがネイティブ コードではなく .NET 開発を主な対象としていることです。これは驚くことではありません。しかし、提供されているアサート メソッドの大半は、.NET スタイルのオブジェクトを想定しているためネイティブ C++ では使用するのが難しく、開発者は、テストを作成するために C++/CLI (または C++ のマネージ拡張) について多少なりとも知っている必要があります。このアプローチでの制限事項の詳細は、Visual Studio 2005 製品機能の比較ページ (msdn2.microsoft.com/vstudio/aa700921.aspx) の脚注に記載されています。前に述べたように、"使用するよう同僚を簡単に説得できること" は重要な要素ですが、マネージ ツールの使用によって、この分野に障壁が追加されます。

今すぐテストを開始する
コード カバレッジと利便性

単体テストとの 関係について言及する価値のある Visual Studio Team System (VSTS) の機能を簡単に説明します。その機能とは、一連のコード カバレッジ ツールです。コード カバレッジの数値の厳密な意義には異論がありますが、考え抜いたテストを入念に作成した後でカバレッジを確認することは、何はともあれ優れた健全性チェックです。

あいにく、VSTS は、ネイティブ コードにコード カバレッジ ツールを簡単に使用できるようにはしていません (コード カバレッジ ツールについて知ることすら簡単ではありません)。このプロジェクトのコードと共に、WinUnit およびコードをテスト実行するための他のシナリオと組み合わせて、ネイティブ プロジェクトでコード カバレッジを簡単に行えるようにするマクロのセットを含めました。

これらのマクロを使用するには、単純に WinUnit.vsmacros を Visual Studio の Macro Explorer に読み込みます。_Variables モジュールでは、特定のローカル マシン用の変数を設定する必要があります。このような変数には、Visual Studio パフォーマンス ツール (コード カバレッジ ツールを含む) へのパスや、WinUnit.exe および WinUnit.h へのパスが含まれます。詳細は、readme.txt およびマクロ プロジェクトの _Readme モジュールに記載されています。

CodeCoverage モジュールには、カバレッジ データ収集の開始と停止、バイナリのインストルメンテーション (プロジェクトごとにインストルメンテーションするバイナリの一覧を指定できます)、および結果での .coverage ファイルの起動を行うマクロが含まれます。

RunningTests モジュールには、現在開かれているドキュメント内のカーソルを含むテスト、現在開かれているファイル内のすべてのテスト、および選択したプロジェクト内のすべてのテストを実行するためのマクロが含まれます。その他のマクロは、関連するプロジェクト設定を変更したり、コード カバレッジをオンにして異なるテスト セットを実行したりするためのショートカットを提供します。

WinUnit の使用法の説明に入る前に、単体テストに関する筆者の全般的な方針と、ネイティブ コードの単体テストに関する詳細な方針を説明します。筆者と同様に、単体テストの例で示している簡潔で洗練されたモジュール形式のクラスが、通常処理する必要のあるレガシー コードの BLOB とは似ても似つかないことに気付いた方がいるかもしれません。単体テストを実行する前にコード ベース全体の書き直しが必要であると思われている方には、これらのテストに長い時間がかかるということをお伝えできます。さいわいにも、テスト可能な単位が簡潔で洗練されたクラスである必要はありません。単に、分離してテストできる最小のコード単位です。それがアプリケーション全体の場合は、今すぐ開始するのが最善です。

深刻なことに、アプリケーションが 1 つの実行可能ファイルだけから構成され、その唯一のインターフェイスが GUI ベースの場合は、GUI でない部分を自動化可能にするためにリファクタリングが少々必要な場合があります。ただし、これは徐々に行うことができます。GUI アプリケーションをテストしやすくする実用的なアプローチとして、Michael Feathers の「質素なダイアログ ボックス」というホワイト ペーパーを読むことをお勧めします (objectmentor.com/resources/articles/TheHumbleDialogBox.pdf)。

最終的な目標は、自動化された開発者テストをビルド プロセスの一部として実行し、開発者がバグの修正や新しいコードの作成を行うときのテストの追加をできるだけ簡単にすることです。インフラストラクチャがセットアップされたら、既存のコードのリファクタリングを作業して、問題の領域のテスト カバレッジを向上させることができます。同じく Michael Feathers が記した『レガシー コードの効率的な処理』(Prentice Hall、2004 年) は、このプロセスをガイドする優れたリソースです (多くの例が C++ で作成されています)。

アプリケーションが明確に定義されたインターフェイスを持つ静的ライブラリに既に分割されている場合は、これらのディレクトリをテスト DLL にリンクし、個々のクラスを取得してテストできます。明確なインターフェイス境界を維持するために、ライブラリごとに 1 つのテスト DLL を用意することを検討してください。

アプリケーションに独立した DLL が含まれている場合、またはアプリケーション自体が DLL 形式で提供されているライブラリである場合は、2 通りの方法でエクスポートにアクセスし、テストで実行できます。インポート ライブラリを使用する場合は、これらをテスト DLL にリンクし、テストする実稼働機能に透過的にアクセスできます。インポート ライブラリを使用しない場合は、GetProcAddress を LoadLibrary および FreeLibrary と共に使用して DLL エクスポートを取得し、テストで直接呼び出すことができます。

DLL が COM DLL の場合は、テストのために登録する必要はありません。Windows XP 以降には、"Registration-Free COM" というメカニズムが用意されています。このメカニズムを使用して、COM DLL の通常の登録要件を回避できます。詳細は、msdn2.microsoft.com/ms973913 に記載されています。さらに、テストで動作するようにした後は、実稼働コードで機能を使用できます。

アプリケーションがモノリシック実行可能ファイルの場合、たいていは少なくとも 1 つのライブラリが存在するようにそのアプリケーションを分割します。ただし、実行可能ファイル全体をテストして、終了コードが適切かどうかをチェックすることなどが必要な場合もあります。WinUnit 自体の単体テストでは、実行可能ファイル全体を実行するテストをいくつか含めました。これらは、付属のコード ダウンロードの MainTest.cpp ファイル内の TestWinUnit プロジェクトにあります。

スタイル取締官という印象を与えてしまう恐れがありますが、言及しないのは怠慢なので、実稼働コードの物理的な編成について 1 点指摘しておきます。筆者が Microsoft で大規模な C++ プロジェクトに従事していたときに、複数のクラスを任意の .cpp ファイルや .h ファイルに寄せ集めるとモジュール化 (およびテストの容易性) が非常に困難になることに気付きました。実稼働コードでこの方針にどの程度厳密に従うかは読者の判断に任せますが、クラスを類似する名前の .cpp ファイルと .h ファイルのペアに 1 つだけ関連付けると便利です。さらに、クラスごとに 1 つの一致するテスト ファイルを使用すると、テストの整理を簡略化できます。プロジェクト WinUnitLib および TestWinUnit に、いくつか例があります。

単体テストの全体的な概念がわからない場合、またはテスト担当者の思考方法について簡単な紹介を必要としている開発者には、お勧めする本が他に 1 冊あります。Andrew Hunt および David Thomas による『NUnit での C# を使用したプログラムによる単体テスト』(Pragmatic Bookshelf、2006 年) は、優れた入門書です (Java 版もあります)。タイトルが言語固有であるにもかかわらず、この本の内容は実際には言語に依存していません。この本の中の気の利いた記憶法は、単体テストを A-TRIP (Automatic (自動的)、Thorough (徹底的)、Repeatable (反復的)、Independent (独立)、および Professional (専門的)) にする必要があるというものです。反復的とは、テストを実行するたびに同じ結果が生成されることを意味します。独立とは、相互に依存することなく、各テストを任意の順序で実行できる必要があることを意味します。後でこの点を思い出すことが重要になります。

WinUnit 入門

最初に行う必要があるのは、WinUnit を作成し、WinUnit.exe および WinUnit.h をマシン上の既知の場所に置くことです。詳細については、コードと共に含まれている readme.txt を参照してください。次に、テストの作成を開始するために、テスト プロジェクトを作成します。前に示したように、これは単なる通常の C++ DLL です。Visual Studio でのネイティブ C++ DLL プロジェクトの作成の詳細については、msdn2.microsoft.com/ms235636 のチュートリアルを参照してください。

他の単体テスト フレームワークと同様に、テスト作成の便宜を図るために、WinUnit には前に示した WIN_ASSERT_TRUE などのいくつかのアサート マクロが用意されています (WinUnit で使用できるその他の WIN_ASSERT マクロについては、後でこのセクションで説明します)。アサートは、C++ 例外を使用して機能を実行するため、テスト DLL の作成時には、Visual Studio 2005 の既定のコンパイラ オプション /EHsc を保持する必要があります。ただし、実稼働コードでは、テスト DLL にリンクされている場合でも C++ 例外処理の使用は必須ではありません (Visual C++® 2005 以降のツールセットは、提供されているアサート マクロを使用するために必要です)。WinUnit の Include ディレクトリをテスト プロジェクトのインクルード パス (図 1 を参照)、またはグローバルなインクルード パスに追加することも必要です。

図 1 プロジェクトのインクルード パスの設定
図 1 プロジェクトのインクルード パスの設定(画像を縮小するには、ここをクリックします)
図 1 プロジェクトのインクルード パスの設定
図 1 プロジェクトのインクルード パスの設定 (画像を拡大するには、ここをクリックします)

筆者は、WinUnit をコマンド ラインからも実行できるようにデザインしました。このため、ビルドの後に毎回テスト プロジェクトに対して WinUnit が実行されるように Visual Studio をセットアップできます。この処理を行うには、テスト プロジェクトのプロパティ ページに移動し、[構成プロパティ] の [ビルド イベント] で [ビルド後のイベント] を選択します (図 2 を参照)。[コマンド ライン] には、WinUnit.exe をコピーした場所の完全パスに続けて「"$(TargetPath)"」と入力します (二重引用符を含めます)。[説明] には、「Running WinUnit...」と入力します。WinUnit.exe が格納されているフォルダをグローバル実行可能ファイル パスに追加する場合は、完全パスをここで指定する必要はありません。

図 2 ビルド後のイベントとしての WinUnit の構成
図 2 ビルド後のイベントとしての WinUnit の構成(画像を縮小するには、ここをクリックします)
図 2 ビルド後のイベントとしての WinUnit の構成
図 2 ビルド後のイベントとしての WinUnit の構成 (画像を拡大するには、ここをクリックします)

代わりに (または、これに加えて)、テストを実行するために WinUnit.exe を [ツール] メニューに追加することもできます。これを行うには、[ツール] の [外部ツール] に移動し、[追加] をクリックします。[タイトル] には「&WinUnit」と入力します。[コマンド] では WinUnit.exe を参照します。[引数] には「"$(TargetPath)"」と入力します (二重引用符を含めます)。[初期ディレクトリ] には「$(TargetDir)」と入力します。[終了時にウィンドウを閉じる] をオフにし、[出力ウィンドウを使用] をオンにします (図 3 を参照)。プロセスのこの時点で、[ツール] メニューから WinUnit を選択できるようになります。WinUnit は、現在選択されている任意のプロジェクトに対して実行されます。

図 3 WinUnit の外部ツールへの追加
図 3 WinUnit の外部ツールへの追加

最後に、プロジェクトをデバッグ用にセットアップするには、プロジェクトのプロパティ ページの [構成プロパティ] の [デバッグ] に移動し、WinUnit.exe の完全パスを [コマンド] テキスト ボックスに入力します。[コマンド引数] に「"$(TargetPath)"」と入力します (二重引用符を含めます)。

プロジェクトのすべての項目が正しく設定されていることを確認するために、DummyTest.cpp をプロジェクトに追加してビルドできます。プロジェクトはビルドされますが、示されているように間違ったアサーション行を入力した場合は、その行にテスト エラーが明確に表示されます。

ここでセットアップから離れて、WinUnit 機能を使用してテストを記述する別の方法を見てみましょう。差し当たり WinUnitComplete.sln に切り替えて、筆者の例を参照します。例が格納されているプロジェクトは、SampleLib と TestSampleLib です。SampleLib は、BinaryNumber という 1 つのクラスのみ含む静的ライブラリです。TestSampleLib は、SampleLib.lib にリンクし、他のいくつかの例と共に BinaryNumber クラスのテストを含むテスト DLL です。

一般的な WinUnit テスト機能は、常に BEGIN_TEST(TestName) で開始し、END_TEST で終了します。各テスト内では、機能のさまざまな部分を検証するために 1 つ以上の WIN_ASSERT マクロが使用されます。

BinaryNumber クラスの例には、2 つのコンストラクタがあります。一方は unsigned short を受け取り、もう一方は 1 と 0 の文字から構成される文字列を受け取ります。コンストラクタでは、等価な値を渡したときに、等価なオブジェクトが生成されるようにしました。したがって、テストは次のようになります。

BEGIN_TEST(BinaryNumberConstructorsShouldBeEquivalent)
{
  unsigned short numericValue = 7;
  BinaryNumber bn1(numericValue);
  BinaryNumber bn2("111");
  WIN_ASSERT_EQUAL(bn1.NumericValue, bn2.NumericValue,
    _T("Both values should be %u."), numericValue);
  WIN_ASSERT_STRING_EQUAL(bn1.StringValue, bn2.StringValue);
}
END_TEST

2 つのアサート行のいずれかが偽の場合、このテストは失敗します。この例の WIN_ASSERT_EQUAL マクロは、比較される 2 つの値に加えて、2 つの追加引数を渡されることに注意してください。これらは、アサートが失敗した場合に表示される情報メッセージを構成します。すべての WIN_ASSERT マクロは、オプションの printf スタイル形式の文字列と引数をこの目的で受け取ります。

BinaryNumber クラスには演算子 '==' を実装したため、次のコンストラクションも使用できます (これはサンプル ファイル BinaryNumberTest.cpp にあります)。

BEGIN_TEST(
  BinaryNumberConstructorsShouldBeEquivalent)
{
  BinaryNumber bn1(7);
  BinaryNumber bn2("111");
  WIN_ASSERT_EQUAL(bn1, bn2);
}
END_TEST

次に、別の種類のアサート WIN_ASSERT_THROWS を示します。実稼働コードのエラー処理に例外を使用することを想定し、テストでエラー条件を強制的に起こして適切な例外がスローされることを確認します。テストは次のようになります。

BEGIN_TEST(
  BinaryNumberPlusRecognizesIntegerOverflow)
{
  unsigned short s = USHRT_MAX / 2 + 1;

    BinaryNumber bn1(s);
    BinaryNumber bn2(s);

    WIN_ASSERT_THROWS(bn1 + bn2,
      BinaryNumber::IntegerOverflowException)
  }
  END_TEST

筆者の BinaryNumber クラスは、unsigned short のみ保持することがわかっています。大きすぎる 2 つの数値を加算しようとした場合に、演算子 '+' がその状況を検出する必要があることもわかっています。WIN_ASSERT_THROWS は、C++ 例外をスローする必要のある式を、例外の種類自体と共に受け取ります。例外がスローされない場合はテストが失敗します。

図 4 に、WinUnit.h で使用可能な WIN_ASSERT マクロと、1 つの非アサート WIN_TRACE を示します。WIN_TRACE を使用すると、テストで OutputDebugString API 関数を通じてトレースできます。

Figure 4 WinUnit.h 内のアサート マクロとトレース マクロ (Close Figure 4)

マクロ | 説明
WIN_ASSERT_EQUAL(expected, actual, ...) | == を使用して予測と実際を比較します。等しくない場合は失敗します。
WIN_ASSERT_NOT_EQUAL(notExpected, actual, ...) | != を使用して notExpected と実際の値を比較します。等しい場合は失敗します。
WIN_ASSERT_STRING_EQUAL(expected, actual, ...) | 大文字と小文字を区別して予測と実際の文字列を比較します。比較する文字列は、ワイド文字またはそれ以外にすることができます。ただし、ワイド文字かどうかは両方とも同じである必要があります (これは、オプションのメッセージがワイドかどうかには影響しません)。
WIN_ASSERT_ZERO(zeroExpression, ...) | zeroExpression を 0 と比較します。等しくない場合は失敗します。
WIN_ASSERT_NOT_ZERO(nonzeroExpression, ...) | nonzeroExpression を 0 と比較します。等しい場合は失敗します。
WIN_ASSERT_NULL(nullExpression, ...) | nullExpression を NULL と比較します。等しくない場合は失敗します。ポインタに対してのみ動作します。
WIN_ASSERT_NOT_NULL(notNullExpression, ...) | notNullExpression を NULL と比較します。等しい場合は失敗します。ポインタに対してのみ動作します。
WIN_ASSERT_FAIL(message, ...) | 常に失敗します。情報メッセージが必要です。
WIN_ASSERT_TRUE(trueExpression, ...) | trueExpression が true に評価される場合に成功します。
WIN_ASSERT_FALSE(falseExpression, ...) | !falseExpression が true に評価される場合に成功します。
WIN_ASSERT_WINAPI_SUCCESS(trueExpression, ...) | trueExpression が true に評価される場合に成功します。失敗時に GetLastError を呼び出してエラーの詳細情報を取得することがドキュメントに記載されている Windows 関数に使用します。このマクロ/関数は、GetLastError を呼び出し、エラー コードに関連付けられている文字列をメッセージの一部として含めます。
WIN_ASSERT_THROWS(expression, exceptionType, ...) | 式が exceptionType 型の C++ 例外をスローする場合に成功します。
WIN_TRACE(message, ...) | デバッグの目的で情報メッセージを出力する場合に使用します。使用してもテストが失敗する原因にはなりません。"message" は、後に引数が続く printf スタイル形式の文字列です。

数値リテラルの整数は常に int とテンプレート照合されるため、WIN_ ASSERT_EQUAL または WIN_ASSERT_NOT_EQUAL で値の 1 つとして数値リテラルが渡され、それ以外の値として符号なしの数値が渡された場合は、符号付き/符号なしの不一致が発生します。これを回避するには、数値リテラルの末尾に 'U' を追加して、符号なしとして照合されるようにします。

すべてのアサートは、オプションの printf スタイル形式の文字列に加えて引数を受け取ります。_UNICODE が定義されている場合、これらのメッセージ文字列は wchar_t* になるため、書式文字列の周囲に _T ("") マクロを使用するか、Unicode のみでビルドしている場合は L"" を使用します。

テスト名は、テストされるクラスの名前に続けて、テストされるメソッドの名前または説明、さらにテストで表示されると想定される内容を説明する部分的な文から開始していることにお気付きかもしれません。この表記規則には 2 つの目的があります。まず、テストしようとしている内容の明確化が簡単になります。これはテスト出力を参照するときに役立つ場合があります。次に、さまざまな細分化レベルで共に実行されるようにテストをグループ化できます。本質的に WinUnit には、.NET Framework ベースの同等のツールのようなテストのグループ化の概念がありません。プロジェクト内のテストのサブセットを実行するよう WinUnit に指示するには、-p (プレフィックス) オプションを使用します。プレフィックス文字列を指定すると、WinUnit は名前がその文字列で始まるすべてのテストを実行します。汎用的な単語から順に使用してテストに名前を付けることで、関連するテスト グループをコマンド ラインから簡単に実行できます。

フィクスチャ : セットアップと破棄

現実の世界では、多くの場合にテストの対象はこれまでに示した例ほど単純ではありません。検証対象の機能を実際に実行できるポイントに到達するために、複数のセットアップ手順が必要な場合があります。たとえば、ディレクトリ内のすべてのファイルを削除する関数をテストする場合は、最初にディレクトリを作成し、その中にファイルを入れる必要があります。また、テストする機能に必要な環境変数の設定が必要な場合があります。

テストの独立性と反復性を維持するために、テストの開始時に行うセットアップ作業がテストの終了時にすべて元に戻されることが重要です。WinUnit では、単一テスト フィクスチャの概念がサポートされます。これらは、関連付けられている各テストの開始時と終了時に実行されるセットアップおよび破棄の関数のペアです。同じセットアップとクリーンアップが必要な複数のテストがある場合に特に役立ちます。

図 5 に、セットアップ関数と破棄関数が実装され、2 つのテストが関連付けられているフィクスチャの例を示します。この例は、フィクスチャでもさまざまな WIN_ASSERT マクロを使用できるため、定義するフィクスチャが正しく動作しない場合にエラーをレポートできることも示しています。

Figure 5 セットアップと破棄によるテスト フィクスチャ (Close Figure 5)
//  DeleteFileTest.cpp

#include "WinUnit.h"

#include <windows.h>

// Fixture must be declared.
FIXTURE(DeleteFileFixture);

namespace
{
    TCHAR s_tempFileName[MAX_PATH] = _T("");
    bool IsFileValid(TCHAR* fileName);
}

// Both SETUP and TEARDOWN must be present.
SETUP(DeleteFileFixture)
{
    // This is the maximum size of the directory passed to GetTempFileName.
    const unsigned int maxTempPath = MAX_PATH - 14;
    TCHAR tempPath[maxTempPath + 1] = _T("");
    DWORD charsWritten = GetTempPath(maxTempPath + 1, tempPath);
    // (charsWritten does not include null character)
    WIN_ASSERT_TRUE(charsWritten <= maxTempPath && charsWritten > 0,
        _T("GetTempPath failed."));

    // Create a temporary file
    UINT tempFileNumber = GetTempFileName(tempPath, _T("WUT"),
        0, // This means the file will get created and closed.
        s_tempFileName);

    // Make sure that the file actually exists
    WIN_ASSERT_WINAPI_SUCCESS(IsFileValid(s_tempFileName),
        _T("File %s is invalid or does not exist."), s_tempFileName);
}

// TEARDOWN does the inverse of SETUP, as well as undoing
// any side effects the tests could have caused.
TEARDOWN(DeleteFileFixture)
{
    // Delete the temp file if it still exists.
    if (IsFileValid(s_tempFileName))
    {
        // Ensure file is not read-only
        DWORD fileAttributes = GetFileAttributes(s_tempFileName);
        if (fileAttributes & FILE_ATTRIBUTE_READONLY)
        {
            WIN_ASSERT_WINAPI_SUCCESS(
                SetFileAttributes(s_tempFileName,
                    fileAttributes ^ FILE_ATTRIBUTE_READONLY),
                    _T("Unable to undo read-only attribute of file %s."),
                    s_tempFileName);
        }

        // Since I'm testing DeleteFile, I use the alternative CRT file
        // deletion function in my cleanup.
        WIN_ASSERT_ZERO(_tremove(s_tempFileName),
            _T("Unable to delete file %s."), s_tempFileName);
    }

    // Clear the temp file name.
    ZeroMemory(s_tempFileName,
        ARRAYSIZE(s_tempFileName) * sizeof(s_tempFileName[0]));
}

BEGIN_TESTF(DeleteFileShouldDeleteFileIfNotReadOnly, DeleteFileFixture)
{
    WIN_ASSERT_WINAPI_SUCCESS(DeleteFile(s_tempFileName));
    WIN_ASSERT_FALSE(IsFileValid(s_tempFileName),
        _T("DeleteFile did not delete %s correctly."),
        s_tempFileName);
}
END_TESTF

BEGIN_TESTF(DeleteFileShouldFailIfFileIsReadOnly, DeleteFileFixture)
{
    // Set file to read-only
    DWORD fileAttributes = GetFileAttributes(s_tempFileName);
    WIN_ASSERT_WINAPI_SUCCESS(
        SetFileAttributes(s_tempFileName,
        fileAttributes | FILE_ATTRIBUTE_READONLY));

    // Verify that DeleteFile fails with ERROR_ACCESS_DENIED
    // (according to spec)
    WIN_ASSERT_FALSE(DeleteFile(s_tempFileName));
    WIN_ASSERT_EQUAL(ERROR_ACCESS_DENIED, GetLastError());
}
END_TESTF

namespace
{
    bool IsFileValid(TCHAR* fileName)
    {
        return (GetFileAttributes(fileName) != INVALID_FILE_ATTRIBUTES);
    }
}

この例では、筆者が Windows DeleteFile 関数の作成者であること、およびいくつかのテスト関数を作成していることを示しています。セットアップ関数では、最初に一時ファイルを作成します。テスト関数では、一時ファイルの削除を試行することで DeleteFile 機能をテスト実行します。1 つのテストの終了時にファイルは削除されていると予想されますが、ファイルが存在するかどうかをチェックし、破棄関数でそのファイルを削除します。破棄では、セットアップで行われた処理をすべて元に戻す必要があります。テストの成功に頼ってはなりません。これにより、テストが独立し、反復可能になります。

ご覧のように、フィクスチャを使用するテストの構文は若干異なります。BEGIN_TEST コンストラクションと END_TEST コンストラクションの代わりに、BEGIN_TESTF と END_TESTF が使用され、BEGIN_TESTF ではテスト名の後にフィクスチャ名が続きます。セットアップ関数と破棄関数は、すべて大文字の SETUP および TEARDOWN と、かっこで囲まれたフィクスチャ名で識別されます。

この例では WIN_ASSERT_WINAPI_SUCCESS マクロを数回使用していることがわかります。図 4 に示したように、このマクロは、エラーの場合に GetLastError を呼び出して詳細情報を取得することがドキュメントに明記されている Windows 関数と共に使用します。アサートの最初のパラメータには、関数呼び出しに関連する、真と予想されるステートメントを指定する必要があります (関数呼び出しが Boolean を返す場合は、関数呼び出し自体を指定できます)。関数自体が式の一部でない場合は、このアサートが起動した場合に失敗した関数の名前を示すメッセージを含めて、厳密に何が起きたかが出力から一目でわかるようにすることをお勧めします。アサート実装は、GetLastError を呼び出し、エラー コードに関連付けられているメッセージ文字列を取得して、テスト結果に追加します。

別の方法で使用されているフィクスチャを参照するには、TestSampleLib プロジェクトの BinaryNumberTest.cpp を参照してください。そこでは、フィクスチャを使用して、テストで使用するテスト データの行を読み取るデータ プロバイダを開き、閉じます。この場合、データ プロバイダは単なるテキスト ファイルであり、各行は 1 行ですが、簡単に XML ファイルにしたりデータベース テーブルにしたりすることもできます。実行されている大量のデータの恩恵を受ける機能をテストしている場合は、同様のアプローチの使用を検討する必要があります。

WinUnit フィクスチャがその他の単体テスト フレームワークと異なる 1 つの点は、テストに自動的に関連付けられないことです。関連付けは明示的に行う必要があります。これは、単一ファイル内に複数のフィクスチャを持てることも意味します。

WinUnit を実行する

ここで、WinUnit の動作を見ていきましょう。WinUnit は、Windows 上のネイティブ コードで使用可能なリフレクション型の機能の 1 つである DLL のエクスポートの検出を使用します。2002 年 2 月および 3 月の 2 回にわたって MSDN® マガジンに掲載された Matt Pietrek の優れた記事 (「Windows の内部 : Win32® Portable Executable ファイル形式の詳細」) があります。筆者は、この処理を行う方法を把握する際にこの記事を何度も紹介してきました。WinUnit は 32 ビットと 64 ビットの両方の実行可能ファイルに対して動作しますが、64 ビットのテスト バイナリを実行するには WinUnit の 64 ビット ビルドを使用してください (32 ビット実行可能ファイルは 64 ビット実行可能ファイルを実行できません)。

WinUnit のコマンド ライン構文は次のようになります。

WinUnit [options] {dllName | directoryName}+

ツールは、1 つ以上の DLL、または含まれているすべての DLL が列挙されるディレクトリを受け取ります。エクスポートを検出し、名前 (装飾されていないことが必要) が TEST_ で始まるエクスポートを実行します。関数プロトタイプが想定されているため、この制限は、テストとして実行することが意図された関数のみ実行されるようにすることを目的としています。予想される関数プロトタイプは次のようになります。

bool __cdecl TEST_TestName(wchar_t* errorBuffer, size_t bufferSize);

errorBuffer パラメータは、テストからエラー出力を受け取ります。bufferSize は、ワイド文字単位でのバッファ サイズです。テスト関数が false を返す (または、構造化例外処理 (SEH) 例外をスローする) 場合は、テストが失敗したと見なされ、errorBuffer に挿入された出力がすべて表示されます。

DLL とディレクトリ名の他に、WinUnit はいくつかのオプションのコマンド ライン引数を受け取ります。これを図 6 に示します。既定では、WinUnit の出力はコンソールに出力されます。情報メッセージは stdout に出力され、エラー メッセージは stderr に出力されます。WinUnit は、指定された DLL を処理した後で、成功した場合は 0、それ以外の場合は 0 以外のコードで終了します (図 7 を参照)。

Figure 7 WinUnit の終了コード (Close Figure 7)

コード | 説明
0 | エラーはありません (実行されたテストに成功しました)。
1 | 1 つ以上のテストに失敗しました。
2 | 処理されない例外により、途中終了しました。
-1 | 使用法のエラー。
Figure 6 WinUnit のコマンド ライン引数 (Close Figure 6)

パラメータ | 説明
-q | コンソールに出力しません。
-n | 非対話型 UI です (エラー ダイアログはできる限りオフになります)。自動化ビルドに対して推奨されます。
-b | デバッガに出力します (既定では、コンソールにのみ出力されます)。
-p string | プレフィックス文字列から開始するテストのみ実行します (大文字と小文字は区別されません)。
-x | エクスポートの TEST_ プレフィックスに関する要件を無視します。名前にかかわらずすべてのエクスポートを処理します (間違った関数プロトタイプで関数を実行する危険性を回避するために、このオプションは -s オプションと組み合わせた場合にのみ動作します)。
-s | テスト名のみ表示します。既定では実行されます。
-o filename | 指定されたファイルに出力を挿入します。これは、-q オプションや -b オプションと関係しません。出力は、コンソール、ファイル、デバッガのいずれかまたはすべてに送信できます。
-v number | 指定された冗長性で実行します。1 が既定です。0 では、失敗したテストと全体的な結果のみ表示されます。
-l customLoggerString | カスタム ロガー文字列は、カスタム ロガーを実装した DLL の名前の後に、コロンと、ロガーの Initialize 関数に渡される初期化文字列がオプションで続く文字列です。
--variable value | このオプションは、WinUnit プロセスの有効期間にわたって環境変数 variable を値 value に設定します。コマンド ラインを介してテストにデータを渡すために使用できます。
-e ( exactTestName* ) | (メモ : このオプションは完全を期すために含まれていますが、自動化プロセスのみを対象としています。通常は –p を使用してください。) スペースで区切り、かっこで囲まれた完全な名前 (大文字と小文字を区別) で指定されたテストのみ実行します。

例で使用されている BEGIN_TEST マクロは、テスト名の先頭に TEST_ を追加し、'extern "C" __declspec(dllexport)' を使用して装飾されていない関数名をエクスポートします。これは、DLL の .def ファイルに挿入するのと同等です。

WIN_ASSERT マクロは C++ 例外を通じて動作しますが、すべての例外はテスト関数の中でキャッチされるため、WinUnit は例外が使用されていたかどうかを気にしません。例外を使用すると、任意のポイントにある任意のブロックで簡単に終了でき、その場合も適切なクリーンアップが行われていることを確認できるため、筆者は例外を使用しました。前に示した関数プロトタイプを使用して、名前が TEST_ で始まることを確認し、装飾されていない形式で DLL からエクスポートすることにより、マクロをまったく使用しない WinUnit でも動作する独自のテスト関数を実装できます。

BEGIN_TEST マクロと END_TEST マクロは、関数を宣言し、try/catch ブロックをセットアップします。WIN_ASSERT マクロは、エラー時に AssertException クラス例外をスローします。この例外は、エラーを説明するメッセージ文字列を保持します。スローされた AssertException がキャッチされた場合、メッセージはバッファにコピーされ、関数は false を返します。

実装の詳細

筆者は最初、テスト関数にパラメータを使用しないで、関数から直接例外をスローし、それらをテスト実行ツールでキャッチして処理するのがスマートであると考えました。これは、END_TEST マクロが不要であることを意味しました。残念ながら、例外が DLL 境界を越えてスローされた場合にスタックのアンワインドが正しく動作しなかったため、最終的に、実装の詳細として例外を使用し、テストの中でそれらをキャッチする方がよいという結論に達しました。

フィクスチャの概念の実装も別の課題でした。最初に考えたのは、C++ の自動格納を利用し、コンストラクタとデストラクタでセットアップと破棄を行うテスト関数の一番上でオブジェクトを宣言することでした。しかし、デストラクタで例外をスローしてはならない理由がすぐに見つかりました。C++ 仕様の一部に、スタックのアンワインド中にデスクトラクタ関数によって例外がスローされた場合は、終了関数が呼び出されると規定されています。説明したとおりにフィクスチャを実装し、そのデストラクタに例外スロー アサートを持つフィクスチャ オブジェクトがあった場合は、この状態が発生する可能性があります。関数の本体で例外がスローされたことにより、スタックのアンワインド中にフィクスチャ オブジェクトのデストラクタ内のアサートが起動した場合は、終了が呼び出されます。

その解決策は、関数のメインの try/catch ブロックの外側にある固有の try/catch ブロックにフィクスチャ オブジェクトのデストラクタを単体で挿入することにより、このデストラクタがスタックのアンワインド中に決して呼び出されないようにすることでした。そのためには、2 つの try/catch ブロックとフィクスチャ オブジェクト宣言を収容するために、BEGIN_TESTF と END_TESTF のペアを使用した特殊なフィクスチャ構文が必要でした。

不適切な終了動作を見かけたことで、確実に処理したかった他のものを思い出しました。非対話型モードで実行するオプション (現在は –n コマンド ライン オプション) を提供しようと考えました。WinUnit は、一般に非対話的に実行されますが、終了関数が呼び出されたときに、ダイアログ ボックスが表示されることに気付きました。このようなダイアログ ボックスは、自動化タスクを遮断する可能性があるため、自動化シナリオでは不都合だと考えました。

WinUnit プロジェクトの ErrorHandler.cpp で、このようなエラー メッセージを無効にしました。興味深いことに、エラー条件下で表示される可能性のあるダイアログのいくつかは、C のランタイム (CRT) に関連付けられています。このため、CRT でグローバル変数をオフに設定しても、変数がテスト実行ツール (WinUnit) で設定され、テスト バイナリが別の CRT を使用している場合には役立ちません。CRT 固有のエラー ダイアログがオフになるようにするには、WinUnit とテスト バイナリの両方のビルドに同じランタイム ライブラリ オプションを使用していることと、それが DLL オプションの 1 つであることを確認する必要があります。このオプションは、プロジェクトのプロパティ ページの [構成プロパティ] の [C/C++] の [コード生成] にあります。既定では、リリース ビルドの場合は /MD、デバッグ ビルドの場合は /MDd です。WinUnit とテスト バイナリの両方を、同じオプションを使用して Visual Studio 2005 でビルドした場合は、同じ CRT インスタンスが生成されます。

テストのデバッグに役立てるために、トレース機能が必要でした。ツールが Windows で実行され、PE 形式がわかっているという事実を利用することにしました。したがって、実行されるテスト DLL で OutputDebugStringA および OutputDebugStringW をフック (オーバーライド) し、必要な出力を送信します。この部分は主に、このプロジェクトでの使用を快く承諾してくれた John Robbins の『Microsoft .NET および Microsoft Windows 用のデバッグ アプリケーション』(Microsoft Press®、2003) に付属のコードを借用しました。次に、引数を OutputDebugString に送信する WIN_TRACE マクロを提供しました。

コマンド ラインからテストに情報を渡すことも考えました。たとえば、ツールを自動化ビルドの一部として使用していて、テスト データ ファイルを含む既知の場所があり、テストでその場所について知る必要がある場合に、この操作が役に立つと考えました。環境変数について決定しました。"--" コマンド ライン オプションを使用して、任意の変数をプロセスに対して設定できます。これらは、WinUnit::Environment::GetString (GetEnvironmentVariable に対するラッパー)、GetEnvironmentVariable 自体、または getenv_s/_wgetenv_s を使用してテスト内で取得できます。

最後に、ユーザーがカスタム ロガーを作成できるようにすることを考えました。内部的には、ロガー チェーンが使用されているため、出力をコンソール、OutputDebugString、ファイル、またはこれらの任意の組み合わせに送信できます。4 つ目のオプションは、次に説明する固有のカスタム ロガーの提供です。

さらに先に進む

WinUnit は、基本的なネイティブ C++ 単体テストのニーズを解決しますが、これに追加することを検討できる項目がいくつかあります。

カスタム ロガーで渡すための図 6 の -l コマンド ライン オプションにお気付きかもしれません。この機能を使用して、WinUnit 出力を XML ファイルに、またはレポート システムで使用するためにデータベースに送信することもできます。また、既定のロガーは Unicode を記述しません。–o オプションは ANSI テキスト ファイルを作成します。Unicode に出力する場合は、そのためのカスタム ロガーを作成する必要があります。カスタム ロガー機能を実装するには、10 個のロガー関連関数のセットを 1 つ以上実装する DLL を作成し、-l オプションを使用してその DLL へのパスを WinUnit に渡すだけです。詳細については、プロジェクト SampleLogger.cpp を参照してください。

既存のアサートを例として使用して、固有のカスタム アサートを追加することもできます。追加のヘッダー ファイルまたは静的ライブラリをテスト DLL にリンクできます。正規表現での文字列比較を含む、.NET ベース コードの Visual Studio 単体テスト フレームワークに付属の StringAssert クラスに似た、より綿密な文字列アサートのセットを実装するとおもしろいかもしれません。FileAssert クラスも役立つ可能性があります。このクラスは、ファイル全体またはファイルの属性を比較します。筆者がアサートの実装に対して Assert クラスを静的メソッドと共に使用しており、WIN_ASSERT マクロがそれらのシン ラッパーであることがわかります。マクロをまったく使用しないで、ファイルと行番号を簡単に含められるようにしたいと考えました。当然、マクロはグローバル名前空間を煩雑にするため、マクロを独自に作成する場合は、別のプレフィックスで開始することをお勧めします。

別の便利な Visual Studio アドインは、右クリックすることで (コード カバレッジあり、またはなしで) テストを個別に実行できるようにするものです。これは、.NET Framework の単体テストに対する TestDriven.NET の動作と似ています。コード カバレッジ ツールの簡単な説明については、「コード カバレッジと利便性」のリンクを参照してください。

目標は、ネイティブ C++ の単体テストが簡単でおもしろく、今すぐ開始できることを示すことでした。WinUnit の能力を示すために、WinUnit を使用して WinUnit 自体をテストする TestWinUnit プロジェクトを見てみましょう。これらの例は完全に現実のものであり、自身の単体テストに適用できる高度な使用方法を示します。ネイティブ C++ の単体テストに苦労していたなら、WinUnit によって簡単にすることができます。テストを簡単にすることができれば、実際に行う可能性ははるかに高くなります。

Maria Blees は開発者としてマイクロソフトに 10 年間勤務しており、ネイティブ コードとエンジニアリング エクセレンスをこよなく愛好しています。Maria の連絡先アドレスはコードに記載されており、このツールに関するバグ報告や提案を歓迎しています。


Silverlight: Expression Encoder 用のカスタム公開プラグインを作成する

Silverlight

Expression Encoder 用のカスタム公開プラグインを作成する

Laurence Moroney
この記事の内容 : :
  • Expression Encoder とプラグインの基本
  • 定型プラグインを構築する
  • FTP アップロード機能を追加する
  • 構築、インストール、およびデバッグを行う
この記事は次のテクノロジを使用しています:
WPF、Visual Studio、Expression Encoder
コンテンツ
Expression Encoder とプラグイン
プラグインを作成する
FTP アップロード プラグインを構築する
公開クラスを実装する
構成の設定を保存する
コンパイル、展開、およびデバッグを行う

最近リリースされたツール、Microsoft® Expression® Encoder を使用すると、Microsoft SilverlightTM でレンダリング可能なリッチ メディア エクスペリエンスをエンコード、強化、および公開することができます。Expression Encoder には拡張可能なプラグイン アーキテクチャが採用されており、Expression Encoder 出力を別のソースに公開するためのプラグインを記述する機能が提供されます。

この記事では、Microsoft Visual Studio® 2008 Beta 2 を使用して Expression Encoder 用のプラグインを構築する方法について詳細に説明します。このプラグインは、Expression Encoder セッションの標準ワークフロー内に配置され、エンコーディング セッションの最後に結果の出力が FTP サーバーにアップロードされます。

Expression Encoder とプラグイン

図 1 は、Expression Encoder を示しています。ワークスペースの左側にはビデオ プレビューおよびコントロール バーがあり、ビデオのどの部分を再エンコードするかを設定できます。画面の右側には 3 つのタブがあります。エンコード セッションを構成する [Settings] (設定)、新しい情報をメディアに適用する [Metadata] (メタデータ)、およびエンコード後のアクションを設定する [Output] (出力) です。

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

公開するプラグインが <インストール ディレクトリ>\Plugins にあり、正しく読み込まれた場合は、利用可能な公開オプションの一覧を示す [Publish] (公開) ペインが [Output] (出力) タブに表示されます。各タイプに別々のプラグインが必要です。図 2 は、FTP プラグインが利用可能であることを示しています。

図 2 FTP プラグインを選択する
図 2 FTP プラグインを選択する

[Publish] (公開) ペイン上部はすべてのプラグインに対して固定されています。[Publish To] (公開先) 一覧には、プラグイン ディレクトリで検出された有効なプラグインの名前が表示されます。[Auto Start Publish] (自動公開開始) チェックボックスは常に利用可能で、このオプションをオンにするとエンコーディングの最後に公開が自動的に開始されます。

エンコーディングが完了するまで [Publish] (公開) ボタンは灰色表示で、完了すると利用可能になります。このボタンをクリックすると、エンコーディングに関連付けられたファイルをすべて一度に公開できます (テンプレートが使用されていない場合は 1 つのファイル、テンプレートが使用されている場合はビデオ プレーヤーに必要なすべてのファイル)。[Settings] (設定) セクションで、出力を公開するのに必要な詳細を指定できます。FTP 出力の場合は、サーバー アドレス、ユーザー名、およびパスワードが必要です。プラグインにより提供される詳細設定領域の機能は最初は表示されていませんが、ペイン展開ボタンを使用して展開できます。

[Settings] (設定) ペインは自由な形式で柔軟性に富んでいるため、XAML および Windows® Presentation Foundation (WPF) を使用し、対象のプラグインに合わせて実装できます。

プラグインを作成する

プラグインを構築するときの最初の手順は、新しい Microsoft .NET Framework 3.0 クラス ライブラリを作成することです。次に、WPF およびエンコーダ アプリケーションをコンパイルするために必要な参照を追加します。これらを図 3 に示します。

Figure 3 必要な Encoder リファレンス (Close Figure 3)

リファレンス | 場所
EncoderTypes | C:\Program Files\Microsoft Expression\Encoder 1.0\EncoderTypes.dll
PresentationCore | C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll
PresentationFramework | C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll
WindowsBase | C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll

プラグインは単なる標準 .NET クラスです。PublishPlugin 基本クラスから派生し、EncoderPluginAttribute 属性 (プラグインを説明するメタデータを追加) で修飾されます。コンストラクタは、2 つの文字列、プラグイン名、および長い説明を受け取ります。

必要なコードを次に示します。

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Expression.Encoder.Plugins.
  Publishing;

namespace TestPlugin
{
    [EncoderPlugin("Boiler Plate",
      "Boiler Plate App")]
    public class Class1 : PublishPlugin
    {
    }
}

プラグインを正常にコンパイルするには、PublishPlugin クラスの 3 つのメンバ関数を実装する必要があります。CreateStandardSettingsEditor は標準設定ダイアログを作成し、UI を記述するオブジェクトを返します。CreateAdvancedSettingsEditor は、詳細設定ダイアログを作成し、ダイアログの UI を記述するオブジェクトを返します。[Publish] (公開) ボタンを押すと PerformPublish が呼び出されます。受け取る 2 つのパラメータは root と filesToPublish です。root には公開するファイルのディレクトリが含まれます。filesToPublish は Expression Encoder が公開するファイルへのパスの配列です。

図 4 は、これら 3 つの関数を実装する定型クラスの例を示しています。このクラスは DLL にコンパイルされます。この DLL を Plugins ディレクトリにコピーすると、後で公開プラグインとして使用できます。

Figure 4 定型プラグイン (Close Figure 4)
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Expression.Encoder.Plugins.Publishing;

namespace TestPlugin
{
    [EncoderPlugin("Boiler Plate", "Boiler Plate App")]
    public class Class1 : PublishPlugin
    {
        public override void PerformPublish(string root,
            string[] filesToPublish)
        {
        }
        #region IPublishPlugin Members
        public override object CreateStandardSettingsEditor()
        {
            return "Standard Settings";
        }
        public override object CreateAdvancedSettingsEditor()
        {
            return "Advanced Settings";
        }
        #endregion
    }
}

属性のコンストラクタ引数によって [Publish To] (公開先) ドロップダウン リストのエントリが定義され、CreateStandardSettingsEditor および CreateAdvancedSettingsEditor 関数で返されたオブジェクトによって [Settings] (設定) ペインの UI が定義されます。

FTP アップロード プラグインを構築する

単純な定型プラグイン (実際には何も行わない) から始めて、機能する FTP アップロード プラグインに拡張します。標準および詳細パラメータ ペインは WPF XAML に実装されますが、ユーザーが設定に加えた変更をプラグインで検出できるように、INotifyPropertyChanged を継承するクラスにパラメータのデータを実装する必要があります。両方のパラメータ ペインに 1 つのクラス (この場合は PublishData) で十分です。

FTP アップロード プラグイン用に 5 つのプロパティが実装されます。UseProxy は、システム プロキシを使用するか、バイパスするかを決定します。ServerAddress は、アップロード先の FTP サーバーのアドレスを提供します。UserName はサーバーのログイン ID、UserPassword はログイン ID に関連付けられたパスワードです。ServerDirectory は、アップロードしたファイルを受け取るサーバーのディレクトリです。図 5 は、これらのプロパティを実装する完全なクラスを示しています。

Figure 5 FTP プラグインのプロパティ (Close Figure 5)
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using System.Threading;
using System.ComponentModel;

namespace FTPPublishPlugin
{
    public class PublishData : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private bool _UseProxy = false;
        public bool UseProxy
        {
            get { return _UseProxy; }
            set
            {
                _UseProxy = value;
                PropertyChanged(this,
                  new PropertyChangedEventArgs("UseProxy"));
            }
        }

        private string _ServerAddress = "";
        public string ServerAddress
        {
            get { return _ServerAddress; }
            set
            {
                _ServerAddress = value;
                PropertyChanged(this,
                  new PropertyChangedEventArgs("ServerAddress"));
            }
        }

        private string _UserName = "";
        public string UserName
        {
            get { return _UserName; }
            set
            {
                _UserName = value;
                PropertyChanged(this,
                  new PropertyChangedEventArgs("UserName"));
            }
        }

        private string _UserPassword = "";
        public string UserPassword
        {
            get { return _UserPassword; }
            set
            {
                _UserPassword = value;
                PropertyChanged(this,
                  new PropertyChangedEventArgs("UserPassword"));
            }
        }

        private string _ServerDirectory = "";
        public string ServerDirectory
        {
            get { return _ServerDirectory; }
            set
            {
                _ServerDirectory = value;
                PropertyChanged(this,
                  new PropertyChangedEventArgs("ServerDirectory"));
            }
        }
    }

}

このクラスの構造は簡単です。最初に、UI からのプロパティの通知を受け取るために INotifyPropertyChanged を実装する必要があります。次に、標準プロパティ ハンドラ設計パターンを実装します。このパターンを使用して、プライベート メンバはプロパティ値を格納し、getter および setter アクセサ メソッドでカプセル化されます。getter が呼び出されると、プライベート メンバ変数の値が返されます。setter が呼び出されると、プライベート メンバが更新され、PropertyChanged イベントが発生します。PropertyChanged イベントは、変更されるプロパティの名前を含む引数によって初期化されます。UI は XAML データ バインディングを使用してこのコードにフックし、ユーザー アクションによって getter および setter を呼び出します。Server Address プロパティのダイアログで使用される XAML の例を次に示します。

<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=ServerAddress, Mode=TwoWay}" Margin="0,3,0,3" />

パラメータ ペインの UI コントロールは、標準 WPF XAML ユーザー コントロールを使用して実装されます。コントロールの x:Class では、それまでコード内で使用してきた名前空間を使用するように設定する必要があります。たとえば、標準ペインに FTPPublishPlugin.StandardParams、詳細ペインに FTPPublishPlugin.AdvancedParams を設定します。

これらは PublishData クラス内の関連付けられたプロパティにデータバインドされます。PublishData クラスをユーザー コントロールのデータ コンテキストに設定すると実行されます。プラグインが StandardParams ユーザー コントロールをインスタンス化すると、PublishData クラスのインスタンスがコントロールに渡されます。次に、ユーザー コントロールはこのインスタンスを使用してデータ コンテキストを設定します。

public StandardParams(PublishData data)
{
    this.InitializeComponent();
    this.DataContext = data;

}

Server Address のラベルおよびテキストボックスを実装するには、次の XAML をユーザー コントロールで使用するだけです。

<Label Grid.Column="0" Grid.Row="0" Margin="0,3,0,3">Server Address
</Label>
<TextBox Grid.Column="1" Grid.Row="0" Text=
"{Binding Path=ServerAddress, Mode=TwoWay}" Margin="0,3,0,3" />

TextBox の text プロパティは ServerAddress プロパティにバインドされます。バインド モードは双方向なので、WPF によってプロパティの設定と取得が自動的に行われます。

パスワード エントリ フィールドはセキュリティ上の理由でデータバインドできないため、処理方法はこれより複雑です。さらに、パスワード ボックスにはエンコーダが提供するラベルおよびテキストボックスの自動スタイルを利用できないので、スタイルを自分で設定する必要があります。したがって、パスワード ボックスを実装するには、図 6 に示すように XAML を使用して UI を実装します。

Figure 6 パスワード ボックス XAML (Close Figure 6)
<PasswordBox PasswordChar="*" Grid.Column="1" Grid.Row="2"
    x:Name="pass" PasswordChanged="HandlePW" Margin="0,3,0,3">
    <PasswordBox.Style>
        <Style TargetType="{x:Type PasswordBox}" >
            <Setter Property="BorderBrush"
                Value="{DynamicResource BackgroundBrush}"/>
            <Setter Property="Foreground"
                Value="{DynamicResource Text1Brush}"/>
            <Setter Property="Background"
                Value="{DynamicResource BackgroundBrush}"/>
            <Setter Property="FontFamily"
                Value="{DynamicResource
                {x:Static SystemFonts.MessageFontFamilyKey}}"/>
            <Setter Property="FontSize"
                Value="{DynamicResource
                {x:Static SystemFonts.MessageFontSizeKey}}"/>
            <Setter Property="FontWeight"
                Value="{DynamicResource
                {x:Static SystemFonts.MessageFontWeightKey}}"/>
        </Style>
    </PasswordBox.Style>
</PasswordBox>

これにより、パスワードのイベント ハンドラを HandlePW に変更するように指定します。このイベント ハンドラを使用して、入力されたパスワードを特定し、現在のデータ コンテキストである PublishData クラスに格納します。次にこの関数の例を示します。

public void HandlePW(object sender, RoutedEventArgs e)
{
    PublishData d = (PublishData) this.DataContext;
    d.UserPassword = pass.Password;
}

公開クラスを実装する

基礎が構成できたので、FTP サーバーの公開クラスを作成するのは簡単です。最初に必要なことは、PublishData クラスのインスタンスを確実にプラグインで作成することです。作成に最適な場所は、プラグインのコンストラクタ内です。

private StandardParams _standardParams;
private PublishData _publishData;
public Publish() : this(new PublishData())
{
}
public Publish(PublishData publishData)
{
    _publishData = publishData;
    _standardParams = new StandardParams(_publishData);
}

これにより、StandardParams ユーザー コントロールの初期化も行われ、プラグインがレンダリングされるときに表示されます。前の手順で説明したように、PublishData クラスのインスタンス (_publishData と呼ばれる) がユーザー コントロールに渡され、データ コンテキストの設定に使用されます。

次の手順は、標準設定および詳細設定のエディタ ペインを作成するために必要な関数を実装することです。定型の例では、これらの関数で返された文字列がレンダリングされました。ここでは、それぞれの設定ペインのユーザー コントロールをレンダリングします。

public override object CreateStandardSettingsEditor()
{
    return _standardParams;
}
public override object CreateAdvancedSettingsEditor()
{
    return new AdvancedParams(this, _publishData);
}

最後の手順は、[Publish] (公開) ボタンを押すか、[Auto Publish] (自動公開) オプションを選択し、ユーザーが出力の公開を要求した場合に呼び出される Publish 関数を実装することです。ここでは、FTP に公開するので、サーバーに接続するコードが実装されます。図 7 に例を示します。

Figure 7 FTP サーバーへの接続 (Close Figure 7)
public override void PerformPublish(string root, string[] filesToPublish)
{
    try
    {
        int nFileCount = filesToPublish.Length;
        int nFileProgressSize = 100 / nFileCount;
        int nCurrentProgress = 0;
        WebClient wc = new WebClient();
        if (!_publishData.UseProxy)
            wc.Proxy = null;
        wc.BaseAddress = "ftp://" + _publishData.ServerAddress + "/";
        string strURI = "";
        wc.Credentials =
        new NetworkCredential(_publishData.UserName,
            _publishData.UserPassword);

        foreach (string theFile in filesToPublish)
        {
            if (!m_Cancelled)
            {
                strURI = theFile.Substring(theFile.LastIndexOf("\\") + 1);
                if (_publishData.ServerDirectory != String.Empty)
                    strURI = _
                        publishData.ServerDirectory + "\\" + strURI;
                nCurrentProgress += nFileProgressSize;
                OnProgress(strURI, nCurrentProgress);
                wc.UploadFile(strURI, theFile);
            }
        }

        if (m_Cancelled)
        {
            PublishingHost.ShowMessageBox("Upload Cancelled",
                System.Windows.MessageBoxButton.OK,
                System.Windows.MessageBoxImage.Error);
        }
    }

    catch (Exception ex)
    {
        string strIE = ex.Message;
        if (ex.InnerException != null)
            strIE += "\n: " + ex.InnerException.Message;

        throw new Exception(strIE);
    }

    protected override void CancelPublish()
    {
        m_Cancelled = true;
        base.CancelPublish();
    }
}

これは、FTP サーバーへのファイル セットのアップロードを管理する、とても簡単な C# コードです。FTP サーバーに必要なデータは、前に説明したコントロールによって設定されている _publishData クラスから取得します。

注意が必要な 1 つの関数は OnProgress です。これは Plugins API の一部であり、実装する必要はありません。文字列と 0 ～ 100 までの数字を指定してこの関数を呼び出すだけです。すると、進行状況を表す値を示すバーおよび文字列がレンダリングされます。この場合は、アップロードされたファイルの数によって進行状況が決定されるので、たとえばファイル数が 10 の場合はファイル サイズにかかわらず各ファイルが進行状況の 10％ です。

CancelPublish 関数についても説明します。API はこの関数をオーバーライド用に提供します。UI によって公開時に自動生成される [キャンセル] ボタンをユーザーがクリックすると実行されます。この場合は、m_Cancelled Boolean を true に設定して公開をキャンセルするだけです。

このコードは単なるサンプル アプリケーションなので、1 つの例外ハンドラですべての例外をキャプチャし、ユーザーにスローします。実際のアプリケーションでは、アプリケーションで発生するさまざまな例外に対してさらに詳細なエラー情報を提供する必要があります。

構成の設定を保存する

公開する FTP サーバーに対してユーザーが資格情報を何度も入力せずに済むように、PublishPlugin 基本クラスから SaveJobSettings および LoadJobSettings メソッドをオーバーライドすることができます。ユーザーがファイル メニューの [Save Job] (ジョブの保存) を選択すると、これらのメソッドがプラグイン インスタンスで Expression Encoder によって呼び出されるので、構成プロパティのシリアル化および復元を行うことができます。たとえば、構成プロパティを保存するために次のように記述します。

public override void SaveJobSettings(XmlWriter writer)
{
  writer.WriteStartElement("FtpSample");
  writer.WriteElementString("UseProxy",
      _publishData.UseProxy.ToString());
  writer.WriteElementString("ServerAddress",
      _publishData.ServerAddress);
  writer.WriteElementString("UserName", _publishData.UserName);
  writer.WriteElementString("ServerDirectory",
      _publishData.ServerDirectory);
  writer.WriteEndElement();
}

ここで 1 つ注意すべき点として、構成プロパティは最上位レベルの XML 要素内にネストする必要があります。エンコードするジョブの XML ジョブ ファイルにプロパティがシリアル化され、そのファイルに多数の追加データが含まれているからです。保存したプロパティは次のように復元できます。

public override void LoadJobSettings(XmlReader reader)
{
  reader.ReadStartElement("FtpSample");
  _publishData.UseProxy = bool.Parse(xr.ReadElementString("UseProxy"));
  _publishData.ServerAddress = xr.ReadElementString("ServerAddress");
  _publishData.UserName = xr.ReadElementString("UserName");
  _publishData.ServerDirectory = xr.ReadElementString("ServerDirectory");
  reader.ReadEndElement();
}

コンパイル、展開、およびデバッグを行う

コンパイルが正常に終了すると、プロジェクトの bin\debug ディレクトリに 3 つのファイルが格納されます。プラグイン DLL、DLL の .pdb ファイル、および EncoderTypes.dll です。プラグイン DLL ファイルのみ Plugins フォルダにコピーします。これで、Expression Encoder を起動したときにプラグインが適切に読み込まれるはずです。

Expression Encoder を読み込んだ後で、Visual Studio に戻り、[デバッグ] の [プロセスにアタッチ] を選択します。[プロセスにアタッチ] ダイアログが表示されたら、MediaEncoderUI.exe を選択します。

公開プロセスを調べる場合は、Publish 関数にブレークポイントを設定します。エンコーディング ジョブの完了後に Expression Encoder で [Publish] (公開) ボタンをクリックするとブレークポイントで停止するので、ステップ実行して Expression Encoder がファイルを公開するときの動作を確認することができます。

Laurence Moroney は、マイクロソフトで Silverlight を専門にしている上級テクノロジ エバンジェリストです。Silverlight、AJAX、相互運用性、セキュリティなど、コンピューティングに関する多くの本を執筆しています。blogs.msdn.com/webnext で彼のブログをご覧いただけます。


PIAB と WCF: Policy Injection Application Block と WCF サービスを統合する

PIAB と WCF

Policy Injection Application Block と WCF サービスを統合する

Hugh Ang and David San Filippo
コードのダウンロード : : WCFandPIAB2008_02.exe (196KB)
この記事の内容 : :
  • Policy Injection Application Block と WCF を統合する
  • WCF のカスタム動作を作成する
  • .NET 属性または構成を使用して動作を適用する
  • コードを変更せずに統合する
この記事は次のテクノロジを使用しています:
WCF、PIAB
コンテンツ
実際の例
WCF のアーキテクチャの概要
ディスパッチャ
IEndpointBehavior と IContractBehavior を拡張する
構成を使用した方法
属性を使用した方法
サンプル コード

ソフトウェア設計の原則の中で最も重要なものの 1 つは、アプリケーション内のさまざまな役割を分離することです。サービス指向の設計では、アプリケーションをサービスと操作に分離します。しかし実際の実装では、操作のコードもサービスのコードの中に記述される傾向があります。

これはサービス指向の設計に限らず、オブジェクト指向の設計にも当てはまる問題です。Enterprise Library 3.0 では、オブジェクト指向の設計におけるこの問題を解決するために Policy Injection Application Block (PIAB) が導入されました。この記事の執筆時点では、Enterprise Library の最新バージョン (3.1) は PIAB と Windows® Communication Foundation (WCF) サービスの統合を直接サポートしていないため、PIAB を使用してサービス指向アプリケーションのコードを分割することは難しいと思われるかもしれません。しかし、これから説明する適切な方法を使用すれば、非常に簡単に分割できます。では、始めましょう。

PIAB を活用するには、オブジェクトのインスタンス化方法を調節する必要があります。オブジェクトのインスタンス化を抽象化する WCF などのサービス フレームワークを開発者が調整しなければ、PIAB を統合しようとしたときに問題が発生します。しかし、WCF には動作などのさまざまな拡張ポイントが用意されています。この記事では、WCF のカスタム動作を活用して、コードを書き足さずに PIAB を WCF サービス ポイントに適用する方法について説明します。WCF サービスを適切に統合できたとき、PIAB は、サービスのロジックを横断的な実装から分離する真の普遍的なフレームワークになります。

実際の例

上記の傾向に関する理解を深めるために、顧客 ID を使用してカスタム顧客オブジェクトを検索する単純なサービスをビルドする状況について考えてみましょう。Visual Studio® 2005 で、データ アクセス オブジェクトを呼び出し、顧客オブジェクトを返すという単純なメソッドを次のように記述したとします。

public Customer GetCustomer(string customerID)
{
    return CustomerDAO.GetCustomer(customerID);
}

その後、顧客 ID フィールドを空にしてはいけないことを思い出します。このため、検証用に数行のコードを追加します。トレース用のステートメントも数行追加して、正しい値がデータ アクセス オブジェクトとの間でやり取りされるようにします。最後に例外処理を追加して、カスタム例外を設定したデータ アクセス オブジェクトからスローされる例外をラップします。気が付けば、コードは 15 行になっています。これがさらに増える可能性もあります。承認ロジック、キャッシュ ロジック、またはパフォーマンス カウンタを更新するロジックを追加すれば、コードはさらに膨れ上がるでしょう。

しかし、このようにして追加されたコードは、顧客オブジェクトの取得とはまったく関係ありません。結局、これほど苦労しても、作成したコードの理解、保守、および再利用が難しくなるだけです。残念ながら、特にビジネス ロジックがコードの複数の行にまたがる場合、上記の例のような状況はよく発生します。

PIAB を使用すると、メソッドのコードは 1 行だけで済みます。検証や例外処理などのコードは、実行時に必要な実装の内容を定義するポリシーを作成することによって追加します。ポリシーをオブジェクトに適用するには、構成を使用するか、クラスを属性で修飾します。各ポリシーには、適用先を決定する一連の一致規則と、オブジェクトのメソッドを呼び出す際に挿入される動作を決定するハンドラのコレクションが含まれています。PIAB では、検証、ログ記録、例外処理、パフォーマンス カウンタの管理、承認、およびキャッシュに使用できる便利なハンドラが多数提供されます。これらのハンドラのほとんどは、Enterprise Library の他のアプリケーション ブロックを活用しています。

上記のようなポリシーが適用されるオブジェクトを使用するには、PolicyInjectorFactory を使用してクラスのインスタンスを作成する必要があります。"new" 演算子でクラスをインスタンス化する代わりに、ファクトリの "Create" メソッドに型を渡します。ファクトリは一致規則を使用して、その型に適用するポリシーを決定します。PIAB では、型、アセンブリ、メンバ名、メソッドのシグネチャ、名前空間、パラメータの型、プロパティの型、戻り値の型、カスタム属性、タグ (PIAB には比較用に値を設定できるタグ属性が提供されます) に基づいた一致規則、および独自に作成したカスタム一致規則がサポートされています。

該当する一致規則がない場合、ファクトリは単純に型をインスタンス化して返します。該当する一致規則がある場合は、そのオブジェクトのプロキシを作成します。作成されたプロキシは、オブジェクトの呼び出しを取得し、パイプライン内のハンドラを呼び出します (図 1 参照)。

図 1 ハンドラのパイプライン
図 1 ハンドラのパイプライン(画像を縮小するには、ここをクリックします)
図 1 ハンドラのパイプライン
図 1 ハンドラのパイプライン (画像を拡大するには、ここをクリックします)

PIAB は、GetCustomer サービス メソッドを 1 行のコードのまま維持するという要件を完全に満たしています。必要な作業は、"GetCustomer" 操作に適したポリシーを定義して、ログ記録、検証、および例外処理のハンドラを含めることだけです。

PIAB ポリシー、一致規則、およびハンドラについて理解したところで、このソリューションを WCF に統合するという問題に取り組みます。GetCustomer 操作が WCF サービスで定義されている場合に、どのように PIAB を活用できるかを見ていきましょう。

WCF のアーキテクチャの概要

WCF は、TCP/IP、HTTP、MSMQ、名前付きパイプ、WS-* プロトコルなどのさまざまなトランスポートがサポートされるサービスをビルドするためにマイクロソフトが開発したプラットフォームです。大まかに考えると、WCF のアーキテクチャは、送信側と受信側の両方にあるサービス モデル層とチャネル層で構成されます (図 2 参照)。サービス モデル層では、ユーザー コードと WCF との間のインターフェイスを確立する API が提供され、チャネル層ではメッセージングとトランスポートに関する詳細な処理が行われます。

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

WCF サービスをホストする場合、サービスのエンドポイントを定義するアドレス、バインド、およびコントラクト (3 つを総称して ABC といいます) という基本事項を定義する必要があります。これらの基本事項はコードまたは構成ファイルで定義することができ、チャネル層を効率的に制御します。WCF はバインドとプロトコルに必要なチャネルを構築し、受信メッセージを待機して、受信したメッセージを目的のサービス インスタンスに送信します。メッセージの送信は、WCF のメソッドを呼び出すことによって行います。

既に説明したように、型で PIAB を有効にするには、その型のインスタンスを PIAB ファクトリ経由で作成する必要があります。WCF サービスをホストする場合は、サービス クラスの型のみを指定します。つまり、WCF ランタイムの役割はサービスのインスタンスを必要に応じて作成することです。では、この動作をどのように変更すれば、WCF ランタイム内の PIAB ファクトリでサービス クラスのインスタンスを作成できるようになるでしょうか。そのためには、WCF ランタイムのアーキテクチャをさらに掘り下げて、WCF がサービスをどのようにインスタンス化するかを理解する必要があります。

ディスパッチャ

図 2 の受信側アプリケーションをご覧ください。この図には、WCF ディスパッチャの物理的なモデルが簡単に示されています。実際に存在するのは、チャネル ディスパッチャとエンドポイント ディスパッチャです。このような細部を無視しても、今回の目的を達成する上では問題ありません。むしろ無視した方がずっと簡単に説明できます。ディスパッチャが WCF のサービス モデル層に位置していることに注意してください。ディスパッチャは WCF メッセージのシリアル化を解除し、サービス インスタンスを作成して、最終的には呼び出しのディスパッチを行うため、WCF にとって重要な役割を果たしています。ディスパッチャ オブジェクトが作成されると、WCF ランタイムは、インスタンス プロバイダ オブジェクトが格納されたインスタンス動作オブジェクトを初期化します。インスタンス プロバイダはサービスのインスタンス化を担当します。図 3 はこの処理を示しています。

図 3 サービスのインスタンス化
図 3 サービスのインスタンス化(画像を縮小するには、ここをクリックします)
図 3 サービスのインスタンス化
図 3 サービスのインスタンス化 (画像を拡大するには、ここをクリックします)

動作は WCF の主要な拡張ポイントです。独自の動作を作成することによって、WCF が対象のサービスのインスタンスを作成する方法を変更し、代わりに PIAB のメカニズムを使用することができます。図 4 は、独自のカスタム動作とカスタム インスタンス プロバイダを作成するときに、どのようにして PIAB を導入するかを示しています。

図 4 PIAB の導入
図 4 PIAB の導入(画像を縮小するには、ここをクリックします)
図 4 PIAB の導入
図 4 PIAB の導入 (画像を拡大するには、ここをクリックします)

では、カスタム動作とカスタム インスタンス プロバイダを作成して、WCF ランタイムに追加しましょう。ここでは、構成データを使用した方法と、Microsoft® .NET Framework の属性を使用した方法を説明します。最終的には、WCF が PIAB を使用してサービスをインスタンス化できるようにするために、追加のコードを記述する必要がないことがおわかりいただけると思います。今回のカスタム動作とカスタム インスタンス プロバイダは汎用的に設計されているため、サービスの型と密接には関連付けられていません。つまり、サンプル コードを使用して、あらゆるサービスに PIAB 経由で横断的なロジックを適用できるということです。

IEndpointBehavior と IContractBehavior を拡張する

カスタム動作を作成する前に、WCF の動作が検出および適用されるしくみを、特にサービスのインスタンス化との関連において理解する必要があります。WCF ランタイムがサービスをホストするよう指示を受けると、サービス モデル層は次の処理を実行します。

  1. 構成内の動作を検出します。
  2. サービス クラスの属性または操作の属性として定義されている動作を検出します。
  3. メッセージングおよびトランスポート用のチャネル リスナをビルドします。
  4. 手順 1. の構成用、および手順 2. の属性用のディスパッチ動作を適用します。

今回は、サービス コードに適用できる構成または属性を定義して、カスタム動作が WCF ランタイムで検出されるようにする必要があります。WCF ランタイムは、上記の処理の手順 4. で、カスタム動作の定義に基づいてディスパッチ動作を適用します。このタイミングで、既定のインスタンス プロバイダを独自に作成したインスタンス プロバイダに置き換えることができます。

WCF は、動作のインターフェイスとして IServiceBehavior、IEndpointBehavior、IContractBehavior、および IOperationBehavior を公開しています。IServiceBehavior はホスト レベルの動作、IOperationBehavior は操作にそれぞれスコープが限定されているため、サービス インスタンスの作成には適していません。IEndpointBehavior と IContractBehavior には、ApplyDispatchBehavior メソッドを実装する必要があります。このメソッド内から切り替えを行い、既定のインスタンス プロバイダを独自のインスタンス プロバイダに置き換えることができます。

では、IEndpointBehavior と IContractBehavior の両方に、PIAB ファクトリを使用してサービス インスタンスを作成するための拡張ポイントが用意されている場合、どちらのインターフェイスを使用すればよいでしょうか。答えは両方です。IEndpointBehavior を使用すると、構成ファイルのディスパッチ動作を拡張できます。また、IContractBehavior を使用すると、ディスパッチ動作を .NET 属性として拡張できます。

構成を使用した方法

WCF の構成は、単純に .NET Framework の構成設定を利用しています。WCF ランタイムをホストする .NET アプリケーションの app.config ファイルや web.config ファイルには、<system.serviceModel> セクションがあります。このセクションについては、後ほど説明します。

このセクションは複雑に見えますが、サービスとクライアントにアドレス、バインディング、およびコントラクトの定義から構成されるエンドポイントが含まれているという WCF のアーキテクチャと非常にうまく対応しています。

<behaviors>、<extensions> などの要素は、単に <service> 要素や <endpoint> 要素によって参照される詳細情報です。この構成では、<service> 要素または <endpoint> 要素で動作をカスタマイズできます。<behaviors> 要素には、<serviceBehaviors> と <endpointBehaviors> が含まれています。サービス レベルで動作をカスタマイズしてサービスの型をインスタンス化するのは適切ではないため、ここではエンドポイントのカスタム動作を提供します。

図 5 のように、このカスタム動作には IEndpointBehavior インターフェイスを実装します。さらに、このカスタム動作が構成を使用して適用されるように、この動作を System.ServiceModel.Configuration.BehaviorExtensionElement から派生させ、BehaviorType プロパティと CreateBehavior メソッドをオーバーライドする必要があります。

Figure 5 カスタム動作 (Close Figure 5)
public class PolicyInjectionEndpointBehavior : BehaviorExtensionElement,
    IEndpointBehavior
{

    public override Type BehaviorType
    {
        get { return typeof(PolicyInjectionEndpointBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new PolicyInjectionEndpointBehavior();
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
        EndpointDispatcher endpointDispatcher)
    {
        Type contractType = endpoint.Contract.ContractType;
        endpointDispatcher.DispatchRuntime.InstanceProvider = new
            PolicyInjectionInstanceProvider(contractType);
    }

    // ...
}

ApplyDispatchBehavior は IEndpointBehavior の重要なメソッドです。ここで作成するエンドポイントのカスタム動作は、このメソッドを使用して、WCF の既定のインスタンス プロバイダを独自のインスタンス プロバイダ (PolicyInjectionInstanceProvider) に置き換えることができます。図 6 のように、このプロバイダには System.ServiceModel.Dispatcher.IInstanceProvider インターフェイスを実装します。次に、サービスの型のインスタンスを取得するために、GetInstance メソッドが WCF ランタイムによって呼び出されます。GetInstance の実装は単純です。受信したメッセージのサービスの種類とコントラクトの種類 (インターフェイス) に基づいてポリシー挿入プロキシを作成するよう Enterprise Library の PIAB に要求します。PIAB の既定のインターセプト メカニズムでは .NET リモート処理の透過プロキシが使用されるため、特定のコントラクトの型 (インターフェイス) が指定されていない場合は、MarshalByRefObject から派生した型が必要です。通常、PIAB を WCF サービスと共に使用する場合、この要件を気にする必要はありません。これは、CF コントラクトが .NET インターフェイスであるためです。

Figure 6 PolicyInjectionInstanceProvider (Close Figure 6)
public class PolicyInjectionInstanceProvider : IInstanceProvider
{
  private Type serviceContractType;
  private static PolicyInjectorFactory fac = new PolicyInjectorFactory();
  private static PolicyInjector injector;

  public PolicyInjectionInstanceProvider(Type t)
  {

    this.serviceContractType = t;
    if (injector == null)
        injector = fac.Create();
    }

  public object GetInstance(InstanceContext instanceContext,
    System.ServiceModel.Channels.Message message)
  {
    Type type = instanceContext.Host.Description.ServiceType;

    if (serviceContractType != null)
      return injector.Create(type, serviceContractType);
    else
    {
      return injector.Create(type);
    }
  }

  public object GetInstance(InstanceContext instanceContext)
  {
    return GetInstance(instanceContext, null);
  }

public  void  ReleaseInstance(InstanceContextinstanceContext,  object  instance)
  {
    IDisposable disposable = instance as IDisposable;
    if (disposable != null)
    {
      disposable.Dispose();
    }
  }

これで、IEndpointBehavior と IInstanceProvider 用の 2 つのカスタム型を用意することができたため、図 7 のように、これらのカスタム型を構成のコードに挿入できます。<BehaviorExtensions> 要素で、PolicyInjectionBehavior の型定義を追加して、サービスのエンドポイントに適用していることに注目してください。PIAB の構成だけでなく、ポリシーもコードを 1 行も書き足すことなく CustomerService サービスに挿入できるようになりました。

Figure 7 構成ファイル (Close Figure 7)
<system.serviceModel>
  <services>
    <service name="CustomerService.CustomerService">
      <endpoint
        address="http://localhost:8000/CustomerService/
          CustomerService.svc"
        binding="wsHttpBinding"
        bindingConfiguration=""
        contract="CustomerService.ICustomerService"
        behaviorConfiguration="PolicyInjectionEndpointBehavior" />
    </service>
  </services>
  <behaviors>
    <endpointBehaviors>
      <behavior name="PolicyInjectionEndpointBehavior" >
        <PolicyEndpointBehavior />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <extensions>
    <behaviorExtensions>
      <add
        name="PolicyInjectionEndpointBehavior"
        type="PolicyInjectionBehaviors.PolicyInjectionEndpointBehavior,
          PIBehaviors, Version=1.0.0.0, Culture=neutral,
          PublicKeyToken=null" />
    </behaviorExtensions>
  </extensions>
</system.serviceModel>

属性を使用した方法

.NET の宣言型プログラミングで (構成ファイルを使用する方法以外に) もう 1 つよく使用されるのが、属性を使用した方法です。属性は、メタデータとしてアセンブリにコンパイルされ、実行時に CLR によって認識されます。図 8 では、PolicyInjectionBehaviorAttribute というクラスを定義しています。このクラスには、System.ServiceModel.Description.IContractBehavior インターフェイスと System.ServiceModel.Description.IContractBehaviorAttribute インターフェイスが実装されています。PolicyInjectionBehaviorAttribute クラスは、属性として適用するために、.NET Framework の Attribute 基本クラスから派生させる必要があります。

Figure 8 PolicyInjectionBehaviorAttribute クラス (Close Figure 8)
[AttributeUsage(AttributeTargets.Class)]
public class PolicyInjectionBehaviorAttribute : Attribute,
  IContractBehavior, IContractBehaviorAttribute {
  public PolicyInjectionBehaviorAttribute() { }

  public Type TargetContract {
    get {
      return null; //return null so we apply to all contracts
    }
  }

  public void AddBindingParameters(ContractDescription description,
    ServiceEndpoint endpoint,
    BindingParameterCollection parameters) { }

  public void ApplyClientBehavior(ContractDescription description,
    ServiceEndpoint endpoint,
    ClientRuntime clientRuntime) { }

  public void ApplyDispatchBehavior(ContractDescription description,
    ServiceEndpoint endpoint,
    DispatchRuntime dispatch) {
    Type contractType = description.ContractType;
    dispatch.InstanceProvider = new
      PolicyInjectionInstanceProvider(contractType);
  }

  public void Validate(ContractDescription description,
    ServiceEndpoint endpoint) { }
}

IContractBehaviorAttribute の実装は非常に単純であり、TargetContract プロパティに null を返します。これは、PolicyInjectionBehaviorAttribute を適用するすべてのコントラクトを対象としているためです。ApplyDispatchBehavior の実装は PolicyInjectionEndpointBehavior の実装と似ています。ここでは、既定のインスタンス プロバイダを独自に作成した PolicyInjectionInstanceProvider に置き換えます。PolicyInjectionInstanceProvider は、構成を使用した方法で作成したエンドポイントの動作に使用されているものと同じインスタンス プロバイダです。次のコードは、PolicyInjectionBehaviorAttribute を使用してポリシーを挿入する方法を示しています。この属性を適用する以外に、開発者が行う作業はありません。

[PolicyInjectionBehavior]
public class DecoratedCustomerService : ICustomerService {
  public Customer GetCustomer(string customerID) {
    return CustomerDAO.GetCustomer(customerID);
  }
}

サンプル コード

構成を使用して実装した WCF のカスタム動作、および属性を使用して実装した WCF のカスタム動作の使用方法を説明するために、サンプルを作成しました。このサンプルでは、サービス コントラクトを ICustomerService というインターフェイスとして定義しました。また、このインターフェイスの具体的な実装を 2 つ作成しました。これらは、CustomerService と DecoratedCustomerService です。この 2 つのクラスの違いは、DecoratedCustomerService が PolicyInjectionBehavior 属性で修飾されているということだけです。

サンプル コードには、CustomerService と DecoratedCustomerService の両方をホストするコンソール アプリケーションが含まれています。PolicyInjectionEndPointBehavior を適用するために、CustomerService クラスの構成を変更しました。また、各サービスを 1 回ずつ呼び出して GetCustomer 関数を実行するクライアント アプリケーションも含まれています。次に示すのは callCustomerService ですが、callDecoratedCustomerService も内容は同じです。

private static void callCustomerService() {
  CustomerService.CustomerServiceClient client =
    new CustomerService.CustomerServiceClient();
  Console.WriteLine("Retreiving Customer 1");
  CustomerService.Customer cust = client.GetCustomer("1");
  Console.WriteLine("Retreived Customer, Name: [" + cust.Name + "]");
}

構成を使用した方法と属性を使用した方法の両方で、サービスの呼び出しをログに記録するためのハンドラが含まれるように PIAB の構成を設定しました (図 9 参照)。例外処理やキャッシュなど、その他のハンドラを必要に応じて追加できます。このサンプルのログ記録ハンドラでは、audit.log というフラット ファイルに監査記録が出力されます。サービスが呼び出されると、他の場所に監査ログを作成するように構成を変更していない限り、実行中のコードと同じディレクトリに監査ログが作成されます。

図 9 Visual Studio に表示された App.config
図 9 Visual Studio に表示された App.config(画像を縮小するには、ここをクリックします)
図 9 Visual Studio に表示された App.config
図 9 Visual Studio に表示された App.config (画像を拡大するには、ここをクリックします)

このように、PIAB は横断的なロジックをドメイン固有のロジックから分離するための優れた方法であり、WCF サービスで使用することもできます。サンプル コードを msdn.microsoft.com/msdnmag/code08.aspx からダウンロードして、PIAB を使用してみてください。PIAB を WCF サービスに適用するには、単に PIBehaviors アセンブリへの参照を追加してから、構成を使用して PolicyInjectionEndpointBehavior を適用するか、サービスに直接 PolicyInjectionBehaviorAttribute を適用します。

Hugh Ang は Avanade のシニア ソリューション アーキテクトです。顧客向けアプリケーション開発の責任者として、エンタープライズ アーキテクチャの統括、およびアプリケーション開発部門の管理を行っています。彼のブログは tigerang.blogspot.com で公開されています。

David San Filippo は Avanade のソリューション開発者です。MCPD のエンタープライズ アプリケーション デベロッパー、MCSD.Net、および MCDBA の資格を取得しています。彼のブログは www.mtelligent.com で公開されています。


WF の使い方: Windows Workflow Foundation でステート マシンを構築する

WF の使い方

Windows Workflow Foundation でステート マシンを構築する

Keith Pijanowski
この記事の内容 : :
  • シーケンシャル ワークフローとステート マシン ワークフロー
  • ステート マシン ワークフローを使用するタイミング
  • ステート マシン ワークフローを構築および設計する
  • 実行中のワークフローにイベントを発生させる
  • 実行中のワークフローにデータを渡す
この記事は次のテクノロジを使用しています:
Windows Workflow Foundation、Visual Studio
コードのダウンロード : : WFStateMachines2008_02.exe (177KB)
コンテンツ
ステート マシンの理論
ステート マシンと Windows WF
ステート マシン ワークフローと通信する
データ交換および状態遷移に必要なイベント
外部データ交換インターフェイスを定義する
EventDriven アクティビティを追加する
外部データ交換インターフェイスを参照する
外部データ交換サービスを実装する
外部データ交換サービスをワークフロー ランタイムに追加する
外部データ交換を使用する
便利なステート マシン API
まとめ

Microsoft® .NET Framework 3.0 では、マネージ コードでホスト可能なワークフローを視覚的方法および宣言を使用して構築する新機能が導入されました。標準の .NET Framework 3.0 の優れた機能を使用して、シーケンシャル ワークフローおよびステート マシン ワークフローを構築できます。シーケンシャル ワークフローは従来のプログラミング方法ときわめて類似しているため、解説書が多数出ています。

一方、ステート マシン ワークフローはプログラム ロジックの考え方が異なりますが、正しく設計し実装すれば、シーケンシャル ワークフローと同様に有益です。また、このワークフロー スタイルはヒューマン ワークフロー構築の初歩に適しています。したがって、ステート マシン ワークフローはすべての設計者および開発者にとって必携のスキル セットです。

ステート マシンの理論
関連情報

この記事のコード サンプルには、説明にあるすべてのワークフロー、サンプル コードの図、およびコード例が含まれています。コード サンプルはステート マシン ワークフローのサンドボックスまたはテスト環境として作成されているため、一度に実行できるワークフロー インスタンスは 1 つのみです。

この Visual Studio 2005 ソリューションには、以下のプロジェクトが含まれています。
StateMachineLibrary この記事で説明している 2 つのワークフロー (SimpleStateMachine ワークフローと SupplyFulfillment ワークフロー) が含まれています。
ConsoleHost この記事の最初に説明する単純なステート マシン ワークフローをホストするコンソール アプリケーションです。
WinFormHost SupplyFulfillment ワークフローをホストするために使用する Windows フォーム アプリケーションです。このプロジェクトで、ExternalDataExchange を使用して SupplyFulfillment ワークフローのインスタンスに対するイベントを生成します。
ExternalDataExchange IEventService インターフェイス、このインターフェイスの実装 (EventService)、および SupplyFulfillmentArgs クラスを含むクラス ライブラリです。このサービスで、外部イベント (およびデータ) をステート マシン ワークフローの現在の状態に送信することができます。

詳細については、以下のリソースを参照してください。

  • SqlTrackingService の使用
  • SqlWorkflowPersistenceService の使用
  • Windows ワークフロー アクティビティの概要
  • Windows Workflow Foundation の宣言モデルを介した開発の単純化
  • Windows Workflow Foundation
  • Windows Workflow Foundation (第 2 部)

ステート マシン ワークフローとして実装されるビジネス ロジックは、シーケンシャル ワークフローとして実装されるビジネス ロジックとはまったく異なる経路を辿ります。アクティビティ間を遷移するシーケンシャル ワークフローと異なり、ステート マシンは状態間を遷移します。この場合、状態をビジネス プロセス内の既知の手順、段階、またはステータスと考えてください。状態は、ワークフローの進行状況を示すという点で有効です。この特性は実行に時間のかかるワークフローに適しています。

ビジネス プロセスがステート マシン ワークフローに適していることは、その要件が、データやロジックが一定の開始点から終了点までの個々の手順 (または状態) に従って遷移するように規定される点を考えればすぐにわかります。次に、ステート マシンとして実装可能な在庫調達ビジネス プロセスの概要を示します。

  1. 顧客からの注文を処理するために、従業員が在庫を追加しなければならない場合があります。この場合、追加在庫の要求を集中システムに送信して処理を待ちます。
  2. 送信された要求は、特定のユーザーの作業キューに割り当てられ、確認を待ちます。
  3. 割り当てられた要求は、ビジネス プロセスの外部の条件に基づいて承認または拒否されます。
  4. 拒否された要求は、ビジネス プロセスの外部の条件に基づいて、再割り当てされるか、取り消されます。再割り当てされた注文は、割り当て済みの注文と同様に処理されます。取り消された注文は完了したものと見なされます。
  5. 従業員の要求が承認されると、必要なリソースが発注されます。
  6. 注文が受領されると、その注文に関連するビジネス プロセスは完了と判断されます。

状態として認識可能なこのビジネス プロセスの主要な手順を図 1 に示します。

図 1 在庫調達ワークフローのロジック設計
図 1 在庫調達ワークフローのロジック設計

このワークフローを Windows® Workflow Foundation (Windows WF) で構築する前に、注意しておいた方がよい重要事項がいくつかあります。まず、このワークフローには、1 つの初期状態 (送信済み) と 1 つの完了状態 (完了) があります。ステート マシン ワークフローの各インスタンスは、初期状態として指定された状態から始まり、ワークフローが完了状態として指定された状態に達すると終了します。

また、状態の遷移は事前に定義されます。つまり、各状態の定義の一部は、有効な後続の状態のリストです。図 2 に、状態と有効な遷移のリストを示します。

Figure 2 在庫調達の有効な状態遷移 (Close Figure 2)

状態 | 有効な遷移
送信済み | 割り当て済み
割り当て済み | 承認済みまたは拒否
承認済み | 発注済み
拒否 | 割り当て済みまたは完了
発注済み | 完了
完了 | (なし)

また、バックトラックが許可されます。例では、拒否された要求を再割り当て可能にするという要件が指定されています。再割り当て状態をロジックの設計に追加することもできますが、効率的な方法とは言えません。要件は、再割り当てされた要求を新規に割り当てられた要求と同じ方法で処理すると規定しているため、再割り当てされた要求の処理に対して拒否状態から別の分岐を作成する必要がありません。この場合、ワークフローが以前の状態に戻った方がワークフロー内の再利用を有効に行うことができます。

ステート マシン ワークフローは実行時間の長いプロセスに適しています。ステート マシン ワークフローの初期状態から完了状態への移行が数ミリ秒で終わり、外部入力を待機する処理がない場合は、シーケンシャル方式で実装した方が効率的である可能性があります。

ステート マシンと Windows WF

前述のロジック設計を実装する前に、単純な Windows WF ステート マシンを検討して WF のすべてのツールに対する感覚をつかむことにしましょう。ここでは、Visual Studio® .NET および .NET Framework 3.0 のツールのデザイン時の操作概要を示す単純なステート マシン ワークフローを作成します。ここに記載されているすべてのワークフローおよびコード例は、この記事のコード サンプルとしてパッケージ化された、実際に使用可能な Visual Studio ソリューションを基にしています。詳細については、「関連情報」のリンクを参照してください。

.NET Framework 3.0 (Windows WF) の Visual Studio 2005 の拡張機能を go.microsoft.com/fwlink/?LinkId=105219 からインストールする場合、複数の新しいプロジェクト テンプレートが Visual Studio .NET 2005 環境に追加されます (図 3 を参照)。Visual Studio 2008 を使用する場合、これらのプロジェクト テンプレートのすべてはコア製品に含まれています。ステート マシン プロジェクト テンプレートを選択すると、Visual Studio .NET プロジェクトに適切な参照およびワークフロー ファイルが設定され、このプロジェクトでステート マシン ワークフローの開発を開始できます。

図 3 Workflow Foundation のプロジェクト テンプレート
図 3 Workflow Foundation のプロジェクト テンプレート(画像を縮小するには、ここをクリックします)
図 3 Workflow Foundation のプロジェクト テンプレート
図 3 Workflow Foundation のプロジェクト テンプレート (画像を拡大するには、ここをクリックします)

ステート マシン ワークフローの設計には、シーケンシャル ワークフローの設計よりはるかに多くの制限があります。ほとんどのステート マシンの場合、必要な操作は State アクティビティをデザイナにドラッグするだけです (EventDriven アクティビティをデザイナに追加して、特定の状態に関連付けられていないイベントを処理することもできます)。State アクティビティはステート マシン ワークフロー内の手順、段階、またはステータスを示します。このアクティビティは、初期状態、完了状態、およびその中間で発生する可能性のあるすべての状態を表すために使われます。

ステート マシンを作成する最初の手順は、State アクティビティをワークフロー ツールボックスからドラッグして各状態にわかりやすい名前を割り当てることにより、必要なすべての状態をデザイン画面に配置することです。ワークフローの状態に名前を付ける場合は、Windows フォーム プロジェクトで Windows コントロールに名前を付ける場合と同様にプロパティ ダイアログを使用します。図 4 は、3 つの状態 (送信済み、発注済み、完了) を持つ単純なステート マシン ワークフローの例を示しています。

図 4 単純なステート マシン ワークフロー
図 4 単純なステート マシン ワークフロー(画像を縮小するには、ここをクリックします)
図 4 単純なステート マシン ワークフロー
図 4 単純なステート マシン ワークフロー (画像を拡大するには、ここをクリックします)

実行時の管理を適切に行うには、Workflow Foundation が初期状態と完了状態を認識している必要があります。そのため、次に、ワークフローのプロパティ ダイアログで初期状態と完了状態を指定します。このダイアログを表示するには、プロパティ ダイアログが Visual Studio .NET 内に表示されていることを確認し、ワークフローのデザイン画面をクリックするか、ワークフロー デザイナを右クリックしてプロパティを選択します。図 4 の右下にステート マシン ワークフローのプロパティ ダイアログが表示されています。InitialStateName プロパティは初期状態を指定し、CompletedStateName プロパティは完了状態を指定します。これらのプロパティを指定すると、デザイナは指定された状態のグラフィカル表示を変更します。図 4 をよく見ると、初期状態に緑のアイコンが表示され、完了状態に赤のアイコンが表示されているのがわかります。

State アクティビティは複合アクティビティであるため、他のアクティビティが含まれる可能性があります。ここでも、考慮すべき制限があります。State アクティビティには、StateInitialization アクティビティと StateFinalization アクティビティがそれぞれ 1 つのみ、1 つ以上の EventDriven アクティビティ、および入れ子状態の 1 つ以上の他の State アクティビティが含まれる可能性があります。State アクティビティ内に設定可能な標準アクティビティはこれらの 4 つだけです。入れ子状態はこの記事の対象外であり、EventDriven アクティビティの使い方については次のセクションで説明します。単純なステート マシンの例では、StateInitialization アクティビティと StateFinalization アクティビティのみを使用します。

StateInitialization アクティビティおよび StateFinalization アクティビティを使ってみると、ステート マシン デザイナの使い方の感覚がつかめます。この 2 つのアクティビティをさまざまに操作してみると、ステート マシン ワークフローの流れの制御方法に対する理解が深まります。

図 4 の単純な例では、完了状態以外のすべての状態に StateInitialization アクティビティと StateFinalization アクティビティがあります。状態を "完了" に指定すると、その状態にはアクティビティを追加できなくなります。これは、完了状態に遷移する操作がワークフローのライフサイクルの終了を意味するためです。

プログラムの制御が State アクティビティに渡り、State アクティビティの実行が開始すると、StateInitialization アクティビティが存在するかどうかの確認が行われます。存在する場合、アクティビティが実行されます。同様に、State アクティビティが別の状態に制御を渡すと、StateFinalization アクティビティが存在するかどうかの確認が行われます。存在する場合、アクティビティが実行されます。StateInitialization アクティビティおよび StateFinalization アクティビティが複合アクティビティであることにも注意する必要があります。State アクティビティと異なり、これらのアクティビティの設定に関する制限はごくわずかです。つまり、シーケンシャル ワークフローで使用可能なすべてのアクティビティをこれらのアクティビティに設定できます。Visual Studio には、これらの複合アクティビティにロジックを追加するためのデザイン ビューが別にあります。図 5 は、図 4 の送信済み状態の StateInitialization アクティビティをダブルクリックしたときに表示されるデザイン ビューを示しています。この送信済み状態には SubmittedInitialization という名前の StateInitialization アクティビティがあります。このデザイン ビューの目的は、StateInitialization アクティビティにアクティビティを追加する機能を提供することです。図 5 の例では、Code アクティビティおよび SetState アクティビティが SubmittedInitialization アクティビティに追加されています。Code アクティビティの実行イベントには次のコードが記述されています。

図 5 StateInitialization アクティビティのデザイン ビュー
図 5 StateInitialization アクティビティのデザイン ビュー

private void codeSubmittedInitialization_ExecuteCode(object sender,
    EventArgs e)
{
    Console.WriteLine("The Submitted state has been initialized at " +
    DateTime.Now.ToString() + ".");
}

SetState アクティビティ (図 5 の setStateOrdered) を使用して StateMachine ワークフローの別の状態に制御を移します。このアクティビティを利用する場合、コードは不要です。図 6 は、setStateToOrdered のプロパティ ダイアログを示しています。StateMachine ワークフローの TargetStateName プロパティを有効な状態に設定する必要があります。このアクティビティを実行すると、すぐに現在の状態の StateFinalization アクティビティが実行され、発注済み状態に制御が移ります。以後、SubmittedInitialization アクティビティ内でその他のアクティビティは実行されません。そのため、確実にこれが最後のアクティビティになるように注意する必要があります。例に示した送信済み状態の StateFinalization アクティビティは 1 つの Code アクティビティで構成されており、実行イベントには次のコードが記述されています。

図 6 SetState アクティビティのプロパティ
図 6 SetState アクティビティのプロパティ(画像を縮小するには、ここをクリックします)
図 6 SetState アクティビティのプロパティ
図 6 SetState アクティビティのプロパティ (画像を拡大するには、ここをクリックします)

  private void
  codeSubmittedFinalization_ExecuteCode(
  object sender, EventArgs e)
  {
    Console.WriteLine("The Submitted " +
    "state has been finalized at " +
    DateTime.Now.ToString()+".");
  }

SetState アクティビティを StateFinalization アクティビティに追加することはできないことに注意してください。StateFinalization アクティビティを実行できるのは、SetState アクティビティが既に実行済みの場合に限られるため、SetState アクティビティを追加しても意味がありません。SetState アクティビティは、StateInitialization アクティビティまたは EventDriven アクティビティにのみ追加可能です。

デザイナを元のデザイン ビューに戻してワークフロー全体を表示するには、図 5 に示されているデザイナの左上に表示されたワークフロー名をクリックしてください。デザイナのこの領域はナビゲーションに使用します。[SimpleStateMachine] をクリックするとデザイン ビューは元のビューに戻り、ワークフローのすべての状態が表示されます。ステート マシンが複雑で、状態の入れ子階層が深い場合は、このナビゲーション領域でワークフロー内の現在の位置が一目でわかります。

この時点で、ステート マシン デザイナは setState アクティビティで指定された状態の遷移を検出し、デザイン画面上に適切な遷移矢印を描画します。SetState アクティビティの TargetStateName プロパティの変更方法は 2 とおりあります。1 つは、前述したプロパティ ダイアログを使う方法です。もう 1 つは、遷移矢印の先端を目的の State アクティビティにドラッグ アンド ドロップして TargetStateName プロパティをワークフローのデザイン画面から直接設定する方法です。

図 5 の単純なステート マシン ワークフローの発注済み状態は、送信済み状態と同様の方法で設定されています。つまり、コンソールへのメッセージを作成するために、StateInitialization アクティビティと StateFinalization アクティビティの両方で Code アクティビティを使用しています。また、StateInitialization アクティビティで SetState アクティビティを使用して、ステート マシン ワークフローの完了状態に制御を移行しています。

これでワークフローを使用する準備が完了しました。ワークフロー ランタイムのインスタンスを作成し、このワークフローのインスタンスを作成して、コンソール アプリケーションからワークフローを開始するコードを次に示します。

static void Main(string[] args)
{
    // Create the workflow runtime.
    using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())
    {
        WorkflowInstance instance =
            workflowRuntime.CreateWorkflow(typeof(SimpleStateMachine));
        instance.Start();

        Console.ReadLine();
    }
}

このコードを実行すると、コンソールの出力ウィンドウに次の情報が出力されます。

The Submitted state has been initialized at 8/6/2007 7:40:36 PM.
The Submitted state has been finalized at 8/6/2007 7:40:36 PM.
The Ordered state has been initialized at 8/6/2007 7:40:36 PM.
The Ordered state has been finalized at 8/6/2007 7:40:36 PM.

このセクションで説明したワークフローでは、この記事の冒頭に示した在庫調達ワークフローに必要な状態のうちの 3 つ (送信済み、発注済み、完了) を実装しています。今度は、このワークフローに別の状態 (割り当て済み、承認済み、拒否) を追加して、バックトラックの例および複数の状態に遷移可能な状態の例を示します。また、イベントも追加します。

ステート マシン ワークフローと通信する

WF ステート マシンの基本的な動作についての説明が終わったので、次に実際の現場での検討事項について説明します。現実のステート マシンは実行が長時間にわたることが多く、有効期間のほとんどは特定の状態で停止し、ワークフローの外部からの入力を待機しています。実行中状態を停止して、外部情報を待機するには、EventDriven アクティビティを使用します。前述したように、EventDriven アクティビティは State アクティビティにドロップ可能なアクティビティのうちの 1 つです (State アクティビティにドロップ可能な他のアクティビティは StateInitialization と StateFinalization です)。外部情報を EventDriven アクティビティに渡す場合にはイベントを使用します。外部データ サービスによってイベントが生成されると、そのイベントを EventDriven アクティビティが受け取ります。EventDriven アクティビティは設定が多少難しくなる場合がありますが、手始めとして、以下の手順を参考にしてください。

  1. データ交換および状態遷移に必要なイベントを特定します。
  2. 外部データ交換インターフェイスを定義します。
  3. EventDriven アクティビティをステート マシン ワークフローに追加します。
  4. 外部データ交換インターフェイスを参照します。
  5. 外部データ交換インターフェイスを実装します。
  6. 外部データ交換をワークフロー ランタイムに追加します。
  7. 外部データ交換を使用してデータ交換および状態遷移を行います。

図 7 に前述の手順をまとめます。

図 7 ステート マシン ワークフローを構成および利用する
図 7 ステート マシン ワークフローを構成および利用する(画像を縮小するには、ここをクリックします)
図 7 ステート マシン ワークフローを構成および利用する
図 7 ステート マシン ワークフローを構成および利用する (画像を拡大するには、ここをクリックします)

データ交換および状態遷移に必要なイベント

まず、ステート マシン ワークフローの各状態に必要なイベントを特定します。ステート マシンのイベントでは、一般にワークフローの状態変更が発生します。したがって、必要なイベントを特定する最善の方法は、要件を確認して状態の遷移を示すキーワードを探すことです。図 1 のロジック設計では、要件を確認して必要な状態および状態遷移を特定する必要がありました。そのときの情報を使用して、必要なイベントを特定することもできます。図 8 は、在庫調達ワークフローの各状態の状態遷移を示した図 2 のデータを拡張したものです。図 8 には、ワークフローの各状態、各状態がリスンするイベント、各イベントでの遷移後の状態が示されています。新しい状態に移行するためにワークフロー インスタンスにデータを指定する必要がある場合、Workflow Foundation の機能を使用して、情報をイベント パラメータとして渡すことができます。

Figure 8 在庫調達ワークフローのイベント (Close Figure 8)

状態 | イベント名 | 遷移
送信済み | OnAssigned | 割り当て済み
割り当て済み | OnApproved | 承認済み
割り当て済み | OnRejected | 拒否
承認済み | OnOrdered | 発注済み
拒否 | OnReassigned | 割り当て済み
拒否 | OnCanceled | 完了
発注済み | OnOrderReceived | 完了

割り当て済み状態は 2 つのイベントをリスンしています。2 つのイベントのリスンは同時に行われます。受信したイベントによって状態遷移が決まります。この方法で、外部エンティティはステート マシン ワークフローがたどる経路を判断することができます (拒否状態も 2 つのイベントをリスンします)。

外部データ交換インターフェイスを定義する

次に、これらのイベントを標準の .NET インターフェイスで定義します。図 9 は在庫調達ワークフローに必要なインターフェイスを示し、図 10 は、いずれかのイベントが発生するたびにワークフローのインスタンスにデータを渡すカスタム EventArgs クラスを示しています。

Figure 10 カスタム EventArgs クラス (Close Figure 10)
namespace ExternalDataExchange
{
    [Serializable]
    public class SupplyFulfillmentArgs : ExternalDataEventArgs
    {
        public SupplyFulfillmentArgs(System.Guid InstanceId) :
            base(InstanceId) { }

        private string assignedTo;
        private string orderID;

        public string AssignedTo
        {
            get { return assignedTo; }
            set { assignedTo = value; }
        }

        public string OrderID
        {
            get { return orderID; }
            set { orderID = value; }
        }
    }
}
Figure 9 外部データ サービス インターフェイス (Close Figure 9)
namespace ExternalDataExchange
{
    [ExternalDataExchange()]
    public interface IEventService
    {
        event EventHandler<SupplyFulfillmentArgs> Assigned;
        event EventHandler<SupplyFulfillmentArgs> Approved;
        event EventHandler<SupplyFulfillmentArgs> Rejected;
        event EventHandler<SupplyFulfillmentArgs> Reassigned;
        event EventHandler<SupplyFulfillmentArgs> Canceled;
        event EventHandler<SupplyFulfillmentArgs> Ordered;
        event EventHandler<SupplyFulfillmentArgs> OrderReceived;
    }
}

このインターフェイスについて注意する点がいくつかあります。まず、ExternalDataExchange 属性を使用しているということです。ExternalDataExchange 属性が設定されたインターフェイスを実装するクラスは、他のサービスと同様にワークフロー ランタイム エンジンに追加できます (ワークフロー ランタイムに追加可能なその他のサービスの例として、永続化と追跡があります)。

インターフェイスで宣言されたイベントのパラメータとして使用される SupplyFulfillmentArgs が ExternalDataEventArgs を継承しなければならない点にも注意が必要です。また、ExternalDataEventArgs はワークフロー インスタンス ID をコンストラクタに渡して、イベントが適切なワークフロー インスタンスで発生するようにする必要があります。さらに、ワークフロー ランタイムを永続ストレージにシリアル化する必要がある場合を考慮して、SupplyFulfillmentArgs をシリアル化可能として設定する必要があります。

EventDriven アクティビティを追加する

外部データ交換インターフェイスを設定したら、次に、EventDriven アクティビティをステート マシンに追加します。StateInitialization アクティビティおよび StateFinalization アクティビティと同様に、EventDriven アクティビティは複合アクティビティです。したがって、その目的は他のアクティビティを追加することです。EventDriven アクティビティを State アクティビティに追加すると、StateInitialization アクティビティや StateFinalization アクティビティの場合と同じ方法でこのアクティビティにアクセスし、追加ロジックを定義することができます。ただし、StateInitialization アクティビティや StateFinalization アクティビティの場合と異なり、EventDriven アクティビティの内部にアクティビティを追加する場合はいくつかの規則に従う必要があります。EventDriven アクティビティは Sequence アクティビティを継承しており、最初の子アクティビティにパブリック インターフェイス IEventActivity を実装する必要があるという制限が加わります。2 番目以降のすべてのアクティビティの型は任意です。例では、HandleExternalEvent アクティビティを使用しています。このアクティビティには、ワークフロー ランタイムの外部のイベントを受信する機能があります。IEventActivity インターフェイスを実装する他の 2 つのアクティビティは Delay アクティビティと WebServiceInput アクティビティです。

SupplyFulfillment ワークフローには、EventDriven アクティビティを必要とする 7 つの論理イベントがあります。図 8 は、EventDriven アクティビティを適切な状態に追加するために必要な情報を示しています。これらのアクティビティに追加されたロジックにより、最終的に状態が変更されるという点も重要です。図 11 は、すべての EventDriven アクティビティを追加した後の SupplyFulfillment ステート マシンのデザイナ ビューを示しています。遷移矢印が EventDriven アクティビティから始まり、State アクティビティで終了していることに注目してください。図 12 は、OnAssigned EventDriven アクティビティに記述されているロジックを示しています。

図 11 ステート マシン ワークフローの EventDriven アクティビティ
図 11 ステート マシン ワークフローの EventDriven アクティビティ(画像を縮小するには、ここをクリックします)
図 11 ステート マシン ワークフローの EventDriven アクティビティ
図 11 ステート マシン ワークフローの EventDriven アクティビティ (画像を拡大するには、ここをクリックします)
図 12 EventDriven アクティビティ内のロジック
図 12 EventDriven アクティビティ内のロジック

図 11 に示されている StateInitialization アクティビティおよび StateFinalization アクティビティはもう不要です。これらのアクティビティでコンソール メッセージを処理しているのは、あくまで例を示すためです。

図 11 の在庫調達ワークフローの視覚的表現は、Microsoft Visio® で作成した図 1 の論理図と同様に見やすくなっています。開発アーティファクトが開発者、設計者、およびエンド ユーザーにとって理解しやすく、確認しやすいということは、ビジネス プロセス管理の基本原則です。

外部データ交換インターフェイスを参照する

EventDriven アクティビティを追加し、各 EventDriven アクティビティの子アクティビティを図 11 と同様の方法で設定したら、次に、HandleExternalEvent アクティビティを構成します。EventDriven アクティビティを設定する場合、最初の子アクティビティは HandleExternalEvent アクティビティなどのアクティビティでなければならないという作成上の規則があったことを思い出してください。この規則は図 12 のロジックにも見られます。ここでは、EventDriven アクティビティ (名前 = OnAssigned) の最初の子アクティビティは HandleExternalEvent アクティビティ (名前 = handleAssigned) であり、その次に SetState アクティビティ (名前 = SetStateAssigned) が続きます。

HandleExternalEvent アクティビティにより、外部データ交換サービスによって生成されたイベントの処理を待機している間、StateMachine ワークフローの処理がブロックされます。HandleExternalEvent アクティビティが待機するイベントは、プロパティ ダイアログ (図 13 を参照) で指定します。最初に InterfaceType プロパティを設定します。このプロパティは、ExternalDataExchange 属性が設定されたインターフェイスに設定する必要があります。在庫調達ワークフローの場合、このインターフェイスは図 9 に示されている IEventService インターフェイスです。InterfaceType プロパティを設定すると、EventName プロパティを指定できるようになります。このプロパティ ダイアログに InterfaceType プロパティで指定したインターフェイスが反映され、EventName プロパティのドロップダウン ボックスが表示されます。図 13 のダイアログは、在庫調達ワークフローの OnAssigned イベントを示しています。

図 13 handleAssigned のプロパティ
図 13 handleAssigned のプロパティ(画像を縮小するには、ここをクリックします)
図 13 handleAssigned のプロパティ
図 13 handleAssigned のプロパティ (画像を拡大するには、ここをクリックします)

このダイアログの説明を終了する前に、もう 1 つ注意事項があります。ExternalDataExchange.IEventService インターフェイスのすべてのイベントに SupplyFulfillmentArgs 引数が必要だということを覚えておいてください。この引数は、発生した外部イベントをワークフローが受け取るときに、ワークフローのインスタンスにデータを渡すために使用できます。ワークフロー内の他のアクティビティからこのデータ型にアクセスできるようにする方法は 2 とおりあります。1 つは、図 13 に示されている "e" プロパティを使用する方法です。このプロパティで、イベントによって渡された引数を保持可能なワークフロー内のプロパティを指定できます。このイベント パラメータをワークフロー プロパティに設定すると、ワークフロー内の他のアクティビティで使用することができます。もちろん、指定するプロパティのデータ型はイベント パラメータと同じである必要があります。SupplyFulfillment ワークフローの場合は SupplyFulfillmentArgs 型です。または、Invoked プロパティでワークフロー内にイベント ハンドラを指定することもできます。このイベント ハンドラは、外部イベントが受け取られた後に呼び出され、ExternalDataEventArgs 型のパラメータが渡されます。このパラメータは適切な型 (SupplyFulfillmentArgs) にキャストすることができます。

この時点で、HandleExternalEvent アクティビティは ExternalDataExchange インターフェイスのみを処理し、インターフェイスの実際の実装を処理していないことに注意してください。

外部データ交換サービスを実装する

外部データ交換サービスを正しく実装するには、ワークフローの HandleExternalEvent アクティビティによって参照されるインターフェイス (IEventService インターフェイス) を実装する必要があります。EventService クラスを作成し、このインターフェイスを実装するコードを次に示します。

[Serializable]
public class EventService : IEventService
{
    public event EventHandler<SupplyFulfillmentArgs> Assigned;
    public event EventHandler<SupplyFulfillmentArgs> Approved;
    public event EventHandler<SupplyFulfillmentArgs> Rejected;
    public event EventHandler<SupplyFulfillmentArgs> Reassigned;
    public event EventHandler<SupplyFulfillmentArgs> Canceled;
    public event EventHandler<SupplyFulfillmentArgs> Ordered;
    public event EventHandler<SupplyFulfillmentArgs> OrderReceived;
    ...
 }

イベントは、そのイベントを定義しているクラス内のみから生成可能であるため、ヘルパ関数を追加してイベントを簡単に生成できるようにする必要があります。このクラスには、イベントごとに 1 つのヘルパ関数が必要になります。次のコードは、Assigned イベントのヘルパ関数を示しています。

public void RaiseAssignedEvent(System.Guid instanceId, string
    assignedTo)
{
      // Check to see if event is defined
    if (this.Assigned != null)
    {
        // Create the EventArgs for this event
        LocalService.SupplyFulfillmentArgs args = new
            LocalService.SupplyFulfillmentArgs(instanceId);
        args.AssignedTo = assignedTo;

        // Raise the event
        this.Assigned(this, args);
    }
}

イベントを生成する場合、登録済みのすべてのデリゲートに渡す 2 つのパラメータがあります。最初のパラメータは送信元です。前述のコードでは、EventService クラスの現在のインスタンスを渡しています。送信元として渡すには、インスタンスをシリアライズ可能として設定する必要があります。シリアライズ可能にしないと、イベントを送信できないことを示すエラーが返されます。この場合のエラー メッセージの例を次に示します。

  Event "Assigned" on interface type
  "ExternalDataExchange.IEventService"
  for instance id "0f9e8545-39bf-48b6-b890-
  c12dec11cb9f" cannot be delivered.

2 番目のパラメータは SupplyFulfillmentArgs です。このクラスをインスタンス化する場合は、ワークフロー インスタンス ID (GUID) が必要です。GUID は、ワークフローを最初にインスタンス化したときに生成されています。ワークフロー ランタイムはこの GUID を使用して、イベントを受け取るワークフロー インスタンスを検索します。ただし、そのためには、EventService クラスのインスタンスを外部サービスとしてワークフロー ランタイムに追加する必要があります。

外部データ交換サービスをワークフロー ランタイムに追加する

インスタンス化されたワークフローのすべての実行および操作はワークフロー ランタイムで管理されるため、先ほど作成したクラスを外部データ交換サービスとしてワークフロー ランタイムに追加する必要があります。次のコードは、EventService クラスのインスタンスを ExternalDataExchange サービス クラスとしてワークフロー ランタイムに追加する方法を示しています。

// Create a new instance of the local service and host it in an
//ExternalDataExchangeService.
ExternalDataExchangeService externalDataSvc = new
    ExternalDataExchangeService();
wr.AddService(externalDataSvc);
eventService = new EventService();
externalDataSvc.AddService(eventService);

ワークフロー ランタイムの処理方法は個々に異なります。まず、外部データ サービスを作成し、ワークフロー ランタイムに追加して初めて、EventService クラスを ExternalDataExchangeService クラスに追加することができます。ExternalDataExchangeService オブジェクトをワークフロー ランタイムに追加する前に使おうとすると、WorkflowRuntime コンテナに ExternalDataExchangeService が含まれていないことを示すエラーが返されます。

この操作が完了すると、EventService クラスのインスタンスから生成されたイベントが、ワークフローの適切なインスタンスへの経路およびワークフロー内の適切な HandleExternalEvent アクティビティへの経路を探します。

外部データ交換を使用する

これですべての設定が完了し、外部データ交換を使用してデータ交換およびワークフローの状態遷移を行うことができるようになりました。外部データ交換の使い方は、前述の EventService クラス内で設定されたヘルパ関数の呼び出しと同様に簡単です。次のコード例は、ワークフローの状態を送信済みから割り当て済みに変更するヘルパ関数の使い方を示しています。ワークフロー インスタンス ID および assignTo パラメータが関数に渡される点に注目してください。この 2 つのパラメータは SupplyFulfillmentArgs クラスのインスタンスにパッケージ化されるため、Assigned イベントでワークフローに渡すことができます。前述したように、ワークフロー ランタイムは workflowInstanceID パラメータを使用することにより、適切なワークフロー インスタンスに対して Assigned イベントを生成することができます。assignTo パラメータ ("Joe") は、ワークフローの実行時に使用するワークフローに直接送信されます。

eventService.RaiseAssignedEvent(workflowInstanceId, "Joe");

任意の時点では、実行中のステート マシン ワークフローは現在の State アクティビティ内のイベントをリスンしているだけです。他のイベントが発生すると、呼び出し元コードに例外が返されます。たとえば、SupplyFullfilment ワークフローをインスタンス化すると、ワークフローは初期状態 (送信済み状態) になります。これは、ワークフローが Assigned イベントの発生を待機していることを意味します。その他のイベント (Approved、Rejected、Reassigned、Canceled、Ordered、OrderReceived) を生成する他のヘルパ関数が呼び出された場合は、イベントを送信できないことを示すエラーが返されます。ワークフローの状態が承認済み以外のときに Ordered イベントが呼び出された場合に発生するエラー メッセージの例を次に示します。

Event "Ordered" on interface type "ExternalDataExchange.IEventService" for instance id "59657ed0-1532-42c5-8abe-6523bf121fff" cannot be delivered.

ご覧のように、イベントの送信に問題がある場合に返されるのはこのようなきわめて汎用的なエラー メッセージであり、問題の内容を特定するために役立つ情報が含まれていません。

これでは、脆弱でないワークフロー コードの作成方法について疑問が生じます。たとえば、ステート マシン インスタンスの現在の状態をコードで認識し、プログラムでワークフローに適切なイベントを送信できるようにするにはどうしたらよいでしょう。これについては、ステート マシン ワークフローを利用するコードで使用可能な便利な API があります。

便利なステート マシン API

WF には、ステート マシン ワークフローのインスタンス管理専用に StateMachineWorkflowInstance という名前のユーティリティ クラスが付属しています。図 14 は、StateMachineWorkflowInstance クラスのコンストラクタ、プロパティ、およびメソッドの概要を示しています。

Figure 14 StateMachineWorkflowInstance のコンストラクタ、プロパティ、およびメソッド (Close Figure 14)

パブリック コンストラクタ
名前 | 説明
StateMachineWorkflowInstance | コンストラクタです。2 つのパラメータが必要です。最初のパラメータはワークフロー ランタイムのインスタンスであり、2 番目のパラメータはこのクラスのプロパティおよび関数での問い合わせまたは管理の対象となるステート マシン インスタンスの GUID です。
パブリック プロパティ
名前 | 説明
CurrentState | 読み取り専用。現在の状態の StateActivity オブジェクトを返します。
CurrentStateName | 読み取り専用。現在の State アクティビティの名前を表す文字列を返します。
InstanceID | 読み取り専用。ワークフロー ランタイム内のステート マシン ワークフローを一意に識別する GUID を返します。
PossibleStateTransitions | 読み取り専用。現在の状態から遷移可能な状態を表す ReadOnlyCollection<文字列> を返します。
StateHistory | 読み取り専用。現在のワークフローの遷移後の状態の ReadOnlyCollection<文字列> を返します。このプロパティはワークフロー ランタイムに追跡サービスを追加する必要があります。
StateMachineWorkflow | 読み取り専用。StateMachineWorkflowActivity 型を返します。このプロパティはワークフロー定義のコピーであり、ライブ インスタンス ツリーではないため、ランタイムの値の取得に使用することはできません。
States | 読み取り専用。現在のワークフローの定義に含まれるすべての状態の ReadOnlyCollection<文字列> コレクションを返します。StateMachineWorkflow プロパティと同様、このプロパティには定義のみが含まれ、ライブ データは含まれていません。
WorkflowInstance | 読み取り専用。現在のインスタンスを表す現在の WorkflowInstance 型を返します。このプロパティでライブ インスタンス ツリーにアクセスできます。
パブリック メソッド
名前 | 説明
EnqueueItem | ステート マシン ワークフローのキューにメッセージを配信します。通常は不要です。前述の HandleExternalEvent アクティビティは、エラーが比較的発生しにくいプログラミング モデルを使用してメッセージをキューに登録します。
SetState | このメソッドの唯一のパラメータで指定された状態に遷移します。ワークフローの任意の状態への遷移に使用できます。通常のワークフローの実行では発生しない遷移が指定されている場合でも、遷移可能です。

ただし、このクラスを使用する場合は次の点に注意してください。まず、SupplyFulfillment ワークフローのようなイベント駆動型のステート マシンは本質的に非同期です。そのため、図 14 に示されているパブリック プロパティを、ステート マシン インスタンスにイベントを生成した直後に問い合わせると、ワークフローの状態が正確に反映されません。イベント駆動型ワークフローは、現在のイベントの処理が終了し、次のイベントを待機する間はアイドル状態になります。そのため、ワークフロー ランタイムの OnWorkflowIdled イベントは、これらのプロパティを使用してステート マシン インスタンスを問い合わせるのに適しています。

次に、StateHistory プロパティはワークフロー ランタイムに追跡サービスを追加する必要があります。このプロパティに追跡サービスが存在しない場合、次のエラーが返されます。

'StateHistory' property requires the 'System.Workflow.Runtime.Tracking.SqlTrackingService' service.

また、SetState メソッドの機能をエンド ユーザーに公開するときには注意が必要です。このメソッドを実行すると、ワークフローの状態が遷移する可能性があるため、標準外のシナリオを処理する管理者またはマネージャが使用した方が無難です。

まとめ

ステート マシン理論、デザイン時の操作、使用可能なステート マシンの作成に必要な基本アクティビティについて説明してきましたが、重要な注意事項がまだいくつか残っています。まず、ステート マシンをサーバー環境で実行する場合、実行中にシステム リソースを最適化し、システムの再起動後も維持する必要があります。また、ビジネス プロセスを追跡レポートに基づいて最適化できるように履歴データを追跡および保存することが望ましい場合があります。たとえば、この記事の在庫調達ワークフローを小規模ビジネスで使用する場合、各ワークフローが発注済み状態から完了状態に移行するまでにかかる時間を確認することが望まれます。特定のサプライヤがサービス レベル合意書に常習的に違反している場合、新しい在庫を調達するビジネス プロセスは、違反したサプライヤを除外するだけで最適化できます。

ヒューマン ワークフローもさらに詳しく検討する価値のあるトピックです。ステート マシンはヒューマン ワークフロー構築の初歩に適しています。ステート マシンを使用してユーザー入力を処理するビジネス プロセスを管理する場合、ユーザー インターフェイスを構築する時期に注意する必要があります。ユーザーに対しては、ステート マシンの最適経路をわかりやすく示し、現在の状態に関連するオプションのみを表示する必要があります。

Keith Pijanowski は、マイクロソフトの開発者およびプラットフォーム エバンジェリズム チームのプラットフォーム戦略アドバイザとして、顧客のビジネス上の問題に新しい概念やテクノロジを適用するための支援を行っています。また、ニュージャージーおよびニューヨーク地区で開催されるマイクロソフトのイベントで頻繁に講演も行っています。連絡先は www.KeithPij.com です。


編集者のメモ: 優れたデザインによって言語の壁を乗り越える

編集者のメモ
優れたデザインによって言語の壁を乗り越える
Howard Dierking

数週間前、 私は TechEd Europe カンファレンスの講演者としてバルセロナに行きました。私のセッションにお出でくださった皆さん、また展示場の MSDN ブースにお立ち寄りいただいた皆さん、非常にすばらしい経験をさせていただき、ありがとうございました。またこのような機会を持つことができるよう願っています。

カンファレンスの最終日、私は少し大胆なことをしようと思い、地下鉄に飛び乗ってバルセロナの繁華街に出かけました。私は小さなレストランに入り、料理を注文しようとしました。注文 "しようとした" ということを強調しなければなりません。なぜなら、そのレストランには英語を話す人が 1 人もいなかったためです。私は非常に多くのチューリング完全言語を話す (記述する) ことができますが、スペイン語を話すことはできません。いずれにしても、私は自分の食べたいものをうまく伝えることができなかったのですが、レストランのスタッフは非常に愛想よく接してくれました (私の注文を聞きに来てくれたウェイトレスさんはジェスチャー ゲームの天才だと思います)。しかしこの経験によって、私は使用する言語ツールとその使用方法について深く考えさせられました。

この記事の執筆中、Visual Studio 2008 (および C# 3.0 と Visual Basic 9.0) の製品版が正式にリリースされ、MSDN サブスクリプションの会員の皆さんにオンラインで提供されるようになりました。ご存知だと思いますが、これらの新しいバージョンでは、レキシカル クロージャなどの主要な言語機能によって、機能的なプログラミングのサポートがさらに強化されています。また、より宣言的になっているプログラミングの形式に関して言えば、LINQ をサポートするための組み込みの言語拡張によって、まったく新しいレベルの表現力が加わりました。

特に Web 開発など、周りを見回してみると、動的言語の人気がますます高まっています。これに伴い、マイクロソフトは新たなランタイムと一連の言語ツールを構築しました。これらを使用すると、人気のある Ruby や Python などの動的言語を実際に Microsoft .NET Framework 上で実行したり、これらの言語で .NET Framework を利用したりできるようになります。良いところをすべて (動的言語の要素が含まれた、強力で静的な型指定の安全性を) 取り入れたい皆さんには嬉しいお知らせだと思いますが、F# が Visual Studio で正式にサポートされる言語としてリリースされることが最近マイクロソフトから発表されました。

しかし、もはやプログラミング言語の選択に関する話だけではなくなっています。大部分は Ruby コミュニティのおかげですが、ドメイン固有の言語も主流になり始めています。主要プログラミング言語として多くの開発者が使用している言語でも、ソフトウェアのデザインに関する議論の対象が、オブジェクト モデルやデザイン パターンから API の表現力へと移ってきています。ソフトウェアの複雑さは急激に高まっているため、それに伴ってプログラミング言語の要件も増加しています。言語は単にコンピュータへの命令を生成するための手段だけでなく、開発者がコードで実際の意図を表現するための手段も提供する必要があります。

この表現力という考えが実現されれば、プログラミング言語に個性がより反映されるようになるため、非常に面白くなります。たとえば、この記事の中で 8 文字を超える単語 (大まかに定義すると、スペースによって区切られた文字のかたまり) の数を知りたいとします。表現力が高いのは次のどちらのコードでしょうか。

article.Split(new char[]{' '}).Where(w => w.Length > 8);

または

[\S\(\)]{9,}

答えはもちろん、ソフトウェア デザインの要素に関する理解の仕方と考え方だけでなく、テキストの解析に関する理解の仕方と考え方によっても判断が異なるということです。1 つのアプリケーションの中でさえ、それぞれの開発者がさまざまな問題を持ちます。ほとんどの場合、これらの問題はさまざまな言語で自然に表現されます。これらの問題をそれぞれの開発者が自然な表現を使用して解決できることが、優れたデザインの証です。

ところで、食べ物を注文するための共通言語を決めませんか。

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Cagri Aslan、Keith Boyd、Shawn Burke、Bill Chiles、James Clarke、Peter Donnelly、Charles Finkelstein、Elisa Flasko、Ted Hart、Pravin Indurkar、John Justice、Masahiko Kaneko、Lawrence Liu、John Robbins、および Chris Tavares です。


ツールボックス: 強力なテキスト編集機能、.NET の Web テスト、強化された単体テストなど

ツールボックス
強力なテキスト編集機能、.NET の Web テスト、強化された単体テストなど
James Avery

コンテンツ
E Text Editor で編集機能を向上させる
Web アプリケーションの機能テストを作成する
新しいテスト用フレームワーク
実践的な単体テスト

E Text Editor で編集機能を向上させる

すべてのプログラマにとって、優れたテキスト エディタは不可欠なツールです。プログラマによるテキスト エディタの選択は、会議場や作業現場で最も頻繁に耳にする議題の 1 つでもあります。Mac OS X のテキスト エディタである TextMate は、使いやすさと強力な拡張機能のために、そのプラットフォームで一躍人気になりました。E Text Editor は、同じ単純さと強力な拡張機能を Windows® プラットフォームに提供するための新しいテキスト エディタです。

E Text Editor では、堅固な基盤の上に、使いやすく単純なエディタ機能が構築されています。このプログラムでは、多くのブラウザで提供されているタブ ブラウズ形式と同様の方法で、タブを使用して複数のファイルを一度に開くことができます。また、特定のディレクトリのディレクトリ構造ビューを表示するプロジェクト ウィンドウも用意されています。サンプルのスクリーンショットでは、ASP.NET Web アプリケーション フォルダが開いており、異なる各ディレクトリとファイル (適切なアイコンを含む) が表示されています。このプロジェクト ウィンドウを使用すると、プロジェクト内のディレクトリ間およびファイル間を非常に簡単に移動できます。

E Text Editor の拡張機能は、他のソフトウェアのプラグインやアドインと同様に、無駄なネイティブ アプリケーションを増やすことなく、さまざまなプログラミング言語用の機能を追加する方法を提供するバンドルの概念を中心にしています。既定では、E Text Editor には、C、C#、JavaScript、HTML、CSS、XML など、マイクロソフトの開発者が求めている大部分の言語用のバンドルが含まれています。これには Visual Basic® .NET が欠けているように思われます。しかし、E Text Editor には独自のバンドルを作成する機能があるため、すぐに別のユーザーが Visual Basic .NET のバンドルを作成して公開してくれるでしょう。

バンドルには多数の機能を含めることができます。その最初がテキスト補完です。残念ながら、これは Visual Studio® の IntelliSense® のようなテキスト補完ではありません。特定のキーボード操作に基づいたテキスト補完です (Visual Studio のコード スニペットに類似しています)。たとえば、C# のバンドルには、「ns」と入力して Tab キーを押すとすぐに新しい名前空間を宣言できるテキスト補完設定があります。新しいプロパティを作成するには、「prop」と入力して Tab キーを押します。それ以外の項目についても同様の方法を使用できます。各バンドルには、そのバンドルが記述されている対象の言語に固有のテキスト補完が用意されています。

バンドルには、特定の言語に対応した書式ルールもあります。新しいファイルを開くときは、ドキュメントで使用される書式の種類が下部のステータス バーの中央に表示されます。たとえば、C# ドキュメントを開くと、ツール バーに C# と表示され、C# の構文が強調表示および書式設定されます。ステータス バーの書式をクリックすると、ドキュメントの書式設定の方法を変更できます。特定のファイルをさまざまな形式で表示できるため、これは重要です。たとえば、ある .html ファイルを ASP、ASP.NET、または別の Web フレームワークにすることができます。形式を正しく選択すれば、最適な構文の強調表示および書式設定が提供されるだけでなく、そのバンドルでテキスト補完を使用することもできます。

E Text Editor はテキスト エディタの優れた新しい選択肢です。次に私が参加するテキスト エディタに関する議論の際には、E Text Editor について聞くことができると十分に予想できます。

価格 : 34.95 ドル

www.e-texteditor.com

単純なテキスト編集機能を提供する E-Text Editor
単純なテキスト編集機能を提供する E-Text Editor (画像を縮小するには、ここをクリックします)
単純なテキスト編集機能を提供する E-Text Editor
単純なテキスト編集機能を提供する E-Text Editor (画像を拡大するには、ここをクリックします)

Web アプリケーションの機能テストを作成する

単体テストはアプリケーションのテストおよび再テストの繰り返しを避けるために大いに役立ちますが、通常、単体テストでは、アプリケーション自体ではなく、アプリケーションの基になる層とオブジェクトをテストします。そのため、すべての単体テストに合格しても、ASP.NET ページまたは単体テストの対象でなかった Windows フォームのコードでエラーが発生する可能性があります。

Watin (Web Application Testing in .NET) は、Ruby で記述された同様のオープン ソース フレームワークである Watir を基に考え出されました。Watin は、Web アプリケーションに対して直接テストを記述するために使用できるオープン ソース ツールです。この方法で記述されるテストは、単体テストのように単一のコード単位ではなくアプリケーションの機能をテストすることから、機能テストと呼ばれます。

Watin では、Internet Explorer® オブジェクトを使用して、ユーザーと同じ方法でブラウザを起動および操作できます。Watin では、フィールドへのテキストの入力、オプション ボタンの選択、チェック ボックスやボタンのクリック、およびフォームやページからの値の読み取りが可能です。また、画面上で Watin が作業を行うのを座って観察することもできます。これにより、巧妙に隠された記録を利用して、普段の作業がどれだけ大変かを明らかにすることができます。

Watin のテストの記述には、お好みの単体テスト用フレームワークと Watin ライブラリとの組み合わせが使用されます。Watin ライブラリには、Internet Explorer オブジェクトとのやり取りに必要なメソッドとオブジェクトが用意されています。一方、テスト用フレームワークにはテストとアサーションが用意されています。

Watin のテストを手動で記述するのはやや面倒であるかもしれません。テキスト ボックスとボタンを識別するには、クライアント識別子を使用する必要があります。これを見つけるには、ページのソースを表示するか、またはよく使われているいずれかのブラウザ ツール バーを使用します。

しかし、Watin のテストを記述するためのさらに便利な方法は、Watin Test Recorder という別のオープン ソース プロジェクトを使用することです。Watin Test Recorder は、ブラウザを使用したテストを通じて Watin のテストを作成して記録するための Windows フォーム アプリケーションです。

単純なテストを作成するために必要なのは、ページへの移動、テスト名の指定、およびテストの複製に必要なアクションの実行だけです。たとえば、ホーム ページに移動し、ユーザー名テキスト ボックスにユーザー名、パスワード テキスト ボックスにパスワードを入力して、ログイン ボタンをクリックします。

テストのコードは、アプリケーションの下部にあるボックスに表示されます。このコードをコピーするか、または直接 Watin Test Recorder からコードを保存できます。テストをコピーまたは保存したら、アサーションを追加する必要があります。前の例を使用して、ユーザーが正常にログインしたことを確認するアサーションを記述します。このアサーションでは、ブラウザが適切なページにリダイレクトされることをテストするか、またはそのページ上のテキストの特定のセクションを探すことができます。

Watin は、Web アプリケーションの手動テストの繰り返しに代わる優れたツールであり、ユーザーよりも先に問題を見つけるために役立ちます。

価格 : 無料 (ソース コードを入手可能)

watin.sourceforge.net

Watin を使用して Web アプリケーションに対するテストを記述する
Watin を使用して Web アプリケーションに対するテストを記述する (画像を縮小するには、ここをクリックします)
Watin を使用して Web アプリケーションに対するテストを記述する
Watin を使用して Web アプリケーションに対するテストを記述する (画像を拡大するには、ここをクリックします)

新しいテスト用フレームワーク

Microsoft® .NET Framework には、nUnit、csUnit、mbUnit (先月のこのコラムで紹介しました) などの確立されたいくつかの単体テスト用フレームワークがありますが、最近、xUnit.net という別の新しいフレームワークが登場しました。

いくつかの点でこのフレームワークは既存のフレームワークとは大きく異なります。最も重要な点は、James Newkirk と Brad Wilson が構築したということです。nUnit の構築に貢献した Newkirk は、マイクロソフトのプロダクト マネージャであり、CodePlex を担当しています。彼は、単体テストに関する多くの書籍を執筆しています。Brad Wilson は、thedotguy という名前で長期にわたってブログを投稿しており、以前は patterns & practices チームのメンバでした。現在はマイクロソフトの従業員です。この新しいフレームワークを使用する目標は、過去 5 年間で学んだ単体テストに関するベスト プラクティスを採用して、それらを実現および促進する新しいフレームワークを構築することでした。

既存の .NET の単体テスト用フレームワークと比較すると、このフレームワークには多くの変更点があります。最も注目すべき点は、削除された機能です。フレームワークおよびそれを使用して記述されるテストを簡略化するために、多くのテスト用フレームワークに存在するいくつかの属性が削除されました。テストを読みやすく、かつわかりやすくするために、setup 属性と teardown 属性を削除しました。すべての初期化コードと破棄コードは個々のテスト内で実行する必要があります。

このような属性の削除の背景にある考え方は、ロジックが異なる 3 つのメソッドに分かれてしまうために、これらの属性があってもテストの正確な内容がわかりにくくなるだけである、というものです。setup 属性と teardown 属性を削除することにより、Newkirk と Wilson はテストのすべてのロジックを単一のテスト メソッドに組み込むよう開発者に勧めています。

Throws という Assert オブジェクトの新しいメソッドが採用されたため、ExpectedException 属性は削除されました。Throws アサーションを使用すると、開発者は例外の発生が予期される場所をより明示的に指定して、誤検知の頻度を少なくすることができます。TestFixtureSetup と TextFixtureTeardown は、ITestFixture インターフェイスに置き換えられました。Ignore 属性は削除され、代わりに Test 属性のプロパティとして実装されました。

xUnit.net では機能が削除されているだけではありません。このフレームワークには、多数の非常に便利な機能が追加されています。xUnit.net にはジェネリックと匿名デリゲートの完全なサポートが含まれており、テストをさらに簡潔かつ正確に行うことができます。また、xUnit.net では、カスタム比較機能、カスタム テスト メソッド、およびカスタム クラスを使用してフレームワークの機能を拡張することができます。

このリリースの xUnit.net には GUI テスト ツールは付属していません。コンソール実行ツールおよび TestDriven.net を使用した Visual Studio のサポートだけが用意されています。

現時点では現在お使いのテスト用フレームワークを削除して xUnit.net に移行することはお勧めしませんが、このツールを通じて表面化した優れた考え方やベスト プラクティスから学ぶだけだとしても、これは明らかに注目すべきことです。

価格 : 無料 (ソース コードを入手可能)

codeplex.com/xunit

実践的な単体テスト

単体テストを開始するのは簡単かもしれませんが、単体テストを正しく行うのは非常に困難な場合があります。私が初めて単体テストの記述を開始したときには、何件かのブログの投稿と記事しか参考にするものがありませんでした。このような参照用の書籍があればよかったのにと思っています。Andrew Hunt と David Thomas の共著『NUnit を使用した C# での実践的な単体テスト』(2006 年、Pragmatic Bookshelf) は、全 3 巻シリーズの『実践的なスタート キット』の第 2 巻です。第 1 巻はソース管理の使用について、第 3 巻はプロジェクトの自動化について記載されています。

この書籍では、単体テストの基本的な考え方、原則、利点および単体テストの計画と記述について説明しています。サンプルとして NUnit を使用していますが、その理論は .NET で使用可能なほとんどすべての単体テスト用フレームワークに適用されます。

テストに関する実用的なアドバイスを豊富に記載したこの書籍は、単体テストの役立つ入門書でもあり、より良い単体テストを記述し、テストの一般的な問題を解決する方法を探すための優れたガイドでもあります。

価格 : 29.95 ドル

pragprog.com/titles/utc2

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

ご意見やご質問は、こちらまで英語でお送りください。 toolsmm@microsoft.com.

James Avery は、Infozerk Inc. という会社で独自の .NET コンサルティング業務を行っています。彼は多数の書籍や記事の執筆に携わっており、最新の著書は、『Visual Studio Hacks』(O'Reilly、2005 年) です。連絡先は javery@infozerk.com (英語) です。また、www.dotavery.com/blog で彼のブログをご覧いただけます。


CLR 徹底解剖: 動的言語と Silverlight

CLR 徹底解剖
動的言語と Silverlight
Jimmy Schementi

コンテンツ
DLRConsole を入手する
DLRConsole を使用する
コードの内容
2 + 2 を計算してみましょう
でも待ってください、まだ続きがあります
今すぐお試しください

子供は、創造的な遊びを通して学習します。LEGO から GI ジョーやバービー (これは私ではなく妹が持っていたものですよ) に至るまで、おもちゃを手にして直感的に何かを行います。少なくともおもちゃを理解するという目的では、マニュアルを手に取って読んだりしません。特に "正しいやり方" というものが脳に刻み込まれていない子供の段階では、ある事を実験したり、ある物で遊んだりしてそれらを理解するというのは、人間にとって自然なことです。このような実践的な方法は、ほぼ間違いなく最も優れた学習方法の 1 つだと言えます。

同じ考えをソフトウェアに当てはめると、動的言語は従来、対話型環境に関連付けられます。Read-Eval-Print ループ (REPL) が提供されるため、試行錯誤を繰り返して探索的にプログラミングを進めることができます。ある意味、REPL はプログラミングを遊びに変える方法です。動的言語の支持者が「とにかく遊んでみるしかない」と言うのはこのためです。

私のチームでは、動的言語と Silverlight™ の統合を進めている際に、Silverlight で遊べるようにしたいとすぐに考えました。そこで、Jim Hugunin が巧みにプログラミングを行い、DLRConsole が誕生しました。ここでは、DLRConsole のしくみと、IronPython、DLR のホスト、および DLR と Silverlight の統合について説明します。

DLRConsole を入手する
DLRConsole をローカルに実行する

DLRConsole はローカル コンピュータにダウンロードして実行できますが、Web サーバーが必要になるため、セットアップ作業は少し複雑です。Web サーバーをセットアップしたら、DLRConsole.zip を展開し、展開したフォルダを Web サーバーのフォルダに保存して、Web サーバーの /DLRConsole/index.htm に移動します (このコラムでは、コンピュータに Web サーバーをセットアップする方法については説明しませんが、Windows Vista の場合は iis.net/articles/view.aspx/IIS7/Deploy-an-IIS7-Server/Installing-IIS7/Install-IIS7-on-Vista、Mac OS 10.4 の場合は docs.info.apple.com/article.html?path=Mac/10.4/en/mh1921.html で、それぞれ関連情報を参照できます)。

独自に用意した Web サーバーで DLRConsole を実行することの唯一の欠点は、サポートされている DLR 言語用にサーバーの構成を設定する必要があることです。これが問題であることは、次のようなエラーが発生すればわかるでしょう。

Silverlight error message
ErrorCode: 2024
ErrorType: ParserError
Message: Invalid attribute value text/python.
XamlFile: DLRConsole.xaml
Line: 6
Position: 53

次の MIME の種類のマッピングをサーバーの構成に追加する必要があります。

.py == text/python
.jsx == text/jscript

これで完了です。DLR Silverlight アプリケーションを実行できるようになりました。

DLRConsole の説明に入る前に、動的言語ランタイム (DLR) について説明しておきましょう。DLR は、CLR の上に位置する動的言語サポート システムであり、ほとんどの動的言語の実装に必要な言語サービスを提供します。このサービスには、動的な型システム、動的メソッドのディスパッチ、動的なコードの生成、およびホスト API があります。DLR の詳細については、Jim Hugunin のブログの記事 (blogs.msdn.com/hugunin/ archive/2007/04/30/a-dynamic-language-runtime-dlr.aspx) を参照してください。

DLRConsole は、Silverlight を使用したコードの開発が非常に簡単であることと、Silverlight が実験型のツールであることを示す、Silverlight 1.1 のサンプルです (2008 年 1 月 1 日から、Silverlight 1.1 は Silverlight 2.0 に名前が変更されることに注意してください)。このサンプルを使用すると、XAML と Python のコードを編集および実行できるほか、コードの色分けおよび補完機能がサポートされている対話型エディタで、Python と JScript® を試すことができます。

DLRConsole は、2 つの方法で DLR と Silverlight を統合しています。まず、DLRConsole は、DLR に準拠した言語である IronPython で記述された Silverlight アプリケーションです。また、DLR のホスト API を使用して、コンソールやエディタに入力された DLR コードを実行します。

DLRConsole のコードは、silverlight.net/Samples/1.1/DLR-Console/DLRConsole.zip からダウンロードしてローカルに実行できます。DLRConsole を対話形式で使用する必要がある場合は、silverlight.net/Samples/1.1/DLR-Console/python にアクセスし、ホストされた DLRConsole を使用してください。DLRConsole を実行するには、コンピュータに Silverlight をインストールする必要があります。Silverlight がインストールされていない状態で DLRConsole を実行すると、Silverlight をダウンロードするためのリンクが表示されますが、Windows® または Apple プラットフォーム用の Silverlight を事前にインストールする場合は、microsoft.com/silverlight/install.aspx を参照してください。Linux ベースの OS を実行している場合は、Linux 用の Silverlight の実装である Moonlight に関する Mono のプロジェクト (mono-project.com/Moonlight) を参照してください。

DLRConsole を使用する

DLRConsole をブラウザで実行する方法は 2 つあり、silverlight.net から実行するか、コンピュータでローカルに実行します。このコラムでは、Silverlight.net から実行する方法を説明しますが、DLRConsole.zip をダウンロードして、コード全体を確認し、開発用コンピュータまたはローカル サーバーで実行する場合は、「DLRConsole をローカルに実行する」を参照してください。役に立つヒントが紹介されています。

操作を開始するには、silverlight.net/Samples/1.1/DLR-Console/python をブラウザで開きます。これで準備は完了です。ブラウザで DLRConsole を開くと、図 1 のように表示されます。

図 1 ブラウザに表示された DLRConsole
図 1 ブラウザに表示された DLRConsole(画像を縮小するには、ここをクリックします)
図 1 ブラウザに表示された DLRConsole
図 1 ブラウザに表示された DLRConsole (画像を拡大するには、ここをクリックします)

DLRConsole が読み込まれると、Python のコマンド プロンプトが表示され、1 行ずつ Python のコードを入力して、その結果をコンソールの右側か、Silverlight のキャンバスで確認することができます (図 2 参照)。別の言語をクリックすると、その言語に合わせてコンソールが切り替わり、選択した言語のサンプル コードが表示されます。さまざまな言語でステートメントを入力し、ある言語で記述した動作が他の言語ではどのように記述されるかを確認してみてください。

図 2 コンソールに記述されたコード
図 2 コンソールに記述されたコード(画像を縮小するには、ここをクリックします)
図 2 コンソールに記述されたコード
図 2 コンソールに記述されたコード (画像を拡大するには、ここをクリックします)

[XAML] タブには、[Silverlight Canvas] (Silverlight キャンバス) ウィンドウの初期表示の基になっている XAML コードが表示されます (図 3 参照)。このコードを編集するかコードを追加し、Ctrl キーを押しながら Enter キーを押すと、キャンバスの表示が更新されます。

図 3 [XAML] ウィンドウ
図 3 [XAML] ウィンドウ(画像を縮小するには、ここをクリックします)
図 3 [XAML] ウィンドウ
図 3 [XAML] ウィンドウ (画像を拡大するには、ここをクリックします)

[Python Code] (Python コード) タブには、Python のコードが表示されます。このコードによって、背景が青色になり、テキストの上にカーソルを移動するとテキストの色が変わります。この場合も、コードを初めて実行するか、変更を加えた後に実行するとき、Ctrl キーを押しながら Enter キーを押す必要があります。

[About] (関連情報) タブには、CodePlex の Dynamic Silverlight Web ページ (codeplex.com/dynamicsilverlight) へのリンクが表示されます。

コードの内容

ここまでは、DLRConsole の機能について説明しました。次は、DLRConsole を構成するファイルと、DLRConsole のしくみについて説明します。図 4 は、DLRConsole のダウンロード ファイルに含まれているソース ファイルの一覧です。

Figure 4 DLRConsole のソース ファイル (Close Figure 4)

ファイル | 説明
Index.htm | Silverlight.js と CreateSilverlight.js が含まれている、CreateSilverlight を呼び出す単純な HTML ページです。
Silverlight.js | ブラウザで Silverlight アプリケーションを作成するためのロジックが記述されています。
CreateSilverlight.js | Silverlight を初期化し、DLRConsole.xaml を読み込む CreateSilverlight が定義されています。
DLRConsole.xaml | DLRConsole.py を参照し、Loaded イベントを OnLoaded Python メソッドにバインドする小さな XAML ファイルです。
DLRConsole.py | アプリケーションのすべての機能が記述されています。
Wpf.py | WPF API と対話するヘルパ メソッドが定義されています。
README.txt | DLRConsole に関するドキュメントです。

Index.htm、Silverlight.js、および CreateSilverlight.js は、Silverlight の初期化に使用されます。これらのファイルは重要ですが、ここでは詳しい説明を省略します。基本的な Silverlight プロジェクトをセットアップする方法については、silverlight.net/QuickStarts/Start/CreateProject.aspx を参照してください。

Wpf.py は、LoadXaml(url) など、WPF と対話するためのヘルパを定義する Python ファイルです。

def LoadXaml(url):
    uri = MakeUri(url)
    request = System.Windows.Browser.Net.BrowserHttpWebRequest(uri)
    response = request.GetResponse()
    reader = System.IO.StreamReader(response.GetResponseStream())
    xaml = reader.ReadToEnd ()
    reader.Close()
    return XamlReader.Load(xaml)

このメソッドは、指定された URL をダウンロードし、XamlReader.Load の結果を返します。

DLRConsole.xaml は、DLRConsole.py を読み込んだ後、Python のコードで定義されている OnLoaded メソッドを呼び出します。Silverlight 1.1 Alpha アプリケーションには 1 つの XAML ファイルが必要であるため、この XAML ファイルがプレースホルダの役割を果たします (この動作は、Silverlight の将来のリリースでは変更され、DLRConsole.xaml などの XAML ファイルを順に処理しなくても読み込みが可能になる予定です)。注目すべき部分は x:Code タグです。この部分で DLR を参照することによって、Silverlight がファイルを読み込み、DLR を使用してそのファイルを実行できるようにしています。これにより、XAML で定義されているすべてのイベントを DLR 言語で定義されているメソッドにバインドできます。Canvas の定義は、単純に Python のコードを実行するためのものです。

<Canvas x:Name="root"  Width="1000" Height="500"
 xmlns="http://schemas.microsoft.com/client/2007"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
    <x:Code Source="DLRConsole.py" Type="text/python" />
    <Canvas x:Name="loadingHelper" Loaded="OnLoaded" />
</Canvas

DLRConsole.py は 2164 行の Python のコードで構成されているため、その内容をすべて記載することはできませんが、ここでは最も興味深い部分について詳しく説明します。

XAML ファイルが読み込まれ、OnLoaded が呼び出された時点から、Silverlight が Python と DLR によって動作します。OnLoaded が Application クラスを初期化し、最終的には Application.Initialize(self) メソッドが実行されます。画面に表示されているものはすべて、このメソッドによって処理されています。このメソッドは、KeyHandler を登録し、ConsoleWindow、Editor、およびその他のユーザー インターフェイスを作成して、対話型プログラミングを行うためのコンソールを起動します。

2 + 2 を計算してみましょう

これまでの操作により、対話型コンソールがブラウザで実行されています。問題ありませんね。動的言語の基本的なテストとして 2 + 2 を計算し、結果を見てみましょう。

まず、コンソールに「2 + 2」と入力します。

py> 2 + 2
4
py>

入力に合わせて、入力した内容が表示されると思うかもしれませんが、WPF の観点から見ると、この処理は Application の Initialize メソッドの 1 行目に記述されている次のコードによって実行されています。

self.KeyHandler = TextInputHandler(self.root)

このコードは、キーのテキスト表現を定義する TextInputHandler を構築し、TextInputHandler の OnKeyDown メソッドを、ルートである Canvas の KeyDown イベントにバインドします。TextInputHandler は、KeyPress イベントが発生するたびに、押されたキーのテキスト表現を使用して KeyPress オブジェクトを構築し、該当するクラスにキーを処理するよう指示します。該当するクラスとは、HandleKey メソッドを持ついずれかのクラスです。この例では、ConsoleInput が該当します。ConsoleInput は、カーソルの位置に TextBuffer オブジェクトを介してテキスト表現を挿入することでキーを処理し、カーソルを次の列に移動します。

これはちょっとした作業ですが、WPF を使用すること以外、何も目新しいことはありません。ただし、通常は、Enter キーを押すと、言語がそのコード行の結果を計算し、答えを表示します。この場合は、ユーザーのブラウザ上で選択された言語がこの計算を実行します。さて、ここからが面白いところです。このしくみを詳しく見てみましょう。

まず、この記事の例でキーを処理するメソッドを以下に示します。

def HandleKey(self, key):
    if key.Text == '\n':
        self.DoInput(key.Ctrl)
    elif key.Text == '.':
        self.DoCompletions()
    elif key.Name == 'Up' and self.Caret.line == 0:
        self.DoHistory()
    else:
        Editor.HandleKey(self, key)

この例では、DoInput が式の解析を試行する DoSingleLine を呼び出しています。解析が成功すると、式が評価され、出力がコンソールに書き込まれ、"_" コンビニエンス変数が設定されます (図 5 参照)。興味がある皆さんは、ソース コードを参照して、TryExpression がどのように式を解析しているか、および GetEngine がどのように LanguageEngine を作成しているかを確認してみてください。

Figure 5 DoSingleLine による解析 (Close Figure 5)
def DoSingleLine(self, text, forceExecute):
    text1 = self.le.TryExpression(text)
    if text1:
        self.WriteInputToConsole(text)
        try:
            ret = self.Engine.Evaluate(text1, self.CurrentModule)
            if ret is not None:
                self.console.write(repr(ret) + '\n', 'output')
                self.CurrentModule.SetVariable('_', ret)
                except Exception, e:
                    self.HandleException(e)
        else:
            self.DoMultiLine(text, forceExecute)

以上が、DLRConsole で 2 + 2 を計算するために実行される処理です。実際、この処理によって、1 行のコードで記述された操作はすべて実行できます。また、少し変更を加えるだけで、複数行のコードで記述された操作も実行できます。最も興味深いのは、DLRConsole が DLR を呼び出してコードを実行する部分です。

でも待ってください、まだ続きがあります

2 + 2 は、非常に単純な例です。もう少し複雑な操作の場合はどうなるでしょうか。たとえば、.NET Framework を統合するとどうなるでしょうか。次のコードを見てみましょう。

py> import System
py> from System.Collections import *
py> h = Hashtable()
py> h["a"] = "IronPython"
py> h{"b"] = "in Silverlight"
py> for e in h: print e.Key, ":", e.Value
a: IronPython
b: in Silverlight

ご覧のとおり、IronPython から System.Collections を使用できます。この名前空間は、選択した形式に応じて、Python のハッシュまたは .NET Framework のハッシュのような役割を果たします。MSDN® Magazine の 2006 年 10 月号 (msdn.microsoft.com/msdnmag/issues/06/10/CLRInsideOut) でこれと同じ例を使用していますが、今回はブラウザ内で実行されます。IronPython と .NET の統合の詳細については、上記の記事を参照してください (ただし、記事の内容がやや古いことに注意してください)。その他の情報については、blogs.msdn.com/ironpython を参照してください。

キャンバスとの対話についてはどうでしょうか。次に示す Application クラスの StartConsole メソッドを見てみましょう。

def StartConsole(self, console, canvas, langInfo):
    ...
    console.CurrentModule.SetVariable("canvas", canvas)
    console.CurrentModule.SetVariable("wpf", wpf)
    ...

DLRConsole がコンソールを初期化すると、WPF ヘルパ モジュールやキャンバスへのポインタを含め、いくつかの変数が自動的に設定されます。これらの変数を使用して、図 6 のようなコードを作成できます。図 6 のコードは、マウスが置かれたときに色を変更する効果を実装しています。このコードの実際の動作を確認するには、[Python Code] (Python コード) タブを選択し、Ctrl キーを押しながら Enter キーを押します。

Figure 6 マウスが置かれたときのイベント (Close Figure 6)
import wpf

canvas.Background = wpf.BlueBrush

r = wpf.TextBlock()
r.Text = "Touch Me"
canvas.Children.Add(r)
r.FontSize = 60
wpf.SetPosition(r, 40, 60)

def light(s, e):
  s.Foreground = wpf.SolidColorBrush(wpf.Colors.White)

def dark(s, e):
  s.Foreground = wpf.SolidColorBrush(wpf.Colors.Black)

r.MouseEnter += light
r.MouseLeave += dark

上記の例のいずれかを入力しているときに、クラスまたはオブジェクト名の後にピリオドを入力すると、小さなポップアップが表示されることに気付いたでしょうか。DLRConsole では、"コードセンス" と呼ばれる機能がサポートされています。これは Visual Studio® の IntelliSense® 機能ほど高度ではありませんが (所詮サンプルなので)、特定のオブジェクトで利用できるメソッドやプロパティを確認するときには非常に役立ちます (図 7 参照)。

Figure 7 Code-sense Autocompletion
Figure 7 Code-sense Autocompletion

この状態で、上下の方向キーを使用して必要な要素を選択できます。Enter キーを押すとその要素が選択され、引き続き入力を行うことができます。このしくみを確認するために、再び以下の HandleKey メソッドを確認してみましょう。

def HandleKey(self, key):
    if key.Text == '\n':
        self.DoInput(key.Ctrl)
    elif key.Text == '.':
        self.DoCompletions()
    elif key.Name == 'Up' and self.Caret.line == 0:
        self.DoHistory()
    else:
        Editor.HandleKey(self, key)

Python のほんの 100 行余りのコードとしては、かなり優れた機能です。

Python から JScript へ

前述のとおり、DLRConsole では複数の DLR 言語がサポートされており、現在のリリースでは Python と Jscript のサポートが実装されています。これらの言語はすべて、DLR を基盤として構築されているため、.NET と統合できるだけでなく、言語間で宣言を共有することもできます。以下に、Python と Jscript との間で変数とメソッドを共有する例を示します。

js> a = 2 + 2
4.0
py> b = a * 2
py> b
8.0
js> function times10(n) {
js|   return n * 10
js| }
py> times10(5)
50.0

この共有を実現するには、プログラミングに使用する言語をクリックして、コンソール プロンプト、構文の色付け、そしてもちろん言語も変更します。次のように、指定された言語に設定するようコンソールに指示すると、エディタにも指示が伝わります。

def SetLanguage(self, name):
    self.le = GetEngine(name)
    self.Engine = self.CurrentEngine = self.le.engine
    self.TokenCategorizer =
        self.Engine.LanguageProvider.GetTokenCategorizer()

一見あまり実用的ではないように思えますが、開発者が好みの言語でコードを記述できるだけでなく、実装する言語によってコードの使用方法が制限されることがないため、かつてないほど有効にコードを再利用できます。開発者は、好みの言語でコードを記述するときに最も創造性を発揮できると思われるため、ソフトウェアの品質も向上する可能性があります。

今すぐお試しください

DLRConsole を使用すると、Silverlight だけでなく動的言語も最小限の準備で試すことができます。Silverlight 以外のソフトウェアをインストールする必要はありません。他人がホストしているサンドボックスで実験を開始するほど便利な方法はあるでしょうか。

引き続き、Silverlight に統合される新しい動的言語のリリースや、codeplex.com/dynamicsilverlight から提供される Silverlight 動的言語の新しいサンプルまたは更新されたサンプルのリリースに注目してください。Silverlight の次のバージョンには、IronRuby (現在 www.ironruby.net からダウンロードできます) のサポートも実装される予定です。また、IronPython は codeplex.com/ironpython からいつでもダウンロードできます。Silverlight のその他のサンプルをお探しの場合は、次の Silverlight コミュニティのギャラリーで多くの興味深いサンプルを見つけることができます。silverlight.net/community/communitygallery.aspx

ご意見やご質問は、こちらまで英語でお送りください。 clrinout@microsoft.com.

Jimmy Schementi は、マイクロソフトの動的言語チームのプログラム マネージャを務めています。主に Silverlight アプリケーションの開発に従事し、Ruby や Python などの動的言語を使用して楽しく生産性の高い機能を作成したり、これらの言語に関するコミュニティを構築したりしています。連絡先については、http://jimmy.schementi.com にアクセスしてください。


基本的な本能: Visual Basic 9.0 における XML プログラミング

基本的な本能
Visual Basic 9.0 における XML プログラミング
Jonathan Aneja

おそらく、Visual Studio® 2008 で新しく登場したクエリ技術である LINQ (Language Integrated Query) について耳にしたことがあるでしょう。Visual Basic® などの LINQ を使用できる言語では、メモリ内コレクション、データベース、データセット、XML などのさまざまなデータ ソースに、豊富なクエリ演算子を適用できます。それだけでも十分に優れたものですが、Visual Basic 9.0 ではそれをさらに超えて、XML を言語内で直接第一級のデータ型にしています。

なぜ XML データ型を Visual Basic に直接統合するのかと思うかもしれません。今日では、多くのアプリケーションがストレージとデータ転送の両方に XML を使用しています。XML は、その柔軟性と簡単さのために広く採用されており、多くのアプリケーションでストレージとデータ転送に使用されています。自己記述型 (つまり、データの構造がデータに含まれる) という特徴のため、システム間のデータの転送には特に適しています。さらに、さまざまなカスタム ファイル形式の解析ロジックを記述するより、XML タグの内部に構造化されているデータを読む方がはるかに簡単です。

ただし、問題は、XML は開発者にとって特に使いやすいわけではありませんでした。Document Object Model (DOM) などの扱いにくくて一貫性のない API、および XSLT や XQuery などの言語のために、読んで理解するのが困難なことの多い面倒なコードを大量に作成する必要があります。しかし、LINQ と Visual Basic 9.0 の登場により、XML の開発はとても簡単になりました。このコラムでは、XML の現在のプログラミング環境、LINQ による環境の改善、および Visual Basic で XML を使用する場合のサポートの向上について説明します。

DOM の使用

まず、XML で顧客のリストを作成する必要があるものとします。この顧客リストには、FirstName、LastName、Address、City、State、ZipCode などのプロパティが含まれます。これを XML に変換すると、図 1 のようになります。

Figure 1 XML での顧客リストのプロパティ (Close Figure 1)
<Customers>
  <Customer FirstName="Jane" LastName="Dow">
    <Address>123 Main St</Address>
    <City>Redmond</City>
    <State>WA</State>
    <ZipCode>10104</ZipCode>
  </Customer>
  <Customer FirstName="Matt" LastName="Berg">
    <Address>456 First St</Address>
    <City>Seattle</City>
    <State>WA</State>
    <ZipCode>10028</ZipCode>
  </Customer>
</Customers>

DOM と Visual Studio 2005 を使用する場合は、図 2 のように、リストをループして適切な XML ノードを作成するコードを記述します。このように、簡単なはずの処理に多くのコードを作成する必要があります。また、生成される XML がコードの構造とまったく一致していないため、XML の構造を見た目に反映するのが非常に困難です。

Figure 2 DOM を使用した XML ノードの作成 (Close Figure 2)
'With Visual Basic 9, you no longer have to write code like this.
Public Function ConvertToXML(ByVal custList As List(Of Customer)) _
  As String

  Dim doc As New XmlDocument
  Dim root As XmlElement = doc.CreateElement("Customers")

  For Each cust As Customer In custList
    Dim custElement As XmlElement = doc.CreateElement("Customer")
    custElement.SetAttribute("FirstName", cust.FirstName)
    custElement.SetAttribute("LastName", cust.LastName)

    Dim address As XmlElement = MakeElement(doc, "Address", cust.Address)
    Dim city As XmlElement = MakeElement(doc, "City", cust.City)
    Dim state As XmlElement = MakeElement(doc, "State", cust.State)
    Dim zipcode As XmlElement = MakeElement(doc, "ZipCode", cust.Zip)

    With custElement
      .AppendChild(address)
      .AppendChild(city)
      .AppendChild(state)
      .AppendChild(zipcode)
    End With
    root.AppendChild(custElement)
  Next
  doc.AppendChild(root)
  Return doc.InnerXml
End Function

Private Function MakeElement(ByVal doc As XmlDocument, _
                             ByVal elementName As String, _
                             ByVal value As String) As XmlElement
  Dim element As XmlElement = doc.CreateElement(elementName)
  element.InnerText = value
  Return element
End Function

長期間このようなコードを保守していくのはとても不便です。ビジネス要件が変化し、プロパティや属性を追加することが必要になった場合は、このコードを苦労して調べて、更新するノードを探し出す必要があります。

LINQ to XML API

DOM に関するコーディングの場合、私は、基本的に、XML ツリーを完全に作成することを試みます。それはまるで、API に立ち向かって仕事を成し遂げるようなものです。大量のノードを作成してそれを DOM の中で結び付けるのではなく、XML の構造を表現し、インラインで値を置き換えるだけで済めば、その方が簡単でしょう。LINQ to XML API を使用すると、まさにこのようなことが可能になります。XML をトップダウン (または機能的構築) で作成できます。

LINQ を使用すると、図 2 の関数をもっと短く、そしてわかりやすく記述できます。図 3 は、この新しい簡単なソリューションを示したものです。このコードは最初のソリューションとまったく同じ出力を生成しますが、見るとわかるように、新しいコードは短く、簡単に読むことができます。このコードでは、XElement オブジェクトと XAttribute オブジェクトのコンストラクタを使用して、ツリーを作成しています。さらに、これらのコンストラクタのオーバーロードを使用すると、追加の LINQ to XML オブジェクトを渡すことができます。たとえば、XElement を別の XElement に渡すことができます。

Figure 3 LINQ を使用した新しいソリューション (Close Figure 3)
Public Function ConvertToXML(ByVal custList As List(Of Customer)) As _
  String

  Dim doc = New XElement("Customers", _
                From cust In custList _
                Select New XElement("Customer", _
                         New XAttribute("FirstName", cust.FirstName), _
                         New XAttribute("LastName", cust.LastName), _
                         New XElement("Address", cust.Address), _
                         New XElement("City", cust.City), _
                         New XElement("State", cust.State), _
                         New XElement("ZipCode", cust.Zip)))

  Return doc.ToString
End Function

1 行目では、新しい XElement を作成し、Customers という名前を設定しています。また、関連する <Customer> タグと </Customer> タグを作成します。コンストラクタに渡す 2 番目のパラメータは LINQ クエリで、これが Customer タグの内容になります。このクエリは、リスト内の全顧客をループして、結果を XML に変換します。Select ステートメントから後の部分すべてが評価され、実行時には Customer 要素のコレクションが生成されます。

データをクエリしているので、LINQ を利用して、フィルタ、並べ替え、グループ化、結合などのさまざまな操作を実行できます。たとえば、クエリに Where 句を追加することで、シアトルに住む顧客だけを返すように関数を変更できます。DOM を使用してこれを行う場合は、For ループに If ステートメントを追加する必要があります。LINQ で結果を並べ替える場合は、Order By 句をクエリに追加するだけで済みます。DOM では、XML を作成する前に結果を手動で並べ替える必要があります。

ここで使用している例はとても簡単なので、LINQ が備える機能のほんの表面的な部分しか説明していません。これまでは、XElement 型と XAttribute 型を使用してきました。API では、XDocument、XNamespace、XComment など、他にも多くの型が定義されています。

XML リテラルと埋め込み式

LINQ to XML により操作はこれまでよりはるかに簡単になりましたが、それでもまだ十分ではありません。これは、Visual Basic 9.0 で新しく導入された概念である XML リテラルが活躍する部分です。

XML リテラルを理解するには、リテラルとは何かをまず理解する必要があります。次のような変数を考えてみます。

Dim someString = "A string literal"
Dim someNumber = 256
Dim someDate = #2/27/2008#
Dim someBoolean = True

ここで示されている各変数には値が直接代入されており、いずれの場合も実際の値のことをリテラルと呼びます (Visual Basic 9.0 の新機能である型の推定により、各変数に "As <Type>" 句を指定しなくても、これらの宣言は厳密な型指定になります)。

引用符で囲まれた部分全体を、文字列リテラルと呼びます。また、たとえば someDate に割り当てられている値は日付リテラルです。XML リテラルの概念もまったく同じです。他の場所で (リテラル表現で) データを表現するのとまったく同じように、XML を表現できます。つまり、引用符で囲む必要はありません。XML リテラルは文字列ではありません。図 4 で示すように、customers 変数にデータを直接代入でき、コンパイラは値がリテラル XML であると認識します。つまり、変数の型は XElement と推定されます。

Figure 4 Customers 変数へのデータの直接代入 (Close Figure 4)
Dim customers = <Customers>
                  <Customer FirstName="Jane" LastName="Dow">
                    <Address>123 Main St</Address>
                    <City>Redmond</City>
                    <State>WA</State>
                    <Zip>10104</Zip>
                  </Customer>
                  <Customer FirstName="Matt" LastName="Berg">
                    <Address>456 First St</Address>
                    <City>Seattle</City>
                    <State>WA</State>
                    <Zip>10028</Zip>
                  </Customer>
                </Customers>

コンパイラは、この XML を検出すると、LINQ to XML API の一連の呼び出しに変換します。このようにすると、コードが読みやすくなるだけでなく、コンパイラがこの構造を最適化するので、API を直接使用する場合より処理の速いコードが生成されます。

これまで行ってきたことはすべて、何らかの XML をプロジェクトに貼り付けて、それを変数に格納するというものでしたが、これでは不便な場合があります。Visual Basic 9.0 での XML サポートの使用に関してよくある誤解の 1 つは、XML は XML ドキュメントをコードの内部に格納するために使用する必要があるというものです。事実はこれとはまったく異なります。ポイントは、XML ドキュメントを作成、クエリ、および変換するための簡単な構文を提供するということです。XML リテラルを使用すると簡単にドキュメントを作成できることを紹介しました。しかし、これを活用するには、XML に値を直接挿入する手段が必要です。ここで活躍するのが埋め込み式です。

埋め込み式は、基本的に、"この XML リテラルの処理をしばらく中断し、式を評価して、結果を XML に挿入する" ようコンパイラに指示します。そのためには、<%= (expression) %> という構文を使用します。図 5 では、ConvertToXml 関数を例にとり、XML リテラルと埋め込み式を使用することでこの関数を改善する方法を示します。

Figure 5 XML リテラルと埋め込み式の使用 (Close Figure 5)
Public Function ConvertToXML(ByVal custList As List(Of Customer)) _
  As String

  Dim doc = <Customers>
              <%= From cust In custList _
                Select <Customer FirstName=<%= cust.FirstName %>
                                 LastName=<%= cust.LastName %>>
                         <Address><%= cust.Address %></Address>
                         <City><%= cust.City %></City>
                         <State><%= cust.State %></State>
                         <ZipCode><%= cust.Zip %></ZipCode>
                       </Customer> %>
            </Customers>

  Return doc.ToString
End Function

この例では、LINQ クエリを使用して XML を生成しますが、クエリ対象のデータ ソースは、実際には、メモリ内にある List(Of T) 型のコレクションです。これを見ると、LINQ のよさがよくわかります。データベースや他の XML ドキュメントのコレクションに対して使用するのと同じクエリ演算子を簡単に使用でき、データ ソースごとに新しい API を覚える必要はありません。また、ほんの数行のコードを使用するだけで、SQL Server® データベースからデータを取得し、それを XML に簡単に変換できることもわかります。

XML プロパティ

アプリケーションでは、XML (おそらく、ファイルまたは RSS フィードで提供されたもの) を調べ、そのデータに基づいて決定を下す必要があることがよくあります。そのためには XSL 変換をよく行いますが、LINQ to XML を使用するとこれは必要ありません。

図 6 の例では、XML ドキュメントを読み込み、シアトルに住む顧客でフィルタリングを行って、姓と名を選択しています。その後、クエリをループして、一致する結果をすべて画面に表示します。

Figure 6 XML 軸プロパティを使用したサンプル クエリ (Close Figure 6)
Public Sub DisplaySeattleCustomers()

  Dim filePath = My.Application.Info.DirectoryPath & "\customers.xml"
  Dim doc = XDocument.Load(filePath)

  Dim query = From cust In doc...<Customer> _
              Where cust.<City>.Value = "Seattle" _
              Select Name = cust.@FirstName & " " & cust.@LastName

  For Each name In query
    MsgBox(name)
  Next
End Sub

一目見たときには、何かおかしいように感じるかもしれません。コンパイラが doc を XDocument 型 (そして、cust を XElement 型) と推測した場合、cust.<City> や cust.@FirstName などと入力できるのでしょうか。明らかに、XElement にはこれらのプロパティはありません。これらは読み込んだ XML に固有のプロパティです。実際には、Visual Basic コンパイラは XML プロパティと呼ばれる機能を使用して、私たちの作業を軽減してくれているのです。

XML プロパティは、XML 軸プロパティとも呼ばれますが、XML に格納されている値を取得するための簡単な手段を提供します。図 6 のクエリでは、子孫、要素、および属性という、使用可能な 3 つの軸をすべて実際に使用しています。

XML では、子孫とは現在の要素より 1 つ以上低いレベルにネストされている要素です。"doc...<Customer>" という式は子孫軸を使用しており、"Customer という名前のすべての子孫を検索する" ことを表します。ここで使用している XML には Customers というルート ノードがあり、Customer 要素が含まれていることを思い出してください。3 個のドットの構文は、基本的に、doc.Descendants("Customer") に変換されます。

次に、cust.<City>.Value という式に注目してください。この場合は要素軸を使用しており、この行は cust.Elements("City").Value に変換されます。ここで比較を行う前に .Value を明示的に呼び出す必要があることを不思議に思うかもしれません (属性の場合は必要ありません)。これは、City のような要素は他の要素を含む場合があるためであり、cust.<City> という式は実際には IEnumerable(Of XElement) を返します。.Value 拡張プロパティはその要素に格納されているすべての値を連結し、この例の場合は単なるテキスト値です。

最後の式 cust.@FirstName は、属性軸を使用しています。これは cust.Attributes("FirstName") に変換されて、属性に格納されている値を返します。属性は要素を含むことができないので、.Value を呼び出す必要はありません。その後、表示を簡単にするため、Select メソッドを使用して、2 つのフィールドを Name という 1 つのフィールドに結合しています。

XML スキーマ IntelliSense

最後の例をコーディングしているときに、IntelliSense® が XML ドキュメント内のフィールドの名前を教えてくれるととても助かります。しかし、コンパイラがどのようにして XML ドキュメントの構造を認識するかが問題です。XSD スキーマがその答えです。XML のスキーマがある場合は、Visual Basic で Imports ステートメントを使用することで、スキーマをスコープに含めることができます。その後は、スキーマに基づいて IntelliSense を利用できます。

XML to Schema Tool for Visual Basic 2008 (go.microsoft.com/fwlink/?LinkId=102501 からダウンロードできます) を使用すると、既存の XML ファイルを基にして XSD スキーマを簡単に作成できます。XML ファイルまたは Web URL を指定するだけで、XSD を生成する単調な作業はツールが行ってくれます。

まとめ

Visual Basic のすべての機能は生産性のためにあります。また、ストレージ、データ転送、さらにはユーザー インターフェイスなどについて XML への依存を強めているビジネス アプリケーションでは、開発者が XML を簡単に使用できる機能が Visual Basic に組み込まれていることは重要です。Visual Basic 9.0 は機能が一段と向上し、充実した XML サポートや強力な新しいクエリ機能が提供されています。

LINQ が提供する簡単でありながら強力なクエリ構文を使用すると、データベースやメモリ内のコレクションに対するのと同じように簡単に、XML に対するクエリを作成できます。また、Visual Basic 9.0 を実際に使用すると、XML 名前空間、コメント、およびフラグメントの組み込みサポートなど、このトピックでは触れることのできなかった新しい XML 機能が他にも数多くあることがわかります。

ご意見やご質問は、こちらまで英語でお送りください。 instinct@microsoft.com.

Jonathan Aneja は、マイクロソフトの Visual Basic チームのプログラム マネージャを務めています。主として、LINQ などのコンパイラ機能、および Interop Forms Toolkit などの他のプロジェクトで働いています。マイクロソフトには約 2 年間在籍しており、最初は ISV Advisory Services にいました。


データ ポイント: エンティティ データ モデルをデザインする

データ ポイント
エンティティ データ モデルをデザインする
John Papa

コードのダウンロード : : DataPoints2008_02.exe (174KB)
コンテンツ
EDM について理解する
ウィザードを使用して EDM を構築する
エンティティ モデルのストアド プロシージャ
EDM に関するウィンドウ
派生エンティティ
まとめ

Entity Framework は、現在開発が進められている、ADO.NET 用の魅力的な新しいテクノロジです。開発者は、Entity Framework を使用すると、物理モデルではなく論理モデルを使用してデータを確認できるため、柔軟性が向上します。Entity Framework (2008 年の前半に提供が開始される予定です) の概要については、2007 年 7 月号の「データ ポイント」で詳しく説明しました。Entity Framework についてまだよく知らない場合は、この概要が記載された記事 (msdn.microsoft.com/msdnmag/issues/07/07/DataPoints) を参照してください。

Entity Framework の中核となるのが、エンティティ データ モデル (EDM) です。EDM は、開発者がコードを使用して操作する、エンティティ型、リレーションシップ、およびコンテナを定義します。Entity Framework は、これらの要素をリレーショナル データベースによって公開されるストレージ スキーマにマップします。EDM は、アプリケーションの概念モデルを定義する XML を介して Entity Framework に公開されます。概念モデルは、単独で定義される場合もあれば、実際のストレージ スキーマを定義する XML や、モデルとスキーマの間のマッピングを定義する XML と共に定義される場合もあります。この XML を手動で編集することもできます (また、その必要が生じることもあります) が、エンティティ データ モデル用の新しいビジュアル デザイナ ツールを使用してエンティティ モデルとマッピングを作成および変更する方がはるかに簡単です。

今月のコラムでは、新しいビジュアル EDM デザイナ ツールを使用してエンティティ モデルをデザインする方法について説明します。また、モデルとマッピングの両方が定義された、基になる XML を変更する方法についても説明します。まず、Entity Framework 内で相互に連携するもの (LINQ を含む) について説明します。次に、EDM が役立つ状況について説明します。さらに、ビジュアル デザイナ ツールを使用してエンティティ モデルと関連するマッピングを作成する方法を示します。最後に、モデルとマッピングを変更および確認する際に役立ついくつかのウィンドウを紹介します。

このコラム全体をとおして、EDM のさまざまな構成要素 (EntityType、Association など) の役割についても説明します。このコラムの例では、基本的なエンティティの作成方法を示します。この記事のすべての例では、Visual Studio® 2008、および対応する Entity Framework Beta 3 を使用しています (この 2 つは個別にインストールする必要があります)。使用するデータベースは、Beta 3 のサンプルに付属している、内容が変更された Northwind データベースです。

EDM について理解する

エンティティ モデルの作成および管理方法について説明する前に、まず EDM とは何か、および EDM が Entity Framework のその他の要素とどのように連携するかについて説明します。Entity Framework は、EDM の仕様と関連するマッピング、EDM を操作する API、エンティティ モデルとマッピングの定義と管理に役立つツールなど、多くの要素で構成されています。エンティティ モデルをデザインしたら、EntityClient プロバイダや Object Services (LINQ to Entities を含む) などのさまざまな API を使用して、そのモデルに基づいたコードを記述することができます。

EntityClient データ プロバイダのモデルは従来の ADO.NET オブジェクトに類似しており、EntityConnection オブジェクトと EntityCommand オブジェクトを使用して DbDataReader を返します。EntityClient プロバイダ用のコマンドは、T-SQL に類似した Entity SQL を使用して記述されますが、データベース オブジェクトを操作するのではなく、エンティティ モデルで定義されたエンティティ、および Object Services を使用して具体化されたオブジェクトを操作します。Entity SQL または LINQ to Entities を使用すると、Object Services を使用して EDM を操作することができます。Object Services を使用すると、概念モデルから生成したクラスを利用できます。このクラスには、厳密に型指定されたオブジェクトや永続化などの機能があります (図 1 参照)。

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

これらのデータ アクセス手法を使用すると、リレーショナル データベースなどの物理ストアのオブジェクトではなく、エンティティ モデル内で定義された概念エンティティを操作することができます。このデータ モデルと関連するマッピングを作成するには、ビジュアル デザイナ ツールを使用するか、これらを定義する XML を手動で編集します。Entity Framework (図 2 参照) は、アプリケーションとそのデータベースを結び付ける役割を果たします。EDM を使用してビジネス エンティティを概念エンティティ モデルとして表現した後、Entity Framework を使用して、このモデルをマッピングの仕様に基づいて、データベースのテーブル、ビュー、関数、およびプロシージャの物理ストレージに変換します。

図 2 アプリケーションをデータベースに結び付ける Entity Framework
図 2 アプリケーションをデータベースに結び付ける Entity Framework(画像を縮小するには、ここをクリックします)
図 2 アプリケーションをデータベースに結び付ける Entity Framework
図 2 アプリケーションをデータベースに結び付ける Entity Framework (画像を拡大するには、ここをクリックします)

アプリケーションのエンティティ モデルは、概念スキーマ定義言語 (CSDL: Conceptual Schema Definition Language) を使用して記述されます。CSDL は XML 形式であり、開発者が LINQ to Entities などの API を使用して操作するエンティティと、それらのエンティティ間の関連付けを定義します。Entity Framework は、リレーショナル データベースのストレージ スキーマを定義する XML 形式のストア スキーマ定義言語 (SSDL: Store Schema Definition Language) も使用します。また、CSDL のエンティティと SSDL で記述されたストレージ スキーマとの間のマッピングを解釈するために、マッピング スキーマ言語 (MSL : Mapping Schema Language) も使用します。

CSDL は開発者が最も大きな影響を与える部分です。これは、開発者が最も頻繁に操作するエンティティを定義しているためです。図 2 は、データベース内の 1 つのテーブルに直接マップできるエンティティもあれば、複数のテーブルにマップできるエンティティもあることを示しています。このエンティティに関する決定は、開発チームがビジネス モデルに基づいて行います。多くの場合、ビジネス モデルは、データベース内の複数の物理テーブルに存在する 1 つのエンティティに影響を与えます。図 2 は、エンティティをデータベースのビューにマップできることや、ストアド プロシージャを呼び出すメソッドをエンティティが取得できることも示しています。また、エンティティは、概念モデル内の継承を使用して他のエンティティから派生させることもできます。これらは、EDM デザイナ ツールを使用してエンティティ モデルをデザインする方法のうち、ほんの一部にすぎません。

ウィザードを使用して EDM を構築する

エンティティ モデルを作成するには、まず新しい ADO.NET エンティティ データ モデル ファイルをプロジェクトに追加します (図 3 参照)。このファイルを追加すると、エンティティ データ モデル ウィザードで、データベースからモデルを生成するか、空のモデルを使用することを求められます。データベースにアクセスできる場合は、既存のデータベース テーブルからモデルを生成するとよいでしょう。一部の開発方法論 (ドメイン主導型のデザインなど) では、データベースをデザインする前にエンティティ ドメイン モデルをデザインすることを推奨しています。このような方法を使用する場合は、空のモデルを作成してから EDM ビジュアル デザイナを使用してエンティティを作成するとよいでしょう。この記事のサンプルでは、Northwind データベースを基にしてエンティティ モデルを構築しました。

図 3 プロジェクトへの EDM ファイルの追加
図 3 プロジェクトへの EDM ファイルの追加(画像を縮小するには、ここをクリックします)
図 3 プロジェクトへの EDM ファイルの追加
図 3 プロジェクトへの EDM ファイルの追加 (画像を拡大するには、ここをクリックします)

ウィザードの次の画面では、データベースへの接続に関する情報を入力します。続いて、モデルに含めるデータベース オブジェクトを選択します。図 4 は、Northwind データベースのテーブルとストアド プロシージャのうち、ダイアグラムの作成に役立つもの以外はすべて選択したことを示しています。最初にテーブルがエンティティに直接マップされます。ストアド プロシージャは、生成されたコンテナのメソッドにマップすることができます。

図 4 データベース オブジェクトの選択
図 4 データベース オブジェクトの選択

モデルに含めるデータベース オブジェクトを指定すると、モデルとマッピングを定義する .edmx ファイルが生成されます。また、Entity Framework によって使用される、プロジェクトへの適切な参照が追加されます。.edmx ファイルは、4 つの主要なセクションから構成される XML ファイルです。これらのセクションには、デザイナ内での概念モデルの視覚的なレイアウト、概念モデルの CSDL、マッピング層の MSL、および物理モデルの SSDL に関する情報がそれぞれ記述されています。これらの情報がすべて 1 つのファイルに含まれています。

.edmx ファイルに記述されているデザイナ関連の情報は、Visual Studio がデザイナ内でのエンティティ モデルのレイアウトを決定するために使用されます。この情報はデザイン時にのみ使用されます。CSDL、MSL、および SSDL は、EDM を表すクラスを生成するために、コンパイル時に使用されます。

エンティティ モデルのストアド プロシージャ

作成したエンティティ モデルにストアド プロシージャを追加することを選択しましたが、ストアド プロシージャは SSDL の定義のみに追加されていました。ストアド プロシージャはデータベース内のさまざまなテーブルやその他のオブジェクトを操作する場合があるため、Entity Framework はストアド プロシージャを CSDL 内の特定の 1 つのエンティティに自動的にマップしません。ストアド プロシージャへのマップを行うメソッドを作成する必要がある場合は、.edmx ファイル内の XML を編集します。

特定の型のエンティティを返すメソッドを作成するために、まずストアド プロシージャを選択します。この例では、GetTenMostExpensiveProducts というメソッドを追加します。このメソッドは、最も高価な 10 個の製品を取得するストアド プロシージャを実行し、結果を Product オブジェクトとして返します。SSDL には既にこのストアド プロシージャが Function 要素として記述されているため、次は CSDL にこのメソッドを追加します。これを行うには、次のように、FunctionImport 要素を EntityContainer 要素の子として追加します。

<FunctionImport Name="GetTenMostExpensiveProducts"
  EntitySet="Products"
  ReturnType="Collection(Self.Products)">
</FunctionImport>

Name 属性は、エンティティ コンテナ上のメソッドの名前を示しています。また、EntitySet 属性は EntitySet を示し、ReturnType 属性は返される EntityType (この例の場合は EntityType のコレクション) を示しています。

この例の Self への参照は、現在の名前空間 (NWModel) を別名で参照しているため、NWModel と Self のどちらを指定してもかまいません。

メソッドにパラメータを指定する必要がある場合は、<Parameter> タグを含めてパラメータを追加することができます。たとえば、あるストアド プロシージャが CategoryId パラメータを使用し、メソッドにもこのパラメータを含める必要がある場合は、FunctionImport 要素の内側に次の XML 要素を含めることによって、メソッドにパラメータを追加することができます。

<Parameter Name="CategoryId" Type="Int32" Mode="in"/>

ただし、今回の例ではパラメータを指定する必要がないため、この手順は実行しませんでした。

この時点で、CSDL は、メソッド、メソッドから返されるエンティティの型、および返されるエンティティが属する EntitySet を定義し、SSDL はストアド プロシージャを定義しています。次に、実行するストアド プロシージャを概念メソッドが認識できるように、CSDL を SSDL にマップする必要があります。このマッピングを行うには、MSL の EntityContainerMapping セクションに次の FunctionImportMapping を挿入します。

<FunctionImportMapping
  FunctionImportName="GetTenMostExpensiveProducts"
  FunctionName="NWModel.Store.Ten_Most_Expensive_Products"/>

ここでは、FunctionImportMapping 要素が FunctionName 属性を使用して、SSDL の Function 要素の完全修飾名を参照しています。FunctionImportName 属性は、CSDL の FunctionImport 要素の名前を示しています。

EDM に関するウィンドウ

EDM のデザインと構築が完了すると、さまざまなウィンドウを使用して、作成したエンティティ モデルの状態を確認できるようになります。[Class View] (クラス ビュー) ウィンドウ (図 5 参照) は新しい機能ではありませんが、開発に使用できるオブジェクトを確認する必要がある場合に役立ちます。たとえば、このウィンドウでは、新しく追加した GetTenMostExpensiveProducts メソッドが NWEntities EntityContainer クラスのメソッドとして表示されます。また、NWModel 名前空間に含まれるすべてのクラスが表示されます。これには、CSDL の各 EntityType を表すクラス、およびメインの NWEntities モデル用のクラスが含まれています。

図 5 クラス ビューに表示された EDM
図 5 クラス ビューに表示された EDM

エンティティ モデルと関連するマッピングを確認できる新しいウィンドウもいくつか提供されています。これらのウィンドウには、Entity Data Model Designer (エンティティ データ モデル デザイナ)、[Entity mapping Details] (エンティティのマッピングの詳細) ウィンドウ、[Entity Model Browser] (エンティティ モデル ブラウザ) ウィンドウなどがあります。

Entity Model Browser には、CSDL と SSDL の構成要素がすべて表示されます。CSDL の構成要素である EntityType、Association、EntitySet、AssociationsSet、FunctionImport や、SSDL のすべての構成要素が表示されます。

Entity Data Model Designer (図 6 参照) には、生成したモデルを含むレイアウトが表示されます。このデザイナを使用して、エンティティ モデルを表示および編集できます。このデザイナでは、概念モデルのすべての要素が視覚的なレイアウトで表示されます。図 6 に表示されている Product エンティティは、CSDL の Product EntityType を表しています。各 EntityType には、プロパティ (スカラ プロパティとナビゲーション プロパティ) のリストが格納されています。

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

ナビゲーション プロパティは、CSDL 内の関連付けられた要素間の移動に使用されます。ナビゲーション プロパティは EntityType クラスのパブリック プロパティになり、元のエンティティに関連付けられた 1 つまたは複数のエンティティを参照するために使用されます。たとえば、Product EntityType には Categories という名前のナビゲーション プロパティがあります。このプロパティは、特定の Product エンティティのインスタンスが属する Category エンティティを参照します。また、Category EntityType には Products という名前のナビゲーション プロパティがあります。このプロパティは、Category エンティティのインスタンスに関連付けられた Product エンティティを参照するために使用されます。

このデザイナを使用して、エンティティ型、関連付け、スカラ プロパティ、およびナビゲーション プロパティを追加、編集、または削除することができます。たとえば、すべての EntityType の名前を、開発者がエンティティに名前を付けるときによく使用している単数形に変更することができます。これを行うには、EntityType の名前をクリックし、その名前を編集するだけです。また、EntityType を選択し、[Properties] (プロパティ) ウィンドウで名前を変更することもできます。この例では、すべての EntityType の名前を単数形に変更しました。この変更は図 6 に反映されています。この名前の変更を行った後、ストアド プロシージャの呼び出しを NWEntities EntityContainer のメソッドとして追加するために変更した XML も変更する必要がありました。次に示すように、必要な作業は (Products ではなく) Product EntityType を参照するようにコードを更新する作業のみであるため、非常に簡単です。

<FunctionImport Name="GetTenMostExpensiveProducts"
  EntitySet="Products"
  ReturnType="Collection(Self.Product)">
</FunctionImport>

このことは重要です。データ モデルの変更を開始する前に、EntityType、EntityContainer、Association などの主な要素に名前を付けることをお勧めします。これにより、手動で行う変更の数が最小限に抑えられます。また、データ モデルに基づいて記述したコードが既にある場合は、そのコードに加える変更の数も最小限に抑えられます。問題が発生した場合は、[Error List] (エラー一覧) ウィンドウが強力な味方となります。このウィンドウには、XML 内の無効な参照を持つ要素が非常に高い確率で表示されます。

最後に、このデザイナでは概念モデルが表示されますが、[Entity Mapping Details] (エンティティのマッピングの詳細) ウィンドウでは、EDM とデータ ストアとの間のマッピング (MSL) を表示および編集する手段が提供されます。

派生エンティティ

エンティティ モデルのデザインに役立つツールは、モデルの変更にも役立ちます。オブジェクト指向プログラミングの鍵となるものの 1 つは、継承という概念です。EDM では、継承エンティティの作成と変更を XML で記述することも、EDM デザイナで視覚的に行うこともできます。

継承エンティティをデザインする作業の例を示すために、Product EntityType を基にして DiscontinuedProduct EntityType を作成します。Product EntityType には、Discontinued というブール型のスカラ プロパティがあります。このプロパティは、製品のインスタンスの型を判断する際に評価する条件として使用します。Entity Data Model Designer で、デザイナ内の任意の場所を右クリックし、ポップアップ メニューの [Add] (追加) を選択して、[Entity] (エンティティ) を選択します。次に、新しいエンティティ名として「DiscontinuedProduct」と入力し、基本エンティティとして Product を選択します (図 7 参照)。

図 7 継承エンティティの作成
図 7 継承エンティティの作成

次に、条件を使用して、各エンティティを識別するための要素を定義します。これを行うために、デザイナで Product EntityType を選択してから [Entity Mapping Details] (エンティティのマッピングの詳細) ウィンドウに移動します。続いて、列マッピング内の Discontinued プロパティを選択し、そのマッピングを削除します。これにより、Product EntityType と DiscontinuedProduct EntityType の両方から Discontinued プロパティが削除されます (条件を作成する場合、その条件に使用できる式は、プロパティとして使用されていない式に制限されます)。次に、[Entity Mapping Details] (エンティティのマッピングの詳細) ウィンドウの [Maps to Products] (Products へのマップ) セクションに移動し、Discontinued = 0 という条件を追加します。続いて、DiscontinuedProduct EntityType を選択し、Discontinued = 1 という条件を追加します。

この手法を使用すると、きわめて簡単に派生エンティティを作成できます。必要に応じて、基本クラスのプロパティ以外のプロパティを派生エンティティに追加することもできます。.NET コードで DiscontinuedProduct のインスタンスを作成して保存すると、Entity Framework は、このインスタンス上に存在する条件を基に、データベースの Discontinued 列の値を 0 に設定する必要があることを認識します。

条件が MSL の EntityTypeMapping セクションに作成され、行を取得するときに単純にフィルタが適用されます。データが保存されると、基になるデータベースの列に書き込む値が、条件と派生型に基づいて決定されます。

まとめ

Visual Studio 2008 のデザイン ツールと XML ファイルを使用すると、リレーショナル データベース スキーマを直接操作するコードを記述することなく、継承を使用し、ストアド プロシージャの呼び出しを行い、ビジネス モデルを作成するエンティティ データ モデルをデザインすることができます。堅固なエンティティ モデルを一度デザインすれば、Object Services などの API から非常に簡単にそのモデルを操作できるようになります。また、マッピングによって概念モデルをストレージ モデルから切り離すことができるため、データベースに変更を加えることなくデータ モデルを変更することができます。ここで説明した Entity Framework に関する概念の中に聞き慣れないものがあった場合は、私が以前に書いた Entity Framework の概要に関する記事 (msdn.microsoft.com/msdnmag/issues/07/07/DataPoints) を読んでいただくことを強くお勧めします。

ご意見やご質問は、John まで英語でお送りください。 mmdata@microsoft.com.

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


Cutting Edge: AJAX エクステンダを使用してコントロールをカスタマイズする、第 2 部

Cutting Edge
AJAX エクステンダを使用してコントロールをカスタマイズする、第 2 部
Dino Esposito

コードのダウンロード : : CuttingEdge2008_02.exe (434KB)
コンテンツ
マスク編集の必要性
MaskedEdit エクステンダ
マスク入力の検証
テキストボックスのオートコンプリート
オートコンプリート Web サービスの作成
AutoComplete エクステンダのスタイル設定
リストでの増分検索
フライング ショートカット メニュー
まとめ

先月、AJAX Control Extender を使用して、テキストボックスおよびボタンなどの ASP.NET 入力コントロールを強化する方法について説明しました。今月は、Microsoft® .NET Framework 3.5 および ASP.NET の最新バージョン (この記事の執筆時点では Beta 2) を使用して、マスク編集やオートコンプリートなどのより高度な機能について補足説明します。また、著者は ASP.NET AJAX Control Toolkit の使用可能な最新のビルドを使用しています。必要なソフトウェア ツールキットの入手方法については、先月の Cutting Edge コラムを参照してください (msdn.microsoft.com/msdnmag/issues/08/01/CuttingEdge を参照)。

マスク編集の必要性

HTML では、入力データを受け入れる唯一の方法は <input> タグを使用することです。ASP.NET では、入力タグは TextBox コントロールによって単にラップされています。このコントロールを使用する場合の 1 つの問題は、入力可能な内容が制限されないという点です。ただし、ほんの少しの JavaScript コードを使用することにより、不要なテキストをフィルタ処理することができます。これについては先月説明しました。 今月は、入力時に文字をフィルタ処理することが可能で、さらにカルチャ固有の形式で指定が可能なマスク編集について補足します。マスク編集は、フィルタ処理、検証、自動的な書式設定、およびローカライズに使用できます。また、日付、通貨、時刻、郵便番号、電話番号、米国の Social Security Number (社会保障番号)、VAT ID など、多くの現実的なデータ型に適用することができます。MaskedEdit エクステンダは、AJAX Control Toolkit の無料のコンポーネントであり、TextBox コントロールに追加すると、さまざまな一般的なシナリオでの入力を制御できます。

MaskedEdit エクステンダ

AJAX Control Toolkit では、MaskedEdit エクステンダは、MaskEditType の enum 型によって指定されるいくつかのデータ形式をサポートしています。

public enum MaskedEditType
{
    None,
    Date,
    Number,
    Time,
    DateTime
}

エクステンダを使用して、数字、日付、時刻および日付/時刻を入力できます。エクステンダは、特定のカルチャ設定に基づいて出力を決定します。次のコードに、日付を受け入れるテキストボックスで MaskedEdit エクステンダを使用する一般的な方法を示します。

<asp:TextBox runat="server" ID="TextBox1" />
<act:MaskedEditExtender ID="MaskedEditExtender1" runat="server"
    TargetControlID="TextBox1"
    Mask="99/99/9999"
    MaskType="Date" />

このエクステンダには、図 A に示すさまざまなプロパティがあります。入力マスクの定義には、主に次の 2 つのプロパティを使用します。それらは、Mask および MaskType です。Mask には "" という既定値があり、エクステンダで受け入れることのできる文字のマスクを指定します。MaskType には "" という既定値があり、MaskedEditType 列挙が定義する任意の値を使用してマスクの種類を示します。

Figure A MaskedEdit エクステンダのプロパティ (Close Figure A)

プロパティ | 既定値 | 説明
AcceptAMPM | False | AM/PM 記号を使用する必要があるかどうかを示すブール型プロパティ。
AcceptNegative | None | マイナス記号 (-) を使用できるかどうかを示します。利用可能な値は MaskedEditShowSymbol 列挙の None、Left、Right から取得されます。
AutoComplete | True | ユーザーが指定しない空のマスク文字を自動的に入力する必要があるかどうかを示すブール型プロパティ。
AutoCompleteValue | "" | オートコンプリートが有効である場合に使用する既定の文字を指定します。
Century | 1900 | 日付マスクが 2 桁のみの年を使用している場合に使用する世紀を指定します。
ClearMaskOnLostFocus | True | テキストボックスから入力フォーカスが外れたときにマスクを削除するかどうかを指定するブール型プロパティ。
ClearTextOnInvalid | False | 無効なテキストが入力されたときにテキストボックスをクリアするかどうかを指定するブール型プロパティ。
ClipboardEnabled | True | クリップボードを使用したコピー/貼り付けを許可するかどうかを示すブール型プロパティ。
ClipboardText | Your browser security settings don't permit the automatic execution of paste operations (ブラウザのセキュリティ設定では貼り付け操作の自動実行が許可されていません) | クリップボードによる貼り付けを実行するときに使用するプロンプト テキストを示します。
CultureName | "" | 使用するカルチャ名を取得および設定します。
DisplayMoney | None | 通貨記号を表示するかどうかを指定します。利用可能な値は MaskedEditShowSymbol 列挙の None、Left、Right から取得されます。
ErrorTooltipCssClass | "" | ツールチップ メッセージの CSS クラスを取得および設定します。
ErrorTooltipEnabled | False | マウスが無効なコンテンツを含むテキストボックスに重ねられたときにツールチップ メッセージを表示するかどうかを指定するブール型プロパティ。
Filtered | "" | "C" プレースホルダが指定された場合、マスクの種類に対して有効な文字のリストを取得および設定します。
InputDirection | LeftToRight | テキストの入力方向を指定します。利用可能な値は MaskedEditInputDirection 列挙の LeftToRight、RightToLeft から取得されます。
Mask | "" | エクステンダで受け入れることのできる文字のマスクを指定します。
MaskType | "" | MaskedEditType 列挙で定義する任意の値を使用するマスクの種類を示します。
MessageValidatorTip | True | テキストボックスへの入力時にヘルプ メッセージを表示するかどうかを指定するブール型プロパティ。
OnBlurCssNegative | "" | テキストボックスから入力フォーカスが外れ、負の値を含むときに使用する CSS クラスを取得および設定します。
OnFocusCssClass | "" | テキストボックスが入力フォーカスを取得したときに使用する CSS クラスを取得および設定します。
OnFocusCssNegative | "" | テキストボックスが入力フォーカスを取得し、負の値を含むときに使用する CSS クラスを取得および設定します。
OnInvalidCssClass | "" | テキストが無効な場合に使用する CSS クラスを取得および設定します。
PromptCharacter | _ | 指定されていないマスク文字に対して使用されるプロンプト文字を取得および設定します。
UserDateFormat | None | 特定の日付形式を指定します。利用可能な値は、MaskedEditUserDateFormat 列挙で定義されます。
UserTimeFormat | None | 特定の時刻形式を指定します。利用可能な値は、MaskedEditUserTimeFormat 列挙で定義されます。

MaskType プロパティは、ターゲット コントロールが特定のデータ型を受け入れることをエクステンダに通知します。Mask プロパティ (文字列型) は、テキストボックスに対して有効な入力を表す一連の文字を示します。たとえば、"12/6/07" および "12-09-2007" は両方とも有効な日付ですが、異なる入力マスクを使用しています。

マスクを作成するには、定義済みの記号をプレースホルダとして使用します。図 1 に、サポートされている記号の一覧を示します。たとえば、"999,999.99" マスクを使用すると、コードで小数点記号と 1 つの千の位の区切り記号を持つ数値が受け入れられます。図 2 に、マスク エディタで拡張されたテキストボックスに表される最終的なユーザー インターフェイスを示します。通貨記号の外観は DisplayMoney プロパティによって制御され、ユーザーが入力する必要のある各文字はプロンプトで表されます。既定のプロンプトは、アンダースコアですが、PromptCharacter プロパティを使用して変更することができます。

Figure 1 定義済みの入力マスク用のプレースホルダ (Close Figure 1)

記号 | 説明
9 | 英数字。
L | 文字。
$ | 文字または空白。
C | カスタムの大文字と小文字を区別する文字。Filtered プロパティで定義される。
A | 文字またはカスタムの文字。Filtered プロパティで定義される。
N | 数値またはカスタムの文字。Filtered プロパティで定義される。
? | 任意の文字。
/ | 現在のカルチャに基づく日付の区切り記号。
: | 現在のカルチャに基づく時間の区切り記号。
. | 現在のカルチャに基づく小数点記号。
, | 現在のカルチャに基づく千の位の区切り記号。
\ | エスケープ文字。
{ | マスクの繰り返しの最初の区切り記号。
} | マスクの繰り返しの最後の区切り記号。
図 2 MaskedEdit エクステンダの操作時
図 2 MaskedEdit エクステンダの操作時(画像を縮小するには、ここをクリックします)
図 2 MaskedEdit エクステンダの操作時
図 2 MaskedEdit エクステンダの操作時 (画像を拡大するには、ここをクリックします)

また、日付の場合は、次に示すように、MaskedEditUserDateFormat 列挙にある定義済みの形式の他に、AcceptAMPM、Century、あるいはカスタム ユーザー形式などの追加のプロパティを使用することもできます。

public enum MaskedEditUserDateFormat
{
    None,
    DayMonthYear,
    DayYearMonth,
    MonthDayYear,
    MonthYearDay,
    YearDayMonth,
    YearMonthDay
}

MaskedEdit エクステンダによって適用される書式設定に影響する設定の多くが、現在のカルチャから継承されています。CultureName プロパティは適用するカルチャを示します。この設定により、@Page ディレクティブの UICulture 属性を使用してページに定義されているカルチャ設定がオーバーライドされることに注意してください。

マスク入力の検証

マスク エクステンダにより動的な書式設定機能が提供されますが、追加のコンポーネントであるマスク バリデータでは、次のように、入力されたテキストを解析して、期待されている型に戻すことができます。

  <act:MaskedEditValidator
      ID="MaskedEditValidator1" runat="server"
      ControlExtender="MaskedEditExtender1"
      ControlToValidate="TextBox2"
      IsValidEmpty="False"
      EmptyValueMessage="Number is required "
      InvalidValueMessage="Number is invalid" />

MaskedEditValidator コントロールは、カスタム バリデータであり、オプションで MaskedEdit エクステンダに添付することにより、編集されたテキストボックスの内容を慎重に検証することができます。このバリデータにより、テキストが確実にマスクに一致します。他の ASP.NET バリデータ コントロールと同様に、サーバーおよびクライアントの検証を実行して、検証グループに関連付けることができます。図 B に、バリデータによって公開されるプロパティを示します。マスクされたテキストボックスの Text プロパティでは、書式設定されたテキストが返されます。日付の場合は、このプロパティは "02/04/2007" などを返し、数値入力フィールドの場合は、"1,200.00" などのテキストを返します。通貨記号は、ページ内で表示されている場合でも、Text プロパティに含まれません。

Figure B MaskedEditValidator のプロパティ (Close Figure B)

プロパティ | 説明
AcceptAMPM | 時刻の値で AM/PM を受け入れるかどうかを指定します。
ConTRolToValidate | 検証するテキストボックスの ID を指定します。
ConTRolExtender | テキストボックスに添付する MaskedEditExtender コントロールの ID を指定します。
ClientValidationFunction | カスタム検証に使用するクライアントの JavaScript 関数の名前を取得および設定します。
EmptyValueBlurredText | テキストボックスに入力フォーカスがなく、空である場合に表示するメッセージを取得および設定します。
EmptyValueMessage | テキストボックスに入力フォーカスがあり、空である場合に表示するメッセージを取得および設定します。
InitialValue | テキストボックスの初期値を取得および設定します。
InvalidValueMessage | テキストボックスに入力フォーカスがあり、コンテンツが無効である場合に表示するメッセージを取得および設定します。
InvalidValueBlurredMessage | テキストボックスに入力フォーカスがなく、コンテンツが無効である場合に表示するメッセージを取得および設定します。
IsValidEmpty | テキストボックスを空のままにできるかどうかを指定します。
MaximumValue | 入力の最大値を取得および設定します。
MaximumValueBlurredMessage | 最大値を超えた場合、テキストボックスにフォーカスがないときに、表示するメッセージを取得および設定します。
MaximumValueMessage | 最大値を超えた場合、テキストボックスにフォーカスがあるときに、表示するメッセージを取得および設定します。
MinimumValue | 入力の最小値を取得および設定します。
MinimumValueBlurredText | 最小値を超えた場合、テキストボックスにフォーカスがないときに、表示するメッセージを取得および設定します。
MinimumValueMessage | 最小値を超えた場合、テキストボックスにフォーカスがあるときに、表示するメッセージを取得および設定します。
ValidationExpression | 入力の検証に使用する正規表現を取得および設定します。
TooltipMessage | テキストボックスに入力フォーカスがある場合に表示するメッセージを取得および設定します。

では、Text によって返された値を、日付や小数点数などの論理的なデータ型に解析するにはどうしたらよいのでしょうか。DateTime 型や Decimal 型では、静的な Parse メソッドを使用できます。ただし、使用するカルチャに注意する必要があります。たとえば、"02/04/2007" は 2 月 4 日 (米国のカルチャ)、または 4 月 2 日 (ヨーロッパのカルチャ) のいずれかの可能性があります。入力ページとサーバー ページで使用されているカルチャが一致している保証がないのは事実です。ヨーロッパのカルチャに基づいて日付を入力し、そのデータが米国のカルチャに基づく日付として処理されるおそれがあります。さらに悪いことに、たとえば、イタリアの小数点および千の位の区切り記号を使用して数値テキストボックスに入力した 1200 という値で、例外がスローされる可能性があります。これは、Decimal 型のパーサーが既定で米国のカルチャに設定されているためです。このような問題を回避する方法を検討してみましょう。

思い出すべき重要な事実は、CultureName プロパティが明示的に設定されていない限り、エクステンダは、既定で en-US (英語、米国) カルチャに設定されているということです。サーバーでは、システムは既定で、Page クラスの UICulture プロパティの値に設定されます。

codebehind クラスでは、まず、ユーザー インターフェイスに使用するカルチャを反映する CultureInfo オブジェクトを取得します。次のように進めることができます。

string culture = "en-us";
if (!String.IsNullOrEmpty(MaskedEditExtender1.CultureName))
    culture = MaskedEditExtender1.CultureName;
CultureInfo info = new CultureInfo(culture);

次に、選択したカルチャに基づいて形式プロバイダを指定する Parse メソッドを呼び出します。

NumberFormatInfo numberInfo = info.NumberFormat;
DateTimeFormatInfo dateInfo = info.DateTimeFormat;
DateTime when = DateTime.Parse(TextBox1.Text, dateInfo);
decimal amount = Decimal.Parse(TextBox2.Text, numberInfo);

図 3 に、入力時に異なるカルチャを使用した場合の同一のページの動作を示します。

図 3 異なるカルチャを使用した場合の .NET 型へのデータの解析
図 3 異なるカルチャを使用した場合の .NET 型へのデータの解析(画像を縮小するには、ここをクリックします)
図 3 異なるカルチャを使用した場合の .NET 型へのデータの解析
図 3 異なるカルチャを使用した場合の .NET 型へのデータの解析 (画像を拡大するには、ここをクリックします)

テキストボックスのオートコンプリート

オートコンプリートは、間違いなくユーザーになじみのある機能です。入力された最初の数文字からユーザーが入力する単語を予測します。Internet Explorer は、オートコンプリートを挿入するために、アドレス バーやフォーム フィールドに入力されたすべての内容を記録しています。

この機能は、全体的にブラウザ ベースであり、オートコンプリート属性をオフに設定することで、<input> タグや <form> タグのオンとオフを切り替えることができます。オートコンプリート属性は、標準の HTML 属性ではありませんが、今日では、ほとんどすべてのブラウザがサポートしています。

AJAX Control Toolbox の AutoComplete エクステンダはテキストボックス コントロールに同じ動作を提供しますが、ユーザーに対して入力候補を提供するすべてのロジックは開発者の責務となります。エクステンダでは、ドロップダウン リスト形式のパネルが作成され、テキストボックスの下に配置されます。好みに合わせて、パネルのスタイルとアニメーション処理を設定できます。次のコードは、オートコンプリート エクステンダをテキストボックスに関連付けます。

<act:AutoCompleteExtender runat="server" ID="AutoComplete1"
    TargetControlID="TextBox1"
    EnableCaching="true"
    MinimumPrefixLength="1"
    ServicePath="Suggestions.asmx"
    ServiceMethod="GetSuggestions" />

エクステンダはリストを作成する単語を提供する Web サービスにバインドされています。MinimumPrefixLength プロパティは、Web サービスへの呼び出しを配置する状況についてコントロールに指示します。すでに入力済みのテキストは、指定された Web サービス メソッド用の入力として使用されます。応答を使用して、ドロップダウン リストが作成されます。キャッシュもオンにされる場合があります。この方法では、同じプレフィックスが複数回入力された場合、Web サービスへの呼び出しは 1 回行われます。さらに、Web サービスがデータを取得する方法に応じて、サーバー上でのキャッシングを有効にしたり、データベースや別のリモート データ ストアへの追加のラウンドトリップを保存することができます。

図 C に、AutoComplete エクステンダがサポートするプロパティの完全な一覧を示します。他のエクステンダでは、この一覧のプロパティの他にもプロパティを備えていることに注意してください。拡張コントロールの ID を取得および設定する TargetControlID と、既定で true に設定され、エクステンダの機能のオンとオフをプログラムで切り替えることのできる Enabled があります。また、拡張動作を提供するクライアント側の JavaScript オブジェクトの名前を設定する BehaviorID プロパティもあります。最後に、ClientState および EnableClientState という、さらに 2 つの興味深いプロパティがあります。ClientState はエクステンダのクライアント状態を保持する文字列プロパティです。この状態は、ClientStateFieldID プロパティを通じて名前を設定できる非表示のフィールドを使用して保存されます。EnableClientState は、クライアント状態が有効であるかどうかを制御するブール型プロパティです。

Figure C AutoComplete エクステンダのプロパティ (Close Figure C)

プロパティ | 説明
Animations | フライアウトが表示および非表示されるときに再生されるアニメーションを設定します。
CompletionInterval | バインドされている Web サービスを使用して、エクステンダが入力候補を取得するまでのミリ秒単位の時間を取得および設定します。
CompletionListCssClass | 入力候補リストのフライアウトのスタイル設定に使用する CSS クラスを指定します。
CompletionListHighlightedItemCssClass | 入力候補リストのフライアウトで強調表示される項目のスタイル設定に使用する CSS クラスを指定します。
CompletionListItemCssClass | 入力候補リストのフライアウトの項目のスタイル設定に使用する CSS クラスを指定します。
CompletionSetCount | バインドされている Web サービスから取得する入力候補の数を取得および設定します。既定は 10 です。
ContextKey | STRing プロパティは、バインドされている Web サービスに渡すページまたはユーザー固有の情報を指定します。
DelimiterCharacters | エクステンダがテキストボックスの内容をトークン作成に使用する 1 つ以上の文字を指定します。その後、Web サービスでは、これらのトークンの最後のトークンを使用して、入力候補を提供します。既定では、設定されていません。
EnableCaching | クライアント側キャッシュが有効であるかどうかを示すブール型プロパティ。既定では、True です。
FirstRowSelected | リストの最初のオプションが自動選択されるようにするかどうかを指定するブール型プロパティ。既定では、False です。
MinimumPrefixLength | バインドされている Web サービスをトリガするテキスト ボックス バッファの最小の文字数を取得および設定します。既定は 3 です。
ServiceMethod | バインドされている Web サービスで呼び出すメソッド名を取得および設定します。
ServicePath | バインドされている Web サービスの URL を取得および設定します。
UseContextKey | 指定されている場合は、ContextKey プロパティの値を使用するべきかどうかを指定するブール型プロパティ。既定では、False です。

オートコンプリート Web サービスの作成

AutoComplete エクステンダで機能する Web サービスは、ASP.NET AJAX スクリプト サービスです。このサービスは、通常の ASP.NET Web サービスとほぼ同じですが、クラスが ScriptService 属性で修飾されている必要があるという点が異なります。この属性を含まない Web サービスを使用すると、関連付けられている ASMX エンドポイントに対して要求を行うたびに HTTP 500 エラーが発生します。例外インターフェイスはエンド ユーザーには表示されません (エクステンダは常に適切にレベルを下げることができます) が、入力候補を含むフライアウトも表示されません。

[ScriptService]
public class SuggestionService : WebService
{
   ...
}

スクリプト サービスには WebService 属性は必要ありませんが、ネームスペース情報を提供するために、依然として、この属性を使用する場合があることに注意してください。

WebMethod 属性でフラグが付けられているスクリプト サービス クラスの任意のパブリック メソッドの名前を、エクステンダの ServiceMethod プロパティに割り当てることができます。入力候補を提供するメソッドは、次のシグネチャを含んでいる必要があります。

[WebMethod]
public string[] GetSuggestions(string prefixText, int count)

最初の引数は、入力候補を生成するためのプレフィックス テキストです。この引数は、テキストボックスの現在の内容と一致し、長さは MinimumPrefixLength プロパティの値を超えます。カウント パラメータは、提供する入力候補の数を指定します。カウント パラメータの値は、CompletionSetCount プロパティの値から取得されます。

バインドされている Web サービスに渡す任意のぺージまたはユーザー固有情報を指定する、文字列型プロパティであるコンテキスト キーを利用する場合は、使用する予定のすべての Web サービスのメソッドに対してオーバーライドを提供する必要があります。シグネチャを次に示します。

public string[]GetSuggestions(
    string prefixText, int count, string contextKey)
{
   ...
}

戻り値は、必ず、文字列の配列としてパックされます。ScriptService 属性のため、サーバーとクライアント間の通信は JavaScript Object Notation (JSON) 文字列を使用して実行されます。

Web サービス メソッドでサポートされている任意の属性を活用して、サービスのパフォーマンスを強化することができます。たとえば、WebMethod 属性の CacheDuration 属性では、サービスがメソッド呼び出しの応答を指定された期間キャッシュするよう強制します。同様に、メソッドのロジックによって厳密に要求されている場合、セッション状態を有効にすることができます。

図 4 に、Northwind データベースから取得される顧客名に対する入力候補を提供するサンプルのサービスを示します。パフォーマンスの観点からは、数文字を入力するごとに Web サービスを呼び出すことは、適切ではないように思えるかもしれません。ただし、皆さんが考える以上にオートコンプリートが妥当と見なされる要因がいくつかあります。AutoComplete エクステンダのクライアント側のキャッシュはローカル メモリに基づいており、非表示フィールドを使用して保存されることはありません。特定のプレフィックスに関連付けられている入力候補リストは、プレフィックスをキーとして使用して、内部配列に格納されます。その後、同じプレフィックスがテキストボックス バッファ内で分離されるたびに、このリストが取得されます。その結果、同じプレフィックスが繰り返し入力されても、バックエンド サービスに対して新規の要求は発行されません。

Figure 4 サンプルの入力候補サービス (Close Figure 4)
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Data;
using System.Data.SqlClient;
using System.Web.Script.Services;

[WebService(Namespace = "MsdnMag.Articles")]
[ScriptService]
public class SuggestionService : System.Web.Services.WebService
{
    [WebMethod]
    public string[] GetSuggestions(string prefixText, int count)
    {
        DataView data = GetData();
        data = FilterData(data, prefixText);

        int totalCount = data.Count;
        if (data.Count > count)
            totalCount = count;
        string[] suggestions = new string[totalCount];

        int i = 0;
        foreach (DataRowView row in data)
        {
            suggestions[i++] = (string) row["companyname"];
            if (i >= count)
                break;
        }

        return suggestions;
    }

    private DataView GetData()
    {
        DataView view =
            (DataView)HttpContext.Current.Cache["Suggestions"];
        if (view == null)
        {
            SqlDataAdapter adapter = new SqlDataAdapter(
                "SELECT * FROM customers", "...");
            DataTable table = new DataTable();
            adapter.Fill(table);
            view = table.DefaultView;

            HttpContext.Current.Cache["Suggestions"] = view;
        }

        return view;
    }

    private DataView FilterData(DataView view, string prefix)
    {
        view.RowFilter = String.Format("companyname LIKE '{0}%'",
            prefix);
        return view;
    }
}

クライアント側のキャッシュは、サーバー側のキャッシュより明らかに効果的であり、データベースへの数回のラウンドトリップが省略されますが、Web サーバーへのラウンドトリップは省略されません。図 4 に示すコードで、サービスはすべての顧客をデータベース内に読み込み、ASP.NET キャッシュ内にそのリストを保存します。その後、そこから顧客のリストを取得し、プレフィックスと一致する名前をフィルタ処理します。

既に説明したとおり、入力候補を提供するために使用する Web サービスは ASP.NET AJAX スクリプト サービスである必要があり、JSON を介してクライアントと通信します。そのサービスを、SOAP クライアントに対しても公的に公開するかどうかを決定する必要があります。ASP.NET AJAX サービスは、既定で、JSON と SOAP の 2 つの機能性を備えています。次のように、ホスト アプリケーションの web.config ファイルに次の行を追加して、SOAP クライアントをオフにすることができます。

<system.web>
  <webServices>
    <protocols>
      <clear />
    </protocols>
  </webServices>
</system.web>

ASP.NET 3.5 では、サービス (SVC) エンドポイントを使用して、AutoComplete エクステンダを Windows® Communication Foundation (WCF) サービスにバインドすることができます。エクステンダは、次の JavaScript コードを使用して呼び出しを配置します。

Sys.Net.WebServiceProxy.invoke(
   this.get_servicePath(),
   this.get_serviceMethod(),
   false,
   params,
   Function.createDelegate(this, this._onMethodComplete),
   Function.createDelegate(this, this._onMethodFailed),
   text);

Microsoft AJAX クライアント ライブラリの WebServiceProxy クラスで重要なのは、コンテンツ タイプの要求ヘッダーが "application/json; charset=utf-8" に設定された状態で、指定されたエンドポイントが要求を正常に処理できるということです。入力候補を提供する WCF サービスを作成するには、次のようなクラスが必要です。

[ServiceContract(Namespace = "MsdnMag.Services")]
[AspNetCompatibilityRequirements(
     RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MySuggestionService
{
    [OperationContract]
    public string[] GetSuggestions(string prefixText, int count)
    {
      ...
    }
}

また、次に示すような SVC エンドポイントが必要です。

<%@ ServiceHost Language="C#"
                Service="MySuggestionService"
                CodeBehind="~/App_Code/WcfSuggestions.cs" %>

サービスを登録するには、webHttpBinding バインディング モデルを使用して、web.config ファイルの serviceModel セクションに明示的なエンドポイントを追加することができます。または、@ServiceHost ディレクティブで Factory 属性を指定し、次のクラスを参照するようにできます。

System.ServiceModel.Activation.WebScriptServiceHostFactory

AutoComplete エクステンダのスタイル設定

AutoComplete エクステンダでは、コントロールの部分のスタイル設定に使用する CSS クラスを指定するためのプロパティをサポートしています。特に、境界線や背景色を含む全体的なフライアウト領域をスタイル設定できます。また、すべての項目や選択した項目に異なる設定を使用することもできます。代替項目に対しては、定義済みのスタイルは提供されていません。既定では、ドロップダウンの入力候補リストの最初の要素は選択されていません。ただし、FirstRowSelected プロパティを使用すると、強制的に最初の項目が自動的に選択されます。

入力候補リストは、ミリ秒単位で指定した時間が経過すると表示されます。これは、CompletionInterval プロパティを使用して制御され、既定では 1 秒に設定されています。この時間が経過すると、エクステンダは、入力候補リストを取得するサービス呼び出しを配置します。

Animations プロパティを使用すると、リストを表示する手順にアクションを追加することができます。OnShow および OnHide の 2 つのイベントのアニメーションを定義できます。アニメーションは、アニメーション エクステンダおよびその関連フレームワークを使用して実行されます (図 5 の例を参照)。

Figure 5 Animations プロパティの使用 (Close Figure 5)
<Animations>
    <OnShow>
        <Sequence>
            <OpacityAction Opacity="0" />
            <HideAction Visible="true" />
            <Parallel Duration=".4">
                <FadeIn />
            </Parallel>
        </Sequence>
    </OnShow>
    <OnHide>
        <Parallel Duration=".4">
            <FadeOut />
        </Parallel>
    </OnHide>
</Animations>

アニメーション スクリプトは、補完リストをフェード インまたはフェード アウトします。OpacityAction ノードが透明度を設定し、HideAction が他の視覚効果と共に補完リストを表示します。最後に、FadeIn および FadeOut アクションがフェーディングを実行します。図 6 に、アニメーション処理された補完リストを示します。

図 6 AutoComplete エクステンダの操作時
図 6 AutoComplete エクステンダの操作時(画像を縮小するには、ここをクリックします)
図 6 AutoComplete エクステンダの操作時
図 6 AutoComplete エクステンダの操作時 (画像を拡大するには、ここをクリックします)

リストでの増分検索

世界各国のすべての国名からある国の名前を検索するなど、長いリスト内で特定の項目を検索したことがありますか。連続する 2 ～ 3 個の文字列をすばやく入力することができると、必要な国の名前が取得されます。ただし、数ミリ秒でもためらうと、次に入力する文字が検索語の最初の文字となります。ListSearch エクステンダではこの制限を修正します。エクステンダは ASP.NET Web コントロールと HTML コントロールのみに適用されるため、SELECT や OPTION などの通常の HTML 要素では使用できないことに注意してください。

そのため、基本的に、ListSearch エクステンダの主なメリットは、入力することによってリスト コントロール内の項目を検索できるようにし、数秒後にその検索文字列が失われないようにできることです。その代わりに、Esc キーを押すか、サーバーにポスト バック (完全ポストバックおよび部分ポストバックの両方が含まれます) するまでその検索文字列は保持されます。入力するその他の任意の文字は検索文字列に追加され、項目のリストをさらに絞りこむために使用されます。次は、ListSearch エクステンダの使用方法です。

<act:ListSearchExtender ID="ListSearchExtender1" runat="server"
     TargetControlID="DropDownList1"
     PromptCssClass="Prompt" />

エクステンダには 3 つの主なプロパティがあります。それらは PromptText、PromptCssClass、および PromptPosition です。これらのプロパティはすべて、プロンプト テキスト、つまり、検索対象について視覚的なフィードバックを提供するターゲット コントロールの前後に指定される文字列を参照します。PromptText の既定値は "Type to search" です。通常、このテキストはコントロールがフォーカスを取得するとすぐに、ターゲットのリスト コントロールのすぐ上に表示されます。PromptCssClass プロパティは、プロンプト メッセージに適用する CSS クラスの名前を取得および設定します。プロンプト テキストはユーザーが入力する検索テキストに置換されます。ここでは、Esc ボタンおよび Delete ボタンが重要な役割を果たします。Esc ボタンは現在の検索をクリアし、リストを既定で選択されている項目にリセットします。Delete ボタンは、検索テキストから最後に入力された文字を削除し、リスト内の選択を更新します。また、Animations プロパティは、先ほどの AutoComplete エクステンダでの説明のとおりに機能します。

フライング ショートカット メニュー

ユーザーがこれから操作を実行しようとしているときに、実行できる操作のオプションを提供することは役に立ちます。数年前、ツールチップは、特定のコントロールの役割についてその場で追加情報を提供できるようになり、大幅に改良されました。Dynamic HTML の登場により、HTML ツールチップが使用可能になりました。

今日では、HoverMenu エクステンダにより、任意の ASP.NET サーバー コントロールの横にフライアウト パネルを表示できるようになりました。このエクステンダの最も一般的な使用方法の 1 つは、右クリックではなくマウス移動イベントによってアクティブ化されるショートカット メニューを表示することです。

HoverMenu エクステンダは、コントロールにマウスが重ねられるとアクティブ化されます。この結果、指定されたポップアップ パネルがそのコントロールの横に表示されます。この実際の位置はプログラムで制御できます。

また、CSS スタイルをコントロールに適用し、そのコントロールをホットな状態でマークすることができます。

図 7 に、テキストボックスに挿入するテキストの入力候補を指定するラジオ ボタン メニューを示します。次は、関連するエクステンダの宣言です。

図 7 HoverMenu エクステンダの操作時
図 7 HoverMenu エクステンダの操作時(画像を縮小するには、ここをクリックします)
図 7 HoverMenu エクステンダの操作時
図 7 HoverMenu エクステンダの操作時 (画像を拡大するには、ここをクリックします)

<act:HoverMenuExtender ID="HoverMenu1" runat="server"
    TargetControlID="TextBox1"
    PopupControlID="Panel1" />

Panel1 コントロールにはラジオ ボタンのリストが含まれています。任意の選択がテキストボックスのターゲット コントロールに確実に適用されるようにするには、ラジオ ボタン リスト内に SelectedIndexChanged イベントを定義し、UpdatePanel コントロールにすべてをラップします (詳細については、このコラムに付属のソース コードを参照してください)。

HoverMenu エクステンダには、PopupControlID および TargetControlID の他にもいくつかのプロパティがあります。HoverCssClass プロパティは、ホバー メニューが表示されるときターゲット コントロールに適用される CSS クラスを示します。PopupPosition は、ターゲット コントロールに関するポップアップの位置を示します。使用可能な値は、center、top、right、left、および bottom です。OffsetX プロパティおよび OffsetY プロパティは、決定された位置からの水平および垂直オフセットを設定します。最後に、PopDelay は、DOM イベントからポップアップ表示までの遅延を示します。既定では、100 ミリ秒です。

HoverMenu エクステンダをトリガする DOM イベントは、mouseover です。HoverMenu エクステンダは Popup エクステンダとそれほど異なりません。主な違いは、アクティブ化メカニズムとトリガです。Popup エクステンダは、ユーザーが特定のターゲット コントロールにフォーカスを与えると、アクティブ化されます。HoverMenu エクステンダをアクティブ化するには、ユーザーはコントロールにマウスを重ねるだけで済みます。

まとめ

さらに豊富な機能を持つ対話的な Web サイトを求めるほど、より多くの JavaScript が必要になります。ただし、手作業でのコーディングによる JavaScript は、単純な機能の場合は十分ですが、より高度な対話性を実現するには、多くの場合不十分です。このような複雑な機能では、エクステンダや Microsoft AJAX クライアント ライブラリが本当に必要になります (2007 年 12 月の Cutting Edge コラム msdn.microsoft.com/msdnmag/issues/07/12/cuttingedge を参照)。

この Cutting Edge のコラムでは、Microsoft AJAX Control Toolkit のエクステンダに関する著者の考察を補足しました。ただし、ツールキットにはこのコラムで説明した機能の他にもさらに多くの機能が含まれています。それを確かめるには、asp.net/ajax までアクセスしてください。

ご意見やご感想は、Dino まで英語でお送りください。 cutting@microsoft.com.

Dino Esposito は Solid Quality Learning の指導者であり、『ASP.NET AJAX 入門』(日経BPソフトプレス、2008 年) の著者です。Dino はイタリアに在住し、世界各地で開催される業界のイベントで頻繁に講演しています。連絡先は cutting@microsoft.com (英語) ですが、weblogs.asp.net/despos のブログにも参加してみてください。


Office Space: SharePoint 2007 におけるセキュリティ プログラミング

Office Space
SharePoint 2007 におけるセキュリティ プログラミング
Ted Pattison

コードのダウンロード : : OfficeSpace2008_02.exe (209KB)
コンテンツ
外部セキュリティ プリンシパルと SPUer オブジェクト
認証されたユーザーと外部ユーザーの追加
アクセス許可レベルの操作
WSS グループ
ID、昇格、偽装
保護できるオブジェクト
まとめ

Windows® と ASP.NET のセキュリティ機能を使用したセキュリティ プログラミングの基礎については既にご存じだと思いますが、さらに Windows SharePoint® Services 3.0 (WSS) によるセキュリティ層が加わると、どのようになるかについては、どの程度、ご存じでしょうか。今月の Office Space では、WSS で新しく紹介された新しいセキュリティ用語と概念を中心に説明し、WSS オブジェクト モデルを使用したセキュリティ プログラミングの世界へとご案内します。

このコラムに付属のサンプル プロジェクトをダウンロードしてから、このコラムで示すコードに従って進めていくことをお勧めします。このプロジェクトは、ビルド プロセスの後にバッチ ファイルを実行するように構成されており、すべてのプロジェクト コンポーネントが WSS ソリューション パッケージにコンパイルされ、パッケージがローカル WSS ファームにインストールされます。いったんプロジェクトをビルドし、ソリューションをインストールすると、いずれのサイト コレクションにも移動でき、サイト コレクション内を詳しく調べる Security Demo という機能を有効にすることができます。[サイトの操作] メニューを使用すると、WSS セキュリティ プログラミングの技法を使ったコードが含まれているカスタム アプリケーション ページに移動できます。

外部セキュリティ プリンシパルと SPUer オブジェクト

ほとんどのセキュリティ モデルはセキュリティ プリンシパルに基づいています。各セキュリティ プリンシパルは、ユーザーまたはグループのいずれかを表します。ユーザーは自身が認証するアカウントを所有しています。認証された後、各ユーザーは ID を受け取ります。ユーザーが Windows アカウントを認証するシナリオでは、System.Security 名前空間の Microsoft® .NET Framework セキュリティ クラスを使用して、特定の Windows アカウントを指す ID を取得し、ユーザーのログイン名を見つけることができます。

WindowsIdentity identity = WindowsIdentity.GetCurrent();
string WindowsLogin = identity.Name;

WindowsIdentity を使用すると、WindowsPrincipal を動的に作成でき、これによって現在のユーザーが Active Directory® グループまたはローカル Windows グループのどちらのメンバであるかを判別するためのテストを実施することができます。これは次のように行います。

WindowsIdentity identity = WindowsIdentity.GetCurrent();
WindowsPrincipal principal = new WindowsPrincipal(identity);
if( principal.IsInRole(@"LITWAREINC\AllFTE") ){
  // perform operation allowed for fulltime employees
}

ASP.NET は、Windows 認証とフォームベース認証 (FBA) の両方をサポートします。ASP.NET のユーザー オブジェクトは、WindowsPrincipal クラスの代わりに IPrincipal インターフェイスのユーザー オブジェクトをモデル化することにより、Windows アカウントへの依存関係を取り除きます。ASP.NET ランタイムは、現在のユーザーが認証する際に Windows アカウントまたは FBA アカウントのどちらを使用したかに従って、異なる種類の IPrincipal オブジェクトを動的に作成します。

IPrincipal AspUser = HttpContext.Current.User;
string AspUserName = AspUser.Identity.Name;

また、ASP.NET ユーザー オブジェクトは、IsInRole メソッドを使用して、ユーザーが特定のロールに属しているかどうかを確認する方法も提供します。Windows ユーザーの場合、IsInRole メソッドを使用して、現在のユーザーが Active Directory グループのメンバであるかどうかを確認できます。ASP.NET ロール プロバイダと共に FBA アカウントを使用している場合は、IsInRole メソッドを使用して、FBA ユーザーが特定の ASP.NET ロールに追加されたかどうかを確認することもできます。

IPrincipal AspUser = HttpContext.Current.User;
if(AspUser.IsInRole("Site Administrators") {
  // perform privileged operation
}

認証によって、ある受信のフォームが生成されます。システムは実行時にこのフォームを使用して、ユーザー ID を表し、グループまたはロール内のメンバシップを追跡します。ユーザーが Windows アカウントを認証する場合、認証として受信するのは Windows セキュリティ トークンです。ユーザーが FBA アカウントを認証する場合、認証として受信するのは、ASP.NET ランタイムと特定の認証プロバイダによって作成された HTTP Cookie です。

WSS では、ユーザー認証に対してサポートを行わないことに注意してください。その代わり、WSS は さまざまな ASP.NET 認証プロバイダが提供する、基礎となる認証コンポーネントを使用します。サイトをセキュリティで保護する際に WSS がその価値を発揮するのは、認証とアクセス制御の設定です。WSS によって、サイト コレクション内で Windows ユーザー、FBA ユーザー、Windows グループ、ASP.NET ロールなどの外部セキュリティ プリンシパルを追跡することが可能になります。WSS を使用することで、これらの外部プリンシパルに割り当てられたアクセス許可を設定することもできます。つまり、サイト、リスト、アイテム、およびドキュメントなどの保護できる WSS オブジェクトへのアクセス許可をユーザーに付与することができます。

認証とアクセス制御を設定しているときに、サイト コレクションが重要な役割を果たすことに注意してください。外部プリンシパルを追跡し、アクセス許可を設定するときに、WSS は各サイト コレクションを独自の異なるものと見なします。WSS の高度な設計により、各サイト コレクションは意図的に分離されるため、1 つのサイト コレクションのセキュリティ設定が他のサイト コレクションのアクセス許可やアクセス制御ポリシーに影響を与えることはありません。

WSS オブジェクト モデルは SPUer オブジェクトを使用して、外部セキュリティ プリンシパルを表します。現在の SPWeb オブジェクトを通して、現在のユーザーの SPUser オブジェクトを取得できます。

SPWeb site = SPContext.Current.Web;
SPUser user = site.CurrentUser;
string DisplayName = user.Name;
string Login = user.LoginName;
string EMail = user.Email;
string User Notes = user.Notes;

SPUser オブジェクトは、ログイン名、表示名、電子メール アドレスなどの外部セキュリティ プリンシパルのプロパティを公開します。外部プリンシパルがサイトに追加されたときに、通常、これらのプロパティは、Active Directory ドメインなどの、基となるユーザー レポジトリから取得されます。SPUser オブジェクトは、ノート フィールドなどの WSS 固有のメタデータを追跡するプロパティも公開します。

WSS は、外部ユーザー、グループ、およびロールのプロファイル データを隠れたリストの中に保持します。このリストはユーザー情報リストと呼ばれています。WSS が新しいサイト コレクションを準備するたびに、ユーザー情報リストが最上位レベルのサイトに隠れたリストとして自動的に作成されます。WSS は、外部プリンシパルが初めてアクセス許可を割り当てられるとき、または保護できるオブジェクトにアクセスするためのセキュリティ チェックに初めて合格するときに、それぞれの外部プリンシパルに対して新しいプロファイルを追加します。ユーザー情報リストに保持されるユーザー プロファイルは、サイト コレクション全体をカバーしないことに注意してください。ユーザーが 1 つのサイト コレクションのプロファイル設定を更新しても、他のサイト コレクションにおけるユーザーのプロファイル設定は変更されません。

その他に混乱を招く恐れがあるのは、SPUser オブジェクトが常に実際のユーザーを表すわけではないということです。SPUser オブジェクトは Active Directory グループと ASP.NET ロールを表すこともあります。WSS は、外部ユーザーのプロファイルデータと共に、ユーザー情報リストにあるそれぞれの外部プリンシパルの種類のプロファイルを追跡します。

SharePoint セキュリティ モデルのプログラミング的側面の多くは、SPWeb オブジェクトを通じて、サイト レベルで公開されます。どのユーザーが現在のサイトのメンバであるかを確認する場合がこれに当たります。SPWeb オブジェクトは、次のコードに示すように、ユーザーの 3 つの異なるコレクションを公開します。

SPWeb site = SPContext.Current.Web;
SPUserCollection c1 = site.Users;
SPUserCollection c2 = site.AllUsers;
SPUserCollection c3 = site.SiteUsers;

Users コレクションには、これら 3 つのコレクションの内、最小のメンバシップが含まれます。このコレクションには、現在のサイトへのアクセス権が明示的に割り当てられた外部プリンシパルのすべてが含まれています。

AllUsers コレクションには、Users コレクションのすべてのメンバの他に、グループまたはロール メンバシップを通じた暗黙的なアクセス許可を使用してサイト内のオブジェクトにアクセスした外部ユーザーが含まれています。たとえば、LITWAREINC\BrianC というログイン名の Brian という名前のユーザーが、サイトにアクセスして特定のリストを表示する明示的なアクセス権を 1 度も与えられたことがないとします。しかし、このユーザーはリストを表示する権限が設定されている Active Directory グループのメンバシップによって、リストを表示できる可能性があります。Brian が初めてサイトまたはオブジェクトの 1 つにアクセスすると (たとえば、明示的な権限を使用して、リストにアクセスすると)、彼は AllUsers コレクションのメンバとして追加されますが、Users コレクションのメンバとしては追加されません。

SiteUsers コレクションは、現在のサイト コレクション内の各 AllUsers コレクションのメンバシップを組み合わせたものです。このコレクションのメンバシップには、サイト コレクション内のすべてのオブジェクトに対するアクセス許可が割り当てられたすべての外部プリンシパル、および明示的アクセス許可を使用して、サイト コレクションのオブジェクトのすべてに対するアクセス許可を割り当てられたすべての外部ユーザーが含まれています。

認証されたユーザーと外部ユーザーの追加

Active Directory アカウントを使用して認証したユーザーの WSS ユーザー プロファイルを新しく作成するには、どうすればよいでしょうか。標準ユーザーまたはサイト コレクションの所有者が Active Directory ドメインからユーザーまたはグループを選択できるようにするカスタム ユーザー インターフェイスのコンポーネントを作成する必要がある場合、PeoplePicker コントロールの使い方を知っておく必要があります (図 1 を参照)。これは WSS に装備されている、使いやすく、再利用可能な種類のコントロールです。次に示すとおり、コントロール タグを使用してこのコントロールをカスタム アプリケーション ページやユーザー コントロールに追加します。

図 1 PeoplePicker コントロール
図 1 PeoplePicker コントロール(画像を縮小するには、ここをクリックします)
図 1 PeoplePicker コントロール
図 1 PeoplePicker コントロール (画像を拡大するには、ここをクリックします)

<SharePoint:PeopleEditor
       ID="pickerPrincipal"
       AllowEmpty="false"
       ValidatorEnabled="true"
       MultiSelect="false"
       SelectionSet="User, SecGroup, DL"
       Width="280px"
       runat="server"   />

このサンプルでは、SelectSet プロパティに User、SecGroup、および DL の値を割り当てることによって、PeoplePicker コントロールを設定しています。これらの SelectSet 設定では、ユーザーが Active Directory に対して、ユーザー、グループ、または配布リストを選択および解決できるようにコントロールが構成されています。

プログラミングにより PeoplePicker コントロール プロパティにアクセスし、ユーザーがコントロールにより 1 つ以上のセキュリティ プリンシパルを選択した後に、基となるアカウントに関連付けられているログイン アカウント名を取得することができます。その後、これらのプリンシパルをサイト メンバとして実際に追加し、これらのアクセス権を設定するコードを指定します。

次に、外部ユーザーまたはグループをサイト メンバとして追加する方法について説明します。ここまでの WSS オブジェクト モデルの概要をご覧になって、後は SiteUsers などのSPUser コレクションの 1 つに直接外部セキュリティ プリンシパルを単に追加すればよいと思われるでしょう。

SPWeb site = SPContext.Current.Web;

site.SiteUsers.Add(@"LITWAREINC\BrianC",
                    "brianc@litwareinc.com",
                    "Brian Cox",
                    "Notes about Brian Cox");

site.SiteUsers.Add(@"LITWAREINC\AllFTE",
                    "allFTE@litwareinc.com",
                    "All Full-time Employees",
                    "Notes about FTE DL");

このアプローチでは、確かに外部プリンシパルのユーザー情報リストの中にプロファイルが作成されますが、アクセス許可を割り当てていないため、セキュリティの効果はほとんどありません。新しく外部セキュリティ プリンシパルを追加するより良い方法は、ユーザーが現在のサイトにアクセスできるようにアクセス許可を割り当てることです。ただし、まずアクセス許可レベルを作成し、割り当てる方法を学ぶ必要があります。

アクセス許可レベルの操作

アクセス許可レベルは、サイトについて定義される、名前の付いた権限のセットです。WSS には、読み取り、投稿、デザイン、フル アクセスの 4 つのアクセス許可レベルが既定で含まれています。これよりもレベルを細かく分ける必要がある場合は、WSS オブジェクト モデルを使用するか、サイト コレクションの所有者がアクセスできる標準の WSS 管理ページを通じて、独自のカスタム アクセス許可レベルを作成することができます。

アクセス許可レベルは "ロール" と呼ばれることがあり、SPRoleDefinition オブジェクトを使用した WSS オブジェクト モデルで表されます。外部ユーザーまたはグループにアクセス権レベルを割り当てるには、SPRoleAssignment オブジェクトを使用します。たとえば、次に、LITWAREINC\BrianC というログイン名の Windows ユーザーに既定の投稿のアクセス許可レベルを割り当てます。

SPWeb site = SPContext.Current.Web;
SPRoleDefinition role = site.RoleDefinitions["Contribute"];
SPRoleAssignment roleAssignment;
roleAssignment = new SPRoleAssignment(@"LITWAREINC\BrianC",
                                       "brianc@litwareinc.com",
                                       "Brian Cox",
                                       "Notes about Brian Cox");

roleAssignment.RoleDefinitionBindings.Add(role);
site.RoleAssignments.Add(roleAssignment);

この技法では、SPUser コレクションの 1 つにユーザーを追加する必要はありません。それは、外部ユーザーまたはグループが初めてサイト内でアクセス許可を割り当てられるときに、WSS によって自動的に行われるからです。上記のコードでは、ユーザー情報リスト内にユーザー プロファイルがない場合に、ユーザー プロファイルが作成され、ユーザーが現在のサイトの Users コレクションのメンバとして追加されます。

WSS グループ

WSS セキュリティ モデルでは、外部セキュリティ プリンシパルが SPUser オブジェクトとして表されますが、サイト コレクション内のアクセス許可の設定を容易にする方法として、WSS グループも提供されています。たとえば、サイト メンバ、コンテンツ管理者、サイト管理者などの特定のユーザー ロールについて、サイト コレクション内に WSS グループのセットを設計することができます。いったん、この設計を行った後は、アクセス許可レベルを直接 SPUser オブジェクトに割り当てる代わりに、アクセス許可レベルを単にWSS グループに対して割り当てるだけで、サイトのセキュリティ設定を行うことができます。

WSS グループを作成する明らかな利点として、新しい外部ユーザーやグループが追加されたり削除されるたびにアクセス許可を継続的に再設定する必要がなくなることが挙げられます。その代わりに、サイトを作成したときにすべてのアクセス許可の設定を 1 回行えば十分です。その後は、単に外部ユーザーとグループを WSS グループに追加したり、WSS グループから削除するだけです。WSS グループの設計原則は Active Directory グループと同じです。大きな違いは、WSS グループは単一のサイト コレクションについて定義され、その中にのみ存在する点です。

WSS グループでは WSS オブジェクト モデルで SPGroup オブジェクトとして表されます。SPWeb オブジェクトは、Groups および SiteGroups という名前の 2 つの SPGroup オブジェクトのコレクションを公開します。Groups コレクションには、現在のサイトへのアクセス許可を直接割り当てられたすべての WSS グループが含まれます。一方、SiteGroups コレクションは Groups コレクションのスーパー セットで、現在のサイト コレクション内で作成されたすべての WSS グループが含まれます。

新しい WSS グループを作成する場合、SiteGroups コレクションによって公開される Add メソッドを呼び出し、新しい WSS グループに対して目的のサイトへのアクセス許可レベルを 1 つ以上割り当てる必要があります。図 2 では、Site Members という名前の WSS グループを作成し、現在のサイトへの既定の投稿アクセス許可レベルを割り当てる例を示します。

Figure 2 新しい WSS グループの作成 (Close Figure 2)
SPWeb site = SPContext.Current.Web;
SPUser currentUser = site.CurrentUser;

// create new group
site.SiteGroups.Add("Site Members", currentUser, currentUser,
                     "Site Group created at " + DateTime.Now.ToString());

// assign permission level to new group
SPGroup NewGroup = site.SiteGroups["Site Members"];
SPRoleAssignment roleAssignment = new SPRoleAssignment(NewGroup);
SPRoleDefinition permLevel = site.RoleDefinitions["Contribute"];
roleAssignment.RoleDefinitionBindings.Add(permLevel);
site.RoleAssignments.Add(roleAssignment);

新しい WSS グループをいったん作成すると、外部ユーザーとグループをメンバとして追加することは非常に簡単です。SPGroup オブジェクトは、AddUser メソッドを公開します。このメソッドでは、SPUser オブジェクトが受け入れられ、外部ユーザーとグループを追加できるようになります。

SPWeb site = SPContext.Current.Web;
SPUser currentUser = site.CurrentUser;
SPGroup group = site.SiteGroups["Site Members"];
SPUser user1 = site.SiteUsers[@"LITWAREINC\BrianC"];
SPUser user2 = site.SiteUsers[@"LITWAREINC\AllFTE"];
group.AddUser(user1);
group.AddUser(user2);

ID、昇格、偽装

WSS Web アプリケーションのワーカー プロセスは、IIS アプリケーション プールを通じて制御されます。Web アプリケーションのワーカー プロセス ID は注目に値するものです。これは SharePoint Central Administration アプリケーションで設定することができます。ローカル アカウント (NETWORK SERVICE など) に依存せずに、ドメイン アカウント (LITWAREINC\SP_WorkerProcess など) についての Web アプリケーションのワーカー プロセス ID を設定する必要があります。

Web アプリケーションのワーカー プロセス ID は、1 つ以上のコンテンツ データベースの読み取りおよび書き込みの SQL Server アクセス許可が設定されている、特権付きの Windows アカウントである必要があることに注意してください。SharePoint Central Administration サイトで実行されている Web アプリケーションのワーカー プロセス ID は、ファームの構成データベースへの読み取り/書き込みのアクセス許可が必要なため、より高い特権が必要となります。

Web パーツまたはカスタム アプリケーション ページのコードはユーザーの要求に応じて実行されますが、ホスト Web アプリケーションのワーカー プロセス ID については実行されません。その代わりに、WSS では偽装を使用して、Windows セキュリティ コンテキストを他の Windows アカウントに切り替えます。実際、WSS Web アプリケーションの web.config ファイルの中には、次のエントリが含まれています。

<configuration>
  <system.web>
    <identity impersonate="true" />
  </system.web>
</configuration>

Windows アカウントの認証を受けたユーザーの要求が実行されると、現在のユーザーの Windows ID が偽装されます。ただし、FBA 認証では Windows セキュリティ トークンは作成されず、Windows ID が存在しないため、FBA ユーザーに対して同じアプローチを取ることはできません。したがって、FBA 認証を受けたユーザーの要求によって、匿名アクセスに設定された Windows アカウントの ID が偽装されます。IIS におけるこのアカウントの既定の割り当ては IUSER_MACHINENAME アカウントですが、1 つのドメイン アカウントを指すように再設定することができますし、通常は再設定する必要があります。

ここで、WSS セキュリティ プログラミングから 1 歩離れて、より高いレベルで見ていくことにします。WSS セキュリティ モデルでは、開発者が Windows ID と WSS ユーザー ID を区別しなければならないケースがよくあります。これは、現在の Windows ID と現在の WSS ユーザー ID の両方が同じ Windows ログインを指しているときの要求では、それほど顕著ではないかもしれません。しかし、FBA を使用するシナリオでは、状況はより複雑になります。たとえば、基になる Windows ID が IUSER_MACHINENAME アカウントに基づく一方で、WSS ユーザー ID は Andrew という名前の FBA ユーザーを指すことができます。コードが WSS オブジェクトへのアクセスを試みると、WSS では、ユーザーの WSS ID を使用したアクセス検査が実行されます。次に、Windows オペレーティング システムによって保持されるファイルなど、WSS の外にある外部オブジェクトへのアクセスをコードが試みると、オペレーティング システムによって、コードが現在実行される基となっている Windows ID を使用したアクセス検査が実行されます。

時には、現在のユーザーが所有しているよりも高いレベルのアクセス許可でコードを実行する必要がある場合があります。たとえば、読み取りのみのアクセス許可しか所有していないユーザーの要求を処理するときに、リストにデータを書き込まなければならないコードを作成するシナリオを考えてみましょう。既定では、コードは現在のユーザーと同じアクセス許可の下で実行されます。ただし、SPSecurity クラスの RunWithElevatedPrivileges メソッドを呼び出して、コードのセキュリティ コンテキストを昇格させることができます。RunWithElevatedPrivileges の呼び出しにより、WSS ユーザー ID と Windows ID の両方が昇格されることに注意してください。

次に、LITWAREINC\BrianC というログイン名で Windows アカウントの認証を行ったユーザーのシナリオを考えてみましょう。RunWithElevatedPrivileges の呼び出しにより、WSS ユーザー ID は SHAREPOINT\System アカウントに昇格されます。SHAREPOINT\System アカウントは WSS ランタイムに組み込まれ、WSS 認証モデル内のすべての権限を取得します。また、RunWithElevatedPrivileges の呼び出しにより、実行されているコードの Windows ID も切り替えられ、現在の Web アプリケーションのワーカー プロセス ID の下に実行されます。

// BEFORE ELEVATION
// WSS User identity = LITWAREINC\BrianC
// Windows identity = LITWAREINC\BrianC

SPSecurity.RunWithElevatedPrivileges(delegate() {
  // AFTER ELEVATION
  // WSS User identity = SHAREPOINT\System
  // Windows identity = LITWAREINC\SP_WorkerProcess
});

いくつかのシナリオでは、Windows ファイル システムまたは SQL Server データベース内のファイルへのアクセスを試みる前に、RunWithElevatedPrivileges メソッドを呼び出して、現在の呼び出しの Windows ID を変更することもできます。また、Windows ID をLITWAREINC\SP_WorkerProcess などのプロセス ID に切り替えることにより、Active Directory 環境の委任を設定する必要がなくなることに注意してください。Windows 認証を使用して SQL Server データベースのデータにリモートでアクセスするカスタム Web パーツがある場合、この方法はたいへん役に立ちます。

また、他のシナリオでは、RunWithElevatedPrivileges メソッドを呼び出して WSS ユーザー ID を SHAREPOINT\System に昇格することにより、現在のユーザーには許可されていない操作をコードが実行できるようになります。コードがいったん SHAREPOINT\System として実行されると、WSS 認証サブシステム内で、まさに何でもできるようになります。

SHAREPOINT\System アカウントに昇格させるために RunWithElevatedPrivileges を呼び出す場合、1 つだけ注意すべき点があります。たとえば、RunWithElevatedPrivileges を呼び出し、SPContext.Current プロパティを通じて現在のサイト コレクション、またはサイト内のオブジェクトへのアクセスを試みるとします。コードがエラーを起こすことなどはおそらく想像していないと思いますが、実はその可能性があります。

SPSecurity.RunWithElevatedPrivileges(delegate() {
  SPSite siteCollection = SPContext.Current.Site;
  // next line fails if current user is Contributor
  string siteCollectionOwner = siteCollection.Owner;
});

WSS ユーザー ID を SHAREPOINT\System に昇格した後、なぜこのサンプル コードはエラーを起こすのでしょうか。それは、SPSite オブジェクトが作成されたときに関係があります。SPSite オブジェクトとその子 SPWeb オブジェクトの実際のアクセス許可は、現在の WSS ユーザー ID に依存しません。その代わり、アクセス許可は SPSite オブジェクトが作成されたときの WSS ユーザー ID に依存します。コードが WSS ユーザー ID を切り替える前に、要求の初期の段階で SPContext.Current を通じてアクセス可能な SPSite オブジェクトが作成されたためです。

このように、この技法を使用するには、RunWithElevatedPrivileges を呼び出し、WSS ユーザー ID を SHAREPOINT\System に切り替えた後に、新しい SPSite オブジェクトを作成する必要があります。

SPSecurity.RunWithElevatedPrivileges(delegate() {
  using (SPSite elevatedSiteCollection = new SPSite(this.Site.ID)) {
    using (SPWeb elevatedSite =
      elevatedSiteCollection.OpenWeb(this.Web.ID)) {
      // access elevatedSiteCollection and
      //elevatedSite as SHAREPOINT\System
    }
  }
});

これにより、サイト コレクションとその中のサイトを開けるようになるため、コードは SHAREPOINT\System としてオブジェクトにアクセスできます。

特定の WSS ユーザー ID を偽装することが必要になる場合があります。コードが既定で SHAREPOINT\System として実行されている場合、イベント ハンドラまたはカスタム ワークフロー テンプレートのコードを書くときがこれに当たります。たとえば、新しいオブジェクトを作成する前に特定の WSS ユーザー ID を偽装して、WSS ユーザーが新しいオブジェクトの所有者として認識されるようにすることができます。

WSS ユーザー ID を偽装するには、まず SPUserToken オブジェクトを作成する必要があります。これは、SPUser オブジェクトの UserToken プロパティにアクセスすることによって作成できます。いったん SPUserToken オブジェクトを作成したら、このオブジェクトと SPSite クラス コンストラクタのオーバーロード バージョンを使用して、新しい SPSite オブジェクトを作成できます。この技法を図 3 に示します。

Figure 3 WSS ユーザー ID の偽装 (Close Figure 3)
SPWeb siteCollection = SPContext.Current.Site;
SPWeb site = SPContext.Current.Web;

// get SPUser object and acquire token
SPUser targetUser = site.SiteUsers[@"LITWAREINC\BrianC"];
SPUserToken token = targetUser.UserToken;

// create new SPSite and SPWeb object to impersonate user
using (SPSite impersonatedSiteCollection =
              new SPSite(siteCollection.ID, token)) {
  using (SPWeb impersonatedSite =
               impersonatedSiteCollection.OpenWeb(site.ID)) {
    // WSS identity switched to impersonate BrianC
    // Windows identity does not change
  }
}

WSS ユーザーを偽装する際に注意すべき点がいくつかあります。まず、WSS ユーザーの偽装は現在の Windows ID を変更しないため、RunWithElevatedPrivileges の呼び出しとは異なります。たとえば、もしも WSS ユーザーを偽装する前に LITWAREINC\SP_WorkerProcess の Windows ID の下で要求が実行される場合、コードは引き続き同じ Windows ID の下で実行されます。WSS ユーザーの偽装は、現在の Windows ID を偽装されたユーザーの ID に変更しません。

また、他のユーザーを偽装するには、コードが特権を持った状態で実行されている必要があります。コードは既定で SHAREPOINT\System として実行されているため、イベント ハンドラやカスタム ワークフロー テンプレートについては心配する必要はありません。ただし、Web パーツ内またはカスタム アプリケーション ページのコードは、他の WSS ユーザー ID を偽装できるようになる前に RunWithElevatedPrivileges を呼び出す必要がある場合があります。

保護できるオブジェクト

WSS セキュリティ設定の優れた点は、サイト、リスト、およびリスト アイテムなどの保護できるオブジェクトによって提供される、その柔軟性です。それぞれの保護できるオブジェクトには、セキュリティ プリンシパルがアクセス許可を付与されているかどうかを判別するために、WSS が実行時に使用するバイナリ データ構造である、アクセス制御リスト (ACL) を含めることができます。既定では、ACL を含んでいる唯一の保護できるオブジェクトは最上位レベルのサイトです。リスト、リスト アイテム、子サイトなどのすべての子オブジェクトは、親からの継承が断たれ、独自の ACL を提供されない限り、親の ACL を継承します。

WSS オブジェクト モデルには、WSS サイト コレクション内で保護できるオブジェクトをモデル化する IsecurableObject という名前のインターフェイスが含まれています (図 4 を参照)。SPWeb オブジェクト、SPList オブジェクト、および SPItem オブジェクトによって実装される IsecurableObject インターフェイスは、実行時のアクセス検査、およびアクセス許可の設定の基礎を提供します。

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

サイト コレクション内のアクセス許可の設定を開始する場合、サイト、リスト、およびリスト アイテムのすべてが保護できるオブジェクトの 1 つの階層を構成していることを理解しておくことが大切です。既定では、最上位レベルのサイトのみが固有の ACL を含み、ユーザーがオブジェクトにアクセスするためにはどのようなアクセス許可が必要かを指定するアクセス許可レベルの割り当てを定義します。すべての子オブジェクトは最上位レベル サイトのアクセス権を継承します。ただし、保護できるオブジェクトに独自のアクセス許可レベルのセットを割り当てることにより、アクセス制御をより細かく設定することができます。たとえば、図 5 に示すコードでは、新しいドキュメント ライブラリを作成し、それに固有のアクセス許可のセットを設定することができます。

Figure 5 独自のアクセス許可のセットの設定 (Close Figure 5)
SPWeb site = SPContext.Current.Web;
Guid listID = site.Lists.Add("Proposals",
                            "Library desc",
                            SPListTemplateType.DocumentLibrary);

SPDocumentLibrary doclib = (SPDocumentLibrary)site.Lists[ListID];
doclib.OnQuickLaunch = true;
doclib.BreakRoleInheritance(false);
SPUser AllFteGroup = Web.SiteUsers[@"LITWAREINC\AllFTE"];
SPRoleAssignment assignAllFteGroup = new SPRoleAssignment(AllFteGroup);
SPRoleDefinition roleDesign = this.Web.RoleDefinitions["Read"];
assignAllFteGroup.RoleDefinitionBindings.Add(roleDesign);
doclib.RoleAssignments.Add(assignAllFteGroup);
doclib.Update();

このサンプル コードでは、BreakRoleInheritance の呼び出しを使用して、既定のアクセス許可の親からの継承を断ちます。BreakRoleInheritance を呼び出し、True のパラメータ値を渡すと、保護できるオブジェクトには最初は親オブジェクトの ACL のコピーである ACL が設定されます。BreakRoleInheritance を呼び出し、False のパラメータ値を渡すと、保護できるオブジェクトには最初は空の ACL が設定されます。つまり、このドキュメント ライブラリは、サイトの所有者でもサイト管理者でもないユーザーからはアクセスできないことになります。

Windows SharePoint Services 3.0 の拡張されたセキュリティにより、アイテムやドキュメントのレベルまでアクセス許可を設定できるようになりました。この機能は、SPListItem オブジェクトが ISecurableObject インターフェイスも実装するため、WSS オブジェクト モデルを通じて可能になりました。

図 6 に示すコードでは、ドキュメント ライブラリ内に新しいドキュメントを作成し、それに親ドキュメント ライブラリとは異なる固有のアクセス許可のセットを設定します。このコードでは、SPDocumentLibrary 参照とファイル名を受け入れる、WriteDocument と呼ばれるユーティリティ メソッドが使用されていることに注意してください。このメソッド実装では、Office オープン XML ファイル形式を使用して Word 文書を作成し、それがドキュメント ライブラリに書き戻されます。WriteDocument メソッドが SPFile 参照を返し、この参照を使用して文書に関連付けられている SPListItem にアクセスできるため、アクセス権の継承を断ち、固有のアクセス許可のセットを割り当てることができます。

Figure 6 親とは異なるアクセス許可のセット (Close Figure 6)
SPWeb site = SPContext.Current.Web;
Guid listID = site.Lists.Add("Proposals",
                            "Library desc",
                            SPListTemplateType.DocumentLibrary);

SPDocumentLibrary doclib = (SPDocumentLibrary)Web.Lists[ListID];
doclib.OnQuickLaunch = true;
doclib.Update();
SPFile doc1 = WriteDocument(doclib, "Adventure Works Merger.docx");
doc1.Item.BreakRoleInheritance(false);
SPGroup group = Web.Groups["Litware Contact Managers"];
SPRoleAssignment assignContribute = new SPRoleAssignment(group);
SPRoleDefinition roleContibute = this.Web.RoleDefinitions["Contribute"];
assignContribute.RoleDefinitionBindings.Add(roleContibute);
doc1.Item.RoleAssignments.Add(assignContribute);
doc1.Item.Update();

まとめ

今回の話は、WSS のセキュリティ モデルを通して、世界旅行をしたように感じられることでしょう。WSS がどのようにユーザー情報リスト内のプロファイルを使用してサイト コレクション レベルの外部セキュリティ プリンシパルを追跡するかをご覧いただきました。また、WSS がどのようにして SPUser オブジェクトを使用して WSS オブジェクト モデルでこれらの外部セキュリティ プリンシパルを表すかを説明しました。また、WSS が WSS グループをどのようにサポートするかを説明し、特権の昇格と WSS ユーザーの偽装を行う、いくつかのプログラミング技法をご紹介しました。これらの技法は、実際のアプリケーションを作成する際に必要な機能と柔軟性を提供します。

WSS は、認証を行う際にその基となるコンポーネントのシステムに依存する一方で、承認とアクセス制御に関する責任を負います。WSS 承認モデルの多くは、アクセス許可レベルまたはロールとして知られる、名前の付いた権限のセットに基づいています。アクセス許可レベルを SPUser オブジェクトに割り当てることはできますが、実際のところ、一般的にはアクセス許可レベルを WSS グループに割り当てることをお勧めします。

ご意見やご質問は、Ted まで英語でお送りください。 mmoffice@microsoft.com.

Ted Pattison は、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。彼の会社 Ted Pattison Group (www.TedPattison.net) では、企業の開発者を対象とする SharePoint の上級者向けトレーニングを実施しています。Ted は Microsoft Press 発行の『Inside Windows SharePoint Services 3.0』の著者です。


テストの実行: Microsoft UI オートメーション ライブラリ

テストの実行
Microsoft UI オートメーション ライブラリ
Dr. James McCaffrey

コードのダウンロード : : TestRun2008_02.exe (178KB)
コンテンツ
StatCalc の内部
UI テスト自動化のコード
テスト ハーネスを拡張する

Windows® アプリケーションのユーザー インターフェイスをテストする方法はいくつかあります。たとえば、Microsoft® .NET Framework アプリケーションのテストには System.Reflection クラスを使用し、.NET アプリケーションとネイティブ アプリケーションの両方をテストする場合には、P/Invoke メカニズムを使用してマネージまたはアンマネージの C++、C#、または Visual Basic® で FindWindow などの Win32® API 関数を呼び出します。

このコラムでは、.NET Framework 3.0 の Windows Presentation Foundation (WPF) に含まれる新しい Microsoft UI オートメーション ライブラリを使用して UI テスト自動化を開始する方法を説明します。このライブラリは、当初アクセシビリティ専用機能として設計され、その後 UI テスト自動化に有用であることが判明し、UI 自動化に採用された Microsoft Active Accessibility (MSAA) ライブラリの後継と考えられます。一方、UI オートメーション ライブラリは、当初からアクセシビリティと UI テスト自動化の両方を意識して設計されています。UI オートメーション ライブラリを使用して、Win32 アプリケーション、.NET Windows フォーム アプリケーション、および WPF アプリケーションを .NET Framework 3.0 対応のオペレーティング システム (Windows XP、Windows Vista®、Windows Server® 2003、Windows Server 2008 など) が動作するホスト コンピュータ上でテストすることができます。

要するに筆者が言いたいことは、UI オートメーション ライブラリはテスト自動化における最近の最も重要な進歩の 1 つであり、Windows アプリケーションの UI テスト自動化の技法として急速に普及していくと確信しているということです。UI オートメーション ライブラリは他の UI テスト自動化方式より高機能で、操作性や一貫性の面でもより優れています。.NET Framework がソフトウェア アプリケーションの開発に変革をもたらしたように、UI オートメーション ライブラリは UI テスト自動化を一新する可能性があります。

内容をわかりやすく示すため、スクリーンショットを用意しました。図 1 は、これからテストする単純な Windows アプリケーションを示しています。この StatCalc アプリケーションでは、複数の整数の算術平均、幾何平均、調和平均を計算します。算術平均は単純な平均値です。たとえば、30 インチと 60 インチの算術平均は 45 インチです。幾何平均は比率に使用します。たとえば、30:1 と 60:1 の幾何平均は 42.4264:1 です。調和平均は割合に使用します。たとえば、一定距離を時速 30 マイルと時速 60 マイルで進む場合の調和平均は時速 40 マイルです。

図 1 UI テスト自動化用のサンプル アプリケーション
図 1 UI テスト自動化用のサンプル アプリケーション(画像を縮小するには、ここをクリックします)
図 1 UI テスト自動化用のサンプル アプリケーション
図 1 UI テスト自動化用のサンプル アプリケーション (画像を拡大するには、ここをクリックします)

図 1 の UI テスト自動化は、テスト対象のアプリケーションを起動し、UI オートメーション ライブラリを使用してアプリケーション上のアプリケーション コントロールおよびユーザー コントロールへの参照を取得して、ユーザーが 30 と 60 を入力し、[Calculate] ボタン コントロールをクリックした場合をシミュレートするコンソール アプリケーションです。次に、このテスト自動化では、結果を示す TextBox コントロールの予期される値を調べ、合否結果を出力することにより、アプリケーションの結果状態を確認します。スクリーンショットは、テスト自動化がテスト対象アプリケーションを閉じる直前のものをキャプチャしています。

以降では、テストに使用する StatCalc Windows アプリケーションについて簡単に説明し、テスト対象アプリケーションを起動する方法、UI オートメーション ライブラリを使用してアプリケーション コントロールおよびユーザー コントロールへの参照を取得する方法、ユーザー操作をシミュレートし、アプリケーションの状態を確認する方法を示します。また、このコラムで示したテスト システムを各自の要件に応じて拡張および変更する方法についても説明します。読み進めていくうちに、新しい UI オートメーション ライブラリがテスト ツール セットを飛躍的に向上させるものであることがおわかりいただけると思います。

StatCalc の内部

テスト自動化の目標を理解していただくために、テスト対象のアプリケーションについて簡単に説明します。UI テスト自動化を作成する際に、アプリケーションのソース コードにアクセスできれば便利ですが、必ずしもその必要はないということがおわかりいただけると思います。

StatCalc アプリケーションは単純な Windows ベースのフォームです。筆者はアプリケーションのコードを C# で記述しましたが、UI オートメーション ライブラリは Win32 アプリケーションおよび WPF アプリケーションでも使用できます。簡素化のため、Visual Studio® の既定のコントロール名 (Form1、label1、textBox1、groupBox1、radioButton1、radioButton2、radioButton3、button1、textBox2) をそのまま使用しています。また、古い MainMenu コントロールではなく、.NET Framework 2.0 から導入された MenuStrip コントロールを使用して、最上位のメニュー項目 File および Help を追加しました。StatCalc アプリケーションの機能は button1_Click メソッドに記述されており、その内容を図 2 に示します。

Figure 2 StatCalc アプリケーション コード (Close Figure 2)
private void button1_Click(object sender, EventArgs e) {
  string[] sVals = textBox1.Text.Split(' ');
  int[] iVals = new int[sVals.Length];
  for (int i = 0; i < iVals.Length; ++i)
    iVals[i] = int.Parse(sVals[i]);

  if (radioButton1.Checked) {
    double sum = 0.0;
    foreach (int v in iVals)
      sum += v;
    double result = (double)(sum / iVals.Length);
    textBox2.Text = result.ToString("F4");
  }
  else if (radioButton2.Checked) {
    double product = 1.0;
    foreach (int v in iVals)
    product *= (double)v;
    double result = NthRoot(product, iVals.Length);
    textBox2.Text = result.ToString("F4");
  }
  else if (radioButton3.Checked) {
    double sum = 0.0;
    foreach (int v in iVals)
      sum += (1/ (double)v);
    double result = (double)(iVals.Length / sum);
    textBox2.Text = result.ToString("F4");
  }
}

ユーザーが [Calculate] ボタンをクリックすると、制御は button1_Click メソッドに移り、このメソッドで textBox1 の値を文字列としてフェッチします。次に、String.Split メソッドを使用して、各値を文字列配列に解析します。さらに、クリック ハンドラで文字列配列を整数配列に変換します。button1_Click のロジックは選択された radioButton コントロールに基づいて分岐し、適切な平均を倍精度浮動小数点数型で計算して、その結果を小数点以下 4 桁に書式設定された文字列として textbox2 コントロールに表示します。

幾何平均を計算する場合、アプリケーションはローカル ヘルパ メソッド NthRoot を呼び出します。このメソッドの定義は次のとおりです。

private static double NthRoot(double x, int n) {
  return Math.Exp( Math.Log(x) / (double)n );
}

アプリケーション コードを短く、わかりやすくするため、通常のエラー チェックは省略します。

一般的に、UI オートメーション ライブラリを使用してテスト自動化を作成する場合、テスト対象システムのソース コードにアクセスする必要はありません。ほとんどの場合、コントロールへのアクセスには、内部 Name プロパティ (button1 など) ではなく、キャプション (Calculate など) を使用します。ただし、テキスト ボックス コントロールなどのコントロールにはキャプションがありません。

キャプションがないコントロールに UI 自動化でアクセスする場合は、暗黙のインデックス順序を知っている必要があります。インデックス順序は、フォームの初期化時にコントロールがメインのフォーム コントロール (または親コントロール) に読み込まれる順序です。暗黙のインデックス順序は、ユーザーがデザイン時に Form オブジェクトにコントロールを追加する順序や、タブ インデックスの順序と同じであるとは限りません。

アプリケーションのソース コードを表示すれば、キャプションがないコントロールの暗黙のインデックス順序が簡単にわかります。たとえば、筆者の環境では、StatCalc アプリケーションの Form1.Designer.cs ファイルを参照して Windows フォーム デザイナで生成したコード領域を展開すると、次のようなコードが表示されます。

this.Controls.Add(this.button1);
this.Controls.Add(this.groupBox1);
this.Controls.Add(this.label1);
this.Controls.Add(this.textBox2);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.menuStrip1);

この場合、button1 は暗黙のインデックス 0、groupBox1 はインデックス 1 という具合になります。StatCalc アプリケーションのデザイン時に textBox1 を textBox2 より前に Form オブジェクトに追加したにもかかわらず、textBox2 コントロールの方が textBox1 コントロールより暗黙のインデックスの値が小さいことに注目してください。また、radioButton1、radioButton2、および radioButton3 の各コントロールは、Form1 ではなく、groupBox1 コントロールの子コントロールです。詳しくは後述しますが、UI オートメーション ライブラリを使用してテスト自動化を作成する場合は、コントロール ツリーの階層の深さが重要です。

そのため、テスト対象アプリケーションのソース コードにアクセスできれば、コントロールの暗黙のインデックス順序が簡単にわかります。では、ソース コードにアクセスできない場合はどうすればよいでしょうか。コントロールの暗黙のインデックス順序は、Visual Studio に付属の Spy++ ツールを使用して確認することもできます。Spy++ を起動し、Ctrl + F5 キーを押して、GUI Finder Tool をコントロールまたはウィンドウにドラッグすると、そのコントロールまたはウィンドウに関する詳細情報を取得できます。図 3 は、Spy++ で textBox1 コントロールを対象とした場合の結果を示しています。背面の画面を見ると、textBox1 にハンドル 00080820 があることがわかります。前面の画面を見ると、textBox1 の次のコントロールのキャプションが menuStrip1 で、textBox1 の前のコントロールにはキャプションがないことがわかります。これは textBox2 コントロールです。

図 3 Spy++ を使用して暗黙のインデックス順序を確認する
図 3 Spy++ を使用して暗黙のインデックス順序を確認する(画像を縮小するには、ここをクリックします)
図 3 Spy++ を使用して暗黙のインデックス順序を確認する
図 3 Spy++ を使用して暗黙のインデックス順序を確認する (画像を拡大するには、ここをクリックします)

このように、Spy++ で各コントロールを確認すれば、テスト対象アプリケーションのソース コードにアクセスできなくても、アプリケーションのすべてのコントロールの暗黙のインデックス順序を推定できます。

この StatCalc アプリケーションのような小さいアプリケーションでさえ、手動でテストするとなると、面倒で、間違いを犯す可能性があり、手間がかかって非効率的です。入力を行い、[Calculate] ボタン コントロールをクリックし、結果の値を目で確認し、合否結果を手動でスプレッドシートまたはデータベースに記録する必要があります。これよりはるかに優れた方法で、UI オートメーション ライブラリを使用してユーザーのアプリケーション操作をシミュレートし、アプリケーションが正しく応答したかどうかを確認することができます。面倒なテスト ケースを自動化することで、空いた時間を経験や直感がものを言うようなおもしろみのある有益な手動のテスト ケースに費やすことができます。

UI テスト自動化のコード

筆者のテスト ハーネス全体の構造は、図 4 のようになります。Visual Studio を起動し、新規のコンソール アプリケーション プログラムを作成します。サンプルは C# で作成されていますが、このテスト自動化のコードを Visual Basic などの .NET 対応言語に変換することも簡単にできます。次に、UIAutomationClient.dll ライブラリおよび UIAutomationTypes.dll ライブラリへのプロジェクト参照を追加します。これらのライブラリは .NET Framework 3.0 の一部であり、通常は %PROGRAMFILES%\Reference Assemblies\Microsoft\Framework\v3.0 ディレクトリに保存されています。

Figure 4 UI テスト自動化のコード構造 (Close Figure 4)
using System;
using System.Windows.Automation;
using System.Diagnostics;
using System.Threading;

namespace TestScenario {
  class Program {
    static void Main(string[] args) {
      try {
        Console.WriteLine("\nBegin WPF UIAutomation test run\n");
        // launch StatCalc application
        // get refernce to main Form control
        // get references to user controls
        // manipulate application
        // check resulting state and determine pass/fail
        Console.WriteLine("\nEnd test run\n");
      }
      catch(Exception ex) {
        Console.WriteLine("Fatal error: " + ex.Message);
      }
    }
  }
}

UI オートメーション ライブラリのアーキテクチャでは、クライアント/サーバーの観点と名前付け規則が使用されています。UI テスト自動化の観点からは、テスト対象アプリケーションをサーバー、テスト ハーネスをクライアントと考えます。テスト ハーネス クライアントはテスト対象アプリケーション (サーバー) から UI 情報を要求します。UIAutomationClient.dll ライブラリは、UI オートメーション クライアントで使用されるテスト自動化のライブラリです。また、UIAutomationTypes.dll ライブラリには、UIAutomationClient.dll などの UI オートメーション サーバー ライブラリで使用される各種の型定義が含まれています。

UIAutomationClient.dll ライブラリおよび UIAutomationTypes.dll ライブラリ以外に、UIAutomationClientSideProvider.dll ライブラリおよび UIAutomationProvider.dll ライブラリもあります。UIAutomationClientSideProvider.dll ライブラリには、自動化をサポートしていないコントロールを操作する一連のコードが含まれています。この中には、レガシ コントロールやカスタム .NET コントロールを含めることができます。サンプル アプリケーションは、UI テスト自動化をサポートするためにすべての標準コントロールを使用して作成しているため、このライブラリは不要です。UIAutomationProvider.dll ライブラリは、カスタム UI コントロールを作成する場合や、UI オートメーション ライブラリでコントロールにアクセスする場合に使用する一連のインターフェイス定義です。

プロジェクト参照を追加すると、System.Windows.Automation 名前空間がテスト ハーネス プログラムから認識可能になり、この名前空間に using ステートメントを追加することができます。筆者は利便性を考えて、Process クラスを簡単に使用するための System.Diagnostics 名前空間および Thread.Sleep メソッドを簡単に使用するための System.Threading 名前空間を参照する using ステートメントを追加しました。また、テスト自動化の一般的な処理として、ハーネスを最上位の try/catch ブロックでラップして致命的なエラーに対処しています。テスト自動化は本質的に複雑であり、例外は、例外というよりもむしろ規則といった方が適しています。

テスト自動化のサンプル コードでは、まずテスト対象アプリケーションを起動しています。

Console.WriteLine("\nBegin WPF UIAutomation test run\n");
Console.WriteLine("Launching StatCalc application");
Process p =
    Process.Start("..\\..\\..\\StatCalc\\bin\\Debug\\StatCalc.exe");

テスト対象アプリケーションのプロセス情報をキャプチャしている点に注意してください。次に、ホスト コンピュータのデスクトップ ウィンドウへの参照を AutomationElement としてフェッチします。

AutomationElement aeDesktop = AutomationElement.RootElement;

UI オートメーション ライブラリを使用したテスト自動化で必要なオブジェクトの大部分は AutomationElement 型です。ここでは、AutomationElement の静的プロパティ RootElement を使用して、最上位のビジュアル要素 (Desktop) への参照を取得できます。UI テスト自動化を実行する場合は、ホスト コンピュータ上のすべてのビジュアル エンティティ (コントロールやウィンドウなど) を、デスクトップ ウィンドウをルート要素とする階層ツリー構造の一部と考えると便利です。これは、UI オートメーション ライブラリを使用する自動化を含め、ほとんどの種類の UI テスト自動化の一般原則です。

次に、メインの Form オブジェクト (ここでは StatCalc アプリケーション) への参照を取得する必要があります。そのための簡単な方法を次に示します。

// naive approach
Thread.Sleep(5000);
AutomationElement aeForm =
    AutomationElement.FromHandle(p.MainWindowHandle);
if (aeForm == null)
    Console.WriteLine(
        "Could not find the StatCalc Form");

テスト対象アプリケーションが起動するまで数秒待機した後、FromHandle メソッドでアプリケーションへの参照を取得します。アプリケーションの起動時にテスト対象アプリケーションのプロセス情報を保存したことを思い出してください。

この方法でも動作しますが、ちょっとした問題が 2 つあります。1 つは、Form オブジェクトへのアクセスを試行するまでの待機時間を推定する良い方法がないということです。そのため、安全を考えて遅延時間を大幅に長めにとる必要があり、テスト ケースが膨大な場合は特に、テスト自動化の実行に時間がかかります。もう 1 つは、この方法はテスト ハーネスがテスト対象アプリケーションを起動することを前提にしているということです。この前提があればこそ、アプリケーションのプロセス情報を保存できます。ところが、アプリケーションを起動済みの状態でテストする必要がある場合には、テスト対象アプリケーションのプロセス情報にすぐにアクセスできません (プログラムでプロセス情報を取得することは可能です)。

ほとんどの場合、テスト対象アプリケーションの Form オブジェクトへの参照を AutomationElement として取得する方法がより効果的です。このコード例を図 5 に示します。サンプル コードでは、アプリケーションへの参照を要求するまでの待機時間をやみくもに設定するのではなく、短い遅延時間 (この場合は 100 ミリ秒) のループ内で参照の取得を試行しています。ハーネスが待機ループに入った回数を追跡し、NULL 以外の参照を取得した場合 (Form オブジェクトが見つかった場合)、または最大試行回数 (この場合は 50 回) を超えた場合にループを終了します。

Figure 5 テスト アプリケーション AutomationElement を取得する (Close Figure 5)
// a better approach
AutomationElement aeForm = null;
int numWaits = 0;
do
{
    Console.WriteLine("Looking for StatCalc . . . ");
    aeForm = aeDesktop.FindFirst(TreeScope.Children,
        new PropertyCondition(
            AutomationElement.NameProperty,
            "StatCalc"));

    ++numWaits;
    Thread.Sleep(100);
}

while (aeForm == null && numWaits < 50);

if (aeForm == null)
    throw new Exception("Failed to find StatCalc");
else
    Console.WriteLine("Found it!");

また、ここでは、FromHandle ではなく、一組の引数として渡された指定の条件を満たす AutomationElement への最初の有効な参照を検索する FindFirst メソッドを使用しています。最初の引数 TreeScope.Children は、FindFirst に対して、コンテキスト (この場合は aeDesktop) のすべての子孫を検索対象とするのではなく、コンテキストの直接の子コントロールのみを検索対象とするように指示しています。

FindFirst の 2 番目の引数は条件です。

new PropertyCondition(AutomationElement.NameProperty, "StatCalc")

このコードを解釈すると、"キャプション (Name プロパティ) を持つ自動化要素が StatCalc に等しい" という意味になります。UI オートメーション ライブラリの設計者は、コントロールの検索条件を Property == StatCalc などのように従来の文字列で指定するのではなく、PropertyCondition クラスを作成することにしました。一見、この方法は無粋で冗長に見えますが、すぐに慣れます。

基本的に、単純な文字列ではなく、完全にオブジェクト指向の PropertyCondition クラスを使用してコントロールを検索すると、テスト自動化を作成するときよりもコードが多くなりますが、いくつかの利点もあります。PropertyCondition を使用すると、少なくともそのパラダイムに慣れれば、テスト自動化のコードの目的がわかりやすくなります。PropertyCondition オブジェクトは文字列より型指定が厳密であるため、PropertyCondition を使用して、Visual Studio などの統合開発環境でデザイン時に IntelliSense® オート コンプリート機能を使用できます。また、コンパイラのエラー チェックを強化することもできます。

サンプル コードでは、テスト対象アプリケーションのメインの Form オブジェクトへの参照を取得後、すべてのユーザー コントロールへの参照をフェッチしています。たとえば、[Calculate] ボタン コントロールへの参照を取得するには、次のようなコードを記述します。

Console.WriteLine("Finding all user controls");
AutomationElement aeButton = aeForm.FindFirst(TreeScope.Children,
  new PropertyCondition(AutomationElement.NameProperty, "Calculate"));

ユーザー コントロールの取得パターンは、フォーム コントロールの取得パターンとまったく同じです。このような統一性は、他の多くの自動化ライブラリと比較して UI オートメーション ライブラリが優れている点です。ほとんどの種類のコーディングの場合と同様に、通常、UI テスト自動化にはタスクを実行する複数の方法があります。たとえば、[Calculate] ボタン コントロールへの参照を取得する前述のコードの代わりに、次のようなコードを作成することもできます。

AutomationElement aeButton = aeForm.FindFirst(TreeScope.Children,
  new PropertyCondition(AutomationElement.ControlTypeProperty,
    ControlType.Button));

この場合、NameProperty ではなく、ControlTypeProperty を使用して ControlType.Button の値を取得します。このコードを解釈すると、"Form 上で最初に見つかったボタン コントロールをフェッチする" という意味になります。この方法は、Form オブジェクトにボタン コントロールが 1 つしか存在しない場合にのみ有効です。

ControlType クラスは Microsoft UI オートメーションの重要な概念です。このクラスはきわめて単純で、あらゆる型のコントロールの識別に使用できます。キャプションも名前もないテキスト ボックス コントロールへの参照を取得する場合は、別の方法を使う必要があります。その例を次に示します。

AutomationElementCollection aeAllTextBoxes =
  aeForm.FindAll(TreeScope.Children,
    new PropertyCondition(AutomationElement.ControlTypeProperty,
      ControlType.Edit));

AutomationElement aeTextBox1 = aeAllTextBoxes[1];
AutomationElement aeTextBox2 = aeAllTextBoxes[0];

このコードでは、FindAll メソッドを使用して、フォーム コントロールの直接の子となるすべてのテキスト ボックス コントロールを取得しています。テキスト ボックス コントロールを指定する際に、ご想像される ControlType.TextBox などではなく、ControlType.Edit を使用している点に注目してください。また、すべてのテキスト ボックス コントロールを AutomationElementCollection に保存することで、インデックスによってアクセスできるようにしています。

前のセクションの説明を思い出してください。textBox2 コントロールの暗黙のインデックス値が textBox1 コントロールより小さいため、textBox2 はコレクションの配列 [0] に格納され、textBox1 は配列 [1] に格納されます。キャプションがないコントロールへの参照を取得する方法は他にもいくつかありますが、多くの場合、FindAll メソッドを使用すると便利です。

次に、radioButton1 コントロールへの参照の取得方法を示します。

AutomationElement aeRadioButton1 =
  aeForm.FindFirst(TreeScope.Descendants,
    new PropertyCondition(AutomationElement.NameProperty,
      "Arithmetic Mean"));

現在のコンテキストが Form オブジェクトであるため、TreeScope.Children ではなく、TreeScope.Descendants を使用しています。radioButton コントロールは groupBox1 コントロールの子であり、groupBox1 コントロールは Form1 コントロールの子であるため、radioButton コントロールは Form1 の子ではなく、子孫です。別の方法として、最初に groupBox1 コントロールへの参照を Form1 の子として取得し、次に radioButton1 への参照を groupBox1 コントロールの子として取得することもできます。

radioButton1 の場合と同じ方法で radioButton2 (幾何平均) および radioButton3 (調和平均) への参照を取得すれば、テスト対象アプリケーションを実行する準備は完了です。次に、ユーザーによる textBox1 コントロールへの入力をシミュレートする方法を示します。

Console.WriteLine("\nSetting input to '30 60'");
ValuePattern vpTextBox1 =
  (ValuePattern)aeTextBox1.GetCurrentPattern(ValuePattern.Pattern);
vpTextBox1.SetValue("30 60");

SetValue メソッドはお馴染みだと思いますが、ここでは aeTextBox1 オブジェクトで直接 SetVaue にアクセスしていないことに注意してください。代わりに、中継手段として ValuePattern オブジェクトを使用しています。ValuePattern などの AutomationPattern オブジェクトの概念は、UI オートメーション ライブラリに馴染みのないエンジニアにとって特に理解しにくい概念でしょう。パターン オブジェクトは、コントロールの型や外観から独立したコントロールの機能を抽象的に公開する方法と考えることができます。つまり、ValuePattern などの特定の AutomationPattern インスタンスを使用して、コントロールの特定の機能を有効にすることができるということです。

さらにわかりやすく言えば、コントロールの ControlType はコントロールの種類を公開し、コントロールの Pattern はコントロールが実行可能な機能を公開します。ここでは、コントロールは複数のパターンをサポートできるという点が重要です。次に、AutomationPattern を使用して radioButton2 コントロールを選択する方法を示します。

Console.WriteLine("Selecting 'Geometric Mean' ");
SelectionItemPattern ipSelectRadioButton2 =
  (SelectionItemPattern)aeRadioButton2.GetCurrentPattern(
    SelectionItemPattern.Pattern );
ipSelectRadioButton2.Select();

ここでは、SelectionItemPattern を使用して、選択を有効にしています。GetCurrentPattern メソッドは、UI オートメーション ライブラリの初心者にとっては紛らわしい名前です。テスト自動化の観点から見ると、このメソッドは特定の AutomationPattern を取得するのではなく、設定します。ところが、クライアント/サーバーの観点から見ると、自動化クライアント コードは、テスト対象アプリケーションのサーバー コードから特定のプロパティをフェッチする処理を行います。[Calculate] ボタン コントロールのクリックをシミュレートする次のコード例を見れば、話がわかりやすくなります。

  Console.WriteLine("Clicking on Calculate button");
  InvokePattern ipClickButton1 =
    (InvokePattern)aeButton.GetCurrentPattern(
    InvokePattern.Pattern);
  ipClickButton1.Invoke();
  Thread.Sleep(1500);

ここでは、InvokePattern を使用してボタン クリックを有効にし、Invoke メソッドでクリックを実行しています。アプリケーションが応答するまで 1.5 秒停止します。代わりに、遅延ループを使用して、結果を示す textBox2 フィールドが空であるかどうかを定期的にチェックすることもできます。この方法については、後で簡単に例示します。18 種類の AutomationPattern があります。その一覧を図 6 に示します。AutomationPattern クラスを理解することは、Microsoft UI オートメーション ライブラリを使用してテスト対象アプリケーションを実行する方法を習得するうえで重要です。

Figure 6 パターン (Close Figure 6)

DockPattern
ExpandCollapsePattern
GridPattern
GridItemPattern
InvokePattern
MultipleViewPattern
RangeValuePattern
ScrollPattern
ScrollItemPattern
SelectionPattern
SelectionItemPattern
TablePattern
TableItemPattern
TextPattern
TogglePattern
TransformPattern
ValuePattern
WindowPattern

ここまで、テスト自動化のサンプル コードでは、テスト対象アプリケーションを起動し、入力コントロール (textBox1) に「30 60」と入力し、[Geometric Mean] (radioButton2) コントロールを選択し、[Calculate] (button1) コントロールをクリックしました。次に、textBox2 コントロールに予期される値が入力されているかどうかを確認します。

Console.WriteLine("\nChecking textBox2 for '42.4264'");
TextPattern tpTextBox2 =
  (TextPattern)aeTextBox2.GetCurrentPattern(TextPattern.Pattern);
string result = tpTextBox2.DocumentRange.GetText(-1);

ここでも AutomationPattern を使用し、今度は TextPattern を使用して、GetText メソッドの呼び出しを準備します。また、DocumentRange プロパティを使用して GetText を間接的に呼び出しています。これによりドキュメント (この場合はテキスト ボックス) のメイン テキストのテキスト範囲が返されます。GetText の引数は、返される文字列の最大長を制限しないことを示しています。

textBox2 コントロールの内容を読み取る場合、次のように GetCurrentPropertyValue メソッドを使用することもできます。

string result =
  (string)aeTextBox2.GetCurrentPropertyValue(
    ValuePattern.ValueProperty);

GetCurrentPropertyValue メソッドは、プロパティが設定されている場合は AutomationElement コントロールの現在のプロパティを返し、コンテキスト コントロールのプロパティが設定されていない場合はコントロールの既定のプロパティの値を返します。例では、textBox2 コントロールのプロパティを設定していないため、GetCurrentPropertyValue に ValueProperty 引数を指定すると、textBox2 コントロールの既定のプロパティの値 (Text プロパティ) が返されます。GetCurrentProperty を使用すればコーディングの手間が省けますが、実行時にコントロールの現在のプロパティが変更される可能性があるため、確実とは言えません。

手元の計算結果を使用して予期される値と入力値を照合し、テスト シナリオの合否結果を判定します。

if (result == "42.4264") {
  Console.WriteLine("Found it");
  Console.WriteLine("\nTest scenario: Pass");
}
else {
  Console.WriteLine("Did not find it");
  Console.WriteLine("\nTest scenario: *FAIL*");
}

これで、テスト対象アプリケーションを閉じることができます。

Console.WriteLine("\nClosing application in 5 seconds");
Thread.Sleep(5000);
WindowPattern wpCloseForm =
  (WindowPattern)aeForm.GetCurrentPattern(WindowPattern.Pattern);
wpCloseForm.Close();

サンプル コードでは、WindowPattern クラスの Close メソッドを使用しています。これは UI オートメーション ライブラリのテーマを絞った手法です。

テスト対象アプリケーションを閉じる代わりに、次のように始まる [ファイル] メニューの [終了] を操作することもできます。

AutomationElement aeMenu =
  aeForm.FindFirst(TreeScope.Children,
    new PropertyCondition(AutomationElement.ControlTypeProperty,
      ControlType.Menu));

ただし、ControlType.Menu 型は .NET Framework 1.1 の MainMenu コントロールであり、新しい MenuStrip コントロールでは機能しません。サンプル コードでは、テスト対象アプリケーションで MenuStrip を使用しているため、ControlType.Menu 型は使えません。これは珍しい例外です。UI オートメーション ライブラリは多様なユーザー コントロールをサポートし、UIAutomationProvider.dll ライブラリの機能を使用してカスタム コントロールの自動化もサポートしています。これでサンプルのテスト自動化は完成し、テスト ケースの結果を記録して、別のテスト シナリオを起動することができるようになりました。

テスト ハーネスを拡張する

このコラムでは、UI オートメーション ライブラリを使用して固有のテスト自動化を作成するための堅実な基礎を説明しています。これまで見てきたように、このライブラリはきわめて幅広く、その純粋なオブジェクト指向プログラミング モデルは今までに使用してきた他のライブラリとは若干異なる傾向があります。それでも、読者はこのライブラリの使い方にすぐに慣れて、高度な UI テスト自動化を作成できるようになるでしょう。3 つの重要な概念は、すべての UI コントロールの型を識別する ControlType クラス、UI コントロールの機能を識別する AutomationPattern クラス、および特定の UI コントロールを指定する PropertyCondition クラスです。

この記事で示した初歩的なテスト自動化ハーネスの考え方を拡張する方法は多数あります。たとえば、ハーネスをパラメータ化することが考えられます。入力値と予期される値をハードコーディングする代わりに、この情報をコマンド ラインでテスト ハーネスに渡すことができます。さらに優れた方法としては、テスト ケースのデータ ストアをテキスト ファイル、データベース テーブル、または XML ファイルの形式で作成し、各テスト ケースを反復処理し、テスト ケースのデータを解析して、そのデータをテスト ハーネスに渡すことができます。このようなデザイン パターンの詳細については、筆者の 2005 年 8 月のコラム (msdn.microsoft.com/msdnmag/issues/05/08/TestRun) (英語) を参照してください。

このコラムのサンプル コードでは、結果をログに記録していません。ほとんどの場合、コマンド シェルに合否のメッセージを出力するだけではなく、テスト ケースの結果をデータ ストアに記録する必要があります。

UI オートメーション ライブラリがすぐに UI テスト自動化の最も一般的なフレームワークとして普及することは難しいでしょう。何より、現時点でも、標準的かつ包括的で一貫性のある、十分にテスト済みの UI オートメーション ライブラリをソフトウェア エンジニアが無償で入手することができます。このライブラリは、既にパブリック フォーラム、ブログ、サンプル コードなどのコミュニティを生み出しています。

筆者は、やがて UI オートメーション ライブラリが市販の UI テスト自動化フレームワークやカスタム UI 自動化フレームワークなどの代替機能に完全に取って代わるだろうとは考えていません。しかし、UI オートメーション ライブラリにはこれらの代替機能にはない大きな利点があります。市販の UI テスト自動化フレームワークと比べると、UI オートメーション ライブラリは低価格であり、.NET Framework 3.0 に含まれているため、事実上の業界標準と言えます。

少数のソフトウェア エンジニアが作成したカスタム UI テスト自動化と比較して、UI オートメーション ライブラリには、はるかに豊富な機能が含まれており、すべてのカスタム自動化ライブラリの新しい API を習得するのではなく、1 つの API セットについて学習するだけで済みます。しかし、市販の UI テスト自動化フレームワークにも、テスト ケース マネージャやバグ報告ソフトウェア ツールなどのツールと密接に統合されている場合が多いという利点があります。また、軽量版カスタム UI テスト自動化ライブラリには、一般に、急場しのぎのテスト自動化が簡単にできて、特殊なテスト状況に適応しやすいという利点があります。それでも、Windows Vista の導入によって .NET Framework 3.0 の普及が進むと共に、UI オートメーション ライブラリの利用は急速に広まるでしょう。

ご意見やご質問は、James まで英語でお送りください。 testrun@microsoft.com.

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


基礎: Visual Studio 2008 の WCF 向け新機能

基礎
Visual Studio 2008 の WCF 向け新機能
Juval Lowy

コードのダウンロード : : Foundations2008_02.exe (329KB)
コンテンツ
複数のバージョンの .NET Framework からターゲットを指定する
WCF 提供のホスト
WCF 提供のテスト クライアント
WCF サービス ライブラリ
サービス参照を追加する

Visual Studio® 2008 と基になる Microsoft® .NET Framework 3.5 では、Windows® Communication Foundation (WCF) 向けの新しいツールとサポートが提供されます。これらのツールとサポートでは、WCF 1.0 (.NET Framework 3.0 でリリース) の基本機能は変更されません。むしろ、WCF が拡張され、完全なものになります。

Visual Studio 2008 では、WCF の手動タスクが自動化されます。これには、プロキシ参照の更新、単純なホスト プロジェクトの作成のような反復タスクの回避などがあります。また、Visual Studio では、複数のバージョンからのターゲット指定やデータ コントラクト型の共有といったいくつかの難題に対処できます。このコラムでは、新機能、新機能の利点と欠点、および欠点の回避策について見ていきます。ここでは C# プロジェクト設定を使用しますが、特に指定のない限り、Visual Basic® にもすべて同様に当てはまります。

複数のバージョンの .NET Framework からターゲットを指定する

以前のリリースの Visual Studio では、常に、そのバージョンに付属する .NET Framework をターゲットとしていました。たとえば、Visual Studio 2005 では、.NET Framework 2.0 をターゲットとするアセンブリのみを生成できます。この方法は、多くの開発者がかかえている現状を反映していませんでした。通常、開発者は、次期バージョンのアプリケーションの開発に新しいバージョンの Visual Studio を使用しながら、以前のバージョンの .NET Framework 用に作成した古いバージョンのアプリケーションのメンテナンスも行う必要があります。

さらに、この場合、開発者が以前のバージョンの .NET Framework 用に作成したアプリケーションをメンテナンスする際に、コードのリファクタリングのサポートなど、Visual Studio 2005 で導入された生産性を向上するメリットを得られませんでした。

複数のバージョンの .NET Framework からターゲットを指定する機能がないことが問題でした。複数のバージョンの Visual Studio をインストールするか、テスト ビルドと配置ビルドを分離して補正する必要がありました。Visual Studio 2008 では、複数のバージョンの .NET Framework からターゲットを指定するための (完全ではなくても) 十分なサポートを提供することで、この問題に対処しています。.NET Framework 3.0 と .NET Framework 3.5 では、実際には .NET Framework 2.0 と同じバージョンの CLR が使用され、違いは新しく参照されるアセンブリにしかありません。そのため、Visual Studio では、同じランタイムをターゲットとしながら、.NET Framework 2.0、3.0、および 3.5 の複数のバージョンからターゲットを指定できます (.NET Framework のバージョン番号はランタイムのバージョンではなくリリース番号に対応し、ランタイムのバージョンは CLR 2.0 のままです)。

Visual Studio 2008 では、プロジェクトの [Properties] (プロパティ) の [Application] (アプリケーション) ペインに、[Target Framework] (ターゲット フレームワーク) という新しいコンボ ボックスがあります。これにより、.NET Framework 2.0、3.0、および 3.5 をターゲットに指定できます (図 1 参照)。

図 1 Visual Studio 2008 の [Target Framework] (ターゲット フレームワーク) プロパティ
図 1 Visual Studio 2008 の [Target Framework] (ターゲット フレームワーク) プロパティ(画像を縮小するには、ここをクリックします)
図 1 Visual Studio 2008 の [Target Framework] (ターゲット フレームワーク) プロパティ
図 1 Visual Studio 2008 の [Target Framework] (ターゲット フレームワーク) プロパティ (画像を拡大するには、ここをクリックします)

[Target Framework] (ターゲット フレームワーク) 値は開発時にのみ有効になり、実行時には無効になります (アセンブリは依然として .NET 2.0 CLR を対象とします)。選択する値は、アセンブリをビルドできる最も古いバージョンの .NET Framework を表します。新しいプロジェクトは、既定で .NET Framework 3.5 をターゲットに指定するように構成されます。そのため、参照の追加時にはやや複雑になります。[Target Framework] (ターゲット フレームワーク) のバージョンをダウングレードするときに、それより新しいバージョンのアセンブリを参照していると、Visual Studio 2008 によってメッセージが表示され、参照エラーが発生して、ビルドに失敗します。Visual Studio 2008 では、プロジェクトが使用しているバージョンよりも新しいバージョンの .NET Framework を必要とする .NET Framework アセンブリへの参照は追加できません。同じソリューションで新しいバージョンが設定された別のプロジェクトへの参照を追加すると、Visual Studio 2008 から競合の可能性が警告されます。アセンブリを検索して参照を追加する場合、Visual Studio 2008 がそれを阻止することはありません。

C# (Visual Basic は除く) では、言語と複数バージョンからのターゲット指定に関して、コンパイラのバージョンを制限することで、.NET Framework 2.0 または 3.0 のプロジェクトの匿名型や拡張メソッドなどの機能の使用を制限できることに注意してください。これを実行するには、[Build] (ビルド) ペインで [Advanced] (詳細) をクリックし、(既定の代わりに、まだ今のところ標準化されていない) ISO-2 (C# 2.0) を言語バージョンとして選択します。

Visual Studio 2005 WCF プロジェクトを Visual Studio 2008 で開くと、アップグレード処理によってフレームワークのバージョンが 2.0 のまま維持されます。それでも実際に機能しますが (既に述べたように、基になるランタイムのバージョンは変更されていません)、必要に応じて手動でバージョンを 3.0 または 3.5 に設定することをお勧めします。

[Target Framework] (ターゲット フレームワーク) のバージョンが最も重要となるのは、新しいプロジェクト テンプレートを活用するときです。WCF ワークフロー プロジェクトおよびシンジケーション プロジェクトは、.NET Framework 3.5 をターゲットに指定してビルドする必要があります。サービス ライブラリ プロジェクトは、.NET Framework 3.0 または 3.5 をターゲットに指定する必要があります。[Add Service Reference] (サービス参照の追加) の機能 (このコラムで後述) を使用できるのは、プロジェクトに .NET Framework 3.0 または 3.5 を選択しているときのみです。

WCF 提供のホスト

Visual Studio 2008 には、WcfSvcHost.exe という既存の汎用サービス ホストが付属しています。これは、C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE にあります。使いやすくするため、この場所をシステムの Path 変数に追加することをお勧めします。WcfSvcHost は、2 つのパラメータを受け取る、簡単なコマンド ライン ユーティリティです。受け取るパラメータは、サービス クラスを含む .NET アセンブリへのファイル パスと、ホスト .config ファイルへのファイル パスです。次に例を示します。

WcfSvcHost.exe /service:MyService.dll  /config:App.config

指定するサービス アセンブリは、クラス ライブラリ アセンブリ (DLL) またはアプリケーション アセンブリ (EXE) です。WcfSvcHost により、新しいプロセスが開始され、指定された .config ファイルの Services セクションに一覧されたすべてのサービス クラスが自動的にホストされます。これらのサービス クラス、サービス コントラクト、およびデータ コントラクトはいずれも、public 型である必要はなく、internal に設定できることに注意してください。また、自動ホストされるサービスは、メタデータを提供する必要はありませんが、選択に応じてメタデータを公開できます。

WcfSvcHost は、デスクトップのトレイ アイコンとして常駐する Windows フォーム アプリケーションです。ホストを終了するには、単純に、トレイ アイコンのコンテキスト メニューから [Exit] (終了) を選択します。この方法によるホストの終了は、適切な終了方法ではありません。WcfSvcHost では、現在進行中のすべての呼び出しを中止するため、クライアントが例外を受け取る可能性があります。WcfSvcHost のトレイ アイコンをクリックすると、ホストされるすべてのサービスを一覧するダイアログが表示されます (図 2 参照)。

図 2 WcfSvcHost のサービスの一覧
図 2 WcfSvcHost のサービスの一覧(画像を縮小するには、ここをクリックします)
図 2 WcfSvcHost のサービスの一覧
図 2 WcfSvcHost のサービスの一覧 (画像を拡大するには、ここをクリックします)

ダイアログには、サービスの状態やメタデータ アドレスも表示されます。これらはクリップボードにコピーでき、サービスへの参照の追加時などに後から使用することもできます。WcfSvcHost の UI を閉じても、ただアイコン化されてトレイの表示に戻るだけです。

WcfSvcHost は、開発中に、サービス ライブラリに付属する個別のホスト アセンブリの必要性をなくすようにデザインされています。こうしたホスト プロジェクトの開発は反復タスクです。これらのホストには、通常、同じコード行が繰り返し含まれます。また、複数のサービス ライブラリがある場合に、ソリューションが膨れ上がる傾向があります。開発やテストを目的として、Visual Studio 2008 サービス ライブラリ プロジェクトに WcfSvcHost を直接統合できます。プロジェクトのプロパティの [Debug] (デバッグ) ペインで、開始する外部プログラムとして WcfSvcHost.exe を指定し、クラス ライブラリ名と (bin フォルダに自動生成および自動コピーされた) .config ファイルを引数として指定します。

完了したら、(以前はできなかった) クラス ライブラリの実行時に、その処理にデバッガがアタッチされて、WcfSvcHost によって自動的にホストされます。デバッグを停止すると、Visual Studio 2008 によってホストが不適切に中止されます。

WcfSvcHost が必要とするのは .NET Framework 3.0 だけなので、.NET Framework 3.0 アプリケーションや Visual Studio 2005 プロジェクトでも WcfSvcHost を使用できます。単純に、Visual Studio 2008 がインストールされているコンピュータから WcfSvcHost をコピーします。作業を簡単にするため、.NET Framework 3.0 コンピュータのグローバル アセンブリ キャッシュ (GAC) に WcfSvcHost を追加することをお勧めします。

WcfSvcHost の最後の機能は、クライアント アプリケーションを自動的に起動して、そのアプリケーション固有の省略可能なパラメータをクライアントに提供する機能です。

WcfSvcHost.exe /service:MyService.dll  /config:App.config
               /client:MyClient.exe    /clientArgs:123,ABC

これは、自動テストと、ホストとクライアントの両方を起動する簡単な配置シナリオで役立ちます。

WcfSvcHost の最大の欠点は、簡単なシナリオにしか適していない点です。適しているのは、開く前にホスト インスタンスにプログラムからアクセスしたり、一度開いたイベント モデルにプログラムからアクセスしたりする必要のないシナリオです。IIS や Windows プロセス アクティブ化サービス (WAS) のホスティングとは異なり、これらと同等のサービス ホスト ファクトリのサポートがありません。したがって、ベース アドレスの動的な追加、エンドポイントの構成、呼び出しの抑制、ホスト レベルでのカスタム動作の構成などの機能がありません。WCF を使用した経験から言うと、簡単なシナリオでない場合、最終的に、ホスト インスタンスへのプログラムからのアクセスが必要になります。そのため、WcfSvcHost は、WAS や専用の自己ホストのように、完全な稼働に適したホストにはなりません。

WCF 提供のテスト クライアント

Visual Studio 2008 には、サービス ホストに加えて、基本テスト用の簡単で汎用的なテスト クライアントが付属しています。このテスト クライアントを使用すると、ほとんどのサービスの操作を呼び出すことができます。通常のインストールを行った場合、テスト クライアント WcfTestClient.exe は、C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE にあります。WcfTestClient には、テストを実行するサービスのメタデータ アドレスを含むコマンド ライン引数を 1 つ指定する必要があります。

WcfTestClient.exe http://localhost:9000/

あらゆるメタデータ アドレス (HTTP-GET など) や、HTTP、TCP、または IPC (名前付きパイプ) 経由のメタデータ エンドポイントを指定できます。また、複数のメタデータ アドレスを指定することもできます。

WcfTestClient.exe http://localhost:8000/ net.tcp://localhost:9000/MEX

WcfTestClient は、Windows Forms 3.5 アプリケーションです (図 3 参照)。この図では、左側のツリー コントロールに、テスト対象のサービスとそのエンドポイントが含まれます。エンドポイントのコントラクトを展開し、操作を選択できます。その呼び出しに固有の情報が、右ペインのタブに表示されます。図 4 は、簡単なコントラクトと実装の例です。

Figure 4 A Sample Service (Close Figure 4)
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   string MyMethod(int someNumber,string someText);
}
class MyService : IMyContract
{
   public string MyMethod(int someNumber,string someText)
   {
      return "Hello";
   }
}
図 3 WcfTestClient を使用する
図 3 WcfTestClient を使用する(画像を縮小するには、ここをクリックします)
図 3 WcfTestClient を使用する
図 3 WcfTestClient を使用する (画像を拡大するには、ここをクリックします)

図 3 に示すように、メソッドのタブの [Request] (要求) では、操作パラメータとして整数および文字列を指定できます。[Invoke] (呼び出し) をクリックすると、呼び出しがサービスにディスパッチされ、戻り値または出力パラメータが [Response] (応答) に表示されます。操作が一方向の場合、WcfTestClient によって、操作のディスパッチが成功したことを通知するメッセージ ボックスが表示されます。例外が発生した場合、WcfTestClient によってメッセージ ボックスに例外情報が表示され、新たな呼び出しを発行できます。

WcfTestClient では、テスト対象のサービスとの、トランスポート レベル (またはその他すべて) のセッションは維持されません。呼び出しはすべて、新しいプロキシ インスタンスで行われます。また、UI の応答性が維持されるように、すべての呼び出しは非同期に行われます。ただし、呼び出しが非同期のため、WcfTestClient では、一度に 1 つしか呼び出しをディスパッチできません。

WcfTestClient は、(.config ファイルを含む) プロキシ ファイルからアセンブリを自動的に作成し、それを一時的な場所から読み込むことで機能します。ツリーで [Config File] (構成ファイル) をクリックすると、その .config ファイル (サービス参照の追加時に生成されたファイルと同じ) を実際に取得し、独自のタブに表示できます。

従来の Visual Studio ASMX Web サービスのテスト ページとは異なり、WcfTestClient では、列挙型、クラスや構造体などの複合パラメータ (それぞれが他のクラスや構造体の複合)、およびパラメータのコレクションや配列で操作を呼び出すことができます。単純に、[Request] (要求) の項目を展開し、その値をドロップダウン リスト (列挙値など) から選択して、呼び出しを実行します。操作がコレクションまたは配列を受け取る場合、長さも設定する必要があります。たとえば、次の操作について、図 5 に結果の要求と応答を示します。

図 5 配列の長さと値を指定する
図 5 配列の長さと値を指定する(画像を縮小するには、ここをクリックします)
図 5 配列の長さと値を指定する
図 5 配列の長さと値を指定する (画像を拡大するには、ここをクリックします)

[OperationContract]
bool ProcessNumbers(int[] numbers]

同様に、[Response] (応答) ペインには、返された複合値または出力パラメータが含まれます。これにより、テストを実行する別のサービスを指定するためには、シャットダウンし、コマンド ライン引数を変更して、再起動する必要があるという、WcfTestClient の欠点が生じます。サービスのアドレスを同じように GUI で提供できるほうが適切でしょう。

WcfTestClient は、Visual Studio 2008 ソリューションに直接統合できます。まず、クラス ライブラリ プロジェクトをソリューションに追加し、必要のないすべての参照、フォルダ、ソース ファイルを削除します。次に、WcfTestClient.exe を外部開始プログラムとして設定し、テスト対象のサービスのメタデータ アドレスを指定します。たとえば、IIS または WAS のホスト プロジェクトの .svc アドレス、さらに言うと、ソリューション内外のホスト プロジェクトの他のメタデータ アドレスなどです。

.NET Framework 3.5 内部のツリー グリッド コントロール (複合パラメータの表示に使用) が使用されるため、WcfTestClient は、.NET Framework 3.0 しかインストールされていないコンピュータでは使用できないことに注意してください。

もちろん、WcfTestClient と WcfSvcHost の両方をシングル ステップで組み合わせて、サービス ライブラリでサービスを自動的にホストし、テストを実行することができます。

WcfSvcHost.exe /service:MyService.dll    /config:App.config
               /client:WcfTestClient.exe
               /clientArgs:http://localhost:9000/

ただし、WcfSvcHost では、メタデータ引数の指定はオプションです。既定では、WcfSvcHost によって、指定のクライアント アプリケーションに、サービスの .config ファイルで見つかったメタデータ アドレスがパイプされます。サービスから固有のメタデータが提供されない場合、またはテスト クライアントで別のアドレスを使用する場合にのみ、メタデータ アドレスを明示的に指定する必要があります。また、サービスの .config ファイルに、特定のサービスのメタデータ エンドポイントが複数含まれる場合、それらは HTTP、TCP、IPC、HTTP-GET という優先順位で提供されます。ホストおよびテストのシームレスなエクスペリエンスのため、Visual Studio 2008 にこれらのステップを組み込むことができます。.config ファイルと共に WcfSvcHost.exe をスタートアップ プログラムとして指定し、WcfTestClient.exe をクライアントとして指定します。

WCF サービス ライブラリ

ターゲット フレームワークの機能として、Visual Studio 2008 には、いくつかの新しい WCF プロジェクト テンプレートが用意されています。図 6 に示すように、[New Project] (新しいプロジェクト) ダイアログ ボックスでは、ターゲット フレームワークのバージョン (2.0、3.0、または 3.5) を指定できる、コンボ ボックスが提供されます。

図 6 WCF プロジェクト テンプレート
図 6 WCF プロジェクト テンプレート(画像を縮小するには、ここをクリックします)
図 6 WCF プロジェクト テンプレート
図 6 WCF プロジェクト テンプレート (画像を拡大するには、ここをクリックします)

.NET Framework 2.0 を選択すると、新しいテンプレートは使用できません。.NET Framework 3.0 では、WCF サービス ライブラリという新しいプロジェクト テンプレートを使用できます。このプロジェクトの種類は、単に、WcfSvcHost および WcfTestClient の組み込みの使用方法で、既に説明した手法 (2 つの組み合わせ) に非常に似ています。プロジェクト ファイルには、WCF サービス ライブラリの新しい ProjectTypeGuids 要素が含まれます。そのため、WCF サービス ライブラリのテンプレートでは、WcfSvcHost.exe をスタートアップ プログラムとして指定したり .config ファイルを指定したりする必要はないことに注意してください。

残念ながら、テンプレートには、デバッガを停止してもテスト クライアントが終了されないという副作用があります。これにより、やがて、デスクトップには孤立したクライアントがいくつもある状態になります。この問題を解決するには、単純に、既に説明した手動による明示的な方法に戻します。

WCF サービス ライブラリでは、サービス コントラクト、その実装、および一致する .config ファイル用の、簡単なテンプレートも提供されます。

シンジケーション サービス ライブラリでは、WCF エンドポイント経由で RSS フィードを実装でき、フィード、その実装、および対応する .config ファイルを返す簡単なサービス コントラクトを使用できます。フィードは、他のサービスのようにホストおよび公開できます。シンジケートされたエンドポイントでは、新しい WebHttpBinding バインディングが使用されます。この新しいバインディングは、Web 要求を受信するように設計されていて、通常のサービスの呼び出しには使用できません。

シーケンシャル ワークフロー サービス ライブラリのテンプレートでは、エンドポイントのコントラクト操作をワークフロー アクティビティとして実装できます。さらに言うと、ワークフローをサービスとして公開できます。プロジェクトには、簡単なコントラクトおよび対応する .config ファイルを実装する、シーケンシャル アクティビティが 1 つ含まれます。クライアントは、依然として、従来のエンドポイントのように見えるものと対話しますが、実装は完全にワークフロー駆動型です。

ステート マシン ワークフロー サービス ライブラリのテンプレートでは、操作 (状態遷移のトリガ) を実装するため、シーケンシャル ワークフローの代わりにステート マシンが使用されます。ワークフロー プロジェクト テンプレートでは、プレーンな WCF サービス ライブラリとまったく同様に、WcfSvcHost と WcfTestClient が使用されます。ワークフロー テンプレートでは、ワークフロー インスタンス ID を渡して永続的なワークフローをサポートするために、新しいコンテキスト バインディングも使用されます。これらのバインディングについては、今後のコラムで詳しく説明する予定です。

サービス参照を追加する

Visual Studio 2005 の .NET Framework 3.0 向け拡張機能では、多数の SvcUtil の高度な機能なしで、WCF サービスへの参照を追加するための基本的な機能が提供されました。図 7 に示すように、Visual Studio 2008 には、新しいサービス参照のダイアログがあります。

図 7 [Add Service Reference] (サービス参照の追加) ダイアログ
図 7 [Add Service Reference] (サービス参照の追加) ダイアログ(画像を縮小するには、ここをクリックします)
図 7 [Add Service Reference] (サービス参照の追加) ダイアログ
図 7 [Add Service Reference] (サービス参照の追加) ダイアログ (画像を拡大するには、ここをクリックします)

新しいダイアログは、どのプロジェクトでも表示できます。表示するには、ソリューション エクスプローラで、プロジェクト内の任意の場所を右クリックし、コンテキスト メニューから [Add Service Reference] (サービス参照の追加) を選択します。このオプションを有効にするには、プロジェクトが、.NET Framework 3.0 以上をターゲットに指定するように構成されている必要があることに注意してください。

[Add Service Reference] (サービス参照の追加) ダイアログ ボックスでは、まず、(ダイアログにも示されているように、サービスの URL ではなく) サービスのメタデータ アドレスを指定する必要があります。続いて、[Go] (移動) をクリックし、(ラベルで示されている [Services] (サービス) ではなく) 使用できるサービス エンドポイントを表示します。生成されたプロキシを含めるように、名前空間 (MyService など) を指定する必要があります。次に、[OK] をクリックし、プロキシを生成して .config ファイルを更新します。多くの場合、Visual Studio 2008 では、最もわかりやすいバインディング値の推定はできないことに注意してください。そのため、そのバインディングのすべての既定値を宣言することになり、.config ファイルが乱雑になります。この問題は、Visual Studio の今後のリリースで対処される予定です。.config ファイルを維持するには、参照を追加する前に .config ファイルを開き、参照を追加して、元に戻す操作 (Ctrl+Z) を 1 回実行してから、client セクションに .config ファイルのエントリを手動で追加します。

[Discover] (検出) を使用すると、Web サイト プロジェクトまたは新しい WCF サービス ライブラリのいずれかでホストされている限り、独自のソリューションの WCF サービスを検出できます。Web サイト プロジェクトの場合、Visual Studio 2008 によって、IIS からメタデータが取得されるか、ASP.NET ファイル システム ベース開発サーバーが起動されます。WCF サービス ライブラリの場合、WCF によって、そのホスト (WcfSvcHost) が自動起動され、メタデータが取得されます。

[Advanced] (詳細) をクリックすると、設定ダイアログが表示されます。これにより、SvcUtil を使用している場合とほとんど同じように、プロキシの生成を調整できます (図 8 参照)。より直感的なオプションにより、生成されるプロキシおよびコントラクトの可視性 (パブリックまたは内部) を構成できます。既存の (通常はカスタムの) メッセージ形式に従う必要がある、高度な相互運用性シナリオのデータ型のメッセージ コントラクトを生成できます。そして、[Add Web Reference] (Web 参照の追加) をクリックして、参照を古い ASMX Web サービス参照に変換できます。

図 8 サービス参照の詳細オプション
図 8 サービス参照の詳細オプション(画像を縮小するには、ここをクリックします)
図 8 サービス参照の詳細オプション
図 8 サービス参照の詳細オプション (画像を拡大するには、ここをクリックします)

[Generate asynchronous operations] (非同期操作を生成する) チェックボックスにより、インポートされたコントラクトの操作ごとに、対応する Begin<操作> 要素および End<操作> 要素のペアが追加されます。これにより、クライアントでは、ワーカー スレッドで非同期に呼び出しを発行し、完了時のコールバック メソッドを提供するか完了をブロックすることで、後から操作の完了と同期させることができます。たとえば、次のようなコントラクト定義があるとします。

[ServiceContract]
interface ICalculator
{
   [OperationContract]
   int Add(int number1,int number2);
}

インポートされたコントラクトは、図 9 のようになります。

Figure 9 Imported Async Contract (Close Figure 9)
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   int Add(int number1,int number2);

   [OperationContract(AsyncPattern = true)]
   IAsyncResult BeginAdd(int number1,int number2,
                         AsyncCallback callback,object asyncState);
   int EndAdd(IAsyncResult result);

   //Rest of the methods
}

非同期の呼び出しに対応するクライアント コードは、次のようになります。

CalculatorClient proxy = new CalculatorClient();
int sum;
AsyncCallback completion = (result)=>
                           {
                              sum = proxy.EndAdd(result);
                              Debug.Assert(sum == 5);
                              proxy.Close();
                           };
proxy.BeginAdd(2,3,completion,null);

これらのメソッドをそのまま使用することもできますが、Begin<操作> に提供された完了時のコールバックは、スレッド プールからのスレッドで呼び出されます。これは、コールバックが、特定のスレッドとの関係を持つリソースへのアクセスに使用される場合、重大な問題となります。典型的な例は、(UI がブロックされないように) 時間のかかるサービスの呼び出しを非同期にディスパッチし、呼び出しの結果で UI を更新する Windows フォーム (または WPF) アプリケーションです。UI スレッドしか更新できないため、元の Begin<操作> は使用できません。この状況に適切に対処するため、ClientBase<T> 基本クラスが InvokeAsync プロテクト メソッドによって拡張されています。図 10 に示すように、これは、クライアントの同期コンテキストを取得し、それを使用して完了時のコールバックを呼び出します。

Figure 10 Async Callback Management in ClientBase<T> (Close Figure 10)
public abstract class ClientBase<T> : ...
{
   protected delegate IAsyncResult BeginOperationDelegate(
             object[] inValues,AsyncCallback asyncCallback,object state);

   protected delegate object[] EndOperationDelegate(IAsyncResult result);

   //Picks up sync context used for completion callback
   protected void InvokeAsync(BeginOperationDelegate beginOpDelegate,
                              object[] inValues,
                              EndOperationDelegate endOpDelegate,
                              SendOrPostCallback opCompletedCallback,
                              object userState)
   {}
   //More members
}

ClientBase<T> では、非同期呼び出しの開始と終了に使用される、イベント引数のヘルパ クラスと 2 つの専用のデリゲートも提供されます。ClientBase<T> から派生、生成されたプロキシ クラスでは、基本的な機能が使用されます。プロキシには、<操作>Completed というパブリック イベントがあります。これは、非同期メソッドの結果固有の厳密に型指定されたイベント引数のクラスを使用します。また、呼び出しを非同期にディスパッチする <操作>Async というメソッドも 2 つあります。

partial class AddCompletedEventArgs : AsyncCompletedEventArgs
{
   public int Result
   {get;}
}

class CalculatorClient : ClientBase<ICalculator>,ICalculator
{
   public event EventHandler<AddCompletedEventArgs> AddCompleted;

   public void AddAsync(int number1,int number2,object userState);
   public void AddAsync(int number1,int number2);

   //Rest of the proxy
}

クライアントでは、イベント ハンドラを <操作>Completed イベントにサブスクライブすることもできます。これにより、そのハンドラが完了時に呼び出されます。Begin<操作> ではなく <操作>Async を使用する場合の大きな違いは、図 11 に示すように、<操作>Async メソッドでは、クライアントの同期コンテキストが取得され、その同期コンテキストで <操作>Completed イベントが呼び出されることです。

Figure 11 UI-Friendly Asynchronous Call Invocation (Close Figure 11)
partial class CalculatorForm : Form
{
   CalculatorClient m_Proxy;

   public MyClient()
   {
      InitializeComponent();

      m_Proxy = new CalculatorClient();
      m_Proxy.AddCompleted += OnAddCompleted;
   }
   void CallAsync(object sender,EventArgs args)
   {
      m_Proxy.AddAsync(2,3); //Sync context picked up here
   }
   //Called on the UI thread
   void OnAddCompleted(object sender,AddCompletedEventArgs args)
   {
      Text = "Sum = " + args.Result;
   }
}

[Collection type] (コレクション型) コンボ ボックスを使用すると、サービス メタデータの特定の種類のコレクションおよび配列をクライアントに表す方法を指定できます。たとえば、サービス操作によって IEnumerable<T> コレクション、IList<T> コレクション、または ICollection<T> コレクションのいずれかが返されると、プロキシは既定でそれを 1 つの配列として表します。たとえば、次のようなサービス側の操作があるとします。

[OperationContract]
IEnumerable<int> GetNumbers();

これは、プロキシで次のように表されます。

[OperationContract]
int[] GetNumbers();

ただし、Visual Studio 2008 では、データ バインドに BindingList、List<T>、Collection、LinkedList<T> など、別のコレクションを使用するように要求できます。変換が可能な場合、プロキシでは、次のように、配列ではなく要求されたコレクション型が使用されます。

[OperationContract]
List<int> GetNumbers();

同様の機能は、ディクショナリにも使用できます。サービス操作によって、次のようなシリアル化可能なディクショナリが返されるとします。

[Serializable]
class MyDictionary<K,T> : IDictionary<K,T>
{...}

[OperationContract]
MyDictionary<int,string> GetDictionary();

通常、この場合、プロキシ クラスでは、そのディクショナリは Dictionary<T,K> として表されます。これは、次の [Dictionary collection type] (ディクショナリ コレクション型) コンボ ボックスの既定値です。

[OperationContract]
Dictionary<int,string> GetDictionary();

ただし、別のディクショナリ型 (SortedDictionary<T,K>、HashTable、ListDictionary など) を要求できます。その結果、可能であれば、プロキシでは代わりにそのディクショナリが使用されます。

[OperationContract]
SortedDictionary<int,string> GetDictionary();

新しいサービス参照機能の中で最も重要な機能は、アセンブリ間でデータ コントラクトの型を共有できる機能です。Visual Studio 2005 では、クライアントが、同じデータ コントラクトをサポートする 2 つの独立したサービスへのサービス参照を追加すると、クライアントでは、同じデータ コントラクトを表す 2 つの型が同じ型でありながら個別に取得されます。Visual Studio 2008 の既定では、クライアントが参照するアセンブリに、参照されるサービスのメタデータで公開されるデータ コントラクトの型に一致するデータ コントラクトの型が既にあれば、Visual Studio 2008 によってその型が再びインポートされることはありません。もう一度強調しますが、既存のデータ コントラクトの参照は、クライアント プロジェクト自体ではなく、参照される別のアセンブリに含まれていなければなりません。この制限は、Visual Studio の今後のリリースで対処される予定です。現時点では、回避方法およびベスト プラクティスは明らかです。共有されるすべてのデータ コントラクトを指定のクラス ライブラリに組み込み、すべてのクライアントがそのアセンブリを参照するようにすることです。

サービス参照の詳細設定ダイアログを使用すると、データ コントラクトの共有を構成できます。[Reuse types in the referenced assemblies] (参照されるアセンブリの型を再利用する) チェック ボックスは既定でオンになっていますが、この機能は無効にすることもできます。その名前とは無関係に、サービス コントラクトではなく、データ コントラクトのみが共有されます。その下のオプション ボタンを使用すると (図 8 参照)、参照されるすべてのアセンブリ間でデータ コントラクトを再利用するように Visual Studio 2008 を設定することもできます。また、共有を特定のアセンブリに制限することもできます。それには、一覧で、特定のアセンブリの横にあるチェック ボックスをオンにします。

参照が追加されると、プロジェクトには、[Service References] (サービス参照) という新しいフォルダが追加されます。そのフォルダ内には、参照される各サービスのサービス参照の項目があります (図 12 参照)。

図 12 サービス参照のフォルダ
図 12 サービス参照のフォルダ

参照を右クリックし、[Update Service Reference] (サービス参照の更新) をクリックすることで、プロキシの再生成およびクライアントの .config ファイルの更新をいつでも実行できます。これは、サービス参照の項目に、使用される元のメタデータ アドレスを記録するファイルが含まれるため可能です。

また、[Configure Service Reference] (サービス参照の構成) をクリックして、参照の追加時に使用した詳細設定ダイアログと同様のダイアログを表示することもできます。サービス参照の構成ダイアログを使用すると、サービスのメタデータ アドレスおよび残りのプロキシ詳細設定を変更できます。

ご意見やご質問は、こちらまで英語でお送りください。 mmnet30@microsoft.com.

Juval Lowy は、WCF トレーニングと WCF アーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。彼は、シリコン バレー地域の Microsoft Regional Director も務めています。最近の著書としては、『Programming WCF Services』 (O'Reilly、2007) があります。連絡先は、www.idesign.net です。


.NET の問題: ストリームのパイプライン処理

.NET の問題
ストリームのパイプライン処理
Stephen Toub

Q アプリケーションで大量のデータを暗号化および圧縮しています。これらは計算を集中的に実行する操作であるため、タスク マネージャに表示される CPU 使用率は 100% に達すると思っていましたが、私のデュアルコア コンピュータでは使用率が最高でも 50% ほどにしか達しませんでした。この理由はコアが 1 つしか使用されていないからだと思いますが、この処理にかかる時間がそれほど短くないことを考えると、もったいないことをしている気がします。この暗号化および圧縮処理で両方のプロセッサを使用する方法はありますか。使用しているのは Microsoft® .NET Framework の CryptoStream と GZipStream です。

A まず確認ですが、圧縮操作は暗号化操作の後に実行していますか。そうでない場合は、そうなるように順序を入れ替えてください。厳重に暗号化を行うと、比較的展開しにくいデータが生成されます。圧縮操作の後に暗号化操作を実行するように順序を入れ替えると、ファイルのサイズが小さくなるだけでなく、暗号化するデータのサイズも小さくなるため、ほとんどの場合は暗号化操作が短時間で完了するようになります。この例を示すために、Gutenberg Project (www.gutenberg.org) から『War and Peace』(戦争と平和) のテキストをダウンロードし、2 つの順序で処理を実行しました。(RijndaelManaged と既定のサイズのキーを使用して) 暗号化を実行し、その後に圧縮を実行したところ、処理後のデータ ストリームは、その逆の順序で処理されたデータ ストリームの 2.5 倍の大きさになり、処理時間も 1.5 倍かかりました。

では、実際の質問にお答えします。はい、使用できる方法はいくつかあります。1 つ目は、実際の圧縮操作と暗号化操作を並列化することです。GZipStream と CryptoStream 内の機能を再実装したくはない (しない方がよい) でしょうから、.NET Framework チームからこの並列化機能が提供されるまで、代わりに使用できる方法が必要です。

実際の出力形式にこだわらない (圧縮する必要があるが、gzip 標準に準拠した形式でもかまわない) のであれば、入力データをいくつかのブロックに分割し、それぞれのブロックを並列に処理するとよいでしょう。たとえば、使用中のデュアルコア コンピュータで、現在 GZipStream に渡している入力バイト配列を半分に分割し、各配列を別々の GZipStream で処理します。処理の完了後、これらの配列を出力ファイルに順次保存することができます。

しかし、出力には何らかのヘッダー情報を含める必要があるでしょう。これは、ある GZipStream の末尾と次の GZipStream の先頭を展開処理で適切に識別できるようにするためです。現在の GZipStream では入力ストリームから読み取られたデータがバッファに格納されるため、入力ストリームのデータが実際に必要な量よりも多く使用される可能性があります。つまり、最初の GZipStream の展開が完了した後、入力ストリーム内のデータの位置を再設定する必要があります。

データをブロックに分割する方法の利点は、処理を複数のコアに分散することが比較的容易であることです。これは単に、プロセッサと同じ数だけブロックを作成できるためです。しかし、コアが 2 つしかなく、実行する操作も 2 つ (圧縮と暗号化) である場合、より実用的な解決策は、並列に実行されるストリームのパイプライン処理を作成することです。この処理を作成すると、ある操作を 1 つのプロセッサで実行しながら、もう 1 つのプロセッサで別の操作を実行できます。ただしこの場合、2 つのプロセッサが同じデータを同時に処理できず、出力が 2 つ (1 つは圧縮されたもの、もう 1 つは暗号化されたもの) 生成されることは明らかであるため、ここで解決しようとしている問題 (圧縮および暗号化された 1 つの出力) を考えると、あまり役立つとは言えません。ですが、皆さんがおそらく日常的に行っているであろう作業を模倣することはできます。これを行うには、次のようにデコレータ パターンを使用して、あるストリームの出力を次のストリームの入力として渡します (図 1 参照)。

図 1 ストリーム間でのデータの受け渡し
図 1 ストリーム間でのデータの受け渡し

using (CryptoStream encrypt = new CryptoStream(
        output, transform, CryptoStreamMode.Write))
using (GZipStream compress = new GZipStream(
        encrypt, CompressionMode.Compress, true))
    CopyStream(input, compress);
...
static void CopyStream(Stream input, Stream output){
    byte[] buffer = new byte[0x1000];
    int read;
    while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
        output.Write(buffer, 0, read);
}

上記のコードでは、CopyStream メソッドが入力ストリームのデータを圧縮ストリームにコピーしています。圧縮ストリームはバッファに格納されているデータを圧縮した後、そのデータを暗号化ストリームの入力として書き込みます。そして同様に、暗号化ストリームはバッファに格納されているデータを暗号化した後、今度はそのデータを出力ストリームに書き込みます。これはパイプライン処理として知られています。

パイプライン処理を並列化することは、"現実の世界" でもよくある非常に自然な考え方です。たとえば、何人かのグループが、招待状を送付するための作業を行うとしましょう。ある人 (1 人目) は封筒のサイズに合うように手紙を折り、ある人 (2 人目) は折られた手紙を封筒の中に入れ、さらにある人 (3 人目) は封をして切手を貼ります。最初の招待状を作成するときは、1 人目が手紙を折っている間、他の 2 人は手が空いています。1 人目は、折った手紙を 2 人目に渡します。この後 1 人目は次の手紙を折り、2 人目は手紙を入れますが、3 人目はまだ手が空いています。1 人目と 2 人目の作業が完了したら、2 人目は手紙が入った封筒を 3 人目に渡し、3 人目が封をして切手を貼ります。また、1 人目は折った手紙を 2 人目に渡し、2 人目は手紙を封筒の中に入れます。そして、1 人目は次の手紙を折り始めます。この時点で残りの手紙が 2 通になっていない限り、3 人全員が同時に作業を行う (手紙と封筒を次の人に渡す時間以外) ことになります。このため、1 人が一度に 1 つの作業しか行わなくても、送付する手紙の数が十分にあれば、理論的には 3 人いればほとんどの時間は 3 倍の作業を行うことができます。

ストリームのパイプライン処理でも、これとまったく同じことを行います。もちろん、渡すのは手紙と封筒ではなくバッファに格納されたデータです。ストリームはこのデータを使用して通信を行います。ただし、1 つのストリームがもう 1 つのストリームに直接データを書き込む、上記で説明した順次処理のように、同期を取りながら直接通信を行うことはできません。これは、これらのストリームの処理が別々のスレッドで並列に実行されるためです。この不可能な通信を可能にするには、別のメカニズムが必要です。この作業のために、図 2 のような BlockingStream を作成しました。

Figure 2 BlockingStream の実装 (Close Figure 2)
public class BlockingStream : Stream
{
    private object _lockForRead;
    private object _lockForAll;
    private Queue<byte[]> _chunks;
    private byte[] _currentChunk;
    private int _currentChunkPosition;
    private ManualResetEvent _doneWriting;
    private ManualResetEvent _dataAvailable;
    private WaitHandle[] _events;
    private int _doneWritingHandleIndex;
    private volatile bool _illegalToWrite;

    public BlockingStream()
    {
        _chunks = new Queue<byte[]>();
        _doneWriting = new ManualResetEvent(false);
        _dataAvailable = new ManualResetEvent(false);
        _events = new WaitHandle[] { _dataAvailable, _doneWriting };
        _doneWritingHandleIndex = 1;
        _lockForRead = new object();
        _lockForAll = new object();
    }

    public override bool CanRead { get { return true; } }
    public override bool CanSeek { get { return false; } }
    public override bool CanWrite { get { return !_illegalToWrite; } }

    public override void Flush() { }
    public override long Length {
        get { throw new NotSupportedException(); } }
    public override long Position {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); } }
    public override long Seek(long offset, SeekOrigin origin) {
        throw new NotSupportedException(); }
    public override void SetLength(long value) {
        throw new NotSupportedException(); }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (buffer == null) throw new ArgumentNullException("buffer");
        if (offset < 0 || offset >= buffer.Length)
            throw new ArgumentOutOfRangeException("offset");
        if (count < 0 || offset + count > buffer.Length)
            throw new ArgumentOutOfRangeException("count");
        if (_dataAvailable == null)
            throw new ObjectDisposedException(GetType().Name);

        if (count == 0) return 0;

        while (true)
        {
            int handleIndex = WaitHandle.WaitAny(_events);
            lock (_lockForRead)
            {
                lock (_lockForAll)
                {
                    if (_currentChunk == null)
                    {
                        if (_chunks.Count == 0)
                        {
                            if (handleIndex == _doneWritingHandleIndex)
                                return 0;
                            else continue;
                        }
                        _currentChunk = _chunks.Dequeue();
                        _currentChunkPosition = 0;
                    }
                }

                int bytesAvailable =
                    _currentChunk.Length - _currentChunkPosition;
                int bytesToCopy;
                if (bytesAvailable > count)
                {
                    bytesToCopy = count;
                    Buffer.BlockCopy(_currentChunk, _currentChunkPosition,
                        buffer, offset, count);
                    _currentChunkPosition += count;
                }
                else
                {
                    bytesToCopy = bytesAvailable;
                    Buffer.BlockCopy(_currentChunk, _currentChunkPosition,
                        buffer, offset, bytesToCopy);
                    _currentChunk = null;
                    _currentChunkPosition = 0;
                    lock (_lockForAll)
                    {
                        if (_chunks.Count == 0) _dataAvailable.Reset();
                    }
                }
                return bytesToCopy;
            }
        }
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        if (buffer == null) throw new ArgumentNullException("buffer");
        if (offset < 0 || offset >= buffer.Length)
            throw new ArgumentOutOfRangeException("offset");
        if (count < 0 || offset + count > buffer.Length)
            throw new ArgumentOutOfRangeException("count");
        if (_dataAvailable == null)
            throw new ObjectDisposedException(GetType().Name);

        if (count == 0) return;

        byte[] chunk = new byte[count];
        Buffer.BlockCopy(buffer, offset, chunk, 0, count);
        lock (_lockForAll)
        {
            if (_illegalToWrite)
                throw new InvalidOperationException(
                    "Writing has already been completed.");
            _chunks.Enqueue(chunk);
            _dataAvailable.Set();
        }
    }

    public void SetEndOfStream()
    {
        if (_dataAvailable == null)
            throw new ObjectDisposedException(GetType().Name);
        lock (_lockForAll)
        {
            _illegalToWrite = true;
            _doneWriting.Set();
        }
    }

    public override void Close()
    {
        base.Close();
        if (_dataAvailable != null)
        {
            _dataAvailable.Close();
            _dataAvailable = null;
        }
        if (_doneWriting != null)
        {
            _doneWriting.Close();
            _doneWriting = null;
        }
    }
}

BlockingStream の背景にある概念は、.NET Framework の他の大部分のストリームとは少し異なります。他のストリームと同様に、BlockingStream も System.IO.Stream から派生しており、このクラスの抽象メソッドおよびプロパティをオーバーライドします。しかし、位置とスレッド セーフに関しては、従来のストリームと動作が異なります。.NET Framework のほとんどのストリームはスレッド セーフではありません。つまり、複数のスレッドが特定のストリームのインスタンスに同時かつ安全にアクセスすることはできません。また、ほとんどのストリームは、次の読み取りまたは書き込みが発生しても 1 つの位置を維持します。一方、BlockingStream はスレッド セーフです。また、ある意味では、暗黙的に 2 つの位置を維持します。ただし、この型のユーザーにはどちらの数値も公開されません。

BlockingStream は、データが書き込まれたバッファの内部キューを維持することによって機能します。ストリームにデータが書き込まれると、データが書き込まれたバッファがキューに格納されます。データがストリームから読み取られると、バッファは先入れ先出し (FIFO) 方式の順番に従ってキューから取り出され、バッファ内のデータが呼び出し側に返されます。この場合、ストリームの中には、次の書き込みが発生する位置と次の読み取りが発生する位置、つまり 2 つの位置があると言えます。

ただし、読み取り要求が発生しても、必ずキューが変更されるわけではありません。リーダーがキューに格納されている次のバッファのサイズを超える量のデータを要求した場合、その 1 つのバッファに格納されているデータのみが返されます (Read は返されるデータのサイズが要求されたものより小さい場合でも、正しくデータを返します)。しかし、ユーザーが次のバッファのサイズに満たないデータを要求した場合、バッファ内の残りのデータは (キュー以外の) 別の場所に格納され、次の読み取り要求に返されるデータとして使用されます (図 2 の _currentChunk を参照)。この残りのデータのサイズが次の読み取りで要求されたサイズを満たしている場合、その読み取り要求ではキューが変更されません。

BlockingStream は、ManualResetEvent を使用して、データが利用可能であるかどうかをコンシューマに通知します。スレッドが BlockingStream からのデータの読み取りを試行したが、読み取り可能なデータが存在しない場合、このスレッドは、データが書き込まれて ManualResetEvent にそのことを示すデータが設定されるまでブロックされます。データが書き込まれたら、読み取り可能なデータが書き込まれたことをコンシューマに通知するよう、ライタがイベントを設定します。実際は、複数のリーダーがデータを待機している場合、すべてのリーダーが通知を受け取ることになるため、Read メソッドにロジックを追加する必要があります。

Read メソッドは、1 つの大きなループとして実装されており、読み取り要求を満たすデータを検出するか、ライタが SetEndOfStream を呼び出してこれ以上書き込むデータがないことをストリームに通知するまでループを続けます。この場合、リーダーの待ち時間が非常に長くなるため、データを待機するリーダーをブロックし続けるのは効率的ではありません。それぞれの状態は ManualResetEvent によって通知されます。1 つは先ほど説明した状態、もう 1 つは書き込みが完了したかどうかを示す状態です。Read がループに入ったときの最初の動作は、これらのうちいずれかのイベントが設定されるのを (WaitHandle.WaitAny を使用して) 待機することです。

BlockingStream は、ここで作成しようとしているストリームのパイプライン処理にとって有用なクラスです。これは、順次処理を実行する方法で暗黙的に使用されていた、ストリーム間でデータを受け渡すパターンが可能になるだけでなく、複数のスレッドにまたがった実装もサポートされるためです。つまり、実行するストリーム ベースの各操作を表すメソッドを作成できます (図 3 参照)。

Figure 3 ストリーム ベースの操作を行うためのメソッド (Close Figure 3)
static void Compress(Stream input, Stream output){
    using (GZipStream compressor = new GZipStream(
           output, CompressionMode.Compress, true))
        CopyStream(input, compressor);
}

static void Encrypt(Stream input, Stream output) {
    RijndaelManaged rijndael = new RijndaelManaged();
    ... // setup crypto keys
    using (ICryptoTransform transform = rijndael.CreateEncryptor())
    using (CryptoStream encryptor = new CryptoStream(
            output, transform, CryptoStreamMode.Write))
        CopyStream(input, encryptor);
}

これらのメソッドは、それぞれ別のスレッドで実行されます。出力は BlockingStream としてパイプライン処理の次の段階に渡されます。まず、2 番目の操作がブロックされ、入力の BlockingStream を待機します。最初の操作がストリームにデータを書き込むと、2 番目の操作のブロックが解除され、2 番目の操作がデータを取得および処理します。この機能のコードは StreamPipeline クラスに記述されています (図 4 参照)。

Figure 4 StreamPipeline (Close Figure 4)
public class StreamPipeline : IDisposable
{
    private Action<Stream, Stream>[] _filters;
    private List<BlockingStream> _blockingStreams;

    public StreamPipeline(params Action<Stream, Stream>[] filters)
    {
        if (filters == null) throw new ArgumentNullException("filters");
        if (filters.Length == 0 || Array.IndexOf(filters, null) >= 0)
            throw new ArgumentException("filters");

        _filters = filters;

        _blockingStreams = new List<BlockingStream>(_filters.Length - 1);
        for (int i = 0; i < filters.Length-1; i++)
        {
            _blockingStreams.Add(new BlockingStream());
        }
    }

    public void Run(Stream input, Stream output)
    {
        if (_blockingStreams == null)
            throw new ObjectDisposedException(GetType().Name);
        if (input == null) throw new ArgumentNullException("input");
        if (!input.CanRead) throw new ArgumentException("input");
        if (output == null) throw new ArgumentNullException("output");
        if (!output.CanWrite) throw new ArgumentException("output");

        ThreadStart lastStage = null;
        for (int i = 0; i < _filters.Length; i++)
        {
            Stream stageInput = i == 0 ? input : _blockingStreams[i - 1];
            Stream stageOutput =
                i == _filters.Length - 1 ? output : _blockingStreams[i];
            Action<Stream, Stream> filter = _filters[i];
            ThreadStart stage = delegate
            {
                filter(stageInput, stageOutput);
                if (stageOutput is BlockingStream)
                    ((BlockingStream)stageOutput).SetEndOfStream();
            };
            if (i < _filters.Length - 1)
            {
                Thread t = new Thread(stage);
                t.IsBackground = true;
                t.Start();
            }
            else lastStage = stage;
        }
        lastStage();
    }

    public void Dispose()
    {
        if (_blockingStreams != null)
        {
            foreach (BlockingStream stream in _blockingStreams)
                stream.Dispose();
            _blockingStreams = null;
        }
    }
}

StreamPipeline は非常に使いやすいクラスです。コンストラクタはデリゲートのパラメータ配列を受け取ります。各配列には、入力ストリームと出力ストリームが格納されます。これらのストリームはフィルタとして使用されます。配列を受け取った後、コンストラクタは 1 つの BlockingStream を作成および格納し、それぞれのフィルタのペア間で使用します。先ほど説明した Compress メソッドと Encrypt メソッドのシグネチャは、必要なデリゲート シグネチャに一致しているため、フィルタとして使用できます (図 5 参照)。

図 5 StreamPipeline の構造
図 5 StreamPipeline の構造(画像を縮小するには、ここをクリックします)
図 5 StreamPipeline の構造
図 5 StreamPipeline の構造 (画像を拡大するには、ここをクリックします)

Run メソッドは入力ストリームと出力ストリームを受け取ります。これらのストリームは、パイプライン処理の最初と最後に使用されます。この例では、圧縮および暗号化するディスク上のデータを表す入力の FileStream と、処理後のデータを再びディスクに格納する出力の FileStream がこれに該当します。

using (FileStream input = File.OpenRead("inputData.bin"))
using (FileStream output = File.OpenWrite("outputData.bin"))
using (StreamPipeline pipeline = new StreamPipeline(Compress, Encrypt))
{
    pipeline.Run(input, output);
}

内部的には、パイプライン処理の各段階で、適切な入力および出力ストリームが格納された適切なフィルタを実行するデリゲートが Run メソッドによって生成されます。このメソッドは、新しいスレッドを作成して、最後の作業項目以外の作業項目をそれぞれ実行します。最後の作業項目は、このメソッドを呼び出したスレッドで実行されます。現在のスレッドは、作業の完了を待機するためにブロックしておく必要があるため、このスレッドに割り当てられたリソースを無駄にするよりも、スレッドを再利用して、いくつかの処理を完了する方が効率的です。

ここでは ThreadPool を使用しないことにしました。通常は、新しいスレッドを作成するよりも、ThreadPool に作業項目を格納することを考えます。しかしこの場合、ThreadPool に格納する作業項目は、同じく ThreadPool に格納されている他の作業項目がいくつかの操作を実行するまでブロックされる可能性があります。これはデッドロックを引き起こす可能性がある (msdn.microsoft.com/msdnmag/issues/04/12/NETMatters に、.NET Framework 1.x を使用する開発者がどのような場合にこの状況を経験するかを示す例が記載されています) ため、その状況を回避するために、各操作用のスレッドを独自に作成しました。

もちろん、ThreadPool を使用しない場合、それ相応のデメリットがあります。たとえば、スレッドの作成と削除は負荷がかかる処理であるため、フィルタが短時間しか使用されない場合、この作成および削除処理によって他の計算処理が影響を受ける可能性があります。StreamPipeline で大量の異なる入力ストリームと出力ストリームを処理する場合は、このクラスの関連部分のコードを記述し直してスレッドが削除されないようにし、次の処理で再利用できるようにした方がよいでしょう。アプリケーションの制限によっては、ThreadPool を使用した方が適切な場合もあります。

これで StreamPipeline を構築できたため、次の問題は、どのようにしてこのクラスを使用するかということです。何と言っても、今回の最も重要な目的は、コンピュータに搭載されているコアを両方とも使用して計算にかかる時間を短縮することです。私のデュアルコア コンピュータで、最初に紹介した『War and Peace』のデータを処理したときには、パイプライン処理を実装したコードの方が、単に処理を順次実行するコードよりも 20 ～ 25% 短い時間で実行されました。がっかりしたでしょうか。ここで疑問に思っていただきたいのは、なぜ 100% 短い時間で実行されなかったのかということです。どこか実装に適切でない部分があるのでしょうか。2 つのプロセッサで実行しているのですから、速度は 2 倍になるはずではないでしょうか。

このコラムの最初で紹介した、封筒に手紙を入れる例で、少し誤解を招くような説明をしてしまいました。皆さんはどうかわかりませんが、私は折った手紙を封筒に入れるよりも、手紙を 3 分の 1 に折る方が時間がかかります。このことを考慮すると、私たち人間のパイプライン処理では、封筒に手紙を入れる 2 人目は、封筒に手紙を入れた後、1 人目が折った次の手紙を受け取るまで待機している可能性があります。つまり、問題を解決するために多くの人数を投入しましたが、その人数に比例した速度では解決されなかったということです。

圧縮と暗号化の例にも同じことが当てはまります。実装した順次パイプライン処理にかかった時間のうち、圧縮と暗号化にどれくらい時間がかかったかを計測しました。どれくらいかかったと思いますか。暗号化にかかった時間はわずか 20% でした。したがって、この状況で可能な、ほぼ最大限の時間短縮を実現していると言えます。

この実装が最も優れていると言っているわけではありません。プロセッサの数が増加すればより明白になるかもしれませんが、パフォーマンスに関するいくつかの問題があります。最も明白な問題は、コンピュータの処理能力をすべて活用するために、プロセッサごとに 1 つ以上のフィルタを用意する必要があることです。他に考えられる問題は、BlockingStream の実装内で複数のロックを取得していることです (StreamPipeline で BlockingStream を使用する場合、インスタンスを同時に使用しているリーダーが 1 つのみであれば、これらのロックのうち 1 つは不要になります)。しかし、記述する必要があるコードは比較的少なかったため、圧縮および暗号化処理で 20 ～ 25% でもパフォーマンスが向上したことは非常に有益です。

また、パイプライン処理の各段階で複数のデータを並列に処理できれば、効率が大きく向上する可能性があります。たとえば、封筒に手紙を入れる例で言うと、複数の人が手紙を折れば (または封筒に入れる手紙がなくなって手が空いたときに、2 人目が手紙を折る作業を手伝えば)、システム全体のスループットは向上します。圧縮と暗号化の例で言えば、先ほど説明した、データをいくつかのブロックに分割する方法と同じように、複数のブロックがパイプライン処理の圧縮段階で並列に圧縮された後、暗号化段階に渡され、ここでも圧縮されたブロックが並列に暗号化される、というように処理が進めば、効率が大きく向上します。アプリケーションが細かく同時処理を実行できるようになるほど、コンピュータに搭載されているプロセッサが活用され、全体の処理が高速に完了する可能性が高くなります。特に現在はテクノロジが進歩しており、標準的なパーソナル コンピュータに搭載されるプロセッサも増加しているため、その可能性は広がっています。

ご意見やご質問は、Stephen まで英語でお送りください。 netqa@microsoft.com.

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


{ End Bracket }: とびきり最高のものを見つける

{ End Bracket }
とびきり最高のものを見つける
Howard Dierking

MSDN Magazine は最近ある機会を得て、 マイクロソフトの Talent Acquisition (人材発掘) 部門の統括マネージャの Scott Pitasky および彼のチームのメンバに、マイクロソフトの人材採用の方法について話を聞きました。国際化が進む中で優れた人材を発掘して雇用するために、マイクロソフトが採用戦略の調整と採用活動の文化的な側面の見直しを継続的に進めていることがわかりました。今回は熱心な採用担当マネージャおよびテクニカル リーダーの皆さんが参加してくれました。本社 (Redmond) の James Hunt、Randall Boseman、Microsoft India の Amit Vasishta、Microsoft China の Chris Gootherts に感謝します。

Web で "マイクロソフトの面接" を検索すると、無数とも言えるストーリーが検索結果に表示されます。愉快な話もあれば、ぞっとするような話も出てきます。これらのストーリーに共通していることは、非常に基本的な問いかけです。マイクロソフトはソフトウェア開発者に対してどのような資質を求めているのか?この件について Pitasky と彼のチームは、従来求められていた資質の見直し、または完全な変更が行われたことを説明してくれました。核となる 3 つの資質があります。情熱、知性、そして問題解決能力です。

"情熱" という言葉が頻繁に使われます。これはマイクロソフトの優先事項です。ここで注目すべき点は、応募者の情熱を評価するための面接プロセスが変更されたことです。かつては、マイクロソフトのその時点の事業内容に対して情熱を注ぐことのできる人材が歓迎されました。その結果、多くの才能を見過ごしていました。現在では、応募者が個人的に夢中になっている対象や活動にどのくらい熱意を持っているかということも、その人の情熱を評価する目安の 1 つです。Boseman が面接で出会ったある応募者は、トルコでバイクを買って中東と欧州をバイク旅行したことについて事細かに語ったのだそうです。応募者が自分の体験談を熱心に語る様子からは、技術的な議論を交わすよりも鮮明にその人の情熱の強さが伝わりました。

"知性" とは、ソフトウェア エンジニアリング分野に関する資質を意味しているように思えます。しかし、情熱と同じように、知性に関してもここ数年の間に定義が見直されました。マイクロソフトの面接での "パズル試験" はもう有名ですね。このパズル試験の問題点は、ある特定の知性しか見極められないことです。しかし、優れたソフトウェアを作るには、人間関係などさまざまなタイプの知性も必要とされます。この問題を実感したマイクロソフトでは、より広い視点から知性を評価することによって応募者の優れた資質と経験を見出し、マイクロソフトが新たに展開する分野での改革と成長に貢献できる人材の確保を目指しています。Pitasky はこう言いました。「スマートさをいかに定義するかについて、我々は以前よりもスマートになったわけです。」

Pitasky によると、マイクロソフトに "適応" できるかを見極めることは、役に立たなくなった古い概念です。その代わりに、採用担当マネージャが探しているのは、学習して問題を解決できる能力に長けている人材です。そのためには、過去何年も受け継がれてきたマイクロソフトの面接の文化を変える必要がありました。

現在、こういった能力を正しく評価するために面接担当者はどうしているのでしょうか。Boseman はこのように述べています。「チームに適した人材であるかどうかは、バスケットボールの試合を 30 分もやれば判断できます。チームの中でどのようにプレイするかを観察することで、その人物について多くのことがわかるのです。」バスケットボールの試合を面接に取り入れる予定はありませんが、(実際的な、または空想的な) ロールプレイ シナリオを活用する方法については検討されました。この方法では、所定のチーム設定で応募者がどのように行動するかを見ることができます。

マイクロソフトの規模が拡大し、国際化が進むにつれて、"世界中から採用して世界中に配置する" ことが、新しい国際的な人材採用の哲学となっています。ただし、地域間に大きな違いがあることを考慮する必要があります。たとえば、米国ではコンピュータ科学専攻の学生が年に約 15％ の割合で減少しています。その結果、マイクロソフトではその他の学位にも採用の範囲を広げ、テクニカル コミュニティで才能のある人物を探す努力も必要になりました。一方、インドではエンジニアリングの学位を持つ多数の応募者を集めることができます。インドでの課題は、複雑な技術的問題を解決できるだけでなく、指導や管理の能力を持つ人材を見つけることです。また、イスラエルでは多数の上級レベル開発者が存在し、中国では中間レベルと上級ランクとの差が大きくなっています。総括すると、マイクロソフトをさらに国際的な企業に発展させるための最優先事項は、最適な人材を採用することです。

最後に、面接にまつわる驚くべき話を紹介しましょう。ある採用担当者が本社 (Redmond) でミシガン州からの応募者との面接を設定しました。その応募者は、自分が身体的な理由で飛行機を使えないことなど一言も言いませんでした。面接の当日、担当者が出社すると駐車場に応募者の自家用車が置いてありました。彼はミシガン州から車で来ていたのです。これこそが情熱ではないでしょうか!

Howard Dierking は MSDN Magazine の編集長です。