Windows Phone

Erstellen einer plattformübergreifenden mobilen Golf-App mit C# und Xamarin

Wallace B. McClure

Codebeispiel herunterladen.

Zu den Highlights der Golfsaison gehört immer wieder die Teilnahme an Turnieren mit Sonderwettbewerben, zum Beispiel um den längsten Abschlag. Dabei wird jeweils der erste Abschlag der einzelnen Turnierteilnehmer an einem bestimmten Loch gemessen. Wer an diesem Tag den längsten Abschlag schafft, gewinnt. Bei dieser Art von Wettbewerb gibt es in der Regel jedoch keine zentrale Punktzahltabelle. Wenn Sie zur ersten Gruppe gehören, wissen Sie erst nach dem Turnier, an welcher Stelle Ihr Abschlag im Vergleich zu den anderen Teilnehmern steht. Warum verwenden wir also nicht ein Mobiltelefon, um den Start- und Endpunkt des Abschlags aufzuzeichnen und die Daten in einer Cloud-gehosteten Datenbank zu speichern?

Es gibt zahlreiche Optionen zum Erstellen einer solchen App, was verwirrend sein kann. In diesem Artikel erläutere ich, wie ich diese App mithilfe der Back-End-Optionen in Windows Azure erstellt habe und wie ich mit den verschiedenen Herausforderungen umgegangen bin. Ich zeige Ihnen den Code zum Erstellen einer App für Windows Phone und für iOS unter Verwendung von Xamarin.

Es waren verschiedene Features erforderlich. Die App muss auf mobilen Geräten und mit den Betriebssystemen der verschiedenen Geräte ausführbar sein. Es muss sich um eine systemeigene App handeln, die jeweils wie alle anderen Apps auf dem Gerät aussieht. Der Back-End-Server muss immer verfügbar sein und dem Entwickler (mir) tunlichst wenig Umstände machen. Die Cloud-Dienste müssen die plattformübergreifende Entwicklung so umfangreich wie möglich unterstützen. Die Back-End-Datenbank muss gewisse Geolocation-Funktionen ermöglichen.

Warum C#?

Es gibt unter anderem folgende Optionen zum Erstellen plattformübergreifender Apps: mobile Web-Apps; Xamarin C# für iPhone (und Android); Verzicht auf plattformübergreifende Apps, um stattdessen Apps in der vom Anbieter empfohlenen Sprache zu erstellen (Microsoft .NET Framework für Windows Phone und Objective-C für iPhone). Die Wahl von C#/.NET Framework als Clientsprache hielt ich für sinnvoll. Denn dank meiner Erfahrung mit C# gab es für mich so – abgesehen von den plattformspezifischen Features eines Geräts – eine Sache weniger zu lernen. Und alles in Visual Studio 2013 entwickeln zu können, war ein noch entscheidenderer Grund dafür, mich für eine Xamarin-Lösung für iPhone zu entscheiden. Das Problem mit der mobilen Weboption ist, dass sich Benutzer Apps wünschen, die möglichst umfassend in ihre Plattform integriert sind. Dies ist bei einer mobilen Weblösung schwierig, aber mit einer systemeigenen Lösung einfacher. Das Verwenden einer vom Anbieter empfohlenen anstelle einer plattformübergreifenden Lösung war nicht zweckmäßig, da ich für jede Plattform eine neue Sprache erlernen müsste.

Entwicklungstools

Um eine App für mehrere Plattformen zu erstellen, waren bisher meist mehrere Entwicklungstools erforderlich. Vor der Einführung von Xamarin.iOS war die Entwicklung für iPhone nur auf einem Mac mit Xamarin Studio möglich (früher MonoDevelop, ein Port der Open-Source-Umgebung SharpDevelop). Zwar gibt es an Xamarin Studio nichts auszusetzen, aber Entwickler bleiben in der Regel lieber in der gewohnten IDE. Mit Visual Studio 2013 in Kombination mit Xamarin.iOS for Visual Studio können Sie Apps für Windows Azure, Windows Phone und iPhone entwickeln, ohne Ihre geliebte IDE verlassen zu müssen.

Windows Azure

Ein mobiles Gerät kann auf verschiedene Arten mit Windows Azure interagieren, zum Beispiel per virtuellem Computer mit Windows Azure, Webrolle, Windows Azure-Websites und Windows Azure Mobile Services.

Ein virtueller Computer bietet die beste Steuerung aller Variablen. Sie können Änderungen an der App und an allen Servereinstellungen vornehmen. Das ist insbesondere für Apps von Vorteil, die das Anpassen zugrunde liegender Betriebssystemeinstellungen, die Installation einer weiteren App oder andere Änderungen erfordern. Dies wird als „Infrastructure as a Service“ (IaaS) bezeichnet.

In Windows Azure gibt es einen Cloud-Dienst-Projekttyp, der einen Rollensatz enthält. Rollen werden üblicherweise Projekten in Visual Studio zugeordnet. Eine Webrolle ist im Prinzip ein Webprojekt, das als einzelnes bereitstellbares Paket zusammengefasst und hochgeladen wurde und auf einem virtuellen Computer ausgeführt wird. Eine Webrolle stellt Webbenutzeroberflächen bereit und kann sogar integrierte Webdienste besitzen. Eine Workerrolle ist ein Projekt, das ständig auf dem Server ausgeführt wird. Dies wird als „Platform as a Service“ (PaaS) bezeichnet.

