Share via


Silverlight

Erstellen von Branchenanwendungen für Unternehmen mit Silverlight, Teil 2

Hanu Kommalapati

Themen in diesem Artikel:

  • Die Silverlight-Laufzeitumgebung
  • Asynchrone Silverlight-Programmierung
  • Domänenübergreifende Richtlinien
  • Beispiel einer Unternehmensanwendung
In diesem Artikel werden folgende Technologien verwendet:
Silverlight 2

Codedownload verfügbar in der MSDN-Codegalerie
Code online durchsuchen

Inhalt

Integration in Geschäftsdienste
Dienstaufruf
Synchronisierte Dienstaufrufe
Konvertierung der Nachrichtenentität
Silverlight-Zustandsänderung nach den Dienstaufrufen
Domänenübergreifende Richtlinien
Domänenübergreifende Richtlinien für außerhalb von IIS gehostete Webdienste
Domänenübergreifende Richtlinien für innerhalb von IIS gehostete Dienste
Anwendungssicherheit
Anwendungspartitionierung
Produktivität und mehr

Im ersten Artikel dieser Reihe habe ich ein Callcenterszenario beschrieben. Gezeigt wurde eine Bildschirmmeldungsimplementierung über die verbundenen Sockets, bei der die von Silverlight unterstützten asynchronen TCP-Sockets verwendet wurden (siehe Erstellen von Branchenanwendungen für Unternehmen mit Silverlight, Teil 1).

Implementiert wurden die Bildschirmmeldungen durch einen simulierten Anrufverteiler, der einen Anruf aus einer internen Warteschlange übernahm und Benachrichtigungen mittels Push über die vorher akzeptierte Socketverbindung übermittelte, die in einer generischen Liste auf dem Server zwischengespeichert wurde. Schwerpunkte dieses Artikels, der das Thema abschließt, sind die Implementierung der Anwendungssicherheit, die Integration in Geschäftsdienste und die Implementierung domänenübergreifender Richtlinien für Webdienste sowie Anwendungspartitionierung. Die logische Architektur der Callcenteranwendung ist in Abbildung 1 zu sehen. Der Authentifizierungsdienst wird im Hilfsdienst implementiert, während die Geschäftsdienste (ICallService und IUserProfile) innerhalb des Geschäftsdienstprojekts implementiert werden, wie der Name vermuten lässt.

Abbildung 1 Logische Architektur eines Silverlight-Callcenters

Das Diagramm zeigt zwar Ereignisstreaming in Hilfsdienste, aber aus Zeitgründen ist diese Funktionalität nicht in der herunterladbaren Demo enthalten. Die Implementierung des Ereignisaufzeichnungsdienst-Features ähnelt der Implementierung der Geschäftsdienste. Geschäftsereignisse, bei denen es sich nicht um kritische Fehler handelt, können allerdings lokal im isolierten Speicher zwischengespeichert und im Batchmodus auf dem Server gesichert werden. Ich beginne die Erörterung mit der Implementierung von Geschäftsdiensten und schließe mit dem Thema der Anwendungspartitionierung.

Integration in Geschäftsdienste

Die Integration in Dienste ist einer der wichtigen Aspekte einer Branchenanwendung. Silverlight bietet zahlreiche Komponenten für den Zugriff auf webbasierte Ressourcen und Dienste. HttpWebRequest, WebClient und die Proxyinfrastruktur von Windows Communication Foundation (WCF) sind einige der Netzwerkkomponenten, die am häufigsten für die HTTP-basierte Interaktion verwendet werden. In diesem Artikel erfolgt die Integration in die Back-End-Geschäftsprozesse mithilfe von WCF-Diensten.

Die meisten von uns verwenden Webdienste für die Integration in die Back-End-Datenquellen im Laufe der Anwendungsentwicklung. Der WCF-Webdienstzugriff mit Silverlight unterscheidet sich nicht wesentlich von jenem mit traditionellen Anwendungen wie ASP.NET, Windows Presentation Foundation (WPF) oder Windows Forms-Anwendungen. Unterschiede bestehen in der Bindungsunterstützung und im asynchronen Programmiermodell. Silverlight bietet nur Unterstützung für basicHttpBinding und PollingDuplexHttpBinding. Beachten Sie, dass HttpBinding die größte Interoperabilität bietet. Aus diesem Grund wird diese Bindung in diesem Artikel für die gesamte Integration verwendet.

PollingDuplexHttpBinding ermöglicht die Verwendung von Rückrufverträgen zur Übertragung von Benachrichtigungen über HTTP mittels Push. Mein Callcenter hätte diese Bindung für Bildschirmbenachrichtigungen verwenden können. Die Implementierung erfordert jedoch die Zwischenspeicherung der HTTP-Verbindung auf dem Server. Dadurch wird eine der beiden gleichzeitigen HTTP-Verbindungen, die von Browsern wie Internet Explorer 7.0 zugelassen werden, monopolisiert. Dies kann Leistungsprobleme verursachen, da sämtliche Webinhalte über eine Verbindung serialisiert werden müssen. Internet Explorer 8.0 erlaubt sechs gleichzeitige Verbindungen pro Domäne und löst Leistungsprobleme dieser Art. (Pushbenachrichtigungen unter Verwendung von PollingDuplexHttpBinding könnten ein Thema für einen zukünftigen Artikel sein, wenn Internet Explorer 8.0 allgemein verfügbar ist.)

