Standortbasierte Programmierung

Visualisierung der Bingrouten auf Windows Phone 7

Sandrino Di Di

Beispielcode herunterladen

Microsoft Windows Phone 7 ist mit einer benutzerfreundlichen API für den geografischen Standort ausgestattet, mit der Sie die derzeitige Position und Bewegung eines Benutzers bestimmen können. Dazu werden Breiten- und Längengrad angegeben (manchmal auch die Höhe über NN). Sobald Sie Zugriff auf diese Daten erlangt haben, können Sie für Ihre Windows Phone 7-Anwendung standortbasierte Features entwickeln.

Wenn Sie eine Anwendung für ein Hamburger-Restaurant entwickeln, wäre es doch großartig, wenn Sie dem Benutzer – neben dem Menü und Angeboten – anhand seines aktuellen Standorts auch das nächstgelegene Restaurant anzeigen könnten. Ein weiteres gutes Feature wäre die Möglichkeit, Menschen in der Nähe zu finden. Denken Sie dabei an Situationen von Vertriebsmitarbeitern, die herausfinden könnten, ob zwischen zwei Meetings eine Besprechung mit Kunden möglich wäre.

Dieser Artikel konzentriert sich auf die Integration dieser Daten in eine Windows Phone 7-Anwendung. Zudem erfahren Sie, wie Sie Routen und Standorte auf verschiedene Arten visualisieren. Die tatsächlichen Daten stammen von der Bing Maps-API. Bevor ich Ihnen einige neue Konzepte vorstellen kann, sollten wir uns zunächst die Grundlagen dieser Bing Maps-API ansehen.

Einführung in die Bing Maps-API

Zuerst benötigen Sie ein funktionierendes Bing-Konto. Im Bing Maps-Kontocenter (bingmapsportal.com) ist für die Anmeldung an ein Bing Maps-Konto eine Windows Live ID erforderlich. Sobald Ihr Konto erstellt ist, haben Sie Zugriff auf Ihre Kontodaten. Dort können Sie einen Schlüssel anlegen. Da Sie eine Anwendung für Windows Phone 7 entwickeln, können Sie als URL für die Anwendung zum Beispiel „http://localhost“ wählen.

Zusätzlich zur Kontoerstellung können Sie auf dieser Seite auch die Nutzung der Bing Maps-API überwachen. Falls Sie sich entscheiden, Bing Maps in einer Produktionsanwendung einzusetzen, nutzen Sie diese Seite, um wegen einer Lizenzierung einen Ansprechpartner zu kontaktieren.

Von der Bing Maps-API werden einige Dienste bereitgestellt, und die Windows Phone 7-Anwendung nutzt die SOAP-Dienste. Sie erhalten einen kurzen Überblick über die folgenden Dienste:

  • Geocode dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc
  • Imagery dev.virtualearth.net/webservices/v1/imageryservice/imageryservice.svc
  • Route dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc
  • Search dev.virtualearth.net/webservices/v1/searchservice/searchservice.svc  

Mit dem Geocode-Dienst können Sie mit Koordinaten und Adressen arbeiten; der Imagery-Dienst ermöglicht Ihnen die Nutzung von tatsächlichen Ansichten (Luftansicht, Vogelperspektive, Straßenbilder); mit dem Route-Dienst können Sie die Strecke zwischen zwei oder mehr Punkten berechnen lassen; mit dem Search-Dienst ist es möglich, auf Basis von Benutzereingaben nach geografischen Standorten zu suchen (z. B. „Restaurants in Brüssel“).

Um diese Dienste nutzen zu können, benötigen Sie nur einen „Dienstverweis“ auf eine der vorherigen URLs. Beachten Sie, dass durch diesen Vorgang die Datei „ServiceReferences.ClientConfig“ erstellt oder aktualisiert wird. In Abbildung 1 wird in einem Beispiel veranschaulicht, wie Sie die Geocode-Methode des Geocode-Diensts aufrufen.

Abbildung 1 Aufrufen der Geocode-Methode des Geocode-Diensts

// Create the request. 
var geoRequest = new GeocodeRequest(); 
geoRequest.Credentials = new Credentials(); 
geoRequest.Credentials.ApplicationId = "<my API key>"; 
geoRequest.Address = new Address(); 
geoRequest.Address.CountryRegion = "Belgium"; 
geoRequest.Address.PostalTown = "Brussels"; 
             
// Execute the request and display the results. 
var geoClient = new GeocodeServiceClient("BasicHttpBinding_IGeocodeService"); 
geoClient.GeocodeAsync(geoRequest); 
geoClient.GeocodeCompleted += (s, e) => 
{
  if (e.Result != null && e.Result.Results.Any(o => 
    o.Locations != null && o.Locations.Any()))
    Location = e.Result.Results.FirstOrDefault().Locations.FirstOrDefault();
  else if (e.Error != null)
    Error = e.Error.Message;
  else
    Error = "No results or locations found."; 
};

