Silverlight

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

Hanu Kommalapati

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

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

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

目次

ビジネス サービスとの統合
サービス呼び出し
同期されたサービス呼び出し
メッセージ エンティティの変換
サービス呼び出し後の Silverlight の状態変更
ドメイン間ポリシー
IIS 外でホストされる Web サービスのドメイン間ポリシー
IIS 内でホストされる Web サービスのドメイン間ポリシー
アプリケーション セキュリティ
アプリケーションのパーティション
生産性を超えて

このシリーズの第 1 回では、コール センターのシナリオを紹介し、Silverlight でサポートされている非同期の TCP ソケットを利用する、接続されたソケット経由の画面の生成 (スクリーン ポップ) の実装を示しました (「Silverlight を使用して基幹業務エンタープライズ アプリケーションを構築する (第 1 部)」を参照)。

スクリーン ポップの実装には、シミュレートされた呼び出しディスパッチャを使用しました。ディスパッチャは、内部キューから通話を取り出し、サーバー上のジェネリック リストにキャッシュされている以前に受け入れられたソケット接続を介して通知を発行します。今回は、アプリケーション セキュリティの実装、ビジネス サービスとの統合、および Web サービスとアプリケーションのパーティション用のドメイン間ポリシーの実装を行って締めくくります。コール センター アプリケーションの論理アーキテクチャを図 1 に示します。認証サービスはユーティリティ サービスで実装し、ビジネス サービス (ICallService および IuserProfile) は、その名が示すとおり、ビジネス サービス プロジェクトで実装します。

fig01.gif

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

図にはユーティリティ サービスへのイベント ストリーミングが示されていますが、時間の関係で、ダウンロードできるデモにこの機能は含まれていません。イベントのキャプチャ サービス機能の実装は、ビジネス サービスの実装に似ています。異なるのは、重大なエラーではないビジネス イベントを分離ストレージにローカルにキャッシュし、バッチ モードでサーバーにダンプできることです。ビジネス サービスの実装の説明から始めて、アプリケーションのパーティションの説明で締めくくります。

ビジネス サービスとの統合

サービスとの統合は基幹業務 (LOB) アプリケーションの重要な側面の 1 つで、Silverlight には Web ベースのリソースやサービスにアクセスするためのコンポーネントがたくさん用意されています。HttpWebRequest、WebClient、および Windows Communication Foundation (WCF) のプロキシ インフラストラクチャは、HTTP ベースの相互運用に一般的に使用されるネットワーク コンポーネントの一部です。この記事では、バックエンドのビジネス プロセスとの統合に WCF を使用します。

ほとんどの場合、アプリケーション開発の過程で、バックエンドのデータ ソースとの統合に Web サービスが使用されます。Silverlight を使用した WCF Web サービス アクセスは、ASP.NET、Windows Presentation Foundation (WPF)、Windows フォーム アプリケーションなどの通常のアプリケーションを使用したアクセスとさほど変わりません。異なるのは、バインドのサポートと非同期プログラミング モデルの 2 点です。Silverlight でサポートされているのは basicHttpBinding と PollingDuplexHttpBinding のみで、HttpBinding は最も相互運用性の高いバインドです。そのため、この記事では、すべての統合にこのバインドを使用します。

PollingDuplexHttpBinding を使用すると、HTTP 経由で通知を発行するコールバック コントラクトを使用できます。前回紹介したコール センターで、スクリーン ポップ通知にこのバインドを使用することもできました。しかし、実装するには、サーバー上に HTTP 接続をキャッシュする必要があるため、Internet Explorer 7.0 などのブラウザで許可されている 2 つの同時 HTTP 接続のうちの 1 つが独占されてしまいます。Internet Explorer 8.0 では、ドメインあたり 6 つの同時接続が許可されるため、こうしたパフォーマンスの問題に対処できます (PollingDuplexHttpBinding を使用したプッシュ通知については、Internet Explorer 8.0 が普及したときに、記事のトピックとして取り上げるかもしれません)。

アプリケーションに話を戻します。エージェントが通話を承諾すると、スクリーン ポップ プロセスによって、発信者の情報 (この場合は、発信者の注文明細) が画面に表示されます。発信者の情報には、バックエンド データベース内の注文を一意に識別するために必要な情報が含まれています。このデモ シナリオでは、対話型音声応答 (IVR) システムに向かって注文番号が読み上げられたと仮定します。Silverlight アプリケーションは、一意な識別子として注文番号を使用して、WCF Web サービスを呼び出します。サービス コントラクト定義と実装を図 2 に示します。

図 2 ビジネス サービスの実装

ServiceContracts.cs

[ServiceContract]
public interface ICallService
{
    [OperationContract]
    AgentScript GetAgentScript(string orderNumber);
    [OperationContract]
    OrderInfo GetOrderDetails(string orderNumber);
}

[ServiceContract]
public interface IUserProfile    
{
    [OperationContract]
    User GetUser(string userID);
}

CallService.svc.cs

 [AspNetCompatibilityRequirements(RequirementsMode = 
                            AspNetCompatibilityRequirementsMode.Allowed)]
public class CallService:ICallService, IUserProfile
{
  public AgentScript GetAgentScript(string orderNumber)
  {
    ... 
    script.QuestionList = DataUtility.GetSecurityQuestions(orderNumber);
    return script;
  }

  public OrderInfo GetOrderDetails(string orderNumber)
  {
    ... 
    oi.Customer = DataUtility.GetCustomerByID(oi.Order.CustomerID);
    return oi;
  }

  public User GetUser(string userID)
  {
    return DataUtility.GetUserByID(userID);
  }
 }

Web.Config

<system.servicemodel> 
   <services>
     <endpoint binding="basicHttpBinding"                contract="AdvBusinessServices.ICallService"/>
     <endpoint binding="basicHttpBinding"                contract="AdvBusinessServices.IUserProfile"/>
   </services>       
   <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<system.servicemodel>