Zurück zur Anwendung. Wenn der Agent einen Anruf annimmt, wird der Bildschirm durch den Bildschirmmeldungsprozess mit den Anruferdaten gefüllt, in diesem Fall den Auftragsdetails des Anrufers. Die Anruferdaten sollten alle Informationen enthalten, die zur eindeutigen Identifizierung des Auftrags in der Back-End-Datenbank erforderlich sind. Für dieses Demoszenario nehme ich an, dass die Auftragsnummer in das IVR-System (interactive voice response) gesprochen wurde. Die Silverlight-Anwendung ruft WCF-Webdienste mit der Auftragsnummer als eindeutigem Bezeichner auf. Die Dienstvertragsdefinition und die Implementierung werden in Abbildung 2 gezeigt.

Abbildung 2 Geschäftsdienstimplementierung

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>

Die Implementierung dieser Dienstendpunkte ist nicht sonderlich interessant, da es sich um einfache WCF-Implementierungen handelt. Der Einfachheit halber werde ich keine Datenbank für Geschäftsentitäten verwenden, sondern speicherinterne List-Objekte zum Speichern von Customer-, Order- und User-Objekten. Die DataUtil-Klasse (wird hier nicht gezeigt, ist aber im Codedownload verfügbar) kapselt den Zugriff auf diese speicherinternen List-Objekte.

fig03.gif

Abbildung 3 Agentskript mit Sicherheitsfragen

WCF-Dienstendpunkte für Silverlight-Nutzung benötigen Zugriff auf die ASP.NET-Pipeline und erfordern daher das AspNetCompatibilityRequirements-Attribut in der CallService-Implementierung. Dies muss mit der <serviceHostingEnvironment/>-Einstellung in der Datei „web.config“ übereinstimmen.

Wie bereits erwähnt, unterstützt Silverlight nur basicHttpBinding und PollingDuplexHttpBinding. Wenn Sie die Visual Studio-Vorlage des WCF-Diensts verwenden, wird als Endpunktbindung „wsHttpBinding“ konfiguriert. Dies muss manuell in „basicHttpBinding“ geändert werden, bevor Silverlight einen Dienstverweis für die Proxygenerierung hinzufügen kann. Die Änderungen der ASP.NET-Hostingkompatibilität und der Bindungen erfolgen automatisch, wenn CallService.svc dem AdvBusinessServices-Projekt mithilfe einer Silverlight-fähigen Visual Studio-Vorlage des WCF-Diensts hinzugefügt wird.

Dienstaufruf

Nach der Implementierung eines aufrufbaren Silverlight-Diensts werden jetzt Dienstproxys erstellt und zur Verbindung der Benutzeroberfläche mit den Back-End-Dienstimplementierungen verwendet. Die einzige Möglichkeit zur zuverlässigen Generierung von Proxys für WCF-Dienste besteht darin, die Option „Dienstverweise“ | „Dienstverweis hinzufügen“ in Visual Studio zu verwenden. Die Proxys in meiner Demo wurden in den CallBusinessProxy-Namespace generiert. Silverlight ermöglicht nur asynchrone Aufrufe der Netzwerkressourcen. Der Dienstaufruf bildet hierbei keine Ausnahme. Wenn ein Kundenanruf eingeht, hört der Silverlight-Client die Benachrichtigung ab und zeigt ein Dialogfeld zum Akzeptieren/Ablehnen an.

Wenn der Anruf vom Agent akzeptiert wurde, besteht der nächste Schritt im Prozess darin, den Webdienst aufzurufen, um das Agentskript abzurufen, das der Anrufsituation entspricht. Für diese Demo verwende ich nur ein Skript, wie in Abbildung 3 gezeigt. Das angezeigte Skript enthält eine Begrüßung und eine Liste mit Sicherheitsfragen. Der Agent stellt sicher, dass vor dem nächsten Schritt eine Mindestanzahl von Fragen beantwortet wird.

Das Agentskript wird durch Zugriff auf ICallService.GetAgentScript() abgerufen, wobei die Auftragsnummer als Eingabe verwendet wird. Im Einklang mit dem asynchronen Programmiermodell, das vom Silverlight-Webdienststapel durchgesetzt wird, ist GetAgentScript() als CallServiceClient.BeginGetAgentScript() verfügbar. Während des Dienstaufrufs müssen Sie einen Rückrufhandler, GetAgentScriptCallback, angeben (siehe Abbildung 4).

Abbildung 4 Dienstaufruf und Änderung der Silverlight-Benutzeroberfläche

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));
   }
   ... 
}

Da das Ergebnis des Dienstaufrufs nur aus dem Rückrufhandler abgerufen werden kann, müssen alle Änderungen am Zustand der Silverlight-Anwendung im Rückrufhandler erfolgen. CallServiceClient.BeginGetAgentScript() wird durch _notifyCallPopup_OnAccept aufgerufen, das im Benutzeroberflächenthread ausgeführt wird, stellt die asynchrone Anforderung in die Warteschlange und kehrt sofort zur nächsten Anweisung zurück. Da das Agentskript noch nicht verfügbar ist, müssen Sie bis zur Auslösung des Rückrufs warten, bevor Sie das Skript zwischenspeichern und die Datenbindung an die Benutzeroberfläche durchführen.

