Share via



January 2009

Volume 24 Number 01

Silverlight - Silverlight を使用して基幹業務エンタープライズ アプリケーションを構築する (第 1 部)

Hanu Kommalapati | January 2009

この記事では、次の内容について説明します。

  • Silverlight のランタイム環境
  • Silverlight の非同期プログラミング
  • ドメイン間ポリシー
  • サンプル エンタープライズ アプリケーション
この記事では、次のテクノロジを使用しています。
Silverlight 2

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

目次

Silverlight の基本: CoreCLR
Silverlight ランタイム
アプリケーションのシナリオ
ソケット サーバーを使用したプッシュ通知
非同期 I/O ループ
Silverlight のモーダル ダイアログ ボックス
プッシュ通知の実装
TCP サービスのドメイン間アクセス
TCP サービスでのドメイン間ポリシー
まとめ

先日、Silverlight がビジネスにもたらすものというテーマで、ヒューストンのある大企業に対してエグゼクティブ ブリーフィングを行ったのですが、出席者の反応はあまり芳しくありませんでした。それまで筆者は、DeepZoom、Picture-In-Picture、および HD 品質のビデオを紹介するクールなデモと、クオリティの高いアニメーションを見せれば、簡単に出席者の心をつかむことができるだろうと考えていました。そこで、あまり興味をそそられない理由を出席者に答えてもらったところ、グラフィックはすばらしいが、企業での使用に堪えるデータ中心の基幹業務 (LOB) アプリケーションを Silverlight を使用して構築するときに、利用できる実用的なガイダンスが非常に少ないということがわかりました。

今日、エンタープライズ クラスのアプリケーションでは、ネットワーク境界を越えて (そして多くの場合、インターネット上で) LOB 情報を安全に送信できなければなりません。また、ロールベースの UI や、ビジネス コンテキストに合わせたデータのトリミングも不可欠です。クライアントで Silverlight を実行し、サーバーで Microsoft .NET Framework 3.5 を実行することで、このようなスケーラビリティと安全性を兼ね備えた LOB アプリケーションを構築するための優れた機能を手に入れることができます。サンドボックス内で実行される軽量の Silverlight ランタイムには、バックオフィス データ サービスと統合できるフレームワーク ライブラリが用意されています。Silverlight を使用して堅牢なアプリケーションを構築するためには、アーキテクトと開発者は、Silverlight のプログラミング モデルとそのフレームワーク機能を現実のアプリケーションのコンテキストに即して理解する必要があります。

この記事では、LOB シナリオを取り上げ、アプリケーションを一から構築します。またその過程で、Silverlight での開発のさまざまな側面を紹介します。ここで説明するソリューションは、コール センター アプリケーションです。その論理アーキテクチャを図 1 に示します。今回は、スクリーン ポップ通知、非同期プログラミング モデル、Silverlight のダイアログ ボックス、およびドメイン間 TCP ポリシー サーバーの実装について重点的に取り上げます。第 2 部では、アプリケーションのセキュリティ、Web サービスの統合、アプリケーションのパーティションのほか、アプリケーションの数多くの側面について説明します。

Silverlight コール センターの論理アーキテクチャ

図 1 Silverlight コール センターの論理アーキテクチャ

Silverlight の基本: CoreCLR

まず、Silverlight の基本をおさらいしましょう。最初に Silverlight ランタイムの詳細を見ていきます。これにより、Silverlight では何が可能なのかを明確にすることができます。CoreCLR は、Silverlight によって使用される仮想マシンです。CoreCLR は .NET Framework 2.0 以降で使用される CLR と似ており、同様の型読み込みおよびガベージ コレクション (GC) システムが含まれています。

CoreCLR には、非常にシンプルなコード アクセス セキュリティ (CAS) モデルがあります。Silverlight ではアプリケーション レベルでのみセキュリティ ポリシーを適用すればよいため、このモデルはデスクトップ CLR のモデルよりもシンプルになっています。これは、プラットフォームに依存しない Web クライアントの場合、特定の有効なエンタープライズ ポリシーまたはコンピュータ ポリシーに依存できないうえに、ユーザーに既存のポリシーの変更を許可してはならないためです。ただし、OpenFileDialog や IsolatedStorage (ストレージのクォータの変更) などの例外もあります。これらの場合、サンドボックスの既定のルール セットを変更するためには、ユーザーの明示的な同意が必要です。OpenFileDialog はファイル システムへのアクセスに使用されます。一方、IsolatedStorage は分離ストレージにアクセスし、ストレージのクォータを増やすために使用されます。

デスクトップ アプリケーションの場合、各実行可能ファイルは CLR のコピーを 1 つだけ読み込みます。また、OS のプロセスで実行されるアプリケーションは 1 つだけです。各アプリケーションには、システム ドメイン、共有ドメイン、既定のドメイン、および明示的に作成された多数の AppDomain があります (「JIT and Run: .NET Framework の内部: CLR がランタイム オブジェクトを作成するしくみ」を参照)。同様のドメイン モデルは CoreCLR にもあります。Silverlight の場合、異なるドメインに属していることもある複数のアプリケーションが同じ OS のプロセスで実行されます。

Internet Explorer 8.0 では、各タブは独自の分離プロセスで実行されます。したがって、同じタブ内にホストされている Silverlight アプリケーションは、いずれも同じ CoreCLR インスタンスのコンテキストで実行されます (図 2 を参照)。各アプリケーションは、それぞれ異なる起点のドメインに属している可能性があるため、セキュリティ上の理由から、独自の AppDomain に読み込まれます。そのため、Silverlight アプリケーションをホストしているタブと同数の CoreCLR インスタンスが存在することになります。

デスクトップ CLR の場合と同様に、各 AppDomain は独自の静的変数のプールを取得します。ドメイン固有の各プールは、AppDomain のブートストラップ プロセス中に初期化されます。

fig02.gif

図 2 各 Silverlight アプリケーションは独自の AppDomain で実行される