Jede Dienstmethode basiert auf Anforderung/Antwort. Sie erstellen ein Anforderungsobjekt, mit dem Sie die Frage für den Server vorbereiten und den API-Schlüssel konfigurieren. In diesem Fall habe ich eine Geocode-Anforderung (GeocodeRequest) erstellt und fordere damit den Server auf, mir den Geocode-Standort (GeocodeLocation) von Brüssel in Belgien (Brussels, Belgium) mitzuteilen. Wenn ich die Anforderungserstellung abgeschlossen habe, rufe ich einfach den Client asynchron auf. Abschließend erhalte ich eine Antwort mit den angeforderten Informationen. Falls Probleme auftreten, wird der Fehler mitgeteilt. Führen Sie die Beispielanwendung im Download zu diesem Artikel aus, um die GeocodeServiceClient-Klasse in Aktion zu erleben und zu sehen, wie der Standort (oder die Fehlermeldung) mithilfe der Datenbindung im Display angezeigt wird.

Um die Route zwischen zwei Adressen zu berechnen und dem Benutzer anzuzeigen, werden von der Anwendung die Dienste „Geocode“ und „Route“ genutzt.

Berechnen der Route

Mithilfe des Route-Diensts von Bing können Sie die Route von Punkt A zu Punkt B berechnen. Wie im vorherigen Beispiel wird auch hier die Kombination Anforderung/Antwort verwendet. Zunächst ermitteln Sie mit einer Geocode-Anforderung (GeocodeRequest) den tatsächlichen geografischen Standort der einzelnen Adressen, dann erstellen Sie anhand dieser Standorte eine Route-Anforderung (RouteRequest). In der Beispielanwendung im Download zu diesem Artikel ist sämtlicher Code enthalten, aber in Abbildung 2 finden Sie ein kurzes Beispiel dafür, wie dies erfolgt.

Abbildung 2 Erstellen einer Route-Anforderung (RouteRequest)

// Create the request. 
var routeRequest = new RouteRequest(); 
routeRequest.Credentials = new Credentials(); 
routeRequest.Credentials.ApplicationId = "<my API key>"; 
routeRequest.Waypoints = new ObservableCollection<Waypoint>(); 
routeRequest.Waypoints.Add(fromWaypoint); 
routeRequest.Waypoints.Add(toWaypoint); 
routeRequest.Options = new RouteOptions(); 
routeRequest.Options.RoutePathType = RoutePathType.Points; 
routeRequest.UserProfile = new UserProfile(); 
routeRequest.UserProfile.DistanceUnit = DistanceUnit.Kilometer; 
                 
// Execute the request. 
var routeClient = new RouteServiceClient("BasicHttpBinding_IRouteService"); 
routeClient.CalculateRouteCompleted += 
  new EventHandler<CalculateRouteCompletedEventArgs>(OnRouteComplete); 
routeClient.CalculateRouteAsync(routeRequest);

Beachten Sie, dass es sich bei der Waypoints-Eigenschaft in der Anforderung um eine Auflistung handelt, mit der Sie mehrere Wegpunkte hinzufügen können. Das kann hilfreich sein, wenn Sie statt einer Route von Punkt A zu Punkt B einen kompletten Reiseplan in Erfahrung bringen wollen.

Wenn Sie die Methode „CalculateRouteAsync“ ausführen, übernimmt der Dienst die „harte Arbeit“ für Sie: Route berechnen, alle Reiseplanpunkte einschließlich der erforderlichen Aktionen (wie z. B. abbiegen, eine Ausfahrt nehmen usw.) auflisten, Dauer und Distanz berechnen, alle Wegpunkte (geografische Standorte) angeben etc. In Abbildung 3 finden Sie eine Übersicht über wichtige, in der Route-Antwort (RouteResponse) enthaltene Daten.

RouteResponse Content

Abbildung 3 Inhalt einer Route-Antwort (RouteResponse)

Anzeigen der Route auf einer Karte

In meinem ersten Beispiel arbeite ich mit RoutePath-Punkten (Points), um die Route auf einer Karte anzuzeigen. Da das Toolkit von Windows Phone 7 bereits Steuerelemente für Bing Maps umfasst, muss nur ein Verweis auf die Assembly „Microsoft.Phone.Controls.Maps“ hinzugefügt werden. Danach ist es einfach, die Karte im Display des Mobiltelefons anzuzeigen. Hier ist ein Beispiel für die Anzeige einer Karte, auf der Brüssel abgebildet ist (der API-Schlüssel wird vom Anmeldeinformationsanbieter (CredentialsProvider) festgelegt):

<maps:Map Center="50.851041,4.361572" ZoomLevel="10" 
  CredentialsProvider="{StaticResource MapCredentials}" />

Wenn Sie beabsichtigen, eines der Steuerelemente in der Maps-Assembly einzusetzen, empfehle ich Ihnen, erst einen Verweis auf diese Assembly einzubinden, bevor Sie einen Dienstverweis auf die Bing-Dienste hinzufügen. Vom Dienstverweis werden dann Typen wie „Microsoft.Phone.Controls.Maps.Platform.Location“ erneut verwendet, das Erstellen neuer Typen ist nicht erforderlich. Zudem müssen Sie keine Konvertierungsmethoden oder Wertkonverter schreiben, damit die vom Dienst zurückgegebenen Daten verarbeitet werden können.

