Silverlight Tipp der Woche: Dynamisches Nachladen und Caching von XAP-Dateien

by St. Lange 19. December 2010 11:20

In diesem Tipp wird das dynamische Laden und lokale Caching von Teilen einer Silverlight-Anwendung zur Verbesserung des Ladeverhaltens vorgestellt.

Zusammenfassung

Auch sehr große Silverlight-Anwendungen sollten beim ersten Aufruf möglichst schnell starten und beim nächsten Start am besten gar keine Assemblies mehr übers Web transportieren müssen. Durch das Zwischenspeichern von Teilen der Anwendung im Isolated Storage kann dies recht einfach erreicht werden.

Überblick

Bevor eine Silverlight-Anwendung ausgeführt werden kann, muss zunächst ihre XAP-Datei vom Webserver heruntergeladen werden. Bei größeren Anwendungen kann dies schon mal unangenehm lange dauern. Mit der Projektoption „Reduce XAP size by using application library caching“ kann die Größe der XAP-Datei dadurch vermindert werden, dass bestimmte Assemblies im Browser Cache zwischengespeichert werden. Diese Methode hat jedoch zwei Nachteile: Zum einen wird der erste Start dadurch nicht beschleunigt, denn beim allerersten Mal müssen ja trotzdem alle Assemblies zunächst heruntergeladen werden. Zum anderen funktioniert das Verfahren nicht bei Out-of-Browser Anwendungen.

Ich möchte hier ein Verfahren vorstellen, bei dem die Gesamtanwendung in mehrere XAP-Dateien zerlegt und im Isolated Storage zwischengespeichert wird. Tim Heuer hat die Grundidee in zwei Blog-Artikeln hier und hier schon vor längerem beschrieben. Bevor ich dieses Verfahren jedoch in einem unserer Produkte eingesetzt habe, wollte ich bestimmte Aspekte noch genauer untersuchen.

Zum „Proof of Concept“ dient eine minimalistische Testanwendung, die die Konstellation in einem realen Projekt modelliert. In unserem echten Projekt gibt es unter anderem eine Komponente, die PDF-Dateien im Corporate Design des Kunden generieren wird. Diese Komponente kann aufgrund von eingebetteten Bildern, Grafiken, Fonts etc. so groß werden, dass ein regelmäßiges Herunterladen aufgrund der Wartezeit nicht akzeptabel wäre.

Beispiel Solution

Das vorgestellte Beispielprojekt berücksichtigt auch Versions- und Lokalisierungsaspekte und kann zum Experimentieren für eigene Lösungen als Ausgangspunkt dienen. Es besteht aus 6 Silverlight Projekten. XapLoadingAndCaching ist die eigentliche Startanwendung. GeneralLibrary steht exemplarisch für eine Assembly, die von allen Projekten referenziert wird. Die drei Projekte LargePart1 bis LargePart3 stehen für drei große Teilkomponenten, die einzeln geladen werden können, jedoch auch voneinander abhängig sind.

Wenn ein Silverlight Application Projekt compiliert wird, erzeugt Visual Studio dabei eine XAP-Datei. Diese ist ein Zip-Archiv mit allen benötigten Assemblies und sonstigen Ressourcen, die Teil dieser Anwendung sind. Das Projekt XapLoadingAndCaching referenziert die Projkete GeneralLibrary, LargePart1 und LargePart2. Allerdings ist für LargePart1 und LargePart2 die Projekt-Eigenschaft Copy Local auf False gesetzt. Durch diesen Trick werden diese beiden Assemblies nicht in die XAP-Datei der Anwendung aufgenommen, obwohl es im Code Aufrufe in beide Assemblies gibt.

XapLoadingAndCaching.xap enthält daher nur folgende Dateien:

Tipp: Wer WinZip oder ein vergleichbares Tool verwendet, kann darin „.xap“ als Dateierweiterung hinzufügen und dann XAP-Dateien einfach anklicken, um ihren Inhalt zu kontrollieren.

Durch das Weglassen von referenzierten Assemblies lassen sich also sehr kleine und damit schnell zu ladende XAP-Dateien für die Startanwendung erzeugen. Die Anwendung läuft problemlos, sofern man keine Funktionen aus noch nicht geladenen Teilen aufruft. Eine echte Anwendung kann so sehr schnell ihre Hauptseite anzeigen. Und während der Anwender sich darin umschaut, werden asynchron weitere Teile nachgeladen.

Hier im Beispiel können die Applikationsteile zum Experimentieren jeweils einzeln über die drei Load Buttons nachgeladen werden.

Mit den beiden Call Buttons werden Testfunktionen in den jeweiligen Teilen aufgerufen. Drückt man einen der Buttons, bevor der benötigte Teil geladen wurde, kommt es erwartungsgemäß zu einer Exception.

Nachladen und Caching

