Windows Phone

Hinter den Kulissen: Eine Windows Phone-Feed-Reader-App

Matt Stroshane

Ich bin süchtig nach Feeds. Ich liebe die Magie der RSS- und Atom-Feeds, dass Nachrichten zu mir kommen anstatt andersrum. Da der Zugang zu so viel Informationen einem so leicht gemacht wird, ist es jedoch zu einer Herausforderung geworden, die Informationen auf sinnvolle Weise zu konsumieren. Deshalb war ich, als ich hörte, dass einige Praktikanten bei Microsoft eine Windows Phone-Feed-Reader App entwickelten, sehr gespannt darauf herauszufinden, wie sie dieses Problem angehen würden.

Francisco Aguilera, Suman Malani und Ayomikun (George) Okeowo hatten im Rahmen ihres Praktikums 12 Wochen Zeit, um eine Windows Phone App zu entwickeln, die einige neue Merkmale der Windows Phone SDK 7.1 Version enthielt. Als Neulinge bei der Entwicklung von Windows Phone, waren sie gute Versuchspersonen für unsere Plattform, Tools und Dokumentierung.

Nachdem sie ihre Optionen in Erwägung gezogen hatten, entschieden sie sich für eine Feed-Reader-App, die eine lokale Datenbank, Live-Tiles und einen Background-Agent aufzeigen würde. Sie haben weit mehr als das gezeigt! In diesem Artikel werde ich Ihnen Schritt für Schritt aufzeigen, wie sie diese Merkmale verwendet haben. Installieren Sie also Windows Phone SDK 7.1, laden Sie den Code herunter und lassen Sie ihn sich auf Ihrem Bildschirm anzeigen. Legen wir los!

Verwenden der App

Die Schaltzentrale der App ist die Hauptseite, MainPage.xaml (Abbildung 1). Sie besteht aus vier Panorama-Panels: "Was gibt es Neues", "Gezeigt", "Alle" und "Einstellungen". Das "Was gibt es Neues"-Panel zeugt die neuesten Updates für die Feeds. "Gezeigt" zeigt sechs Artikel an, von denen, ausgehend von Ihrem Leseverlauf, angenommen wird, dass sie Sie interessieren. Das "Alle"-Panel listet all Ihre Kategorien und Feeds auf. Verwenden Sie die Einstellung im "Einstellungen"-Panel, um Artikel nur über WLAN herunterzuladen.

The Main Page of the App After Creating a Windows Phone News CategoryAbbildung 1 Die Hauptseite der App nach der Erstellung einer Windows Phone News Category

Die "Was gibt es Neues"- und "Gezeigt"-Panels stellen einen Weg bereit, um direkt zu einem Artikel zu navigieren. Das "Alle"-Panel stellt eine Liste von Kategorien und Feeds bereit. Vom "Alle"-Panel können Sie direkt zu einer Sammlung von Artikeln navigieren, die nach Feed oder nach Kategorie eingeteilt sind. Sie können auch die Anwendungsleiste im "Alle"-Panel verwenden, um einen neuen Feed oder eine neue Kategorie hinzuzufügen. Abbildung 2 zeigt wie die Hauptseite mit den anderen acht Seiten der App in Zusammenhang steht.

The Page Navigation Map, with Auxiliary Pages in GrayAbbildung 2 Die Seitennavigations-Karte mit Hilfsseiten (in grau)

Ähnlich zum Drehen, können Sie auf den Kategorie-, Feed- oder Artikelseiten horizontal navigieren. Wenn Sie auf einer dieser Seiten sind, erscheinen Pfeile in der Anwendungsleiste (siehe Abbildung 3). Die Pfeile ermöglichen Ihnen, die Daten für die/den vorige(n) oder nächste(n) Kategorie, Feed oder Artikel in der Datenbank anzuzeigen. Wenn Sie zum Beispiel die Kategorie "Business" auf der Kategorieseite ansehen, wird Ihnen, wenn Sie auf den Pfeil "weiter" tippen, die Kategorie "Unterhaltung" auf der Kategorieseite angezeigt.

The Category, Feed and Article Pages with Their Application Bars ExpandedAbbildung 3 Die Kategorie-, Feed- und Artikelseiten mit ihren ausgeklappten Anwendungsleisten

Tatsächlich navigieren die Pfeiltasten jedoch nicht zu einer anderen Kategorieseite. Stattdessen ist die gleiche Seite an eine andere Datenquelle gebunden. Wenn Sie auf die "Zurück"-Schaltfläche des Mobiltelefons tippen gelangen Sie zurück zum "Alle"-Panel ohne dafür einen speziellen Navigations-Code zu benötigen.

Von der Artikelseite können Sie zur Seite "Mit anderen teilen" navigieren und per Kurznachricht, E-Mail oder über ein soziales Netzwerk einen Link verschicken. Die Anwendungsleiste ermöglicht auch die Ansicht des Artikels im Internet Explorer, "Hinzufügen zu Favoriten" oder dessen Entfernung aus der Datenbank.

Einblick in die Hintergründe

Wenn Sie die Lösung in Visual Studio öffnen, werden Sie sehen, dass es eine C#-App ist, die in drei verschiedene Projekte unterteilt ist:

  1. FeedCast: Der Teil den der Benutzer sieht - die Vordergrund-App (Ansicht und ViewModel-Code).
  2. FeedCastAgent: Der Background-Agent-Code (periodisch geplante Aufgabe).
  3. FeedCastLibrary: Der geteilte Netzwerk- und Datencode