Durch den erfolgreichen Abschluss des Dienstaufrufs wird GetAgentScriptCallback ausgelöst, das das Agentskript abruft, eine globale Variable auffüllt und die Benutzeroberfläche durch Datenbindung des Agentskripts an die entsprechenden Benutzeroberflächenelemente anpasst. Während der Anpassung der Benutzeroberfläche sorgt GetAgentScriptCallback durch Verwendung von Dispatcher.CheckAccess() für die Aktualisierung im Benutzeroberflächenthread.

UIElement.Dispatcher.CheckAccess() vergleicht die Benutzeroberflächenthread-ID mit der des Arbeitsthreads und gibt „true“ zurück, wenn beide übereinstimmen. Andernfalls wird „false“ zurückgegeben. Wenn GetAgentScriptCallback auf einem Arbeitsthread ausgeführt wird (da dies eigentlich immer der Fall ist, könnten Sie einfach Dispatcher.BeginInvoke aufrufen), gibt CheckAccess() den Wert „false“ zurück, und die Benutzeroberfläche wird durch Verteilung eines anonymen Delegaten durch Dispatcher.Invoke() aktualisiert.

Synchronisierte Dienstaufrufe

Aufgrund der asynchronen Natur der Silverlight-Netzwerkumgebung ist es fast unmöglich, einen asynchronen Dienstaufruf für den Benutzeroberflächenthread durchzuführen und auf dessen Abschluss zu warten in der Absicht, den Anwendungszustand in Abhängigkeit von den Ergebnissen des Aufrufs zu ändern. In Abbildung 4 muss _notifyCallPopup_OnAccept Auftragsdetails abrufen, die Ausgabenachricht in eine Cliententität transformieren und threadsicher in einer globalen Variablen speichern. Um dies zu erreichen, könnten Sie versucht sein, den folgenden Handlercode zu schreiben:

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

Dieser Code führt jedoch dazu, dass die Anwendung nicht mehr reagiert, wenn sie auf die Anweisung „this._orderDetailDownloadHandle.WaitOne()“ trifft. Dies liegt daran, dass die WaitOne()-Anweisung den Benutzeroberflächenthread am Empfang verteilter Nachrichten aus anderen Threads hindert. Stattdessen können Sie den Arbeitsthread so planen, dass er den Dienstaufruf ausführt, auf den Abschluss des Aufrufs wartet und die Nachverarbeitung der Dienstausgabe vollständig im Arbeitsthread abschließt. Dieses Verfahren wird in Abbildung 5 gezeigt. Um die unbeabsichtigte Verwendung des Sperrens von Aufrufen im Benutzeroberflächenthread zu verhindern, habe ich ManualResetEvent in ein benutzerdefiniertes SLManualResetEvent gewrappt und führe die Überprüfung auf einen Benutzeroberflächenthread durch, wenn ein Aufruf an WaitOne() erfolgt.

Abbildung 5 Abrufen von Auftragsdetails

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();
  }

Da SLManualResetEvent eine allgemeine Klasse ist, können Sie sich nicht auf das Dispatcher.CheckAccess() eines bestimmten Steuerelements verlassen. ApplicationHelper.IsUiThread() kann Application.RootVisual.Dispatcher.CheckAccess() überprüfen. Allerdings wird durch den Zugriff auf diese Methode eine unzulässige threadübergreifende Zugriffsausnahme ausgelöst. Die einzige zuverlässige Möglichkeit, dies in einem Arbeitsthread zu testen, wenn kein Zugriff auf eine UIElement-Instanz möglich ist, besteht daher in der Verwendung von Deployment.Current.Dispatcher.CheckAccess():

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

Für die Hintergrundausführung von Aufgaben könnten Sie statt ThreadPool.QueueUserWorkItem auch BackGroundWorker verwenden. Diese Komponente verwendet ebenfalls ThreadPool, ermöglicht Ihnen aber, Handler zu verbinden, die auf dem Benutzeroberflächenthread ausgeführt werden können. Dieses Muster ermöglicht die gleichzeitige Ausführung mehrerer Dienstaufrufe und wartet mithilfe von SLManualResetEvent.WaitOne() auf den Abschluss aller Aufrufe, bevor die Ergebnisse für die weitere Verarbeitung aggregiert werden.

Konvertierung der Nachrichtenentität

GetAgentScriptCallback konvertiert auch die Ausgabenachrichtenentitäten (auch bekannt als DataContracts) aus dem Dienst in eine clientseitige Entität, die die clientseitige Verwendungssemantik repräsentiert. So wäre es zum Beispiel möglich, dass der Entwurf serverseitiger Nachrichtenentitäten sich nicht um die Datenbindung kümmert, aber genau auf den Mehrzweckcharakter des Diensts achtet, der sich für vielfältige Verwendungsmöglichkeiten eignen muss, nicht nur für Callcenter.

