Silverlight Online

Silverlight in einer zeitweise verbundenen Welt

Mark Bloodworth

Beispielcode herunterladen .

Wir Menschen leben in einer Online-Welt – zumindest einige von uns zumindest zeitweise. Irgendwann in Zukunft wird es vielleicht eine permanente, überall verfügbare Form der Konnektivität mit mehr Bandbreite als nötig geben, aber noch ist es nicht so weit. Heute sind wir nur zeitweise verbunden – zeitweise sogar mit genügend Bandbreite. Und nur selten kennen wir unsere aktuelle Verbindungssituation.

Beim Design von Anwendungen, die trotz dieser Gegebenheiten eine optimale Benutzererfahrung ermöglichen sollen, gilt es, zahlreiche architektonische Faktoren zu berücksichtigen.

Intelligente Clients, und zwar Rich Clients ebenso wie Poor Clients, haben eines gemeinsam: Sie werden auf einem lokalen Rechner bereitgestellt. Daher ist es prinzipiell möglich, diese Anwendungen auch ohne Verbindung zu einem Netzwerk auszuführen. Eine herkömmliche browserbasierte Anwendung dagegen muss mit ihrem Remotewebserver verbunden sein, um ausgeführt werden zu können.

Zwischen diesen beiden Extremen existiert ein ständig wachsendes Spektrum unterschiedlicher Optionen. Sie alle bieten unterschiedliche Möglichkeiten, Anwendungen offline auszuführen, sowie unterschiedliche Grade an Flexibilität und Interaktivität beim Design der Benutzeroberfläche und erlegen dem Benutzer unterschiedliche Sicherheitseinschränkungen auf. Wir sprechen im Folgenden über die neuesten Versionen zeitweise verbundener Anwendungen, die eine hochgradig interaktive Benutzererfahrung bieten und sowohl innerhalb als auch außerhalb von Browsern ausgeführt werden können. Dazu präsentieren wir Codebeispiele im Zusammenhang mit der Netzwerkkonnektivitätserkennung sowie BackgroundWorker-Komponenten zum Hoch- oder Herunterladen von Daten bei bestehender Online-Verbindung.

Kontext

Betrachten wir die Entwicklung einer typischen Anwendung der genannten Art. Unsere Beispielanwendung existierte zunächst als einfache Thick-Clientanwendung, die nur unter Windows-Betriebssystemen ausgeführt werden konnte. Die Benutzer konnten zwar offline damit arbeiten, aber die Einschränkungen der ursprünglichen Lösung wurden immer offensichtlicher:

  • Als Erstes kam die Anforderung nach der Unterstützung mehrerer Betriebssysteme hinzu. Denn in der ursprünglichen Version konnte nur ein Teil der potenziellen Benutzer unterstützt werden.
  • Aufgrund von Bereitstellungsproblemen kam es zu Diskrepanzen zwischen den von der Benutzerbasis installierten Versionen.

Die Notwendigkeit einer einfacheren Lösung, die mehrere Betriebssysteme übergreifend eingesetzt werden und Bereitstellungsprobleme minimieren sollte, wurde immer deutlicher. So wurde die Anwendung als einfache HTML-Anwendung für Thin Clients umgeschrieben. Dies führte jedoch zu neuen Problemen:

  • Die Benutzeroberfläche bot nur eingeschränkte Möglichkeiten, was zu einer nur begrenzt intuitiven Benutzererfahrung führte.
  • Umfangreiche Browserkompatibilitätstests waren erforderlich.
  • Bei vielen Netzwerkinfrastrukturen war die Leistung nicht zufriedenstellend. Beispielsweise mussten jedes Mal, wenn ein Benutzer ein Formular auszufüllen hatte, große Mengen an Referenzdaten heruntergeladen werden, zusammen mit umfangreichen Skripts für die Validierungslogik.
  • Die Anwendung konnte nicht offline verwendet werden.

Diese Version war also auch noch nicht der Weisheit letzter Schluss.

Die ideale, aber nicht leicht realisierbare Lösung ist in diesem Fall eine umfassende Internetanwendung (Rich Internet Application – RIA) mit einer intuitiven und flexiblen Benutzeroberfläche. Eine solche Anwendung sollte online die Verwaltung großer Datenmengen sowie asynchrone Datenuploads und Datenvalidierungen zulassen, ohne dabei die Benutzeroberfläche zu blockieren. Darüber hinaus sollte sie die Offlinearbeit und den Zugriff auf einen Datenspeicher auf dem Client unterstützen. Sie sollte mit Hardwaregeräten am Client, beispielsweise Kameras, integrierbar sein. Und nicht zuletzt sollte diese ideale Lösung über das Startmenü oder ein Anwendungssymbol gestartet werden können – und unabhängig von einem Webbrowser existieren.

Silverlight erfüllt diese Anforderungen. Bereits bei Silverlight 3 wurde das Konzept der Browserunabhängigkeit eingeführt und nun in Silverlight 4 erweitert. Darüber hinaus wurde in Silverlight 4 die Möglichkeit der Interaktion mit bestimmten Ordnern wie etwa „Eigene Bilder“ und mit Hardwaregeräten wie Webcams eingeführt. Anwendungen mit dieser erweiterten Funktionalität informieren den Benutzer, dass sie eine erhöhte Vertrauenswürdigkeit erfordern. Außerdem ist vor der Installation einer solchen Anwendung die Zustimmung des Benutzers erforderlich. Weitere Informationen über vertrauenswürdige Anwendungen finden Sie in folgendem Artikel: msdn.microsoft.com/library/ee721083(v=VS.95)). Dieser Artikel behandelt häufig auftretende Probleme bei der Architekturkonzeption einer Anwendung, die online und offline genutzt werden kann.