Das Team verwendete Silverlight für das Windows Phone Toolkit (November 2011) und Microsoft Silverlight 4 SDK. Die Steuerungen vom Toolkit—Microsoft.Phone.Controls.Toolkit.dll—werden auf den meisten Seiten der App verwendet. Zum Beispiel werden HubTile-Steuerungen verwendet, um Artikel im "Gezeigt"-Panel der Hauptseite anzuzeigen. Um den Netzwerkbetrieb zu unterstützen, verwendete das Team System.ServiceModel.Syndication.dll von Silverlight 4 SDK. Diese Baugruppe ist im Windows Phone SDK nicht enthalten und wurde nicht speziell für Mobiltelefon-Apps optimiert, aber die Mitglieder des Teams fanden, dass sie für ihre Bedürfnisse gut geeignet war.

Das Vordergrund-App-Projekt, FeedCast, ist das größte der drei bei dieser Lösung. Wie bereits erwähnt, handelt es sich hierbei um den Teil der App, den der Benutzer sieht. Dieser Teil ist in neun Ordner unterteilt:

  1. Konverter: Wertkonverter, die die Lücke zwischen Daten und Benutzerschnittstelle überbrücken.
  2. Symbole: Die auf der Anwendungsleiste verwendeten Symbole.
  3. Bilder: Bilder, die von den HubTiles verwendet werden, wenn Artikel keine Bilder haben.
  4. Bibliotheken: Die Toolkit- und Syndication-Baugruppen.
  5. Modelle: Datenbezogener Code, der vom Background-Agent nicht verwendet wird.
  6. Ressourcen: Lokalisierungsressourcen-Dateien auf Englisch und Spanisch.
  7. Themen: Anpassungen für die HeaderedListBox-Steuerung.
  8. ViewModels: ViewModels und andere Helferklassen
  9. Ansichten: Code für jede Seite in der Vordergrund-App.

Diese App folgt dem Model-View-ViewModel(MVVM)-Muster. Code im Ordner "Ansichten" konzentriert sich hauptsächlich auf die Benutzerschnittstelle. Die Logik und die Daten, die den einzelnen Seiten zugeordnet sind, sind durch Code im Ordner "ViewModels" definiert. Auch wenn der Ordner "Modelle" einigen datenbezogenen Code enthält, sind die Datenobjekte im FeedCastLibrary-Projekt definiert. Der "Modell"-Code darin wird von der Vordergrund-App und dem Background-Agent wiederverwendet. Weitere Informationen zu MVVM finden Sie unter wpdev.ms/mvvmpnp.

Das FeedCastLibrary-Projekt enthält den Daten- und Netzwerk-Code, der von der Vordergrund-App und dem Background-Agent verwendet wird. Dieses Projekt enthält zwei Ordner: Daten und Netzwerke. Im Ordner "Daten" wird das FeedCast-Modell von partiellen Klassen in vier Dateien beschrieben: LocalDatabaseDataContext.cs, Article.cs, Category.cs und Feed.cs. Die Datei DataUtils.cs enthält den Code, der die üblichen Datenbankvorgänge durchführt. Eine Helferklasse zur Verwendung von isolierten Speichereinstellungen befindet sich in der Datei Settings.cs. Der Ordner "Netzwerk" des FeedCastLibrary-Projekts enthält den Code, der zum Herunterladen und Parsen von Inhalt aus dem Web verwendet wird, wobei das Wichtigste in diesem Ordner die Downloadmethoden in der Datei WebTools.cs sind.

Es gibt nur eine Klasse im FeedCastAgent-Projekt, Scheduled­Agent.cs, und dabei handelt es sich um den Background-Agent-Code. Die OnInvoke-Methode wird aufgerufen, wenn sie ausgeführt wird und die SendToDatabase-Methode wird aufgerufen wenn die Downloads vollständig sind. Ich werde das Herunterladen später noch näher erläutern.

Lokale Datenbank

Um maximale Produktivität zu erzielen, konzentrierte sich jedes Teammitglied auf einen anderen Bereich der App. Aguilera konzentrierte sich auf die Benutzerschnittstelle, Ansichten und Ansichtenmodelle in der Vordergrund-App. Okeowo arbeitete am Netzwerk und daran Daten aus den Feeds zu erhalten. Malani arbeitete an der Architektur der Datenbank und an Datenbankvorgängen.

Bei Windows Phone können Sie Ihre Daten in einer lokalen Datenbank speichern. Die Datenbank wird als lokal bezeichnet, weil es sich um eine Datenbankdatei handelt, die in isoliertem Speicher liegt (der auf dem Gerät befindliche Speicherbehälter Ihrer App, der von anderen Apps isoliert ist). Im Wesentlichen lassen sich die Datenbanktabellen als Plain Old CLR Objekte beschreiben, wobei die Eigenschaften dieser Objekte die Datenbankspalten repräsentieren. Dies ermöglicht es jedem Objekt dieser Klasse als eine Reihe in der entsprechenden Tabelle gespeichert zu werden. Um die Datenbank zu repräsentieren, erzeugt man ein spezielles Objekt, das als Datenkontext bezeichnet wird, der vom System.Data.Linq.DataContext erbt.

Die magische Zutat der lokalen Datenbank ist die LINQ to SQL Laufzeit - Ihr Datenbutler. Sie rufen die Datenkontext-Methode CreateDatabase auf und LINQ to SQL erzeugt die SDF-Datei in isoliertem Speicher. Sie erzeugen LINQ-Anfragen, um zu spezifizieren, welche Daten Sie wollen, und LINQ to SQL sendet stark typisierte Objekte zurück, die Sie an Ihre Benutzerschnittstelle binden können. LINQ to SQL ermöglicht Ihnen, sich auf Ihren Code zu konzentrieren, während es jeglichen Datenbankbetrieb auf niedriger Ebene handhabt. Weitere Informationen zur Verwendung einer lokalen Datenbank finden Sie unter wpdev.ms/localdb.

Anstatt alle Klassen abzutippen, verwendete Malani Visual Studio 2010 Ultimate, um einen anderen Ansatz zu wählen. Sie erstellte die Datenbanktabellen visuell, wobei sie den Server Explorer Add Connection Dialog verwendete, um eine SQL Server CE Datenbank zu erstellen, und dann den New Table Dialog verwendete, um die Tabellen zu erstellen.