Außerdem hat es sich bewährt, keine enge Kopplung mit den Nachrichtenentitäten einzurichten, da Änderungen an den Nachrichtenentitäten außerhalb der Kontrolle des Clients liegen. Die Praxis der Konvertierung von Nachrichtenentitäten in clientseitige Entitäten gilt nicht nur für Silverlight, sondern allgemein für jeden Webdienstnutzer, wenn die enge Kopplung zur Entwurfszeit vermieden werden soll.

Ich habe mich für eine sehr einfache Implementierung der Entitätskonvertierer entschieden: keine exotisch verschachtelten Generika, Lambda-Ausdrücke und keine Inversion von Steuerelementcontainern. Die abstrakte ClientEntityTranslator-Klasse definiert die ToClientEntity()-Methode, die von jeder Unterklasse außer Kraft gesetzt werden muss:

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

Jede untergeordnete Klasse ist eindeutig einem Dienstaustauschtyp zugeordnet. Daher werde ich so viele Konvertierer wie nötig erstellen. In meiner Demo wurden die folgenden drei Typen von Dienstaufrufen verwendet: IUserProfile.GetUser(), ICallService.GetAgentScript() und ICallService.GetOrderDetails(). Also habe ich drei Konvertierer erstellt (siehe Abbildung 6).

Abbildung 6 Konvertierung von Nachrichtenentität in clientseitige Entität

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  ...
    }
}

Sicher ist Ihnen aufgefallen, dass die obigen Konvertierer zustandslos sind und ein Singletonmuster verwenden. Der Konvertierer muss aus Konsistenzgründen in der Lage sein, von ClientEntityTranslator zu erben, und muss ein Singleton sein, um die Garbage Collection-Änderung zu vermeiden.

Für den jeweiligen Dienstaufruf verwende ich dieselbe Instanz immer wieder. Mit der folgenden Klassendefinition könnte ich auch ServiOutputEntityTranslator für eine Dienstinteraktion erstellen, die große Eingabenachrichten erfordert (was im Allgemeinen bei Aufrufen von Transaktionsdiensten der Fall ist):

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

Der Rückgabewert der obigen Funktion lautet „object“, da ich die Basisklasse der Nachrichtenentitäten nicht kontrolliere (in dieser Demo wäre das möglich, nicht aber in der Praxis). Die Typsicherheit wird von den jeweiligen Konvertierern implementiert. Um die Demo möglichst einfach zu halten, sehe ich davon ab, Daten wieder auf dem Server zu speichern. Deshalb enthält diese Demo keine Konvertierer zum Umwandeln von Cliententitäten in Nachrichtenentitäten.

Silverlight-Zustandsänderung nach den Dienstaufrufen

Eine Änderung eines visuellen Zustands in Silverlight kann nur von dem Code durchgeführt werden, der auf dem Benutzeroberflächenthread ausgeführt wird. Da die Ergebnisse bei asynchroner Ausführung der Dienstaufrufe immer im Rückrufhandler zurückgegeben werden, ist der Handler der richtige Ort, um Änderungen am visuellen oder nicht visuellen Zustand der Anwendung vorzunehmen.

Änderungen des nicht visuellen Zustands sollten threadsicher ausgetauscht werden, wenn es mehrere Dienste geben könnte, die versuchen, den Freigabezustand asynchron zu ändern. Es empfiehlt sich, immer Deployment.Current.Dispatcher.CheckAccess() zu verwenden, bevor Sie die Benutzeroberfläche ändern.

Domänenübergreifende Richtlinien

Im Unterschied zu den Medienanwendungen und den Anwendungen, die Bannerwerbung zeigen, erfordern echte Branchenanwendungen für Unternehmen die Integration in verschiedene Diensthostumgebungen. Die Callcenteranwendung, auf die im Artikel mehrfach Bezug genommen wird, ist z. B. typisch für die Unternehmensanwendung. Diese auf einer Website gehostete Anwendung greift für Bildschirmmeldungen auf einen zustandsbehafteten Socketserver und für Branchensystemdaten auf WCF-basierte Webdienste zu. Darüber hinaus lädt sie möglicherweise zusätzliche XAP-Pakete (gezippte Silverlight-Bereitstellungspakete) von einer anderen Domäne herunter. Zum Übertragen von Instrumentationsdaten verwendet diese Anwendung wieder eine andere Domäne.

Die Silverlight-Sandbox erlaubt standardmäßig keinen Netzwerkzugriff auf eine andere Domäne außer der Ursprungsdomäne „advcallclientweb“ (siehe Abbildung 1). Die Silverlight-Laufzeit überprüft die optionalen Richtlinien, wenn die Anwendung auf eine Domäne zugreift, die nicht der Ursprungsdomäne entspricht. Hier ist eine typische Liste der Diensthostszenarios, die domänenübergreifende Richtlinienanforderungen des Clients unterstützen müssen:

  • In einem Dienstprozess (oder der Einfachheit halber in einer Konsolenanwendung) gehostete Webdienste
  • In IIS oder anderen Webservern gehostete Webdienste
  • In einem Dienstprozess (oder einer Konsolenanwendung) gehostete TCP-Dienste

Domänenübergreifende Richtlinienimplementierung für TCP-Dienste war das Thema des letzten Monats. Daher werde ich mich nun auf Webdienste konzentrieren, die in benutzerdefinierten Prozessen und in IIS gehostet werden.