これらのサービス エンドポイントの実装は、簡単な WCF 実装なので、あまり注目すべき点はありません。わかりやすくするために、ビジネス エンティティ用のデータベースを使用する代わりに、メモリ内 List オブジェクトを使用して、Customer、Order、および User の各オブジェクトを格納します。DataUtil クラス (ここには示されていませんが、コード サンプルには含まれています) は、メモリ内 List オブジェクトへのアクセスをカプセル化します。

fig03.gif

図 3 セキュリティ関連の質問を含むエージェント スクリプト

Silverlight を利用する WCF サービス エンドポイントには、ASP.NET パイプラインへのアクセスが必要なため、CallService 実装に AspNetCompatibilityRequirements 属性が必要です。この属性は、web.config ファイルの <serviceHostingEnvironment/> 設定と一致している必要があります。

既に説明したように、Silverlight でサポートされているのは、basicHttpBinding と PollingDuplexHttpBinding のみです。WCF サービスの Visual Studio テンプレートを使用すると、エンドポイントは wsHttpBinding にバインドされるため、Silverlight がプロキシの生成のためにサービス参照を追加できるように、手動でバインド先を basicHttpBinding に変更する必要があります。Silverlight に対応している WCF サービスの Visual Studio テンプレートを使用して CallService.svc を AdvBusinessServices プロジェクトに追加した場合は、ASP.NET ホスティングの互換性とバインドは自動的に変更されます。

サービス呼び出し

Silverlight の呼び出しが可能なサービスを実装したので、次は、サービス プロキシを作成し、それを使用してバックエンド サービスの実装に UI を定義します。Visual Studio で [サービス参照] の [サービス参照の追加] を使用することで、WCF サービスのプロキシのみを確実に生成できます。デモでは、プロキシが名前空間 CallBusinessProxy に生成されています。Silverlight で許可されているのは、ネットワーク リソースの非同期の呼び出しのみです。サービス呼び出しも例外ではありません。顧客から電話がかかってくると、Silverlight クライアントは通知をリッスンし、[Accept] (承諾) および [Reject] (拒否) のダイアログを表示します。

エージェントが通話を承諾したら、次は Web サービスを呼び出し、通話状況に応じたエージェント スクリプトを取得します。このデモでは、図 3 に表示されているスクリプトだけを使用します。そのスクリプトには、あいさつ文とセキュリティ関連の質問の一覧が含まれています。エージェントは、サポートを開始する前に、最低限の質問に回答していることを確認します。

エージェント スクリプトを取得するには、ICallService.GetAgentScript() にアクセスし、入力として注文番号を渡します。Silverlight Web サービス スタックによって実現される非同期プログラミング モデルと同様、GetAgentScript() は CallServiceClient.BeginGetAgentScript() として利用できます。図 4 に示すように、サービス呼び出しを行うときに、コールバック ハンドラ GetAgentScriptCallback を渡す必要があります。

図 4 サービス呼び出しと Silverlight の UI の変更

class Page:UserControl
{   
   ... 
   void _notifyCallPopup_OnAccept(object sender, EventArgs e)
   {
     AcceptMessage acceptMsg = new AcceptMessage();
     acceptMsg.RepNumber = ClientGlobals.currentUser.RepNumber;
     ClientGlobals.socketClient.SendAsync(acceptMsg);
     this.borderCallProgressView.DataContext = ClientGlobals.callInfo;
     ICallService callService = new CallServiceClient();
     IAsyncResult result = 
        callService.BeginGetAgentScript(ClientGlobals.callInfo.OrderNumber, 
                     GetAgentScriptCallback, callService);
     //do a preemptive download of user control
     ThreadPool.QueueUserWorkItem(ExecuteControlDownload);
     //do a preemptive download of the order information
     ThreadPool.QueueUserWorkItem(ExecuteGetOrderDetails, 
                ClientGlobals.callInfo.OrderNumber);
   }

   void GetAgentScriptCallback(IAsyncResult asyncReseult)
   {

     ICallService callService = asyncReseult.AsyncState as ICallService;
     CallBusinessProxy.AgentScript svcOutputAgentScript = 
                     callService.EndGetAgentScript(asyncReseult);
     ClientEntityTranslator astobas =  
                               SvcScriptToClientScript.entityTranslator;
     ClientEntities.AgentScript currentAgentScript =  
                             astobas.ToClientEntity(svcOutputAgentScript)
                             as ClientEntities.AgentScript;
     Interlocked.Exchange<ClientEntities.AgentScript>(ref 
                   ClientGlobals.currentAgentScript, currentAgentScript);
     if (this.Dispatcher.CheckAccess())
     {
       this.borderAgentScript.DataContext = ClientGlobals.agentScript;
       ... 
       this.hlVerifyContinue.Visibility = Visibility.Visible;
     }
     else
     {
       this.Dispatcher.BeginInvoke(
        delegate()
        {
          this.borderAgentScript.DataContext = ClientGlobals.agentScript;
          ...
          this.hlVerifyContinue.Visibility = Visibility.Visible;

        } );
       }
     }
   private void ExecuteControlDownload(object state)
   {
     WebClient webClient = new WebClient();
     webClient.OpenReadCompleted += new   
       OpenReadCompletedEventHandler(OrderDetailControlDownloadCallback);
     webClient.OpenReadAsync(new Uri("/ClientBin/AdvOrderClientControls.dll", 
                                                     UriKind.Relative));
   }
   ... 
}

サービス呼び出しの結果はコールバック ハンドラからしか取得できないため、Silverlight アプリケーションの状態の変更はコールバック ハンドラで行われる必要があります。CallServiceClient.BeginGetAgentScript() は、UI スレッド上で実行されている _notifyCallPopup_OnAccept によって呼び出され、非同期要求をキューに登録し、次のステートメントに即座に戻ります。エージェント スクリプトはまだ使用できないため、コールバックがトリガされるまで待ってから、スクリプトをキャッシュし、UI にそのデータをバインドする必要があります。