Silverlight アプリケーションは、独自のカスタム アプリケーション ドメインを作成することはできません。この機能は内部使用のために予約されています。CoreCLR の詳細については、CLR チームによる「CLR 徹底解剖」コラム (「CoreCLR を使用して Silverlight のプログラムを記述する」および「Silverlight 2 のセキュリティ」) を参照してください。

Silverlight ランタイム

Silverlight は、必要なフレームワークとライブラリのサポート レベルが一様でない、幅広いアプリケーション向けに設計されています。たとえば、シンプルなアプリケーションの場合、辞書サイト上で単語の発音を示すために数バイトのオーディオ ファイルを再生する機能や、バナー広告を表示する機能だけが必要なものもあります。一方、エンタープライズ クラスの LOB アプリケーションには、セキュリティ保護、データのプライバシー保護、状態管理、他のアプリケーションやサービスとの統合、インストルメンテーションのサポートなどの機能が必要です。それと同時に、Silverlight のランタイムは、インターネット上で展開したときに接続が遅くならないように、サイズが小さくなければなりません。

これらの要件は競合するように思えますが、Silverlight チームは、フレームワークを図 2 に示すような階層型ビューに分割することでこの問題に対処しています。CoreCLR と Silverlight ランタイムの組み合わせは "プラグイン" と呼ばれ、ユーザーがアプリケーションを実行するためには必ずインストールする必要があります。ほとんどのコンシューマ中心のアプリケーションでは、このプラグインで十分です。アプリケーションで SDK ライブラリ (WCF の統合、または IronRuby などの DLR ランタイム) またはカスタム ライブラリを使用する必要がある場合は、Silverlight が実行時に必要な型を解決する方法を認識できるように、アプリケーションでこれらのコンポーネントを XAP パッケージにパッケージ化する必要があります (XAP の詳細については、今月号の「Cutting Edge」を参照してください)。

Silverlight ランタイムのサイズはおよそ 4 MB で、agcore.dll、coreclr.dll などの CoreCLR ライブラリのほか、アプリケーション開発者が必要とするライブラリ (mscorlib.dll、System.dll、System.Net.dll、System.Xml.dll、System.Runtime.Serialization.dll などの基本的なライブラリ) が含まれています。ブラウザ プラグインをサポートするランタイムは、通常は C:\Program Files\Microsoft Silverlight\2.0.30930.0\ ディレクトリにインストールされます。このディレクトリは、Web ブラウズ セッションで Silverlight をコンピュータにダウンロードおよびインストールするときに作成されます。

開発者が同じコンピュータ上でアプリケーションを構築およびテストする場合、ランタイムの 2 つのコピーを使用します。プラグインによってインストールされるコピーと、SDK のインストール時にインストールされるコピーです。後者は C:\Program Files\Microsoft SDKs\Silverlight\v2.0\Reference Assemblies ディレクトリにあります。このコピーは、コンパイル時の参照リストの一部として Visual Studio テンプレートで使用されます。

Silverlight アプリケーションは、サンドボックスのために、ほとんどのローカル リソースを操作できません。これは、一般的な Web アプリケーションに当てはまる制約です。既定では、Silverlight アプリケーションでは、ファイル システム (分離ストレージ以外) へのアクセス、ソケット接続、コンピュータに接続しているデバイスとの通信、ソフトウェア コンポーネントのインストールは不可能です。そのため、開発者が Silverlight プラットフォーム上で構築できるアプリケーションの種類は限られます。ただし、Silverlight には、バックエンドのビジネス プロセスやサービスと統合する必要のある、エンタープライズ クラスのデータ駆動型 LOB アプリケーションを開発するうえで必要な要素はすべて揃っています。

アプリケーションのシナリオ

ここで構築する LOB アプリケーションでは、サードパーティの通話コントロール アーキテクチャ (集中管理サーバーで構内交換機 (PBX) インフラストラクチャを活用し、電話を中央制御するアーキテクチャ) を示します。ここでは UI の外観としての Silverlight について重点的に説明したいので、テレフォニーの統合については軽く触れるだけにしておきます。そこで、着信イベントを生成するためにシンプルな通話シミュレータを使用します。このシミュレータは、通話のデータ パケットをコール マネージャの待機キューに送信します。これにより、このプロジェクトの最も重要なプロセスがトリガされます。

この記事のシナリオで必要なのは、デスクトップ アプリケーションとしてさまざまなユーザー操作が可能な、Web ブラウザ内部でプラットフォームに依存しない方法で実行されるコール センター アプリケーションだとしましょう。Windows 以外のクライアント環境では ActiveX はあまり使用されないため、Silverlight を使用することにします。

アプリケーションのアーキテクチャの側面を見てみましょう。ここでは、プッシュ通知、イベントの統合、ビジネス サービスの統合、キャッシュ、セキュリティ、およびクラウド サービスとの統合を実装します。

プッシュ通知: この機能は必須です。このシステムでは、"スクリーン ポップ" を行うために (つまり、着信情報が含まれる UI 画面を生成するために)、着信イベントをキャプチャし、発信者が入力した対話型音声応答 (IVR) データを転送する必要があるためです。また、ユーザーが通話を承諾または拒否できなければなりません。

イベント ストリーミング: 一般的な Web アプリケーションでは、Web サーバーはあらゆるビジネス イベントを把握します。ビジネス プロセスの大半を実行するのが Web サーバーであるためです。ただし、RIA (Rich Internet application) の場合は、Web ブラウザ内部で実行されているアプリケーションと、ビジネス Web サービスを実装しているサーバーの両方がビジネス プロセスの実装を共有します。そのため、Silverlight アプリケーション内で生成されたテクノロジ イベントとビジネス イベントの両方を、一連の特別な Web サービスを通じてサーバーに送信する必要があります。

このソリューションでのビジネス イベントは、ユーザー (担当者) による通話の拒否 ("担当者が通話を拒否した") または通話の承諾 ("担当者が通話を承諾した") です。代表的なテクノロジ イベントは、"コール マネージャ TCP サーバーへの接続の失敗" や "Web サービスの例外" です。