Nun wissen Sie, wie die Route zwischen zwei Punkten berechnet und eine Karte im Display des Mobiltelefons dargestellt wird. Bringen wir jetzt diese beiden Methoden zusammen, um die Route auf der Karte visuell abzubilden. Unter Verwendung der RoutePath-Punkte (Points) wird auf der Karte die Strecke eingezeichnet. Mit dem Map-Steuerelement können Sie beispielsweise Formen wie „Pushpin“ (Pin zum Kennzeichnen des Beginns und des Ziels einer Route) sowie „MapPolyline“ (Karten-Polylinie zum Zeichnen der Route auf Basis der geografischen Koordinaten) hinzufügen.

Da die vom Dienst zurückgegebenen Punkte einen anderen Typ aufweisen als die vom Map-Steuerelement verwendeten Punkte, habe ich zwei kleine Erweiterungsmethoden für die Konvertierung der Punkte in den korrekten Typ geschrieben. Sie werden in Abbildung 4 veranschaulicht.

Abbildung 4 Erweiterungsmethoden zur Konvertierung in den korrekten Punkttyp

public static GeoCoordinate ToCoordinate(this Location routeLocation) 
{ 
  return new GeoCoordinate(routeLocation.Latitude, routeLocation.Longitude); 
} 
  
public static LocationCollection ToCoordinates(this IEnumerable<Location> points)
{ 
  var locations = new LocationCollection(); 
  
  if (points != null) 
  { 
    foreach (var point in points) 
    { 
      locations.Add(point.ToCoordinate()); 
    } 
  } 
  
  return locations; 
}

Nachdem die CalculateRoute-Methode abgeschlossen ist, können Sie diese Erweiterungsmethoden für die Route-Antwort (RouteResponse) verwenden. Nach der Konvertierung werden durch die Erweiterungsmethoden Typen zurückgegeben, die beispielsweise für eine Bindung an die Bing Maps-Steuerelemente eingesetzt werden können. Da es sich hier um eine Silverlight-Anwendung handelt, sollten wir den IValueConverter-Wertkonverter für die tatsächliche Konvertierung nutzen. In Abbildung 5 finden Sie ein Beispiel für einen Wertkonverter, mit dem die Standorte (Location) in geografische Koordinaten (GeoCoordinate) konvertiert werden.

Abbildung 5 Verwenden des IValueConverter-Wertkonverters

public class LocationConverter : IValueConverter 
{ 
  public object Convert(object value, Type targetType,  
    object parameter, CultureInfo culture) 
  { 
    if (value is Location) 
    { 
      return (value as Location).ToCoordinate(); 
    } 
    else if (value is IEnumerable<Location>) 
    { 
      return (value as IEnumerable<Location>).ToCoordinates(); 
    } 
    else 
    { 
      return null; 
    } 
  }
}

Nun ist es an der Zeit, die Datenbindung zu konfigurieren. Die Datenbindung arbeitet mit den Konvertern, es ist also wichtig, diese zuerst zu deklarieren. Dies kann in den Seitenressourcen oder den Anwendungsressourcen geschehen (falls Sie planen, die Konverter erneut zu verwenden), wie im Folgenden gezeigt:

<phone:PhoneApplicationPage.Resources>
  <converters:LocationConverter x:Key="locationConverter" />
  <converters:ItineraryItemDisplayConverter x:Key="itineraryConverter" />
</phone:PhoneApplicationPage.Resources>

Wenn Sie die Konverter deklariert haben, können Sie das Maps-Steuerelement sowie weitere Overlay-Steuerelemente (wie die Steuerelemente „MapPolyline“ und „Pushpin“) hinzufügen und diese an die erforderlichen Eigenschaften binden, wie hier veranschaulicht:

<maps:Map Center="50.851041,4.361572" ZoomLevel="10" 
  CredentialsProvider="{StaticResource MapCredentials}">
  <maps:MapPolyline Locations="{Binding RoutePoints, 
    Converter={StaticResource locationConverter}}" 
    Stroke="#FF0000FF" StrokeThickness="5" />
  <maps:Pushpin Location="{Binding StartPoint, 
    Converter={StaticResource locationConverter}}" Content="Start" />
  <maps:Pushpin Location="{Binding EndPoint, 
    Converter={StaticResource locationConverter}}" Content="End" />
</maps:Map>

Wie Sie sehen, werden die bereits zuvor deklarierten Konverter von den Bindungen verwendet, um die Daten in ein Format zu konvertieren, das vom Maps-Steuerelement erkannt wird. Abschließend werden diese Eigenschaften so festgelegt, dass sie nach Vollendung von „CalculateMethod“ ausgeführt werden, wie im Folgenden gezeigt:

private void OnRouteComplete(object sender, CalculateRouteCompletedEventArgs e)
{
  if (e.Result != null && e.Result.Result != null 
    && e.Result.Result.Legs != null & e.Result.Result.Legs.Any())
  {
    var result = e.Result.Result;
    var legs = result.Legs.FirstOrDefault();
 
    StartPoint = legs.ActualStart;
    EndPoint = legs.ActualEnd;
    RoutePoints = result.RoutePath.Points;
    Itinerary = legs.Itinerary;
  }
}