サービス呼び出しが正常に終了すると、GetAgentScriptCallback がトリガされます。GetAgentScriptCallback は、エージェント スクリプトを取得し、グローバル変数を設定し、適切な UI 要素にエージェント スクリプトのデータをバインドして UI を調整します。UI を調整するときに、GetAgentScriptCallback は Dispatcher.CheckAccess() を使用して、UI スレッド上でその UI が更新されたことを確認します。

UIElement.Dispatcher.CheckAccess() は UI スレッド ID とワーカー スレッド ID を比較し、両方のスレッドが同じ場合には true を、それ以外の場合には false を返します。GetAgentScriptCallback がワーカー スレッド上で実行される場合 (実際、常にワーカー スレッド上で実行されるので、単純に Dispatcher.BeginInvoke を呼び出すことができます)、CheckAccess() は false を返し、UI は Dispatcher.Invoke() を介して匿名デリゲートをディスパッチすることで更新されます。

同期されたサービス呼び出し

Silverlight のネットワーク環境の非同期の特質により、UI スレッド上で非同期のサービスを呼び出してサービスが完了するのを待ち、呼び出しの結果に基づいてアプリケーションの状態を変更することはほとんど不可能です。図 4 では、_notifyCallPopup_OnAccept で注文明細を取得し、出力メッセージをクライアント エンティティに変換し、それをスレッドセーフ方式でグローバル変数に保存する必要があります。この作業を行うために、次のようなハンドラ コードを記述したらどうなるでしょうか。

CallServiceClient client = new CallServiceClient();
client.GetOrderDetailsAsync(orderNumber);
this._orderDetailDownloadHandle.WaitOne();
//do something with the results

残念ながら、このコードを実行すると、this._orderDetailDownloadHandle.WaitOne() ステートメントの箇所でアプリケーションは停止します。これは、UI スレッドが他のスレッドからディスパッチされたメッセージを受け取るのを WaitOne() ステートメントがブロックするからです。代わりに、ワーカー スレッドをスケジュールして、サービス呼び出しを実行し、呼び出しが完了するのを待ってから、ワーカー スレッド上でサービスの出力全体の後処理を終わらせます。この技法を図 5 に示します。誤って UI スレッドで呼び出しがブロックされないように、ManualResetEvent をカスタム SLManualResetEvent の内側にラップし、WaitOne() が呼び出されたときに UI スレッドをテストするようにしています。

図 5 注文明細の取得

void _notifyCallPopup_OnAccept(object sender, EventArgs e)
{
  ... 
  ThreadPool.QueueUserWorkItem(ExecuteGetOrderDetails, 
        ClientGlobals.callInfo.OrderNumber);
}
private SLManualResetEvent _ orderDetailDownloadHandle = new 
        SLManualResetEvent();
  private void ExecuteGetOrderDetails(object state)
{
  CallServiceClient client = new CallServiceClient();
  string orderNumber = state as string;
  client.GetOrderDetailsCompleted += new
        EventHandler<GetOrderDetailsCompletedEventArgs>
        (GetOrderDetailsCompletedCallback);
  client.GetOrderDetailsAsync(orderNumber);
  this._orderDetailDownloadHandle.WaitOne();
  //translate entity and save it to global variable
  ClientEntityTranslator oito = SvcOrderToClientOrder.entityTranslator;
  ClientEntities.Order currentOrder = 
        oito.ToClientEntity(ClientGlobals.serviceOutputOrder)
        as ClientEntities.Order;
  Interlocked.Exchange<ClientEntities.Order>(ref ClientGlobals.
       currentOrder, currentOrder);
}

void GetOrderDetailsCompletedCallback(object sender, 
        GetOrderDetailsCompletedEventArgs e)
  {
    Interlocked.Exchange<OrderInfo>(ref ClientGlobals.serviceOutputOrder, 
         e.Result);
    this._orderDetailDownloadHandle.Set();
  }

SLManualResetEvent は汎用クラスなので、特定のコントロールの Dispatcher.CheckAccess() を利用できません。ApplicationHelper.IsUiThread() を使用して Application.RootVisual.Dispatcher.CheckAccess() をチェックできますが、この方法でアクセスすると、不正なスレッド間アクセスの例外がトリガされます。そのため、UIElement インスタンスへのアクセス権がないときにワーカー スレッドでテストする唯一の確実な方法は、次に示すように、Deployment.Current.Dispatcher.CheckAccess() を使用することです。

public static bool IsUiThread()
    {
        if (Deployment.Current.Dispatcher.CheckAccess())
            return true;
        else
            return false;
    }

タスクのバックグラウンド実行には、ThreadPool.QueueUserWorkItem の代わりに、BackGroundWorker を使用できます。その場合は、ThreadPool も使用することになりますが、UI スレッド上で実行可能なハンドラを記述できます。このパターンでは、複数のサービス呼び出しを並列に実行し、SLManualResetEvent.WaitOne() を使用してすべての呼び出しが完了するまで待ってから、以降の処理のために結果を集約することができます。

メッセージ エンティティの変換

GetAgentScriptCallback は、サービスからの出力メッセージ エンティティ (DataContracts) をクライアント側の使用セマンティクスを表すクライアント側のエンティティに変換します。サーバー側のメッセージ エンティティの設計が、コール センターだけでなくさまざまな用途に使用できるサービスの汎用的な特質に十分な注意を払っているのに、データ バインドには注意を払っていないことがあります。

また、メッセージ エンティティへの変更はクライアント側では制御できないので、メッセージ エンティティを密結合しないことをお勧めします。メッセージ エンティティをクライアント側のエンティティに変換する方法は Silverlight に適用できませんが、通常、設計時の密結合を回避する目的では、すべての Web サービスのコンシューマに適用できます。