ビジネス サービスの統合: LOB アプリケーションと同じように、コール センター ソリューションは、リレーショナル データベースに格納される可能性のあるデータと統合する必要があります。この統合の手段として Web サービスを使用します。

キャッシュ: より良いユーザー エクスペリエンスを提供するために、情報をディスクだけでなくメモリにもローカルにキャッシュします。キャッシュする情報は、担当者用プロンプタ スクリプトの XML ファイルや、頻繁に変更されることのないその他の参照データなどです。

セキュリティ アプリケーション: このタイプのアプリケーションでは、セキュリティが最も重要な要件です。セキュリティを確保するための手段には、認証、承認、転送中のデータおよび非アクティブなデータのプライバシー保護、ユーザー プロファイルに基づくデータ トリミングなどがあります。

クラウド サービスとの統合: ストレージ サービスなどのクラウドベースの基本サービスと統合するには、サーバー側の特別なインフラストラクチャが必要になります。これは、アカウンタビリティとサービス レベルに応じてクラウド サービスの使用を詳細に監視し、制限するためです。

ビジネス サービスとの統合、アプリケーション セキュリティ、Web サービスのドメイン間ポリシー、アプリケーションのパーティションについては、この記事の第 2 部で取り上げます。

ソケット サーバーを使用したプッシュ通知

スクリーン ポップは、コール センター アプリケーションの最も重要な要件の 1 つであり、通話のコンテキストをテレフォニー インフラストラクチャからエージェントの画面に転送するうえで不可欠です。転送される通話のコンテキストには、顧客が電話で話した内容 (IVR システムの場合) や入力した情報が含まれることがあります。

通知は、クライアント ポーリングとサーバー プッシュのどちらかの手段でブラウザ内の Silverlight アプリケーションに送信できます。ポーリングは実装がかなり簡単ですが、コール センターのシナリオではテレフォニー イベントとクライアント アプリケーション間で状態の同期が正確に行われる必要があるため、必ずしもお勧めできません。そのため、ここでは Silverlight ソケットを使用するプッシュ通知を使用します。

Silverlight の重要な機能の 1 つが、TCP ソケットとの通信です。セキュリティ上の理由から、Silverlight で接続できるサーバー ポートは 4502 ~ 4532 の範囲に制限されています。これは、サンドボックスで実装されている数多くのセキュリティ ポリシーの 1 つです。また、Silverlight はリスナになることができないため、受信ソケット接続を受け入れることができません。これもサンドボックスの重要なポリシーです。このような理由から、ポート 4530 でリッスンするソケット サーバーを作成し、接続のプールを保持することにします (各接続は、アクティブなコール センターの担当者を表します)。

Silverlight ソケット ランタイムは、すべてのソケット接続用のサーバーにもドメイン間オプトイン ポリシーを適用します。Silverlight アプリケーションのコードが許可されているポート番号 (ユーザー コードに対しては不透明) の IP エンドポイントに対する接続を開こうとすると、ランタイムは IP アドレスが同じでポート番号が 943 の IP エンドポイントへの接続を行います。このポート番号は Silverlight の実装に固定的に関連付けられているため、アプリケーションで構成したり、アプリケーション開発者が変更したりすることはできません。

図 1 で、ポリシー サーバーがアーキテクチャのどこに配置されるかを確認してください。Socket.ConnectAsync が呼び出されたときのメッセージ フロー シーケンスは、図 3 のとおりです。設計上、メッセージ 2、3、および 4 はユーザー コードに対して完全に不透明です。

fig03.gif

図 3 ソケット接続のために Silverlight ランタイムがドメイン間ポリシーを自動的に要求する

ポリシー サーバーは、コール マネージャ サーバーと同じ IP アドレスで実装する必要があります。両方のサーバーを単一の OS プロセスに実装することも可能ですが、わかりやすいように、別個のコンソール プログラムに実装することにします。これらのコンソール プログラムは簡単に Windows サービスに変換できるうえ、フェールオーバーのためにクラスタに対応させて、信頼性と可用性を確保することもできます。

非同期 I/O ループ

.NET Framework 3.5 では、新しい非同期プログラミング ソケット API が導入されました。これらは、Async() で終わるメソッドです。サーバーで使用するメソッドは、Socket.AcceptAsync、Socket.SendAsync、および Socket.ReceiveAsync です。Async メソッドは、I/O 完了ポートを使用することで、スループットの高いサーバー アプリケーション向けに最適化されています。また、再利用可能な SocketAsyncEventArgs クラスを通じて、送受信バッファ管理を効率的に行えるようになっています。

Silverlight では TCP リスナを作成できないため、その Socket クラスでサポートされるのは ConnectAsync、SendAsync、および ReceiveAsync だけです。Silverlight では非同期プログラミング モデルだけがサポートされます。これは、ソケット API だけでなく、あらゆるネットワーク対話にも当てはまります。

ここでは、クライアントだけでなくサーバーでも非同期プログラミング モデルを使用するため、設計パターンを利用できます。繰り返し使用されるのが、I/O ループという設計パターンです。この設計パターンは、すべての非同期操作に適用できます。まず、次に示すソケット受け入れループの通常の同期実行について見てみましょう。

_listener.Bind(localEndPoint); 
_listener.Listen(50); 
while (true) { 
     Socket acceptedSocket = _listener.Accept(); 
     RepConnection repCon = new RepConnection(acceptedSocket); 
     Thread receiveThread = new Thread(ReceiveLoop); 
     receiveThread.Start(repCon); 
}

同期受け入れは直感的で、プログラムの記述と管理が容易です。しかし、この実装は実際にはサーバー向けに拡張することができません。クライアント接続ごとに専用のスレッドがあるためです。やり取りの多い接続がいくつかあると、この実装はすぐに限界に達してしまいます。