Die Implementierung domänenübergreifender Richtlinien für in IIS gehostete Webdienstendpunkte ist unkompliziert. In den anderen beiden Fällen sind jedoch Kenntnisse der Natur der Richtlinienanforderungen und -antworten erforderlich.

Domänenübergreifende Richtlinien für außerhalb von IIS gehostete Webdienste

Bei der effektiven Zustandsverwaltung kann es Situationen geben, in denen es sich anbietet, Dienste in einem Betriebssystemprozess außerhalb von IIS zu hosten. Für den domänenübergreifenden Zugriff solcher WCF-Dienste muss der Prozess Richtlinien im Stammverzeichnis des HTTP-Endpunkts hosten. Wenn ein domänenübergreifender Webdienst aufgerufen wird, sendet Silverlight eine HTTP Get-Anforderung an clientaccesspolicy.xml. Wenn der Dienst in IIS gehostet wird, kann die Datei „clientaccesspolicy.xml“ in das Stammverzeichnis der Website kopiert werden, und IIS stellt die Datei bereit. Im Fall des benutzerdefinierten Hosting auf dem lokalen Computer sollte http://localhost:<Port>/clientaccesspolicy.xml eine gültige URL sein.

Da für die Callcenterdemo keine benutzerdefinierten gehosteten Webdienste verwendet werden, verwende ich einen einfachen TimeService in einer Konsolenanwendung, um die Konzepte zu veranschaulichen. Mithilfe der neuen REST-Funktionen (Representational State Transfer) von Microsoft .NET Framework 3.5 macht die Konsole einen REST-Endpunkt verfügbar. Die UriTemplate-Eigenschaft muss exakt auf das Literal in Abbildung 7 gesetzt werden.

Abbildung 7 Implementierung für benutzerdefiniert gehostete WCF-Dienste

[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;
    }
}

Der Schnittstellen- oder Methodenname hat keinen Einfluss auf das Ergebnis. Sie können ihn beliebig auswählen. WebInvoke besitzt weitere Eigenschaften wie RequestFormat und ResponseFormat, die standardmäßig auf XML gesetzt sind und nicht explizit angegeben werden müssen. Außerdem gehen wir davon aus, dass „BodyStyle.Bare“ der Standardwert der BodyStyle-Eigenschaft ist. Dies bedeutet, dass die Antwort nicht gewrappt wird.

Die Dienstimplementierung ist sehr einfach: Als Antwort auf die Silverlight-Clientanforderung wird lediglich clientaccesspolicy.xml gestreamt. Den Richtliniendateinamen können Sie selbst nach eigenem Gutdünken auswählen. Die Implementierung des Richtliniendiensts ist in Abbildung 3 dargestellt.

Nun müssen wir IPolicyService zur Übermittlung von HTTP-Anforderungen im REST-Stil konfigurieren. Das App.Config der Konsolenanwendung (ConsoleWebServices) wird in Abbildung 8 gezeigt. Im Hinblick auf die Konfigurationsanforderungen gilt es, einige Dinge zu beachten: Für die Bindung des ConsoleWebServices.IPolicyServer-Endpunkts muss „webHttpBinding“ festgelegt werden. Außerdem sollte das Verhalten des IPolicyService-Endpunkts mit „WebHttpBehavior“ konfiguriert werden, wie in der Konfigurationsdatei gezeigt. Als Basisadresse von PolicyService sollte die Stamm-URL festgelegt werden (wie in http://localhost:3045/), und die Endpunktadresse sollte leer gelassen werden (z. B. <endpoint address=" " … contract="ConsoleWebServices.IPolicyService" />.

Abbildung 8 WCF-Einstellungen für die benutzerdefinierte Hostingumgebung

<?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>

Schließlich sollten die konsolengehosteten Dienste, z. B. der in den Codebeispielen und der Konfiguration gezeigte Dienst „TimeService“, mit einer URL konfiguriert werden, die ihren IIS-Pendants ähnelt. So könnte zum Beispiel die URL eines IIS-gehosteten TimeService-Endpunkts für Standard-HTTP wie folgt aussehen: http://localhost/TimeService.svc. In diesem Fall können die Metadaten von http://localhost/TimeService.svc?WSDL abgerufen werden.

Im Fall des Konsolenhosting können die Metadaten jedoch durch Anfügen von „?WSDL“ an die Basisadresse des Diensthosts abgerufen werden. In der Konfiguration in Abbildung 8 sehen Sie, dass als Basisadresse von TimeService „http://localhost:3045/TimeService.svc“ festgelegt wurde. Daher können die Metadaten von http://localhost:3045/TimeService.svc?WSDL abgerufen werden.

Diese URL ähnelt dem, was wir beim IIS-Hosting verwenden. Wenn Sie als Basisadresse des Hosts „http://localhost:3045/TimeService.svc/“ festlegen, lautet die Metadaten-URL „http://localhost:3045/TimeService.svc/?WSDL“, was etwas ungewöhnlich aussieht. Achten Sie also auf dieses Verhalten, um bei der Ermittlung der Metadaten-URL Zeit zu sparen.

Domänenübergreifende Richtlinien für innerhalb von IIS gehostete Dienste

Wie bereits besprochen, ist die Bereitstellung domänenübergreifender Richtlinien für IIS-gehostete Dienste unkompliziert: Sie kopieren einfach die Datei „clientaccesspolicy.xml“ in das Stammverzeichnis der Website, auf der die Webdienste gehostet werden. In Abbildung 1 haben Sie gesehen, dass die Silverlight-Anwendung auf advcallclientweb (localhost:1041) gehostet wird und von AdvBusinessServices (localhost:1043) aus auf Geschäftsdienste zugreift. Die Silverlight-Laufzeit erfordert, dass clientaccesspolicy.xml im Stamm der AdvBusinessServices-Website mit dem in Abbildung 9 gezeigten Code bereitgestellt wird.

Abbildung 9 Clientaccesspolicy.xml für IIS-gehostete Webdienste

<?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>

Rufen Sie sich das domänenübergreifende Richtlinienformat für den Socketserver (advpolicyserver) aus dem ersten Artikel dieser Reihe ins Gedächtnis zurück: Das Format von <allow-from> ist ähnlich. Der Unterschied liegt im Abschnitt <grant-to>. Hier erfordert der Socketserver eine <socket-resource>-Einstellung mit Portbereich und Protokollattributen, wie im Folgenden gezeigt:

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

Wenn Sie die Website, die den WCF-Dienst hostet, mithilfe der ASP.NET-Websitevorlage erstellen und später WCF-Endpunkte hinzufügen, ordnet der Testwebserver das virtuelle Verzeichnis dem Namen des Projekts zu (z. B. „/AdvBusinessServices“). Dies sollte auf den Eigenschaftenseiten des Projekts in „/“ geändert werden, damit clientaccesspolicy.xml aus dem Stammverzeichnis übermittelt werden kann. Wenn Sie diese Änderung nicht vornehmen, befindet sich clientaccesspolicy.xml nicht im Stammverzeichnis, und für Silverlight-Anwendungen werden beim Zugriff auf den Dienst Serverfehler ausgegeben. Beachten Sie, dass dies für die Websites, die mithilfe der Projektvorlage für den WCF-Webdienst erstellt wurden, kein Problem darstellt.

Abbildung 10 Anmeldesteuerelement mit PasswordBox

<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";
    }

  }
}