私は、エンティティのトランスレータの実装を非常に単純なままにしました。複雑に入れ子になっているジェネリック、ラムダ式、および制御の反転コンテナは使用していません。ClientEntityTranslator は ToClientEntity() メソッドを定義する抽象クラスで、次のように、すべてのサブクラスによってオーバーライドされる必要があります。

public abstract class ClientEntityTranslator
{
  public abstract ClientEntities.ClientEntity ToClientEntity(object 
                                                 serviceOutputEntity);
}

各子クラスは、サービスの交換の種類に対して一意であるため、必要な数だけトランスレータを作成します。デモでは、3 種類のサービス呼び出しとして IUserProfile.GetUser()、ICallService.GetAgentScript()、および ICallService.GetOrderDetails() を作成しました。そのため、図 6 に示すように、3 つのトランスレータを作成しました。

図 6 メッセージ エンティティからクライアント側のエンティティに変換するトランスレータ

public class SvcOrderToClientOrder : ClientEntityTranslator
{
  //singleton
  public static ClientEntityTranslator entityTranslator = new                 
                                           SvcOrderToClientOrder();
  private SvcOrderToClientOrder() { }
  public override ClientEntities.ClientEntity ToClientEntity(object                   
                                                  serviceOutputEntity)
  {
    CallBusinessProxy.OrderInfo oi = serviceOutputEntity as 
                                         CallBusinessProxy.OrderInfo;
    ClientEntities.Order bindableOrder = new ClientEntities.Order();
    bindableOrder.OrderNumber = oi.Order.OrderNumber;
    //code removed for brevity  ... 
    return bindableOrder;
  }
}

public class SvcUserToClientUser : ClientEntityTranslator
{
    //code removed for brevity  ... 
}

public class SvcScriptToClientScript : ClientEntityTranslator
{
    //code removed for brevity  ...
    }
}

お気付きかもしれませんが、上記のトランスレータはステートレスで、シングルトン パターンを使用しています。トランスレータは、一貫性を保つために ClientEntityTranslator から継承できる必要があり、ガベージ コレクションのチャーンを避けるためにシングルトンでなければなりません。

それぞれのサービス呼び出しが行われるたびに、同じインスタンスを再利用します。次のクラス定義を使用して、(通常、トランザクション サービスの呼び出しの場合に) 大きな入力メッセージを必要とするサービスの対話のために ServiOutputEntityTranslator を作成することもできます。

public abstract class ServiOutputEntityTranslator
{
  public abstract object ToServiceOutputEntity(ClientEntity  
                                                      clientEntity);
}

この場合、メッセージ エンティティの基本クラスを制御していない (このデモでは制御できますが、現実の世界ではできない) ため、上記の関数の戻り値は "オブジェクト" です。タイプ セーフは、それぞれのトランスレータによって実装されます。デモを簡潔にするために、サーバーにまったくデータを保存しません。そのため、このデモにはクライアント エンティティをメッセージ エンティティに変換するトランスレータは含まれていません。

サービス呼び出し後の Silverlight の状態変更

Silverlight の表示状態は、UI スレッド上でコードを実行することでのみ変更できます。サービス呼び出しの非同期実行の結果は常にコールバック ハンドラに返されるため、コールバック ハンドラはアプリケーションの表示状態や表示以外の状態を変更するのに利用できます。

共有状態を非同期で変更しようとしているサービスが複数あるかもしれない場合は、表示以外の状態変更はスレッドセーフ方式で交換する必要があります。UI を変更する前に、Deployment.Current.Dispatcher.CheckAccess() を確認することをお勧めします。

ドメイン間ポリシー

メディア アプリケーションやバナー広告を表示するアプリケーションとは違って、実際のエンタープライズ クラスの LOB アプリケーションは、さまざまなサービス ホスティング環境と統合する必要があります。たとえば、この記事で取り上げているコール センター アプリケーションは、代表的なエンタープライズ アプリケーションです。Web サイトでホストされているこのアプリケーションは、ステートフルなソケット サーバーにアクセスしてスクリーン ポップを行い、WCF ベースの Web サービスを通じて LOB データにアクセスします。別のドメインから追加 XAP パッケージ (zip 形式で圧縮された Silverlight 展開パッケージ) をダウンロードすることもあります。また、インストルメンテーション データを転送するために、さらに別のドメインを使用します。

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

  • サービス プロセスにホストされた Web サービス (または、簡素化のためにコンソール アプリケーション)
  • IIS または他の Web サーバーにホストされた Web サービス
  • サービス プロセスにホストされた TCP サービス (または、コンソール アプリケーション)

TCP サービスのドメイン間ポリシーの実装については先月説明したので、カスタム プロセスおよび IIS 内でホストされる Web サービスに重点を置いて説明します。

IIS にホストされた Web サービス エンドポイント向けにドメイン間ポリシーを実装するのは容易ですが、その他のケースでは、ポリシーの要求および応答の特質に関する知識が求められます。

IIS 外でホストされる Web サービスのドメイン間ポリシー

効果的に状態を管理するために、IIS 外の OS のプロセスでサービスをホストする場合があります。そのような WCF サービスのドメイン間アクセスの場合、プロセスは HTTP エンドポイントのルートでポリシーをホストする必要があります。ドメイン間 Web サービスが呼び出されると、Silverlight は clientaccesspolicy.xml に対する HTTP Get 要求を発行します。サービスが IIS 内でホストされている場合は、Web サイトのルートに clientaccesspolicy.xml ファイルをコピーし、IIS にそのファイルの残りの処理を行わせることができます。ローカル マシン上でのカスタム ホスティングの場合、http://localhost:<port>/clientaccesspolicy.xml が有効な URL である必要があります。

コール センターのデモではカスタム ホストの Web サービスを使用していないので、この概念について説明するために、コンソール アプリケーションで簡単な TimeService を使用します。コンソールは、Microsoft .NET Framework 3.5 の新しい Representational State Transfer (REST) 機能を使用して、REST エンドポイントを公開します。UriTemplate プロパティは、図 7 に示したとおり正確に設定する必要があります。