Silverlight がブラウザのランタイム環境で適切に動作するように、リソースへの介入はできるだけ少なくする必要があります。前に示した "ソケット受け入れ" 擬似コード内のすべての呼び出しは、呼び出しが実行されるスレッドをブロックしてしまうため、スケーラビリティにはマイナスの影響があります。このような理由から、呼び出しのブロックに関する Silverlight の制約は非常に厳しく、事実、ネットワーク リソースの非同期操作しか許可されていません。非同期ループを使用する場合は、自分のメンタル モデルを調整し、頭の中であるメッセージ ボックスをイメージしてみる必要があります。それは、ループを機能させるために、内部に常に 1 つ以上の要求が必要なメッセージ ボックスです。

図 4 に受信ループを示します (詳細な実装はコード サンプルに含まれています)。前に示した同期ソケット受け入れ疑似コード内の while (true) ループのような、無限ループのプログラミング構成要素はありません。Silverlight 開発者にとって、この種のプログラミングに慣れることは必要不可欠です。受信ループでデータの受信を継続するためには、メッセージを受信して処理した後に、I/O 完了ポート (接続されたソケットに関連付けられているもの) に対するキュー内に 1 つ以上の要求が存在していなければなりません。図 5 に示す通常の非同期ループは、ConnectAsync、ReceiveAsync、および SendAsync に適用可能です。また、.NET Framework 3.5 を使用するサーバーでは、AcceptAsync にも適用できます。

public class CallNetworkClient 
{ 
     private Socket _socket; 
     private ReceiveBuffer _receiveBuffer; 
     public event EventHandler<EventArgs> OnConnectError; 
     public event EventHandler<ReceiveArgs> OnReceive; 
     public SocketAsyncEventArgs _receiveArgs; 
     public SocketAsyncEventArgs _sendArgs; 
     //removed for space 
     public void ReceiveAsync() 
     {
          ReceiveAsync(_receiveArgs); 
     } 

     private void ReceiveAsync(SocketAsyncEventArgs recvArgs) 
     { 
          if (!_socket.ReceiveAsync(recvArgs)) 
          { 
               ReceiveCallback(_socket, recvArgs); 
          } 
     } 

     void ReceiveCallback(object sender, SocketAsyncEventArgs e) 
     { 
          if (e.SocketError != SocketError.Success) 
          { 
               return; 
          } 

          _receiveBuffer.Offset += e.BytesTransferred; 
          if (_receiveBuffer.IsMessagePresent()) 
          { 
               if (OnReceive != null) 
               { 
                    NetworkMessage msg = NetworkMessage.Deserialize(_receiveBuffer.Buffer);
                    _receiveBuffer.AdjustBuffer(); 
                    OnReceive(this, new ReceiveArgs(msg)); 
               } 
          } 
          else 
          { 
               //adjust the buffer pointer e.SetBuffer(_receiveBuffer.Offset, _receiveBuffer.Remaining); 
          } 
          //queue an async read request ReceiveAsync(_receiveSocketArgs); 
     } 
     
     public void SendAsync(NetworkMessage msg) 
     {
           ... 
     } 

     private void SendAsync(SocketAsyncEventArgs sendSocketArgs) 
     { 
          ... 
     } 

     void SendCallback(object sender, SocketAsyncEventArgs e) 
     { 
          ... 
     } 
}

fig05.gif

図 5 非同期ソケット ループ パターン

図 4 に示した受信ループの実装における ReceiveAsync は、再入可能な ReceiveAsync(SocketAsyncEventArgs recvArgs) メソッドに対するラッパーです。このメソッドは、ソケットの I/O 完了ポート上の要求をキューに登録します。.NET Framework 3.5 で導入された SocketAsyncEventArgs は、Silverlight のソケットの実装で同様の役割を果たし、複数の要求間で再利用可能なため、ガベージ コレクションのチャーンを避けるうえで役立ちます。メッセージを抽出し、メッセージ処理イベントをトリガして、ループを継続するために次の受信アイテムをキューに格納するのは、コールバック ルーチンの役割です。

部分的なメッセージの受信に対処するために、ReceiveCallback は、別の要求をキューに格納する前にバッファを調整します。NetworkMessage は ReceiveArgs のインスタンスにラップされ、受信メッセージを処理するために外部のイベント ハンドラに渡されます。

部分的なメッセージがある場合は、それをバッファの先頭にコピーした後、NetworkMessage を完全に受信するごとにバッファはリセットされます。同様の設計はサーバーでも使用されますが、実際の実装では循環バッファが役立ちます。

"通話受け入れ" シナリオを実装するためには、拡張可能なメッセージ アーキテクチャを作成する必要があります。このアーキテクチャでは、新しいメッセージごとにシリアル化ロジックを書き直すことなく、任意のコンテンツを含むメッセージをシリアル化または逆シリアル化できなければなりません。

fig06.gif

図 6 シリアル化された NetworkMessage 型のレイアウト

このメッセージ アーキテクチャは比較的単純です。NetworkMessage の各子オブジェクトは、インスタンス作成時にそのシグネチャを適切な MessageAction で宣言します。NetworkMessage.Serialize および Deserialize の実装は、Silverlight と NET Framework 3.5 (サーバー上) で機能します。これらにはソース コード レベルで互換性があるためです。シリアル化されたメッセージのレイアウトは、図 6 のようになります。

メッセージの先頭に長さを挿入する代わりに、適切なエスケープ シーケンスを用いて "開始" および "終了" マーカーを使用することもできます。バッファを処理する場合は、長さをメッセージにエンコードした方がはるかに簡単です。

シリアル化された各メッセージの最初の 4 バイトには、その後に続くシリアル化されたオブジェクトのバイト数が含まれます。Silverlight では、Silverlight SDK に含まれている System.Xml.dll の中にある XmlSerializer がサポートされています。このシリアル化コードは、コード サンプルに含まれています。このコードが子クラス (RegisterMessage や、UnregisterMessage および AcceptMessage を含む他のメッセージなど) に直接的には依存しないことにお気付きになるでしょう。一連の XmlInclude の注釈は、シリアライザが子クラスをシリアル化するときに .NET 型を適切に解決するうえで役立ちます。