Anwendungssicherheit

Zu den wichtigsten Anforderungen einer Branchenanwendung gehört die Authentifizierung. Bevor der Callcenteragent die Schicht beginnt, authentifiziert er sich durch Angabe einer Benutzer-ID und eines Kennworts. In ASP.NET-Webanwendungen können diese Anforderungen durch Nutzung des Mitgliedschaftsanbieters und der serverseitigen ASP.NET-Anmeldesteuerelemente ganz einfach erfüllt werden. In Silverlight gibt es zwei Möglichkeiten, Authentifizierung durchzusetzen: äußere Authentifizierung und inner Authentifizierung.

Äußere Authentifizierung ist unkompliziert und ähnelt der Authentifizierungsimplementierung von ASP.NET-Anwendungen. Bei dieser Methode erfolgt die Authentifizierung in einer ASP.NET-basierten Webseite vor dem Anzeigen der Silverlight-Anwendung. Der Authentifizierungskontext kann durch InitParams-Parameter in die Silverlight-Anwendung übertragen werden, bevor eine Silverlight-Anwendung geladen wird, oder durch einen benutzerdefinierten Webdienstaufruf (zur Extrahierung der Informationen zum Authentifizierungsstatus) nach dem Laden der Anwendung.

Diese Methode ist nützlich, wenn die Silverlight-Anwendung Teil eines größeren ASP.NET/HTML-basierten Systems ist. In Fällen, in denen Silverlight der Haupttreiber der Anwendung ist, ist es jedoch sinnvoll, die Authentifizierung innerhalb von Silverlight durchzuführen. Ich verwende das PasswordBox-Steuerelement von Silverlight 2, um das Kennwort zu erfassen, und führe die Authentifizierung mithilfe des ASP.NET-AuthenticationService-WCF-Endpunkts zur Überprüfung der Anmeldeinformationen des Benutzers durch. AuthenticationService, ProfileService und RoleService sind Teil des Namespace „System.Web.ApplicationServices“, der in .NET Framework 3.5 eingeführt wurde. Abbildung 10 zeigt das XAML für das Anmeldesteuerelement, das für diesen Zweck erstellt wurde. Das Anmeldesteuerelement ruft ASP.NET-AuthenticationService.LoginAsync() mit der Benutzer-ID und dem eingegebenen Kennwort auf.

fig11.gif

Abbildung 11 Benutzerdefiniertes Silverlight-Anmeldesteuerelement

Der Anmeldebildschirm des Callcenters, dargestellt in Abbildung 11, ist relativ einfach, erfüllt aber den Zweck der Demo. Ich habe einen Handler zur Behandlung des LoginCompleted-Ereignisses innerhalb des Steuerelements implementiert, damit das Anzeigen von Meldungen zu ungültigen Anmeldungen und von Dialogen für das Zurücksetzen von Kennwörtern für anspruchsvollere Implementierungen eigenständig erfolgen kann. Nach erfolgreicher Anmeldung wird das Ereignis „OnSuccessfulLogin“ ausgelöst. Dadurch wird das übergeordnete Steuerelement (in diesem Fall Application.RootVisual) angewiesen, den ersten Anwendungsbildschirm anzuzeigen, der mit den Benutzerinformationen aufgefüllt wird.

