Windows Phone SDK 7.1

Erstellen einer „Mango“-Anwendung

Andrew Whitechapel

„Mango“ ist der interne Codename für die Windows Phone SDK 7.1-Version. Außerdem ist es natürlich der Name einer köstlichen tropischen Frucht. Mangos sind vielseitig verwendbar, zum Beispiel in Obstkuchen, Salaten und Cocktails. Die Mango soll auch gesund sein und hat eine interessante Kulturgeschichte. Ich behandele in diesem Artikel eine Windows Phone SDK 7.1-Anwendung, die Mangos als Thema hat: Mangolicious. Die Anwendung bietet eine Reihe von Mangorezepten, Cocktails und Fakten. Eigentlich dient sie aber dazu, einige der wichtigen neuen Features der 7.1-Version zu entdecken, insbesondere die folgenden:

  • Lokale Datenbank und LINQ to SQL
  • Sekundäre Kacheln und Deep Linking
  • Silverlight/XNA-Integration

Die Benutzeroberfläche der Anwendung ist einfach: Die Hauptseite bietet ein Panorama, das als erstes Element ein Menü, als zweites Element eine dynamische Auswahl saisonabhängiger Rezepte und Cocktails und als drittes Element einige einfache „Info“-Angaben enthält, siehe Abbildung 1.

Mangolicious Panorama Main PageAbbildung 1: Panorama der Mangolicious-Hauptseite

Über das Menü und die Elemente im Abschnitt „Seasonal Highlights“ können die Benutzer zu den anderen Seiten der Anwendung navigieren, von denen die meisten einfache Silverlight-Seiten sind und eine ein integriertes XNA-Spiel bereitstellt. Zum Erstellen dieser Anwendung von Anfang bis Ende müssen Sie folgende Aufgaben ausführen:

  1. Erstellen der grundlegenden Projektmappe in Visual Studio
  2. Unabhängiges Erstellen der Datenbank mit Daten für die Rezepte, Cocktails und Fakten
  3. Aktualisieren der Anwendung, sodass sie die Datenbank verwendet und für die Datenbindung verfügbar ist
  4. Erstellen der verschiedenen Benutzeroberflächenseiten und Hinzufügen der entsprechenden Datenbindungen
  5. Einrichten des Secondary Tiles-Features, mit dem die Benutzer Recipe-Elemente auf der Startseite des Telefons fixieren können
  6. Integrieren eines XNA-Spiels in die Anwendung

Erstellen der Projektmappe

Ich verwende für diese Anwendung die Windows Phone Silverlight and XNA Application-Vorlage in Visual Studio. Dadurch wird eine Projektmappe mit drei Projekten erstellt, die nach ihrer Umbenennung in Abbildung 2 zusammengefasst sind.

Abbildung 2: Projekte in einer Silverlight- und XNA-Lösung für Windows Phone

Projekt Beschreibung
MangoApp Enthält die eigentliche Telefonanwendung, mit einer Standard-MainPage-Seite und einer sekundären GamePage-Seite.
GameLibrary Ein im Grunde leeres Projekt, das alle richtigen Verweise, aber keinen Code enthält. Entscheidend ist, dass es einen Content-Verweis auf das Content-Projekt enthält.
GameContent Ein leeres Content-Projekt für die Ressourcen, die für das Spiel benötigt werden (u. a. Bilder und Audiodateien).

Erstellen der Datenbank und der DataContext-Klasse

Die Windows Phone SDK 7.1-Version unterstützt jetzt lokale Datenbanken. Das heißt, dass eine Anwendung Daten in einer lokalen SDF-Datenbankdatei auf dem Telefon speichern kann. Es wird empfohlen, die Datenbank im Code zu erstellen, entweder als Teil der Anwendung selbst oder über eine separate Hilfsanwendung, die Sie ausschließlich für die Erstellung der Datenbank entwickeln. In Szenarios, in denen die meisten oder alle Daten nur erstellt werden, während die Anwendung ausgeführt wird, ist das Erstellen der Datenbank innerhalb der Anwendung sinnvoll. Bei der Mangolicious-Anwendung verwende ich nur statische Daten, mit denen ich die Datenbank vorab auffüllen kann.

Ich erstelle zu diesem Zweck eine separate Hilfsanwendung zum Entwickeln der Datenbank und beginne mit der einfachen Windows Phone Application-Vorlage. Zum Erstellen der Datenbank im Code benötige ich eine von DataContext abgeleitete Klasse, die in der benutzerdefinierten Windows Phone-Version der System.Data.Linq-Assembly definiert wird. Dieselbe DataContext-Klasse kann sowohl in der Hilfsanwendung, die die Datenbank erstellt, als auch in der Hauptanwendung, die die Datenbank verwendet, genutzt werden. In der Hilfsanwendung muss ich als Speicherort der Datenbank einen isolierten Speicher angeben, da dies der einzige Speicherort ist, in den ich aus einer Telefonanwendung schreiben kann. Die Klasse enthält auch für jede Datenbanktabelle einen Satz Table-Felder:

public class MangoDataContext : DataContext
{
  public MangoDataContext()
    : base("Data Source=isostore:/Mangolicious.sdf") { }
 
  public Table<Recipe> Recipes;
  public Table<Fact> Facts;
  public Table<Cocktail> Cocktails;
}

Es gibt eine 1:1-Zuordnung zwischen den Table-Klassen im Code und den Tabellen in der Datenbank. Die Column-Eigenschaften sind den Spalten der Datenbanktabelle zugeordnet und enthalten die Eigenschaften des Datenbankschemas, u. a. den Datentyp und die Größe (INT, NVARCHAR usw.), Informationen darüber, ob die Spalte null sein darf oder ob sie eine Schlüsselspalte ist. Ich definiere die Table-Klassen für alle übrigen Tabellen in der Datenbank auf die gleiche Weise, wie in Abbildung 3 gezeigt.

Abbildung 3: Definieren der Table-Klassen

[Table]
public class Recipe
{
  private int id;
  [Column(
    IsPrimaryKey = true, IsDbGenerated = true,
    DbType = "INT NOT NULL Identity", CanBeNull = false,
    AutoSync = AutoSync.OnInsert)]
  public int ID
  {
    get { return id; }
    set
    {
      if (id != value)
      {
        id = value;
      }
    }
  }
 
  private string name;
  [Column(DbType = "NVARCHAR(32)")]
  public string Name
  {
    get { return name; }
    set
    {
      if (name != value)
      {
        name = value;
      }
    }
  }?
    ... additional column definitions omitted for brevity
}

Ich verwende einen Model-View-ViewModel(MVVM)-Standardansatz und benötige jetzt in der Hilfsanwendung noch eine ViewModel-Klasse, die unter Verwendung der DataContext-Klasse zwischen der Benutzeroberfläche („View“) und den Daten („Model“) vermittelt. ViewModel enthält ein DataContext-Feld und einen Satz Auflistungen für die Tabellendaten (Recipes, Facts und Cocktails). Einfache List<T>-Auflistungen sind für die statischen Daten ausreichend. Aus demselben Grund benötige ich nur get-Eigenschaftenaccessoren, keine set-Modifizierer (siehe Abbildung 4).

Abbildung 4: Definieren von Auflistungseigenschaften für Tabellendaten im ViewModel

public class MainViewModel
{
  private MangoDataContext mangoDb;
 
  private List<Recipe> recipes;
  public List<Recipe> Recipes
  {
    get
    {
      if (recipes == null)
      {
        recipes = new List<Recipe>();
      }
    return recipes;
    }
  }
 
    ... additional table collections omitted for brevity
}

Ich mache außerdem eine öffentliche Methode verfügbar, die ich von der Benutzeroberfläche aus aufrufen kann, um die Datenbank und alle Daten tatsächlich zu erstellen. In dieser Methode erstelle ich die Datenbank an sich, wenn sie noch nicht vorhanden ist, und ich erstelle dann der Reihe nach jede Tabelle und fülle diese jeweils mit statischen Daten. Um zum Beispiel die Recipe-Tabelle zu erstellen, gehe ich folgendermaßen vor: Ich erstelle mehrere Instanzen der Recipe-Klasse, die den Zeilen in der Tabelle entsprechen. Ich füge alle Zeilen in der Auflistung zu DataContext hinzu, und schließlich übermittle ich die Daten an die Datenbank. Die Facts- und Cocktails-Tabellen werden nach demselben Muster erstellt (siehe Abbildung 5).

Abbildung 5: Erstellen der Datenbank

public void CreateDatabase()
{
  mangoDb = new MangoDataContext();
  if (!mangoDb.DatabaseExists())
  {
    mangoDb.CreateDatabase();
    CreateRecipes();
    CreateFacts();
    CreateCocktails();
  }
}
 
private void CreateRecipes()
{
  Recipes.Add(new Recipe
  {
    ID = 1,
    Name = "key mango pie",
    Photo = "Images/Recipes/MangoPie.jpg",
    Ingredients = "2 cans sweetened condensed milk, ¾ cup fresh key lime juice, ¼ cup mango purée, 2 eggs, ¾ cup chopped mango.",
    Instructions = "Mix graham cracker crumbs, sugar and butter until well distributed. Press into a 9-inch pie pan. Bake for 20 minutes. Make filling by whisking condensed milk, lime juice, mango purée and egg together until blended well. Stir in fresh mango. Pour filling into cooled crust and bake for 15 minutes.",
    Season = "summer"
  });
 
    ... additional Recipe instances omitted for brevity
 
  mangoDb.Recipes.InsertAllOnSubmit<Recipe>(Recipes);
  mangoDb.SubmitChanges();
}

An einer geeigneten Position in der Hilfsanwendung, zum Beispiel in einem Click-Ereignishandler einer Schaltfläche, kann ich dann diese CreateDatabase-Methode aufrufen. Wenn ich die Hilfsanwendung ausführe (entweder im Emulator oder auf einem physischen Gerät), wird die Datenbankdatei im isolierten Speicher der Anwendung erstellt. Als Letztes muss ich diese Datei auf den Desktop extrahieren, damit ich sie in der Hauptanwendung verwenden kann. Dazu nutze ich das Befehlszeilentool Isolated Storage Explorer, das in Windows Phone SDK 7.1 enthalten ist. Mit dem folgenden Befehl übertragen Sie eine Momentaufnahme des isolierten Speichers vom Emulator auf den Desktop:

"C:\Program Files\Microsoft SDKs\Windows Phone\v7.1\Tools\IsolatedStorageExplorerTool\ISETool" ts xd {e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} C:\Temp\IsoDump

Dieser Befehl setzt voraus, dass das Tool an einem Standardspeicherort installiert ist. Die Erklärungen zu den Parametern finden Sie in Abbildung 6.

Abbildung 6: Isolated Storage Explorer-Befehlszeilenparameter

Parameter Beschreibung
ts Momentaufnahme erstellen („Take snapshot“, der Befehl zum Herunterladen vom isolierten Speicher auf den Desktop)
xd Kurzform von XDE (also dem Emulator)
{e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} Die ProductID der Hilfsanwendung. Sie wird in der Datei „WMAppManifest.xml“ aufgeführt und ist für jede Anwendung unterschiedlich.
C:\Temp\IsoDump Ein beliebiger gültiger Pfad auf dem Desktop, in den Sie die Momentaufnahme kopieren möchten.

Nachdem ich nun die SDF-Datei auf den Desktop extrahiert habe, ist meine Arbeit mit der Hilfsanwendung abgeschlossen, und ich kann mit der Mangolicious-Anwendung fortfahren, die die Datenbank verwendet.

Verwenden der Datenbank

In der Mangolicious-Anwendung füge ich dem Projekt die SDF-Datei hinzu. Außerdem ergänze ich die Lösung mit derselben benutzerdefinierten DataContext-Klasse, die ich geringfügig ändere. In Mangolicious muss ich nichts in die Datenbank schreiben und kann diese daher direkt aus dem Installationsordner der Anwendung verwenden. Die Verbindungszeichenfolge weicht deswegen leicht von derjenigen in der Hilfsanwendung ab. Mangolicious definiert zudem eine SeasonalHighlights-Tabelle im Code. In der Datenbank existiert keine entsprechende SeasonalHighlight-Tabelle. Der Code ruft stattdessen Daten von zwei zugrunde liegenden Datenbanktabellen (Recipes und Cocktails) ab und wird dazu verwendet, das Seasonal Highlights-Panoramaelement aufzufüllen. Die DataContext-Klassen in der Hilfsanwendung zur Datenbankerstellung und in der Mangolicious-Anwendung, die die Datenbank verwendet, unterscheiden sich nur durch diese beiden Änderungen:

public class MangoDataContext : DataContext
{
  public MangoDataContext()
    : base("Data Source=appdata:/Mangolicious.sdf;File Mode=read only;") { }
 
  public Table<Recipe> Recipes;
  public Table<Fact> Facts;
  public Table<Cocktail> Cocktails;
  public Table<SeasonalHighlight> SeasonalHighlights;
}

Die Mangolicious-Anwendung erfordert ebenfalls eine ViewModel-Klasse, und ich kann die ViewModel-Klasse aus der Hilfsanwendung als Ausgangspunkt nehmen. Ich benötige das DataContext-Feld und den Satz von List<T>-Auflistungseigenschaften für die Datentabellen. Darüber hinaus füge ich eine Zeichenfolgeneigenschaft zum Erfassen der aktuellen Jahreszeit hinzu, die im Konstruktor berechnet wird:

public MainViewModel()
{
  season = String.Empty;
  int currentMonth = DateTime.Now.Month;
  if (currentMonth >= 3 && currentMonth <= 5) season = "spring";
  else if (currentMonth >= 6 && currentMonth <= 8) season = "summer";
  else if (currentMonth >= 9 && currentMonth <= 11) season = "autumn";
  else if (currentMonth == 12 || currentMonth == 1 || currentMonth == 2)
    season = "winter";
}

Die entscheidende Methode im ViewModel ist die LoadData-Methode. Hier initialisiere ich die Datenbank und führe LINQ-to-SQL-Abfragen aus, um die Daten über den DataContext in die Auflistungen im Speicher zu laden. Ich könnte an diesem Punkt alle drei Tabellen vorab laden, aber ich möchte die Startzeit optimieren, indem ich die Daten erst dann lade, wenn die relevante Seite tatsächlich besucht wird. Die einzigen Daten, die ich zur Startzeit laden muss, sind die Daten für die SeasonalHighlight-Tabelle, da diese auf der Hauptseite angezeigt werden. Ich nutze dafür zwei Abfragen, die nur die Zeilen aus den Recipes- und Cocktails-Tabellen auswählen, die mit der aktuellen Jahreszeit übereinstimmen, und ich füge die kombinierten Zeilensätze zur Auflistung hinzu, wie in Abbildung 7 gezeigt.

Abbildung 7: Laden von Daten beim Start