NetworkMessage.Serialize および Deserialize は、図 4 の ReceiveCallback および SendAsync で使用されています。受信ループでは、実際のメッセージ処理は NetworkClient.OnReceive イベントにアタッチされているイベント ハンドラによって行われます。メッセージを CallNetworkConnection 内で処理することもできますが、メッセージを処理するための受信ハンドラを関連付けると、デザイン時にハンドラを CallNetworkConnection から切り離すことができるため、拡張性が高まります。

図 7 に、図 4 の CallNetworkClient を起動する Silverlight アプリケーションの RootVisual を示します。Silverlight コントロールは、すべて単一の UI スレッドにアタッチされており、コードがその UI スレッドのコンテキストで実行された場合にのみ、UI の更新が可能です。Silverlight の非同期プログラミング モデルでは、スレッド プールのワーカー スレッドでネットワーク アクセス コードと処理ハンドラを実行します。すべての FrameworkElement 派生クラス (Control、Border、Panel、ほとんどの UI 要素など) は、Dispatcher プロパティを (DispatcherObject から) 継承します。これにより、UI スレッドでコードを実行できます。

図 7 では、case MessageAction.RegisterResponse がエージェントのコール センターの勤務情報が含まれる UI を非同期デリゲートを通じて更新します。図 8 に、デリゲートの実行の結果更新された UI を示します。

 

図 7 受信メッセージを処理する Silverlight UserControl

public partial class Page : UserControl 
{ 
     public Page() 
     { 
          InitializeComponent(); 
          ClientGlobals.socketClient = new CallNetworkClient(); 
          ClientGlobals.socketClient.OnReceive += new EventHandler<ReceiveArgs>(ReceiveCallback); 
          ClientGlobals.socketClient.Connect(4530); //code omitted for brevity 
     } 
     
     void ReceiveCallback(object sender, ReceiveArgs e) 
     { 
          NetworkMessage msg = e.Result; ProcessMessage(msg); 
     } 

     void ProcessMessage(NetworkMessage msg) 
     { 
          switch(msg.GetMessageType()) 
          { 
               case MessageAction.RegisterResponse: 
                    RegisterResponse respMsg = msg as RegisterResponse; 
                    //the if is unncessary as the code always executes in the 
                    //background thread 
                    this.Dispatcher.BeginInvoke( delegate() 
                    { 
                         ClientGlobals.networkPopup.CloseDialog(); 
                         this.registrationView.Visibility = Visibility.Collapsed; 
                         this.callView.Visibility = Visibility.Visible; 
                         this.borderWaitView.Visibility = Visibility.Visible; 
                         this.tbRepDisplayName.Text = this.txRepName.Text; 
                         this.tbRepDisplayNumber.Text = respMsg.RepNumber; 
                         this.tbCallServerName.Text = respMsg.CallManagerServerName; 
                         this.tbCallStartTime.Text = respMsg.RegistrationTimestamp.ToString(); 
                    }); 
                    break; 

               case MessageAction.Call: 
                    CallMessage callMsg = msg as CallMessage; 
                    //Code omitted for brevity 
                    if (!this.Dispatcher.CheckAccess()) 
                    { 
                         this.Dispatcher.BeginInvoke( delegate() 
                         { 
                              ClientGlobals.notifyCallPopup.ShowDialog(true); 
                         }); 
                    } 
                    break; 

               // 
               //Code omitted for brevity 
               // 

               default: 
                    break; 
          } 
     } 
}

 

fig08.gif

図 8 担当者の初期登録画面

fig08.gif

図 9 コール センター サーバーへの登録中に表示される画面

Silverlight のモーダル ダイアログ ボックス

コール センターの担当者は、ログインするときに、コール センター サーバーに登録して勤務を開始するよう求められます。サーバーでの登録プロセスでは、担当者番号でインデックス付けされたセッションが保存されます。このセッションは、その後のスクリーン ポップやその他の通知に使用されます。図 8図 9 に、登録プロセスでコール センター アプリケーションの画面が変化している様子を示します。ここでは、ネットワークへの送信処理の進捗状況を示すモーダル ダイアログ ボックスを使用します。一般的なエンタープライズ LOB アプリケーションでは、モーダルおよび非モーダル ポップアップ ダイアログ ボックスがかなり自由に使用されます。Silverlight SDK には組み込みの DialogBox がないので、このアプリケーションで使用する DialogBox を Silverlight で作成する方法を説明します。

Silverlight が登場するまでは、モーダル ダイアログを構築するのは簡単ではありませんでした。キーボード イベントが UI に送信されるのを防ぐ簡単な手段がなかったためです。UserControl.IsTestVisible = false と設定することで、マウスとの対話は間接的に無効にできます。RC0 で Control.IsEnabled = false と設定すると、キーボード イベントまたはマウス イベントが UI コントロールによって受信されなくなります。既存のコントロール上にダイアログの UI を表示するには、System.Windows.Controls.Primitives.Popup を使用します。

図 10 に、抽象メソッドである GetControlTree、WireHandlers、および WireUI を含むベースの SLDialogBox コントロールを示します。これらのメソッドは、図 11 に示すように、子クラスによってオーバーライドされます。Primitives.Popup には、Popup がアタッチされるコントロール ツリーに属していないコントロール インスタンスが必要です。図 10 のコードでは、ShowDialog(true) メソッドがコントロール ツリー全体を再帰的に無効にします。これにより、ツリーに含まれているどのコントロールもマウス イベントまたはキーボード イベントを受信しません。ここでのポップアップ ダイアログは対話型にする必要があるので、Popup.Child を新しいコントロール インスタンスから設定しなければなりません。子クラスの GetControlTree の実装はコントロール ファクトリとして機能し、ダイアログの UI 要件に合ったユーザー コントロールの新しいインスタンスを提供します。

 

