 MSDN Magazine March 2008

MSDN Magazine March 2008

ASP.NET MVC: Web フォームを使用しないで Web アプリケーションを作成する

Chris Tavares は、ASP.NET MVC Framework の Model View Controller パターンが、柔軟性に富み簡単にテストできる Web アプリケーションの作成にどのように役立つかを説明しています。

Chris Tavares 疎結合: ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める

James Kovacs が、密結合アーキテクチャの負の側面、およびそのテストが困難な理由と適応上の制限について説明します。また、いくつかのソリューションを提案します。

James Kovacs CI サーバー: 継続的な統合でビルド プロセスを再定義する

Jay Flowers が、個別のツールやより包括的な CI Factory ソリューションを使用した継続的な統合サーバーの設定方法と使い方を説明します。

Jay Flowers パフォーマンス: Visual Studio プロファイラでアプリケーションのボトルネックを見つける

パフォーマンス調査のサンプルを検討し、サンプル アプリケーション内のコードの非効率な箇所を特定しながら Visual Studio プロファイラについて説明します。

Hari Pulapaka and Boris Vidolov Office 開発: 実世界での OBA ソリューション パターン

OBA ソリューション パターンは、アーキテクトと開発者が Office Business Application (OBA) を構築するのに役立ちます。この記事では、中核となる 7 つの OBA ソリューション パターンを紹介し、その 1 つを現実の問題点に適用します。

Steve Fox 編集者のメモ: スイミング プールが多すぎる

今月は、Howard Dierking が現実離れしたアプリケーション デザインへの警告を発します。

Howard Dierking ツールボックス: 簡単な Web デバッグ、Reflector の拡張、その他

今月の内容は、Web アプリケーションのデザイン時における操作感の向上、Scott Guthrie のブログ、Lutz Roeder の Reflector の拡張などです。

Scott Mitchell CLR 徹底解剖: アプリケーションの起動パフォーマンスを向上させる

アプリケーションの起動パフォーマンスはユーザーにとって重要な問題です。パフォーマンスを向上させるにはさまざまな方法があります。何から始めればよいかを見ていきましょう。

Claudio Caldato データ ポイント: LINQ の標準クエリ演算子

LINQ to Objects および LINQ to Entities には、シーケンスに対してさまざまな操作を実行する標準クエリ演算子が用意されています。

John Papa Advanced Basics: Office 2007 のファイルと LINQ

2007 Office system Open XML ドキュメントのデータを取得したり編集したりする場合、LINQ to XML および Microsoft SDK for Open XML Formats を使用すると、ドキュメントの構成要素である "パーツ" に対して、短く簡潔なコードで簡単にアクセスできます。

Ken Getz Office Space: STSDEV を使用して SharePoint の開発を単純化する

新しい STSDEV ユーティリティを活用した簡単で再利用可能な手順を用いて、Visual Studio で SharePoint 開発プロジェクトを設定および展開する方法を、Ted Pattison が紹介します。

Ted Pattison テストの実行: Windows PowerShell を使用した Web UI テストの自動化

ここでは、Windows PowerShell を使用して、ASP.NET および従来の ASP Web アプリケーション用にすばやく簡単な UI テスト自動化を作成する方法を紹介します。

Dr. James McCaffrey セキュリティに関するブリーフィング: Visual C++ の防御機能でコードを保護する

Michael Howard が、Visual C++ 2005 以降で使用可能なバッファ オーバーラン防御機能の概要を説明します。

Michael Howard すてきな ASP.NET: 必要な唯一のデータバインド コントロール

Fritz Onion は、CSS によるスタイル設定、柔軟なページネーション、および並べ替え、挿入、削除、更新機能の完全な補完をサポートする ASP.NET 3.5 の ListView コントロールによって、データバインド タスクがどれほど簡単になるかを示しています。

Fritz Onion 基礎: ベクタ グラフィックスと WPF の Shape クラス

WPF で 2 次元ベクタ グラフィックスをレンダリングするための名前空間として、Charles Petzold は System.Windows.Shapes を使用することにしました。その理由をここで説明します。

Charles Petzold .NET の問題: 非同期ストリーム処理

今月は、非同期ストリーム処理について、Stephen Toub が説明します。

Stephen Toub { End Bracket }: ALT .NET とは

Jeremy D. Miller が、ALT .NET ムーブメントの目的について説明します。

Jeremy D. Miller


ASP.NET MVC: Web フォームを使用しないで Web アプリケーションを作成する

ASP.NET MVC

Web フォームを使用しないで Web アプリケーションを作成する

Chris Tavares
この記事は、ASP.NET MVC Framework のプレリリース版に基づいて書かれています。ここに記載されている情報は変更される可能性があります。
この記事の内容 : :
  • Model View Controller パターン
  • コントローラとビューの作成
  • フォームの構築とポストバック
  • コントローラ ファクトリとその他の拡張ポイント
この記事は次のテクノロジを使用しています:
ASP.NET
コードのダウンロード : : MVCFramework2008_03.exe (189KB)
コンテンツ
Model View Controller パターン
コントローラを作成する
ビューを作成する
より大きな例
フォームを作成し、ポストバックする
コントローラの作成
その他の拡張ポイント
Web フォームとはお別れか

筆者は現時点で約 15 年の経験を持つプロフェッショナルな開発者であり、その前の少なくとも 10 年は趣味で開発を行う個人ユーザーでした。同世代のほとんどの人と同様に、8 ビット マシンから開始し、PC プラットフォームに移行しました。複雑さを増していくマシンで徐々に技術を向上させ、小さなゲームから個人データ管理、さらには外部ハードウェアの制御まで、あらゆることを行うアプリケーションを作成しました。

しかし、経歴の最初の半分については、作成したすべてのソフトウェアに共通する点が 1 つありました。それは、ユーザーのデスクトップで実行するローカル アプリケーションであることです。90 年代の初めに、World Wide Web と呼ばれる新しいものについて耳にするようになりました。当時、実際に作業現場からオフィスに足を引きずって戻らなくてもタイムカード情報を入力できるようにする Web アプリケーションを作成する機会がありました。

その経験は、一言で言えば困惑でした。ステートレス Web への取り組みは、筆者のデスクトップ指向の考え方になじみませんでした。うんざりするデバッグ、ルート アクセス権のない UNIX サーバー、そして見慣れない山かっこの中身が追加され、若かった筆者は恥ずかしながらさらに数年間はデスクトップ開発に戻りました。

筆者は Web 開発から離れました。明らかに重要であるにもかかわらず、筆者が本当に理解していなかったのは、プログラミング モデルです。その後、Microsoft® .NET Framework と ASP.NET がリリースされました。最終的に、Web アプリケーションに取り組めて、デスクトップ アプリケーションのプログラミングとほぼ同じであるフレームワークが登場しました。ウィンドウ (ページ) を作成でき、コントロールをイベントに関連付けることができ、デザイナではいまいましい山かっこを処理する必要がなくなりました。最もすばらしいのは、ASP.NET がビュー ステートで Web のステートレスな性質を自動的に処理してくれることです。筆者は幸せなプログラマに戻りました。少なくともしばらくの間は。

経験を積むにつれて、設計における選択肢も増えました。デスクトップ アプリケーションに取り組むときに適用できるいくつかのベスト プラクティスも学習しました。その 2 つは次のとおりです。

  • 懸念事項の分離 : UI ロジックと基になる動作を混在させないこと
  • 自動化された単体テスト : コードが予想どおりに動作するかどうかを確認する自動化されたテストを作成すること

ここに示した基本原則は、テクノロジに関係なく適用されます。懸念事項の分離は、複雑さを処理するのに役立つ基本的な原則です。残りの労働時間の計算、データの書式設定、グラフの描画など、異なる役割を同じオブジェクト内に混在させると、メンテナンスの問題が発生します。また、自動化されたテストは、既存のプロジェクトを更新しているときには特に、正気を保ったまま実稼働品質のコードを作成するのに不可欠です。

ASP.NET Web フォームによって非常に簡単に着手できるようになりましたが、その他の方法では、Web アプリケーションにこの設計原則を適用するのが困難でした。Web フォームは執拗なまでに UI 中心です。基本単位はページです。まず、UI をデザインし、コントロールをドラッグします。(Windows® アプリケーションに対して有効になっている Visual Basic® と同じように) アプリケーション ロジックをページのイベント ハンドラに渡すだけでよいのは非常に魅力的です。

それに加え、ページの単体テストは難しい場合がほとんどです。ASP.NET のすべてをスピンアップせずに、ライフサイクル全体を通して Page オブジェクトを実行することはできません。HTTP 要求をサーバーに送信するかブラウザを自動化することで Web アプリケーションをテストすることもできますが、その種のテストは脆弱で (1 つのコントロール ID を変更するとテストが無効になります)、設定が難しく (すべての開発者のマシンで完全に同じようにサーバーを設定する必要があります)、実行が低速です。

より高度な Web アプリケーションの構築を開始したとき、コントロール、ビュー ステート、ページ ライフサイクルなど、Web フォームが提供する抽象化は役に立つどころか苛立つものでした。データ バインドの構成 (および正しく構成するための多数のイベント ハンドラの作成) には非常に多くの時間を費やしました。ページを高速に読み込むためにビュー ステートのサイズを削減する方法を調べる必要がありました。Web フォームでは、物理的なファイルがすべての URL に存在する必要があります。これは動的サイト (wiki など) によって難しくなります。また、カスタム WebControl を正しく作成するのは際立って複雑なプロセスであり、ページ ライフサイクルと Visual Studio® デザイナの両方に関する広範囲の知識が必要です。

マイクロソフトで働くようになってから、.NET のさまざまな苦痛の種について学んだことを共有し、その苦痛のいくつかを和らげることを期待できる機会がありました。最近のこのような機会は、patterns & practices Web Client Software Factory プロジェクト (codeplex.com/websf) に開発者として参加することで生じました。特に、patterns & practices が成果物に組み込んだものの 1 つは、自動化された単体テストです。Web Client Software Factory で、筆者は、Model View Presenter (MVP) パターンを使用してテスト可能な Web フォームを構築することを提案しました。

簡単に言うと、ロジックをページに挿入する代わりに、MVP でページを作成し、ページ (ビュー) が Presenter という別のオブジェクトを呼び出すようにします。Presenter オブジェクトは、通常は他のオブジェクト (Model など) を使用してデータベースにアクセスしたり、ビジネス ロジックを実行したりすることなどによって、ビューに対するアクティビティに応答するために必要なロジックを実行します。これらの手順が完了すると、Presenter はビューを更新します。このアプローチによって、プレゼンタが ASP.NET パイプラインから分離されるため、テストが容易になります。インターフェイスを通じてビューと通信し、ページから分離してテストできます。

MVP は動作しますが、実装は少し厄介な場合があります。独立したビュー インターフェイスが必要であり、分離コード ファイルに多数のイベント転送関数を作成する必要があります。しかし、Web フォーム アプリケーションにテストが容易な UI が必要な場合には、やってみるのが最善です。改良には、基礎となるプラットフォームの変更が必要です。

Model View Controller パターン

さいわい、ASP.NET チームは筆者のような開発者の話に耳を傾け、新しい Web アプリケーション フレームワークの開発を開始しました。このフレームワークは、皆さんがよくご存知で好んでいる Web フォームと共存しますが、明らかに異なる設計目標を持っています。

  • HTTP と HTML を利用し、これを隠さない。
  • テスト容易性が最初から組み込まれている。
  • ほとんどすべてのポイントで拡張できる。
  • 出力を完全に制御できる。

この新しいフレームワークは、Model View Controller (MVC) パターンに基づいているため、ASP.NET MVC という名前が付いています。MVC パターンは、元々は 70 年代に Smalltalk の一部として考案されました。この記事で示すように、実際に Web の性質に非常によく適合します。MVC は、UI を 3 つの異なるオブジェクトに分割します。それは、入力を受け取って処理するコントローラ、ドメイン ロジックを含むモデル、および出力を生成するビューです。Web のコンテキストでは、入力は HTTP 要求であり、要求フローは図 1 のようになります。

Figure 1 MVC パターンの要求フロー
Figure 1 MVC パターンの要求フロー(画像を縮小するには、ここをクリックします)
Figure 1 MVC パターンの要求フロー
Figure 1 MVC パターンの要求フロー (画像を拡大するには、ここをクリックします)

これは、実際には Web フォームのプロセスとかなり異なります。Web フォーム モデルでは、入力はページ (ビュー) に送られ、ビューは入力の処理と出力の生成の両方を行います。一方 MVC の場合は、役割が分かれています。

ここで、おそらく皆さんの頭に浮かんでいるのは次の 2 つのいずれかでしょう。「それはすばらしい。どうやって使うのかな」でしょうか。それとも、「以前は 1 つしか作成する必要のなかったオブジェクトをどうして 3 つも作成しなければならないのだろうか」でしょうか。どちらもすばらしい疑問であり、例を見るのが最もよい説明になります。そのため、MVC Framework を使用した小さな Web アプリケーションを作成して、その利点を示します。

コントローラを作成する

先に進むためには、Visual Studio 2008 をインストールし、MVC Framework を入手する必要があります。この記事の執筆時点では、2007 年 12 月の ASP.NET Extensions の Community Technology Preview (CTP) の一部として入手できます (asp.net/downloads/3.5-extensions)。非常に有用なヘルパ オブジェクトを含む拡張 CTP と MVC Toolkit の両方を入手できます。CTP をダウンロードしてインストールした後で、ASP.NET MVC Web アプリケーションという新しいプロジェクトの種類が [新しいプロジェクト] ダイアログに作成されます。

MVC Web アプリケーション プロジェクトを選択すると、通常の Web サイトまたはアプリケーションとは少々異なる外観のソリューションが与えられます。ソリューション テンプレートは、(図 2 に示すように) いくつかの新しいディレクトリと共に Web アプリケーションを作成します。具体的には、Controllers ディレクトリにはコントローラ クラスが含まれ、Views ディレクトリ (およびそのすべてのサブディレクトリ) にはビューが含まれます。

Figure 2 MVC プロジェクトの構造
Figure 2 MVC プロジェクトの構造

これから、URL で渡された名前を返す非常に単純なコントロールを作成します。[コントローラ] フォルダを右クリックし、[項目の追加] を選択すると、MVC Controller クラスやいくつかの MVC View コンポーネントなどが新たに追加された通常の [新しい項目の追加] ダイアログが表示されます。ここでは、非常に想像力に富んだ HelloController という名前のクラスを追加します。

using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Controllers
{
    public class HelloController : Controller
    {
        [ControllerAction]
        public void Index()
        {
            ...
        }
    }
}

コントローラ クラスは、ページよりもずっと軽量です。実際に、本当に必要なのは、System.Web.Mvc.Controller から派生させ、アクション メソッドに [ControllerAction] 属性を追加することだけです。アクションは、特定の URL に対する要求への応答として呼び出されるメソッドです。アクションは、必要な処理の実行と、ビューのレンダリングを行います。まず、次に示すように、ビューに名前を渡す単純なアクションを作成します。

[ControllerAction]
 public void HiThere(string id)
 {
     ViewData["Name"] = id;
     RenderView("HiThere");
 }

アクション メソッドは、id パラメータを通じて URL から名前を受け取り (詳細は後述します)、それを ViewData コレクションに格納してから、HiThere という名前のビューをレンダリングします。

このメソッドの呼び出し方法やビューの外観を説明する前に、テストの容易性について説明します。Web フォーム ページ クラスをテストするのがどれほど難しいかについての筆者の発言を覚えていますか。そうです、コントローラの方がはるかに簡単にテストできます。実際に、追加のインフラストラクチャがなくても、コントローラを直接インスタンス化し、アクション メソッドを呼び出すことができます。HTTP コンテキストは不要で、単なるテスト ハーネスとしてのサーバーも不要です。例として、このクラスの Visual Studio Team System (VSTS) 単体テストを図 3 に示します。

Figure 3 Controller Unit Test (Close Figure 3)
namespace HelloFromMVC.Tests
{
    [TestClass]
    public class HelloControllerFixture
    {
        [TestMethod]
        public void HiThereShouldRenderCorrectView()
        {
            TestableHelloController controller = new
              TestableHelloController();
            controller.HiThere("Chris");

            Assert.AreEqual("Chris", controller.Name);
            Assert.AreEqual("HiThere", controller.ViewName);
        }

    }

    class TestableHelloController : HelloController
    {
        public string Name;
        public string ViewName;

        protected override void RenderView(
            string viewName, string master, object data)
        {
            this.ViewName = viewName;
            this.Name = (string)ViewData["Name"];
        }
    }

}

ここで行うことはいくつかあります。実際のテストは非常に簡単です。コントローラをインスタンス化し、予期されるデータでメソッドを呼び出し、正しいビューがレンダリングされたことを確認します。RenderView メソッドを上書きするテスト固有のサブクラスを作成することで、この確認を行います。これにより、実際の HTML 作成を省略できます。留意するのは、正しいデータがビューに送信されたことと、正しいビューがレンダリングされたことだけです。このテストでは、ビューの基礎となる詳細は気にしません。

ビューを作成する

もちろん最終的にはなんらかの HTML を生成する必要があるため、その HiThere ビューを作成しましょう。そのためには、まずソリューションで Views フォルダの下に Hello という名前の新しいフォルダを作成します。既定では、コントローラは Views\<ControllerPrefix> フォルダ内でビューを探します (コントローラ プレフィックスは、コントローラ クラスの名前から "Controller" という単語を取り除いたものです)。したがって、HelloController によってレンダリングされるビューの場合は、Views\Hello を探します。最終的に、ソリューションは図 4 のようになります。

Figure 4 プロジェクトへのビューの追加
Figure 4 プロジェクトへのビューの追加(画像を縮小するには、ここをクリックします)
Figure 4 プロジェクトへのビューの追加
Figure 4 プロジェクトへのビューの追加 (画像を拡大するには、ここをクリックします)

ビューの HTML は次のようになります。

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Hi There!</title>
</head>
<body>
    <div>
        <h1>Hello, <%= ViewData["Name"] %></h1>
    </div>
</body>
</html>

いくつかのことが目に留まります。runat="server" タグがありません。form タグもありません。コントロール宣言もありません。実際に、これは ASP.NET よりも従来の ASP によく似ています。MVC ビューは出力の生成のみ行うため、Web フォーム ページが行うイベント処理や複雑な制御は不要です。

MVC Framework は、便利なテキスト テンプレート作成言語として .aspx ファイル形式を借用します。必要に応じて分離コードも使用できますが、既定では分離コード ファイルは次のようになります。

using System;
using System.Web;
using System.Web.Mvc;

namespace HelloFromMVC.Views.Hello
{
    public partial class HiThere : ViewPage
    {
    }
}

ページの Init メソッドや load メソッドはなく、イベント ハンドラもありません。基本クラスの宣言以外には何もありませんが、この基本クラスは Page ではなく ViewPage です。MVC ビューに必要なものはこれがすべてです。アプリケーションを実行し、http://localhost:<port>/Hello/HiThere/Chris にナビゲートすると、図 5 のような画面が表示されます。

Figure 5 正常な MVC ビュー
Figure 5 正常な MVC ビュー(画像を縮小するには、ここをクリックします)
Figure 5 正常な MVC ビュー
Figure 5 正常な MVC ビュー (画像を拡大するには、ここをクリックします)

図 5 の代わりに、厄介に見える例外が発生しても慌てないでください。F5 キーを押したときに Visual Studio にアクティブなドキュメントとして HiThere.aspx ファイル セットがある場合は、Visual Studio が .aspx ファイルに直接アクセスしようとします。MVC ビューでは、表示の前にコントローラを実行する必要があるため、ページに直接ナビゲートしようとしても動作しません。図 5 に表示されている内容に合わせて URL を編集すると、正常に動作します。

MVC Framework は、アクション メソッドの呼び出し方法をどのようにして知るのでしょうか。その URL のファイル拡張子さえありませんでした。その答えは URL ルーティングです。global.asax.cs ファイルの内部を見ると、図 6 のコード チャンクがあります。グローバル RouteTable は、Route オブジェクトのコレクションを格納します。各 Route は、URL フォームと、そのフォームで行う処理を記述します。既定では、2 つのルートがテーブルに追加されます。最初のルートは魔法のような処理を行います。つまり、サーバー名の後に 3 つの部分が続く各 URL について、最初の部分がコントローラ名、2 番目の部分がアクション名、3 番目の部分が ID パラメータとして取得されます。

Figure 6 Route Table (Close Figure 6)
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        // Change Url= to Url="[controller].mvc/[action]/[id]"
        // to enable automatic support on IIS6

        RouteTable.Routes.Add(new Route
        {
            Url = "[controller]/[action]/[id]",
            Defaults = new { action = "Index", id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });

        RouteTable.Routes.Add(new Route
        {
            Url = "Default.aspx",
            Defaults = new {
                controller = "Home",
                action = "Index",
                id = (string)null },
            RouteHandler = typeof(MvcRouteHandler)
        });
    }
}

Url = "[controller]/[action]/[id]"

この既定のルートは、HiThere メソッドで有効にされて呼び出されます。http://localhost/Hello/HiThere/Chris という URL を覚えていますか。このルートは Hello をコントローラにマップし、HiThere をアクションにマップし、Chris を ID にマップしました。MVC Framework は HelloController インスタンスを作成し、HiThere メソッドを呼び出し、Chris を ID パラメータの値として渡しました。

この既定のルートは多くの機能を与えてくれますが、独自のルートを追加することもできます。たとえば、パーソナライズされたあいさつ文に対して自分の名前を入力することのみ必要な非常にフレンドリーなサイトが必要だとします。このルートをルーティング テーブルの一番上に追加します。

  RouteTable.Routes.Add(new Route
  {
    Url = "[id]",
    Defaults = new {
        controller = "Hello",
        action = "HiThere" },
    RouteHandler = typeof(MvcRouteHandler)
  });

すると、http://localhost/Chris にアクセスするだけでアクションが呼び出され、見慣れたフレンドリーなあいさつ文が表示されます。

システムは、呼び出すコントローラとアクションをどのようにして知ったのでしょうか。その答えは Defaults パラメータです。このパラメータは、新しい C# 3.0 匿名型を使用して擬似辞書を作成します。Route の Defaults オブジェクトには任意の追加情報を含めることができますが、MVC の場合は、いくつかの既知のエントリを含めることもできます。それは、コントローラとアクションです。URL で指定されたコントローラまたはアクションがない場合は、Defaults 内の名前が使用されます。そのため、これらを URL から省いても、要求は正しいコントローラとアクションにマッピングされます。

注意しておく点がもう 1 つあります。「テーブルの一番上に追加する」と言ったのを覚えていますか。一番下に追加するとエラーが発生します。ルーティングは上から順に機能します。URL を処理するときに、ルーティング システムはテーブルを上から順に調べ、最初に一致したルートが使用されます。この場合は、アクションと ID の既定値があるため、既定のルート "[controller]/[action]/[id]" が一致します。したがって、ChrisController を探し、コントローラがないためエラーになります。

より大きな例

MVC Framework の基礎については説明したので、文字列を表示する以外のことを行うより大きな例を示します。wiki はブラウザで編集できる Web サイトです。ページは簡単に追加または編集できます。MVC Framework を使用して小さなサンプル wiki を作成しました。[Edit this page] 画面を図 7 に示します。

Figure 7 ホーム ページの編集
Figure 7 ホーム ページの編集(画像を縮小するには、ここをクリックします)
Figure 7 ホーム ページの編集
Figure 7 ホーム ページの編集 (画像を拡大するには、ここをクリックします)

この記事のコードをダウンロードして、基になる wiki ロジックがどのように実装されているかを確認できます。ここでは、MVC Framework によって Web への wiki の表示がどれほど簡単になるかに注目します。まず、URL 構造を設計します。目的は次のとおりです。

  • /[pagename] は、その名前のページを表示します。
  • /[pagename]?version=n は、要求されたバージョンのページを表示します。0 = 最新バージョン、1 = 1 つ前のバージョンなどです。
  • /Edit/[pagename] は、そのページの編集画面を開きます。
  • /CreateNewVersion/[pagename] は、編集内容を送信するためにポストされる URL です。

wiki ページの基本的な表示から始めましょう。これを行うために、WikiPageController という名前の新しいクラスを作成しました。次に、ShowPage という名前のアクションを追加しました。WikiPageController は、最初は図 8 のようになります。ShowPage メソッドはかなり単純です。WikiSpace クラスと WikiPage クラスは、それぞれ一連の wiki ページと特定のページ (およびそのリビジョン) を表します。このアクションは、単にモデルを読み込んで RenderView を呼び出します。しかし、そこにある "new WikiPageViewData" 行は何でしょうか。

Figure 8 WikiPageController Implementation of ShowPage (Close Figure 8)
public class WikiPageController : Controller
{
  ISpaceRepository repository;

  public ISpaceRepository Repository
  {
    get {
      if (repository == null)
      {
        repository = new FileBasedSpaceRepository(
            Request.MapPath("~/WikiPages"));
      }
      return repository;
    }

    set { repository = value; }
  }

  [ControllerAction]
  public void ShowPage(string pageName, int? version)
  {
    WikiSpace space = new WikiSpace(Repository);
    WikiPage page = space.GetPage(pageName);

    RenderView("showpage",
      new WikiPageViewData
      {
        Name = pageName,
        Page = page,
        Version = version ?? 0
      });
  }
}

前の例では、コントローラからビューにデータを受け渡す 1 つの方法を示しました。それは ViewData 辞書です。辞書は便利ですが、危険もあります。辞書には何でも含めることができ、コンテンツに対する IntelliSense® はありません。また、ViewData 辞書はそのコンテンツを使用するために Dictionary<string, object> 型となっているため、すべてをキャストする必要があります。

ビューに必要なデータがわかっている場合は、代わりに厳密に型指定された ViewData オブジェクトを渡すことができます。この例では、図 9 に示すように、WikiPageViewData という単純なオブジェクトを作成しました。このオブジェクトは、wiki マークアップの HTML バージョンの取得などを行ういくつかのユーティリティ メソッドと共に wiki ページ情報をビューに渡します。

Figure 9 WikiPageViewData Object (Close Figure 9)
public class WikiPageViewData {

    public string Name { get; set; }
    public WikiPage Page { get; set; }
    public int Version { get; set; }

    public WikiPageViewData() {
        Version = 0;
    }

    public string NewVersionUrl {
        get {
            return string.Format("/CreateNewVersion/{0}", Name);
        }
    }

    public string Body {
        get { return Page.Versions[Version].Body; }
    }

    public string HtmlBody {
        get { return Page.Versions[Version].BodyAsHtml(); }
    }

    public string Creator {
        get { return Page.Versions[Version].Creator; }
    }

    public string Tags {
        get { return string.Join(",", Page.Versions[Version].Tags); }
    }
}

これでビュー データが定義されましたが、どのように使用するのでしょうか。ShowPage.aspx.cs を見るとわかります。

namespace MiniWiki.Views.WikiPage {
    public partial class ShowPage : ViewPage<WikiPageViewData>
    {
    }
}

基本クラスを ViewPage<WikiPageViewData> 型に定義したことに注目してください。これは、ページの ViewData プロパティが WikiPageViewData 型であり、前の例のような Dictionary ではないことを意味します。

.aspx ファイル内の実際のマークアップは非常に単純です。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true" CodeBehind="ShowPage.aspx.cs"
  Inherits="MiniWiki.Views.WikiPage.ShowPage" %>
<asp:Content
  ID="Content1"
  ContentPlaceHolderID="MainContentPlaceHolder"
  runat="server">
  <h1><%= ViewData.Name %></h1>
  <div id="content" class="wikiContent">
    <%= ViewData.HtmlBody %>
  </div>
</asp:Content>

ViewData を参照するときに、インデックス付け演算子 [] を使用していないことに注意してください。代わりに、厳密に型指定された ViewData があるため、プロパティに直接アクセスできます。キャストは不要であり、Visual Studio が IntelliSense を提供します。

注意してみると、このファイルに <asp:Content> タグがあることがわかります。そうです。マスタ ページは MVC ビューと連動します。そして、マスタ ページはビューになることもできます。マスタ ページ分離コードを見てみましょう。

namespace MiniWiki.Views.Layouts
{
    public partial class Site :
        System.Web.Mvc.ViewMasterPage<WikiPageViewData>
    {
    }
}

関連付けられているマークアップを図 10 に示します。ここでは、マスタ ページはビューが取得するのとまったく同じ ViewData オブジェクトを取得します。マスタ ページの基本クラスは ViewMasterPage<WikiPageViewData> として宣言したため、適切な型の ViewData があります。その後、さまざまな DIV タグを設定してページをレイアウトし、バージョン リストを入力し、通常のコンテンツ プレースホルダで終わります。

Figure 10 Site.Master (Close Figure 10)
<%@ Master Language="C#"
  AutoEventWireup="true"
  CodeBehind="Site.master.cs"
  Inherits="MiniWiki.Views.Layouts.Site" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<%@ Import Namespace="MiniWiki.DomainModel" %>
<%@ Import Namespace="System.Web.Mvc" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title><%= ViewData.Name %></title>
  <link href="http://../../Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="inner">
    <div id="top">
      <div id="header">
        <h1><%= ViewData.Name %></h1>
      </div>
      <div id="menu">
        <ul>
          <li><a href="http://Home">Home</a></li>
          <li>
            <%= Html.ActionLink("Edit this page",
                  new { controller = "WikiPage",
                        action = "EditPage",
                        pageName = ViewData.Name })%>
        </ul>
      </div>
    </div>
    <div id="main">
      <div id="revisions">
        Revision history:
        <ul>
          <%
            int i = 0;
            foreach (WikiPageVersion version in ViewData.Page.Versions)
            { %>
              <li>
                <a href="http://<%= ViewData.Name %>?version=<%= i %>">
                  <%= version.CreatedOn %>
                  by
                  <%= version.Creator %>
                </a>
              </li>
          <%  ++i;
          } %>
        </ul>
      </div>
      <div id="maincontent">
        <asp:ContentPlaceHolder
          ID="MainContentPlaceHolder"
          runat="server">
        </asp:ContentPlaceHolder>
      </div>
    </div>
  </div>
</body>
</html>

また、Html.ActionLink の呼び出しにも注目してください。これはレンダリング ヘルパの例です。さまざまなビュー クラスには、Html および Url という 2 つのプロパティがあります。それぞれに、HTML のチャンクを出力する便利なメソッドがあります。この場合は、Html.ActionLink がオブジェクトを取得し (ここでは匿名型)、ルーティング システムを通じて実行します。これにより、指定したコントローラとアクションにルーティングする URL が生成されます。このようにして、ルートをどのように変更しても、[Edit this page] リンクは常に正しい場所を指します。

また、リンク (以前のページ バージョンへのリンク) を手動で構築する手段も必要であることがわかります。残念ながら、現在のルーティング システムは、クエリ文字列が含まれる URL の生成に対してうまく機能しません。この問題はフレームワークの今後のバージョンで修正されます。

フォームを作成し、ポストバックする

では、コントローラに対する EditPage アクションを見てみましょう。

[ControllerAction]
public void EditPage(string pageName)
{
  WikiSpace space = new WikiSpace(Repository);
  WikiPage page = space.GetPage(pageName);

  RenderView("editpage",
    new WikiPageViewData {
      Name = pageName,
      Page = page });
}

この場合も、アクションはあまり処理を行いません。与えられたページでビューをレンダリングするだけです。図 11 に示すビューはもっとおもしろくなります。このファイルは HTML フォームを構築していますが、Runat="server" はありません。Url.Action ヘルパは、フォームがポストバックする URL の生成に使用されます。TextBox、TextArea、SubmitButton などのさまざまな HTML ヘルパもいくつか使用されています。これらはほぼ期待どおりのことを行います。つまり、さまざまな入力フィールドの HTML を生成します。

Figure 11 EditPage.aspx (Close Figure 11)
<%@ Page Language="C#"
  MasterPageFile="~/Views/Shared/Site.Master"
  AutoEventWireup="true"
  CodeBehind="EditPage.aspx.cs"
  Inherits="MiniWiki.Views.WikiPage.EditPage" %>
<%@ Import Namespace="System.Web.Mvc" %>
<%@ Import Namespace="MiniWiki.Controllers" %>
<asp:Content ID="Content1"
  ContentPlaceHolderID="MainContentPlaceHolder"
  runat="server">
  <form action="<%= Url.Action(
    new { controller = "WikiPage",
    action = "NewVersion",
    pageName = ViewData.Name })%>" method=post>
    <%
      if (ViewContext.TempData.ContainsKey("errors"))
      {
    %>
    <div id="errorlist">
      <ul>
      <%
        foreach (string error in
          (string[])ViewContext.TempData["errors"])
        {
      %>
        <li><%= error%></li>
      <% } %>
      </ul>
    </div>
    <% } %>
    Your name: <%= Html.TextBox("Creator",
                   ViewContext.TempData.ContainsKey("creator") ?
                   (string)ViewContext.TempData["creator"] :
                   ViewData.Creator)%>
    <br />
    Please enter your updates here:<br />
    <%= Html.TextArea("Body", ViewContext.TempData.ContainsKey("body") ?
        (string)ViewContext.TempData["body"] :
        ViewData.Body, 30, 65)%>
    <br />
    Tags: <%= Html.TextBox(
              "Tags", ViewContext.TempData.ContainsKey("tags") ?
              (string)ViewContext.TempData["tags"] :
              ViewData.Tags)%>
    <br />
    <%= Html.SubmitButton("SubmitAction", "OK")%>
    <%= Html.SubmitButton("SubmitAction", "Cancel")%>
  </form>
</asp:Content>

Web プログラミングで処理するのが面倒なものの 1 つに、フォーム上のエラーがあります。より具体的に言うと、エラー メッセージを表示する一方で、前に入力されたデータを保持する必要があります。私たちは皆、35 個のフィールドのあるフォームで間違いを犯し、大量のエラー メッセージと新しい空白フォームのみ表示されたという経験を持ちます。MVC Framework は、以前に入力された情報を格納してフォームを再設定できるようにするための場所として TempData を提供しています。ViewState では、コントロールのコンテンツの保存がほぼ自動化されているため、Web フォームでのこの処理が非常に簡単になります。

これは MVC でも行う必要があり、ここで TempData が役立ちます。TempData は、型のない ViewData とよく似た辞書です。ただし、TempData のコンテンツは 1 つの要求に対してのみ存続し、その後は削除されます。この使用方法を確認するために、図 12 の NewVersion アクションを見てみましょう。

Figure 12 NewVersion Action (Close Figure 12)
[ControllerAction]
public void NewVersion(string pageName) {
  NewVersionPostData postData = new NewVersionPostData();
  postData.UpdateFrom(Request.Form);

  if (postData.SubmitAction == "OK") {
    if (postData.Errors.Length == 0) {
      WikiSpace space = new WikiSpace(Repository);
      WikiPage page = space.GetPage(pageName);
      WikiPageVersion newVersion = new WikiPageVersion(
        postData.Body, postData.Creator, postData.TagList);
      page.Add(newVersion);
    } else {
      TempData["creator"] = postData.Creator;
      TempData["body"] = postData.Body;
      TempData["tags"] = postData.Tags;
      TempData["errors"] = postData.Errors;

      RedirectToAction(new {
        controller = "WikiPage",
        action = "EditPage",
        pageName = pageName });
      return;
    }
  }

  RedirectToAction(new {
    controller = "WikiPage",
    action = "ShowPage",
    pageName = pageName });
}

まず、NewVersionPostData オブジェクトを作成します。これは、ポストといくつかの検証のコンテンツを格納するプロパティとメソッドを持つ別のヘルパ オブジェクトです。postData オブジェクトを読み込むために、MVC Toolkit のヘルパ関数を使用しています。UpdateFrom は、実際にはツールキットによって提供されている拡張メソッドであり、リフレクションを使用してフォーム フィールドの名前をオブジェクトのプロパティの名前と照合します。最終的に、すべてのフィールド値が postData オブジェクトに読み込まれます。ただし、UpdateFrom を使用すると、HttpRequest からフォーム データを直接取得するため単体テストが困難になるというデメリットがあります。

NewVersion が最初にチェックするのは SubmitAction です。ユーザーが [OK] ボタンをクリックし、編集したページを実際にポストする場合はこれでかまいません。他の値がここにある場合は、アクションが最終的に ShowPage にリダイレクトされるため、元のページが再表示されるだけです。

ユーザーが [OK] をクリックした場合は、postData.Errors プロパティをチェックします。これによって、ポストのコンテンツに対して単純な検証が実行されます。エラーがない場合は、ページの新しいバージョンを wiki に記述するための処理を行います。一方、エラーがある場合はおもしろいことになります。

エラーがある場合は、TempData 辞書のさまざまなフィールドを設定して、PostData のコンテンツが含まれるようにします。次に、[Edit] ページにリダイレクトします。ここで、TempData が設定されているため、ページには、ユーザーが前回ポストした値で初期化されたフォームが再表示されます。

ポスト、検証、および TempData を処理するこのプロセスは、現在は少々面倒で、実際に必要であるより多くの手動処理が必要です。将来のリリースには、TempData のチェックの少なくとも一部を自動化するヘルパ メソッドが含まれるようになります。最後に TempData について注意しておきます。TempData のコンテンツはユーザーのサーバー側セッションに格納されます。セッションをオフにした場合は TempData が動作しません。

コントローラの作成

wiki の基礎は動作するようになりましたが、実装には、先に進む前にクリーンアップが必要な未完成の点がいくつかあります。たとえば、Repository プロパティは wiki のロジックを物理的な記憶域と分離するために使用されます。ファイル システム (ここで使用しました)、データベース、またはその他任意の目的の場所にコンテンツを格納するリポジトリを提供できます。あいにく、解決が必要な問題が 2 つ生じました。

まず、コントローラ クラスは具象 FileBasedSpaceRepository クラスと緊密に結合されています。プロパティが設定されていない場合でも適切に使用できるものが存在するように、既定値が必要です。さらに悪いことに、ディスク上のファイルへのパスもここでハードコーディングされています。少なくとも、この内容は構成から取得する必要があります。

第 2 に、リポジトリは実際には必須の依存関係です。オブジェクトはリポジトリがないと動作しません。優れた設計では、リポジトリをプロパティではなくコンストラクタ パラメータにする必要があることが指定されます。ただし、MVC Framework ではコントローラに引数なしのコンストラクタが必要であるため、これをコンストラクタに追加できません。

さいわい、この束縛から逃れることのできる拡張フックがあります。それはコントローラ ファクトリです。コントローラ ファクトリは、名前が示すとおりのことを行います。つまり、Controller インスタンスを作成します。必要なのは、IControllerFactory インターフェイスを実装し、それを MVC システムに登録するクラスを作成することだけです。コントローラ ファクトリは、すべてのコントローラまたは特定の種類のコントローラに対して登録できます。図 13 に、リポジトリをコンストラクタ パラメータとして渡す WikiPageController のコントローラ ファクトリを示します。

Figure 13 Controller Factory (Close Figure 13)
public class WikiPageControllerFactory : IControllerFactory {

  public IController CreateController(RequestContext context,
    Type controllerType)
  {
    return new WikiPageController(
      GetConfiguredRepository(context.HttpContext.Request));
  }

  private ISpaceRepository GetConfiguredRepository(IHttpRequest request)
  {
    return new FileBasedSpaceRepository(request.MapPath("~/WikiPages"));
  }
}

この場合、実装はきわめて簡単ですが、これにより、(依存関係注入コンテナの場合は特に) はるかに強力なツールを使用するコントローラを作成できます。いずれにしても、コントローラの依存関係をオブジェクトに分離して管理と保守を簡単にすることについて詳細をすべて入手できました。

この作業の最後の手順は、フレームワークへのファクトリの登録です。この処理は、Global.asax.cs の Application_Start メソッド (ルートの前または後) に次の行を追加することにより、ControllerBuilder クラスを通じて行います。

ControllerBuilder.Current.SetControllerFactory(
  typeof(WikiPageController), typeof(WiliPageControllerFactory));

これにより、WikiPageController のファクトリが登録されます。このプロジェクトに他のコントローラがある場合、それらはこのファクトリを使用しません。このファクトリは、WikiPageController 型についてのみ登録されているためです。すべてのコントローラに使用するファクトリを設定する必要がある場合は、SetDefaultControllerFactory を呼び出すこともできます。

その他の拡張ポイント

コントローラ ファクトリは、フレームワーク拡張の始まりでしかありません。この記事ではそのすべてを詳細に説明するスペースがないため、ハイライトのみ示します。まず、HTML 以外のものを出力する場合、または Web フォーム以外のテンプレート作成エンジンを使用する場合は、コントローラの ViewFactory を他のものに設定できます。IViewFactory インターフェイスを実装し、出力の生成方法を完全に制御できます。これは、RSS、XML、またはグラフィックスの生成に役立ちます。

既に説明したように、ルーティング システムは柔軟性に優れています。ただし、ルーティング システムには MVC に固有のものがありません。すべてのルートには RouteHandler プロパティがあります。今までは、これを常に MvcRouteHandler に設定していました。しかし、IRouteHandler インターフェイスを実装し、ルーティング システムを他の Web テクノロジにフックできます。フレームワークの将来のドロップには WebFormsRouteHandler が付属し、将来は他のテクノロジが汎用ルーティング システムを利用できるようになります。

コントローラは、System.Web.Mvc.Controller から派生する必要がありません。コントローラが行う必要があるのは、IController インターフェイスの実装だけです。このインターフェイスには、Execute という単一のメソッドだけがあります。そこから、どのような操作でも行うことができます。一方、Controller 基本クラスの動作のいくつかを調整するだけの場合、Controller にはオーバーライドできる多くの仮想関数があります。

  • OnPreAction、OnPostAction、および OnError では、実行されるすべてのアクションに対して汎用的な前処理および後処理をフックします。OnError では、コントローラ全体のエラー処理メカニズムが提供されます。
  • HandleUnknownAction は、コントローラがルートで要求されているアクションを実装していない場合に、URL がコントローラにルーティングされたときに呼び出されます。既定では、このメソッドは例外をスローしますが、任意の処理を行うようにオーバーライドできます。
  • InvokeAction は、呼び出すアクション メソッドを調べて、そのアクション メソッドを呼び出すメソッドです。(たとえば [ControllerAction] 属性の必要性をなくすために) プロセスをカスタマイズする場合は、このメソッドで行います。

Controller には他にもいくつかの仮想メソッドがありますが、基本的には拡張ポイントとしてではなくテスト フックとして存在しています。たとえば、RedirectToAction は、実際にはリダイレクトしない派生クラスを作成できるため仮想的です。これにより、完全な Web サーバーを実行しなくても、リダイレクトするアクションをテストできます。

Web フォームとはお別れか

この時点で、「Web フォームはどうなるのか。MVC によって置換されるのか」と疑問を感じているかもしれません。答えはノーです。Web フォームは汎用的なテクノロジであり、マイクロソフトはサポートと拡張を継続します。Web フォームが非常によく機能する多くのアプリケーションがあります。たとえば、典型的なイントラネット データベース レポート作成アプリケーションは、Web フォームを使用すると、MVC での作成に要する時間の数分の 1 で作成できます。さらに、Web フォームは広大なコントロールの市場をサポートしています。コントロールの多くは非常に洗練されており、作業時間を大幅に節約できます。

それでは、Web フォームではなく MVC を選択する必要があるのはどのような場合でしょうか。たいていは、要件と優先事項によって決まります。意図したとおりの URL を生成するのに苦労していますか。UI を単体テストしますか。このいずれかのシナリオでは、MVC に軍配が上がります。一方、編集可能なグリッドと手の込んだツリー ビュー コントロールを使用して大量のデータ表示を行いますか。その場合は、おそらく今のところは Web フォームのほうがよいでしょう。

いずれ MVC Framework は UI コントロール部門で遅れを取り戻すと思われますが、機能のほとんどが単なるドラッグ アンド ドロップである Web フォームほど簡単に使い始められるようにはならない可能性が高いでしょう。しかし、ASP.NET MVC Framework は Web 開発者に Microsoft .NET Framework で Web アプリケーションを構築する新しい方法を提供します。Framework はテストを容易にするように設計され、抽象化を試みる代わりに HTTP を利用し、あらゆる点で拡張可能です。Web アプリケーションの完全な制御を望む開発者にとっては、Web フォームを強力に補完します。

Chris Tavares は、マイクロソフトの patterns & practices チームの開発者であり、マイクロソフト プラットフォームにシステムを構築するためのベスト プラクティスを開発コミュニティが理解できるよう努めています。また、ASP.NET MVC チームの仮想メンバでもあり、新しいフレームワークの設計を支援しています。連絡先は cct@tavaresstudios.com です。

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


疎結合: ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める

疎結合

ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める

James Kovacs
この記事の内容 : :
  • 密結合アーキテクチャの短所
  • テストと依存関係の問題
  • 依存関係の逆転
  • 依存関係の注入
この記事は次のテクノロジを使用しています:
.NET Framework
コードのダウンロード : : DependencyInjection2008_03.exe (5408KB)
コンテンツ
内部依存関係の問題
依存関係の逆転
依存関係の注入
コンテナ
完全な IoC コンテナ
有効期間管理
依存関係の自動接続
結合を緩和して変更に対応する

疎結合設計を目指すのは好ましくないということに異論を挟む人は少ないと思います。しかしながら、一般に、設計は意図したよりはるかに密接に結合されます。設計が密結合しているかどうかはどうやって確認したらよいでしょうか。NDepend などの静的分析ツールを使用して依存関係を分析することもできますが、アプリケーションの結合を感知する最も簡単な方法は、クラスのうちの 1 つを単独でインスタンス化してみることです。

ビジネス層から InvoiceService などのクラスを選択し、そのコードを新しいコンソール プロジェクトにコピーするだけです。これをコンパイルしてみてください。Invoice や InvoiceValidator などの一部の依存関係が失われる可能性があります。これらのクラスをコンソール プロジェクトにコピーして再度コンパイルを試行してください。他にも不足しているクラスが見つかる可能性があります。最終的にコンパイルが成功すると、コードベースの大部分が新しいプロジェクトに含まれている可能性もあります。それは、まるでセーターのほつれた糸がほどけていくのを眺めているようなものです。設計上のすべてのクラスが他のすべてのクラスに直接または間接に結合されています。このようなシステムの変更は、1 つのクラスの変更がさざなみのようにシステムのその他の全体に影響する可能性があるため、非常に困難です。

重要なことは、結合を完全に回避することではありません。そんなことは不可能です。たとえば、次のような場合を考えてみましょう。

string name = "James Kovacs";

これは、自作のコードを Microsoft® .NET Framework の System.String クラスに結合したものです。何か問題があるでしょうか。筆者は問題ないと考えています。System.String が好ましくない方法で変更されたり、System.String の操作方法の変更が必要になるような要件の変更が発生したりする可能性はきわめて低いと考えられます。したがって、この結合に不満はありません。重要なのは、結合をなくすことではなく、結合を意識して賢明に選択することです。

もう 1 つの例として、多くのアプリケーションのデータ層で見られる次のようなコードを検討してみましょう。

SqlConnection conn = new SqlConnection(connectionString);

また、次のようなコードも考えられます。

XmlDocument settings = new XmlDocument();
settings.Load("Settings.xml");

データ層で SQL Server® のみにアクセスするか、それとも常に XML ドキュメント Settings.xml からアプリケーション設定を読み込むようにするかについてどの程度自信を持っていますか。ここでの目的は、無限に拡張可能できわめて複雑な、実用に耐えない汎用的なフレームワークを構築することではありません。重要なことは可逆性です。設計上の決定をどの程度簡単に変更できるでしょうか。アプリケーション アーキテクチャは変更への対応力に優れていますか。

変更にこだわる理由とはなんでしょうか。それは、このシステム産業で唯一不変なものが変化だからです。要件、テクノロジ、開発者、ビジネスはいずれも変化します。これらの変化に対応する準備はできていますか。疎結合設計により、ソフトウェアは不可避で予測のつかないことが多い変化に対応しやすくなります。

内部依存関係の問題

まず、平均的な階層型アプリケーション アーキテクチャに見られる一般的な、堅く結合された設計を検討していきます (図 1 を参照)。単純な階層化スキームでは UI 層がサービス (またはビジネス) 層にアクセスし、サービス (またはビジネス) 層がリポジトリ (またはデータ) 層にアクセスします。これらの階層間の依存関係は同様の方法で下位に移動します。リポジトリ層はサービス層を意識せず、サービス層は UI 層を意識しません。

Figure 1 一般的な階層型アーキテクチャ
Figure 1 一般的な階層型アーキテクチャ

前述の階層以外にプレゼンテーション層やワークフロー層などの階層が存在する場合もありますが、下位層のみを意識するという階層パターンはかなり一般的です。階層を一貫した役割分担のクラスタとして扱う設計手法は優れています。ただし、上位層から下位層に直接結合すると結合の数が増え、アプリケーションのテストが難しくなります。

テストの容易性に注目する理由はなんでしょうか。それは、結合の優れたバロメーターになるためです。テスト時にクラスを簡単にインスタンス化できない場合は結合の問題が存在します。たとえば、サービス層はリポジトリ層と密接に関連し、リポジトリ層に依存します。サービス層をリポジトリ層から切り離してテストすることはできません。そのため、実用上、多くのテストでは基盤となるデータベース、ファイル システム、またはネットワークにアクセスします。これにより、テストに時間がかかる、保守コストが高くつくなどのさまざまな問題が生じます。

テストに時間がかかる テストを厳密にメモリ内で実行できれば、1 テストあたりミリ秒の範囲で完了可能です。テストでデータベース、ファイル システム、ネットワークなどの外部リソースにアクセスすると、多くの場合、1 テストあたり 100 ミリ秒以上かかります。テスト カバレッジが十分なプロジェクトであれば、一般にテスト項目は数百～数千件に及ぶということを考えると、テストの実行時間は数秒で終わるか、数分ないし数時間かかるかの違いになります。

エラーの切り離しが不十分 データ層コンポーネントでエラーが発生すると、多くの場合、上位層コンポーネントのテストもエラーになります。数件のテスト エラーで済めば問題を即座に切り離すことが可能ですが、エラーが数百件も発生すれば問題の特定が難しくなり、時間もかかります。

保守コストが高い 多くのテストには初期データが必要です。テストでデータベースにアクセスする場合は、各テストの前にデータベースが既知の状態になっていることを確認する必要があります。また、各テストの初期データが他のテストの初期データから独立していることも確認する必要があります。そうしないと、順序が変わると一定のテストが失敗するというテスト順序の問題に直面することになります。データベースを既知の適切な状態に保つ操作は手間がかかり、間違いやすい作業です。

また、下位層の実装を変更する必要がある場合、上位層が下位層に暗黙的または明示的に依存していることが多いため、上位層も変更せざるを得なくなります。アプリケーションを階層化しても、疎結合は実現していません。

具体的な例として、請求書を受領するサービスを見ていきましょう (図 2 を参照)。InvoiceService.Submit は、請求書の提出を受け入れることができるようにするため、クラスのコンストラクタで作成された AuthorizationService、InvoiceValidator、および InvoiceRepository に依存しています。InvoiceService の単体テストを行うには、具象的な依存関係が必要です。そのため、単体テストを実行する前にデータベースの状態を確認し、InvoiceRepository が新しい請求書を挿入したときに主キーまたは一意キーの違反が発生しないこと、および InvoiceValidator で検証エラーが通知されないことを確かめる必要があります。また、単体テストを実行するユーザーに適切なアクセス許可があり、AuthorizationService が "送信" 操作を許可するようになっていることも確認する必要があります。

Figure 2 Invoice Service (Close Figure 2)
public class InvoiceService {
  private readonly AuthorizationService authoriazationService;
  private readonly InvoiceValidator invoiceValidator;
  private readonly InvoiceRepository invoiceRepository;

  public InvoiceService() {
    authoriazationService = new AuthorizationService();
    invoiceValidator = new InvoiceValidator();
    invoiceRepository = new InvoiceRepository();
  }

  public ValidationResults Submit(Invoice invoice) {
    ValidationResults results;
    CheckPermissions(invoice, InvoiceAction.Submit);
    results = ValidateInvoice(invoice);
    SaveInvoice(invoice);
    return results;
  }

  private void CheckPermissions(Invoice invoice, InvoiceAction action) {
    if(authoriazationService.IsActionAllowed(invoice, action) == false) {
      throw new SecurityException(
        "Insufficient permissions to submit this invoice");
    }
  }

  private ValidationResults ValidateInvoice(Invoice invoice) {
    return invoiceValidator.Validate(invoice);
  }

  private void SaveInvoice(Invoice invoice) {
    invoiceRepository.Save(invoice);
  }
}

これは無理難題です。依存関係があるこれらのコンポーネントのいずれかにコード エラーまたはデータ エラーを引き起こすような問題があれば、InvoiceService のテストで予期しない失敗が起こります。テストが成功しても、データベースに適切なデータを設定し、テストを実行し、テストによって作成されたデータをクリーンアップするまでの総実行時間は数百ミリ秒かかります。テストをバッチにグループ化し、バッチの前後にスクリプトを実行するようにして設定およびクリーンアップにかかる時間を削減しても、メモリ内でテストを実行した場合より実行時間ははるかに長くなります。

また、さらに微妙な問題もあります。それは、InvoiceRepository の監査サポートを追加する場合に AuditingInvoiceRepository を作成するか、InvoiceRepository 自体を変更せざるを得なくなるということです。InvoiceService とその子コンポーネントが結合しているため、新しい機能をシステムに導入するためのオプションは限られます。

依存関係の逆転

上位コンポーネント InvoiceService を下位の依存関係から切り離すには、具象クラスではなく、インターフェイスを使用して依存関係を操作します。

public class InvoiceService : IInvoiceService {
    private readonly IAuthorizationService authService;
    private readonly IInvoiceValidator invoiceValidator;
    private readonly IInvoiceRepository invoiceRepository;
    ...
}

インターフェイス (または抽象基本クラス) を使用するようにしたこの単純な変更により、任意の依存関係の代替実装を置き換えることができます。InvoiceRepository を作成する代わりに AuditingInvoiceRepository を作成することができます (AuditingInvoiceRepository が IInvoiceRepository を実装していることが前提です)。また、テスト時にフェイクやモックを置き換えることもできます。この設計手法をコントラクトに対するプログラミングと呼びます。

この記事で上位コンポーネントと下位コンポーネントの切り離しに適用している原則を依存関係の逆転原則と呼びます。Robert C. Martin もこのトピックに関する記事 (objectmentor.com/resources/articles/dip.pdf) の中でこう指摘しています。"上位モジュールが下位モジュールに依存することは好ましくありません。上位モジュールも下位モジュールも抽象化に依存するようにする必要があります"。

この例の場合、InvoiceService および InvoiceRepository は IInvoiceRepository による抽象化に依存するようになりました。ただし、これで問題が完全に解決したわけではありません。まだ問題を移行しただけです。具象実装がインターフェイスに依存するようになっただけで、具象クラスがどのような方法で相互を "認識" するかという問題が残っています。

InvoiceService にはまだ依存関係の具象的な実装が必要です。単純に InvoiceService のコンストラクタでこれらの依存関係をインスタンス化することもできますが、それでは以前の状態から改善されません。AuditingInvoiceRepository を使用するとしても、相変わらず InvoiceService を変更して AuditingInvoiceRepository をインスタンス化する必要があります。また、IInvoiceRepository に依存するすべてのクラスを変更して、AuditingInvoiceRepository をインスタンス化する必要があります。InvoiceRepository と AuditingInvoiceRepository をグローバルに置き換える簡単な方法はありません。

1 つの解決策として、IInvoiceRepository インスタンスの作成にファクトリを使用する方法があります。これにより、ファクトリ メソッドを変更するだけで、1 か所で AuditingInvoiceRepository に切り替えることができます。この方法をサービス ロケーションとも呼び、インスタンスを管理するファクトリ クラスをサービス ロケータと呼びます。

public InvoiceService() {
  this.authorizationService =
    ServiceLocator.Find<IAuthorizationService>();
  this.invoiceValidator = ServiceLocator.Find<IInvoiceValidator>();
  this.invoiceRepository = ServiceLocator.Find<IInvoiceRepository>();
}

ServiceLocator 内の機能には、構成ファイルまたはデータベースから読み取ったデータを使用するか、コードに直接接続することができます。どちらの場合にも依存関係のオブジェクトは集中的に作成されます。

切り離されたコンポーネントを単体テストするには、実際の実装ではなく、フェイクまたはモック オブジェクトを使用してサービス ロケータを構成します。そこで、たとえば、テスト時に ServiceLocator.Find<IInvoiceRepository> が FakeInvoiceRepository を返す場合があります。FakeInvoiceRepository は、請求書の保存時に既知の主キーを割り当てますが、請求書を実際にデータベースに保存するわけではありません。複雑な設定やデータベースの破棄を不要にし、フェイク依存関係から既知のデータを返すことができます (補足記事「依存関係のフェイク化は有効か」を参照)。

ただし、サービス ロケーションにはいくつかの短所があります。まず、依存関係が上位クラスに隠蔽されています。InvoiceService が AuthorizationService、InvoiceValidator、または InvoiceRepository に依存していることは公開署名からはわかりません。コードを確認する必要があります。

同じインターフェイスの別の具象タイプを指定する必要がある場合、オーバーロードされた Find メソッドを使用する必要があります。この場合、ファクトリ クラスの実装時に代替型を必須にするかどうかの選択が必要です。たとえば、特定の IInvoiceRepository の要求の展開時に ServiceLocator を再構成して AuditingInvoiceRepository を置き換えることはできません。このような短所はありますが、サービス ロケーションはわかりやすく、依存関係をハードコーディングするよりは優れた方法です。

依存関係の注入
依存関係のフェイク化は有効か

依存関係を "フェイク化" することの安全性に疑問を持たれるかもしれません。見かけ上だけ成功する場合があるのではないかということです。この場合、実際に InvoiceRepository などの依存関係が適切に機能しているかどうかを検証するテストは済んでいることが前提です。実際のデータベースにアクセスして InvoiceRepository が正しく動作していることを確認する作業はこれらのテストで済ませておく必要があります。

InvoiceRepository.Save の動作を確認済みであれば、InvoiceRepository に依存するすべてのテストを再実行する理由がどこにあるのでしょう。データベースに接続することで上位レベルのテスト時間が長くなり、InvoiceRepository に問題がある場合は InvoiceRepository のテストが失敗するばかりでなく、InvoiceService のテストや InvoiceRepository に依存するその他のコンポーネントのテストも失敗します。

InvoiceService のテストが失敗し、InvoiceRepository のテストが失敗しなかった場合、InvoiceRepository のテストが不足していることを意味します。この種の不具合の捕捉には、コンポーネントの具象的な依存関係をテストする統合テストの方が適しています。統合テストの方が実行に時間がかかりますが、フェイクまたはモックの依存関係を使用した単体テストより実行回数が少なく済みます。

単体テストは成功したので、InvoiceRepository は正しく動作するものと仮定すると、2 つの選択肢があります。1 つは、複雑なスクリプトを作成し、管理することにより、データベース内のデータの正確性を確保し、InvoiceRepository から各 InvoiceService のテストに予期したとおりのデータが返されることを確認することです。もう 1 つは、予期したとおりのデータを返す InvoiceRepository のフェイクまたはモック実装を作成することです。後者のオプションの方が簡単で、実用的です。

上位コンポーネントの単体テストを行う場合に、依存関係のフェイクまたはモック実装を使用する必要があります。このとき、サービス ロケータにフェイクまたはモックを構成して上位コンポーネントで検索するのではなく、パラメータ化したコンストラクタで依存関係を上位コンポーネントに直接渡すだけで済みます。この方法を依存関係の注入と呼びます。図 3 に例を示します。

Figure 3 Dependency Injection (Close Figure 3)
[Test]
public void CanSubmitNewInvoice() {
  Invoice invoice = new Invoice();
  ValidationResults validationResults = new ValidationResults();
  IAuthorizationService authorizationService =
    mockery.CreateMock<IAuthorizationService>();
  IInvoiceValidator invoiceValidator =
    mockery.CreateMock<IInvoiceValidator>();
  IInvoiceRepository invoiceRepository =
    mockery.CreateMock<IInvoiceRepository>();

  using(mockery.Record()) {
    Expect.Call(authorizationService.IsActionAllowed(
      invoice, InvoiceAction.Submit)).Return(true);

    Expect.Call(invoiceValidator.Validate(invoice))
      .Return(validationResults);
    invoiceRepository.Save(invoice);
  }

  using(mockery.Playback()) {
    IInvoiceService service = new InvoiceService(authorizationService,
      invoiceValidator, invoiceRepository);
    service.Submit(invoice);
  }
}

この例では、InvoiceService の依存関係のモック オブジェクトを作成し、InvoiceService コンストラクタに渡します。モック オブジェクト フレームワークの詳細については、Mark Seemann の「単体テスト : テスト代替の連続性について検討する」(msdn.microsoft.com/msdnmag/issues/07/09/MockTesting) を参照してください。要するに、テストの実行後に InvoiceService の状態を確認するのではなく、モックの操作方法を定義することによって InvoiceService の動作を指定します。

依存関係の注入を使用することにより、単体テストで上位コンポーネントに簡単に依存関係を指定できます。ただし、アプリケーションの実行時や統合テスト時に、単体テストの外部でクラスの依存関係をどうやって見つけるかという問題が残ります。UI 層がサービス層に依存関係を指定したり、サービス層がリポジトリ層に依存関係を指定することを期待するのは不合理です。そんなことをしたら、問題をさらに悪化させる結果になります。それでも、UI 層がサービス層に依存関係を指定すると仮定してみましょう。

// Somewhere in UI Layer
InvoiceSubmissionPresenter presenter =
  new InvoiceSubmissionPresenter(
    new InvoiceService(
      new AuthorizationService(),
      new InvoiceValidator(),
      new InvoiceRepository()));

おわかりのように、UI は固有の依存関係だけでなく、その依存関係に対する依存関係もデータ層に至るまで無限に認識する必要があります。これは明らかに望ましい状態ではありません。この問題を解決する一番簡単な方法は、"貧者のための" 依存関係の注入を呼び出すことです。

貧者のための依存関係の注入では、次のように、上位コンポーネントの既定のコンストラクタを使用して依存関係を指定します。

public InvoiceService() :
  this(new AuthorizationService(),
    new InvoiceValidator(),
    new InvoiceRepository()) { }

最もオーバーロードされたコンストラクタにデリゲートする方法に注目してください。これにより、インスタンスの作成にどのコンストラクタを使用しても、クラスの初期化ロジックが同一になります。クラスを具象的な依存関係に結合する処理は、既定のコンストラクタ 1 か所で行われます。単体テスト時にクラスの依存関係を指定可能なオーバーロードされたコンストラクタがまだ存在するため、クラスは引き続きテストできます。

コンテナ

次に、依存関係を集中管理する場所を提供する、制御の反転 (IoC: inversion of control) コンテナについて説明します。コンテナは、実際にはインターフェイスの実装タイプに対する便利な辞書にすぎません。最も単純な形式では、IoC コンテナはサービス ロケータの単なる別称です。コンテナが単なるサービス ロケーションと比べてどれほど多くの機能を持っているかについては後述します。

当面の問題に戻って、InvoiceService をその依存関係の具象実装から完全に切り離すとします。ソフトウェアのあらゆる問題と同様、間接的な層を追加することで問題を解決できます。依存関係リゾルバの概念を導入し、インターフェイスを具象実装にマップします。次に、インターフェイス T を受け取る汎用メソッドを使用して、そのインターフェイスを実装する型を取得します。

public interface IDependencyResolver {
    T Resolve<T>();
}

では、辞書を使用してインターフェイスとそのインターフェイスを実装するオブジェクトとのマッピング情報を保存する SimpleDependencyResolver を実装してみましょう。最初に辞書を設定する手段が必要です。その処理は Register<T>(object obj) メソッドで行います (図 4 を参照)。依存関係を登録するのは SimpleDependencyResolver の作成者のみであるため、Register メソッドは IDependencyResolver インターフェイス上に存在する必要はありません。通常、この処理はアプリケーションの起動時に Main メソッド内で呼び出されるヘルパ クラスで行います。

Figure 4 SimpleDependencyResolver (Close Figure 4)
public class SimpleDependencyResolver : IDependencyResolver
{
  private readonly Dictionary<Type, object> m_Types =
    new Dictionary<Type, object>();

  public T Resolve<T>() {
    return (T)m_Types[typeof(T)];
  }

  public void Register<T>(object obj) {
    if(obj is T == false) {
      throw new InvalidOperationException(
        string.Format("The supplied instance does not implement {0}",
        typeof(T).FullName));
    }
    m_Types.Add(typeof(T), obj);
  }
}

CompanyService で SimpleDependencyResolver を検索してその依存関係を見つけるにはどうしたらよいでしょうか。IDependencyResolver を必要とする各クラスに渡すことも可能ですが、この方法はすぐに煩雑になります。最も簡単な解決法は、構成済みの SimpleDependencyResolver インスタンスをグローバルにアクセス可能な場所に配置することです。静的ゲートウェイ パターンを使用すればそれが可能です (シングルトン パターンを使用することも可能ですが、シングルトンはテストが難しいことで知られています。シングルトンは見かけ上のグローバル変数程度の役割しか果たさないため、密結合コードのテストが難しい最大の原因の 1 つです。なるべく使用を避けてください)。

では、静的ゲートウェイの説明に移ります。この静的ゲートウェイを IoC と呼びます。DependencyResolver という名前でもよいのですが、IoC の方が簡単です。IoC 上の静的メソッドは IDependencyResolver 上のメソッドと一致します (静的クラスでインターフェイスを実装することはできないため、IoC は IDependencyResolver を実装しません)。実際の IDependencyResolver を受け取る Initialize メソッドもあります。IoC 静的ゲートウェイは単純にすべての Resolve<T> 要求を構成済み IDependencyResolver に転送します。

public class IoC {
  private static IDependencyResolver s_Inner;

  public static void Initialize(IDependencyResolver resolver) {
    s_Inner = resolver;
  }

  public static T Resolve<T>() {
    return s_Inner.Resolve<T>();
  }
}

アプリケーションの起動時に、IoC を構成済みの SimpleDependencyResolver で初期化します。これで、既定のコンストラクタ内で貧者のための依存性の注入を IoC.Resolve に置き換えることができます。

public InvoiceService() :
  this(IoC.Resolve<IAuthorizationService>(),
  IoC.Resolve<IInvoiceValidator>(),
  IoC.Resolve<IInvoiceRepository>()) { }

アプリケーションの起動後は読み取り専用で、更新されることはないため、内部 IDependencyResolver へのアクセスを同期する必要はありません。

IoC クラスには、アプリケーション内で破壊防止層の役割を果たすという利点もあります。異なる IoC コンテナを使用する場合、IDependencyResolver を実装するアダプタを実装する必要があります。IoC をアプリケーション全体で広範囲に使用する場合でも、特定のコンテナには結合していません。

完全な IoC コンテナ

SimpleDependencyResolver などの単純な IoC コンテナでは、疎結合コンポーネントを連結できます。ただし、完全な IoC コンテナには以下をはじめとする多くの機能が不足しています。

  • XML、コード、スクリプトなどのより多様な構成オプション
  • シングルトン、一時的、スレッド単位、プール済みなどの有効期間管理
  • 依存関係の自動接続
  • 新しい機能に接続する機能

これらの機能の詳細について説明します。具体的な例として、一般的なオープン ソース IoC コンテナ、Castle Windsor を使用します。多くのコンテナは外部 XML ファイルで構成できます。たとえば、Windsor は次のように構成することが可能です。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <components>
    <component id="Foo"
      service="JamesKovacs.IoCArticle.IFoo, JamesKovacs.IoCArticle"
      type="JamesKovacs.IoCArticle.Foo, JamesKovacs.IoCArticle"/>
  </components>
</configuration>

XML 構成はアプリケーションを再コンパイルせずに変更可能であるという利点がありますが、変更を適用するには、多くの場合にアプリケーションの再起動が必要です。XML 構成の短所はこれだけではなく、冗長な傾向がある、実行するまでエラーが検出されない、汎用型を一般的な C# の汎用表記ではなく、CLR のバッククォート表記で宣言する、などの問題もあります (Company.Application.IValidatorOf<Invoice> は Company.Application.IValidatorOf`1[[Company.Application.Invoice, Company.Application]], Company.Application と記述されます)。

XML 以外に、Windsor を C# などの Microsoft .NET Framework 準拠言語で構成することもできます。構成のコードを独立したアセンブリに切り離してある場合、構成を変更後、構成アセンブリを再コンパイルしてアプリケーションを再起動するだけで済みます。

Binsor を使用して Windsor の構成スクリプトを作成できます。Binsor は、Windsor の構成専用のドメイン固有言語 (DSL) です。Binsor を使用して構成ファイルを Boo で作成することもできます (Boo は、言語やコンパイラの拡張を重視した、静的に型指定された CLR 言語であるため、DSL の作成に適しています)。Binsor で前述の XML 構成ファイルを次のように変更可能です。

import JamesKovacs.IoCArticle
Component("Foo", IFoo, Foo)

Boo が本格的なプログラミング言語であることを理解すると、さらに関心が深まります。Binsor を使用すれば、Windsor の型を自動登録でき、XML ベースの構成のようにコンポーネントの登録を手動で追加する必要がありません。

import System.Reflection
serviceAssembly = Assembly.Load("JamesKovacs.IoCArticle.IoCContainer")
for type in serviceAssembly.GetTypes():
  continue if type.IsInterface or type.IsAbstract or
    type.GetInterfaces().Length == 0
  Component(type.FullName, type.GetInterfaces()[0], type)

Boo に詳しくない場合でも、このコードの目的は理解する必要があります。新しいサービスを JamesKovacs.IoCArticle.Services 名前空間に追加するだけで、サービスはサービス インターフェイスの既定の実装として自動的に登録されます。たとえば、次のようなクラスを作成します。

public class AuthorizationService : IAuthorizationService {
   ...
}

その他のクラスが IAuthorizationService への依存関係をコンストラクタに渡すパラメータとして追加することによって宣言する場合、Binsor は依存関係を自動的に接続するため、構成ファイルにその依存関係を明示的に指定する必要はありません。Binsor の詳細については、ayende.com/Blog/category/451.aspx を参照してください。また、Boo の詳細については、boo.codehaus.org を参照してください。

有効期間管理

SimpleDependencyResolver は常にインターフェイスに対して登録された同一インスタンスを返すため、事実上そのインスタンスはシングルトンになります。SimpleDependencyResolver を変更して、インスタンスではなく、具象型を登録することができます。その後、別のファクトリを使用して具象型のインスタンスを作成できます。シングルトン ファクトリは常に同一のインスタンスを返します。一時ファクトリは常に新しいインスタンスを返します。スレッド単位ファクトリは要求するスレッドごとに 1 つのインスタンスを保持します。

インスタンスの作成方法を制約するものは作成者の発想力のみです。その発想を Windsor が提供しています。XML 構成ファイルに属性を適用することにより、特定の具象型のインスタンスを作成するために使用するファクトリの種類を変更できます。既定では、Windsor はシングルトン インスタンスを使用します。IFoo がコンテナから要求されるたびに新しい Foo を返すようにする場合は、構成を次のように変更するだけです。

<component id="Foo"
  service="JamesKovacs.IoCArticle.IFoo, JamesKovacs.IoCArticle"
  type="JamesKovacs.IoCArticle.Foo, JamesKovacs.IoCArticle"
  lifestyle="transient"/>

依存関係の自動接続

依存関係の自動接続とは、コンテナが要求されたタイプの依存関係を確認し、その依存関係を作成することであり、開発者が既定のコンストラクタを指定する必要はありません。

public InvoiceService(IAuthorizationService authorizationService,
  IInvoiceValidator invoiceValidator,
  IInvoiceRepository invoiceRepository) {
  ...
}

クライアントがコンテナから IInvoiceService を要求すると、コンテナは、具象型に IAuthorizationService、IInvoiceValidator、および IInvoiceRepository の具象実装が必要であることを認識します。コンテナは適切な具象型を検索し、存在する可能性がある依存関係を解決し、その依存関係を作成します。次に、これらの依存関係を使用して InvoiceService を作成します。自動接続では既定コンストラクタを保持する必要がないため、コードが簡素化され、多くのクラスの IoC 静的ゲートウェイへの依存関係がなくなります。

具象実装ではなく、コントラクトにコーディングし、コンテナを使用することにより、アーキテクチャの柔軟性がはるかに高まり、変更に対応しやすくなります。InvoiceRepository の構成可能な監査ログを実装するにはどうしたらよいでしょうか。密結合アーキテクチャでは、InvoiceRepository を変更する必要があります。また、監査ログを有効にするかどうかを指定するためにアプリケーションの構成を設定する必要があります。

では、疎結合アーキテクチャでは、より優れた方法があるでしょうか。IInvoiceRepository を実装する AuditingInvoiceRepositoryAuditor を実装することができます。監査担当者は監査機能のみを実装し、コンストラクタに指定された実際の InvoiceRepository にデリゲートします。このパターンをデコレータと呼びます (図 5 を参照)。

Figure 5 Using the Decorator Pattern (Close Figure 5)
public class AuditingInvoiceRepository : IInvoiceRepository {
  private readonly IInvoiceRepository invoiceRepository;
  private readonly IAuditWriter auditWriter;

  public AuditingInvoiceRepository(IInvoiceRepository invoiceRepository,
    IAuditWriter auditWriter) {
    this.invoiceRepository = invoiceRepository;
    this.auditWriter = auditWriter;
  }

  public void Save(Invoice invoice) {
    auditWriter.WriteEntry("Invoice was written by a user.");
    invoiceRepository.Save(invoice);
  }
}

監査を有効にするには、IInvoiceRepository に対する要求が行われた場合に AuditingInvoiceRepository で修飾された InvoiceRepository を返すようにコンテナを構成します。クライアントは相変わらず IInvoiceRepository にアクセスしています。この方法には次のような利点があります。

  1. InvoiceRepository は変更されていないため、コードが破壊される可能性はありません。
  2. AuditingInvoiceRepository を実装し、InvoiceRepository から切り離してテストすることができます。これにより、実際にデータベースが存在するかどうかに左右されずに監査を適切に行うことができます。
  3. InvoiceRepository を複雑化させずに監査、セキュリティ、キャッシュなどの用途で複数のデコレータを作成できます。つまり、疎結合システムのデコレータ方式は、新しい機能を追加する際の拡張性が優れています。
  4. コンテナは、注目に値するアプリケーションの拡張性メカニズムです。AuditingInvoiceRepository を InvoiceRepository や IInvoiceRepository と同じアセンブリに実装しなければならない理由はありません。構成ファイルで参照されるサードパーティ製のアセンブリにも簡単に実装可能です。

結合を緩和して変更に対応する

ソフトウェア アーキテクチャが階層化されていても、階層間の密結合によりアプリケーションのテストや進化が妨げられる場合があります。ただし、この設計の結合は切り離すことが可能です。依存関係の反転および依存関係の注入を使用して、具象実装ではなく、コントラクトにコーディングする利点を得ることができます。コントロール コンテナの反転を導入することにより、アーキテクチャの柔軟性が向上します。結論として、疎結合設計の方が変更への対応力に優れています。

James Kovacs は、アルバータ州カルガリー在住のフリー設計者、開発者、兼トレーナーの、いわゆる何でも屋です。.NET Framework を使用したアジャイル開発を専門にしています。ソリューション アーキテクチャの Microsoft MVP でもあり、ハーバード大学で修士号を取得しました。James の連絡先は、jkovacs@post.harvard.edu または www.jameskovacs.com です。


CI サーバー: 継続的な統合でビルド プロセスを再定義する

CI サーバー

継続的な統合でビルド プロセスを再定義する

Jay Flowers
この記事の内容 : :
  • 基本的な CI サーバー
  • ビルドおよびテストの起動
  • ソース管理とレポート
  • CI Factory の設定
この記事は次のテクノロジを使用しています:
MSBuild
コードのダウンロード : : ContinuousIntegration2008_03.exe (168KB)
コンテンツ
導入コストを削減する
作業の開始
ビルドおよびテスト
ソース管理
ログとレポート
ビルドをトリガする
CI サーバーを実行する
CI Factory をカスタマイズする
残りの作業

継続的な統合 (CI) システムを使用すると、チーム メンバはアプリケーション コンポーネントを短時間で統合できるようになり、多い時では 1 人の開発者が 1 日に複数の統合を行うことも可能です。ユーザーが歯がゆい思いをしなくて済むように、各統合は自動ビルドですぐに検証され、エラーは即座に検出されます。CI の概念に慣れるまでに多少時間がかかるかもしれませんが、これはきわめて有用なシステムです。

CI システムの基本原則は、"ビルド" という用語の再定義が必要だということです。以前なら、ビルドとは単なるコンパイルのことと考えられていたかもしれません。CI の場合は、ビルドの定義に検証およびテストのすべての重要なステップが含まれることを理解する必要があります。CI はテストの存在に依存します。テストを行わない場合は単に継続的なコンパイルになります。

CI システムの主要ユーザーは開発者であるため、システムの成否は開発者によって決まります。開発者が本当に求めているものは 2 つだけです。それは、5 分以内に完了するビルドと、ビルド エラーの原因を突き止める簡単な手段です。成功はチームのビルド作法にもかかっています。そこで、以下の単純なガイドラインに従うことをお勧めします。

  • ビルドが破損した場合にはビルドを放置しない。
  • 破損したビルドに送信しない。
  • 適切に対処する。ビルドの破損に気付いたら、ビルド レポートを参照してください。問題の修正方法がわかった場合は、ビルドを破損した開発者に連絡してサポートしてください。

導入コストを削減する

最近、CI をベスト プラクティスとして導入する開発現場が増えていることを筆者は実感しています。導入コストが低ければ、その数はさらに増加するでしょう。基本的な CI サーバーを稼動させるには、熟練プログラマでも 2 ～ 3 日かかることが見込まれます。初心者なら最大 1 週間程度かかるかもしれません。

問題なのは、CI サーバーの準備にかかる作業およびサーバーで実行する作業の 8 割は常に同じだということです。その 8 割の作業とは、前回のビルド後のクリーンアップ、最新のソース コードの入手、バージョンのインクリメント、コンパイル、テストの実行、統計分析の実行、インストーラの作成です。CI サーバー インスタンス間の相違は、あったとしても微小なものです。この共通性が、継続的な統合を導入してコストを削減する鍵になります。

この記事では、CI サーバーの設定方法と使い方、およびその作業を支援する各種ソリューションについて説明します。まず、CruiseControl.Net、MSBuild 2.0、MbUnit、および WatiN を使用して CI サーバーを手動で作成する方法を示します。次に、CI Factory と呼ばれるオープン ソース プロジェクトを使用して、同じ作業をはるかに簡単に行う方法を示します。

使用するサンプル アプリケーションは、オープン ソース プロジェクト NunitAsp からの単純なゲストブックです。このアプリケーションをコンパイルし、Microsoft インストーラ (MSI) セットアップ アプリケーションを作成してインストールし、WatiN でテストします。完成したゲストブック アプリケーションでは、図 1 のように名前とコメントを保存できます。

Figure 1 名前とコメントを保存する完成後のサンプル アプリケーション
Figure 1 名前とコメントを保存する完成後のサンプル アプリケーション(画像を縮小するには、ここをクリックします)
Figure 1 名前とコメントを保存する完成後のサンプル アプリケーション
Figure 1 名前とコメントを保存する完成後のサンプル アプリケーション (画像を拡大するには、ここをクリックします)

ご想像どおり、このアプリケーションの名前とコメントはセッション状態に保存されます。図 2 からわかるように、この WatiN テストは単純です。

Figure 2 Testing the Sample App (Close Figure 2)
[Test]
public void Register()
{
  Ie = new IE("about:blank");
  Ie.GoTo("http://localhost/GuestBook/GuestBook.aspx");
  Ie.TextField(Find.ByName("name")).TypeText("Jay");
  Ie.TextField(Find.ByName("comments")).TypeText("Hello");
  Ie.Button(Find.ByName("save")).Click();

  Assert.AreEqual("Jay", Ie.TableCell(Find.By("innerText", "Jay"))
    .Text, @"innerText does not match");
  Assert.AreEqual("Hello", Ie.TableCell(Find.By("innerText",
    "Hello")).Text, @"innerText does not match");
}

WatiN は、Internet Explorer® を基に構築された Microsoft® .NET テスト フレームワークです。Internet Explorer の新しいインスタンスを開いてテストを行います。テストでは [Name] に「Jay」、[Comments] に「Hello」と入力し、[Save] ボタンをクリックし、予期される内容がゲスト一覧に表示されることを確認します。

ゲストブック アプリケーションにはインストーラが付属していません。そこで、筆者はこのソリューションに Visual Studio® Web セットアップ プロジェクトを追加してインストーラを作成しました。その後で、ゲストブック プロジェクトのプライマリ出力を bin フォルダに追加し、Global.asax、GuestBook.aspx、web.config の各ファイルを Web アプリケーション フォルダに追加しました。これでインストーラができました。

残るは 3 つのプロジェクト (製品アプリケーション guestbook、テスト プロジェクト GuestBook.Test、展開プロジェクト GuestBookSetup) を含むソリューションです。CI システムがありさえすれば簡単なことです。

作業の開始

開発者の観点からは、CI をタスク ベースの開発と捉えると便利です。タスクが完了したら、ビルドに送信します。話をもう少し詳しくして、タスク サイズについて考えてみましょう。タスクとは、1 日あれば完了可能な作業単位です。タスクが完了すると、システムは使用可能な状態になります。また、タスクは一般に要件より小規模です。

CI サーバーの設定時に行うタスクは数多くあります。CI サーバーをホストする物理サーバーから始めましょう。まず、開発環境を CI サーバーにインストールする必要があります。これには、統合テストに必要な外部依存関係 (Microsoft SQL Server® など) や、コンパイルに必要なコントロールまたはランタイム依存関係が含まれる場合があります。残りの開発環境には開発ディレクトリ ツリーが含まれます。Tree Surgeon (codeplex.com/treesurgeon) は、開発ツリーを作成するための無償のツールです。ツリーは、ビルド スクリプト、製品ソース コード、テスト ソース コード、およびサードパーティ製品との依存関係の場所を明確に認識している必要があります。そこで、スクリプトの作成では、最初にツリーを定義します (図 3)。

Figure 3 CI Build Script (Close Figure 3)
<PropertyGroup>
  <ProjectName>GuestBook</ProjectName>
  <RootDirectory>c:\Projects</RootDirectory>
  <ProjectRootDirectory>$(RootDirectory)\$(ProjectName)
  </ProjectRootDirectory>
  <ProjectCodeLineDirectory>$(ProjectRootDirectory)\Trunk
  </ProjectCodeLineDirectory>
  <BuildDirectory>$(ProjectCodeLineDirectory)\Build</BuildDirectory>
  <SourceDirectory>$(ProjectCodeLineDirectory)\Source</SourceDirectory>
  <ThirdPartyDirectory>$(ProjectCodeLineDirectory)\Third Party
  </ThirdPartyDirectory>
  <UnitTestDirectory>$(SourceDirectory)\Unit Test</UnitTestDirectory>
  <InstallDirectory>$(SourceDirectory)\Install</InstallDirectory>
</PropertyGroup>

無理せず、外部依存関係は使わないことにします。その方が開発環境がシンプルになります。外部依存関係が存在しなければ、初級開発者はサーバー上のソースから開発ツリーをコピーするだけで済みます。これならきわめて単純です。ただし、多くのプロジェクトでは外部依存関係を使用するため、外部依存関係のコードはすべてインストール スクリプトに記述するようにします。依存関係が存在するかどうかを確認し、存在しなければインストールします。スクリプトでは、ネットワーク上の共有ドライブからインストールを実行したり、Web サイトからインストーラをダウンロードしたりすることもできます。

外部依存関係の構成もスクリプトに記述する必要があります (データベース、IIS、レジストリ エントリ、環境変数など)。重要なことは、開発環境のコーディングに数分の時間を割くということです。これにより、追加の CI サーバーを設定する必要が生じた場合に新しい環境を簡単に設定できます。

ビルドおよびテスト

ここで、外部依存関係を管理し、コンパイルし、単体テストを実行する基本的なビルド スクリプトの基礎として、次のようなコードを作成します。

<Target Name="Build">
  <CallTarget Targets="SetupVirtualDirectory"/>
  <CallTarget Targets="Compile"/>
  <CallTarget Targets="RunTests"/>
</Target>

最初のターゲット SetupVirtualDirectory (図 4 を参照) は、IIS 内の Web アプリケーション プロジェクト ディレクトリを仮想ディレクトリとしてマップします。これは、このプロジェクトの唯一の外部依存関係です。また、SetupVirtualDirectory によって、開発者は時間のかかる Web アプリケーションの展開手順を省くことができます。後で、このスクリプトを CI サーバー上で実行するときに、完全なインストールを行います。

Figure 4 Targeting SetupVirtualDirectory (Close Figure 4)
<Target Name="SetupVirtualDirectory">
  <Exec
    WorkingDirectory="$(SystemRoot)\System32\inetsrv\APPCMD.EXE"
    Command='Add APP /site.name:"Default Web Site"
    /path:/GuestBook/physicalPath:$(SourceDirectory)
    \Production\GuestBook' />
  <Exec
    WorkingDirectory="$(SystemRoot)\System32\inetsrv\APPCMD.EXE"
    Command='Set APP "Default Web Site/GuestBook"
    /virtualDirectoryDefaults.physicalPath:$(SourceDirectory)
    \Production\GuestBook' />
</Target>

コンパイル ターゲットも単純です。次のように、任意の C# プロジェクトのソース ディレクトリをスキャンしてコンパイルします。

<ItemGroup>
  <Projects Include="$(SourceDirectory)\**\*.csproj" />
</ItemGroup>

<Target Name="Compile">
  <MSBuild Projects="@(Projects)" Targets="Build"
      StopOnFirstFailure="false" ContinueOnError="false">
  </MSBuild>
</Target>

2 つの C# プロジェクト (アプリケーション プロジェクトとテスト プロジェクト) があります。

C:\Projects\GuestBook\Trunk\Source\
  Unit Test\GuestBook.Test\GuestBook.Test.csproj
C:\Projects\GuestBook\Trunk\Source\Production\
  GuestBook\GuestBook.csproj

この方法では、ビルド スクリプトを変更せずに新しいプロジェクトを追加できますが、プロジェクトをビルドする順序を制御することはできません。プロジェクト間に依存関係がある場合、これは重要な問題です。ビルドの順序を制御する必要がある場合は、プロジェクトを ItemGroup に明示的に指定します。

単体テストを実行するため、関連付けられた MSBuild タスクを介して MbUnit を使用します (図 5 を参照)。MbUnit はアセンブリの一覧を引数として受け取り、各アセンブリ内ですべてのテストを実行します。スクリプトは *.Test.dll 形式に一致する bin ディレクトリ内のすべてのファイルの単体テスト ディレクトリをスキャンするように設定されています。開発者がこの規則に従っている限り、テスト プロジェクトを追加してもビルド スクリプトを変更する必要はありません。

Figure 5 Running Unit Tests with MbUnit and MSBuild (Close Figure 5)
<UsingTask
  TaskName="MbUnit.MSBuild.Tasks.MbUnit"
  AssemblyFile="$(ThirdPartyDirectory)\MbUnit\MbUnit.MSBuild.Tasks.dll"
  />

<ItemGroup>
  <TestAssemblies Include="$(UnitTestDirectory)\**\bin\**\*.Test.dll" />
</ItemGroup>

<Target Name="RunTests">
  <MbUnit
    Assemblies="@(TestAssemblies)"
    ReportTypes="Xml"
    ReportFileNameFormat="unittest"
    ReportOutputDirectory="$(BuildDirectory)\UnitTestReport"
  />
</Target>

このスクリプト (GuestBook.msbuild) は Build ディレクトリに存在し、バッチ ファイル Build.bat は Source ディレクトリ内に配置されています。開発者は Source ディレクトリを使用するため、バッチ ファイルをこの場所に配置すると、ビルド スクリプトの実行が容易になります。

call "%ProgramFiles%\Microsoft Visual Studio 8\VC\vcvarsall.bat"
msbuild ..\Build\GuestBook.msbuild /t:Build
SET /P variable="Hit Enter to exit."

ビルド スクリプトを CI サーバー上で実行する場合は、より実用的な環境でテストすることができます。インストーラを実行すれば、テストは簡単です。ゲストブック ソリューションには Web 展開プロジェクト GuestBookSetup.vdproj が含まれています。このプロジェクトをビルドするには、展開プロジェクトは現在 MSBuild でサポートされていないため、MSBuild ではなく、devenv.exe を呼び出す必要があります。

<Target Name="CreateInstaller">
  <Exec
    WorkingDirectory="$(SourceDirectory)"
    Command='"C:\Program Files\Microsoft Visual
    Studio 8\Common7\IDE\devenv.exe" $(ProjectName).sln
    /Build Debug /project GuestBookSetup' />
</Target>

完全な自動ビルドにするため、インストールは暗黙的に実行する必要があります。これは、インストーラを作成する場合に最もよく知られた事前事項です。インストーラの作成後にこの機能を追加するには困難が伴います。MSBuild スクリプトでは、次のような方法でターゲットをインストールおよびアンインストールする必要があります。

<Target Name="InstallApplication">
  <Exec
    WorkingDirectory="$(InstallDirectory)\GuestBookSetup\Debug"
    Command='msiexec /i GuestBookSetup.msi /q'
  />
</Target>

<Target Name="UninstallApplication">
  <Exec
    WorkingDirectory="$(InstallDirectory)\GuestBookSetup\Debug"
    Command='msiexec /x GuestBookSetup.msi /q'
  />
</Target>

これらのターゲットでは、Exec タスクを使用して、PATH にあるアプリケーション msiexec.exe で MSI ファイルを実行します。スイッチ /i はインストール、/x はアンインストール、/q は自動インストールです。このインストーラ パッケージにはユーザー入力は不要です。テストでは既定の設定を指定可能です。ただし、インストーラ パッケージに msiexec を使用してパラメータを渡すこともできます。

CI サーバーのエントリ ターゲットは次のようになります。

<Target Name="Triggered">
  <CallTarget Targets="UninstallApplication" ContinueOnError="true"/>
  <CallTarget Targets="Compile"/>
  <CallTarget Targets="CreateInstaller"/>
  <CallTarget Targets="InstallApplication"/>
  <CallTarget Targets="RunTests"/>
</Target>

これで、開発者のコンピュータ上でローカルなビルドを実行することも、CI サーバー上でパブリック ビルドを実行することも可能なビルド スクリプトができました。ここでは、この新しいビルド スクリプトの開始に CruiseControl.Net (通称 CCNet) を使用します。CCNet を Program Files ディレクトリにインストールし、複数のプロジェクトで共有することができます。CCNet の構成には 2 つのファイル (サーバーの ccnet.config とダッシュボードの dashboard.config) を使用します。ダッシュボードの構成は表示専用に使われ、製品 (この場合はゲストブック) の作成には使われません。したがって、.config ファイルはソース管理では保持されません。

これと異なり、サーバーの .config ファイルはビルド プロセスに不可欠です。ソース管理のゲストブックの .config を管理するため、CCNet サーバーの .config ファイルから参照されている文書型定義 (DTD) を使用して新しい XML ファイルを作成し、構成を定義します。

C:\Program Files\CruiseControl.NET\server\ccnet.config
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE cruisecontrol [
  <!ENTITY GuestBook SYSTEM
    "C:\Projects\GuestBook\Trunk\Build\CCNetProject.xml">
]>
<cruisecontrol>
  &GuestBook;
</cruisecontrol>

ソース管理

次に、観点を変えて、ソース管理について説明します。CI サーバーには、コンパイル済みのアーティファクトだけでなく、ソース ファイルやテスト実行環境も削除するクリーン ビルドの機能が必要です。ダーティ ビルドを実行することは好ましくありません。"下手な" ビルドを実行することはさらに好ましくありません。そこで、ビルドを成功させるために、以前のビルドが存在する必要があります。

すべてのビルドのプログラム ソースの最新コピーを取得するソース管理ブロックに注目してください。次のように、Cleancopy を true に設定します。

<sourcecontrol type="vss" autoGetSource="true" applyLabel="true">
  <project>$/GuestBook/Trunk</project>
  <username>Build</username>
  <password>password</password>
  <workingDirectory>c:\projects\GuestBook\Trunk</workingDirectory>
  <executable>C:\Program Files\Microsoft Visual
    Studio\VSS\win32\SS.EXE</executable>
  <ssdir>c:\Source Safe Databases\GuestBook</ssdir>
  <cleanCopy>true</cleanCopy>
</sourcecontrol>

多くのソース管理システムには同様の構成情報 (リポジトリの場所、資格情報、プロジェクトのルート フォルダへのパス、ディスク上の作業フォルダ) が必要です。この記事のサンプルでは Visual SourceSafe® (VSS) を使用していますが、他のソース管理プロバイダ (Visual Studio Team Foundation Server、Subversion、Perforce、Vault など) でも CCNet のすべてのソース管理ブロックは同様です。

ビルドの最初にクリーンな取得を行うと共に、ビルドが成功した場合には、ビルドの最後にラベルを作成します。このラベルはプロジェクト ノード (この場合は Trunk) に適用されます。Trunk には製品のこのバージョンをビルドするために必要なすべてが含まれていることを思い出してください。そのため、半年後にこのバージョンに戻す必要が発生した場合、このラベルから取得すると、アプリケーションのビルドを正常に行うために必要なすべての情報が得られます。筆者は、この機能を多くのチームが無視していることに気付いていました。

多くのチームは、CI システムが自分たちの製品の一部であるという認識に欠けています。CI システムは顧客に提供するものではないかもしれませんが、製品を提供するための機能として重要な役割を果たします。CI システムをそれ相応に重視して、ソース管理を行い、製品のソースでラベルを付ける必要があります。

この設定のもう一つの利点は、ビルド サーバーから変更する必要性が減少するということです。MSBuild スクリプトの変更はワークステーションから簡単に行われ、ソース管理にチェックインされ、CCNet によってビルド サーバーに取得されます。これにより、ビルド サーバー上で変更を行う面倒な手間が省け、変更のチェックインを忘れる心配がなくなります。CCNet MSBuild タスクは、このソリューションを次のようにコンパイルします。

<msbuild>
  <executable>
    C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe
  </executable>
  <workingDirectory>
    C:\Projects\GuestBook\Trunk\Source
  </workingDirectory>
  <projectFile>
    C:\Projects\GuestBook\Trunk\Build\GuestBook.msbuild
  </projectFile>
  <buildArgs>
    /noconsolelogger /p:Configuration=Debug /v:diag
  </buildArgs>
  <logger>
    C:\...\ThoughtWorks.CruiseControl.MSBuild.dll
  </logger>
</msbuild>

ログとレポート

MSBuild や CCNet には XML ロガーは付属していません。これらは CCNet Wiki からダウンロード可能です。ここでは、Build ディレクトリから参照されています。ダッシュボード UI がビルドから XML 出力に対して XSLT を適用して HTML を生成するため、XML ログは CCNet にとって重要です。MSBuild タスクは XML を収集し、各ビルドの CCNet XML ログ ファイルに含めます。ダッシュボード上で適切に書式設定された MSBuild レポートを表示するには、構成エントリを C:\Program Files\CruiseControl.NET\webdashboard\dashboard.config にある dashboard.config ファイルに追加する必要があります。次のコードに示すように、/dashboard/plugins/buildPlugins でこのノードを追加します。

<xslReportBuildPlugin description="MSBuild Output"
  actionName="MSBuildOutputBuildPlugin"
  xslFileName="xsl\msbuild.xsl" />

ビルド スクリプト ターゲット RunTests に戻って、単体テストの実行によって \Trunk\Build\UnitTestReport ディレクトリに出力された XML レポートを見てみましょう。単体テスト レポートを生成するには、この XML を追加する必要があります。CCNet では、ファイル マージ パブリッシャ ブロックを ccnet.config ファイルに追加して、この XML を Web ダッシュボード上のレポートの最終的なビルド ログにマージできます。

<merge>
  <files>
    <file>c:\projects\GuestBook\Trunk\Build\
      UnitTestReport\unittest.xml</file>
  </files>
</merge>

MbUnit の単体テスト レポートは 2 箇所 (概要ビルド レポートと詳細レポート) に表示できます。次のように、詳細レポートのメタデータを /dashboard/plugins/buildPlugins の dashboard .config ファイルに追加する必要があります。

<xslReportBuildPlugin
  description="Unit Details"
  actionName="UnitDetailsBuildReport"
  xslFileName="xsl\MBUnitDetails.xsl" />

また、次のように、/dashboard/plugins/buildReportBuildPlugin/xslFileNames で概要レポートを追加する必要があります。

<xslFile>xsl\MBUnitSummary.xsl</xslFile>

ビルドをトリガする

CCNet 構成の主要な部分はすべて完了しましたが、引き続き、ビルドを実行するトリガを追加する必要があります。

<triggers>
  <intervalTrigger
    seconds="60"
    buildCondition="IfModificationExists" />
</triggers>

このトリガ ブロックでは、60 秒間隔でビルドが発生する可能性があります。CCNet は、最後のビルドの発生以後に変更があったかどうかをソース管理リポジトリに問い合わせ、変更があった場合はビルドをトリガします。

CCNet .config ファイルには、その他にもさまざまな構成要素を追加できます。電子メールを送信してビルドの結果をユーザーに通知することも可能です。ユーザーが各ビルドのラベルやバージョン番号を管理することもできます。さまざまな便利な操作が可能です。

CI サーバーを実行する

CCNet の構成も完了し、CI サーバーが完全に機能するために必要な作業はあとわずかです。ソース管理に必要な処理を追加し、CCNet サーバーを起動する必要があります。

CCNet のインストール方法に応じて、Windows サービスを起動するか、ccnet.exe コンソール アプリケーションを実行するかのいずれかの方法でサーバーを有効にします。Windows サービスを選択した場合、WatiN で必要なため、デスクトップからサービスにアクセス可能であることを確認する必要があります。

ダッシュボードを表示するには、ブラウザで http://localhost/ccnet を開きます。メイン ダッシュボード プロジェクト グリッドに、ビルドの進行状況に関する簡単な情報が表示されます。左側に CCTray ツールのダウンロード リンクがあります。デスクトップで CCTray を使用するには、CCTray をインストール後に構成して、ビルド サーバーに関する情報およびサブスクライブするプロジェクトを指定します。このツールはシステム トレイに存在し、ビルド サーバーの状態やアクティビティの変更をユーザーに通知します。また、ビルドの完了時間を予測します。図 6 は、ゲストブック プロジェクトの成功したビルド レポートを示しています。

Figure 6 成功したビルドの CCNet レポート
Figure 6 成功したビルドの CCNet レポート(画像を縮小するには、ここをクリックします)
Figure 6 成功したビルドの CCNet レポート
Figure 6 成功したビルドの CCNet レポート (画像を拡大するには、ここをクリックします)

ビルド レポートには概要ページと複数の詳細レポートが含まれます。単体テストの概要レポートと、詳細レポートへのリンクが左側に表示されます。[MSBuild Output] (MSBuild 出力) 詳細レポートも左側に表示されます。[View Build Log] (ビルド ログの表示) リンクには、このビルド インスタンスの XML ログ ファイルがそのまま表示されます。

CI Factory を使用する

次に、CI Factory を使用して、前述と同じ処理およびそれ以上の処理をはるかに少ない手間で行う方法を示します。CI Factory をダウンロードしてインストールしたら、ソリューション ファイル CI Factory.sln を開きます。ソリューション エクスプローラで Arguments.xml ファイルを開いて新しいプロジェクトを構成します。

まず、次のように、プロジェクトの名前を GuestBook に設定します。

<property name="ProjectName" value="GuestBook" />

次に、開発ツリーのフォルダ名を設定します。図 7 のように、既定値を使わずに、自分の開発ツリーを反映するように設定を変更しています。

Figure 7 Setting the Folder Names for the Dev Tree (Close Figure 7)
<property name="ProjectsDirectory"
  value="c:\Projects"/>
<property name="ProjectCodeLineName"
  value="Trunk"/>
<property name="ProductDirectoryName"
  value="Source"/>
<property name="ThirdPartyDirectoryName"
  value="Third Party"/>
<property name="ProductionDirectoryName"
  value="Production"/>
<property name="UnitTestDirectoryName"
  value="Unit Test"/>
<property name="InstallDirectoryName"
  value="Install"/>

次に、ビルドのマスタ用および開発者用の電子メール アドレスを設定します。CI Factory は、ビルドが完了した時点でそのビルドの変更に関与した開発者にビルド レポートが電子メールで送信されるように CCNet を構成します。ビルドが失敗した場合には、次のコードに示されているように、ビルドの所有者にも電子メールが送信されます。

<property name="BuildMaster.Email" value="alice@contoso.com"/>
<largeproperty name="Developer.List">
  <value expand="true" xml="true">
    <user name="MyBrotherDarell"
      group="developer"
      address="bob@contoso.com"/>
    <user name="MyOtherBrotherDarell"
      group="developer"
      address="charles@contoso.com"/>
  </value>
</largeproperty>

ここでの説明上はほとんどのオプションに既定値を使用して問題ありませんが、このファイルに設定可能なオプションは他にもあります。たとえば、WatiN テストのカバレッジ統計値を収集するには、次のプロパティを true に設定します。

<property name="NCover.IIS" value="test" />

次に、図 8 に示すように、ビルドのツール スタックを選択します。

Figure 8 Configuring Build Tools (Close Figure 8)
<strings id="Packages.InstallList">
  <string value="VisualSourceSafe"/>
  <string value="CSDiff" />
  <string value="SourceModificationReport"/>
  <string value="Versioning"/>
  <string value="MSBuild" />
  <string value="VS.NETDeploy" />
  <string value="NCover" />
  <string value="DotNetUnitTest"/>
  <string value="nDepend"/>
  <string value="FxCop"/>
  <string value="Simian" />
  <string value="Analytics" />
  <string value="Alerts" />

  <string value="Deployment"/>
  <string value="Workspace"/>
</strings>

CI Factory は、拡張しやすいようにパッケージの概念で構成されています。VisualSourceSafe パッケージはソース管理との統合機能です。CSDiff はダッシュボードの差分レポートを生成します。バージョン管理では、アセンブリとファイルのバージョン情報が保持されます。MSBuild はコンパイルおよびレポート作成を管理します。NCover は... これ以上説明は不要ですね。

CI Factory インストーラの構成が完了したので、次にバッチ ファイル C:\Tools\CI Factory\run.bat を実行してゲストブック プロジェクトを作成します。スクリプトは、不足している必要なソフトウェアをインストールし、ライセンスへの同意を要求します。

次に、バッチ ファイルで CCNet サーバーをコンソール アプリケーションとして起動し、各ビルド ファイルの Visual Studio ソリューションを開きます。ビルド スクリプトの管理専用のプロジェクトを用意すると便利です。まず、編集する必要があるファイルのみをプロジェクトに追加します。XML を作成する際、Visual Studio エディタを利用すると手間が省けます。Visual Studio を使用すれば、誤った形式の XML ができあがる可能性ははるかに少なくなります。Visual Studio エディタはユーザーにはっきりわかるようにエラーを表示します。エディタでは、XSD (XML スキーマ定義) を使用した IntelliSense® の機能を利用することもできます。対照的に、メモ帳を使用した場合、エラーは一切表示されません。

CI Factory はプレーン インストールの状態です。ここで作成されたツリーは、先ほど手動で作成した内容とほぼ同じです。Build ディレクトリ内のサーバー ディレクトリとダッシュボード ディレクトリを見てください。CCNet のゲストブック プロジェクト専用のコピーが格納されています。これは外部依存関係ではなく、内部依存関係であるため、前述の例と依存関係を共有している可能性が高いと考えられます。CCNet プロジェクトの構成ファイルは前述の例と同じ場所 (C:\Projects\GuestBook\Trunk\Build\ccnetproject.xml) にあります。

注意を要する主な相違点は、ゲストブック製品をビルドするプロジェクトとビルド スクリプトを管理するプロジェクトの 2 つのプロジェクトが存在するということです。ビルド スクリプト プロジェクトはソース管理のビルド ディレクトリを監視し、トリガされると、リポジトリからディスク上のファイルの更新だけを行います。製品をビルドするプロジェクトはビルド ディレクトリの変更を無視します。

前の例では MSBuild スクリプトを作成する必要がありましたが、ここでは、その処理の多くはインストール時に行われ、大部分は Main.Build.xml スクリプトで管理されます。先ほど作成した MSBuild スクリプトと同様、Main.Build.xml には triggered という名前のターゲットが格納されていますが、Build という名前のターゲットはありません。Build ターゲットは、Personal.Build.xml スクリプトに存在し、Source ディレクトリ内にあります。

Build.bat より多くのバッチ ファイルがあります。たとえば、Test.bat はビルド スクリプトのテスト部分のみを実行します。一方、Build.bat は、ソース管理からの開発者ワークスペースの更新など、処理する機能が増えています。

..\Build\nant\bin\nant.exe -buildfile:Personal.Build.xml UpdateSource
IF NOT %ERRORLEVEL%==0 exit /B %ERRORLEVEL%
..\Build\nant\bin\nant.exe -buildfile:Personal.Build.xml %1 %2 %3 %4 %5 %6 %7 %8
SET /P variable="Hit Enter to continue."

CI Factory をカスタマイズする

この CI Factory の既定のインストールで要件の大部分は満たされますが、インストール後の構成などによるカスタマイズで固有の環境を詳細に設定できます。CI Factory のカスタマイズで注意すべき点は、NAnt を使用して構成されているということです。そのため、MSBuild で CCNet を使用してビルドをコントロールしている場合でも、CI Factory をカスタマイズするには NAnt の XML 構文を使用する必要があります。

CCNet ダッシュボードは構成済みであるため、別の XSLT ファイルを使用する際に構成情報を追加する必要はありません。既定のインストールでは IIS も構成され、複数の仮想ディレクトリが作成されます。例では、図 9 のように dashboard.config を若干変更していますが、メイン ビルド レポートのセクションの表示順序を変更しただけです。これは、ビルド時に作成される MSI ファイル、実行可能ファイル、ZIP ファイルなどのダウンロード ファイルへのすべてのハイパー リンクを表示するセクションであるため、展開ファイルをレポートの上の方に表示することが目的です。

Figure 9 Customizing the CCNet Build Report (Close Figure 9)
<buildReportBuildPlugin imageFileName="images/BuildReport.gif">
  <xslFileNames>
    <xslFile>xsl\header.xsl</xslFile>
    <xslFile>xsl\modifications.xsl</xslFile>
    <xslFile>Packages\Deployment\deployment.xsl</xslFile>
    <xslFile>xsl\compile.xsl</xslFile>
    <xslFile>Packages\VS.NETCompile\Compile.xsl</xslFile>
    <xslFile>Packages\MSBuild\compile-msbuild.xsl</xslFile>
    <xslFile>Packages\DotNetUnitTest\MbUnitDisplaySummary.xsl</xslFile>
    <xslFile>Packages\NCover\NCoverDisplaySummary.xsl</xslFile>
    <xslFile>Packages\Simian\SimianDisplaySummary.xsl</xslFile>
    <xslFile>Packages\nDepend\nDependSummaryDisplay.xsl</xslFile>
    <xslFile>Packages\FxCop\FxCopSummaryDisplay.xsl</xslFile>
  </xslFileNames>
</buildReportBuildPlugin>

MSBuild パッケージの構成も変更しています。既定では、製品ツリーにすべてのプロジェクトのプライマリ出力の ZIP ファイルが作成されます。この例では、プロジェクトはゲストブック 1 つのみであるため、ZIP ファイルを作成する必要はなく、MSI で十分です。そこで、パッケージ内の次の 2 つのプロパティを false に変更して ZIP ファイルの作成を無効にします。

<property name="Compile.ToCopyToBin" value="false" overwrite="false"/>
<property name="Compile.ToDeployZip" value="false" overwrite="false"/>

前に作成した MSBuild スクリプトと同様、この MSI ファイルをインストールおよびアンインストールする必要があります。そこで、次のように Build ディレクトリ内の Main.Build.xml ファイルを編集し、単体テストの実行処理を呼び出す前後に exec タスクを追加します。

<exec program="msiexec"
  commandline="/i ${Install.MsiSourceDir}\${Install.MsiSourceFile} /q"
  verbose="true"/>
<call target="UnitTest.RunTests" />
<exec program="msiexec"
  commandline="/x ${Install.MsiSourceDir}\${Install.MsiSourceFile} /q"
  verbose="true"/>

また、ここでも前の例からの開発者向けの優れたユーザー エクスペリエンスを維持し、開発者が必ずゲストブックの Web Application Project ディレクトリを IIS に仮想ディレクトリとしてマップするようにします。そのために構成スクリプトを Workspace パッケージに追加します。ワークスペースの Configuration ディレクトリに新しい IIS ディレクトリを作成し、新しいファイル IIS.Script.xml を追加します。このスクリプトは単純で、仮想ディレクトリを作成する 1 つのターゲットのみを指定します。

<target name="Workspace.Configuration.IIS">
  <mkiisdir
    dirpath="${ProductionDirectory}\GuestBook"
    vdirname="GuestBook"
    defaultdoc="GuestBook.aspx"
    enabledefaultdoc="true"
  />
</target>

このターゲットを呼び出すには、Workspace パッケージに登録する必要があります。そのためには、IIS を構成スクリプトの一覧に追加し、インクルード タスクを追加します。

<strings id="Workspace.Configuration">
  <string value="MbUnit"/>
  <string value="IIS"/>
</strings>

<include buildfile="Configuration\IIS\IIS.Script.xml"/>

これでビルド サーバーが使用可能になったので、ゲストブック製品をソース管理に追加し、ビルドがトリガされたことを確認します。

CI Factory のメイン ダッシュボード プロジェクト グリッド (図 10 を参照) に、ビルドの経過時間や検出された変更など、現在のビルドの状態やアクティビティに関するさまざまな情報が表示されます。CCTray のダウンロードは前の例と同様に左側に表示されますが、このインストーラは CCTray を自動的に構成します。

Figure 10 ダッシュボードに表示されたビルドの状態
Figure 10 ダッシュボードに表示されたビルドの状態(画像を縮小するには、ここをクリックします)
Figure 10 ダッシュボードに表示されたビルドの状態
Figure 10 ダッシュボードに表示されたビルドの状態 (画像を拡大するには、ここをクリックします)

ビルドが完了すると、図 11 のように、バージョン 1.0.0.1 のレポートを表示できます。ご覧のように、MSI ファイルへのリンクがビルド レポートに表示されています。

Figure 11 ビルド レポート
Figure 11 ビルド レポート(画像を縮小するには、ここをクリックします)
Figure 11 ビルド レポート
Figure 11 ビルド レポート (画像を拡大するには、ここをクリックします)

下にスクロールすると、インストール時にパッケージ一覧に含めるように選択した他のツール (NCover、Simian、および FxCop) で概要レポートを表示できます。NDepend などの詳細レポートへのリンクも表示できます。ここでは、これらのパッケージに構成を追加する必要はありません。

残りの作業

この記事では、あらゆる CI システムの作業の 8 割は共通しているということを示してきました。では、残りの 2 割の作業は何だという疑問を持たれるかもしれません。アプリケーションに使用するデータベースの管理はその一般的な例です。筆者は、ソース管理リポジトリを使用してチーム内の同期を維持しています。CI システムは、個人的な用途でも、パブリック ビルド サーバーのテストでも、チームのデータベースの同期に主要な役割を果たすことができます。

もう 1 つの一般的な例にシステム展開があります。これには、開発環境やテスト環境への展開ばかりでなく、顧客環境への展開も含まれます。最も高度で優れた CI システムは、直接に顧客環境への展開を処理します。CI をユーザーのデスクトップにまで行き渡らせることは究極の目標です。

中間目標としては、余裕のある作業を行うために、リリース ノートの作成を自動化したり、CI システムをテスト チームのデスクトップに拡張したり (方法は build.bat を開発者のデスクトップに拡張する場合と同様)、インストーラ以外にリリース ビルドに含める単体の仮想マシンまたは一連の仮想マシンを作成することなどが考えられます。共通のテーマは、自動化できるものは自動化し、その自動化機能を多用するということです。

Jay Flowers は、CI Factory オープン ソース プロジェクトの開発者です。書籍『Windows Developer Power Tools』の CI Factory のセクションを担当しています。MbUnit プロジェクトや他の小規模なオープン ソース プロジェクトにも携わっています。そのソフトウェア開発への情熱は jayflowers.com に掲載されている記事やブログが証明しています。


パフォーマンス: Visual Studio プロファイラでアプリケーションのボトルネックを見つける

パフォーマンス

Visual Studio プロファイラでアプリケーションのボトルネックを見つける

Hari Pulapaka and Boris Vidolov
この記事の内容 : :
  • パフォーマンス ボトルネックのターゲットの絞り込み
  • アプリケーション コードのプロファイリング
  • プロファイル データの比較
  • パフォーマンス レポート
この記事は次のテクノロジを使用しています:
Visual Studio 2008
コンテンツ
アプリケーションのプロファイリング
アプリケーションを最適化する
ターゲットを絞ったプロファイリング

この 10 年間に数多くの新しいソフトウェア テクノロジやプラットフォームが登場しました。これらの新しいテクノロジを利用して高パフォーマンスのアプリケーションを作成するには、専門の知識が必要です。ブログなどのインターネット テクノロジによって、不満を抱えたユーザーが簡単にアプリケーションに否定的な目を向けることができるようになった昨今では、パフォーマンスを最優先課題と考える必要があります。計画の初期段階で応答性に関する要件を追加し、潜在的なテクノロジの制限を特定するためのプロトタイプを作成する必要があります。また、開発全体を通して、アプリケーションのパフォーマンスをさまざまな側面から測定し、手戻りの可能性を発見して、低速なシナリオのテストでバグを管理および追跡することも欠かせません。

たとえ計画が万全でも、製品開発の段階でパフォーマンスの問題を調査する必要があります。この記事では、Visual Studio® Team System Development Edition または Visual Studio Team Suite を使用してアプリケーション パフォーマンスのボトルネックを特定する方法について説明します。サンプル パフォーマンス調査の検討を通じて Visual Studio プロファイラについても説明します。この記事のコード サンプルは C# で作成されていますが、記事内の多くの例はネイティブ C/C++ や Visual Basic® でも同様に有効です。

アプリケーションのプロファイリング

便宜上、前述した 2 つのエディションの Visual Studio に付属しているプロファイラを使用します。まずは、図 1 に示すような、マンデルブロ フラクタルを描画する簡単なサンプル プロジェクトから始めます。このアプリケーションはあまり効率的ではなく、フラクタルの描画に 10 秒程度かかります。

Figure 1 パフォーマンスをテストするターゲット プログラム
Figure 1 パフォーマンスをテストするターゲット プログラム(画像を縮小するには、ここをクリックします)
Figure 1 パフォーマンスをテストするターゲット プログラム
Figure 1 パフォーマンスをテストするターゲット プログラム (画像を拡大するには、ここをクリックします)

調査を開始するには、Visual Studio 2008 の新しい [分析] メニューでパフォーマンス ウィザードを起動します。Visual Studio 2005 の場合、この機能を [ツール] メニューの [パフォーマンス ツール] から起動します。これにより、3 つの手順から成るウィザードが起動します。まず、このウィザードでターゲットとなるプロジェクトまたは Web サイトを指定します。次に、2 種類のプロファイリング方式 (サンプリングとインストルメンテーション) を指定します (これらのプロファイリング方式の詳細については、補足記事「パフォーマンス プロファイリングの解説」を参照してください)。ここでは既定値を使用します。

ウィザードが完了すると、[パフォーマンス エクスプローラ] ダイアログが表示され、新しいパフォーマンス セッションが作成されます。このセッションにはターゲット アプリケーション (ここでは Mandel) が含まれ、レポートは存在しません。プロファイルを開始するには、ツール ウィンドウのツール バーの [Launch with Profiling] (プロファイルの起動) ボタンをクリックします。

アプリケーションでフラクタルの描画が完了したら、すぐにフォームを閉じてプロファイルを停止します。新規に作成されたレポートは自動的にパフォーマンス セッションに追加され、分析が開始されます。分析が完了すると、最も計測値の大きい関数の一覧を示すパフォーマンス レポートの概要が Visual Studio プロファイラに表示されます (図 2 を参照)。レポートに関数を表示する方法は 2 とおりあります。1 つは、表示された関数で直接または間接に実行された処理を測定します。関数ごとに、関数本体およびそのすべての子の呼び出しで収集されたサンプルの累計を数値で表します。2 番目のリストでは、子の呼び出しで収集されたサンプルはカウントされません。概要ページに、DrawMandel メソッドの実行時に Visual Studio プロファイラがサンプルの 30.71% を収集したことを示す情報が表示されています。サンプルの残りの 69% は別の関数に分散しているため、ここには表示されません。レポート オプションの詳細については、「レポートの視覚化オプション」のリンクを参照してください。

Figure 2 最も計測値の大きい関数呼び出しを示すパフォーマンス テスト
Figure 2 最も計測値の大きい関数呼び出しを示すパフォーマンス テスト(画像を縮小するには、ここをクリックします)
Figure 2 最も計測値の大きい関数呼び出しを示すパフォーマンス テスト
Figure 2 最も計測値の大きい関数呼び出しを示すパフォーマンス テスト (画像を拡大するには、ここをクリックします)

図 3 に示されているレポートのコール ツリー ビューの [Inclusive Samples %] (包含的サンプル %) 列は、関数およびその子で収集されたサンプルを表しています。[Exclusive Samples %] (排他的サンプル %) 列は、関数の本体のみで収集されたサンプルを表します。DrawMandel メソッドは直接 Bitmap.SetPixel を呼び出しています。DrawMandel はサンプル全体の 30.71% を占め、Visual Studio プロファイラは Bitmap.SetPixel およびその子から 64.54% を収集しています。Bitmap.SetPixel の本体はわずか 0.68% であるため、概要ページには表示されていません。ただし、Bitmap.SetPixel の処理の大部分は子を通じて実行されています。これがこのアプリケーションのボトルネックの正体です。

Figure 3 テスト済みアプリケーションのコール ツリー サンプル
Figure 3 テスト済みアプリケーションのコール ツリー サンプル(画像を縮小するには、ここをクリックします)
Figure 3 テスト済みアプリケーションのコール ツリー サンプル
Figure 3 テスト済みアプリケーションのコール ツリー サンプル (画像を拡大するには、ここをクリックします)

明らかに、Bitmap.SetPixel は Mandel プロジェクトに最適とはいえません。サンプル アプリケーションには、フォーム上のすべてのピクセルにより高速にアクセスする方法が必要です。さいわい、Bitmap クラスには Bitmap.LockBits という別の有効な API があります。この関数を使用してプログラムで直接ビットマップ メモリに書き込むことにより、個々のピクセルの設定に伴うオーバーヘッドが軽減されます。さらに、描画を最適化するために、整数のフラット配列を作成し、各ピクセルの色の値を格納します。その後、その配列の値を 1 回の操作でビットマップにコピーします。

アプリケーションを最適化する
パフォーマンス プロファイリングの解説

プロファイリングを サンプリング方式で行う場合、プロファイラはデバッガと同様の方法で実行中のプロセスにアタッチします。次に、プロファイラは定期的にプロセスを中断して、どの関数がスタックの最上位にあるかを確認し、その関数へのコード パスを検査します。つまり、Visual Studio プロファイラは現在のプロセスの状態のサンプルを収集します。サンプリングは、プロファイリングの非侵入型統計手法です。関数内で収集されたサンプルが多いほど、関数が実行する処理も多くなる傾向があります。

Visual Studio プロファイラはこの実行への呼び出しパスに関する情報も収集します。そのため、収集されたデータを分析した後、完全なコール スタックを表示できます。既定では、Visual Studio プロファイラは 1 千万 CPU サイクルごとに 1 つのサンプルを収集します。サンプリングは、CPU サイクル以外のページ フォールト、システム コール、CPU のキャッシュ ミスなどのイベントで発生する可能性があります。プロファイリング セッションのプロパティは、プロファイラのサンプリングする内容と頻度を制御します。

オーバーヘッドの少ないソリューションとして、サンプリングは多くの場合に推奨されるオプションです。ただし、サンプリングで情報が収集されるのは、プログラムが CPU を能動的に使用しているときだけであるという点に注意が重要です。したがって、プロセスがディスクやネットワークなどのリソースを待機している間は、Visual Studio プロファイラでサンプルの収集は行われません。そこで、アプリケーションが CPU を能動的に使用しない場合は、インストルメンテーション プロファイリングを使用することをお勧めします。

インストルメンテーション モードでは、Visual Studio プロファイラは各関数の最初と最後にプローブと呼ばれる特殊な命令を挿入することにより、バイナリを変更 (インストルメント) します。プロファイラでプローブを使用して各関数の実行時間を測定できます。また、外部関数の各呼び出しに 1 組のプローブを追加して、外部呼び出しのコストを確認することができます。

インストルメンテーション プロファイリングでは、関数の正確な実行時間 (経過時間)、関数が呼び出された回数、さらには OS に切り替わらずに関数が CPU を能動的に使用した時間 (アプリケーション時間) などのデータを測定できます。インストルメンテーションの短所は、膨大なデータを収集するため分析により時間がかかる点です。また、インストルメンテーション モードの方が実行時のオーバーヘッドも大きくなります。オーバーヘッドが大きいと、プロファイル対象のアプリケーションのパフォーマンス特性が誤って変更される可能性があります。

サンプリングおよびインストルメンテーションの両方で、Microsoft® .NET Framework に基づいてアプリケーションのメモリ割り当てデータを収集することもできます。パフォーマンス セッションのプロパティ ページを使用して、.NET のメモリ割り当てデータの収集を有効化および調整できます。この機能は一般にメモリ プロファイリングとも呼ばれ、MSDN® にこのトピックに関するドキュメントが豊富に存在します。これは、.NET Framework 準拠のコード専用のプロファイラだけが持つ機能です。その他の機能については、Visual Studio プロファイラはネイティブ C/C++ アプリケーションおよび .NET ベースのアプリケーションとの間で完全に同等です。

では、SetPixel ではなく LockBits を使用するように DrawMandel メソッドを変更し、この変更によるパフォーマンスの変化を確認してみましょう。ビットマップを作成したら、次の行を追加してビットマップ ビットをロックし、ビットマップ メモリへのポインタを取得します。

BitmapData bmpData =
    bitmap.LockBits(
        new Rectangle(0, 0, Width, Height),
        ImageLockMode.ReadWrite,
        bitmap.PixelFormat);
IntPtr ptr = bmpData.Scan0;
int pixels = bitmap.Width * bitmap.Height;
Int32[] rgbValues = new Int32[pixels];

次に、ピクセルを設定する内部ループで Bitmap.SetPixel の呼び出しをコメント アウトし、次のような新しいステートメントで置き換えます。

//bitmap.SetPixel(column, row, colors[color]);
rgbValues[row * Width + column] =
    colors[color].ToArgb();

また、次の行を追加して、配列をビットマップ メモリにコピーします。

Marshal.Copy(rgbValues, 0, ptr, pixels);
bitmap.UnlockBits(bmpData);

ここで、プロファイラを使用してアプリケーションを再実行すると、フラクタルの描画が約 3 倍高速になることがわかります (図 4 を参照)。新しいパフォーマンス レポートの概要ページに、DrawMandel の本体がサンプル全体の 83.66% を占めていることを示す情報が表示されます。描画を最適化したため、ボトルネックはフラクタルの計算に含まれるようになりました。

Figure 4 修正後のコードのパフォーマンス プロファイル
Figure 4 修正後のコードのパフォーマンス プロファイル(画像を縮小するには、ここをクリックします)
Figure 4 修正後のコードのパフォーマンス プロファイル
Figure 4 修正後のコードのパフォーマンス プロファイル (画像を拡大するには、ここをクリックします)

次の手順では、その計算も最適化します。今度は、ボトルネックを 1 つの関数内で見つけ出す必要があります。DrawMandel は複雑なメソッドであるため、どの計算に注目すべきかを把握することが困難です。さいわい、Visual Studio 2008 のサンプリング プロファイラでは既定で行レベルのデータも収集されるため、関数内のどの行で特に多くの時間が費やされるかを特定しやすくなります。

行レベルのデータを表示するため、パフォーマンス レポートを別の観点から検査してみましょう。[現在のビュー] メニューでモジュール ビューに切り替えます。コール ツリー ビューと異なり、モジュール ビューには関数相互の呼び出し、および親関数のコンテキストでのこれらの関数呼び出しのコストに関する情報は表示されません。代わりに、モジュール ビューには実行可能ファイル (アセンブリまたは DLL) 単位およびその実行可能ファイルの関数単位のサンプルの総累計が表示されます。Visual Studio プロファイラはすべてのコール スタックからこのデータを累計します。

モジュール ビューは概要の確認に便利です。たとえば、[Exclusive Samples %] (排他的サンプル %) 列で並べ替えると、Mandel.exe が処理の 87.57% を実行していることがわかります。最適化の結果、GDI+ は 3% 未満であることがわかりました。これらのモジュールを展開し、各メソッドに関して同様の情報を表示することもできます。また、Visual Studio 2008 では、ツリーを関数レベル以上に展開する機能があり、同じデータを行単位で表示したり、各行の命令単位に表示したりすることもできます (図 5 を参照)。

Figure 5 プロファイルしたコード行へのジャンプ
Figure 5 プロファイルしたコード行へのジャンプ(画像を縮小するには、ここをクリックします)
Figure 5 プロファイルしたコード行へのジャンプ
Figure 5 プロファイルしたコード行へのジャンプ (画像を拡大するには、ここをクリックします)

ソースにジャンプすると、図 6 のようなコードが表示されます。このコードは、最内部のループ条件内の平方根を計算します。この処理には時間がかかり、アプリケーションの処理全体の 18% を占めます。図 6 の強調表示された行は、最適化の余地があるコードを示しています。最初の行では不要な平方根を使用し、2 番目の行では while ループ内で固定処理を行っています。

Figure 6 Code-Level Optimizations (Close Figure 6)

元のコード

for (int column = 1; column < Width; column++)
{
 y = yStart;
 for (int row = 1; row < Height; row++)
 {
  double x1 = 0;
  double y1 = 0;
  int color = 0;
  int dept = 0;
  while (dept < 100 && Math.Sqrt((x1 * x1) + (y1 * y1)) < 2)
  {
   dept++;
   double temp = (x1 * x1) - (y1 * y1) + x;
   y1 = 2 * x1 * y1 + y;
   x1 = temp;
   double percentFactor = dept / (100.0);
   color = ((int)(percentFactor * 255));
  }
  //Comment this line to avoid calling Bitmap.SetPixel:
  //bitmap.SetPixel(column, row, colors[color]);
  //Uncomment the block below to avoid Bitmap.SetPixel:
  rgbValues[row * Width + column] = colors[color].ToArgb();

  y += deltaY;
 }
 x += deltaX;
}

最適化されたコード

for (int column = 1; column < this.Width; ++column)
{
 y = yStart;
 int index = column;
 for (int row = 1; row < Height; row++)
 {
  double x1 = 0;
  double y1 = 0;
  int dept = 0;
  double x1Sqr, y1Sqr;
  while (dept < 100 && ((x1Sqr = x1 * x1) + (y1Sqr = y1 * y1)) < 4)
  {
   dept++;
   double temp = x1Sqr - y1Sqr + x;
   y1 = 2 * x1 * y1 + y;
   x1 = temp;
  }
  rgbValues[index] = colors[((int)(dept * 2.55))].ToArgb();
  index += Width;

  y += deltaY;
 }
 x += deltaX;
}

これらを修正後、アプリケーションを再度プロファイルし、最適化されたコードのパフォーマンスを確認します。このアプリケーションをビルドして実行すると、フラクタルは 1 ～ 2 秒で再描画されます。アプリケーションの起動時間は顕著に削減されました。

Visual Studio 2008 には、2 つのパフォーマンス レポートを比較する新機能があります。この機能の動作を確認するには、プロファイラを使用してアプリケーションを再実行し、最新のパフォーマンス レポートをキャプチャします。アプリケーションの 2 つのバージョン間の相違を確認するには、パフォーマンス エクスプローラで元のレポートと最新のレポートを選択します。レポートを右クリックし、コンテキスト メニューの [パフォーマンス レポートの比較] をクリックします。このコマンドで、すべての関数のフラットなビューと、2 つのレポートの関数の各 [Exclusive Samples %] (排他的サンプル %) の値の相違を示す新しいレポートが表示されます。全体的な実行時間が短縮されたため、DrawMandel の相対的な割合が 31.76 から 70.46 に上昇しています。

実際の最適化をよりわかりやすくするため、比較オプション ウィンドウでこの列を包含的サンプルに変更します (図 7 を参照)。また、しきい値を 1500 サンプルに増やして、微小な変動は表示されないようにします。お気付きかもしれませんが、レポートは手戻りの発見に使われることが多いため、既定では、負のデータまたは最適化の度合いが低い関数から先に表示されます。ここでは最適化を目的とするため、[デルタ] 列を逆順に並べ替え、最適化の度合いが高い関数が先頭に表示されるようにします。DrawMandel とその子のサンプルは 2,064 から 175 に変わってます。これは 10 倍以上の最適化率です。パフォーマンスの向上を明確にするため、レポートの任意の部分をコピーして貼り付けることができます。

Figure 7 DrawMandel の最適化の結果の比較
Figure 7 DrawMandel の最適化の結果の比較(画像を縮小するには、ここをクリックします)
Figure 7 DrawMandel の最適化の結果の比較
Figure 7 DrawMandel の最適化の結果の比較 (画像を拡大するには、ここをクリックします)

ターゲットを絞ったプロファイリング
レポートの視覚化オプション

Visual Studio では、 [コール ツリー]、[モジュール]、[関数] などの各種のパフォーマンス レポート オプションを使用して、さまざまな方法でパフォーマンス データを表示できます。レポートを開いたときの既定のビューは [概要] です。たとえば、Visual Studio 2008 で処理の大部分を占める呼び出しパスを検索するには、[現在のビュー] メニューでコール ツリー ビューを選択します (Visual Studio 2005 の場合、レポートの下部にある [コール ツリー] タブを選択します)。コール ツリー ビューに、すべてのコール スタックを集計したツリーが表示されます。[Inclusive Samples %] (包含的サンプル %) 列に、これらのコード パスの各分岐を集計したコストが表示されます。最も高コストな分岐を追跡することにより、パフォーマンスのボトルネックを見つけることができます。

Visual Studio 2008 では、プロファイラ チームがパフォーマンス レポートの使用を簡素化する 2 つの新機能を追加しています。その 1 つは、不要項目の非表示オプションの追加です。既定では、レポートから重要性の低い小さい関数を除外して、重要な影響をわかりやすくするようになっています。このオプションは一般にトリミング (trimming) と呼ばれます。また、プロファイラ チームは、固有の処理を持たず、処理を実行する他の関数を呼び出すだけの関数をひとまとめにすることにより、コール ツリーの階層を少なくしました。Visual Studio プロファイラでは、これを折りたたみ (folding) と呼びます。

パフォーマンス レポートの不要項目の非表示オプションは、トリミングおよび折りたたみのしきい値を制御します。パフォーマンス レポートの特定の関数を検索中に問題が発生した場合は、不要項目の非表示を無効にできます。

Visual Studio 2008 のコール ツリーを大幅に改善したもう 1 つの機能は、[ホット パス] ボタンと各コンテキスト メニューです。ホット パスは、プログラム内で最も高コストなコード パスを強調表示し、1 つの関数によって重要な処理が実行されたこと (およびその処理が委任されていないこと) を確認するまでそのパスを辿ります。次に、ホット パスはその関数を強調表示します。複数の重要なコード パスが存在する場合、ホット パスはツリー内の分岐が発生した箇所で停止します。アプリケーションのホット パスに複数の分岐が存在する場合、より注目すべき分岐を選択し、その分岐にホット パスを適用し直すことができます。

ここまで、Visual Studio プロファイラを使ってアプリケーションのパフォーマンスを改善する方法を説明してきました。しかし、現実の多くのアプリケーションでは、パフォーマンスの問題を把握するために複数のユーザー アクションの実行が要求されます。通常は、シナリオを開始する以前に収集したデータは無視した方がよいと考えます。また、1 回の実行で複数のシナリオからデータを収集する必要がある場合もあります。

このような場合のプロファイラの使い方を説明するため、今度はサンプル電子商取引 Web サイトをプロファイルします (ここでは、asp.net/downloads/starter-kits/the-beer-house からダウンロード可能な TheBeerHouse サンプルを変更して使用しました)。この Web サイトは読み込みに時間がかかりますが、これは 1 度限りのコストであり、ここでの目的は製品カタログの読み込みとショッピング カートへのアイテムの追加に時間がかかる原因を突き止めることなので、起動時間には注目しません。

この記事の都合上、最初のシナリオのみを検討します。ただし、データは 2 つのシナリオから収集し、これらのデータを特定のシナリオのパフォーマンスの問題に対象を絞ってフィルタ処理する方法を示します。

まず、新しいプロファイリング セッションを作成します。前述のように、パフォーマンス ウィザードを [分析] メニューから起動し、ウィザードの各ページで既定値を選択します。Web サイトの場合、既定でインストルメンテーション プロファイリングが選択されています。多くの場合、Web サイトは CPU を消費せず、複雑な作業には一般にデータベース サーバー アプリケーションを使用します。このような場合はインストルメンテーションの方が適しています。

パフォーマンス セッションを作成したら、プロファイラで Web サイトを起動します。このとき、起動に時間がかからないようにし、対象は一度に 1 つのシナリオに絞ります。この操作を Visual Studio 2008 で行う場合、パフォーマンス エクスプローラで [Launch with Profiling Paused] (プロファイルを一時停止状態で起動) を使用して、Visual Studio プロファイラがアタッチされたアプリケーションを起動できますが、ユーザーがプロファイリングを再開するまでデータは収集されません (図 8 を参照)。

Figure 8 起動時にプロファイラを一時停止する
Figure 8 起動時にプロファイラを一時停止する

Web サイトの読み込み中に Visual Studio に切り替えます。Visual Studio プロファイラに [Data Collection Control] (データ収集の制御) という新しいツール ウィンドウが表示されます。このウィンドウで収集の一時停止と再開を繰り返すことができます。この制御の重要な部分は、定義済みマークの一覧です。これらは注目すべき時点に印を付けるためにプロファイリング データに挿入可能なブックマークまたはラベルです。これらのマークを使用して、各ユーザー シナリオの開始と終了を区切ります。

まず、コンテキスト メニューの [Rename Mark] (マーク名の変更) コマンドでこれらのマークのうちの 4 つの名前を変更します。また、未使用のマークは削除します (図 9 を参照)。これまでは起動時の収集を回避し、シナリオを準備するために、プロファイリングを一時停止状態にしておきました。Web サイトの読み込みが完了したら、プロファイリングを再開します。

Figure 9 テスト シナリオのプロファイル マークに名前を付ける
Figure 9 テスト シナリオのプロファイル マークに名前を付ける

最初のシナリオを開始する準備ができました。[Product Catalog Request] (製品カタログの要求) マークを選択し、[Insert Mark] (マークの挿入) ボタンをクリックして、シナリオの先頭にマークを付けます。次に、Internet Explorer® に切り替え、製品カタログを表示して最初のシナリオを完了します。Web サイトに製品カタログが表示されたら、[Product Catalog Rendered] (製品カタログ レンダリング済み) マークを挿入して、このシナリオの最後であることを示します。次のシナリオに移るには、Beer cap 製品を選択します。ここでも、追加の前後にそれぞれのマークを挿入します。これですべてのシナリオが終了したので、アプリケーションを終了します。

データの分析が完了すると、Visual Studio プロファイラにパフォーマンス レポートの概要が表示されます。このレポートはサンプリング レポートと若干異なり、最も呼び出される頻度が高い関数や、最も時間のかかる関数を示します。このデータはアプリケーションの有効期間を通じて集計されるため、2 つのシナリオおよびそれ以前のすべてのアクティビティが含まれているという点に注意することが重要です。

本当は、パフォーマンス レポートに特定のシナリオのデータのみを表示し、その他のデータはフィルタで除外したいところです。Visual Studio 2008 のプロファイラには、挿入されたすべてのマークを表示するマーク ビューが新しく追加されています (Visual Studio プロファイラはプログラムの最初と最後に自動的にマークを追加します)。最初のシナリオのフィルタを作成するため、シナリオの最初と最後を示すマークを選択し、コンテキスト メニューの [Add Filter on Marks] (マークにフィルタを追加) を選択します。必要なフィルタが自動的に作成されます (図 10 を参照)。マーク以外に、スレッド、プロセス、または時間間隔でフィルタすることもできます。フィルタの設定が完了したら、次に進み、これを実行します。

Figure 10 パフォーマンスをテストするターゲット プログラム
Figure 10 パフォーマンスをテストするターゲット プログラム(画像を縮小するには、ここをクリックします)
Figure 10 パフォーマンスをテストするターゲット プログラム
Figure 10 パフォーマンスをテストするターゲット プログラム (画像を拡大するには、ここをクリックします)

このフィルタリングはパフォーマンス レポートのすべてのビューに適用されます。そのため、Visual Studio プロファイラには、フィルタ処理されたデータの新しい概要ページが自動的に表示されます。概要ページは製品カタログのレンダリング シナリオに固有です。たとえば、System.IDisposable.Dispose の実行には 3.4 秒 (シナリオの実行時間の 61%) かかっていますが、以前は 41% でした。フィルタリングにより、特定の問題に関してその関数がどの程度重要であるかを正確に知ることができます。

では、パフォーマンスの問題を解決します。コード内でこれらのオブジェクトを破棄した関数を検出する必要があります。最も簡単な方法は、図 11 のようにコール ツリーでホット パス機能を使用することです。SetInputControlsHighlight 関数が IDisposable.Dispose の呼び出しの大部分を占めていることがすぐにわかります。

Figure 11 ホット パスを使用して問題を見つける
Figure 11 ホット パスを使用して問題を見つける(画像を縮小するには、ここをクリックします)
Figure 11 ホット パスを使用して問題を見つける
Figure 11 ホット パスを使用して問題を見つける (画像を拡大するには、ここをクリックします)

この関数に次のような非効率なログ機能のメカニズムが存在することがわかりました。

foreach (Control ctl in container.Controls) {
  log += "Setting up control: " + ctl.ClientID;
  string tempDir =
    Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
  using (StreamWriter sw = new StreamWriter(
    Path.Combine(tempDir, "WebSite.log"), true)) {
    sw.WriteLine(log);
  }
 ...

この膨大なログはデバッグ中に誤って残されたものであり、特定の診断に役立たなくなっているため、削除した方が無難です。この場合も、Visual Studio 2008 のホット パス機能でアプリケーションのボトルネックを即座に特定することができます。

アプリケーションをネイティブ C/C++、C#、または Visual Basic のいずれで作成した場合でも、Visual Studio プロファイラはパフォーマンス調査を大幅に簡素化し、より高速で効率的なアプリケーションを作成するのに役立ちます。Visual Studio 2008 では Visual Studio プロファイラをさらに改善し、プログラムのパフォーマンス上のボトルネックが従来より簡単に見つかるようになりました。

Hari Pulapaka は、Microsoft Visual Studio プロファイラ チームでテストを担当するソフトウェア設計エンジニアです。連絡先は、haripul@microsoft.com です。

Boris Vidolov は、マイクロソフトのプログラム マネージャです。ソフトウェア業界で 10 年以上の経験を持ち、多数のパフォーマンス重視アプリケーションを手がけてきました。Boris の詳細については、本人のブログ perfcop.spaces.live.com を参照してください。


Office 開発: 実世界での OBA ソリューション パターン

Office 開発

実世界での OBA ソリューション パターン

Steve Fox
この記事の内容 : :
  • OBA の基礎
  • OBA パターン
  • Microsoft Office と LOB の統合
  • OBA パターンの実際のアプリケーション
この記事は次のテクノロジを使用しています:
Visual Studio 2008
コンテンツ
OBA パターン
OBA ソリューション パターンを使用する
問題点
リーチ チャネルとしての OBA アプリケーション
デザインに取りかかる
その他の使用方法

SAP や PeopleSoft などの大規模システム、およびその他の包括的な基幹業務 (LOB) システムは、あらゆる種類のビジネス データとプロセスの管理を成功させるために不可欠です。ただし、組織のすべての人がこれらのシステムにアクセスできるわけではなく、選ばれた数人のみがシステム内のビジネス データを使用できる場合が多々あります。そのため、処理のためにデータがシステムから抽出され、ビジネス データ ソースとそのデータを使用するインフォメーション ワーカーの間の接続が断たれることになります。

Office Business Applications (OBA) は、Microsoft® Office を使用して LOB システム内のビジネス データとインフォメーション ワーカーの間の境界線に橋を架けることで、この問題に対処します。Office 機能により、カスタム フォーム領域とフォルダを通じてカスタマ リレーションシップ マネジメント (CRM) データを Microsoft Outlook® に統合する、ビジネス インテリジェンスを Microsoft Office SharePoint® Server (MOSS) に統合して販売実績データへのビューを提供する、さらには Microsoft Excel® と財務データを統合して LOB システムのデータを直接使用する予測テンプレートを提供するなどのタスクを実行できます。

OBA を理解するために、OBA を、LOB システム、LOB システムと統合されるカスタマイズされた Office クライアント、および LOB システムと統合されるサーバー コンポーネント (基本的には MOSS) という 3 つの主要部分を含む非常に単純なモデルと考えることができます。OBA を構築するときに、Exchange Server 2007 や PerformancePointTM Server 2007 などの他の Microsoft サーバー製品も利用できます。

アーキテクチャの観点からは、LOB システムとの統合には主にサービス指向アーキテクチャ (SOA) を使用しますが、OBA を使用すると、サービス層とのインターフェイスをとるときにさまざまな方法でクライアントをカスタマイズできます。たいていは、Visual Studio® Tools for Office (VSTO) を使用してこれを行います。たとえば、図 1 に、カスタム リボンおよびカスタム作業ウィンドウとのインターフェイスを示します。どちらも SAP からのイベント情報を Excel 2007 スプレッドシートに挿入するサービス層とのインターフェイスをとり、どちらも VSTO 3.0 を使用して構築されています。VSTO 3.0 は VSTO の最新リリースであり、Visual Studio 2008 Professional 以上に含まれています。Microsoft Word コンテンツ コントロールを使用してこれらのコントロールをデータにバインドするようにインターフェイスをデザインしたり、カスタム ツール バー、フォーム領域、またはカスタム リボンで Outlook を拡張したりすることもできます。クライアントをカスタマイズする方法については、補足記事「OBA 情報リソース」を参照してください。

Figure 1 SAP と Excel 2007 の統合
Figure 1 SAP と Excel 2007 の統合(画像を縮小するには、ここをクリックします)
Figure 1 SAP と Excel 2007 の統合
Figure 1 SAP と Excel 2007 の統合 (画像を拡大するには、ここをクリックします)

MOSS 2007 には、使用できる多数のオプションがあります。たとえば、さまざまな種類の Web パーツをホストするサイトを構築できます。普及している OBA 用の Web パーツには、ビジネス データ カタログ (BDC) Web パーツ (企業全体のエンティティと、それに関連するサービスまたは ADO.NET 接続を管理します)、Excel サービス Web パーツ (スプレッドシート データを Web パーツにリンクします)、および主要業績評価指標 (KPI) Web パーツ (パフォーマンス メトリックを提供します) があります。もちろん、Office Business Applications の作成に使用できる Web パーツは他にもたくさんあります。図 2 に、SharePoint サイトの BDC Web パーツと Excel サービス Web パーツを示します。

Figure 2 BDC Web パーツと Excel サービス Web パーツ
Figure 2 BDC Web パーツと Excel サービス Web パーツ(画像を縮小するには、ここをクリックします)
Figure 2 BDC Web パーツと Excel サービス Web パーツ
Figure 2 BDC Web パーツと Excel サービス Web パーツ (画像を拡大するには、ここをクリックします)

可能性は無限のように思えますが、作業を開始するのに役立つヘルプがあります。このヘルプは、中核となる 7 つの OBA ソリューション パターンのセットで提供されています。次に、これらのソリューション パターンを紹介し、実際にその 1 つを OBA のデザインと開発に適用します。

OBA パターン

OBA パターンの目的は、OBA を設計および開発する方法、特定のパターンで使用するテクノロジ、および OBA の開発に取りかかる方法を示すことです。関連するテクノロジの一部として、OpenXML、MOSS 2007、Web サービス、および VSTO 3.0 があります。

7 つの中核パターンの一部には、サブパターンがあります。また、OBA は Office プラットフォームの複数の部分を利用する複合アプリケーションであるため、パターンの間に関係があることがわかります。7 つのパターンとそれぞれの目的を図 3 に示します。

Figure 3 OBA Patterns (Close Figure 3)

パターン | 説明
リーチ チャネルとしての OBA アプリケーション | LOB アプリケーション機能をより広範なユーザー ベースに拡張する方法を示します。
ドキュメント統合 | LOB アプリケーションから Office ドキュメントを生成し、LOB データを Office ドキュメントに埋め込む方法を説明します。
複合ユーザー インターフェイス | Office ドキュメントまたは SharePoint Web ページでの複数のアプリケーション ユーザー インターフェイスの作成について説明します。
補完ドキュメント ワークフロー | ワークフローを使用してドキュメント中心のプロセスを制御および監視する方法について説明します。
検出ナビゲーション | LOB システム ビジネス データとのより自然な対話方法を提供し、複数の LOB アプリケーションを検索してデータを検出する方法を示します。
コラボレーション サイト | 構造化ビジネス プロセスを非構造的な人的交流で補強する方法について説明します。
アプリケーションで生成されるタスクおよび通知 | Outlook を基本ユーザー インターフェイスとして使用し、LOB アプリケーションで生成されたタスクおよびアラートを受信して処理する方法を示します。

各パターンは、1 つのパターンを表すか、より詳細なガイダンスを提供するサブパターンから構成されています。たとえば、リーチ チャネルとしての OBA アプリケーションは、SOA ソリューションと非 SOA ソリューションに関して詳細なガイダンスを提供する 2 つのサブパターン (直接統合と仲介統合) から構成されます。リーチ チャネル パターンとしての OBA アプリケーションについては、後で詳しく説明します。

パターンについて重要なこととして、OBA の開発に使用する Office プラットフォーム内のテクノロジに関するガイダンスをパターンが提供することもあります。たとえば、ドキュメント統合パターンとそのサブパターンは、OpenXML、VSTO、BDC、またはより一般的にはドキュメント マークアップを提示します。図 4 に、7 つの中核 OBA ソリューション パターンを構成するサブパターンの概要 (存在する場合) と、特定の OBA パターンを使用して OBA ソリューションをデザインするときに必要なテクノロジを示します。

Figure 4 OBA Solution Patterns, Sub-Patterns, and Associated Technologies (Close Figure 4)

パターン | サブパターン | 使用されるテクノロジ
リーチ チャネルとしての OBA アプリケーション | 直接統合 - サービスを使用せずに複合 UI にカスタム形式でビジネス データを提供するカスタム コンポーネント。仲介統合 - 相互の通信を中間層が抽象化します。より緩やかに結合され、宣言的でサービス指向です。 | VSTO MOSS 2007 BDC
ドキュメント統合 | アプリケーションで生成されるドキュメント - LOB システムから (サーバー側バッチ プロセスを使用して) 生成されます。直接統合または仲介統合も使用できます。インテリジェント ドキュメント - ドキュメントに直接埋め込まれた LOB システム データ (コンテンツ制御、名前付き範囲)。 | OpenXML VSTO BDC ドキュメント マークアップ
複合ユーザー インターフェイス | コンテキスト駆動の複合ユーザー インターフェイス - イベントおよびアプリケーション コンテキスト (特定のデータまたはユーザー プロファイル) に結び付けられたカスタム UI コンポーネント。メッシュ複合ビュー - マスタ/詳細複合ユーザー インターフェイス (SharePoint の BI ビュー) を作成するための接続可能な Web パーツ。RSS および Web サービス複合 - RSS および Web サービスのメッシュ複合ビュー。分析 - データ分析ダッシュボード用のメッシュ複合ビュー。 | Web パーツ VSTO (リボン、カスタム作業ウィンドウなど) BDC Excel サービスおよびダッシュボード
補完ドキュメント ワークフロー | LOB で開始されたドキュメント ワークフロー - ワークフローにマッピングされたドキュメントがビジネス データで変換、拡張、または設定されます。追加のワークフロー機能はドキュメントに埋め込まれます。協調ドキュメント ワークフロー - ワークフローはドキュメントのコンテナの一部であるため、多くのユーザーが関係し、ドキュメントを使用して構造化された方法で協調する必要があります。 | Windows Workflow Foundation SharePoint Storage BDC
検出ナビゲーション | N/A | エンタープライズ検索 BDC
コラボレーション サイト | N/A | Windows SharePoint Services InfoPath® Forms Services
アプリケーションで生成されるタスクおよび通知 | 単純なタスクおよび通知配信 - 電子メール通知を使用した、タスクおよび割り当ての一方向フロー。タスク同期 - Outlook でのタスクの双方向同期。インテリジェント タスクおよび通知 - 割り当てられたタスクに基づいてアクションを実行する方法を説明します。フォームベースのタスクおよび通知 - InfoPath フォームを統合して機能豊富な検証とビジネス ルールの自動化を提供する方法を説明します。 | Outlook 2007 および MOSS Integration VSTO (カスタム作業ウィンドウ、Outlook フォーム領域など) InfoPath Forms Services

他の OBA コンテンツの使用の一部としてパターンを使用して、OBA の構成内容と OBA の構築に使用するテクノロジを理解したり、独自の OBA のデザインにパターンを使用したりできます。使用可能な参照アーキテクチャ パックもいくつかあります。これらは、製造や銀行などの特定の業界用の OBA アーキテクチャ (およびアプリケーション) ガイダンス パックです。詳細については、補足記事「OBA 情報リソース」を参照してください。

OBA パターンの必須要素の一部について説明しましたので、実世界のソフトウェア デザインに OBA ソリューション パターンを適用しましょう。

OBA ソリューション パターンを使用する

まず、ソフトウェア開発ライフサイクル (SDLC) プロセスのどこで実際に OBA ソリューション パターンを参考にするかを見てみましょう。筆者は問題点の識別から開始します。問題点 (または必要性) がないという方には、それではなぜソフトウェアを開発するのですか? とおたずねします。問題点を識別したら、その問題点にマッピングされる要件を識別します。この 2 つのステップは、プロセスの最も重要な部分です。正しく行わないと、問題が SDLC 全体に流れ込みます。要件を見きわめたら、デザインを開始できます。

デザイン フェーズでは、いくつかの質問について検討します。例を以下に示します。要件では、特定の環境が指定されているでしょうか (これはデザインの制約を暗示します)。この作業を行うために特定のソフトウェア (市販またはカスタム) が必要でしょうか。必要なハードウェアは何でしょうか。図 5 に、OBA パターンを組み込むためのデザイン フェーズの進め方を示します。

Figure 5 デザイン フェーズでの OBA パターン
Figure 5 デザイン フェーズでの OBA パターン(画像を縮小するには、ここをクリックします)
Figure 5 デザイン フェーズでの OBA パターン
Figure 5 デザイン フェーズでの OBA パターン (画像を拡大するには、ここをクリックします)

一連の要件を掌握し、OBA について理解したら、OBA パターンを参照して、あるパターンが要件に適合するかどうか、適合する場合、それはどのパターンかを判断します。さらに、特定のパターンを掘り下げて、サブパターンが要件にマッピングされるかどうかを確認します。いずれかのパターンと要件の間にマッピングが見つかった場合は、そのパターンをデザインのテンプレートとして使用できます。

問題点

前に説明したように、ソフトウェア開発は一般になんらかの問題点から開始します。ここに示す例では、問題点は、販売管理チームが最新の売り上げデータを表示する必要があるということです。この問題に取り組むために、チーム、環境、およびいくつかの基本要件についてもう少し詳しく知る必要があります。

次に、この環境について把握していることを示します。販売管理チームのメンバは 100 人で、そのすべてが Windows Vista®、2007 Office system、SharePoint、および最新バージョンの Internet Explorer® を実行しています。ほとんどのメンバはリモートですが、そのすべてが企業ネットワーク上で中央 SharePoint サイトまたはサーバーにアクセスできます。

説明の目的で簡略化した要件を次に示します。メンバは企業ネットワークに常時接続しているわけではないため、販売チームはその日の売り上げデータを Excel スプレッドシートに記録し、企業ネットワークに接続しているときにデータを LOB システムに手動で再入力します。また、このプロセスには、手動で実行されるデータ検証などの特定のステップがあります。チームは、断続的に接続する販売チームをサポートでき、検証などの手動プロセスの多くを自動化できるシステムを求めています (売り上げデータ入力と検証プロセスには、毎日約 30 分を要します。チームはこの時間を 5 分に短縮することを希望しています)。販売チームは Excel や Internet Explorer などのツールを使い慣れているため、管理チームは、メンバがチーム管理サイトにレポートを作成できるように Excel ベースのソリューションと SharePoint ビューを求めています。

問題点の定義を見ると、販売管理に必要なのは、売り上げデータの管理に使用されている LOB アプリケーションをリモートの営業員に直接拡張する方法であることがすぐにわかります。この定義を OBA ソリューション パターンの一覧と比較すると、リーチ チャネル パターンとしての OBA アプリケーションとよく一致することがわかります。

リーチ チャネルとしての OBA アプリケーション

このパターンの目的は、Office アプリケーションを使用して、より広範なユーザー ベースに LOB システム機能を拡張する方法を示すことです。パターン実装では、より多くのユーザーが LOB システムのビジネス データにアクセスできるようにします。このパターンに従い、たとえば Outlook または Excel をリッチ クライアントとして使用するか、SharePoint Web パーツをサーバー側シン クライアントとして使用して、LOB システムと統合できます。この場合は、OBA を小規模な組織に展開し、Excel と SharePoint は売り上げデータとのインターフェイスをとるのに適した 2 つの方法を提供するため、このパターンが適切です。実際には、クライアント側とシン クライアントの両方が LOB システムにアクセスできるようにするソリューションが理想です。したがって、売り上げデータ OBA は図 6 のようになります。

Figure 6 LOB システムへのクライアント側アクセスとシン クライアント アクセス
Figure 6 LOB システムへのクライアント側アクセスとシン クライアント アクセス(画像を縮小するには、ここをクリックします)
Figure 6 LOB システムへのクライアント側アクセスとシン クライアント アクセス
Figure 6 LOB システムへのクライアント側アクセスとシン クライアント アクセス (画像を拡大するには、ここをクリックします)
Figure 6a LOB システムへのクライアント側アクセスとシン クライアント アクセス
Figure 6a LOB システムへのクライアント側アクセスとシン クライアント アクセス(画像を縮小するには、ここをクリックします)
Figure 6a LOB システムへのクライアント側アクセスとシン クライアント アクセス
Figure 6a LOB システムへのクライアント側アクセスとシン クライアント アクセス (画像を拡大するには、ここをクリックします)
Figure 6b LOB システムへのクライアント側アクセスとシン クライアント アクセス
Figure 6b LOB システムへのクライアント側アクセスとシン クライアント アクセス(画像を縮小するには、ここをクリックします)
Figure 6b LOB システムへのクライアント側アクセスとシン クライアント アクセス
Figure 6b LOB システムへのクライアント側アクセスとシン クライアント アクセス (画像を拡大するには、ここをクリックします)

より詳細に確認すると、リーチ チャネルとしてのアプリケーションには、直接統合と仲介統合の 2 つのサブパターンがあることがわかります。それでは、それぞれを詳しく見ていきましょう。

直接統合では、基礎となる LOB システムへのアクセスは、Office クライアントまたは SharePoint Web パーツに直接統合されます。このパターンを使用する場合、通常はバックエンド プロセスまたはデータ ソースへの直接接続を構築し、追加のビジネス ロジックが最低限で済みます。

直接統合は一般的ですが、複数のシステム間での再利用が困難です。仲介統合では、LOB システムとカスタム クライアント コンポーネント (たとえば、カスタム作業ウィンドウ) との間の統合を仲介するサービス層の作成が必要であるため、複数のシステム間での再利用とクライアントおよびサーバー間の疎結合が容易になります。売り上げデータ OBA にとって、これは朗報です。ここで、売り上げデータ モジュール (ここにすべての売り上げデータが入力されます) をラップする Web サービスを作成し、その Web サービスをすべてのクライアント インターフェイスと統合できます。したがって、LOB システムを Excel と統合する必要がある場合は統合できます。Visual Studio 2008 で作成する参照、または SharePoint 内の BDC を通じて、このサービスを使用できます。

これで、問題提起と要件が確定し、OBA パターンを適用する準備ができました。結果として生成されるアーキテクチャでは、仲介統合パターンを利用します。つまり、Web サービスを利用して Excel と SharePoint に販売機能を提供する OBA です。(図 6 と比較すると) 図 7 の内容は若干異なることに注目してください。それは、BDC がデータ ビジュアル化のための読み取り専用機能としてのみ動作するのに対し、クライアント側カスタマイズからは読み取り/書き込み呼び出しを発行できるためです。これがアーキテクチャ ダイアグラムに反映されている必要があります。

Figure 7 売り上げデータ OBA アーキテクチャ
Figure 7 売り上げデータ OBA アーキテクチャ(画像を縮小するには、ここをクリックします)
Figure 7 売り上げデータ OBA アーキテクチャ
Figure 7 売り上げデータ OBA アーキテクチャ (画像を拡大するには、ここをクリックします)

現在、アーキテクチャには Excel 2007 アドイン、MOSS 2007 サイト、および LOB システムへの仲介接続として機能する Web サービスが含まれています。さらに、関連するテクノロジについて理解したので、より詳細な仕様を作成し、(OBA ソリューション パターンに関連するテクノロジを使用して) 開発環境を構築できます。

この時点で、OBA ソリューション パターンを棚上げにすることもできます。ただし、選択したパターンを 7 つの中核ソリューション パターン内の他のパターンと比較する必要があります。これによりアーキテクチャが検証され、Office 開発プラットフォームのサーバー、ツール、およびサービスをより多く含むソリューションのバージョン 2.0 に関するアイデアが浮かぶ場合があります。

最後に、アーキテクチャ内の各コンポーネントを構築する方法を検討します。概要を示し、説明した各テクノロジをドリルダウンできる追加リンクも示します。

デザインに取りかかる
OBA 情報リソース

OBA 開発の領域はかなり広いため、いくつかの推奨資料をまとめました。
『6 Microsoft Office Business Applications for Office SharePoint Server 2007』
microsoft.com/mspress/books/9471.aspx
『Programming Microsoft Office Business Applications』
microsoft.com/mspress/books/12194.aspx
Office Business Applications の使用開始
msdn2.microsoft.com/bb614541
OBA Central
obacentral.com
OBA 参照アーキテクチャ パック
msdn2.microsoft.com/bb265266
VSTO デベロッパー ポータル
msdn2.microsoft.com/aa905533
Office デベロッパー ポータル
msdn2.microsoft.com/office
SharePoint デベロッパー ポータル
msdn2.microsoft.com/sharepoint
ビジネス データ カタログ定義エディタを使用してデータベース接続を作成する
msdn2.microsoft.com/bb736296
ビジネス データ カタログ定義エディタを使用して Web サービス接続を作成する
msdn2.microsoft.com/bb737887
LOB システムと Microsoft Office System を統合する
msdn2.microsoft.com/bb896607

アーキテクチャが作成されたら、この OBA の構築に取りかかる方法を理解する必要があります。MOSS サイト設定 (または既存のサイトに対して開発するための管理者権限) が必要なことがわかっています。新しいサイトを設定することを選択した場合は、Windows Server® 2003 (または Windows Server 2008) に MOSS (およびすべての前提ソフトウェア) をインストールする必要があります。また、Visual Studio 2008 Professional Edition が、2007 Office system Professional Edition、および Visual Studio 2008 のセットアップ オプションである Office Primary Interop Assemblies (PIA) と共にインストールされている必要もあります。LOB システムのインスタンスに対して Web サービスを開発およびテストする必要があるため、LOB システムへのアクセス権も必要です。

この環境を設定するには少なくとも 1 日かかります。筆者は、サーバー オペレーティング システムを設定し、サーバーに MOSS を設定し、サーバーに Office をインストールしてから、ローカル マシンに Visual Studio 2008 (および PIA) をインストールすることでこれを行います。この時点で、作成する (または既に存在する) サービスへのアクセスをテストできます。

環境を設定したら、Web サービスを作成する必要があります。売り上げデータ OBA の場合、通常は少なくとも 2 つの Web サービスを作成する必要があります。売り上げデータ (Excel クライアント アドインおよび MOSS サイトに表示されます) を取得する Web サービスと、Excel クライアント アドインからのデータを更新する Web サービスです。LOB システムは汎用的ですが、Web サービス ツールを使用して、既存のビジネス モジュールなど、よりドメイン固有の機能をラップするサービスを作成できます (SAP 用語を使用)。

さまざまな LOB システムは明らかに異なる方法で動作しますが、目標は、BDC 内および VSTO アドイン内で使用できるサービスを最終的に作成することです。ここで関連分野 (つまり、SAP と Office の統合) に関するヘルプが必要な場合は、SAP 用の Web サービスの作成方法に関するヘルプを提供している OBA Starter Kit for SAP を参照してください (msdn2.microsoft.com/bb498189)。

Web サービスを作成したので、VSTO アドインを作成できます。これを作成するには、Visual Studio 2008 を使用して、VSTO アプリケーションレベルのアドインまたはドキュメントレベルのソリューションを作成します。それぞれ、同様の方法で Web サービスを使用します。通常は、売り上げデータを管理するためのカスタム作業ウィンドウをアドインに作成し (表示されるデータを制御するフィルタ)、そのデータでの表示を管理するカスタム リボンを作成します (チャートの作成、データに対する機能の検証などを行います)。この処理を行う方法に関する多数の有益な情報が VSTO デベロッパー センター (msdn2.microsoft.com/aa905533) にあります。

次のステップは、MOSS サイトへの Web サービスの追加です。これは、Web サービスのアプリケーション定義ファイル (ADF) の作成 (手作業での XML ファイルのコーディング、または BDC 定義エディタや BDC MetaMan などのツールを使用した作成)、SharePoint への ADF のインポート、BDC Web パーツの作成、さらに BDC Web パーツと LOB システム間の統合を完成させるための ADF への Web パーツの結び付けを意味します。これを行う方法の詳細については、msdn2.microsoft.com/bb737887 を参照してください。

このソリューションを配置することは、クライアント側の配置メカニズムと MOSS サイトの両方を、販売チームが BDC Web パーツと Web サービスにアクセスできる場所に配置することを意味します。VSTO クライアント側アセンブリを SharePoint サイトに公開でき、販売管理チームはアセンブリをローカル クライアントにダウンロードできます。ローカル クライアントは、アドインが呼び出されるたびに配置マニフェストを介してサーバー上の更新をチェックします。

Web パーツの配置プロセスには既存のサイトへの Web パーツの作成が含まれるため、BDC Web パーツを配置するには、既存の MOSS サイトが設定され、チームが使用できるように構成されている必要があります。配置の詳細については、VSTO デベロッパー センター (msdn2.microsoft.com/aa905533) と SharePoint デベロッパー センター (msdn2.microsoft.com/sharepoint) の両方を参照してください。

その他の使用方法

OBA パターンの適用方法を説明しましたが、自身の目的に合わせてこれらを別の方法で適用することが必要になる場合があります。たとえば、既存のパターンおよび提案されたパターンを理解するためにこれらをアーキテクチャ デザインのガイダンスとしてのみ使用し、その後は不要になる場合、これらを OBA ソリューションのアーキテクチャ テンプレートとしてデザインで使用する場合、または OBA ソリューションに構造を提供する方法として繰り返し使用し、その後、仕様プロセスに対する前提条件とする場合があります。先に進むときに、OBA 開発の追加のガイダンスを目にする可能性があります。これにはおそらく、既存のパターンへの拡張が含まれます。注意して見てください。

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


編集者のメモ: スイミング プールが多すぎる

編集者のメモ
スイミング プールが多すぎる
Howard Dierking

私は昔から、 デザインすることが大好きでした。小学 4 年生のときに自分の夢の家を設計したのが、私の最初のデザイン プロジェクトです。そのときからずっと、自分の奇想天外なアイデアをすべて盛り込んでビジュアル化し、代替案を模索し、新しい可能性を探求するデザインという作業は、私にこのうえない満足感を与えてくれました。自分の頭の中にあったアイデアが完璧な形で結実したときには、他では体験できないような感動を覚えます。

私の経歴のほとんどの期間は、ソフトウェア システムの完璧なデザインを追い求めることに費やされました。パターンを学習し、UML 仕様を読み、テスト駆動開発 (TDD: Test Driven Development) やドメイン駆動設計 (DDD: Domain-Driven Design) などの事例を取り入れて、今でも非常に誇りに思えるシステムをいくつか構築することができました。

その一方で、すぐに書き直さなければならないシステムもありました。それは、最新の優れたデザイン パターンを適用することばかりに気をとられて、解決すべきビジネス上の問題を置き去りにしたことが原因でした。

さて、私がデザインという分野で経験を重ね、学んだことを何だったでしょうか。ここで少しだけ、4 年生の時の "夢の家プロジェクト" のことをお話します。あのときは、実際に家を建てることが前提ではなかったので、いささか現実とかけ離れた設計ができあがりました。私が設計したのは、地上 14 階建ての家です。スイミング プールを 4 つ設置し、ボーリングのレーンもいくつか用意し、そして体育館も 2、3 作る計画でした (そういえば、寝室のことは忘れていました)。地下 400 フィート (およそ 120 メートル) の建物で、ローラーコースターの乗降口が家とつながっているのです。

私がこれまでに設計したシステムに話を戻しましょう。完全に失敗したシステムには 2 つのパターン (反パターン) があるようです。まず 1 つは、初期のデザイン作業の多くがプログラミング言語ではなくモデリング ツールを使用し、現実とは隔絶されたような状態で行われていたことです。もう 1 つは、多くのデザインが主に特定のパターンを適用することによって作られており、パターンが出現したらそれを許容するという考えに基づいていなかったことです。その結果として生成されるアプリケーションは、まるで私が設計した夢の家のように、理論上はすばらしいものであっても実現がほぼ不可能だったのです。スイミング プールが多すぎて、十分な居住スペースがないのですから。

MSDN Magazine の読者の皆さんにとって、これは何を意味しているでしょうか。最新の読者アンケートの結果の中で注目すべき事柄の 1 つは、ソフトウェア設計についてもっと取り上げてほしいという要望が皆さんから寄せられたことです。その声にお応えして、ソフトウェア設計に関する記事をもっと増やすことにしました。ご覧のとおり、この号では依存関係の注入や継続的な統合といった話題がトピックで取り上げられています。さらに、現在、ソフトウェア設計のベスト プラクティスに関する新しいコラムのプランが進行中です。もちろん、現実に即した情報をお届けするという姿勢を守り、皆さんが実際のプロジェクトに適用できるようなテクニックを紹介していきます。私が 4 年生で作った夢の家のようなプロジェクトになっては大変ですから。

そして、これまで同様、サービスおよび Web 2.0 開発から同時実行やセキュリティの問題に至るまで、プログラミングに関する多彩な記事を偏りなく提供していくつもりです。ここで、いくつかお知らせがあります。Keith Brown は「セキュリティに関するブリーフィング」コラムの執筆者の任を離れ、他の仕事に専念することになりました。これは非常に残念なことではありますが、さいわい、このコラムの執筆は Michael Howard が受け継ぐことになり、とても嬉しく思っています。Michael はその豊富な経験に基づき、すばらしいコラムを届けてくれるに違いありません。

最後になりましたが、次号から開始されるコラムについてお知らせします。モバイルのあらゆる話題を取り上げる新しいコラムです。より洗練された新しいデバイスが次々と発売される中で、どこでもどのデバイスでも実行できるシステムへのニーズはさらに高まるものと思われます。開発者のコミュニティがこのニーズにどのように対応するかについて、もっと日常的に話し合われるようになってほしい。このコラムがその一助になればさいわいです。

Thanks to この記事の執筆に携わったマイクロソフトの技術部門の専門家は、Glenn Block、James Brundage、Ben Carter、John Cunningham、Elisa Flasko、Steve Fox、Scott Hanselman、Habib Heydarian、Lee Holmes、Ade Miller、Larry Olson、Polita Paulus、Michael Puleio、Karthik Ravindran、Stephen Toub、Eric White、および Jon White です。


ツールボックス: 簡単な Web デバッグ、Reflector の拡張、その他

ツールボックス
簡単な Web デバッグ、Reflector の拡張、その他
Scott Mitchell

コンテンツ
クライアント側の Web 開発
注目のブログ
Lutz Roeder の Reflector を拡張する
推薦図書

クライアント側の Web 開発

Visual Studio® のデザイン時における操作感は、Web アプリケーションに対して HTML、CSS、および JavaScript を記述する作業を真に単純化するものですが、完璧ではありません。デザイナは、ブラウザでページが表示されるイメージをかなり正確に描くことができますが、それは必ずしも厳密に一致するわけではなく、そしてもちろん、HTML や CSS が動的に追加されるような状況はサポートできません。さらに、デザイナはクライアント側スクリプトを実行しません。その結果、Web ページの外観とクライアント側のスクリプトをテストするときには、デザイナとブラウザとの間で頻繁な切り替えが必要になります。

幸運にも、レンダリングされた Web ページのマークアップ、CSS、およびスクリプトを直接ブラウザ内で検査して変更できるツールがいくつかあります。そのようなツールの 1 つが、Joe Hewitt の Firebug 1.05 です。Firebug は、Mozilla Firefox ブラウザ用の無料のオープン ソース アドオンであり、ページの HTML、CSS、JavaScript、イメージ、および Flash コンテンツに関する情報をタブ付きインターフェイスで表示します。

Firebug を使用すると、ページの HTML 要素が検索可能なツリー形式で表示されるため、[表示] メニューの [ソース] を使用する必要がなくなります。ツリー内のノードにマウス ポインタを合わせると、対応する領域がブラウザ内で強調表示され、ノードを選択すると、その CSS スタイルおよびドキュメント オブジェクト モデル (DOM) 属性が表示されます。Firebug では、ページの CSS 定義および JavaScript も表示されます。

また、HTML および CSS は Firebug 内で編集することができ、編集結果はブラウザに直ちに反映されます。マウスをクリックすることで、HTML 要素の追加または削除、属性の変更、CSS 定義の作成、更新、または無効化が可能です。これらは、ブラウザでのメモリ内の変更であり、変更を永続的に適用するには Visual Studio に戻る必要がありますが、Firebug では直ちにフィードバックが得られるので、レイアウトや CSS に加えた変更のテストやデバッグを格段に速く行うことができます。

また、Firebug は、スクリプト エラーを見つけて修正するのにも役立ちます。ページで使用されている外部 JavaScript ファイルがドロップダウン リストに列挙され、項目を選択するとそのスクリプトが強力な JavaScript デバッガ内に表示されます。このデバッガはブレークポイントをサポートし、ウォッチ ウィンドウを備え、JavaScript のステップ実行が可能です。つまり、これはすべての Web 開発者のツールボックスに必携のツールです。

価格 : 無料 (オープンソース)

getfirebug.com

Firebug Displays HTML, CSS, JavaScript, Images, and Flash Information
Firebug Displays HTML, CSS, JavaScript, Images, and Flash Information (画像を縮小するには、ここをクリックします)
Firebug Displays HTML, CSS, JavaScript, Images, and Flash Information
Firebug Displays HTML, CSS, JavaScript, Images, and Flash Information (画像を拡大するには、ここをクリックします)

注目のブログ

私の好きなブロガーの 1 人である Scott Guthrie は、マイクロソフト開発部門の統括マネージャです。彼のブログで最も気に入っているのは、深い内容を持ったマルチパートのチュートリアル シリーズです。Scott は、データ駆動型 Web サイト、ASP.NET Model View Controller (MVC) Framework、および LINQ to SQL についての詳細なチュートリアルを執筆しています。各チュートリアルには、手順ごとの説明と、豊富なスクリーンショットが含まれ、どのようなスキル レベルの開発者でも容易に理解できるようになっています。また、重要な詳細が不足していることはなく、必要なすべての情報が含まれています。

Scott は、製品ロードマップなど、将来の展望に関する他のトピックについてもブログを執筆しています。Visual Studio、Microsoft® ASP.NET AJAX Framework、およびデータ、セキュリティ、パフォーマンス、LINQ、Silverlight®、その他のテクノロジの使用に関して、ヒント、秘訣、課題などが記述されています。さらに、月に 1 回、お気に入りのブログ、記事、および書籍が紹介されます。

Scott のブログのもう 1 つの優れた点は、読者のコメントに対する応答の早さです。Scott が作成に携わるいずれかの製品を使用している開発者であれば、購読する価値のあるブログの 1 つといえます。

価格 : 無料

Weblogs.asp.net/scottgu

Scott Guthrie Announces .NET Code Release
Scott Guthrie Announces .NET Code Release (画像を縮小するには、ここをクリックします)
Scott Guthrie Announces .NET Code Release
Scott Guthrie Announces .NET Code Release (画像を拡大するには、ここをクリックします)

Lutz Roeder の Reflector を拡張する

Reflector for .NET Version 5.0 (aisto.com/roeder/dotnet) は、Microsoft .NET Framework 用の無料のクラス ブラウザ、アナライザ、およびデコンパイラであり、.NET Framework 1.0 の初期の頃から提供されています。Reflector は、任意のマネージ アセンブリについて型、メソッド、プロパティ、およびイベントを表示します。単純なクラス ブラウザ インターフェイスに加えて、Reflector は、特定の型、メソッド、またはプロパティが他のメソッドやプロパティからどのように呼び出されるか、および他のメソッドやプロパティにどのように依存するかを分析することができます。さらに、Reflector は、メソッドまたはプロパティを対応する C# または Visual Basic® のコードに逆コンパイルできます。

Reflector は、マネージ アセンブリの検査および分析のための強力なツールです。デコンパイラとして使用すれば、コードが失われてしまった .NET アセンブリのソース コードを調べて取得することもできます。アナライザでは、メソッドおよびプロパティ間の依存関係をすばやく示すことができます。最も優れている点は、Reflector はアドインをサポートするようにデザインされていることです。これまでに .NET コミュニティの何人かの開発者が 30 の強力な Reflector アドインを作成しており、codeplex.com/reflectoraddins で提供されています。

Code Metrics アドインは、選択されたアセンブリ上の大量のコード品質メトリックスを計算することで、型、メソッド、およびプロパティについて、最も大きいもの、最も使用頻度の高いもの、および最も複雑なものの概要やその他の情報を提供します。Graph アドインは、依存関係グラフを使用してアセンブリの依存関係をビジュアルに表示し、クラス ダイアグラムを使用して型階層を示すことができます。また、Diff アドインは、アセンブリの 2 つのバージョン間で違いを比較するのに役立ちます。

他の便利なアドインには、アセンブリ全体の逆コンパイルされたソース コードを .vb または .cs ファイルに保存する FileDisassembler、SQL Server® 2005 データベースに格納された .NET アセンブリをブラウズするための SQL2005Browser、および Reflector の検索機能を強化して逆コンパイルされたコード内で文字列や正規表現を検索できる CodeSearch などがあります。

価格 : 無料

Reflector: aisto.com/roeder/dotnet

Reflector アドイン : codeplex.com/reflectoraddins

Code Metrics Add-Ins in Reflector
Code Metrics Add-Ins in Reflector (画像を縮小するには、ここをクリックします)
Code Metrics Add-Ins in Reflector
Code Metrics Add-Ins in Reflector (画像を拡大するには、ここをクリックします)

推薦図書

SQL について解説する書籍のほとんどは、無味乾燥な文章で、ビジネスや会計のシナリオに焦点を絞っています。Lynn Beighley の『Head First SQL』(O'Reilly) は、このアプローチをまったく逆転させ、優れた効果を生み出しています。『Head First SQL』で取り上げられているトピックは、SELECT、INSERT、UPDATE、DELETE クエリの基本、JOIN、サブクエリ、データ正規化、データとリレーショナルの整合性など、おなじみのものです。『Head First SQL』では、ユニークな取り上げ方により、これらを楽しく学習し、簡単に覚えることができます。

筆者は O'Reilly の『Head First』シリーズ全体の大ファンです。このシリーズの各タイトルでは、さまざまなテクノロジが魅力的なビジュアル構成で紹介されています。一般的なコンピュータ本とは異なり、『Head First』の各タイトルは、テキストの段落間にスクリーンショットが散在する従来のレイアウトの代わりに、図やコード スニペットを豊富に使用しています。また、ほとんどの書籍に見られる大げさな企業事例ではなく、より興味深いストーリー形式の例を使用しています。たとえば、典型的な従業員データベースの例の代わりに、『Head First SQL』では、紙ベースのお見合いシステムをデータベース利用システムに変換する Greg 氏の作業を追っています。

『Head First SQL』は、最も複雑なシナリオでさえも常識的に見えるようなやり方で概念を説明するという、優れた特長を持っています。JOIN に関する章は、私がこれまで読んだ中で最も明快で理解しやすい説明です。サブクエリに関する章にも同じことが言えます。

『Head First』シリーズは、新しいテクノロジを学ぶための優れた手段ですが、全般的に、格式ばったコンピュータ本に見られるような詳細なレベルの情報は含んでいません。段落に次ぐ段落でテキストが羅列されているような構成は『Head First』シリーズの楽しいレイアウトに比べて魅力に乏しいかもしれませんが、テクノロジに習熟したり、特に複雑または深遠なトピックについて説明したりするには、より伝統的なアプローチがしばしば必要になります。手短に言えば、あなたや同僚が SQL のスキルをすばやく向上させる必要がある場合には、『Head First SQL』を手に取ることを考えてください。ただし、トピックに真に習熟するために必要な内容の深さを期待するのであれば、他の書籍をお勧めします。

価格 : $44.99

oreilly.com

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

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


CLR 徹底解剖: アプリケーションの起動パフォーマンスを向上させる

CLR 徹底解剖
アプリケーションの起動パフォーマンスを向上させる
Claudio Caldato

コンテンツ
コールド起動
ディスクから読み込まれるコードを識別する
システム アセンブリと他のプロセス
NGen パフォーマンス
Authenticode 検証
まとめ

アプリケーションの起動を待つ時間というのは、多くのユーザーをイライラさせるものです。あなたがクライアント アプリケーションの起動パフォーマンスを重点的に改善すれば、お客様に与える第一印象が格段にアップするだけでなく、その後の信頼関係にもつながるでしょう。また、起動パフォーマンスはユーザーにとって重要な問題なので、パフォーマンス低下の要因を探ることは有益な作業で、その結果として最も発生しやすいミスを防ぐこともできます。

アプリケーションの起動は、一般的にコールドまたはウォームに区分されます。マネージ アプリケーションにおいては、Microsoft® .NET Framework システム アセンブリとアプリケーションのコードがいずれもメモリに存在しない場合、それらをディスクからフェッチする必要があるためにコールド起動となります。これに対して、アプリケーションの 2 回目以降の起動の場合、またはシステム コードの大半が (以前に別のマネージ アプリケーションに使用されて) 既にメモリ内にある場合はウォーム起動です。

コールド起動

ほとんどの場合、コールド起動は I/O バウンドです。つまり、命令を処理する時間よりもデータを待機する時間が長くなります。アプリケーションの起動にかかる時間は、OS がディスクからコードをフェッチする時間と追加の処理 (アプリケーションの起動パスで実行される JIT 処理、IL コード、およびその他の初期化) を実行する時間の合計です。通常、コールド起動では処理はボトルネックになりませんので、アプリケーションの起動パフォーマンス調査での最初の目的は、読み込まれるコードの量を減らしてディスク アクセスを低減することです。

また、コードの記述方法も起動パフォーマンスの低下と大きな関係があるので、余分なファイルが開かれる、I/O リソース競合の可能性がある他のプロセスが呼び出される、といったアプリケーション起動時の問題点を見つけ出すことが大切です。

コールド起動は I/O バウンド シナリオであるため、従来の CPU プロファイラの使用は、(インストルメンテーションベースまたはサンプリングベースのいずれであっても) 調査の大きな助けにはなりません。インストルメンテーションベース プロファイラは、I/O で待機に費やされた時間をブロック時間として報告します。問題なのは、ブロック時間の原因が明らかに特定のコール スタックである場合でも、ブロック時間が 1 度しかカウントされないことです。以降のディスク I/O が含まれていないので、結果として、全実行時間にディスク I/O が与える影響を部分的にしか把握できません。

サンプリングベース プロファイラを使用する場合は、収集された情報を正しく分析するよう注意する必要があります。このプロファイラでは追跡の対象が I/O ではなく CPU 使用率なので、レポートには I/O に費やされた時間がすべて反映されるわけではありません。

図 1 をご覧ください。アプリケーションを 2 回続けて起動してみると、コールド起動が I/O バウンドであることが実感できます。2 回目の起動は、最初の起動に比べて大幅にスピードがアップすることが予想されます。実行に必要なコードの大半が最初の起動で使用されて既にメモリ内にあるため、2 回目の起動ではディスク アクセスが低減されて時間が短縮されるからです。もちろん、このテストでは最初の起動を確実にコールド起動にしなければなりません。マシンを事前に再起動し、フォルダにマネージ アプリケーションがないこと、およびマネージ コードを使用する Windows® サービスがユーザーのログイン時に実行されないことを確認しておきましょう。

Figure 1 コールド起動でのディスク読み取り時間と CPU 時間
Figure 1 コールド起動でのディスク読み取り時間と CPU 時間

コールド起動の理想的なテストのためには、SuperFetch サービスを無効にする必要があります。そうしないと、アプリケーションに必要なコードの一部が事前に読み込まれてウォーム起動シナリオになってしまうおそれがあります。SuperFetch を無効にしておけば、その心配はありません。アプリケーションに必要なすべてのコードがアプリケーション起動時に確実にメモリに読み込まれるので、I/O のコスト測定への信頼性が増します。ただし、実際のユーザー エクスペリエンスと測定内容は必ずしも一致しないということを忘れないでください。SuperFetch をオフにして収集したデータから、アプリケーションの実際のパフォーマンスに関する確定的な結論を導き出すことはできません。

I/O におけるコールド起動の影響を確認するために、% processor time と % disk read time という 2 つのパフォーマンス カウンタを利用できます。I/O がコールド起動の多くを占めている場合は、% processor time と % disk read time の間に大きな差が出るはずです。パフォーマンス カウンタは、PerfMon を使用して収集できます (詳細については「起動パフォーマンスのリソース」のリンクを参照)。

図 1 では、赤い線が % disk read time を示し、緑の線が % processor time を示しています。コールド起動の場合は、ディスクからの読み取り時間に比べて CPU 使用率が低いことがわかります。

アプリケーションが 2 回目に起動されたときはウォーム起動なので、パフォーマンス カウンタは異なる結果を示します。図 2 では、シナリオが CPU バウンドとなり、% disk read time が % processor time に比べて非常に小さくなっています。

Figure 2 ウォーム起動の時間短縮
Figure 2 ウォーム起動の時間短縮

ウォーム起動の場合、コードが既にメモリ内にあるので (追加の I/O が不要ですから) CPU バウンドですが、アプリケーションを実行する前にコードを JIT 処理する必要があります。現在、.NET Framework を使用してアプリケーションを実行したときに JIT で生成されたネイティブ コードは、次の実行時まで保存されません。

ウォーム起動がコールド起動に比べてさほど時間が短縮されない場合は、何が CPU サイクルを消費しているかを調べる必要があります。ウォーム起動ではほとんどのコードが事前に読み込まれているので、I/O バウンドになることが考えにくいからです。原因として可能性の高いものは、JIT 処理が必要な大量のコードや、アプリケーションで実行しなければならない複雑な計算などです。

JIT 処理が問題であるかどうかを判断するには、JIT のパフォーマンス カウンタの .NET CLR JIT\% time をチェックします。この値 (起動時は標準的に 30% ～ 40%) が高くない場合は JIT が大きな原因ではないと考えられるので、今度はプロファイラを使用して、CPU 時間が最も消費される処理を特定する必要があります。パフォーマンス カウンタが更新されるのは、メソッドが実際に JIT 処理される場合のみであることに注意してください。最後のメソッドが JIT 処理された後もカウンタは最新の値を報告します。0 には戻りません。したがって、アプリケーション起動時の最初の数秒間のみカウンタをチェックする必要があります。このとき、カウンタが急激に増えるかもしれません。これは、JIT コンパイラによって CPU 使用率が急増したことを示します。

もう 1 つ注意すべき点として、ユーザーのログオン時に読み込まれるアプリケーションはすべて他のサービスやアプリケーションと I/O リソースが競合するので、その数が多いほど起動時間にかかる時間が長くなるという問題があります。したがって、スタートアップ グループにはできるだけアプリケーションを追加しないことが肝心です。マシンの起動時に実行するよう設定されているアプリケーションを特定するには、優れたツール AutoRuns があります (microsoft.com/technet/sysinternals/Security/Autoruns.mspx で入手可能)。

ディスクから読み込まれるコードを識別する
起動パフォーマンスのリソース

過去に発行された「CLR 徹底解剖」コラムには、詳しい調査のための貴重な情報が記載されています。
アプリケーションの起動時間を向上させる (Claudio Caldato 著)
msdn.microsoft.com/msdnmag/issues/06/02/CLRInsideOut
アプリケーションへの悪影響が少ないコードを記述する方法について、有用なヒントが含まれています。
NGen によるパフォーマンス上の利点 (Surupa Biswas 著)
msdn.microsoft.com/msdnmag/issues/06/05/CLRInsideOut
NGen テクノロジの概要 (特にパフォーマンスについて) がわかりやすく解説されています。この記事は、ウォーム起動、コードの共有、コールド起動など異なるシナリオを比較検討する場合に NGen のコストと利点を理解するうえで役立ちます。
メモリ問題の調査 (Claudio Caldato、Maoni Stephens 共著)
msdn.microsoft.com/msdnmag/issues/06/11/CLRInsideOut
この記事は起動時のワーキング セット (重要なパフォーマンス メトリックの 1 つ) について論じていますが、起動そのものとは関連していません。マネージ アプリケーションで使用されるメモリが多い理由を調べるための高度なテクニックを取り上げています。

また、起動時間を短縮するための詳しい情報が記載されている以下の記事も参照してください。
バグへの挑戦 : マネージ デバッグ アシスタントを使用しての CLR による不具合の検出 (Stephen Toub 著)
msdn.microsoft.com/msdnmag/issues/06/05/BugBash
MDA (CLR と連携してランタイムの状態に関する情報を提供するデバッグ支援ツール) について書かれています。このツールにより、通常は生成できないランタイム イベントに関する情報が提供されます。
Windows Vista カーネルの内部 (Mark Russinovich 著) microsoft.com/technet/technetmag/issues/2007/03/VistaKernel
SuperFetch の動作の詳細、および (RAM を物理的に増設する代わりに) USB メモリ デバイスで RAM の容量を拡張できる ReadyBoost の機能について説明しています。
Authenticode による署名およびコードのチェック msdn2.microsoft.com/ms537364
Authenticode 署名プロセスを理解するために必要な情報をすべて提供します。
ランタイム プロファイリング
msdn2.microsoft.com/w4bz2147
PerMon を使用する方法、および各種カウンタが示す内容について説明しています。

次の手順は、ディスクから読み込む対象を決定し、意図せずに読み込まれるコードがあるかどかを確認することです。メモリに何が読み込まれるかを最も速やかに確認する方法は、VADump ツール (Windows Platform SDK に付属) を使用することです。図 3 には、次のコマンドを実行することで生成されたレポートの抜粋が示されています。

Figure 3 VADump Output (Close Figure 3)
Category                        Total        Private Shareable    Shared
                           Pages    KBytes    KBytes    KBytes    KBytes
      Page Table Pages       177       708       708         0         0
      Other System            39       156       156         0         0
      Code/StaticData       8169     32676      2160      8336     22180
      Heap                 14042     56168     56168         0         0
      Stack                    0         0         0         0         0
      Teb                      0         0         0         0         0
      Mapped Data              8        32         0         4        28
      Other Data               1         4         4         0         0

      Total Modules         8169     32676      2160      8336     22180
      Total Dynamic Data   14051     56204     56172         4        28
      Total System           216       864       864         0         0
Grand Total Working Set    22436     89744     59196      8340     22208

Module Working Set Contributions in pages
    Total   Private Shareable    Shared Module
       72         2        70         0 HeadTrax - HeadTrax.exe
      107         7         0       100 ntdll.dll
       37         4         6        27 mscoree.dll
       77         3         0        74 KERNEL32.dll
        6         2         0         4 LPK.DLL
       27         4         0        23 USP10.dll
      116         4         0       112 comctl32.dll
      878        23        79       776 mscorwks.dll

Heap Working Set Contributions
   0 pages from Process Heap (class 0x00000000)
   0 pages from Process Heap (class 0x00000000)
9332 pages from Process Heap (class 0x00000000)
  0x0255850F - 0xC255350F 9332 pages
   0 pages from Process Heap (class 0x00000000)
   0 pages from Process Heap (class 0x00000000)
4710 pages from Process Heap (class 0x00000000)
  0x00040000 - 0x10040000 4710 pages
   0 pages from Process Heap (class 0x00000000)

Stack Working Set Contributions
   0 pages from stack for thread 00001018
   0 pages from stack for thread 000017EC
   0 pages from stack for thread 0000187C

VADump –sop <proc ID>

ここで注意すべき点があります。VADump はツールが実行されたときにメモリに読み込まれる内容のみを把握するので、短い間だけメモリに読み込まれたモジュールが見落される可能性があることです。ディスクにページ アウトされたアプリケーションの一部 (コードまたはデータ) も同様です。VADump を使用する目的は、VADump レポートのリストに示されたモジュールをチェックし、すべて読み込むことが妥当かどうかを見極めることです。たとえば、XML を使用しないアプリケーションの起動時に System.Xml が読み込まれる場合などは、詳しく調査する必要があります。

Windows デバッガ (windbg) で sxe コマンドを使用して、アセンブリがどの関数で読み込まれたのかを確認できます。"sxe ld:<dll name>" コマンドを実行すると、指定の DLL が読み込まれたときにデバッガが停止します。次に、コール スタックをチェックし、どの関数によって DLL がメモリに読み込まれたのかを調べます。調査のこの部分は慎重に行ってください。アプリケーションが実際にメモリに読み込んだ内容を見つけ出すことは、そう簡単ではありません。

システム アセンブリと他のプロセス

起動時に不要なアセンブリが一切読み込まれないように処理した後は (さらに、起動時の初期化作業の一部が遅延されるようにアプリケーション コードを修正することも可能ですが)、次の手順としてシステム アセンブリから読み込まれるコードの量を減らします。残念ながら、システム API が使用された場合に読み込まれるコード量を確認できるツールは今のところ存在しないようです。そのようなツールがあれば、システム アセンブリから読み込むコードの少ない API をスタートアップ コードに使用できるので、開発者の大きな助けになります。現状ではそういったツールを利用できないので、インストルメンテーションベースのプロファイラ (Visual Studio® パフォーマンス ツールなど) を使用することにより、API のページ コストの概算を求めます。

プロファイル データを参照することにより、大きなコール ツリーを含むシステム コールに関連した API の回避を試みることができます。大きなツリー、つまり深く入れ子になっている呼び出しとは、各メソッド呼び出しのコードがディスクから取得されたことを示しています。したがって、これは呼び出しがどのくらいコスト高であるかを概算するための方法です。大きなコール ツリーと同じ機能を、深いシステム コール ツリーを持たない API を呼び出して実装できれば時間が短縮されます。コール ツリーをカットして節約できるコード量は簡単には確認できないので科学的とは言えませんが、コール ツリーが大きいほどディスクから読み込まれるコード量が多くなるということは、たいていの場合は理にかなっています。

アプリケーションの起動では、他のプロセスの起動が明示的または暗黙的に行われるようになっていることがあります。Windows デバッガ (windbg) で –o オプションを使用し、何が起動されるかを簡単に見つけることができます。–o オプションにより、デバッガは任意の子プロセスにアタッチされます。アプリケーションによって暗黙的に起動されるプロセスの典型的な例は、アプリケーションがシリアル化クラスをプリコンパイルしないで XML シリアル化を使用する場合です (Sgen ユーティリティを使用)。この状況においては、コンパイルのために C# コンパイラが起動されます。他のプロセスを呼び出すとアプリケーションの起動自体に大きな影響を与える可能性があるので、通常、これは非常にコストの高い動作です。

NGen パフォーマンス

ネイティブ イメージ生成 (NGen) は、JIT 処理コードのコストを回避するので、確実にウォーム起動の改善に役立ちます。また、コールド起動シナリオにおいても、アプリケーションで使用されるコードがすべて NGen でプリコンパイルされていれば mscorjit.dll を読み込む必要がなくなるので、NGen の使用は有益です。ただし、1 つのモジュールが対応するネイティブ イメージを持っていないだけでも mscorjit.dll は読み込まれます。そうすると、コードが JIT 処理されて CPU サイクルが消費されるだけでなく、JIT コンパイラでメタデータを読み取る必要があるために NGen イメージ内の多くのページが参照されます。その結果、起動時のパフォーマンスがかえって低下します。この理由から、起動時に JIT 処理を引き起こす可能性のあるコードを削除することが推奨されています。ただし、この手法を実行すべきかどうかは、生成されたネイティブ イメージがある場合とない場合の両方のケースで、コールド起動のパフォーマンスを測定して決定します。コールド起動における NGen の実際の利点は、アプリケーションのコードやサイズによって異なるからです。起動時に JIT 処理が行われなくても、起動パフォーマンスが大幅に向上するとは限りません。

JIT 処理が発生するかどうかを確認し、発生するタイミングを特定する唯一の方法は、マネージ デバッグ アシスタント (MDA) の使用です。JIT MDA により、メソッドが JIT 処理される前にデバッガに割り込むか、デバッグ情報を印刷することができます。MDA を有効にするには、次に示すように環境変数を設定します。

COMPLUS_MDA=JitCompilationStart

コードが JIT 処理される場合に、アプリケーションはデバッガに割り込みます。MDA は、レジストリを使用して、またはアプリケーションの .config ファイルを使用して設定することもできます。MDA を使用する方法については、「起動パフォーマンスのリソース」のリンクを参照してください。

一般的に、NGen で確実にコールド起動パフォーマンスを向上させるには、以下を確認する必要があります。

  • アプリケーション全体で NGen が使用される。
  • ベース変更がない。ベース変更は非常にコストの高い操作であり、ベース変更されたコードは共有できません。ベース アドレスを設定する方法の詳細については、msdn.microsoft.com/msdnmag/issues/06/05/CLRInsideOut を参照してください。
  • アセンブリがグローバル アセンブリ キャッシュ (GAC) にインストールされている。強力な名前検証ではファイル全体を参照する必要がありますが、GAC にインストールされているすべてのアセンブリに対しては強力な名前検証がスキップされます。

Authenticode 検証

アセンブリに Authenticode 署名するには signcode ツールを使用します。Authenticode 検証では、Authenticode 署名されたアセンブリを Certificate Authority (CA) によって確認する必要があるため、必ず起動に悪影響を及ぼします。この検証にはアセンブリの署名に使用された CA の確認が必要ですが、これはネットワーク アクセスを必要とする非常にコストの高い処理です (CA が同じマシンにローカルでインストールされていればネットワーク アクセスは不要)。

理想的には、アセンブリの Authenticode 署名を回避し、代わりに厳密な名前の署名を使用します。Authenticode 署名を回避できない場合は、次の構成オプションを使用することにより .NET Framework 3.5 で検証をスキップできます。

<configuration>
    <runtime>
        <generatePublisherEvidence enabled="false"/>
    </runtime>
</configuration>

ただし、Authenticode 署名が必須の場合でも、CA 証明書をクライアント マシンにインストールしておけば、認証に必要な時間の大半を節約できます。

まとめ

コールド起動でパフォーマンスを向上させる最良の方法は、起動時に実行されるコードをできるだけ小さく保つことです。そのためには、遅延が許容される初期化は後で処理されるようにし、各参照についても直ちに読み込む必要があるかどうかをチェックし、クラスおよびメソッドは大量のコードを読み込まないものを使用するようにします。ディスク アクセスを減らすことが目標であることを忘れないでください。これは簡単な作業ではありませんが、次の Windows Server® 2008 SDK で提供される Xperf という新しいツールを使えば、アプリケーション起動時に何が行われるかを確認できます。Xperf は、Event Tracing for Windows (ETW) を使用し、読み込まれたモジュール、コンテキストの切り替え、およびその他のイベントを追跡するツールです。XPerf では、アプリケーション起動時の非常に正確なメトリックスを収集できます。詳細については、「起動パフォーマンスのリソース」のリンクに多数の参照が含まれていますのでどうぞご覧ください。

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

Claudio Caldato は、マイクロソフトの CLR チームでパフォーマンスおよびガベージ コレクタのプログラム マネージャを務めています。


データ ポイント: LINQ の標準クエリ演算子

データ ポイント
LINQ の標準クエリ演算子
John Papa

コードのダウンロード : : DataPoints2008_03.exe (958KB)
コンテンツ
演算子と LINQ
演算子の種類
ラムダ式
First および Single
集計、階層、および射影
射影および順序付け
限定子および変換
まとめ

統合言語クエリ (LINQ: Language Integrated Query) により、開発者は、Microsoft® .NET Framework 3.5 コードで厳密に型指定された構文を使用して、SQL に似たコードを記述できるようになります。これにより、たとえば (オブジェクト階層に対してクエリを記述するための) LINQ to Objects や (Entity Framework の概念モデルに対してクエリを記述するための) LINQ to Entities などの異なる LINQ プロバイダで、基になるデータ ストアに従ってこれらのクエリを効果的に処理できるようになります。

厳密に型指定された構文に加え、LINQ クエリには、機能を拡張するための標準クエリ演算子という武器を備えています。これらの標準クエリ演算子は、シーケンスを操作します。標準クエリ演算子を使用すると、シーケンスに値が存在するかどうかを判定したり、シーケンスに対して集計関数 (たとえば合計) を実行したりできます。

今月のコラムでは、LINQ to Objects と LINQ to Entities の両方を使用した LINQ による実用的なクエリと操作を紹介します。ここでは、エンティティの 1 つのコレクションに対してクエリを実行し、ナビゲーション プロパティを使用してエンティティの階層セットの中を深く探索します。また、標準クエリ演算子の多くを配列やコレクションに適用する方法についても説明します。さらに、ラムダ式を使用して LINQ の標準クエリ演算子がどのように拡張されるかについて、およびシーケンスから特定の情報を解析し、シーケンスに対して複雑なロジックを実行する方法についても説明します。このコラムのコード例はすべてダウンロードできます (msdn.microsoft.com/msdnmag/code08.aspx を参照してください)。

演算子と LINQ

LINQ は、LINQ to XML、LINQ to DataSets、LINQ to Entities、LINQ to Objects、または付属するその他の LINQ プロバイダを使用しているかどうかに関係なく、そのままで非常に強力です。LINQ の中核となるのは、これらのプロバイダのすべてを操作する、厳密に型指定されたクエリ構文です。LINQ を 1 つまたは複数の標準クエリ演算子と組み合わせると、データのセットに対してより細かな制御を行うことができる強力なツールセットとなります。

標準クエリ演算子は、静的クラス Enumerable および Queryable の拡張メソッドとして、System.Linq 名前空間内の System.Core.dll アセンブリに存在し、IEnumerable<T> または IQueryable<T> を実装するオブジェクトで使用できます。その結果、LINQ to Entities や LINQ to SQL などのプロバイダを使用して、メモリ内のコレクションや配列 (シーケンス) からリモート データベースまでのさまざまな種類に対して、標準クエリ演算子を使用できます。

特定のタスクに対して使用できる演算子のセットを調べるのは簡単です。LINQ クエリ内で演算子を使用する場合は、Queryable 静的クラス内の使用可能な拡張メソッドのリストに含まれている演算子を使用できます。IEnumerable<T> を実装するシーケンスに演算子を使用する場合は、Enumerable 静的クラスのいずれかの拡張メソッドを使用できます。ただし、必ずしも Queryable クラスのすべての演算子を基になるデータ ストアに適用できるとは限らず、実行時にサポートされない演算子も存在することに注意してください。

演算子の種類

演算子にはさまざまな種類があります (オブジェクト ブラウザを使用して Enumerable クラスおよび Queryable クラスを見るとすべての型を確認できます)。図 A に、各種の演算子をアルファベット順に示します。この図で、演算子の機能を一覧することができます。以降では、LINQ to Objects と LINQ to Entities の両方を使用してこれらの演算子のサブセットのデモを示し、これらが現実のアプリケーションに対していかに有益であるかを説明します。

Figure A Categories of Operators (Close Figure A)

演算子 | 説明
集計 |
Aggregate | シーケンスに対してカスタム メソッドを実行します。
Average | 数値のシーケンスの平均を計算します。
Count | シーケンス内のアイテムの数を int 型の値として返します。
LongCount | シーケンス内のアイテムの数を long 型の値として返します。
Min | 数値のシーケンス内で最小の値を見つけます。
Max | 数値のシーケンス内で最大の値を見つけます。
Sum | シーケンス内の数値を合計します。
連結 |
Concat | 2 つのシーケンスを 1 つのシーケンスに連結します。
変換 |
Cast | シーケンス内の要素を指定された型にキャストします。
OfType | 指定された型に基づいてシーケンス内の要素をフィルタ処理します。
ToArray | シーケンスから配列を返します。
ToDictionary | シーケンスからディクショナリを返します。
ToList | シーケンスからリストを返します。
ToLookup | シーケンスからルックアップを返します。
ToSequence | IEnumerable シーケンスを返します。
要素 |
DefaultIfEmpty | 空のシーケンスの既定の要素を作成します。
ElementAt | シーケンスの指定されたインデックスにある要素を返します。
ElementAtOrDefault | シーケンスの指定されたインデックスにある要素、またはインデックスが範囲外の場合は既定値を返します。
First | シーケンスの最初の要素を返します。
FirstOrDefault | シーケンス内の最初の要素、または要素が見つからない場合は既定値を返します。
Last | シーケンスの最後の要素を返します。
LastOrDefault | シーケンス内の最後の要素、または要素が見つからない場合は既定値を返します。
Single | シーケンスの単独の要素を返します。
SingleOrDefault | シーケンスの単独の要素、または要素が見つからない場合は既定値を返します。
等価性 |
SequenceEqual | 2 つのシーケンスを比較して等価であるかどうかを調べます。
生成 |
Empty | 空のシーケンスを生成します。
Range | 指定された範囲のシーケンスを生成します。
Repeat | アイテムを指定された回数繰り返してシーケンスを生成します。
グループ化 |
GroupBy | 指定されたグループ化の条件に基づいてシーケンス内のアイテムをグループ化します。
結合 |
GroupJoin | 2 つのシーケンスに対してグループ化結合を実行します。
Join | 2 つのシーケンスに対して内部結合を実行します。
順序付け |
OrderBy | シーケンスを値の昇順に順序付けます。
OrderByDescending | シーケンスを値の降順に順序付けます。
ThenBy | 既に順序付けられているシーケンスを昇順に順序付けます。
ThenByDescending | 既に順序付けられているシーケンスを降順に順序付けます。
Reverse | シーケンス内のアイテムの順序を反転します。
パーティション分割 |
Skip | 指定された数のアイテムをスキップしてシーケンスを返します。
SkipWhile | 式を満たさないアイテムをスキップしてシーケンスを返します。
Take | 指定された数のアイテムを取得してシーケンスを返します。
TakeWhile | 式を満たすアイテムを取得してシーケンスを返します。
射影 |
Select | シーケンスのパーツの射影を作成します。
SelectMany | シーケンスのパーツの一対多の射影を作成します。
限定子 |
All | シーケンス内のすべてのアイテムが条件を満たすかどうかを判定します。
Any | シーケンス内のいずれかのアイテムが条件を満たすかどうかを判定します。
Contains | シーケンスに指定されたアイテムが含まれているかどうかを判定します。
制限 |
Where | シーケンス内のアイテムをフィルタ処理します。
セット |
Distinct | 重複するアイテムを取り除いてシーケンスを返します。
Except | 2 つのシーケンスの相違を表すシーケンスを返します。
Intersect | 2 つのシーケンスの交差を表すシーケンスを返します。
Union | 2 つのシーケンスの和を表すシーケンスを返します。

ラムダ式

多くの標準クエリ演算子は、シーケンスを操作するときに Func デリゲートを使用して個々の要素を処理します。ラムダ式は、標準クエリ演算子と組み合わせることでデリゲートを表すことができます。ラムダ式とは、デリゲートの実装を作成するための手軽なやり方で、匿名デリゲートを使用できる場所ならどこでも使用できます。ラムダ式は、C# と Visual Basic® .NET の両方でサポートされています。Visual Basic .NET では匿名メソッドがサポートされないので、単一のステートメントでラムダ式が構成される場合があることに注意してください。

では、Single 演算子を整数の配列で使用する方法を検討しましょう。ここでは、それぞれの要素が 2 の 1 乗から 2 の 10 乗を表す整数の配列を作成した後、Single 演算子を使用して、ラムダ式に指定された条件を満たす単独の整数要素を取得します。

int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int singleNum = nums.Single(x => x > 16 && x < 64);
Console.WriteLine(singleNum.ToString());

ラムダ式には、いくつかの重要な要素があります。ラムダ式は、デリゲートに渡される変数の定義から始まります。前述のコード例の (=> 演算子の左側に宣言されている) x は、渡される nums 配列の各要素を表す引数です。ラムダ式の残りの部分は、配列の各要素に対して評価されるロジックを表します。前述の式は、次のように匿名デリゲートを使用して簡単に書き直すことができます。

int singleNum = nums.Single<int>(
  delegate(int x) {return (x > 16 && x < 64); }
) ;

ただし、このコードは、ラムダ式を使用したコードよりもやや読みづらくなっています。C# 2.0 では、匿名デリゲートが導入されてデリゲートの受け渡しが簡単になっていますが、ラムダ式では、簡潔な構文によりさらに進んだ簡潔さが実現されます。

First および Single

シーケンスから単一の値を取得する必要がある場合は、First 演算子、FirstOrDefault 演算子、Single 演算子、および SingleOrDefault 演算子が便利です。First メソッドは、シーケンスの最初の要素を返します。First には、条件を表すラムダ式を渡すことができるオーバーロード メソッドがあります。たとえば、整数のシーケンスにおいて整数要素が 50 を超える最初の要素を返すには、次のコード サンプルを使用できます。

int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int num1 = nums.First<int>();
int num2 = nums.First<int>(x => x > 50);
int num3 = nums.FirstOrDefault<int>(x => x > 5000);

Console.WriteLine(
  num1.ToString() + "-" +
  num2.ToString() + "-" +
  num3.ToString());

このコードは、最初の要素 (1)、50 を超える最初の要素 (64)、および 5,000 を超える最初の要素を検索します。配列には 3 番目のラムダ式を満足させる要素がないので (配列内に 5,000 を超える整数はありません)、コードで FirstOrDefault 演算子の代わりに First 演算子を使用していた場合は例外がスローされます。FirstOrDefault 演算子を使用することで、ラムダ式を満足させる要素がない場合は 0 が返されます。LINQ to Entities クエリ内では、次のように First 演算子を使用することができます。

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               select c).First(c => c.City.Equals("London"));
  Console.WriteLine(query.CompanyName);
}

この例では、London に住所を持つ最初の顧客が返されます。ご覧のように、First メソッドを使用するための構文は、別の LINQ プロバイダ (たとえば LINQ to Objects や LINQ to Entities) を使用する場合と同じです。

First 演算子は、特にクエリによって 1 つのレコードが返されることがわかっている場合に、LINQ to Entities のコンテキストで非常に便利です。たとえば、CustomerID を与えたときに常に顧客レコードが取得されるクエリを作成できます。この場合、常に 0 個または 1 個のレコードが返されるので、シーケンスはエンティティそのものと同程度には役に立ちません。言い換えると、1 つの Customer エンティティのシーケンスの代わりに、Customer エンティティをフェッチするだけで済みます。これは、次のコード スニペットに示すように、First メソッドが有用なケースです。Entity Framework では、単一のクエリの実行がクライアントとサーバーに分配されないので、LINQ to Entities では単一のメソッドがサポートされていません。したがって、First メソッドの使用が手軽な代替方法になります。

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               where c.CustomerID.Equals("BOLID")
               select c).First();
  Console.WriteLine(query.CompanyName);
}

集計、階層、および射影

Sum などの集計演算子を LINQ to Entities クエリで使用すると、クエリを単純化できます。たとえば、次のコードは、注文の合計が $10,000 を超える注文のシーケンスを取得します。

using (Entities entities = new Entities())
{
  var query = from o in entities.Orders
              where o.OrderDetails.Sum(
                od => od.UnitPrice * od.Quantity) >= 10000
              select o;
  foreach (Orders order in query)
    Console.WriteLine(order.OrderID);
}

LINQ では階層エンティティ コレクションを照会できるので、標準クエリ演算子を使用して、入れ子になったエンティティのシーケンスを操作できます。これは、派生データの計算または問い合わせを行う必要がある場合に便利です。派生データは、顧客注文の詳細に単価と数量の値しか含まれていない場合のように、その基本形式のみで存在する可能性があります。この場合、注文の合計金額を表す集計データは、モデルのどのポイントにおいても提供されません。ただし、次のように Sum 演算子を LINQ クエリに適用することで、支払い額が $20,000 を超えるすべての顧客を取得できます。

using (Entities entities = new Entities())
{
  var query = from c in entities.Customers
              where c.Orders.Sum(
                o => o.OrderDetails.Sum(
                  od => od.UnitPrice * od.Quantity)) >= 25000
              select c;
  foreach (Customers customer in query)
    Console.WriteLine(customer.CompanyName);
}

この例は、LINQ クエリの複数のレベルで標準クエリ演算子を適用する操作を示しています。このクエリは、最終的に Customers エンティティのシーケンスを返しますが、そこに到達するには、最初にそれぞれの顧客の注文と各注文の受注明細を調べて、明細行品目ごとの価格、各注文の明細行品目合計の合計、および顧客ごとの注文合計の合計を求めるのに必要なデータを取得する必要があります。

Count 演算子は、もう 1 つの集計標準クエリ演算子です。次のコードを使用すると、支払い額が $25,000 を超える顧客を調べることができます。

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               where c.Orders.Sum(
                 o => o.OrderDetails.Sum(
                   od => od.UnitPrice * od.Quantity)) >= 25000
               select c).Count();
  Console.WriteLine(query);
}

Max 演算子を使用すると、最高の顧客を調べることができます。次のコード サンプルでは、支払い額が最も大きい顧客が返されます。ここでは、複数の階層レベルに対して Sum と Max の集計演算子の組み合わせを使用しています。

using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               select new
               {
                 c.CustomerID,
                 Total = c.Orders.Sum(
                   o => o.OrderDetails.Sum(od => od.UnitPrice))
               }).Max(c2 => c2.Total);
  Console.WriteLine(query);
}

射影および順序付け

前述の例にこっそりと仕込んでおいた射影に気が付いた読者もいることでしょう。Max 演算子を使用する前には、この LINQ クエリは顧客のリストを返しません。その代わり、CustomerID のプロパティと Total (顧客の合計支払い額) のプロパティを格納する新しいエンティティを作成する射影を返します。射影は LINQ に不可欠であり、前の例に示すように、シーケンスに射影することで、標準クエリ演算子を使用してさらに処理することができます。

図 1 は、(既に説明した Sum 演算子を使用して) CustomerID とこの顧客の注文合計の合計を格納する新しいエンティティの射影を作成する方法を示しています。また、図 1 では、OrderByDescending 演算子を使用して、射影されたエンティティのシーケンスを計算された合計で順序付けています。2 つの顧客が同じ合計を持つ場合、追加の順序付け演算子を使用して、さらに順序付けを定義できます。たとえば、図 1 の foreach ステートメントを次のコードのように修正して、追加の順序付けルールを設定できます。

Figure 1 Aggregates, Projections, and Ordering (Close Figure 1)
using (Entities entities = new Entities())
{
  var query = from c in entities.Customers
              where c.Orders.Sum(
                o => o.OrderDetails.Sum(od => od.UnitPrice)) > 0
              select new
              {
                c.CustomerID,
                Total = c.Orders.Sum(
                  o => o.OrderDetails.Sum(od => od.UnitPrice))
              };
  foreach (var item in query.OrderByDescending(x => x.Total))
    Console.WriteLine(item.CustomerID + " == " + item.Total);
}

foreach (var item in
  query.OrderByDescending(x => x.Total)
  .ThenBy(x => x.CustomerID))
{
  Console.WriteLine(item.CustomerID + " == " + item.Total);
}

このコード スニペットでは、ThenBy 演算子とラムダ式を追加して、最初にシーケンスを Total プロパティの降順に順序付けした後、射影の CustomerID プロパティの降順に順序付けするよう指定しています。

限定子および変換

シーケンスに値が存在するかどうかを調べる必要がある場合は、Any 標準クエリ演算子を使用できます。Any、All、Contains などの限定子は、要素のシーケンスを検索し、シーケンスがラムダ式の条件を満たすかどうかを評価します。これは、特定の住所の顧客が存在するかどうか、すべての顧客が同じ国に住所を持つかどうかなど、分析決定論的な質問の回答を求めるためにシーケンスを調べる必要がある場合に非常に有効です。

たとえば、次の LINQ クエリは、英国のすべての顧客がロンドン (London) に住所を持っているかどうかを調べます。このクエリでは、All 修飾子を、市町村がロンドンかどうかを単純に評価するラムダ式に渡しています。シーケンス内のすべての要素がこの条件を満たし、ラムダ式の結果として true を返すと、All 演算子は true を返します。

using (Entities entities = new Entities())
{
  bool allUKCustomerAreFromLondon = (from c in entities.Customers
                                     where c.Country == "UK"
                                     select c).All(
                                       c => c.City.Equals("London"));
  Console.WriteLine(allUKCustomerAreFromLondon ? "Yes" : "No");
}

このクエリで回答を求める必要があると思われるもう 1 つの質問は、英国の都市カウズ (Cowes) を住所とするエンティティがシーケンスに含まれているかどうかという質問です。この質問に対しては、Any 修飾子を使用して、次のようにシーケンスに対して操作を行うことができます。

using (Entities entities = new Entities())
{
  bool isOneUKCustomerFromCowes = (from c in entities.Customers
                                   where c.Country == "UK"
                                   select c).Any(
                                     c => c.City.Equals("Cowes"));
  Console.WriteLine(isOneUKCustomerFromCowes? "Yes" : "No");
}

Contains 演算子は、シーケンスに対象の値が含まれているかどうかを評価する点で Any 演算子に似ています。Any 演算子を使用すると、シーケンス内のアイテムに値が存在するかどうかを調べることができます。これに対し、Contains 演算子を使用すると、特定のアイテムのインスタンスがシーケンスに含まれているかどうかを調べることができます。たとえば、オブジェクトをシーケンスに追加する前に、当該オブジェクトが既にシーケンスに含まれていないことを確認できます。図 2 に、そのチェック方法を示します。

Figure 2 Using Contains and Conversion (Close Figure 2)
using (Entities entities = new Entities())
{
    Customers customerBSBEV = (from c in entities.Customers
                               where c.CustomerID == "BSBEV"
                               select c).First();

    var customersUK = from c in entities.Customers
                      where c.Country == "UK"
                      select c;

    bool isCustomerInSequence = customersUK.Contains(customerBSBEV);

    Console.WriteLine(isCustomerInSequence? "Yes" : "No");
}

図 2 において、Customers エンティティが BSBEV 顧客に対して取得されていることに注意してください。次に、英国に住所を持つ顧客の Customers エンティティのシーケンスが取得されます。最後に、Contains 演算子を使用して、customerBSBEV 変数に保持されているインスタンスが顧客のシーケンスに含まれているかどうかがチェックされます。

図 2 に示されている Contains 演算子の実装は、確信を持って実際のインスタンスに基づいてオブジェクトを比較できる場合に有効です。では、論理 ID に基づいて Contains 演算子をテストする必要がある場合はどうでしょうか。幸運にも、Contains 演算子では、オーバーロードを使用して、IEqualityComparer<T> インターフェイスを実装するオブジェクトを渡すことができます。CustomerID に基づいて Contains を使用するには、図 2 のコードを次のように書き直すことができます。

using (Entities entities = new Entities())
{
  ...

  bool isCustomerInSequence = customersUK.Contains(customerBSBEV,
    new CustomerComparer());

  Console.WriteLine(isCustomerInSequence? "Yes" : "No");
 }

CustomerComparer は、次のように定義します。

private class CustomerComparer : IEqualityComparer<Customers>
{
  public bool Equals(Customers x, Customers y) {
    if (x == null || y == null)
      return false;
    return x.CustomerID.Equals(y.CustomerID);
  }

  ...
}

まとめ

Enumerable および Queryable シーケンス クラスの拡張メソッドとして、多くの標準クエリ演算子が定義されています。これらの演算子を使用すると、これまでに説明したように LINQ の機能を拡張できます。ここでは、.NET Framework 3.5 の数多くの新しい拡張機能 (ラムダ式、LINQ、Entity Framework、暗黙的な型付き変数など) により、堅牢なコードとロジックを書くのが容易になった事例を紹介しました。

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

John Papa は、ASPSOFT (aspsoft.com) で働く上級 .NET コンサルタントです。野球を熱狂的に愛し、夏の夜のほとんどを家族および忠犬 Kadi (カーディ) と共にヤンキースのために費やします。C# MVP であり、INETA の講演者でもある John は、データ アクセス テクノロジに関して何冊かの書籍を執筆しています。また、主に業界のカンファレンスでの講演やブログ (www.johnpapa.net) などで活躍しています。


Advanced Basics: Office 2007 のファイルと LINQ

Advanced Basics
Office 2007 のファイルと LINQ
Ken Getz

コードのダウンロード : : AdvancedBasics2008_03.exe (162KB)
コンテンツ
作業の開始
コードの詳細解説
まとめ

この連載コラムの 2006 年 6 月号では、Microsoft® 2007 Office system Open XML ファイル形式を、System.IO.Packaging 名前空間と System.Xml 名前空間のクラスを使ってプログラムから操作する方法について説明しました (msdn.microsoft.com/msdnmag/issues/06/06/AdvancedBasics を参照)。そのコラムでも述べたように、いくつかの新しいテクノロジを使えば、私のコードをもっと単純化することが可能です。ただ、当時は、それらのテクノロジがまだ十分に利用できる段階にありませんでした。Visual Studio® 2008 がリリースされた今、LINQ to XML と Community Technology Preview (CTP) エディションの Microsoft SDK for Open XML Formats を使って、改めてそのコードにアプローチしてみることにします。ここで紹介するコードをコンパイルしたりテストしたりするには、Microsoft SDK for Open XML Formats を go.microsoft.com/fwlink/?LinkId=106944 からダウンロードする必要があります。この 2 つのテクノロジは、(Microsoft Word、Excel®、または PowerPoint® で作成された) 2007 Office system Open XML ドキュメントの特定のパーツにアクセスし、データを取得したり編集したりする方法を劇的に変える可能性があります。

2006 年 6 月のコラムでも書いたように、Office Open XML ファイル形式をプログラムから操作するには、ドキュメントの特定のパーツに移動し、その参照を取得した後、該当パーツの XML を操作する必要があります。このファイル形式のパーツ階層内を移動するには、System.IO.Packaging 名前空間を使用するのが一般的です。いったん、プログラミングの対象となるパーツに移動すれば、既存の XML テクノロジを使って操作できます。通常は、XmlDocument クラスを使用するか、XmlReader クラスと XmlWriter クラスを使用することになります。

たしかに、System.IO.Packaging 名前空間のクラスを使用すれば、このファイル形式のパーツ階層内を移動できますが、きわめて汎用的 (2007 Office system ドキュメントに限らず、オープンな XML ファイル形式であればどのようなファイルにでも使用できる) であるため、この API を使って作成したコードは、皆さんの期待に反して、どうしても冗長になってしまいます。マイクロソフトの Office チームでは、階層内の移動をより簡単に行えるように、汎用的な System.IO.Packaging 名前空間をカプセル化し、2007 Office system のファイル形式に的を絞った API を提供する SDK の開発を進めています。現時点では CTP リリースですが、Office ドキュメントを扱うコードを大幅に簡素化できるため、私はこの SDK を使用することにしました。

2007 Office system Open XML 用 SDK が扱う範囲は、操作対象となる個々のパーツへの移動のみで、ドキュメントの情報の取得や変更など、特定パーツの XML コンテンツを操作するためのものではありません。以前のコラムでは、XmlDocument クラスを使ってドキュメントのプロパティを操作しました。現在は、.NET Framework 3.5 (Visual Studio 2008 で使用可能) の新機能である LINQ to XML を使用することで、XML コンテンツをより簡単に操作できるようになっています。

このコラムでは、WDSetCustomProperty というコードに修正を加える形で話を進めます。これは、2007 Office system の発売が開始された頃に私が作成した 40 個のコード スニペットの中の 1 つです (go.microsoft.com/fwlink/?LinkId=106943)。この記事で紹介しているコードについて調べる前に、元のコード スニペットの動作を確認しておくことをお勧めします。コードのチュートリアルやデモ ビデオには、msdn2.microsoft.com/bb308936 でアクセスできます。

また、2007 Office system Open XML ファイル形式についての知識が若干必要なので、まずは、以前のコラムを振り返ってみましょう。この記事のサンプル コードを見るとわかるように、2007 Office system Open XML 用 SDK に関連したコードはきわめて簡単明瞭です (SDK を使用するには、あらかじめ SDK をダウンロードおよびインストールし、プロジェクト内で、SDK に収録されている Microsoft.Office.DocumentFormat.OpenXml.dll アセンブリを参照設定しておく必要があります)。ここで紹介する最後のテクノロジは、LINQ to XML です。このテクノロジ全体に関しては話題が広範囲に及ぶため、ここではこのコラムに関連した用法に的を絞って説明します。これは注目されているテクノロジであり、解説も豊富に存在しますが、第一歩として、LINQ to XML 入門 (msdn2.microsoft.com/bb466226) のビデオ プレゼンテーションをご覧になるとよいかもしれません。

このコラムのコード サンプルを作成するにあたっては、作業時間の 90 パーセントを XML 名前空間の操作に費やしました。最新バージョンの Visual Basic® は、LINQ to XML の機能を有効活用できるように十分に配慮されていますが、名前空間の扱いにとまどう可能性があります。この機能については、msdn2.microsoft.com/bb687701 でわかりやすく解説されています。私も随分助けられました。ぜひ、ご覧ください。

作業の開始

Word 2007 のカスタム ドキュメント プロパティを設定したり取得したりするには、docProps/custom.xml パーツを操作する必要があります。プロパティの設定時に考慮すべき状況としては、次の 4 とおりが考えられます。

  • カスタム プロパティ パーツがまだ存在しない。この場合は、カスタム プロパティ パーツを作成する必要があります。
  • カスタム プロパティが存在しない。この場合は、カスタム プロパティを作成する必要があります。
  • カスタム プロパティが存在し、その型が新しい値の型と一致している。この場合は、単にプロパティの値を設定できます。
  • カスタム プロパティは存在するが、新しい値とは型が異なる。この場合は、プロパティを削除し、新しい型を使って作成し直します。どのような方法で型を設定し直してもかまいませんが、紹介しているコードでは、プロパティが存在しなかった場合も、存在するが型が異なっている場合も、同じ処理を適用しています。どちらの場合も、プロパティの新しい要素が作成されます。

一連のカスタム プロパティのサンプルを図 1 に示しました。このドキュメントの場合は、4 つのカスタム プロパティが存在します。Author (Ken)、Birthdate (1956/1/1)、Chapter (2)、Revisions (5) です。XML コンテンツのスコープは、既定の名前空間に設定されていることに注意してください。

Figure 1 Some Custom Properties (Close Figure 1)
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Properties
  xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006
    /docPropsVTypes"
  xmlns="http://schemas.openxmlformats.org/officeDocument/2006
    /custom-properties">
  <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
    pid="2" name="Author">
    <vt:lpwstr>Ken</vt:lpwstr>
  </property>
  <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
    pid="3" name="Birthdate">
    <vt:filetime>1956-01-01T06:00:00Z</vt:filetime>
  </property>
  <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
    pid="4" name="Chapter">
    <vt:i4>2</vt:i4>
  </property>
  <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
    pid="5" name="Revisions">
    <vt:i4>5</vt:i4>
  </property>
</Properties>

http://schemas.openxmlformats.org/officeDocument/2006/custom-properties

このコンテンツには、プロパティの型を表すために、既定以外の名前空間も使用されています。

vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"

このパーツを操作するコードを作成する場合は、必ず、これらの名前空間を記述する必要があります。そのようにしなかった場合、Word 2007 がドキュメントを読み込んでくれません (私は、試行錯誤の末、やっとこの事実に気付くことができました)。

いずれのプロパティ要素にも、同じ GUID 値 ({D5CDD505-2E9C-101B-9397-08002B2CF9AE}) を持つ fmtid 属性が存在します。また、各プロパティには、2 から始まる pid 属性があり、プロパティごとに値が増やされます。この属性は必ず数値になります。Word 2007 では、必ずしも値が連続している必要はなく、プロパティの pid 値が 2、3、5 のようになっていてもかまいません。ただし、値を重複させることはできません。また、必ず、数値の序列に従って出現する必要があります。最後に、各プロパティには、その名前を格納する name 属性があります。

それぞれのプロパティ要素には、そのプロパティの値を格納する要素を追加する必要があります。この要素の名前は、各プロパティが保持する値の型を表します。サンプル コードでは、これらの型を表す列挙型を定義しました。

Public Enum PropertyTypes
  YesNo
  Text
  DateTime
  NumberInteger
  NumberDouble
End Enum

XML コンテンツでは、これらの型が、それぞれ bool、lpwstr、filetime、i4、r8 に対応しています。それぞれの型の名前は、vt というエイリアスに割り当てられている XML 名前空間に属しています。

XML の名前空間を扱いやすくするために、Visual Basic では、ファイル レベルの Imports ステートメントの動作がオーバーライドされます。ファイルの先頭に名前空間の省略名を指定すれば、そのファイル内で、これらの省略名を使用できるようになります。サンプル コードには、次の Imports ステートメントを使用しています。

Imports System.IO
Imports Packaging = Microsoft.Office.DocumentFormat.OpenXml.Packaging
Imports <xmlns="http://schemas.openxmlformats.org/officeDocument/2006
/custom-properties">
Imports <xmlns:vt="http://schemas.openxmlformats.org/officeDocument
/2006/docPropsVTypes">

Packaging というエイリアスを指定しました。これで、2007 Office system Open XML 用 SDK を使ったコードをすっきりと記述できます。サンプル コードには、他にも、既定の名前空間とエイリアス vt の名前空間に対する Imports ステートメントが指定されています。このように名前空間をインポートできることで、コーディングがしやすくなるというメリットはありますが、1 つだけ制限があります。既定の名前空間はファイルごとに 1 つしか定義できません。別途、既定の名前空間を使った XML コードを含める必要がある場合は、そのコードを別のファイルに記述する必要があります。もちろん、部分クラスを使用することによって、1 つのクラスを複数のファイルに分けることもできるので、プロジェクトのファイル管理という点を除けば、必ずしも制限とは言えません。

ドキュメントのプロパティを設定するには、次のような方法で呼び出すことのできるメソッドを作成する必要があります。

WDSetCustomProperty("C:\YourDocument.docx", "PropertyName", _
  "PropertyValue", PropertyTypes.Text)

指定したプロパティ値が、指定した PropertyTypes の列挙値と一致しなかった場合、コードから例外を発生させて、何も処理を実行せずに制御が戻るようにする必要があります。

サンプル コードをひととおり眺めてみると、Visual Studio 2008 で導入された新機能を垣間見ることができます。その中でも、暗黙的な型宣言を使ったコードに注目してください。変数ごとに型を指定しなくても、Visual Basic の型の推定機能によって、コンパイル時に型が推定されます。したがって、次のようにコードを記述することもできます。

Using wdPackage = _
  Packaging.WordprocessingDocument.Open(docName, True)
  ...
End Using

明示的に宣言した場合は、次のようになります。

Using wdPackage As Packaging.WordprocessingDocument = _
  Packaging.WordprocessingDocument.Open(docName, True)
  ' Code removed here.
End Using

型を厳密に指定しなかったからといって、コードの読みやすさが損なわれることはありません。また、どちらの方法で記述しても同じようにコンパイルされます。この機能は、ファイル レベルのオプションである Option Infer によって制御されます。既定では、オンに設定されています。

コードの詳細解説

それでは、サンプル コードをブロックごとに詳しく解説していきます。WDSetCustomProperty プロシージャのコード全体は、MSDN® Magazine Web サイトからダウンロードできます。プロシージャの全体の流れを把握するための資料としてご利用ください。まず、図 2 のように、パラメータを検証して、入力値を必要に応じて変換します。

Figure 2 Checking the Types and Performing Conversions (Close Figure 2)
Dim propertyTypeName As String = Nothing
Dim propertyValueString As String = Nothing

' Calculate the correct type:
Select Case propertyType
  Case PropertyTypes.DateTime
    propertyTypeName = "filetime"
    ' Make sure you were passed a real date,
    ' and if so, format in the correct way.
    ' The date/time value passed in should
    ' represent a UTC date/time.
    If TypeOf (propertyValue) Is DateTime Then
      propertyValueString = String.Format("{0:s}Z", _
        Convert.ToDateTime(propertyValue))
    End If
  Case PropertyTypes.NumberInteger
    propertyTypeName = "i4"
    If TypeOf (propertyValue) Is Integer Then
      propertyValueString = propertyValue.ToString()
    End If

  Case PropertyTypes.NumberDouble
    propertyTypeName = "r8"
    If TypeOf propertyValue Is Double Then
      propertyValueString = propertyValue.ToString()
    End If

  Case PropertyTypes.Text
    propertyTypeName = "lpwstr"
    propertyValueString = propertyValue.ToString()

  Case PropertyTypes.YesNo
    propertyTypeName = "bool"
    If TypeOf propertyValue Is Boolean Then
      ' Must be lower case!
      propertyValueString = _
        Convert.ToBoolean(propertyValue).ToString().ToLower()
    End If
End Select

If propertyValueString Is Nothing Then
  ' If the code wasn't able to convert the
  ' property to a valid value, throw an exception:
  Throw New InvalidDataException("Invalid parameter value.")
End If

このコードでは、指定された列挙値 (値の型を表す列挙定数) に基づいて、指定された Object 値を正しい型に変換します。Word 2007 に値を受け入れてもらうためには、日時の値は協定世界時 (UTC) (つまり、グリニッジ標準時ベース) に、Boolean 値は小文字にする必要があります。入力値を検証した結果、propertyValueString 変数が Nothing であった場合は、呼び出し元に例外を戻し、プロシージャを終了します。

次に、Microsoft SDK for Open XML Formats を使用して、パッケージ内のドキュメント パーツとカスタム プロパティ パーツを検索します。次のコードを元のスニペットのコードと比較してみてください。SDK を使ったコードの方が、ずっと簡単にドキュメント パッケージ内を移動できることがわかるはずです。

Using wdPackage = _
  Packaging.WordprocessingDocument.Open(docName, True)

  Dim documentPart = wdPackage.MainDocumentPart
  Dim customPropsPart = wdPackage.CustomFilePropertiesPart
  ...
End Using

カスタム プロパティ パーツへの参照を取得できた場合 (つまり、customPropsPart 変数が Nothing ではなかった場合)、XDocument.Load メソッドを使って、そのパーツのコンテンツを読み込みます (図 3 を参照)。XML コンテンツは、パーツの GetStream メソッドを使って取得できます。このコードでは、パーツが存在しなかった場合、SDK を使用して、カスタム プロパティ パーツを作成します。パーツが存在した場合も、name 属性が、指定されたプロパティ名と一致する最初の property ノードを取得しようと試みます。このコードも、元のコード スニペットと比較してみてください。以前と比べていかに簡単にコードを記述できるようになったかを実感できると思います。

Figure 3 Find the Custom Properties Part and Set Properties (Close Figure 3)
Dim customDoc As XDocument = Nothing
Dim propertyNode As XElement = Nothing
If customPropsPart IsNot Nothing Then
  ' Load the existing custom properties part:
  customDoc = XDocument.Load( _
    New StreamReader(customPropsPart.GetStream()))

  propertyNode = _
    (From node In customDoc.<Properties>.<property> _
     Where node.@name = propertyName).FirstOrDefault()
Else
  customPropsPart = wdPackage.AddCustomFilePropertiesPart
End If

このコード ステートメントには、XML の操作を単純化するために、LINQ to XML と Visual Basic の軸プロパティが活用されています。

propertyNode = _
  (From node In customDoc.<Properties>.<property> _
   Where node.@name = propertyName).FirstOrDefault()

区切り記号の "." (ピリオド) は、親 (customDoc) の 1 レベル下のノードを表します。<> 構文は要素名を表します。@ 構文は属性名を表します。これらは、Visual Basic 固有の機能です。同じ情報を C# で取得する場合、XDocument オブジェクトの Element プロパティまたは Attribute プロパティを使用する必要があります。このコードには、LINQ 拡張メソッドである FirstOrDefault が使用されています。このメソッドは、最初に一致したノードを返します。一致が見つからなかった場合は、既定値 (Nothing) が返されます。拡張メソッドの First または Single を使用することもできますが、どちらのメソッドも、一致が見つからなかった場合は例外が発生します。このコードでは、一致が見つからないケースを、例外を生成することなく処理しています。

このコードが実行された後、customPropsPart には、カスタム プロパティ パーツの参照または Nothing が格納されます。同様に、propertyNode には、変更対象のプロパティに対応する XElement オブジェクトの参照が格納され、プロパティが存在しなかった場合は Nothing が格納されます。

propertyNode 変数が Nothing でない場合、このコードは、First メソッドを使ってノードの最初の子要素を取得します。First メソッドでは、最初の子要素が見つからなかった場合に例外が発生します。プロパティ ノードに必ず要素が存在する必要があります。要素が存在しなければ、ドキュメントが破損しているということなので、処理を続行できません。子要素の名前が新しいプロパティ値の型と一致した場合、要素の値が変更されます。一致しなかった場合は、ノードが削除され、propertyNode 変数が Nothing に設定されます。このコードが実行された後、propertyNode 変数は、新しい値を持つか、Nothing に設定されます (図 4 を参照)。

Figure 4 Setting the PropertyNode Variable Value (Close Figure 4)
If propertyNode IsNot Nothing Then
  Dim propertyContentNode = _
    propertyNode.Elements.First

  If propertyContentNode.Name.LocalName = _
    propertyTypeName Then

    ' Type is the same, so just change the value.
    propertyContentNode.Value = propertyValue.ToString()
  Else
    ' Type is different, so delete the node.
    propertyNode.Remove()
    propertyNode = Nothing
  End If
End If

propertyNode 変数が Nothing の場合は、ノードを作成し、そのコンテンツを設定する必要があります。図 5 のコードでは、最初に、fmtid 定数を作成し、使用する pid 値を計算しています。新しい要素に使用する pid を計算するために、Properties 要素の参照が取得され、要素の最後の子が検索されます。最後の子が存在した場合、その子の pid 値を取得して、値をインクリメントし、新しい値を使用します。図 5 で使用されている各種の LINQ to XML 機能に注目してください。たとえば、特定の条件を満たした子要素へのアクセスが、First メソッドと LastOrDefault メソッドによって単純化されていることがわかります。

Figure 5 Creating the Node (Close Figure 5)
If propertyNode Is Nothing Then
  Const fmtid As String = _
    "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"

  Dim pid As String = "2"

  Dim propertiesNode As XElement = Nothing
  If customDoc IsNot Nothing Then
    propertiesNode = customDoc.<Properties>.First

    Dim lastChild = propertiesNode.Elements.LastOrDefault
    If lastChild IsNot Nothing Then
      pid = (Integer.Parse(lastChild.@pid) + 1).ToString()
    End If
  End If
  ...
End If

次に、このコードでは、Visual Basic 固有の機能をいくつか使用して、新しいプロパティ ノードを作成します。

Dim propertyTypeXName As XName = _
  GetXmlNamespace(vt) + propertyTypeName

propertyNode = _
  <property fmtid=<%= fmtid %>
    pid=<%= pid %> name=<%= propertyName %>>
    <<%= propertyTypeXName %>><%= propertyValueString %></>
  </property>

このコードでは、プロパティ ノードの値要素を作成するために、完全な型名 (vt:i4 など) を表す XName オブジェクトを使用します。XName オブジェクトを作成するには、新しい GetXmlNamespace キーワードを使用します。GetXmlNamespace に、Imports ステートメントで定義した名前空間の省略名を指定することにより、対応する XML 名前空間を取得できます。XML 名前空間と + 演算子を組み合わせて使用した場合、Visual Basic によって + 演算子がオーバーロードされます。したがって、次のようなステートメントを記述することによって、完全な XName インスタンスを作成できます。

Dim propertyTypeXName As XName = _
  GetXmlNamespace(vt) + propertyTypeName

このコードには、Visual Basic の最新の機能の 1 つである、XML リテラル内置換トークンが使用されています。Visual Basic では、XML コンテンツを直接コードに埋め込むことができるため、ドキュメント オブジェクト モデル (DOM) オブジェクトを直接プログラミングすることなく、XML を参照したり作成したりできます。ここでは、新しい XML 要素を作成しています。

propertyNode = _
  <property fmtid=<%= fmtid %>
    pid=<%= pid %> name=<%= propertyName %>>
    <<%= propertyTypeXName %>><%= propertyValueString %></>
  </property>

このステートメントを実行すると、次のような XML を表す XElement オブジェクトが propertyNode 変数に格納されます。

<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
  pid="4" name="Chapter">
  <vt:i4>2</vt:i4>
</property>

置き換えのようすは、2 つのコードを比較すると一目瞭然です。Visual Basic は、置換トークン (<%= と %> で囲まれた部分) を見つけるたびに、指定されたトークンを対応する値に置き換えます。後で説明しますが、このトークンには、式を指定することもできます。要素名の置換トークンは、XName の参照で指定する必要があります。文字列など、他の値を指定することはできません (ご教示いただいた Visual Basic チームの Avner Aharoni 氏に感謝します)。

カスタム XML パーツをドキュメントに戻す前の最後の処理として、パーツのコンテンツ全体を再構築する必要があります。コードで、コンテナの XML ドキュメントを作成し、既存のカスタム プロパティをすべて挿入した後、新しいカスタム プロパティを追加します。

既存のカスタム プロパティ パーツを基にして、単純に Properties 要素の Add メソッドを呼び出すことによって、新しいプロパティの子要素を追加することも可能です。実際、元のコードではそのようにしていました。ただし、そうすると、名前空間の処理に関連した Visual Basic の重要な機能を紹介できません。また、Add メソッドを使用した場合、名前空間の構成要素をどのように扱えばよいかを Visual Basic が判別できず、冗長な名前空間情報が挿入されてしまいます。その場合、Visual Basic によって、明示的に既定の名前空間参照が挿入されます。作成される XML は完全に妥当な XML ですが、Word の XML パーサーがカスタム プロパティ パーツを読み込もうとしたときにエラーが発生します。Add メソッドを使用する場合は、冗長な名前空間参照を明示的に削除する必要があります (この点についても、ご助言いただいた Aharoni 氏に感謝します)。

この問題を回避するため、サンプル コードでは、カスタム プロパティ パーツを一から作成し直すことにしました。この技法を用いることで、冗長な名前空間参照を排除し、Word の要件を完全に満たした XML を作成できます。

customDoc = _
  <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  <Properties
    xmlns:vt=
    "http://schemas.openxmlformats.org/officeDocument
      /2006/docPropsVTypes">
    <%= If(propertiesNode IsNot Nothing, _
      propertiesNode.Elements, Nothing) %>
    <%= propertyNode %>
  </Properties>

このコードでは、既定の名前空間を参照する必要はありません。既定の名前空間に関する情報は、Imports ステートメントで指定済みなので、この情報は自動的に推定されます。先ほどの vt 名前空間のように、特定の名前空間が必要になった場合は、その情報を XML リテラルに含める必要があります。

この XML コンテンツには、既存のすべてのプロパティ要素に対する置換トークンが含まれています。また、Visual Basic の新しい If 関数が使用されています。この If 関数には、従来の IIf 関数に見られたような副作用はありません。つまり、両方の式を評価してから、どちらを返すかを決めるのではなく、パラメータを左から右に評価します。これにより、IIf 関数であれば発生していたようなエラーを回避できます (IIf 関数の場合、propertiesNode が Nothing だったとしても、Elements プロパティを取得しようと試みるため、そこで例外が発生します)。既存のプロパティ要素 (存在した場合) が出力されると、今度は、新しいプロパティ ノードが出力されます。結果的に、カスタム プロパティ パーツの完全な XML コンテンツを含んだ customDoc が作成されることになります。

最後にもう 1 つだけ必要な処理が残っています。パーツをファイル ストレージに戻して保存することです。XDocument クラスおよび SDK には、そのためのソリューションが用意されています。XDocument オブジェクト (customDoc) の Save メソッドを、XML コンテンツの書き込み先となるストリームを指定して呼び出します。幸い、CustomFilePropertiesPart クラスには、パッケージ内のパーツのストレージに対応するストリームを返す GetStream メソッドが存在します。

customDoc.Save(New StreamWriter( _
  customPropsPart.GetStream( _
  FileMode.Create, FileAccess.Write)))

まとめ

一見、簡単なことをするのに大量のコードが必要であるかのように見えますが、コメントを削除し、パラメータの形式を整えるためのコードを除外して考えてみると、ドキュメントを操作するために最低限必要なコードは比較的短く収まっています。元のスニペットと見比べてみてください。2007 Office system 用の Microsoft SDK for Open XML Formats と Visual Basic の LINQ to XML を使用することによって、2007 Office system Open XML ドキュメントの処理が、劇的に省力化されていることがおわかりいただけると思います。

Web で "LINQ to XML" を検索すると、LINQ を使った XML 処理について易しく解説したドキュメントが多数見つかります。たとえば、Visual Basic だけを対象にしたものではありませんが、皆さんが知っておく必要のあることの多くは、「XML データの .NET 統合言語クエリ」(msdn2.microsoft.com/bb308960) で取り上げられています。また、Visual Basic チームのブログ (blogs.msdn.com/vbteam) も参考になります。できれば、40 個あった元のコード スニペットをすべて、新しいテクノロジを使って手直ししたいところですが、残念ながら、時間の関係でそれはできません。もし、皆さんが 2007 Office system Open XML ファイル形式を扱うためのコードを過去に作成したことがあるのであれば、ここで知ったことを応用して、ぜひ、手直ししてみてください。複雑なコードを単純化し、短く収めることができるはずです。

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

Ken Getz は、MCW Technologies のシニア コンサルタントであり、AppDev (www.appdev.com) のコースウェアの作成者でもあります。Ken は、『ASP.NET Developers Jumpstart』、『Access Developer's Handbook』、および『VBA Developer's Handbook, 2nd Edition』の共著者です。連絡先は keng@mcwtech.com (英語) です。


Office Space: STSDEV を使用して SharePoint の開発を単純化する

Office Space
STSDEV を使用して SharePoint の開発を単純化する
Ted Pattison

コードのダウンロード : : OfficeSpace2008_03.exe (762KB)
コンテンツ
RootFiles ディレクトリ
ソリューション パッケージのビルドを自動化する
STSDEV の使用を開始する
MSBuild を使用してカスタム ビルド ターゲットを作成する
STSDEV ユーティリティを拡張する

Windows® SharePoint® Services (WSS) 3.0 および Microsoft® Office SharePoint Server (MOSS) 2007 の開発に筆者が携わるようになったのは、2005 年の夏からでした。それ以来、数百にも及ぶ新しいクラス ライブラリ プロジェクトを作成し、それらを手動で構成しながら、機能、アプリケーション ページ、ページ テンプレート、Web パーツなど各種の SharePoint コンポーネントを開発してきました。

SharePoint 開発用に Visual Studio® 2005 で作成したソリューションに含まれる、ファイルやフォルダの共通プロジェクト構造を標準化する作業に着手したのは、少し後になってからです。ただし、この作業には厄介な側面がありました。プロジェクトごとに、手始めとなる基本的な構造を作成するだけで約 5 分～ 10 分の時間が常にかかったのです。その一方で、異なるプロジェクトのソース ファイル間の違いは、プロジェクト名以外にはほとんどありませんでした。

SharePoint の開発で多くの開発者が直面するもう 1 つの苦労は、テストや展開に必須のソリューション パッケージ ファイル (.wsp) を生成するために必要なファイルの作成に関するものです。他の多くの SharePoint 開発者と同様に、筆者もソリューション パッケージの生成のために manifest.xml ファイルおよび .ddf ファイルを手動で作成、更新するのが習慣になっていました。

この 1 年間、筆者は John Holliday、Andrew Connell、Scot Hillier など何人かの SharePoint MVP と共同作業をしてきましたが、彼らはみな同じ目的のために、ほとんどよく似た方法をそれぞれに考え出していました。そこで彼らと、新しい Visual Studio プロジェクトおよびソリューションに必要なすべてのソース ファイルを自動的に生成できるコード ジェネレータのデザインおよび実装のアイデアについて話し合いました。その目的はもちろん、新しい SharePoint 開発プロジェクトをこれまでよりずっと速く立ち上げる手法を編み出すことです。

それぞれ共通のフォルダ構造を持つ新しい Visual Studio ソリューションを構築できるコード ジェネレータを作成すれば、ソリューション パッケージの .wsp ファイルの生成に必要な manifest.xml ファイルおよび .ddf ファイルを再生成するコードも記述できるようになります。それにより、manifest.xml ファイルや .ddf ファイルを手作業で修正する必要がなくなります。

今月のコラムには、SharePoint 開発用の単純なコード ジェネレータの作成方法を示す、STSDEV.EXE という名前の概念証明アプリケーションが付属しています。STSDEV は、MOSS 2007 のコンポーネント開発を容易にするコンソール アプリケーションです。このアプリケーションはコードを反復可能な方法で生成するため、ベスト プラクティスに従った SharePoint 開発用 Visual Studio ソリューションの構築も可能になります。

今月のコラムの目的は、SharePoint 開発用の効率的で柔軟な環境を実現する Visual Studio プロジェクトを作成、拡張するための、STSDEV ユーティリティの使用方法を紹介することです。ここで紹介するアプローチは、Visual Studio 2005 と Visual Studio 2008 のどちらでも使用できるため、柔軟な対応が可能です。また、Visual Studio の 1 つのバージョンから次のバージョンへ移行する際の手間も軽減されます。

RootFiles ディレクトリ

SharePoint の開発で展開がどのように行われるかを理解するためには、テンプレート ファイルをフロントエンド Web サーバー上のどこにコピーする必要があるのかを知っておくことが重要です。大部分のファイルは、各フロントエンド Web サーバー上の SharePoint RootFiles ディレクトリと呼ばれる場所に展開される必要があります。このディレクトリは、WSS 3.0 では次の場所にあります。

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12

図 1 に、RootFiles ディレクトリ内に入れ子になっている子ディレクトリを示します。カスタム テンプレート ファイルを展開するディレクトリとして最も一般的なのは、TEMPLATE ディレクトリです。ただし、ファイルによっては、他のいくつかの子ディレクトリに展開する必要があります。たとえば、カスタム Web サービスを展開する場合は ISAPI ディレクトリ、グローバル リソースを含む .resx ファイルを展開する必要がある場合は Resources ディレクトリに展開します。

Figure 1 RootFiles 内の フォルダ
Figure 1 RootFiles 内の フォルダ

TEMPLATE ディレクトリには、いくつかの重要な子ディレクトリも含まれています。たとえば、各カスタム機能に対しては、機能マニフェスト feature.xml を含む固有の名前付きディレクトリが FEATURES ディレクトリ内に必要となります。アプリケーション ページは、LAYOUTS ディレクトリ内の子ディレクトリにコピーする必要があります。

ソリューション パッケージのビルドを自動化する

MOSS 2007 の開発では、ソリューション パッケージを使用してすべてのカスタム テンプレートおよびコンポーネントを展開するのがベスト プラクティスです。実際、ソリューション パッケージ経由では展開できない種類のコンポーネントがある場合には、SharePoint ベースのソリューションをデザインおよび展開する際に、そのようなコンポーネントの使用を避けることを検討します。なぜなら、そのようなコンポーネントを使用すると、開発の成果を実稼動環境またはステージング環境でインストール、保守、およびアップグレードする際に、機能が制限されるからです。

WSS 3.0 または MOSS 2007 の開発時には、ソリューション パッケージを生成するように Visual Studio プロジェクトを構成する必要があり、それはどのようなプロジェクトでも開発サイクルの早い段階で行う必要があります。筆者はこれまで、開発ライフサイクルの遅い段階になるまでテスト用のソリューション パッケージを作成しない開発者を多く見てきましたが、それは問題の発生や予期しない遅延につながる可能性があります。したがって、追加の作業が発生することにはなりますが、ソリューション パッケージを構築するように Visual Studio プロジェクトを構成して、すべてのテストをソリューション パッケージ展開を使用して実行する方がずっと適切です。

それでは、ソリューション パッケージを表す .wsp ファイルのビルドを自動化するよう Visual Studio プロジェクトを構成するにはどのようにすればよいでしょうか。方法はいくつかあります。たとえば、MAKECAB.EXE ユーティリティを呼び出して .wsp ファイルを生成するバッチ ファイルを記述し、それを Visual Studio のビルド イベントから呼び出すことができます。この方法は、Visual Studio ビルド プロセスからのコマンド実行をすばやく簡単に自動化する手段となりますが、カスタム ビルド ターゲットを使用した方が、より柔軟で保守性も高くなります。

STSDEV ユーティリティは、Visual Studio 開発環境内から多くの SharePoint 固有コマンド シーケンスを直接実行できるような Visual Studio ソリューションの作成用にデザインされています。STSDEV ユーティリティで新しいプロジェクトを作成するときには、いくつかの異なる構成でプロジェクトが作成され、各構成に MSBuild カスタム ビルド ターゲットが追加されます。それにより、単純な構成切り替え手順の後で、標準の Visual Studio ビルド コマンドを実行して特定のビルド ターゲットを実行することができます。このターゲットには、SharePoint 開発で使用される一般的なコマンドのシーケンスが含まれています。たとえば、MAKECAB.EXE ユーティリティを使用してソリューション パッケージをビルドし、STSADM.EXE ユーティリティを使用してソリューション パッケージをローカル ファームにインストールして展開する、などです。

STSDEV の使用を開始する

それでは、STSDEV ユーティリティがどのように動作するのかを簡単に見ていきましょう。最初は、何もパラメータを渡さずに STSDEV を実行します。ダイアログが表示され、生成する SharePoint 開発ソリューションの種類を選択するよう求められます (図 2 を参照)。

Figure 2 STSDEV で Visual Studio ソリューションを作成する
Figure 2 STSDEV で Visual Studio ソリューションを作成する(画像を縮小するには、ここをクリックします)
Figure 2 STSDEV で Visual Studio ソリューションを作成する
Figure 2 STSDEV で Visual Studio ソリューションを作成する (画像を拡大するには、ここをクリックします)

STSDEV ユーティリティは、拡張性を考慮してデザインされています。基本的に、使用可能なソリューションの種類はそれぞれ、ISolutionProvider という非常に単純な STSDEV 固有のインターフェイスを実装する、ソリューション プロバイダ クラスです。

   public interface ISolutionProvider {
     string Title { get; }
     void InitializeSolution();
     void AddSolutionItems();
   }

このデザインの目的は、経験を積んだ SharePoint 開発者が独自のソリューション プロバイダを作成して STSDEV を容易に拡張できるようにすることであり、それを使用して特定のシナリオに合わせた Visual Studio プロジェクトを生成したり、特定の企業の標準やガイドラインに従ったりすることができます。

ただし、STSDEV を使用して作成されたソリューションおよびプロジェクトはすべて、出力ソリューション パッケージを生成するための共通の構造や標準的な展開ファイルのセットを常に共有します。また、STSDEV ユーティリティは、構成およびカスタム ビルド ターゲットの標準的なセットを使用して各 Visual Studio ソリューションを作成するので、開発者は、開発中に必要な操作の多く (現在のプロジェクトのソリューション パッケージのインストール、展開、取り消しなど) を自動化することができます。

より良く理解するために、空のソリューション プロバイダを使って生成された HelloWorld ソリューションの構造を調べてみましょう。図 3 からわかるように、STSDEV は、HelloWorld.sln という名前のソリューション ファイルと、HelloWorld.csproj という名前のプロジェクト ファイルを生成します。これらは、Visual Studio 2005 ソリューション形式または Visual Studio 2008 ソリューション形式を使用して作成できます。また、STSDEV ユーティリティは標準のクラス ライブラリ プロジェクトの種類を使用して Visual Studio プロジェクトを作成するため、Visual Studio 2005 プロジェクトから Visual Studio 2008 へのアップグレードが簡単であることも覚えておいてください。

Figure 3 HelloWorld プロジェクトの 構造
Figure 3 HelloWorld プロジェクトの 構造

STSDEV ユーティリティは、標準のプロジェクト構造を使用して、DeploymentFiles という名前のフォルダと、RootFiles という名前のフォルダを作成します。DeploymentFiles フォルダには、出力ソリューション パッケージのビルド、展開、およびデバッグに使用される 4 つのファイルが含まれています。

Manifest.xml: SharePoint で必要とされるソリューション パッケージ マニフェストであり、出力ソリューション パッケージ ファイル (たとえば、HelloWorld.wsp) に組み込む必要があります。

SolutionPackage.ddf: ソリューション パッケージ ファイルをビルドするために MAKECAB.EXE ユーティリティへの入力として使用される、ダイヤモンド定義ファイルです。

SolutionConfig.xml: manifest.xml および SolutionPackage.ddf を再生成するために必要な追加のソリューション メタデータの追跡用に STSDEV で使用されるファイルです。このメタデータには、ソリューション ID の GUID や出力ソリューション パッケージ名が含まれ、さらにオプションとして、SafeControl エントリやカスタムのコード アクセス セキュリティ (CAS) 設定など、追加のアセンブリ展開情報が含まれています。

Microsoft.SharePoint.targets: SharePoint 開発で使用される MSBuild 形式のカスタム ビルド ターゲットを含んでいるファイルです。

DeploymentFiles フォルダには常にこの 4 つのファイルが含まれていますが、RootFiles フォルダの内容は、選択したソリューションの種類によって異なります。空のソリューションを作成したときの初期状態では、RootFiles フォルダにはファイルやフォルダは何も含まれていません。ただし、RootFiles フォルダは、各フロントエンド Web サーバーの RootFile ディレクトリ内に展開する必要のある各テンプレート ファイルを追加する場所となります。

RootFiles フォルダに新しいファイルを追加するたびに、manifest.xml ファイルおよび SolutionPackage.ddf の更新が必要となります。ただし、STSDEV ユーティリティは、プロジェクトの再ビルドのたびにこれらのファイルを自動的に更新します。これは、RefreshDeploymentFiles コマンドによって行われます。このコマンドは、RootFiles ディレクトリ内に見つかったフォルダおよびファイルを列挙することで、manifest.xml および SolutionPackage.ddf ファイルを再生成します。

それでは例を見てみましょう。空のソリューションを作成すると、manifest.xml ファイルは次のようになっています。

<Solution
  SolutionId="24F91DED-8BA7-4633-8BA0-4C9B2A4387D7"
  ResetWebServer="True"
  xmlns="http://schemas.microsoft.com/sharepoint/" >

</Solution>

同様に、SolutionPackage.ddf ファイルは次のようになっています。

; Generated by STSDEV at 1/7/2008 8:42:21 AM

.OPTION EXPLICIT
.Set CabinetNameTemplate=HelloWorld.wsp
.set DiskDirectoryTemplate=CDROM
.Set CompressionType=MSZIP
.Set UniqueFiles=off
.Set Cabinet=on
.Set DiskDirectory1=DeploymentFiles

DeploymentFiles\manifest.xml

では、RootFiles フォルダ内にいくつかの新しいフォルダを追加し、AfricanPith32.gif のようなイメージ ファイルをコピーするとどうなるでしょうか。次に Visual Studio のビルド コマンドを実行したときには、STSDEV ユーティリティを呼び出して RefreshDeploymentFiles コマンドを実行するための、カスタム ビルド ターゲットが存在しています。manifest.xml は、次のように更新されています。

<Solution
  SolutionId="24F91DED-8BA7-4633-8BA0-4C9B2A4387D7"
  ResetWebServer="True"
  xmlns="http://schemas.microsoft.com/sharepoint/" >
  <!--TEMPLATE files-->
  <TemplateFiles>
    <TemplateFile
      Location="IMAGES\HelloWorld\AfricanPith32.gif" />
  </TemplateFiles>
</Solution>

そして、SolutionPackage.ddf ファイルは次のような内容に更新されています。

...
;*** Solution manifest
DeploymentFiles\manifest.xml

.Set DestinationDir=IMAGES\HelloWorld
RootFiles\TEMPLATE\IMAGES\HelloWorld\AfricanPith32.gif

RefreshDeploymentFiles コマンドを使用すれば、manifest.xml および SolutionPackage.ddf にどのファイルを追加すればよいかがわかりますが、ソリューション自体に関して、およびソリューション パッケージ内に展開する必要のあるアセンブリに関しては、追加のメタデータを追跡する必要があります。したがって、STSDEV は、SolutionConfig.xml という名前の XML メタデータ ファイルを生成し、manifest.xml および SolutionPackage.ddf の再生成時にこのファイルの内容を読み取るようデザインされています。

最も基本的な SolutionConfig.xml ファイルの内容は、次のようになっています。

<Solution>  <SolutionId>24F91DED-8BA7-4633-8BA0-4C9B2A4387D7</SolutionId>
  <SolutionName>HelloWorld</SolutionName>
  <ResetWebServer>True</ResetWebServer>
  <AssemblyDeployment>False</AssemblyDeployment>
</Solution>

図からわかるように、SolutionConfig.xml ファイルは、ソリューション ID を含む GUID、ソリューション名、および ResetWebServer という名前のブール値を追跡します。このブール値は、出力ソリューション パッケージの展開、更新、または取り消し後に WSS が各フロントエンド Web サーバー上で IISRESET を実行するかどうかを制御する命令を、manifest.xml 内に追加するために使用されます。

また、SolutionConfig.xml には、RefreshDeploymentFiles コマンドによって読み取られる AssemblyDeployment という名前の要素も含まれています。これは、必要なアセンブリ構成情報に加えて出力ソリューション パッケージ内に展開する必要のある、1 つまたは複数のアセンブリに関する構成を追加するために使用されます。

たとえば、空のソリューション (C# アセンブリ) を選択して HelloWorld プロジェクトを作成した場合、SolutionConfig.xml ファイルの初期状態は、図 4 のようになります。アセンブリ展開のために、manifest.xml および SolutionPackage.ddf にバイナリ ファイルおよびメタデータ情報が追加されます。

Figure 4 Deploying an Assembly with SolutionConfig.xml (Close Figure 4)
<!-- SolutionConfig.xml file for an Empty Solution with C# assembly -->
<Solution>
  <SolutionId>24F91DED-8BA7-4633-8BA0-4C9B2A4387D7</SolutionId>
  <SolutionName>HelloWorld</SolutionName>
  <ResetWebServer>True</ResetWebServer>
  <AssemblyDeployment>True</AssemblyDeployment>
  <!--Assembly files-->
  <Assemblies>
    <Assembly Location="HelloWorld.dll"
              DeploymentTarget="GlobalAssemblyCache">
      <SafeControls>
        <SafeControl Assembly="HelloWorld, [full 4-part assembly name]"
                     Namespace="HelloWorld"
                     TypeName="*"
                     Safe="True" />
      </SafeControls>
    </Assembly>
  </Assemblies>
</Solution>

MSBuild を使用してカスタム ビルド ターゲットを作成する

バッチ ファイルを使用する代わりに、STSDEV は、MSBuild の機能を利用して、Visual Studio ソリューションに統合可能なカスタム ビルド ターゲットを作成します。特に、STSDEV は、DeploymentFiles フォルダ内に、カスタム ビルド ターゲットを含む Microsoft.SharePoint.targets という名前のファイルを作成します。これにより、SharePoint 開発で一般に使用されるコマンドの完全なセットを使用して、新しい各ソリューションを作成できるようになります。図 5 には、それぞれの新しいプロジェクトに対して生成されるカスタム ビルド ターゲットの一覧を示しています。

Figure 5 Custom Build Targets (Close Figure 5)

ターゲット | 説明
DebugBuild | このコマンドは、プロジェクトの出力アセンブリ DLL のデバッグ バージョンをビルドした後、ソリューションの展開ファイルを更新し、MAKECAB.EXE を使用して出力ソリューション パッケージの .wsp ファイルをビルドします。
DebugInstall | このコマンドは、STSADM の addsolution 操作を使用して、ソリューション パッケージの .wsp ファイルをローカル ファームのソリューション パッケージ ストアにインストールします。
DebugDeploy | このコマンドは、STSADM の deploysolution 操作を使用して、ソリューション パッケージをファーム内に展開します。
DebugUpgrade | このコマンドは、STSADM の upgradesolution 操作を使用して、ソリューション パッケージをアップグレードします。
DebugQuickCopy | このコマンドは、ソリューションの RootFiles フォルダの内容をローカル Web サーバーの WSS RootFiles ディレクトリにコピーする、単純なコピー コマンドを実行します。
DebugRetract | このコマンドは、STSADM の retractsolution 操作を使用して、ソリューション パッケージをファームから取り消します。
DebugDelete | このコマンドは、STSADM の deletesolution 操作を使用して、ソリューション パッケージの .wsp ファイルをローカル ファームのソリューション パッケージ ストアから削除します。
ReleaseBuild | このコマンドは、プロジェクトの出力アセンブリ DLL のリリース バージョンをビルドした後、ソリューションの展開ファイルを更新し、MAKECAB.EXE を使用して出力ソリューション パッケージの .wsp ファイルをビルドします。

カスタム ビルド ターゲットを使用すると、セキュリティ上の問題が生じることに注意してください。Visual Studio では、予期しない攻撃から開発者を保護するために、カスタム ビルド ターゲットを使用したプロジェクトまたはソリューションを開こうとすると警告が表示されます。したがって、STSDEV ユーティリティで作成されたプロジェクトを初めて開くときには、セキュリティ警告が表示されることを覚えておいてください。

ちょうどいいタイミングなので、ここで Microsoft.SharePoint.targets ファイルの内容を詳しく見ておきます。このファイルを開くと、図 6 に示されるような構造の XML が表示されます。図からわかるように、MSBuild では最上位に Project 要素が必要です。Project 要素内には、PropertyGroup や Target などの子要素があります。

Figure 6 Microsoft.SharePoint.targets (Close Figure 6)
<?xml version="1.0" encoding="utf-8" ?>

<Project DefaultTargets="DebugBuild"
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <PackageName>HelloWorld.wsp</PackageName>
    <PackageFile>HelloWorld.wsp</PackageFile>
    <WssRootFilesFolder> <!--RootFiles path--> </WssRootFilesFolder>
    <MAKECAB> <!-- MAKECAB.EXE path--> </MAKECAB>
    <STSADM> <!-- STSADM.EXE path--> </STSADM>
    <STSDEV> <!—STSDEV.EXE path--> </STSDEV>
  </PropertyGroup>

<Target Name="DebugBuild">
    <!—custom build target 1 -->
  </Target>

  <Target Name="DebugInstall" DependsOnTargets="DebugBuild">
    <!—custom build target 2 -->
  </Target>
</Project>

Microsoft.SharePoint.targets の最上位にある PropertyGroup セクションは、個別のターゲット コマンド内で必要となるファイルのパスや名前などのメタデータおよび値を追跡するために使用されます。Microsoft.SharePoint.target は、出力ソリューション パッケージのファイル名に加えて、MAKECAB や STSADM などのユーティリティの完全修飾ファイル パスも追跡することがわかります。また、コマンド ターゲットが STSDEV ユーティリティを呼び出して RefreshDeploymentFiles コマンドを実行できるように、STSDEV 自体のローカル ファイル パスを追跡するプロパティもあります。

では、ビルド ターゲットをいくつか見てみましょう。最初に、図 7 に示す DebugBuild という名前のビルド ターゲット内に定義されたコマンドを説明します。

Figure 7 DebugBuild Build Target (Close Figure 7)
<Target Name="DebugBuild">
  <Message Text="Refreshing Deployment Files..." Importance="high" />
  <Exec Command="$(STSDEV) /refresh $(TargetName) $(SolutionDir)"
    ContinueOnError="false" />
  <Message Text="Deleting Solution Package File..." Importance="high" />
  <Delete Files="$(ProjectDeploymentFilesFolder)\$(PackageFile)"
    ContinueOnError="true" />
  <Message Text="Building Solution Package (Debug Version)"
    Importance="high" />
  <Exec Command="$(MAKECAB) /F
    $(ProjectDeploymentFilesFolder)\SolutionPackage.ddf /D
    CabinetNameTemplate=$(PackageFile)" ContinueOnError="false" />
</Target>

図からわかるとおり、DebugBuild ターゲット内の最初のコマンドは、STSDEV を呼び出して RefreshDeploymentFiles コマンド実行のための /refresh パラメータを渡すコマンドです。これにより、STSDEV ユーティリティは、ソリューション パッケージのビルド プロセスの最初の手順として、manifest.xml および SoutionPackage.ddf を再生成します。その後、既存の .wsp ファイルを削除し、MAKECAB.EXE ユーティリティを使用してソリューション パッケージを再ビルドするための MSBuild コマンドが実行されます。また、Message コマンドの使用法にも注意してください。これは、コマンドの実行中に、Visual Studio の出力ウィンドウに開発者への情報メッセージを表示するために使用されます。

DebugInstall コマンドは、STSADM で提供される addsolution 操作を使用して、新しく生成されたソリューション パッケージの .wsp ファイルをローカル ファームのソリューション パッケージ ストアにインストールするために記述されています。また、DependsOnTargets 属性の使用にも注意してください。この属性により、DebugInstall ターゲットが実行されると、最初に DebugBuild ターゲット コマンドが実行されます。

<Target Name="DebugInstall" DependsOnTargets="DebugBuild">
<Message Text="Installing Solution..." Importance="high" />
  <Exec Command="$(STSADM) -o addsolution -filename
    $(ProjectDeploymentFilesFolder)\$(PackageFile)"
    ContinueOnError="true" />
  <Exec Command="$(STSADM) -o execadmsvcjobs" />
  <Message Text="" Importance="high" />
</Target>

STSADM ユーティリティで実行できる操作の多くは、タイマ ジョブを使用して非同期に実行されます。したがって、execadmsvcjobs 操作を実行するために別の STSADM 呼び出しを使用します。この操作により、コマンドは一時停止し、ローカル SharePoint Web サーバーがすべての保留中タイマ ジョブを実行できるようになるまで待ちます。execadmsvcjobs 操作は、deploysolution 操作の呼び出し直後にも DebugDeploy ターゲット内で呼び出されます。

<Target Name="DebugDeploy" DependsOnTargets="DebugInstall">
  <Message Text="Deploying Solution..." Importance="high" />
  <Exec Command="$(STSADM) -o deploysolution -name
    $(PackageName) -local -allowGacDeployment -force" />
  <Exec Command="$(STSADM) -o execadmsvcjobs" />
  <Message Text="" Importance="high" />
</Target>

STSDEV ユーティリティで生成された Microsoft.SharePoint.targets ファイル内に存在するさまざまなカスタム ビルド ターゲットについて理解した後は、それらを Visual Studio 環境内から実行する方法を知る必要があります。これは、非常に単純です。図 8 に示すように、単に目的のビルド ターゲットに関連付けられた Visual Studio 構成を選択し、標準の Visual Studio ビルド コマンドを実行するだけです。

Figure 8 STSDEV で生成されたビルド ターゲットを実行する
Figure 8 STSDEV で生成されたビルド ターゲットを実行する(画像を縮小するには、ここをクリックします)
Figure 8 STSDEV で生成されたビルド ターゲットを実行する
Figure 8 STSDEV で生成されたビルド ターゲットを実行する (画像を拡大するには、ここをクリックします)

これらのカスタム ビルド ターゲットを結び付けるのに使用されている MSBuild の手法では、Target 要素を Visual Studio プロジェクト ファイル (たとえば、HelloWorld.csproj) に追加することが必要となります。これは、新しいソリューションの作成時に STSDEV が自動的に実行します。.csproj ファイルの 1 つを開いてその最後の部分を見ると、図 9 のような Target セクションがあります。

Figure 9 Targets in a Visual Studio Project (Close Figure 9)
<Target Name="AfterBuild">
  <CallTarget Targets="DebugBuild"
   Condition="'$(Configuration)|$(Platform)' == 'DebugBuild|AnyCPU'"/>
  <CallTarget Targets="DebugInstall"
   Condition="'$(Configuration)|$(Platform)' == 'DebugInstall|AnyCPU'"/>
  <CallTarget Targets="DebugDeploy"
   Condition="'$(Configuration)|$(Platform)' == 'DebugDeploy|AnyCPU'"/>
  <CallTarget Targets="DebugRedeploy"
   Condition="'$(Configuration)|$(Platform)' == 'DebugRedeploy|AnyCPU'"/>
  <CallTarget Targets="DebugUpgrade"
   Condition="'$(Configuration)|$(Platform)' == 'DebugUpgrade|AnyCPU'"/>
  <CallTarget Targets="DebugQuickCopy"
   Condition="'$(Configuration)|$(Platform)' == 'DebugQuickCopy|AnyCPU'"/>
  <CallTarget Targets="DebugRetract"
   Condition="'$(Configuration)|$(Platform)' == 'DebugRetract|AnyCPU'"/>
  <CallTarget Targets="DebugDelete"
   Condition="'$(Configuration)|$(Platform)' == 'DebugDelete|AnyCPU'"/>
  <CallTarget Targets="ReleaseBuild"
   Condition="'$(Configuration)|$(Platform)' == 'ReleaseBuild|AnyCPU'"/>
</Target>

この Target セクションには AfterBuild という名前が付いていることがわかります。これは、Visual Studio が各ビルドの完了時に自動的に実行する既知のターゲット名です。これにより、選択された構成に応じて適切なカスタム ビルド ターゲットを実行する条件と共に、ビルド ターゲットごとに 1 つの CallTarget コマンドを簡単に追加できます。

STSDEV ユーティリティを拡張する

STSDEV ユーティリティには、いくつかのサンプル ソリューション プロバイダが付属しています。ただし、このユーティリティの真の能力は、独自のソリューション プロバイダを追加して STSDEV を拡張することで発揮されます。それを行う場合は、今月のコラムに付属する STSDEV プロジェクトのソース コードを開き、既存のソリューション プロバイダのいくつかに対するクラス実装を調べてください。

SimpleFeatureSolutionProvider.cs または WebPartSolutionProvider.cs などのソース ファイルを開き、InitializeSolution および AddSolutionItems メソッドのコードをひととおり見ると、ソリューション プロバイダを作成するのはかなり簡単であることがわかります。SharePoint 開発プロジェクトで必要な共通ファイルの多くを作成するためのパブリックな静的メソッドを含む、SolutionBuilder という名前のクラスがあります。また、いくつかのソリューション プロバイダ (SimpleFeatureSolutionProvider.cs 内のものなど) は、Windows フォームを使用して作成されたモーダル ダイアログ ボックスを使用して、追加の構成アイテムを指定するようユーザーに指示します。

STSDEV ユーティリティが、SharePoint 開発者の日常にとって便利でためになるツールであると感じていただけたらさいわいです。このユーティリティは、SharePoint 開発者コミュニティによって生み出されたものであり、そのソース コードの最新バージョンは、codeplex.com/stsdev の CodePlex Web サイトからいつでも入手できます。CodePlex の STSDEV サイトには、スクリーンキャストの入門セットなど、このユーティリティをすばやく使いこなすための他のリソースも用意されています。フィードバックや、ご自身の SharePoint 開発作業で作成したソリューション プロバイダの例などがありましたら、ぜひお知らせください。

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

Ted Pattison は、フロリダ州タンパ在住で、執筆、教育活動に従事しています。また、SharePoint MVP でもあります。最新の著書として、『Inside Windows SharePoint Services 3.0』があります。彼の会社 Ted Pattison Group (www.TedPattison.net) では、企業の開発者を対象とする SharePoint の上級者向けトレーニングを実施しています。


テストの実行: Windows PowerShell を使用した Web UI テストの自動化

テストの実行
Windows PowerShell を使用した Web UI テストの自動化
Dr. James McCaffrey

コードのダウンロード : : TestRun2008_03.exe (155KB)
コンテンツ
Web アプリケーション
Windows PowerShell を使用したテスト自動化スクリプト
もう一歩進む

Windows PowerShellTM は、マイクロソフトの新しいコマンド シェルおよびスクリプト言語であり、種々の軽量なテスト自動化に対して優れたプラットフォームとなります。今月の「テストの実行」コラムでは、Windows PowerShell を使用し、Internet Explorer® の自動化によって、あらゆる種類の Web アプリケーション用にすばやく簡単な UI テスト自動化を作成する方法を紹介します。このコラムは主に初心者向けに書かれていますが、熟練したエンジニアの方々にも興味深い内容が含まれています。

始める前に、これからテストを行うサイトがすべて、Internet Explorer の "信頼済みサイト" の一覧に追加されていることを確認してください。追加されていないと、スクリプトが動作しない場合があります。まず Windows PowerShell の次のコマンドを実行することから、自動化の説明を始めます。

PS C:\> $ie = new-object -com "InternetExplorer.Application"

このコマンドにより、SHDocVw.dll ライブラリから従来の InternetExplorer COM 自動化オブジェクトのインスタンスが作成されます。キーワード new-object は、Windows PowerShell のコマンドレットの 1 つです。コマンドレットは約 130 個あり、Windows PowerShell の機能の中核を形成しています。get-command を実行すればコマンドレットがどのようなものかを調べることができ、get-help コマンドを実行すればいつでもコマンドのヘルプを見ることができます。ここでは、new-object に対して -com スイッチ (実際には -comObject の省略形) を指定しています。これは、マネージ オブジェクトをインスタンス化するのではなく、オブジェクトの ProgID を使用して従来の COM オブジェクトをインスタンス化することを示すものです。

結果のオブジェクトは、$ie 変数に格納します (Windows PowerShell のすべての変数は、先頭に "$" が付加され、他の種類のトークンと簡単に区別できるようになっています)。次に、Navigate メソッドを使用して、テスト対象のダミー Web アプリケーションである MiniCalc をブラウザ自動化オブジェクトに読み込みます。

PS C:\> $ie.navigate("http://localhost/MiniCalc/Default.aspx")
PS C:\> $ie.visible = $true

Windows PowerShell の優れた機能の 1 つは、オブジェクトの機能を調べる手助けをしてくれることです。古いスクリプト テクノロジでは、InternetExplorer オブジェクトに Navigate メソッドがあることを知らなかった場合、何らかの外部リファレンスを使用してその情報を探すしかありませんでした。Windows PowerShell では、オブジェクトで使用できるメソッドやプロパティをすばやく把握するための方法がいくつかあります。たとえば、タブ補完機能を利用して、「$ie」と入力してから Tab キーを押します。各キーを押すたびに、使用できるプロパティまたはメソッドが表示されます。また、get-member コマンドレットを使用して、すべての使用可能なプロパティおよびメソッドとそれらのシグネチャの一覧を表示することもできます。

PS C:\> $ie | get-member | more

Windows PowerShell のもう 1 つの探索機能として、コマンド補完があります。たとえば、「$ie.vi」と入力してから Tab キーを押すと、Windows PowerShell によって自動的に ie.visible ステートメントの入力が完了されます。Windows PowerShell が備えるこれらの探索機能により、コーディングにかかる時間を大幅に短縮できます。

次に、テスト対象アプリケーションのすべての入力コントロールへの参照を取得します。

PS C:\> $doc = $ie.document
PS C:\> $tb1 = $doc.getElementByID("TextBox1")
PS C:\> $tb2 = $doc.getElementByID("TextBox2")
PS C:\> $add = $doc.getElementByID("RadioButton1")
PS C:\> $btn = $doc.getElementByID("Button1")

Document プロパティを使用してアクティブなドキュメントを取得してから、getElementById メソッドを使用して各コントロールへの参照を取得します。この手法が機能するためには、すべての HTML 要素に ID 値が必要であることに注意してください。ほとんどの状況では、これは問題とはなりません。Web アプリケーションを Visual Studio® で作成する場合は、すべてのコントロールに自動的に ID が付与されます。要素に ID がない Web アプリケーションに対してテスト自動化を記述する場合でも、getElementsByTagName メソッドを使用して要素のコレクションを取得してから、特定の要素にインデックスでアクセスすることができます。

次に、自動化ではいくつかのユーザー入力をシミュレートします。

PS C:\> $tb1.value = 5
PS C:\> $tb2.value = 7
PS C:\> $add.checked = $true
PS C:\> $btn.click()

$tb1 および $tb2 で参照される TextBox1 および TextBox2 の値は文字列型なので、次のように記述することもできますが、Windows PowerShell によって正しいデータ型が類推されるため、引用符は省略可能です。

PS C:\> $tb1.value = "5"

MiniCalc Web アプリケーションの結果の状態を調べることで、この対話型の自動化を完了します。

PS C:\> $tb3 = $doc.getElementById("TextBox3")
PS C:\> $ans = $tb3.value
PS C:\> if ($ans -eq "12.0000") { 'Pass' } else { '*FAIL*' }

最初に、TextBox3 コントロールへの参照を取得します。これをもっと早い段階で行わなかったのは、$btn click メソッドを呼び出すと、HTTP 要求が Web サーバーに送信され、新しい TextBox3 値を持つ新しいページが生成されてクライアント ブラウザに返されるからです。ここでは write-host 'Pass' の代わりに単に 'Pass' と入力しています。これは、文字列値に対する Windows PowerShell の既定の動作が、ホストへの値の出力であるためです。

今月のコラムの以降のセクションでは、実際に何をテストするのかがはっきり理解できるように、テスト対象のダミー Web アプリケーション MiniCalc について簡単に説明します。その後で、図 1 に示される対話型コマンドをより実際的な Windows PowerShell スクリプトに再構成して、面倒なタイミングの問題にどのように対処するかを示します。最後に、Windows PowerShell を使用した Web UI テスト自動化の長所と短所を、市販のテスト フレームワークの使用、Visual Studio によるカスタム C# ハーネスの作成、JavaScript アプローチを使用したカスタム自動化の作成など、いくつかの代替手段と比較しながら検討します。ここで紹介する手法を読者の皆さんのソフトウェア テスト ツールキットに加えていただき、非常に便利で役に立つと評価していただければさいわいです。

Figure 1 Windows PowerShell を使用した Web アプリケーション UI テストの自動化
Figure 1 Windows PowerShell を使用した Web アプリケーション UI テストの自動化(画像を縮小するには、ここをクリックします)
Figure 1 Windows PowerShell を使用した Web アプリケーション UI テストの自動化
Figure 1 Windows PowerShell を使用した Web アプリケーション UI テストの自動化 (画像を拡大するには、ここをクリックします)

Web アプリケーション

最初に、図 1 の背景に示されている MiniCalc という ASP.NET Web アプリケーションについて見ていきます。これは、この UI テスト自動化の対象となるアプリケーションです。MiniCalc は、単純な ASP.NET Web アプリケーションです。今月のコラムで紹介する手法は、ASP.NET Web アプリケーション、従来の ASP Web アプリケーション、および PHP や Ruby などのテクノロジを使って作成されたアプリケーションを自動化できます。

Web アプリケーションのコードをできるだけ短く単純に保つために、このアプリケーションはメモ帳で記述し、ロジック コードと表示コードを 1 つのファイルに配置しました。コード全体を図 2 に示します。(このコラムで説明するすべてのコードは、MSDN® Magazine Web サイトからダウンロードできます。) この Web アプリケーションは、Default.aspx という名前で Web サイト http://localhost/MiniCalc に配置されています。

Figure 2 Web Application under Test (Close Figure 2)
<%@ Page Language="C#" %>
<script runat="server">
  private void Button1_Click(object sender, System.EventArgs e)
  {
    int alpha = int.Parse(TextBox1.Text.Trim());
    int  beta = int.Parse(TextBox2.Text.Trim());

    System.Threading.Thread.Sleep(3000);

    if (RadioButton1.Checked) {
      TextBox3.Text = Sum(alpha, beta).ToString("F4");
    }
    else if (RadioButton2.Checked) {
      TextBox3.Text = Product(alpha, beta).ToString("F4");
    }
    else
     TextBox3.Text = "Select method";
  }

  private static double Sum(int a, int b) {
    double ans = a + b;
    return ans;
  }

  private static double Product(int a, int b) {
    double ans = a * b;
    return ans;
  }
</script>

<html>
  <head>
    <style type="text/css">
     fieldset { width: 16em }
     body { font-size: 10pt; font-family: Arial }
    </style>
    <title>Default.aspx</title>
  </head>
  <body bgColor="#ccffff">
    <h3>MiniCalc by ASP.NET</h3>
    <form method="post" name="theForm" id="theForm"
      runat="server" action="Default.aspx">
      <p>
        <asp:Label id="Label1" runat="server">
          Enter integer:&nbsp&nbsp
        </asp:Label>
        <asp:TextBox id="TextBox1" width="100" runat="server" />
      </p>
      <p>
        <asp:Label id="Label2" runat="server">
          Enter another:&nbsp
        </asp:Label>
        <asp:TextBox id="TextBox2" width="100"  runat="server" />
      </p>
      <p></p>
      <fieldset>
        <legend>Arithmentic Operation</legend>
        <p>
          <asp:RadioButton id="RadioButton1"
            GroupName="Operation" runat="server"/>
            Addition
        </p>
        <p>
          <asp:RadioButton id="RadioButton2"
            GroupName="Operation" runat="server"/>
            Multiplication
        </p>
        <p></p>
      </fieldset>
      <p>
        <asp:Button id="Button1" runat="server"
          text=" Calculate " onclick="Button1_Click" />
      </p>
      <p>
        <asp:TextBox id="TextBox3" width="120"  runat="server" />
      </p>
    </form>
  </body>
</html>

Web アプリケーションの UI テスト自動化を実行する場合は、操作または調査するユーザー コントロールの ID を把握しておく必要があります。図 2 のコードを見ると、コントロールに Visual Studio スタイルの既定の ID が付けられていることがわかります。ユーザーが入力した整数を保持する 2 つのテキストボックス コントロールには、TextBox1 および TextBox2 という ID を使用しています。

<asp:TextBox id="TextBox1" width="100" runat="server" />

ユーザーが加算または乗算のいずれかを選択するための RadioButton コントロールには RadioButton1 という ID を使用し、Web アプリケーションが TextBox1 および TextBox2 の値に加算または乗算を行うための Button コントロールには Button1 という ID を使用しました。結果を保持するテキストボックス コントロールには TextBox3 という ID を使用しています。すべてのロジックは、Button1 のクリック イベントを処理する Web アプリケーションの Button1_Click メソッドに含まれています。

まず、次に示すように、ユーザー入力を取り込み、文字列を int 型に変換します。

int alpha = int.Parse(TextBox1.Text.Trim());
int  beta = int.Parse(TextBox2.Text.Trim());
System.Threading.Thread.Sleep(3000);

ユーザー入力の取り込みの後に、Thread.Sleep ステートメントを挿入しています。これは、実際の Web アプリケーションがバックエンド データベースにアクセスするときに生じるような、一定の処理時間をシミュレートするために使用されています。後で示すように、予測不可能な HTTP 応答時間を処理することが、Web アプリケーション用に軽量な UI テスト自動化を記述する上で最も難しい部分です。

必要な 2 つの整数をユーザーから取得した後は、ユーザーが指定した演算 (加算または乗算) を確認し、結果を計算して TextBox3 コントロールに配置します。

if (RadioButton1.Checked) {
  TextBox3.Text = Sum(alpha, beta).ToString("F4");
}
else if (RadioButton2.Checked) {
  TextBox3.Text = Product(alpha, beta).ToString("F4");
}
else
 TextBox3.Text = "Select method";
}

ここでは答えを暗黙的に double 型にキャストし、また、結果を TextBox3 コントロールに配置する際には、ToString メソッドに "F4" 引数を使用して小数点以下 4 桁の形式に設定しています。

Windows PowerShell を使用したテスト自動化スクリプト

このコラムの最初のセクションで説明した Windows PowerShell による対話型のテスト自動化は、多くのテスト状況で有用ですが、テスト自動化スクリプトの作成が必要になる場合もしばしばあります。図 3 に、MiniCalc Web アプリケーションをテストする 1 つの方法を示します。図 4 に、実行中のスクリプトを示します。

Figure 3 UI Test Automation with Windows PowerShell (Close Figure 3)
# file: testScript.ps1

function main()
{
  write-host "`nBegin test automation using Windows PowerShell`n"

  write-host 'Launching IE'
  $ie = new-object -com "InternetExplorer.Application"
  $ie.navigate("about:blank")
  $ie.visible = $true
  [System.Threading.Thread]::Sleep(2000)

  write-host "`nResizing IE to 425 x 535"
  $ie.height = 535
  $ie.width = 425

  write-host "`nNavigating to MiniCalc Web application"
    navigateToApp $ie "http://localhost/MiniCalc/Default.aspx"`
      "TextBox1" 100 2

  write-host "`nGetting input controls"
  $doc = $ie.document
  $tb1 = $doc.getElementByID("TextBox1")
  $tb2 = $doc.getElementByID("TextBox2")
  $add = $doc.getElementByID("RadioButton1")
  $btn = $doc.getElementByID("Button1")

    if ($tb1 -eq $null -or $tb2 -eq $null -or $add -eq $null -or $btn`
      -eq $null) {
    write-host "One or more controls are null" -backgroundcolor "red"`
      -foregroundcolor "yellow"
  }
  else {
    write-host "All controls found"
  }

  write-host "`nSetting TextBox1 to 5"
  $tb1.value = 5
  write-host "Setting TextBox2 to 7"
  $tb2.value = 7

  write-host "Selecting 'Addition' operation"
  $add.checked = $true

  write-host "`nClicking 'Calculate' button"
  $tb3 = $doc.getElementByID("TextBox3")
  $old = $tb3.value
  $btn.click()
  $wait = $true
  $numWaits = 0
  while ($wait -and $numWaits -lt 100) {
    $numWaits++
    [System.Threading.Thread]::Sleep(50)
    $tb3 = $doc.getElementByID("TextBox3")
    if ($tb3.value -ne $old) {
      $wait = $false
    }
    else {
      write-host "Waiting for app to respond $numWaits . . ."
    }
  }
  if ($numWaits -eq 100) {
    throw "Application did not respond after 100 delays"
  }
  else {
    write-host "Application has responded"
  }

  write-host "`nChecking for 12.0000"
  $tb3 = $doc.getElementByID("TextBox3")
  $ans = $tb3.value

  if ($ans -eq '12.0000') {
    write-host "Target value found"
    write-host "`nTest scenario: Pass" -foregroundcolor 'green'
  }
  else {
    write-host "Target value not found"
    write-host "`nTest scenario: *FAIL*" -foregroundcolor 'red'
  }

  trap {
    write-host "Fatal error: " $_.exception.message -backgroundcolor red`
      -foregroundcolor yellow
  }

  write-host "`nEnd test automation`n"
} # main

function navigateToApp($browser, [string] $url, [string] $controlID,`
  [int] $maxDelays, [int] $delayTime)
{
  $numDelays = 0
  $loaded = $false
  $browser.navigate($url)
  while ($loaded -eq $false -and $numDelays -lt $maxDelays) {
    $numDelays++
    [System.Threading.Thread]::Sleep($delayTime)
    $doc = $browser.document
    if ($doc -eq $null) {
      continue
    }
    $controlRef = $doc.getElementByID($controlID)
    if ($controlRef -eq $null) {
      write-host "Waiting for Web app to load $numDelays . . ."
    }
    else {
      write-host "Web app loaded after $numDelays pauses"
      $loaded = $true
    }
  }

  if ($numDelays -eq $maxDelays) {
    throw "Browser not loaded after $maxDelays delays"
  }
}

main
# end script
Figure 4 スクリプトでテスト自動化を実行する
Figure 4 スクリプトでテスト自動化を実行する(画像を縮小するには、ここをクリックします)
Figure 4 スクリプトでテスト自動化を実行する
Figure 4 スクリプトでテスト自動化を実行する (画像を拡大するには、ここをクリックします)

図 4 を見ると、Windows PowerShell シェルを起動した後、Windows PowerShell セッションの実行ポリシーでスクリプト実行が許可されているのを確認してから、スクリプトを起動していることがわかります。Windows PowerShell では、スクリプトが現在のディレクトリに存在する場合でも、スクリプトのパスを指定する必要があることに注意してください (現在のディレクトリにある場合は .\)。

テスト スクリプト全体の構造は、次のようになります。

# file: testScript.ps1
function main
{
  # code
}

function navigateToApp($browser, [string] $url,
 [string] $controlID, [int] $maxDelays,
 [int] $delayTime)
{
  # code
}

main
# end script

ここではメイン関数に main という名前を付けていますが、Windows PowerShell には既定のスクリプト エントリ ポイントはないため、この関数はどのような名前でもかまいません。補助的な navigateToApp 関数を定義した後、1 つのステートメント "main" を実行することで、スクリプトの実行を起動します。main 関数の最初の数行を次に示します。

write-host "`nBegin test automation using Windows PowerShell`n"
write-host 'Launching IE'
$ie = new-object -com "InternetExplorer.Application"
$ie.navigate("about:blank")
$ie.visible = $true
[System.Threading.Thread]::Sleep(2000)

最初の 2 つの write-host ステートメントは、Windows PowerShell での二重引用符の機能を示しています。これにより、特定のエスケープ シーケンス ('n 改行文字および $ で始まるオブジェクト参照など) がスクリプト実行エンジンで評価されるようになります。単一引用符で囲まれた文字列は、文字どおりに解釈されます。

前に説明したとおり、ここでは new-object コマンドレットを使用して従来の InternetExplorer COM 自動化オブジェクトをインスタンス化しています。その後、about:blank ページに移動して、ブラウザを表示します。Microsoft® .NET Framework 内に直接呼び出しを行って Thread.Sleep メソッドにアクセスし、テスト自動化を 2 秒間一時停止させます。ここで 2 つの点に注意してください。1 つは、.NET Framework に直接呼び出しを行えることが、他のほとんどのスクリプト テクノロジを凌駕する Windows PowerShell の重要な長所の 1 つであることです。もう 1 つは、テスト対象の Web アプリケーションが応答できるようにスレッドをスリープさせる手法は適切に機能しますが、それよりもさらに良いアプローチが他にあることです。これについては、後で簡単に説明します。

自動化スクリプトの次の数行は、Internet Explorer ブラウザを既知の状態に設定します。

write-host "`nResizing IE to 425 x 535"
$ie.height = 535
$ie.width = 425

一般に、自動化で明らかになるバグをより簡単に検出できるよう、ブラウザの特性を既知の状態に設定しておくことは、ほとんどの種類の Web アプリケーション UI テストの自動化で役に立ちます。たとえば、SQL Server® データ ストアのフロント エンドであり、リストボックス コントロールに結果を表示する検索機能を持つような Web アプリケーションを考えます。アプリケーションのロジックが、特定の検索後にリストボックス コントロールをクリアし忘れるとしましょう。ここで、結果のリストボックスが空である標準の既定状態から始めると、テスト自動化は Web アプリケーションが正しく動作していると判断し、バグが明らかにならない場合があります。ただし、リストボックスに既に何らかの結果が含まれている状態から自動化を開始すれば、新しい結果が古い結果に連結されるため、バグが明らかになります。また、初期状態を変更できるようなスクリプトを記述すると、そのような変更によって影響を受けやすいバグを見つけるのに役立ちます。

次に、ブラウザでテスト対象の MiniCalc Web アプリケーションにアクセスします。

write-host "`nNavigating to MiniCalc Web application"
navigateToApp $ie "http://localhost/MiniCalc/Default.aspx" "TextBox1" 100 80

ここでは、navigateToApp ヘルパ関数を呼び出しています。この呼び出しの意味は、"Internet Explorer を URL http://localhost/MiniCalc/Default.aspx にナビゲートして、TextBox1 という ID を持つ HTML 要素への参照がアクセス可能になるまで待ち、アクセス間で 80 ミリ秒だけ一時停止しながら、TextBox1 へのアクセスを最大 100 回まで試行する" と解釈できます。アプリケーションがいつ読み込まれるかを決定することは、簡単ではありません。この Web アプリケーションを読み込む場合のおおまかな代替手段は、次のとおり単純ですが、一般にそれほど効果的ではありません。

$ie.navigate("http://localhost/MiniCalc/Default.aspx")
 [System.Threading.Thread]::Sleep(8000)

このアプローチにおける 2 つの問題は、自動化をどれぐらいの時間一時停止すればよいかを予測する良い方法がないことと、テスト対象アプリケーションが割り当てられた時間内に読み込まれなかった場合の明確な対処方法がないことです。図 5 に示される navigateToApp 関数は、この両方の問題を解決します。

Figure 5 NavigateToApp Helper Function (Close Figure 5)
function navigateToApp($browser, [string] $url, [string] $controlID,
  [int] $maxDelays, [int] $delayTime)
{
  $numDelays = 0
  $loaded = $false
  $browser.navigate($url)
  while ($loaded -eq $false -and $numDelays -lt $maxDelays) {
    $numDelays++
    [System.Threading.Thread]::Sleep($delayTime)
    $doc = $browser.document
    if ($doc -eq $null) {
      continue
    }
    $controlRef = $doc.getElementByID($controlID)
    if ($controlRef -eq $null) {
      write-host "Waiting for Web app to load $numDelays . . ."
    }
    else {
      write-host "Web app loaded after $numDelays pauses"
      $loaded = $true
    }
  }

  if ($numDelays -eq $maxDelays) {
    throw "Browser not loaded after $maxDelays delays"
  }
}

navigateToApp 関数の基本動作は、遅延ループに入り、各繰り返しにおいて、指定されたユーザー コントロール参照が使用できるかどうかを確認するというものです。また、無限ループ状態を避けるために、ループ内で特定の最大繰り返し回数を超えるとループは終了します。

Windows PowerShell はオブジェクト ベースですが、変数のような単純なオブジェクトを参照することも許容されます。navigateToApp 関数はローカル変数 $numDelays および $loaded を使用しますが、これらはローカル変数として明示的に宣言する必要はありません。これらの変数の先頭に $private: 修飾子を付けてローカル スコープを明示したり、$global: 修飾子を付けてこれらの変数の値を関数のスコープ外部で使用可能にしたりもできます。navigateToApp の制御ロジックは、ドキュメント オブジェクトが使用できるかどうかを確認し、使用できない場合は、continue ステートメントを使用してループの現在の繰り返しから抜け、一定の遅延時間後に再試行します。ドキュメント オブジェクトへの有効な参照がある場合は、ターゲット要素への参照の取得を試行します。

また、Windows PowerShell の elseif 制御構造も使用できます。さらに、明示的な $loaded 変数を使用する代わりに、Windows PowerShell の break ステートメントを使用して遅延ループを抜けることも可能です。Windows PowerShell には、自分が慣れ親しんでいるプログラミング スタイルを含め、さまざまな異なるスタイルでプログラミングができる豊富な制御構造のセットが用意されているため、学習曲線が高速化されます。

遅延ループから抜けた後は、最大遅延回数を超えたためにループが終了したのかどうかをチェックします (これは、ターゲット ユーザー コントロールへの参照をまったく取得していないことを意味し、Web アプリケーションが正しく読み込まれていないことを示します)。ここでは、例外をスローし、Windows PowerShell のトラップ メカニズムを使用して main 関数内で例外を捕捉します。

テスト対象の Web アプリケーションが完全に読み込まれたら、すべてのユーザー入力コントロールへの参照を取得します。

write-host "`nGetting input controls"
$doc = $ie.document
$tb1 = $doc.getElementByID("TextBox1")
$tb2 = $doc.getElementByID("TextBox2")
$add = $doc.getElementByID("RadioButton1")
$btn = $doc.getElementByID("Button1")

前述したとおり、getElementById メソッドでは、すべてのコントロールが ID 属性を持つ必要がありますが、ID のないコントロールにアクセスする必要のある状況では、getElementsByTagName メソッドを使用することもできます。

次に、HTML 要素の参照が有効であることを確認するための簡単なチェックを実行します。

if ($tb1 -eq $null -or $tb2 -eq $null –or
    $add -eq $null -or $btn -eq $null) {
  write-host "One or more controls are null"
    -backgroundcolor "red" -foregroundcolor "yellow"
}
else {
    write-host "All controls found"
}

Windows PowerShell ベースの UI テスト自動化を記述するときには、一般に、例外をスローするか、またはエラー チェックの際に write-host コマンドレットを使用して単にメッセージを表示するかは、個人的なコーディング習慣の問題です。概念的には、例外をスローする方がより論理的なアプローチですが、write-host を使用すると、–backgroundcolor および –foregroundcolor 引数を使用して、簡単に表示可能なテキストを指定できます。

すべてのユーザー入力コントロールが使用できることを確認した後は、それらを次のように簡単に操作できます。

write-host "`nSetting TextBox1 to 5"
$tb1.value = 5
write-host "Setting TextBox2 to 7"
$tb2.value = 7
write-host "Selecting 'Addition' operation"
$add.checked = $true

ここで、Web サーバーへのポストをトリガ (この場合はボタンをクリック) してサーバーからの応答を待つユーザー動作を、シミュレートする準備ができました。これはそれほど簡単ではありません。テスト対象アプリケーションの読み込みと同様に、次に示すような単純すぎるアプローチは、テスト自動化を一時停止する時間を事前に知る確実な方法がないため、効果的ではありません。

write-host "`nClicking 'Calculate' button"
$btn.click()
[System.Threading.Thread]::Sleep(5000)

いくつかの可能な解決策の 1 つは、Web アプリケーション上で何らかの要求前コントロール値を取得してから、HTTP 要求をトリガし、要求前コントロール値が変化するまで遅延ループを実行することです。たとえば、最初に TextBox3 コントロールの値を取り込んで保存しておきます。

$tb3 = $doc.getElementByID("TextBox3")
$old = $tb3.value

これで、ユーザーによる計算ボタンのクリックをシミュレートできます。

$btn.click()

その後、TextBox3 の値が変化するか、または特定の最大遅延回数を超えるまでの間、遅延ループに入ります。

$wait = $true
$numWaits = 0
while ($wait -and $numWaits -lt 100) {
  $numWaits++
  [System.Threading.Thread]::Sleep(50)
  $tb3 = $doc.getElementByID("TextBox3")
  if ($tb3.value -ne $old) {
    $wait = $false
  }
  else {
    write-host "Waiting for app to respond $numWaits . . ."
  }
}

遅延ループが終了した後、この終了がターゲット コントロール値の変化を検出するための最大試行回数の超過によることをチェックします。

if ($numWaits -eq 100) {
  throw "Application did not respond after 100 delays"
}
else {
  write-host "Application has responded"
}

ここまで、テスト対象の Web アプリケーションを正しく読み込み、要素を操作し、HTTP 要求をトリガして、サーバーからの応答があったことを確認しました。次に、Web アプリケーションの結果の状態をチェックして、結果が合格か不合格かを判断できます。まず、TextBox3 コントロールへの参照および値を取得します。

write-host "`nChecking for 12.0000"
$tb3 = $doc.getElementByID("TextBox3")
$ans = $tb3.value

HTTP 要求前に取得した参照は HTTP 要求後に失われているため、この時点までは TextBox3 への参照を取得できません。ここで、結果の値を確認し、テスト シナリオの結果を表示できます。

if ($ans -eq '12.0000') {
  write-host "Target value found"
  write-host "`nTest scenario: Pass" -foregroundcolor 'green'
}
else {
  write-host "Target value not found"
  write-host "`nTest scenario: *FAIL*" -foregroundcolor 'red'
}

main 関数の終わりに、trap ステートメントを使用して、テスト実行中にスローされた例外があれば処理します。

trap {
  write-host "Fatal error: " $_.exception.message
  -backgroundcolor red -foregroundcolor yellow
}

ここでは、単に例外メッセージを表示しています。場合によっては、continue ステートメントを使用して、致命的エラーが発生した場合でもテスト自動化の実行を強制的に継続させる必要があります。または、自動化を完全に終了する場合もあります。

もう一歩進む

これまではわかりやすいように、多くの値をハードコーディングしてきましたが、実稼動環境では自動化スクリプトのパラメータ化が必要になることも多くあります。たとえば、これまではテスト対象の Web アプリケーションのパスもハードコーディングしましたが、Windows PowerShell には、スクリプトにコマンド ライン引数を渡すための優れたメカニズムが備えられており、スクリプトの先頭に param($param1, $param2) などを追加することで、スクリプトにパラメータを追加できます。

また、テスト ケースの入力値および対応する期待値をパラメータ化することで、自動化スクリプトを拡張することもできます。Windows PowerShell には、外部のフラット テキスト ファイル、外部 XML ファイル、SQL データベース、または他のテスト ケース データ ストアからテスト ケース データを読み取る洗練された方法も用意されています。

同様に、この例のテスト自動化スクリプトでは、合格/不合格の結果をシェルに書き込んでいますが、Windows PowerShell では、結果を必要に応じて任意の種類のデータ ストアに簡単に保存できます。興味深い使用例の 1 つとして、テスト結果を Microsoft Team Foundation Server に書き込めば、優れたテスト管理能力が得られます。Team Foundation Server との統合の詳細については、MSDN Magazine 2008 リリース号の「テストの実行」コラム (msdn.microsoft.com/msdnmag/issues/08/LA/TestRun) を参照してください。

テスト自動化は、手動によるテストに置き換わるものではなく、それを補完するものです。それとほぼ同様に、Windows PowerShell による超軽量ソフトウェア テスト自動化は、他の種類のテスト自動化やテスト フレームワークに代わるものではなく、それらを補完するものです。たとえば、ここで紹介した UI テスト手法は Internet Explorer オブジェクト モデルを使用しているため、他の Web ブラウザまたはデバイスで動作する Web アプリケーションのテストには使用できません。そのような状況では、JavaScript アプローチ (2007 年 2 月号の「テストの実行」コラムの「AJAX テストの自動化」(msdn.microsoft.com/msdnmag/issues/07/02/TestRun) を参照) を採用するか、または市販のテスト フレームワークを使用できます。

ここで紹介した手法は、ある程度は、他のスクリプト言語でも使用できます。ただし、筆者の経験に基づくと、スクリプト ベースのテスト自動化に Windows PowerShell を使用することには、他のスクリプト環境と比べて有利な、小さいけれども重要な特長が 5 つあります。

1 つ目は、Windows PowerShell は COM オブジェクトと .NET Framework の両方に直接 (ラッパ メカニズムを介してではなく) アクセスできることです。

2 つ目は、Windows PowerShell の対話型モードによって、自動化スクリプトを開発しながらすばやくそれを試すことができ、スクリプトの作成プロセスが大幅にスピードアップすることです。

3 つ目は、Windows PowerShell に組み込まれた探索メカニズム (ドットタブ補完や get-member コマンドレットなど) により、実質的な仮想ドキュメント ヘルプ機能が提供されることです。

4 つ目は、Windows PowerShell に組み込まれたコマンドレットのコレクションにより、多くの平凡な自動化タスクが単純化されることです。

5 つ目は、個人的意見として、単純に Windows PowerShell は、より簡単でより直観的に使用できるということです。

以上の長所のそれぞれは大きなものではありませんが、それらが組み合わされることで、自動化を記述するためのコストに対して、Windows PowerShell でのテスト自動化によって得られる利益の比率は高いものになります。

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

Dr. James McCaffrey は Volt Information Sciences, Inc. に勤務し、ワシントン州レドモンドにあるマイクロソフト本社で働くソフトウェア エンジニアの技術トレーニングを管理しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes』の著者でもあります。James の連絡先は jmccaffrey@volt.com または v-jammc@microsoft.com (英語) です。


セキュリティに関するブリーフィング: Visual C++ の防御機能でコードを保護する

セキュリティに関するブリーフィング
Visual C++ の防御機能でコードを保護する
Michael Howard

コンテンツ
スタック ベースのバッファ オーバーランの検出 (/GS)
安全な例外処理 (/SafeSEH)
DEP との互換性 (/NXCompat)
イメージのランダム化 (/DynamicBase)
より安全な関数呼び出し
C++ の operator::new
失敗した場合の心構え

数多くのコードが C および C++ で作成されていますが、その多くには、多数の開発者が認識していないセキュリティ脆弱性が存在します。どの言語で作成したプログラムにもユーザーが攻撃を受ける余地のある脆弱性は存在する可能性がありますが、C および C++ 言語はインターネットの歴史において特殊な立場にあり、その普及に役立った機能が、同時に多くのセキュリティ脆弱性の原因にもなっています。それらの機能とは、コンピュータ ハードウェアへの無制限のアクセスおよびパフォーマンスです。バッファはメモリへの直接アクセスの一般的な例であるため、セキュリティや C または C++ の問題に関する資料には "バッファ" や "オーバーラン" という言葉が多く見られます。この種の直接アクセスはきわめて高速ですが、非常に危険でもあります。

実稼動環境の C および C++ のコードでバッファ オーバーランの発生が多い理由はいくつかあります。1 つの理由は、前述したように、これらの言語が脆弱なメモリに直接アクセスする機能を提供しているということです。2 番目の理由は、開発者によるバグです。3 番目の理由は、通常のコンパイラに防御機能が存在しないことです。最初の問題については対策の余地がありますが、それでは C および C++ が持つ言語の特性を活かすことはできなくなります。

開発者のバグは教育によってある程度解消することが可能ですが、そのような教育を推進している教育機関にお目にかかったことはありません。業界内でもセキュリティ教育は実施されていますが、業界関係者はこのソリューションや問題の当事者であるため、大学で学生に対するソフトウェア セキュリティの教育を充実させることが望まれます。「このような重要な課題をなぜ教育機関で教えようとしないのか」という疑問をお持ちかもしれませんが、正直なところ、筆者にも見当がつきません。ともかく残念なことです。

仮に優れた教育を施しても、セキュリティの問題は複雑で、いかに高度な教育を受けたエンジニアでもすべてを理解し尽すというわけにはいきません。人間は完全ではありません。

そこで、Microsoft Visual C++ チームはコンパイラの防御機能を充実させるという課題に取り組み、何年もかけて徐々に強化しています。我がセキュリティ チームもこの取り組みに協力しています。このコラムでは、Visual C++® 2005 以降で使用可能なバッファ オーバーラン防御機能の概要を説明します。他にもいくつか防御機能を備えたコンパイラは存在しますが、Visual C++ には gcc などのコンパイラと比べて 2 つの大きな利点があります。1 つは、すべての防御機能がツールセットに既定で含まれているということです。垢抜けないアドインをダウンロードする必要がありません。もう 1 つは、オプションを簡単に使用できるということです。

以下に Visual C++ ツールセットの防御機能を示します (順不同)。

  • スタック ベースのバッファ オーバーランの検出 (/GS)
  • 安全な例外処理 (/SafeSEH)
  • データ実行防止 (DEP) との互換性 (/NXCompat)
  • イメージのランダム化 (/DynamicBase)
  • 安全な関数呼び出しの自動使用
  • C++ の operator::new

各防御機能の詳細について説明する前に、これらの防御機能では安全でないコードは保護されないということを一言しておきます。最大限に安全なコードを作成することを常に心がける必要があります。安全なコードの作成方法がわからない場合は、優れた参考書を選んで参照してください。

Typical Stack Compared to One Compiled with /GS (画像を縮小するには、ここをクリックします)

Typical Stack Compared to One Compiled with /GS (画像を拡大するには、ここをクリックします)

また、各防御機能はすべてマイクロソフトのセキュリティ開発ライフサイクル (SDL) 要件に含まれているため、C および C++ コードの出荷時には SDL オプションを使用する必要があります。例外事項もありますが、比較的少ないので、その詳細の説明は省きます。

もう 1 点、重要な注意事項があります。それは、コードの内容によっては、これらの高度な防御機能が回避される可能性があるということです。コードで防御機能を多く使用するほど防御機能の回避は難しくなりますが、完全な防御は存在しません。攻撃の余地を軽減するには、必ず処理速度の低下が伴います。これについては先ほども警告しました。事態を変える唯一の方法は、より安全な関数呼び出しを行うことです。これにより本格的な防御を施し、脆弱性を解消することが可能です。それでは、各防御機能を詳しく見ていきましょう。

スタック ベースのバッファ オーバーランの検出 (/GS)

スタック ベースのバッファ オーバーラン検出は、Visual C++ で使用可能な最も古く、よく知られた防御機能です。/GS コンパイラ フラグの目的は単純で、悪意のあるコードの正常な実行を抑制することです。/GS オプションは Visual C++ 2003 以降では既定で有効になっており、実行時の一定の種類のスタック破壊を検出します。この処理では、関数のスタック上のリターン アドレスの直前に乱数を追加し、関数が戻ってきたときのエピローグ コードでこの値に変更がないことをチェックします。Cookie と呼ばれるこの値が変更されていれば、実行は停止します。

Cookie を設定する関数のプロローグ コードは次のようになります。

sub    esp, 8
mov    eax, DWORD PTR ___security_cookie
xor    eax, esp
mov    DWORD PTR __$ArrayPad$[esp+8], eax
mov    eax, DWORD PTR _input$[esp+4]

また、Cookie をチェックする関数のエピローグ コードは次のようになります。

mov    ecx, DWORD PTR __$ArrayPad$[esp+12]
add    esp, 4
xor    ecx, esp
call    @__security_check_cookie@4
add    esp, 8

Visual C++ 2005 では、スタック上のデータ移動も行って、データ破壊をさらに難しくします。たとえば、バッファを非バッファより上位のメモリに移動します。この手順は、スタック上の関数ポインタの保護などに役立つ場合があります。または、ポインタ引数およびバッファ引数を実行時に下位メモリに移動して、各種のバッファ オーバーラン攻撃を軽減することもできます。通常のスタックと /GS スタックを比較した次の図を参照してください。

/GS コンパイラ オプションは以下の場合には適用されません。

  • 関数にバッファが存在しない。
  • 最適化が有効になっていない。
  • 関数が変数引数リストを持つように定義されている。
  • 関数に naked キーワード (C++) がマークされている。
  • 関数の最初のステートメントにインライン アセンブリ コードが含まれている。
  • バッファが 8 バイト型ではなく、サイズが 4 バイト未満である。

Visual C++ 2005 SP1 には、/GS ヒューリスティックを強化して、より多くの関数を保護するオプションが追加されました。マイクロソフトがこの追加を行ったのは、スタック ベースのバッファ オーバーランが発生したコード内でわずかなセキュリティ情報が発行され、/GS を指定してコンパイルしたコードも Cookie で保護されなかったためです。この新しいオプションでは、保護される関数の数が大幅に増えています。

このオプションを使用するには、インターネットから取得したデータを処理するコードなど、モジュール内で保護を追加する箇所に次の 1 行を追加します。

#pragma strict_gs_check(on)

このプラグマは、マイクロソフトが常に /GS 機能を発展させていることを示すほんの一例です。Visual C++ 2003 の元のバージョンはごく単純でしたが、新しい攻撃や、既存の攻撃を防ぐ新しい方法に関する情報の入手に伴って、Visual C++ 2005 SP1 で更新され、さらに Visual C++ 2008 で更新されました。分析の結果、/GS では互換性またはパフォーマンス上の重大な問題は発生しません。

安全な例外処理 (/SafeSEH)

Internet Information Server (IIS) 4.0 に影響を及ぼした CodeRed ワームは、スタック ベースのバッファ オーバーランが原因で発生していました。ところが、/GS では、感染した関数のリターン アドレスがコードでオーバーランされなかったため、このワームによる悪用の問題は発生せず、スタック上の例外ハンドラが破壊されました。これは、この種のコンパイラ ベースの防御だけに頼らず、常に安全なコードを作成することに留意しなければならない理由を示す良い例です。

例外ハンドラは、ゼロによる除算などの例外状況が発生した場合に実行されるコードです。ハンドラのアドレスは関数のスタック フレーム上に保持されるため、破損する可能性があります。Visual Studio® 2003 以降のリンカには、コンパイル時にイメージの PE ヘッダーに有効な例外ハンドラのリストを保存するオプションがあります。実行時に例外が発生すると、オペレーティング システムがイメージ ヘッダーをチェックし、例外ハンドラのアドレスが正しいかどうかを確認します。アドレスが正しくない場合、アプリケーションは終了します。これにより、コードのリンク時に CodeRed のテクノロジが存在しても、その影響は阻止されます。例外が発生した場合を除き、/SafeSEH オプションによるパフォーマンスへの影響はないので、リンク時には常にこのオプションを指定することをお勧めします。

DEP との互換性 (/NXCompat)

最近製造された CPU はいずれも No Execute (NX: データ実行防止) 機能をサポートし、非コード ページを実行しません。この機能の影響について考えてみましょう。あらゆるバッファ オーバーランの脆弱性はデータ バグにあります。攻撃者はバッファ オーバーランを利用してデータをプロセスに挿入し、悪意のあるデータ バッファ内で実行を続けます。CPU がデータの実行を続ける意味が一体どこにあるのでしょうか。

/NXCompat オプションを使用してリンクすると、実行可能ファイルが CPU の No Execute 機能で保護されます。このオプションを指定したことが原因でマイクロソフトのセキュリティ チームが経験した互換性の問題はごくわずかで、パフォーマンスの低下も一切ありませんでした。

Windows Vista® SP1 にも実行中のプロセスの DEP を有効にする新しい API が追加されており、この機能は一度設定したら、解除できません。

SetProcessDEPPolicy(PROCESS_DEP_ENABLE);

イメージのランダム化 (/DynamicBase)

Windows Vista および Windows Server® 2008 はイメージのランダム化をサポートしています。これは、システムの起動時にオペレーティング システムのイメージをメモリ内で移動する機能です。この機能の目的は、単に攻撃者に予測されないようにすることにあります。この機能は ASLR (Address Space Layout Randomization) とも呼ばれます。ASLR を使用する場合には、DEP も有効にする必要があります。

既定では、Windows® はシステム コンポーネントのみを移動します。オペレーティング システムによってイメージが移動されるようにする場合は (強く推奨します)、リンク時に /DynamicBase オプションを指定する必要があります (このオプションは Visual Studio 2005 SP1 以降のツールセットで使用可能です)。/DynamicBase を指定してリンクすると、注目すべき副次効果が現れます。オペレーティング システムはスタックもランダム化するため、予測可能性が減少し、攻撃者がシステムに危害を加えることがはるかに難しくなります。Windows Vista および Windows Server 2008 ではヒープもランダム化されますが、この機能は既定で有効になっているため、コンパイルまたはリンク時に特にオプションを指定する必要はありません。

より安全な関数呼び出し

次のコード行を見てください。

void func(char *p) {
    char d[20];
    strcpy(d,p);
    // etc
}

信頼されていないデータが *p に格納されていると仮定すると、このコードにはセキュリティ脆弱性があります。このコードの問題は、コンパイラで strcpy の呼び出しをより安全な関数呼び出しに変換し、コピー操作をコピー先バッファのサイズに制限する余地が残っているという点にあります。理由は、バッファ サイズが静的で、コンパイル時に認識されているためです。

Visual C++ では、stdafx.h ヘッダー ファイルに次の 1 行を追加できます。

#define _CRT_SECURE_COPP_OVERLOAD_STANDARD_NAMES 1

これにより、コンパイラは初期の安全でない関数から次のコードを出力します。

void func(char *p) {
    char d[20];
    strcpy_s(d,__countof(d), p);
    // etc
}

ご覧のように、開発者が #define を追加するだけでコードは安全になりました。この Visual C++ の追加機能は、安全でない関数呼び出しの約 50% をより安全な呼び出しに自動的に変換できるため、筆者は愛用しています。

C++ の operator::new

また、Visual C++ 2005 以降では、operator::new の呼び出し時に整数オーバーフローを検出する防御機能が追加されています。まず、次のようなコードを作成します。

CFoo *p = new CFoo[count];

Visual C++ はこのコードをコンパイルして次のようなコードを生成します。

00004    33 c9           xor    ecx, ecx
00006    8b c6           mov    eax, esi
00008    ba 04 00 00 00  mov    edx, 4
0000d    f7 e2           mul    edx
0000f    0f 90 c1       seto    cl
00012    f7 d9           neg    ecx
00014    0b c8            or    ecx, eax
00016    51             push    ecx
00017    e8 00 00 00 00 call    ??2@YAPAXI@Z    ; operator new

割り当てるメモリ量の計算が完了すると (mul edx)、乗算後のオーバーフロー フラグの値に応じて CL レジスタの設定または未設定が決まるため、ECX は 0x00000000 または 0xFFFFFFFF になります。その次の処理 (ecx) により、ECX レジスタは 0xFFFFFFFF または EAX に保持された値 (初期乗算の結果値) のいずれかになります。この値が operator::new に渡されますが、2^N-1 の割り当てで失敗します。

この防御は自律的に行われます。コンパイラ スイッチはなく、コンパイラが自動的に処理します。

失敗した場合の心構え

そうでした。厄介な問題が残っていました。前述の防御機能のいずれかがトリガされた場合、アプリケーションが終了するという困った結果が生じます。これは望ましい結果とはいえませんが、攻撃者の悪意のあるコードが実行されるよりははるかにましです。

SDL では、無数の攻撃が存在する状態でコードの脆弱性が 100% 解消されていることを確認する術はないという理由から、新しいコードでこれらのすべての防御オプションを使用することを必須にしています。SDL の宣伝文句に、"コード エラーが発生したらどうしますか?" というフレーズがあります。現実には、"どうしますか?" の部分は "戦いなさい" と解釈してください。攻撃者のコードに楽をさせてはいけません。悪用を困難にする工夫が必要です。諦めないでください。

そのために、コンパイルには最新バージョンの C++ コンパイラを使用してより高機能な /GS を利用し、リンクには最新のリンカを使用して CPU およびオペレーティング システムの防御機能を活用してください。

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

Michael Howard は、マイクロソフトのプリンシパル セキュリティ プログラム マネージャとして、セキュリティを向上させるためのプロセスの変更とベスト プラクティスに取り組んでいます。また、『Writing Secure Code for Windows Vista』、『The Security Development Lifecycle』、『Writing Secure Code』、『19 Deadly Sins of Software Security』など、セキュリティに関する 5 冊の書籍を共同執筆しています。blogs.msdn.com/michael_howard で彼のブログをご覧いただけます。


すてきな ASP.NET: 必要な唯一のデータバインド コントロール

すてきな ASP.NET
必要な唯一のデータバインド コントロール
Fritz Onion

コードのダウンロード : : ExtremeASPNET2008_03.exe (192KB)
コンテンツ
ListView の基礎
ListView と CSS
ページネーション
並べ替え、編集、挿入、削除
グループ化
ListView を使用する

Visual Studio® 2008 に付属の ASP.NET 3.5 リリースには、新しいデータバインド コントロールである ListView が導入されています。皆さんが何を考えているのかはわかります。なぜ ASP.NET にこれ以上データバインド コントロールが必要なのかとお考えですね。なにしろ、現役を半分引退した DataGrid、変更された新しい GridView、相変わらず堅牢で単純な Repeater、独特で柔軟性のある DataList、便利な FormView とその若干冗長な兄弟である DetailsView など、データのコレクションを表示するときに選択できるコントロールは既に 10 個を超えているのですから。それにもちろん、BulletedList、ListBox、DropDownList、RadioButtonList、CheckBoxList といった 1 次元リスト コントロールもあります。

ListView は、ASP.NET の他のすべてのデータバインド コントロールを文字どおり置き換えることができます。そうです、ことごとく置き換えることができます。実際に、上記のリストにある他のすべてのコントロールを ListView コントロールで置き換えることができます。また ListView によって、CSS でのスタイル設定、柔軟なページネーション、および並べ替え、挿入、削除、更新機能の完全な補完などのいくつかのデータバインド タスクが、以前のコントロールを使用した場合よりもはるかに簡単になります。

まず ListView の一般的な使用モデルについて説明してから、コントロールの機能に進み、その柔軟性と能力を示します。このコラムを読み終えるまでには、ASP.NET ツールボックスに入れておくデータバインド コントロールの数を決定するのに十分な情報が得られるはずです。

ListView の基礎

ListView は、テンプレート駆動のコントロールであるため、既定では何もレンダリングしません。レンダリングする HTML をテンプレートの形式で完全に指定する必要があります。ほとんどのテンプレート コントロールと同様に、バインドされたデータ セットの各行について繰り返される HTML コンテンツを ItemTemplate に配置するため、ItemTemplate がほとんどの作業の中心になります。

ListView の新機能であり、他のコントロールとまったく異なる点は、LayoutTemplate の導入です。LayoutTemplate では、コントロール レンダリングの一部として出力する最上位レベルの HTML を定義します。たとえば、ListView をテーブルとしてレンダリングする場合は、図 1 に示すように、最上位レベルの <table> 要素と、場合によっては <thead> 要素を LayoutTemplate に含めて、行とセルのレンダリングを ItemTemplate に任せます (この場合は、ムービーのタイトルとリリース日を含む単純なテーブルを公開するデータ ソースにバインドされています)。図 2 に、ブラウザのレンダリングを示します。

Figure 1 Using LayoutTemplate and ItemTemplate (Close Figure 1)
<asp:ListView runat="server" ID="_simpleTableListView"
  DataSourceID="_moviesDataSource">
  <LayoutTemplate>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Title</th>
          <th>Release Date</th>
        </tr>
      </thead>
      <tbody>
        <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
      </tbody>
    </table>
  </LayoutTemplate>
  <ItemTemplate>
    <tr>
      <td><%# Eval("movie_id") %></td>
      <td><%# Eval("title") %></td>
      <td><%# Eval("release_date", "{0:d}") %></td>
    </tr>
  </ItemTemplate>
</asp:ListView>
Figure 2 テーブルに表示されるリスト
Figure 2 テーブルに表示されるリスト(画像を縮小するには、ここをクリックします)
Figure 2 テーブルに表示されるリスト
Figure 2 テーブルに表示されるリスト (画像を拡大するには、ここをクリックします)

LayoutTemplate と ItemTemplate の間の関連付けは、ID が itemPlaceholder に設定された LayoutTemplate 内の単一のサーバー側コントロールによって行われます (ListView の ItemPlaceholderID プロパティで ID 文字列を既定から変更できます)。最初の例では、ItemTemplate のコンテンツを挿入するテンプレート内の場所に PlaceHolder コントロールのインスタンスを配置する手法を使用しました。これは子コントロールをサポートする必要がありますが、プレースホルダとして使用するコントロールの種類に制限がないことに注意してください。重要なのは ID です。たとえば、PlaceHolder コントロールの代わりにサーバー側テーブル行を使用して LayoutTemplate を書いても、同じ結果を実現できます。

<LayoutTemplate>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Title</th>
        <th>Release Date</th>
      </tr>
    </thead>
    <tbody>
      <tr runat="server" ID="itemPlaceholder" />
    </tbody>
  </table>
</LayoutTemplate>

通常、筆者は 2 つの理由で汎用 PlaceHolder コントロールを使用することを好みます。最初の理由は、名前が適切に一致することです。さらに、このコントロールは自身の HTML をレンダリングせず、ItemTemplate のコンテンツで置換されるため、コントロール階層内の場所を確保しておく以外の目的を持たないコントロールを使用することがより論理的な選択であると思われます。

もちろん、ListView が非常に柔軟なのは、LayoutTemplate に挿入されるものを完全に制御できるという事実によります。テーブルの使用に限定されません。itemPlaceholder コントロールの場所に挿入されたときに ItemTemplate のコンテンツが意味をなす限り、レンダリングする任意の HTML を LayoutTemplate に配置できます。次に、同じムービー データ ソースにバインドされる ListView の例を示します。ただし今回はテーブルではなく、ムービーがタイトルとリリース日を示す箇条書きリストに表示されます (結果のリストを図 3 に示します)。

Figure 3 形式の異なる同じリスト
Figure 3 形式の異なる同じリスト(画像を縮小するには、ここをクリックします)
Figure 3 形式の異なる同じリスト
Figure 3 形式の異なる同じリスト (画像を拡大するには、ここをクリックします)

   <asp:ListView runat="server"
     ID="_simpleTableListView"
     DataSourceID="_moviesDataSource">
     <LayoutTemplate>
       <ul>
         <asp:PlaceHolder runat="server"
              ID="itemPlaceholder" />
       </ul>
     </LayoutTemplate>
     <ItemTemplate>
       <li><%# Eval("title") %>,
           <%#Eval("release_date","{0:d}") %></li>
     </ItemTemplate>
                         </asp:ListView>

ListView と CSS

ASP.NET 開発者は長い間、CSS 駆動サイトを作成する場合に個々のコントロールに支配されてきました。既定のコントロールの多くは、インライン スタイルをレンダリングしているか、CSS クラスと HTML 出力の一部との関連付けを困難にしています。マイクロソフトでは、2006 年 4 月に、この問題に対処するため、完全に CSS 駆動だったいくつかのコントロール (GridView を含む) の代替レンダリング メカニズムを提供する CSS Control Adapter Toolkit というツールキットをリリースしました (詳細については、msdn.microsoft.com/msdnmag/issues/06/10/ExtremeASPNET にある 2006 年 10 月の「すてきな ASP.NET」のコラムを参照してください)。ただし、これらの代替レンダリングは完全リリース版には組み込まれなかったため、別途インストールが必要であり、デザイナ サポートがありません。

ListView は、スタイルシートを適用する場所と場合を完全に制御できるようにすることで、CSS をサイトで簡単に利用できるようにしています。非常に一般的な 1 つのシナリオは、通常は HTML と CSS から構成される特定のページの設定済みデザインを渡される開発者に関するものです。GridView クラスが提供する、結果として生成される HTML を変更するフックのセットは限られているため、テーブルの特定のデザインをレンダリングするために従来の GridView を使用する場合は、正しく行うのが常に厄介です。

筆者は、多くの開発者がグリッドにスタイル属性を適用し、ページのソースを表示してスタイルが配置される場所を正確に理解し、グリッドが目的どおりにレンダリングされるまで繰り返すという試行錯誤の手段をとっているのを見てきました。ListView では、レイアウトとコンテンツについて開発者が責任を持つようになったため、当て推量は不要です。

たとえば、図 4 に示すような外観を必要とし、図 5 に示すような .htm ファイルと .css ファイルから構成されるデザインのテーブルがあるとします。

Figure 5 HTML and CSS for the Table (Close Figure 5)

HTML
<div class="PrettyGrid">
  <table cellpadding="0" cellspacing="0" summary="">
    <thead>
      <tr>
        <th scope="col"><a href="http://.">ID</a></th>
        <th scope="col"><a href="http://.">Title</a></th>
        <th scope="col"><a href="http://.">Release date</a></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>Where the Wild Things Are</td>
        <td>12/15/2008</td>
      </tr>
  <!-- ... -->
    </tbody>
  </table>
  <div class="Pagination">
    <span>1</span>
    <a href="http://.">2</a>
    <a href="http://.">3</a>
  </div>
</div>
CSS
.PrettyGrid
{
  width: 100%;
}

.PrettyGrid div.Pagination,
.PrettyGrid div.Pagination a,
.PrettyGrid div.Pagination span
{
  color: #00FFFF;
  background: #284775;
  font-weight: normal;
  padding: 2px;
}
.PrettyGrid table
{
  border: solid 1px #CCCCCC;
  width: 100%;
}
/*...*/
Figure 4 テーブルのターゲット デザイン
Figure 4 テーブルのターゲット デザイン(画像を縮小するには、ここをクリックします)
Figure 4 テーブルのターゲット デザイン
Figure 4 テーブルのターゲット デザイン (画像を拡大するには、ここをクリックします)

図 6 に示すように、HTML の適切なセクションを取得し、対応するテンプレート内に配置することで、HTML/CSS の組み合わせが必要とするとおりに厳密にレンダリングする ListView をすばやく構築できます。最終結果の外観は、CSS で完全にスタイル設定された元の HTML とまったく同じになります。HTML または対応する CSS を更新するだけで、簡単にデザインに変更を加えることもできます。

Figure 6 ListView to Construct the Table (Close Figure 6)
<asp:ListView ID="_moviesGrid" runat="server" DataKeyNames="movie_id"
  DataSourceID="_moviesDataSource">
  <LayoutTemplate>
    <div class="PrettyGrid">
      <table cellpadding="0" cellspacing="0" summary="">
        <thead>
          <tr>
            <th scope="col"><a href="http://.">ID</a ></th>
            <th scope="col"><a href="http://.">Title</a></th>
            <th scope="col"><a href="http://.">Release date</a></th>
          </tr>
        </thead>
        <tbody>
          <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
        </tbody>
      </table>
      <div class="Pagination">
        <span>1</span>
        <a href="http://.">2</a>
        <a href="http://.">3</a>
      </div>
    </div>
  </LayoutTemplate>

  <AlternatingItemTemplate>
    <tr class="Alternate">
      <td><asp:Label ID="movie_idLabel" runat="server"
        Text='<%# Eval("movie_id") %>' /></td>
      <td><asp:Label ID="titleLabel" runat="server"
        Text='<%# Eval("title") %>' /></td>
      <td><asp:Label ID="release_dateLabel" runat="server"
        Text='<%# Eval("release_date", "{0:d}") %>' />  </td>
    </tr>
  </AlternatingItemTemplate>

  <ItemTemplate>
    <tr>
      <td><asp:Label ID="movie_idLabel" runat="server"
        Text='<%# Eval("movie_id") %>' /></td>
      <td><asp:Label ID="titleLabel" runat="server"
        Text='<%# Eval("title") %>' /></td>
      <td><asp:Label ID="release_dateLabel" runat="server"
        Text='<%# Eval("release_date", "{0:d}") %>' /> </td>
    </tr>
  </ItemTemplate>
</asp:ListView>

ページネーション

前のセクションで開始した元の HTML デザインには、デザインによってページネーションと並べ替えの両方が暗黙に指定されていたため、仕様に基づいてこのグリッドを完全に実装するタスクはまだ完了していません。最初にページネーションについて説明し、次に並べ替えについて説明します。

ListView でのページネーションは、別の新しいコントロールである DataPager を導入することにより行われます。ページングを別のコントロールに分離することで、DataPager は ListView がデータの表示に使用する機能からページング UI を分離します。したがって、ページング UI はページ上のほぼどこにでも配置でき、DataPager コントロールはいくつでも作成できます。複数のページャ コントロールの一般的な応用の 1 つは、ユーザーがグリッドをスクロールしなくても次のページに移動できるように (DataPager ではこれを簡単に行うことができます)、データのグリッドの上部と下部の両方にページネーション インターフェイスを提供することです。

前のセクションの ListView の例にページネーションを実装することから始めましょう。ListView に関連付けられる DataPager コントロールを作成する最も単純な方法は、実際に DataPager コントロールを ListView の LayoutTemplate に埋め込むことです。

<asp:ListView ID="_moviesGrid"
  runat="server" DataKeyNames="movie_id"
  DataSourceID="_moviesDataSource">
  <LayoutTemplate>
    <!-- ... -->
    <div class="Pagination">
      <asp:DataPager ID="_moviesGridDataPager" runat="server">
        <Fields>
          <asp:NumericPagerField />
        </Fields>
      </asp:DataPager>
    </div>
  </LayoutTemplate>
</asp:ListView>

DataPager を ListView 内の LayoutTemplate に埋め込むことで、これらが暗黙的に関連付けられます。別のオプションは、ページ上の他の場所に DataPager を配置し、PagedControlID を関連する ListView の ID に設定することで ListView に関連付けることです。

この場合は、NumericPagerField が目的どおりのインターフェイスを表示します。つまり、ページをナビゲートするためのハイパーリンクとして表示される一連の数字です。DataPager では、3 種類のフィールドがサポートされます。

  • NumericPagerField は 1 2 3... のページネーション インターフェイスを表示します。
  • NextPreviousPagerField は行を繰り返し処理するために Next、Previous、First、Last の各ボタンを表示します。
  • TemplatePagerField では、PagerTemplate を使用してページネーション インターフェイスの厳密なデザインと機能を定義します。

DataPager コントロールは、一般に、IPageableItemContainer インターフェイス (現在、ListView はこのインターフェイスを実装する唯一のコントロールです) などを実装するコントロールのページング サポートを提供するために実装されます。

public interface IPageableItemContainer
{
    event EventHandler<PageEventArgs> TotalRowCountAvailable;
    void SetPageProperties(int startRowIndex, int maximumRows,
                           bool databind);
    int MaximumRows { get; }
    int StartRowIndex { get; }
}

図 7 に、ListView、DataPager、および関連 DataSource コントロール間の関係を示します。DataPager は、ListView の設定に使用される DataSource と直接には対話せず、このインターフェイスを通じて必要なデータをクエリします。

Figure 7 ListView、DataPager、および DataSource 間の関係
Figure 7 ListView、DataPager、および DataSource 間の関係(画像を縮小するには、ここをクリックします)
Figure 7 ListView、DataPager、および DataSource 間の関係
Figure 7 ListView、DataPager、および DataSource 間の関係 (画像を拡大するには、ここをクリックします)

ページネーションの準備で最初に行う必要があるのは、ページングをサポートしているかどうか、サポートしている場合には行の総数を返せるかどうかを ListView が DataSource にクエリすることです。可能な場合、ListView はデータ ソース内の行の総数を取得し、IPageableItemContainer インターフェイスの一部として実装されている TotalRowCountAvailable イベントを起動します。関連する DataPager コントロールはこのイベントをサブスクライブし、行の総数を使用して、ページネーション インターフェイスの表示に必要なフィールドを初期化します。DataPager は、ListView の SetPageProperties メソッドを呼び出して、初期の行インデックスおよび返す最大行数を設定します。

ListView は、関連データ ソースからデータを取得するときに、DataPager によって設定された値に基づいて行のサブセットのみ要求します。DataPager は、現在のページ インデックスを (通常はユーザー操作を理由として) 変更するたびに、ListView の SetPageProperties を再び呼び出して、取得する行の現在のサブセットを反映します。DataPager コントロールの PageSize プロパティを設定することで、ページに表示されるレコード数を変更できます。これは、対応する ListView に設定する最大行数情報に影響します。

DataPager では、DataPager の動作を大幅に変更する QueryStringField プロパティもサポートされます。QueryStringField プロパティをなんらかの文字列 (pageNum など) に設定することで、ユーザーがページ番号をクリックしたときに DataPager が HTTP GET 要求を発行するように要求します。要求されたページ番号は、従来の POST バック モデルの代わりに、割り当てた文字列を使用してクエリ文字列パラメータを通じて送信されます。

この変更の有益な副次的効果は、ページ番号が URL に表示されるため、クライアントが、データバインドされた ListView コントロールに特定のページへのブックマークを作成できることです。この GET モデルの通信に切り替えた場合、ASP.NET AJAX UpdatePanel コントロールで使用されている POST バック フック メカニズムは、ページネーション要求をインターセプトして非同期ポストバックにすることができませんので注意してください。

<asp:DataPager ID="_moviesGridDataPager" runat="server"
  QueryStringField="pageNum" >
  <Fields>
    <asp:NumericPagerField />
  </Fields>
</asp:DataPager>

DataPager はデータの実際のページネーションの実行を ListView に完全に依存し、ListView は関連する DataSource コントロールに依存するため、他のデータバインド コントロールで行う場合と同じページングの制約があります。たとえば、DataSet モードに設定されている場合、ページングは SqlDataSource コントロールでのみ動作します。したがって、ページネーションを実行するために結果セット全体がメモリに読み込まれます。もちろん、カスタム DataSource コントロールまたは ObjectDataSource コントロールを使用してカスタム ページングを実装できます。

並べ替え、編集、挿入、削除

ListView は、並べ替え、および作成、読み取り、更新、削除 (CRUD) 操作の完全な補完をサポートする機能がなければ完全ではありません。これらの各コマンドの実装は、FormView コントロールがコマンドを実装する方法に似ています。

ListView は完全にテンプレート駆動であるため、CommandName プロパティが 7 つの特定のコマンド文字列のいずれかに設定されているテンプレート内の特定のボタンを認識します。それらのコマンド文字列は、Cancel、Delete、Select、Edit、Insert、Update、および Sort です。各コマンドは、ListView に対して対応するアクションを開始するため、ListView に並べ替えを追加する場合は、CommandName プロパティを Sort に設定し、CommandArgument をデータ ソースの並べ替え基準にする列の名前に設定して、LayoutTemplate 内にボタンを配置する必要があります (図 8 の例では LinkButton を使用しています)。図 8 では、グリッド内の各列の以前の静的ヘッダー リンクを、列に基づいてデータを並べ替えるよう ListView に要求するクリック可能なリンクに変更しました。

Figure 8 Sorting in ListView (Close Figure 8)
<asp:ListView ID="_moviesGrid" runat="server" DataKeyNames="movie_id"
  DataSourceID="_moviesDataSource">
  <LayoutTemplate>
    <div class="PrettyGrid">
      <table cellpadding="0" cellspacing="0" summary="">
        <thead>
          <tr>
            <th scope="col">
              <asp:LinkButton ID="_movieIdSortLink"
                CommandName="Sort" CommandArgument="movie_id"
                runat="server">ID</asp:LinkButton>
            </th>
            <th scope="col">
              <asp:LinkButton ID="_titleSortLink"
                CommandName="Sort" CommandArgument="title"
                runat="server">Title</asp:LinkButton>
            </th>
            <th scope="col">
              <asp:LinkButton ID="_releaseDateSortLink"
                CommandName="Sort" CommandArgument="release_date"
                runat="server">Release date</asp:LinkButton>
            </th>
          </tr>
        </thead>
    <!-- ... -->
  </LayoutTemplate>
</asp:ListView>

同様に、編集モードの開始、行の削除、またはデータ セットへの新しい行の挿入を行うコマンド ボタンを追加できます。その詳細は、基本的に他のテンプレートベースのデータバインド コントロール (FormView や GridView など) と同じであるため、ここには示しません。

グループ化

ListView の最後の主要機能は、DataList コントロールが提供しているのとよく似た、サブセットへのデータのグループ化機能です。DataList は、レンダリングされたテーブルの各セルに 1 行のデータをレンダリングする表形式コントロールです。RepeatColumns プロパティを設定して、単一のテーブル行にグループ化される基礎となるデータセットの行数を制御できます。

ListView はテーブルとしてのレンダリングに制約されないため、まとめてレンダリングするアイテムのグループを指定する、より汎用的な方法が必要です。これを GroupTemplate が行います。図 9 に、ListView 内の LayoutTemplate、GroupTemplate、ItemTemplate の各要素間の関係を示します。GroupTemplate では、基礎となるデータセット内の n 要素ごとに周囲の HTML を指定します。n は、ListView の GroupItemCount プロパティで指定されます。

Figure 9 ListView 内のテンプレート
Figure 9 ListView 内のテンプレート(画像を縮小するには、ここをクリックします)
Figure 9 ListView 内のテンプレート
Figure 9 ListView 内のテンプレート (画像を拡大するには、ここをクリックします)

ListView 内で GroupTemplate を使用する場合は、LayoutTemplate 内の itemPlaceholder の ID でコントロールを指定しません。このコントロールは、GroupTemplate にある必要があります。代わりに、LayoutTemplate 内の groupPlaceholder の ID でコントロールを指定して (ListView の GroupPlaceholderID プロパティを設定してコントロール ID を変更できます)、基礎となるデータセット内に見つかった n アイテムごとに GroupTemplate のコンテンツを挿入する必要がある場所を記述します。

たとえば、図 10 の ListView は、GroupTemplate を定義して行を説明し、ItemTemplate を残してセルのみレイアウトすることにより、テーブルの各行にデータベースの 4 つのムービーを表示する方法を示しています。その結果を図 11 に示します。

Figure 10 Defining Rows with GroupTemplate (Close Figure 10)
<asp:ListView ID="_groupListView" runat="server"
  DataKeyNames="movie_id" DataSourceID="_moviesDataSource"
  GroupItemCount="4" >
  <GroupTemplate>
    <tr>
      <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
    </tr>
  </GroupTemplate>
  <LayoutTemplate>
    <table>
      <asp:PlaceHolder ID="groupPlaceholder" runat="server" />
    </table>
  </LayoutTemplate>
  <ItemTemplate>
    <td>
      movie_id:
      <asp:Label ID="_movie_idLabel" runat="server"
        Text='<%# Eval("movie_id") %>' /> <br />
      title:
      <asp:Label ID="_titleLabel" runat="server"
        Text='<%# Eval("title") %>' /> <br />
      release_date:
      <asp:Label ID="_release_dateLabel" runat="server"
        Text='<%# Eval("release_date", "{0:d}") %>' /> <br />
      <br />
    </td>
  </ItemTemplate>
</asp:ListView>
Figure 11 結果として生成される Web ページ内の GroupTemplate 行
Figure 11 結果として生成される Web ページ内の GroupTemplate 行(画像を縮小するには、ここをクリックします)
Figure 11 結果として生成される Web ページ内の GroupTemplate 行
Figure 11 結果として生成される Web ページ内の GroupTemplate 行 (画像を拡大するには、ここをクリックします)

これは、DataList で行えることと非常によく似ていますが、ListView を操作するため、グリッド レンダリングで以前に行っていたのと同じぐらい簡単にページネーションと並べ替えの両方を追加できます。これは、DataList では困難なタスクです。この記事のコード ダウンロードには、参照のためにページネーションと並べ替えの両方を実装する例が含まれています。

ListView を使用する

Visual Studio 2008 のデザイナを使用して、ListView コントロールの実験を開始できます。グリッド、タイル、箇条書きリスト、フロー、および単一行という 5 つの異なるレイアウトから選択できます。使用できるさまざまなレイアウト オプションはすぐにわかりますが、ListView の真の能力はレンダリングする HTML の制御にあるため、実際のプロジェクトでは LayoutTemplate の構築に時間を費やす可能性が高いでしょう。将来、データバインドの機会があるたびに ListView を使用することになるでしょうか。それは少々極端かもしれませんが、それも可能性としてあることを知っておくとよいでしょう。筆者はこの柔軟なデータバインド コントロールに手を伸ばすことが多くなりそうです。

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

Fritz Onion は、Web 開発のカリキュラムを提供している Microsoft .NET のトレーニング プロバイダである Pluralsight の共同創立者です。また、『Essential ASP.NET』と『Essential ASP.NET 2.0』の著者でもあります。連絡先は pluralsight.com/fritz です。


基礎: ベクタ グラフィックスと WPF の Shape クラス

基礎
ベクタ グラフィックスと WPF の Shape クラス
Charles Petzold

コードのダウンロード : : Foundations2008_03.exe (192KB)
コンテンツ
Shapes 名前空間の役割
Pen について
2 つの描画パラダイム
3 つの取得専用プロパティ
平行な線と曲線
ヒープの割り当てを最小限にする
効果的なパス拡張の方法
その他のパラダイム

Windows® Presentation Foundation (WPF) では、比較的単調な 2 次元ベクタ グラフィックスを扱う場合でさえ、プログラマは新しい概念を数多く習得しなければなりません。WPF では、グラフィカル オブジェクトに、コントロールとほぼ同等の "地位" が与えられています。つまり、レイアウトの一部として配置したり、マウス、キーボード、またはスタイラスからの入力を渡したりすることができます。これらのグラフィカル オブジェクトはグラフィカル システムによって管理されるため、従来のグラフィックスのように頻繁に再描画する必要はありません。しかも、動きを与えたり、データ バインドのターゲットとして使用することも可能です。

当初、WPF についていろいろ調べ始めたとき、私は System.Windows.Shapes を "基本的" なグラフィックス クラスが集まった名前空間として認識していました。これらのクラスは、単純に線や四角形を表示するのには適しているが、高度な WPF プログラムでは、OnRender メソッドをオーバーライドし、DrawingContext クラスのメソッドを呼び出すのが正攻法だと考えたのです。

特に私の興味を引いたのが DrawGeometry メソッドです。WPF の Geometry オブジェクトは、連続または不連続の直線、弧、およびベジエ曲線を組み合わせたものです。従来のグラフィックス プログラミングでは "パス" と呼ばれていました。DrawGeometry には 3 つの引数があります。Geometry オブジェクト、ジオメトリの線や曲線を引くための Pen、閉じた領域を塗りつぶすための Brush です。

Shapes 名前空間の役割

WPF のベクタ グラフィックスに対して私が最初に抱いた印象は、後になって間違いであったことに気付きました。通常、WPF プログラムで OnRender メソッドをオーバーライドし、DrawingContext クラスのメソッドを呼び出す必要はありません。OnRender をオーバーライドすることは、よい練習になるとは思いますが、一般的なアプリケーションでは不要です。

そのような経緯から、私は、WPF で 2 次元ベクタ グラフィックスをレンダリングするための名前空間として System.Windows.Shapes を使用することにしました。System.Windows.Shapes 名前空間は、抽象クラスである Shape のほか、シールされたクラスである Line、Polyline、Polygon、Path、Rectangle、および Ellipse で構成されます。

Shape クラスそのものは、FrameworkElement から派生しています。Shape の派生クラスとして最も重要なのは明らかに Path クラスです。Path クラスは、DrawingContext の DrawGeometry メソッドと同等の機能を備えているにもかかわらず、その手間ははるかに少なくて済みます。XAML で Path クラスを使用すると、コード化された一連の描画コマンドで Geometry オブジェクトを定義することもできます。

Shapes 名前空間にあるクラスが、すべてのアプリケーションに対する汎用的なベクタ グラフィックス ソリューションであると言っているわけではありません。Shapes 名前空間に存在する各クラスのインスタンスは、それぞれが本格的な WPF 要素であり、必要以上のオーバーヘッドが伴うことも考えられます。加えて、これらのクラスには、ストローク ペンとフィル ブラシがそれぞれ 1 つあるだけで、必要な色数が満たせない可能性もあります。

複数の色を使って複雑なベクタ画像をレンダリングする場合は、いくつかの選択肢があります。もちろん、複数の Path オブジェクトを作成することもできますが、複雑な画像をひとまとめにして扱う必要がある場合には、過剰な手間がかかってしまいます。この場合、より優れた解決方法として、DrawingGroup クラスの使用が挙げられます。このクラスは、複数の GeometryDrawing オブジェクト (それぞれが Geometry で構成される)、ストローク ペン、およびフィル ブラシを持つことができます。この DrawingGroup オブジェクトは、おそらく、WPF において、従来のグラフィックス メタファイルに最も近い存在です。DrawingGroup オブジェクトを (DrawingBrush を介して) ブラシの基礎として利用できるほか、DrawingImage に変換して Image クラスで表示することもできます。

グラフィックス プリミティブの数がさほど多くないときは (これらのオブジェクトにマウス、キーボード、またはスタイラスからの入力を渡したり、独自の変換を適用したりする必要がある場合は特に)、Shapes 名前空間のクラスを使用するのが理想的です。

以降は、Shapes 名前空間で唯一シールされていない Shape クラスの派生について説明していきます。Shape から派生することによって、独自のカスタム ベクタ グラフィックス プリミティブを実装できます。カスタム プリミティブで WPF レイアウト システムのプロトコルを有効活用するための最も簡単な方法は、Shape から派生することです。

Pen について

Shape のソース コードを実際に見たことはありませんが、一般的な原則から、このクラスに関して、いくつかわかっていることがあります。Shape は FrameworkElement から派生されているため、MeasureOverride、ArrangeOverride、OnRender の各メソッドをオーバーライドできます。OnRender のオーバーライドはとても簡単です。単に、DrawingContext の DrawGeometry メソッドを呼び出して、そこに Brush、Pen、Geometry の各オブジェクトを渡すだけです。

DrawGeometry を呼び出すときに指定する Brush 引数が、Shape の Fill プロパティから来ていることは間違いありませんが、Shape に Pen 型のプロパティは定義されていません。その代わり、Shape には、内部的に Pen オブジェクトを構築する 9 つのプロパティが別途定義されています。9 つのプロパティの名前は、すべて "Stroke" で始まります。実は、これらのプロパティのおかげで、XAML においてもコードにおいても、Shape を派生させることがきわめて容易になっています。

たとえば、EllipseGeometry を含んだ GeometryDrawing オブジェクトを XAML で定義した場合、次のようになります。

<GeometryDrawing Brush="Red">
    <GeometryDrawing.Geometry>
        <EllipseGeometry ... />
    </GeometryDrawing.Geometry>
    <GeometryDrawing.Pen>
        <Pen Brush="Blue" Thickness="3" />
    </GeometryDrawing.Pen>
</GeometryDrawing>

Pen オブジェクトはプロパティ要素内に定義する必要がある点に注意してください。また、このマークアップでさえ、GeometryDrawing を実際にどのようにレンダリングするかは指定されていません。ただし、Ellipse クラスであれば、これに相当する XAML を次のように記述できます。

<Ellipse Fill="Red" Stroke="Blue" StrokeThickness="3" ... />

Shape クラスが DrawingContext の DrawGeometry メソッドを呼び出す場合は、Geometry オブジェクトも必要です。このコラムの残りの部分では、この Geometry オブジェクトを中心に説明していきます。

2 つの描画パラダイム

Shape クラスの最もわかりにくい側面の 1 つに、2 種類の描画パラダイムをカプセル化する機能があります。

1 つ目のパラダイムは比較的従来の考え方に近いものです。ここでは、それを "座標" パラダイムと呼ぶことにしましょう。Line、Polyline、Polygon、Path の各クラスを使用する場合は、次のように、図を構成する直線と曲線を定義する実際の座標点を指定します。

Line line = new Line();
line.X1 = 100;
line.Y1 = 50;
line.X2 = 200;
line.Y2 = 150;
line.Stroke = Brushes.Blue;
line.StrokeThickness = 12;

その結果を図 1 に示します。Line 要素の最も一般的な親はおそらく Canvas パネルです。ただし、実際には、他のコントロールや要素を配置できる場所であれば、Line はどこに配置してもかまいません。図 1 のプログラムでは、Line をウィンドウの Content プロパティに設定しています。

Figure 1 ウィンドウ内の Line 要素
Figure 1 ウィンドウ内の Line 要素

Line、Polyline、Polygon、Path の各クラスは Shape の Stretch プロパティを継承しており、既定値は Stretch.None に設定されています。このプロパティを Stretch.Fill に設定した場合、図 2 のように、Line が親のサイズに合わせて伸長されます。

Figure 2 Stretch が Fill に設定された Line 要素
Figure 2 Stretch が Fill に設定された Line 要素

Shape には、これとは異なる描画パラダイムも実装されています。こちらの方が、より WPF らしいレンダリングかと思います。これを "自動サイズ調整" パラダイムと呼ぶことにします。このパラダイムは、Rectangle クラスと Ellipse クラスで実現されています。図 3 は、ウィンドウの Content プロパティに設定された Ellipse 要素を示しています。この Ellipse 要素は次のようにして作成されています。

Figure 3 ウィンドウ内の Ellipse 要素
Figure 3 ウィンドウ内の Ellipse 要素

   Ellipse elips = new Ellipse();
   elips.Fill = Brushes.Red;
   elips.Stroke = Brushes.Blue;
   elips.StrokeThickness = 12;

Ellipse 要素には座標やサイズ情報は一切与えられていません。この図では、特に何もしなくても、親の内部が塗りつぶされます。Rectangle および Ellipse では、Stretch プロパティの既定値が Stretch.Fill に設定されているためです。このプロパティを Stretch.None に設定した場合、楕円が小さな点になり、かろうじて枠線だけが見える状態になります (図 4)。

Figure 4 Stretch が None に設定された Ellipse 要素
Figure 4 Stretch が None に設定された Ellipse 要素

この効果は、Rectangle または Ellipse の HorizontalAlignment プロパティと VerticalAlignment プロパティを Stretch 以外の値に設定することでも表現できます (このコラムのダウンロード可能なコードは、StretchExplore という名前が付いています。このプログラムでは、これから Shape を派生することによって作成する 2 つのクラスのほか、Line および Ellipse に対する各種の伸縮オプションを確認できます)。

Rectangle 要素または Ellipse 要素を特定のサイズにする必要がある場合は、FrameworkElement で定義されている Width プロパティと Height プロパティを使用するか、MinWidth、MaxWidth、MinHeight、および MaxHeight を設定することによって一連の値を指定します。Rectangle または Ellipse を Canvas パネル上にレンダリングする場合は、これらのプロパティを設定する必要があります。

Shape からカスタム グラフィックス プリミティブを派生する場合は、"自動サイズ調整" パラダイムよりも、"座標" パラダイムによる実装の方が簡単です。

3 つの取得専用プロパティ

Shape には、その派生クラスで継承可能な 14 個のプロパティが定義されています。そのうち 11 個については既に触れました。ペンに関連した 9 つのプロパティ (名前が Stroke で始まるもの)、Fill プロパティ、および Stretch です。他の 3 つのプロパティには get アクセサしか定義されていません。DefiningGeometry (protected abstract)、GeometryTransform (public virtual)、RenderedGeometry (public virtual) の 3 つです。

"座標" パラダイムで Shape を派生させてグラフィックス プリミティブを実装する場合、オーバーライドする必要があるプロパティは DefiningGeometry だけです。その名前からもわかるように、DefiningGeometry は、目的のグラフィックス プリミティブを定義する Geometry 型のオブジェクトを返すことによって実装します。Shape の派生クラスには、グラフィックスを定義するプロパティが別途存在しますが、それと非常によく似ています。ほとんどの場合、データ バインドおよびアニメーションのターゲットとすることができるように、これらのプロパティを依存関係プロパティでサポートする必要があります。

DefiningGeometry はさまざまな状況で呼び出されますが、特にプリミティブに影響を及ぼすようなプロパティが変化したときは必ず呼び出されます。そのため、DefiningGeometry を実装する際は、ヒープ メモリの定期的な割り当てを避けることが重要です。DefiningGeometry を呼び出すたびにヒープの割り当てが生じた場合、最終的には、Microsoft® .NET Framework のガベージ コレクタがそれを処理しなければならなくなります。DefiningGeometry のコードには、できるだけ、クラスをインスタンス化するようなコードが含まれないように配慮してください。また、特定のメソッドに伴う暗黙的なヒープ割り当てにも注意が必要です。次に、Shape の派生でヒープの割り当てを回避するための技法をいくつか紹介します。

最初に、単純な例を取り上げます。開始座標点と終了座標点を (double オブジェクトのペアではなく) Point オブジェクトで設定できる、Line プリミティブに代わるクラスを定義するとします。図 5 は、Shape から派生する PointLine クラスを示しています。

Figure 5 PointLine.cs—the PointLine Class (Close Figure 5)
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Petzold.Shapes
{
    public class PointLine : Shape
    {
        LineGeometry linegeo = new LineGeometry();

        // Dependency properties
        public static readonly DependencyProperty StartPointProperty =
            LineGeometry.StartPointProperty.AddOwner(
                typeof(PointLine),
                new FrameworkPropertyMetadata(new Point(0, 0),
                    FrameworkPropertyMetadataOptions.AffectsMeasure));

        public static readonly DependencyProperty EndPointProperty =
            LineGeometry.EndPointProperty.AddOwner(
                typeof(PointLine),
                new FrameworkPropertyMetadata(new Point(0, 0),
                    FrameworkPropertyMetadataOptions.AffectsMeasure));

        public Point StartPoint
        {
            set { SetValue(StartPointProperty, value); }
            get { return (Point)GetValue(StartPointProperty); }
        }

        public Point EndPoint
        {
            set { SetValue(EndPointProperty, value); }
            get { return (Point)GetValue(EndPointProperty); }
        }

        // Required DefiningGeometry override
        protected override Geometry DefiningGeometry
        {
            get
            {
                linegeo.StartPoint = StartPoint;
                linegeo.EndPoint = EndPoint;
                return linegeo;
            }
        }
    }
}

StartPoint と EndPoint は、どちらも依存関係プロパティで定義されています。PointLine クラスをこれらのプロパティの新しい所有者として追加するだけで済むように、プロパティ名には、LineGeometry クラス内の対応するプロパティと同じ名前を使用しました。どちらのプロパティも要素のサイズに影響するため、AffectsMeasure フラグが設定されています。いずれかのプロパティが変化した場合、Shape に実装された OnRender を呼び出す新しいレイアウト パスが発生します。図形の外観だけに影響し、サイズには影響しないプロパティを定義する場合は、代わりに AffectsRender フラグを使用することでレイアウト パスの実行を回避できます。

DefiningGeometry プロパティの get アクセサは、StartPoint プロパティと EndPoint プロパティに基づいて LineGeometry オブジェクトを返します。DefiningGeometry を呼び出すたびに、新しい LineGeometry オブジェクトを作成するのではなく、フィールドとして定義された単一の LineGeometry オブジェクトを再利用している点に注目してください。Shape から派生させる場合、新しいオブジェクト インスタンスの作成に伴うヒープの浪費を避けるうえで、これはきわめて重要な技法です。

図 6 では、Shape の比較的単純な派生クラスをもう 1 つ紹介します。CenteredEllipse クラスでは、中心、水平方向と垂直方向の半径、および回転角度を指定することによって楕円を描画できます。CenteredEllipse では、EllipseGeometry によって定義された Center、RadiusX、RadiusY の各プロパティに対し、自分自身を所有者として追加しています。また、RotationAngle プロパティは、ArcSegment から取得しています。フィールドとして保存される RotationTransform を作成し、それを再利用することによって、変換と EllipseGeometry オブジェクトとを関連付けている点に注目してください。

Figure 6 CenteredEllipse.cs—the CenteredEllipse Class (Close Figure 6)
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Petzold.Shapes
{
  public class CenteredEllipse : Shape
  {
    EllipseGeometry elipGeo = new EllipseGeometry();
    RotateTransform xform = new RotateTransform();

    // Dependency properties
    public static readonly DependencyProperty CenterProperty =
      EllipseGeometry.CenterProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(new Point(0, 0),
          EllipsePropertyChanged));

    public static readonly DependencyProperty RadiusXProperty =
      EllipseGeometry.RadiusXProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(0.0,
          EllipsePropertyChanged));

    public static readonly DependencyProperty RadiusYProperty =
      EllipseGeometry.RadiusYProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(0.0,
          EllipsePropertyChanged));

    public static readonly DependencyProperty RotationAngleProperty =
      ArcSegment.RotationAngleProperty.AddOwner(
        typeof(CenteredEllipse),
        new FrameworkPropertyMetadata(0.0,
          TransformPropertyChanged));

    static void EllipsePropertyChanged(DependencyObject obj,
      DependencyPropertyChangedEventArgs args)
    {
      (obj as CenteredEllipse).EllipsePropertyChanged(args);
    }

    static void TransformPropertyChanged(DependencyObject obj,
      DependencyPropertyChangedEventArgs args)
    {
      (obj as CenteredEllipse).TransformPropertyChanged(args);
    }

    void EllipsePropertyChanged(DependencyPropertyChangedEventArgs args)
    {
      elipGeo.Center = Center;
      elipGeo.RadiusX = RadiusX;
      elipGeo.RadiusY = RadiusY;
      InvalidateMeasure();
    }

    void TransformPropertyChanged(DependencyPropertyChangedEventArgs args)
    {
      xform.Angle = RotationAngle;
      xform.CenterX = Center.X;
      xform.CenterY = Center.Y;
      InvalidateMeasure();
    }

    public CenteredEllipse()
    {
      elipGeo.Transform = xform;
    }

    // Public CLR properties
    public Point Center
    {
      set { SetValue(CenterProperty, value); }
      get { return (Point)GetValue(CenterProperty); }
    }

    public double RadiusX
    {
      set { SetValue(RadiusXProperty, value); }
      get { return (double)GetValue(RadiusXProperty); }
    }

    public double RadiusY
    {
      set { SetValue(RadiusYProperty, value); }
      get { return (double)GetValue(RadiusYProperty); }
    }

    public double RotationAngle
    {
      set { SetValue(RotationAngleProperty, value); }
      get { return (double)GetValue(RotationAngleProperty); }
    }

    // Required DefiningGeometry override
    protected override Geometry DefiningGeometry
    {
      get
      {
        return elipGeo;
      }
    }
  }
}

CenteredEllipse に示した構造は、Geometry を定義すると処理に著しく時間がかかる場合に効果的です。このクラスでは、依存関係プロパティで AffectsMeasure フラグを設定する代わりに、2 つのプライベート フィールドに対するプロパティ変更ハンドラを定義しています。このハンドラによって、各フィールドのプロパティが設定された後、InvalidateMeasure が呼び出されて、新しいレイアウト パスが開始されます。

PointLine クラスと CenteredEllipse クラスは、通常の Line クラスと同じように動作することがおわかりいただけると思います。Stretch プロパティを Stretch.None 以外に設定した場合、Shape クラスは、ジオメトリのサイズを変更するために必要な変換を計算し、親の内部にちょうど収まるような方法でジオメトリをシフトさせます。当然、この変換は Geometry そのもののサイズ (Bounds プロパティ経由でアクセスできる) に基づきますが、少なくとも、オブジェクトの大部分がその親の四角形の中に収まるよう、StrokeThickness プロパティも考慮されます。

これらの計算は、Shape クラスで内部的に実行されます。Shape によって計算される変換には、パブリックの GeometryTransform プロパティを使ってアクセスできます (既定値は、静的プロパティの Transform.Identity)。また、Shape では、GeometryTransform プロパティによって変換された DefiningGeometry プロパティとして、RenderedGeometry プロパティが計算されます。Shape の OnRender メソッドがグラフィックスを描画する際に使用するのが、この RenderedGeometry プロパティです。

Shape にとって重要なことは、この変換を図そのものではなく、ジオメトリに適用することです。仮に変換が図に適用された場合、線を引くために使用されているペンの幅にまで影響が及びます。

もちろん、プログラマがこの変換計算を意識する必要はありません。必要なことは DefiningGeometry を指定することだけです。

このコラムのソース コードは、DerivingFromShape という名前の単一の Visual Studio ソリューションで構成されています。Shape の派生クラスは、Petzold.Shapes という DLL プロジェクト内にあります。この DLL には、先端に矢印の付いた線とポリラインのプリミティブを実装する、Shape の派生クラスも含まれています。2007 年 4 月のブログ (charlespetzold.com/blog/2007/04/191200.html) で紹介したものです。

平行な線と曲線

今度は、もう少し難しいものに挑戦してみましょう。より凝ったバージョンの Path クラスが必要になったと仮定してください。Path クラスには、Geometry 型の Data という名前のプロパティが 1 つ定義されています。おそらく、Path は、この同じ Geometry オブジェクトをその DefiningGeometry プロパティから返します。その点で、Path は、間違いなく、Shape の最も単純な派生クラスと言えるでしょう。

これから作成する新しいクラスそのものの Data プロパティは、Path の Data プロパティに所有者を追加ことによって定義します。ただし、単に Geometry オブジェクトをレンダリングするだけではありません。新しいクラスでは、図 7 に示すように、Geometry を平行線でレンダリングします。この新しいクラスの名前は ParallelPath です。名前からもわかるように、このクラスは、線の本数を表す Number プロパティ (図 7 では 5) と、線の間隔を表す Gap プロパティを持ちます。図 7 では、StrokeThickness が 3 に、Gap が 4 に設定されています。

Figure 7 ParallelPathDemo プログラム
Figure 7 ParallelPathDemo プログラム(画像を縮小するには、ここをクリックします)
Figure 7 ParallelPathDemo プログラム
Figure 7 ParallelPathDemo プログラム (画像を拡大するには、ここをクリックします)

なお、ParallelPath に使用しているアルゴリズムは、特定の条件下でうまく機能しなかったり、余分な線が表示されてしまうことがあります。ただし、平行線の数が常識的な範囲内であれば、各種のパス線分でスムースに動作します。また、ブラシで塗りつぶした場合に、(あらかじめわかっていることではありますが) 予期しない結果が起こる場合があります。

平行線を生成するという概念は、直線が対象であれば、きわめて単純です。ただし、ParallelPath クラスは、ベジエ曲線や円弧にも対応しています。Path と同様、ParallelPath の Data プロパティは、Geometry 型の任意のオブジェクトに設定できます。Geometry 型は、シールされた 7 つのクラス (CombinedGeometry、EllipseGeometry、GeometryGroup、LineGeometry、PathGeometry、RectangleGeometry、StreamGeometry) の派生元となる抽象クラスです。

図 7 の画像は、次のコードによって生成されています。

<ps:ParallelPath Stroke="Black" StrokeThickness="3"
  Number="5" Gap="4" Tolerance="10">
  <ps:ParallelPath.Data>
    <PathGeometry>
      <PathFigure StartPoint="200 100">
        <PolyLineSegment Points="350 200, 450 50" />
        <PolyBezierSegment
          Points="500 0, 600 300, 350 300,
                  100 300, 50 0, 200 100" />
      </PathFigure>
    </PathGeometry>
  </ps:ParallelPath.Data>
</ps:ParallelPath>

XML 名前空間 "ps" は、CLR 名前空間 Petzold.Shapes に関連付けられています。Data プロパティには、パスのミニ言語で文字列を設定することもできます。ただし、その場合は、個々の座標をデータ バインドやアニメーションのターゲットにすることはできません。

Geometry の派生クラスの中で最も汎用性が高いのは、PathGeometry クラスです。PathGeometry オブジェクトは、PathFigure オブジェクトのコレクションです。それぞれの PathFigure オブジェクトは、連続する直線または曲線の集まりであり、PathSegment オブジェクトのコレクションとして格納されます。PathSegment は、直線、弧、およびベジエ曲線をレンダリングするための 7 つのクラスの派生元となる抽象クラスです。これらの線分の座標点は、データ バインドやアニメーションのターゲットにすることができます。

ParallelPath は、Data プロパティに設定する Geometry オブジェクトの種類に関係なく、平行な直線や曲線を表す PathGeometry オブジェクトを生成します。ただし、このアルゴリズムは、ベジエ曲線に対する平行線をベジエ曲線として求めることはしません。完全にポリラインをベースとしています。1 つまたは複数のポリラインを入力として受け取り、そのそれぞれについて、複数のポリラインを出力します。そのため、ParallelPath は入力ジオメトリを "フラット化" する必要があります。つまり、ジオメトリ全体 (弧やベジエ曲線を含む) を近似のポリラインへと変換する必要があります。

線分の端点から余分な線が引かれているなど、ParallelPath で思いどおりの結果が得られなかった場合は、Tolerance プロパティを 1 より大きな値に設定してください。ParallelPathDemo プログラムでは "10" に設定しています。この Tolerance 値は、フラット化されたジオメトリの各ポリラインにおける、おおよその DIU 数 (Device Independent Units: デバイス固有の単位) です。

幸い、Geometry クラスには、ジオメトリをフラット化するためのメソッドが存在します。それが GetFlattenedPathGeometry メソッドです。このメソッドは、完全に PolyLineSegment オブジェクトの PathFigure オブジェクトで構成された PathGeometry オブジェクトを返します (PathFigure クラスそのものには、別の PathFigure を返す GetFlattenedPathFigure メソッドが定義されています)。

残念ながら、GetFlattenedPathGeometry メソッドは、PathGeometry オブジェクト (さらには、そのジオメトリを構成する子の PathFigure および PolyLineSegment) を作成する際、マネージ ヒープからメモリを割り当てる必要があります。こうしたメモリ割り当てが問題を引き起こす可能性があります。ParallelPath の Data プロパティを PathGeometry オブジェクトに設定し、PathGeometry のいずれかの座標点をアニメーション化することを考えてみます。当然、ParallelPath は、アニメーション化された座標点で変化が起こるたびに DefiningGeometry オブジェクトを再生成する必要があります。ParallelPath は、GetFlattenedPathGeometry を呼び出すことで、暗黙的にヒープ割り当てを繰り返すことになり、最終的には .NET ガベージ コレクタの実行が必要になります。

ヒープの割り当てを最小限にする

GetFlattenedPathGeometry では、メモリ割り当てが何度も実行されてしまう可能性があるため、私は独自のパス フラット化ルーチンを作成することにしました。通常、そのためには、PathGeometry、PathFigure、および PolyLineSegment オブジェクトの新しいインスタンスを作成しなければならないところですが、私は、これらのオブジェクトをキャッシュして再利用する簡単なメソッドもいくつか作成しました。パスをフラット化するためのメソッドとキャッシュ メソッドは、PathGeometryHelper というクラスにあります。当然、少なくとも最初の段階ではメモリ割り当てを実行する必要がありますが、定期的なメモリ割り当ては回避できます。そうすることで、全体的なパフォーマンスが向上すると思われます。

私が作成した PathGeometryHelper クラスには、独自のフラット化アルゴリズムを実行できないケースが 2 つ存在します。1 つは、Geometry の種類が StreamGeometry である場合です。これは、StreamGeometry が、StreamGeometryContext オブジェクトの呼び出しから構築されるためです。ただし、ジオメトリ自体には、静的メソッド PathGeometry.CreateFromGeometry を使用しなければアクセスできません。もう 1 つは、オブジェクトの種類が CombinedGeometry である場合です。CombinedGeometry は、Boolean で組み合わされた 2 つの Geometry オブジェクトで構成されます。

この 2 つのケースに該当した場合、PathGeometryHelper クラスは、やむをえず、単に GetFlattenedPathGeometry を呼び出します (実際には、GeometryGroup オブジェクトが、StreamGeometry 型または CombinedGeometry 型のオブジェクトを子や孫として持つことも考えられるため、パスにこれらの種類のオブジェクトが存在する場合にも、GetFlattenedPathGeometry が呼び出されます)。こうした例外がある以上、決して完璧とは言えませんが、実用上はさほど問題ないと思います。StreamGeometry 型のオブジェクトはどちらにしてもアニメーション化できません。また、CombinedGeometry を使用する機会も、おそらくきわめてまれです。

さらに、メモリの割り当てを最小限にするために、ParallelPath クラスには、PathGeometry 型の 2 つのフィールドが定義されています。フラット化されたパスを格納する pathGeoSrc ("PathGeometry source") と、平行線として展開されたパスを格納する pathGeoDst ("PathGeometry destination") です。また、それらとは別に、PathGeometryHelper のインスタンスが格納する pathHelper というフィールドも設けてあります。

ParallelPath が定義している 4 つのプロパティは、SourcePropertyChanged および DestinationPropertyChanged という、2 つのプロパティ変更コールバックに関連付けられています。SourcePropertyChanged ハンドラは、Data プロパティまたは Tolerance プロパティが変更されると、まず、次のようにして、以前の PathGeometry ソースをキャッシュします。

pathHelper.CacheAll(pathGeoSrc);

CacheAll は、PathGeometry オブジェクト自体に加え、PathGeometry に保持されているすべての PathFigure オブジェクトおよび PolyLineSegment オブジェクトをキャッシュするメソッドです。ただし、以前の PathGeometry が Geometry の GetFlattenedPathGeometry メソッドの呼び出しから取得されていた場合、オブジェクトは固定化され、今後は変更できなくなります。このようなオブジェクトはキャッシュされません。

次に、SourcePropertyChanged ハンドラは、受け取ったジオメトリをフラット化し、それを次のコードで pathGeoSrc として保存します。

pathGeoSrc = pathHelper.FlattenGeometry(Data, Tolerance);

FlattenGeometry メソッドは、可能な限り、PathGeometry、PathFigure、PolyLineSegment の各オブジェクトをキャッシュから取得します。SourcePropertyChanged は、最後に、Number プロパティと Gap プロパティに関連付けられたプロパティ変更コールバックを呼び出します。

DestinationPropertyChanged(args);

DestinationPropertyChanged コールバックは、まず、平行線を含んだ PathGeometry の構成要素を、次のようにして、キャッシュに戻します。

pathHelper.CacheAll(pathGeoDst);

次に、ParallelPath の GenerateGeometry メソッドを呼び出して、複数の平行パスを生成します。

pathGeoDst = GenerateGeometry(pathGeoSrc);

GenerateGeometry メソッドも、PathGeometry、PathFigure、PolyLineSegment の各オブジェクトをキャッシュから取得して使用します。フィールドとして格納され、呼び出しのたびに再利用される List<Point> 型のオブジェクトもキャッシュから取得されます。条件を満たしたオブジェクトがキャッシュに存在する場合は、すべての処理が、ヒープの割り当てを伴うことなく実行されます。DestinationPropertyChanged コールバックの最後には、次のようにしてレイアウト パスが開始されます。

InvalidateMeasure();

DefiningGeometry プロパティからは、単に pathGeoDst が返されます。

効果的なパス拡張の方法

ParallelPath クラスを作成した後、もう 1 つ、こんなクラスがあったらよいなというものを思いつきました。15 年以上も悩まされてきた問題を解決するよい機会です。

太線を描画する際、アウトライン (縁取り) を付ける方法はないものかと悩むことがあります。意外にも、あまり知られていませんが、それは可能です。そればかりか、Win32® の初期からサポートされてきました。WPF では、この機能が GetWidenedPathGeometry メソッドとして、Geometry クラスに組み込まれています。

それでは例を見てみましょう。geo という名前の Geometry があるとします。これを次のコードのように、Path オブジェクトと組み合わせて使用します。

path = new Path();
path.Data = geo;
path.Stroke = Brushes.Blue;
path.StrokeThickness = 40;

図 8 は、この Path オブジェクトを示しています。

Figure 8 Path で描画された太線
Figure 8 Path で描画された太線

Geometry クラスの GetWidenedPathGeometry には Pen オブジェクトを指定する必要があります。ただし、使用されるのは Pen の物理的なサイズと特性だけで、Brush プロパティは無視されます。したがって、次のように指定することになります。

Pen pen = new Pen(null, 40);
PathGeometry pathGeo = geo.GetWidenedPathGeometry(pen);

ペンの太さには、Path のコードと同じ 40 単位を指定していることに注目してください。この新しい PathGeometry は、Pen で描画した場合の Geometry の輪郭を表します。今度は、この新しい Geometry で、新しい Path 図を描画してみます。ただし、先ほどよりも細いペンとフィル ブラシを指定します。

path = new Path();
path.Data = pathGeo;
path.Stroke = Brushes.Red;
path.StrokeThickness = 6;
path.Fill = Brushes.Blue;

その結果を図 9 に示します。基本的には、元の線にアウトラインが描画されています。

Figure 9 Path で描画された拡張パス
Figure 9 Path で描画された拡張パス

もちろん、このようにパスを拡張する方法でも目的を達成できますが、問題が 2 つあります。1 つは、GetWidenedPathGeometry がメソッドであるという点です。なんらかのアニメーション化が可能な点で PathGeometry を作成した場合、拡張パスをアニメーション化するために、GetWidenedPathGeometry を繰り返し呼び出す必要があります。

パスの拡張に伴う 2 つ目の問題は、アーティファクト (処理の形跡) が目立ってしまうことです。図を見ると、処理の過程がはっきりと残ってしまっていることがわかります。これは、Windows でパスを拡張しようとした方ならだれでも経験することです。

このような理由から、私は WidenedPath クラスを自作することにしました。このクラスには、Path クラスのような Data プロパティと、拡張パラメータ用の WideningPen プロパティが実装されています。図 10 の画像は、次のコードによって作成されています。

Figure 10 ウィンドウ内の Line 要素
Figure 10 ウィンドウ内の Line 要素

   WidenedPath widePath = new WidenedPath();
   widePath.Data = geo;
   widePath.WideningPen = pen;
   widePath.Stroke = Brushes.Red;
   widePath.StrokeThickness = 6;
   widePath.Fill = Brushes.Blue;

WidenedPath は、単に ParallelPath を応用したものと思われるかもしれませんが、他にも複雑な事情が関係しています。適切に実行された場合には、パスの拡張時に、線の先端や屈曲部の形状 (つまり、Pen のプロパティである StartLineCap、EndLineCap、および LineJoin) が考慮されます。そのため、パス拡張アルゴリズムでは、状況に応じてパスに曲線を追加する必要があります (図 11 を参照)。

Figure 11 線の先端と屈曲部が考慮された WidenedPath クラス
Figure 11 線の先端と屈曲部が考慮された WidenedPath クラス

ただし、WidenedPath では、Pen のプロパティである DashStyle、DashCap、および MiterLimit が考慮されません。WidenedPath では、アーティファクトが生成されてしまう可能性は GetWidenedPathGeometry よりも低いですが、完全にゼロというわけではありません。ParallelPath と同様、Tolerance プロパティを増やすことによって、こうしたアーティファクトを最小限に抑えることができます。

その他のパラダイム

以上、Shape クラスに実装された "座標" パラダイムを使ったクラスを中心に取り上げてきましたが、"自動サイズ調整" パラダイムのことが気になっている方もいらっしゃることと思います。

Rectangle や Ellipse で何度か実験を試みた結果、これらのクラスは常に、対応する GeometryTransform メソッドから Transform.Identity を返していることがわかりました。その代わり、DefiningGeometry のサイズは、MeasureOverride メソッドおよび ArrangeOverride メソッドをオーバーライドすることによって得られた情報が基になっています。

私は、このアプローチに沿って実験的に RegularPolygon クラスを作成した後、試しに "座標" パラダイムを使用したらどうなるか、という視点でクラスをコーディングしました。座標やサイズは一切考慮しませんでしたが、中心 (0, 0)、半径 1 とする円の上に多角形を置き、残りの処理を WPF にまかせてみることにしました。

その結果は、Rectangle や Ellipse とは明確に異なるものでしたが、特に差が生じたのは、Width プロパティと Height プロパティを NaN に設定し、Stretch を Uniform または Fill に設定したときでした。こうしたケースの多くでは、Ellipse が小さな点になっても、RegularPolygon が可視状態になっていました。

ソース コードには、StretchExplore というプログラムが付属しています。Stretch、HorizontalAlignment、VerticalAlignment、Width、Height の各設定をいろいろ変えながら、Ellipse、RegularPolygon、Line、PointLine などの図形を確認できます。そのうえで、私が RegularPolygon に使用した簡単なアプローチを皆さんのアプリケーションに応用できるかどうかを判断してください。

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

Charles Petzold は MSDN Magazine の寄稿編集者です。最新の著書には『3D Programming for Windows』があります。


.NET の問題: 非同期ストリーム処理

.NET の問題
非同期ストリーム処理
Stephen Toub

Q 以前、C# の反復子を使用して、非同期処理を容易に実装する手法についての記事を読みました (MSDN® Magazine の先日の記事で、Jeffrey Richter が、この技法のメリットを強調していました)。しかし、同様のことを、コード変換を伴わないような方法で実行しようとしたのですが、どうしてもうまくいきません。たとえば、Stream の非同期メソッドだけを使って、あるストリームを別のストリームにコピーするとします。非同期要求を 1 度だけ (読み取りと書き込み時にそれぞれ 1 回) 発行する方法なら理解しているのですが、どうすれば、このような要求を連続的にループさせて、ストリーム全体を処理できるのかがわかりません。そもそも、そのようなことができるのでしょうか。

A はい。できます。挙げていただいた例でその方法を紹介します。C# と Visual Basic® の両方で説明しますが、Visual Basic では、反復子に対するコンパイラ サポートがない点に注意してください。図 1 では、非同期で何を実行するのかを明らかにするために、まず、同期処理で行った場合のコードを示します。次のようなコードで呼び出すことになるかと思います。

Figure 1 Synchronous CopyStreamToStream (Close Figure 1)
public static void CopyStreamToStream(
        Stream source, Stream destination,
        Action<Stream, Stream, Exception> completed)
{
    byte[] buffer = new byte[0x1000];
    int read;
    try
    {
        while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
        {
            destination.Write(buffer, 0, read);
        }
        if (completed != null) completed(source, destination, null);
    }
    catch (Exception exc)
    {
        if (completed != null) completed(source, destination, exc);
    }
}

FileStream input = ...;
FileStream output = ...;
CopyStreamToStream(input, output, (src,dst,exc) => {
    src.Close();
    dst.Close();
});

同期の実装では、ストリームがコピーされて completed コールバックが実行されるまで、CopyStreamToStream がブロックされた状態になります。これを非同期的に呼び出すように実装するのは簡単です。方法はいくつか考えられます。たとえば、ThreadPool のキューに、このメソッドを呼び出す作業項目を明示的に追加することです。

ThreadPool.QueueUserWorkItem(delegate
{
    CopyStreamToStream(input, output, (src,dst,exc) =>
    {
        src.Close();
        dst.Close();
    });
});

また、次のように、非同期的にデリゲートを呼び出す方法もあります。こちらも、ほぼ同じ効果が得られます。

delegate void CopyStreamToStreamHandler(
    Stream source, Stream destination,
    Action<Stream, Stream, Exception> completed);
...
CopyStreamToStreamHandler handler = CopyStreamToStream;
handler.BeginInvoke(source, destination, (src,dst,exc) =>
{
    src.Close();
    dst.Close();
}, iar => handler.EndInvoke(iar), null);

しかし、どちらの方法も、ご希望の非同期処理とは異なります。それぞれのスレッドが実行を開始するという点ではたしかに非同期ですが (つまり、呼び出し元のスレッドが完了待ちの状態でブロックされることはない)、どちらの場合も、CopyStreamToStream メソッドは単にプールのスレッド上で実行されているだけで、メソッドが呼び出された後に関しては、要求された読み取りと書き込みがすべて同期的に実行されます。

このような方法で仮に 100 回、CopyStreamToStream を非同期に呼び出すとしたら、コピーを実行するために 100 個のスレッドが必要になります (再利用される可能性もあります)。これではオーバーヘッドが著しく大きくなります。それだけではありません。スレッド プールで利用可能なスレッド数と、必要なスレッド数が増える速度を考慮すると、実行待ちの状態になるコピー処理が大量に生じ、結果的に完了までに長時間かかってしまうことが予想されます。

ご質問の意図は、CopyStreamToStream を本当の意味で非同期に実行することかと思います。つまり、ストリームの Read と Write を呼び出す代わりに、BeginRead と BeginWrite を呼び出し、適宜、対応する EndRead と EndWrite を呼び出すことです。

同期実装の例を見るとわかるように、必要なことは、読み取り要求をコピー元のストリームに対して発行し、取得したデータを受け取って、それをコピー先のストリームに書き込む、という処理を連続的に行うことです。非同期実装では、同期実装で用いたようなループは使用しません。継続渡しスタイルを使用し、非同期要求の後の処理をすべてパッケージ化し、それを、非同期要求の完了時に実行されるクロージャとして渡すようにします。最初に、非同期の読み取り要求を発行します。

byte[] buffer = new byte[0x1000];
source.BeginRead(buffer, 0, buffer.Length, readResult =>
{
    int read = source.EndRead(readResult);
    ... // process read results in buffer here
}, null);

BeginRead メソッドは、Read メソッドと同じ 3 つのパラメータを受け取ります。データを格納するためのターゲット バッファ、バッファに対するデータの格納位置を指定するインデックス、および、読み取るデータのサイズです。BeginRead メソッドには、それ以外にも 2 つのパラメータがあります。1 つは、読み取り要求が完了したときに呼び出されるデリゲートで、もう 1 つは、状態オブジェクト (なんらかの状態をコールバックで使用する必要があればここに指定する) です。コールバック内では、コピー元に対して EndRead 呼び出しを発行します。そのときに、BeginRead から返された IAsyncResult をコピー元に渡すことによって、読み取りの結果を取得できます (EndRead では、読み取りの結果、例外が発生した場合、その例外もスローされます)。その後、取得したデータをコピー先のストリームに書き込む必要があります。

int read = source.EndRead(readResult);
if (read > 0)
{
    destination.BeginWrite(buffer, 0, read, writeResult =>
    {
        destination.EndWrite(writeResult);
        ...
    }, null);
}

同様に、書き込み時もコピー先のストリームの Write を呼び出す代わりに、BeginWrite を呼び出します。その際、先ほど Write に渡したパラメータのほかに、書き込み要求の完了時に実行されるデリゲートと、状態オブジェクト (ここでは無視) を渡します。ここまでは順調です。非同期に読み取り要求を発行しました。要求を発行したスレッドが完了待ちの状態になることはありません。読み取りが完了すると、非同期に書き込み要求が発行されます。読み取り要求からのコールバックが、書き込み要求の完了待ちになることもありません。

しかし、ここで困ったことがあります。BeginWrite のコールバックで、コピー元のストリームからさらに読み取るために、BeginRead の呼び出しを追加すべきでしょうか。だとすれば、読み取りのためのコールバックを渡すことになるのでしょうか。すべての読み取り/書き込みを明示的にコーディングすることはできません。読み取りと書き込みは気が遠くなるほど繰り返されるので、そのようなことをしたら、長大なコード ファイルができあがってしまいます。

もちろん、そのような必要はありません。EndRead の呼び出しを、AsyncCallback デリゲートを介して呼び出されるメソッド内に記述すればよいのです。このメソッドが、BeginRead のコールバックとして機能することになります。その後、BeginWrite のコールバックで、前回の書き込みを終了させた後、次の BeginRead 要求を発行するメソッドを呼び出すことができます。まず、BeginRead の呼び出しを 1 回発行し、今説明したコールバック デリゲートを渡します。これを実装すると、次のようになります。

AsyncCallback rc = readResult =>
{
    int read = source.EndRead(readResult);
    if (read > 0)
    {
        destination.BeginWrite(buffer, 0, read, writeResult =>
        {
            destination.EndWrite(writeResult);
            source.BeginRead(buffer, 0, buffer.Length, rc, null);
        }
    }
}
source.BeginRead(buffer, 0, buffer.Length, rc, null);

一見、これで問題ないように見えますが、コンパイルしようとすると、C# のコンパイラから次のようなメッセージが表示されます。

error CS0165: Use of unassigned local variable 'rc'

つまり、私は "rc" 変数をコールバック内で使おうと、直接 "rc" 変数をコールバックに初期化していますが、C# コンパイラから見ると、その時点ではまだ "rc" が定義されていません。詳細については、Eric Lippert のブログ記事 (go.microsoft.com/fwlink/?LinkId=109263) を参照してください。最も手軽な回避方法は、最初に "rc" 変数を宣言し、null に初期化しておくことです。その後、別のステートメントで、"rc" にコールバックを代入します。

AsyncCallback rc = null;
rc = readResult =>
{
    ...
};

実装例の全体を図 2 に示します。これを見ると、Read/Write を BeginRead/BeginWrite に変更したことに加え、いくつかの機能が追加されていることがわかります。まず、このコードには例外処理が追加されています。同期実装では、例外がキャッチされたときに、completed デリゲートを呼び出すようにしていました。キャッチされた例外をデリゲートに渡すことによって、なんらかの問題が発生したことをアプリケーションに伝え、例外を適切に処理することが可能となっています。もう 1 つは、Microsoft® .NET Framework 2.0 で導入された AsyncOperation クラスの活用です。AsyncOperationManager を使用して AsyncOperation のインスタンスを作成し、現在の SynchronizationContext を取得することにより、対応する SynchronizationContext のコンテキストで、処理を呼び出すことができます。詳細については、msdn.microsoft.com/msdnmag/issues/06/06/NETMatters/#qa7 を参照してください。

Figure 2 Async CopyStreamToStream in C# (Close Figure 2)
public static void CopyStreamToStream(
    Stream source, Stream destination,
    Action<Stream,Stream,Exception> completed) {
    byte[] buffer = new byte[0x1000];
    AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);

    Action<Exception> done = e => {
        if (completed != null) asyncOp.Post(delegate {
            completed(source, destination, e); }, null);
    };

    AsyncCallback rc = null;
    rc = readResult => {
        try {
            int read = source.EndRead(readResult);
            if (read > 0) {
                destination.BeginWrite(buffer, 0, read, writeResult => {
                    try {
                        destination.EndWrite(writeResult);
                        source.BeginRead(
                            buffer, 0, buffer.Length, rc, null);
                    }
                    catch (Exception exc) { done(exc); }
                }, null);
            }
            else done(null);
        }
        catch (Exception exc) { done(exc); }
    };

    source.BeginRead(buffer, 0, buffer.Length, rc, null);
}

たとえば、CopyStreamToStream を Windows® フォーム アプリケーションの GUI スレッドから呼び出した場合、実際に取得される SynchronizationContext は WindowsFormsSynchronizationContext になります。結果の AsyncOperation で Post を呼び出すと、指定されたデリゲートが GUI スレッド上で実行されます。これで、実行されている環境に関係なく、CopyStreamToStream が適切に動作するようになります。

非同期処理の実装例を C# と Visual Basic の両方で紹介すると約束しましたので、以降、その点について説明します。ここまで紹介してきたコードには、C# の反復子は利用されていません。ただし、匿名メソッドなど、他の C# 機能は利用しています。なお、匿名メソッドは、まだ Visual Basic ではサポートされていません (Visual Basic 2008 ではラムダ式がサポートされますが、単一の式に限定されます)。同じことを Visual Basic で行うことになんら問題はありませんが、図 3 を見るとわかるように、必要なコード量がどうしても多くなってしまいます。コードが増えた最も大きな要因は、Visual Basic では、C# で利用できるクロージャが使えないことにあります。C# であれば、クロージャで済む部分を逐一記述する必要があるためです。

Figure 3 Async CopyStreamToStream in Visual Basic (Close Figure 3)
Public Shared Sub CopyStreamToStream( _
    ByVal source As Stream, ByVal destination As Stream, _
    ByVal completed As Action(Of Stream, Stream, Exception))

    Dim buffer(&H1000) As Byte
    Dim data = New CopyData With { _
        .Source = source, .Destination = destination, _
        .Buffer = buffer, .Completed = completed, _
        .AsyncOperation = AsyncOperationManager.CreateOperation(Nothing)}
    source.BeginRead(buffer, 0, buffer.Length, _
        AddressOf ReadCallbackSub, data)
End Sub

Private Shared Sub ReadCallbackSub(ByVal readResult As IAsyncResult)
    Dim copyData = CType(readResult.AsyncState, CopyData)
    Try
        Dim read = copyData.Source.EndRead(readResult)
        If read > 0 Then
            copyData.Destination.BeginWrite( _
                copyData.Buffer, 0, read, _
                AddressOf WriteCallbackSub, copyData)
        Else
            copyData.AsyncOperation.Post( _
                Function() Done(copyData, Nothing), Nothing)
        End If
    Catch ex As Exception
        copyData.AsyncOperation.Post( _
            Function() Done(copyData, ex), Nothing)
    End Try
End Sub

Private Shared Sub WriteCallbackSub(ByVal writeResult As IAsyncResult)
    Dim copyData = CType(writeResult.AsyncState, CopyData)
    Try
        copyData.Destination.EndWrite(writeResult)
        copyData.Source.BeginRead( _
            copyData.Buffer, 0, copyData.Buffer.Length, _
            AddressOf ReadCallbackSub, copyData)
    Catch ex As Exception
        copyData.AsyncOperation.Post( _
            Function() Done(copyData, ex), Nothing)
    End Try
End Sub

Private Shared Function Done( _
        ByVal copyData As CopyData, ByVal ex As Exception) As Boolean
    If copyData.Completed <> Nothing Then
        copyData.Completed(copyData.Source, copyData.Destination, ex)
    End If
    Return True
End Function

Private Class CopyData
    Public Source, Destination As Stream
    Public Buffer() As Byte
    Public AsyncOperation As AsyncOperation
    Public Completed As Action(Of Stream, Stream, Exception)
End Class

C# であれ、Visual Basic であれ、すべてのデータをストリームからストリームへとコピーする処理を、完全に非同期化することができました。ただし、その利点は、ストリームそのものが、非同期メソッドである BeginRead と BeginWrite をどのように実装しているかに依存します。基本 Stream クラスにおけるこれらのメソッドの実装は、単に Read と Write を、非同期デリゲートの呼び出しを使用して、ThreadPool から実行しているだけです。したがって、それぞれの読み取りと書き込みは非同期に実行されますが、パフォーマンスの観点から見ると、単に読み取りと書き込みを同期的に実行した場合よりも全体の速度は低下する可能性があります。

通常、Stream の派生クラスには、完全な非同期読み取り/書き込みをサポートするために、それぞれ独自の BeginRead と BeginWrite が実装されています。たとえば、FileStream の BeginRead と BeginWrite では、Windows ファイル システムの非同期入出力機能を利用できます。FileStream のインスタンスを作成する際、コンストラクタの useAsync パラメータに true を渡した場合、実行環境の Windows のバージョンが非同期入出力をサポートしていれば、FileStream の BeginRead メソッドと BeginWrite メソッドを完全に非同期で動作させることができます。ディスクに対するデータの読み取りまたは書き込み時に、アプリケーションのスレッドがブロックされることはありません。つまり、これらの操作にアプリケーションのスレッドが使用されることはありません。読み取りデータにアクセスできた場合、または、書き込みが完了した場合にのみ、指定されたコールバックが発行され、ThreadPool からのスレッドが使用されます。これにより、リソース使用量を最小限に抑えたスケーラブルなシステムを作成することが可能となります。

ご覧のように、この種のコードを手作業で記述することには、高性能なアプリケーションを作成できる一方、面倒でエラーを招きやすいという欠点もあります。将来、C# や Visual Basic などの主流言語では、こうしたしくみをもっと簡単に作成できるようになることでしょう。

非同期ワークフローという点では、F# が大幅な進化を遂げています (blogs.msdn.com/dsyme/archive/2007/10/11/introducing-f-asynchronous-workflows.aspx を参照)。Web には、C# の反復子を使って非同期コードを容易に作成できるライブラリもいくつか公表されています。たとえば、msdn.microsoft.com/msdnmag/issues/06/09/ConcurrentAffairs で紹介している Concurrency and Coordination Runtime (CCR) もその 1 つです。

ご質問でも触れられている Jeffrey Richter が、より汎用的な AsyncEnumerator 実装を作成しています。既に、彼のコラム (msdn.microsoft.com/msdnmag/issues/07/11/ConcurrentAffairs) でも取り上げられています。この話題については、引き続き、来月発行される MSDN Magazine のコラム「同時実行の問題」で詳しく解説されることになっています。

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

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


{ End Bracket }: ALT .NET とは

{ End Bracket }
ALT .NET とは
Jeremy D. Miller

昨年、David Laribee は、 Microsoft® .NET Framework の広い世界の中から生まれたコミュニティを説明するために、"ALT .NET" という言葉を作りました。このコミュニティに結集した人々は、マイクロソフトのツール、ガイダンス、および .NET カルチャ全体で自分たちにとって重要な基本的価値観が反映または実践されていない、そしてそれに対する不満がどんどん膨らみつつあるという共通の認識を持っていました。2007 年 10 月、テキサス州オースティンで初めての ALT .NET のオープン スペース イベントが開催され、数百人が集まりました。ブログ社会のバズワードをもっと実現性のある言葉で発信し、真のコミュニティを作り上げる第一歩です。

ALT .NET とは何か。既によく知られ、広く利用されている .NET とどのように違うのか。私たちの多くが実践を望む価値観とは何か。ALT .NET の推進者が支持する代替のツール、テクニック、およびプラクティスとは何か。まず最初に、ALT .NET 開発者の原則とされるものを紹介しましょう。

  1. 常により良い方法を探し求めながら、役に立つものは何でも使用する開発者であること。ALT .NET イベントにおける共通のトピックの 1 つは、要件とテストとコードのギャップを埋めることでした。ソフトウェア開発には、解消できそうな無駄がまだいくらでも存在します。
  2. メインストリーム以外の各種コミュニティ、たとえば、Open Source、Agile、Java、Ruby などの長所を取り入れること。マイクロソフトまたは .NET コミュニティだけでは、優れたソフトウェア開発を推進することはできません。たとえば、アジャイル プロセスおよびデザイン パターンは Smalltalk で始められました。同様に、Inversion of Control ツールおよびテクニックは Java から生まれています。Ruby on Rails の 2 つの基本原則は "同じことを繰り返さない" および "設定よりも規約" ですが、これは .NET にも適用できます。
  3. 現状に満足しないこと。何事においても、より洗練され、より柔軟に変化し、より高品質であるのものを実現する可能性が常にあるはずです。私たちは皆、コーディングとテストをビジネス ドメインにもっと近づけるためのテクニックを試しています。たとえば、振舞駆動開発 (BDD: Behavior Driven Development) は、従来の xUnit テストよりも読み取りやすい方法でコードの目的の動作を指定することにより、テスト駆動開発 (TDD: Test Driven Development) を変更かつ拡張したものです。また、言語指向プログラミングには、抽象レイヤをドメイン ロジックのレベルに上げる可能性があります。
  4. ツールの有用性を認めているが、正しく使わないと良い結果が得られないということを理解していること。これは、必要不可欠な原則であり、重要な認識です。最良のツールは、この原則を具体化するものです。たとえば、ReSharper はその一例です。また、ソリューションの最も重要な品質は保守性と維持可能性であるという考えも大切です。保守しやすいコードというのは、つまりデザインが優れているということです。優れたデザインは、デザインに関する知識を十分なスキルで実践することから生み出されます。これまで .NET コミュニティは、API およびフレームワークの詳細を学ぶことに集中しすぎたため、デザインおよびコーディングへの熱意が十分ではありませんでした。

ALT .NET のムーブメントが起こるきっかけの 1 つは、2007 年 3 月に行われた新しい Entity Framework ベータ版のデモンストレーションでした。他の仲間たち同様、私も Entity Framework のいくつかの点、特に "永続性の無視" の欠如、およびデータベースとの緊密な結びつきに落胆を感じました。そして、Entity Framework が TDD、ドメイン駆動設計、継続的なデザインなど、私が保守しやすい開発に有用であると考えるどの要素にも適合するとは思えませんでした。

他方、新しい Model View Controller (MVC) Framework for ASP.NET の開発はとても魅力的です。私がソフトウェア開発を成功させるために重要だと感じているものを明示的にサポートし、実現できるからです。MVC Framework は、保守性とテストの容易性という価値観を明確に反映し、フレームワークのデザイン全体を通して緊急事項の分離とテストの容易性に重点が置かれています。

開発の状況に対してシニカルになったり、冷淡な態度をとったりすることは簡単です。しかし、組織内の開発者について "プログラミングができない" と嘆いたり、良い開発者を見つけるのがいかに難しいかと不平をこぼしたりする代わりに、大きな問題の存在を認める必要があるのではないでしょうか。我々は開発者の育成がひどく苦手だということです。

何から始めればよいかを知っていれば、多くの開発者が喜んでオブジェクト リレーショナル マッピングおよび TDD を使用するでしょう。一方、既にこれらのテクニックを実践している開発者は、実践経験のある開発者同士の交流を通してより良い開発手法を手に入れることができるでしょう。ALT .NET ムーブメントに対して私が一番期待しているのは、いずれの状況の開発者にも対応し、皆が強力な開発者になれるよう手助けするコミュニティへと成長してくれることです。この最終目標に向かって、Altnetpedia Web サイト (altnetpedia.com) がビジネス用に開かれたばかりです。まず、.NET で上述のテクニックをどのように適用するかを学ぶためのリソースを紹介することから始めました。また、.NET の専門家が開発のトピックを討論する場として altnetconf の掲示板 (tech.groups.yahoo.com/group/altdotnet) が開設され、活発に議論が交わされています。ぜひ、お立ち寄りください!

Jeremy D. Miller は、テキサス州オースティンにある Bayern Software 社の開発マネージャです。codebetter.com/blogs/jeremy.miller で彼のブログをご覧いただけます。