Mit der Klasse WebClient kann man jede Art von Dateien und somit auch Assemblies vom Server downloaden und dann über die Klasse AssemblyPart in die laufende AppDomain hinzufügen. Das ist aber recht unpraktisch. Typische Komponenten bestehen meist aus mehreren zusammengehörigen Assemblies, die auf einen Schlag geladen werden sollten. Weiterhin sind Assembly-Dateien nicht komprimiert, was die Downloadzeit im Vergleich zu einer gezippten Datei mindestens verdoppelt.

Der nächste Trick besteht nun darin, die drei LargePart Projekte nicht als Silverlight Class Libraries, sondern als Silverlight Applications anzulegen. Dadurch werden automatisch XAP-Dateien generiert, die von Visual Studio auch gleich noch nach jedem Compilieren ins ClientBin-Verzeichnis der Web-Anwendung kopiert werden.

Im Beispiel ist festgelegt worden, dass LargePart1 vor LargePart2 geladen werden muss. Daher ist die gemeinsame Assembly PartLibrary nur in LargePart1.xap enthalten. LargePart2 referenziert zwar auch PartLibaray, aber Copy Local ist hier wieder auf False gesetzt, denn diese Assembly ist ja schon im vorher geladenen Part enthalten gewesen. Dadurch wird LargePart2.xap kleiner.

Die durch die Projektvorlage generierte Datei App.xaml wird übrigens nicht benötigt und kann aus dem Projekt herausgelöscht werden, denn diese XAP-Dateien benötigen ja kein Startup-Objekt. Es stört aber auch nicht, wenn sie wie in LargePart1 einfach drin bleiben. Man könnte App.xaml aber auch drin lassen und so diese XAP-Datei beispielsweise zu Testzwecken alleinstehend startbar machen.

Der nächste Trick ist das Zwischenspeichern der XAP-Dateien im Isolated Storage. Beim nächsten Start wird zunächst einmal nachgeschaut, ob die benötigte XAP-Datei schon auf dem Client Rechner vorliegt. Im Normalfall startet so auch eine extrem große Browser-Anwendung blitzschnell.

Hier zunächst der Code zum Downloaden einer XAP-Datei. Es ist jedoch nur eine Skizze zum Verständnis des Konzepts. Fehlerbehandlung oder die Erhöhung des Isolated Storage Kontingents wurden hier weggelassen.

public static void LoadXap(string name, string version)
{
  // Do not load XAP file more than once.
  if (_loadedXaps.ContainsKey(name))
    return;
 
  using (var store = IsolatedStorageFile.GetUserStoreForSite())
  {
    string cachedFileName = GetCachedFileName(name, version);
    if (store.FileExists(cachedFileName))
    {
      // Take file from cache.
      IsolatedStorageFileStream fileStream =
        store.OpenFile(cachedFileName, FileMode.Open, FileAccess.Read);
      LoadXap(fileStream);
      _loadedXaps.Add(name, null);
    }
    else
    {
      // Clear older version of file, if any exists.
      ClearCache(store, name);
 
      // Download file from site of origin.
      var webClient = new WebClient();
      webClient.OpenReadAsync(new Uri(name, UriKind.Relative));
      webClient.OpenReadCompleted += (sender, e) =>
      {
        using (var store2 = IsolatedStorageFile.GetUserStoreForSite())
        {
          // Save copy of new file in store.
          int length = (int)e.Result.Length;
          byte[] buffer = new byte[length];
          e.Result.Read(buffer, 0, length);
          using (var fileStream = store2.CreateFile(cachedFileName))
          {
            fileStream.Write(buffer, 0, length);
          }
          LoadXap(e.Result);
          _loadedXaps.Add(name, null);
        }
      };
    }
  }
}

Die Funktion erhält neben dem XAP-Dateinamen auch noch einen Version-String. Dieser wird an den Namen der zwischengespeicherten Datei angehängt. Dadurch wird sichergestellt, dass die Datei erneut heruntergeladen wird, wenn auf dem Server eine neuere Version vorliegt. In einem realen Projekt muss man einfach bei jedem Deployment die Version hochzählen. Hier im Beispiel kann man in der Textbox eine Versionsnummer eingeben oder mit dem Clear Cache Button die Dateien manuell löschen.

Um zu sehen, welche Dateien im Cache sind, kann man im Pfad

C:\Users\CurrentUser\AppData\LocalLow\Microsoft\Silverlight\is\

nach „LargePart“ suchen. Dann findet man den Isolated Storage der Anwendung.

Nachdem die XAP-Datei auf dem Client vorliegt, werden die in ihr enthaltenen Assemblies geladen:

static void LoadXap(Stream stream)
{
  StreamResourceInfo sri = new StreamResourceInfo(stream, null);
 
  // Get a list of all assembly part names from application manifest.
  string appManifest = new StreamReader(Application.GetResourceStream(sri,
      new Uri("AppManifest.xaml", UriKind.Relative)).Stream).ReadToEnd();
  XElement deploy = XDocument.Parse(appManifest).Root;
  List<XElement> parts = (from assemblyParts in deploy.Elements().Elements()
      select assemblyParts).ToList();
 
  // Load all assembly parts.
  foreach (var source in parts.Select(element => element.Attribute("Source")))
  {
    var streamInfo = Application.GetResourceStream(sri, new Uri(source.Value, UriKind.Relative));
    new AssemblyPart().Load(streamInfo.Stream);
  }
}