Sobald Malani ihr Schema entworfen hatte, verwendete sie SqlMetal.exe um einen Datenkontext zu erzeugen. SqlMetal.exe ist ein Kommandozeilen-Dienstprogramm vom Desktop LINQ to SQL. Es hat die Aufgabe eine Datenkontextklasse basierend auf einer SQL-Server-Datenbank zu erstellen. Der Code, den es erzeugt, ist dem Windows Phone-Datenkontext recht ähnlich. Durch das Verwenden dieser Technik, war sie in der Lage die Tabellen visuell zu erstellen und den Datenkontext schnell zu erzeugen. Weitere Informationen zu SqlMetal.exe finden Sie unter wpdev.ms/sqlmetal.

Die von Malani erstellte Datenbank ist in Abbildung 4 gezeigt. Die drei Haupttabellen sind Kategorie, Feed und Artikel. Außerdem wird eine verknüpfende Tabelle, Category_Feed, verwendet, um eine viele-zu-viele Beziehung zwischen Kategorien und Feeds zu ermöglichen. Jede Kategorie kann mehreren Feeds zugeordnet werden und jedes Feed kann mehreren Kategorien zugeordnet werden. Beachten Sie, dass das Merkmal "Favoriten" der App eine besondere Kategorie ist, die nicht gelöscht werden kann.

The Database SchemaAbbildung 4: Das Datenbank-Schema

Der von SqlMetal.exe erzeugte Datenkontext ist jedoch immer noch in manchem Code enthalten, der von Windows Phone nicht unterstützt wird. Nachdem Malani die Datenkontext-Code-Datei zu dem Windows Phone-Projekt hinzugefügt hatte, kompilierte sie das Projekt, um zu ermitteln welcher Code nicht gültig war. Sie erinnert sich, dass sie einen Konstruktor entfernen musste, aber der Rest ließ sich gut kompilieren.

Bei näherer Untersuchung der Datenkontextdatei, LocalDatabase­DataContext.cs, stellen Sie vielleicht fest, dass alle Tabellen partielle Klassen sind. Der Rest des diesen Tabellen zugeordneten Codes (der nicht automatisch von SqlMetal.exe erzeugt wurde) wird in den Code-Dateien Article.cs, Category.cs und Feed.cs gespeichert. Dadurch dass Malani den Code auf diese Weise trennte, konnte sie Veränderungen am Datenbank-Schema vornehmen ohne die Extensibility Method Definitionen, die sie von Hand geschrieben hatte, zu beeinträchtigen. Hätte sie dies nicht getan, hätte sie die Methoden jedes Mal bei der automatischen Erzeugung von LocalDatabaseDataContext.cs neu hinzufügen müssen (da SqlMetal.exe jeglichen Code in der Datei überschreibt).

Wahren der Nebenläufigkeit

Wie bei den meisten Windows Phone Apps, die bestrebt sind für eine ansprechende, ununterbrochene Erfahrung zu sorgen, verwendet auch diese nebenläufige Threads, um ihre Arbeit zu verrichten. Zusätzlich zum Benutzerschnittstellen-Thread, der Benutzereingaben annimmt, könnten mehrere Background-Threads mit dem Herunterladen und Parsen von RSS-Feeds beschäftigt sein. Jeder dieser Threads wird schließlich Änderungen an der Datenbank vornehmen müssen.

Wenngleich die Datenbank selbst stabilen nebenläufigen Zugang bietet, ist die DataContext-Klasse nicht Thread-sicher. Anders ausgedrückt kann das in dieser App verwendete einzelne globale DataContext-Objekt nicht über mehrere Threads geteilt werden ohne eine Art Nebenläufigkeitsmodell hinzuzufügen. Um dieses Problem anzugehen, verwendete Malani die LINQ to SQL-Nebenläufigkeits-APIs und ein Mutex-Objekt aus dem System.Threading-Namespace.

In der Datei DataUtils.cs werden die Mutex WaitOne- und ReleaseMutex-Methode verwendet um den Zugang zu Daten in den Fällen zu synchronisieren, in denen es zu Konflikten zwischen den DataContext-Klassen kommen könnte. Wenn zum Beispiel mehrere nebenläufige Threads (von der Vordergrund-App oder dem Background-Agent) die SaveChangesToDB-Methode etwa zur gleichen Zeit aufrufen würden, würde der Code, der als erster WaitOne ausführt, fortfahren können. Der WaitOne Aufruf des anderen wird nicht beendet bis der erste Code ReleaseMutex aufruft. Deshalb ist es wichtig den ReleaseMutex-Aufruf in die Endanweisung zu schreiben, wenn Try/Catch/Finally für Datenbankvorgänge verwendet wird. Ohne den Aufruf des ReleaseMutex wartet der andere Code beim WaitOne-Aufruf bis der Owning Thread beendet. Aus der Sicht des Benutzers könnte dies "ewig" dauern.

Anstatt nur ein einzelnes globales DataContext-Objekt, können Sie Ihre App auch so entwerfen, dass sie kleinere DataContext-Objekte auf einer pro-Thread-Basis erzeugt und zerstört. Die Teammitglieder stellten jedoch fest, dass der globale DataContext-Ansatz die Entwicklung vereinfachte. Ich sollte auch anmerken, dass das Team auch eine Zugriffssperre statt eines Mutex hätten verwenden können, da die App nur vor Cross-Thread-Zugang und nicht vor Cross-Prozess-Zugang schützen sollte. Die Zugangssperre hätte vielleicht eine bessere Leistung erbracht.

Nutzung von Daten