public void LoadData()
{
  mangoDb = new MangoDataContext();
  if (!mangoDb.DatabaseExists())
  {
    mangoDb.CreateDatabase();
  }
 
  var seasonalRecipes = from r in mangoDb.Recipes
                        where r.Season == season
                        select new { r.ID, r.Name, r.Photo };
  var seasonalCocktails = from c in mangoDb.Cocktails
                          where c.Season == season
                          select new { c.ID, c.Name, c.Photo };
 
  seasonalHighlights = new List<SeasonalHighlight>();
  foreach (var v in seasonalRecipes)
  {
    seasonalHighlights.Add(new SeasonalHighlight {
      ID = v.ID, Name = v.Name, Photo = v.Photo, SourceTable="Recipes" });
  }
  foreach (var v in seasonalCocktails)
  {
    seasonalHighlights.Add(new SeasonalHighlight {
      ID = v.ID, Name = v.Name, Photo = v.Photo, SourceTable = "Cocktails" });
  }
 
  isDataLoaded = true;
}

Ich kann ähnliche LINQ-to-SQL-Abfragen verwenden, um separate LoadFacts-, LoadRecipes- und LoadCocktails-Methoden zu erstellen. Mithilfe dieser Methoden werden nach dem Start deren jeweilige Daten bei Bedarf geladen.

Erstellen der Benutzeroberfläche

Die Hauptseite umfasst ein Panorama mit drei PanoramaItem-Elementen. Das erste dieser Panoramaelemente besteht aus einem ListBox-Element, das ein Hauptmenü für die Anwendung bietet. Wenn die Benutzer eines der enthaltenen ListBox-Elemente auswählen, erfolgt die Navigation zur entsprechenden Seite, d. h. zur Recipes-, Facts- oder Cocktails-Auflistungsseite oder zur Game-Seite. Unmittelbar vor der Navigation lade ich die entsprechenden Daten in die Recipes-, Facts- oder Cocktails-Auflistungen:

switch (CategoryList.SelectedIndex)
{
  case 0:
    App.ViewModel.LoadRecipes();
    NavigationService.Navigate(
      new Uri("/RecipesPage.xaml", UriKind.Relative));
    break;
 
... additional cases omitted for brevity
}

Wenn die Benutzer in der Benutzeroberfläche ein Element aus der Seasonal Highlights-Liste auswählen, prüfe ich, ob es sich um ein Rezept oder einen Cocktail handelt, und navigiere zu der einzelnen Recipe- oder Cocktail-Seite. Dabei übergebe ich die Element-ID als Teil der Abfragezeichenfolge für die Navigation, wie in Abbildung 8 dargestellt.

Abbildung 8: Auswählen aus der Seasonal Highlights-Liste

SeasonalHighlight selectedItem =
  (SeasonalHighlight)SeasonalList.SelectedItem;
String navigationString = String.Empty;
if (selectedItem.SourceTable == "Recipes")
{
  App.ViewModel.LoadRecipes();
  navigationString =
    String.Format("/RecipePage.xaml?ID={0}", selectedItem.ID);
}
else if (selectedItem.SourceTable == "Cocktails")
{
  App.ViewModel.LoadCocktails();
  navigationString =
    String.Format("/CocktailPage.xaml?ID={0}", selectedItem.ID);
}
NavigationService.Navigate(
  new System.Uri(navigationString, UriKind.Relative));

Die Benutzer können aus dem Menü auf der Hauptseite zu einer von drei Auflistungsseiten navigieren. Die Daten dieser Seiten sind jeweils an eine der Auflistungen im ViewModel gebunden, um eine Liste mit Elementen anzuzeigen: Recipes, Facts oder Cocktails. Jede dieser Seiten bietet ein einfaches ListBox-Element. Jedes Element in der Liste enthält ein Image-Steuerelement für das Foto und ein TextBlock-Element für den Namen des Elements. Abbildung 9 zeigt als Beispiel die FactsPage-Seite.

Fun Facts, One of the Collection List Pages
Abbildung 9: Interessante Fakten, eine der Auflistungsseiten.

Wenn die Benutzer ein einzelnes Element aus den Recipes-, Facts- oder Cocktails-Listen auswählen, navigiere ich zu der einzelnen Recipe-, Fact- oder Cocktail-Seite und übergebe die ID des einzelnen Elements in der Abfragezeichenfolge für die Navigation. Diese Seiten sind für die drei Typen wieder fast identisch. Jede bietet ein Image-Element und darunter etwas Text. Ich definiere für die datengebundenen TextBlock-Elemente keinen ausdrücklichen Stil, aber sie verwenden trotzdem alle das TextWrapping=Wrap-Format. Dies wird durch die Deklarierung eines TextBlock-Stils in der „App.xaml.cs“ erreicht:

<Style TargetType="TextBlock" BasedOn="{StaticResource
  PhoneTextNormalStyle}">
  <Setter Property="TextWrapping" Value="Wrap"/>
</Style>

Auf diese Weise verwendet jeder TextBlock in der Lösung, der nicht ausdrücklich seinen eigenen Stil definiert, stattdessen implizit diesen Stil. Implizite Stile sind ein weiteres neues Feature in der Windows Phone SDK 7.1-Version als Teil von Silverlight 4.