Windows Azure-Websites ähneln dem Konzept der Webrolle. Bei dieser Lösung hostet eine App eine Website oder ein Projekt. Das Projekt kann Webdienste enthalten. Diese Webdienste können per SOAP oder REST aufgerufen werden. Windows Azure-Websites sind eine hervorragende Lösung, wenn eine Anwendung nur IIS benötigt.

Für die ersten drei Optionen müssen Sie sich gut mit Webdiensten auskennen – wie sie aufgerufen und in der Datenbank gespeichert werden und wie die Infrastruktur zwischen dem mobilen Gerät und der Cloud funktioniert. Microsoft bietet eine Lösung, mit der Sie schnell und einfach Daten in der Cloud speichern, Pushbenachrichtigungen verwenden und Benutzer authentifizieren können: WAMS. Da ich unter Verwendung der anderen Optionen bereits Lösungen erstellt habe, hielt ich den Einsatz von WAMS für sinnvoll, weil die Infrastruktur möglichst kompakt gehalten werden soll und WAMS eine gute plattformübergreifende Unterstützung bietet. WAMS wird mitunter als „Back-End, das Sie nicht erst erstellen müssen“ beschrieben.

Windows Azure Mobile Services

WAMS kann Ihre mobile Entwicklung vorantreiben – es bietet Speicher, Benutzerauthentifizierung für mehrere soziale Netzwerke, Mechanismen zum Erstellen von Logik auf dem Server (mit „Node.js“), Pushbenachrichtigungen und eine gepackte clientseitige Codebibliothek für einfachere CRUD(Create, Read, Update, Delete)-Operationen.

Bei Verwendung von WAMS müssen Sie als Erstes den mobilen Dienst und eine zugeordnete Datenbanktabelle erstellen. Die Datenbanktabelle ist nicht unbedingt erforderlich. Weitere Informationen zum Einrichten finden Sie im Windows Azure-Lernprogramm unter bit.ly/Nc8rWX.

Serverskripts Grundlegende CRUD-Operationen erfolgen in WAMS über Serveroperationen. Dabei handelt es sich um die Dateien „delete.js“, „insert.js“, „read.js“ und „update.js“, die über „Node.js“ auf dem Server verarbeitet werden. Weitere Informationen zu „Node.js“ in WAMS finden Sie im Artikel zum Arbeiten mit Serverskripts in Mobile Services auf der Windows Azure-Website unter bit.ly/1cHASFA.

Betrachten wir zunächst die Datei „insert.js“ in Abbildung 1. Der Parameter „item“ in der Methodensignatur enthält die übergebenen Daten. Die Objektmember sind dem Datenobjekt zugeordnet, das vom Client übergeben wurde. Sie können die Member dieses Objekts besser einordnen, wenn Sie sich den Abschnitt über das Verwenden von clientbasierten Daten angeschaut haben. Der Parameter „user“ enthält Angaben über den verbundenen Benutzer. In diesem Beispiel muss der Benutzer authentifiziert werden. Die App verwendet Facebook und Twitter zur Authentifizierung. Daher besitzt das zurückgegebene userId-Element das Format „Netzwerk:12345678“, wobei der „Netzwerk“-Teil des Werts den Namen des Netzwerkanbieters enthält. In diesem Beispiel steht entweder Facebook oder Twitter zur Verfügung, also ist einer der beiden Namen Teil des Werts. Die Zahl „12345678“ stellt das userId-Element dar. Zwar verwenden wir in unserem Beispiel Twitter und Facebook, aber Windows Azure kann auch mit Microsoft- und Google-Konten arbeiten.

Abbildung 1: Die Datei „Insert.js“ wird beim Übertragen eines Golf-Abschlags in die Cloud verwendet