In Abbildung 6 wird das Display nach dem Starten der Anwendung und erfolgter Routenberechnung abgebildet.

Visual Representation of the Route

Abbildung 6 Visuelle Darstellung der Route

Anzeigen von Richtungen

Wie Sie sehen, zählt das Anzeigen der Route auf einer Karte zum Standard. Im nächsten Beispiel zeige ich Ihnen, wie Sie ein benutzerdefiniertes Steuerelement entwickeln, mit dem unter Nutzung von Text und Übersicht der einzelnen ItineraryItem-Elemente die Richtungen vom Start- bis zum Zielpunkt angegeben werden. In Abbildung 7 ist das Endergebnis dargestellt.

Displaying Start-to-End Directions

Abbildung 7 Anzeigen von Start-Ziel-Richtungen

In Abbildung 7 sehen Sie, dass „Legs“(Etappen) zu den Eigenschaften des Route-Ergebnisses (RouteResult) zählt. Die Legs-Eigenschaft umfasst ein oder mehrere Leg-Objekte, von denen jedes eine ItineraryItems-Auflistung enthält. Mithilfe von „ItineraryItems“ können Sie das in Abbildung 7 angezeigte Steuerelement auffüllen. Jede Zeile in Abbildung 7 steht für ein ItineraryItem-Objekt, die Gesamtzeit in Sekunden und die gesamte Entfernung für diesen Schritt sowie der Index des aktuellen Schritts werden angezeigt. Da „ItineraryItem“ die aktuelle Schrittanzahl nicht verfolgt, habe ich eine kleine Klasse mit der Bezeichnung „ItineraryItemDisplay“ erstellt:

public class ItineraryItemDisplay  
{
  public int Index { get; set; }
  public long TotalSeconds { get; set; }
  public string Text { get; set; }
  public double Distance { get; set; }
}

Das Codebeispiel im zugehörigen Download für diesen Artikel enthält auch eine Erweiterungsmethode mit der folgenden Signatur:

public static ObservableCollection
  <ItineraryItemDisplay> ToDisplay(this 
  ObservableCollection<ItineraryItem> items)

Der Codeabschnitt für diese Methode durchläuft alle Elemente, schreibt die wichtigen Werte in ein neues ItineraryItemDisplay-Objekt und verfolgt die aktuelle Schrittanzahl in der Index-Eigenschaft. Abschließend sorgt der Konverter „ItineraryItemDisplayConverter“ für die Konvertierung während der Datenbindung. Wie Sie sicher in Abbildung 7 gesehen haben, ist jeder Routenschritt ordentlich formatiert (Orte und Straßen sind fett formatiert). Dazu wird ein benutzerdefiniertes Steuerelement mit der Bezeichnung „ItineraryItemBlock“ verwendet. Dessen einziger Zweck besteht darin, den ItineraryItem-Text übersichtlich zu formatieren. In Abbildung 7 sehen Sie außerdem einen blauen Block mit zusätzlichen Informationen, aber das ist die reguläre Datenbindung:

[TemplatePart(Name = "ItemTextBlock", Type = 
  typeof(TextBlock))] public class
  ItineraryItemBlock : Control

Mit dem TemplatePart-Attribut wird ein Element definiert, das im Template-Steuerelement vorhanden sein sollte; auch der Typ des Elements wird festgelegt. In diesem Fall handelt es sich um ein TextBlock-Element mit der Bezeichnung „ItemTextBlock“:

<Style TargetType="controls:ItineraryItemBlock" x:Key="ItineraryItemBlock">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType=
        "controls:ItineraryItemBlock">
        <TextBlock x:Name="ItemTextBlock" 
          TextWrapping="Wrap" 
          LineStackingStrategy=
          "BlockLineHeight" LineHeight="43" />
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Der Grund für die Auswahl von „TextBlock“ ist offensichtlich. Mit der Inlines-Eigenschaft von „TextBlock“ können Sie Codeinhalt zum TextBlock-Element hinzufügen. In einem benutzerdefinierten Steuerelement kann die Methode „OnApplyMethod“ überschrieben werden, und hier kommt „ItemTextBlock“ ins Spiel (siehe Abbildung 8).

Abbildung 8 Bestimmen von „TextBlock“

/// <summary> 
/// When the template is applied, find the textblock. 
/// </summary> 
public override void OnApplyTemplate() 
{ 
  base.OnApplyTemplate(); 
  
  // Get textblock. 
  textBlock = GetTemplateChild("ItemTextBlock") as TextBlock; 
  if (textBlock == null) 
    throw new InvalidOperationException
      ("Unable to find 'ItemTextBlock' TextBlock in the template."); 
  
  // Set the text if it was assigned before loading the template. 
  if (!String.IsNullOrEmpty(Text)) 
    SetItinerary(Text); 
}