Der Codebehind für jede dieser Seiten ist einfach. In der OnNavigatedTo-Überschreibung extrahiere ich die einzelne Element-ID aus der Abfragezeichenfolge. Ich bestimme dieses Element in der ViewModel-Auflistung und nehme eine Datenbindung daran vor. Der Code für die RecipePage-Seite ist ein wenig komplexer als der für die anderen. Der zusätzliche Code dieser Seite gehört zu dem HyperlinkButton-Objekt in der oberen rechten Seitenecke. Sie können dies in Abbildung 10 sehen.

A Recipe Page with Pin ButtonAbbildung 10: Rezeptseite mit Stecknadelschaltfläche

Sekundäre Kacheln

Wenn die Benutzer auf der einzelnen Recipe-Seite auf den Stecknadel-HyperlinkButton klicken, hefte ich dieses Element als Kachel auf die Startseite des Telefons. Durch das Anheften gelangen die Benutzer auf die Startseite, und die Anwendung wird deaktiviert. Wenn eine Kachel auf diese Art angeheftet wird, wird sie regelmäßig animiert. Sie wechselt zwischen der Vorder- und der Rückseite hin und her, wie in Abbildung 11 und 12 gezeigt.

Pinned Recipe Tile (Front)Abbildung 11: Vorderseite der gehefteten Recipe-Kachel

Pinned Recipe Tile (Back)Abbildung 12: Rückseite der gehefteten Recipe-Kachel

Anschließend können die Benutzer auf diese angeheftete Kachel tippen, wodurch sie direkt zu diesem Element in der Anwendung navigieren. Wenn die Benutzer auf der Seite sind, weist die Stecknadel-Schaltfläche ein Bild zum Loslösen auf. Wenn die Benutzer die Seite lösen, wird sie von der Startseite entfernt, und die Anwendung wird weiter ausgeführt.

Ich gehe dazu folgendermaßen vor: Ich erledige in der OnNavigatedTo-Überschreibung für die RecipePage zunächst die Standardarbeit, um zu bestimmen, welches bestimmte Recipe für die Datenbindung verwendet wird. Danach formuliere ich eine Zeichenfolge, die ich später als URL für diese Seite nutzen kann:

thisPageUri = String.Format("/RecipePage.xaml?ID={0}", recipeID);

Im Click-Ereignishandler der Stecknadelschaltfläche prüfe ich erst, ob für diese Seite bereits eine Kachel existiert. Wenn nicht, erstelle ich sie jetzt. Zum Erstellen der Kachel verwende ich die aktuellen Recipe-Daten: das Bild und den Namen. Ich lege auch ein statisches Einzelbild und statischen Text für die Rückseite der Kachel fest. Gleichzeitig nutze ich die Möglichkeit, die Schaltfläche selbst mit dem Bild zum Loslösen zu aktualisieren. Wenn andererseits die Kachel schon existiert, dann muss ich mich im Click-Ereignishandler befinden, da die Benutzer das Loslösen der Kachel ausgewählt haben. In diesem Fall lösche ich die Kachel und aktualisiere die Schaltfläche mit dem Stecknadelbild, wie in Abbildung 13 gezeigt.

Abbildung 13: Anheften und Loslösen von Seiten

private void PinUnpin_Click(object sender, RoutedEventArgs e)
{
  tile = ShellTile.ActiveTiles.FirstOrDefault(
    x => x.NavigationUri.ToString().Contains(thisPageUri));
  if (tile == null)
  {
    StandardTileData tileData = new StandardTileData
    {
      BackgroundImage = new Uri(
        thisRecipe.Photo, UriKind.RelativeOrAbsolute),
      Title = thisRecipe.Name,
      BackTitle = "Lovely Mangoes!",
      BackBackgroundImage =
        new Uri("Images/BackTile.png", UriKind.Relative)
    };
 
    ImageBrush brush = (ImageBrush)PinUnpin.Background;
    brush.ImageSource =
      new BitmapImage(new Uri("Images/Unpin.png", UriKind.Relative));
    PinUnpin.Background = brush;
    ShellTile.Create(
      new Uri(thisPageUri, UriKind.Relative), tileData);
  }
  else
  {
    tile.Delete();
    ImageBrush brush = (ImageBrush)PinUnpin.Background;
    brush.ImageSource =
      new BitmapImage(new Uri("Images/Pin.png", UriKind.Relative));
    PinUnpin.Background = brush;
  }
}

Wenn die Benutzer auf die angeheftete Kachel tippen, um zur Recipe-Seite zu gelangen, und dann auf die ZURÜCK-Hardwaretaste auf dem Telefon drücken, beenden sie die Anwendung vollständig. Das kann verwirrend sein. Die Benutzer erwarten nämlich normalerweise, dass sie die Anwendung nur beenden, wenn sie auf der Hauptseite und nicht auf einer anderen Seite auf ZURÜCK drücken. Als Alternative könnten Sie eine Art Home-Schaltfläche auf der Recipe-Seite bereitstellen, mit der die Benutzer zurück zum übrigen Teil der Anwendung navigieren könnten. Leider ist auch diese Lösung irritierend, denn wenn die Benutzer zur Hauptseite gelangen und auf ZURÜCK drücken, kehren sie zur angehefteten Recipe-Seite zurück, anstatt die Anwendung zu beenden. Obwohl ein Home-Mechanismus realisierbar ist, sollten Sie daher dieses Verhalten berücksichtigen, bevor Sie ihn hinzufügen.