function insert(item, user, request) {
  if ((!isNaN(item.StartingLat)) && (!isNaN(item.StartingLon)) &&
    (!isNaN(item.EndingLat)) && (!isNaN(item.EndingLon))) {
    var distance1 = 0.0;
    var distance2 = 0.0;
    var sd = item.StartingTime;
    var ed = item.EndingTime;
    var sdate = new Date(sd);
    var edate = new Date(ed);
    var res = user.userId.split(":");
    var provider = res[0].replace("'", "''");
    var userId = res[1].replace("'", "''");
    var insertStartingDate = sdate.getFullYear() + "-" +
       (sdate.getMonth() + 1) + "-" + sdate.getDate() + " " +
      sdate.getHours() + ":" + sdate.getMinutes() + ":" +
      sdate.getSeconds();
    var insertEndingDate = edate.getFullYear() + "-" +
      (edate.getMonth() + 1) + "-" + edate.getDate() + " " +
      edate.getHours() + ":" + edate.getMinutes() + ":" + 
      edate.getSeconds();
    var lat1 = item.StartingLat;
    var lon1 = item.StartingLon;
    var lat2 = item.EndingLat;
    var lon2 = item.EndingLon;
    var sp = "'POINT(" + item.StartingLon + " " + 
      item.StartingLat + ")'";
    var ep = "'POINT(" + item.EndingLon + " " + 
      item.EndingLat + ")'";
    var sql = "select Max(Distance) as LongDrive from Drive";
    mssql.query(sql, [], {
      success: function (results) {
        if ( results.length == 1)
        {
          distance1 = results[0].LongDrive;
        }
      }
    });
    var sqlDis = "select [dbo].[CalculateDistanceViaLatLon](?, ?, ?, ?)";
    var args = [lat1, lon1, lat2, lon2];
    mssql.query(sqlDis, args, {
      success: function (distance) {
        distance2 = distance[0].Column0;
      }
    });
    var queryString = 
      "INSERT INTO DRIVE (STARTINGPOINT, ENDINGPOINT, " +
      "STARTINGTIME, ENDINGTIME, Provider, UserID, " +
      "deviceType, deviceToken, chanelUri) VALUES " +
      "(geography::STPointFromText(" + sp + ", 4326), " +
      " geography::STPointFromText(" + ep + ", 4326), " +
      " '" + insertStartingDate + "', '" +
      insertEndingDate + "', '" + provider + "', " + userId + ", " +
      item.deviceType + ", '" + item.deviceToken.replace("'", "''") +
       "', " + "'" + item.ChannelUri.replace("'", "''") + "')";
    console.log(queryString);
    mssql.query(queryString, [], {
      success: function () {
        if (distance2 > distance1) {
          if (item.deviceType == 0) {
            push.mpns.sendFlipTile(item.ChannelUri, {
              title: "New long drive leader"
            }, {
                  success: function (pushResponse) {
                    console.log("Sent push:", pushResponse);
                  }
               });
            }
          if (item.deviceType == 1) {
            push.apns.send(item.deviceToken, {
              alert: "New Long Drive",
              payload: {
                inAppMessage: "Hey, there is now a new long drive."
              }
            });
          }
        }
      },
      error: function (err) {
        console.log("Error: " + err);
      }
    });
    request.respond(200, {});
  }
}

Zuerst muss der Code zur Überprüfung der Eingabe getestet werden. Ich möchte sicherstellen, dass die übertragenen Breiten- und Längengrade gültige Zahlen sind. Andernfalls wird der Einfügevorgang sofort abgebrochen. Als Zweites wird das übergebene userId-Element analysiert, um den Netzwerkanbieter und die numerische Benutzer-ID abzurufen. Im dritten Schritt werden die Datumsangaben eingerichtet, damit sie in die Datenbank eingefügt werden können. JavaScript und SQL Server stellen Daten und Uhrzeiten unterschiedlich dar, daher müssen diese Angaben analysiert und ins richtige Format umgewandelt werden.

Nun muss eine Abfrage ausgeführt werden. Ein Node.js-Befehl zum Ausführen einer CRUD-Anweisung ruft „mssql.query(command, parameters, callbacks)“ auf. Der command-Parameter ist der auszuführende SQL-Befehl. Der parameters-Parameter ist ein JavaScript-Array, das mit den im Befehl angegebenen Parametern übereinstimmt. Der callbacks-Parameter enthält die JavaScript-Rückrufe, die nach Abschluss der Abfrage zu verwenden sind, je nachdem, ob der Vorgang erfolgreich war oder nicht. Den Inhalt einer erfolgreichen ersten Abfrage erläutere ich im Abschnitt über Pushbenachrichtigungen.

Nun kommen wir zum Debugging. Woher wissen Sie, was im Skript passiert? JavaScript verfügt über die Methode „console.log(info)“. Wenn diese Methode mit dem info-Parameter aufgerufen wird, wird der Parameter in der Protokolldatei des Diensts gespeichert (siehe Abbildung 2). Beachten Sie die integrierte Aktualisierungsfunktion rechts oben auf dem Bildschirm.

Log File Information in Visual Studio 2013
Abbildung 2: Protokolldateidaten in Visual Studio 2013

Nach der Einrichtung von WAMS erfolgt die Verwaltung über das windowsazure.com-Portal oder Visual Studio.

Anmerkung: Der Aufruf einer Methode von einer WAMS-Skriptdatei kann bei der Standardeinrichtung zu einem Fehler führen, da die Elemente in unterschiedlichen Schemas ausgeführt werden. Je nach Situation müssen Berechtigungen gewährt werden. Unter bit.ly/1cHQ4Cu finden Sie einen Blogbeitrag von Jeff Sanders zu diesem Thema.

Skalieren

Mobile Apps können eine große Belastung für die Infrastruktur darstellen, aber zum Glück gibt es in Windows Azure mehrere Optionen für den Umgang mit Lasten. Es gibt zum Beispiel Nachrichtenwarteschlangen und mehrere Alternativen.

Warteschlangen stehen in Windows Azure per Service Bus und über den Warteschlangendienst von Windows Azure zur Verfügung. So können Sie Daten schnell speichern, ohne die App zu beschäftigen. Wenn eine App stark ausgelastet ist, wartet sie möglicherweise auf die Antwort von einer Remotedatenquelle. Anstatt direkt mit einer Datenquelle zu interagieren, kann die App Daten in einer Warteschlange speichern und so die Verarbeitung fortsetzen. In meiner Erfahrung ist die Verwendung von Warteschlangen eine einfache Möglichkeit, die Skalierbarkeit einer App zu erhöhen. Zwar verwendet unsere App keine Warteschlangen, aber diese Funktion ist je nach Aufwand der Operationen und der Anzahl mobiler Geräte, die auf das System zugreifen, eine erwähnenswerte Option. Glücklicherweise besitzen sowohl der Service Bus als auch der Warteschlangendienst von Windows Azure die erforderlichen APIs, damit die Serverskripts in WAMS auf sie zugreifen können.