Die Eigenschaft „Text“ von „ItineraryItem“ wird analysiert und verwendet, um zusätzliche Formatierung auf „TextBlock“ anzuwenden. Das ist recht einfach auszuführen, da die Eigenschaft „Text“ fast nur regulären Text umfasst. Einige Abschnitte sind von XML-Tags eingefasst:

<VirtualEarth:Action>Turn</VirtualEarth:Action> 
  <VirtualEarth:TurnDir>left</VirtualEarth:TurnDir>onto  
  <VirtualEarth:RoadName>Guido Gezellestraat
  </VirtualEarth:RoadName>

Da ich nur Orte und Straßennamen hervorheben möchte, habe ich eine kleine Methode geschrieben, mit der Tags wie „VirtualEarth:Action“ oder „VirtualEarth:TurnDir“ entfernt werden. Nach Abruf von „TextBlock“ wird die SetItinerary-Methode aufgerufen. Hier werden Inlines zu „TextBlock“ hinzugefügt (siehe Abbildung 9).

Abbildung 9 Hinzufügen von Inlines zu „TextBlock“ mithilfe der SetItinerary-Methode

// Read the input 
string dummyXml = String.Format(
  "<Itinerary xmlns:VirtualEarth=\"http://dummy\">{0}</Itinerary>", 
  itinerary); 
using (var stringReader = new StringReader(dummyXml)) 
{ 
  // Trace the previous element. 
  string previousElement = ""; 

  // Parse the dummy xml. 
  using (var xmlReader = XmlReader.Create(stringReader)) 
  { 
    // Read each element. 
    while (xmlReader.Read()) 
    { 
      // Add to textblock. 
      if (!String.IsNullOrEmpty(xmlReader.Value)) 
      { 
        if (previousElement.StartsWith("VirtualEarth:")) 
        { 
          textBlock.Inlines.Add(new Run() 
            { Text = xmlReader.Value, FontWeight = FontWeights.Bold }); 
        } 
        else 
        { 
          textBlock.Inlines.Add(new Run() { Text = xmlReader.Value }); 
        } 
      } 

      // Store the previous element. 
      if (xmlReader.NodeType == XmlNodeType.Element) 
        previousElement = xmlReader.Name; 
      else 
      previousElement = ""; 
    } 
  } 
}

Wie Sie im vorangegangenen XML-Testbeispiel gesehen haben, ist nicht der gesamte Text in einem XML-Element enthalten. Damit dieser Text mit „XmlReader“ gelesen werden kann, muss er zunächst in ein Dummy-XML-Element eingebunden werden. Damit ist es uns möglich, ein neues XmlReader-Element mit diesem Text zu erstellen. Mit der Read-Methode von „XmlReader“ können Sie die einzelnen Abschnitte der XML-Zeichenfolge als Schleife ausführen.

Basierend auf „NodeType“ können Sie die aktuelle Position in der XML-Zeichenfolge ermitteln. Betrachten Sie beispielsweise das folgende Element: <VirtualEarth:RoadName>Guido Gezellestraat</VirtualEarth:RoadName>. Mit der Read-Methode von „XmlReader“ erhalten Sie drei Iterationen. Die erste lautet <VirtualEarth:RoadName>, hierbei handelt es sich um „XmlNodeType.Element“. Da unsere Absicht darin besteht, die Straßen und Orte fett zu formatieren, wird hier ein Run-Objekt zu den Inlines in „TextBlock“ hinzugefügt, mit dem „FontWeight“ auf fett gesetzt wird. Durch das Einbinden eines Run-Objekts in die Inlines wird „TextBlock“ nur weiterer Text hinzugefügt.

In allen anderen Fällen ist keine Formatierung erforderlich, deshalb wird dort ein normales Run-Objekt eingefügt, das nur Text ohne festgelegte Formatierungseigenschaften enthält.

Dann sind wir mit dem benutzerdefinierten Steuerelement fertig. Der gesamte ItineraryItemDisplay-Datensatz wird mithilfe eines benutzerdefinierten DataTemplate-Objekts für das ListBox-Element angezeigt. In „DataTemplate“ ist auch ein Verweis auf das benutzerdefinierte Steuerelement enthalten (siehe Abbildung 10).

Abbildung 10 Gesamter ItineraryItemDisplay-Datensatz in benutzerdefiniertem DataTemplate-Objekt für ListBox-Element