Integrieren eines XNA-Spiels

Wie Sie wissen, habe ich die Anwendung ursprünglich als eine Windows Phone Silverlight and XNA Application-Lösung erstellt. Ich erhielt drei Projekte. Zum Erstellen der Funktionalität ohne das Spiel habe ich mit dem MangoApp-Hauptprojekt gearbeitet. Das GameLibrary-Projekt bildet die Brücke zwischen dem Silverlight-MangoApp-Projekt und dem XNA-GameContent-Projekt. Das MangoApp-Projekt enthält Verweise auf das GameLibrary-Projekt, welches wiederum auf das GameContent-Projekt verweist. Daran muss nichts verändert werden. Sie müssen zwei Hauptaufgaben erledigen, um ein Spiel in eine Telefonanwendung zu integrieren:

  • Erweitern der GamePage-Klasse im MangoApp-Projekt, um die vollständige Spiellogik hinzuzufügen
  • Erweitern des GameContent-Projekts, um Bilder und Töne für das Spiel bereitzustellen (ohne Codeveränderungen)

Wenn Sie die Erweiterungen betrachten, die Visual Studio für ein Silverlight und XNA integrierendes Projekt generiert, fällt zuerst auf, dass die „App.xaml“ einen SharedGraphicsDeviceManager deklariert. Dieses Objekt verwaltet die gemeinsame Nutzung des Bildschirms durch die Silverlight- und XNA-Laufzeiten. Das Objekt ist auch der einzige Grund für die zusätzliche AppServiceProvider-Klasse in dem Projekt. Diese Klasse dient zum Zwischenspeichern des Managers für gemeinsam genutzte Grafikgeräte, damit dieser für alle Elemente in der Anwendung verfügbar ist, die dies erfordern, sowohl für Silverlight als auch für XNA. Die App-Klasse besitzt ein AppServiceProvider-Feld und macht auch einige zusätzliche Eigenschaften für die XNA-Integration verfügbar: eine ContentManager- und eine GameTimer-Eigenschaft. Diese werden sämtlich in der neuen InitializeXnaApplication-Methode initialisiert, zusammen mit einem GameTimer, der für die XNA-Nachrichtenwarteschlange verwendet wird.

Das Interessante dabei ist, wie ein XNA-Spiel in eine Silverlight-Anwendung für Windows Phone integriert wird. Das Spiel selbst ist eigentlich nicht die Hauptsache. Für diese Übung wandle ich daher ein existierendes Spiel ab, anstatt ein vollständiges Spiel mit viel Mühe neu zu entwickeln. Ich verwende das XNA-Spiellernprogramm auf AppHub: http://msdn.microsoft.com/en-us/library/windowsphone/develop/ff472340(v=vs.105).aspx.

In meiner Adaption habe ich einen durch die Player-Klasse im Code dargestellten Mixbecher, der Projektile auf ankommende Mangos (Feinde) feuert. Wenn ich eine Mango treffe, zerbricht sie und verwandelt sich in einen Mangotini-Cocktail. Bei jedem Treffer einer Mango erhöht sich der Punktestand um 100 Punkte. Jedes Mal, wenn der Mixbecher mit einer Mango kollidiert, wird die Stärke der Spieler um 10 reduziert. Wenn die Spielstärke null beträgt, endet das Spiel. Die Benutzer können das Spiel auch jederzeit erwartungsgemäß beenden, indem sie die ZURÜCK-Taste auf dem Telefon drücken. Abbildung 14 zeigt das Spiel, während es ausgeführt wird.

The XNA Game in ProgressAbbildung 14: Das XNA-Spiel während seiner Ausführung

An der fast leeren „GamePage.xaml“ muss ich nichts ändern. Die wirkliche Arbeit findet im Codebehind statt. Visual Studio generiert Startcode für die GamePage-Klasse, wie in Abbildung 15 beschrieben.

Abbildung 15: GamePage-Startcode