Die in der XAP-Datei enthaltenen Assemblies werden über das Manifest herausgesucht und geladen.

Lokalisierung

LargePart2 enthält zwei RESX-Dateien mit einem deutschen bzw. englischen Text. Ein Blick in das für LargePart2.xap generierte Manifest zeigt, dass die deutsche Satellite Assembly LargePart2.resources.dll korrekt im Unterverzeichnis de enthalten ist:

<Deployment xmlns=http://schemas.microsoft.com/client/2007/deployment …>
  <Deployment.Parts>

    <AssemblyPart x:Name="LargePart2" Source="LargePart2.dll" />
    <AssemblyPart Source="de/LargePart2.resources.dll" />
  </Deployment.Parts>
</Deployment>

Der Testcode bestätigt, dass alles wie gewünscht funktioniert:

ResourceManager rm = new ResourceManager("LargePart2.StringResources",
    Assembly.GetExecutingAssembly());
var de = rm.GetString("String1");
var en = rm.GetString("String1", CultureInfo.InvariantCulture);

Sowohl der englische als auch der deutsche Text werden korrekt geladen.

Das Unterverzeichnis de existiert nur im Zip-Archiv der XAP-Datei. Im Isolated Storage liegt die Datei LargePart2.xap-1.00. Man braucht sich also um nichts zu kümmern, was im Vergleich mit einem Download einzelner Assemblies äußerst praktisch ist.

Es ist übrigens egal, in welcher Reihenfolge voneinander abhängige Assemblies geladen werden, da erst bei ihrer ersten Verwendung das Vorhandensein von referenzierten Assemblies überprüft wird.

Statische Ressouce-Dateien

Neben Code und lokalisierten Strings kann eine XAP-Datei natürlich auch Assemblies mit sonstigen statischen Ressourcen enthalten. LargePart3.xap enthält eine Assembly mit einem Beispiel-Font. Da XAP-Dateien Zip-Container sind, ist diese Datei signifikant kleiner als die Font-Datei selbst. Es macht also sehr viel Sinn, statische Ressourcen ebenfalls in XAP-Dateien zu verpacken.

Der folgende Code zeigt den Zugriff auf die Font Ressource:

void LoadFont(object sender, RoutedEventArgs e)
{
  var stream = Application.GetResourceStream(
    new Uri("/LargePart3;component/fonts/Early_Tickertape.ttf",
    UriKind.Relative));
  txtBlock.FontSource = new FontSource(stream.Stream);
  txtBlock.FontFamily = new FontFamily("Early Tickertape");
}

Wenn LargePart3.xap geladen wurde, ändert dieser Code den Font des angezeigten Beispieltextes.

Out-of-Browser Anwendungen

Das vorgestellte Verfahren funktioniert bei Browser und Out-of-Browser Anwendungen identisch. Bei Out-of-Browser Anwendungen muss man allerdings zwei Dinge beachten, wenn man offline arbeiten möchte: Alle XAP-Dateien müssen im Cache vorliegen und der Cache darf nicht gelöscht werden.

Managed Extensibility Framework

Der im Beispiel verwendete Trick mit dem Referenzieren von Assemblies, die nicht in der eigenen XAP-Datei vorhanden sind, sollte in Produktivcode besser nicht verwendet werden, da das Risiko eines Funktionsaufrufes in eine nicht geladene Assembly besteht. Während der Entwicklungszeit und zum Debuggen ist es allerdings praktisch.

Im echten Projekt verwenden wir das Managed Extensibility Framework. Bevor eine Komponente nicht geladen ist, steht sie im Composition-Container auch nicht zur Verfügung und kann somit auch nicht unbeabsichtigt aufgerufen werden. Unmittelbar nach Programmstart werden die XAP-Dateien in einer definierten Reihenfolge im Hintergrund heruntergeladen und in die laufende AppDomain hinzugefügt. Nach jedem Download findet ein Recomposition des Containers statt, damit die neu hinzugekommenen Teile auch verfügbar sind.

Nur durch MEF bzw. einem vergleichbaren Framework kann zuverlässig sichergestellt werden, dass man nicht doch einmal versehentlich einen Funktionsaufruf ins Leere macht.

Fazit

XAP-Dateien lassen sich auf sehr einfache Weise als universelle Zip-Container für Code und Daten verwenden. Der Verwaltungsaufwand bei der Entwicklung ist minimal. Das Nachladen und lokale Zwischenspeichern der Dateien führt zu einer spürbaren Verbesserung der Benutzererfahrung, vor allem wenn die Anwendung sehr groß geworden ist. Das Ganze ist auch keinerlei Hack, sondern einfach die konsequente Nutzung der Möglichkeiten von Silverlight.

Hier das Projekt:

XapLoadingAndCaching.zip (119 kB)

 
kick it on dotnet-kicks.de

Tags:

Silverlight

Comments are closed

Powered by BlogEngine.NET 1.6.1.0 - Impressum