<!-- Template for a full item (includes duration and time) --> 
<DataTemplate x:Key="ItineraryItemComplete"> 
  <Grid Height="173" Margin="12,0,12,12"> 
    <!-- Left part: Index, Distance, Duration. --> 
    <Grid HorizontalAlignment="Left" Width="75"> 
      <Grid.ColumnDefinitions> 
        <ColumnDefinition Width="25*" /> 
        <ColumnDefinition Width="25*" /> 
        <ColumnDefinition Width="25*" /> 
        <ColumnDefinition Width="25*" /> 
      </Grid.ColumnDefinitions> 
      <Grid.RowDefinitions> 
        <RowDefinition Height="50*"></RowDefinition> 
        <RowDefinition Height="20*"></RowDefinition> 
        <RowDefinition Height="20*"></RowDefinition> 
      </Grid.RowDefinitions> 

      <!-- Gray rectangle. --> 
      <Rectangle Grid.ColumnSpan="4" Grid.RowSpan="3" Fill="#FF0189B4" /> 

      <!-- Metadata fields. --> 
      <TextBlock Text="{Binding Index}" 
        Style="{StaticResource ItineraryItemMetadata}"    
        Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" /> 
      <TextBlock Text="{Binding Distance, 
        Converter={StaticResource kilometers}}" 
        Style="{StaticResource ItineraryItemMetadata}" 
        FontSize="{StaticResource PhoneFontSizeSmall}"                 
        Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" /> 
      <TextBlock Text="{Binding TotalSeconds, 
        Converter={StaticResource seconds}}" 
        Style="{StaticResource ItineraryItemMetadata}" 
        FontSize="{StaticResource PhoneFontSizeSmall}" 
        Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" /> 
    </Grid> 

    <!-- Right part to show directions. --> 
    <StackPanel Margin="84,-4,0,0" VerticalAlignment="Top" > 
      <controls:ItineraryItemBlock Text="{Binding Text}" 
        Style="{StaticResource ItineraryItemBlock}" 
        FontSize="{StaticResource PhoneFontSizeLarge}" 
        Foreground="{StaticResource PhoneForegroundBrush}"   
        Padding="0,3,0,0" Margin="0,0,0,5" /> 
    </StackPanel> 
  </Grid> 
</DataTemplate>

Das benutzerdefinierte Steuerelement sowie die Formatierung sind definiert. Nun bleibt noch die Aufgabe, dies in das Pivotsteuerelement und in Code zu implementieren. Wie bereits gesagt, werden das benutzerdefinierte Steuerelement und „DataTemplate“ in einem ListBox-Element verwendet:

<controls:PivotItem Header="Directions">
  <ListBox ItemsSource=
    "{Binding Itinerary, Converter={StaticResource itineraryConverter}}" 
    Grid.RowSpan="2" ItemTemplate="{StaticResource ItineraryItemComplete}" />
</controls:PivotItem>

Das ItemSource-Element von „ListBox“ ist an die Itinerary-Eigenschaft gebunden, und auf diese Weise wird diese Eigenschaft auch gefüllt. Den nachfolgenden Rest übernimmt „ItineraryItemDisplayConverter“. Wie Sie sehen, können Sie mit einem benutzerdefiniertem Steuerelement und ein bisschen Formatierung die Reiseplandaten aus dem Dienst „Route“ für den Benutzer ansprechend darstellen:

private void OnRouteComplete(object sender, CalculateRouteCompletedEventArgs e)
{
  if (e.Result != null && e.Result.Result != null && 
    e.Result.Result.Legs != null & e.Result.Result.Legs.Any())
  {
    ...
    Itinerary = e.Result.Result.Legs.FirstOrDefault().Itinerary;
  }
}

Ermitteln des aktuellen Standorts

In den vorigen Beispielen haben Sie erfahren, wie Sie die Dienste „Geocode“ und „Route“ nutzen, um die Richtungsbestimmung von Punkt A zu Punkt B zu ermitteln. Zudem wissen Sie nun, wie Sie diese Richtungen visualisieren. Nun beschäftigen wir uns mit der API für den geografischen Standort.

Mithilfe der Klasse „GeoCoordinateWatcher“ ermitteln Sie die derzeitigen GPS-Koordinaten:

coordinateWatcher = new GeoCoordinateWatcher
  (GeoPositionAccuracy.High); coordinateWatcher.StatusChanged += 
  new EventHandler<GeoPositionStatusChangedEventArgs>(OnCoordinateUpdate); 
coordinateWatcher.Start();

Die Klasse „GeoCoordinateWatcher“ durchläuft nach dem Ausführen der Start-Methode verschiedene Phasen. Wird der Status „Ready“ erreicht, haben Sie Zugriff auf die aktuelle Position. Wenn „GeoCoordinateWatcher” alle Aufgaben ausgeführt hat, besteht die beste Vorgehensweise darin, die Stop-Methode aufzurufen:

private void OnCoordinateStatusChanged(object sender,  
  GeoPositionStatusChangedEventArgs e) 
{ 
  if (e.Status == GeoPositionStatus.Ready) 
  {
    coordinateWatcher.Stop();
 
    // Get position.
    fromLocation = coordinateWatcher.Position.Location;
    LocationLoaded(); 
  } 
}

Die Beispielanwendung bietet auch standortbasierte Features. „GeoCoordinateWatcher“ verfügt über ein PositionChanged-Ereignis, mit dem Sie Standortänderungen verfolgen können. Wenn Sie eine Anwendung für die Richtungsanzeige entwickeln, können Sie anhand der Standortänderungen die einzelnen Schritte automatisch durchblättern lassen. Sie können sogar auf Basis von „VirtualEarth:Action“ im ItineraryItem-Text Sound abspielen. Am Ende haben Sie eine echte GPS-Navigationsanwendung erstellt.