Der LoginCompleted-Handler (ctrlLoginView_OnSuccessfulLogin) innerhalb der Silverlight-Hauptseite ruft den Profildienst auf, der auf der Geschäftsdienst-Website gehostet wird (siehe Abbildung 12). AuthenticationService ist standardmäßig keinem .svc-Endpunkt zugeordnet. Daher ordne ich die .svc-Datei der physikalischen Implementierung zu, wie im Folgenden gezeigt:

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

Abbildung 12 Verwendung von Login.xaml innerhalb von Page.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 kann nur solche Webdienste aufrufen, die für den Aufruf durch Skriptingumgebungen wie AJAX konfiguriert wurden. Wie alle aufrufbaren AJAX-Dienste benötigt der AuthenticationService-Dienst Zugriff auf die ASP.NET-Laufzeitumgebung. Diesen Zugriff ermögliche ich durch Festlegen von <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/> direkt unter dem <system.servicemodel>-Knoten. Damit der Authentifizierungsdienst durch den Silverlight-Anmeldeprozess (oder durch AJAX) aufgerufen werden kann, muss web.config entsprechend den Anweisungen in Gewusst wie: Aktivieren des WCF-Authentifizierungsdiensts festgelegt werden. Die Dienste werden automatisch für Silverlight konfiguriert, wenn sie mithilfe der Silverlight-fähigen WCF-Dienstvorlage in der Silverlight-Kategorie erstellt werden.

Abbildung 13 zeigt die bearbeitete Konfiguration mit wichtigen Elementen, die für den Authentifizierungsdienst notwendig sind. Zusätzlich zur Dienstkonfiguration habe ich auch die SQL Server-Konfigurationseinstellung für aspnetdb ersetzt, die Authentifizierungsinformationen speichert. Machine.config definiert eine LocalSqlServer-Einstellung, die erwartet, dass aspnetdb.mdf in das App_Data-Verzeichnis der Website eingebettet wird. Diese Konfigurationseinstellung entfernt die Standardeinstellung und verweist auf aspnetdb, das an die SQL Server-Instanz angehängt ist. Dies lässt sich problemlos in einen Verweis auf eine Datenbankinstanz ändern, die auf einem separaten Computer ausgeführt wird.

Abbildung 13 Einstellungen für den ASP.NET-Authentifizierungsdienst

//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>

Um die Kapselung des Anmeldesteuerelements und die lose Kopplung mit dem übergeordneten Steuerelement zur Entwurfszeit beizubehalten, wird der Erfolg des Anmeldeprozesses durch Auslösung des OnSuccessfulLogin-Ereignisses mitgeteilt. Die Klasse „Application.RootVisual“ (die eine Page-Klasse ist) führt den erforderlichen Geschäftsprozess aus und zeigt nach erfolgreicher Anmeldung den ersten Bildschirm an. Der erste nach erfolgreicher Anmeldung angezeigte Bildschirm ist registrationView, wie in der userProfile_GetUserCompleted-Methode in Abbildung 12 gezeigt. Vor dem Anzeigen dieser Ansicht rufe ich Benutzerinformationen ab, indem ich CallBusinessProxy.UserProfileClient.GetUserAsync() aufrufe. Achten Sie auf den asynchronen Dienstaufruf, ähnlich wie bei der Geschäftsdienstintegration, die später diskutiert werden soll.

Bedenken Sie, dass die vorige Konfiguration kein SSL (Secure Sockets Layer) verwendet. Sie müssen sie bei der Erstellung für Produktionssysteme so ändern, dass sie SSL verwendet.

fig14.gif

Abbildung 14 OrderDetails.xaml-Steuerelement mit Auftragsdetails

Anwendungspartitionierung

Einer der Faktoren, die zur Startzeit von Silverlight-Anwendungen beitragen, ist die Größe des Ausgangspakets. Die Richtlinien für die Größe des XAP-Pakets unterscheiden sich nicht von denen für das Seitengewicht für Webanwendungen. Bandbreite ist eine begrenzte Ressource. Die stringenten Antwortzeiten von Webanwendungen erfordern, dass Sie genau auf die Startzeit von Silverlight-Anwendungen achten.

Die Größe des Anwendungspakets wirkt sich nicht nur auf die Verarbeitungszeit vor dem Anzeigen des ersten UserControl-Steuerelements aus, sondern hat darüber hinaus direkten Einfluss auf diese wichtige Eigenschaft der Anwendung. Um die Startgeschwindigkeit zu verbessern, müssen Sie monolithische XAP-Dateien vermeiden, die bei komplexen Anwendungen eine Größe von Dutzenden Megabyte erreichen können.

Die Silverlight-Anwendung kann in eine Sammlung von XAP-Dateien, einzelne DLLs, einzelne XML-Dateien, Bilder und andere Dateitypen mit anerkannten MIME-Typen zerlegt werden. Um die granulare Anwendungspartitionierung zu demonstrieren, werde ich im Callcenterszenario das Silverlight-Steuerelement „OrderDetail“ als separate DLL (AdvOrderClientControls.dll) zusammen mit AdvCallCenterClient.xap im ClientBin-Verzeichnis des AdvCallClientWeb-Projekts bereitstellen (siehe Abbildung 1).