図 7 カスタム ホストの WCF サービスの実装

[ServiceContract]
public interface IPolicyService
{
    [OperationContract]            
    [WebInvoke(Method = "GET", UriTemplate = "/clientaccesspolicy.xml")]  
    Stream GetClientAccessPolicy();
}
public class PolicyService : IPolicyService
{
    public Stream GetClientAccessPolicy()
    {
        FileStream fs = new FileStream("PolicyFile.xml", FileMode.Open);
        return fs;
    }
}

インターフェイス名やメソッド名は結果と関係ないので、どんな名前でもかまいません。WebInvoke には、他に RequestFormat、ResponseFormat などのプロパティがありますが、既定では、それらは XML に設定されているため、明示的に指定する必要はありません。また、BodyStyle プロパティの既定値が BodyStyle.Bare であること (つまり応答がラップされないこと) を利用しています。

サービスの実装は非常に単純で、Silverlight クライアントからの要求に応じて、clientaccesspolicy.xml をストリーミングするだけです。ポリシー ファイルの名前は自由に付けてかまいません。ポリシー サービスの実装を図 7 に示します。

HTTP 要求を REST スタイルで処理するように、IPolicyService を構成する必要があります。コンソール アプリケーション (ConsoleWebServices) の App.Config を図 8 に示します。特別な構成が必要なため、注意点がいくつかあります。ConsoleWebServices.IPolicyServer エンドポイントのバインドは webHttpBinding に設定します。また、構成ファイルに示すように、IPolicyService エンドポイントの動作は WebHttpBehavior で構成します。PolicyService のベース アドレスはルート URL (たとえば、http://localhost:3045/) に設定し、エンドポイントのアドレスは空のまま (たとえば、<endpoint address=" " … contract="ConsoleWebServices.IPolicyService" />) にします。

図 8 カスタム ホスティング環境の WCF 設定

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <!-- IPolicyService end point should be configured with 
           webHttpBinding-->
      <service name="ConsoleWebServices.PolicyService">
         <endpoint address="" 
               behaviorConfiguration="ConsoleWebServices.WebHttp"
               binding="webHttpBinding" 
               contract="ConsoleWebServices.IPolicyService" />
         <host>
           <baseAddresses>
             <add baseAddress="http://localhost:3045/" />
           </baseAddresses>
         </host>
      </service>
      <service behaviorConfiguration="ConsoleWebServices.TimeServiceBehavior"
               name="ConsoleWebServices.TimeService">
         <endpoint address="TimeService" binding="basicHttpBinding" 
               contract="ConsoleWebServices.ITimeService">
         </endpoint>
         <host>
            <baseAddresses>
              <add baseAddress="http://localhost:3045/TimeService.svc" />
            </baseAddresses>
         </host>
       </service>
     </services>
     <behaviors>
        <endpointBehaviors>
          <!--end point behavior is used by REST endpoints like 
              IPolicyService described above-->
          <behavior name="ConsoleWebServices.WebHttp">
            <webHttp />
          </behavior>
        </endpointBehaviors>
       ... 
      </behaviors>
    </system.serviceModel>
</configuration>

最後に、コード サンプルや構成で示した TimeService などのコンソール ホスト上のサービスは、対応する IIS のサービスと同様の URL を持つように構成します。たとえば、既定の HTTP にある IIS でホストされた TimeService エンドポイントの URL は、http://localhost/TimeService.svc のようになります。この場合、メタデータは http://localhost/TimeService.svc?WSDL から取得できます。

コンソール ホスティングの場合、メタデータを取得するには、サービス ホストのベース アドレスに "?WSDL" を追加します。図 8 に示した構成では、ご覧のとおり、TimeService のベース アドレスは http://localhost:3045/TimeService.svc に設定されているため、メタデータは http://localhost:3045/TimeService.svc?WSDL から取得します。

この URL は、IIS ホスティングで使用する URL に似ています。ホストのベース アドレスを http://localhost:3045/TimeService.svc/ に設定した場合は、少々奇妙に見えますが、メタデータの URL は http://localhost:3045/TimeService.svc/?WSDL になります。メタデータの URL を特定する時間を節約するのに役立つので、この取得方法に注意してください。

IIS 内でホストされるサービスのドメイン間ポリシー

既に説明したように、IIS ホスト上のサービスのドメイン間ポリシーの展開は容易で、clientaccesspolicy.xml ファイルを Web サービスがホストされているサイトのルートにコピーするだけです。図 1 で示したように、この Silverlight アプリケーションは advcallclientweb (localhost:1041) でホストされ、AdvBusinessServices (localhost:1043) からビジネス サービスにアクセスします。Silverlight ランタイムでは、図 9 に示したコードを使用して、AdvBusinessServices Web サイトのルートに clientaccesspolicy.xml を展開する必要があります。

図 9 IIS ホスト上の Web サービスの clientaccesspolicy.xml

<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <!--allows the access of Silverlight application with localhost:1041
           as the domain of origin-->  
        <domain uri="http://localhost:1041"/>
        <!--allows the access of call simulator Silverlight application
           with localhost:1042 as the domain of origin-->  
        <domain uri="http://localhost:1042"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

このシリーズの初回で示したソケット サーバー (advpolicyserver) のドメイン間ポリシーの書式を思い出していただければ、<allow-from> の書式が類似していることがわかります。異なるのは <grant-to> セクションです。ここでは、次に示すように、ソケット サーバーに対して <socket-resource> でポート範囲とプロトコル属性を設定する必要があります。

<grant-to>
  <socket-resource port="4530" protocol="tcp" />
</grant-to>

ASP.NET Web サイト テンプレートを使用して WCF サービス ホスティング サイトを作成し、後から WCF エンドポイントを追加する場合、テスト Web サーバーは仮想ディレクトリをプロジェクト名 ("/AdvBusinessServices" など) にマップします。これをプロジェクトのプロパティ ページで "/" に変更し、clientaccesspolicy.xml をルートから処理できるようにする必要があります。変更しないと、clientaccesspolicy.xml がルートに配置されないため、サービスへのアクセス時に、Silverlight アプリケーションでサーバー エラーが発生します。ただし、WCF Web サービス プロジェクト テンプレートを使用して Web サイトを作成した場合は、問題になりません。

図 10 PasswordBox を使用した Login コントロール

<UserControl x:Class="AdvCallCenterClient.Login">
  <Border x:Name="LayoutRoot" ... >
    <Grid x:Name="gridLayoutRoot">
     <Border x:Name="borderLoginViw" ...>
       <TextBlock Text="Pleae login.." Style="{StaticResource headerStyle}"/>
       <TextBlock Text="Rep ID" Style="{StaticResource labelStyle}"/>
       <TextBox x:Name="txRepID" Style="{StaticResource valueStyle}"/>
       <TextBlock Text="Password" Style="{StaticResource labelStyle}"/>
       <PasswordBox x:Name="pbPassword" PasswordChar="*"/>
       <HyperlinkButton x:Name="hlLogin" Content="Click to login"  
            ToolTipService.ToolTip="Clik to login" Click="hlLogin_Click" />
     </Border>
     <TextBlock x:Name="tbLoginStatus" Foreground="Red" ... />
      ...
</UserControl>

public partial class Login : UserControl
{
  public Login()
  {
    InitializeComponent();
  }
  public event EventHandler<EventArgs> OnSuccessfulLogin;
  private void hlLogin_Click(object sender, RoutedEventArgs e)
  {
    //validate the login
    AuthenticationProxy.AuthenticationServiceClient authService 
                  = new AuthenticationProxy.AuthenticationServiceClient();
    authService.LoginCompleted += new 
                EventHandler< AuthenticationProxy.LoginCompletedEventArgs>
                                           (authService_LoginCompleted);
    authService.LoginAsync(this.txRepID.Text, this.pbPassword.Password, 
                                                          null, false);     
  }

  void authService_LoginCompleted(object sender, 
                           AuthenticationProxy.LoginCompletedEventArgs e)
  {
    if (e.Result == true)
    {
       if (OnSuccessfulLogin != null)
          OnSuccessfulLogin(this, null);
    }
    else
    {
      this.tbLoginStatus.Text = "Invalid user id or password";
    }

  }
}

アプリケーション セキュリティ

LOB アプリケーションの重要な要件の 1 つに認証があります。コール センターのエージェントは勤務を開始する前に、ユーザー ID とパスワードを入力して認証します。ASP.NET Web アプリケーションでは、メンバシップ プロバイダとサーバー側の ASP.NET ログイン コントロールを使用して簡単に認証を行うことができます。Silverlight には、認証を強制する方法として外部認証と内部認証の 2 つがあります。

外部認証は非常に簡単で、ASP.NET アプリケーションの認証の実装に似ています。この方法では、Silverlight アプリケーションが表示される前に、ASP.NET ベースの Web ページで認証を行います。認証コンテキストは、Silverlight アプリケーションが読み込まれる前に InitParams パラメータを介して、またはアプリケーションが読み込まれた後に (認証状態の情報を抽出するために) カスタム Web サービスの呼び出しを行うことで、Silverlight アプリケーションに転送されます。

この方法は、Silverlight アプリケーションが大規模な ASP.NET/HTML ベースのシステムの一部である場合に適しています。しかし、Silverlight がアプリケーションのメイン ドライバである場合は、通常、Silverlight の内部認証を実行します。Silverlight 2 の PasswordBox コントロールを使用してパスワードを取得し、ユーザーの資格情報を検証するために ASP.NET AuthenticationService WCF エンドポイントを使用して認証を行います。AuthenticationService、ProfileService、および RoleService は、.NET Framework 3.5 で導入された新しい名前空間 System.Web.ApplicationServices に含まれています。この目的のために作成した Login コントロールの XAML を図 10 に示します。Login コントロールは、入力されたユーザー ID とパスワードを使用して、ASP.NET AuthenticationService.LoginAsync() を呼び出します。

fig11.gif

図 11 Login カスタム Silverlight コントロール

図 11 に示したコール センターのログイン画面は洗練されていませんが、デモとしての目的は果たしています。LoginCompleted イベントをコントロールの内部で処理するハンドラを実装しました。これは自己完結型の実装で、高度な実装のために無効なログインであることを示すメッセージとパスワードをリセットするダイアログを表示します。ログインに成功すると、OnSuccessfulLogin イベントがトリガされ、親コントロール (この場合は Application.RootVisual) によって、ユーザー情報を含む最初のアプリケーション画面が表示されます。

図 12 に示すように、Silverlight のメイン ページ内に配置された LoginCompleted (ctrlLoginView_OnSuccessfulLogin) ハンドラは、ビジネス サービス Web サイトでホストされているプロファイル サービスを呼び出します。既定では、AuthenticationService はどの .svc エンドポイントにもマップされていないため、次のように、物理的実装に .svc ファイルをマップします。

<!-- AuthenticationService.svc -->
<%@ ServiceHost Language="C#" Service="System.Web.ApplicationServices.  
    AuthenticationService" %>

図 12 Page.xaml 内部での Login.xaml の使用方法

<!-- Page.xaml of the main UserControl attached to RootVisual-->
<UserControl x:Class="AdvCallCenterClient.Page" ...>
   <page:Login x:Name="ctrlLoginView" Visibility="Visible"   
         OnSuccessfulLogin="ctrlLoginView_OnSuccessfulLogin"/>
   ...
</UserControl>
<!-- Page.xaml.cs of the main UserControl attached to RootVisual-->
public partial class Page : UserControl
{       
   ... 

   private void ctrlLoginView_OnSuccessfulLogin(object sender, EventArgs e)
   {
     Login login = sender as Login;
     login.Visibility = Visibility.Collapsed;
     CallBusinessProxy.UserProfileClient userProfile 
                           = new CallBusinessProxy.UserProfileClient();
     userProfile.GetUserCompleted += new  
     EventHandler<GetUserCompletedEventArgs>(userProfile_GetUserCompleted);
     userProfile.GetUserAsync(login.txRepID.Text);
   }
   ... 
   void userProfile_GetUserCompleted(object sender, 
                                             GetUserCompletedEventArgs e)
   {
     CallBusinessProxy.User user = e.Result;
     UserToBindableUser utobu = new UserToBindableUser(user);
     ClientGlobals.currentUser = utobu.Translate() as ClientEntities.User;
     //all the time the service calls will be complete on a worker thread 
     //so the following check is redunant but done to be safe
     if (!this.Dispatcher.CheckAccess())
     {
       this.Dispatcher.BeginInvoke(delegate()
       {
         this.registrationView.DataContext = ClientGlobals.currentUser;
         this.ctrlLoginView.Visibility = Visibility.Collapsed;
         this.registrationView.Visibility = Visibility.Visible;
       });
      }
    }
}

Silverlight では、AJAX などのスクリプト環境で呼び出されるように構成された Web サービスのみ呼び出すことができます。AJAX のすべての呼び出し可能なサービスと同様、AuthenticationService サービスは ASP.NET ランタイム環境にアクセスする必要があります。そのために、<system.servicemodel> ノードの下に <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> を設定しています。認証サービスを Silverlight のログイン プロセスで呼び出せるようにする (または AJAX で呼び出す) には、「方法 : WCF 認証サービスを有効にする」の指示に従って web.config を設定する必要があります。Silverlight カテゴリに配置された Silverlight 対応の WCF サービス テンプレートを使用して作成されたサービスは、Silverlight 向けに自動的に構成されます。

認証サービスに必須の重要な要素を追加した構成を図 13 に示します。サービスの構成のほかに、認証情報を格納する aspnetdb のために、SQL Server の構成設定も置き換えました。Machine.config は、aspnetdb.mdf が Web サイトの App_Data ディレクトリに埋め込まれるように LocalSqlServer 設定を定義します。この構成設定によって、既定の設定を無効化し、SQL Server インスタンスにアタッチされた aspnetdb を既定に指定します。既定を別のマシン上で実行しているデータベース インスタンスに簡単に変更できます。

図 13 ASP.NET 認証サービスの設定

//web.config
<Configuration>  
  <connectionStrings>
  <!-- removal and addition of LocalSqlServer setting will override the   
   default asp.net security database used by the ASP.NET Configuration tool
   located in the Visul Studio Project menu-->
  <remove name="LocalSqlServer"/>
    <add name="LocalSqlServer" connectionString="Data 
             Source=localhost\SqlExpress;Initial Catalog=aspnetdb; ... />
</connectionStrings>
<system.web.extensions>
   <scripting>
     <webServices>
   <authenticationService enabled="true" requireSSL="false"/>
     </webServices>
   </scripting>
</system.web.extensions>
... 
<authentication mode="Forms"/>
... 
<system.serviceModel>
   <services>
     <service name="System.Web.ApplicationServices.AuthenticationService" 
              behaviorConfiguration="CommonServiceBehavior">
    <endpoint 
              contract="System.Web.ApplicationServices.AuthenticationService" 
              binding="basicHttpBinding" bindingConfiguration="useHttp" 
              bindingNamespace="https://asp.net/ApplicationServices/v200"/>
     </service>
   </services>
   <bindings>
     <basicHttpBinding>
    <binding name="useHttp">
          <!--for production use mode="Transport" -->
      <security mode="None"/>
     </binding>
     </basicHttpBinding>
   </bindings>
   ... 
   <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
</configuration>

Login コントロールのカプセル化と、親コントロールの設計時の緩やかな結合を維持するために、ログイン プロセスの成功は OnSuccessfulLogin イベントがトリガされることで伝えられます。Application.RootVisual (Page クラス) は、ログインが成功すると、最初の画面を表示するために必要なビジネス プロセスを実行します。ログイン成功後に表示される最初の画面は、図 12 の userProfile_GetUserCompleted メソッドに示した registrationView です。この画面が表示される前に、CallBusinessProxy.UserProfileClient.GetUserAsync() を呼び出してユーザー情報を取得します。非同期のサービス呼び出しは、後で説明するビジネス サービスの統合と類似しています。

先ほどの構成では Secure Sockets Layer (SSL) を使用していないことに注意してください。運用システムを構築する場合は、SSL を使用するように変更する必要があります。

fig14.gif

図 14 注文明細を含む OrderDetails.xaml コントロール

アプリケーションのパーティション

Silverlight アプリケーションの起動時間に影響する要素の 1 つに初期パッケージのサイズがあります。XAP パッケージのサイズについてのガイドラインは、Web アプリケーションのページの重みとまったく同じです。帯域幅は限られたリソースです。Web アプリケーションの応答時間が非常に長い場合は、Silverlight アプリケーションの起動時間に十分に注意する必要があります。

最初の UserControl が表示されるまでの処理時間だけでなく、アプリケーション パッケージのサイズもこのアプリケーションの重要な品質に直接影響します。起動速度を改善するためには、アプリケーションが複雑な場合にサイズが数十 MB にもなることがあるモノリシックな XAP ファイルを避ける必要があります。

Silverlight アプリケーションは XAP ファイル (個別の DLL、個別の XML ファイル、イメージ、および MIME と認識されるその他の種類) の集合に分割できます。コール センターのシナリオでは、アプリケーションを細かく分割するために、OrderDetail Silverlight コントロールを独立した DLL (AdvOrderClientControls.dll) として、AdvCallCenterClient.xap と共に、AdvCallClientWeb プロジェクトの ClientBin ディレクトリに展開します (図 1 を参照)。

その DLL は、エージェントが通話を承諾したときに、ワーカー スレッドに事前にダウンロードされます。図 4 に示した ThreadPool.QueueUserWorkItem(ExecuteControlDownload) 呼び出しがこの処理を行います。発信者がセキュリティ関連の質問に答えたら、リフレクションを使用して OrderDetail コントロールのインスタンスを作成し、画面に表示される前にコントロール ツリーに追加します。注文明細を含む、コントロール ツリーに読み込まれた OrderDetail.xaml コントロールを図 14 に示します。

OrderDetail コントロールを含む DLL はコール センター クライアントと同じ Web サイトに展開されます。これは、同じアプリケーションに属する DLL では一般的なことなので、この場合はドメイン間の問題は発生しません。しかし、これがサービスに当てはまるとは限りません。アーキテクチャ図 (もう一度図 1 を参照) に示すように、Silverlight アプリケーションは複数のドメイン (ローカルやクラウドを含む) に展開されているサービスにアクセスすることがあるからです。

ExecuteControlDownload メソッド (図 4 を参照) は、バックグラウンドのワーカー スレッド上で実行され、DLL のダウンロードに WebClient クラスを使用します。既定では、WebClient は元のドメインからダウンロードを行うことを前提としているため、相対 URI のみを使用します。

OrderDetailControlDownloadCallback ハンドラは DLL ストリームを受け取り、図 15 に示す ResourceUtility.GetAssembly() を使用してアセンブリを作成します。アセンブリの作成は UI スレッド上で行われる必要があるため、次のように、GetAssembly() と、グローバル変数へのアセンブリの (スレッド セーフな) 割り当てを UI スレッドにディスパッチします。

void OrderDetailControlDownloadCallback(object sender,
       OpenReadCompletedEventArgs e)
  {
    this.Dispatcher.BeginInvoke(delegate() {
    Assembly asm = ResourceUtility.GetAssembly(e.Result);
    Interlocked.Exchange<Assembly>(ref 
        ClientGlobals.advOrderControls_dll, asm ); });
  }

図 15 リソースを抽出するためのユーティリティ関数

public class ResourceUtility
{ 
  //helper function to retrieve assembly from a package stream
  public static Assembly GetAssembly(string assemblyName, Stream 
                                                        packageStream)
  {
    StreamResourceInfo srInfo =
    Application.GetResourceStream(
              new StreamResourceInfo(packageStream, "application/binary"),
              new Uri(assemblyName, UriKind.Relative));
    return GetAssembly(srInfo.Stream);
  }
  //helper function to retrieve assembly from a assembly stream
  public static Assembly GetAssembly(Stream assemblyStream)
  {
    AssemblyPart assemblyPart = new AssemblyPart();
    return assemblyPart.Load(assemblyStream);
  }
  //helper function to create an XML document from the stream
  public static XElement GetXmlDocument(Stream xmlStream)
  {
    XmlReader reader = XmlReader.Create(xmlStream);
    XElement element = XElement.Load(reader);
    return element;
  }
  //helper function to create an XML document from the default package
  public static XElement GetXmlDocumentFromXap(string fileName)
  {
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.XmlResolver = new XmlXapResolver();
    XmlReader reader = XmlReader.Create(fileName);
    XElement element = XElement.Load(reader);
    return element;
  }
  //gets the UIElement from the default package
  public static UIElement GetUIElementFromXaml(string xamlFileName)
  {
    StreamResourceInfo streamInfo = Application.GetResourceStream(new 
                                  Uri(xamlFileName, UriKind.Relative));
    string xaml = new StreamReader(streamInfo.Stream).ReadToEnd();
    UIElement uiElement = null;
    try
    {
      uiElement = (UIElement)XamlReader.Load(xaml);
    }
    catch
    {
      throw new SLApplicationException(string.Format("Can't create 
                                  UIElement from {0}", xamlFileName));
    }
    return uiElement;
  }
}

ディスパッチされたデリゲートはコールバック ハンドラとは別のスレッド上で実行されるため、匿名デリゲートからアクセスされるオブジェクトの状態を意識する必要があります。上記のコードでは、ダウンロードされた DLL ストリームの状態は実に重要です。OrderDetailControlDownloadCallback 関数内にストリームのリソースを再要求するコードを記述しないでください。そうしたコードでは、UI スレッドがアセンブリを作成する機会を得る前に、ダウンロードされたストリームは早急に破棄されてしまいます。ここでは、次に示すように、リフレクションを使用して OrderDetail ユーザー コントロールのインスタンスを作成し、Panel に追加します。

_orderDetailContol = ClientGlobals.advOrderControls_dll.CreateInstance
                  ("AdvOrderClientControls.OrderDetail") as UserControl;
spCallProgressPanel.Children.Add(_orderDetailContol);

図 15 の ResourceUtility には、XAML から UIElement を、ダウンロードされたストリームや既定のパッケージから XML ドキュメントを抽出するさまざまなユーティリティ関数も示されています。

生産性を超えて

従来のエンタープライズ アプリケーションの観点から Silverlight について説明し、アプリケーションのアーキテクチャに関する側面をいくつか紹介しました。Silverlight ソケットを使用したプッシュ通知の実装はコール センターなどの LOB シナリオを実現するための鍵です。リリース予定の Internet Explorer 8.0 では、ホストあたり 6 つの同時接続が可能になる予定なので、インターネットを介したプッシュ通知の実装は、二重 WCF バインドを使用したときに、より説得力を持つでしょう。LOB データとプロセスとの統合は、従来のデスクトップ アプリケーションの場合と同じくらい簡単です。

そのため、AJAX や他の Rich Internet application (RIA) プラットフォームと比較した場合、生産性は非常に向上します。Silverlight アプリケーションは、ASP.NET の最新リリースで提供される WCF 認証と承認エンドポイントを使用して保護できます。Silverlight を使用した LOB アプリケーションの開発について簡単に説明してきましたが、この記事がメディアや広告シナリオ以外にも Silverlight を利用するきっかけになることを願います。

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