11 月号 2008
セキュリティ クイズ: あなたのセキュリティ IQ をテストする
セキュリティ エキスパートの Michael Howard が、10 個の脆弱なコードを提示します。あなたの使命は、コードからホール (いわゆる誤ったセキュリティ プラクティス) を探し出すことです。 Michael Howard および Bryan Sullivan
アジャイル SDL: アジャイル開発のセキュリティ プラクティスを効率化する
Bryan Sullivan が、リリース サイクルの短い Web アプリケーションおよびアジャイル プロジェクト向きの新しい SDL について解説します。 Bryan Sullivan
アクセス制御: Windows のファイルとレジストリのアクセス許可について
操作の実行を許可する前にアクセス許可および権限を管理する ACL について理解することは、セキュリティ強化のために不可欠です。 John R. Michener
ユーティリティ スポットライト: Visual Round Trip Analyzer で Web ページを高速化する 12 のステップ
Visual Round-trip Analyzer (VRTA) を使用して、Web ページの読み込みに関する問題の根本的な原因を特定する方法を説明します。陥りやすいミスや重要ポイントが 12 のステップとして紹介されています。 Jim Pierson
RIA: Silverlight 2 Web パーツで SharePoint に磨きをかける
Silverlight メディア プレーヤーを作成し、それを SharePoint Web パーツとして配置することで SharePoint と Silverlight を統合する方法を説明します。 Steve Fox および Paul Stubbs
Columns
編集者のメモ: 身分証明書を見せていただけますか?: セキュリティ対策はコンテキストに大きく左右され、状況に応じて変化します。今月は、Howard Dierking がセキュリティに関する話題をいくつか取り上げます。Howard Dierking | ツールボックス: オンライン スケジュール、Jeff Smith の SQL ブログ、簡単な正規表現、その他: 今月の記事で Scott は、開発スキルの向上、正規表現の記述、Web スケジュール管理、および SQL のヒントを提供するブログについて取り上げています。Scott Mitchell | CLR 徹底解剖: 国際化対応のアプリケーション: CLR チームが、System.Globalization 名前空間の詳細について示し、ローカライズとグローバリゼーションを適切に行うためのデータ形式の処理方法について説明します。Melitta Andersen
データ ポイント: Silverlight 2 での Web サービス利用に関する疑問点: John Papa が Silverlight 2 アプリケーションからのサービス呼び出しに関する質問に回答します。John Papa | Cutting Edge: Silverlight 2 におけるブラウザの相互運用性: 今月は、Dino Esposito が Silverlight のブラウザの相互運用層で Silverlight/Web ページの数多くの対話のニーズに対処する方法を説明します。Dino Esposito | セキュリティに関するブリーフィング: 脅威モデルによりセキュリティ プロセスを強化する: 脅威モデルを使用してセキュリティ エンジニアリング プロセスを促進することは、コード レビュー、ファジー テスト、および攻撃対象領域分析タスクの優先順位を付けるために役立ちます。Michael Howard
テストの実行: ソフトウェア テストにおける集団意思決定: 複数のオプションから最良のものを 1 つ選択する集団意思決定の分析手法については、幅広い研究が行われてきました。Dr. James McCaffrey が 5 つの方式を取り上げて要点を説明します。Dr. James McCaffrey | Wicked Code: Silverlight 2 変換およびクリッピング領域: Silverlight は、非常にわずかなコード (そのほとんどが XAML) で Web サイトのイメージ拡大機能を簡単に構築できる強力なツールです。詳しくはこちらからJeff Prosise | 基礎: ワークフローとアクティビティの単体テスト: Matt Milner が、Windows Workflow Foundation のアクティビティ、ワークフロー、および関連するコンポーネントのテストについて、いくつかの課題や技法を取り上げます。Matt Milner
Team System: Team Build 2008: Brian Randell が、Visual Studio 2008 Team Foundation Server Build (Team Build) を紹介しています。さらに、チーム ビルドを作成し、実行します。Brian A. Randell | { End Bracket }: 文書を書くエンジニア: 文書を書くことは、開発者やソフトウェア エンジニアにとって良い練習になります。自分の考えをまとめ、より正確なコミュニケーションを行うために、文書を書くことがどのように役立つかについて Richard Ward が説明します。Richard Ward


セキュリティ クイズ
あなたのセキュリティ IQ をテストする
Michael Howard および Bryan Sullivan
目次
バグ #1 (C または C++)
バグ #2 (C または C++)
バグ #3 (すべての言語 — 例は C#)
バグ #4
バグ #5
バグ #6 (C#)
バグ #7 (C#)
バグ #8 (C#)
バグ #9 (C#)
バグ #10 (Silverlight CLR C#)

著者はどちらもコードでセキュリティ バグを探すのを楽しんでいます。バグ探しならお手のもの、とさえ言っていいかもしれません。だれにも負けないとまでは言いませんが、たいていは多くのバグをたちまち発見できます。あなたはどうですか。
セキュリティ バグを見たらそうとわかりますか。このクイズで試してみてください。どのコード サンプルにも、少なくとも 1 つのセキュリティ脆弱性が存在します。バグを見つけて、何点くらい取れるか試してみてください。コードの後で、脆弱性の要点と若干のコメント、および必要に応じてセキュリティ開発ライフサイクル (SDL) でそのバグを発見する方法を説明しています。情報とコード サンプルを提供してくれた Peter Torr と Eric Lippert に感謝します。
バグ #1 (C または C++)
void func(char *s1, char *s2) {
  char d[32];
  strncpy(d,s1,sizeof d - 1);
  strncat(d,s2,sizeof d - 1);
  ...
}
解答 まず手始めは、とても簡単なものということで、お馴染みのバッファ オーバーランです。多くの場合、制限付きの strncpy 関数および strncat 関数を使用しているコードは、安全で問題がないように見えます。しかし、これらの関数は、バッファ サイズが正しい場合にのみ安全であり、この例ではバッファ サイズが間違っています。まったく違います。
技術的には、1 番目の呼び出しは安全ですが、2 番目の呼び出しは正しくありません。strncpy 関数および strncat 関数の最後の引数はバッファに残された領域の大きさであり、strncpy を呼び出すことでその領域の一部または全部を使い果たしています。バッファ オーバーフロー。Michael が、このバグの種類について 2004 年のブログで的確に解説しています。
Visual C++ 2005 以降では、警告 C4996 によって、不正な関数呼び出しを安全な呼び出しに変えるように指摘されます。また、/analyze オプションを指定すると、strncat が文字列をゼロで終了しない可能性があることを示す C6053 の警告が発生します。
率直に言うと、strncpy と strncat (およびその "n" の同類) は、いくつかの理由で strcpy と strcat (およびその同類) より劣っています。第 1 に、戻り値に問題があります。バッファへのポインタが返されますが、バッファは有効かもしれないし、無効かもしれません。それを知る方法はないのです。第 2 に、対象のバッファのサイズを正確に取得するのは非常に困難です。このバグを見つけられたら、1 ポイント獲得です。

バグ #2 (C または C++)
int main(int argc, char* argv[]) {
  char d[32];
  if (argc==2)
    strcpy(d,argv[1]);

  ...
  return 0;
}
解答 これは有害なバグです。バッファ オーバーランの例としてこのバグがよく使用されているのを目にしますが、多くの場合、コードにセキュリティ バグがあるのかどうかを判別するのは不可能です。すべてはコードの使用方法しだいです。
これが標準の Win32 EXE であれば、最悪でも自分で自分を攻撃し、自分でコードを実行する程度なので、セキュリティ バグにはなりません。
一方、このコードが、たとえば SYSTEM として実行する Windows サービスの ServiceMain にある場合、または Linux アプリケーションの setuid root のメイン関数にある場合には、コードは紛れもなくセキュリティ バグになります。
コードが setuid root としてマークされた Linux アプリケーションであるものとします。普通のユーザーがアプリケーションを起動すると、アプリケーションは実際には root として実行し、これはローカル特権昇格の脆弱性を意味します。
バグ #1 で示したコード例と同じように、strcpy の呼び出しに対しては C4996 の警告が発生し、/analyze ではバッファ オーバーランの可能性を示す C6204 が発生します。"なるほど。もっと状況がわからないと答えられないな" と解答した人は 2 ポイント獲得です。それ以外の人はポイントなしです。

バグ #3 (すべての言語 — 例は C#)
byte[] GetKey(UInt32 keySize) {
  byte[] key = null;

  try {
    key = new byte[keySize];
    RNGCryptoServiceProvider.Create().GetBytes(key);
  }
  catch (Exception e) {
    Random r = new Random();
    r.NextBytes(key);
  }

  return key;
}
解答 このひどいキー生成コードには 2 つのバグがあります。1 つ目はすぐにわかります。暗号的には問題のない乱数生成関数の呼び出しが失敗すると、コードは例外をキャッチして、まったくお粗末な予測できてしまう乱数生成関数を呼び出します。この点を見つけられたら、1 ポイント差し上げます。暗号化された乱数を使用してキーを生成することは、SDL の要件です。
しかし、バグはこれだけではありません。このコードはすべての例外をキャッチします。まれな場合を除き、C++ の例外、Microsoft .NET Framework の例外、または Windows の構造化例外処理のどれでも見境なくすべての例外をキャッチしてしまうと、本当のエラーが隠れてしまいます。ですから、このようなことはしないでください。
すべての例外 (バッファ オーバーランのようなアクセス保護エラーを含む) をキャッチする C または C++ の構造化例外ハンドラは、/analyze を指定してコンパイルすると C6320 警告が発生します。アニメーション化されたカーソルのバグ MS07-017 に対して攻撃者が攻撃を繰り返すことを可能にしたのは、この種の設計でした。例外処理のバグを見つけられたら、さらに 1 ポイント獲得です。

バグ #4
void func(const char *s) {
  if (!s) return;
  char t[3];
  memcpy(t,s,3);
  t[3] = 0;
  ...
}
解答 このようなバグを発見したのは、数年前、まだ開発中の Windows Vista においてでした。しかし、これはセキュリティ バグでしょうか。コーディングのバグであることは明らかです。コードは配列の 4 番目の要素に書き込んでいますが、配列には要素 3 個分の長さしかありません。配列は 0 から始まるのであって、1 ではないことを思い出してください。攻撃者は何も制御できないのだからこれはセキュリティ バグではないと、私は強く主張します。
一方、攻撃者が i を制御するこのようなかたちのバグの場合は、攻撃者がメモリ内のどこにでもゼロを書き込めることを意味します。これは正真正銘のセキュリティ バグです。
void func(const char *s, int i) {
  if (!s) return;
  char t[3];
  memcpy(t,s,3);
  t[i] = 0;
  ...
}
このコードを /analyze でコンパイルすると、C6201 "有効なインデックス範囲外" のエラーが発生します。"セキュリティ バグではない" と答えられたら、1 ポイント獲得です。

バグ #5
public class Barrel {
  // By default, a barrel contains one rhesus monkey.
  private static Monkey[] defaultMonkeys =
    new[] { new RhesusMonkey() };
  // backing store for property.
  private IEnumerable<Monkey> monkeys = null;

  public IEnumerable<Monkey> Monkeys {
    get {
      if (monkeys == null) {
        if (MonkeysReady())
          monkeys = PopulateMonkeys();
        else
          monkeys = defaultMonkeys;
      }
      return monkeys;
    }
  }
}
解答 これは難しい問題です。このクラスの作成者は、コードが安全かつ効率的であると考えています。補助ストアはプライベートで、プロパティは読み取り専用で、プロパティの型は IEnumerable<T> なので、呼び出し元は Barrel の状態を読み取る以外に何もできません。
しかし、敵意ある攻撃者はプロパティの戻り値を Monkey[] にキャストしてみる可能性があることを、作成者は忘れています。2 つの Barrel があり、それぞれに既定の Monkey リストがあるとすると、そのうちの 1 つを手に入れた敵意ある攻撃者は、既定の静的リストの RhesusMonkey を他の任意の Monkey または null に置き換えることができ、それによって他の Barrel の状態を実質的に変更できます。
この場合の解決策は、ReadOnlyCollection<T> をキャッシュするか、または敵意ある呼び出し元による変更から基の配列を保護する真に読み取り専用の記憶域を使用することです。このことに気付いたら、2 ポイント差し上げます。

バグ #6 (C#)
protected void Page_Load(object sender, EventArgs e) {
  string lastLogin = Request["LastLogin"];
  if (String.IsNullOrEmpty(lastLogin)) {
    HttpCookie lastLoginCookie = new HttpCookie("LastLogin",
      DateTime.Now.ToShortDateString());
    lastLoginCookie.Expires = DateTime.Now.AddYears(1);
    Response.Cookies.Add(lastLoginCookie);
  }
  else {
    Response.Write("Welcome back! You last logged in on " + lastLogin);
    Response.Cookies["LastLogin"].Value =
      DateTime.Now.ToShortDateString();
  }
}
解答 これは、Web で最も一般的な脆弱性である、簡単なクロス サイト スクリプティング脆弱性です。このコードは lastLogin の値が常に Cookie から取得されることをほのめかしているように見えますが、実際には、HttpRequest.Item プロパティは Cookie の値よりクエリ文字列の値を優先します。
言い換えると、lastLogin Cookie にどのような値が設定されていたとしても、攻撃者が lastLogin=<script>alert('0wned!')</script> という名前/値ペアをクエリ文字列に追加すると、アプリケーションは lastLogin 変数の値に対して悪意のあるスクリプトの入力を選択してしまいます。XSS と解答したあなたには、1 ポイント差し上げます。

バグ #7 (C#)
private decimal? lookupPrice(XmlDocument doc) {
  XmlNode node = doc.SelectSingleNode(
    @"//products/product[id/text()='" +
    Request["itemId"] + "']/price");
  if (node == null)
    return null;
  else
    return (Convert.ToDecimal(node.InnerText));
}
解答 XPath インジェクションと解答したら、1 ポイント獲得です。XPath インジェクションは、はるかに (悪) 名高い同類の SQL インジェクションとまったく同じ原理で動作します。XPath コードと、検証もエスケープもされていないユーザー入力を結合するクエリを作成することで、このコードはインジェクション攻撃に対して脆弱です。後で何らかの形式の操作の実行に使用されるテキストを扱うすべてのアプリケーションは、インジェクション脆弱性の対象になります。

バグ #8 (C#)
public class CustomSessionIDManager : System.Web.Session    State.SessionIDManager
{
    private static object lockObject = new object();

    public override string CreateSessionID(HttpContext context)
    {
        lock (lockObject)
        {
            Int32? lastSessionId = (int?)context.Application                ["LastSessionId"];
            if (lastSessionId == null)
                lastSessionId = 1;
            else
                lastSessionId++;
            context.Application["LastSessionId"] = lastSessionId;
            return lastSessionId.ToString();
        }
    }
}
解答 このコードには 2 つの主な問題があります。このコードは、アプリケーションのロジックの周囲にロックを適切に適用し、2 つのスレッドが同時に同じセッション ID を作成しないようにしていますが、それでもサーバー ファームに展開するには安全ではありません。HttpContext.Application オブジェクトによって参照されているアプリケーション状態が、サーバー間で共有されていません。このアプリケーションをサーバー ファームに展開すると、セッション ID の競合が発生する可能性があります。このバグがわかったら、1 ポイント獲得です。
もう 1 つの重大な問題は、このクラスによって生成されるセッション ID が、簡単に推測される連続した整数であることです。ユーザーが自分のセッション トークンを見て、セッション ID が 100 であることがわかると、簡単なブラウザ ユーティリティを使用してセッション ID を 99 や 98 または他のもっと小さい値に変更し、そのユーザーのセッションをハイジャックできます。
この場合に開発者が使用できるもっとよい方法は、GUID または他の大きくてランダムな文字と数字から成る文字列を使用することです。セッション ID トークンとして連続した整数が不適切な選択であることがわかった場合は、1 ポイント追加してください。

バグ #9 (C#)
bool login(string username,
           string password,
           SqlConnection connection,
           out string errorMessage) {
  SqlCommand selectUserAndPassword = new SqlCommand(
    "SELECT Password FROM UserAccount WHERE Username = @username",
    connection);
  selectUserAndPassword.Parameters.Add(
    new SqlParameter("@username",  username));
  string validPassword =
    (string)selectUserAndPassword.ExecuteScalar();

  if (validPassword == null) {
    // the user doesn't exist in the database
    errorMessage = "Invalid user name";
    return false;
  }
  else if (validPassword != password) {
    // the given password doesn't match
    errorMessage = "Incorrect password";
    return false;
  }
  else {
    // success
    errorMessage = String.Empty;
    return true;
  }
}
解答 このコードの最大の問題は、ログインが失敗した場合に、アプリケーションからユーザーに返される情報が多すぎることです。たしかに、単純な入力ミスか、それともユーザー名を完全に忘れてしまったのかを、ユーザーが判断するには役立ちますが、この情報はアプリケーションに対してブルート フォース攻撃を試みる攻撃者にも役立つものです。直感には反するかもしれませんが、この状況では役に立たない方がよいのです。ログインが失敗した場合には、"ユーザー名が無効です" や "パスワードが無効です" ではなく、"ユーザー名またはパスワードが無効です" のようなメッセージを表示する必要があります。
このことに気付いたら、1 ポイントです。さらに、アプリケーションではパスワードをデータベースにプレーンテキストで保存してはならないことも覚えていたら、ボーナス ポイントを差し上げます。パスワードは、ソルト処理されたハッシュで保存および比較する必要があります。
ランクの確認
ポイント | コメント
15+ | 私たちと共に働けます。
11-14 | かなり優秀です。これからは、その才能を活かして、同僚が作成するすべてのコードもチェックしてください。
7-10 | そう、まあまあです。多くのことを学習する必要があるのは明らかですが、いま Web アプリケーションを作成している開発者の 50% よりはおそらく優秀でしょう。
4-6 | 学習しなければならないことがたくさんあります。お気に入りの本屋に行って、この記事の著者の 2 人が書いた本をすべて買ってください。
0-3 | エディタやコンパイラからは身を引きましょう。そうすれば、だれも傷つかなくて済みます。

バグ #10 (Silverlight CLR C#)
bool verifyCode(string discountCode) {
  // We store the hash of the secret code instead of
  // the plaintext of the secret code.
  // Hash the incoming value and compare it against
  // the stored hash.
  SHA1Managed hashFunction = new SHA1Managed();
  byte[] codeHash =
    hashFunction.ComputeHash(
      System.Text.Encoding.Unicode.GetBytes(discountCode));
  byte[] secretCode = new byte[] {
    116, 46, 130, 122, 36, 234, 158, 125, 163, 122,
    157, 186, 64, 142, 51, 153, 113, 79, 1, 42 };

  if (codeHash.Length != secretCode.Length) {
    // The hash lengths don't match, so the strings don't
    // match this should never happen, but we check anyway
    return false;
  }

  // perform an element-by-element comparison of the arrays
  for (int i = 0; i < codeHash.Length; i++) {
    if (codeHash[i] != secretCode[i])
      return false; // the hashes don't match
  }

  // all the elements match, so the strings match
  // the discount code is valid, inform the server
  WebServiceSoapClient client = new WebServiceSoapClient();
  client.ApplyDiscountCode();

  return true;
}
解答 開発者は賢明な判断を行って、シークレット コードをコード内にプレーンテキストで埋め込んでいません。ユーザーがシークレット (割引コードやパスワードなど) を知っているかどうかだけをテストする必要がある場合は、プレーンテキストを保存して文字列を直接比較するよりも、シークレットのハッシュを保存してハッシュを比較する方が常によい方法です。残念ながら、開発者は SHA-1 ハッシュ アルゴリズムを選択していますが、これは重大な脆弱性の兆候を示しており、後に SDL で禁止されています。もっとよい選択肢は、SDL が承認して推奨している SHA-256 ハッシュ アルゴリズムを実装する SHA256Managed クラスを使用することです。このことに気付いたら、1 ポイントです。
SHA-256 ではなく SHA-1 を選択したことよりもっとまずいのは、開発者がハッシュ値のソルト処理を疎かにしていることです。ソルト処理されていないハッシュは、事前に計算されたハッシュ テーブル (レインボー テーブルと呼ばれることがよくあります) によるクラックに対してはるかに脆弱です。おそらく、攻撃者は、ソルト処理されていないハッシュ値から元のプレーンテキストのシークレット コードを短時間で特定してしまうでしょう (著者は、プレーンテキストのシークレット コードで私たちに応答してくれた最初の人に対し、お祝いの言葉を SDL ブログに投稿します)。ソルト処理されていないハッシュに気付いたなら、1 ポイント獲得です。
しかし、このコードの最大の問題は、そもそもコードがクライアント コンピュータで実行されるという事実です。最初にこれはユーザーのブラウザで実行される Silverlight CLR コードであると書かれていたことを思い出してください。クライアントで実行するすべてのコードは、攻撃者が操作できてしまいます。それだけのことです。Silverlight コードを実行するブラウザ インスタンスにデバッガをアタッチし、ブラウザを動かしてステップ実行しようと決意したユーザーを妨げるものは何もありません。
codeHash 変数を secretCode ハッシュと同じに設定し、比較ロジックが常に成功するようにするかもしれません。または、検証ロジックをそっくりスキップし、現在の命令から割引コードを適用する Web サービスの呼び出しにジャンプすることもできます。一番簡単なのは、デバッガを完全に回避して、単に Web サービスのメソッド ApplyDiscountCode を直接呼び出すことです。
C# または Visual Basic を使用して、ASP.NET Web フォームとまったく同じように Silverlight アプリケーションを作成しているかもしれませんが、Silverlight のコードはクライアント コンピュータで動作し、Web フォームはサーバーで動作するということを理解しておくことが重要です。クライアントで実行するコードは、攻撃者が見たり変更したりできます。クライアント側コードにシークレットを埋め込んだり、クライアント側コードが特権の必要な決定 (割引コードが有効かどうか、ユーザーが特定のアクションの実行を許可されているかどうか、など) を行うことができるようにしたりしないでください。このバグがわかったら、1 ポイント獲得です。


アジャイル SDL
アジャイル開発のセキュリティ プラクティスを効率化する
Bryan Sullivan

この記事では、次の内容について説明します。
  • アジャイル セキュリティ
  • 脅威のモデル化
  • セキュリティ プロセスの自動化
 この記事では、次のテクノロジを使用しています。
SDL
目次
新しいプロセス: SDL とアジャイルを結合する
すべてのスプリントの要件
開始時の要件
バケットの要件
制約事項
まとめ
2008 年 9 月号の MSDN Magazine では、オンライン サービスでのセキュリティ上の脆弱性に対処するためにマイクロソフトが行った、セキュリティ開発ライフサイクル (SDL) プロセスに対する追加についてのコラムを書きました。クロスサイト スクリプティング攻撃を防ぐための入力検証と出力エンコードの重要性、SQL インジェクション攻撃を防ぐためのパラメータ化されたストアド プロシージャの使用とデータベースのアクセス許可の制限などについて説明しました。また、リクエスト フォージェリ攻撃を防御できるようにドメイン間ポリシー ファイルでのワイルドカードの使用を制限することについても説明しました (「Web に対する SDL の適用」を参照)。
このような SDL の追加機能はすべて Web アプリケーションの保護に必要ですが、Web アプリケーション開発チームにとっては新しい課題になります。SDL を Web アプリケーション チームやオンライン サービス チームにとっていっそう実用的なものにするには、これらのチームが使用する開発プロセスに合わせて、プロセス自体を変更する必要があります。つまり、SDL では新しい Web 固有の要件が必要になるだけではなく、要件を異なる方法で適用する必要もあります。
Web アプリケーションのニーズに SDL を適合させるときに最も難しいのは、単に時間の難しさです。もともと、SDL は Windows、Microsoft Office、SQL Server などの大きくて複雑な製品のセキュリティを向上させるために開発されたもので、結果として非常に成功しました。成功の一因は、それが徹底したものであることによります。最新バージョンの SDL には、80 を超える異なる要件と推奨事項があり、製品チームは製品のセキュリティとプライバシーを向上させるためこれらに準拠します。図 1 は、SDL に関係するプロセスの概念です。
図 1 従来のセキュリティ開発ライフサイクル
開発サイクルが 2 ～ 3 年にもなる SQL Server のような製品の場合は、リリース スケジュールの全体をとおして 80 個の要件を実施することは、不当な負担とはいえません。しかし、ほとんどの Web アプリケーションの開発サイクルはそれほど長くはなく、2 ～ 3 週間程度です (実際、マイクロソフトのオンライン サービス チームの中には、開発サイクルがわずか 1 週間というものもあります)。そのため、Office の場合は 3 年間で 80 の SDL 要件を満たすことは問題でなくても、Office Live のようなオンライン サービスに対して 3 週間ごとに 80 の要件を実施するのを期待することはまったくの無理難題です。また、この膨大なセキュリティ作業を実施できたとしても、新しい機能を実装する時間はあまり残らないでしょう。セキュリティは非常に重要なことですが、他のすべてのことからセキュリティを完全に切り離して考えることはできません。

新しいプロセス: SDL とアジャイルを結合する
この問題に対処するため、SDL チームは、迅速なアプリケーション開発 (RAD) またはアジャイル開発プロセスを使用するチームのニーズに合うように SDL を修正しました。この新しい SDL バリエーションの正式な名前はまだ決まっていませんが、この記事では SDL/Agile または略して SDL/A と呼ぶことにします。段階的ではない反復的な開発方法論を使用するすべての開発チームは、"従来の" バージョンの SDL ではなく、SDL/A プロセスを選択するのが望ましいと考えられます。SDL/A の対象は Web アプリケーションだけではありません。箱売り製品の多くも、アジャイル開発方法論を使用しています。
従来の SDL と SDL/A の最大の違いは、SDL/A では、すべての要件をすべてのリリース (またはすべての "スプリント") で実施する必要はないということです。これには異論があるかもしれません。たしかに、これまでは SDL のすべての要件は重要で省略できないと言ってきました。
SDL のすべての要件は、セキュリティまたはプライバシーの重大な脆弱性を防ぐことが実証されているので組み込まれていることは確かです。特定の禁止された API を避けるための要件は、バッファ オーバーフローを防ぐことが実証されているために組み込まれています。Web ページで表示する前にユーザー入力をエンコードするための要件は、クロスサイト スクリプティング攻撃を防ぐことが実証されています。しかし、説明したように、短いリリース サイクルでは、すべての SDL 要件を実施しながら機能の開発も行う時間は本当に取れないのです。そこで、SDL/A では、3 レベルの要件頻度 (つまり、要件を実施する必要のある頻度) を定義し、各 SDL 要件をこれら 3 つのカテゴリのいずれかに分類しています。

すべてのスプリントの要件
SDL/A の第 1 の頻度レベルは "スプリントごと" のレベルです。これらの要件は、交渉の余地がないものと考えられ、スプリントの長さに関係なく、すべてのスプリントで実施する必要があります。これらの要件は、頻繁に実施されるため開発チームにとっては最も大きな負担となるので、2 つの要因に従って非常に慎重に選択されています。
第 1 の要因は、要件の重要性です。つまり、その要件によっていくつの脆弱性が防止され、それらの脆弱性はどの程度重要かということです。すべての要件が重要ですが、脆弱性はそれぞれ不具合に対する重みが異なるので、脆弱性を防止する要件の重みも異なります。
たとえば、一時的なサービス拒否 (DoS) 攻撃は、リモートで実行される特権の昇格ほど重大な攻撃ではないと考えられます。特権の昇格攻撃を防ぐ SDL の要件の方が、すべてのスプリント頻度のカテゴリに含まれる可能性ははるかに高くなります。過去の脆弱性データも考慮します。要件が重大なセキュリティ脆弱性を防いだことがある場合 (たとえば、要件が存在したことで Slammer や Blaster ワームなどによって悪用される脆弱性の発生が防がれた場合)、その要件はほぼ確実にすべてのスプリント カテゴリに組み込まれます。
すべてのスプリント カテゴリに分類される要件の 1 つの例は、ASP.NET で提供される ValidateRequest クロスサイト スクリプティング (XSS) 防御を使用するための要件です。ValidateRequest 要件は、すべてのスプリントに組み込むための両方の条件を満たします。つまり、この要件は、重大性の高い XSS 攻撃から製品を保護すると共に、ValidateRequest は既定で有効になっているので基本的に無料です。
すべてのスプリント カテゴリに要件を含めるかどうかを判断する第 2 の要因は、要件を簡単に自動化できるかどうかということです。要件が別の Code Red 攻撃を防ぐ可能性が低い場合であっても、開発ビルド プロセスまたはコード チェックイン ポリシーで自動化できる場合は、すべてのスプリントで必要な場合があります。
すべてのスプリント要件のもう 1 つの例は、アプリケーションに対する脅威をモデル化するための要件です。ValidateRequest の有効化とは異なり、脅威のモデル化には多大な時間と労力が必要な場合があります。ただし、脅威のモデル化は SDL の基盤であり、省略はできません。脅威のモデル化は、開発ライフサイクルで後になって発見されたのでは軽減のために大がかりな再設計が必要になる可能性のある設計レベルでの脆弱性を明らかにするためには不可欠です。心臓手術のように、脅威のモデル化にはコストがかかりますが、それだけの価値はあります。
多くの場合、脅威のモデル化のコストはそれほど高くありません。アジャイル開発プロセスの信条の 1 つに、現在のスプリントの間に完了する作業以外は計画しない、というものがあります。これは脅威のモデル化にも当てはまります。ごく初期のスプリントの間に、完全なシステムの詳細な脅威のモデル化を作成する必要はありません。現在のスプリントでは、そのスプリントで開発している機能についてのみ脅威をモデル化する必要があります。このようにすることで、脅威のモデル化の要件は費やされる時間に関して自動的に規制されます。

開始時の要件
要件の第 2 のグループは、"開始時" の要件と呼ばれるものです。これらの要件は、開発チームがプロジェクトの開始時に 1 回実施する必要があるもので、同じプロジェクトで再び実施する必要はありません。従来の SDL では、すべての要件がこれに該当します。ただし、従来の SDL を使用する製品は、通常、明確に定義された開発フェーズを持ち、それには明確に定義された終了 (製品のリリース) も含まれます。一方、Web アプリケーションには明確に定義された終了がないことがよくあります。たとえば、チームが毎週金曜日の午後に新しい更新をリリースし、月曜日の朝には新しい機能の開発を開始するということが、いつまでも続くような場合があります。
開始時 1 回のみの要件の例としては、セキュリティ固有のデータを追跡するための製品のバグ追跡システムを構成する要件があります。マイクロソフトでは、開発中に検出されて、製品のバグ障害で緊急、重要、または中程度と評価されたすべてのセキュリティ上の欠陥を、リリース前に解決する必要があります。しかし、どの欠陥がセキュリティ上の問題で、どれが機能上の問題であるかを追跡できなくては、リリース前にセキュリティ上の問題を確実に修正できません。さらに、セキュリティ上の欠陥の原因 (バッファ オーバーフロー、ディレクトリ トラバーサル、SQL インジェクションなど) やセキュリティ上の欠陥の影響 (いずれかの STRIDE 値) などのセキュリティ固有の欠陥フィールドは、欠陥を修正して再発しないことを保証するのに有効な場合があります。プロジェクトの長さに関係なく、チームがこのような追跡システムを設定する必要があるのは 1 回だけです。
もう 1 つの、もう少し複雑な開始時の要件は、アプリケーションのベースライン脅威モデルを作成するための要件です。アプリケーションが新しいプロジェクトで、それまでにコードがまったく作成されていない場合は、この要件は不要になります。チームではスプリントごとに、新しい機能を設計するときに、その機能の脅威をモデル化することだけが必要です。一方、製品チームが既存のプロジェクトに対して SDL/A プロセスを初めて適用する場合もあります。この場合は、ベースライン脅威モデルを作成して、アプリケーションの既存機能をカバーする必要があります。
セキュリティ障害対応計画の作成などの、他の開始時要件が重要になる場合もあります。チームが、特に新しくて不慣れな SDL プロセスの最初のステージで、セキュリティに対して必要以上に時間を費やすことがないように、SDL/A では開始時要件を実施するための 3 か月の猶予期間がチームに与えられています。

バケットの要件
すべてのスプリントまたは開始時のどちらの要件カテゴリにも属さない他のすべての SDL 要件は、要件の 3 つのバケット、つまりセキュリティ検証、設計レビュー、またはレスポンス計画のいずれかに分類されます。したがって、たとえば、入力処理ルーチンをファジー化する SDL 要件は、セキュリティ検証バケットに分類されます。同様に、障害復旧計画を定義する要件は、レスポンス計画バケットに分類されます。
これらのすべての要件を製品リリース前に実施する必要がある場合、従来の SDL とは異なり、SDL/A では、各バケットの 1 つの要件だけを各スプリントの間に実施する必要があります。これは、アジャイル開発プロジェクトの短いリリース スケジュールに対する SDL/A の譲歩です。諺に言うように、10 ポンドの小麦粉を 5 ポンドの袋に入れることはできません。バケット要件は、取り除いた余分な 5 ポンドです。図 2 は、SDL/A での新しいフローです。印の付いている項目を対応するステージの間に実施することに注意してください。
図 2 SDL/A のライフサイクル
ただし、製品チームはすべてのバケット要件をすべてのスプリントで実施する必要はありませんが、それはいつまでも省略してよいという意味ではありません。実際には、チームは少なくとも 1 年に 1 回は、各バケット要件を実施する必要があります。また、連続する 2 つのスプリントで同じバケット要件を実施することは禁止されています。たとえば、プロジェクト X のチームが、3 月のスプリントではセキュリティ検証バケット要件を満たすために攻撃対象領域分析を実施することにした場合、4 月に再び攻撃対象領域分析を実施することはできません。さらに正確に言えば、本当に必要であると考えるなら 4 月に別の攻撃対象領域分析を実施してもかまいませんが、それだけでは SDL の責任を果たしたことになりません。
これら 2 つの制約を除くと、チームは特定のスプリントで実施するバケット要件を自由に選択できます。SDL/A では、どのような種類の順序またはラウンドロビン要件の選択も必須ではありません。これにより、製品チームは自分たちが最も有効であると考えるセキュリティ アクティビティを柔軟に選択できます。したがって、これまでにチームが Application Verifier を実行したときに多数の欠陥が検出され、SOAP Fuzzer を実行したときに欠陥がまったく検出されなかった場合は、Application Verifier を 2 スプリントに 1 回の割合で実施し、SOAP Fuzzer を年に 1 回だけ実施するようにしてもかまいません。

制約事項
正式に禁止されてはいませんが、設計、実装、検証、リリースの各フェーズが個別に明確に定義されているウォーターフォール スタイルの開発プロセスに従うチームには、SDL/A には適していません。SDL/A は、反復的な、フェーズのない開発方法論で使用するために設計されています。明確に定義されたフェーズを使用する製品の場合は、従来の SDL の方がはるかに適しています。

まとめ
アジャイル セキュリティの概念は言葉の矛盾ではありません。SDL/A において、マイクロソフトの SDL チームは、セキュリティの目的を強化しながら、極端に短い期間で新しいコード (新しいセキュリティ向上だけでなく新しい機能も含みます) をリリースする必要性についても留意した、一連のプロセス向上を定義しました。短いスプリントで従来の SDL 要件を実施した経験 (成功または失敗のどちらでも) がある場合は、SDL/Agile についての意見をぜひお聞かせください。連絡先は blogs.msdn.com/sdl です。また、SDL の公式サイト www.microsoft.com/sdl もご覧ください。

Bryan Sullivan は、マイクロソフトのセキュリティ開発ライフサイクル チームのセキュリティ プログラム マネージャであり、Web アプリケーションのセキュリティ問題を専門に扱っています。最初の著作である『Ajax Security』は、2007 年 12 月に Addison-Wesley から出版されました。


アクセス制御
Windows のファイルとレジストリのアクセス許可について
John R. Michener

この記事では、次の内容について説明します。
  • アクセス制御リスト
  • ユーザー権限
  • ファイル システムのアクセス許可
  • レジストリとそのアクセス許可
 この記事では、次のテクノロジを使用しています。
Windows Server 2008
目次
オブジェクトのセキュリティ記述子
セキュリティ記述子 string_ace について理解する
標準権限と固有の権限
整合性ラベルとその使用法
object_guid と inherit_object_guid
セキュリティ記述子 string_ace を解釈する
Windows リソース保護
安全なファイル システム アクセス許可を設定する
レジストリとそのアクセス許可を管理する
まとめ
システムで何かが起こるときには、必ず何らかのプリンシパル (ユーザーやサービスのために動作しているプロセスやスレッドなど) が、オブジェクトに対して動作を行います。よく知られたオブジェクトの例としては、ファイル、ディレクトリ、レジストリ キーがあります。Windows の基本的なセキュリティ メカニズムでは、操作の実行を許可する前に、信頼されたシステム コンポーネントによってアクセス許可および権限がチェックされます (AccessCheck)。したがって、アクセス許可と権限を設定することで、システムの動作を管理できます。水面下で何が行われているかを理解しなければアクセス許可を適切に設定することはできないため、この記事ではまず、オブジェクトに対する各種のセキュリティ設定とその処理のしくみについて説明してから、それらの値の設定方法を説明します。
技術的な詳細に踏み込む前に、Windows アクセス制御リスト (ACL) の GUI を使用して、Windows Server 2008 のシステム ドライブのルートでのアクセス許可について確認しておきます。Windows エクスプローラを開いてセキュリティ タブを選択し、ローカル ディスク (C:) を右クリックして [プロパティ] をクリックすると、管理者にフル コントロールが許可されていることがわかります。[グループ名またはユーザー名] で SYSTEM をクリックすると、SYSTEM にもフル コントロールが許可されています。[グループ名またはユーザー名] で Users をクリックした場合、アクセス許可の状態はそれほど単純ではありません。図 1 に示したシステム上の Users グループには、"読み取りと実行"、"一覧表示"、"読み取り" などのアクセス許可があります。[詳細設定] ボタンをクリックすると、Users グループに関連付けられたアクセス許可の詳細を表示できます (図 2 を参照)。
図 1 ローカル ディスク C のユーザー権限
図 2 ドライブ C のユーザー権限の詳細 (クリックすると拡大画像が表示されます)
ここで、Users グループのメンバには、システム ドライブのルートでフォルダを作成することと、ファイルにデータを追加することが許可されています。[編集] ボタンをクリックすると、図 3 に示すように、サブフォルダに対する "特別な" 許可を確認できます。この操作には、管理者特権が必要です。
図 3 特別なユーザー権限の編集画面
このように、通常のユーザーに対して Windows Server 2008 のシステム ドライブのルートでは、サブフォルダを作成することと、それらのサブフォルダに内容を追加することが既定で許可されています。Windows Server 2008 で Users グループのメンバにこの機能が提供されたのは、一部のサードパーティ ソフトウェアがこれらのアクセス許可を前提とし、マイクロソフトでもアプリケーションの互換性保持を望んだためです。
それでは、これらの問題の技術的な側面と、ユーザーに提供される GUI インターフェイスでそれらがどのように扱われるかについて見ていきましょう。Windows 内のすべての名前付きオブジェクトにはセキュリティ記述子があります。この記述子は、オブジェクトに関する情報、および指定されたアクセス許可を持つユーザーおよびサブジェクトの一覧を保持しています。また、システム イベント ログにどのオブジェクト アクセスを記録する必要があるのかもセキュリティ記述子で指定できます。
あるサブジェクト (ユーザー、プロセスなど) がオブジェクトまたはリソースに対して何を許可されているかという情報は、ACL と呼ばれるデータ構造に指定されます。ACL には、特定のオブジェクトに対してだれが (どのプリンシパルが) どのようなアクセスを行えるかが列挙されています。随意 ACL (DACL) は、オブジェクトの所有者による変更が許可されている ACL です。オブジェクトがアクセスされるときには、セキュリティ記述子をプリンシパルのアクセス許可と比較することで、要求されたアクセスが許可されるかどうかを確認します。
Windows ではオブジェクトに対してシステム ACL (SACL) もサポートされ、多くのリリースでは SACL 設定を使用して、監査ログにどのイベントを記録するかを設定しています。Windows Server 2008 および Windows Vista では、整合性レベル情報を保持するように SACL が拡張されています。
この整合性ラベルにより、低い権限の Internet Explorer で使用される Internet Explorer プロセスには "低" ラベルが設定されます。重要なシステム リソースを保護するには、"システム" および "高" ラベルが使用されます。Windows メッセージ ポンプでは、メッセージの整合性レベルに基づいてメッセージをフィルタ処理します。たとえば、中レベルのプロセスは低レベルのプロセスから送信されたメッセージを受信せず、高レベルのプロセスは低レベルまたは中レベルのプロセスから送信されたメッセージを受信しません。
現時点で、整合性レベルによる保護は速度の低下を招き、セキュリティを保証できる真のセキュリティ バリアではありません。これが本当のセキュリティ バリアとなる可能性のある将来のリリースでは、この速度低下はずっと大きくなります。
現在使用されている他の各種の OS と同様に、Windows は DACL を利用して一般的なアクセス制御の決定を行っています。ここでは、DACL に重点を置いて説明します。プリンシパルがオブジェクトに対して特定の操作を実行できるかどうかをシステムが決定する際には、いくつかのことがチェックされます。プリンシパルの特権、プリンシパルのトークン、およびオブジェクトのセキュリティ記述子です。オブジェクトに対するバイナリ セキュリティ記述子は、プリンシパルのトークンと共に AccessCheck ルーチンに渡されます。要求されたアクセスのマスク ビット ベクトルが作成され、これはアクセス チェックの成功のために付与される必要のあるアクセス権限を表しています。このベクトルはプリンシパルのセキュリティ記述子と共に AccessCheck ルーチンに渡されます。ルーチンはユーザーのセキュリティ トークンを調べ、要求されたアクセスおよびオブジェクトの DACL と共に、プリンシパルの特権 (通常は、管理者などの役割やメンバシップに基づく) を考慮に入れます。
要求されたアクセスがプリンシパルの特権によって満足される場合は、アクセスが許可されます。それ以外の場合は、DACL のアクセス制御エントリ (ACE) が順番に調べられます。セキュリティ システムで、要求されたアクセス コンポーネントがすべて許可されること、またはそのいずれかが拒否されることを決定できた場合には、直ちに、前者の場合は成功、後者の場合は失敗が返されます。
したがって、DACL の ACE のリストは適切に順序付けられている必要があります。標準的な順序としては、最初に明示的な拒否を配置してから、明示的な許可、全般的な (グループ) 拒否、グループ許可の順に配置します。標準的な順序を使用しないと、予期しない許可や拒否が行われる場合があります。

オブジェクトのセキュリティ記述子
セキュリティ記述子はバイナリのデータ構造ですが、セキュリティ記述子の文字列形式として、ある程度人間が読み取れるテキスト形式が用意されています。文字列形式のセキュリティ記述子は、Null で終わる文字列で、4 つの主要なコンポーネントのそれぞれを示すトークンを含んでいます。これらは、図 4 に示すように、所有者 (O:)、プライマリ グループ (G:)、DACL (D:)、および SACL (S:) です。
図 4 セキュリティ記述子
セキュリティ識別子 (SID) は、解析情報を提供するための構造を持ち、所有者に対する固有の識別子として機能する 96 ビットのランダムな情報を含んでいます (また、32 ビットのシーケンス カウントを含む場合もあります)。string_ace (文字列形式で表現された ACE) は、DACL でアクセス許可を明示的に付与または拒否したり、SACL でポリシーを設定したりするための構造です。各 string_ace はかっこで囲まれ、次のような構造をしています。
(ace_type;ace_flags;rights;object_guid;inherit_
   object_guid;account_sid)
対象となるオブジェクトへの適切なアクセスに必要な許可だけが含まれる必要があります。owner_sid および group_sid は、セキュリティ記述子から省略されることがよくあります。作成時に明示的に指定されていなくても、セキュリティ記述子の所有者フィールドは、オブジェクトの作成を起動したプリンシパルの SID に設定されます。グループ フィールドは、プリンシパルのセキュリティ トークンのプライマリ グループに設定されます。オブジェクトの監査や整合性ラベルの設定が必要でない場合には、SACL は存在しません。
string_ace 内には、必要なフィールドだけが含まれ、最小の組み合わせは、ace_type、権限、およびサブジェクト (通常は account_sid) です。通常、object_guid と inherit_object_guid は含まれません。システムは、アクセスが許可または拒否されるまで、ACE を最初から最後まで順番に解析します。したがって、ACE の順序は重要です。"拒否" は "許可" よりも前に配置する必要があります。
ACE を含まない ACL は、空の DACL と呼ばれます。指定されたサブジェクト アクセスをオブジェクトに対して許可するのは ACE であるため、空の DACL ではだれもオブジェクトにアクセスできません。また、DACL のないオブジェクトは、NULL DACL を持つオブジェクトと呼ばれます。NULL DACL を持つオブジェクトは、セキュリティで保護されておらず、だれでもフル コントロールが可能です。そのため、空の DACL も NULL DACL も設定しないようにしてください。
ここで、現実に使用されるセキュリティ記述子がどのようなものかを見ておくことは、価値があります。Windows Server 2008 システム ドライブのルートに対するセキュリティ記述子を次に示します (cacls は、ACL を調べて設定するための従来のコマンド ライン ルーチンであり、icacls で置き換えられます。残念ながら、icacls は、結果を標準のセキュリティ記述子定義言語 (SDDL) で出力するためのコマンド ライン スイッチをサポートしていません。cacls にはこのスイッチとして /S フラグがあります)。
C:\>cacls c:/ /s
c:\"D:PAI(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CIIO;DC;;;BU)(A;OICIIO;GA;;;CO)"
セキュリティ記述子に関してこれまでに得た知識に照らせば、先頭の "D:" から、所有権やグループ メンバシップが指定されず、記述子が DACL であることがわかります。この DACL は保護されています。"P" および Windows NT 5.0 の継承フラグが設定されています。そして、解読する必要のあるいくつかの ace_string が含まれています。

セキュリティ記述子 string_ace について理解する
前に示した string_ace の形式を思い出してください。許可の ace_type を図 5、許可の ace_flag を図 6 に示します。継承の ace_flag は、ACE の継承に関して支配的な役割を持つ要素です。
図 5 許可の ace_type
図 6 許可の ace_flag
ACE 内で権限は、ACE で制御されるアクセス権限を表す文字列によって示されます。この文字列には、"0x7800003F" のような 16 進文字列表現、または、後で説明する "CCLCSWLOCRRC" のような権限文字列の連結を使用できます。16 進表現および関連付けられたビット値を図 7 に示します。
図 7 ACL アクセス マスク
システムでは、すべてのオブジェクトに対して ACE 権限のビットマップ表現を 1 つ使用します。さまざまなオブジェクトに対してすべてのビットに意味があるわけではありません。オブジェクトに対して適切な権限だけが適用されます。標準権限は、セキュリティで保護できるすべてのオブジェクトに共通の権限です。汎用権限は、同様な意図を持った権限を各種のオブジェクトに対して指定するための簡便な方法です。汎用権限を指定すると、適切な固有の権限のセットにマップされます。また、SACL の指定時には、ACE 権限フィールドを使用して整合性ラベルがエンコードされます。図 8 に、各種オブジェクトに対して使用できる権限の一覧を示します。
図 8 汎用権限
あまり区別されずに使用される、ほぼ等価な権限マッピングがいくつかあります。FC (Full Control) は、GA (Generic_All) と等価です。ファイル システムの場合は、FA (File All) が適切なフル コントロール宣言です。KA (Key All) は、レジストリに対して適切なフル コントロール宣言です。ジェネリック宣言は、より適切な宣言の代わりに使用されることが多いですが、場合に応じて適切なファイル システム宣言やレジストリ キー宣言にマップされます。SDDL の表記ではこれらの用語がよく混在しているため、同じものを表していることに注意が必要です。

標準権限と固有の権限
多くのオブジェクトに対して、権限を割り当てることができます。オブジェクトには、ファイルやディレクトリの他に、レジストリ キー、プロセス、デスクトップなどがあります。すべての権限の一覧については、図 A ～ J を参照してください。ここではファイル システムとレジストリのアクセス許可について述べるため、これらのオブジェクトに対する固有の権限を図 9 および 10 に示します。
図 9 固有のファイル権限
図 10 固有のレジストリ権限

整合性ラベルとその使用法
前に述べたように、整合性ラベルが使用される場合は、オブジェクトの SACL に格納されます。オブジェクトには暗黙的に中レベルの整合性が設定されているため、整合性ラベルがない場合、オブジェクトは中レベルの整合性を持ちます。同様に、セキュリティ トークンに整合性ラベルがない場合も、中レベルの整合性を持ちます。低レベルの整合性ラベルは、低い権限の Internet Explorer および関連する信頼されていないオブジェクトなど、低い権限のプロセスを示すために使用されます。"高" および "システム" レベルは、それらのオブジェクトを中レベルおよび低レベルのプロセスおよびオブジェクトから分離するのに役立ちます。図 11 に整合性ラベルを示します。
図 11 整合性ラベル

object_guid と inherit_object_guid
object_guid および inherit_object_guid は、Active Directory でオブジェクトのセキュリティを指定するときに使用されます。これらは、ファイル システムおよびレジストリのセキュリティ保護には使用されません。ACE 文字列の ace_type フィールドの "OA" および "OD" は、それぞれオブジェクト許可およびオブジェクト拒否の ACE に対応しています。この場合、object_guid はアクセス許可されるオブジェクトの GUID を保持し、inherit_object_guid は、アクセス許可の継承元となるオブジェクトの GUID を保持します。
ACE 構造の account_sid フィールドは、ACE で指定されるアクセス権限が許可または拒否されるセキュリティ プリンシパルを表します。account_sid フィールドには SID を保持できます。これは、人間にとっては基本的に意味のない長い構造の識別子であるか、または一般的なアカウントに対する簡単な "SID 文字列" です。システムを読み取りやすくするために、可能であれば、一般的なアカウントに対する SID 文字列表記が使用されます。一般的なアカウントとそれらの SID 文字列の一覧は、図 J に示してあります。
所有者権限の OW 宣言は、Windows Server 2008 および Windows Vista で新たに導入されたものです。以前には、オブジェクトの作成者/所有者 (CO) がそのオブジェクトに対する RC (Read Control) と WD (Write_DAC) の標準権限を持っており、所有者がオブジェクトのセキュリティを設定できました。そのため、ユーザーがグループのメンバであり、多数のオブジェクトを作成する場合には、問題が生じます。そのユーザーがグループを抜けた場合でも、それらのオブジェクトに対して引き続き権限を持つことになります。なぜなら、そのユーザーがそれらのオブジェクトの所有者であり、RC および Write_DAC アクセス許可が付与されているからです。OW の所有者 ACE 制限が存在すると、所有者に対する RC/WD の暗黙的な許可は、ACL 内の他の関連する ACE の所有者 ACE に対してそれらの許可が明示的に付与されない限り、ブロックされます。これにより、上記のセキュリティ問題を緩和できます。

セキュリティ記述子 string_ace を解釈する
システム ドライブのルートに対する cacls からの出力は次のようなものでした。
"D:PAI(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CIIO;DC;;;BU)
    (A;OICIIO;GA;;;CO)"
これを解析して読みやすくすると、次のようになります。
"D:PAI
(A;OICI;FA;;;SY)
(A;OICI;FA;;;BA)
(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CIIO;DC;;;BU)
(A;OICIIO;GA;;;CO)"
これは、保護された DACL であり、現代のファイル システムに対して自動継承フラグがセットされています。保護フラグは、継承可能な親許可が継承されないことを意味します。つまり、DACL はオブジェクトの親からの継承に対して保護されています。この場合は、ルートであるため親はありません。
組み込みの管理者およびシステムに対しては、ファイル (オブジェクト継承により) およびディレクトリ (コンテナ継承 (CI) により) の両方について、継承可能な File All が許可されています。つまり、この DACL は、ルート以下にあるすべてのファイルとディレクトリに対して再帰的に File All を許可します。ただし、保護された DACL によって継承が停止される場合は除きます。その場合は、保護された DACL 内の許可を調べる必要があります。CO には Generic_All が許可され、これは、ルート ディレクトリ以下のファイルとディレクトリに対する File All へとマップされます ("継承のみ" のフラグにより)。
組み込みユーザーへの許可は、これよりずっと興味深い内容です。最初の string_ace はルート以下のファイルとディレクトリの両方に適用され、List、Read、ReadEA、Traverse、Execute、ReadAttr、ReadControl、および Sync が許可されます。2 番目の string_ace ではルート以下で AddSubDir が許可され (IO (継承のみ) フラグにより)、3 番目の string_ace ではルートより下位のディレクトリで AddFile が許可されています。これは、Windows エクスプローラの ACL グラフィカル インターフェイスでこれらのアクセス許可を確認したときに表示されるのと同じ内容です。

Windows リソース保護
Windows Server 2008 および Windows Vista から、コンポーネントは必要なセキュリティ設定をそれぞれのマニフェストで宣言するようになりました。これらのマニフェストは、マイクロソフトのコード署名ルートによって署名されます。マニフェストは、ファイルに関連付けられた ACL および他のアクセス許可を指定します。したがって、コンポーネントがインストールされると、適切なセキュリティ設定も導入されます。さらに、OS ファイルは、Windows リソース保護 (WRP) を使用して、管理者による不慮の損傷から保護されます。WRP は新しいシステム レベルのエンティティである Trusted Installer を利用して、システム ファイルおよびフォルダを所有し、管理します。
Windows Vista では、承認されたコンポーネントを通常のユーザーがインストールできるようにするための優れた機能が追加されています。したがって、Power User の役割は不要になり、Power User SID を含む ACE のインスタンスは削除されました。Power User グループはまだ存在しますが、コンポーネントのマニフェストがスキャンされ、検出された PU への許可のインスタンスはすべて削除されています。
システム ディレクトリで新しいアクセス許可を確認してみましょう。これは、SDDL を読み取るための良い練習にもなります。
C:\>cacls c:\windows /s
C:\Windows "D:PAI(A;;FA;;;S-1-5-80-956008885-3418522649-18310
38044-1853292631-2271478464)(A;CIIO;GA;;;S-1-5-80-956008885-3
418522649-1831038044-1853292631-2271478464)(A;;0x1301bf;;;SY}
(A;OICIIO;GA;;;SY)(A;;0x1301bf;;;BA)(A;OICIIO;GA;;;BA)(A;;0x1200a9;;;BU)
(A;OICIIO;GXGR;;;BU)(A;OICIIO;GA;;;CO)"
Trusted Installer の SID は、S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464 です。簡単にするために "TI" で置き換えると、次のようになります。
C:\Windows "D:PAI
(A;;FA;;;TI)(A;CIIO;GA;;;TI)
(A;;0x1301bf;;;SY)(A;OICIIO;GA;;;SY)
(A;;0x1301bf;;;BA)(A;OICIIO;GA;;;BA)
(A;;0x1200a9;;;BU)(A;OICIIO;GXGR;;;BU)
(A;OICIIO;GA;;;CO)"
これを解釈すると、Windows NT 5.0 の継承モデルを使用して C:\Windows に適用される、保護された ACE であることがわかります。
Trusted Installer には C:\Windows に対するフル コントロールが許可され、C:\Windows の下位にあるすべての子コンテナに対して Generic_All が許可されます (CI、継承のみであるため)。
システムおよび管理者には、C:\Windows に対して Read、Write、Append、ReadEA、WriteEA、Execute、ReadAttr、WriteAttr、Del、RCtl、および Sync (= SDGRGWGX) が許可されます。これは、Generic_All から Write_Owner および Write_DAC を除いたものです。管理者とシステムには、所有者または ACL を変更する権限以外のすべてが許可されます。"BA" および "SY" には、子ファイルおよび子ディレクトリ オブジェクトに対して Generic_All が許可されます。
管理者は所有権取得の特権を持つため、WriteOwnership をアサートすれば、いずれにしても制御権を得ることができます。管理者とシステムとは、セキュリティ的に等価です。ただし、この特権により、管理者はその気になれば WRP ACL 制御を回避できます。
CO にも、子ファイルおよび子ディレクトリ オブジェクトに対して Generic_All が許可されます。組み込みユーザーには C:\Windows に対して Read、ReadEA、Execute、ReadAttr、RCtl、および Sync (= GRGX) が許可され、C:\Windows より下位のサブディレクトリとそのファイルに対して GRGX が許可されています。
すべてのシステム ファイルおよびフォルダには、Trusted Installer にフル コントロールを許可する保護された ACL があります。Trusted Installer によるファイルの制御は、システム ルート ディレクトリに対する宣言には記述されず、Windows コンポーネントの個別の宣言で記述されています。

安全なファイル システム アクセス許可を設定する
ファイル システム ACL がどのように機能するのかがある程度わかったところで、その設定方法を見ていきましょう。アプリケーションをインストールする場合には、既定の Program Files フォルダにインストールする必要があります。この場所に対する既定の ACL は、管理者およびローカル システムに対してフル コントロールを許可しますが、それは適切です。
アプリケーションをどこか他の場所にインストールするか、またはユーザーにインストール先の選択を許可する場合には、問題が生じます。他のドライブ、およびシステム ドライブの非システム/非アプリケーション領域に対する既定の ACL は、十分に安全ではありません。そのような場合は、適切な保護された DACL を使用して、フォルダに明示的に ACL を適用する必要があります。アプリケーションをインストールする際に最も単純で最も安全な選択肢は、Program Files フォルダのセキュリティ設定を複製することです。それを行わない場合は、DACL を設定して、管理者以外が DACL や実行可能ファイルの所有権を変更できず、実行可能ファイルを含むディレクトリ内のファイルを書き込み、追加、または削除できないようにする必要があります。
DACL を設定する場合の基本的なルールは、ユーザーによって書き込まれたコードが管理者や他のユーザーによって実行されるのを避けることです。これは、該当するフォルダが信頼されていると推定される場合に、特に問題となります。これは一般に、フォルダが信頼された領域 (Windows、Program Files など) に存在している場合です。それにより、管理者に対して特権の昇格 (EoP) が許可され、ユーザー間攻撃のリスクが高まります。したがって、ユーザーがそのようなフォルダにファイルを書き込むことができる場合、他のユーザーや管理者がそれらのファイルを実行できないようにする必要があります。
一見、ユーザーにはどのようなときも Windows、System、Program Files などのフォルダに書き込みを許可すべきでないように思えます。しかし、そうすることには妥当な理由があります。最も一般的な理由は、エラー ログ データを記録するためです。実行可能ファイルがユーザーの資格情報で実行中に、エラーを記録する必要がある場合には、エラー ログ フォルダへの書き込み/追加アクセスが必要になります (マルチユーザー システムでユーザーごとの場所にエラーを記録した場合には、ログ データが実行可能ファイルに関連付けられる代わりに、システム全体に散らばってしまいます。通常、アプリケーションおよびサービスは、共有フォルダまたはレジストリ キーに書き込みを行います)。ユーザーのアクセス許可に基づいて動作しているプロセスが、指定されたコンピュータ レジストリ キーにエラー情報を頻繁に格納する場合、同じ問題がレジストリでも生じます。
ユーザーが書き込み可能なファイルを実行可能ファイルと混在させないでください。信頼される必要のあるファイル (実行可能ファイルなど) と、信頼されてはならないファイル (信頼されないユーザーによって書き込まれる可能性のあるファイル) に対しては、別々のディレクトリを使用します。ディレクトリには適切な ACL を適用します。管理者には実行可能ファイルのコントロールを許可し、ユーザーには読み取り/実行を許可しますが書き込みは許可しません。レジストリ キーおよびサブキーにも同じガイドラインが当てはまります。
ここで、クライアントとサーバーの違いに注意してください。サーバー管理者は、管理者として実行しているユーザーよりも、ずっと多くの知識を持つことが想定されます。サーバー管理者は、システム上の信頼されない領域からコードを実行してはならないことを知っています。システム上で実行可能ファイルとデータ ファイルを分離するような名前付け規則を設定することにより、実行可能ファイルの信頼性に関する手掛かりを管理者に提供できます。ログ用のサブディレクトリは信頼できないため、それらのサブディレクトリをユーザーが作成または変更できないようにする必要があります。それを可能にすると、安全な名前付け規則を装うことができてしまうからです。
クライアント環境では、管理者ユーザーを騙してコードを実行させることはずっと簡単です。そのように騙されやすい管理者がいるため、ユーザーが書き込み可能なディレクトリでは管理者に実行特権を与えないようにします。それにより、ユーザーが実行可能ファイルをインストールするのを防ぎ、管理者ユーザーがそれを実行してシステムを危険にさらすのを防ぎます。たとえば、アプリケーションまたはサービスが、ユーザー特権で書き込まれるログ情報を格納する必要がある場合には、ログ用のサブディレクトリを作成してこのデータを保持する必要があります。このサブディレクトリでは、管理者に実行権限を与えないようにします。そのための方法の 1 つは、D;OI;WP;;;WD のように、全員に対する明示的なファイル実行拒否を先頭に追加することです。これにより、ユーザーにファイルの書き込みまたは変更を許可しているディレクトリで、ユーザー間攻撃を防ぐことができます。
複数のユーザーがデータを共有することが想定されているケースも多くあります (ただし、ユーザーがコードを共有したり、共有領域からコードを実行したりするのは望ましくありません)。たとえば、ホーム ユーザー (またはそのユーザーが使用している写真表示アプリケーション) は、C:\Photos のようなフォルダを作成する可能性があります。このユーザーは、複数のユーザーがこのフォルダに書き込み、フォルダ内のさまざまな写真を編集できるようになることを期待しています。Windows Vista のシステム ドライブのルートに対する既定の ACL では、これをサポートしています。他の一般的な共有状況としては、あるユーザーが別のユーザーも読み取れるようにデータを配置するようなフォルダが考えられます。データの削除または変更が許可されるのはデータの作成者のみですが、他のユーザーはデータをコピーしてから、そのコピーを編集できます。これは共有読み取りシナリオであり、Windows Server 2008 のシステム ドライブに対する既定の設定です。
システム ドライブ、そして具体的には ACL フォルダを共有に対してロックダウンする場合を考えます。次の 2 つの一般的なシナリオに対して適切である ACL を選択する必要があります。1 つは管理者がオブジェクトを管理できるようにすることであり、もう 1 つは、これらのフォルダ内のコードの実行に関連したセキュリティ問題を防ぐことです (ここに示す ACE では、これらのフォルダからのコードの実行を所有者にも禁じています)。
共有読み取り ACE を次に示します。
D:P(D;OI;WP;;;WD)(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICI;FA;;;CO)(A;CI;0x1200af;;;AU)(A;OI;GR;;;AU)
次に、共同作業 ACE を示します。
D:P(D;OI;WP;;;WD)(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICI;FA;;;CO)(A;OICI;SDGRGW;;;AU)
どちらの ACL も、全員に対する実行拒否 ACE、オブジェクト継承 (ファイルに適用するため) から始まることで、ユーザー システム攻撃およびユーザー間攻撃を防いでいます。次に、管理者、システム (実際にはシステムには必要ありませんが)、および CO に対して、フル コントロール (= File All) を許可しています。
共有読み取りシナリオでは、認証されたユーザーに対して List、AddFile、AddSubDir、ReadEA、Traverse、ReadAttr、RCtl、および Sync が許可され (CI に対する、したがってディレクトリに対するこの許可の制限により)、そしてすべてのファイルに対して別個に Generic Read が許可されます。共同作業シナリオでは、認証されたユーザーに対して、ファイルおよびディレクトリに対する Delete、Generic Read、および Generic Write が許可されます。

レジストリとそのアクセス許可を管理する
Windows では、その状態情報の多くが Windows レジストリに格納されます。レジストリのデータ ストアはハイブと呼ばれ、データはキーおよびサブキーに格納されます。これらはいずれもコンテナとして表示されます (サブキーはオブジェクトとしては表示されません)。
ユーザー固有のデータは、HKEY_USERS というハイブの該当するユーザー セクションに格納されます。予想されるとおり、このデータの多くはユーザーによって書き込み可能です。任意のセッションで、HKey_Current_User (HKCU) は HKEY_USERS の該当するセクションを指し示します。
システムおよびコンピュータの情報は、HKEY_LOCAL_MACHINE (HKLM) ハイブに格納されます。HKLM には、さまざまなシステム サービスすべてに対する情報が含まれています。現在、そのほとんどは、各種の Local Service グループまたは Network Service グループの下で制限されたアクセス許可により実行されています。サービスおよびアプリケーションは、状態情報をそれぞれのレジストリ キーに格納できます。この情報は、サービス キー内のサブキー、またはサービス キー下のいずれかのキー内のサブキーに格納する必要があります。サービスがその固有のサービス キーに対して SetKey (または同様な攻撃を可能にする WDac や WOwn) 権限を持つような ACL を適用してはなりません。そのような場合、サービスが別の実行可能ファイルを指し示すことが可能になるためです。その結果、システムの読み込み時に、サービス コントロール マネージャは指し示された実行可能ファイルを読み込むため、サービス ホストに対して EoP の危険性が生じます。
HKLM に対する DACL の一般的なガイドラインは、ユーザーに対して、このデータまたは関連する ACL および所有権の書き込みおよび変更を許可しないことです。システム領域のファイル システム DACL の設定に関するガイドラインと同様、ユーザー コンテキストまたは制限されたコンテキストで動作中のアプリケーションやサービスがエラー情報を記録する必要がある場合には、エラー ログについての例外が生じます。そうした状況についてのガイドラインは、ファイル システムでの同じ問題についてと同様に、そのような情報に対して別個のキーを作成し、それらに適切な ACL を適用することです。したがって、信頼されているサブジェクト (管理者、システムなど) に対して機密性の高い情報への ACL を適用する一方、ログ データは必要に応じて書き込み可能にできます。
避けたい状況は、ユーザーが信頼されたパラメータを変更 (ウイルス対策またはマルウェア対策サービスをオフにするなど) したり、ユーザーや管理者用のツールを改ざんしたりすることです。メモ帳が起動されるときに C:\windows\notepad.exe を読み込むと仮定しましょう。C:\windows に対する既定の ACL では、攻撃者が実行可能ファイルを変更することは許可されません。攻撃者がメモ帳アイコンからのリンクを自分の実行可能ファイルに書き換えることができた場合、攻撃者は別のファイル (たとえば、C:\tools\load_rootkit.exe) を読み込ませることができます。その場合、ルートキットを読み込んでからメモ帳を読み込むことが可能であるため、ユーザーはセキュリティの侵害に気付きません。
攻撃者がレジストリを通じてリンクをドライブできる場合、ファイル システムに保護 ACL を適用してもあまり意味がありません。また、制限されたシステム サービスから他のシステム サービスに対する攻撃にも注意する必要があります。Windows Vista および Windows Server 2008 では、サービスは必要とする特権によっていくつかのグループに分けられています。このようなサービスの分離によって提供される多層防御を利用するには、各サービスが他のサービス (特に、別のサービス グループのサービス) を改ざんできないように、サービスのアクセス許可を構成する必要があります。
ユーザーが悪意のある実行可能ファイルを追加したりリンクしたりすることを防ぐのと同様に、サービスがそれぞれのアクセス許可や機能を変更する能力を持たないようにする必要があります。サービスに対する ChangeConf 特権を付与するのは、管理者、システム、または Trusted Installer に限定する必要があります。この特権を持っていればサービスのアクセス許可を変更できるためです。

まとめ
Windows には、アクセス許可に関する機能が豊富に用意され、これらを使用して操作を許可したり、ブロックしたり、新しい脅威に対して多層防御を行ったりできます。このような豊富なアクセス制御能力には、複雑さの問題がどうしても伴います。
いくつかの一般的なガイドラインに従うことで、問題の回避に役立ちます。たとえば、システムの既定の設定は、合理的な妥協案です。それらの設定は、使用する必要があります。Program Files フォルダ以外にアプリケーションをインストールする場合には、プログラム ファイル ACL を使用します。場合によっては、ドライブ上でのユーザーに対する既定の許可など、既定値を多少厳密にする必要がありますが、それを行う場合には、発生する可能性のあるアプリケーション互換性の問題を調べ、適切に対処する必要があります。
最も重要なガイドラインは、管理者またはシステム アカウントが、ユーザーに書き込みまたは変更が許可されているコードを実行したり、そのようなコードへのポインタを参照したりしてはならないことです。それとほぼ同じくらい重要なこととして、各ユーザーも、他のユーザーが書き込みまたは変更できるコードを実行したり、そのようなコードへのポインタを参照したりしてはなりません。これらのガイドラインは、ここで説明したすべてのセキュリティ問題にかかわるものです。これらのガイドラインに従って変更を加えれば、ほとんどの深刻なセキュリティ問題は回避されます。
アクセス制御コンポーネントの詳細については、MSDN の「アクセス制御コンポーネント」を参照してください。ace_string アクセス マスク コンポーネントについては、MSDN の記事「ACCESS_MASK」に記載されています。この記事では、ファイル、ディレクトリ、レジストリ キー、および共有セクションに対する固有の権限について取り上げています。制限付き SID の詳細については、MSDN の記事「制限付きトークン」を参照してください。

全権限の一覧
図 A 標準権限
図 B ファイルとディレクトリに対する固有の権限
図 C ファイル マップとレジストリに対する固有の権限
図 D サービス コントロール マネージャとサービスに対する固有の権限
図 E プロセスとスレッドに対する固有の権限
図 F ウィンドウ ステーションとデスクトップに対する固有の権限
図 G シンボリック リンクとイベントに対する固有の権限
図 H セマフォとミューテックスに対する固有の権限
図 I パイプとトークンに対する固有の権限
図 J 一般的なアカウントとそれらの SID 文字列
John R. Michener は、マイクロソフトのシニア セキュリティ プログラム マネージャを務めています。約 5 年前にマイクロソフトの Windows Security に加わりました。システム セキュリティの分野で 20 年以上の経験を持ち、3 つのセキュリティ企業を立ち上げました。Windows Software Assurance チームで暗号化とアクセス許可の専門家として活躍しています。連絡先は jmichene@microsoft.com です。


ユーティリティ スポットライト
Visual Round Trip Analyzer で Web ページを高速化する 12 のステップ
Jim Pierson

この記事は、VRTA のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • ラウンドトリップの詳細
  • Web ページの高速化のヒント
  • Visual Round Trip Analyzer
 この記事では、次のテクノロジを使用しています。
Visual Round Trip Analyzer
目次
ロケーションの重要性
簡単な 12 のステップ
Visual Round Trip Analyzer の使用
最後に
Web ページのパフォーマンスは、サーバーとクライアント間の距離、ページ上の要素のサイズ、ブラウザが要素を読み込む方法、利用可能な帯域幅など、実にさまざまな要因の影響を受けます。ボトルネックを探し当て、原因を特定することは簡単ではありません。しかし、原因さえ特定できれば、状況を大幅に改善できる可能性があります。
この記事では、パフォーマンス低下の一般的な原因を特定して対処する方法について取り上げます。また、パフォーマンスの問題を引き起こしている各種の要因を特定し、グラフィカルなマップとして表現することのできるツール、Visual Round Trip Analyzer (VRTA) も紹介します。さらに、VRTA を使用した解析の方法について説明します。ただし、その前に、巻末にある「ページの読み込みを改善する方法」という簡単なクイズに答えてみましょう。

ロケーションの重要性
ページの読み込みを遅延させる 2 大要因は、ラウンドトリップの時間 (RTT) で評価される "距離" と、クライアントとサーバー間のラウンドトリップの "回数" です。ラウンドトリップとは、クライアントからサーバーにネットワーク パケットが送信された後、サーバーから応答パケットが返されるまでをいいます。単一の Web ページをダウンロードするのに要するラウンドトリップは、ページの内容が希薄であれば 2 ～ 3 回、ページの外観が凝ったものだと、数十回のラウンドトリップが発生します。TCP 接続を開くのに要するラウンドトリップは 1 回です。1 回のラウンドトリップで、Get を送信し、最初の 3 KB を受信します。確認応答を送信しながら、受信するデータの量を段階的に増やしていくと、それだけ多くのラウンドトリップが発生します。
ユーザーと Web サイトのデータ センターの距離が近い場合 (数百マイル以内など)、たとえば、ロサンゼルスのユーザーが、シリコン バレーのサーバーに接続する場合で言えば、RTT はわずか 20 ms (0.020 秒) で、ページの読み込み時間への影響はさほど大きくありません。サーバー側の処理時間を除く、ネットワークのみの遅延で考えた場合、この条件では、仮にクライアントとサーバー間で 50 回のラウンドトリップが発生したとしても、ページの読み込み時間は、たったの 1 秒です (50 RT × 20 ms = 1 秒)。
しかし、同じシリコン バレーのサーバーに対し、欧州やアジアのユーザーが接続するとなると、話はまったく変わってきます。たとえば、日本や英国のケースで言うと、現在、カリフォルニアとのラウンドトリップ時間は、最短でも 120 ms です。1 要求あたりの、サーバーのディスク シーク タイムは一般に 10 ms 未満と考えられています。これに対し、数百ミリ秒のラウンドトリップが何度も何度も繰り返されることを想像してみてください。距離とラウンドトリップ回数が、ページの読み込みに時間がかかる最も大きな要因であることは容易に想像がつきます。
ラウンドトリップに時間がかかることで生じる影響を最小限に抑えるためには、どうすればよいのでしょうか。試してみる価値のある手段としては、主に 3 つの方法があります。1 つ目は、より多くの処理を並列的に行い、余分な負荷をなくすことで、直列的なラウンドトリップの回数を減らすことです。2 つ目は、ロケーションをユーザーに近づけることによって、ラウンドトリップ時間を減らすことです。3 つ目は、サーバー側の処理時間を減らすことです。
パフォーマンス分析を行うためには、ページのダウンロードを正確に、つまり、実際のデータの流れを反映して視覚化することが不可欠です。たとえば、具体的にページのどの部分が直列的にダウンロードされ、どの部分が並列的にダウンロードされているかを知るのは困難です。一般に、ファイルのダウンロードは、1 本の横棒が 1 つのファイルを表す、ウォーター フォール (落水) のようなものとして描写されます (図 1 を参照)。しかし、この描写だと、"HTTP1.1 以降では TCP 接続が再利用される" という事実が反映されません。接続を開いたまま、既存の接続で別のファイルを呼び出した場合、ラウンドトリップの回数が減ることになりますが、これを、従来のようにウォーター フォールを使った図で表現することは困難です。実際にページが読み込まれるようすを、より正確に描写するには、TCP ポートを表す次元 (軸) を追加する必要があります。
図 1 HTTP1.0 ウォーター フォールの図
図 2 は、7 つの TCP ポートでファイルが読み込まれるようすを示しています。縦軸 (灰色の水平線) は TCP ポートを、横軸は経過したミリ秒数を表します。色分けされたバーはファイルです。赤は HTML テキスト、ゴールドはスタイル シート、ベージュは JavaScript、青は画像を表します。この図を見ると、最初に単一のポートが開かれた後、さらに 2 つのポートが開かれているのがわかります。その 2 つのポートでは、最初のファイル (CSS) が読み込まれた後、他のファイル (JavaScript および画像) が読み込まれています。
図 2 HTTP1.1 の Keep Alive
VRTA は Microsoft Network Monitor (NetMon) 3.2 というパケット アナライザをベースにしています。ただし、必ずしも NetMon の使い方を知っている必要はありません。近日公開予定の最新バージョンでは、ファイルやパケットの詳細を視覚的に表示できる機能が、グローバル パフォーマンス チームによって追加されています。図 3 にその例を示します。TCP 接続が作成されると、ピンクがかった線が表示されます。この線は、サーバーからの TCP 応答パケットを表します。灰色の線は、サーバーから最初にデータを受け取るまでの時間 (TTFB: Time To First Byte) です。階段状になっているのは、"TCP スロー スタート" が使用されているために、応答パケットの送信量が徐々に増えていくからです。
図 3 ファイルとパケットの詳細

簡単な 12 のステップ
よく Web ページは常に最高速度でダウンロードされ、バイト数が増えた場合にだけ、ページのダウンロード速度が低下すると考えられがちです。しかし、実際は違います。ユーザーがデータ センターのすぐ近くにいるのでなければ、ブラウザが帯域幅をフルに利用できることはまずありません。ここで、この問題を緩和するための 12 のステップを紹介します。
1. 十分な数のポートを開く ブラウザもアプリケーションも、同時に開くことができるポート数を制限することができます。Internet Explorer 7.0 およびそれ以前のバージョンでは、HTTP1.1 ポートの数がドメインあたり 2 つに制限されていました。たとえば、msn.com という 1 つのドメインにつき、2 つのポートしか利用できないことになります。静的な JavaScript、CSS、および画像がすべて単一のドメインでホストされている場合、以前のブラウザでは 2 つのポートしか開くことができません (図 4)。コンテンツを複数のドメインに分散すると、ブラウザがより多くのポートを開けるようになります。もっと多くのポートを開くことができるように、ブラウザの動作を変更することも、1 つの方法です。実際、これは、Internet Explorer チームが現在検討していることでもあります。
通常、単一ファイル (最悪の場合は、順次ダウンロードされる一連のファイル) のダウンロードのボトルネックはページそのものにあります。1 つのページが同時に読み込む理想的なファイル数は 6 ～ 8 個です。図 4 に示したビット レート グラフを見てください。最初の 3 秒間はトラフィックがきわめて少ないことがわかります。図 5 のオレンジ色のファイル (グラフ左上隅) は、SSL ハンドシェイクです。四角形の内側は、この後で詳しく説明するパケット フローを示しています。この図を左から右に見ながら、活発にダウンロードされているファイルを数えていくと、開始から 3 秒が経過するまでは、複数ファイルの同時読み込みは行われていません。まず SSL が開始され、次に、赤色で示された他の 2 つの HTML ファイルが順次読み込まれた後、最後に 6 つのポートを使用して、9 個のファイルが読み込まれています。
図 4 少なすぎるポート数と多数の小さなファイル
図 5 同時に複数のポートを使用
2. 小さなファイルのダウンロード数を制限する 多数の小さなファイルを送信する場合、ワイド エリア ネットワーク (WAN) 経由で帯域幅をフルに利用することは困難です。図 4 を見ると、ページの読み込み処理全体 (特に 10 秒目以降) を通じて、この傾向が現れています。これらのアイコン イメージ ファイルは、1 ファイルあたり 500 バイト未満です。ユーザーとサーバー間の RTT を 200 ms とし、1 ファイルにつき 1 回のラウンドトリップが必要だと仮定すると、これらのファイルのうち毎秒 5 つしか読み込めないことになります (5 ファイル/秒 × 500 バイト/ファイル × 8 ビット/バイト = TCP ポートあたり 20 Kbps)。
小さなファイルをより大きなファイルにまとめることにより、1 回のラウンドトリップで送信できるバイト数を増やすことが可能です。これを画像のクラスタリングまたはスプライトと呼びます。この場合、単一のファイルを、隣り合わせに並ぶ多数の画像 (後でページ上で合成されて配置される) と共にダウンロードすることになります。この概念の実例を紹介しましょう。以前、Windows 2000 を出荷した際、我々は、レドモンドからドイツにアップグレード データをダウンロードした場合の所要時間を 6 時間と報告しました。しかし、詳しく分析した結果、実際には 30,000 個のごく小さなファイルが 1 つずつ転送されていることがわかりました。これらのファイルをすべて zip 圧縮して 1 ファイルにまとめたところ、ダウンロード時間が 35 分に短縮されたのです。
3. JavaScript ファイルを JavaScript エンジン以外で読み込む Internet Explorer 7 およびそれ以前のバージョンでは、JavaScript ファイルがブロックされます。通常、これらのブラウザでは、いわゆる "投機モード" で可能な限り高速にファイルがダウンロードされます。しかし、JavaScript に遭遇すると、ブラウザはこのモードを離脱し、JavaScript のダウンロードに集中し始めます。この状況を VRTA で観察すると、いずれの JavaScript ファイルも、他の JavaScript ファイルとダウンロードのタイミングが重なることはなく、また、他の種類のファイルもほとんど同時に読み込まれていないことがわかります。
この問題を回避する方法の 1 つとして、JavaScript ファイルを最後に移動することが考えられます。しかし、この方法には、使用可能な帯域幅があり余るほどに存在するという事実が見過ごされています。次のように、document.write を使用して、ファイルをブラウザの JavaScript エンジン以外の場所で読み込んだ方が適切です。
function AsyncLoad()
{
var l = arguments.length;
for (var i=0;i<l;i++)
{
document.write("<script src='" + arguments[i] + "'></" + "script>");
}
}
AsyncLoad(
"file1.js",
"file2.js",
"file3.js");
Internet Explorer 8.0 ではこの問題が解消されることを期待します。
4. Keep Alive を有効にする TCP ポートを開くには 1 回のラウンドトリップが、ファイルを取得するには少なくとも 1 回のラウンドトリップが必要になります。それ以降のファイルについても、開いた接続を維持しておくことができれば、ラウンドトリップの回数を減らし、実効スループットを増やすことができます。図 6 に注目してください。TCP 接続ごとに 1 ポートしか存在しません (灰色の水平線)。大きなファイルの場合だけ、ビット レートが急上昇しています。各ファイルを表す四角形の中には、応答データ量の増加を反映した階段状の模様が見えます。
図 6 Keep Alive
このような模様になるのは、TCP のスロー スタート アルゴリズムにより、ラウンドトリップに成功するたびに送信バイト数が増やされ、応答パケットの送信量が徐々に増えていくためです。大きなファイルの方が高速に送信できるのはそのためです。TCP 接続を再利用することで、後続のファイルは、増大したウィンドウ サイズを引き続き利用でき、最小のウィンドウ サイズからやり直す必要はありません。IIS では、Keep Alive が既定で有効化されています。ホストしているファイルが 1 つしかないという場合を除けば、常に有効のままにすることをお勧めします。
5. ネットワークの輻輳を特定する サーバーの遅延とネットワークの遅延を区別することが重要です。通常、ネットワークの遅延は、TCP 接続時間に注目するか、再送信の発生を観察することによって調べることができます。図 7 のデルタ (Δ) 列は、パケットとパケットの間隔 (ミリ秒単位) を示しています。クライアントによって送信された最初のパケットは、接続を開くための TCP 要求です (図 7 のフレーム 67)。サーバーから応答が得られなかったため、その 2,914 ms (2.9 秒) 後に、クライアントが要求を再度送信しています (同図のフレーム 306)。2 回目の要求の後、110 ms 後にサーバーから応答があったことがわかります (フレーム 307)。
図 7 パケットの間隔 (時間)
興味深いのは、TCP 接続パケットの再送信には、いったん接続された後でデータ パケットを再送信する場合と比べ、はるかに時間がかかっている点です。そのため、接続に失敗した場合は、最初の再送信で 2 秒以上かかることも珍しくありません。一方、データ パケットであれば、わずか数百ミリ秒でタイムアウトになります。
6. ネットワークの最大転送単位 (MTU: Max Transmit Unit) または TCP ウィンドウ サイズを大きくする イーサネットでのパケット サイズは、最大 1,514 バイトです。しかし、サーバー側で MTU (パケットのサイズ) が、これよりも小さなサイズに制限されていることがあります。比較的大きなファイルで、応答パケットが 1,500 バイトを大幅に下回っているような場合は、この値をチェックする必要があります。
これに関連したもう 1 つの問題として、TCP ウィンドウ サイズがあります。通常は、OS に応じて、16 KB または 32 KB のウィンドウ サイズが使用されます。TCP の受信側は、ある程度まとまった量のデータがたまるのを待って受信確認を送信します。ウィンドウ サイズを増やした場合、TCP の送信側は、そのウィンドウ サイズ (バイト数) までであれば、受信確認を待つことなく、次々にデータを送信できるようになります。極端に単純化すると、ラウンドトリップ 1 回あたりのバイト数と考えてもかまいません。通常、TCP/HTTP は、まず 1 回目の応答で 2 パケットのウィンドウを送信します (約 3 KB のデータ)。次に、サーバーが受信確認を待って、別のウィンドウを送信します。受信確認を正常に受け取ることができると、その都度、ウィンドウのサイズが増やされていきます。TCP では、このようにして、2 つのデバイス間で利用できる帯域幅が判断されます。
サーバーから受信する際のウィンドウ サイズが 16 KB 未満になっていて、それが遅延を引き起こしているようなら、これらの値を既定値に戻すようにしてください。ウィンドウ サイズを調べるには、NetMon を開いて、サーバーの応答パケットの "window size" という項目を探します。
7. サーバーの輻輳を特定する TCP 接続時間も、TCP レベルでの受信確認応答も速いにもかかわらず、その後の TTFB (Time To First Byte) データ応答パケットに時間がかかるようなら、低速なサーバーであることが疑われます。OSI モデル (Open System Interconnection) と同様、サーバーは、それぞれが独立して処理を行う、複数の層を成していると考えることができます。TCP レイヤはカーネル レベルまたは NIC カードで実行されます。これは、非常に高速です。要求パケットが着信すると、TCP レイヤは、そのデータを HTTP レイヤまで転送します。HTTP レイヤは、ディスク シークを伴うレイヤです。HTTP レイヤから応答があるまで、TCP レイヤは受信確認を送信できません (図 8 を参照)。
図 8 低速なサーバー
8. 不要なラウンドトリップが生じていないかチェックする 実行されているすべての処理が、本当に必要な処理であるとは限りません。ブラウザに対する指示 (ファイルをキャッシュできる期間など) が不十分であるために、不要な処理や判断が行われている可能性もあります。
9. 有効期限を設定する 静的ファイル (JavaScript、CSS、画像、XML など) は、ユーザーが次回そのページを読み込むときのために、ブラウザによってキャッシュされます。ブラウザは、ファイルの HTTP ヘッダーから有効期限を探すことによって、そのファイルがまだ有効かどうかを確認します。残念ながら、この日付を設定しているサイトは多くありません。そのため、ブラウザは、キャッシュからファイルを取り出した後、日付を見ることなく、Get-if-Modified をサーバーに送信します。ほとんどの場合、サーバーは 304 Not-Modified を返すので、結局は、キャッシュから取り出されたファイルが使用されることになります。
パフォーマンスの高い多くのサイトでは、有効期間が 3 年に設定されています。ファイルの名前やパスを変更したり、引数文字列を追加したりすると、ブラウザによってファイルがリロードされます。新しいサーバーが何台ぐらい存在するか、また、既にユーザーの手元にあるファイルの Get 要求を処理するために、どの程度の帯域幅が消費されているかなどを十分考慮してください。
有効期限には、もう 1 つ利点があります。Entity タグ (Etag) に起因する問題を回避できることです。Etag は、パフォーマンスの問題を引き起こすことがあります。本来、このタグの目的は、サーバーによって生成される番号を使って、ファイルを一意に識別することです。サーバーのクラスタ環境では、サーバーごとに異なる番号が生成されます。たとえば、キャッシュに見つかったファイルに関して、ブラウザが Get-If-Modified を送信するとします。サーバーが 1 台しかなければ、まったく同じファイルが存在する可能性が高く、もし、そうであれば、304 Not-Modified エラーが送信されます。しかし、ユーザーが、もっと大きなサーバー ファームにアクセスしているとしたら、どうでしょうか。アクセス先のサーバーは、毎回異なることが予想されます。サーバーが違えば、Etag も異なるため、サーバーは、ファイル全体をブラウザにリロードすることになります。
VRTA を使用しているとき、複数の JavaScript ファイルでサーバーから 200-OK が返されているようだと、サーバーが Etag を認識しておらず、ファイル全体を再送信していることが考えられます。ファイルがリロードされるだけではありません。ブラウザは、そのファイルがキャッシュにあるものと同じであると認識し、サーバーに再送信を完全に止めるように働きかけます。
具体的には、ブラウザから、TCP ポートを閉じるための Reset フラグが送信されます。結局は、残りのファイルのダウンロードを完了するために新しいポートが必要となるため、ラウンドトリップの回数が増えることになります。これを簡単に解決するには、ファイルに有効期限を設定することです。そうすれば、ブラウザは、サーバーに問い合わせることなく、キャッシュされたファイルを再利用することができます。
クイズ: ページの読み込みを改善する方法
記事を読む前に、Web のパフォーマンスに関する次の問題に答えてください。記事を読み終えたら、再度、問題に目を通し、きちんと知識が身に付いているか確認しましょう。
1. 同時に実行するポート数として最適なものを選んでください。
a. できるだけ少なく
b. ブロードバンドでは多くし、ナローバンドでは少なくする
2. WAN 経由で最も高速にダウンロードできるものを選んでください。
a. 多数の小さなファイル
b. 同サイズで少数の大きなファイル
3. 「JavaScript ファイルのダウンロード時にブロックされるのは他の JavaScript ファイルだけである」。この文は正しいと思いますか。
a. 誤り
b. 正しい
4. Keep Alive は既定で有効になっていると思いますか。
a. はい
b. いいえ
5. 既定では、有効期限がどのように設定されていますか。
a. 空
b. ダウンロードした日付から 30 日間
6. Etag によってファイルがリロードされるのを防ぐには、どうすればよいと思いますか。
a. 有効期限を設定する
b. ファイルがリロードされるまで待つよう、ユーザーにお願いする
7. HTML、JavaScript、および CSS ファイルの圧縮率は一般にどの程度だと思いますか。
a. 1.5 : 1
b. 3 : 1 (またはそれ以下のサイズに圧縮可能)
8. 「イメージ ファイルは常に圧縮される」。この文は正しいと思いますか。
a. 正しい
b. 誤り

10. リダイレクトを見直す サイト アドレスが変更された場合、リダイレクトによって、ユーザーを新しいアドレスに導く方法がよく用いられます。しかし、こうしたリダイレクトは時間がかかるので、別の方法を用いることをお勧めします。古い URL から応答することはできないか検討するようにします。
11. 圧縮を使用する 通常、JavaScript、CSS、および HTML ファイルは、最大で 4 対 1 程度まで圧縮できます。XML の場合は、もっと小さく圧縮できます。これらのファイル形式の特長は、サーバー側で一度圧縮すれば、その圧縮したファイルを保管しておくことができる点です。これにより、サーバーの CPU 負荷を下げ、ネットワークを流れるビット量を減らすことができます。
動的なファイル (ASPX など) は、ユーザーひとり一人についてその都度、圧縮する必要があるため、負荷分散装置など、外部のデバイスで圧縮されるのが一般的です。よく、圧縮するとブラウザ側の処理時間が増えるのではないか、または、圧縮に対応していないブラウザもあるのではないか、といった質問を耳にします。まず、大きなファイルをネットワーク経由で読み込むのに、どのぐらいの時間がかかるかを考えてみてください。VRTA はすべてのファイルを圧縮した後、圧縮前後のファイル サイズを比較し、Compressibility 列に圧縮前/圧縮後の比率を表示します。既に圧縮されているファイルは、比率が 1 未満として表示されます。たとえば、このサイトでは、JavaScript ファイルの読み込みに 1 ～ 5 秒かかっています。
最近の PC であれば、圧縮済みのファイルは数ミリ秒あれば解凍できます。圧縮処理に対応していることをサーバーに伝えるためには、ブラウザが Get 要求で "will accept gzip, deflate" ステートメントを送信する必要があります。
フルサイズの画像をサムネイルとして使用する場合は、常に再フォーマットが必要になります。また、各種の画像の形式を精査しながら、最適な圧縮方法 (JPG、GIF、または PNG) を見極めることをお勧めします。不思議なことに、画像が圧縮されないまま配信されていることがあります。画像はすべて適切に圧縮されるという誤解があるのかもしれませんが、そのようなことはないので注意してください。
ファイルの空白文字を削除することも重要です。空白文字を削除することで、サイズを 20% 削減できる場合があります。空白文字には、タブや改行も含まれます。また、これを実行するためのツールもいくつかあります。空白文字を削除した上で圧縮することによって、より大きな効果が得られます。
12. CSS を編集する どういうわけか、開発者は、CSS ファイルで、すべてのフォント インスタンスを略さずに記述する傾向があります。これでは、カスケード効果 (つまり、定義の継承) が活かされません。CSS ファイルに目を通し、同じ単語が何度も出現するようなら、より最適な書き方がある可能性があります。
Visual Round Trip Analyzer の使用
今度は、実際に皆さんのサイトの状況を調べてみましょう。VRTA は、MSDN Magazine Web サイトからダウンロードしてインストールできます。NetMon のバージョン 3.2 以降がインストールされていない場合は、NetMon も併せてインストールするように求められます。インストールには、コンピュータの管理者権限が必要です。
インストールが完了すると、VRTA が自動的に起動します。また、デスクトップにはアイコンが作成されています。開始ボタンをクリックすると、VRTA による NetMon キャプチャが有効になります。この状態で再度 Web サイトにアクセスしてみましょう。一度に行う操作は 1 つだけ (不可分な最小単位の操作) とします。たとえば、"あるページに移動して、ページを表示させる"、これが最小単位の操作です。VRTA を停止すると、結果が表示されます。次のページへの移動ボタンをクリックする前に VRTA を開始し、ページが表示されたら停止する、という操作を繰り返します。
次に、左側のフォルダ アイコンをクリックすると、既存のキャプチャ ファイルを開くことができます。この方法は、すべてのファイルを表示したい場合にも利用できます。VRTA では、すべてのファイルのテキスト版が作成されます (RTA.txt)。NetMon アイコンをクリックすると、キャプチャ ファイルに直接移動できます。保存、コピー、貼り付け、印刷など、一般的な機能も付属しています。開始ボタンと停止ボタンは既に使用しました。ズーム ボタンを使用すると、ファイルを拡大表示できます。細かくて見づらい情報を見たい場合には、非常に役立ちます。[Select Network] ボックスで適宜、ネットワークを選択することによって、有線 LAN インターフェイスやワイヤレス ネットワークをリッスンすることができます。グラフの尺度を設定するには、[Max Time] ドロップダウンを使用します。
このツールには、3 つのホバー ウィンドウがあります。1 つ目は [Main Chart] タブのウィンドウです。ファイルのポートごとのダウンロード状況を確認できます。いずれかのファイルの上にマウス ポインタを重ねると、そのファイルに関する詳細な情報が表示されます。2 つ目は [All Files] タブです。ファイルがスタート シーケンスで並べ替えて表示されます。URI にマウス ポインタを重ねると、各ファイルの詳細が表示されます。3 つ目は [Analysis] タブです。いずれかのルールにマウス ポインタを重ねると、ファイルの例外が一覧表示されます。
VRTA の実行結果をコピーし、貼り付けることもできますが、このツールでは、実行結果として 2 つのファイルが作成されます。1 つは、未加工のパケット情報が格納される NetMon キャプチャ ファイルです。このファイルから、VRTA のすべての UI を再現することができます。また、キャプチャをドリルダウンしながら、本格的な調査を行うこともできます。RTA.txt ファイルには、ページの読み込み時間の統計がタブ区切りで記録されています。このファイルは、Microsoft Office Excel で開くと、見やすく表示できます。
このツールを使ってさまざまな Web ページを調査していると、Web パフォーマンス テストにおける、いくつかの重要なポイントに気付くことと思います。テストはサーバーのすぐ近く (数百マイル以内など) で行っていても、実際のユーザーは世界中にいるかもしれません。そのような場合は、本番の環境でテストするか、WAN シミュレータ (エミュレータ) を使用する必要があります。WAN シミュレータは、ネットワーク上のビット レート、RTT、およびパケット損失を変化させることのできるソフトウェア ユーティリティです。
マイクロソフトで使用している WAN シミュレータ ベンチマークは、ビット レートが 300/64 Kbps で、RTT が 300 ms (グローバル コンテンツの場合) と 50 ms (ローカルのコンテンツ配信ネットワークの場合) です。パケット損失は 3% に設定しています。このベンチマーク テストにより、複数のアプリケーションやバージョンを対象に、同じ環境で相対的なパフォーマンスを評価することが可能となっています。
ただ 1 つ忘れてはいけないことがあります。サンプリングには、あくまでその 1 回のテストしか反映されていないという点です。キャプチャの結果は毎回微妙に変化します。めまぐるしく変わる広告の内容や、ネットワークやサーバーの輻輳によって、キャプチャの結果がまったく違ってくることもあります。サンプリングは、安定した状態が確認できるまで、繰り返し行うことを強くお勧めします。
NetMon をインストールすると、コマンド ライン版の NM_cap.exe もインストールされます。NM_cap を起動し、特定のページに移動して、ページが表示されたら、NM_cap を停止してファイルを保存する、という一連の処理を行うスクリプトを作成することも可能です。その結果は VRTA の UI から開いて確認できます。
初回使用時のユーザー エクスペリエンス (Page-Load-Time 1) を作り出す必要があります。これは、"About:Blank" ページに移動した後、(Internet Explorer の場合は [ツール] メニューの [インターネット オプション] タブで) キャッシュ済みのファイルを削除することによって行います。Blank (空白) ページを使用するのは、Internet Explorer では、閲覧中のページが自動的にリロードされてしまうためです。ブラウザを閉じて、すべての TCP 接続を強制的に切断します。次に、ブラウザを開いて、テスト対象のページに移動します。翌日、ユーザーが 2 回目に同じページにアクセスする状況 (Page-Load-Time 2) を作り出すには、単にブラウザを閉じて、テスト ページを開き直すだけです。
最後に、もう 1 つ、気を付けなければならないことがあります。社内プロキシ サーバーです。プロキシ サーバーには、悪意を持った侵入者がインターネットから社内リソースにアクセスできないように保護する役割があります。しかし、正確なパフォーマンス サンプリングを妨げる、という副作用もあります。圧縮が無効にされていると、大元のサーバーで圧縮が無効になっているように見えるなど、間違った判断につながる可能性があります。プロキシがあると、Keep Alive が有効かどうかも覆い隠されてしまいます。テストは、社内プロキシを介さず、直接インターネットに接続して行うのがベストです。

最後に
VRTA によって生成されたレポートも、この記事で紹介したベスト プラクティスも、今現在、知りうる範囲の知識に基づいたものです。それだけで、すべての傾向を把握したことにはなりません。NetMon によってキャプチャされた生データをドリルダウンし、新しいパターンやベスト プラクティスを自分で探し出す必要があります。
この新しい視覚化ツールの開発を支援してくださった Ron Oshima 氏、Lucius Fleuchaus 氏、Jason He 氏、および Doug Franklin 氏に感謝します。

Jim Pierson は、マイクロソフトに 12 年間在籍しており、そのうちの 5 年間を Web パフォーマンス エンジニアリング関連の業務に費やしてきました。MSN および Windows Live のオンライン サービスに対するベスト プラクティスについてのトレーニングや、測定、分析のためのツールを提供する GlobalPerf チームで Principle Group Manager を務めています。

クイズの答え:1=b、2=b、3=a、4=a、5=a、6=a、7=b、8=b


RIA
Silverlight 2 Web パーツで SharePoint に磨きをかける
Steve Fox および Paul Stubbs

この記事は、Silverlight 2 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。
  • Silverlight メディア プレーヤーを作成する
  • Web パーツを作成する
  • SharePoint サイトに配置する
  • アプリケーションをトラブルシューティングする
 この記事では、次のテクノロジを使用しています。
Silverlight 2、SharePoint
目次
メディア プレーヤーを作成する
アプリケーション UI をコーディングする
アプリケーション コード
Web パーツを作成および配置する
アプリケーションを SharePoint サイトに追加する
トラブルシューティング
Web ベース アプリケーション開発の世界では、ユーザーは絶えず向上するアプリケーション エクスペリエンスを要求しています。さらに、これらの Web ベース アプリケーションの需要は、単一のブラウザ テクノロジやプラットフォームのサポートにとどまりません。現在 Web アプリケーションを構築している場合は、より魅力的なユーザー エクスペリエンスを提供する方法だけでなく、開発者がこのエクスペリエンスを複数のテクノロジと対象ユーザーに迅速に提供できる方法をサポートするテクノロジが必要です。
Silverlight 2 は、堅牢な Web アプリケーション開発の機会をもたらすテクノロジの一例です。Silverlight は、Web ベースのユーザー エクスペリエンスを構築するために開発者がリッチ プログラミング モデルにアクセスできるようにするクロスブラウザ、クロスプラットフォーム対応のプラグインです。
SharePoint も、Web ベースのアプリケーション開発に使用されることの多いテクノロジです。イントラネット開発のために SharePoint に投資した企業は、外部向けの Web サイトにこのテクノロジを拡張することを望んでいます。Silverlight と SharePoint が連携して、洗練されたユーザー インターフェイス デザインと操作性を備えたイントラネットおよびエクストラネット アプリケーションを構築するための、単純だが強力なインフラストラクチャを提供します。
この記事では、Silverlight アプリケーションを SharePoint Web パーツ内に統合することで SharePoint と Silverlight を同時に使用できる主要な方法の 1 つについて説明します。まず、Silverlight 2 Beta 2 を使用してかなり単純なメディア アプリケーションを構築する手順を説明します (図 1 を参照)。次に、SharePoint Web パーツを Silverlight アプリケーションのラッパーとして使用する方法を示します。分離され、ホストされたアプリケーションを作成できるだけでなく、ネイティブ SharePoint オブジェクト モデル (たとえばプロバイダ/コンシューマ モデル) を通じて Web パーツを相互接続することもできるため、これは興味深いアプローチです。
図 1 Silverlight WMV Player Web パーツ (クリックすると拡大画像が表示されます)

メディア プレーヤーを作成する
入稿時点ではベータ 2 である Silverlight 2 は、Silverlight 1.0 からかなり進化しています。たとえば、Silverlight 2 ではマネージ コードがサポートされるため、JavaScript に加えて、Visual Basic と C# をアプリケーションで使用できるようになりました。さらに、Silverlight 2 にはコントロールのセットが付属しており、Silverlight ベースのユーザー インターフェイスを作成するジョブをはるかに簡単にするコントロールの拡張を備えています。デザイナ エクスペリエンスは大幅に向上し、Visual Studio 2008 と Expression Blend 内の UI デザイナ ツールでサポートされます。
SharePoint 用の Silverlight アプリケーションを構築するために環境を準備するには、次のソフトウェアがインストールされていることを確認してください。
  • Windows Server 2003 または Windows Server 2008
  • Windows SharePoint Services 3.0 および SP1
  • Microsoft Office SharePoint Server 2007 および SP1
  • Visual Studio 2008 Professional Edition
  • Silverlight Tools Beta 2 for Visual Studio 2008
  • Silverlight 2 Beta 2 ランタイム
  • Windows SharePoint Services 3.0 ツール: Visual Studio Extensions、Version 1.2
環境を設定した後で、Visual Studio 2008 を起動し、新しいプロジェクトを作成します。C# または Visual Basic ノードに、2 つのプロジェクト テンプレートを含む Silverlight という新しいプロジェクト ノードがあります。一方のプロジェクト テンプレートは Silverlight アプリケーションを作成するためのもので、もう一方は Silverlight クラス ライブラリを作成するためのものです (図 2 を参照)。
図 2 Silverlight アプリケーションの [新しいプロジェクト] テンプレート (クリックすると拡大画像が表示されます)
サンプル アプリケーションでは、Silverlight アプリケーション テンプレートを選択し、名前を付けて (ここでは MediaPlayer)、その他の既定のオプションを受け入れました。これらの手順が完了し、[OK] をクリックすると、新しい Web をソリューションに追加するか、プロジェクトに Silverlight をホストするために HTML テスト ページを動的に生成するかを確認するプロンプトが表示されます。最初の選択肢 (ソリューションに新しい Web を追加) を選択し、再び既定のオプションを受け入れます。終了すると、ソリューションは、(単なる HTML ページとは対照的に) Silverlight プロジェクトとホスト Web アプリケーションの両方から構成されます。
この例では、MediaPlayer ソリューションに 2 つのプロジェクトがあります。最初のプロジェクトは、MediaPlayerWeb という Web サイトです。これは、Silverlight アプリケーションの開発とテストに使用する Web サイトです。2 つ目のプロジェクトは、MediaPlayer という Silverlight アプリケーションです。この記事では、MediaPlayer プロジェクトの Page.xaml ファイルと Page.xaml.cs ファイルに注目します。
Silverlight 2 では、UI は XAML という XML ベースの宣言型言語を使用して生成されます。これは、クライアント アプリケーションの Windows Presentation Foundation (WPF) UI の作成に使用されるのと同じ言語です。Page.xaml には UI を定義する XAML コードが含まれており、Page.xaml.cs にはアプリケーションの分離コードが含まれています。
Page.xaml.cs ファイルは 1 つしか使用できませんが、XAML ファイルは一元的な Page.xaml ファイルを通じて複数管理できます。1 つの Page.xaml で個別の Silverlight コントロールを管理する必要がある場合に、これを行うことができます。

アプリケーション UI をコーディングする
Visual Studio 2008 では、XAML ファイルを開くと、分割されたデザイン/XAML ビューが表示されます。また、Silverlight 2 Beta 2 では、ツールボックスからコントロールをドラッグし、XAML ビューにドロップできます (デザイナ画面へのコントロールのドラッグは、Visual Studio 2008 ではサポートされていませんが、Expression Blend のデザイナ画面にドラッグ アンド ドロップすることはできます)。
統合の基本的な側面を示すために、MediaPlayer に対して作成した UI は意図的に単純になっています。図 3 に、Silverlight アプリケーションの UI を表す XAML コードを示します。StackPanel 要素を使用して、UI 内の他のコントロールを格納および配置しています。一方は MediaElement をホストします。これは、短いムービーのタイムラインを表す WMV ファイルおよび Slider 要素を再生する要素です。もう一方は Button、ToggleButton、および CheckBox 要素をホストします。これらの要素は、再生、一時停止、停止、およびミュート機能を提供します。
図 3 メディア プレーヤー XAML
<UserControl x:Class="MediaPlayer.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="400" Height="500">

    <StackPanel Grid.Row="1"
                Margin="20,10,20,10"
                Background="White"
                Visibility="Visible">

        <MediaElement x:Name="moviePlayer"
                      Source="Bear.wmv"
                      Margin="5">
        </MediaElement>

        <Slider x:Name="timeline"
                ValueChanged="timeline_ValueChanged"
                Maximum="1"
                Margin="5" >
        </Slider>
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Center">
            <Button x:Name="movieStop"
                    Content="Stop"
                    Height="25"
                    Width="40"

                    Click="movieStop_Click"
                    Margin="5">
            </Button>
            <ToggleButton x:Name="moviePlay"
                          Content="Play"
                          Height="25"
                          Width="40"

                          Click="moviePlay_Click"
                          Margin="5">
            </ToggleButton>
            <CheckBox x:Name="movieMute"
                      Content="Sound On"
                      Click="movieMute_Click">
            </CheckBox>
        </StackPanel>
    </StackPanel>
</UserControl>
XAML ベースの UI で注意する必要のある重要なプロパティの 1 つは、x:Name プロパティです。これはコントロールに対して提供される名前を表し、コントロールを分離コードから操作するために使用できます。また、クリックされたときに分離コードで moviePlay_Click メソッドを呼び出すボタンの click=moviePlay_Click など、XAML のコントロールの一部に対応するイベントにも注目してください。
XAML コードでは、ムービー Bear.wmv を再生するように MediaElement コントロールを構成します。独自のムービーを ClientBin フォルダから読み込むことができますが、XAML の Source プロパティを変更して、プロジェクトに追加するファイルの名前を反映する必要があります。SharePoint サイトのムービーのリポジトリとしてのピクチャ ライブラリの使用など、WMV ファイルの場所への HTTP 参照を提供できることにも注意してください。
各イベント ハンドラ (Click="movieMute_Click" など) について Page.xaml ファイル内にイベント ハンドラを作成する場合は、「Click」と入力してから IntelliSense を使用してイベント ハンドラを追加すると、Visual Studio によって、イベント ハンドラのスタブ メソッドが Page.xaml.cs ファイルに自動的に追加されます。
分離コードに移る前に、同じコードの Expression Blend ビューを簡単に見てみましょう。すべてのコーディングを Visual Studio 2008 で行いましたが、Expression Blend を使用して、後で Visual Studio の Silverlight プロジェクトに統合できる UI を作成することもできます。Expression Blend でページを開いた後で (図 4 を参照)、アイテムを Silverlight アプリケーションに追加し、ファイルを保存してから、Visual Studio 2008 に再び切り替えることができます。これにより、Expression Blend からの変更を Visual Studio 2008 プロジェクトで再読み込みするかどうかを確認するプロンプトが表示されます (Expression で開くには、Page.xaml を右クリックし、[Expression Blend を開く] を選択します)。
図 4 Expression Blend のプロジェクト ビュー (クリックすると拡大画像が表示されます)

アプリケーション コード
Silverlight アプリケーションの分離コードを追加するには、ソリューション エクスプローラで Page.xaml ファイルを右クリックし、[コードの表示] をクリックします。Page.xaml ノードを展開し、Page.xaml.cs ファイルを右クリックしてから [コードの表示] を選択することもできます。このビューのコードは、前に作成した UI に対応します。ここで、Silverlight コントロールの特定のプロパティを設定します。図 5 に、Page.xaml.cs ファイルに追加したコードを示します。
図 5 メディア プレーヤーの分離コード
namespace MediaPlayer {
    public partial class Page : UserControl {
        public Page() {
            InitializeComponent();
        }

        private void timeline_ValueChanged(
          object sender, RoutedPropertyChangedEventArgs<double> e) {
            long ticks = (long)((sender as Slider).Value *
              this.moviePlayer.NaturalDuration.TimeSpan.Ticks);
            TimeSpan movieTimespan = new TimeSpan(ticks);
            this.moviePlayer.Position = movieTimespan;
            if (this.moviePlayer.CurrentState == MediaElementState.Playing) {
                this.moviePlay.IsChecked = true;
                this.moviePlay.Content = "Pause";
            }
        }

        private void movieStop_Click(object sender, RoutedEventArgs e) {
            this.timeline.Value = 0;
            this.moviePlayer.Stop();
            this.moviePlay.Content = "Play";
        }
        private void moviePlay_Click(object sender, RoutedEventArgs e) {
            if (moviePlay.IsChecked == true) {
                moviePlay.Content = "Pause";
                this.moviePlayer.Play();
            }
            else {
                moviePlay.Content = "Play";
                this.moviePlayer.Pause();
            }
        }

        private void movieMute_Click(object sender, RoutedEventArgs e) {
            if (movieMute.IsChecked == true) {
                movieMute.Content = "Sound On";
                this.moviePlayer.IsMuted = true;
            }
            else {
                movieMute.Content = "Sound Off";
                this.moviePlayer.IsMuted = false;
            }
        }
    }
}
分離コードにいくつかのメソッドがあることがわかります。そのすべてが、XAML コードでコントロールに追加したイベント ハンドラに対応します。たとえば、このコードでは、movieStop_Click メソッドはユーザーによる [停止] ボタンのクリックに応答して、タイムライン値を 0 に設定し (つまり、ムービーを開始位置に戻し)、[再生] ボタンのテキストを変更します (元の状態に応じて [Pause] から [Play] に変更します)。
この分離コードは、ユーザーによる Play/Pause トグル ボタンのクリックに応答して、Play および Pause メソッドを適宜呼び出し、トグル ボタンのテキストを設定します。ユーザーは、スライダ コントロールを操作することもできます。このコントロールは、特定のポイントまでムービーをジャンプさせます。これは早送り/巻き戻し機能と同等です。最後に、ユーザーは [ミュート] チェックボックスをクリックしてムービーをミュートできます。
現時点では、キーボードの F5 キーを押すか、Visual Studio プロジェクト メニューの [デバッグ]、[デバッグ開始] の順にクリックして、Silverlight アプリケーションをデバッグできます。これにより、既定のブラウザが起動し、Silverlight アプリケーションが読み込まれます。デバッグが有効になっていないことを示す警告が表示されることがあります。[OK] をクリックして、Visual Studio で web.config ファイルを変更し、デバッグを有効にします。すべて正常に動作している場合は、WMV ファイル (この場合は Bear.wmv) が読み込まれ、再生が開始します。[停止] または [一時停止] をクリックするか、スライダ コントロールを使用して、再生状態を変更できます。[ミュート] チェックボックスをオンにして、音声に影響を与えることもできます。
Silverlight アプリケーションは動作していますが、SharePoint との統合に進む前に、もう 1 つ手順を完了する必要があります。この手順では、構築した Silverlight アプリケーションを含むアーカイブである XAP ファイルが、SharePoint Web パーツで選択して読み込める場所にあることを確認します。
XAP ファイルには、Silverlight アプリケーションのすべてのコア アプリケーション ファイルが含まれます。XAP ファイルは基本的に構築済みプロジェクト ファイルのアーカイブであるため、.zip 拡張子を付けて XAP ファイルの名前を変更してから開き、XAP パッケージに含まれるファイルを確認できます。
MediaPlayer.xap ファイルを開くと、複数の異なるファイルが表示されます。それらは、プロジェクトの DLL と AppManifest.xaml ファイルです。アーカイブ内の DLL は、作成するコード (この例で作成した DLL は MediaPlayer.dll) と、コンパイルしたアセンブリの実行に必要な Microsoft DLL です。AppManifest.xaml ファイルが生成され、プロジェクトからの依存関係が一覧表示されます。 次に、この記事で説明した MediaPlayer 例の AppManifest.xaml ファイルのコンテンツを示します。これには、Silverlight アプリケーションの配置に固有のいくつかの主要な参照が含まれています。
<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  EntryPointAssembly="MediaPlayer"
  EntryPointType="MediaPlayer.App"
  RuntimeVersion="2.0.30523.6">
  <Deployment.Parts>
    <AssemblyPart x:Name="MediaPlayer" Source="MediaPlayer.dll" />
    <AssemblyPart x:Name="System.Windows.Controls.Extended"
      Source="System.Windows.Controls.Extended.dll" />
  </Deployment.Parts>
</Deployment>
これは、アセンブリの名前 (MediaPlayer)、アプリケーションのコンパイルに使用した Silverlight ランタイム バージョン (2.0.30523.6)、および XAP アーカイブにも含まれている必須アセンブリの一覧が表示されます。
XAP ファイルが SharePoint サイト内の場所にコピーされていることを確認します。MediaPlayer 例では、ClientBin というフォルダを SharePoint サイトのルート仮想ディレクトリに作成しました。XAP ファイルを SharePoint ドキュメント ライブラリまたは SharePoint サーバー上の _layouts フォルダにコピーすることもできます。ClientBin 内に、XAP というフォルダを作成しました。このフォルダには、すべての XAP ファイル (または、構築して SharePoint と統合する Silverlight アプリケーション) が含まれます。この場所は、SharePoint Web パーツ プロジェクトで Silverlight コントロールのインスタンスを作成する場合に参照する必要があります。また、Silverlight アプリケーションを更新および再コンパイルする場合は、最新の XAP ファイルを適切なディレクトリに忘れずにコピーしてください。
XAP ファイルをサーバー上の場所にコピーすると、Silverlight アプリケーションの SharePoint Web パーツ ホスト コンテナの作成に進む準備が整います。
Web パーツの名前を変更する
Web パーツ プロジェクトの既定の名前を受け入れた場合、プロジェクト シェルには WebPart1 という Web パーツが含まれます。これを保持することもできますが、おそらく Web パーツにはより説明的な名前を付ける必要があります。
この操作を行うには、既存の Web パーツ (WebPart1) を右クリックし、[削除] を選択します。次に、[WSP] タブをクリックし、WebPart1WebPart の下にあるものをすべて削除します (SharePoint に配置するときの Web パーツ パッケージのビューである WSP ビューが表示されない場合は、Visual Studio のメイン メニューで [表示]、[その他のウィンドウ]、[WSP View] (WSP ビュー) の順に選択します)。Web パーツの削除を確認した後、ソリューション エクスプローラでプロジェクトを右クリックし、[新しい項目の追加] を選択します。[新しい項目の追加] ダイアログで、SharePoint ノードを選択し、Web パーツを選択します。Web パーツに、よりわかりやすい名前を指定します。
新規 Web パーツをプロジェクトに追加する (クリックすると拡大画像が表示されます)

Web パーツを作成および配置する
Web パーツを作成するには、Visual Studio を開き、新しい SharePoint Web パーツ プロジェクト テンプレートを開きます (図 6 を参照)。この例では、新しい Web パーツ プロジェクトに MediaPlayerWebPart という名前を付けました。
図 6 Web パーツ プロジェクト テンプレート (クリックすると拡大画像が表示されます)
新しい Web パーツを追加した後で、コードを追加する前にプロジェクトに 2 つの参照を追加する必要があります。最初の参照は System.Web.Extensions DLL で、2 つ目の参照は System.Web.Silverlight DLL です。Web パーツにコードを追加するには、Web パーツ コード ファイル (たとえば MediaPlayerWebPart.cs) を右クリックし、[コードの表示] をクリックします。図 7 に、Web パーツ プロジェクトに追加したコードを示します。
説明が必要な重要項目が 2 つあります。まず、ScriptManager オブジェクトを使用して Silverlight アプリケーションをレンダリングしているという事実です。Silverlight アプリケーションは AJAX ScriptManager コントロールに対して依存関係を持つため、まず ScriptManager オブジェクトのインスタンスをプロジェクトに追加する必要があるというのがその理由です。
2 つ目は、Silverlight コントロールのインスタンス化です。図 7 のコードでは、myMediaCtrl は Silverlight コントロールのインスタンス (事実上、作成した Silverlight メディア プレーヤー) を表します。コントロールの新しいインスタンスを作成した後で、プロパティをいくつか設定する必要があります。これらのプロパティの中でも非常に重要なのは、XAP ファイルの場所を指し、Web パーツ内に読み込む Source プロパティです。
図 7 メディア プレーヤー Web パーツ
namespace MediaPlayerWebPart {
    [Guid("7e7ef885-130a-45e6-ab14-15f344b1939d")]
    public class MediaPlayerWebPart :
      System.Web.UI.WebControls.WebParts.WebPart {
        public MediaPlayerWebPart() {
        }

        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);

            ScriptManager scriptManager = ScriptManager.GetCurrent(this.Page);
            if (scriptManager == null) {
                scriptManager = new ScriptManager();
                this.Controls.Add(scriptManager);
            }
        }

        protected override void CreateChildControls() {
            System.Web.UI.SilverlightControls.Silverlight myMediaCtrl =
              new System.Web.UI.SilverlightControls.Silverlight();
            myMediaCtrl.ID = "myMediaPlayer";
            myMediaCtrl.Source = "/ClientBin/XAP/MediaPlayer.xap";
            myMediaCtrl.Width = new System.Web.UI.WebControls.Unit(900);
            myMediaCtrl.Height = new System.Web.UI.WebControls.Unit(650);

            this.Controls.Add(myMediaCtrl);
        }
    }
}
コードを追加すると、2 つ目の手順を実行する準備ができます。その手順とは、配置する Web パーツ プロジェクトの構成です。この操作を行うには、プロジェクトの [プロパティ] ビューを開き、[デバッグ] タブをクリックします。[ブラウザを開始時に使用する URL] チェックボックスをオンにしてから、SharePoint サイトのルート URL を入力します (たとえば、http://spvm)。
Web パーツの配置をテストするには、[ビルド] をクリックしてから、[ソリューションの配置] をクリックします。これにより、WSP パッケージ (SharePoint アセンブリ配置で CAB と同等のもの) が自動的に作成されます。このパッケージには、Web パーツ アセンブリを SharePoint に配置し、ソリューション ファイルを SharePoint サイトの適切な場所に配置し、インターネット インフォメーション サービス (IIS) Web アプリケーションを停止して再起動するために必要なすべてのファイルが含まれます。
UAC をオンにした Windows Server 2008 に配置しようとしている場合は、Visual Studio を管理者として実行する必要があります。そうしないと、"オブジェクト参照がオブジェクトのインスタンスに設定されていません" というエラーが発生します。すべてが正しく構成されたら、SharePoint 内のサイトに Web パーツを追加できます。これは、SharePoint と Silverlight 間の統合の最後の手順です。

アプリケーションを SharePoint サイトに追加する
この時点で、Silverlight アプリケーションが作成され、Silverlight アプリケーションの Web パーツ コンテナが作成および配置されています。最後の手順は、Web パーツをホストできるサイトの作成です。ただし、この操作を行う前に、検証が必要な項目が 2 つあります。
まず、System.Web.Silverlight.dll がグローバル アセンブリ キャッシュ (GAC) にインストールされていることを検証します。インストールされていない場合は、\Program Files\Microsoft SDKs\Silverlight\v2.0\Libraries\Server というインストール場所からインストールします。
次に、SharePoint ルート ディレクトリ内の web.config ファイルに、Silverlight アプリケーションのサポートに必要な要素がすべて含まれ、Silverlight アプリケーションが安全なコントロールとしてマークされていることを確認します。web.config ファイル内の MediaPlayerWebPart の安全なコントロール エントリを次に示します (これは、プロジェクトをビルドして SharePoint サイトに配置したときに自動的に追加されます)。
<SafeControl Assembly="MediaPlayerWebPart,
             Version=1.0.0.0,
             Culture=neutral,
             PublicKeyToken=9f4da00116c38ec5"
             Namespace="MediaPlayerWebPart"
             TypeName="MediaPlayerWebPart"
             Safe="True" />
web.config ファイルについては詳細に説明しませんが、SharePoint サイトのルート ディレクトリの web.config ファイルで Silverlight アプリケーションがサポートされていることを確認する場合は、Silverlight プロジェクトの一部としてビルドされる web.config ファイルを調べて、これらすべての要素が、SharePoint サイトのルート フォルダにある web.config に存在することを確認するのが最も簡単な方法です。存在しない要素がある場合、Silverlight アプリケーションは SharePoint Web パーツに表示されません。
これで、Silverlight ベースの Web パーツをホストする SharePoint サイトを作成する準備ができました。この操作を行うには、SharePoint サイトに移動します。Media Player という Silverlight アプリケーション用の独立したテスト サイトを作成しました。サイトを作成するには、SharePoint サイトのホーム ページに移動し、[サイトの操作]、[サイトの設定] の順にクリックします。[サイトの管理] の [サイトとワークスペース] をクリックします。[サイトとワークスペース] ページで、[作成] をクリックします。サイトの名前、説明、および種類 (空のサイトを選択しました) とサイトの URL を指定し、[作成] をクリックします。
新しいサイトが作成されると、SharePoint によって自動的に読み込まれます。[サイトの操作]、[ページの編集] の順にクリックします。サイトで、[Web パーツの追加] をクリックします。これにより、Web パーツ ギャラリーが呼び出されます (図 8 を参照)。SharePoint に追加した Web パーツ プロジェクト (たとえば MediaPlayerWebPart) が見つかるまで下にスクロールし、チェックボックスをオンにして [追加] をクリックします。これにより、Web パーツをホスト コンテナとして使用して Silverlight メディア プレーヤー アプリケーションが SharePoint に追加されます。Silverlight アプリケーションは、Visual Studio 2008 内でドラッグしたときと同じように応答し、SharePoint 内で単純に Web パーツとして表示されます。Web パーツを SharePoint に追加した後、Silverlight アプリケーション内のさまざまなコントロールを試してすべて動作することを確認します。
図 8 Web パーツ ギャラリー (クリックすると拡大画像が表示されます)
Web パーツに関する参考資料

SharePoint と Silverlight の青写真
ssblueprints.net/sharepoint

Silverlight ホーム ページ
silverlight.net

SharePoint 開発リソース
MSSharePointDeveloper.com

トラブルシューティング
Web パーツを SharePoint に追加した後、Silverlight アプリケーション内のさまざまなコントロールを試してすべて動作することを確認してください。ご存知のように、開発ではある程度のトラブルシューティングを行う場合があるため、Silverlight アプリケーションを SharePoint と統合するときに注意すべき点をいくつか示します。
メディアを読み込むときにネットワーク エラーが発生した場合、たいていは配置したアセンブリがメディアを見つけられないことが原因です。メディア パスが相対的な場合は、XAP の場所からの相対であるため、Silverlight アプリケーションでメディアを正しく読み込めることを確認します。
Web パーツ ファイル内で XAP ソースの場所が正しいことを確認します。Silverlight アプリケーションなしで Web パーツが読み込まれているケースをいくつも見てきました。これは通常、Web パーツが XAP ファイルを見つけられないことが原因です。また、Silverlight アプリケーションを更新する場合は、XAP ファイルの最新バージョンを、Web パーツ プロジェクトで指定したソースの場所に再コピーしてください。
Web パーツが正常に追加されても、Silverlight がまだ表示されない場合は、ルート SharePoint ディレクトリにある web.config ファイルを再チェックし、Web パーツが安全なコントロールとして (前に説明しました) マークされているだけでなく、Silverlight アプリケーションをサポートするために必要なすべての要素があることを確認します。

Steve Fox は、マイクロソフトの開発者プラットフォーム エバンジェリズム (DPE) チームのテクニカル エバンジェリストです。Office および SharePoint 開発の宣伝と、開発作業でこれらを使用する企業との共同作業にほとんどの時間を費やしています。『Professional Microsoft SharePoint Development Using Silverlight 2』など、多数の共著書があります。blogs.msdn.com/steve_fox で彼のブログをご覧いただけます。

Paul Stubbs は、プラットフォーム アーキテクチャ チームのアーキテクトです。このチームで、彼はインフォメーション ワーカーの生産性、企業から顧客へのソリューション、および Web 2.0 ソーシャル ネットワーキングに重点を置いたソリューション アーキテクチャに専念しています。Microsoft Office を使用したソリューション開発に関する 2 冊の書籍、MSDN Magazine のいくつかの記事を記し、Microsoft Tech•Ed および Tech Ready 会議でも講演を行いました。blogs.msdn.com/pstubbs で彼のブログをご覧いただけます。


編集者のメモ
身分証明書を見せていただけますか?
Howard Dierking

先日、私は妻と連れ立って映画を見に行きました。チケット販売係からチケットを購入しなければならなかったのですが、半インチの厚さのガラスを通して音声を伝えるインターコム システムのために、販売係の声がまるでアンプのゆがみを使った合成音声のように聞こえました。さて、私が身分証明書の提示を求められたときの驚きを想像してください (映画は R 指定でした)。
米国での映画の評定基準についてご存じない方のために、ご説明しましょう。R 指定の映画とは、17 歳未満の若者が大人同伴でないと見ることのできない映画を指します。私はおそらく、実際の年齢よりもほんの少し若く見えるのですね。しかし、いくら何でも 17 歳未満とは。私は身分証明書を提示して映画のチケットを購入しましたが、この出来事によって、セキュリティの要件とコンテキストに関連するより幅広い問題について考えさせられることになりました。
私が考え付いたことの 1 つは、私と販売係を隔てていた薄いガラスの壁に関係しています。こうした壁が設置されたのは、かつては映画を見に訪れる人々のほとんどがチケットを現金で購入していたためだと思われます。クレジット カードの登場によってこうした習慣は薄れてきているでしょうが、窓口の後ろ側にいる人々は、やはり大量の現金を扱う必要があります。通常のチケット カウンタと劇場のそれ以外の部分との位置関係を考えると、このような窓口 (および声をゆがませるインターコム) がなければ、倫理観の欠如した、現金を奪おうと考える人々に対して、チケットの販売係は非常に無防備になってしまいます。
コンテキストによってセキュリティの要件が変わるケースは、年齢確認の場面にも見られます。例の映画館のカウンタでチケットを購入するには、インターコムからの指示を黙って聞くほかありません。販売係は最小限の労力で、またはチケットの販売プロセスにほとんど影響を与えることなく、私の ID をチェックして年齢確認を行うことができます。
しかし、私がチケットをオンラインで購入する場合、ビジネス プロセスには変わりがありませんが、コンテキストの変化によって、セキュリティの目的は映画館の資産 (現金) を保護することから、購入者の資産 (クレジット カードの番号) を保護することにシフトします。チケット購入者が実質的に匿名であるコンテキストでは、購入者の年齢確認などの踏み込んだ確認ステップは、形だけのものに思えることがあります。
私が繰り返し述べているように、アプリケーションや企業に取り入れる必要のあるテクノロジの数は非常に多く、しかも増え続けています。また、テクノロジの変化に応じてセキュリティのコンテキストが変わり、セキュリティの要件が変化することもあります。私はこれまで、既存のアプリケーションを Web に移すというシンプルな目標を掲げて開始されたプロジェクトで、後々になって大量の作業が必要になる (または完全に行き詰まってしまう) ケースを数多く目にしました。これは、コンテキストの変化の影響を前もって考慮していなかったことに原因があります。
この場合、テクノロジの領域から簡単な解決策を見つけることはできません。セキュリティ対策は、システム、ユーザー、およびシステムが実行されるコンテキストに関する確実な理解に基づいてデザインおよび実装する必要があります。私はこの点に基づき、安全なシステムの構築は、人々とプロセスにより密接に関係するということを確信しています。
私たちは、セキュリティ開発ライフサイクル (SDL) を非常に重視しています。SDL は、コンテキストがセキュリティを左右するという考えを強調するためのものです。また SDL は、開発ライフサイクルの他の主要フェーズと共に、永続的な規範としなければなりません。今月号の記事で紹介するように、アジャイル開発方法論が最も効果的である変更の多い環境においても SDL の有効性を保つためには、SDL 自体にも変更を加える必要があります。実際に、コンテキストの変化に応じて SDL も変化し続けることに皆さんもお気付きではないでしょうか。

msdn.microsoft.com/magazine をご覧ください。MSDN Magazine に関するご質問、ご意見、ご提案は、編集者までお送りください (mmeditor@microsoft.com)。

今月号の記事の執筆に携わったマイクロソフトの技術部門の次の専門家に感謝します。Paul Andrew、Jonathan Aneja、Bob Brumfield、Jeff Cao、Kiran Dowluru、Ryan Duguid、Mike Flasko、Matt Gibbs、Kevin Gjerstad、Christian Kleinerman、Bertrand Le Roy、Varsha Mahadevan、Duane Need、Wayne Norton、Eugene Osovetsky、Dave Reed、Michael Rys、Gerhard Schneider、Michael Wang。

長年にわたって MSJ と MSDN Magazine の記事を執筆され、これらのマガジンの数多くのスタッフを支えてくださっていた Paul DiLascia 氏が逝去されました。心からお悔やみ申し上げます。


ツールボックス
オンライン スケジュール、Jeff Smith の SQL ブログ、簡単な正規表現、その他
Scott Mitchell

すべての価格は 2008 年 9 月 18 日時点のものであり、変更される可能性があります。このコラムに記載されている見解は、執筆者の個人的な見解であり、必ずしもマイクロソフトの見解を反映するものではありません。

目次
カレンダーおよびスケジュールのオンライン インターフェイスを作成する
注目のブログ
自分専用の正規表現アシスタント
推薦図書

カレンダーおよびスケジュールのオンライン インターフェイスを作成する
多くのビジネス アプリケーションでは、ユーザーが特定の日付および時刻に対して、資産をリソースに簡単に割り当てられることが求められます。たとえば、医師のオフィスで利用されるソフトウェアでは、医師、患者、装置などの資産を、オフィスや部屋などのリソースに対してスケジュール設定する必要があります。また、タイム シート アプリケーションでは、従業員の時間および活動をプロジェクトや会議に関連付けます。
ASP.NET には Calendar Web コントロールが含まれていますが、このコントロールには、堅牢なスケジュール ユーザー インターフェイスに必要な特性や機能が欠けています。幸運なことに、Dan Letecky の DayPilot 4.8 を使用すれば、プロ仕様の機能豊富なスケジュール機能を簡単に実装できます。DayPilot は、カレンダーおよびスケジュール用の AJAX 対応ユーザー インターフェイスを表示する Web コントロールのコレクションです。DayPilot では、インストール後すぐに、月、週、または日のビューを使用して特定の資産のスケジュールを表示できます。これらのユーザー インターフェイスは、Microsoft Outlook の予定表など、デスクトップ ベースのスケジュール アプリケーションで使用されているのと同様なものです。
また、スケジューラ ビューでは、左の列にリソースの一覧が表示され、日付と時刻が上の行に表示されます。このビューでは、リソースの割り当て状況がすぐにわかり、競合や使用可能なリソースを簡単に特定できます。
DayPilot コントロールに表示される情報は、いくつかの方法で指定できます。この情報は、プログラムで配列として提供できます。データが XML ファイルまたはデータベースに格納されている場合は、データ ソース コントロールを使用するか、またはプログラムでデータをバインドできます。データが表示されたら、各種のプロパティを使用して外観をカスタマイズできます。さらに、多くのイベントおよび拡張ポイントがあるため、これらのコントロールの表示や動作を簡単にカスタマイズできます。たとえば、わずか数行のコードを使用するだけで、資産またはリソースに基づいてイベントに異なる背景色を使用するよう DayPilot に指示できます。
DayPilot の最も魅力的な側面は、その統合された AJAX サポートです。DayPilot のユーザー インターフェイスでは、エンド ユーザーがスケジュール設定されたイベントをドラッグ アンド ドロップしたり、クリック、ドラッグ、およびダブルクリックによりイベントの長さや情報を変更したりできます。また、ユーザーが既存のスケジュール設定済みイベントを右クリックすると、統合されたコンテキスト メニューが表示されます。このコンテキスト メニューを使用して、選択したイベントを変更または削除したり、何らかのカスタム アクションを実行したりできます。
この記事を書いている時点で、DayPilot は Internet Explorer 6.0 以上および Firefox 2.0 以上で十分に動作確認済みです。開発ロードマップでは、Safari と Opera の両ブラウザのサポートも予定されています。
DayPilot には、2 つのエディションがあります。無料のオープンソース バージョンと、Pro バージョンです。簡単に言うと、オープンソース バージョンでは日と週のビューを読み取り専用で使用できます。月およびスケジューラのビューを使用したり、ユーザーが DayPilot ユーザー インターフェイスからイベントを移動して編集できるようにしたりするには、Pro バージョンを使用する必要があります。
価格: オープンソース バージョンは無料、Pro バージョンは 549 ドルです。
daypilot.org/daypilot-pro-4-8.htm
DayPilot によって機能豊富なスケジュール機能を簡単に実装 (クリックすると拡大画像が表示されます)

注目のブログ
ほとんどのアプリケーション開発者は、データベースの情報を取得および変更するためのクエリ言語である SQL の実用的な知識を持っています。データを選択し、結果のフィルタ処理や並べ替えを行い、レコードを更新、削除、挿入するなど、データ駆動型アプリケーションで実行されるほとんどの一般的なタスクの実行方法はわかっていますが、通常の範囲を超えたデータ関連の問題に突き当たると、行き詰ってしまいます。SQL のスキルを強化するために、Jeff Smith のブログをチェックしてみてください。Jeff は、SQL に精通した .NET 開発者です。データベース デザインに関する記事やガイダンスと共に、毎週いくつかのヒントや秘訣を投稿しています。
Jeff のブログ エントリでは、GROUP BY ステートメントでの ALL キーワードの機能、左側と右側の外部結合の違いとその比較、グループごとの上位 n パーセントの取得方法、データベースのデザイン時に列のデータ型を選択する最良の方法など、さまざまなトピックが扱われています。また、各ブログ エントリは、現実に即して記述されており、読みやすく理解しやすいものになっています。
どうにも手に負えないような特別に厄介なクエリはありませんか。Jeff に質問を送ってみてください。あつかましいと思われないかなどと遠慮する必要はありません。Jeff は読者からの SQL に関する電子メールでの質問を歓迎しています。これらの質問や回答は、他のユーザーにも役立つように、Jeff のブログに投稿されます。質問がない (または質問を送る勇気がない) 場合でも、他のユーザーからの質問と Jeff の解決策を読むことで、多くを学ぶことができます。
weblogs.sqlteam.com/jeffs

自分専用の正規表現アシスタント
正規表現は、文字列パターンのマッチングに使用する簡潔な構文であり、テキストの本文中で文字列を検索または置換するための理想的な方法でもあります。正規表現は一般に、特定のパターンに従う必要のあるユーザー入力 (電話番号、電子メール アドレスなど) の検証や、高度な検索置換シナリオで使用されます。
通常、短く単純な正規表現であれば、読むのも書くのも簡単ですが、より長く、より複雑なものになると、扱いは一般に困難です。長い正規表現は、リテラル文字、特殊記号、グループ、キャプチャ、検索機能などが寄せ集められたものであり、判読不能な文字のかたまりとなってしまいがちです。正規表現の作成、解析、テスト、デバッグの助けとして、Just Great Software の RegexBuddy 3.1.1 をお試しください。
RegexBuddy では、[Insert Token] (トークンの挿入) ツール バーにより、正規表現をポイント アンド クリックで簡単に作成できます。まず、正規表現に追加するトークンを選択します。これは、リテラル テキスト、および数字、スペース、英字などの文字クラス、キャプチャまたは非キャプチャのグループ、前方参照などです。RegexBuddy は、正規表現をテキストとして表示する一方、トークンのツリーとしても表示します。[Insert Token] (トークンの挿入) ツール バーから新しいトークンを追加すると、該当するトークンがツリーに追加され、正規表現が更新されます。ツリーからトークンを選択すると、正規表現で対応する文字が強調表示されます。逆に、正規表現で文字を選択すると、ツリーで対応するトークンが強調表示されます。
このトークン ツリー ビューは、既存の扱いにくい正規表現を理解するのに役立ちます。正規表現を RegexBuddy に貼り付けると、トークン ツリーが自動的に読み込まれ、正規表現の各トークンの動作が 1 つずつ順番に説明されます。
RegexBuddy は、正規表現のテストやデバッグのためのツールを提供します。正規表現を作成または入力したら、[Test] (テスト) タブでそれを試すことができます。テキストの本文は、手動で入力するか、またはファイルや URL から読み込みます。テキストの本文に一致するパターンがあると、自動的に強調表示されます。[Debug] (デバッグ) タブでは、パーサーがテキスト本文を正規表現に対してどのように評価するかを調べることができます。
RegexBuddy は、正規表現のリポジトリとしても機能します。正規表現を保存できる [Library] (ライブラリ) タブがあります。また、[Library] (ライブラリ) タブには、HTML および XML 処理、数値および日付文字列の処理など、一般的なタスクを実行するためのさまざまな正規表現があらかじめ用意されています。RegexBuddy では、アプリケーション内から正規表現を使用するために必要な Visual Basic または C# のコードを作成することもできます。言語と実行する動作 (テキスト本文内の正規表現との一致の検索、一致した正規表現から別の文字列への置換など) を選択すると、RegexBuddy は対応するソース コードを表示します。
価格: 39.95 ドル
Regexbuddy.com
RegexBuddy で正規表現をテキストおよびトークンのツリーとして表示 (クリックすると拡大画像が表示されます)

推薦図書
『The Productive Programmer』 (O'Reilly、2008 年) では、Neal Ford が開発者の生産性向上に役立つ実証済みの技法を紹介しています。この本の最初の部分では、開発者の生産性を高めるための動作およびツールについて述べられ、次の部分では、より合理化された開発プロセスのために寄与するソフトウェア開発手法に焦点を当てています。
Ford は早くから、コンピュータを生産的に使用するメカニズムに注目してきました。ここでは、一般的なプログラミング関連タスクの実行に必要な時間を短縮するためのヒントおよびツールが紹介されています。彼は、生産性向上のために DRY (Don't Repeat Yourself) 原則を信奉し、バージョン管理から技術文書までのさまざまな領域で無用な繰り返しを避ける方法の例を示しています。
ソフトウェア開発手法が不適切であると、これらの生産性向上が簡単に帳消しになってしまうと Neal は注意を述べています。不要な機能の追加、コードの不十分なテスト、オブジェクトを正しくカプセル化しないなどは、すべてバグや保守性の低いコードへとつながる悪習です。
開発者の生産性で重要なもう 1 つの側面は、進んで現状を疑ってみる態度です。あまりに多くの開発者が、型にはまってしまい、単にいつもそうしているからという理由で特定のデザイン パターンやコーディング技法を繰り返し使用し、より効率が高いかもしれない別の手法を見落としたり拒否したりしています。
『The Productive Programmer』には、特定のフレームワークに対する多数のコード例や説明が含まれていますが、それらは .NET 開発者にとっては残念なことに、Java および Ruby に焦点が当てられています。また、取り上げているツールのいくつかは UNIX ベースまたは Mac OS システムに対応したものです。ただし、提示されたアイデアの大部分はアーキテクチャ、フレームワーク、プログラミング言語に依存しないので、すべてのプログラマにとって、ソフトウェア アプリケーションのより適切で迅速な構築の手助けとなります。
価格: 39.99 ドル
oreilly.com

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


CLR 徹底解剖
国際化対応のアプリケーション
Melitta Andersen

このコラムは、Silverlight のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
目次
CLR によるローカライズ
テキストを使用する
Silverlight のグローバリゼーション
参考資料
ほとんどのプログラムでは、何らかのデータをユーザーに提供します。Windows Presentation Foundation (WPF) や Windows フォーム アプリケーションなどの、GUI を持つアプリケーションがユーザーにデータを提供するように設計されているのは明らかです。しかし、単純なコンソール アプリケーションであってもテキストを表示します。グローバリゼーションのプロセスは、異なるカルチャのデータをアプリケーションで処理できるようにすることです。これは、カレンダーのタイトルの幅を広げてハワイ語の月の名前を処理できるようにしたり、ユーザーが求める負の数の形式を使用したりすることと同様に単純に行うことができます。
グローバリゼーションはローカライズとは異なり、実際には、アプリケーションのリソースが別の言語に変換されることを指します。アプリケーションは、1 つの言語で記述されている場合でも、グローバリゼーション対応である必要があります。
ここでは、変換されていないアプリケーションであってもバリエーションが存在する例を示します。日付などのデータ型は、さまざまな方法で書式設定できます。図 1 は、英語名の月と曜日で使用されるいくつかの書式を示しています。すべてのエントリは同じデータを表します。しかし、ユーザーにはそれぞれ使用したい日付形式があります。ユーザーは、コントロール パネルの設定で好みの形式を指定できます。そのため、プログラムが DateTime.Now.ToString を呼び出すと、任意の数の文字列が返されます。形式を指定しない場合、ToString は自動的にユーザー設定を選択します。
図 1 日付形式の一部
日付文字列 "1/2/03" は、その各部分が何を表すかを把握していない限り、意味を成しません。あなたは、書式文字列を理解しておくとデータの意味の解釈に役立つと考えているでしょう。しかし、これには何の保証もありません。すべての人が同じカレンダーを使用しているわけではないからです。実際、System.Globalization 名前空間は、現在のところ 15 の異なるカレンダーをサポートしています。そのため、"M/d/yy" と解釈される文字列 "6/26/1980" は、グレゴリオ暦とユリウス暦では別の日付を表します (多くの Web サイトでは、日付を別のカレンダーに変換できます)。
非常に興味深いことに、同じ解釈の問題が数値にも当てはまります。小数点記号にピリオドを使用する人もいれば、コンマを使用する人もいます。つまり、小数を 1.23 と表記するユーザーもいれば、1,23 と表記するユーザーもいます。負の数を扱う場合はさらに複雑になります。System.Globalization.NumberFormatInfo.NumberNegativePattern を見ると、Microsoft .NET Framework では、負の記号の配置の 5 つのパターンが格納されていることがわかります。この場合、使われている記号は 1 つだけです。NumberFormatInfo.CurrencyNegativePattern では、通貨の負の値の異なる 16 のパターンが格納されています。
ご想像のとおり、データを文字列として格納するときは、使用する形式を必ず明示的に示し、その同じ形式を ToString メソッドと Parse メソッドの両方に使用する必要があります。一部の文字列は、コンテキストなしではあいまいになることに注意してください。もちろん、この書式設定は、表示目的の場合にのみ重要になります。コンピュータによってのみ読み取られる格納済みデータの場合は、不変の表現を選択するのが最適です。Framework の CultureInfo.InvariantCulture には、そのためのメカニズムが用意されています。
書式設定のオプションはたくさんありますが、どれを選択するかを判断するにはどうすればよいでしょうか。さいわいなことに、.NET Framework には、表示するデータの書式設定を行うためのインフラストラクチャがあります。これまで見てきたように、ユーザー設定は自動的に捕捉することができます。しかし、ユーザーによる指定がない場合、Framework では使用する書式をどのようにして判断するのでしょうか。
System.Globalization 名前空間を詳しく見てみると、この疑問の答えがわかります。System.Globalization の内容は、カルチャの情報の記述に使用されています。この名前空間に 15 のカレンダーと通貨の負の値を書式設定するための 16 の方法が格納されていることは既にわかっています。また、他のカルチャ固有の時刻と数値のプロパティおよび言語と地域の情報を格納するためのクラスも格納されています。さらに、この情報を使用するために整理してグループ化するメカニズムも格納されています。そのメカニズムは CultureInfo クラスです。このクラスは、数値形式、日付形式、カレンダーを含むカルチャの情報、およびカルチャの名前と表記体系の情報を格納します。
CultureInfo オブジェクトは、特定のカルチャの識別子を使用して作成されます。たとえば、スウェーデン語の情報を格納するオブジェクトを作成し、それをフィンランドで使用するには、"sv-FI" という名前をコンストラクタで指定します。Framework には多数のカルチャの情報が含まれており、Windows にもさまざまなロケール用のデータが用意されています。このほか、ユーザーは独自のカスタム カルチャを作成できます。
CultureInfo は、ToString が日付と数値の引数として受け取ることのできる IFormatProvider 型を実装します。したがって、元の DateTime.Now.ToString の例に戻ると、sv-FI カルチャに関連付けられた既定の書式が設定された日付を取得する必要がある場合は、次のように記述できます。
DateTime.Now.ToString(new CultureInfo("sv-FI"));
ユーザーが sv-FI をオーバーライドせず、対象の日付が "6/26/1980" である場合は、日付が "26.6.1980" と出力され時刻の文字列が続きます。sv-FI に関連付けられた長い日付形式を取得する必要がある場合は、次のように記述できます。
CultureInfo ci = new CultureInfo("sv-FI");
Console.WriteLine(DateTime.Now.ToString(ci.DateTimeFormat.LongDatePattern, ci));
この場合は、文字列 "den 26 juni 1980" が生成されます。LongDatePattern では、DateTime の日付部分だけが含まれ、時刻は省略されることに注意してください。これらの例を使用できるのは、CultureInfo オブジェクトに格納されている書式設定の情報が使用されるためです。
このことは、特定の書式設定の情報が渡されたときの ToString の動作について説明しています。しかし、最初に見たとおり、ToString のオーバーロードでは書式設定の情報は受け取りません。しかし、データは書式設定されています。では、情報をどこから受け取っているのでしょうか。
CultureInfo クラスの静的プロパティの 1 つに CurrentCulture があります。これは、現在のスレッドに関連付けられているカルチャを返します。このプロパティは、Thread.CurrentThread.CurrentCulture を使用して設定できます。スレッドが作成されると、既定でオペレーティング システムの現在のカルチャに設定されます。ToString または解析操作のための書式情報が指定されていない場合は、CultureInfo.CurrentCulture の情報が使用されます。
一般に、カルチャ情報は、変更される場合があります。たとえば、ある国で、ユーロが採用されたり、タイム ゾーンが変更されたり、別の日付形式が追加されたりする場合などです。そのため、カルチャを指定する場合でも、使用する日付は変更される可能性があります。グローバリゼーション情報は表示目的にのみ使用されることに注意してください。データを文字列として格納する必要がある場合は、CultureInfo の InvariantCulture プロパティが適しています。

CLR によるローカライズ
アプリケーションを複数の言語にローカライズする場合はどうでしょう。その場合は、System.Globalization で処理されるすべてのデータ表示機能のほか、アプリケーションの各部分の変換が必要になることがあります。また、カルチャごとに異なるイメージを表示する必要がある場合もあります。これを実現する方法の 1 つは、アプリケーションの複数のコピーを作成して、製品のローカライズ対象の各言語に割り当てることです。
しかし、これでは不十分です。さいわいなことに、Framework には、System.Resources 名前空間のクラスを使用して、ローカライズ可能なこれらのリソースを抽象化する方法が用意されています。リソースを作成し、読み取り、使用するための主要なクラスは、それぞれ ResourceWriter、ResourceReader、および ResourceManager です。
ResourceWriter クラスを使用すると、リソースを名前と値のペアとして格納できます。ResourceWriter のインスタンスを作成すると、そのインスタンスは特定のファイル専用になります。これらのファイルの名前付け規則は [basename].[culturename].resources です。"basename" は、リソースを整理するために使用されます。これは、アプリケーションまたはクラスの名前です。CultureInfo.Name プロパティを使用すると、カルチャ名を検索できます。
たとえば、サンプル アプリケーション MySampleApplication の英語 (米国) のリソースは "MySampleApplication.en-US.resources" ファイルに保存されます。また、アプリケーションには、[basename].resources という名前の、言語に依存しないカルチャの既定のリソース ファイルが必要です。一般には、リソース ファイルをサテライト アセンブリにパッケージ化して、メイン アセンブリからローカライズ データを個別にバージョン管理できるようにすることをお勧めします。ただし、メイン アセンブリには必ずリソースの 1 つのセット (言語に依存しないリソース) を含めて、リソースの検索で少なくとも 1 つのリソースのセットが見つかるようにすることをお勧めします。
ResourceReader クラスを使用すると、リソース ファイルの名前と値のペアを列挙できます。ただし、最も一般的に実行されるタスクは、特定のカルチャの特定のリソースを検索することです。そのためには、ResourceManager クラスを使用します。GetObject メソッドと GetString メソッドを使用して個々のリソースにアクセスできます。または、GetResourceSet メソッドを使用して、特定のカルチャのすべてのリソースをハッシュテーブルに読み込むことができます。
グローバリゼーション情報と同様に、リソースはカルチャによって格納されます。また、グローバリゼーションを既定のカルチャで構成できるのと同様に、ResourceManager では CurrentCulture.CurrentUICulture を既定のカルチャとして使用します。この場合も、これは現在のスレッドのプロパティです。多くの場合、CurrentCulture と CurrentUICulture は一致しますが、これらは別のものを規定します。CurrentCulture はデータの書式設定を制御し、CurrentUICulture はどのリソースが読み込まれるかを判断します。
Framework では System.Globalization のクラスで必要なほとんどのデータを受け渡しますが、アプリケーションではその独自のリソースを制御します。アプリケーションが特定のカルチャにローカライズされない場合、リソースをそのカルチャで使用することはできません。しかし、通常、リソースの検索を失敗させたくはありません。そのため、言語に依存しないリソースを最初にメイン アセンブリに含めて、アプリケーションに必ずフォールバック対象のリソースのセットが用意されているようにします。
まだ説明していない CultureInfo のプロパティは Parent プロパティです。カルチャ情報は階層構造にまとめられ、そこでは言語に依存しないカルチャ (特定の地域に関連付けられていないカルチャ) が、特定の地域に関連付けられたカルチャの親になります。たとえば、en-US、en-CA、および en-UK の親は、言語に依存しないカルチャ en です。カルチャのリソースが見つからない場合、ResourceManager は、そのリソースが見つかるまで親のチェーンを調べます。最後には、メイン アセンブリに埋め込まれているリソースである InvariantCulture が調べられます。
カルチャの階層を使用すると、特定のカルチャから共通のリソースを削除して、言語に依存しないカルチャに追加できます。たとえば、自分のアプリケーションを en-US、en-CA、および en-UK にローカライズするとします。これらのカルチャに必要なリソースの多くは、わずかな違いを除いて同じである可能性があります。この場合、私は共通のすべてのリソースを、言語に依存しないカルチャ en のファイルに格納し、異なる部分だけを特定のカルチャのリソース ファイルに保存できます。
図 2 に示すようなリソースで "greeting" という名前に対応するリソースを検索した場合、en-US は "hi" を返しますが、en-UK と en-CA は両方とも en リソースの "hello" にフォールバックします。
図 2 リソースの階層 (クリックすると拡大画像が表示されます)

テキストを使用する
テキストの場合はまったく新しい問題があります。たとえば、テキストを他の言語に変換するために必要なすべての文字を実際にはどのように表現しますか。文字列は、.NET Framework では System.String クラスによって表されます。さいわいなことに、CLR における文字列では Unicode (特に UTF-16 エンコード) を使用するため、標準的な文字を表すことができます。
System.Text 名前空間には、Unicode 文字をバイトにエンコードし、バイトを Unicode 文字にデコードするために使用できるいくつかのクラスが含まれています。これにより、必要に応じて、異なる Unicode エンコード間での変換が可能になります。
Unicode は現在の標準ですが、特定の言語または地域で使用される文字だけを表していた従来のエンコードがいくつか存在します。このようなエンコードは "コード ページ" と呼ばれます。これらの古いコード ページでエンコードされたテキストを扱う必要がある場合は、System.Text のクラスを使用する方法もあります。一般的に、この方法が必要なのは、レガシ アプリケーションを操作する場合だけです。新しいアプリケーションでは、Unicode 標準を使用する必要があります。
コンソールまたはラベルに出力されるだけの文字列もありますが、多くの場合は文字列を使用して何らかのアクションを実行する必要があります。おそらく、文字列のリストは、並べ替えて表示されると読みやすくなります。たとえば、文字列内の文字の大文字小文字を変換する必要があるとします。このようなアクションはごく簡単なように見えますが、それは錯覚です。すぐにはわからないかもしれませんが、単語の並べ替えにおける基本的な問題があります。たとえば、英語には、順序が固定された 26 文字の英字があります。しかし、文字には大文字と小文字があります。大文字小文字が異なる同じ単語のインスタンスを並べ替えるにはどうすればよいでしょうか。
Framework には、文字列比較のモードが 3 つあります。これらは序数、単語、および文字列です。序数比較では、比較するそれぞれの文字の数値を調べます。つまり、String.CompareOrdinal に基づく順序付けでは、"alphabet" は "Alphabet" よりも大きいと見なされます。単語比較は既定値です。この比較はカルチャ依存であり、ハイフンなどの英数字以外の特定の文字を特殊なケースとして扱うことができます。これらの文字に小さい重みを付けることができるため、たとえば、"a-lphabet" と "alphabet" は互いに近くに並べられます。文字列比較もカルチャ依存ですが、特殊なケースはなく、英数字以外の文字はすべて英数字の前に並べられます。
String.Compare を使用して en-US についてカルチャ依存の比較を行うと、"Alphabet" は "alphabet" より大きくなります。大文字小文字を無視する序数比較を行うと、これらは等しくなります。System.Globalization.CompareOptions には、並べ替えのときに考慮するパラメータを示す 9 つの異なるオプションが含まれています。
各カルチャには、固有の文字セットと並べ替え順序があります。これらを複数持つカルチャも存在します。たとえば、es-ES (スペイン語 (スペイン)) では、既定で国際化対応の並べ替えを使用しますが、従来の並べ替え順序を代わりに使用することもできます。zh-CN (中国語 (中国)) では、既定で発音を基準に並べ替えますが、画数を基準に並べ替えることもできます。Framework の各 CultureInfo オブジェクトには、文字列の比較に使用される CompareInfo プロパティがあります。Framework はこのデータを格納します。
一般に、カルチャ依存の順序付けは、並べ替えや表示に適しています。しかし、等値の文字列をテストする場合、特にセキュリティに影響する特定のケースでは、序数比較が最善の選択であることは間違いありません。序数比較では、比較対象の文字の値だけを調べます。したがって、カルチャ依存の比較では、使用されるカルチャに基づいて結果が異なる可能性がありますが、序数比較では一貫性が保たれます。
セキュリティ目的でカルチャ依存の文字列比較を使用する際に最もよく取り上げられる問題として、"トルコ語の I の問題" があります。ほとんどのラテン文字では、文字 i (\u0069) は単に文字 I (\u0049) の小文字です。ラテン文字を使う人々の大部分は、この文字にバリエーションがあるとは考えもしないため、これが既定値と見なされます。しかし、トルコ語 (tr-TR) では、そのマッピングは正しくありません。文字 i の大文字は文字 İ (\u0130) です。つまり、大文字の I にドットが付いたものです。同様に、文字 I の小文字は文字 ı (\u0131) です。つまり、小文字の i のドットを除いたものです。
URI (Uniform Resource Identifier) が文字列 "FILE:" で始まるかどうかを確認する場合について考えてみましょう。"file:" などの語がフィルタから漏れることのないように、大文字小文字を区別しない方法を使用するとします。en-US カルチャを使用し、大文字小文字を無視して "file:" と "FILE:" を比較すると、これらは等値になります。しかし、tr-TR カルチャを使用した場合、これらは等値にはなりません。String.Compare の既定値は CurrentCulture です。
そのため、このような状況では、StringComparison.OrdinalIgnoreCase を使用する必要があります。等値かどうかをテストするだけの場合は、さらに便利なオプションとして、String.Equals を使用する方法があります。これは、既定で序数比較を行います。図 3 は、文字 I について ToUpper と ToLower を使用した結果を示しています。文字列比較の詳細については、「Microsoft .NET 2.0 で文字列を使用する場合の新しい推奨事項」を参照してください。
図 3 ToUpper と ToLower の結果

Silverlight のグローバリゼーション
グローバリゼーションは、Silverlight アプリケーションにとって特に重要です。デスクトップ アプリケーションの場合は、対象とするユーザーが正確にわかっています。しかし、Web 上の Silverlight アプリケーションはだれでも参照することができます。Silverlight のグローバリゼーション インフラストラクチャの多くはデスクトップ フレームワークと同じですが、そのデータの大部分は基になるオペレーティング システムから取得されます。これにより、Windows で実行する Silverlight アプリケーションは Windows ベースのアプリケーションにより近くなり、Mac OS で実行する Silverlight アプリケーションは Mac OS アプリケーションにより近くなります。
デスクトップ CLR と Silverlight (CoreCLR) との違いの多くは、ダウンロードの速度と簡単さのために Silverlight ではより軽量なランタイムが必要とされた事実から生じます。したがって、Silverlight では、CoreCLR にあらゆる情報を格納するのではなく、基になるオペレーティング システムからより多くの情報を取得します。たとえば、CoreCLR には並べ替えテーブルが格納されていないため、アクセスできるのはオペレーティング システムが保持するテーブルだけです。さらに、Silverlight では Unicode のみを使用し、従来のコード ページはありません。また、オペレーティング システムからカルチャ情報を取得します。つまり、さまざまなデータを処理する準備をしておく必要があるということです。
カスタム カルチャまたは OS のカルチャに依存するデータを使用できない場合があるのはよくあることです。しかし、すべてのデータを OS から取得すると、単一の Silverlight アプリケーションを実行しても別のデータが取得される場合があります。使用する UI カルチャをターゲットの OS がサポートするようにして、要求されたカルチャが存在しない場合でも、アプリケーションが適切な方法で動作するようにしておくことをお勧めします。
カルチャ データは .NET Framework に組み込まれているため、データの変更と同じ頻度で更新を行うことは困難です。そのため、データが古くなってしまう可能性があります。また、ネイティブ アプリケーションとマネージ アプリケーションで別のデータが表示される状況を招くこともあります。Silverlight ではこれらの 2 つの間で一貫性が保たれ、アプリケーションでは更新された OS の情報を利用できます。
OS のデータを使用すると、さらに詳細な情報を利用することもできます。たとえば、デスクトップ バージョンの .NET Framework の各 CultureInfo には、親を 1 つだけ持つカルチャがあります。そのため、リソースを検索するときに、ResourceManager はその親チェーンを調べるだけで済みます。
de-DE リソースと fr-FR リソースを持つアプリケーションについて考えてみましょう。プライマリ カルチャが es-ES であり、さらにドイツ語を理解し de-DE リソースを判読できるユーザーがいるとします。この場合、ユーザーが理解できるリソースが存在したとしても、ResourceManager は不変のカルチャにフォールバックします。最新のオペレーティング システムでは、ユーザーが複数の言語の設定を指定できます。Silverlight では、不変のカルチャにフォールバックする前に、これらの言語の設定に従うように ResourceManager を変更しました。
また、Silverlight では、文字列比較において、サイズではなくセキュリティに関連するいくつかの違いがあります。デスクトップ フレームワークにおける、文字列比較に関連するほとんどの操作の既定値は CurrentCulture です。Silverlight では、String.IndexOf と String.StartsWith の両方を含む部分一致を使用する操作では、現在既定で序数比較が使用されます。目的の特定の動作に対して適切なオーバーロードを使用するかどうかを検討する必要はありますが、Compare と CompareTo では引き続き CurrentCulture が既定値として使用されます。ToUpper と ToLower では、現在は InvariantCulture が既定で使用されます。等値かどうかの判断には、引き続き序数比較が使用されます。

参考資料
Silverlight および Windows 用の国際化対応のアプリケーションの作成の詳細については、いくつかの役立つ参考資料が System.Globalization、System.Resources、および System.Text の MSDN のページに掲載されています。これらのページには、詳細情報へのリンクと便利なコード例が用意されています。最後に、グローバリゼーション デベロッパー センターも、国際化対応のアプリケーションの作成に関する優れた参考資料です。

ご意見やご質問は clrinout@microsoft.com までお送りください。

Melitta Andersen は、CLR の基本クラス ライブラリ チームのプログラム マネージャです。彼女は主に、数値、コレクション、およびこのコラムで紹介したインフラストラクチャの一部に関する業務に携わっています。


データ ポイント
Silverlight 2 での Web サービス利用に関する疑問点
John Papa

コードのダウンロード: MSDN Code Gallery (151 KB)
オンラインでのコードの参照
このコラムは、Silverlight 2 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
目次
Silverlight 2 では Web サービスはどのように処理されるのですか。
データはどのような方法でサービスから Silverlight 2 に渡されるのですか。
ASMX Web サービスではデータはどのような方法で処理されるのですか。
WCF Web サービスではデータはどのような方法で処理されるのですか。
データは REST Web サービスからどのような方法で受信するのですか。
XML のクエリの実行方法を教えてください。
サードパーティの Web サービスにアクセスするとき、ドメイン間ポリシーに注意する必要はありますか。
Silverlight を使用している開発者たちは、数々の疑問で頭の中がぼんやりしているように思えますが、それは無理もありません。多種多様な Web サービスからリッチなインターネット アプリケーション (RIA) のデータを取得する処理は、ますます多くの場面で使用されるようになっています。Silverlight アプリケーションは、ASMX Web サービス、Windows Communication Foundation (WCF) Web サービス、Representational State Transfer (REST) サービス、および Plain Old XML (POX) サービスと通信できます。これらのサービスがサードパーティから提供されている場合も、Silverlight アプリケーションと同じサーバーでホストされているカスタム サービスである場合も、Silverlight では、データを要求し、そのデータを処理して、Silverlight クライアント アプリケーションとこのような Web サービスとの間でデータをやり取りできます。
これまでに筆者の元に寄せられた質問からは、開発者が Silverlight 2 アプリケーションからのサービスの呼び出し、WCF、ASMX、および REST の各サービスを使用する場合の違い、そしてこれらのサービスが公開するデータの処理方法について、詳しく知りたいと考えていることがわかります。今回のデータ ポイントでは、寄せられた質問の一部に回答すると共に、データ サービスの利用方法について説明します。
独自の API へのアクセスからクライアント固有のカスタム データの格納まで、あらゆる機能を提供するサードパーティの Web サービスが多数存在します。ここでは、Silverlight アプリケーションとサービスのクラウドとの間で行われるデータの処理とやり取りについて説明します。また、いくつかの XML 解析ライブラリを使用してデータを処理する方法も紹介します。たとえば、XML ドキュメントを開いたり、反復処理したりできるほか、LINQ to XML を使用して XML のクエリを行うこともできます。Flickr、Amazon、Twitter、Live Search などのサービスは、いずれもさまざまな Web サービスの手法を通じて通信できる API を公開しています。ここでは、これらの API の一部について説明し、REST ベースのサービスおよび SOAP ベースのサービスを使用してこれらの API と通信する方法を示します。このコラムのすべてのサンプルは、MSDN Magazine の Web サイトからダウンロードできます。C# と Visual Basic の両方のバージョンを用意しました。

Silverlight 2 では Web サービスはどのように処理されるのですか。
これは、最初に取り上げるのにふさわしい質問です。これほど多くの開発者が Web サービスとの通信に Silverlight 2 を使用している理由を理解する土台となるためです。Silverlight 1.x は Microsoft .NET Framework に基づくコードをサポートしておらず、.NET コントロールもありませんでした。Silverlight 2 には、これまでの制限を取り払う完全な機能が数多く搭載されています。Silverlight 2 を使用すると、コードを C# または Visual Basic で記述し、これまでに培った .NET CLR に関するあらゆる経験を活かすことができます。Silverlight に含まれるライブラリは .NET ライブラリ全体のサブセットですが、Silverlight 2 には多数の機能が用意されています。たとえば、WebClient クラスと HttpWebRequest クラスは Silverlight からアクセスでき、URI を呼び出すことで Web ベースのサービスとの通信に使用できます。データは、XmlReader オブジェクトまたは LINQ to XML を通じて処理できます。
Silverlight 2 には、サービス間でデータをやり取りするための多数の機能が追加されています。次に示すのは、Silverlight 2 の新機能です。
  • プロキシ クラスを通じて SOAP ベースの Web サービスにアクセスできます。
  • REST ベースの Web サービスにアクセスできます。
  • ADO.NET Data Services (リモート LINQ クエリが可能な REST ベースのサービス) にアクセスできます。
  • JavaScript Object Notation (JSON) と XML の両方を使用して、Web サービスから結果を取得できます。
  • WCF を通じて (サーバー プッシュを使用して) 二重通信がサポートされます。
  • clientaccesspolicy.xml ファイルまたは crossdomain.xml ファイルを通じてクロスドメイン アクセスがサポートされます。
  • HTTP とソケットを使用して、クロスドメイン ネットワークのサポートを利用できます。
  • Web サービスの呼び出しが非同期に開始されます。

データはどのような方法でサービスから Silverlight 2 に渡されるのですか。
図 1 に、Silverlight 2 でアクセスできる数種類のサービスを示します。Silverlight 2 とこれらのサービスの間では、データを XML、JSON、またはスカラ値の形でやり取りできます。SOAP ベースのサービスは、それ自身を記述することができます。したがって、これらのサービスが公開するデータも記述できます。Silverlight 2 クライアントは、サービスのプロキシを作成することで、SOAP ベース サービスのデータにアクセスできます。このプロキシは、公開されるクラスの記述を生成します。
図 1 Silverlight からサービスにアクセスする
SOAP ベースのサービスはそれ自身を記述するため、Silverlight 2 のクライアント アプリケーションは、公開されたエンティティを使用してデータをサービスとやり取りできます。ASMX Web サービスと WCF Web サービスでは、クライアント参照を使用して、エンティティをそのコントラクトの一部として含めることができます。クライアント参照は、公開されるクラスとサービス メソッド (サービス メソッドは、すべて Silverlight 2 クライアントの非同期呼び出しに変換されます) の定義が含まれるプロキシ クラスをクライアントで生成します。
POX ベースのサービスや REST ベースのサービスなど、それ自身を記述しないサービスでは、クライアント アプリケーションはそのサービス メソッドを呼び出し、データをスカラ形式で、あるいは XML または JSON の形で取得できます。これらの種類のサービスは、Web サービス記述言語 (WSDL) を公開しません。したがって、クライアント アプリケーションはサービスのプロキシ クラスを生成しません。サービスに対するクエリは、クラス (WebClient クラスや HttpWebRequest クラスなど) を通じて、URI を使用して行われます。

ASMX Web サービスではデータはどのような方法で処理されるのですか。
ASMX サービスと同様に、SOAP ベースの WCF Web サービスも、WSDL を使用してそれ自身を記述します。クライアントがサービスへの参照を追加する場合、クライアントはサービスのクライアント プロキシを生成します。これにより、クライアントはサービスとの間でデータをネイティブな形式で (つまりクラスとして) やり取りできます。たとえば、Dog エンティティを SOAP ベースの Web サービス (ASMX または WCF) から Silverlight 2 クライアントに返すことができます。その後、Silverlight 2 クライアントは Dog クラスのインスタンスを作成できます。メソッドは、パブリック サービス メソッドを WebMethod 属性で修飾し、ASMX Web サービス クラスを WebService 属性で修飾することで、ASMX Web サービスのクライアントに公開されます。図 2 に、これを C# で示します。
図 2 C# で Web サービスからエンティティを返す
[WebService(Namespace = "http://www.microsoft.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class TestService : System.Web.Services.WebService {
  [WebMethod]
  public List<Dog> GetDogList() {
   return new List<Dog> {new Dog {Name = "Spot", Age = 10},      new Dog {Name = "Kadi", Age = 13}};
  }
}

[Serializable]
public class Dog {
  public string Name { get; set; }
  public int Age { get; set; }
}
この例は、Silverlight 2 クライアント アプリケーションの .NET を対象とするコードを示しています。このコードは、SOAP サービスのプロキシを生成します。イベント ハンドラが GetDogListCompleted イベントに割り当てられた後、サービスの非同期呼び出しが実行されます。Silverlight 2 からの Web サービスの呼び出しは、すべて非同期に行われます。サービスによって UI スレッドが停止し、アプリケーションがフリーズするのを防ぐためです。次に、結果をエンティティ型の配列として受け取るイベント ハンドラを示します。
TestServiceSoapClient proxy = new TestServiceSoapClient();
proxy.GetDogListCompleted +=   new EventHandler<GetDogListCompletedEventArgs>(proxy_  GetDogListCompleted);
proxy.GetDogListAsync();
次に、エンティティを受け取るための C# コードを示します。
void proxy_GetDogListCompleted(object sender,
  GetDogListCompletedEventArgs e)
{
  Dog[] list = e.Result;
}

WCF Web サービスではデータはどのような方法で処理されるのですか。
ASMX Web サービスと同様に、WCF Web サービスも WSDL を使用してそれ自身を記述します。WCF Web サービスは、一連の属性を使用して渡すことのできるデータとサービスを公開します。したがって、クライアント アプリケーションは WCF Web サービスを参照し、プロキシ クラスを生成して、サービスや公開されたデータ コントラクトとやり取りできます。
WCF では、WCF Web サービスから返すことのできる任意のカスタム .NET 型としてデータ コントラクトを定義します。これらの型は、DataContract 属性で修飾されます。クラスのプロパティは、DataMember 属性で修飾される必要があります。図 3 に、適切な属性で修飾されたサンプル クラスを示します。
図 3 WCF のデータを準備する
[DataContract]
public class Employee {
  int _employeeID;
  string _firstName;
  string _lastName;
  string _title;
  DateTime _hireDate;
  byte[] _photo;

  [DataMember]
  public int EmployeeID {
    get { return _employeeID; }
    set { _employeeID = value; }
  }
  [DataMember]
  public string FirstName {
    get { return _firstName; }
    set {_firstName = value; }
  }
  [DataMember]
  public string LastName {
    get { return _lastName; }
    set { _lastName = value; }
  }
}
DataContract 属性と DataMember 属性を使用して WCF Web サービスのデータ形式を定義した後で、サービスとそのサービス メソッドを設定する必要があります。サービスは ServiceContract 属性で修飾され、そのメソッドは OperationContract 属性で修飾されます。サービスは必要に応じてインターフェイスを実装できます。その場合は、インターフェイス (サービス クラスではありません) とそのインターフェイス メソッドを属性で修飾します。これらの属性で修飾された IEmployeeService とそのメンバを次に示します。
[ServiceContract(Namespace = "")]
   public interface IEmployeeService
   {
    [OperationContract]
    List<Employee> FindEmployees();
   }
ここで、Silverlight 2 アプリケーションが処理できるのは basicHttpBinding を使用する SOAP ベースの WCF サービスだけであることに注意してください。
Silverlight 2 クライアント アプリケーションは、このサービスへのサービス参照を追加できます。このサービス参照がプロキシ クラスを作成します。その後、ASMX Web サービスの場合と同様に、サービスを非同期に呼び出すことができます。結果のキャプチャにはハンドラが役立ちます。プロキシと完了イベント ハンドラを実装するためのコードには、ASMX Web サービスで示したコードとまったく同じ形式を使用します。データが List<Employee> の形で返された後、サービス メソッドを通じて、エンティティの反復処理、データ バインド、変更、および送信を適宜行うことができます。

データは REST Web サービスからどのような方法で受信するのですか。
REST サービスは、パラメータの含まれるクエリ文字列を使用して、URI を通じて呼び出すことができます。このサービスは、WebClient クラスまたは HttpWebRequest を使用して、Silverlight 2 から呼び出せます。WebClient を使用して呼び出す方が簡単ですが、HttpWebRequest を使用すると、要求の実行方法をよりきめ細かく制御できます。また、WebClient は UI スレッドで戻りますが、HttWebRequest はバックグラウンド スレッドで戻ります。HttpWebRequest のコールバックでは、Dispatcher を使用して UI とやり取りする必要があります。次のコードは、Digg サービスから最新の記事を受信する System.Net.WebClient を示しています。
string baseUri = "http://services.digg.com/stories/topic";
string topic = txtTopic.Text;
string appKey = "http%3A%2F%2Fwww.microsoft.com";
int count = int.Parse(txtTopicCount.Text);
string url = String.Format("{0}/{1}?appkey={2}&count={3}", baseUri, topic, appKey, count);
WebClient svc = new WebClient();
svc.DownloadStringAsync(new Uri(url));
この場合、データは WebClient の呼び出しから未加工の XML として返されます。その後で、データの解析とクエリを実行できます。REST サービスにパラメータがある場合、それらのパラメータはクエリ文字列の一部として渡すことができます。

XML のクエリの実行方法を教えてください。
呼び出された REST ベースのサービスまたは POX サービスは、データを XML として返すことができます (REST サービスは、データを JSON として返すこともできます)。その後、さまざまな XML 解析ライブラリを使用して XML を解析できます。ただし、LINQ to XML を使用して XML のクエリを行うこともできます。LINQ to XML は、複雑な XML 階層の反復処理を行うことなくデータを XML 構造から抽出するリッチなクエリ インターフェイスを提供します。
先ほど紹介した例の REST サービスでは、最新の記事を表示する Digg サービスから未加工の XML を返します。この XML は XML ライブラリを使用して解析できます。また、図 4 に示すように、LINQ to XML を使用してクエリを行うこともできます。XDocument クラスの Parse メソッドは、前に示した REST サービスからの XML を処理できます。その後、LINQ to XML を使用して XML のクエリを実行できます。図 5 に示す XML のクエリを行うための LINQ to XML 構文を、図 4 に示します。
図 4 Digg データのクエリを行う LINQ to XML
XDocument xml = XDocument.Parse(rawXml);
var storiesQuery = from story in xml.Descendants("story")
  select new DiggStory
    {
       Id = (int)story.Attribute("id"),
       Title = ((string)story.Element("title")).Trim(),
       Description = ((string)story.Element("description")).Trim(),
       ThumbNail =
         (story.Element("thumbnail") == null
              ? string.Empty
              : story.Element("thumbnail").Attribute("src").Value),
       Link = new Uri((string)story.Attribute("link")),
       DiggCount = (int)story.Attribute("diggs")
    };
図 5 Digg の XML 応答
<?xml version="1.0" encoding="utf-8" ?>
<stories timestamp="1222547017" min_date="1219955010" total="2541"
                                                 offset="0" count="10">
  <story link="http://news.msn.com.us/2/hi/technology/7540282.stm"
       submit_date="1222545893" diggs="1" id="8725656" comments="0"
       href="http://digg.com/microsoft/Microsoft_releases_SQL_Server_
       2005_recently" status="upcoming" media="news">
    <description>Microsoft has released SQL Server 2005 recently
       as an upgrade to the popular SQL Server database.</description>
    <title>Microsoft releases SQL Server 2005</title>
    <user name="D3a1i0" icon="http://digg.com/img/udl.png"
      registered="1104422772" profileviews="5616" />
    <topic name="Microsoft" short_name="microsoft" />
    <container name="Technology" short_name="technology" />
    <thumbnail originalwidth="226" originalheight="170" contentType=
      "image/jpeg" src="http://digg.com/microsoft/Microsoft_releases_SQL_
      Server_2005_recently/t.jpg" width="80" height="80" />
  </story>
  <story link="http://navigatetrends.blogspot.com/2008/09/microsofts-
      virtual-receptionist.html" submit_date="1222545326" diggs="1"
      id="8725560" comments="0" href="http://digg.com/microsoft/
      Microsoft_s_virtual_receptionist" status="upcoming" media="videos">
    <description>Microsoft's virtual receptionist</description>
    <title>Microsoft's virtual receptionist</title>
    <user name="billarunk" icon="http://digg.com/img/udl.png"
      registered="1216563445" profileviews="341" fullname="bill.arunk"
    />
    <topic name="Microsoft" short_name="microsoft" />
    <container name="Technology" short_name="technology" />
    <thumbnail originalwidth="400" originalheight="254" contentType="image/
      jpeg" src="http://digg.com/microsoft/Microsoft_s_virtual_
      receptionist/t.jpg" width="80" height="80" />
  </story>
...
...
</stories>
クエリは stories/story 階層で開始され、すべての story 要素に対して実行されます。この XML に対して指定されている名前空間はありませんが、結果が名前空間を参照する場合にすべての XML パスにプレフィックスを付ける方法を示すため、名前空間変数 (ns) を残しました。story 要素は、XDocument インスタンスの Descendants メソッドを使用して処理します。story 要素のプロパティは、story 変数とその Element メソッドを使用して取得できます。たとえば、次のコード行では、story 要素から title 要素の値を取得し、それを Title という名前のプロパティに設定します。
Title = ((string)story.Element("title")).Trim()
LINQ to XML クエリでは、投影を通じて、またはクエリ構造に対してデータを返すことができます。図 4 に示すコードでは、DiggStory クラスを作成して、XML データから取得した各 status 要素の結果を格納しています。開発者が定義した DiggStory クラスでは、コードをわかりやすくするため、すべてのプロパティが文字列として定義されています。LINQ to XML には、その他の要素 (順序付けやフィルタ処理の条件など) を必要に応じて追加することもできます。
REST サービスまたは POX サービスから取得した XML データのクエリと解析に LINQ to XML を使用するとき、開発者は他の形式の LINQ に関する既存の知識を活かすことができます。LINQ の構文は、どれも非常によく似ているためです。データの解析に LINQ to XML を使用した場合の最大のメリットは、入れ子になった foreach ループで要素を反復処理することなく、データのクエリを簡単に実行できるという点でしょう。

サードパーティの Web サービスにアクセスするとき、ドメイン間ポリシーに注意する必要はありますか。
Silverlight 2 アプリケーションが Web サービスの呼び出しを行うと、Web サービスのサーバーでドメイン間ポリシー ファイルがチェックされ、この呼び出しが無効になることがあります。このチェックは、Silverlight アプリケーションが Web サービスのドメインとは別のドメインにホストされているときに行われます。たとえば、図 6 は、Web サーバー johnpapa.net にホストされている Silverlight 2 アプリケーションが、同じく johnpapa.net にホストされている Web サービスを呼び出せることを示しています。これは、両者が同じドメインに配置されているためです。ただし、johnpapa.net にホストされている Silverlight 2 クライアントが microsoft.com にホストされている Web サービスを呼び出す場合、Silverlight 2 はドメイン間ポリシー ファイルが microsoft.com に存在し、呼び出しを許可していることを確認します。
図 6 ドメイン間のチェック
ドメイン間ポリシーの詳細については、2008 年 9 月号のコラム「Silverlight 2 と WCF を使用したサービス駆動型アプリケーション」を参照してください。このコラムでは、ファイル形式とポリシーの機能について説明しています。
Silverlight 2 では、対象の Web サーバーがそのドメイン間ポリシー ファイルを通じて呼び出しを許可している場合にのみ、ドメイン間呼び出しを行うことができます。WCF Web サービスや REST ベースのサービスなど、呼び出す Web サービスの種類は問いません。例として、前のコードをもう一度見てみましょう。
string baseUri = "http://services.digg.com/stories/topic";
string topic = txtTopic.Text;
string appKey = "http%3A%2F%2Fwww.microsoft.com";
int count = int.Parse(txtTopicCount.Text);
string url = String.Format("{0}/{1}?appkey={2}&count={3}", baseUri,
  topic, appKey, count);
WebClient svc = new WebClient();
svc.DownloadStringAsync(new Uri(url));
このコードは、Digg Web サービスを呼び出して最新の記事にアクセスします。Digg では、Flash 形式を使用してドメイン間ポリシー ファイルを http://services.digg.com/crossdomain.xml に保持しています。
この Digg ドメイン間ファイルの内容を次に示します (この記事の執筆時点での内容です)。
<cross-domain-policy>
  <allow-access-from domain="*"/>
</cross-domain-policy>
この Digg ドメイン間ファイルではあらゆるドメインからの呼び出しが許可されていますが、制限を加えている Twitter ドメイン間ファイルと同様に、呼び出しを制限できます。次のコードの Twitter ドメイン間ファイルに注目してください。
<?xml version="1.0" encoding="UTF-8" ?>
<cross-domain-policy
  xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:
  noNamespaceSchemaLocation="http://www.adobe.com/xml/schemas/
  PolicyFile.xsd">
  <allow-access-from domain="*.twitter.com" />
  <site-control permitted-cross-domain-policies="master-only" />
  <allow-http-request-headers-from domain="*.twitter.com" />
</cross-domain-policy>
Twitter のドメイン間ファイルでは、呼び出しは *.twitter.com ドメインからの呼び出しに制限されています。Silverlight 2 アプリケーションが twitter.com ドメインにホストされていない場合、Silverlight クライアントは Twitter の REST ベース サービスに対する Web サービス要求を直接行うことはできません。このとき、プロキシ サービスが役立ちます。たとえば、Twitter.com Web サービスを呼び出す WCF Web サービスを作成し、それを自分のドメインにホストできます。その WCF サービスからの呼び出しは、Silverlight アプリケーションと Twitter 間で情報を中継します。サーバーで実行されている WCF サービスには Silverlight クライアントとは違ってドメイン間ポリシー ファイルの制限が適用されないため、呼び出しを行うことができます。この方法とは別に、Popfly や Yahoo! のクラウド サービスなどのサービスを通じて呼び出しを中継する方法もあります。

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

John Papa (johnpapa.net) は、ASPSOFT (aspsoft.com) の上級コンサルタントです。野球を熱狂的に愛し、夏の夜を家族と共にヤンキースの応援に費やします。C# の MVP であり、INETA の講演者でもある John は、何冊かの書籍を発表しており、現在も最新作の『Data Access with Silverlight 2』を執筆中です。また、主にカンファレンス (DevConnections、VSLive など) での講演で活躍しています。


Cutting Edge
Silverlight 2 におけるブラウザの相互運用性
Dino Esposito

このコラムは、Silverlight 2 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。
コードのダウンロード: MSDN Code Gallery (565 KB)
オンラインでのコードの参照
目次
Silverlight コントロールを構成する
Silverlight から DOM にアクセスする
ブラウザの相互運用層の内部
DOM イベントにマネージ コードをアタッチする
同期呼び出しと Silverlight 2
XAML、マネージ コード、および JavaScript
クロスドメイン アクセスと Silverlight プラグイン
まとめ
Silverlight 2 を使用すると、まるで Windows Presentation Foundation (WPF) で作成したようなフルページの Web アプリケーションを構築したり、アニメーション、広告、特定のアプレットなどの機能を追加して HTML ベースのページの質を高めたりできます。ブラウザは、Silverlight アプリケーションを構成する eXtensible Application Markup Language (XAML) コンテンツを直接処理しません。その代わりに、HTML ページ内で <object> タグを通じて Silverlight 2 プラグインを参照し、そのパラメータ間に URL を保持して、必要な XAML リソースをダウンロードします。
重要なのは、Silverlight プラグインにラップされた HTML ページが常に存在しているということです (ただし、Silverlight アプリケーションが全画面表示モードで実行されるように構成されているために、その HTML ページが表示されないこともよくあります)。また、サイトが同じ場合、Silverlight プラグインによってホストされているコンテンツは周囲のページから切り離されているわけではありません。Silverlight コンテンツがホスト HTML ページとは異なるドメインから提供されている場合は、そのコンテンツはページから切り離されていることになります。
Silverlight は、ブラウザの相互運用層を備えています。この相互運用層を通じて、マネージ コードで基になるページのドキュメント オブジェクト モデル (DOM) にアクセスし、ページレベル イベントのマネージ ハンドラを登録できます。同時に、ページで実行されている JavaScript コードでプラグインの XAML コンテンツにアクセスしたり、変更を加えたりできます。さらに、ページで実行されている JavaScript コードでマネージ関数を呼び出すこともできます (ただし、これらの関数が適切に公開されている場合)。今月は、Silverlight 2 が備えているブラウザの相互運用層と、それを実際のアプリケーションで活用する方法について説明します。

Silverlight コントロールを構成する
既に説明したように、Silverlight プラグインは <object> タグを通じて呼び出されます。ただし、このタグは、ASP.NET ページの ASP.NET コントロールで自動的に作成できます。Visual Studio 2008 で Silverlight のサンプル アプリケーションを作成すると、Silverlight サーバー コントロールの次のマークアップがテスト ページに自動的に挿入されます。
<asp:Silverlight ID="Xaml1" runat="server"
     Source="~/ClientBin/SilverTestApp.xap"
     MinimumVersion="2.0.30523"
     Width="100%"
     Height="100%" />
MinimumVersion 属性は、Source 属性で指定されたアプリケーションを実行するのに必要な Silverlight ランタイムの最小バージョンを示しています。Width と Height では、ホスト ページに対する Silverlight ウィンドウのサイズを定義しています。ご覧のように、既定では、Silverlight プラグインはフルサイズ モードで実行されるように構成されています。
図 1 に、Silverlight プラグインを構成する際に設定できるプロパティを示します。図 1 のプロパティは、Silverlight ASP.NET コントロールを参照します。このコントロールはサーバー側で実行され、<object> タグを出力します。この <object> タグには、異なる名前の付けられているパラメータがある場合や、対応するパラメータがない場合もあります。このコラムの意図を意識して、HtmlAccess プロパティに注目してください (Silverlight SDK のこの部分は、Beta 1 から Beta 2 への移行に伴って若干変更されていることに注意してください)。
図 1 Silverlight ASP.NET サーバー コントロールのプロパティ
プロパティ | 説明
AutoUpgrade | Silverlight プラグインを自動的にアップグレードするかどうかを指定します。既定値は false です。
DefaultScriptType | Silverlight プラグインの作成と関連付けに必要なクライアント JavaScript オブジェクトの既定の種類です。既定では、Sys.UI.Silverlight.Control です。
EnableFrameRateCounter | ホスト ブラウザのステータス バーに現在のフレーム レートを表示するかどうかを指定します。既定値は true です。
EnableRedrawRegions | 各フレームを使用して再描画される Silverlight プラグインの領域を表示するかどうかを指定します。既定値は true です。
HtmlAccess | Silverlight アプリケーションがページ DOM にアクセスできるようにするかどうかを指定します。
InitParameters | ユーザー定義の初期化パラメータのオプション セットを定義します。
MaxFrameRate | 1 秒あたりに描画する Silverlight ドキュメントのフレームの最大数です。
MinimumVersion | 現在のアプリケーションに必要なプラグインの最小バージョンです。既定値は 1.0 です。
PluginBackground | プラグインの背景色を指定します。
PluginNotInstalledTemplate | Silverlight プラグインがインストールされていない場合に描画する HTML マークアップです。
ScaleMode | 使用可能な領域に Silverlight プラグインをどのように配置するかを指定します (値なし、拡大/縮小、または伸縮)。既定は、値なしです。
ScriptType | Silverlight プラグインの作成と関連付けに必要なクライアント JavaScript オブジェクトの種類です。
Source | ダウンロードする XAML ソース ファイルまたは Extensible Ajax Platform (XAP) ソース パッケージの URL です。
SplashScreenSource | ソース ドキュメントの読み込み時に描画するスプラッシュ スクリーン ドキュメントの URL です。
Windowless | Silverlight プラグインをブラウザのクライアント領域に直接描画するか、適切に作成されたウィンドウ内部に描画するかを指定します。既定値は true です。
HtmlAccess プロパティは、Silverlight アプリケーションに許可する、基になるページ DOM へのアクセス レベルを示します。このプロパティは、HtmlAccess 列挙型から値を受け取ります。その値を図 2 に示します。
図 2 ページ DOM へのアクセスを制御する値
値 | 説明
Disabled | アプリケーションは、基になるページの DOM にアクセスできません。
Enabled | アプリケーションは、基になるページの DOM にアクセスできます。
SameDomain | アプリケーションは、基になるページが Silverlight XAP アプリケーションの同じドメインから提供されている場合にのみ、そのページの DOM にアクセスできます。これが既定の設定です。
Enabled と Disabled には、名前が示すとおりの効果があります。既定の設定は SameDomain です。この設定では、ページのマークアップにスクリプトは挿入されません。Silverlight アプリケーションは、ネイティブ ドメイン外部のフレーム内にホストされるページでホストできるという点に注意してください。この場合、Silverlight のマネージ コードは、ドメインをまたいでホスト ページの DOM にアクセスできます。ブラウザは、クロスドメイン スクリプティングを防ぐ独自のバリアを備えていますが、Silverlight プラグイン内のマネージ コードによるアクセスを防ぐことはできません。Silverlight ページの作成者は、HtmlAccess を使用してクロスドメイン アクセスを制御できます。

Silverlight から DOM にアクセスする
基になるページ DOM へのアクセスが許可された後、Silverlight アプリケーションは、静的クラスである HtmlPage のメンバを使用して独自のタスクを実行できます。図 3 に、HtmlPage クラスのプロパティとメソッドを示します。
図 3 HtmlPage クラスのメンバ
メンバ | 説明
BrowserInformation | ブラウザに関する一般的な情報 (名前、バージョン、オペレーティング システムなど) を取得します。
Document | ページのドキュメント オブジェクトへのアクセスを提供します。
IsEnabled | ページ DOM へのアクセスが許可されているかどうかを示します。
Plugin | Silverlight オブジェクトへの参照を取得します。
Window | ページのウィンドウ オブジェクトへのアクセスを提供します。
RegisterCreateableType | JavaScript コードから作成できる型としてマネージ型を登録します。
RegisterScriptableObject | ページの JavaScript コードでスクリプト可能なオブジェクトとしてマネージ オブジェクトを登録します。
UnregisterCreateableType | RegisterCreteableType メソッドを使用して作成可能な型として登録されている型の登録を解除します。
Document プロパティは、基になるページの DOM を表す System.Windows.Browser.HtmlDocument マネージ型のオブジェクトへの参照を返します。Document プロパティは厳密に型指定されており、Silverlight アプリケーションに対して、柔軟に型指定された方法でスクリプトに使用できるほとんどの機能を公開します。プロパティには、Cookie の一覧、クエリ文字列、ドキュメントの本文、DOM のルートへの参照が含まれます。オブジェクトには、ブラウザのドキュメントが完全に読み込まれているかどうかを通知するブール型の IsReady プロパティもあります。このプロパティは、ブラウザの DOM における DocumentReady イベントの転位と対になっています。
HtmlDocument クラスには、Submit、AttachEvent、DetachEvent、GetElementById、CreateElement などのメソッドもあります。また、継承される 1 組のメソッド (GetProperty および SetProperty) を使用すると、マネージ コード内から HTML 要素の属性を取得および設定できます。
BrowserInformation プロパティを通じて、すべてのブラウザ情報を取得できます。また、この場合のプロパティは、ブラウザ レベルで取得できるあらゆるユーザー エージェント情報をラップするマネージ型です。次のコードは、ユーザー エージェント データへのアクセス方法を示しています。
string info = HtmlPage.BrowserInformation.UserAgent;
BrowserInformation オブジェクトは、ブラウザで Cookie をサポートするかどうかを指定するブール型プロパティも公開します。
次のコードは、マネージ コードを使用して DOM 要素への参照を取得する方法を示しています。
HtmlElement label1 = HtmlPage.Document.GetElementById("Label1");
label1.SetProperty("innerHTML", "Dino");
GetElementById メソッドは ID 文字列を受け取り、基になる DOM で対応する要素を探します。返されるオブジェクトは HtmlElement 型です。これは、基になるブラウザ オブジェクトへの参照のラッパーとして機能するマネージ型です。
HtmlElement には、この要素を HTML 要素のように表示するための一連のプロパティがあります。また、AppendChild、RemoveChild、GetAttribute、RemoveAttribute、SetAttribute などのメソッドがあります。プロパティは、Id、Parent、TagName、および Children です。
Silverlight コード内から操作するすべての HTML スクリプト オブジェクトは、ベースの ScriptObject クラスから派生します。このクラスは、HtmlDocument などのすべての派生クラスが継承する 1 組のメソッド (SetProperty および GetProperty) を定義します。
取得および設定する属性とプロパティには、どのような違いがあるのでしょうか。属性は常に文字列として管理されますが、プロパティは厳密に型指定された値として管理されます。

ブラウザの相互運用層の内部
図 4 に、Silverlight のブラウザの相互運用層とページ DOM へのアクセスをグラフィックで示します。相互運用層でクラスに対して行われた要求は、内部ブラウザ ホスト サービスを通じて解決されます。情報はブラウザのアンマネージ環境にマーシャリングされてから、Silverlight に戻されます。型の違いは相互運用層で隠匿および処理されます。DOM レベルのオブジェクトはマネージ オブジェクトとしてラップされ、新しいマネージ インターフェイス (HtmlDocument、HtmlWindow など) を通じて Silverlight コードに提供されます。それでは、GetElementById メソッドについて詳しく見てみましょう。
図 4 Silverlight 2 における HTML ブリッジ
このメソッドは、まずコードが Silverlight の UI スレッドで呼び出されていることを確認します。コードがこのスレッドで呼び出されていない場合は、例外がスローされます。次に、指定した DOM 要素への参照を取得するために、基になるブラウザに対して要求を行います。要求が成功した場合は、メソッドは DOM オブジェクトのアンマネージ参照を取得し、そのオブジェクトに対してマネージ ラッパーを作成して返します。

DOM イベントにマネージ コードをアタッチする
すばらしいことに、Silverlight と DOM の対話の結果、DOM イベントに応答してマネージ コードを実行できるようになります。たとえば、ユーザーがボタンをクリックした際に、JavaScript コードの代わりに C# コードを実行できます。これを実現するための方法を次に示します。
HtmlElement button1;
button1 = HtmlPage.Document.GetElementById("Button1");
button1.AttachEvent("click",
         new System.EventHandler(Button1_Click));
まず、目的のボタン (つまり DOM 要素) へのマネージ参照を取得します。次に、AttachEvent マネージ メソッドを呼び出し、特定のイベントのハンドラを登録します。すばらしいのは、ハンドラはマネージ コードですが、イベントはブラウザのアンマネージ レベルでトリガされるという点です。たとえば、JavaScript では GUID を取得することがほとんど不可能ですが、Silverlight を通じてマネージ コードを活用すれば、これをかなり簡単に行えるようになります。
void Button1_Click(object sender, EventArgs e)
{
    // Get a new GUID
    Guid g = Guid.NewGuid();

    // Display the GUID in the page user interface
    HtmlElement label1 = HtmlPage.Document.GetElementById("Label1");
    label1.SetProperty("innerHTML", g.ToString());
}
言うまでもなく、マネージ コードは Silverlight ページの分離コード クラスのメンバです。処理の結果は、(前の例のように) ホスト ページの HTML に反映させることも、Silverlight のユーザー インターフェイスに直接反映させることもできます。開発者の裁量で、状況に応じて選んでください。
イベントのアタッチは、ブラウザの相互運用層を通じて実行され、最後に DOM オブジェクトで AttachEvent メソッドを呼び出す処理です。ブラウザがページレベルのイベントをトリガすると、Silverlight に対して呼び出しが行われ、マネージ コードが実行されます。
現実の世界では、こうした機能がどのような場合に役立つのでしょうか。Silverlight は、豊富な機能を備えた Web フロント エンドを提供するための製品です。スクリプトよりもマネージ言語の方が適切かつ迅速に処理できる操作を、マネージ コードを使用してブラウザで行う必要がある場合に、Silverlight が活躍します。このような操作の例としては、コンパイル言語の速度がスクリプトの速度をはるかに上回るような、コードの量が多い操作や、機能に制限のある環境 (ブラウザの環境など) では実行不可能な操作などが挙げられます。確かに、DOM の操作だけを行うのであれば、必ずしも Silverlight は必要ありません。マネージ コードでイベントを処理する機能があると非常に便利ですが、この機能を手に入れるために Silverlight が必要なのです。せっかく入手した Silverlight をイベントの処理だけに使用しようと考える開発者はいないでしょう。
Silverlight 2 では、HtmlWindow オブジェクトは、JavaScript ウィンドウ オブジェクトのマネージ表現を提供します。また、DOM オブジェクトへの参照を格納し、一連のマネージ メソッド (Alert、Confirm、Prompt、Submit、Navigate、および Eval) を使用してこれを処理できるようにします。HtmlWindow オブジェクトのインスタンスは、HtmlPage クラスの Window プロパティを通じて Silverlight 開発者に公開されます。次のコードは、ブラウザのメッセージ ボックスを Silverlight から表示する方法を示しています。
HtmlPage.Window.Alert("Hello, world");
このシンプルなコードの背後ではどのような処理が行われているのか、詳細に見てみましょう。ここで見ていくモデルは、HtmlWindow クラスのほとんどすべてのメソッドで繰り返し使用されます。
public void Alert(string message)
{
    HtmlPage.VerifyThread();
    if (message == null)
    {
        message = string.Empty;
    }
    this.Invoke("alert",
                new object[] { message });
}
UI スレッドのテスト後、メソッドは、メッセージ文字列が null の場合はその文字列を修正し、ベースの ScriptObject クラスの Invoke メソッドを呼び出します。Invoke メソッドは、Silverlight のマネージ環境とブラウザを橋渡しする要素です。
このメソッドは、次のように 2 つのパラメータを受け取ります。
public virtual object Invoke(string name, params object[] args)
最初の引数は、ScriptObject の現在のインスタンスに格納されているスクリプト可能なオブジェクトで呼び出すメソッドの名前を示します。2 番目の引数は、単に呼び出すメソッドの引数の一覧です。
このメソッドは、2 つの異なるランタイム環境間で型を適切にマーシャリングします。マネージ オブジェクトをブラウザに渡す際には JavaScript 対応の型に変換し、このオブジェクトをブラウザから受け取る際には逆の処理を行います。
HtmlWindow には、注目に値するメソッドがもう 1 つあります。それは、CreateInstance メソッドです。
public ScriptObject CreateInstance(
    string typeName,
    params object[] args)
このメソッドを使用すると、指定した JavaScript オブジェクトのインスタンスを作成できます。型名のパラメータは、インスタンス化する JavaScript オブジェクトの名前を示します。内部では、このメソッドは指定のオブジェクトを作成する動的な JavaScript 関数を準備し、その関数を Silverlight から呼び出します。
ScriptObject xhr = HtmlPage.Window.CreateInstance("XMLHttpRequest");
CreateInstance メソッドは、JavaScript オブジェクトへのスクリプト可能な参照を取得するのに役立ちます。次に、Invoke メソッドを使用してそれをスクリプト実行します。これらの機能は、Silverlight 2 から同期呼び出しを行う必要がある場合に非常に役立ちます。ただ問題なのは、Silverlight 2 では同期呼び出しがサポートされていないことです。

同期呼び出しと Silverlight 2
Silverlight 2 にはリモート エンドポイントの呼び出しを行うためのさまざまな API が用意されていますが、これらの呼び出しはすべて非同期である必要があります。呼び出しはバックグラウンド スレッドで開始できますが、UI スレッドを同期させることはできません。同期オブジェクトで UI スレッドをブロックすると、UI スレッドが永久に停止してしまい、同期オブジェクトをリセットするコマンドがスレッドを再開できなくなります。コミュニティでは、Silverlight における同期呼び出しについて活発な議論が行われています。
それにもかかわらず、Silverlight API からのリモート エンドポイントの同期呼び出しは現時点ではサポートされていません。遅延が伴うことが判明しているためです。とはいえ、ブラウザ環境では同期呼び出しは重要な機能です。XmlHttpRequest をサポートしているすべてのブラウザでは、同期呼び出しがサポートされています。
XmlHttpRequest は、AJAX シナリオを実現するブラウザ オブジェクトです。このオブジェクトは、ブラウザのメカニズムを活用して、同じドメイン内のエンドポイントの呼び出しを行います。既定では、XmlHttpRequest は非同期で動作し、ほとんどの AJAX フレームワークがこの方法で XmlHttpRequest を使用します。ただし、XmlHttpRequest を同期的に動作するように構成するのは、非常に簡単です。
Silverlight から XmlHttpRequest のインスタンスを作成および制御することで、同期呼び出しを調整し、同期呼び出しによってコーディングが非常に簡単になるようなあらゆるシナリオを修正できます (私個人としては、Silverlight リモート呼び出しの "非同期のみ" の特性は好きではありません。非同期呼び出しだけで事足りるケースがほとんどですが、既存のフロント エンドを Silverlight に適合させる際には、同期呼び出しによって再設計の手間が大幅に省ける可能性があります。鍵となるのは設計だと言い出したのは私ですが、意識的に記述された同期呼び出しによって数時間分または数日分の作業を省けるのであれば、私も採用したいと思います)。
とは言うものの、Silverlight チームには、非同期のみのアプローチを推す正当な理由があります。リモート エンドポイントの同期呼び出しを行うと、Silverlight アプリケーションのユーザー インターフェイスがフリーズすることがあるのです。その場合、エンド ユーザーによるブラウザと Web アプリケーションの使用が妨げられてしまいます。同期呼び出しは技術的には可能ですが、すべてのアプリケーションとそのコンシューマの利便性を考え、チームはプラットフォームにおける同期呼び出しをネイティブにはサポートしていません。同様に、図 5 に示すような、XmlHttpRequest を使用して手動で記述された同期リモート呼び出しを用いることも推奨していません。
図 5 Silverlight 2 内から同期呼び出しを行う
private void Button1_Click(object sender,
    System.Windows.RoutedEventArgs e)
{
    string url = "...";
    ScriptObject xhr = HtmlPage.Window.CreateInstance("XMLHttpRequest");
    xhr.Invoke("open", "POST", url, false);
    xhr.Invoke("setRequestHeader", "Content-Type",
        "application/x-www-form-urlencoded");

    // Prepare the body as the endpoint expects it to be
    string body = "...";
    xhr.Invoke("send", body);
    string response = (string) xhr.GetProperty("responseText");

    // Process the response and update the UI
    ProcessData(response)
}
図 5 に、XmlHttpRequest を使用して Silverlight 2 内からのドメインが同じ URL の同期呼び出しを設定する方法を示します。鍵となるステートメントは、呼び出しを行う際の XmlHttpRequest の open メソッドです。ブール型の引数で、呼び出しを非同期にするかどうかを指定します。オブジェクトを同期的な方法で処理する場合は、false です。
この方法の弱点は、文字列を JavaScript Object Notation (JSON) ストリームにシリアル化および逆シリアル化する機能を備えていない、低レベルなツールを活用するという点です。この方法を採用する場合は、DataContractJsonSerializer クラスを使用して、JSON のシリアル化と逆シリアル化に対処する必要があります。

XAML、マネージ コード、および JavaScript
JavaScript 関数は、Silverlight アプリケーションのコンテンツにアクセスし、読み取り操作と書き込み操作を行うことができます。Silverlight アプリケーションのコンテンツは、XAML 要素のツリーになっています。
一意の名前 (x:Name 属性) で識別される XAML ドキュメントの内容は、JavaScript でのアクセスとスクリプト実行が可能です。それにはまず、Silverlight プラグインへの DOM 参照を取得します。ASP.NET AJAX ページでは、次のコードを使用します。
var plugin = $get("SilverlightControl1");
SilverlightControl1 は、Silverlight コントロールの ID か、ダウンロード可能なコンテンツを指す <object> タグに使用した ID です。次に、content プロパティを使用して、実際の XAML コンテンツを指定します。特定の XAML 要素を探すには、findName メソッドを使用します。
// Retrieve the XAML element tagged with the name of TextBlock1
var xamlTree = $get("SilverlightControl1").content;
var textBlock1 = xamlTree.findName("TextBlock1");

// Modify the current content of the text block element
textBlock1.Text = "...";
JavaScript コードを使用して XAML ドキュメントのコンテンツにアクセスする場合は、途中で出現する XAML 要素への参照をページでキャッシュすることをお勧めします。そうすることで、同じ要素を探すために XAML ツリーを何度もトラバースする必要がなくなります。
JavaScript を使用して XAML ドキュメントのコンテンツにアクセスする方法は、Silverlight 2 ではやや古い方法であることに注意してください。ただし、Silverlight 1.0 を対象としている場合や、少なくとも XAML およびスクリプト コードだけから成る Silverlight アプリケーションを提供する場合には、引き続き有効な方法です。マネージ コードを使用して表示コンテンツに関する決定を行うことのできる開発者は、マネージ コードの代わりに JavaScript を使用した場合、ユーザー インターフェイスにはどのような変更を加えればよいのか、容易に思い付かないでしょう。
Silverlight 2 では、JavaScript コードを使用してマネージ コードを呼び出すことができます。この方法により、重要な処理を行うマネージ コードを使用した HTML ベースのページが存在するシナリオに対処できます。
JavaScript コードでは、スクリプト可能として事前に登録された、Silverlight 2 でホストされている任意のマネージ オブジェクトにスクリプトでアクセスできます。
guidHelper = new GuidHelper();
HtmlPage.RegisterScriptableObject("GuidTools", guidHelper);
RegisterScriptableObject メソッドは、登録されるオブジェクトの非公式の名前と可変インスタンスを受け取ります。最初の引数の文字列は、呼び出し元の JavaScript コードが登録されたオブジェクトを参照するために使用する名前です。
上記のコードに関連する次のコードは、GuidHelper サンプル クラスのメソッドを呼び出すために JavaScript から行う処理を示しています。
// Invoke a method on a scriptable managed object
var guid = $get("Silverlight1").content.GuidTools.Generate();

// Update the user interface with the results
$get("Random1").innerHTML = guid;
スクリプト可能なオブジェクトのパブリック名 (この場合は GuidTools) は、基になるオブジェクトに JavaScript からアクセスするためのメンバとして使用されます。疑似プロパティである GuidTools は、Silverlight プラグインの content プロパティによって公開されます。では、GuidHelper クラスを見てみましょう。
public class GuidHelper
{
    [ScriptableMember]
    public string Generate()
    {
        Guid g = Guid.NewGuid();
        return g.ToString();
    }
}
スクリプト可能なクラスのパブリック メソッドの ScriptableMember 属性は、メソッドを JavaScript から呼び出せることを示しています。クラスのすべてのパブリック メソッドがスクリプト可能な場合は、クラスで ScriptableType 属性を使用することで、スクリプト可能であることを示す属性をすべてのパブリック メソッドに自動的に拡張できます。
スクリプト可能として登録されたオブジェクトはマネージ コードを通じてインスタンス化され、既存のインスタンスは JavaScript に渡されます。すべてのマネージ型を JavaScript で直接インスタンス化できるわけではありません。
JavaScript からマネージ クラスのインスタンスを作成するには、まず JavaScript コードから作成できる型として、目的の型を登録する必要があります。
HtmlPage.RegisterCreateableType("StockPicker", typeof(Samples.Order));
最初の引数は、JavaScript 内から使用される型の別名を示します。Samples.StockPicker クラスのインスタンスの作成方法を次に示します。
var plugin = $get("Silverlight1");
var type = "Samples.StockPicker";
var inst = plugin.content.services.createObject(type);
Silverlight プラグインを取得し、content.services プロパティにアクセスしてから、createObject メソッドを呼び出します。createObject の引数は、インスタンス化するマネージ型の名前を示す文字列です。
メソッドのシグネチャに複雑なカスタム型のあるスクリプト可能な型は、その型のファクトリとしても機能します。例として、次のメソッドを持つ Customer マネージ型を考えてみましょう。
void Add(Order order);
Order 型はどうなるでしょうか。作成可能として宣言する必要はあるでしょうか。いいえ、必ずしも宣言する必要はありません。RegisterCreateableType の呼び出しを省略した場合、スクリプト可能な型 (その型を必要とするスクリプト可能なメンバのある型) はファクトリとしてのみ使用できます。つまり、Customer がスクリプト可能として登録されており、Add メソッドがスクリプト可能である場合は、型を作成可能として宣言することなく JavaScript から Order をインスタンス化できます。次のコードは正常に動作します。
var customer = $get("Silverlight1").content.Customer;
var order = customer.createManagedObject("Order");
order.ID = 1;
o.OrderDate = new Date();
customer.Add(order);
マネージ型は、ブラウザ オブジェクトとしてラップされた JavaScript にマーシャリングされます。ラッパー ブラウザ オブジェクトには、JavaScript から呼び出す有効なメソッドの一覧が含まれます。この一覧には、スクリプト可能として宣言されたすべてのメソッドと createManagedObject が含まれます。メソッドに対する呼び出しは、マネージ コードの対応するメソッドを呼び出すことで解決されます。

クロスドメイン アクセスと Silverlight プラグイン
パブリック マネージ API を公開する Silverlight アプリケーションは、クロスドメイン アクセスの対象となります。クロスドメイン アクセスがセキュリティ上危険である場合や、単に望ましくない機能である場合は、対策を講じる必要があります。
Silverlight プラグインの含まれるページがフレーム内にホストされている場合、ホスト ページは Silverlight アプリケーションとは別のドメインに存在可能です。つまり、ホスト ページ (クロスドメイン) の JavaScript コードは、フレームを通じて Silverlight プラグインにアクセスし、任意のパブリック マネージ オブジェクトにスクリプトでアクセスできます。
Silverlight コンテンツへのクロスドメイン アクセスは、無効にすることも、完全に有効にすることもできます。また、スクリプトに制限することも可能です。既定では無効になっています。クロスドメイン アクセスを制御するには、図 6 に示すように、Silverlight アプリケーションのマニフェスト ファイルの Deployment ノードで ExternalCallersFromCrossDomain を使用します。このマニフェスト ファイルは、Silverlight プロジェクトをコンパイルしたときに Visual Studio 2008 によって生成されるファイルです。
図 6 Silverlight 2 アプリケーションのサンプル マニフェスト ファイル
<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            EntryPointAssembly="BrowserFun"
            EntryPointType="BrowserFun.App"
            RuntimeVersion="2.0.30523.6"
            ExternalCallersFromCrossDomain="FullAccess">
    <Deployment.Parts>
        <AssemblyPart x:Name="BrowserFun"
                  Source="BrowserFun.dll" />
        <AssemblyPart x:Name="System.Windows.Controls.Extended"
                  Source="System.Windows.Controls.Extended.dll" />
    </Deployment.Parts>
</Deployment>
1 つ以上の Silverlight プラグインが含まれているページをブラウザで読み込むとき、ブラウザ プロセスごとに 1 つの CLR インスタンスを取得することになります。ただし、Silverlight プラグインの実行中の各インスタンスは、独自の AppDomain を取得します。プラグイン間で何も共有していない場合も通信は可能ですが、その場合、コードを通じてちょっとしたテクニックを使う必要があります。
このテクニックでは、実質的に、ブラウザの相互運用層を中継手段として使用します。このとき、あるプラグインがマネージ インターフェイスを公開し、別のプラグインがそのインターフェイスに接続してメソッドを呼び出します。
そのため、次に示すように、いずれかのプラグインがいくつかのスクリプト可能なメンバを定義します。
public partial class Page : UserControl
{
  public Page()
  {
    InitializeComponent();
    HtmlPage.RegisterScriptableObject(
        "Action", new ActionPageCommand());
  }
}
ActionPageCommand クラスには、プラグインが外部呼び出し元に対して公開するすべてのメソッドが含まれます。その例を次に示します。
[ScriptableType]
public class ActionPageCommand
{
  public int GetRandomNumber()
  {
     Random rnd = new Random();
     return rnd.Next(0, 100);
  }
}
プラグインのスクリプト可能なメンバは、ページの JavaScript コードに直接表示されます。図 7 に、公開されている Silverlight プラグイン インターフェイスの JavaScript ラッパーを示します。他のプラグインのインターフェイスに接続しているプラグインは、図の JavaScript ラッパーのインスタンスを作成できます。
ScriptObject so = HtmlPage.Window.CreateInstance("ActionBar");
object result = so.Invoke("invokeGetRandomNumber");
図 7 他の Silverlight プラグインへの公開
<script type="text/javascript">
var ActionBar = function()
{}

function invokeGetRandomNumber$Impl()
{
    var plugin2Services = $get("Silverlight2").content;
    var results = plugin2Services.Action.GetRandomNumber();
    return results;
}

ActionBar.prototype =
{
    invokeGetRandomNumber: invokeGetRandomNumber$Impl
}
</script>
もっと直接的な方法で Silverlight プラグインを別のプラグインから呼び出すことはできないでしょうか。もちろん可能です。次のコードを使用してください。
HtmlElement plugin = HtmlPage.Document.GetElementById("Silverlight2");
var content = (ScriptObject) plugin.GetProperty("content");
var action = (ScriptObject) content.GetProperty("Action");
action.Invoke("GetRandomNumber");
最終的な結果は同じですが、この方法の方が、Silverlight と基になるブラウザ間のラウンド トリップが多くなります。

まとめ
Silverlight 基本クラス ライブラリには、ブラウザおよびホスト ページに接続する機能と、情報を読み取り、DOM にアクセスする機能が用意されています。Silverlight 中心のアプリケーションをお持ちの場合は、ホスト HTML ページについて、それほど多くの作業を行う必要はないでしょう。ただし、ASP.NET AJAX と Silverlight が組み合わされたソリューションをお持ちの場合は、マネージ コードを JavaScript から実行するか、JavaScript オブジェクトをマネージ コード内から呼び出す必要があります。
ブラウザの相互運用層 (HTML ブリッジとも呼ばれる) には、Silverlight のマネージ環境と JavaScript 解釈が行われる環境との間で通信を実行するための機能が数多く含まれています。通信の際には、層の間で型とオブジェクトをマーシャリングする必要があります。抽象化の最上位レベルとして、パブリック API とドキュメントが用意されています。このドキュメントでは、JavaScript からマネージ コードを、またマネージ コードから JavaScript オブジェクトを呼び出せるように、把握しておく必要のある事柄が何点か説明されています。
特殊な問題を抱えている場合や、実装の詳細がかかわる部分をできるだけ詳しく調べる必要がある場合は、Wilco Bauwer 氏の有益なブログを定期的にチェックしてください。

ご意見やご感想は、cutting@microsoft.com まで英語でお送りください。

Dino Esposito は、IDesign 社のアーキテクトであり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。


セキュリティに関するブリーフィング
脅威モデルによりセキュリティ プロセスを強化する
Michael Howard
目次
脅威のモデル化
コード レビューとファジー テストの優先順位
ファジー テスト
攻撃対象領域の削減
特権を低く抑える
まとめ
このコラムでは、脅威モデルを使用して主にコード レビュー、ファジー テスト、攻撃対象領域分析タスクの優先順位を付けるために役立てることで、セキュリティ エンジニアリング プロセスを促進し、より全体論的な観点から安全な設計について検討する方法を紹介します。
このコラムの準備として、まずは SDL (セキュリティ開発ライフサイクル) のブログにある Jeremy Dallman の「'Crawling' Toward SDL (SDL をゆっくり開始する)」をお読みになることをお勧めします。このコラムで扱っている概念が Jeremy の考えと非常に一致していることにお気付きになるでしょう。そして、SDL を使ってゆっくり動き出し、歩き出すことから始めて、最終的に疾走できるようになるにつれ、セキュリティへの取り組みに秩序と正確さがもたらされるようになります。
ここで紹介するのは、脅威モデルを使用して、主にコード レビューとファジー テストの優先順位付けや攻撃対象領域の削減など、SDL の他のセキュリティ要件の促進を図ることです。必要な作業はこれだけです。では、そろそろ説明に移って、それぞれについて詳しく見ていきます。脅威のモデル化から始めましょう。

脅威のモデル化
MSDN Magazine の多くの読者は脅威のモデル化という概念をよく理解しています。Adam Shostack は 2008 年 7 月号の (「脅威のモデル化プロセスをよみがえらせる」) でこのテーマに関する優れた記事を書いており、脅威のモデル化プロセスの未来について SDL ブログで詳しく紹介しています。
これは筆者の考えですが、セキュリティの脆弱性について考えるとき、ほとんどの人は実装バグを思い浮かべるようです。SDL は実装バグをやや重視しすぎたのではないかという意見もあるようですが、これは従来マイクロソフトが訂正してきた脆弱性の大部分が実装バグに由来していたためです。ところが、ここ数年は安全な設計に重点を置くために多くのリソースを確保するようになってきており、これは SDL のおかげで実装バグが減ってきたためでもあります。
脅威のモデル化は、開発チームが構造化された手法で安全な設計について検討することを可能にするため、SDL の基礎となります。脅威のモデル化プロセスは、効率的に、以下のタスクに簡素化されます。
  • ソフトウェアのデータ フローを図示する。
  • "要素ごとの STRIDE" 手法を使用して、設計に該当する脅威を見つける。
  • 各脅威に対処する。
  • ソフトウェアのモデル化が十分に行われていること、各脅威を考慮していること、および検出されたすべての脅威に対処していることを確認する。
脅威モデルの中心的な要素は、アプリケーションのエントリ ポイントの描写です。脅威モデルでは、"図の描写" フェーズ中にエントリ ポイントを信頼できる境界として取り込みます。たとえば、ネットワーキング エントリ ポイントや、ファイルとレジストリのエントリ ポイントなどがあります。
優れた脅威モデルでは、ネットワーク アクセシビリティとインターフェイスの認証または承認要件についても取り込む必要があります。これには、IP アドレスを介したネットワーク アクセシビリティが含まれます (ローカルおよびリモート、ローカル サブネット、およびローカルのみのアクセス)。さらに、認証と承認のレベル、匿名アクセス、ユーザー アクセス、および管理者のみのアクセスも含まれます。Windows アクセス制御リスト (ACL) では承認レベルがより詳細に分類されますが、詳細になり過ぎる必要はありません (ACL の詳細な説明については、今月号の John Michener の記事「Windows のファイルとレジストリのアクセス許可について」をお読みください)。単純さを維持します。
脅威モデルで取り込まれるもう 1 つの重要なデータは、プロセス ID です。エントリ ポイントとは単にプロセスで実行されるコード部分へのインターフェイスであり、高い特権を持つプロセスは侵害された場合に非常に危険です。Windows の場合、最も特権が高いプロセスは SYSTEM または管理者として実行するプロセスです。Linux または Mac OS X の場合、ルートとして実行するプロセスが最も高い特権を持ちます。
アプリケーションの各エントリ ポイントでは、以下のデータを記録する必要があります。
  • エントリ ポイント名
  • ネットワーク アクセシビリティ
  • 認証と承認
  • プロセス名とプロセス ID
  • コメント (必要に応じて)
図 1 に、例をいくつか示します。最初のネットワーク ポートは、地球上のすべての人に公開されているため、最も公開度が高いということが明らかです。公開度が最も高いエントリ ポイントは、攻撃を受ける危険性が最も高いエントリ ポイントとなります。これは、リモート アクセスが可能であり、同じネットワーク (この場合はインターネット) 上のコンピュータを使うすべての人がアクセスできるためです。匿名ユーザーによるアクセスが可能であることは、認証または承認なしでアクセスできることを意味します。そのため、匿名ユーザーによるリモート アクセスが可能なエントリポイントには実際にだれもがアクセスすることができ、その中には悪意を持った多数の人々が含まれます。
図 1 プロセスのエントリ ポイント
エントリ ポイント | ネットワーク アクセシビリティ | 認証と承認 | プロセス | コメント
TCP/1234 | リモート | 匿名 | App.exe (SYSTEM) | メインの要求/応答
\\.\pipe\admin | リモート | 管理者 | App.exe (SYSTEM) | 管理の名前付きパイプ
ncacn_ip_tcp 609b954b-4fb6-11d1-9971-00c04fbbb345 | リモート | 認証済みユーザー | User.exe (Network Service) | ユーザーのアカウント情報 RPC インターフェイス
Config.xml | ローカル | 管理者 | App.exe (SYSTEM) | アプリケーション構成ファイル
管理の名前付きパイプにはリモート (およびローカル) 管理者のみアクセスすることができ、これは ACL を使用して適切に実施されます。名前付きパイプを含む Windows のすべての名前付きオブジェクトに対して ACL を検証することが重要です。
また、SYSTEM として実行するコード内に多数のエントリ ポイントがあることにもお気付きでしょう。認証されていないリモート アクセスが可能なエントリ ポイントと、SYSTEM として実行するプロセスの組み合わせは、Windows で可能な限り最も大きい公開ポイントとなります。このコード内にセキュリティ バグが存在していた場合、何者かがどこからでもコードを失敗させることができます。悪用されたコードは SYSTEM として実行するため、コンピュータが完全に乗っ取られます。これについては、後で詳しく述べます。
公開の程度に応じて、コード レビューとファジー テストに優先順位が付けられます。これについては、次で説明します。

コード レビューとファジー テストの優先順位
多くのコードがレビューとテストを必要としていることについて考えてみます。しかし、今後 10 年間起きている時間をすべてコードのレビューに費やすことはできず、ある時点で製品を顧客に納入する必要があります。このため、セキュリティの取り組みに優先順位を付けて、最も危険なコンポーネントから先に最も丹念に取り組む必要があります (最も危険なコンポーネントを探すためのヒントとして、「悪意のある攻撃者はだれか」のリンクを参照することをお勧めします)。したがって、最も危険なコードからレビューとファジー テストを開始します。
手動によるコードのレビューでは、最も公開度が高いコードから先にレビューすることが重要です。たとえば、図 2 に、TCP/1234 ソケットのコード例を示します。recv の呼び出しでは認証されていないリモート ネットワーク ポートからの読み取りを行っているため、検証が済むまで buf を有害なものとして扱う必要があります。セキュリティの分野では、このデータはたいてい汚染されていると見なされ、精度や構造に依存することはできません。
図 2 問題の可能性のあるバッファ
SOCKET  remoteSocket = accept(listenSocket,NULL, NULL);
if (remoteSocket == INVALID_SOCKET) {
  PRINTERROR("accept()");
  closesocket(listenSocket);
  return;
}

// Receive data from the client
char szBuf[256];
memset(buf, 0, sizeof(buf));
nRet = recv(remoteSocket,
      buf,  // Danger!
      sizeof(buf),
      0);

if (nRet == INVALID_SOCKET) {
  PRINTERROR("recv()");
  closesocket(listenSocket);
  closesocket(remoteSocket);
  return;
}

// process data in buf
コード レビューを少し簡単にするために、ソース コードの場所と、エントリ ポイントの API 呼び出しとデータが含まれている表を作成します (図 3 を参照)。これに基づき、エントリ ポイントの API から受信データを検証または浄化するコードまでデータを追跡します。データの整合性を検証するコードが存在せず、受信データに対して危険を伴う可能性のある操作を実行している場合は、良くてコーディング バグですが、最悪の場合は深刻な脆弱性が生じる場合があります。コードの動作にかかわらず、データの形式が適切であると想定してはなりません。
図 3 エントリ ポイントの分析
エントリ ポイント | ソース ファイル | API とデータ
TCP/1234 | Readfromsocket.cpp | recv(,buf,)
\\.\pipe\admin | AdminConsole.cpp | ReadFile(,bCommand)
ncacn_ip_tcp 609b954b-4fb6-11d1-9971-00c04fbbb345 | UserInfo_i.c and UserInfo.idl | さまざま
Config.xml | Config.cpp | ReadFile(,bFileContents)

ファジー テスト
最後の行の "データの形式が適切であると想定してはなりません" に戻って考えてみます。ファジー テストの目標は、この想定が可能であるかどうかを判断し、これが可能な場合は、こうした想定に違反する何者かがアプリケーションを失敗に導くことができるかどうかを判断することです。ファジー テストについては MSDN Magazine で何度も取り上げられて来たためここでは説明しませんが (たとえば、Dan Griffin の記事「ファジー テスト: チーム システム向けにカスタム テスト インターフェイス プロバイダを作成する」を参照してください)、ファジー テストをどのように使うことができるかについて説明します。
コード レビューと同様、すべての可能なコード パスでファジー テストを実施することはできないため、公開の程度に応じて優先順位を付ける必要があります。したがって、最も公開度が高いエントリ ポイントから先にファジー テストを実施します。ここに挙げた単純な例では、TCP/1234 エントリ ポイントから先に、何度も厳しくテストすることになります。実際、筆者は常にこのエントリ ポイントのファジー テストを実行しています。ともあれ、CPU 時間はかなり安価です。
ここでもう少し踏み込んで、なぜこのインターフェイスがこれほど広く公開されているのかを考えたいと思います。この公開を既定で絞り込むことは可能でしょうか。そこで、攻撃対象領域の削減という手法を使います。

攻撃対象領域の削減
これまでに何度も言いましたが、もう一度言わせてください。完璧に正しいコードを作成することは決してできないため、正しいコードを作成するよう努めると同時に、攻撃対象領域を削減することが重要です。この呪文については、2004 年 11 月号の筆者の記事「攻撃対象領域: 信頼されていないユーザーに公開されるコードの最小化によりセキュリティ リスクを緩和する」で詳しく述べています。この記事について簡単に説明します。顧客に被害が及ぶ可能性を減らすためには、攻撃対象領域を削減しなくてはなりません。結局、あらゆる攻撃者に公開されているインターフェイスはローカル サブネット上の攻撃者のみに公開されているインターフェイスに比べて危険度が高くなります。これは単に、後者のシナリオでは潜在的な攻撃者が少ないためです。
ここに挙げた例では、TCP/1234 エントリ ポイントでの認証を既定で行い、認証されたユーザーのみがアクセスできるようにします。これは基本認証 (問題のある認証方法であることはわかっています) または Kerberos などのより複雑な認証方法を使用して、簡単に行えます。これは既定の方法ですが、システム管理者は匿名アクセスに変更しようと思えば変更できます。管理者はこれについて決断する必要があります。
アプリケーションに認証を追加した場合は、認証プロセスと認証資格情報データベースがシステムに追加されるため、脅威モデルの更新が必要となることに注意してください。
悪意のある攻撃者はだれか
セキュリティ コード レビューの黄金律とは、悪意のある攻撃者が制御する場所を常に理解する、というものです。ここで、悪意のある攻撃者はだれか、という問題が生じます。悪意のある攻撃者がだれであるかは、エントリ ポイントの認証と承認のレベルによって決まります。
図 1 に示す最初の TCP ポートの例では、悪意のある攻撃者は地球上のまさにすべての人々です。2 番目のエントリ ポイントの名前付きパイプでは、ローカル管理者グループのメンバに相当するため、悪意のある攻撃者は実際には悪意のある攻撃者ではありません。管理者が信用できないとしたら、だれを信用すればよいでしょう。
時間が限られているとしたら、最も早急かつ綿密にコード レビューを行う必要があるのはどちらのインターフェイスでしょうか。だれもがアクセスできる最初のインターフェイスであるのは明らかです。そのコード内にセキュリティ バグがあった場合、あらゆるユーザーによってコードが侵害される可能性があります。この点をよく理解してもらうには、ワームを思い出してください。ワームは認証されていないリモート ネットワーキング インターフェイスに依存しています。

特権を低く抑える
攻撃対象領域を削減するためのもう 1 つの要素とは、アプリケーションの特権を低く抑えることです。これについて、次に説明します。SYSTEM として実行するプロセスを覚えているでしょうか。侵害がもたらす被害の可能性を減らすには、特権が低いアカウントの使用を検討する必要があります。これは、攻撃対象領域の削減におけるもう 1 つの重要な点です。
最も公開度が高いエントリ ポイントから開始します。この記事の例で、TCP/1234 の匿名アクセスを既定で許可するように決定した場合は、そのポートでリスンしているプロセスがなぜ SYSTEM として実行する必要があるのかを確かめる必要があります。アカウントの特権を低く抑えることは可能でしょうか。
また、2 つのネットワーク エントリ ポイントと 1 つのファイル エントリ ポイントがすべて同じプロセスで実行していることに注意してください。これは適切でしょうか。エントリ ポイントの 2 つは管理者用であり、1 つは匿名ユーザー用です。特権が低いプロセス ID を利用するために、このプロセスから 2 つの個別のプロセスへの分割を検討する必要があります。
IIS 6.0 および IIS 7.0 アーキテクチャを調べると、匿名要求およびリモート ユーザー要求を処理するプロセス (w3wp.exe) は Network Service として実行し、メインの管理プロセス (inetinfo.exe) は SYSTEM として実行していることに気付きます。後者のプロセスは管理者用に制限されています。IIS 4.0 および IIS 5.0 では、たいていの場合、プロセス処理と管理アクセスは特権の高い 1 つのプロセスによって実行されていました。
興味深いことに、攻撃対象領域の削減のいくつかの要素は従来的に知られているものです。それらは伝統的な Saltzer と Schroeder による安全な設計原則の一部であり、最小特権および最小共通性のメカニズムが含まれています。
最小共通性のメカニズムという 2 番目の概念については、少し説明が必要です。これは、複数のユーザーまたはユーザー タイプで共有されるコードの量を削減する必要があることを意味します。この記事の例では、管理タスクと匿名ユーザー要求を実行している単一プロセスで危険が生じる可能性があります。この対策としては、IIS の動作に従い、単独の ID として個別のタスクを実行する 2 つのプロセスにアプリケーションを分割します。

まとめ
正直に言うと、このコラムで定義している新しい手法や概念はありません。すべては SDL に含まれています。しかし、このコラムでは、SDL で定義、説明、および要求されているセキュリティの多くの規律の間にある暗黙的な関係について説明し、顧客に納入する製品の安全性をより高めるために必要な多くのセキュリティの取り組みが、優れた脅威モデルによってどのように簡素化されるかについて示しています。
優れた脅威モデルを作成し、公開する場所を決定し、公開度で順位付けされたエントリ ポイントの一覧を使用して、コード レビューとファジー テストに優先順位を付け、最終的に攻撃対象領域を削減するために役立ててください。これは、ソフトウェアの安全性の向上につながります。
セキュリティについてのあなたの理解度を試してみませんか。今月号の興味深いセキュリティ IQ のテストに挑戦してください。

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

Michael Howard は、マイクロソフトのプリンシパル セキュリティ プログラム マネージャとして、セキュリティを向上させるためのプロセスの改善とベスト プラクティスに取り組んでいます。また、『Writing Secure Code for Windows Vista』、『The Security Development Lifecycle』、『Writing Secure Code』、『19 Deadly Sins of Software Security』など、セキュリティに関する多数の書籍を共同執筆しています。


テストの実行
ソフトウェア テストにおける集団意思決定
Dr. James McCaffrey
コードのダウンロード: MSDN Code Gallery (153 KB)
オンラインでのコードの参照
目次
純粋な多数決方式と決選投票付き多数決方式
ボルダ方式
コンドルセ方式とシュルツ方式
コードにおけるシュルツ方式
各方式の補足事項
1 人の意見や純粋な数量的手法のみで結論を出す代わりに集団の意見を集約して意思決定を行う場合、複数のオプションから最良の 1 つを決定する際に生じる一般的な問題について考えてみましょう。このような意思決定の例としては、州知事選挙で州の居住者全体が知事候補者リストから投票により 1 名を選出する場合や、大企業がより小さい企業を買収する際に取締役会が数社の候補から 1 社を決定する場合などが挙げられます。基本にある考え方は、選ぶ側の集団が何らかの方法で選択対象オプションの順位付けを行い、いずれが最良であるかを決定するということです。
ソフトウェアの開発およびテストでも、集団意思決定によって最良のオプションを選択できます。たとえば、ベータ ユーザーのグループがいくつかのプロトタイプから最良のユーザー インターフェイスを選ぶ場合や、オープン ソース プロジェクトのメンバが投票によってポリシーを選ぶ場合などが考えられます。
私のこれまでの調査によると、ソフトウェア テスト コミュニティにおいては、最良のオプションを選択する集団意思決定の実用的な手法とそのプロセスの潜在的な問題はあまり知られていないようです。集団分析手法には多数の方式があります。今月のコラムでは、集団意思決定で最良のオプションを選択するための最も一般的な手法の中から、ソフトウェア テスト環境に適用できる 5 種類の方式について説明します。それらは、純粋な多数決方式、決選投票付き多数決方式、ボルダ方式、コンドルセ方式、およびシュルツ方式です。
これらの分析手法はいずれも、ソフトウェア エンジニアリング以外の領域ではよく知られており、広範な研究がなされてきました。このコラムでは、ソフトウェア テストへの適用に重点を置きながら各方式について説明します。集団分析手法の知識を皆さんのツールキットに役立てていただければさいわいです。
集団として複数のオプションから 1 つだけを選択するという状況は、さまざまな問題領域で発生します。各問題領域では、それぞれ固有の用語が使用されることが多いようです。社会学では、この種の問題の研究は一般的に社会的選択理論と呼ばれ、"オプション" や "評価者" という言葉がよく使用されます。政治学では、このような問題の分析を投票理論と呼ぶことが一般的であり、"候補者" や "投票者" という言葉が使用されます。数学では、最良のオプションを 1 つ選ぶ集団意思決定は、しばしば意思決定の分枝の理論として扱われます。ソフトウェア開発およびテスト環境には集団分析手法に関する固有の用語は存在しないため、私は各領域の用語を適宜取り入れて説明しようと思います。
このコラムで説明する手法は、非常に狭い範囲の問題を対象とするものであり、最良の選択が明確に定義されるような数量的なシナリオは適用外です。後ほど説明するように、"集団が最良の選択を行う" ときの "最良" が意味する内容は使用された手法に即して定義されます。また、議論と交渉を通じて集団の合意を形成することで意思決定が行われるようなコラボレーションのシナリオにおいても、これから紹介する手法は適用できません。

純粋な多数決方式と決選投票付き多数決方式
最初に、最良のオプションを選択する集団意思決定の最も単純で最も一般的な手法から説明します。ただし、テストへの適用の多くの場面で、この手法は最良のアプローチでないことをお断りしておきます。例として、社内で使用するエンタープライズ ソフトウェア アプリケーションを開発中で、4 種類のまったく異なるユーザー インターフェイス プロトタイプを作成する場合を考えましょう。複数のプロトタイプの評価を 1 人に依頼する場合もありますが、多くの状況でより適切なアプローチと言えるのは、複数の人々が構成する集団によって 4 つの異なるデザインの順位を決定してもらう方法です。
集団による評価を行って結果を分析するプロセスは、皆さんの想像以上に慎重を要する作業です。想定される最も単純な集団評価手法は、評価者に選択肢を提示して選択してもらう、つまり投票によって 1 つを選ぶ方式です。最も高い得票を得たオプションが勝者となります。これは、一般的に純粋な多数決方式と呼ばれる手法です。
具体的な例を示します。4 つの異なるプロトタイプ (仮に A、B、C、D と呼びましょう) を 10 人の評価者に提示し、それぞれに最良のプロトタイプを選んでもらったところ、次のような結果が得られました。プロトタイプ A は 4 人の評価者により最良と評価され、プロトタイプ B は 3 人、プロトタイプ C は 2 人、プロトタイプ D は 1 人により最良と評価されました。したがって、開発を進める対象はプロトタイプ A に決定です。
これにはいくつか問題があります。それぞれの評価者が 4 つのプロトタイプを評価するときに、暗黙的または明示的に図 1 のような順位付けを行ったとします。この図では、列ごとに各評価者によるプロトタイプの順位付けが示されています。
図 1 10 人の評価者によるプロトタイプの順位付け (クリックすると拡大画像が表示されます)
純粋な多数決方式によりプロトタイプ A が選択されましたが、評価集団の 60% がこのプロトタイプを最下位としています。この状況で、プロトタイプ A を残りの各プロトタイプと比較してみましょう。プロトタイプ A の順位が他のプロトタイプよりも上になっている割合は 40% で、残りの 60% では他のプロトタイプに負けています。もう 1 つ、純粋な多数決方式で避けて通れない問題は、選ばれなかった意見が最終結果に直接的に反映されないということです。
純粋な多数決方式の問題を解決するために、一般に "決選投票付き多数決" と呼ばれる集団意思決定手法を使用できます。この手法にはいくつかバリエーションがありますが、勝者になるにはすべての投票の 5 割以上を獲得して 1 位になる必要があります。
そのためには、投票を複数回行うか、または各評価者による順位付けを伴った投票を 1 回だけ行います。たとえば、評価者が最良のプロトタイプ デザインに投票した結果が図 1 のようになった場合、最初の投票ではどのプロトタイプも過半数を獲得していません。そこで、リストに含まれる候補の数を減らします。通常は上位の 2 つ、この例ではプロトタイプ A (4 票獲得) とプロトタイプ B (3 票獲得) に候補を絞ります。
続いて 2 回目の投票を実施します。図 1 の順位付けデータに見られる選好の傾向が 2 回目の投票に反映されたとします。その場合、1 回目でプロトタイプ C および D を選択していた評価者がプロトタイプ B に投票するため、結果としてプロトタイプ B が 6 票を獲得し、プロトタイプ A は 4 票のままです。
私の経験では、ソフトウェア開発およびテスト環境で複数回の投票を実施することは、1 回だけの投票に比べると重要なデメリットがいくつかあります。1 つには、一部のソフトウェア開発シナリオでは複数回の投票が単純に現実的ではないということです。また、必然的に人とのかかわりを伴うため、複数回の投票は時間がかかりすぎることも懸念されます。さらに悪いことには、自らの選んだオプションが何度も除外され (毎回、自分の意思が反映されない)、結果的に自分が好まないオプションを選択せざるを得ないという集団が存在する可能性があることです。結果から不利益を被る可能性のある利害関係者が投票する場合、物理的な決選投票方式のプロセスを経て結局負けてしまった場合、その人々は投票に参加した意味がないように感じてしまうかもしれません。
各評価者が選択肢から 1 つだけを選択する投票を実際に複数回実施する代わりに、単純に、1 回の投票で決めることもできます。ただし、各評価者にはすべての候補に順位を付けてもらいます。その結果に基づいて仮想の複数回投票を行い、投票者の選好を結果に反映することができます。
すべての候補の順位付けが必須となるため、問題点もあります。オプションの数が多い場合 (通常では 8 を超える場合)、この方式はあまり実用的ではありません。オプションが増えると順位付けが難しくなるからです。また、ある調査によると、評価者は自らが順位付けの下位に並べたオプションに対しては評価作業がぞんざいになる傾向があるそうです。さらに、投票を物理的に複数回行った場合、最初の投票で各評価者が行った全候補の順位付けが、候補が絞られた次の回の投票時にそのまま反映されるとは限りません。
シュルツ方式: 長所と短所
シュルツ方式の長所と短所について説明します。特定のシナリオにおいて問題となる短所として、シュルツ方式が直観的ではないことが挙げられます。評価者が利害関係者である場合、シュルツ方式は、予想もしない結果をもたらす不可解なブラック ボックスに見えてしまいがちです。
シュルツ方式の主な利点は、この方式が広範に分析されており、投票システムに望まれる数多くの基準を満たしていることが証明済みである点です (ボルダ方式では、1 位争いに無関係なオプションを取り除いたときに結果に影響が出ることを思い出してください)。
多数の投票システム基準があります。たとえば、単調性基準です。その原則をわかりやすく定義すると、ある勝者オプションに対して 1 人以上の評価者がその順位付けを上げることにより、そのオプションは決して非勝者にはならないということです。また、同種の選択対象からの独立性基準では、既存のオプションと同種のオプションを新しく追加しても分析の結果が変わらないようにすることがその主旨となっています。多数の投票システムの基準が提案されており、シュルツ方式はこれらの基準の大半を満たしていることがわかっています。
ただし、興味深い例外もあります。シュルツ方式は参加基準と呼ばれるものに違反しています。非公式な見解ですが、他のオプションよりも現在の勝者を好ましいとする新しい投票者グループを既存のシステムに追加した場合に、現在の勝者ではないオプションの方がシュルツ方式の勝者となる可能性があります。
その短所にもかかわらず、シュルツ方式は一般に非常に効果的です。特に、教養のある (基になるシュルツ方式に関する知識を持っている) 大きな集団が最終結果の利害関係者であり、その集団が (製品の選択ではなく) ポリシーを選択する場面に適した手法です。たとえば、いくつかのオープン ソース グループでは、一般的なポリシーの意思決定を行う場合や役員を選出する場合にシュルツ方式が使用されています。ソフトウェア テスト シナリオにおいて、私が最初に選択する手法はシュルツ方式です。そして、その結果を他の手法の結果分析の基準として使用します。

ボルダ方式
純粋な多数決方式および決選投票付き多数決方式の問題を解決するために編み出された 2 つの集団分析手法があります。現在非常によく使われているボルダ方式とコンドルセの対比較方式です。これらは、1700 年代にそれぞれの手法を提案した 2 人の数学者の名前をとって命名されました。
ボルダ方式は、非常に単純な手法であり、皆さんにも親しみ深いものだと思います。オプションの数を n 個と仮定します。各評価者がオプションすべてに順位を付けます (高い評価を 1 位として最下位まで順位付けします)。1 位のオプションには n-1 ポイントの値が割り当てられます。2 位には n-2 ポイントが割り当てられ、その後も順に同様の割合でポイントが割り当てられます。最下位のオプションは 0 ポイントになります。ボルダ方式では、単純に各オプションのポイント値を合計します。
わかりやすくするために簡単な例を紹介しましょう。A、B、C、D という 4 つのオプションがあるとします。この場合、1 位は 3 ポイントです。2 位は 2 ポイント、3 位は 1 ポイント、そして最下位の 4 位は 0 ポイントです。図 2 に順位付けの結果と、その下にはオプション A が最上位となっている場合の 7 人の評価者のボルダ方式による得点を示します。
図 2 結果と 7 人の評価者のボルダ方式による得点 (クリックすると拡大画像が表示されます)
ボルダ方式は、多くの問題領域、特にスポーツ競技で広く使用されています。ソフトウェア開発およびテスト環境でボルダ方式を使用した場合の利点は、この手法が直観的に理解できるものであること、そして、すべての評価者の意見が結果に反映されることです。
ただし、ボルダ方式にもいくつか短所があります。明らかに敗者であるオプション D を候補から外した場合を考えてみましょう。常識的に考えると、1 位争いに無関係なオプションを削除しても最終結果に影響を及ぼすことはありません。ところが、図 3 に示されているように、オプション D を削除した結果、オプション A に代わってオプション B が 1 位になっています。つまり、このデータ例で試したところ、1 位争いに無関係なオプションを削除しても結果に影響が出るということです。ボルダ方式においては、1 位のオプションが最下位に転落するケースもあり得ます。この効果は、勝者が敗者になるボルダ方式のパラドックスと呼ばれることもあります。
図 3 オプション D を削除した後の結果とボルダ方式による得点 (クリックすると拡大画像が表示されます)
その他のボルダ方式の問題点として、相互に密接に関連する 2 つの問題があります。1 つ目は、対比較の問題です。ボルダ方式を使用しているときに、勝者を残りの各オプションと比較した場合に、いずれに対しても敗者になってしまうという状況が起こる場合があります。
たとえば、オプションが 4 つあり、評価者が 12 人いるとします (図 4 を参照)。ボルダ方式を使用すると、オプション A は 25 ポイントの得点で 1 位になります。ただし、残りのオプションとそれぞれ比較してみると、オプション B が他のすべてのオプションよりも優位に立っていることがわかります。各評価者が与えた順位の優劣に基づいてオプションのペアを比較すると、B 対 A は 7 対 5 で B が勝利、B 対 C も 7 対 5 で B が勝利、B 対 D も 7 対 5 で B が勝利となります。
図 4 オプション B は直接対決の勝者 (画像をクリックすると拡大表示されます)
ボルダ方式のもう 1 つの問題は、評価者の操作による影響を受けやすいことです。理論上は、既存のオプションと非常に似ている (評価者の選好に関連する要素が類似している) 偽のオプションを追加することで、ボルダ方式の結果に影響を与えることができます。類似オプションの間で票を奪い合うような状況が起こると、結果的に分析の勝者が変わる可能性があります。このコラムの最後に紹介する参考資料には、この影響の例が含まれています。
ボルダ方式には技術的な短所もありますが、この手法は非常に実用的です。ソフトウェア テスト環境では、小さな集団によってポリシーを決定する場合に向いているようです。私の経験から言えることは、一般的に、ボルダ方式は主観が公平に反映される手法として認識されており、利害関係者による投票で負けたオプションへの投票者も、選ばれたオプションに反感を持つことなく、たいていは結果を受け入れるようです。
ここで述べた話の教訓は、ボルダ方式を使用する場合、1 位争いに無関係なオプションを削除した場合の影響、ボルダ方式の勝者と敗者の対比較の結果、中間プロセスでの新しいオプションの追加に対して、慎重に対応し、データを確認する必要があるということです。

コンドルセ方式とシュルツ方式
コンドルセ方式は、主にボルダ方式の直接対決問題への対応として開発された手法であり、他の方式と組み合わせることによって複数のオプションから最良の 1 つを決定することができます。コンドルセ方式は非常に単純です。評価者はすべてのオプションに順位を付ける必要があります。その後で、可能な組み合わせのペアでオプションを比較します。
1 つのオプションが他のオプションとの直接対決にすべて勝利すると、その 1 つのオプションがコンドルセの勝者となります。コンドルセの勝者がいない場合は、別の同点決戦方式を使用します。同点決戦方式を別の方式に置き換えてもよいでしょう。コンドルセ方式のポイントは、直接対決すべてに勝利を収めた勝者は自動的に、他の基準で選択されたオプションに対しても勝者になるということです。
実際には、コンドルセ方式自体が集団評価手法として使用される機会は多くありません。その代わりに、純粋な多数決やボルダ方式などの評価手法を数学的に分析し、あらゆるケースでコンドルセの原則を満たすかどうかを確認します。つまり、「コンドルセ方式を使用して各オプションを評価する」という言い方に代えて、「コンドルセの原則を満たす XYZ 評価方式を使用して評価する」という表現にすると明快ですね。
コンドルセの原則を満たす最も興味深いシステムの 1 つを紹介します。これには、シュルツ方式、clone-proof Schwartz sequential dropping (同種の選択対象に影響されないシュルツ式敗者順次除外) 方式、beat-path (選好強度に基づく決定) 方式など、さまざまな名前が付けられています。ここではシュルツ方式と呼ぶことにします。
シュルツ方式は、これまでに紹介した他の手法よりもさらに複雑です。あるオプションが他のすべてのオプションよりも優位かどうかを確認するという点において、シュルツ方式はコンドルセ方式と似ています。ただし、コンドルセ方式のように未加工の順位付けデータを直接利用するのではなく、シュルツ方式では、オプションの優劣を表す間接的な測定値で 1 対 1 の比較を実行します。この間接的な測定値は、選好強度として表します。
それでは、例を見ていきましょう。図 5 をご覧ください。これは、シュルツ方式を実行している簡単なデモです。最初の部分は、入力例を示しています。4 つのオプション (A、B、C、D) と 11 人の評価者です。最初の行は、3 人の評価者がオプション A を B よりも上位に順位付けしていることを示しています。B は C よりも上で、C は D よりも上です。シュルツ方式では、評価者がオプションの順位付けにおいて同じ順位を割り当てることができます。また、まったく順位付けをしないという評価も可能です。
図 5 シュルツ方式のデモンストレーション (クリックすると拡大画像が表示されます)
出力の次の部分は、対比較の勝利の行列を示しています。勝利の行列は、元の順位付けデータから直接的に計算されます。
行列の最初の行は、6 人の投票者がオプション A をオプション B よりも上位に順位付けし、3 人の投票者が A を C よりも上位に順位付けし、4 人の投票者が A を D よりも上位に順位付けしていることを示しています。行列の 2 行目は、5 人の投票者がオプション B をオプション A よりも上位に順位付けし、7 人の投票者が B を C よりも上位に順位付けし、8 人の投票者が B を D よりも上位に順位付けしていることを示しています。3 行目と 4 行目は、オプション C と D に対して、それぞれ同じように順位付けの結果を示しています。対比較の勝利の行列では、主要項の値は 0 です。これらの値は各オプションのそれ自身との比較を示しているからです。
この時点で、コンドルセ方式では、この直接的な対比較のデータを確認し、すべての直接対決に勝ったオプションがあるかどうかを調べますが、シュルツ方式ではオプション間の優劣を間接的に測定します。出力の次の部分は、各オプションの選好強度です。これは、シュルツ方式の基になる主要な考え方であり、非常に創意に富んだ概念です。この行列の 3 行目は、オプション C の各オプションに対する選好強度を示しています。オプション A に対する選好強度は 8、B に対する選好強度は 4、D に対する選好強度は 10 です。
選好強度の考え方をよりよく理解していただくために、図 6 のグラフを使用して説明しましょう。最初に示されているのは、未加工の順位付けデータに基づいた対比較の勝利の行列です。次に、対比較の勝利のデータを有向グラフとして概念化したものを示しています。
図 6 行列形式とグラフ形式による対比較の勝利のデータ (クリックすると拡大画像が表示されます)
ノード A からノード B への矢印は、6 人の評価者がオプション A を B よりも上位に順位付けしていることを示しています。ノード B からノード A への矢印は記入しませんでした。なぜなら、5 人の評価者だけがオプション B を上位に順位付けしているからです。この結果から、A が B よりも優位であることがわかります。この例では、すべての矢印が単一の方向に向いていますが、2 つのノードが同じ数の評価者から上位に順位付けされている場合は、両方向の矢印を記入することができます。
ここで、ノード A とノード D の間の選好強度を考えてみましょう。A は B に 6 ポイントの差で勝利し、B は D に 8 ポイント勝っています。私たちは、A と B の間の選好強度全体を表すために、これら 2 つの小さい方の値 6 を選びました。この考え方は、中間ノードを持つ 2 つのノード間の選好強度全体を最もよく表しているのは、最も小さい直接的なノード間の選好強度であるということです。A と D を別の順序で比べてみましょう。A が B に 6 ポイント差で勝利し、B は C に 7 ポイント差で勝利し、C は D に 10 ポイント差で勝利しています。最も小さい値は 6 です。つまり、同じ結果が得られます。
結局、ノードのペアそれぞれの選好強度を計算する場合は、後で説明する洗練されたフロイド-ワーシャル法のバリエーションによって実行できます。標準のフロイド-ワーシャル法では、重み付きの有向グラフで最短経路を発見します。選好強度の計算が完了すると、シュルツ方式の勝者を決めることができます。他のオプションの対応する選好強度すべてに対して同等以上の選好強度を持つオプションがあれば、そのオプションが勝者です。
図 7 は、図 6 のデータに基づいた選好強度の行列であり、オプション B がシュルツ方式の勝者であることを表しています。この図の矢印に注目してください。行列の主要項をまたいで対応している値を比較した場合に、そのすべてにおいて比較相手と同じか、またはそれ以上の値を持つ行があれば、その行のオプションがシュルツ方式の勝者です。
図 7 シュルツ方式の勝者を判断するために使用される選好強度のデータ
この図では、オプション B が勝者です。なぜなら、オプション B の A に対する選好強度が 7 (A の B に対する選好強度は 6) であり、オプション B の C に対する選好強度は 7 (C の B に対する選好強度は 6)、およびオプション B の D に対する選好強度は 8 (D の B に対する選好強度は 6) であるためです。なお、オプション D はシュルツ方式の勝者ではありません。オプション D は A には勝利していますが (7 対 6)、C には負けているからです (6 対 10)。

コードにおけるシュルツ方式
この例では、シュルツ方式の勝者は 1 つのオプションのみですが、シュルツ方式では勝者が複数のオプションであることも可能です。そのような状況では、何らかの同点決戦方式を使用する必要があります。シュルツ方式を説明するために、図 5 で出力を生成するプログラムの一部のコードを見てみましょう。
プログラムでは、最初に、オプションを表す列挙型および実際のオプションの数を保持するグローバルな値 N を設定します。
enum options { A, B, C, D, E, F, G, H, I, J };
   static int N; // actual number of options
次に、デモ プログラムにより問題入力を文字列配列として設定します。
string[] rawData = new string[] {
           "A>B>C>D 3",
           "B>C>D>A 4",
           "C>D>A>B 2",
           "D>C>B>A 1",
           "C>A>B>D 1"
                          };
単純化するために、私のデモ プログラムでは引き分けは許容されないものと仮定し、すべてのオプションが順位付けされていることが前提となっています。そうすることにより、未加工のデータ文字列を非常に簡単に解析できます。完全なデモ プログラムは、このコラムに付属のコード サンプルに含まれています。あまり面倒な手順を行わなくても、コードを変更して引き分けオプションおよび除外オプションを許容することができます。
次に、私のデモ プログラムは rawData 文字列配列をすべてスキャンし、オプションの数および評価者の数を判断して出力します。その後で、プログラムは入力配列を再度スキャンし、MakeDefeats という静的なヘルパ メソッドを使用して対比較の勝敗データの行列を作成します。
int[,] defeats = MakeDefeats(rawData);
Console.WriteLine("\n== Pairwise defeats data is: \n");
Print(defeats);
勝利の行列は入力として MakePathStrengths という名前の別のヘルパ メソッドに渡され、このメソッドで選好強度の行列が作成されます。
int[,] strengths = MakePathStrengths(defeats);
Console.WriteLine("\n== Path strength data is: \n");
Print(strengths);
MakePathStrengths メソッド内で、まずは他のすべてのオプションに負けているオプションを除外します。
int[,] result = new int[N, N];
for (int i = 0; i < N; ++i)
{
  for (int j = 0; j < N; ++j)
  {
    if (d[i, j] > d[j, i])
      result[i, j] = d[i, j];
    else
      result[i, j] = 0;
  }
}
ステートメント result[i, j] = 0; が技術上は必須ではないことに注意してください。なぜなら、C# が自動的に int 配列を初期化してすべて 0 値にするからです。
次に、フロイド-ワーシャル法のバリエーションを使用して、可能な経路の選好強度をそれぞれ計算しようと思います。
for (int k = 0; k < N; ++k) {
  for (int i = 0; i < N; ++i) {
    if (k == i) continue;
    for (int j = 0; j < N; ++j) {
      if (k == j || i == j) continue;
      result[i, j] =
        Math.Max(result[i, j], Math.Min(result[i, k], result[k, j]));
    } //j
  } //i
} //k
さらに先へと進めてこのコードの手動トレースを行うと、先に私が説明したように、ノード [i] とノード [j] の間に存在する選好強度の決定がどのように実装されているかがわかります。コード内の Max メソッドの動作により、外側のループにおけるそれぞれの反復処理において、より大きい値の選好強度が検出された場合のみ結果の行列が更新されるようにします。コード内の Min メソッドは、ノード間の直接的な比較における最小の選好強度によって 2 つのノード間の選好強度全体が表されるという考え方に対応しています。
すべての選好強度が算出された後で、その情報を使用して、シュルツ方式の勝者を表す配列を計算できます。その配列で反復処理を行い、各勝者に対応する列挙値を表示します。
bool[] winners = MakeWinners(strengths);
Console.WriteLine("\n== Best option(s) is: \n");
for (int i = 0; i < winners.Length; ++i) {
  if (winners[i] == true)
    Console.Write(Enum.GetName(typeof(options), i) + " ");
}
MakeWinners メソッドは、1 次元の Boolean 配列です。値 true は勝者を示し、値 false は敗者を示しています。私は各オプションが勝者であることを前提にして、選好強度行列で反復処理を行い、インデックス [i,j] の選好強度の値をインデックス [j,i] の値と比較し、そのオプションが実際に非勝者であるかどうかを確認します。
static int[] MakeWinners(int[,] ps) {
  bool[] result = new bool[N];

  for (int i = 0; i < N; ++i)
    result[i] = true;

  for (int i = 0; i < N; ++i)   {
    for (int j = 0; j < N; ++j)
      if (ps[i, j] < ps[j, i])
        result[i] = false;
  }
  return result;
}

各方式の補足事項
集団の評価手法を使用する場合に考慮する必要のある主要な要素は、オプションの数、評価者の数、選択対象 (ポリシーの決定または製品の決定のどちらであるか)、評価者が分析の最終結果によって受ける影響の大きさなどです。通常は、可能な限り複数の集団分析手法を使用し、複数の結果に基づいて分析します (複数の手法で同じ結論が導き出された場合は、結果の信頼性について一定の尺度が得られたことになります)。
このコラムで説明してきた手法が適用される場面というのは、ほとんどの場合、かなり主観的な選択を行う状況であると考えられます。集団意思決定の結果分析で大きな役割を果たすのは、皆さんの経験と洞察力です。
一般的に純粋な多数決方式が効果的なのは、評価者が利害関係者ではない場合や、すべてのオプションの順位付けが現実的な作業ではない場合に限られます。決選投票付き多数決方式では、評価者の最大 49% の意見が無視される可能性があります。私の経験から、通常、この方式は主に他の方式を補完するものであると言えます。
ボルダ方式には短所もありますが、この手法は非常に効果的であり、特に評価者が分析の利害関係者である場合に適しています。コンドルセ方式は、その考え方が非常にシンプルであるため、他の方式の結果を評価する場合によく使用されます。シュルツ方式は、多くのソフトウェア テスト環境で使える非常に優れた汎用的な手法ですが、結果によって影響を受ける評価者の集団が比較的小さい場合や、評価者の集団がシュルツ方式のアルゴリズムを理解していない場合には適していません。
このコラムでは、便利で興味深い手法について概要のみを説明しました。複数のオプションから最良のものを 1 つ選択する集団意思決定の分析手法については、幅広い研究が行われてきました。参考文献としては、Wulf Gaertner 氏による『A Primer in Social Choice Theory』(Oxford University Press、2006 年) および Hannu Nurmi 氏による『Comparing Voting Systems』(Springer-Verlag、1987 年) をお勧めします。
最良のオプションを選択する集団分析の課題と、その関連問題である、複数属性に基づくオプションの順位付けの課題とを比較してみることに興味はおありでしょうか。興味を持たれた皆さんに、複数属性に基づく意思決定の 2 つの手法を説明したトピックをお勧めします。2005 年 6 月 (msdn.microsoft.com/magazine/cc163785) と 2006 年 10 月 (msdn.microsoft.com/magazine/cc300812) に掲載した「テスト ラン」のコラムを参照してください

ご意見やご質問は、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 です。


Wicked Code
Silverlight 2 変換およびクリッピング領域
Jeff Prosise

このコラムは、Silverlight 2 のプレリリース バージョンに基づいています。ここに記載されているすべての情報は、変更される場合があります。

コードのダウンロード: MSDN Code Gallery (51,861 KB)
オンラインでのコードの参照
目次
虫眼鏡で見る
回転する (3D)
ライブ ビデオを簡単にオーバーレイする
Silverlight は、機能性と浸透性が高い対話型のブラウザベース アプリケーションを作成するためのマイクロソフトの画期的なプラットフォームです。Silverlight 2 は、この記事の執筆時点ではベータ版ですが、この記事が発行されるまでにはリリースに近いものになっている予定です。Silverlight 2 では、マルチスレッド、ネットワーキング、ブラウザ統合、分離ストレージ、厳密な型指定、リフレクションなどがサポートされます。しかし、Silverlight はその魅力的なグラフィックスが最もよく知られています。
Silverlight 2 は、ベクタベースの XAML レンダリング エンジンをブラウザベース バージョンの CRL および Microsoft .NET Framework 基本クラス ライブラリと組み合わせています。XAML の実用的な知識を備えた開発者およびデザイナは、すばらしい機能を実現できます。Silverlight アプリケーションに搭載された多くの目を見張るビジュアル効果の鍵は、変換およびクリッピング領域です。たとえば、2008 年 5 月号の「Wicked Code」コラム (「Silverlight のページめくりを簡単に」) で示したページめくりフレームワークは、変換およびクリッピング領域に大きく依存して、本や雑誌をめくるのと同様のページめくり効果をブラウザに作成しています。
これから説明する変換とクリッピングの助けを少々借りて、より優れた手法を実現できます。ただし最初に、ここで示すサンプルは、Silverlight 2 ベータ 2 で構築およびテストしたため、Silverlight 2 RC および RTM リリースで作業するには修正が必要な可能性があることに注意してください。

虫眼鏡で見る
Windows Presentation Foundation (WPF) プログラマは、VisualBrush を使用して図 1 に示すような仮想虫眼鏡を作成することがあります。Silverlight では、VisualBrush がサポートされません。このため、一部の開発者は、Silverlight アプリケーションで同様の効果を作成することが不可能であると信じ、公言さえしています。
図 1 実行中の Silverlight Magnifier (クリックすると拡大画像が表示されます)
さいわいにも、Silverlight では虫眼鏡をシミュレートできます。そのために必要なのは、変換およびクリッピング領域に若干工夫を凝らすことだけです。ここで示す Magnifier アプリケーションで、その方法を実証します。図 2 に、Magnifier の Page.xaml ファイルの簡略バージョンを示します。それぞれ図 1 に示すコンテンツのコピーを含んだ 2 つのほとんど同一のキャンバスを宣言します。最初のキャンバス (MainCanvas) は、通常表示されるキャンバスです。2 つ目のキャンバス (ZoomCanvas) には、最初は同じコンテンツが含まれていますが、4 の倍数ですべてを拡大する ScaleTransform も含まれています。
図 2 Page.xaml
<UserControl x:Class="Magnifier.Page"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid x:Name="LayoutRoot" Background="Black"
    MouseLeftButtonDown="OnMouseLeftButtonDown" MouseMove="OnMouseMove"
    MouseLeftButtonUp="OnMouseLeftButtonUp">
    <Canvas x:Name="RootCanvas" Width="800" Height="800">

      <!-- Main canvas -->
      <Canvas x:Name="MainCanvas" Canvas.Left="0" Canvas.Top="0"
        Width="800" Height="900" Background="Black">
        <Canvas Canvas.Left="90" Canvas.Top="30" Width="620" Height="470">
          <Rectangle Canvas.Left="0" Canvas.Top="0" Width="620"
            Height="470" Fill="White" />
          <Image Canvas.Left="10" Canvas.Top="10" Width="600" Height="450"
            Source="Images/BobCat.jpg" />
        </Canvas>
        <Canvas Canvas.Left="90" Canvas.Top="540">
          <Line Canvas.Left="0" Canvas.Top="0" X1="0" Y1="0" X2="620"
            Y2="0" Stroke="#808080" StrokeThickness="3"
            StrokeDashArray="1,1" />
          <TextBlock Canvas.Left="0" Canvas.Top="10" Foreground="White"
            FontSize="36" Text="BVM BobCat" />
          <TextBlock Canvas.Left="0" Canvas.Top="70" Foreground="White"
            FontSize="12" TextWrapping="Wrap" Width="620" Text="..." />
          <Line Canvas.Left="0" Canvas.Top="180" X1="0" Y1="0" X2="620"
            Y2="0" Stroke="#808080" StrokeThickness="3"
            StrokeDashArray="1,1" />
        </Canvas>
      </Canvas>

      <!-- Zoom canvas -->
      <Canvas x:Name="ZoomCanvas" Canvas.Left="0" Canvas.Top="0"
        Width="800" Height="900" Background="Black" Visibility="Collapsed">
        <Canvas.RenderTransform>
          <ScaleTransform CenterX="0" CenterY="0" ScaleX="4" ScaleY="4"/>
        </Canvas.RenderTransform>
        <Canvas.Clip>
          <EllipseGeometry x:Name="Lens" Center="0,0"
            RadiusX="40" RadiusY="40" />
        </Canvas.Clip>
          ...
        <Path Canvas.Left="0" Canvas.Top="0" Stroke="#808080"
          StrokeThickness="1">
          <Path.Data>
            <EllipseGeometry x:Name="LensBorder" Center="0,0"
              RadiusX="40" RadiusY="40" />
          </Path.Data>
        </Path>
      </Canvas>
    </Canvas>
  </Grid>
</UserControl>
ZoomCanvas は、通常は非表示になっていますが、マウスの左ボタンを押すと、OnMouseLeftButtonDown (図 3 を参照) が Visibility プロパティを切り替えることで ZoomCanvas を表示します。ZoomCanvas の全体が表示されることはありません。一方、その Clip プロパティは、事実上虫眼鏡として表示される完全な円の中にコンテンツを表示する EllipseGeometry で初期化されます。虫眼鏡 (実際にはクリッピング領域) のサイズを変更するには、EllipseGeometry の RadiusX プロパティと RadiusY プロパティを変更します。その場合、LensBorder という名前の EllipseGeometry の同じプロパティも一致するように変更する必要があります (EllipseGeometry は虫眼鏡の周りに枠を描画します)。
図 3 Page.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Magnifier
{
    public partial class Page : UserControl
    {
        private bool _dragging = false;
        private const double _scale = 4.0;

        public Page()
        {
            InitializeComponent();
        }

        private void OnMouseLeftButtonDown(object sender,
            MouseButtonEventArgs e)
        {
            double x = e.GetPosition(RootCanvas).X;
            double y = e.GetPosition(RootCanvas).Y;
            PositionLens(x, y);

            ZoomCanvas.Visibility = Visibility.Visible;
            ((FrameworkElement)sender).CaptureMouse();
            _dragging = true;
        }

        private void OnMouseMove(object sender, MouseEventArgs e)
        {
            if (_dragging)
            {
                double x = e.GetPosition(MainCanvas).X;
                double y = e.GetPosition(MainCanvas).Y;
                PositionLens(x, y);
            }
        }

        private void OnMouseLeftButtonUp(object sender,
            MouseButtonEventArgs e)
        {
            if (_dragging)
            {
                ZoomCanvas.Visibility = Visibility.Collapsed;
                ((FrameworkElement)sender).ReleaseMouseCapture();
                _dragging = false;            }
        }

        private void PositionLens(double x, double y)
        {
            Lens.Center = LensBorder.Center = new Point(x, y);
            ZoomCanvas.SetValue(Canvas.LeftProperty, (1 - _scale) * x);
            ZoomCanvas.SetValue(Canvas.TopProperty, (1 - _scale) * y);
        }
    }
}
アプリケーションを実行し、シーンのどこかで (ページの下部にあるテキストの上でもかまいません) マウスの左ボタンをクリックしたままにすると、虫眼鏡の動作を確認できます。また、ボタンを押したままマウスを動かすと、虫眼鏡も移動します。これは、2 つの EllipseGeometry の Center プロパティを変更してレンズを再配置し、クリッピング領域に表示されているコンテンツが MainCanvas 内のカーソルの下にあるコンテンツと一致するように ZoomCanvas の Canvas.Left プロパティと Canvas.Top プロパティを調整することで実現できます。その厳密な動作を確認するには、OnMouseMove メソッドと PositionLens メソッドを参照してください。
アプリケーションを再生する場合、虫眼鏡はイメージ上に配置されたときにピクセルを拡大しないことに注意してください。より大きなイメージ詳細が表示されます。その理由は、イメージのネイティブ解像度が 2,400 x 1,800 ピクセルであるためです。イメージは、MainCanvas および ZoomCanvas で 600 の幅と 450 の高さで宣言されます。つまり、ネイティブ解像度のちょうど 4 分の 1 です。ただし、ZoomCanvas が ScaleTransform を使用してイメージを 4 倍に拡大すると、イメージがネイティブのサイズに戻ります。したがって、通常表示されるイメージは、実際の幅と高さの 4 分の 1 に圧縮されていますが、虫眼鏡を通して表示されるイメージは、ネイティブ解像度で表示されます。イメージのネイティブ サイズが 600 x 450 だった場合、拡大したビューはピクセル化されます。

回転する (3D)
Silverlight と WPF のもう 1 つの違いは、WPF が 3D グラフィックスをサポートすることです。ただし、それでも Silverlight 開発者がアプリケーションに 3D 効果を採用するのを止める理由にはなりません。3D サポートの欠落は、3D 効果を作成するために Silverlight プログラマの仕事が若干増えることを意味するだけです。このような効果を次の例で示します。
最初に表示されるときに、SpinAndZoom アプリケーションでは溶岩石から透明な海に飛び込む筆者の末娘の写真が表示されます。しかし、マウスの左ボタンを押したまま写真上でドラッグすると、写真が垂直軸で回転し、同じ溶岩構造の土台の海底にいる別の娘の写真が表示されます (図 4)。ところで、娘を常に水中に入れておくのは、男たちから隠しておくためのすばらしい方法です。
図 4 垂直軸で写真を回転する (3D) (クリックすると拡大画像が表示されます)
図 5 に、3D 回転の基礎となる XAML を示します。前面の写真とそのリフレクション用に 2 つ、背面の写真とそのリフレクション用に 2 つ、合計 4 つのイメージを宣言します。リフレクションは、ScaleTransform を使用してイメージの上下をひっくり返し、OpacityMask を LinearGradientBrush と組み合わせて、リフレクション イメージを距離の関数としてフェードすることで生成されます。回転は、ScaleTransform (SpinScaleTransform) および SkewTransform (SpinSkewTransform) を操作することでプログラムで実現できます。回転の角度が増加すると、SkewTransform は上下のエッジの角度を増やし、ScaleTransform はイメージを水平に圧縮します。正しく構成すると、結果は 3D 回転の模倣として納得のいくものになります。
図 5 Page.xaml
<UserControl x:Class="SpinAndZoom.Page"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid x:Name="LayoutRoot" Background="Black"
    MouseLeftButtonDown="OnMouseLeftButtonDown" MouseMove="OnMouseMove"
    MouseLeftButtonUp="OnMouseLeftButtonUp">
    <Canvas x:Name="SpinCanvas" Width="400" Height="300">
      <Canvas.RenderTransform>
        <TransformGroup>
          <ScaleTransform x:Name="SpinScaleTransform" CenterX="200" />
          <SkewTransform x:Name="SpinSkewTransform" CenterX="200" />
          <ScaleTransform x:Name="ZoomScaleTransform" CenterX="200"
            CenterY="150" />
        </TransformGroup>
      </Canvas.RenderTransform>

      <!-- Front -->
      <Image x:Name="Front" Source="Images/Abby.jpg" Canvas.Left="0"
        Canvas.Top="0" Width="400" Height="300" Visibility="Visible"
        Stretch="Fill" />

      <!-- Front reflection -->
      <Image x:Name="FrontReflection" Source="Images/Abby.jpg"
        Canvas.Left="0" Canvas.Top="500" Width="400" Height="200"
        Visibility="Visible" Stretch="Fill">
        <Image.RenderTransform>
          <ScaleTransform ScaleY="-1" />
        </Image.RenderTransform>
        <Image.OpacityMask>
          <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
            <GradientStop Offset="0.5" Color="#00000000" />
            <GradientStop Offset="1" Color="#80000000" />
          </LinearGradientBrush>
        </Image.OpacityMask>
      </Image>

      <!-- Back -->
      <Image x:Name="Back" Source="Images/Amy.jpg" Canvas.Left="0"
        Canvas.Top="0" Width="400" Height="300" Visibility="Collapsed"
        Stretch="Fill" />

      <!-- Back reflection -->
      <Image x:Name="BackReflection" Source="Images/Amy.jpg" Canvas.Left="0"
        Canvas.Top="500" Width="400" Height="200" Visibility="Collapsed"
        Stretch="Fill">
        <Image.RenderTransform>
          <ScaleTransform ScaleY="-1" />
        </Image.RenderTransform>
        <Image.OpacityMask>
          <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
            <GradientStop Offset="0.5" Color="#00000000" />
            <GradientStop Offset="1" Color="#80000000" />
          </LinearGradientBrush>
        </Image.OpacityMask>
      </Image>
    </Canvas>
  </Grid>
</UserControl>
該当するコードはダウンロードできますが、図 6 にその抜粋を示します。主要メソッドである RotateTo は、MouseMove ハンドラによって呼び出され、マウス カーソルがシーン上を移動するときに回転の角度を増減します。RotateTo では、若干の三角法を使用して、図 7 のジオメトリに基づいて SkewTransform パラメータと ScaleTransform パラメータを計算します。回転の角度が指定されると、RotateTo は紫色で強調された三角形の幅と内角 (中心に最も近い角度) を計算します。幅は、ScaleTransform の ScaleX プロパティに設定する値を RotateTo に示し、角度は、SkewTransform の AngleY プロパティに設定する値を示します。
図 6 Page.xaml.cs からの RotateTo
private void RotateTo(double angle)
{
    double radians = (angle * Math.PI) / 180;
    double scaleX = Math.Abs(Math.Cos(radians));

    // Update the ScaleTransform
    // Avoid ScaleX == 0 to prevent images from disappearing!
    SpinScaleTransform.ScaleX = Math.Max(0.005, scaleX);

    // Update the SkewTransform
    if (angle != 90 && angle != 270) // Tangent undefined
    {
        double h = Math.Sin(radians) * _aspect;
        double r = Math.Atan(h / scaleX);
        if (angle > 90 && angle < 270)
            r = -r;
        SpinSkewTransform.AngleY = (r * 180) / Math.PI;
    }
}
図 7 3D 回転のジオメトリ (クリックすると拡大画像が表示されます)
表示の効果を高めるための鍵は、回転イメージの頂点が架空の円柱で定義された軌道をたどっていることです。円柱がユーザーに対してどの程度傾けられるかは、_aspect という名前のプライベート フィールドで制御されます。_aspect の値を増やすことで、円柱の度数を上げることができます。
また、SpinAndZoom は、Silverlight アプリケーションに見られる別の一般的なビジュアル効果の実装方法も示します。それは対話型ズームです。図 8 に示す OnMouseWheelTurned というイベント ハンドラは、ZoomScaleTransform という名前の ScaleTransform を操作することでマウスホイールの移動に応答します。この変換は、シーン内のすべての XAML オブジェクトを拡大/縮小します (試してみてください。シーンのどこかにマウス カーソルを置き、マウス ホイールを前後に回転します)。
図 8 Page.xaml.cs からの OnMouseWheelTurned
private void OnMouseWheelTurned(Object sender, HtmlEventArgs args)
{
    double delta = 0;
    ScriptObject e = args.EventObject;

    if (e.GetProperty("wheelDelta") != null) // IE and Opera
    {
        delta = ((double)e.GetProperty("wheelDelta"));
        if (HtmlPage.Window.GetProperty("opera") != null)
            delta = -delta;
    }
    else if (e.GetProperty("detail") != null) // Mozilla and Safari
    {
        delta = -((double)e.GetProperty("detail"));
    }

    if (delta > 0)
    {
        if (ZoomScaleTransform.ScaleX < _max)
        {
            // Zoom in
            ZoomScaleTransform.ScaleX += 0.1;
            ZoomScaleTransform.ScaleY += 0.1;
        }
    }

    else if (delta < 0)
    {
        if (ZoomScaleTransform.ScaleX > _min)
        {
            // Zoom out
            ZoomScaleTransform.ScaleX -= 0.1;
            ZoomScaleTransform.ScaleY -= 0.1;
        }
    }

    if (delta != 0)
    {
        args.PreventDefault();
        e.SetProperty("returnValue", false);
    }
}
Silverlight はマウスホイール イベントを起動しないため、SpinAndZoom は、Silverlight 2 ではアンマネージ ブラウザ DOM イベントに対してマネージ イベント ハンドラを登録できるという事実を利用して、ブラウザのマウスホイール イベントに応答します。登録は Page コンストラクタで行われ、同じハンドラが 3 回登録されてブラウザ間の違いに対応します。ブラウザによってマウスホイール イベントのレポート方法が大きく異なるため、OnMouseWheelTurned には、マウスホイールの移動方向を検出するための独自の機能があります。

ライブ ビデオを簡単にオーバーレイする
最後の例は、他の例ほど派手ではありませんが、並外れた手段を必要とする可能性のある問題に対する単純でコストの低いソリューションを示します。
筆者は最近、Windows Media Player を使用してライブ イベントをストリーミングしているマイクロソフトの顧客に会いました。この顧客は、Silverlight のクロスプラットフォーム機能を使用して、自分の顧客基盤を Windows 以外のユーザーに拡大することに関心を持っていました。余談ですが、Windows Media Server を使用すれば、ビデオ画像にライブ オーバーレイを埋め込むことができるかという質問を彼から受けました。彼は、テレビのニュース チャンネルの画面の下に水平にスクロール表示されるティッカー行のようなものを望んでいたのです。
Silverlight を使用すれば、ビデオ オーバーレイを実行するために、サーバー上に高価なハードウェアやソフトウェアは必要ないことを彼に説明しました。代わりに、Silverlight のネットワーキング スタックを使用して、ビデオ ストリームの帯域外でフィードを取得し、XAML を使用して Silverlight MediaElement の上にフィードを表示できます。図 9 のアプリケーションで、その方法を示します。
図 9 MediaElement 上に配置されたヘッドラインをスクロール表示する
このアプリケーションでは、MediaElement を使用して、サーバーからダウンロードした WMV (Windows Media Video) ファイルを再生します。ビデオの帯域外で、WebClient オブジェクトを使用して FeedBurner.com からニュース フィードをフェッチし (ドメイン間アクセスを可能にする XML ポリシー ファイルがあります)、SyndicationFeed オブジェクトを使用してフィードを解析し、ニュース ヘッドラインの文字列を生成します。次に、TextBlock と Rectangle から構成されるオーバーレイに文字列を表示します。0.5 の透明度を使用して、オーバーレイによって下のビデオが完全に隠されるのを防ぎます。単純なアニメーションで TextBlock を水平にスクロールし、クリッピング領域で Rectangle の外部のテキストをすべて切り取ります (図 10 を参照)。
図 10 Page.xaml
<UserControl x:Class="VideoOverlay.Page"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid x:Name="LayoutRoot" Background="Black">
    <Canvas Width="720" Height="480">
      <MediaElement x:Name="Player" Source="Videos/CRCC Jet Fly.wmv"
        Width="720" Height="480" MediaEnded="MediaElement_MediaEnded" />
      <Canvas Canvas.Top="448" Width="720" Height="32" Opacity="0.5">
        <Canvas.Clip>
          <RectangleGeometry Rect="0,0,720,32" />
        </Canvas.Clip>
        <Rectangle x:Name="Marquee" Width="720" Height="32" Fill="Black" />
        <TextBlock x:Name="Headlines" Canvas.Left="800" Canvas.Top="8"
          Foreground="White" FontSize="14">
          <TextBlock.Resources>
            <Storyboard x:Name="TickerStoryBoard"
              Completed="TickerStoryBoard_Completed">
              <DoubleAnimation x:Name="TickerAnimation"
                Storyboard.TargetName="Headlines"
                Storyboard.TargetProperty="(Canvas.Left)" />
            ...
</UserControl>
XAML 分離コードの各部分を図 11 に示します (コード ダウンロードには完全なコードが含まれています)。Page コンストラクタは WebClient.OpenReadAsync を呼び出して、ニュース フィードの非同期要求を起動し、完了イベント ハンドラはフィードから TextBlock にヘッドラインをコピーします。次に、Storyboard.Begin を呼び出して TextBlock のスクロールを開始し、Tick イベントを 5 分おきに起動するように DispatcherTimer をプログラムします。Tick イベント ハンドラは、ニュース フィードの更新要求を発行します。これは最終的にプライベート フィールドに格納されます。
図 11 Page.xaml.cs
using System;
namespace VideoOverlay
{
    public partial class Page : UserControl
    {
        private const double _offset = 20.0;
        private const double _secondsPerFrame = 10.0;
        private const string _separator = "   ?   ";
        private readonly Uri _uri =
            new Uri("http://feeds.feedburner.com/AbcNews_TopStories");
        private DispatcherTimer _timer = new DispatcherTimer();
        private string _text = null;

        public Page()
        {
            InitializeComponent();

            // Launch an async request for current news headlines
            WebClient wc = new WebClient();
            wc.OpenReadCompleted += new
                OpenReadCompletedEventHandler(OnInitialDownloadCompleted);
            wc.OpenReadAsync(_uri);
        }

        private void OnInitialDownloadCompleted(object sender,
            OpenReadCompletedEventArgs e)
        {
            if (e.Error == null)
            {
                // Convert the news content into a string
                Headlines.Text = GetHeadlinesFromSyndicationStream(e.Result);

                // Begin scrolling headlines
                StartTicker();

                // Start the refresh timer
                _timer.Tick += new EventHandler(OnTimerTick);
                _timer.Interval = new TimeSpan(0, 5, 0); // 5 minutes
                _timer.Start();
            }
        }

        private void OnTimerTick(object sender, EventArgs e)
        {
            // Launch an async request for current news headlines
            WebClient wc = new WebClient();
            wc.OpenReadCompleted += new
                OpenReadCompletedEventHandler(OnRefreshDownloadCompleted);
            wc.OpenReadAsync(_uri);
        }

        private void OnRefreshDownloadCompleted(object sender,
            OpenReadCompletedEventArgs e)
        {
            if (e.Error == null)
            {
                // Convert the news content into a string and store it away
                _text = GetHeadlinesFromSyndicationStream(e.Result);
            }
        }

...
    }
}
ヘッドラインをスクロールするアニメーションが終了するたびに、Storyboard.Completed イベント ハンドラはそのフィールドで最新のニュース フィードをチェックし、新しいコンテンツが使用可能な場合は TextBlock のコンテンツを更新します。次に、アニメーションの実行を再び開始します。続いて、ヘッドラインが連続ループで画面にスクロール表示され、5 分おきにヘッドラインが更新されます。重要なのは、スレッド同期ロジックやアプリケーションの UI スレッドへのコールバックのマーシャリングが不要なことです。その理由は、DispatcherTimer.Tick イベント ハンドラと Storyboard.Completed イベント ハンドラがどちらも UI スレッドで実行されるためです。
このアプローチの欠点は、ニュース フィードのフェッチと更新によって追加のネットワーク トラフィックが生成されることです。ただし、トラフィックはビデオ ストリーム自体に比べるとわずかであり、ライブ ビデオ ストリームを変更するためにサーバー上に高価なハードウェアやソフトウェアは不要です。さらに、クライアントにレンダリングされるニュース フィードは、ヘッドラインに関する追加情報を表示するマウスオーバーや、ヘッドラインをニュース ページにリンクするハイパーリンクなどの追加の UX 効果で拡張できます。ユーザーに提示されるコンテンツが単なるビデオ ストリームのビットではなく XAML DOM の一部である場合は、可能性は無限です。
変換およびクリッピング領域は、XAML で自由に使用できる最も強力なツールです。これらのツールがないと、Silverlight のグラフィックス サブシステムの魅力は大幅に低下します。筆者は、回転木馬、ローロデックススタイルのページング、対話型チャートなどの効果を実装する方法について最近多くの質問を受けます。今後の Wicked Code のコラムで、より魅力的な Silverlight のグラフィカル機能を探してください。

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

Jeff Prosise は、MSDN Magazine に寄稿している編集者で、『プログラミング Microsoft .NET』など数冊の著書があります。また、ソフトウェアのコンサルティングおよび Microsoft .NET 専門の教育機関である Wintellect (www.wintellect.com) の共同創立者でもあります。


基礎
ワークフローとアクティビティの単体テスト
Matt Milner
コードのダウンロード: MSDN Code Gallery (168 KB)
オンラインでのコードの参照
目次
テストに関する考慮事項
アクティビティをテストする
ルールをテストする
ランタイム サービスをテストする
ワークフローをテストする
執筆者およびインストラクタとして、Windows Workflow Foundation (Windows WF) コンポーネントのテスト方法に関する質問をよく受けます。新しいフレームワークの使用方法を学ぶときには、開発プロセスにツールやコンポーネントをどのように取り込むのかを考える必要があります。今月の記事では、アクティビティ、ワークフロー、および関連するコンポーネントのテストについて、いくつかの課題や技法を取り上げます。これは、単体テストやテスト駆動型開発に対するベスト プラクティス ガイドを意図したものではありません。むしろ、各自が採用する開発方法論に基づいてワークフローをテストする際に、使用できる技法を紹介することを意図しています。

テストに関する考慮事項
単体テストを記述する際には、コード全体にわたる共通パスをテストする必要がありますが、話はそう簡単ではありません。"幸運なパス" とでも呼べるような、期待される実行パスに加えて、例外パスもテストする必要があります。さらに、テストする項目の多くは依存関係を持ち、それらすべてにわたってテストすることは非現実的です。たとえば、ビジネス オブジェクトはデータ アクセス層を利用する場合があります。しかし、テストでは、データベースを利用する代わりに、モック オブジェクトを使用することが一般的です。これは、データ アクセス層と同じインターフェイスを持ちながら、実行時にはデータベースにアクセスしないオブジェクトです。
これらと同じ問題がワークフローについても存在しますが、場合によっては、アプローチが若干異なります。また、一部の開発者は、Windows WF が新しいテクノロジであるという事実だけで、コードのテスト方法の決定に困難を覚える場合もあります。
一般的なテストの問題に加えて、Windows WF などのフレームワークを使用すると、また新たな複雑さが加わります。Windows WF は、ワークフローおよびアクティビティの実行を管理するランタイムに基づいているため、テストはほとんどすべての場合、ランタイムの使用を伴います。ランタイム実行で使用されるクラスの多くは、シールまたは他の方法で保護されているため、そのようなオブジェクトのモック作成はほとんど不可能となります。また、作成したモック オブジェクトで、ランタイムで必要とされる機能のすべてを提供することは困難です。これらの理由により、ワークフローまたはアクティビティのテストには、一般にランタイムが必要となります。
ワークフロー ランタイムを使用すると、いくつかの興味深い問題が生じます。まず、ランタイムは、それぞれ独自のスレッド モデルを持ついくつかの異なるスケジューラを使用できるため、単体テストは特定のスレッド モデルを使用して記述する必要があります。テストに対して最適なスケジューラが、ワークフローの最終的なホストに最も適したスケジューラと同じとは限りません。
ランタイムは、開発者に対して、実行時に役立つサービスを提供しますが、そのようなサービスのためにテストの困難さが増す可能性もあります (たとえば、例外処理など)。ランタイムは、すべての例外をキャッチし、それらをワークフロー内で管理します。これは、予想される例外に対するテストの動作が、標準の Microsoft .NET Framework コードの場合とは多少異なることを意味します。
Windows WF で構築されたソリューションをテストする場合、一般にテストを必要とするコンポーネントがいくつかあります。アクティビティ、ワークフロー、ルール、およびカスタム ランタイム サービスです。以降は、これらのコンポーネントのテスト中に開発者が直面する一般的な問題について取り上げ、そのような問題に対処するための手法を提案します。

アクティビティをテストする
アクティビティをテストする際には、入力とテスト出力を使用し、分離された状態でテストできることが重要です。アクティビティをワークフローのコンテキスト内でテストするのではなく、個別のコンポーネントとしてテストする必要があります。さいわい、Windows WF では、すべてのワークフローは単純にアクティビティです。そしてこれは、ランタイムが、電子メール アクティビティのように単純なものも含め、任意のアクティビティをワークフローとして実行できることを意味します。
アクティビティを実行する前に、ランタイムをセットアップし、適切なスケジューラおよび他のランタイム サービスを追加する必要があります。一般に、アクティビティのテスト時には、ManualWorkflowSchedulerService を使用するのが最も簡単です。単体テストと同じスレッドでアクティビティが動作でき、実行を最もきめ細かく制御できるためです。作業を行うスレッドがホスト (この場合はテスト) によって提供されるまで、スケジューラはワークフローまたはアクティビティ内で何も作業を行いません。
著者がワークフローをテストする際には通常、テストのセットアップおよび破棄のメソッドを使用して、ランタイムの初期化および適切なシャットダウンを行います。これらのテスト メソッドにより、テストを実行するためのクリーンなランタイムを得られることがわかっているため、特定のアクティビティ テストの実行に専念できます。図 1 に、ワークフローにパラメータを渡して初期化する Windows WF の機能を使用した単純なアクティビティ テストのコードを示します。
図 1 アクティビティをワークフローとして実行する
Dictionary<string, object> results = null;
Exception ex = null;

runtime.WorkflowCompleted += delegate(
  object sender, WorkflowCompletedEventArgs wce) {
  results = wce.OutputParameters; };

Dictionary<string, object> wfParams = new Dictionary<string, object>();
wfParams.Add("To", "mmnet30@microsoft.com");
wfParams.Add("From", "donotreply@example.org");
wfParams.Add("Subject", "Unit testing");

WorkflowInstance instance = runtime.CreateWorkflow(
  typeof(WFComponents.SendMailActivity), wfParams);
instance.Start();

ManualWorkflowSchedulerService man =
  runtime.GetService<ManualWorkflowSchedulerService>();
man.RunWorkflow(instance.InstanceId);

//the workflow is done, now we can test the outputs
Assert.IsNotNull(results, "No results found");
Assert.AreEqual<string>(wfParams["To"].ToString(),
  results["To"].ToString(), "To address was changed");
このコードには、注目すべき点がいくつかあります。1 つは、SendMailActivity がワークフローの定義として使用され、それを包含するワークフローとは無関係に実行されていることです。アクティビティには Windows WF の標準的な手法を用いてパラメータが渡されています。つまり、値の辞書がランタイムに渡され、アクティビティのプロパティにマップされています。ワークフローの実行には手動スケジューラが使用されているため、RunWorkflow メソッドはアクティビティの実行準備ができたときに使用されます。RunWorkflow メソッドが動作を完了すると、単純なアクティビティは終了し、出力パラメータを抽出してアサーションを行えるようになります。
WorkflowCompleted イベント中に出力パラメータが収集され、後で検査できるローカル変数に参照がマップされることに注目してください。手動スケジューラを使用しているため、WorkflowCompleted イベントの実行はこのテスト内の他のすべての作業と同じスレッドで行われます。
テスト中には他のスケジューラも使用できますが、より複雑なテスト コードが必要となります。たとえば、既定のワークフロー スケジューラを使用した場合には、ワークフローの完了まで実行をブロックするためにある種の待機ハンドルが必要となります。Visual Studio では、新しいシーケンシャル ワークフロー コンソール アプリケーションを作成し、program.cs ファイルを調べることで、このような種類の待機の例を確認できます。
最初のテストでは通常の実行を扱いますが、例外の場合はどうでしょうか。ほとんどの単体テスト フレームワークでは、テスト開発を単純化するために、予想される例外を宣言できます。たとえば、次のコードは、Visual Studio 2008 の単体テストで、テスト メソッドに対して予想される例外を宣言できるようにする例です。
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MailWithInvalidFrom(){...}
Windows WF のテストでこの方法を使用する際に問題となるのは、ランタイムが例外をキャッチした後、それをホストでスローする代わりに、ワークフローの終了時にイベントを発生させ、その例外をイベント引数の一部として渡すということです。この場合、例外の処理には 2 つのオプションがあります。テスト フレームワークの組み込み動作の使用を避けるか、または終了したイベントを処理して例外を再スローするかです。
イベント ハンドラから例外をスローすればよいと考えるかもしれませんが、残念ながら、再スローされた例外をランタイムがキャッチするため、うまくいきません。図 2 に、SendMailActivity の別のテストを示します。ここでは、From アドレスを渡していないため、コードで ArgumentNullException が発生します。
図 2 例外を取り扱う
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void MailWithInvalidFrom() {
  Exception ex = null;

  runtime.WorkflowTerminated += delegate(
    object sender, WorkflowTerminatedEventArgs wte) {
    ex = wte.Exception; };

  Dictionary<string, object> wfParams =
    new Dictionary<string, object>();
  wfParams.Add("To", "mmnet30@microsoft.com");
  //wfParams.Add("From", "donotreply@example.org");
  wfParams.Add("Subject", "Unit testing exception");

  WorkflowInstance instance = runtime.CreateWorkflow(
  typeof(WFComponents.SendMailActivity), wfParams);

  instance.Start();

  ManualWorkflowSchedulerService man =
    runtime.GetService<ManualWorkflowSchedulerService>();
  man.RunWorkflow(instance.InstanceId);

  //exception should be thrown by now
  if (ex != null)
  throw (ex);
  else
  Assert.Fail("Exception should have been thrown");
}
この例からもわかるように、WorkflowTerminated イベントの引数から例外を引き出すことができます。しかし、一度これが発生すると、ExpectedException 動作を使用するために例外をスローする必要があります。例外をスローして ExpectedException 属性を使用する代わりに、Assert を使用して例外の種類が正しいことを確認しても、手間は変わらないかもしれません。
ここまで、テスト コードが電子メール送信の実際の出力をテストしていないことにお気付きかと思います。電子メールの場合、アプリケーション構成ファイル内の system.net/mailSettings 構成を設定して、メッセージがファイル システムに書き込まれるようにすることは簡単です。しかしその場合、出力を確認するには、それらのメッセージを開いて読み取る必要があります。この機能をテストするためのより良い方法は、電子メール プロセスを抽象化するモック オブジェクトを使用することです。
Windows WF では、アクティビティは作業を直接実行するか、または、ランタイム サービスと呼ばれる、ランタイムに読み込まれるオブジェクトを使用して作業できます。ランタイム サービスを使用して作業を処理する利点の 1 つは、ランタイムへのサービスの追加をホスト プロセスが担当することです。アクティビティを ASP.NET アプリケーションで使用する場合は、Windows サービスでアクティビティを使用する場合とはコードが異なる可能性があります。アクティビティは単に、ランタイムに対して特定の種類のオブジェクトのクエリを実行してから、そのオブジェクトに対してメソッドを呼び出します。
テスト中、ランタイム サービスは、テスト作業を支援するモック オブジェクトで置換することができるため、このモデルの使用はテストに非常に適しています。これはアクティビティに多くの作業を追加するように見えるかもしれません。また、アクティビティをサポートするためにすべてのホストがランタイム サービスを追加する必要があるという要件は、堅苦しいと感じるかもしれません。ただし、アクティビティは、ランタイム サービスが存在する場合はそれを使用し、存在しない場合は独自の実装を提供するように記述できます。
次に示すのは SendMailActivity の実行メソッドであり、ランタイム サービスが存在するかどうかに従って実行がどのように異なるかを示しています。サービスが見つかった場合、メッセージはサービスを介して送信されます。見つからなかった場合は、System.Net.Mail 名前空間を使用して直接送信されます。
IEmailService mailSvc =
  executionContext.GetService<IEmailService>();

MailMessage msg = new MailMessage(From, To);
msg.Subject = Subject;

//use the mail service if it is present
if (mailSvc != null) {
  mailSvc.SendMessage(msg);
}
else {
  SmtpClient client = new SmtpClient();
  client.Send(msg);
}
アクティビティは具象クラスの代わりにインターフェイスを使用し、重要な抽象ポイントを提供していることに注意してください。アクティビティは特定の実装には依存せず、ランタイムによって追加されたサービスの実装に従います。それにより、図 3 に示すように、テストではサービスのモック実装を活用できるようになります。
図 3 モック ランタイム サービスを使用してアクティビティをテストする
[TestMethod]
public void TestEmailWithMock() {
  MockEmailService mock = new MockEmailService();
  runtime.AddService(mock);

  ...

  //the workflow is done, now we can test the outputs
  Assert.IsNotNull(results, "No results found");

  MockEmailService email =
    runtime.GetService<MockEmailService>();
  Assert.IsNotNull(email,
    "No email service found in the runtime");

  Assert.AreEqual<string>(wfParams["To"].ToString(),
    email.Message.To.ToString(),
    "To address was changed");
}
これにより、テストの一部として必ずしも電子メールを実際に送信する必要はなく、追加された実装を使用してテストの結果をより簡単に確認できます。この抽象方法は、テストと同様に、一般的なホストの柔軟性向上にも役立ちます。

ルールをテストする
多くのワークフロー ソリューションに不可欠なコンポーネントであるビジネス ルールは、テストに対して新たな課題をもたらします。Windows WF のルールセットは、ワークフローのコンテキスト外で実行する分にはそれほど困難なものではありませんが、ルールセットは特定の種類に基づいて定義され、その種類は通常はワークフロー クラスです。一般に、ルールには入力としてオブジェクトまたはオブジェクトのセットが必要であり、ルールはそれらに対して評価され、それらに対して動作を実行します。Windows WF の場合、通常、ワークフロー自体はルート オブジェクトであり、ルールにアクセスする他のすべてのオブジェクトは、単にワークフロー内のプロパティまたはアクティビティです。
ワークフローを実行する際、ルールの実行に最も多く使用されるのは Policy アクティビティです。単体テスト シナリオでは、ルールはアクティビティなしで直接テストした方が適切です。ルールセットの実行には、System.Workflow.Activities.Rules 名前空間からいくつかのクラスを使用できます。また、WorkflowMarkupSerializer クラスを使用してストリームから RuleSet クラスを逆シリアル化することで、実行時に使用するルールの表現をテスト環境でも使用できます。
図 4 に、単体テストでルールセットを実行するために使用される基本的なコードを示します。ルールセットの実行が完了した後は、ルール実行中に使用されたオブジェクトをアサーションで使用して、期待される動作を確認できます。この例で最初に注意すべき重要なことは、ルールがワークフローの種類に対してではなく顧客オブジェクトに対して実行されていることです。これは、いくつかの理由からまったく意図的に行われているものです。
図 4 テスト中にルールセットを実行する
//initialize test object
Customer cust = new Customer();
cust.Level = CustomerLevel.Gold;
cust.CurrentAnnualPurchases = 1578;

//create rule execution objects
RuleValidation val =
  new RuleValidation(typeof(Customer), null);
RuleExecution exec = new RuleExecution(val, cust);
RuleSet ruleset = null;

//get rules from file / deserialize
WorkflowMarkupSerializer serializer =
  new WorkflowMarkupSerializer();

string rulePath = Path.Combine(
  testContextInstance.TestDeploymentDir, "Customer.rules");
RuleDefinitions defs =
  (RuleDefinitions)serializer.Deserialize(
  XmlReader.Create(rulePath));
ruleset = defs.RuleSets[0];

//execute ruleset
ruleset.Execute(exec);

//test the outcome
Assert.AreEqual<BLL.CustomerLevel>(cust.Level,
  CustomerLevel.Platinum);
Policy アクティビティを使用してルールセットを作成する既定のモードは、ポリシーがワークフローの種類に基づいて作成され、アセンブリにリソースとして埋め込まれることを意味しています。これはテストに対して問題となるだけでなく、実行時の柔軟性に関しても問題となります。テストで問題が生じるのは、ルールセットをテストするにはワークフローのインスタンスを作成して初期化する必要があるためです。ビジネス オブジェクトなど他の種類に対してルールを作成すると、テストはずっと簡単になり、ワークフロー内からのルールの使用も容易になる可能性があります。Windows WF MSDN サイトでは、カスタム アクティビティの作成など、ルールの使用方法の例がいくつか提供されています。
ルールのテストには、ルールの入力として機能するオブジェクトの作成と初期化、およびルールセットや実行オブジェクト (ルールのバリデータおよび実行プログラムなど) の初期化が含まれます。ここに示したコードは大部分が定型であり、ワークフローの代わりにカスタム オブジェクトに対してルールを実行するよう構築されたカスタム アクティビティで使用できます。
ルールはワークフロー内の各種のアクティビティのプロパティに依存するので、ルールの入力を別の種類へと分離するこの方法は現実的でない場合があります。そのような場合、ランタイムの CreateWorkflow メソッドを使用してワークフローのインスタンスを作成する必要があり、アクティビティのプロパティを初期化する必要があります。ワークフローのプロパティは、ワークフローの作成時にパラメータを渡すことで簡単に初期化できますが、アクティビティの初期化はもう少し困難な場合があります。アクティビティのプロパティが依存関係プロパティである場合、ワークフローのプロパティに単純にバインドできるため、作成時に初期化できます。
しかし、アクティビティのプロパティが標準の .NET プロパティである場合は、異なるアプローチが必要となります。単純なアプローチは、ワークフローの Initialize メソッドにコードを追加し、階層内のすべてのアクティビティ インスタンスを見つけてそれらの値を設定することです。この作業は単体テストそれ自体からは実行できないことに注意してください。なぜなら、いったんワークフローが作成されると、ホスト コードは実行中のインスタンス状態に対して直接アクセスできないからです。
最後の問題点は、While アクティビティまたはステート マシン内のように、アクティビティ自体が動的に作成される場合に、アクティビティをルールへの入力として使用するときに生じます。アクティビティの反復処理に関連した問題の詳細については、2007 年 6 月号の「基礎」コラム「ワークフローにおける ActivityExecutionContext」を参照してください。このようなシナリオでは、テストに対してアクティビティを正しく初期化するための唯一の方法は、ワークフローを実行し、複合アクティビティでアクティビティのクローンを作成することです。ここでは、この手法について時間を割いて詳しく述べることはしません。ルール自体が複雑になりすぎ、ほとんどの開発者はこのシナリオでルールを記述するのにしりごみしてしまうからです。

ランタイム サービスをテストする
ランタイム サービスとは、ワークフロー ランタイムに追加される任意の .NET オブジェクトを指します。Framework には、永続化、追跡、スケジュールなどのサービスや、ランタイムで一般的なその他のサービスがあらかじめ用意されています。これらは、フレームワークの一部となっているサービスであり、テストの対象とはなりません。ただし、カスタム ソリューションを開発する場合には、2 種類のランタイム サービスに対してテストが必要となる可能性があります。ランタイムに既知の機能を提供するカスタム サービスと、アクティビティまたはランタイムにアクセスするカスタム サービスです。
カスタム永続化サービスなど、既知の機能を提供するサービスの場合、これらのサービスをテストする唯一実際的な方法は、それらをランタイムで実行することです。実行を管理してテストを制御するには、ワークフローをドライバとして使用するのが最善の方法です。ワークフローを使用すると、ワークフローのアイドル状態、中断、終了など、サービスの起動につながる可能性のある状態を制御できるようになります。
テスト対象となるその他のサービスについては、サービスの実装に応じてテストの要件が異なります。たとえば、一部のサービスは、発生するイベントを介してランタイムとやり取りします。また、ワークフロー インスタンスに関する情報に対して、より直接的にアクセスするサービスもあります。たとえば、インスタンス識別子を要求したり、アクティビティから呼び出されたときに現在の WorkBatch を使用したりするサービスです。これらのシナリオでは、サービスはドライバ ワークフローのコンテキストでテストするのが最適です。この記事の冒頭に示したメソッドを使用して、テストをドライブできます。
いくつかのオブジェクトは、アクティビティにサービスを提供するためにのみランタイムに存在し、他のサービスは利用しません。そのような場合、それらのサービスは一般に、標準的な .NET コンポーネントとしてテストできます。ワークフロー ランタイムの観点から満足する必要のある要件は存在しませんが、テストでは、他のサービスまたは依存オブジェクトに対してモック オブジェクトを使用する場合があります。オプションとして、前述のように、1 つのアクティビティをドライバとして使用し、このアクティビティがサービスを完全に実行することで、テスト済みの機能がランタイムでの実行時に動作することを確認できます。この最後のオプションは余分な作業のように見えるかもしれませんが、コードが実行時に正しく動作することについて、より大きな確信を得ることができます。

ワークフローをテストする
最大の問題の 1 つは、開発者がワークフロー全体の単体テストを試みるときに直面します。多くの場合、ワークフローは、多くの異なるサービスやアプリケーションとやり取りする、長時間実行されるプロセスをモデル化するため、テストの複雑さはすぐに増大します。たとえば、特定のワークフローが Windows Communication Foundation (WCF) を使用していくつかのサービスを呼び出し、そのロジックの一部として結果を集約する場合があります。
テスト用にモック オブジェクトを挿入することで、これらのやり取りを処理することが可能です。実際、Windows WF での WCF アクティビティについては、テスト可能なサービス エンドポイントの挿入機能は単純化されています。Send アクティビティは、ChannelManagerService クラスを使用してエンドポイントの解決を試みます。テスト クラスは、ワークフローで予想されるものと一致する名前付きエンドポイントを簡単に追加して、ローカル サービス実装を指すようにバインドおよびアドレスを変更できます。この間接性を利用して、いくつかのサービスを起動するワークフローをさまざまなケースでテストできます。ChannelManagerService クラスの使用の詳細については、MSDN Magazine の 2008 年 8 月号を参照してください。
まとめると、実際に問題なのはワークフローをテストする必要があるかどうかではなく、それをどのようにテストするかということです。多くの開発チームで、ワークフローのテストは単体テストのタスクとは見なされず、単体テストが完了し、各種のコンポーネントが良好に動作中であることがわかっている状態で、統合テストとして実施されます。ワークフローに対して、または他の自動化テストへのドライバに対して単体テストを使用する場合には、この記事に示された情報をテストの実行に役立てることができます。

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

Matt Milner は、.NET、Web サービス、Windows Workflow Foundation、Windows Communication Foundation、BizTalk Server などのマイクロソフト テクノロジを専門とする独立系ソフトウェア コンサルタントです。Matt は、Pluralsight のインストラクタとして、Workflow、BizTalk Server、および Windows Communication Foundation のコースで教鞭をとっています。夫人の Kristen および 2 人の息子とミネソタ州に居を構えています。Matt の連絡先は、pluralsight.com/community/blogs/matt にあるブログです。


Team System
Team Build 2008
Brian A. Randell
コードのダウンロード: MSDN Code Gallery (165 KB)
オンラインでのコードの参照
目次
Team Build について
チーム ビルドを作成する
ビルド エージェントを作成する
チーム ビルドを実行する
Team Build をプログラミングする
Team Foundation Server (TFS) はチームに関するものです。確かにチームでこれを使用できますが、TFS の本質は、実際には開発プロジェクトにおける他の人とのコラボレーションです。
ビルドの自動化は F5 ではありません。ビルドの自動化は、収集、アセンブル、検証、および監査に関するものです。その概念は、ソリューションを構成するすべてのアーティファクトを収集し、明確に定義された統合計画に基づいてそのアーティファクトをアセンブルすることです。コンパイルは、アセンブル フェーズで定義される典型的なタスクです。ビルド確認テスト (BVT) またはスモーク テストと呼ばれることの多いさまざまな種類の自動テストを使用して、チームの全体的な作業を検証します。
最後に、何らかの種類の監査を実行して、ビルドの全体的な品質を評価します。監査は、難しい質問に答えるためのデータを提供するのに役立ちます。たとえば、このビルドでは製品を作成しますか。製品は高品質ですか。退行したものはありますか。スケジュールどおりですか。ビルド監査の価値は、チームの脈拍を簡単に測れることです。
このコラムでは、Visual Studio 2008 Team Foundation Server ビルド (Team Build) を紹介し、チーム ビルドを作成および実行するプロセスについて説明します。Team Build 2008 でプログラミングするために使用可能なマネージ API についても示します。

Team Build について
Team Build は TFS 2008 の中核となる機能です。マイクロソフトは、非常に強力なビルド自動化ツールとなるようにこれをデザインしました。Team Build によってチームの F5 エクスペリエンスが提供されます。通常の環境では、Team Build を独自の専用サーバーにインストールします (ただし、チームが非常に小規模であるか予算が厳しい場合は、TFS ボックスに Team Build をインストールできます)。
TFS の他の部分とは異なり、Team Build の 2008 リリースはメジャー アップデートです。2005 リリースで、マイクロソフトは優れたビルド自動化製品を作成しました。MS-Build をエンジンとして使用した Team Build 2005 は、バージョン管理、コンパイル、複数ビルド タイプ、単体テスト、静的分析、コード カバレッジなど、期待される中核自動化機能の多くをサポートしていました。
また、過去の傾向分析をサポートするために、ビルド レポートや TFS データ ウェアハウスへのデータ更新などの優れたレポート機能も提供されます。Team Build の 2005 リリースでは、複数ビルド コンピュータ、ビルド通知、およびコマンドラインまたはチーム エクスプローラ ユーザー インターフェイスからビルドを開始する機能もサポートされていました。
Team Build 2008 では、マイクロソフトは単なる機能強化を超えることを行いました。大きな拡張として、継続的な統合サポート、ビルド キューイング、チーム エクスプローラによるスケジュールされたビルド、およびマネージ オブジェクト モデルがあります。また、ビルド定義からビルド エージェント定義を分離し、ビルド定義編集ユーザー インターフェイスを作成し、より優れたビルド管理ツールを追加しました。
堅固な Team Build 2005 機能セットと組み合わせて、Team Build 2008 は魅力的なビルド自動化ソリューションを提示します。また、マイクロソフトは下位互換性のサポートを継続するため、開発チームがまだ Visual Studio 2005 を使用している場合でも、TFS インストール環境と Team Build サーバーをアップグレードできます。

チーム ビルドを作成する
Team Build を使用するには、当然ながら、シングル サーバー構成またはデュアル サーバー構成で TFS をインストールする必要があります。Team Build をインストールし、ビルド サーバーを正しく構成する必要があります。詳細については、最近更新された「Team Foundation インストール ガイド」に記載されています。最後に、バージョン管理リポジトリにコードがチェックインされたチーム プロジェクトが必要です。
Team Foundation Server クライアントがインストールされた Visual Studio 2008 で、チーム プロジェクトを読み込んだチーム エクスプローラ ウィンドウを開きます。この例では、MSF-Agile テンプレートを使用して、MSDNMag というチーム プロジェクトを作成したと仮定します (詳細を確認するには、このコラムのコードと Team System 2008 TFS および Team Suite Virtual PC イメージをダウンロードしてください)。この例では、MSDNMag というカスタム ワークスペースを、Virtual PC イメージの C:\Work にマップされた樹幹と共に使用します。
すべて揃ったら、チーム エクスプローラ ウィンドウで MSDNMag チーム プロジェクト ノードを展開し、[ビルド] フォルダを探すことができます。[ビルド] フォルダを右クリックすると、いくつかのオプションが表示されます。この時点で関係するのは、[ビルド定義の新規作成] コマンドです。選択すると、[ビルド定義] ダイアログが表示されます (図 1 を参照)。
図 1 空白の [ビルド定義] ダイアログ (クリックすると拡大画像が表示されます)
Team Build 2005 のウィザードとは異なり、ビルド定義を編集する必要がある場合は、このダイアログに戻ることができます。まず、SimpleBuildExplorer-OnDemand などのビルド名を指定します。また、[このビルド定義を無効にする] オプションをオンにすることで、ビルドをオフラインに保つことができます。
新しいビルドを作成する場合は、ビルド定義名と Team Build プロジェクト ファイルを入力し、ビルド エージェントとドロップ場所を指定する必要があります。ただし、筆者の個人的な意見では、2 つ目のオプションである [ワークスペース] が重要です。
Team Build 2005 では、[ワークスペース] オプションを知っておく必要がありました。実装の詳細は変更されましたが、目的は同じです。つまり、ビルド プロセスのソース取得ステップ中に Team Build がダウンロードするアーティファクトを制限することです。Team Build 2005 では、WorkspaceMapping.xml というファイルを変更する必要がありました。Team Build 2008 では、バージョン管理のワークスペース マッピングのダイアログにあるのと同様の GUI を使用します (図 2 を参照)。
図 2 [ビルド定義] ダイアログの [ワークスペース] ページ (クリックすると拡大画像が表示されます)
ワークスペースでは、ビルドの一部として使用するソース コード リポジトリのセクションを定義します。既定では、Team Build はチーム プロジェクトの樹幹をビルドで使用されるソース ダウンロードの場所 (ビルドを実行するビルド エージェントをビルド定義に関連付けるときに制御できます) にマップします。明確化のために、この設定を調整しない場合は、ビルドを実行するたびに、Team Build は樹幹に対して取得を実行して、すべて最新のものを取得します。デモやテストを行う場合には大きな問題になりませんが、チーム プロジェクトと複数のブランチで 6 か月間作業した後では、ビルドの実行は最善の方法ではありません。
ここでのデモ シナリオで使用する Visual Studio ソリューションは 1 つのみですが、それでもマッピングを調整する必要があります。[ソース管理フォルダ] 列の最初のアイテムをクリックすると、制限されたパスを入力するか、[フォルダの参照] ダイアログを使用することができます。この例では、[ソース管理フォルダ] を $/MSDNMag/Main/src/TeamSystem/C08 に設定しました。
バージョン管理ワークスペース マッピングで使用したダイアログでの場合と同様に、複数のフォルダ マッピングを定義できます。変数 $(SourceDir) は、Team Build が、ビルド エージェントのプロパティ ダイアログでビルド エージェントに対して指定されたビルド ディレクトリに展開する環境変数です。一般には、パスが相対になるように $(SourceDir) を使用します。ただし、Team Build はその使用を強制しません。この例では、現状のままにしています。
最後に、すべてのマッピングが希望どおりに構成された既存のワークスペースがある場合は、[既存のワークスペースのコピー] オプションを使用して、マッピングのバージョン管理フォルダ部分をコピーして時間を節約できます。
ワークスペース設定を定義した後で、ビルド プロジェクト ファイルを定義する必要があります。[プロジェクト ファイル] オプションをクリックすると、既にバージョン管理されている既存の TFSBuild.proj ファイルへのパスを指定できます。TFSBuild.proj は、2005 リリースではすべてのビルド構成データを含んだ XML ファイルであり、実際のビルド プロセスを拡張するようにカスタマイズしたファイルでした。Team Build 2008 では、引き続きビルド カスタマイズにこのファイルを使用しますが、ビルド定義に関連するほとんどのデータは TFS データベースに格納されます。ファイルがまだない場合は、[作成] ボタンをクリックし、単純なウィザードを使用して作成します。
Team Build 2008 での 1 つの大きな変更は、TFSBuild.proj ファイルを格納する場所を指定できるようになったことです。Team Build 2005 では、バージョン管理リポジトリの特定の場所にパスがハードコーディングされていました。既定では、Team Build 2008 ダイアログで、Team Build 2005 互換のパスが %TeamProjectName%/TeamBuildTypes/%BuildDefinitionName% の形式で指定されます。
この現在の例では、提案されるバージョン管理フォルダは $/MSDNMag/TeamBuildTypes/SimpleBuildExplorer-OnDemand となります。パスを変更できることの主な利点は、TFSBuild.proj ファイルを他のアーティファクトと分岐できることです。この例では、既定のパスを受け入れ、[作成] ボタンをクリックします。
[作成] ボタンをクリックすると、ウィザードが表示されます。ウィザードは、Team Build 2005 リリースの UI のうち残された数少ないものの 1 つです。最初のページでは、ビルドする Visual Studio ソリューションの選択を求められます。ウィザードは、表示されるリストをワークスペース マッピングに関連して使用可能なソリューションのみにフィルタします。既定のワークスペース マッピングを変更しない場合は、チーム プロジェクト内のすべてのソリューションが表示されます。提示される 1 つのソリューションを選択し、[次へ] をクリックします。
ウィザードの 2 ページ目では、ビルドする Visual Studio 構成の種類を選択できます。ウィザードでは構成に対して [デバッグ] と [リリース] のみ表示されますが、カスタム構成を入力できます。プラットフォームにも同じルールが適用されます。既定を受け入れて、[次へ] をクリックします。
ウィザードの最後のページでは、テストと静的分析の実行を制御します。テスト実行と静的分析が機能するためには、VSTS をビルド エージェント コンピュータにインストールする必要があります。Team Build 2005 リリースで可能だったのと同様に、テスト メタデータ ファイルを選択し、ここから無人実行をサポートするテスト リストを選択できます。ただし、Team Build で MSTest 互換テストの出力アセンブリを自動的にスキャンし、テスト リストを必要とすることなくそれらを実行することも選択できます。1 つ以上の Visual Studio プロジェクトで静的分析を有効にしていた場合は、Team Build で静的分析を実行することも選択できます。このコラムのサンプル ソリューションにはテストまたは静的分析がないため、[完了] をクリックしてウィザードを完了し、TFSBuild.proj ファイルを作成します。
この時点で必要なのは、ビルドの既定値を入力することだけです。まず、保持ポリシーとトリガ設定を見てみましょう。Team Build 2005 リリースに比べて大きく強化された Team Build 2008 では、ビルド定義保持ポリシーを構成できるようになりました (図 3 を参照)。特定のビルドの結果に基づいて、Team Build で保持するビルド結果の数を制御できます。
図 3 ビルド定義保持ポリシーの制御 (画像をクリックすると拡大表示されます)
ビルドが完了した後で、保持ポリシーに関係なく Team Build で保持されるようにビルド結果をマークできます。この例では、保持ポリシーを既定の設定のままにする必要があります。
Team Build 2008 の [トリガ] オプションをクリックした場合は、Team Build 2005 と同様に動作します。つまり、要求したときにのみビルドが実行されます。ただし、現在は、Team Build で継続的な統合 (CI) もサポートされます (CI の詳細については、2008 年 3 月号の記事「継続的な統合でビルド プロセスを再定義する」を参照してください)。これは、だれかがビルド定義のワークスペース マッピングによってマップされたファイルに対する変更をチェックインするたびに、ビルドがキュー登録されることを意味します。ダイアログでは、[チェックインのたびにビルドする (ビルド数多)] オプションでこの機能が公開されます。
2 つ目のオプションである [前のビルドの完了までチェックインを蓄積する (ビルド数少)] では、遅延形式の CI が提供されます。最後のオプションでは、スケジュールされたビルドを構成できます (図 4 を参照)。
図 4 スケジュールに基づいてビルドを実行するための Team Build の構成 (クリックすると拡大画像が表示されます)
トリガは既定の [チェックインにより新しいビルドをトリガしない] に設定したままにし、[ビルドの既定値] オプションを選択する必要があります。ビルドの既定値を使用して、使用するビルド エージェントとビルド出力のドロップ場所を指定します。ドロップ場所は UNC パスを使用して指定します。マイクロソフトが提供している Virtual PC イメージを使用している場合は、「\\localhost\Drops\MSDNMag\SimpleBuildExplorer-OnDemand」と入力できます。共有のみ存在する必要があります。Team Build によって、必要に応じて追加のサブフォルダが作成されます。ビルド番号を使用して、このパスの下にフォルダが作成されます。
共有を作成する場合は、適切な権限を割り当てる必要があります。少なくとも、Team Build および TFS サービス アカウントには、共有に対する完全制御権が必要です。また、ビルドに関連するテスト結果を発行しようとしているユーザーには、書き込みアクセス許可を付与する必要があります。完成したビルドを取得する必要のあるユーザーには、読み取りアクセス許可を付与する必要があります。
共有を構成したら、ビルド エージェントを指定する必要があります。ビルド エージェントを既に定義してある場合は、コンボボックスから選択できます。定義していない場合は、[新規作成] ボタンをクリックすると、標準の [ビルド エージェントのプロパティ] ダイアログが開きます。この例では新規エージェントを定義する必要があるため、[新規作成] をクリックします。

ビルド エージェントを作成する
Team Build 2005 リリースとは異なり、Team Build 2008 のビルド エージェントはビルド定義に依存しません。エージェントを定義した後は、簡単に再使用できます。[ビルド エージェントのプロパティ] ダイアログで、エージェントに関連する重要なプロパティをすべて定義します。まず、表示名と説明 (省略可能) を入力します。次に、NETBIOS または DNS 名の形式でコンピュータ名を指定します。表示名とコンピュータ名はまったく別のものにすることができます。SSL を使用している場合は、そのオプションを指定できます。
重要な設定は作業ディレクトリです。既定では、$(Temp)\$(BuildDefinitionPath) の形式のパスを使用します。これは多くのビルドに対して動作しますが、MAX_PATH の問題が発生するおそれがあるため問題の原因となります。Windows Server 2003 マシンの $Temp は %SystemDrive%\Documents and Settings\<build agent service account>\Local Settings\Temp となります。これは、使用するには文字数が多すぎます。通常は、Database Professional ビルドをビルドする場合には特に、他のもっと短いパスに変更することをお勧めします。この例では、作業ディレクトリを C:\Builds\$(BuildDefinitionPath) に変更します。
最後に、エージェントのステータスを制御できます。この機能はサービスの提供に特に役立ちます。また、TFS はビルド エージェントと通信できない場合にエージェントのステータスを到達不可に変更して、管理者が設定を変更するまでエージェントを使用不能にします。設定を有効なままにして [OK] をクリックし、完了した [ビルド エージェントのプロパティ] ダイアログを閉じます (図 5 を参照)。[ビルド定義] ダイアログに戻ると、すべての警告三角形が消えており、[OK] をクリックしてビルドの定義を完了できます。
図 5 完了した [ビルド エージェントのプロパティ] ダイアログ
チーム エクスプローラ ウィンドウの [ビルド] フォルダに、ビルド定義が表示されます。これを右クリックすると、直前に使用したのと同じ [ビルド定義] ダイアログに戻る [ビルド定義の編集] オプションが表示されています。また、[新しいビルドをキューに配置] オプションを使用して、新しいビルドをキュー登録できます。
Team Build 2005 とは異なり、Team Build 2008 では、ビルドのキュー登録がサポートされるようになりました。これは、マイクロソフトが継続的な統合を適切にサポートする場合に必要な機能でした。このオプションを選択すると、[ビルドをキューに配置] ダイアログが開き (図 6 を参照)、いくつかのオプションを調整できます。まず、必要に応じて既定のビルド エージェントを別のエージェントに簡単に変更できます。次に、ドロップ場所を調整できます。また、[キュー内の優先度] オプションを調整することで、特定のビルドに "キューをジャンプ" させることができます。最後に、ユーザーがビルドの管理アクセス許可を持っている場合は、MSBuild コマンドライン引数を渡すことができます。
図 6 [ビルドをキューに配置] ダイアログ

チーム ビルドを実行する
[キュー] をクリックしてビルドを開始します。すると、更新されたビルド エクスプローラ ウィンドウが開きます。このウィンドウには 2 つの主要タブがあります。キュー登録済みのタブには、エージェントやステータスに関係なく、キューに配置されているすべてのビルドが既定で表示されます。ビルド定義、ビルドのステータス、またはビルド エージェントに基づいてビルド エクスプローラに表示されるビルドを調整するために使用できるコンボボックスが表示されます。
完了したタブでは、完成したビルドが表示され、同じ種類のフィルタ処理がサポートされます。ビルドを開始すると、ビルドをダブルクリックしてライブ ビルド レポートを表示できます。Team Build によって、ビルド プロセスの進行と共にこのレポートがリアル タイムで更新されます。
チーム ビルドを実行すると、いくつかのアクションが実行されます。TFS がキューからビルドを開始した後、ビルド エージェントはビルドを実行する準備が整います。TFS によってビルド番号が作成され、ビルド エージェントがソースをビルド定義で指定された場所と同期します。エージェントはコードをコンパイルし、必要に応じてコード分析を実行します。続いて、ビルド エージェントはテストを実行します。次に、ビルド番号に関連付けられた作業項目を更新するタスクがエージェントによって実行されます。
たとえば、MSF-Agile テンプレートの既定のタスク作業項目には、Team Build によって更新される [統合ビルド] フィールドがあります。エージェントはコード カバレッジを計算し (テストがビルドの一部として実行されるように構成され、コード カバレッジが有効になっている場合)、最後の正常なビルド (コンパイルが正常に終了) と現在のビルドの間のすべての変更セットを判断します。ファイルがドロップ共有にコピーされ、ビルド結果が TFS にパブリッシュされ、TFS によってビルド完了イベントが起動されます。TFS イベントの詳細については、筆者の 2008 年 5 月号のコラムを参照してください。
テストと作業項目に関連してマイクロソフトが Team Build 2008 で行った重要な変更が 2 つあります。最初の変更は、ビルドの 1 つ以上のテストが失敗してもビルドが失敗しないことです。代わりに、1 つ以上のテストに失敗したビルドは、Team Build 2005 のように [失敗] ではなく、[部分的に完了] として表示されます。次に、Team Build 2005 では、ビルドの初回実行時に、Team Build によって、チーム プロジェクトの開始以降の最初のビルド レポートに、ビルドされた項目に関連するすべての作業項目と変更セットが一覧表示されます。Team Build 2008 は、ビルドが初めて正常に実行された後でのみ、作業項目の表示と更新を開始します。筆者の次回のコラムでは、ビルドのカスタマイズによってこれらの動作を調整する方法を説明します。

Team Build をプログラミングする
Team Build 2008 で行われた大きな拡張の 1 つは、マネージ API の追加です。Microsoft.TeamFoundation.Build.Client.dll は、使用する主要アセンブリです。このコラムのサンプル アプリケーションでは、このアセンブリに加えて、Microsoft.TeamFoundation.Client.dll および Microsoft.TeamFoundation.VersionControl.Client.dll というアセンブリを参照しています。
サンプル アプリケーションは、新しい API の一部を検証する単純な Windows フォーム アプリケーションです。アプリケーションは、TFS に接続し、すべてのチーム プロジェクトの一覧を列挙して、それらをツリー ビュー コントロールに読み込みます。各チーム プロジェクトの下に、Build Agents ノードと Build Definitions ノードが追加されます (図 7 を参照)。
図 7 サンプル アプリケーション (クリックすると拡大画像が表示されます)
ノードを展開すると、TreeNode の BeforeExpand イベントのコードが実行され、展開したノードに応じて使用可能なビルド エージェント、ビルド定義、または完成したビルドが列挙されます。また、メイン フォームには、Property Grid コントロールがホストされます。null 以外のタグ値を持つノードを選択すると、アプリケーションによって参照オブジェクトが Property Grid の SelectedObject プロパティに読み込まれます。図 7 に、MSDNMag チーム プロジェクトが展開され、Main Build Agent が選択されているアプリケーションを示します。
アプリケーションの大部分は単純な列挙コードです。ただし、マイクロソフトがチーム エクスプローラで公開しなかった機能が 1 つあります。ビルド エージェントを定義する場合、TFS はビルド エージェントのスコープを特定のチーム プロジェクトに設定します。一方、多くの組織には 1 つか 2 つのビルド エージェントしかないことがわかりました。このため、最終的にビルド エージェントを複数回再作成する必要があります。サンプル アプリケーションでビルド エージェント インスタンスを右クリックすると、コンテキスト メニューと [Copy Agent To] コマンドが表示されます (図 8 を参照)。
図 8 [Copy Agent To] コマンド
このオプションを選択すると、ビルド エージェント定義のコピー先にするチーム プロジェクトを選択できるダイアログが開きます。該当するコードは、図 9 に示す CopyAgentToToolStripMenuItem_Click イベント ハンドラだけです。
図 9 CopyAgentToToolStripMenuItem_Click メソッド
Private Sub CopyAgentToToolStripMenuItem_Click( _
  ByVal sender As System.Object, _
  ByVal e As System.EventArgs) _
  Handles CopyAgentToToolStripMenuItem.Click

  Dim current As TreeNode = tv.SelectedNode
  Dim ba As IBuildAgent = _
    TryCast(current.Tag, IBuildAgent)

  If ba Is Nothing Then Exit Sub

  Dim parentProject As String = ba.TeamProject

  Using frm As New frmPickProject
    frm.cboProjects.DataSource = _
      (From item In teamProjectNames _
       Where item <> parentProject _
       Select item).ToArray()

    If frm.ShowDialog(Me) = Windows.Forms.DialogResult.OK Then
      Dim teamProjectName As String = _
        frm.cboProjects.SelectedItem.ToString()

      Dim existingAgent = _
        (From item As IBuildAgent In _
         m_bs.QueryBuildAgents(teamProjectName) _
         Where item.Name = ba.Name).SingleOrDefault

      If existingAgent Is Nothing Then
        Dim newBuildAgent As IBuildAgent = _
        ba.CopyTo(teamProjectName)
        newBuildAgent.Save()

        MessageBox.Show(newBuildAgent.Name & " copied.")
      Else
        MessageBox.Show(existingAgent.Name & " already exists.")
      End If
    End If
  End Using
End Sub
選択したノードに、IBuildAgent 参照に設定されたタグがあると想定した場合、コードでは frmPickProject フォームが表示されます。このフォームには、現在のビルド エージェントのチーム プロジェクトを除く、使用可能なすべてのチーム プロジェクトを表示するコンボボックスが含まれます。
コードでは、ツリー ビューのルートを初めて展開したときに収集されるすべてのチーム プロジェクト名の文字列配列に対して LINQ クエリを実行することで、コンボボックスのデータ ソースが設定されます。チーム プロジェクトを選択し、プロジェクト選択フォームで [OK] をクリックしたと想定した場合、コードでは LINQ クエリが実行され、ターゲット チーム プロジェクトにまだ同じ名前のビルド エージェントがないことが確認されます。同じ名前のエージェントが見つかった場合は、メッセージが表示されます。一方、見つからなかった場合は、ビルド エージェント定義がコピーされます。
特に興味深いことの 1 つは、マイクロソフトが CopyTo メソッドを提供していることです。筆者が最初にこのコードを考案したときは、新しい IBuildAgent インスタンスに対してプロパティごとのコピーを行うことを計画していました。マイクロソフトは API を追加し、ユーザー インターフェイスには追加しなかったようです。
Team Build 2008 が非常に強力なビルド製品であることをご理解いただけたならさいわいです。Team Build 2005 を使用している場合は、Team Build 2008 にアップグレードすることに大きな価値があります。筆者の次のコラムでは、ビルドのカスタマイズと、ビルドでソリューションをコンパイルするだけでなくテスト環境に配置する方法などについて掘り下げます。
このコラムの執筆に力を貸してくれた Ken Getz と Martin Woodward に感謝します。

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

Brian A. Randell は、MCW Technologies, LLC のシニア コンサルタントです。マイクロソフト テクノロジについての講演や執筆を行っています。Microsoft MVP として、Pluralsight 社の Applied Team System コースの作成にも携わりました。mcwtech.com/cs/blogs/brianr にブログを公開しています。


{ End Bracket }
文書を書くエンジニア
Richard Ward

channel9.msdn.com/Shows/Behind_The_Code で、Richard Ward の「Behind the Code」のインタビューをご覧ください。

私は、MSDN Magazine のスタッフがコラムの執筆を依頼してきたときに、少し愉快な気持ちになりました。エンジニアはコラムを書くなどと考えていないだろうから、こんな依頼をすると無愛想な対応をされてしまうのではないか、と言わんばかりのスタッフの不安な気持ちが垣間見えたからです。このような状況は予想よりも頻繁に起こりますが、これはもっと一般的な問題を暗示しています。
エンジニア、およびソフトウェアの作成にかかわるすべての人は、言葉でのコミュニケーションができる必要があります。ただし、これは話をするという意味ではありません。口頭でのコミュニケーションはすばらしいものです。ホワイトボードを使ったり、昼食をとりながら少人数で詳細について話し合うことは、設計を完成させるうえで非常に重要です。短時間でやり取りすることで、さまざまな意見を検討し、直ちにフィードバックを得ることができます。しかし、この方法は本質的に正確さに欠けています。会話の中で深められた理解や、会話の参加者によって築かれた共通の状況が、その会話の範囲を超えて影響を及ぼすことはまれです。会話に参加していない人々が使用できるのは文書化されたものだけです。
意見や考えを書き留めることは、開発プロセスにとって不可欠です。ソフトウェア製品の適切な設計レベルを構成する内容については多くの考え方がありますが、最終的に、設計のいくつかの部分は書き留めておく必要があります。バックアップ製品はその復元機能を検証しない限り役に立たないという考え方と同様に、文書化する内容は対象読者が理解できる内容でなければなりません。だれかがドキュメントを読み、その目的が理解できない場合、それどころかまったく間違って理解してしまった場合、プロセスは失敗に終わります。
エンジニアリング ドキュメントを記述するときに何を使用する必要があるかについては、さまざまな考え方があります。私は "目的に適した手段を使う" タイプの人間なので、それについて特に論争を起こすつもりはありません。高度に構造化された正確な言語から、それほど形式的でない自然言語に至るまで、さまざまな言語が使われる可能性があります。たとえば、正確なモデルは、2 つのインスタンス間のやり取り (ネットワーク プロトコルなど) を指定するときに役立ちます。これらの形式は、ある意味、設計を表現する実際のコードから抽出された 1 ステップであるため、設計方法を非常に効果的に表現できます。しかし、設計の理由については説明されません。
自然言語は、本質的に正確さには欠けていますが、実際の設計以外の考えを表現するうえで非常に優れています。建築にたとえると、建築家は、アトリウムの構造や土台、照明の特徴、およびその他の多くのサブシステムとディテールを正確に示す構想を持っている場合があります。これらはすべて、それぞれに特化した図面に描かれています。しかし同様に、建築家には、人々を建物の中に集め、階上に上がって窓のそばを通ってもらうというアトリウムの目的、およびその他の多くのディテールの目的に関して伝えたいことがあるかもしれません。このような目的を最もうまく表現するのは技術的でない言語です。これにより、読者は自分なりの理解を深め、設計の他の部分を理解するための基礎とすることができます。
このことは、検討されているドキュメントの対象読者の範囲が広いほど、より明確になります。あるエンジニアリング スタッフは、コンポーネントの動作について詳細に書かれた正確なドキュメントに感謝するかもしれませんが、チームの他のスタッフには別のニーズがあります。設計の構想は、原案から始まって、組織全体に至るまでには段々と広がってくるため、新しい読者とのコミュニケーションで求められるものは多くなっていきます。初歩レベルの設計文書を顧客に提供する必要はめったにありませんが、多くの場合、求められる内容を、多様な読者が理解しやすく、さまざまなレベルの読者にアピールできる方法で記述された形式にしておくと便利です。
最近、私は設計文書の中の、多くのコンポーネントに影響を与える変更の提案について確認するよう別のチームから依頼されました。この文書は非常に詳細に書かれており、変更されるほぼすべての機能、メモリ フットプリント、読み込み順序について明記されていました。すべてが慎重に調べられていました。しかし、1 つ不足していたのは、なぜこの変更が行われるかという説明です。提供されているデータはすべて申し分のないものでしたが、その評価の枠組みがありませんでした。このプロジェクトのエンジニアは作業にかなり深く携わっていたため、変更が提案された理由をすべての読者が把握していると思い込んでいたのです。この文書では、提案された変更 (高度に構造化された言語で記述) の説明を掲載した序文 (自然言語で記述) が大いに役立ちました。
エンジニアは文書を書くことに不信感を持つ必要はありません。書くことは、作業の重要な部分です。製品を完成させるには、他の人と情報をやり取りできる必要があります。より使い慣れた形式があるかもしれませんが、実際に必要なのは、伝えたい内容を読者が受け取って理解することです。そもそも、自然言語ほどわかりやすい表現で自分の言いたいことを伝えられなかったとしたら、まったく正確かつ厳格なコンピュータからあなたの望む結果が得られるでしょうか。

Richard Ward は、マイクロソフトの Distinguished Engineer です。コア オペレーティング システム部門の一環としてコア アーキテクチャ チームにも参加しており、そこで分量の多いドキュメントをよく執筆しています。