Okeowo konzentrierte seine Bemühungen darauf, Daten in die App zu bringen. Die Datei WebTools.cs enthält den Code bei dem am meisten passiert. Die WebTools-Klasse wird jedoch nicht nur zum Herunterladen von Feeds verwendet - sie wird auch auf der neuen Feed-Seite verwendet, um nach neuen Feeds auf Bing zu suchen. Er erreichte dies, indem er eine gemeinsame Schnittstelle, IXmlFeedParser, erstellte und den Parsing-Code in verschiedene Klassen abstrahierte. Die SynFeedParser-Klasse parst die Feeds und die SearchResultParser-Klasse parst die Bing-Suchergebnisse.

Tatsächlich sendet die Bing-Anfrage jedoch keine Artikel zurück (trotz der Sammlung von Artikel-Objekten die von der IXmlFeedParser-Schnittstelle zurückgesendet werden). Stattdessen sendet sie eine Liste von Feed-Namen und URIs zurück. Wo liegt der Fehler? Okeowo erkannte, dass die Artikel-Klasse bereits die Eigenschaften hatte, die er benötigte, um einen Feed zu beschreiben; er brauchte gar keine andere Klasse zu erstellen. Beim Parsen von Suchergebnissen verwendete er ArticleTitle für den Feed-Namen und ArticleBaseURI für den Feed-URI. Siehe SearchResultParser.cs im entsprechenden Codedownload für weitere Informationen.

Der Code auf der neuen Seite "ViewModel" (NewFeedPageViewModel.cs im Beispielcode) zeigt wie die Bing-Suchergebnisse konsumiert werden. Zuerst wird die GetSearchString-Methode verwendet, um den Bing-Search-String-URI basierend auf den Suchbegriffen, die der Benutzer auf der Seite NewFeedPage eingibt, zusammenzufügen, wie im folgenden Code-Ausschnitt gezeigt ist:

private string GetSearchString(string query)
{
  // Format the search string.
  string search = "http://api.bing.com/rss.aspx?query=feed:" + query +
    "&source=web&web.count=" + _numOfResults.ToString() +
    "&web.filetype=feed&market=en-us";
  return search;
}

Der _numOfResults-Wert grenzt ein wie viele Suchergebnisse zurückgesendet werden. Weitere Informationen zum Zugriff auf Bing über RSS finden Sie auf der MSDN Library-Seite, “Zugriff auf Bing über RSS”, unter bit.ly/kc5uYO.

Die GetSearchString-Methode wird in der GetResults-Methode aufgerufen, wo die Daten tatsächlich von Bing abgerufen werden (siehe Abbildung 5). Die GetResults-Methode sieht etwas rückwärts gerichtet aus, da sie eine Lambda-Expression auflistet, die das AllDownloadsFinished-Ereignis "inline" handhabt, bevor der Code zum Initiieren des Downloads tatsächlich aufgerufen wird. Wenn die Download-Methode aufgerufen wird, stellt das WebTools-Objekt Bing eine Anfrage über den URI, der mit GetSearchString erstellt wurde.

Abbildung 5 Die GetResults-Methode im NewFeedPageView­Model.cs stellt Bing eine Anfrage nach neuen Feeds

public void GetResults(string query, Action<int> Callback)
{
  // Clear the page ViewModel.
  Clear();
  // Get the search string and put it into a feed.
  Feed feed = new Feed { FeedBaseURI = GetSearchString(query) };
  // Lambda expression to add results to the page
  // ViewModel after the download completes.
  // _feedSearch is a WebTools object.
  _feedSearch.AllDownloadsFinished += (sender, e) =>
    {
      // See if the search returned any results.
      if (e.Downloads.Count > 0)
      {
        // Add the search results to the page ViewModel.
        foreach (Collection<Article> result in e.Downloads.Values)
        {
          if (null != result)
          {
            Deployment.Current.Dispatcher.BeginInvoke(() =>
              {
                foreach (Article a in result)
                {
                  lock (_lockObject)
                  {
                    // Add to the page ViewModel.
                    Add(a);
                  }
                }
                Callback(Count);
              });
          }
        }
      }
      else
      {  
        // If no search results were returned.
        Deployment.Current.Dispatcher.BeginInvoke(() =>
          {
            Callback(0);
          });
      }
    };
  // Initiate the download (a Bing search).
  _feedSearch.Download(feed);
}

Die WebTools-Download-Methode wird auch vom Background-Agent verwendet (siehe Abbildung 6), aber auf andere Weise. Anstatt nur von einem Feed herunterzuladen, gibt der Agent eine Liste von mehreren Feeds an die Methode weiter. Um Ergebnisse abzurufen, verwendet der Agent eine andere Strategie. Anstatt zu warten, bis Artikel von allen Feeds heruntergeladen wurden (über das AllDownloadsFinished-Ereignis), speichert der Agent die Artikel sobald jeder Feed-Download beendet ist (über das SingleDownloadFinished-Ereignis).

Abbildung 6 Der Background-Agent initiiert einen Download (ohne Debuggen-Kommentare)

protected override void OnInvoke(ScheduledTask task)
{
  // Run the periodic task.
  List<Feed> allFeeds = DataBaseTools.GetAllFeeds();
  _remainingDownloads = allFeeds.Count;
  if (_remainingDownloads > 0)
  {
    Deployment.Current.Dispatcher.BeginInvoke(() =>
      {
        WebTools downloader = new WebTools(new SynFeedParser());
        downloader.SingleDownloadFinished += SendToDatabase;
        try
        {
          downloader.Download(allFeeds);
        }
        // TODO handle errors.
        catch { }
      });
  }
}