Insgesamt sind Warteschlangen eine gute Lösung für datenintensive Apps. Ein weiteres wichtiges Hilfsmittel ist die automatische Skalierung. Mit Windows Azure können Sie die Integrität und die Verfügbarkeit einer App von einem Dashboard aus überwachen. Sie können Regeln einrichten, damit ein Anwendungsadministrator benachrichtigt wird, wenn sich die Verfügbarkeit von Diensten verringert. Mit Windows Azure kann eine App bedarfsgerecht nach oben oder unten skaliert werden. Standardmäßig ist dieses Feature deaktiviert. Wenn es aktiviert ist, prüft Windows Azure in regelmäßigen Abständen die Anzahl der API-Aufrufe an den Dienst und führt eine Skalierung nach oben durch, wenn die Anzahl der Anrufe bei 90 Prozent (oder mehr) des API-Kontingents liegt. Windows Azure führt täglich eine Skalierung nach unten auf den festgelegten Mindestwert durch. Im Allgemeinen sollte das Kontingent für den Tag so festgelegt sein, dass der erwartete tägliche Verkehr bewältigt und Windows Azure bei Bedarf nach oben skaliert werden kann. Während ich dies schreibe, sind Integritätsüberwachung und automatische Skalierung in der Vorschau verfügbar.

Datenbank

Daten sind die Grundlage einer jeden App und die Basis für fast jedes Unternehmen. Sie können eine gehostete Drittanbieterdatenbank verwenden, einen Datenbankdienst, der auf einem virtuellen Computer ausgeführt wird, Windows Azure SQL-Datenbank und wahrscheinlich noch zahlreiche andere Optionen. Ich habe mich aus mehreren Gründen für Windows Azure SQL-Datenbank als Back-End-Datenbank (und gegen SQL Server auf einem virtuellen Computer) entschieden. Erstens unterstützt diese Lösung standortbasierte Dienste im Basisprodukt. Zweitens ist Windows Azure SQL-Datenbank im Gegensatz zu einer Basisinstallation von SQL Server auf einem Clientsystem leistungsoptimiert. Und drittens ist keine fortlaufende Verwaltung des zugrunde liegenden Systems erforderlich.

Windows Azure SQL-Datenbank besitzt dieselben Punkt- und Geografie-Datenbanktypen wie SQL Server, so können Distanzen zwischen zwei Punkten leicht errechnet werden. Zur Vereinfachung habe ich zwei gespeicherte Prozeduren erstellt, mit denen diese Distanzen berechnet werden. Die SQL-Funktion „Calculate­DistanceViaLatLon“ verwendet die Gleitkommawerte der Breiten- und Längengrade. Sie ist für die Ausführung im WAMS-Skript „insert.js“ konzipiert, um die Distanz des eingehenden Abschlags schnell berechnen zu können. Das Ergebnis kann dann mit dem aktuellen längsten Abschlag im System verglichen werden. Die SQL-Funktion „CalculateDistance“ verwendet zwei geografische Punkte und berechnet die Distanz zwischen ihnen (siehe Abbildung 3). Die Daten werden in der Drive-Tabelle als SQL Server-Punkte gespeichert.

Abbildung 3: SQL-Funktion zum Berechnen der Distanz zwischen zwei Punkten

 

    CREATE FUNCTION [dbo].[CalculateDistanceViaLatLon]
    (
      @lat1 float,
      @lon1 float,
      @lat2 float,
      @lon2 float
    )
    RETURNS float
    AS
    BEGIN
      declare @g1 sys.geography = sys.geography::Point(@lat1, @lon1, 4326)
      declare @g2 sys.geography = sys.geography::Point(@lat2, @lon2, 4326)
      RETURN @g1.STDistance(@g2)
    END
    CREATE FUNCTION [dbo].[CalculateDistance]
    (
      @param1 [sys].[geography],
      @param2 [sys].[geography]
    )
    RETURNS INT
    AS
    BEGIN
      RETURN @param1.STDistance(@param2)
    END

Abbildung 4 zeigt die Tabelle, in der die Abschlagdaten gespeichert werden. Da Spalten mit dem Präfix „__“ Windows Azure-spezifisch sind, habe ich versucht, sie zu vermeiden. Von besonderem Interesse sind die Spalten „StartingPoint“, „EndingPoint“, „Distance“, „deviceToken“ und „deviceType“. Die Spalten „StartingPoint“ und „EndingPoint“ enthalten den geografischen Start- und Endpunkt des Abschlags. Die Distance-Spalte enthält berechnete Werte. Dabei handelt es sich um Gleitkommawerte, die die SQL-Funktion „Calculate­Distance“ verwenden. Die Spalten „deviceToken“ und „deviceType“ enthalten ein Token, mit dem das Gerät und der Gerätetyp (Windows Phone-basiert oder iPhone) identifiziert werden. Derzeit kommuniziert die App nur mit dem bereitstellenden Gerät, falls ein neu eingegebener Abschlag zum Spitzenreiter wird. Die Spalten „deviceToken“ und „deviceType“ können dazu verwendet werden, Benachrichtigungen im Falle einer neuen Führungsposition zu versenden und um die Wettbewerbsteilnehmer regelmäßig über andere Neuigkeiten zu informieren.

