ダイナミック HTML と Microsoft Visual C++ による
Web ベース クライアントの構築
Christian Gross
euSOFT
1998 年 5 月
要約: レガシー アプリケーションを Web に移行するときのデザイン パターンについて解説します。以下のトピックを扱っています。
- デカップリングされたクライアント
- フォーマットをロジックから分離するデザイン パターン
- DHTML と ATL による、パターンをベースにしたコンポーネントの構築
問題
Web が普及する前の時代には、個々の企業が独自のソリューションを使ってレガシー アプリケーションを開発していました。今日では、これらの多様な開発ソリューションを使用しているアプリケーションを、Web ベースのフレームワークに移行するという問題が浮上しています。レガシーアプリケーションの Web への移行は簡単な作業ではありません。アプリケーションを部分的に、または完全に書き換える必要があり、多層アプリケーションを効率的に開発できる手法が強く求められています。
デザイン パターン
デザイン パターンはなぜ必要なのでしょうか? デザイン パターンは、細かい部分に立ち入らずにロジックを定義する効率的な手段となります。例として、2 人の機械工学者が面と向かい合って、熱タービンについて話をしている場面を考えてみましょう。2 人の会話は、高水準の概念とアイデアを論じるための、共通の特殊化された言語によって円滑に進みます。デザイン パターンは、複雑なアーキテクチャを定義する効率的な手段を提供することで、これと同じ目的を果たします。
レガシー アプリケーションの Web への移行という問題を検討するなかで、いくつかのデザインパターンが評価されました。そのうちの 1 つである Model-View-Controller (MVC) アプローチ (http://atddoc.cern.ch/Atlas/Notes/004/Note004-7.html を参照) は、最初のうちは優れた選択肢であるように思われました。MVC は、UI を、その作成に使われるロジックから分離 (デカップル) するので、本記事で扱っている問題をうまく解決できるように見えます。しかし、より細かく検討してみると、MVCパターンは中央のコントローラとモデルを使って、さまざまなビューを操作しています。われわれのデザイン パターンには、複数のデータ オブジェクトと単一のビュー (Web ページ) があります。つまり、MVCパターンは複数ドキュメント アプリケーションには理想的ですが、Web ベースのフレームワークにはうまく適合しません。
Strategy Pattern (Design patterns, Gamma, et al, pg. 315) も、やはりUI をロジックから分離します。しかし、UI とのリッチなインタラクションを可能にするほどではありません。StrategyPattern のもう1 つの問題は、コンテキスト オブジェクトが、グローバル操作に使われる外部インターフェイスを定義していないということです。外部インターフェイスについては、特定のインプリメンテーションに動的に接続する特定のインターフェイス クラスを定義する Bridge attern が理想的です。本記事で解説するデザイン パターンは、この 2 つのパターンの優れた特徴、すなわち Strategy Pattern のデカップリングされた UI と、Bridge Pattern の動的なインターフェイス クラスをベースにしています。
デザインのデカップリング
これまで、デカップリングという言葉には望ましい特徴という意味合いを持たせてきました。しかし、「デカップリング」とはそもそもどういう意味なのでしょうか? デカップリングとは、インターフェイスを定義した上で、特定の言語を使ってそのインターフェイスをインプリメントするというプロセスのことです。次のインターフェイス定義の例を考えてみましょう。
class baseOperations {
public:
virtual long operation1( long param1, long param2) = 0;
virtual long operation2( long param1) = 0;
};
この例は、2 つの数値演算のためのインターフェイス定義を使用しています。これらがどのように機能し、何を行うかは、インターフェイスとは独立した問題です。これがデカップリングの本質です。つまり、インターフェイスではなく、インプリメンテーションが機能を定義するということです。次に示すのは、インプリメンテーションの例です。
class Implemented : public baseOperations
{
public:
virtual long operation1( long param1, long param2) {
return param1 * param2;
}
virtual long operation2( long param1) {
return param1 + param2;
}
};
このインプリメンテーションは以下のように使用されます。
void Method1( baseOperations *op, int param1, int param2) {
printf( "Operation1 is %ld, %ld = %ld\n", param1,
param2, op->operation1( param1, param2));
printf( "Operation2 is %ld = %ld\n", param1, op->operation2( param1));
}
int main() {
Implemented imp;
Method1( &imp, 1, 2);
return 0;
}
インプリメントされたオブジェクトは、baseOperations インターフェイスを期待しているメソッド (Method1) に渡されます。これは、Method1 関数は任意のインプリメンテーションを使えるということを意味しています。このデカップリングの例では、Method1 はインプリメンテーションが何であるのかを知らず、単に使用するだけです。
この例では継承を使うこともできます。実際、継承はあらゆる問題の解決に使用できます。しかし、継承を使うことの問題は、メイン関数が特定のクラス インプリメンテーションへの参照を必要とするという点にあります。別のインプリメンテーションを使用するときには、別のクラスを参照しなくてはなりません。このような形で参照を行うと、依存関係が生じ、インターフェイスを独立に拡張、変更、または再利用するのが難しくなります。
デザイン パターン
デカップリングされたインターフェイス デザインと動的なインターフェイス クラスの特徴を組み合わせた新しいデザイン パターンを提案します。以下に、このデザイン パターンの概要を説明し
ます。
名前
その特徴から、このデザイン パターンは "Separating Format from Logic" と呼ぶことにします。
問題
Separating Format from Logic デザイン パターンは、レガシー アプリケーションの Web ベースフレームワークへの移行という問題にどのように対処するのでしょうか? まず、時間追跡アプリケーションであるレガシー アプリケーション、TimeClock を例に取ります。オリジナルのコードは Zafir Anjum によって書かれ、後に My Blenkersによって変更されました。オリジナルの TimeClock のインターフェイスを図 1 に示します。

図 1. オリジナルのTimeClockのインターフェイス
このアプリケーションの目的は、特定のプロジェクトにどれだけの時間を費やしたかを監視することです。最初の形では、アプリケーションは休暇や病欠といったものを定義していました。しかし、唯一可能な操作は、特定のタイムカードに開始と終了の時刻をパンチすることだけでした。このタイムカードは 1 つのプロジェクトとして扱うことができましたが、直接の関連はありませんでした。第 2 の形では、プログラムはプロジェクトを定義していましたが、今回は病欠や休暇といった概念は定義されていませんでした。
この 2 つの形は、どちらも UI をロジックにカップリングすることに内在する問題を表しています。UI とロジックを分離して、粒度の細かい UI を作るのはきわめて困難です。カップリングされた UI では、単純なタスクを実行するのにも、予想以上に長い時間がかかります。その結果、インターフェイスはデータをクライアントからサーバーに送る以外の何もしないというシン クライアント コンピューティングのルネッサンスが始まりました。従来のクライアントと比較すると、シン クライアントは次の 2 つの特徴を備えています。
- ロジックをクライアントからサーバーに移動した。これにより、サーバーの開発に要する期間が長くなった。
- リッチなクライアント サイド機能を持たない、より単純なインターフェイスを持っている。
ラピッド アプリケーション開発 (RAD) ツールは、この傾向を産み出した責任の一端を担っています。というのも、RAD ツールでは、イベントの中にロジック コードを簡単に追加できるからです。現在の RAD ツールは、キャンバスまたはフォームに要素を配置していく形になっています。このアプローチでは、UI をインクリメンタルに組み立てたり、分解したりするのは不可能です。
開発コストの増大に対抗するために、多くのベンダはコンポーネントをツールボックスからキャンバスにドラッグできるようなツールを提供してきました。これらのドラッグ アンド ドロップ ツールでは、ユーザー インターフェイスを短期間に作成することができますが、結果として得られるロジックは UI に緊密に結び付けられ、それを拡張したり、別のキャンバスに移動したりするのが難しくなります。
UI 開発を促進するための戦略の 1 つとして、目的の UI ロジックの 80% を実行する高機能なコントロールを購入または構築するというものがあります。これらのコントロールはうまく動作しますが、オーバーライドしたり変更したりするのは困難です。たとえば、グラフ作成コントロールをオーバーライドして、動的に Virtual Reality Modeling Language (VRML) 互換にするというようなことは不可能です。このためには新しいコントロールを開発または購入しなくてはなりません。いずれにしても、これは簡単な作業ではありません。コントロールを書き直さなくてはならないわけです。このように、シン クライアント、RAD ツール、ドラッグ アンド ドロップ ツール、および高機能のコントロールは、ロジックからデカップリングされたユーザー インターフェイスを開発するという問題の解決にはなりません。明らかに、何か別のものが必要なのです。
ソリューション
われわれが提案するソリューションは、特定のタスクを実行するオブジェクトに動的にカップリングされる UI をベースにしています。このアーキテクチャでは、コントローラがビジネス ロジックの一部を管理しますが、きわめて高い水準でこれを行います。コントローラのタスクは、集計、イベント キャスティング、および編成に限定されます。これらの高水準のタスクは、データの編成と、一般的なビジネス プロセス (タイム クロックのパンチインとパンチアウトなど) に固有のタスクです。コントローラはビジネス ロジックをインプリメントするわけではありません。単に、適切なインプリメンテーションにアクションを指示するだけです。
一般的なビジネス ロジックを受け取るインプリメンテーションは、要求されたタスクを実行しますが、操作の結果を格納することはしません。結果の格納はコントローラに任されており、コントローラは情報の格納と取得に使われる総称インターフェイスを公開します。ロジックとコントローラは、ロジック コンポーネントがコントローラに自分自身を登録するときにインターフェイスを交換します。このプロセスの中で、登録はロジック コンポーネントによって開始され、コントローラは受動的な役割を果たします。ロジック コンポーネントが登録プロセスを通してコントローラに接続した後は、コントローラが必要に応じて具体的な操作を実行します。この形でのデカップリングには、ユーザー インターフェイスとロジックを動的に割り当てられるという利点があります。
ロジック コンポーネントは、グラフィカル ユーザー インターフェイス (GUI) 的な要素は何も持っていません。コンポーネントがインスタンス作成されるときにUI と関連付けられるだけです。この関連付けにより、見掛けは異なるが、同じロジックを実行する別のユーザー インターフェイスを作成することが可能になります。UI 機能は、ロジック コンポーネントやコントローラの動作には影響を与えません。また、同じロジックに基づく別のソリューションのために、別の UI 媒体を使うことも可能です。この、インターフェイスのビジネス ロジックからの分離という特徴が、SeparatingFormat from Logicという名前の由来となっています。
図 2 に示すように、Separating Format from Logic パターンは、1) サービスをインプリメントするコントローラ、2) ダイナミック HTML の形でのインターフェイス、および3) ビジネス ロジックをインプリメントするコンポーネントの 3 つの要素から構成されています。