Die Aufgabe des Background-Agent besteht darin, alle Ihre Feeds auf dem neuesten Stand zu halten. Um dies zu tun, gibt er eine Liste von allen Feeds an die Download-Methode weiter. Der Background-Agent hat nur wenig Zeit um ausgeführt zu werden, und wenn die Zeit um ist, wird der Vorgang sofort angehalten. Deshalb sendet der Agent, während er Feeds herunterlädt, die Artikel an die Datenbank, und zwar immer von einem Feed nach dem anderen. Dadurch ist die Wahrscheinlichkeit viel größer, dass der Background-Agent neue Artikel speichern kann, bevor er angehalten wird.

Die Single-Feed- und Multiple-Feed-Download-Methoden sind eigentlich Überbelastungen für denselben Code. Der Download-Code initiiert eine HttpWebRequest für jeden Feed (asynchron). Sobald die erste Anfrage zurückkommt, ruft er den SingleDownloadFinished-Ereignis-Handhaber. Die Feed-Informationen und -Artikel werden dann unter Verwendung des SingleDownloadFinishedEventArgs in das Ereignis verpackt. Wie in Abbildung 7 gezeigt, wird die SendToDatabase-Methode mit der SingleDownloadFinshed-Methode verschaltet. Wenn diese zurückkommt, nimmt SendToDatabase die Artikel aus den Ereignis-Argumenten heraus und gibt sie an das DataUtils-Objekt namens DataBaseTools weiter.

Abbildung 7 Der Background-Agent speichert Artikel in der Datenbank (ohne Debuggen-Kommentare)

private void SendToDatabase(object sender, 
  SingleDownloadFinishedEventArgs e)
{
  // Ensure download is not null!
  if (e.DownloadedArticles != null)
  {
    DataBaseTools.AddArticles(e.DownloadedArticles, e.ParentFeed);
    _remainingDownloads--;
  }
  // If no remaining downloads, tell scheduler the background agent is done.
  if (_remainingDownloads <= 0)
  {
    NotifyComplete();
  }
}

Sollte der Agent alle Downloads innerhalb der ihm zugewiesenen Zeit beenden, ruft er die NotifyComplete-Methode auf, um das Betriebssystem zu benachrichtigen, dass er fertig ist. Das erlaubt es dem Betriebssystem diese ungenutzten Ressourcen anderen Background-Agents zuzuteilen.

Wenn man dem Code einen Schritt tiefer folgt, prüft die AddArticles-Methode in der DataUtils-Klasse, um sicherzustellen, dass der Artikel neu ist, bevor er zur Datenbank hinzugefügt wird. Beachten Sie in Abbildung 8, wie erneut ein Mutex verwendet wird, um einen Konflikt bezüglich des Datenkontext zu vermeiden. Wenn der Artikel schließlich als neu erachtet wird, wird er mit der SaveChangesToDB-Methode in der Datenbank gespeichert.

Abbildung 8 Hinzufügen von Artikeln zur Datenbank in der Datei DataUtils.cs

public void AddArticles(ICollection<Article> newArticles, Feed feed)
{
  dbMutex.WaitOne();
  // DateTime date = SynFeedParser.latestDate;
  int downloadedArticleCount = newArticles.Count;
  int numOfNew = 0;
  // Query local database for existing articles.
  for (int i = 0; i < downloadedArticleCount; i++)
  {
    Article newArticle = newArticles.ElementAt(i);
    var d = from q in db.Article
            where q.ArticleBaseURI == newArticle.ArticleBaseURI
            select q;
    List<Article> a = d.ToList();
    // Determine if any articles are already in the database.
    bool alreadyInDB = (d.ToList().Count == 0);
    if (alreadyInDB)
    {
      newArticle.Read = false;
      newArticle.Favorite = false;
      numOfNew++;
    }
    else
    {
      // If so, remove them from the list.
      newArticles.Remove(newArticle);
      downloadedArticleCount--;
      i--;
    }               
  }
  // Try to submit and update counts.
  try
  {
    db.Article.InsertAllOnSubmit(newArticles);
    Deployment.Current.Dispatcher.BeginInvoke(() =>
      {
        feed.UnreadCount += numOfNew;
        SaveChangesToDB();
      });
    SaveChangesToDB();
  }
  // TODO handle errors.
  catch { }
  finally { dbMutex.ReleaseMutex(); }
}

Die Vordergrund-App verwendet zum Konsumieren von Daten mit der Download-Methode eine ähnliche Technik, wie die, die man beim Background-Agent vorfindet. Siehe die Datei ContentLoader.cs im entsprechenden Codedownload für vergleichbaren Code.

Planung für den Background-Agent

Der Background-Agent ist genau das - ein Agent der im Hintergrund Arbeit für die Vordergrund-App ausführt. Wie Sie bereits in Abbildung 6 und Abbildung 7 gesehen haben, handelt es sich bei dem Code, der die Arbeit definiert, um eine Klasse namens Scheduled­Agent. Er leitet sich ab vom Microsoft.Phone.Scheduler.ScheduledTaskAgent (der vom Microsoft.Phone.BackgroundAgent abgeleitet ist). Wenngleich dem Agent viel Aufmerksamkeit gewidmet wird, weil er die schweren Lasten trägt, würde er ohne geplante Aufgabe nie ausgeführt werden.

Bei der geplanten Aufgabe handelt es sich um das Objekt, das verwendet wird, um anzugeben, wann und wie oft der Background-Agent ausgeführt werden soll. Die in dieser App verwendete geplante Aufgabe ist eine periodische Aufgabe (Microsoft.Phone.Scheduler.PeriodicTask). Eine periodische Aufgabe wird regelmäßig für kurze Zeit ausgeführt. Damit die Aufgabe auch auf den Zeitplan gesetzt und befragt wird und so weiter, verwenden Sie den geplanten Action Service (ScheduledActionService). Weitere Informationen zu Background-Agents finden Sie unter wpdev.ms/bgagent.

Der geplante Aufgabencode für diese App befindet sich in der Datei BackgroundAgentTools.cs im Vordergrund-App-Projekt. Dieser Code definiert die StartPeriodicAgent-Methode, die von App.xaml.cs im Anwendungskonstruktor aufgerufen wird (siehe Abbildung 9).