Abbildung 4: SQL-Tabelle zum Speichern von Abschlagdaten

    CREATE TABLE [MsdnMagGolfLongDrive].[Drive] (
      [id]            NVARCHAR (255)
         CONSTRAINT [DF_Drive_id] 
         DEFAULT (CONVERT([nvarchar](255),newid(),(0))) 
         NOT NULL,
      [__createdAt]   DATETIMEOFFSET (3) CONSTRAINT
        [DF_Drive___createdAt] DEFAULT (CONVERT([datetimeoffset](3),
        sysutcdatetime(),(0))) NOT NULL,
      [__updatedAt]   DATETIMEOFFSET (3) NULL,
      [__version]     ROWVERSION         NOT NULL,
      [UserID]        BIGINT             NULL,
      [StartingPoint] [sys].[geography]  NULL,
      [EndingPoint]   [sys].[geography]  NULL,
      [DateEntered]   DATETIME           NULL,
      [DateUpdated]   DATETIME           NULL,
      [StartingTime]  DATETIME           NULL,
      [EndingTime]    DATETIME           NULL,
      [Distance]      AS                 ([dbo].[CalculateDistance]
        ([StartingPoint],[EndingPoint])),
      [Provider]      NVARCHAR (20)      NULL,
      [deviceToken]   NVARCHAR (100)     NULL,
      [deviceType] INT NULL,
      PRIMARY KEY NONCLUSTERED ([id] ASC)
    );

Dynamisches Schema

Einer der Vorteile von WAMS ist das standardmäßig dynamische Datenbanktabellen-Schema. Es wird anhand der Daten geändert, die das mobile Gerät an den Client sendet. Sie sollten diese Funktion jedoch deaktivieren, wenn Sie von der Entwicklungsphase zur Produktion wechseln, da Sie ganz sicher vermeiden möchten, dass es aufgrund eines Programmierfehlers zu einer Schemaänderung innerhalb eines laufenden Systems kommt. Rufen Sie einfach im Windows Azure-Portal im Bereich „WAMS“ die Option „Konfigurieren“ auf, und deaktivieren Sie das dynamische Schema.

Zugriff auf Daten

Datenzugriff über ein mobiles Gerät und ein unzuverlässiges Netzwerk mit relativ hoher Latenz ist eine komplett andere Sache als Datenzugriff über eine Kabelverbindung und ein Netzwerk mit geringer Latenz. Es gibt zwei allgemeine Regeln für das Abrufen von Daten über ein Gerät. Zum einen muss der Datenzugriff asynchron erfolgen. Da mobile Netzwerke von Natur aus unzuverlässig sind und eine hohe Latenz aufweisen, sollten Sie den UI-Thread keinesfalls sperren. Benutzer verstehen nicht, warum die Benutzeroberfläche langsam reagiert. Wenn das Abrufen von Daten zu lange dauert, geht das Betriebssystem des Geräts davon aus, dass sich die App aufgehängt hat, und beendet sie. Zum anderen dürfen nur relativ kleine Datenmengen übertragen werden. Wenn zu viele Datensätze an ein mobiles Gerät gesendet werden, kommt es aufgrund der niedrigen Geschwindigkeit, der in mobilen Netzwerken üblichen hohen Latenz und der Tatsache, dass mobile Gerät-CPUs eher für den niedrigen Energieverbrauch als für die Datenverarbeitung (wie Laptop- oder Desktop-CPUs) optimiert sind, zu Problemen. Mit WAMS werden diese beiden Probleme gelöst. Der Datenzugriff ist asynchron und Abfragen erfolgen automatisch über einen Paging-Algorithmus. Ich demonstriere dies anhand von zwei Operationen – einem Einfüge- und einem Auswahlvorgang.

Verwenden eines Proxys Jeder Entwickler, der schon einmal REST-basierte Dienste aufgerufen hat, kennt das Problem: REST hat keinen integrierten Proxydienst. Dadurch kommt es bei der Arbeit mit REST leicht zu Fehlern. Die Verwendung von REST ist nicht unmöglich, nur ein bisschen schwieriger als die Verwendung von SOAP. Um die Entwicklung zu vereinfachen, können Sie einen lokalen Proxy erstellen. Der Proxy für dieses Beispiel ist in Abbildung 5 dargestellt. Auf die Eigenschaften einer Objektinstanz kann über die Serverskripts zugegriffen werden.

Abbildung 5: Proxy für die Arbeit mit REST

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace Support
{
  public partial class Drive
  {
    public Drive() {
      DeviceToken = String.Empty;
      ChannelUri = String.Empty;
    }
    [JsonProperty(PropertyName="id")]
    public string Id { get; set; }
    [JsonProperty(PropertyName = "UserID")]
    public Int64 UserID { get; set; }
    [JsonProperty(PropertyName = "Provider")]
    public string Provider { get; set; }
    public double StartingLat { get; set; }
    public double StartingLon { get; set; }
    public double EndingLat { get; set; }
    public double EndingLon { get; set; }
    [JsonProperty(PropertyName = "StartingTime")]
    public DateTime StartingTime { get; set; }
    [JsonProperty(PropertyName = "EndingTime")]
    public DateTime EndingTime { get; set; }
    [JsonProperty(PropertyName = "Distance")]
    public double Distance { get; set; }
    [JsonProperty(PropertyName = "deviceType")]
    public int deviceType { get; set; }
    [JsonProperty(PropertyName = "deviceToken")]
    public string DeviceToken { get; set; }
    [JsonProperty(PropertyName = "ChannelUri")]
    public string ChannelUri { get; set; }
  }
}