Debuggen Sie die Anwendung mit dem Windows Phone 7-Emulator? Wenn Sie die in Ihre Anwendung eingebundenen Funktionen zur geografischen Standortbestimmung testen, tritt möglicherweise ein kleines Problem mit dem Status von „GeoCoordinateWatcher“ auf: Der Status lautet immer „NoData“, und er ändert sich nie in „Ready“. Deshalb ist es wichtig, dass Sie den Code auf die Schnittstelle (IGeoPositionWatcher<GeoCoordinate>) und nicht die Implementierung (GeoCoordinateWatcher) abstimmen. Im Blogbeitrag von Tim Heuer (bit.ly/cW4fM1) können Sie die Klasse „EventListGeoLocationMock“ herunterladen, die ein echtes GPS-Gerät simuliert.

Die Klasse „EventListGeoLocationMock“ akzeptiert eine Auflistung von „GeoCoordinateEventMocks“, die für eine zeitgleiche Simulation der Benutzerkoordinaten sorgt. Somit können Sie den Standort und die Bewegung des Benutzers testen:

GeoCoordinateEventMock[] events = new GeoCoordinateEventMock[]  
{  
  new  GeoCoordinateEventMock { Latitude = 50, Longitude = 6, 
    Time = new TimeSpan(0,0,5) }, 
  new  GeoCoordinateEventMock { Latitude = 50, Longitude = 7, 
    Time = new TimeSpan(0,15,0) } 
};
  
IGeoPositionWatcher<GeoCoordinate>  coordinateWatcher = 
  new EventListGeoLocationMock(events); coordinateWatcher.StatusChanged += 
  new EventHandler<GeoPositionStatusChangedEventArgs>(...); 
coordinateWatcher.Start();

Basierend auf dem Gerätenamen können Sie ermitteln, ob die Anwendung auf einem echten Gerät oder dem Emulator ausgeführt wird. Dann entscheiden Sie, welches IGeoPositionWatcher-Element verwendet werden soll. Achten Sie auf die erweiterte Eigenschaft „DeviceName“, diese ist bei einer Emulator-Ausführung der Anwendung immer auf „XDeviceEmulator“ gesetzt:

private static bool IsEmulator() 
{ 
  return (Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceName") 
    as string) == "XDeviceEmulator"; 
}

Alternativ finden Sie im Blog von Dragos Manolescu (bit.ly/h72vXj) eine weitere Möglichkeit, um Windows Phone 7-Ereignisströme mit Reactive Extensions (oder Rx) zu täuschen. 

Reale Anwendungen und Leistung

Wenn Sie eine Anwendung entwickeln und diese verkaufen möchten, sollte sie für Benutzer ansprechend gestaltet sein. Benutzer wollen eine schnelle Anwendung mit tollen Features. Das vorige Beispiel hat gezeigt, dass Sie einige Webdienstmethoden aufrufen und ein paar asynchrone Ereignisse berücksichtigen müssen, bevor Sie dem Benutzer Ergebnisse präsentieren können. Denken Sie auch daran, dass Ihre Anwendung auf einem Mobilgerät ausgeführt wird, und dass nicht immer eine WLAN-Verbindung verfügbar ist.

Die Webdienstaufrufe zu vermindern und über WLAN übertragene Datenmengen zu reduzieren, könnte die Anwendung schneller machen. In der Einführung habe ich eine Restaurant-Anwendung erwähnt, mit der das Menü und standortbasierte Features angeboten werden. Beim Entwickeln einer solchen Anwendung könnte ein Dienst in der Cloud ausgeführt werden, der sowohl Menüs als auch Angebote für das Mobiltelefon bereitstellt. Warum nutzen wir nicht diesen Dienst für die komplexen Berechnungen, anstatt sie vom Mobiltelefon ausführen zu lassen? Im Folgenden finden Sie ein Beispiel:

[ServiceContract] 
public interface IRestaurantLocator 
{ 
  [OperationContract] 
  NearResult GetNear(Location location); 
}

Sie könnten einen Dienst entwickeln, der mit dem aktuellen Standort des Benutzers arbeitet. Vom Dienst werden einige Threads gestartet (im Beispiel hier „Parallel.ForEach“), und gleichzeitig wird die Distanz zwischen diesem Standort und anderen Restaurants berechnet (siehe Abbildung 11).

Abbildung 11 Berechnen der Distanz zwischen Benutzerstandort und drei nahegelegenen Restaurants