Abbildung 9 Planung für die periodische Aufgabe in BackgroundAgentTools.cs (Ohne Kommentare)

public bool StartPeriodicAgent()
{
  periodicDownload = ScheduledActionService.Find(periodicTaskName) as PeriodicTask;
  bool wasAdded = true;
  // Agents have been disabled by the user.
  if (periodicDownload != null && !periodicDownload.IsEnabled)
  {
    // Can't add the agent. Return false!
    wasAdded = false;
  }
  // If the task already exists and background agents are enabled for the
  // application, then remove the agent and add again to update the scheduler.
  if (periodicDownload != null && periodicDownload.IsEnabled)
  {
    ScheduledActionService.Remove(periodicTaskName);
  }
  periodicDownload = new PeriodicTask(periodicTaskName);
  periodicDownload.Description =
    "Allows FeedCast to download new articles on a regular schedule.";
  // Scheduling the agent may not be allowed because maximum number
  // of agents has been reached or the phone is a 256MB device.
  try
  {
    ScheduledActionService.Add(periodicDownload);
  }
  catch (SchedulerServiceException) { }
  return wasAdded;
}

Vor der Planung der periodischen Aufgabe führt StartPeriodicAgent einige Überprüfungen durch, da immer die Möglichkeit besteht, dass sich die geplante Aufgabe nicht einplanen lässt. Zunächst einmal können geplante Aufgaben von Benutzern auf der Hintergrundaufgabenliste im Anwendungs-Panel der Einstellungen deaktiviert werden. Außerdem gibt es ein Limit dafür, wie viele Aufgaben auf einem Gerät zu einem Zeitpunkt freigegeben sein können. Es variiert je nach der Konfiguration des Geräts, aber es könnte ganz niedrig, nämlich nur bei sechs liegen. Wenn Sie versuchen eine geplante Aufgabe zu planen nachdem dieses Limit überschritten wurde, oder wenn Ihre App auf einem 256 MB-Gerät ausgeführt wird, oder wenn Sie dieselbe Aufgabe bereits geplant haben, wird die Add-Methode eine Ausnahme auslösen.

Diese App ruft bei jedem Start die StartPeriodicTask-Methode auf, da Background-Agents nach 14 Tagen ablaufen. Die Aktualisierung des Agents bei jedem Start stellt sicher, dass der Agent weiter ausgeführt werden kann, selbst wenn die App einige Tage lang nicht mehr gestartet wird.

Die periodicTaskName-Variable in Abbildung 9, die verwendet wird, um eine bestehende Aufgabe zu finden, ist gleich “FeedCastAgent.” Beachten Sie, dass dieser Name nicht den entsprechenden Background-Agent-Code identifiziert. Es handelt sich hierbei einfach nur um einen netten Namen, den Sie bei der Arbeit mit ScheduledActionService verwenden können. Die Vordergrund-App hat bereits vom Background-Agent erfahren, da er als Bezug für das Vordergrund-App-Projekt hinzugefügt wurde. Da der Background-Agent-Code als Projekt vom Typ Windows Phone Scheduled Task Agent erstellt wurde, waren die Tools in der Lage alles richtig miteinander zu verschalten als der Bezug hinzugefügt wurde. Die Beziehung zwischen der Vordergrund-App und dem Background-Agent ist im Vordergrund-App-Manifest angegeben (WMAppManifest.xml in the sample code), wie hier gezeigt:

<Tasks>
  <DefaultTask Name="_default" 
    NavigationPage="Views/MainPage.xaml" />
  <ExtendedTask Name="BackgroundTask">
    <BackgroundServiceAgent Specifier="ScheduledTaskAgent" 
      Name="FeedCastAgent"
      Source="FeedCastAgent" Type="FeedCastAgent.ScheduledAgent"/>
  </ExtendedTask>
</Tasks>

Tiles

Aguilera arbeitete an der Benutzerschnittstelle, den Ansichten und Ansichtenmodellen. Er arbeitete auch am Lokalisierungs- und Tiles-Merkmal. Tiles, manchmal auch als Live-Tiles bezeichnet, zeigen dynamischen Inhalt an und sind ab Start mit der App verknüpft. Die Tile-Anwendung jeder App kann an Start geheftet werden (ohne dass irgendein Arbeitsschritt seitens des Entwicklers erforderlich ist). Wenn Sie jedoch eine Verknüpfung zu einer anderen Seite als der Hauptseite Ihrer App herstellen möchten, müssen Sie Secondary Tiles implementieren. Diese ermöglichen es Ihnen, den Benutzer tiefer in Ihre App zu navigieren - über die Hauptseite hinaus - zu einer Seite, die Sie auf das zuschneiden können, wofür das Secondary Tile stehen soll.

In FeedCast können Benutzer einen Feed oder eine Kategorie (Secondary Tile) an Start anheften. Mit nur einem Tippen, können Sie sofort die neuesten Artikel, die sich auf diesen Feed oder diese Kategorie beziehen, lesen. Um diese Erfahrung zu ermöglichen, müssen sie zunächst in der Lage sein den Feed oder die Kategorie an Start anzuheften. Aguilera verwendete das Silverlight Toolkit for Windows Phone ContextMenu, um dies zu vereinfachen. Das Antippen und Halten eines Feeds oder einer Kategorie im "Alle"-Panel der Hauptseite, bewirkt, dass das Kontext-Menü erscheint. Dort können Benutzer sich entscheiden, ob sie den Feed oder die Kategorie entfernen möchten oder sie an Start anheften möchten. Abbildung 10zeigt den End-to-End-Prozess aus der Sicht des Benutzers.


Abbildung 10 Das Anheften der Windows Phone News Category an Start und Starten der Kategorie-Seite