Abbildung 1 zeigt eine Übersicht über eine Architektur, die als gutes Beispiel dienen kann.

image: Candidate High-Level Architecture
Abbildung 1 Beispielarchitektur im Überblick

Dies sind einige typische Benutzerszenarios:

  • Ein mobiler Benutzer mit einem Laptop. Der Laptop verfügt eventuell über eine 3G-Karte oder wird mit einem Drahtlosnetzwerk in einem Büro oder an einem Internet-Hotspot verbunden.
  • Ein Benutzer mit einem Desktop-PC in einer Umgebung mit eingeschränkter Konnektivität wie einem älteren oder nur einfach ausgestatteten Bürogebäude.

Erkennen des Netzwerkstatus

Eine Anwendung, die in einer zeitweise verbundenen Umgebung funktionieren soll, muss den aktuellen Status der Netzwerkverbindung abfragen können. In Silverlight 3 wurde diese Fähigkeit in Form der NetworkInterface.GetIsNetworkInterfaceAvailable-Methode eingeführt. Solche Anwendungen können auch das NetworkChange.NetworkAddressChangedEvent nutzen. Dieses Ereignis wird ausgelöst, wenn sich die IP-Adresse der Netzwerkschnittstelle ändert.

Der erste Schritt bei einer Anwendung, die das Problem einer nur zeitweise verfügbaren Verbindung bewältigen soll, besteht also darin, die Anwendung mit NetworkChange.NetworkAddressChangedEvent auszustatten. Die naheliegendste Stelle für die Behandlung dieses Ereignisses ist die App-Klasse, die als Einstiegspunkt in eine Silverlight-Anwendung fungiert. Diese Klasse wird standardmäßig durch App.xaml.cs (bei C#) oder App.xaml.vb (bei VB.NET) implementiert. Die folgenden Beispiele beziehen sich alle auf C#. Der Application_StartUp-Ereignishandler bietet sich als geeignete Stelle für das Abonnement an:

private void Application_Startup(object sender, StartupEventArgs e)
{
  NetworkChange.NetworkAddressChanged += new
    NetworkAddressChangedEventHandler(NetworkChange_ 
    NetworkAddressChanged);
  this.RootVisual = newMainPage();
}

Verwenden Sie folgende using-Anweisung:

using System.Net.NetworkInformation;

Der NetworkChange_NetworkAddressChanged-Ereignishandler enthält die wesentliche Funktionalität für die Netzwerkerkennung. Es folgt eine Beispielimplementierung:

void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
{
  this.isConnected = (NetworkInterface.GetIsNetworkAvailable());
  ConnectionStatusChangedHandler handler = 
    this.ConnectionStatusChangedEvent;
  if (handler != null)
  {
    handler(this.isConnected);
  }
}

Der erste Aufruf geht an GetIsNetworkAvailable, um zu prüfen, ob eine Netzwerkverbindung vorhanden ist. In diesem Beispiel wird das Ergebnis in einem Feld gespeichert, das über eine Eigenschaft verfügbar gemacht wird, und es wird ein Ereignis ausgelöst, das in anderen Teilen der Anwendung verwendet werden kann:

private bool isConnected = (NetworkInterface.GetIsNetworkAvailable());

public event ConnectionStatusChangedHandlerConnectionStatusChangedEvent;

public bool IsConnected
{
  get
  { 
    return isConnected;
  }
}

Dieses Beispiel enthält ein Grundgerüst für die Erkennung und Behandlung der aktuellen Netzwerkkonnektivität. Dennoch bleibt ein Problem: Zwar gibt GetIsNetworkAvailable den Wert „true“ zurück, wenn der Computer an ein Netzwerk angeschlossen ist (keine Loopback- oder Tunnelschnittstelle), aber das Netzwerk ist nicht unbedingt nutzbar. Dies ist beispielsweise der Fall, wenn der Computer an einen Router angeschlossen ist, der keine Internetverbindung mehr hat, oder wenn die Verbindung über einen öffentlichen Wi-Fi-Zugriffspunkt hergestellt wird, an dem sich der Benutzer über einen Browser anmelden muss.

Zu wissen, dass eine gültige Netzwerkverbindung vorliegt, ist nur ein Teil einer robusten Lösung. Die Webdienste, die eine Silverlight-Anwendung nutzt, können aus vielfältigen Gründen unerreichbar sein, und eine zeitweise verbundene Anwendung muss auch diese Eventualität behandeln können.

Es gibt eine Reihe von Strategien, um zu prüfen, ob ein Webdienst verfügbar ist. Bei eigenen Webdiensten – also Webdiensten, die der Entwickler der Silverlight-Anwendung selbst unter Kontrolle hat – ist es unter Umständen sinnvoll, eine einfache leere Methode (No-op-Methode) hinzuzufügen, mit der die Verfügbarkeit regelmäßig überprüft wird. Wenn dies, wie beispielsweise bei Drittanbieter-Webdiensten, nicht möglich oder nicht wünschenswert ist, muss eine geeignete Zeitüberschreitung konfiguriert und behandelt werden. Silverlight 3 nutzt dazu ein Subset der Client-Konfiguration der Windows Communication Foundation. Dieses wird automatisch generiert, wenn das Tool „Add Service Reference“ (Dienstverweis hinzufügen) verwendet wird.

Speichern von Daten

Eine Anwendung muss aber nicht nur auf Änderungen der Netzwerkumgebung reagieren, sondern auch Daten verarbeiten können, die eingegeben werden, während die Anwendung offline ist. Das Microsoft Sync Framework (msdn.microsoft.com/sync) ist eine umfassende Plattform mit breit gefächerter Unterstützung für Datentypen, Datenspeicher, Protokolle und Topologien. Als dieser Artikel entstand, war das Framework für Silverlight noch nicht verfügbar, wird es aber in Zukunft sein. Weitere Informationen dazu finden Sie in der MIX10-Sitzung unter live.visitmix.com/MIX10/Sessions/SVC10 oder im Blogbeitrag unter blogs.msdn.com/sync/archive/2009/12/14/offline-capable-applications-using-silverlight-and-sync-framework.aspx. Selbstverständlich ist Microsoft Sync Framework die beste Option, sobald es für Silverlight verfügbar wird. Bis dahin bedarf es einer einfachen Lösung, um die Wartezeit zu überbrücken.

Überwachbare Warteschlange (ObservableQueue)

Im Idealfall ist es für Elemente der Benutzeroberfläche nicht relevant, ob Daten lokal oder in der Cloud gespeichert werden – außer wenn dem Benutzer signalisiert werden muss, ob die Anwendung gerade offline oder online ist. Eine Warteschlange ist ein gutes Mittel, um die Trennung zwischen der Benutzeroberfläche und dem Datenspeichercode zu realisieren. Die Komponente, die die Warteschlange verarbeitet, muss in der Lage sein, auf neue Daten zu reagieren, die in die Warteschlange gestellt werden. Alle diese Faktoren sprechen für eine überwachbare Warteschlange (ObservableQueue). Abbildung 2 zeigt eine Beispielimplementierung einer überwachbaren Warteschlange.

Abbildung 2 Überwachbare Warteschlange

public delegate void ItemAddedEventHandler();

public class ObservableQueue<T>
{
  private readonly Queue<T> queue = new Queue<T>();

  public event ItemAddedEventHandler ItemAddedEvent;

  public void Enqueue(T item)
  {
    this.queue.Enqueue(item);
    ItemAddedEventHandler handler = this.ItemAddedEvent;
    if (handler != null)
    {
      handler();
    }
  }

  public T Peek()
  {
    return this.queue.Peek();
  }

  public T Dequeue()
  {
    return this.queue.Dequeue();
  }

  public ArrayToArray()
  {
    return this.queue.ToArray();
  }

  public int Count
  {
    get
    {
      return this.queue.Count;
    }
  }
}

Diese einfache Klasse umschließt eine Standardwarteschlange und löst ein Ereignis aus, wenn Daten hinzugefügt werden. Es handelt sich um eine generische Klasse, die keine Annahmen über das Format oder den Typ der hinzugefügten Daten trifft. Eine überwachbare Warteschlange ist natürlich nur dann sinnvoll, wenn sie tatsächlich überwacht wird. In diesem Fall übernimmt das eine Klasse mit der Bezeichnung „QueueProcessor“. Bevor wir uns den Code für QueueProcessor näher ansehen, ist ein weiterer Punkt zu beachten: die Hintergrundverarbeitung. Wenn QueueProcessor benachrichtigt wird, dass neue Daten zur Warteschlange hinzugefügt wurden, sollte er diese Daten in einem Hintergrundthread verarbeiten, sodass die Benutzeroberfläche weiterhin reaktionsfähig bleibt. Dieses Designziel lässt sich am besten mit der BackgroundWorker-Klasse erreichen, die im System.ComponentModel-Namespace definiert wird.

BackgroundWorker

BackgroundWorker ist ein geeignetes Verfahren zur Ausführung von Vorgängen in einem Hintergrundthread. BackgroundWorker macht zwei Ereignisse – ProgressChanged und RunWorkerCompleted – verfügbar, anhand derer eine Anwendung über den Fortschritt der BackgroundWorker-Tasks informiert werden kann. Beim Aufruf der BackgroundWorker.RunAsync-Methode wird ein DoWork-Ereignis ausgelöst. Hier ist ein Beispiel für ein BackgroundWorker-Setup:

private void SetUpBackgroundWorker()
{
  backgroundWorker = new BackgroundWorker();
  backgroundWorker.WorkerSupportsCancellation = true;
  backgroundWorker.WorkerReportsProgress = true;
  backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
  backgroundWorker.ProgressChanged += new
    ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
  backgroundWorker.RunWorkerCompleted += new
    RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
}

Beachten Sie, dass der Code im Ereignishandler für das DoWork-Ereignis regelmäßig prüfen sollte, ob ein Abbruch ansteht. Ausführlichere Informationen dazu finden Sie in der MSDN-Dokumentation unter msdn.microsoft.com/library/system.componentmodel.backgroundworker.dowork%28VS.95%29.

IWorker

Angesichts des generischen Charakters der ObservableQueue ist es ratsam, die Definition der auszuführenden Tasks von der Erstellung und Konfiguration des BackgroundWorker zu trennen. Eine einfache Schnittstelle – IWorker – definiert den Ereignishandler für DoWork. Als Sender-Objekt in der Ereignishandler-Signatur fungiert der BackgroundWorker. So können Klassen, die die IWorker-Schnittstelle implementieren, über den Forschritt informieren und auf anstehende Abbrüche prüfen. Hier ist die IWorker-Definition:

public interface IWorker
{
  void Execute(object sender, DoWorkEventArgs e);
}

Es ist leicht, sich in Designeinzelheiten zu verlieren und Trennungen vorzunehmen, wo keine erforderlich sind. Die Idee der IWorker-Schnittstelle geht jedoch auf praktische Erfahrungen zurück. Die in diesem Artikel vorgestellte ObservableQueue war zweifellos zunächst als Teil einer Lösung für zeitweise verbundene Anwendungen gedacht. Dann aber zeigte sich, dass sich auch andere Aufgaben, wie etwa der Import von Fotos von einer Digitalkamera, mithilfe einer ObservableQueue leichter implementieren lassen. Stellt man beispielsweise die Pfade zu den Fotos in eine ObservableQueue, kann eine IWorker-Implementierung diese Fotos im Hintergrund verarbeiten. Indem man die ObservableQueue generisch anlegt und die IWorker-Schnittstelle erstellt, lassen sich – unter Lösung des ursprünglichen Problems – solche Szenarios realisieren.

Verarbeitung der Warteschlange

QueueProcessor ist eine Klasse, die die ObservableQueue- und die IWorker-Implementierung zusammenführt, sodass eine sinnvolle Funktion möglich wird. Zur Verarbeitung der Warteschlange ist es erforderlich, einen BackgroundWorker einzurichten, was das Einrichten der Execute-Methode von IWorker als Ereignishandler für das BackgroundWorker.DoWork-Ereignis beinhaltet, und das ItemAddedEvent zu abonnieren. Abbildung 3 zeigt eine Beispielimplementierung von QueueProcessor.

Abbildung 3 QueueProcessor-Implementierung

public class QueueProcessor<T>
{
  private BackgroundWorker backgroundWorker;
  private readonly IWorker worker;

  public QueueProcessor(ObservableQueue<T>queueToMonitor, IWorker worker)
  {
    ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
       ConnectionStatusChangedEventHandler(QueueProcessor_
         ConnectionStatusChangedEvent);
    queueToMonitor.ItemAddedEvent += new
      ItemAddedEventHandler(PendingData_ItemAddedEvent);
    this.worker = worker;
    SetUpBackgroundWorker();
    if ((((SampleCode.App)Application.Current).IsConnected) && 
      (!backgroundWorker.IsBusy) 
      && (((SampleCode.App)Application.Current).PendingData.Count>0))
    {
      backgroundWorker.RunWorkerAsync();
    }
  }

  private void PendingData_ItemAddedEvent()
  {
    if ((((SampleCode.App)Application.Current).IsConnected) && 
      (!backgroundWorker.IsBusy))
    {
      backgroundWorker.RunWorkerAsync();
    }
  }

  private void SetUpBackgroundWorker()
  {
    backgroundWorker = new BackgroundWorker();
    backgroundWorker.WorkerSupportsCancellation = true;
    backgroundWorker.WorkerReportsProgress = true;
    backgroundWorker.DoWork += new DoWorkEventHandler(this.worker.Execute);
    backgroundWorker.ProgressChanged += new
      ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
    backgroundWorker.RunWorkerCompleted += new
      RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
  }

  private void backgroundWorker_RunWorkerCompleted(object sender,
    RunWorkerCompletedEventArgs e)
  {
    if (e.Cancelled)
    {
      // Handle cancellation
    }
      else if (e.Error != null)
      {
        // Handle error
      }
    else
    {
      // Handle completion if necessary
    }
  }

  private void backgroundWorker_ProgressChanged(object sender, 

    ProgressChangedEventArgs e)
  {
     // Raise event to notify observers
  }

  private void QueueProcessor_ConnectionStatusChangedEvent(bool isConnected)
  {
    if (isConnected)
    {
      if (!backgroundWorker.IsBusy)
      {
        backgroundWorker.RunWorkerAsync();
      }
    }
    else
    {
      backgroundWorker.CancelAsync();
    }
  }
}

Im Beispielcode in Abbildung 3 wird die Implementierung einiger BackgroundWorker-Funktionen, zum Beispiel der Fehlerbehandlung, nicht näher erläutert. Diese können Sie selbst zu Übungszwecken durchführen. Die RunAsync-Methode wird nur aufgerufen, wenn die Anwendung in diesem Moment verbunden ist und der BackgroundWorker nicht bereits die Warteschlange verarbeitet.

UploadWorker

Der QueueProcessor-Konstruktor erfordert einen IWorker. Das heißt natürlich, dass zum Hochladen von Daten in die Cloud eine konkrete Implementierung erforderlich ist. UploadWorker ist ein passender Name für eine solche Klasse. Abbildung 4 zeigt eine Beispielimplementierung.

Abbildung 4 Hochladen von Daten in die Cloud

public class UploadWorker :  IWorker
{
  public override void Execute(object sender, DoWorkEventArgs e)
  {
    ObservableQueue<DataItem>pendingData = 
      ((SampleCode.App)Application.Current).PendingData;
    while (pendingData.Count>0)
    {
      DataItem item = pendingData.Peek();
      if (SaveItem(item))
      {
        pendingData.Dequeue();
      }
    }
  }

  private bool SaveItem(DataItem item)
  {
    bool result = true;
    // Upload item to webservice
    return result;
  }
}

In Abbildung 4 dient die Execute-Methode zum Hochladen von Elementen in der Warteschlange. Wenn sie nicht hochgeladen werden können, verbleiben sie in der Warteschlange. Beachten Sie Folgendes: Wenn Zugriff auf den BackgroundWorker erforderlich ist, beispielsweise für Forschrittsberichte oder Prüfungen auf anstehende Abbrüche, fungiert der BackgroundWorker als Sender-Objekt. Wird ein Ereignis der Result-Eigenschaft von DoWorkEventArgs e zugewiesen, steht dieses im RunWorkerCompleted-Ereignishandler zur Verfügung.

Isolierter Speicher

Daten in eine Warteschlange zu stellen und diese Daten bei bestehender Verbindung an einen Webdienst zu übermitteln, um sie in der Cloud zu speichern, das funktioniert bei Anwendungen, die nie beendet werden. Für Anwendungen, die beendet werden können, solange die Warteschlange noch Daten enthält, ist eine Strategie zur Speicherung der Daten bis zum nächsten Laden der Anwendung erforderlich. Dafür stellt Silverlight den isolierten Speicher (IsolatedStorage) zur Verfügung.

Der isolierte Speicher ist ein virtuelles Dateisystem, das für Silverlight-Anwendungen zur Verfügung steht und die lokale Speicherung von Daten ermöglicht. Er kann eine begrenzte Datenmenge (Standardgrenzwert: 1 MB) aufnehmen, aber die Anwendung kann vom Benutzer mehr Speicher anfordern. In diesem Beispiel lässt sich durch eine Serialisierung der Warteschlange für den isolierten Speicher erreichen, dass der Warteschlangenstatus zwischen zwei Anwendungssitzungen gespeichert wird. Das funktioniert mit einer einfachen Klasse namens QueueStore, wie in Abbildung 5 zu sehen.

Abbildung 5 Serialisierung einer Warteschlange für den isolierten Speicher

public class QueueStore
{
  private const string KEY = "PendingQueue";
  private IsolatedStorageSettings appSettings = 
    IsolatedStorageSettings.ApplicationSettings;

  public void SaveQueue(ObservableQueue<DataItem> queue)
  {
    appSettings.Remove(KEY);
    appSettings.Add(KEY, queue.ToArray());
    appSettings.Save();
  }

  public ObservableQueue<DataItem>LoadQueue()
  {
    ObservableQueue<DataItem> result = new ObservableQueue<DataItem>();
    ArraysavedArray = null;

    if (appSettings.TryGetValue<Array>(KEY, out savedArray))
    {
      foreach (var item in savedArray)
      {
        result.Enqueue(item as DataItem);
      }
    }

  return result;
  }
}

Wenn die Elemente in der Warteschlange serialisierbar sind, können Warteschlangen mit QueueStore gespeichert und geladen werden. Durch den Aufruf der Save-Methode in der Application_Exit-Methode von App.xaml.cs und der Load-Methode in der Application_Startup-Methode von App.xaml.cs kann die Anwendung den Status zwischen zwei Sitzungen speichern.

Die zur Warteschlange hinzugefügten Elemente müssen vom Typ DataItem sein. Es handelt sich dabei um eine einfache Klasse, die die Daten repräsentiert. Im Fall eines etwas reichhaltigeren Datenmodells kann DataItem ein einfaches Objektdiagramm enthalten. In komplexeren Szenarios kann DataItem eine Basisklasse sein, von der andere Klassen erben.

Abrufen von Daten

Eine zeitweise verbundene Anwendung kann nur dann sinnvoll funktionieren, wenn es eine Möglichkeit gibt, Daten lokal zwischenzuspeichern. Dabei muss als Erstes die Menge an Arbeitsdaten berücksichtigt werden, die die Anwendung benötigt. Bei einer einfachen Anwendung kann der lokale Cache unter Umständen sogar sämtliche Daten aufnehmen, die die Anwendung benötigt. Für eine einfache Reader-Anwendung beispielsweise, die RSS-Feeds verarbeitet, müssen unter Umständen nur die RSS-Feeds als solche sowie die Benutzereinstellungen zwischengespeichert werden. Bei anderen Anwendungen ist die Menge an Arbeitsdaten möglicherweise zu groß oder es lässt sich einfach nicht vorhersehen, welche Daten der Benutzer benötigt, was die Menge an Arbeitsdaten effektiv vergrößert.

Am besten beginnt man mit einer einfachen Anwendung. Daten, die während der gesamten Anwendungssitzung benötigt werden, beispielsweise Benutzereinstellungen, lassen sich beim Start der Anwendung herunterladen und im Speicher halten. Werden diese Daten vom Benutzer geändert, bieten sich die zuvor besprochenen Strategien für das Hochladen an. Das setzt allerdings voraus, dass beim Start der Anwendung eine Verbindung besteht, was jedoch nicht unbedingt der Fall ist. Auch hier ist der isolierte Speicher die Lösung. Die zuvor präsentierten Beispiele sind auch auf dieses Szenario anwendbar. Nur muss ein Aufruf zum Herunterladen der Serverversion der Daten an geeigneter Stelle hinzugefügt werden. Bedenken Sie auch, dass der Benutzer die Anwendung unter Umständen auf einem PC im Büro und einem anderen PC zuhause installiert hat. Ein geeigneter Zeitpunkt wäre daher die Wiederaufnahme der Arbeit mit der Anwendung durch den Benutzer nach einer signifikanten Pause.

Lassen Sie uns in einem weiteren Szenario eine einfache Anwendung betrachten, die relativ statische Daten wie zum Beispiel Nachrichten anzeigt. Hier eignet sich eine ähnliche Strategie: Die Daten werden heruntergeladen, wenn es möglich ist. Sie werden im Arbeitsspeicher gehalten und beim Beenden der Anwendung in den isolierten Speicher gestellt, von dem aus sie beim erneuten Starten der Anwendung wieder geladen werden. Steht eine Verbindung zur Verfügung, können die zwischengespeicherten Daten ungültig gemacht und aktualisiert werden. Steht die Verbindung für mehr als nur ein paar Minuten zur Verfügung, sollte die Anwendung Daten wie zum Beispiel Nachrichten regelmäßig aktualisieren. Wie bereits erläutert, sollte die Benutzeroberfläche von diesen Hintergrundvorgängen nicht betroffen sein.

Beim Herunterladen von Nachrichten beginnt man mit einer einfachen NewsItem-Klasse:

public class NewsItem
{
  public string Headline;
  public string Body;

  public override stringToString()
  {
    return Headline;
  }
}

Die Klasse wurde für dieses Beispiel stark vereinfacht dargestellt. ToString wird außer Kraft gesetzt, um die Bindung an die Benutzeroberfläche zu erleichtern. Zum Speichern der heruntergeladenen Nachrichten ist eine einfache Repository-Klasse erforderlich, die die Nachrichten im Hintergrund herunterlädt, wie in Abbildung 6 zu sehen.

Abbildung 6 Speichern heruntergeladener Nachrichten in einer Repository-Klasse

public class Repository
{
  private ObservableCollection<NewsItem> news = 
    new ObservableCollection<NewsItem>();
  private DispatcherTimer timer = new DispatcherTimer();
  private const int TIMER_INTERVAL = 1;

public Repository()
{
  ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += 
    new ConnectionStatusChangedHandler(Repository_
    ConnectionStatusChangedEvent);
  if (((SampleCode.App)Application.Current).IsConnected)
  {
    RetrieveNews();
    StartTimer();
  }
}

private void Repository_ConnectionStatusChangedEvent(bool isConnected)
{
  if (isConnected)
  {
    StartTimer();
  }
  else
  {
    StopTimer();
  }
}

private void StopTimer()
{
  this.timer.Stop();
}

private void StartTimer()
{
  this.timer.Interval = TimeSpan.FromMinutes(1);
  this.timer.Tick += new EventHandler(timer_Tick);
  this.timer.Start();
}

voidtimer_Tick(object sender, EventArgs e)
{
  if (((SampleCode.App)Application.Current).IsConnected)
  {
    RetrieveNews();
  }
}

private void RetrieveNews()
{
  // Get latest news from server
  List<NewsItem> list = GetNewsFromServer();
  if (list.Count>0)
  {
    lock (this.news)
    {
      foreach (NewsItem item in list)
      {
        this.news.Add(item);
      }
    }
  }
}

private List<NewsItem>GetNewsFromServer()
{
  // Simulate retrieval from server
  List<NewsItem> list = new List<NewsItem>();
  for (int i = 0; i <5; i++)
  {
    NewsItemnewsItem = new NewsItem()
    { Headline = "Something happened at " + 
        DateTime.Now.ToLongTimeString(),
        Body = "On " + DateTime.Now.ToLongDateString() + 
        " something happened.  We'll know more later." };
      list.Add(newsItem);
    }
    return list;
  }

  public ObservableCollection<NewsItem> News
  {
    get
    {
      return this.news;
    }
    set
    {
      this.news = value;
    }
  }
}

In Abbildung 6 wird das Abrufen der Nachrichten der Kürze halber simuliert. Die Repository-Klasse abonniert das ConnectionStatusChangedEvent. Wenn eine Verbindung besteht, ruft sie Nachrichten in angegebenen Zeitabständen mit einem DispatcherTimer ab. Ein DispatcherTimer wird in Verbindung mit einer ObservableCollection verwendet, um eine einfache Datenbindung zu ermöglichen. Der DispatcherTimer ist in die Dispatcher-Warteschlange integriert und wird daher auf dem Benutzerschnittstellenthread ausgeführt. Die Aktualisierung der ObservableCollection hat folgende Wirkung: Ein Ereignis wird ausgelöst, woraufhin ein gebundenes Steuerelement in der Benutzeroberfläche automatisch aktualisiert wird, was beim Herunterladen von Nachrichten ideal ist. Ein System.Threading.Timer steht in Silverlight zur Verfügung, wird aber nicht auf dem Benutzerschnittstellenthread ausgeführt. In diesem Fall müssen alle Vorgänge, die auf Objekte auf dem Benutzerschnittstellenthread zugreifen, mit Dispatcher.BeginInvoke aufgerufen werden.

Zur Verwendung des Repository ist lediglich eine Eigenschaft in App.xaml.cs erforderlich. Wenn das Repository das ConnectionStatusChangedEvent in seinem Konstruktor abonniert, ist Application_StartupeventinApp.xaml.cs die beste Stelle für die Instanziierung.

Es ist unwahrscheinlich, dass Daten wie etwa Benutzereinstellungen von jemand anderem als dem Benutzer geändert werden, während die Anwendung offline ist, obwohl dies bei Anwendungen, die vom gleichen Benutzer auf verschiedenen Geräten eingesetzt werden, nicht ausgeschlossen werden kann. Im Übrigen ändern sich Daten wie Nachrichtenartikel in der Regel nicht. Das heißt, die zwischengespeicherten Daten sind wahrscheinlich gültig, und es gibt bei der Wiederherstellung der Verbindung wahrscheinlich keine Synchronisierungsprobleme. Im Fall flüchtiger Daten dagegen ist unter Umständen eine andere Strategie erforderlich. So kann es beispielsweise sinnvoll sein, den Benutzer zu informieren, wann die Daten das letzte Mal abgerufen wurden, sodass er in geeigneter Weise reagieren kann. Muss die Anwendung auf der Grundlage flüchtiger Daten Entscheidungen treffen, sind Regeln für das Ende von deren Gültigkeit erforderlich, ebenso wie eine Benachrichtigung an den Benutzer, dass die erforderlichen Daten nicht zur Verfügung stehen, weil die Anwendung offline ist.

Können Daten geändert werden, lässt sich eine Konfliktlösung in vielen Fällen durch einen Vergleich, wann und wo die Daten geändert wurden, erzielen. Einerseits können Sie optimistisch davon ausgehen, dass die neueste Version die gültige und bei einem Konflikt die ausschlaggebende ist. Andererseits können Sie Konflikte aber auch danach lösen, wo die Daten aktualisiert wurden. Die Kenntnis der Anwendungstopologie und der Nutzungsszenarios ist der Schlüssel zur korrekten Strategie. Wenn Versionskonflikte nicht gelöst werden können oder sollen, ist es sinnvoll, ein Protokoll dieser Versionen zu speichern und die betreffenden Benutzer zu benachrichtigen, damit sie eine Entscheidung fällen können.

Darüber hinaus gilt es zu überlegen, wo die Synchronisierungslogik residieren soll. Die einfachste Lösung besteht darin, sie auf dem Server zu platzieren, sodass sie bei Versionskonflikten vermitteln kann. In diesem Fall erfordert der UploadWorker eine geringfügige Modifikation dahingehend, dass DataItem auf die neueste Serverversion aktualisiert wird. Wie bereits erwähnt, wird das Microsoft Sync Framework viele dieser Probleme künftig beheben, sodass sich der Entwickler ganz auf die Anwendungsdomäne konzentrieren kann.

Zusammenführen der Teile

Nachdem nun alle Einzelaspekte besprochen sind, lässt sich eine einfache Silverlight-Anwendung, die in einer zeitweise verbundenen Umgebung funktionieren soll, problemlos erstellen. Abbildung 7 zeigt einen Screenshot einer solchen Anwendung.

image: Sample Application for Demonstrating Network Status and Queues
Abbildung 7 Beispielanwendung zur Demonstration von Netzwerkstatus und Warteschlangen

Im Beispiel in Abbildung 7 wird die Silverlight-Anwendung außerhalb des Browsers ausgeführt. Angesichts der typischen Merkmale zeitweise verbundener Anwendungen ist davon auszugehen, dass diese in vielen Fällen außerhalb des Browsers ausgeführt werden, da der Benutzer sie problemlos über das Startmenü bzw. den Desktop aufrufen und unabhängig von der Netzwerkverbindung ausführen kann. Eine Silverlight-Anwendung, die innerhalb eines Browsers ausgeführt werden soll, erfordert dagegen eine Netzwerkverbindung, damit der Webserver, auf dem sie residiert, die Webseite und die Silverlight-Anwendung als solche zur Verfügung stellen kann.

Die einfache Benutzeroberfläche in Abbildung 7 würde zwar kaum einen Designpreis gewinnen, ist aber gut geeignet, um den Beispielcode auszuprobieren. Neben der Anzeige des aktuellen Netzwerkstatus enthält sie Felder zur Dateneingabe und ein Listenfeld, das an die News-Eigenschaft des Repository gebunden ist. Der XAML-Beispielcode zum Erstellen des Bildschirms ist in Abbildung 8 dargestellt. Der CodeBehind ist in Abbildung 9 zu sehen.

Abbildung 8 XAML für die Benutzeroberfläche der Beispielanwendung

<UserControl x:Class="SampleCode.MainPage" 
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:dataInput="clr-namespace:System.Windows.Controls;
    assembly=System.Windows.Controls.Data.Input" 
    Width="400" Height="300">
      <Grid x:Name="LayoutRoot" Background="White" ShowGridLines="False">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="12" />
          <ColumnDefinition Width="120" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="12" />
          <RowDefinition Height="64" />
          <RowDefinition Height="44" />
          <RowDefinition Height="34" />
          <RowDefinition Height="34" />
          <RowDefinition Height="100" />
        </Grid.RowDefinitions>
        <Ellipse Grid.Row="1" Grid.Column="1" Height="30" HorizontalAlignment="Left" 
          Name="StatusEllipse" Stroke="Black" StrokeThickness="1" 
          VerticalAlignment="Top" Width="35" />
        <Button Grid.Row="4" Grid.Column="1" Content="Send Data" Height="23" 
          HorizontalAlignment="Left" Name="button1" VerticalAlignment="Top" 
          Width="75" Click="button1_Click" />
        <TextBox Grid.Row="2" Grid.Column="2" Height="23" HorizontalAlignment="Left" 
          Name="VehicleTextBox" VerticalAlignment="Top" Width="210" />
        <TextBox Grid.Row="3" Grid.Column="2" Height="23" HorizontalAlignment="Left" 
          Name="TextTextBox" VerticalAlignment="Top" Width="210" />
        <dataInput:Label Grid.Row="2" Grid.Column="1" Height="28" 
          HorizontalAlignment="Left" Name="VehicleLabel" VerticalAlignment="Top" 
          Width="120" Content="Title" />
        <dataInput:Label Grid.Row="3" Grid.Column="1" Height="28" 
          HorizontalAlignment="Left" Name="TextLabel" VerticalAlignment="Top" 
          Width="120" Content="Detail" />
        <ListBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="2" Height="100" 
          HorizontalAlignment="Left" Name="NewsListBox" 
          VerticalAlignment="Top" Width="376" />
      </Grid>
</UserControl>

Abbildung 9 CodeBehind für die Benutzeroberfläche der Beispielanwendung

public delegate void DataSavedHandler(DataItem data);

public partial class MainPage : UserControl
{
  private SolidColorBrush STATUS_GREEN = new SolidColorBrush(Colors.Green);
  private SolidColorBrush STATUS_RED = new SolidColorBrush(Colors.Red);

  public event DataSavedHandlerDataSavedEvent;

  public MainPage()
  {
    InitializeComponent();
    ((SampleCode.App)Application.Current).ConnectionStatusChangedEvent += new
      ConnectionStatusChangedHandler(MainPage_ConnectionStatusChangedEvent);
    IndicateStatus(((NetworkStatus.App)Application.Current).IsConnected);
    BindNews();
  }

  private void MainPage_ConnectionStatusChangedEvent(bool isConnected)
  {
    IndicateStatus(isConnected);
  }

  private void IndicateStatus(bool isConnected)
  {
    if (isConnected)
    {
      StatusEllipse.Fill = STATUS_GREEN;
    }
    else
    {
      StatusEllipse.Fill = STATUS_RED;
    }
  }

  private void BindNews()
  {
    NewsListBox.ItemsSource = 
      ((SampleCode.App)Application.Current).Repository.News;
  }

  private void button1_Click(object sender, RoutedEventArgs e)
  {
    DataItem dataItem = new DataItem
    {
      Title = this.TitleTextBox.Text,
      Detail = this.DetailTextBox.Text
    };
    DataSavedHandler handler = this.DataSavedEvent;
    if (handler != null)
    {
      handler(dataItem);
    }

    this.TitleTextBox.Text = string.Empty;
    this.DetailTextBox.Text = string.Empty;
  }
}

Die Klasse in Abbildung 9 löst ein Ereignis aus, um anzuzeigen, dass Daten gespeichert wurden. Ein Beobachter (in diesem Fall App.xaml.cs) abonniert dieses Ereignis und stellt die Daten in die ObservableQueue.

Eine neue Anwendungsklasse

Die Silverlight-Unterstützung für nur zeitweise verbundene Anwendungen ermöglicht eine neue Anwendungsklasse. Solche Anwendungen werfen neue Probleme auf, und die Entwickler müssen überlegen, wie sich die Anwendung bei bestehender bzw. nicht bestehender Verbindung verhalten soll. In diesem Artikel wurden solche Überlegungen vorgestellt, ergänzt durch Strategien und Beispielcode zur Lösung der anfallenden Probleme. Angesichts der Vielfalt möglicher Szenarios in einer zeitweise verbundenen Welt sind diese Beispiele natürlich nur ein erster Denkanstoß.

Mark Bloodworth ist Architekt im Entwickler- und Plattformteam bei Microsoft, wo er mit anderen Unternehmen an innovativen Projekten arbeitet. Vor seiner Tätigkeit für Microsoft war er leitender Lösungsarchitekt bei BBC Worldwide, wo er das Team für Architektur und Systemanalyse leitete. Im Lauf seiner Karriere befasste er sich hauptsächlich mit Microsoft-Technologien, insbesondere mit dem Microsoft .NET Framework, ergänzt durch etwas Java. Unter der Adresse remark.wordpress.com unterhält er einen Blog.

Dave Brown ist seit über neun Jahren für Microsoft tätig. Er begann bei den Microsoft Consulting Services zum Thema Internettechnologien. Zurzeit arbeitet er im Entwickler- und Plattformteam in Großbritannien als Architekt für das Microsoft Technology Centre. Zu seinen Aufgaben gehören Geschäftsanalysen für Kundenszenarios, das Design von Lösungsarchitekturen sowie die Verwaltung und Entwicklung von Code für Lösungen für Machbarkeitsstudien. Seinen Blog finden Sie unter drdave.co.uk/blog.

Unser Dank gilt dem folgenden technischen Experten: Ashish Shetty