Abfragen von Daten Das Abfragen von Daten in Windows Azure ist ganz einfach. Die Daten werden in einem Aufruf über eine LINQ-Abfrage zurückgegeben. Hier ist ein Aufruf für eine einfache Abfrage zum Zurückgeben von Daten: 

var drives = _app.client.GetTable<Support.Drive>();
var query = drives.OrderByDescending(
  drive => drive.Distance).Skip(startingPoint).Take(PageSize);
var listedDrives = await query.ToListAsync();

In diesem Beispiel benötige ich die Liste mit den längsten Abschlägen in absteigender Reihenfolge. Diese Informationen werden dann sowohl in einem Windows Phone-basierten Gerät als auch einem iPhone an ein Raster gebunden. Nur das Binden der Daten ist unterschiedlich, das Abrufen ist identisch.

In der vorherigen Abfrage wurde keine Where-Methode aufgerufen, aber das ließe sich leicht einrichten. Mithilfe der Methoden „Skip“ und „Take“ wird aufgezeigt, wie einfach Paging in die App integriert werden kann. Abbildung 6 zeigt die Anzeigetafel auf einem Windows Phone-basierten Gerät und einem iPhone.

The Scoreboard As Depicted on a Windows Phone-Based Device and an iPhone
Abbildung 6: Anzeigetafel auf einem Windows Phone-basierten Gerät und einem iPhone

Einfügen von Daten

Das Einfügen eines Datensatzes in WAMS ist einfach. Sie erstellen eine Instanz des Datenobjekts und rufen dann die InsertAsync-Method für das Clientobjekt auf. Der Code für den Einfügevorgang auf einem Windows Phone-basierten Gerät ist in Abbildung 7 zu sehen. Der Code zum Einfügen von Daten in Xamarin.iOS ist ähnlich. Unterschiedlich sind nur die Elemente „deviceType“, „ChannelUri“ und „DeviceToken“.

Abbildung 7: Einfügen von Daten (Windows Phone-basiertes Gerät)

async void PostDrive()
{
  Drive d = new Drive();
  d.StartingLat = first.Latitude;
  d.StartingLon = first.Longitude;
  d.EndingLat = second.Latitude;
  d.EndingLon = second.Longitude;
  d.StartingTime = startingTime;
  d.EndingTime = endingTime;
  d.deviceType = (int)Support.AppConstants.DeviceType.WindowsPhone8;
  d.ChannelUri = _app.CurrentChannel.ChannelUri.ToString();
  try
  {
    await _app.client.GetTable<Support.Drive>().InsertAsync(d);
  }
  catch (System.Exception exc)
  {
    Console.WriteLine(exc.Message);
  }
}

Gemeinsamer Code für verschiedene Plattformen

Die gemeinsame Nutzung von Code auf verschiedenen Plattformen ist ein wichtiger Aspekt. Dank der plattformübergreifenden Funktionen von C# und Xamarin gibt es hierfür verschiedene Möglichkeiten. Welche Methode verwendet wird, hängt jeweils von der Situation ab. Ich musste Nicht-UI-Logik freigeben. Dazu gibt es zwei Optionen: PLC(portable Klassenbibliothek)-Projekte und verknüpfte Dateien.

Portable Klassenbibliotheken Viele Plattformen verwenden .NET Framework. Dazu gehören zum Beispiel Windows, Windows Phone, Xbox, Windows Azure und andere von Microsoft unterstützte Plattformen. Bei der ursprünglichen Version von .NET Framework (und mehreren nachfolgenden Versionen) musste .NET-Code für verschiedene Plattformen neu kompiliert werden. Portable Klassenbibliotheken haben diesem Problem ein Ende bereitet. Bei einem PCL-Projekt richten Sie eine Bibliothek ein, die einen definierten Satz von APIs unterstützt, die für die Zielplattformen zur Verfügung stehen. Diese Plattformauswahl wird in den Projekteinstellungen der Klassenbibliothek festgelegt.

Neben der Unterstützung von PLCs hat Microsoft letzten Herbst auch die Lizenzierung für PCLs geändert, sodass nun auch Plattformen von Drittanbietern unterstützt werden. Daher kann nun auch Xamarin Inc. Unterstützung für definierte Microsoft-PCLs für iOS, Android und OS X anbieten. Dokumentation über die Verwendung portabler Klassenbibliotheken ist leicht erhältlich.

Verknüpfte Dateien Portable Klassenbibliotheken sind eine hervorragende Lösung für die plattformübergreifende Entwicklung. Wenn jedoch nur eine der Plattformen ein bestimmtes Feature enthält, sind verknüpfte Dateien eine gute Alternative, um Code gemeinsam zu nutzen. Zur Verwendung von verknüpften Dateien gehören auch eine grundlegende .NET-Klassenbibliothek, plattformspezifische Klassenbibliotheken und das Anwendungsprojekt für die Plattform. Die .NET-Klassenbibliothek enthält den allgemeinen Code, der plattformübergreifend genutzt wird.