Abbildung 11 zeigt die XAML, die das Kontext-Menü ermöglicht. Das zweite MenuItem zeigt "pin to start" (an Start anheften) an (wenn Englisch die Anzeigesprache ist). Wenn diese Item angetippt wird, ruft das Klick-Ereignis die OnCategoryPinned-Methode auf, um das Anheften einzuleiten. Da diese App lokalisiert ist, kommt der Text für das Kontext-Menü tatsächlich aus einer Ressourcen-Datei. Deshalb ist der Header-Wert an LocalizedResources.ContextMenuPinToStartText gebunden.

Abbildung 11 Das Kontext-Menü zum Entfernen oder Anheften an Start einer Kategorie

<toolkit:ContextMenuService.ContextMenu>
  <toolkit:ContextMenu>
    <toolkit:MenuItem Tag="{Binding}"
      Header="{Binding LocalizedResources.ContextMenuRemoveText,
               Source={StaticResource LocalizedStrings}}"
      IsEnabled="{Binding IsRemovable}"
      Click="OnCategoryRemoved"/>
    <toolkit:MenuItem Tag="{Binding}"
      Header="{Binding LocalizedResources.ContextMenuPinToStartText,
               Source={StaticResource LocalizedStrings}}"
      IsEnabled="{Binding IsPinned, 
      Converter={StaticResource IsPinnable}}"
      Click="OnCategoryPinned"/>
  </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

Diese App hat nur zwei Ressourcen-Dateien, eine für Spanisch und die andere für Englisch (Standard). Da jedoch Lokalisierung vorhanden ist, wäre es relativ einfach, mehr Sprachen hinzuzufügen. Abbildung 12 zeigt die Standard-Ressourcen-Datei, AppResources.resx. Weiter Informationen finden Sie unter wpdev.ms/globalized.

The Default Resource File, AppResources.resx, Supplies the UI Text for All Languages Except SpanishAbbildung 12 Die Standard-Ressourcen-Datei, AppResources.resx, liefert den UI-Text für alle Sprachen außer Spanisch

Ursprünglich war sich das Team nicht ganz sicher, wie es genau festlegen sollte, welche Kategorie oder welcher Feed angeheftet werden muss. Dann entdeckte Aguilera das XAML Tag-Attribut (siehe Abbildung 11). Die Teammitglieder fanden heraus, dass sie es an die Kategorie- oder Feed-Objekte im ViewModel binden konnten und die individuellen Objekte dann später programmatisch abrufen konnten. Auf der Hauptseite ist die Kategorie-Liste an ein MainPageAllCategoriesViewModel-Objekt gebunden. Wenn die OnCategoryPinned-Methode aufgerufen wird, verwendet sie die GetTagAs-Methode, um das Kategorie-Objekt (das an das Tag gebunden ist) zu erhalten, das dem bestimmten Gegenstand auf der Liste entspricht, und zwar folgendermaßen:

private void OnCategoryPinned(object sender, EventArgs e)
{
  Category tappedCategory = GetTagAs<Category>(sender);
  if (null != tappedCategory)
  {
    AddTile.AddLiveTile(tappedCategory);
  }
}

Die GetTagAs-Methode ist eine generische Methode zum Erhalten jeglichen Objekts, das an das Tag-Attribut eines Containers gebunden wurde. Wenngleich dies effektiv ist, ist es für die meisten Verwendungen auf MainPage.xaml.cs nicht erforderlich. Die Gegenstände auf der Liste sind bereits an das Objekt gebunden, sodass es ziemlich überflüssig ist sie an das Tag zu binden. Anstatt Tag zu verwenden, können Sie den DataContext des Sender-Objekts verwenden. Abbildung 13 zeigt beispielsweise wie OnCategoryPinned aussehen würde, wenn der empfohlene DataContext-Ansatz verwendet wird.

Abbildung 13 Ein Beispiel für die Verwendung von DataContext an Stelle von GetTagAs

private void OnCategoryPinned(object sender, EventArgs e)
{
  Category tappedCategory = null;
  if (null != sender)
  {
    FrameworkElement element = sender as FrameworkElement;
    if (null != element)
    {
      tappedCategory = element.DataContext as Category;
      if (null != tappedCategory)
      {
        AddTile.AddLiveTile(tappedCategory);
      }
    }
  }
}

Dieser DataContext-Ansatz funktioniert für alle Fälle auf MainPage.xaml.cs gut, mit Ausnahme der OnHubTileTapped-Methode. Diese wird ausgelöst, wenn sie auf einen angezeigten Artikel im "Gezeigt"-Panel der Hauptseite tippen. Die Herausforderung kommt dadurch zustande, dass der Sender nicht an eine Artikel-Klasse gebunden ist - er ist an MainPageFeaturedViewModel gebunden. Dieses ViewModel enthält sechs Artikel, sodass aus dem DataContext nicht eindeutig hervorgeht, welches angetippt wurde. In diesem Fall macht es die Verwendung der Tag-Eigenschaft wirklich leicht, an den entsprechenden Artikel zu binden.

Da Sie Feeds und Kategorien an Start anheften können, hat die AddLiveTile-Methode zwei Überlastungen. Die Objekte und Secondary Tiles sind unterschiedlich genug, sodass das Team beschloss die Funktionalität nicht zu einer einzelnen generischen Methode zusammenzufügen. Abbildung 14 illustriert die Kategorie-Version der AddLiveTile-Methode.

Abbildung 14 Anheften eines Kategorie-Objekts an Start