Die DLL wird präemptiv in den Arbeitsthread heruntergeladen, wenn der Agent den eingehenden Anruf akzeptiert. Verantwortlich hierfür ist der Aufruf in Abbildung 4 – „ThreadPool.Queue­UserWorkItem(ExecuteControlDownload)“. Nach der Beantwortung der Sicherheitsfragen durch den Anrufer verwende ich Reflektion, um eine Instanz des OrderDetail-Steuerelements zu erstellen, und füge diese Instanz vor dem Anzeigen auf dem Bildschirm der Steuerelementstruktur hinzu. Abbildung 14 zeigt das OrderDetail.xaml-Steuerelement, das mit den ausgefüllten Auftragsdetails in die Steuerelementstruktur geladen wurde.

Die DLL, die das OrderDetail-Steuerelement enthält, wird für dieselbe Website bereitgestellt wie der Callcenterclient, was typisch ist für DLLs, die zur selben Anwendung gehören. Deshalb treten in diesem Fall keine domänenübergreifenden Probleme auf. Bei Diensten ist dies jedoch nicht unbedingt der Fall, da Silverlight-Anwendungen möglicherweise auf Dienste zugreifen, die auf mehreren Domänen bereitgestellt werden, einschließlich lokaler und solcher in der Wolke, wie im Architekturdiagramm (siehe wieder Abbildung 1) gezeigt.

Die ExecuteControlDownload-Methode (siehe Abbildung 4) wird auf einem Arbeitsthread im Hintergrund ausgeführt und verwendet die WebClient-Klasse zum Herunterladen der DLL. Die WebClient-Klasse nimmt standardmäßig an, dass das Herunterladen von der Ursprungsdomäne erfolgt, und verwendet deshalb nur relative URIs.

Der OrderDetailControlDownloadCallback-Handler empfängt den DLL-Datenstrom und erstellt die Assembly mithilfe von ResourceUtility.GetAssembly() (siehe Abbildung 15). Da die Assembly im Benutzeroberflächenthread erstellt werden muss, verteile ich GetAssembly() und die (threadsichere) Zuordnung der Assembly zur globalen Variablen an den Benutzeroberflächenthread:

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

Abbildung 15 Hilfsfunktionen zum Extrahieren von Ressourcen

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;
  }
}

Da der verteilte Delegat auf einem anderen Thread ausgeführt wird als der Rückrufhandler, müssen Sie auf den Zustand der Objekte achten, auf die vom anonymen Delegaten zugegriffen wird. Im vorigen Code ist der Zustand des heruntergeladenen DLL-Datenstroms sehr wichtig. Sie dürfen keinen Code schreiben, der die Ressourcen des Datenstroms innerhalb der OrderDetailControlDownloadCallback-Funktion freigibt. Code dieser Art entfernt den heruntergeladenen Datenstrom zu früh, bevor der Benutzeroberflächenthread eine Möglichkeit hat, die Assembly zu erstellen. Ich verwende Reflektion, um eine Instanz des OrderDetail-Benutzersteuerelements zu erstellen und diese Instanz wie hier gezeigt dem Panel hinzuzufügen:

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

Die ResourceUtility-Klasse in Abbildung 15 zeigt verschiedene Hilfsfunktionen zum Extrahieren von UIElement aus dem XAML und des XML-Dokuments aus den heruntergeladenen Datenströmen und Standardpaketen.

Produktivität und mehr

Ich habe Silverlight unter dem Blickwinkel einer traditionellen Unternehmensanwendung betrachtet und dabei verschiedene architektonische Aspekte der Anwendung erörtert. Die Implementierung von Pushbenachrichtigungen mit Silverlight-Sockets ermöglicht den Einsatz in Branchenszenarios wie z. B. Callcentern. Mit der bevorstehenden Veröffentlichung von Internet Explorer 8.0 (das sechs gleichzeitige HTTP-Verbindungen pro Host ermöglichen soll) wird die Implementierung der Pushbenachrichtigung über das Internet bei Verwendung der WCF-Duplexbindung noch überzeugender. Die Integration von Branchendaten und Prozessen ist ebenso einfach wie in traditionellen Desktopanwendungen.

Im Vergleich zu AJAX und anderen RIA-Plattformen (Rich Internet Application) wird es zu enormen Produktivitätssteigerungen kommen. Zur Sicherung von Silverlight-Anwendungen können die WCF-Authentifizierung und die Autorisierungsendpunkte verwendet werden, die ASP.NET in der aktuellen Version bietet. Ich hoffe, dass diese kurze Untersuchung der Branchenanwendungsentwicklung mit Silverlight Sie anregen wird, Silverlight über Medien- und Werbeszenarios hinaus einzusetzen.

Hanu Kommalapati arbeitet als Plattformstrategieberater bei Microsoft und berät in dieser Position Unternehmenskunden bei der Erstellung skalierbarer Branchenanwendungen auf Silverlight- und Azure Services-Plattformen.