Feld/Methode Zweck Erforderliche Änderungen
ContentManager Lädt und verwaltet die Gültigkeitsdauer des Inhalts der Inhaltspipeline. Fügen Sie hier Code hinzu, um Bilder und Ton zu laden.
GameTimer Im XNA-Spielmodell führt das Spiel Aktionen aus, wenn Update- und Draw-Ereignisse ausgelöst werden, und diese Ereignisse werden von einem Timer bestimmt. Keine Änderung.
SpriteBatch Wird zum Zeichnen von Texturen in XNA verwendet. Fügen Sie Code hinzu, um damit in der Draw-Methode die Spielobjekte zu zeichnen (u. a. Spieler, Feinde, Projektile, Explosionen).
GamePage-Konstruktor Erstellt einen Timer und verknüpft die Update- und Draw-Ereignisse mit OnUpdate- und OnDraw-Methoden. Behalten Sie den Timercode bei, und initialisieren Sie zusätzlich die Spielobjekte.
OnNavigatedTo Richtet die gemeinsame Nutzung des Bildschirms durch Silverlight und XNA ein und startet den Timer. Behalten Sie den Code für die gemeinsame Nutzung und den Timer bei. Laden Sie zusätzlich Inhalt in das Spiel, einschließlich des vorherigen Status vom isolierten Speicher.
OnNavigatedFrom Beendet den Timer und die XNA- Bildschirmfreigabe. Behalten Sie den Code für die gemeinsame Nutzung und den Timer bei. Speichern Sie zusätzlich den Punktestand und die Spielergesundheit im isolierten Speicher.
OnUpdate (Leer), verarbeitet das GameTimer.Update-Ereignis. Fügen Sie Code zum Berechnen von Spielobjektänderungen hinzu (Spielerposition, Anzahl und Position der Feinde, Projektile und Explosionen).
OnDraw (Leer), verarbeitet das GameTimer.Draw-Ereignis. Fügen Sie Code zum Zeichnen der Spielobjekte, des Punktestands und der Spielergesundheit hinzu.

Das Spiel ist eine direkte Adaption des AppHub-Lernprogramms, welches zwei Projekte enthält: das Shooter-Spielprojekt und das ShooterContent-Inhaltsprojekt. Der Inhalt besteht aus Bildern und Audiodateien. Ich kann diese ändern, damit sie zum Mangothema meiner Anwendung passen. Dazu tausche ich nur PNG- und WAV-Dateien aus. Auf den Anwendungscode wirken sich die Änderungen nicht aus. Die erforderlichen Änderungen (des Codes) nehme ich alle im Shooter-Spielprojekt vor. Richtlinien für die Migration von der Game-Klasse zu Silverlight/XNA finden Sie auf der folgenden AppHub-Seite: http://channel9.msdn.com/Series/Get-to-Mango/Get-to-Windows-Phone-Mango-1-From-XNA-to-SLXNA.

Zuerst muss ich die Dateien des Shooter-Spielprojekts in mein vorhandenes MangoApp-Projekt kopieren. Ich kopiere außerdem die ShooterContent-Inhaltsdateien in mein bestehendes GameContent-Projekt. Abbildung 16 enthält eine Zusammenfassung der Klassen, die im Shooter-Spielprojekt vorhanden sind.

Abbildung 16: Klassen im Shooter-Spiel

Klasse Zweck Erforderliche Änderungen
Animation Animiert die verschiedenen Sprites im Spiel: die Spieler, Feindobjekte, Projektile und Explosionen. Entfernen Sie GameTime.
Enemy Ein Sprite, das die Feindobjekte darstellt, auf die die Benutzer schießen. In der Adaption sind dies Mangos. Entfernen Sie GameTime.
Game1 Die Steuerungsklasse für das Spiel. Führen Sie sie mit der GamePage-Klasse zusammen.
ParallaxingBackground Animiert die Wolkenhintergrundbilder, um einen 3D-Parallaxeneffekt zu erzeugen. Keine.
Player Ein Sprite, das die Figur der Benutzer im Spiel darstellt. In der Adaption ist dies ein Mixbecher. Entfernen Sie GameTime.
Program Wird nur verwendet, wenn das Spiel für Windows oder die Xbox entwickelt wird. Nicht verwendet, kann gelöscht werden.
Projectile Ein Sprite, das die Projektile darstellt, die die Benutzer auf die Feinde abfeuern. Keine.

Um dieses Spiel in eine Telefonanwendung zu integrieren, müssen Sie die GamePage-Klasse folgendermaßen ändern:

  • Kopieren Sie alle Felder von der Game1-Klasse in die GamePage-Klasse. Kopieren Sie außerdem die Feldinitialisierung der Game1.Initialize-Methode in den GamePage-Konstruktor.
  • Kopieren Sie die LoadContent-Methode und alle Methoden zum Hinzufügen und Aktualisieren von Feinden, Projektilen und Explosionen. Sämtliche dieser Methoden können unverändert bleiben.
  • Extrahieren Sie jede Verwendung vom GraphicsDeviceManager, um stattdessen eine GraphicsDevice-Eigenschaft zu nutzen.
  • Extrahieren Sie den Code in den Game1.Update- und Draw-Methoden, und fügen Sie ihn den GamePage.OnUpdate- und OnDraw-Ereignishandlern für den Timer hinzu.

Ein konventionelles XNA-Spiel erstellt einen neuen GraphicsDeviceManager. In einer Telefonanwendung habe ich aber bereits einen SharedGraphicsDeviceManager, der eine GraphicsDevice-Eigenschaft verfügbar macht. Mehr ist nicht erforderlich. Zur Vereinfachung speichere ich einen Verweis auf die GraphicsDevice-Eigenschaft als Feld in der GamePage-Klasse.