10 Silverlight のポップアップ DialogBox

using System; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Controls.Primitives; 
namespace SilverlightPopups 
{ 
     public abstract class SLDialogBox 
     { 
          protected Popup _popup = new Popup(); 
          Control _parent = null; 
          protected string _ caption = string.Empty; 
          public abstract UIElement GetControlTree(); 
          public abstract void WireHandlers(); 
          public abstract void WireUI(); 
          
          public SLDialogBox(Control parent, string caption) 
          { 
               _parent = parent; 
               _ caption = caption;
                _popup.Child = GetControlTree(); 
               WireUI(); WireHandlers(); 
               AdjustPostion(); 
          } 

          public void ShowDialog(bool isModal) 
          { 
               if (_popup.IsOpen) return; 
               _popup.IsOpen = true; 
               ((UserControl)_parent).IsEnabled = false; 
          } 

          public void CloseDialog() 
          { 
               if (!_popup.IsOpen) return; 
               _popup.IsOpen = false; 
               ((UserControl)_parent).IsEnabled = true; 
          } 

          private void AdjustPostion() 
          { 
               UserControl parentUC = _parent as UserControl; 
               if (parentUC == null) return; 
               FrameworkElement popupElement = _popup.Child as FrameworkElement; 
               if (popupElement == null) return; 
               Double left = (parentUC.Width - popupElement.Width) / 2; 
               Double top = (parentUC.Height - popupElement.Height) / 2; 
               _popup.Margin = new Thickness(left, top, left, top); 
          } 
     } 
}

 

図 11 NotifyCallPopup.xaml スキン

//XAML Skin for the pop up 
<UserControl xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Width="200" Height="95"> 
     <Grid x:Name="gridNetworkProgress" Background="White"> 
          <Border BorderThickness="5" BorderBrush="Black"> 
               <StackPanel Background="LightGray"> 
                    <StackPanel> 
                         <TextBlock x:Name="tbCaption" HorizontalAlignment="Center" Margin="5" Text="&lt;Empty Message&gt;" /> 
                         <ProgressBar x:Name="progNetwork" Margin="5" Height="15" IsIndeterminate="True"/> 
                    </StackPanel> 
                    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" > 
                         <Button x:Name="btAccept" Margin="10,10,10,10" Content="Accept" HorizontalAlignment="Center"/> 
                         <Button x:Name="btReject" Margin="10,10,10,10" Content="Reject" HorizontalAlignment="Center"/> 
                    </StackPanel> 
               </StackPanel> 
          </Border> 
     </Grid> 
</UserControl>

GetControlTree を実装して、アプリケーション パッケージにコンパイルされる Silverlight UserControl をインスタンス化することも、XamlReader.LoadControl を使用してコントロールを eXtensible Application Markup Language (XAML) ファイルから作成することもできます。通常、ダイアログ ボックスは、コンパイルされたハンドラを実行時にアタッチできるスキンで簡単に実装できます。図 11 に、btAccept ボタンと btReject ボタンのある XAML スキンを示します。Microsoft Expression Studio または Visual Studio でのデザイン作業後にクラスの属性 (<userControl class="AdvCallCenter.NotifyCallPopup"…>…</UserControl>) が XAML に残っている場合、LoadControl メソッドは例外をスローします。LoadControl を使用して正常に解析を行うためには、UI イベント ハンドラの属性をすべて削除する必要があります。

スキンを作成する場合は、Silverlight UserControl をプロジェクトに追加し、それを Expression でデザインし、XAML ファイルからのコントロールにアタッチされている "クラス" 属性とイベント ハンドラの名前 (ある場合) を削除します。図 12 に示すように、クリック ハンドラは子ポップアップ クラスに含めることができます。または、リフレクションを使用してコントロールに関連付けることのできる別個のハンドラ ライブラリを作成することもできます。

 

図 12 NotifyCallPopup の実装

public class NotifyCallPopup : SLDialogBox 
{ 
     public event EventHandler<EventArgs> OnAccept; 
     public event EventHandler<EventArgs> OnReject; 
     public NotifyCallPopup(Control parent, string msg) : base(parent, msg) { } 

     public override UIElement GetControlTree() 
     { 
          Return SLPackageUtility.GetUIElementFromXaml("NotifyCallPopup.txt"); 
     } 

     public override void WireUI() 
     { 
          FrameworkElement fe = (FrameworkElement)_popup.Child; 
          TextBlock btCaption = fe.FindName("tbCaption") as TextBlock; 
          if (btCaption != null) btCaption.Text = _caption; 
     } 

     public override void WireHandlers() 
     { 
          FrameworkElement fe = (FrameworkElement)_popup.Child; 
          Button btAccept = (Button)fe.FindName("btAccept"); 
          btAccept.Click += new RoutedEventHandler(btAccept_Click); 
          Button btReject = (Button)fe.FindName("btReject"); 
          btReject.Click += new RoutedEventHandler(btReject_Click); 
     } 

     void btAccept_Click(object sender, RoutedEventArgs e) 
     { 
          CloseDialog(); 
          if (OnAccept != null) OnAccept(this, null); 
     } 

     void btReject_Click(object sender, RoutedEventArgs e) 
     { 
          CloseDialog(); 
          if (OnReject != null) OnReject(this, null); 
     } 
}

ハンドラは、プロジェクトの依存関係の結果、XAP パッケージに自動的にコンパイルされるため、任意の Silverlight ライブラリ プロジェクトに含めることができます。スキン ファイルを XAP パッケージに含めるには、それらのファイルを XML ファイルとして Silverlight プロジェクトに追加し、その拡張子を XAML に変更します。既定のビルド アクションでは、拡張子が XAML のファイルはアプリケーション DLL にコンパイルされます。ここでは、これらのファイルをテキスト ファイルとしてパッケージ化するので、[プロパティ] ウィンドウで次の属性を設定する必要があります。

  • [ビルド アクション] = [コンテンツ]
  • [出力ディレクトリにコピー] = [コピーしない]
  • [カスタム ツール] = 既存の値を削除する