public NearResult GetNear(BingRoute.Location location) 
{ 
  var near = new NearResult(); 
  near.Restaurants = new List<RestaurantResult>(); 
  
  ... 
  
  Parallel.ForEach(restaurants, (resto) => 
  { 
    try 
    { 
      // Build geo request. 
      var geoRequest = new BingGeo.GeocodeRequest(); 
      ... 
  
      // Get the restaurant's location. 
      var geoResponse = geoClient.Geocode(geoRequest); 
  
      // Restaurant position. 
      if (geoResponse.Results.Any()) 
      { 
        var restoLocation = 
          geoResponse.Results.FirstOrDefault().Locations.FirstOrDefault(); 
        if (restoLocation != null) 
        { 
          // Build route request. 
          var fromWaypoint = new Waypoint(); 
          fromWaypoint.Description = "Current Position"; 
          ...; 
  
          var toWaypoint = new Waypoint(); 
          ... 
  
          // Create the request. 
          var routeRequest = new RouteRequest(); 
          routeRequest.Waypoints = new Waypoint[2]; 
          routeRequest.Waypoints[0] = fromWaypoint; 
          routeRequest.Waypoints[1] = toWaypoint; 
          ... 
  
          // Execute the request. 
          var routeClient = new RouteServiceClient(); 
          var routeResponse = routeClient.CalculateRoute(routeRequest); 
  
          // Add the result to the result list. 
          if (routeResponse.Result != null) 
          { 
            var result = new RestaurantResult(); 
            result.Name = resto.Name; 
            result.Distance = routeResponse.Result.Summary.Distance; 
            result.TotalSeconds = routeResponse.Result.Summary.TimeInSeconds;
            results.Add(result); 
          } 
        } 
      } 
    } 
    catch (Exception ex) 
    { 
      // Take appropriate measures to log the error and/or show it to the end user. 
    } 
  }); 
  

  // Get the top 3 restaurants.
  int i = 1;
  var topRestaurants = results.OrderBy(o => o.TotalSeconds)
                       .Take(3)
                       .Select(o => { o.Index = i++; return o; });
 
  // Done.
  near.Restaurants.AddRange(topRestaurants);
  return near;
 
}

Jedes Restaurant in der Restaurantliste wird parallel durchlaufen, und die Standorte der einzelnen Restaurants werden mit „GeocodeServiceClient“ in einen geografischen Standort konvertiert. Unter Verwendung dieses Standorts und des Benutzerstandorts wird die Route zwischen diesen Punkten mithilfe von „RouteServiceClient“ berechnet. Abschließend wird die Eigenschaft „TotalSeconds“ der Routenübersicht genutzt, um die drei nächstgelegenen Restaurants zu ermitteln, die dann an das Gerät gesendet werden.

Der Vorteil liegt darin, dass die Berechnungen zeitgleich ausgeführt werden (mit „Parallel.ForEach“ und in Abhängigkeit von den Ressourcen des Computers). Nach Abschluss der Berechnungen werden dann nur die wichtigen Daten an Windows Phone übermittelt. Dieser Unterschied macht sich in der Leistung bemerkbar: Die Anwendung des Mobiltelefons ruft nur eine einzige Webdienstmethode auf, und nur sehr geringe Datenmengen werden per WLAN übertragen.

Zudem werden Code und asynchrone Aufrufe in Windows Phone 7 massiv reduziert, wie folgendes Beispiel zeigt:

var client = new RestaurantLocatorClient(); 
client.GetNearCompleted += new EventHandler<
  GetNearCompletedEventArgs>(OnGetNearComplete); 
client.GetNearAsync(location);

In Abbildung 12 wird die Anzeige der nahegelegenen Restaurants im Display des Mobiltelefons dargestellt.

The Phone Display of Three Nearby Restaurants

Abbildung 12 Die Anzeige drei nahegelegener Restaurants im Display des Mobiltelefons

Marketplace-Submission

Zum Schluss möchte ich auf das Marketplace-Submissionsverfahren von Windows Phone 7 eingehen. Die Anwendung muss eine Reihe von Anforderungen erfüllen, um im Marketplace zugelassen zu werden. Eine dieser Anforderungen besteht darin, die Funktionen Ihrer Anwendung in der Anwendungsmanifestdatei zu definieren. Entscheiden Sie sich für die Verwendung von „GeoCoordinateWatcher“, ist zudem eine Definition der Funktion ID_CAP_LOCATION in der Anwendungsmanifestdatei erforderlich.

In der MSDN-Bibliothek wird auf der Seite mit dem Thema „Gewusst wie: Einsetzen des Funktionserkennungstools für Windows Phone“ (bit.ly/hp7fjG) erklärt, wie Sie mithilfe des Tools für die Funktionserkennung alle von Ihrer Anwendung genutzten Funktionen erkennen. Lesen Sie diesen Artikel, bevor Sie Ihre Anwendung einreichen!

Beispielanwendung

Der Codedownload für diese Anwendung umfasst eine Lösung mit zwei Projekten. Das erste ist eine Klassenbibliothek mit Steuerelementen, Erweiterungsmethoden und Formatierungen, die Sie problemlos in Ihre eigenen Projekte einbinden können. Das zweite ist eine Windows Phone 7-Pivotanwendung, in der alle Beispiele im Rahmen einer kleinen Anwendung kombiniert werden.

Sandrino Di Mattia zählt zu den Microsoft-Spezialisten. In seinem Job als technischer Berater bei RealDolmen sorgt er durch die Integration von Microsoft-Technologien und -Produkten für Lösungen, die Kunden und ihre Unternehmen zufriedenstellen. In seiner Freizeit engagiert er sich zudem in belgischen Benutzergruppen und unterhält einen Blog unter blog.sandrinodimattia.net.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Dragos Manolescu