Die plattformspezifische Bibliothek umfasst zwei Arten von Dateien. Dabei handelt es sich um verknüpfte Dateien aus der generischen .NET-Klassenbibliothek und Code mit gemeinsamen APIs aber unterschiedlichen plattformspezifischen Implementierungsansätzen. Es soll möglichst eine Datei aus dem Klassenbibliotheksprojekt per „Als Link hinzufügen“ zur plattformspezifischen Klassenbibliothek hinzugefügt werden. Dies wird in Abbildung 8 gezeigt.

Using Linked Files
Abbildung 8: Verwenden von verknüpften Dateien

Andere Optionen Portable Klassenbibliotheken und verknüpfte Dateien sind nur zwei der Optionen für die gemeinsame Verwendung von Code. Alternativen sind zum Beispiel partielle Klassen, die Compileroptionen „if/def“, Beobachtermuster, Xamarin.Mobile (und ähnliche Bibliotheken) und andere Bibliotheken, die über NuGet (oder den Xamarin Component Store) verfügbar sind.

Mit partiellen Klassen können mehrere Klassendateien von der freigegebenen Klassenbibliothek und der plattformspezifischen Bibliothek gemeinsam verwendet werden. Standardmäßig werden in der .NET-Klassenbibliothek und der plattformspezifischen Bibliothek unterschiedliche Namespaces verwendet. Das größte Problem bei partiellen Klassen ist, dass die Namespaces übereinstimmen müssen. Nicht übereinstimmende Namespaces sind ein häufiger Fehler bei der Verwendung von partiellen Klassen.

In Visual Studio kann Code mithilfe der Compileroptionen „if/then“ in oder aus Code kompiliert werden. Darüber hinaus können die Plattformen als bedingte Kompilierungssymbole definiert werden. Sie werden in den Projekteigenschaften eingerichtet (siehe Abbildung 9), wobei die #if-Direktive zur bedingten Kompilierung von Code für Windows Phone verwendet wird.

Defining a Platform as a Conditional Compilation Symbol
Abbildung 9: Definieren einer Plattform als bedingtes Kompilierungssymbol

Xamarin.Mobile ist ein Satz von Bibliotheken mit gemeinsamen APIs. Die Bibliotheken sind für Windows Phone, iOS und Android verfügbar. Xamarin.Mobile unterstützt gegenwärtig Ortungsdienste, Kontakte und Kameras. In dieser App habe ich Geolocation-APIs von Xamarin.Mobile verwendet.

Zur Positionsbestimmung wird das Geolocator-Objekt als Wrapper für das plattformspezifische Geolocation-Objekt verwendet. Hier wird die asynchrone C# 5.0-Syntax eingesetzt. Nachdem eine Position ermittelt wurde, wird „.ContinueWith“ aufgerufen, und die Daten werden verarbeitet:

geo = new Geolocator();
...
await geo.GetPositionAsync(timeout: 30000).ContinueWith(t =>
  {
    first = t.Result;
    LandingSpot.IsEnabled = true;
  }, TaskScheduler.FromCurrentSynchronizationContext());

Beachten Sie, dass Geräte oftmals Annäherungswerte für Geolocation verwenden. Daher ist nicht jede aufgezeichnete Distanz vollkommen exakt.

Bei der Erstellung von Apps gehen die meisten Entwickler zunächst von höheren logischen App-Ebenen aus, die dann die niedrigeren Ebenen aufrufen. Ein Benutzer kann beispielsweise per Tastendruck eine Position ermitteln. Problematisch wird es, wenn die niedrigere logische Ebene einer App eine höhere Ebene aufrufen muss. Eine einfache Lösung ist die Weiterleitung eines Verweises von der höheren an eine niedrigere Ebene. Leider führt das fast immer dazu, dass Code auf niedrigeren Ebenen nicht plattformübergreifend genutzt werden kann. Diese Einschränkung kann über Ereignisse umgangen werden. Lösen Sie ein Ereignis auf der niedrigeren Ebene aus, das dann auf einer höheren Ebene verarbeitet wird. Das ist die Grundlage des Beobachtermusters.

Zahlreiche Drittanbieter haben Bibliotheken erstellt, die auf mehreren Plattformen genutzt werden können. Sie finden sie per NuGet und im Xamarin Component Store.

Pushbenachrichtigungen

Mitunter muss eine Server-App mit dem mobilen Gerät kommunizieren. Dies kann über WAMS oder den Benachrichtigungshub erfolgen. WAMS ist eine gute Lösung, wenn nur wenig Nachrichten gesendet werden. Der Benachrichtigungshub ist für das Senden von Nachrichten an zahlreiche Geräte konzipiert, zum Beispiel an Prämienkunden oder an „alle Kunden in Kalifornien“. Im Folgenden erläutere ich die Verwendung von WAMS.

Sie können WAMS-Pushbenachrichtigungen an ein mobiles Gerät innerhalb der Serverskripts aufrufen. Windows Azure verarbeitet einen Großteil der komplexen Abläufe von Pushbenachrichtigungen, aber es ist nicht in der Lage, alle Unterschiede beim Senden von Nachrichten an alle verschiedenen Plattformen zu abstrahieren. Glücklicherweise gibt es nur geringe Unterschiede.