XAML パーサー (XamlReader.Load) は拡張子を識別しませんが、.xaml 拡張子を付けた方が内容が直感的にわかりやすくなります。SLDialogBox の役割は、ダイアログを表示することと閉じることだけです。子の実装は、アプリケーションのニーズに合わせてカスタマイズしてください。

プッシュ通知の実装

コール センター アプリケーションには、発信者の情報を表示するスクリーン ポップの機能が必要です。コール センターの担当者は、勤務を始める際に、コール センター サーバーへの登録を行います。プッシュ通知は、接続指向のソケットを使用して実装します。コール マネージャ サーバーの実装全体は図に示されていませんが、コード サンプルに含まれています。Silverlight クライアントがサーバーでソケット接続を行うと、新しい RepConnection オブジェクトが RepList に追加されます。RepList は、一意の担当者番号でインデックス付けされるジェネリック リストです。着信時には、このリストを使用して待機中の担当者を探し、RepConnection に関連付けられているソケット接続を使用してエージェントに通話情報を通知します。図 13 に示すように、RepConnection は ReceiveBuffer を使用します。

 

図 13 RepConnection は ReceiveBuffer を使用する

class SocketBuffer 
{ 
     public const int BUFFERSIZE = 5120; 
     protected byte[] _buffer = new byte[BUFFERSIZE] protected int _offset = 0; 
     public byte[] Buffer 
     { 
          get 
          { 
               return _buffer; 
           } 
          set 
          { 
               _buffer = value; 
          } 
     } 
     //offset will always indicate the length of the buffer that is filled 
     public int Offset 
     { 
          get 
          {
               return _offset ;
          } 
          set 
          { 
               _offset = value; 
          } 
     } 

     public int Remaining 
     { 
          get 
          { 
               return _buffer.Length - _offset; 
          } 
     } 
} 

class ReceiveBuffer : SocketBuffer 
{ 
     //removes a serialized message from the buffer, copies the partial message 
     //to the beginning and adjusts the offset 
     public void AdjustBuffer() 
     { 
          int messageSize = BitConverter.ToInt32(_buffer, 0); 
          int lengthToCopy = _offset - NetworkMessage.LENGTH_BYTES - messageSize; Array.Copy(_buffer, _offset, _buffer, 0, lengthToCopy); 
          offset = lengthToCopy; 
     } 

     //this method checks if a complete message is received public bool IsMessageReceived() 
     { 
          if (_offset < 4) return false; 
          int sizeToRecieve = BitConverter.ToInt32(_buffer, 0); 
          //check if we have a complete NetworkMessage 

          if((_offset - 4) < sizeToRecieve) return false; 
          //we have not received the complete message yet 
          //we received the complete message and may be more return true; 
     } 
}

ここでは、Silverlight 通話シミュレータを使用して通話を CallDispatcher._callQueue に送信し、スクリーン ポップ プロセスをトリガします。CallDispatcher はどの図にも示されていませんが、ダウンロード可能なコードに含まれています。CallDispatcher は、ハンドラを _callQueue.OnCallReceived にアタッチします。そしてシミュレータが ProcessMessage の実装内部の _callQueue にメッセージを登録すると、通知を受け取ります。前に説明したポップアップ ダイアログを利用して、クライアントは図 14 に示すような [Accept] (承諾) および [Reject] (拒否) の通知を表示します。実際の通知ダイアログを表示するのは、図 8 のコードに含まれている次のコード行です。

ClientGlobals.notifyCallPopup.ShowDialog(true);

fig14.gif

図 14 着信の通知

TCP サービスのドメイン間アクセス

メディアおよび広告表示アプリケーションとは違って、実際のエンタープライズ クラスの LOB アプリケーションは、さまざまなサービス ホスティング環境と統合する必要があります。たとえば、コール センター アプリケーションを Web サイト (localhost:1041 にホストされている advcallclientweb) でホストして、別のドメイン (localhost:4230) でステートフルなソケット サーバーを使用してスクリーン ポップを行い、さらに別のドメイン (localhost:1043) でホストされているサービスを通じて LOB データにアクセスするとしましょう。また、インストルメンテーション データを転送するために、さらに別のドメインを使用します。

Silverlight サンドボックスでは、既定では元のドメイン (advcallclientweb の localhost:1041) 以外のドメインへのネットワーク アクセスは許可されていません。このようなネットワーク アクセスが検出されると、Silverlight ランタイムはアクセス先のドメインで設定されているオプトイン ポリシーをチェックします。クライアントによって要求されるドメイン間ポリシーをサポートする必要のある一般的なサービス ホスティング シナリオは、次のとおりです。

  • クラウドにホストされたサービス
  • サービス プロセスにホストされた Web サービス
  • IIS または他の Web サーバーにホストされた Web サービス
  • XAML マークアップ、XAP パッケージなどの HTTP リソース
  • サービス プロセスにホストされた TCP サービス

HTTP リソースや IIS にホストされた Web サービス エンドポイント向けにドメイン間ポリシーを実装するのは容易ですが、その他のケースでは、ポリシーの要求/応答セマンティクスに関する知識が求められます。ここでは、TCP スクリーン ポップ サーバー (図 1 に示したコール マネージャ) に必要なポリシー インフラストラクチャを手早く実装してみます。他のドメイン間シナリオについては、この記事の第 2 部で取り上げます。

TCP サービスでのドメイン間ポリシー

Silverlight における TCP サービス アクセスは、すべてドメイン間要求と見なされます。サーバーでは、ポート 943 にバインドされている同じ IP アドレスで TCP リスナを実装する必要があります。図 3 に示したポリシー サーバーは、このために実装されたリスナです。このサーバーが Silverlight ランタイムに必要な宣言型のポリシーを送信するための要求/応答プロセスを実装すると、クライアントのネットワーク スタックでスクリーン ポップ サーバー (図 3 のコール マネージャ) に接続できるようになります。