In einem XNA-Standardspiel sind die Update- und Draw-Methoden Überschreibungen von virtuellen Methoden in der grundlegenden Microsoft.Xna.Framework.Game-Klasse. In einer integrierten Silverlight/XNA-Anwendung hingegen wird die GamePage-Klasse nicht von der XNA-Game-Klasse abgeleitet. Sie müssen den Code aus den Update- und Draw-Methoden abstrahieren und in die OnUpdate- und OnDraw-Ereignishandler einfügen. Einige der Spielobjektklassen (beispielsweise Animation, Enemy und Player), die Update- und Draw-Methoden und einige der von der Update-Methode aufgerufenen Hilfsmethoden verwenden einen GameTime-Parameter. Dies wird in der „Microsoft.Xna.Framework.Game.dll“ definiert, und wenn eine Silverlight-Anwendung einen Verweis auf diese Assembly enthält, ist dies im Allgemeinen als Fehler zu betrachten. Der GameTime-Parameter kann durch die zwei Timespan-Eigenschaften vollständig ersetzt werden. Dabei handelt es sich um die TotalTime- und ElapsedTime-Eigenschaften, die vom GameTimerEventArgs-Objekt verfügbar gemacht werden, welches an die OnUpdate- und OnDraw-Ereignishandler für den Timer übergeben wird. Von GameTime abgesehen kann ich den Draw-Code unverändert übertragen.

Die ursprüngliche Update-Methode testet den GamePad-Status und ruft bedingungsabhängig Game.Exit auf. Da dies in einer integrierten Silverlight/XNA-Anwendung nicht verwendet wird, darf es nicht in die neue Methode übertragen werden:

//if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
//{
//    // this.Exit();
//}

Die neue Update-Methode ist nur wenig mehr als eine Umgebung, die andere Methoden aufruft, um die verschiedenen Spielobjekte zu aktualisieren. Ich aktualisiere den Parallaxenhintergrund auch noch nach Beendigung des Spiels. Die Spieler, Feinde, Kollisionen, Projektile und Explosionen werden aber nur aktualisiert, solange die Spielfigur noch lebt. Diese Hilfsmethoden berechnen die Anzahl und die Positionen der diversen Spielobjekte. Nachdem die Verwendung von GameTime entfernt wurde, können diese unverändert übertragen werden – mit einer Ausnahme:

private void OnUpdate(object sender, GameTimerEventArgs e)
{
  backgroundLayer1.Update();
  backgroundLayer2.Update();
 
  if (isPlayerAlive)
  {
    UpdatePlayer(e.TotalTime, e.ElapsedTime);
    UpdateEnemies(e.TotalTime, e.ElapsedTime);
    UpdateCollision();
    UpdateProjectiles();
    UpdateExplosions(e.TotalTime, e.ElapsedTime);
  }
}

Die UpdatePlayer-Methode muss etwas angepasst werden. In der Originalversion des Spiels wurde die Spielergesundheit auf 100 zurückgesetzt, wenn sie auf den Wert 0 fiel. Dadurch begann das Spiel immer wieder neu. In der Adaptierung setze ich ein Flag auf FALSE, wenn die Spielergesundheit auf 0 fällt. Ich teste sowohl in der OnUpdate- als auch in der OnDraw-Methode auf dieses Flag. In der OnUpdate-Methode bestimmt der Flagwert, ob weitere Veränderungen an den Objekten berechnet werden müssen oder nicht. In der OnDraw-Methode bestimmt der Wert, ob die Objekte oder ein Bildschirm für das Spielende mit dem endgültigen Punktestand gezeichnet wird.

private void UpdatePlayer(TimeSpan totalTime, TimeSpan elapsedTime)
{
...unchanged code omitted for brevity.
 
  if (player.Health <= 0)
  {
    //player.Health = 100;
    //score = 0;
    gameOverSound.Play();
    isPlayerAlive = false;
  }
}

Spielen Sie mit

In diesem Artikel bin ich auf die Anwendungsentwicklung mit einigen der neuen Features in der Windows Phone SDK 7.1-Version eingegangen: lokale Datenbanken, LINQ to SQL, sekundäre Kacheln, Deep Linking und Silverlight/XNA-Integration. Die 7.1-Version bietet viele weitere neue Features und Verbesserungen von vorhandenen Features. Weitere Einzelheiten finden Sie unter den folgenden Links:

Die endgültige Version der Mangolicious-Anwendung ist auf dem Windows Phone Marketplace unter bit.ly/nuJcTA verfügbar. Für den Zugriff benötigen Sie die Software Zune. Das Beispiel verwendet das Silverlight for Windows Phone Toolkit. Sie können es unter bit.ly/qiHnTT kostenlos herunterladen.

Andrew Whitechapel* ist seit über 20 Jahren als Entwickler tätig und arbeitet zurzeit als Programmmanager im Windows Phone-Team, wo er für wesentliche Bestandteile der Anwendungsplattform verantwortlich ist.*

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Nick GravelynBrian Hudson und Himadri Sarkar.