図 2. Separating Format from Logicのアーキテクチャ レイアウト
注:本記事では、コンポーネント、ロジック、ビジネス ロジック、およびロジック コンポーネントという言葉を、いずれも同じ意味に使用しています。
結果
Separating Format from Logic デザイン パターンは、次のような結果をもたらします。
- 言語に対する中立性: コントローラ、インターフェイス、およびコンポーネントは、分散オブジェクト テクノロジを使って連結されるので、新しいインプリメンテーションや新しい UI を追加するのが簡単になります。後に変更を加えやすくするためには、個々のアーキテクチャ要素に適した言語を使えるようにしておくことが重要です。たとえば、UI は動的でなくてはならないので、ダイナミック HTML が適しています。また、ロジック コンポーネントには Microsoft
Visual C++ 開発システムのようなプログラミング言語が使えなくてはなりません。
- コントローラへの依存性: 静的なビジネス プラクティスを反映しない不適切な設計のコントローラは、コントローラのインターフェイスをインプリメントするコンポーネントをすぐに時代遅れにしてしまいます。このようなことが起こると、ビジネス ロジックを書き直さなくてはなりません。
- インプリメンテーション上の細部の隠蔽: コントローラは、UI の細かい部分がどこから来ているのかを知っている必要がありません。実際、コントローラは必要ならば、別の UI 内で別のマシン上のコンポーネントと通信を行うこともできます。登録プロセスはロジック コンポーネントの側から開始されるので、高度な柔軟性が保証されています。
- 単純性: このデザイン パターンは、UI のフォーマットを、その UI が実行すべきロジックから明確に分離しているので、単純性を備えています。
- 拡張性: UI とビジネス ロジック コンポーネントの間の通信は、純粋に動的なプロセスで、個々のコンポーネントが独立に行います。この動的な通信は Component ObjectModel (COM) によって実現されます。
パターンのインプリメント
これまでに、われわれは新しいデザイン戦略が必要であることを確認し、Separating Formatfrom Logic デザイン パターンのアーキテクチャ レイアウトと特徴を定義しました。次は、このパターンを使って既存のレガシー アプリケーションを変更する方法をデモンストレーションすることにします。本記事の残りの部分では、前に述べた例、TimeClock を使って、デザイン パターンのインプリメントの 3 ステップから成るプロセスを解説します。
- インターフェイスを定義する
- コンポーネントを構築する
- コントローラを構築する
使用するテクノロジ
デザイン パターンのインプリメンテーションについて論じる前に、インプリメンテーションに使用するテクノロジを検討する必要があります。われわれが提案するデザイン パターンは、動的な関連付けとデカップリングを使ったテクノロジカルなソリューションを必要とするので、テクノロジに依存する部分が大きくなっています。2 つのテクノロジカルな層が必要となります。
- Component Object Model (COM): インターフェイスとインプリメンテーションのデカップリングを可能にするコンポーネント オブジェクト テクノロジ。
- ダイナミック HTML: 関連付けを動的に行い、部品を組み立てるようにしてUI を構築する UI。
このデザイン パターンで使用される言語は JavaScript と Visual C++ です。このモデルではクライアント コードが使われるので、Java の方が便利だと思う人もいるかもしれません。しかし、C++にはリッチな機能があり、軽量のコードを短期間で作成できるという利点があります。個々のツールの長所を考えると、ダイナミック HTML は動的なスクリプティングと関連付けに適しており、C++ はロジックのインプリメントに適しています。
ステップ1: インターフェイスを定義する
ソリューションをインプリメントするための最初のステップは、ロジック コンポーネントとコントローラが使用するインターフェイスの定義です。最初のインターフェイスは、個々のコンポーネントが
インプリメントしなければならない操作です。レガシー アプリケーションは 2 つの操作 (パンチインとパンチアウト) しかサポートしていないので、われわれのインターフェイスはとりあえずこれらの操作をインプリメントする必要があります。さらに、ハウスキーピングの目的に 2 つの機能を追加する必要があります。インターフェイス定義言語 (IDL) を使用して作られたインターフェイスを次に
示します。
[
object,
uuid(EA55BFDF-BC3E-11d1-9484-00A0247D759A),
pointer_default(unique),
local,
version(1.0)
]
interface ITimeCard : IUnknown
{
HRESULT PunchIn( BSTR time, [out,retval]long *retVal);
HRESULT PunchOut( BSTR time, [out,retval]long *retVal);
HRESULT Descriptor([out, retval]BSTR *description);
HRESULT SetService(IUnknown *serviceProvider);
}
操作がパンチインまたはパンチアウトであるときには、時刻が第 1 パラメータとなります。すべてのタイムカードを同期させる必要があるので、時刻のパラメータはコントローラが生成します。パラメータretvalは、操作によって実行された戻りコードです。
注: エラー コードを返すという作業は、COM 層でも処理できます。つまり、1 つのパラメータとする必要はありません。しかし、COM エラーのインプリメンテーションというステップは、本記事の範囲を超えたトピックなので、ここでは扱いません。
前述の 2 つのハウスキーピング メソッドは、descriptor と SetService です。descriptor は、コントローラが、どのインプリメンテーションが自分自身を登録したかを表示するために使用するオブジェクトの単純な記述です。SetServiceは、サービスに似た総称操作を公開することで、コン
トローラ インターフェイスを関連付けるために使われるメソッドです。
われわれのサンプル アプリケーションのコントローラは、データ レコードセットに似たサービスを公開します。コンポーネントが操作を実行すると、結果として得られたデータはサービス インターフェイスを通してコントローラに格納されます。このため、第 2 のインターフェイスは次のように定義されます。
[
object,
uuid(EA55BFDD-BC3E-11d1-9484-00A0247D759A),
pointer_default(unique),
local,
version(1.0)
]
interface IControllerService : IUnknown
{
HRESULT Reset();
HRESULT Rewind();
HRESULT MoveNext();
HRESULT GetColumn( BSTR colName, [out,retval]BSTR *value);
HRESULT SetColumn( BSTR colName, BSTR value);
HRESULT RecordReference( [out,retval]long *retval);
HRESULT Add();
HRESULT Update();
}
レコードセットについての知識がある方は、上記のサービス インターフェイスに似た要素がいくつもあることがわかるでしょう。基本的な操作には以下のものがあります。
- Reset: コントローラ内のレコードセットを空にします。
- Rewind: ポインタをコントローラ内の最初のレコードセットに移動します。
- MoveNext: コンポーネント レコードセット ポインタを次の位置に移動します。
- GetColumn, SetColumn: コントローラ上のデータの取得と設定を行います。
- RecordReference: 後に参照するためにブックマークを取得します。
- Add: レコードセットにレコードを追加します。
- Update: レコードセット内のデータを更新します。また、コントローラが UI またはリモート データ ソースにアクセスするために必要な任意の操作をサポートします。
上記の 2 つのインターフェイスをベースに、コントローラはコンポーネントとして、またコンポーネントはコントローラとして機能することが可能になっています。また、コンポーネントとコントローラは任意の言語で書くことができます。このように、Separating Format from Logic デザイン パターンはオープンなアーキテクチャ ソリューションを提供します。
2 つのインターフェイスは、プロジェクトの CommonInterface の一部です。これは TimeCard.idl ファイルだけを含んでいるプロジェクトです。IDL ファイルは、オブジェクトのインターフェイスを定義する一連の「プロトタイプ」を含んでいます。このファイルはMidl.exeによってコンパイルされ、いくつかのタイプのファイルを生成します。次のコマンド ラインは IDL の典型的な使用方法を示しています。
midl /h TimeCard.h /iid TimeCard_i.c TimeCard.idl
第 1 オプションの/hは、インターフェイスを C++ で定義するために使用されるヘッダー ファイルを作成します。ヘッダー ファイル (TimeCard_i.c) は、すべてのIID(uuid) をexternaL として宣言するので、IIDが実際にインプリメントされるファイルが必要となります。第 2 オプションの/iidは IID 定義ファイルを作成します。最後に作成されるファイル (コマンド ラインでは指定されていません) はタイプ ライブラリ (.tlb) です。このファイルは、コンポーネントが C++ 以外の言語で書かれる場合にのみ使用されます。タイプ ライブラリを使用する言語の例としては、Microsoft VisualBasic 開発システムがあります。
ステップ 2: ロジック コンポーネントを構築する
ビジネス ロジックを構築するときには、高機能のコントロールまたはコンポーネントを生成するべきかという問題があります。これに対する答えは、どちらでもよいというものです。タイムカードのレガシー アプリケーションを例として使うとすると、唯一重要な側面は、コントロールまたはコンポーネントが ITimeCard インターフェイスをインプリメントしているかどうかということだけです。デザイン パターンの要件に従い、UI はありません。このため、ここではコンポーネントが使用されます。コンポーネントの開発には Active Template Library(ATL) シンプル オブジェクト ウィザードが使用され、スクリプティングによる関連付けをサポートするためにデュアル インターフェイスを持たせる必要があります。
ATL オブジェクトの定義
次の例は、タイムカード アプリケーションの ATL オブジェクト定義に加えなくてはならない変更点を示しています。
class ATL_NO_VTABLE CVacation :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl,
public ITimeCard
{
public:
CVacation();
virtual ~CVacation();
DECLARE_REGISTRY_RESOURCEID(IDR_VACATION)
BEGIN_COM_MAP(CVacation)
COM_INTERFACE_ENTRY( IVacation)
COM_INTERFACE_ENTRY( IDispatch)
COM_INTERFACE_ENTRY( ITimeCard)
END_COM_MAP()
// IVacation
public:
STDMETHOD(SetCommentElement)(IUnknown *component);
// ITimeCard
public:
STDMETHOD( PunchIn)( BSTR time, long *retVal);
STDMETHOD( PunchOut)( BSTR time, long *retVal);
STDMETHOD( Descriptor)( BSTR *description);
STDMETHOD( SetService)( IUnknown *serviceProvider);
private:
bool m_punchedIn;
IControllerService *m_service;
MSHTML::IHTMLInputTextElement *m_comment;
};
強調表示されているのは、ITimeCard インターフェイスに固有の項目です。また、ここではオリジナル コードに 3 つの要素が追加されています。
- 継承チェーンにpublic ITimeCard を追加しています。ITimeCard は、IDL ファイルで定義されたインターフェイスに似た一連の仮想関数を定義します。
- COM マップに COM_INTERFACE_ENTRY マクロを追加しています。このマクロはインターフェイスをコンポーネントの一部として追加します。QueryInterface(QI)と IID_ITimeCard インターフェイスが参照されると、QI の ATL によるインプリメンテーションは COM マップを調べて、インターフェイスが実際に使用可能であるかどうかを確認します。
- ITimeCard が期待している必要なメソッドを追加します。ここではこのステップを無視していますが、仮想関数をインプリメントする必要があるため、コンポーネントは正常にコンパイルできません。
サービス インターフェイスの設定
コンポーネントは、Web ページ上でスクリプトをインスタンス作成するときに、コンポーネントをコントローラに登録します。これを受けて、コントローラはSetServiceメソッドを呼び出して、コンポーネントに自分のインターフェイスを登録します。ただし、このときに渡されるパラメータは IUnknown です。カスタム インターフェイスを直接に渡すという方法も考えられますが、IUnknown を渡す方が簡単です。IUnknown を使うことには、デカップリングをさらに押し進め、インターフェイス ポインタを任意のものにすることができるという利点があります。このようにデカップリングを行うことで、コンポーネントは最も適切なインターフェイスを選択できるようになります。
サービス インターフェイスは次のように設定されます。
HRESULT CVacation::SetService(IUnknown *serviceProvider)
{
try
{
_com_util::CheckError( serviceProvider->QueryInterface
( IID_IControllerService, (void **)&m_service));
}
catch ( _com_error err)
{
::MessageBox( NULL, err.ErrorMessage(),
"registerInterface Error is", MB_OK);
};
return S_OK;
}
クラス宣言の中には IControllerService へのポインタがあります。このポインタは、QI で IID_IControllerService を照会することによって取得されます。QI プロセス全体が例外ブロックで囲まれています。例外ブロックは、コントロールのサイズを20 KB増やすと言われています。しかし、例外をベースにしたコードは、サイズをいくぶん大きくするにしても、予期しない事態に対処するための単純なアプローチを提供してくれます。serviceProvider->QueryInterface は関数 _com_util::CheckError の中に埋め込まれています。この関数はドキュメントには記載されていませんが、HRESULT が有効であることを確認してくれる便利な関数です。HRESULT が有効でなければ、COM 例外が発生します。_com_errorオブジェクトは、何がおかしくなったのかを分析するのに利用できる有益なエラー処理オブジェクトです。catch ブロックの中の MessageBox は COM エラー文字列を表示します。
操作の実行
サービス インターフェイスが取得されたら、操作を実行し、結果として得られたデータをコントローラに格納することができます。われわれのタイムカード アプリケーションの例では、実行可能な操作はパンチインとパンチアウトです。PunchIn メソッドは次のようにインプリメントされます。
HRESULT CVacation::PunchIn( BSTR time, long *retVal) {
*retVal = 0;
try {
if( m_punchedIn == true) {
*retVal = 4;
return S_OK;
}
if( m_comment == NULL) {
*retVal = 3;
return S_OK;
}
_bstr_t strComment = m_comment->Getvalue();
if( strComment.length() == 0) {
*retVal = 2;
return S_OK;
}
m_service->Add();
// I put this into brackets so it will create and
// destroy the class. Note this is does not create
// extra memory because the VC++ catches this and
// optimizes. Neat trick, eh?
{
_bstr_t col( "TimeIn");
m_service->SetColumn( col, time);
}
{
_bstr_t col( "TimeOut");
_bstr_t value("");
m_service->SetColumn( col, value);
}
{
_bstr_t col( "Empty column");
_bstr_t value("");
m_service->SetColumn( col, value);
}
{
_bstr_t col( "Comment");
m_service->SetColumn( col, strComment);
}
m_service->Update();
m_punchedIn = true;
} catch ( _com_error err) {
::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
*retVal = 1;
};
return S_OK;
}
このメソッドの機能を確認するときには、これがロジックをインプリメントする操作を実行している点に注意してください。このメソッドは、最初のステップとして、その人がすでにパンチインを行ったかどうかを確認します。パンチインを行っていた場合には、メソッドは何もせずに返ります。次に、UI に UI 要素が関連付けられているかどうかを確認します (m_comment)。これが NULL ならば、関連付けは存在していないので、メソッドは先に進めません。この要素が有効ならば、現在の値が取得されます (m_comment->Getvalue())。最後に、値をチェックして、それが空でないことを確認します。
すべての操作が完了したら、次のステップとして、結果として得られたデータをコントローラに保存します。データのコントローラへの保存は、レコードを追加し、情報を特定の列に保存するという簡単な手順で行えます。すべてのデータが追加されたら、レコードが更新されます。これでコンポーネントの機能は終わりです。
ダイナミック HTML モデルの使用
前に述べたように、UI のコンポーネントへのバインドはスクリプトによって行われます。この機能の例を、次のダイナミック HTML コードに示します。
<OBJECT ID="TimeVacation" WIDTH=1 HEIGHT=1
CLASSID="CLSID:29F14E14-C10A-11D1-9486-00A0247D759A">
<PARAM NAME="_Version" VALUE="65536">
<PARAM NAME="_ExtentX" VALUE="2646">
<PARAM NAME="_ExtentY" VALUE="1323">
<PARAM NAME="_StockProps" VALUE="0">
</OBJECT>
<div class="components" id=secVacation style="visibility:hidden">
<b>Vaction Options</b>
<table>
<tr>
<td>Comment:</td>
<td><input id="txtVacComment" type="text" name="txtComment"
size="20"></td>
</tr>
</table>
…
</div>
<script language=javascript>
function WindowOnLoad() {
Controller.RegisterComponent( TimeWorking);
Controller.RegisterComponent( TimeVacation)
TimeWorking.SetCommentElement( txtWorkComment);
TimeWorking.SetProjectElement( optProjects);
TimeVacation.SetCommentElement( txtVacComment);
Controller.activeInterface( TimeWorking);
}
この例の中の、強調表示されている3 つのフィールドは、ダイナミック HTML を示しています。第 1 の強調表示されているフィールドは、これまで説明してきたCOM コンポーネントを定義しています。第 2 のフィールドはダイナミック HTML の入力要素を定義しています。この UI 要素は、第3のフィールドで定義されているスクリプティングを使って、コンポーネントに関連付けられます。コンポーネントが単純なオブジェクト参照によって渡されていることに注意してください。
次に、SetCommentElement のインプリメンテーションを示します。
STDMETHODIMP CVacation::SetCommentElement(IUnknown * inpElement)
{
try {
if( m_comment != NULL) {
m_comment->Release();
}
_com_util::CheckError( inpElement->
QueryInterface(__uuidof(MSHTML::IHTMLInputTextElement),
(void **)&m_comment));
} catch ( _com_error err) {
::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
};
return S_OK;
}
SetComment 要素は IUnknown(inpElement) として渡されます。Microsoft Internet Explorer では、すべてのダイナミック HTML 要素が Web ページ上で COM コンポーネントとして公開されます。Internet Explorerコンポーネントが別の COM コンポーネントに渡されると、それは参照し、操作することのできるCOM コンポーネントとなります。この参照モデル全体が、Mshtml.dlL ファイルに格納されています。Platform SDK の最新エディションにはヘッダーが含まれています。これ
よりも簡単に参照モデルにアクセスするには、COM コンパイラ サポートを次のように使用しま
す。
#import "mshtml.dll"
メソッドのインプリメンテーションに戻ると、IUnknown を IHTMLInputTextElement に変換する必要があります。これは、QI を実行し、__uuidof(MSHTML::IHTMLInputTextElement) を要求する
ことによって行われています。インターフェイスを取得したら、要素のメソッドにアクセスすることができます。ダイナミック HTML のメソッド名は C++ 環境では異なる名前になっていますが、どちらも同じメソッドを操作しています。たとえば、スクリプティングでは、データの設定と取得にメソッド名を使用します。一方、C++ では、これらの名前が Getvalue と Putvalue に変換されます。これらの名前は、どちらの環境でも同じメソッドにアクセスします。
ステップ 3: コントローラを構築する
デザイン ソリューションの最後のステップでは、コントローラを構築します。コントローラの構築は、これまでの 2 つのステップよりも複雑なプロセスで、ある程度の総称的なコードを定義する必要があります。われわれのサンプルのコントローラはスレッド セーフではなく、堅牢でもないということに注意してください。これは、コントローラの構築方法の例として示しているのに過ぎません。このため、ここではデザイン パターンのインプリメントに直接関係する新しい概念のみについて
解説します。
コントローラは ATL ベースの Active ControL としてプログラミングされます。これはコンポーネントであってもかまわないのですが、コントロールを使用することにはいくつかの利点があります。第 1 の利点はユーザー フィードバックです。ユーザー フィードバックを何らかの UI 要素にリンクすることは可能ですが、そのためにはプログラミングに不要な負担がかかります。ユーザー フィードバックは、サーバーにリンクして、登録済みコンポーネントのカウントなどに利用することもできます。しかし、コントロールを使って高機能な UI を構築するときには、後の変更が不可能になるので、ユーザー フィードバックをサーバーに接続するのは避けるべきです。
コントローラは、使用される操作の機能を公開します。コントローラの定義を次に示します。
[
object,
uuid(29F14DFC-C10A-11D1-9486-00A0247D759A),
dual,
helpstring("IController2 Interface"),
pointer_default(unique)
]
interface IController2 : IDispatch
{
[id(1), helpstring("method ")] HRESULT RegisterComponent
(IUnknown *component);
[id(2), helpstring("method PunchIn")] HRESULT PunchIn();
[id(3), helpstring("method PunchOut")] HRESULT PunchOut();
[id(4), helpstring("method ActiveInterface")] HRESULT
ActiveInterface(IUnknown *currInterface);
[id(5), helpstring("method ResetActiveInterface")] HRESULT
ResetActiveInterface();
};
これらのメソッドは、スクリプティングからコントローラを利用できなくてはならないので、デュアルインターフェイスに対応しています。パンチインとパンチアウトの 2 つのメソッドがあり、これに加えてハウスキーピング用のメソッドが存在します。これらの操作は、単に呼び出しをコンポーネントに中継するだけなので、詳しく説明する必要はないでしょう。ここで説明する必要があるのは、ハウスキーピング用のメソッドです。
前に示したダイナミック HTML コードでは、スクリプトの最初の部分で、RegisterComponent を使ってコンポーネントをコントローラに登録していました。
STDMETHODIMP CController2::RegisterComponent(IUnknown * component)
{
IOleClientSite *site;
try {
// Set the client site
// The site is set everytime an interface is registered
// Sure its not the most efficient, but its simple and effective
GetClientSite( &site);
m_objectDHTML.setSite( site);
m_dataManager->setDHTMLReference( &m_objectDHTML);
m_dataManager->addComponent( component);
} catch ( _com_error err) {
::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
};
return S_OK;
}
ハウスキーピング操作には、ダイナミック HTML ポインタの取得と環境のセットアップの 2 つがあります。以下に、それぞれの操作について説明します。
ダイナミック HTML ポインタの使用
コントローラは、コンポーネントとは異なり、自分の UI の部品を探し出します。コントローラはUI を動的に変更するので、コントローラに関連付けられるUI の種類には、それほどの柔軟性はありません。UI の動的な変更は必須の条件ではありませんが、ダイナミック HTML のいくつかの便利な機能を示すために追加しています。
コントローラは、COM がコントロールをインスタンス作成する方法のおかげで、自分の環境を判定することができます。コントロールが作成され、実行されるときには、コンテナの中で実行されます。コンテナとコントロールは緊密な関係を持っており、互いに関する情報を交換します。Internet Explorer はこれと同じように動作します。Internet Explorer がコントロールをインスタンス作成するときのコンテナは Web ページです。この関係を利用して、コントロールは Web ページ上の他のコンポーネントや要素にアクセスできるのです。このアクセスは次のコードによって実現されます。
STDMETHODIMP CController2::RegisterComponent(IUnknown * component)
{
IOleClientSite *site;
try {
// Set the client site
// The site is set everytime an interface is registered
// Sure its not the most efficient, but its simple and effective
GetClientSite( &site);
m_objectDHTML.setSite( site);
m_dataManager->setDHTMLReference( &m_objectDHTML);
m_dataManager->addComponent( component);
} catch ( _com_error err) {
::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
};
return S_OK;
}
bool CDHtmlObjectModel::setSite(
LPOLECLIENTSITE clientsite) {
// The next step is a bit touchy because it attempts to retrieve the
// IWebBrowser2 interface from the container. We want to do this
// because once we have this interface we can do almost anything
try
{
IServiceProviderPtr spSP((LPOLECLIENTSITE)clientsite);
if( NULL == spSP) {
return false;
}
spSP->QueryService(__uuidof(SHDocVw::IWebBrowserApp),
__uuidof(SHDocVw::IWebBrowser2), (void**)&m_spWebBrowser);
if( m_spWebBrowser == NULL) {
return false;
}
m_spDocument2 = m_spWebBrowser->GetDocument();
}
catch(...)
{
return false;
}
return true;
}
コンポーネントがコントローラに登録されるときには、IOleClientSite インターフェイスを取得するための呼び出しが行われます。このサイトは、コントロールを保持しているコンテナです。
GetClientSite は ATL Control クラスが提供しているメソッドです。
CDHtmlObjectModel::setSite メソッドは、サイト インターフェイスを取り、WebBrowser インターフェイスを要求します。テストが try - catch ブロックに入っているということが重要です。しかし、この catch ブロックには省略記号が付いており、すべての例外をキャッチすることがわかります。
Visual C++ の例外処理は、一般保護違反 (GPF)、数値演算のオーバーフローなどを含めて、あらゆるタイプの例外をキャッチすることができます。このブロックは、サイト インターフェイスが WebBrowser インターフェイスを照会して、そのインターフェイスが存在しなかった場合に発生する例外である GPF をキャッチするように設計されています。さらに、サイトは QueryService メソッドをサポートしていない可能性があり、この場合にも GPF が発生します。最後のステップでは、m_spWebBrowser->GetDocument() 呼び出しを使って、ルートのダイナミック HTML ポインタを取得しています。これで、現在の Web ページを表すダイナミック HTML ツリーをナビゲートできるようになります。
環境のセットアップ
第 2 のハウスキーピング操作は、サービス インターフェイスのための環境のセットアップです。コントローラは、インスタンス作成を行うときに、CDataManager インスタンスを作成します。このオブジェクトは、サービス インターフェイスと、結果として得られるデータ コンポーネントを管理する責任を負っています。CControllerServiceImpl クラスは CConnector によって管理され、IControllerService をインプリメントしています。CConnector は、IControllerServiceImpl インターフェイスと ITimeCard インターフェイスを保持することを唯一の目的とする単純なクラスです。これらのすべてのクラスに、何らかのストレージを必要とする一連の配列があります。これは Standard Template Library(STL) ベクトル クラスによって提供され、次のように宣言されています。
std::vector< CRecord *> m_records;
標準テンプレート ライブラリを使用していることにより、要素の配列を管理し、保持するのが簡単になっています。STL をできる限り使うようにすることをお勧めします。ATL は、別の名前空間に格納されるので、STL と何の問題もなく共存することができます。STL についての解説は、本記事の範囲を超えていますが、msdn.microsoft.com/visualc/stl/ がリソースとして優れています。
レコードと UI について
コントローラの中の最後の興味深い要素は、UI の更新方法です。コンポーネントがIControllerService::Addメソッドを呼び出すと、m_records ベクトルに Record オブジェクトが追加されます。次に CRecord クラスの定義を示します。
struct _tagColumn {
char name[ 255];
char value[ 255];
};
class CRecord
{
public:
void addColumn( char *name, char *value);
void setColumn( char *name, char *value);
CRecord();
virtual ~CRecord();
MSHTML::IHTMLTableRowPtr m_row;
std::vector< struct _tagColumn *> m_columns;
private:
};
各レコード オブジェクトは、個々の列とその値を表すm_columnsのベクトルを含んでいます。これは最も効率的な方式とはいえませんが、カスタム レコードをサポートすることができます。もう 1 つの変数、m_rowは、Web ページ上の行への参照です。IControllerService::Add を使って行が作成されると、ダイナミック HTML の行が次のように作成されます。
STDMETHODIMP CControllerServiceImpl::Add() {
// Add this row and then add an empty record set
m_currRecord = new CRecord;
m_currRecord->m_row = m_parent->m_objectDHTML->
addTableRow( "tableTimeCard", "Work", "", "", "", "");
m_currRecord->setColumn("Type", "Work");
m_parent->m_records.push_back( m_currRecord);
m_iterator = m_parent->m_records.end();
return S_OK;
}
MSHTML::IHTMLTableRowPtr CDHtmlObjectModel::addTableRow(
char *table,
char *type,
char *inTime,
char *outTime,
char *project,
char *comment) {
// Retrieve all of the page elements
MSHTML::IHTMLTablePtr spTable;
MSHTML::IHTMLElementCollectionPtr spAllElements = m_spDocument2-
>Getall();
_variant_t vaTag( table);
if((spTable = spAllElements->item( vaTag)) != NULL) {
// We have found the table, so now add a row
MSHTML::IHTMLTableRowPtr spRow( spTable->insertRow( 1));
MSHTML::IHTMLTableCellPtr spType( spRow->insertCell( 0));
MSHTML::IHTMLTableCellPtr spTimeIn( spRow->insertCell( 1));
MSHTML::IHTMLTableCellPtr spTimeOut( spRow->insertCell( 2));
MSHTML::IHTMLTableCellPtr spProject( spRow->insertCell( 3));
MSHTML::IHTMLTableCellPtr spComment( spRow->insertCell( 4));
// Here is the compiler trick again
// If a series of variables are created
// that are identical in size, the memory will be
// reused and it will not cost an extra allocation
// Neat trick, eh!
{
MSHTML::IHTMLElementPtr spAnElement = spType;
_bstr_t bstrStr( type);
spAnElement->PutinnerText( bstrStr);
}
{
MSHTML::IHTMLElementPtr spAnElement = spTimeIn;
_bstr_t bstrStr( inTime);
spAnElement->PutinnerText( bstrStr);
}
{
MSHTML::IHTMLElementPtr spAnElement = spTimeOut;
_bstr_t bstrStr( outTime);
spAnElement->PutinnerText( bstrStr);
}
{
MSHTML::IHTMLElementPtr spAnElement = spProject;
_bstr_t bstrStr( project);
spAnElement->PutinnerText( bstrStr);
}
{
MSHTML::IHTMLElementPtr spAnElement = spComment;
_bstr_t bstrStr( comment);
spAnElement->PutinnerText( bstrStr);
}
return spRow;
} else {
MSHTML::IHTMLTableRowPtr spRow;
return spRow;
}
}
addTableRow メソッドは、Web ページ上にあるパラメータ (テーブル) をベースにしています。このテーブルは、ルートのダイナミック HTML ポインタを参照し、コレクションspAllElements->All() に対して指定された要素を問い合わせることによって検索されます。テーブルが見つかったら、行を挿入することができます (spTable->insertRow())。次に、表示する個々の要素について、セルが挿入されます (spRow->insertCell())。レコードがWeb ページ上の行への参照を保持しているのは、これによって行を新しい情報で更新するときに、個々の行とセルを探す必要がなくなるからです。
更新は、コンポーネントが IControllerService::Update を呼び出すときに、次のように実行されます。
STDMETHODIMP CControllerServiceImpl::Update() {
// This is to update the current record on the DHTML page
std::vector< CRecord *>::iterator i;
for( i = m_parent->m_records.begin(); i != m_parent->m_records.end();
i ++) {
m_parent->m_objectDHTML->updateTableRow( (*i)->m_row,
((*i)->m_columns)[ 0]->value, ((*i)->m_columns)[ 1]->value,
((*i)->m_columns)[ 2]->value, ((*i)->m_columns)[ 3]->value,
((*i)->m_columns)[ 4]->value);
}
return S_OK;
}
updateTableRowのインプリメンテーションは、最後の操作のセットの中の addTableRow に似ています。ここでのポイントは、更新プロセスを単純化することにあります。
一歩前に戻って
3 つのインプリメンテーション ステップ、すなわちインターフェイスの定義、ロジック コンポーネントの構築、およびコントローラの構築によって、Separating Format from Logic デザイン パターンは完成します。しかし、デザイン パターンの問題が解決されたことは、どうすればわかるので
しょうか? われわれが行った作業は、動的な関連付けを持つデカップリングされたインターフェイスというデザイン目標を達成したのでしょうか? 以下に、これらの目標について検討します。
動的な関連付け
インターフェイスとロジックの間の動的な関連付けはダイナミック HTML スクリプティングによって実現されています。ダイナミック HTML により、インターフェイスは個々の部品の組み合わせによって構築されるため、UI を変更しながら、同じロジックを実行させることが可能になります。つまり、ロジックに任意の言語をカップリングできるということです。
デカップリング
コントローラは特定のサービス インターフェイス、IControllerService を公開しなければならず、コンポーネントはインターフェイス ITimeCard を公開しなくてはなりません。元のインターフェイスがそのままの形で残っている限り、コンポーネントとコントローラは、別のコントローラまたはコンポーネントと通信を行うように更新することができます。つまり、変更を加える必要が生じたときには、部品ごとに変更していくことができます。
結論
Separating Format from Logic デザイン パターンの要件は満たされ、細かい粒度を持つ、より単純な UI が実現されました。さらに、インターフェイスとロジック コンポーネントは互いに独立しているので、今後もアプリケーションを簡単に強化していくことができます。
プロジェクトをビルドし、テストするには、次の操作を行います。
- プロジェクト timetracker/timetracker.dsw をロードします。
- 次のものをビルドします (順序は重要です):
a. CommonInterface
b. Controller
c. TimeVacation
d. TimeWorking
- Web ページtimetracker/testpage.htmをロードして、コンポーネントとコントローラをロードします。
関連情報
Microsoft Visual C++ 開発システムの最新情報については、
World Wide Web サイト http://msdn.microsoft.com/visualc/ (英語サイト) または
http://www.microsoft.com/japan/msdn/visualc/ (日本語サイト) を参照してください。
Christian Gross は、企業顧客向けにインターネット関連のコンサルティングとトレーニング サービスを提供している euSOFT 社に籍をおくパートナーで、アーキテクチャの問題を専門としています。Gross は、Visual C++ Developers Conference、DeveloperDays、TechEd、および PDC などの Microsoft カンファレンスでよく講演を行っています。また、MIND や Basic Pro などのプログラマ向けの出版物の記事や、Microsoft のホワイト ペーパーを多数執筆しています。