Download the Code Sample
Microsoft Windows Phone 7 comes with an easy-to-use geolocation API that allows you to determine the current position and movement of a user, expressed in latitude and longitude (and sometimes also the altitude). Once you have access to this data, you’re ready to create location-aware features in your Windows Phone 7 application.
If you’re building an application for a hamburger restaurant, it would be great if—besides presenting the menu and promotions—you could also locate the nearest restaurant based on a user’s current location. Another great feature would be the ability to find people near you. Think about scenarios for salespeople who want to see if they can visit their clients in-between meetings.
This article will focus on how to bring this data to a Windows Phone 7 app and how to visualize routes and locations in different ways. The actual data comes from the Bing Maps API. So before I can show you some advanced concepts, it’s important to take a look at the basics of the Bing Maps API.
The first thing you need is a working Bing account. On the Bing Maps Account Center (bingmapsportal.com), you need to use a Windows Live ID to sign up for a Bing Maps account. After creating your account, you’ll have access to your account details, where it will be possible to create a key. Because you’ll be building an application for Windows Phone 7, you’re allowed to write something like http://localhost for the Application URL.
Besides creating an account, this page also allows you to monitor the usage for the Bing Maps API. Should you decide to use Bing Maps in a production application, you’ll also need to come back to this page to contact someone regarding licensing.
The Bing Maps API actually provides a few services, and the Windows Phone 7 application will be consuming the SOAP services. I’ll run through short overviews of the following services:
The Geocode service allows you to work with coordinates and addresses; the Imagery service will let you work with actual images (aerial, bird’s-eye and road); the Route service will help you calculate the route between two or more points; and the Search service allows you to look for locations based on human input (such as “restaurant in Brussels”).
To make use of these services, you only need to add a “Service Reference” to one of the previous URLs. Note that this action will create or update the ServiceReferences.ClientConfig file. Figure 1 shows an example of how you can invoke the Geocode method of the Geocode service.
Figure 1 Invoking the Geocode Method of the Geocode Service
// Create the request. var geoRequest = new GeocodeRequest(); geoRequest.Credentials = new Credentials(); geoRequest.Credentials.ApplicationId = "" ; 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."; };
Each service method is based on request/response. You create a request object where you prepare the question for the server and configure the API key. In this case, I created a GeocodeRequest and I’ll be asking the server to provide me with the GeocodeLocation of “Brussels, Belgium.” After creating the request, I just invoke the client in an asynchronous way. And finally, you’ll always receive a response that will provide you with information—and in case of problems, will also find the error. Run the sample application in the accompanying download to view the GeocodeServiceClient in action and see how the location (or error) is displayed on-screen using data binding.
The application will be using the Geocode and the Route services to calculate the route between two addresses and show them to the user.
Using the Bing Route service, you can calculate the route from point A to point B. Just like the previous example, this works with a request/response. You’ll need to find the actual geolocation of each address first (using a GeocodeRequest), and using these locations you’ll be able to create a RouteRequest. The sample application in the accompanying download contains all the code, but Figure 2 shows a short example of how this is done.
Figure 2 Creating a RouteRequest
// Create the request. var routeRequest = new RouteRequest(); routeRequest.Credentials = new Credentials(); routeRequest.Credentials.ApplicationId = "" ; routeRequest.Waypoints = new ObservableCollection(); 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(OnRouteComplete); routeClient.CalculateRouteAsync(routeRequest);
Note that the Waypoints property of the request is a collection that allows you to add multiple waypoints. This can be interesting when you need to know the route for a complete itinerary instead of just from point A to point B.
When you execute the CalculateRouteAsync method, the service will start doing the hard work: calculating the route; listing all itinerary items (actions such as turning, taking an exit and so on); calculating duration and distance; listing all points (geolocations) and more. Figure 3 shows an overview of some important data present in the RouteResponse.
Figure 3 RouteResponse Content
In my first example, I’ll be using the RoutePath Points to show the route on a map. Because the Windows Phone 7 Toolkit already includes the Bing Maps control, you’ll only need to add a reference to the assembly Microsoft.Phone.Controls.Maps. After that it’s easy to display the map on the phone. Here’s an example of how to display the map showing Brussels (the CredentialsProvider is required to set the API key):
"50.851041,4.361572" ZoomLevel="10" CredentialsProvider="{StaticResource MapCredentials}" />
If you intend to use any of the controls in the Maps assembly, I advise you to add a reference to this assembly before adding a service reference to the Bing services. The service reference will then reuse types such as Microsoft.Phone.Controls.Maps.Platform.Location instead of creating new types, and you won’t need to write conversion methods or value converters to use some of the data returned by the service.
So now you know how to calculate the route between two points and how to display a map on the phone. Let’s bring these two techniques together to visualize the route on the map. The Points in the RoutePath will be used to draw on the map. The Map control allows you to add shapes such as a Pushpin (to show the start and end of the route, for example) and a MapPolyline (to draw the route based on GeoCoordinates).
Because the points returned by the service aren’t of the same type as the points used by the Maps control, I’ve created two small extension methods to convert the points to the correct type, shown inFigure 4.
Figure 4 Extension Methods to Convert Points to Correct Type
public static GeoCoordinate ToCoordinate(this Location routeLocation) { return new GeoCoordinate(routeLocation.Latitude, routeLocation.Longitude); } public static LocationCollection ToCoordinates(this IEnumerable points){ var locations = new LocationCollection(); if (points != null) { foreach (var point in points) { locations.Add(point.ToCoordinate()); } } return locations; }
You can use these extension methods on the RouteResponse when the CalculateRoute method completes. After conversion, these extension methods will return types that can, for example, be used for binding to the Bing Maps control. Because this is a Silverlight application, we should use an IValueConverter to do the actual conversion. Figure 5 shows an example of the value converter that will convert the Locations to GeoCoordinates.
Figure 5 Using an IValueConverter
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) { return (value as IEnumerable).ToCoordinates(); } else { return null; } }}
Now it’s time to configure the data binding. The data binding will use the converters, so it’s important to declare those first. These can be declared in the page resources or in the application resources (if you plan to reuse the converters), as shown here:
"locationConverter" /> "itineraryConverter" />
After declaring the converters you can add the maps control and other overlay controls (like the MapPolyline and Pushpin) and bind them to the required properties, as shown here:
"50.851041,4.361572" ZoomLevel="10" CredentialsProvider="{StaticResource MapCredentials}"> "#FF0000FF" StrokeThickness="5" /> " Content="Start" /> " Content="End" />
As you can see, these bindings use the converters that were declared previously to convert the data into a format that’s understood by the maps control. Finally, you need to set these properties after the CalculateMethod completes, as shown here:
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; }}
Figure 6 shows the screen after launching the application and calculating the route.
Figure 6 Visual Representation of the Route
As you can see, displaying the route on a map is pretty standard stuff. In the next example, I’ll show how you can build a custom control that displays the directions from start to end using the text and summary of each ItineraryItem. Figure 7 shows the final result.
Figure 7 Displaying Start-to-End Directions
In Figure 1, you see that Legs is one of the RouteResult properties. The Legs property contains one or more “Leg” objects, each of which includes a collection of ItineraryItems. Using the ItineraryItems, it will be possible to fill up the control that you can see in Figure 7. Each line in Figure 7 shows an ItineraryItem with the total seconds and total distance for that step, and the index of the current step. The ItineraryItem doesn’t keep track of the current step count, so I created a small class called ItineraryItemDisplay:
public class ItineraryItemDisplay { public int Index { get; set; } public long TotalSeconds { get; set; } public string Text { get; set; } public double Distance { get; set; }}
The sample code in the accompanying download also contains an extension method with the following signature:
public static ObservableCollection ToDisplay(this ObservableCollection items)
The code in this method loops through all items, writes the important values to a new ItineraryItemDisplay object and also keeps track of the current step count in the Index property. Finally, the ItineraryItemDisplayConverter takes care of the conversion during the data binding. As you might have noticed in Figure 7, each itinerary step is nicely formatted (cities and streets are marked in bold) using a custom control called ItineraryItemBlock. Its only goal is to format the ItineraryItem text in a clean way. InFigure 7, you can also see a blue block with some extra information, but this is regular data binding:
[TemplatePart(Name = "ItemTextBlock", Type = typeof(TextBlock))] public class ItineraryItemBlock : Control
The TemplatePart attribute defines an element that should be present in the control Template, and also what type that element should be. In this case, it should be a TextBlock called ItemTextBlock:
The reason for choosing a TextBlock is obvious. Using the TextBlock Inlines property, you can add content to the TextBlock in code. The OnApplyMethod can be overriden in a custom control, and this is when you’ll want to get ahold of the ItemTextBlock (see Figure 8).
Figure 8 Finding the TextBlock
/// /// When the template is applied, find the textblock. /// 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); }
The ItineraryItem Text property will be dissected and used to fill up this TextBlock with some extra formatting. Actually, this is easy to do because the Text property contains little more than regular text. Some parts are surrounded with XML tags:
Turn left onto Guido Gezellestraat
Because I only want to highlight cities and road names, I wrote a small method that strips tags such as VirtualEarth:Action or VirtualEarth:TurnDir. After retrieving the TextBlock, the method SetItinerary is called, and this is where the Inlines are added to the TextBlock (see Figure 9).
Figure 9 Adding Inlines to TextBlock with the SetItinerary Method
// Read the input string dummyXml = String.Format( "{0} ", 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 = ""; } } }
As you can see in the preceeding XML text example, not every part of the text is contained in an XML element. To be able to use this text in an XmlReader, the first thing to do is to wrap it in a dummy XML element. This allows us to create a new XmlReader with this text. Using the XmlReader Read method, you can loop each part of the XML string.
Based on the NodeType, you can figure the current position in the XML string. For example, consider the following element:
In any other case, you don’t need formatting, and this is where you’ll want to add a normal Run object containing only text without any formatting properties set.
That’s it for the custom control. The whole ItineraryItemDisplay record is displayed using a custom DataTemplate for the ListBox. This DataTemplate also contains a reference to the custom control (seeFigure 10).
Figure 10 The Entire ItineraryItemDisplay Record in a Custom DataTemplate for the Listbox
"ItineraryItemComplete"> "173" Margin="12,0,12,12"> "Left" Width="75"> "25*" /> "25*" /> "25*" /> "25*" /> "50*"> "20*"> "20*"> "4" Grid.RowSpan="3" Fill="#FF0189B4" /> "{Binding Index}" Style="{StaticResource ItineraryItemMetadata}" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" /> "{StaticResource ItineraryItemMetadata}" FontSize="{StaticResource PhoneFontSizeSmall}" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" /> "{StaticResource ItineraryItemMetadata}" FontSize="{StaticResource PhoneFontSizeSmall}" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" /> "84,-4,0,0" VerticalAlignment="Top" > "{Binding Text}" Style="{StaticResource ItineraryItemBlock}" FontSize="{StaticResource PhoneFontSizeLarge}" Foreground="{StaticResource PhoneForegroundBrush}" Padding="0,3,0,0" Margin="0,0,0,5" />
Now that the custom control and styling are ready, the only task left to do is to implement this in the Pivot control and in the code. As I mentioned before, the custom control and DataTemplate will be used in a ListBox:
"Directions"> "{Binding Itinerary, Converter={StaticResource itineraryConverter}}" Grid.RowSpan="2" ItemTemplate="{StaticResource ItineraryItemComplete}" />
This ListBox ItemSource is bound to the Itinerary property, and this is how the property is populated; afterward, the ItineraryItemDisplayConverter does the rest. As you can see, using a custom control and a bit of styling, you can take the itinerary data from the Route service and make it appealing to the user:
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; }}
In the previous examples, you learned how to use the Geocode and Route services to get directions from point A to point B and how to visualize these directions. Now it’s time to look at the geolocation API.
GeoCoordinateWatcher is the class you’ll use to find out the current GPS coordinates:
coordinateWatcher = new GeoCoordinateWatcher (GeoPositionAccuracy.High); coordinateWatcher.StatusChanged += new EventHandler(OnCoordinateUpdate); coordinateWatcher.Start();
The GeoCoordinateWatcher will go through different stages after executing the Start method, but when the status is set to Ready, you’ll have access to the current position. Best practice is to call the Stop method after you’re done working with the GeoCoordinateWatcher:
private void OnCoordinateStatusChanged(object sender, GeoPositionStatusChangedEventArgs e) { if (e.Status == GeoPositionStatus.Ready) { coordinateWatcher.Stop(); // Get position. fromLocation = coordinateWatcher.Position.Location; LocationLoaded(); } }
Now the sample application also provides location-aware features. The GeoCoordinateWatcher also exposes a PositionChanged event that allows you to monitor when the position changes. If you’re building an application that displays directions, you could use the position changes to scroll through each step automatically and even play a sound based on the VirtualEarth:Action in the ItineraryItem Text. You’ll end up having an actual GPS navigation application.
Are you debugging the application using the Windows Phone 7 emulator? If you’re testing your application’s geolocalization functionalities, you might bump into a small problem with the GeoCoordinateWatcher Status: It will always stay on NoData and never change to Ready. That’s why it’s important to write your code against the interface (IGeoPositionWatcher
The EventListGeoLocationMock class accepts a collection of GeoCoordinateEventMocks that should simulate the user’s coordinates in time. This will allow you to test the user’s location and movement:
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 coordinateWatcher = new EventListGeoLocationMock(events); coordinateWatcher.StatusChanged += new EventHandler(...); coordinateWatcher.Start();
Based on the device name, you could determine if the application is running on a real device or on the emulator to decide which IGeoPositionWatcher to use. Look for the extended property “DeviceName,” which is always set to XDeviceEmulator when running the application in the emulator:
private static bool IsEmulator() { return (Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceName") as string) == "XDeviceEmulator"; }
Alternatively, on Dragos Manolescu’s blog (bit.ly/h72vXj), you can find another way to mock Windows Phone 7 event streams using Reactive Extensions, or Rx.
When you’re building an application and you want it to sell, obviously it should be appealing to the user. The user will want a fast application that has great features. The previous examples showed that you’ll need to call a few Web service methods and handle a few asynchronous events before you can show some results to the user. Don’t forget that your application is running on a mobile device and a regular Wi-Fi connection isn’t always available.
Reducing the Web service calls and data going over the wire could speed up your application. In the introduction, I talked about a restaurant application that provides the menu and location-aware features. If you’re building such an application, there could be a service running in a cloud that provides menus and promotions to the phone. Why not use this service to perform the complex calculations instead of running them on the phone? Here’s an example:
[ServiceContract] public interface IRestaurantLocator { [OperationContract] NearResult GetNear(Location location); }
You could build a service that uses the user’s current location. The service will start some threads (the example uses Parallel.ForEach) and calculate the distance between this location and other restaurants simultaneously (see Figure 11).
Figure 11 Calculating Distance Between a User’s Location and Three Nearby Restaurants
public NearResult GetNear(BingRoute.Location location) { var near = new NearResult(); near.Restaurants = new List(); ... 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; }
Looping through each restaurant in the restaurants list in parallel, the location of each restaurant will be converted to a geolocation using the GeocodeServiceClient. Using this location and the user’s location, the route is calculated between these points using the RouteServiceClient. Finally, the TotalSeconds property of the route summary is used to find the three nearest restaurants, and those are sent to the device.
The advantage here is that calculations are run at the same time (using Parallel.ForEach and depending on the machine’s resources), and once they’re done, only the relevant data goes to the Windows Phone. Performance-wise, you’ll feel the difference; the mobile application will only call a single Web service method and only a little data goes over the wire.
Besides that, the code and asynchronous calls on the Windows Phone 7 are reduced dramatically, as demonstrated here:
var client = new RestaurantLocatorClient(); client.GetNearCompleted += new EventHandler< GetNearCompletedEventArgs>(OnGetNearComplete); client.GetNearAsync(location);
Figure 12 shows the display of nearby restaurants on the phone.
Figure 12 The Phone Display of Three Nearby Restaurants
The last thing I want to mention is the Windows Phone 7 Marketplace submission process. Your application needs to pass a set of requirements to be allowed on the Marketplace. One of these requirements is defining the capabilities of your application in the application manifest file. If you decide to use the GeoCoordinateWatcher, you’ll also need to define the ID_CAP_LOCATION capability in the application manifest file.
The MSDN Library page, “How to: Use the Capability Detection Tool for Windows Phone” (bit.ly/hp7fjG), explains how to use the Capability Detection Tool to detect all capabilities used by your application. Be sure to read through the article before you submit your application!
The code download for this application contains a solution with two projects. One is a class library that contains the control, extension methods and styles that you can easily integrate into your own projects. The second is a sample Windows Phone 7 Pivot application that combines all the examples in a small application.
Sandrino Di Mattia is a Microsoft enthusiast. In his job as technical consultant at RealDolmen, he integrates Microsoft technologies and products to create solutions that work for customers and their businesses. In his spare time, he participates in Belgian user groups and writes articles on his blog, blog.sandrinodimattia.net.
Thanks to the following technical expert for reviewing this article: Dragos Manolescu
http://msdn.microsoft.com/en-us/magazine/hh148148.aspx