わかりやすくするために、コール マネージャ サーバーをコンソール アプリケーションにホストします。このコンソール アプリケーションは、実際の実装用に Windows サービスに簡単に変換できます。図 3 に、ポリシー サーバーとの通常のやり取りが示されています。Silverlight ランタイムはポート 943 のサーバーに接続し、1 行のテキスト "<policy-file-request/>" を含むポリシー要求を送信します。

XML ベースのポリシーによって、図 3 に示すシナリオが可能になります。ソケット リソース セクションでは、許可されている範囲内 (4502 ~ 4534) のポートのグループを指定できます。範囲が限定されているのは、攻撃ベクトルを最小化することで、ファイアウォールの構成に予想外の弱点が生じるリスクを軽減するためです。コール センター サーバー (図 1 のコール マネージャ) はポート番号 4530 でリッスンするので、ソケット リソースは次のように構成します。

<access-policy> 
     <policy> 
          <allow-from> list of URIs</allow-from> 
          <grant-to> 
               <socket-resource port="4530" protocol="tcp"/>
          </grant-to> 
     </policy> 
</access-policy>

<socket-resource> で port="4502–4534" と指定して、指定可能なすべてのポート番号を許可することもできます。

手間を省くために、ポリシー サーバーを実装するときにコール マネージャ サーバーからコードを再利用します。Silverlight クライアントは、ポリシー サーバーに接続して要求を送信し、応答を読み取ります。ポリシー サーバーは、ポリシーの応答が正常に送信されたら、接続を閉じます。ポリシーのコンテンツは、ポリシー サーバーによってローカル ファイル (clientaccesspolicy.xml) から読み取られます。このファイルはダウンロードに含まれています。

ポリシー サーバーの TCP リスナの実装を図 15 に示します。この実装では、前に説明したのと同じ非同期ループ パターンを TCP 受信に使用しています。clientaccesspolicy.xml はバッファに読み込まれ、各 Silverlight クライアントに送信するときに再利用されます。ClientConnection は、受け入れられたソケットをカプセル化し、SocketAsyncEventArgs に関連付けられるバッファを受信します。

図 15 TCP ポリシー サーバーの実装

class TcpPolicyServer 
{ 
     private Socket _listener; 
     private byte[] _policyBuffer; 
     public static readonly string PolicyFileName = "clientaccesspolicy.xml"; 
     SocketAsyncEventArgs _socketAcceptArgs = new SocketAsyncEventArgs(); 
     public TcpPolicyServer() 
     { 
          //read the policy file into the buffer 
          FileStream fs = new FileStream(PolicyServer.PolicyFileName, FileMode.Open); 
          _policyBuffer = new byte[fs.Length]; 
          fs.Read(_policyBuffer, 0, _policyBuffer.Length); 
          _socketAcceptArgs.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptAsyncCallback); 
     } 

     public void Start(int port) 
     { 
          IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName()); 
          //Should be within the port range of 4502-4532 
          IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, port); 
          _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 

          // Bind the socket to the local endpoint and listen for incoming connections 
          try 
          { 
               _listener.Bind(ipEndPoint); 
               _listener.Listen(50); AcceptAsync(); 
          } 
          //code omitted for brevity 
     } 

     void AcceptAsync() 
     { 
          AcceptAsync(socketAcceptArgs); 
     } 

     void AcceptAsync(SocketAsyncEventArgs socketAcceptArgs) 
     { 
          if (!_listener.AcceptAsync(socketAcceptArgs)) 
          { 
               AcceptAsyncCallback(socketAcceptArgs.AcceptSocket, socketAcceptArgs); 
          } 
     } 

     void AcceptAsyncCallback(object sender, SocketAsyncEventArgs e) 
     { 
          if (e.SocketError == SocketError.Success) 
          { 
               ClientConnection con = new ClientConnection(e.AcceptSocket, this._policyBuffer); con.ReceiveAsync(); 
          } 

          //the following is necessary for the reuse of _socketAccpetArgs 
          e.AcceptSocket = null; 
          //schedule a new accept request 
          AcceptAsync(); 
     } 
}

図 15 に示したコード サンプルでは、複数の TCP 受信で SocketAsyncEventArgs を再利用しています。これを機能させるためには、AcceptAsyncCallback で e.AcceptSocket を null に設定する必要があります。このアプローチにより、高いスケーラビリティが求められるサーバーで GC チャーンを避けることができます。

まとめ

プッシュ通知の実装はコール センター アプリケーションの重要な側面であり、スクリーン ポップ プロセスを実現するための鍵です。Silverlight を使用すると、AJAX や類似のフレームワークを使用した場合に比べて、はるかに簡単にスクリーン ポップを実装できます。サーバーのプログラミング モデルとクライアントのプログラミング モデルは似ているため、ソース コード レベルでの再利用性をある程度活かすことができました。また、コール センターのケースでは、サーバーとクライアント双方の実装でメッセージ定義を使用し、バッファ アブストラクションを受信できました。

このシリーズの第 2 部では、業務用 Web サービスの統合、セキュリティ、クラウド サービスの統合、およびアプリケーションのパーティションを実装します。皆さんが LOB シナリオに対処するときに、このシリーズの内容がお役に立てばさいわいです。皆さんからのフィードバックをお待ちしています。

Silverlight ソケットの実装について詳細にご教示くださった、Microsoft の Dave Murray 氏と Shane DeSeranno 氏に感謝します。また、スクリーン ポップについて説明してくださった、コール センターのドメインに関する専門家、Robert Brooks 氏にもお礼を申し上げます。

Hanu Kommalapati は Microsoft のプラットフォーム戦略アドバイザであり、現在は企業顧客に対して Silverlight および Azure Services プラットフォーム上でスケーラブルな基幹業務アプリケーションを構築できるよう助言を行っています。