public static void AddLiveTile(Category cat)
{
  // Does Tile already exist? If so, don't try to create it again.
  ShellTile tileToFind = ShellTile.ActiveTiles.FirstOrDefault(x => 
    x.NavigationUri.ToString().Contains("/Category/" + 
    cat.CategoryID.ToString()));
  // Create the Tile if doesn't already exist.
  if (tileToFind == null)
  {
    // Create an image for the category if there isn't one.
    if (cat.ImageURL == null || cat.ImageURL == String.Empty)
    {
      cat.ImageURL = ImageGrabber.GetDefaultImage();
    }
    // Create the Tile object and set some initial properties for the Tile.
    StandardTileData newTileData = new StandardTileData
    {
      BackgroundImage = new Uri(cat.ImageURL, 
      UriKind.RelativeOrAbsolute),
      Title = cat.CategoryTitle,
      Count = 0,
      BackTitle = cat.CategoryTitle,
      BackContent = "Read the latest in " + cat.CategoryTitle + "!",
    };
    // Create the Tile and pin it to Start.
    // This will cause a navigation to Start and a deactivation of the application.
    ShellTile.Create(
      new Uri("/Category/" + cat.CategoryID, UriKind.Relative), 
      newTileData);
    cat.IsPinned = true;
    App.DataBaseUtility.SaveChangesToDB();
  }
}

Bevor eine Kategorie "Tile" hinzugefügt wird, verwendet die AddLiveTile-Methode die ShellTile-Klasse, um sich die Navigations-URIs von allen aktiven Tiles anzusehen, um zu bestimmen ob die Kategorie bereits hinzugefügt wurde. Wenn nicht, fährt sie fort und veranlasst eine Bild-URL sich der neuen Tile zuzuordnen. Jedes Mal wenn Sie eine neue Tile erstellen, muss das Hintergrundbild von einer lokalen Ressource kommen. In diesem Fall wird die ImageGrabber-Klasse verwendet, um eine zufällig zugeordnete lokale Bild-Datei zu erhalten. Nachdem Sie eine neue Tile erstellt haben, können Sie jedoch das Hintergrundbild mit einer entfernten URL aktualisieren. Aber diese spezielle App macht das nicht.

Alle Informationen die Sie angeben müssen, um eine neue Tile zu erstellen, sind in der StandardTileData-Klasse enthalten. Diese Klasse wird verwendet, um Text, Zahlen und Hintergrundbilder in die Tiles einzusetzen. Wenn Sie die Tile mit der Create-Methode erstellen, werden die StandardTileData als Parameter weitergegeben. Der andere wichtige Parameter, der weitergegeben wird, ist der Tile-Navigations-URI. Hierbei handelt es sich um den URI, der verwendet wird, um Benutzer an eine bedeutungsvolle Stelle in Ihrer App zu führen.

Bei dieser App leitet der URI von der Tile den Benutzer nur bis zur App. Um noch weiter zu gehen, wird eine UriMapper-Klasse verwendet, um Benutzer zur richtigen Seite zu routen. Das App.xaml-Navigationselement spezifiziert jegliche URI-Abbildung für die App. Bei jedem UriMapping-Element, ist der vom URI-Attribut spezifizierte Wert der eingehende URI. Der vom MappedUri-Attribut spezifizierte Wert gibt an, wohin der Benutzer navigiert werden soll. Um den Kontext einer bestimmten Kategorie, eines bestimmten Feeds oder Artikels beizubehalten, wird der Kennungswert in Klammern, {id}, vom eingehenden URI zum abgebildeten URI übertragen, und zwar folgendermaßen:

<navigation:UriMapping Uri="/Category/{id}" MappedUri=
  "/Views/CategoryPage.xaml?id={id}"/>

Man kann einen URI-Mapper vielleicht auch aus anderen Gründen - wie zum Beispiel Erweiterbarkeit der Suche - verwenden, aber eine Verwendung der Secondary Tile ist nicht erforderlich. Bei dieser App erfolgte die Entscheidung den URI-Mapper zu verwenden aus rein stilistischen Gründen. Das Team fand, dass die kürzeren URIs eleganter und leichter zu verwenden waren. Alternativ hätten die Secondary Tiles, um den gleichen Effekt zu erzielen, einen seitenspezifischen URI spezifizieren können (wie zum Beispiel die MappedUri-Werte).

Unabhängig von den Mitteln gelangt der Benutzer, nachdem der URI von der Secondary Tile auf die entsprechende Seite abgebildet wird, auf die Kategorie-Seite mit einer Liste seiner Artikel. Auftrag ausgeführt. Weitere Informationen zu Tiles finden Sie unter wpdev.ms/secondarytiles.

Doch warten Sie, es gibt noch mehr!

Diese App kann noch viel mehr als das, was ich hier abgedeckt habe. Sie sollten sich unbedingt den Code ansehen, um mehr darüber zu erfahren, wie das Team diese und andere Probleme anging. SynFeedParser.cs bietet zum Beispiel eine angenehme Art die Daten aus Feeds zu säubern, die manchmal von HTML-Tags übersät sind.

Denken Sie daran, dass dies eine Momentaufnahme der Arbeit der Praktikanten nach 12 Wochen ist, abzüglich einer kleinen Säuberungsaktion. Professionelle Entwickler würden manche Teile vielleicht lieber anders codieren. Nichtsdestoweniger hat die App meiner Meinung nach eine lokale Datenbank, einen Background-Agent und Tiles großartig integriert. Ich hoffe Ihnen hat dieser Blick "hinter die Kulissen" gefallen. Frohes codieren!

Matt Stroshane  schreibt Entwicklerdokumentation für das Windows Phone-Team. In anderen Beiträgen für die MSDN Library hat er Produkte wie SQL Server, SQL Azure und Visual Studio besprochen. Wenn er nicht am Schreibtisch sitzt, läuft er durch die Straßen von Seattle und trainiert für den nächsten Marathon. Sie können ihm auf Twitter unter twitter.com/mattstroshane folgen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Francisco Aguilera, Thomas Fennel, John Gallardo, Sean McKenna, Suman Malani, Ayomikun (George) Okeowo und Himadri Sarkar