Das mpns-Objekt wird zum Versenden von Nachrichten per Microsoft-Pushbenachrichtigungsdienst (MPNS) verwendet. Ein mpns-Objekt enthält vier Member. Dabei handelt es sich um „sendFlipTile“, „sendTile“, „sendToast“ und „sendRaw“. Sie besitzen alle eine ähnliche Signatur. Der erste Parameter ist der für die Kommunikation verwendete Kanal. Der zweite Parameter ist ein JSON-Objekt mit den Parametern, die an das Gerät gesendet werden. Der dritte Parameter ist ein Satz von Rückrufen, die in Abhängigkeit davon ausgeführt werden, ob die Anforderung erfolgreich war oder nicht. Der folgende Code enthält das mpns-Objekt und wird in den Windows Azure-Serverskripts verwendet, um eine Nachricht zu senden, wenn eine andere Person im Wettbewerb um den längsten Abschlag die Führung übernommen hat:

push.mpns.sendFlipTile(item.ChannelUri, {
  title: "New long drive leader"
}, {
    success: function (pushResponse) {
      console.log("Sent push:", pushResponse);
    }

Als Ergebnis wird die Kachel wie in Abbildung 10 aktualisiert. Beachten Sie, dass in der Kachel mit den Worten „New long drive leader“ auf einen neuen Spitzenreiter hingewiesen wird.

A Push Message Showing a New Long Drive Leader
Abbildung 10: Pushbenachrichtigung über einen neuen Spitzenreiter

Sie verwenden ein apns-Objekt, um in den WAMS-Skripts Nachrichten an APNS (Apple Push Notification Services) zu senden. Das Konzept ähnelt dem mpns-Objekt. Das interessanteste Member ist die Sendemethode. Ihre Signatur ähnelt den mpns-Sendemethoden. Die Signatur enthält drei Parameter: den deviceToken-Parameter, der ein Gerät eindeutig identifiziert, ein JSON-basiertes Parameterobjekt und einen Satz von Rückrufen als letzten Parameter.

Der folgende Code zeigt, wie mithilfe des apns-Objekts eine neue Spitzenreiternachricht an das iOS-Gerät gesendet wird:

push.apns.send(item.deviceToken, {
  alert: "New Long Drive",
  payload: {
    inAppMessage: "Hey, there is now a new long drive."
  }
});

Abbildung 11 stellt den Code dar, der zur Datei „AppDelegate.cs“ hinzugefügt wird, um die an das iPhone gesendete Nachricht zu verarbeiten. In diesem Beispiel wird dem Benutzer ein UIAlertView-Element angezeigt.

Abbildung 11: Verarbeiten einer Nachricht auf einem iPhone

public override void RegisteredForRemoteNotifications(
  UIApplication application, NSData deviceToken)
{
  string trimmedDeviceToken = deviceToken.Description;
  if (!string.IsNullOrWhiteSpace(trimmedDeviceToken))
  {
    trimmedDeviceToken = trimmedDeviceToken.Trim('<');
    trimmedDeviceToken = trimmedDeviceToken.Trim('>');
  }
  DeviceToken = trimmedDeviceToken;
}
public override void ReceivedRemoteNotification(
  UIApplication application, NSDictionary userInfo)
{
  System.Diagnostics.Debug.WriteLine(userInfo.ToString());
  NSObject inAppMessage;
  bool success = userInfo.TryGetValue(
    new NSString("inAppMessage"), out inAppMessage);
  if (success)
  {
    var alert = new UIAlertView("Got push notification",
      inAppMessage.ToString(), null, "OK", null);
    alert.Show();
  }
}

Sie können bei Bedarf ein gcm-Objekt verwenden, um Nachrichten an die Google Cloud Messaging(GCM)-Plattform zu senden.

Ein wichtiger Unterschied zwischen den Pushbenachrichtigungen (und Google-Benachrichtigungen) unter Windows und Apple besteht darin, wie das mobile Clientsystem diese Nachrichten verarbeitet. Eine vollständige Liste mit Clientsystemen ist im Xamarin.iOS-Projekt in der Datei „AppDelegate.cs“ im entsprechenden Codedownload enthalten.

Und das wär's! Viel Glück bei der Entwicklung Ihrer mobilen App und beim Golfspielen!

Wallace B. McClure besitzt Master- und Bachelor-Abschlüsse in Elektrotechnik des Georgia Institute of Technology (Georgia Tech). Er war für große und kleine Unternehmen in den Bereichen Beratung und Entwicklung tätig. McClure hat Bücher über die iPhone-Programmierung mit Xamarin.iOS, Android-Programmierung mit Xamarin.Android, Anwendungsarchitektur, ADO.NET, SQL Server und AJAX verfasst. Er ist Microsoft MVP, ASPInsider, Xamarin MVP und Xamarin Insider. Außerdem ist er Partner in Scalable Development Inc. Seine Schulungsunterlagen für iOS und Android sind unter „Learn Now Online“ erhältlich. Sie können ihm unter twitter.com/wbm auf Twitter folgen.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Kevin Darty (unabhängiger Auftragnehmer) und Brian Prince (Microsoft)