Contents
This application has a web version on www.bikeincity.com
Update
Since I published this article, I continued working on the application and I've developed a similar application which is now in the process of the market place certification. To pass the certification, you have to correctly implement the "Tombstoning". I added one chapter to this article describing the tombstoning process for this application. If you are interested in just, that you can proceed directly to the Tombstoning chapter.
Introduction
I have a chance to live in Paris this year, which is a great city with a cool bike sharing system called Velib. There are more than 1000 bike stations, where you can just pick up a bike and then return it at some other station when your journey is finished. I was wondering if I could actually get the information about the stations and visualize it using the Bing Maps control. After some research, I found that another French city - Rennes - also possesses a bike sharing system and it has a free developer API to obtain such data. So I said to myself, it's not Paris, but why not. I decided to build an application for Windows Phone 7 which would allow the user to perform the following:
- Find the nearest stations using the GPS of the telephone and get information about them (number of free bikes, number of places to put the bike).
- When user selects a station, he can also compute directions to other stations near the entered destination address.
Background
I got the idea of building this application about two months ago; however, at that time, I was thinking only about a web app using the Bing Maps API. When I was in the middle of coding this web application, Windows Phone 7 was announced and so I said to myself that as soon as I finish the web app, I will port it to the mobile.
You can take a look at the web app on this site. Just note that it was left in the middle of the development so it will not work reliably. Well, the idea of porting the application very fast to mobile was quickly forgotten because I have found that it is just not that easy on the phone.
First - the namespaces for the Bing Maps API and Phone Maps are not the same. So for example, the Location
class is presented in both namespaces but is not the same, thus you have to change your model classes.
Second - The phone is different. The screen is small, so it took me some time to move things around to fit them all on the screen.
In the future, I am planning to consolidate these two applications so that they could share as much code as possible.
This article is not a complete walkthrough - it is not a step by step article on how to build the application, however I will try to give as much detailed description of the application as I can. I hope that after reading this, you will be able to understand how the application was built and how it works.
I hope to provide you with some useful information about Bing Maps, Windows Phone 7, and Silverlight in general. There are no exact prerequisites, but I assume that you are familiar with C# and that you know the basics of Silverlight and WPF.
Architecture
I tried to keep the application as simple as possible so it is composed of just a few classes. The main class is the MainPage
class which contains all the data which is visualized. It uses the data model described below (just two classes). To talk to the Web Services, MainPage
makes use of the ServiceCaller
class. The ServiceCaller
provides methods to access web services and events which get fired when the results have been obtained and processed. GeoCoordinateSimulator
emulates the GPS device API and fires an event when the position of the device has changed. Here is a simple schema of the architecture:
The application contains several other classes which are not important from the architectural point of view.
Data model
In this part, I will describe the model behind the application - classes which will contain the information needed to be visualized on the map. There are just two classes: BikeStation
and BikeRoute
. Both of these classes, like the whole project, make use of the GeoCoordinate
class from the System.Device.Location
namespace. This class represents a location defined by its latitude and longitude. This class also contains some other properties like Course
and Speed
which are not used in this application. To store the location, we can use the Location
class from the Microsoft.Phone.Controls.Maps
namespace. This class does not contain these additional information and is part of the Bing Maps API. There is an automatic conversion from Location
to GeoCoordinate
but not the other way around.
BikeStation
holds all the information about a bike rental place. This is basically the address, location, number of total slots, and number of free bikes. There is also a ObservavleCollection<bikeroute> Routes
property which holds a collection of routes - these routes are loaded when the user searches for directions from the selected station to other stations near the destination address.
public class BikeStation : INotifyPropertyChanged
{
private GeoCoordinate _location;
private int _free;
private bool _isSelected;
private int _walkDistance;
private int _id;
private string _address;
private int _total;
private ObservableCollection<bikeroute> _routes;
...
public GeoCoordinate Location
{
get
{
return _location;
}
set
{
_location = value;
OnPropertyChanged("Location");
}
}
}
BikeStation
also contains a property called WalkDistance
which is the straight distance of the station to the current user location. This distance is computed using the Haversine formula described later.
The BikeRoute
class simply represents a route between two bike stations. The most important property here is the Locations
property, of type LocationCollection
. This is, as the name says, a collection of Location
s that are later used to draw the route on the map.
public class BikeRoute : INotifyPropertyChanged
{
private BikeStation _to;
private BikeStation _from;
private double _distance;
private int _time;
private LocationCollection _locations;
private double _opacity;
private bool _isSelected;
private int _totalTime;
public LocationCollection Locations
{
get {
if (_locations == null)
{
_locations = new LocationCollection();
}
return _locations;
}
set {
_locations = value;
OnPropertyChanged("Locations");
}
}
}
To keep it simple, we can maintain all the data which should be visualized in the MainPage
, which is the main class, and the Phone
page to which the user navigates to after the start of the application.
public class MainPage:PhoneApplicationPage, INotifyPropertyChanged{
private BikeStation[] _stations;
public ObservableCollection<bikestation> DepartureStations
{
get
{
if (_departureStations == null)
{
_departureStations = new ObservableCollection<bikestation>();
}
return _departureStations;
}
set
{
_departureStations = value;
OnPropertyChanged("DepartureStations");
}
}
public ObservableCollection<bikestation> ArrivalStations {...}
public GeoCoordinate Departure
{
get {
return _from;
}
set
{
_from = value;
OnPropertyChanged("From");
}
}
public GeoCoordinate Arrival
public BikeRoute CurrentRoute {...}
public BikeStation CurrentStation {...}
}
To summarize, we have two ObservableCollection
s which store the departure and arrival stations. The arrival station's collection is filled only when the user searches a route. When the user searches for the nearest stations to his actual position, then these stations are stored in the DepartureStations
collection.
The Departure
and Arrival
properties of type GeoCoordinate
serve to visualize on the map the current position and the location of the destination.
CurrentRoute
and CurrentStation
are just properties which hold the station selected by clicking on the station pushpin or the route selected from the route list.
The private array of BikeStations _station
is the collection off all the stations in the city. This collection is queried when the user asks for stations near his location.
The MainPage
class implements INotifyPropertyChanged
to let the UI know when some of the properties have changed.
Using and emulating GPS
To get the current location of the user, we will make use of the phone's GPS; to do so, we use the GeoCoordinateWatcher
class. This class is an API to the phone GPS, and contains a property Position
of type GeoPosition<geocoordinate>
which is in fact the current Location
with a TimeStamp
. Also it provides PositionChanged
event, which is fired when the device changes its position. It is possible specify the accuracy of received location information in the constructor of GeoCoordinateWatcher
. There are two possibilities:
GeoPositionAccuracy.Default
- This option lets the native framework decide which source of location data should be used (WiFi, GSM Cell information, GPS) to optimize the power consumption.
GeoPositionAccuracy.High
- This option will force the GeoCoordinateWatcher
to always use the GPS, which is the most power consuming, but most accurate.
To control how often the PositionChanged
event will fire, we can set the MovementThreshold
property (in meters), which denotes the level of position change which will lead to evocation of the event. Here is the code which initializes the GeoCoordinateWatcher
and subscribes an event handler to the PositionChanged
event:
GeoCoordinateWatcher _watcher =
new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
_watcher.MovementThreshold = 20;
_watcher.PositionChanged+=
new EventHandler<geopositionchangedeventargs<geocoordinate>>(
_watcher_PositionChanged);
_watcher.Start();
GeoCoordinateWatcher
implements IDisposable
so we should call its Dispose
method when we are finished with it, or enclose it with using
directives.
_watcher.Stop();
_watcher.Dispose();
GPS in emulator
When running the application in the emulator, it is not possible to use the GeoCoordinateWatcher
; however, we can simulate the GPS device by defining our own class that implements IGeoPositionWatcher<t>
. GeoCoordinateWatcher
uses GeoCoordinate
as a template class to implement the IGeoPositionWatcher<t>
interface. That means that it uses GeoCoordinate
as the class which stores the location data. That's why the Position
property is of type GeoPosition<geocoordinate>
. We can define our own class which will implement IGeoPositionWatcher<geocoordinate>
and implement its methods and properties. My implementation suites only the case of this application - I have a city and I want to simulate the user's movement around the city. To achieve this, I have to know the borders of the city and then using the timer, simulate a change of position within the city borders every few seconds (or minutes). So let us start by defining a class called GeoCoordinateSimulator
which will implement the IGeoCoordinateWatcher<geocoordinate>
interface.
public class GeoCoordinateSimulator : IGeoPositionWatcher<geocoordinate>
{
private GeoCoordinate _leftCorner;
private GeoCoordinate _rightCorner;
private double _dLat;
private double _dLong;
private int _interval;
private GeoPosition<geocoordinate> _position;
private Timer _timer;
public Object _timerState;
}
The borders of the city will be determined by two GeoCoordinate
fields representing the lower left corner and upper right corner. The GeoCoordinate
object _position
will provide the current location of the simulator. To simulate the movement, I declare two double
variables which represent the change of the position in X (longitude) and Y (latitude) directions. As the last piece of the puzzle, we have a Timer
which will fire regularly on predefined intervals and will apply the changes to the current position and fire the PositionChanged
event. The constructor has three parameters, two of them to describe the corner points of the city and the third is the interval at which the timer should fire. In the constructor, besides some checks to assure that the corner points are in good order, the position is set to the middle of the city.
public GeoCoordinateSimulator(GeoCoordinate left, GeoCoordinate right, int interval)
{
...
double latRange = _rightCorner.Latitude - _leftCorner.Latitude;
double longRange = _rightCorner.Longitude - _leftCorner.Longitude;
_position = new GeoPosition<geocoordinate>(DateTime.Now,
new GeoCoordinate(_leftCorner.Latitude + latRange / 2,
_leftCorner.Longitude + longRange / 2));
_interval = interval;
}
The Start()
method just creates the timer and sets the callback. The callback method adds the values of the change in the latitude and longitude directions to the actual position. If the resulting point would be out of the borders of the city, it will randomly generate a new direction to stay in the city.
public void TimerCallBack(Object obj)
{
Random r = new Random();
double newLatitude, newLongitude;
while (!IsInRange(newLatitude = this.Position.Location.Latitude + _dLat,
newLongitude = this.Position.Location.Longitude + _dLong) ||
(_dLat==0.0 && _dLong==0.0))
{
_dLat = (r.NextDouble() - 0.5) * BikeConst.GPS_SIMULATOR_STEP;
_dLong = (r.NextDouble() - 0.5) * BikeConst.GPS_SIMULATOR_STEP;
}
_position = new GeoPosition<geocoordinate>(DateTime.Now,
new GeoCoordinate(newLatitude,newLongitude));
if (this.PositionChanged != null)
{
PositionChanged(this,
new GeoPositionChangedEventArgs<geocoordinate>(this.Position));
}
}
To obtain the new direction of movement, I generate a random value between -0.5 and 0.5 and then multiply it by a double
constant which represents the size of the change. Also note that you have to check both values of change being zero, because the position would never change. When the new position is set, then the PositionChanged
event will fire to let the observers know about the change. There is a call to the IsInRange()
method which just checks if the given point is still in the rectangle of the city.
public bool IsInRange(double lat,double lng)
{
return (lat > _leftCorner.Latitude && lng > _leftCorner.Longitude
&& lat < _rightCorner.Latitude && lng < _rightCorner.Longitude) ;
}
To use this simulator, we will just use GeoCoordinateSimulator
instead of the GeoPositionWatcher
class. It will throw us the PositionChanged
event as if we would use a real device which changes its position.
GeoCoordinate leftCorner = new GeoCoordinate(48.094133, -1.705112);
GeoCoordinate rightCorner = new GeoCoordinate(48.123018,-1.642971);
_watcher = new GeoCoordinateSimulator(leftCorner,
rightCorner, BikeConst.GPS_SIMULATOR_INTERVAL);
_watcher.Start();
Computing distance on the Earth's surface
In order to see which stations are closest to the current phone's location, we will have to compute the distance between two points defined by spherical coordinates. To do so, the Haversine formula is used which allows computation of the distance of two points on the surface of a sphere. More information can be found on the Wikipedia page and on this site. The computation is implemented in a static method ComputeDistance
in the class GeoMath
.
public static int ComputeDistance(Location start, Location end)
{
var R = 6371;
double lat1 = ToRad(start.Latitude);
double lat2 = ToRad(end.Latitude);
double lng1 = ToRad(start.Longitude);
double lng2 = ToRad(end.Longitude);
double dlng = lng2 - lng1;
double dlat = lat2 - lat1;
var a = Math.Pow(Math.Sin(dlat / 2),2) + Math.Cos(lat1) *
Math.Cos(lat2) * Math.Pow(Math.Sin(dlng/2),2);
var c = 2*Math.Asin(Math.Min(1,Math.Sqrt(a)));
var d = R * c;
return (int)(d * 1000);
}
Getting the data
This part describes how to call the Bing Maps Web Services to geocode addresses and obtain directions and how to call the Rennes city Web Services to obtain data from the bike system. The project contains a class called ServiceCaller
which provides access to all of the data stores. More specifically, it provides methods which asynchronously call the Web Services and return results to the MainPage
.
Getting information from the bike system
The data that I need is accessible via a Web Service provided by the Rennes city. To obtain access to this data, you have to register at http://data.keolis-rennes.com/ as a developer. On this address, you will also find the documentation of the REST API. After registering, you will obtain a developer's key which you will pass to the Web Service to obtain the data. Basically, to obtain any data from the system, you need to compose an HTTP GET request in the following form:
http://data.keolis-rennes.com/xml/?version=1.0&key=XXXXXXXXXXXXXXX&cmd=command
Here, the key
parameter will be your developer's key and cmd
will be the command describing your operation.
Getting a list of all stations
To get the list of all stations, the ServiceCaller
class contains a method GetAllStations()
. This method uses a WebClient
class which lets us asynchronously receive data identified by its URL. When the data is downloaded, ServiceCaller
will fire the StationsLoaded
event. Before we call for the data, we register a method which will be executed when the download process is completed.
public void GetAllStations()
{
WebClient ws = new WebClient();
string url = "http://data.keolis-rennes.com/xml/?" +
"version=1.0&key=key&cmd=getstation";
ws.DownloadStringCompleted +=
new DownloadStringCompletedEventHandler(StationsListRecieved);
ws.DownloadStringAsync(new Uri(url));
}
After invoking the Web Service with the "getstation" command, we will obtain the XML, which is not hard to parse using LINQ with the following structure.
="1.0" ="UTF-8" ="yes"
<opendata>
<request>http://data.keolis-rennes.com/xml/?
version=1.0&key=yourkey&cmd=getstation</request>
<answer>
<status code="0" message="OK"/>
<data>
<station>
<id>75</id>
<number>75</number>
<name>ZAC SAINT SULPICE</name>
<state>1</state>
<latitude>48.1321</latitude>
<longitude>-1.63528</longitude>
<slotsavailable>21</slotsavailable>
<bikesavailable>8</bikesavailable>
<pos>0</pos>
<district>Maurepas - Patton</district>
<lastupdate>2010-12-05T01:29:06+01:00</lastupdate>
</station>
<station>
<id>52</id>
<number>52</number>
<name>VILLEJEAN-UNIVERSITE</name>
<state>1</state>
<latitude>48.121075</latitude>
<longitude>-1.704122</longitude>
<slotsavailable>14</slotsavailable>
<bikesavailable>11</bikesavailable>
<pos>1</pos>
<district>Villejean-Beauregard</district>
<lastupdate>2010-12-05T01:29:06+01:00</lastupdate>
</station>
</data>
</answer>
</opendata>
When the data is downloaded, the StationsListRecieved
event handler executes. The data that you need is stored in the form of XML so we can use LINQ to XML to parse it and obtain an array of BikeStation
classes. We can check if there are any subscribers for the StationsLoaded
event, and we fire it giving it an argument of type StationsLoadedEventArgs
.
if (e.Result != null)
{
XDocument xDoc = XDocument.Parse(e.Result);
BikeStation[] result = null;
result = (from c in xDoc.Descendants("opendata").Descendants(
"answer").Descendants("data").Descendants("station")
select new BikeStation
{
Address = (string)c.Element("name").Value,
Id = Convert.ToInt16(c.Element("id").Value),
Location = new GeoCoordinate(
Convert.ToDouble(c.Element("latitude").Value),
Convert.ToDouble(c.Element("longitude").Value)),
Free = Convert.ToInt16(c.Element("bikesavailable").Value),
FreePlaces = Convert.ToInt16(c.Element("slotsavailable").Value)
}).ToArray();
if (this.StationsLoaded != null)
{
this.StationsLoaded(this, new StationsLoadedEventArgs(result));
}
}
StationsLoadedEventArgs
is a simple class deriving from EventArgs
and encapsulating the BikeStation[]
array.
public class StationsLoadedEventArgs:EventArgs
{
public BikeStation[] Stations { get; set; }
public StationsLoadedEventArgs(BikeStation[] stations)
{
this.Stations = stations;
}
}
In the constructor, the MainPage
class uses ServiceCaller
to get all the stations. When the collection is received, ServiceCaller
will fire its StationsLoaded
event and MainPage
will just assign this collection to its private _stations
collection, which is later queried to obtain the nearest stations.
Getting the details of a station
Here, the situation is a little bit different. We already have a BikeStation
object and we just want to update the information inside. Again, we will use the WebClient
to obtain the data, but before that, we will create a GUID for the BikeStation
and store it in a dictionary. Then we will call the DownloadStringAsync
method with two parameters, passing the GUID as the second parameter. This will cause that on the reception of the "completed" event, we can recuperate the GUID and assign the received values to the right BikeStation
object. The method GetStationInformation(BikeStation)
shows how to call the Web Service.
public void GetStationInformation(BikeStation station)
{
string url = String.Format("http://data.keolis-rennes.com/xml/" +
"?version=1.0&key={0}&cmd=getstation¶m[request]" +
"=number¶m[value]={1}",
BikeConst.RENNES_KEY,station.Id);
Guid stationGuid = Guid.NewGuid();
_stationsDict.Add(stationGuid, station);
WebClient webClient = new WebClient();
webClient.DownloadStringCompleted +=
new DownloadStringCompletedEventHandler(StationInformationReceived);
webClient.DownloadStringAsync(new Uri(url), stationGuid);
}
The URL to the Rennes data service differs from the one before. The "getstation" command can have a parameter "number" corresponding to the ID of the station.
The data that we will receive will be XML with the same structure as when calling for the list of stations (described above), but the XML will contain only one station.
When we receive the data, first we recuperate the GUID of the station. This arrives in the
UserState
parameter of the event arguments. Later, we parse the data again using LINQ to XML and we can update the selected station. After updating, we can remove the GUID from the dictionary.
if(e.Result!=null){
string xmlString = e.Result;
XDocument xDoc = XDocument.Parse(xmlString);
Guid stationGuid = (Guid)e.UserState;
BikeStation station = _stationsDict[stationGuid];
var stInfo = (from c in xDoc.Descendants("opendata").Descendants(
"answer").Descendants("data").Descendants("station")
select new BikeStation
{
Free = Convert.ToInt16(c.Element("bikesavailable").Value),
}).First();
station.Free = stInfo.Free;
_stationsDict.Remove(stationGuid);
}
Using Bing Maps Services
This part is well explained in the WP7 Developers Training Kit - so here is just a brief explanation and how I adapted it to my exact scenario. Note that in order to use Bing Maps Services and components, you need to register at the Bing Maps portal to obtain your developer key. Bing Maps API exposes several WCF services accessible over the internet. This application makes use of two of them: Geocode Service and Route Service.
To access each of these WCF services, you need to add to your project service a reference pointing to the right URL. The list of URLs of Bing Maps SOAP services can be found on this site. If you have any trouble getting access to the services, you can consult this page which is a general article describing how to develop a Silverlight application interacting with the Bing Maps SOAP services.
Geocoding an address
Here I can use code really similar to the one provided on MSDN. We create a new GeoCodeRequest
, to which we pass the address we wish to geocode as a query. When creating the GeocodeServiceClient
, we specify the endpoint of the service as the constructor. Here we can use standard HTTP binding, however there is also secure binding using SSL accessible.
public void GeocodeAddress(string address,State state)
{
if (address != String.Empty)
{
GeocodeRequest geocodeRequest = new GeocodeRequest();
geocodeRequest.Credentials = new Credentials();
geocodeRequest.Credentials.ApplicationId = _mapID;
geocodeRequest.Query = address;
GeocodeServiceClient geocodeService =
new GeocodeServiceClient("BasicHttpBinding_IGeocodeService");
geocodeService.GeocodeCompleted +=
new EventHandler<geocodecompletedeventargs>( GeocodeCompleted);
geocodeService.GeocodeAsync(geocodeRequest, state);
}
}
When we obtain the results from Bing Services, we will fire the BikePlaceGeocoded
event and we pass the geocoded GeoCoordinate
object as the argument of this event in order to deliver it to the MainPage
class.
void GeocodeCompleted(object sender, GeocodeCompletedEventArgs e)
{
if (e.Result.ResponseSummary.StatusCode ==
GeocodeService.ResponseStatusCode.Success)
{
if (e.Result.Results.Count > 0)
{
GeoCoordinate coordinate = e.Result.Results[0].Locations[0];
if (this.BikePlaceGeocoded != null)
{
BikePlaceGeocoded(this,
new AddressGeocodedEventArgs(coordinate,(State)e.UserState));
}
}
}
}
Just to complete your idea, here is the code for AddressGeocodedEventArgs
:
public class AddressGeocodedEventArgs : EventArgs
{
public GeoCoordinate Location {get;set;}
public AddressGeocodedEventArgs(GeoCoordinate c, State s)
{
this.Location = c;
this.StateType = s;
}
}
Calculating the Route
ServiceCaller
exposes a method CalculateRoute
which accepts BikeRoute
as its parameter. So here we assume that we already have a BikeRoute
object containing the starting and ending point and we want to calculate the route - obtain the exact directions and the total time.
public void CalculateRoute(BikeRoute route)
{
RouteServiceClient routeClient =
new RouteServiceClient("BasicHttpBinding_IRouteService");
routeClient.CalculateRouteCompleted +=
new EventHandler<calculateroutecompletedeventargs>(
CalculatedRoute_Completed);
RouteRequest routeRequest = new RouteRequest();
routeRequest.Options = new RouteOptions();
routeRequest.Options.Mode = TravelMode.Driving;
routeRequest.Options.Optimization = RouteOptimization.MinimizeDistance;
routeRequest.Credentials = new Credentials();
routeRequest.Credentials.ApplicationId = _mapID;
routeRequest.Waypoints = new ObservableCollection<waypoint>();
Waypoint from = new Waypoint();
from.Location = route.From.Location;
routeRequest.Waypoints.Add(from);
Waypoint to = new Waypoint();
to.Location = route.To.Location;
routeRequest.Waypoints.Add(to);
Guid routeGuid = Guid.NewGuid();
_routesDict.Add(routeGuid, route);
routeClient.CalculateRouteAsync(routeRequest, routeGuid);
}
In the RouteOptions
object, we specify that we want directions for driving and minimize the distance. That should give us good results for biking (well, even though sometimes we think that on the bike we can do anything, we should obey traffic rules). Then we will add two Waypoint
s to the RouteRequest
corresponding to the bike rental stations. As in the case of updating info for a BikeStation
, I store the BikeRoute
object in the dictionary and the key (GUID) I pass to the request.
void CalculatedRoute_Completed(object sender, CalculateRouteCompletedEventArgs e)
{
if ((e.Result.ResponseSummary.StatusCode ==
RouteService.ResponseStatusCode.Success))
{
Guid routeGuid = (Guid)e.UserState;
BikeRoute route = _routesDict[routeGuid];
foreach (Location p in e.Result.Result.RoutePath.Points)
{
route.Locations.Add(p);
}
route.Distance = e.Result.Result.Summary.Distance;
route.Time = (int)e.Result.Result.Summary.TimeInSeconds / 60 *
BikeConst.DRIVE_TO_BIKE;
_routesDict.Remove(routeGuid);
}
}
In the event handler, when the route is calculated, I recuperate the GUID to get the concerned route. The points which build the desired route are stored in the RoutePath.Point
collection of the Result
. We add them to the Locations
property of the BikeRoute
. As said before, this property is of a special type LocationCollection
, to which the MapPolyline
object can be bound - that comes in the following part.
Preparing the GUI
Before we start creating the GUI, it's good to know that there is a UI Design and Interaction Guide for Windows Phone 7, which gives us the color schemes. So there are Brushes which I use in the application and which I found in this document.
<SolidColorBrush x:Name="LimeBrush" Color="#8CBF26"/>
<SolidColorBrush x:Name="OrangeBrush" Color="#F09609"/>
Now the first thing we want to do is just put the map on the place. The map resides in the Microsoft.Phone.Controls.Maps
namespace so we have to add the declaration to the top of the XAML.
xmlns:map="clr-namespace:Microsoft.Phone.Controls.Maps;
assembly=Microsoft.Phone.Controls.Maps"
...
<map:Map x:Name="map" CredentialsProvider="{Binding CredentialsProvider}"
CopyrightVisibility="Collapsed"
LogoVisibility="Collapsed"
ZoomLevel="{Binding Zoom,Mode=TwoWay}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"></map>
You can see that the ZoomLevel
is bound to the Zoom
property which is exposed on the MainPage
class; the same counts for CredentialsProvider
. The CredentialsProvider
property should contain your Ming Maps developer key.
public double Zoom
{
get { return _zoom; }
set
{
var coercedZoom = Math.Max(MinZoomLevel, Math.Min(MaxZoomLevel, value));
if (_zoom != coercedZoom)
{
_zoom = value;
OnPropertyChanged("Zoom");
}
}
}
public CredentialsProvider CredentialsProvider
{
get { return _credentialsProvider; }
}
When setting the zoom level, we assure that it will be greater than the maximal and minimal levels which are stored in constants. I have decided to use the same buttons for zooming as the ones which are used in the official WP7 Training Kit and for one simple reason - they look better than anything I came up with, so I took them as a starting point and just simplified the styling a bit. So here is the zoom-in button.
<Button x:Name="ButtonZoomIn" Style="{StaticResource ButtonZoomInStyle}"
HorizontalAlignment="Left" VerticalAlignment="Top"
Height="56" Width="56" Margin="8,250,0,0"
Click="ButtonZoomIn_Click"/>
You can see that I am positioning the button on the map by setting the Margin
property. Also, it is visible that ButtonZoomInStyle
is applied to this button. This one is defined in a separate DefaultStyles.xaml file. I will describe the style here in a little detail because the same type of style is used for other buttons in the project.
<Style x:Key="ButtonZoomInStyle" TargetType="Button"
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid Background="Transparent" Width="48" Height="48">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Visibility"
Storyboard.TargetName="image">
<DiscreteObjectKeyFrame KeyTime="0"
Value="Visible"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Visibility"
Storyboard.TargetName="image1">
<DiscreteObjectKeyFrame KeyTime="0"
Value="Collapsed"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Image x:Name="image"
Source="/BikeInCity;component/Icons/Zoom/ZoomIn_White.png"
Stretch="Fill" Visibility="Collapsed"/>
<Image x:Name="image1"
Source="/BikeInCity;component/Icons/Zoom/ZoomIn_Black.png"
Stretch="Fill"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The style overrides the Template
property of the button. We are defining a new ControlTemplate
which contains a Grid
with two images inside (one overlying the other). We are using VisualState
s to set the Visibility
of the top picture to Collapsed
and thus show the image below when the user presses the button.
If you are not familiar with the concept of VisualState
s, you can look at them as declarations which describe how the component looks like in the exact state. VisualState
s are implemented only in Silverlight and work like Triggers and they can be seen as a replacement for Triggers from WPF. The basic idea is that the creator of a component will define several states and has to anticipate which state will be important for the user - so in some ways, we can say that the concept is less powerful than Triggers (where the template designer has more freedom and is not tied by a group of predefined states).
Here, inside the Grid
, we have a VisualStateManager
which contains a group of states "CommonStates
". This is a predefined group for the Button
component and contains four states: Normal
, MouseOver
, Pressed
, and Disabled
. I am interested only in the Normal
and Pressed
states. By leaving the Normal
state without further changes, I am declaring that I don't want any changes to look for the component in the state.
On the other hand, the Pressed
state contains a Storyboard
which provides a timeline for some animation that we might want to perform on the component. In this example, we will just use ObjectAnimationUsingKeyFrame
which allows us to perform changes on a property of a component at specific times. Here we are just saying when the Button
is clicked, set the Visibility
of the first image to Collapsed
.
To control the zoom level, we just add two handlers for the zoom button where we increment or decrement the current value of the Zoom
property.
Adding the address panel
This panel will be visible when the user wants to get directions to a different location.
<Border Background="{StaticResource LimeBrush}" Width="400" Height="100"
x:Name="DirectionsPanel" BorderThickness="2" BorderBrush="Black"
Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="To:" VerticalAlignment="Center" Margin="3,0,0,0"
FontSize="28" FontWeight="Bold" Foreground="Black"/>
<TextBox Name="txtAddressTo" Width="300" Text="71 Rue d'Inkermann"
FontSize="22" FontWeight="Bold" TextWrapping="Wrap"/>
<Button Name="bntSearch" Style="{StaticResource ButtonPlayStyle}"
Click="ComputeDirections_Click"/>
</StackPanel>
</Border>
That is nothing too complicated - a Border
component with a horizontally oriented StackPanel
. The style which is applied to the Button
is analogical to the one applied to the zoom buttons. You see that there is a callback assigned to this button - we will get to it later.
Adding the station panel
The panel which displays the station details is added to the same grid column as the map component - it is actually placed over the map component.
<Grid x:Name="StationPanel" Width="400"
MinHeight="40" Margin="0,10,0,0" VerticalAlignment="Top">
<Border Background="Black" BorderBrush="White"
BorderThickness="2" Opacity="0.8"/>
<StackPanel DataContext="{Binding CurrentStation}">
<Grid DataContext="{Binding}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30"/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Address}" Margin="3,0,0,3"
VerticalAlignment="Center" FontSize="28"
HorizontalAlignment="Center" Grid.ColumnSpan="5"/>
<StackPanel Orientation="Horizontal" Grid.Column="1"
Margin="6,1,6,0" Grid.Row="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="{Binding WalkDistance}"
VerticalAlignment="Center" FontSize="28"/>
<TextBlock Text=" m" FontSize="30" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Column="2"
Margin="6,1,6,0" Grid.Row="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
<Image Source="/Icons/Others/BicycleWhite.png"
Height="40" Width="45"/>
<TextBlock Text="{Binding Path=Free}"
VerticalAlignment="Center" FontSize="28"/>
</StackPanel>
<StackPanel Orientation="Horizontal"
Grid.Column="3" Margin="6,1,6,0" Grid.Row="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
<Image Source="/Icons/Others/HouseWhite.png"
Height="40" Width="45" />
<TextBlock Text="{Binding Path=FreePlaces}"
VerticalAlignment="Center" FontSize="28"/>
</StackPanel>
</Grid>
-->
<ListBox x:Name="RouteList" ItemsSource="{Binding Routes}"
ItemTemplate="{StaticResource RouteListTemplate}"
VerticalAlignment="Top" SelectionChanged="RouteList_SelectionChanged"
MaxHeight="120" ItemContainerStyle="{StaticResource ListItemStyle}"
Width="395" Margin="0,0,0,5"/>
</StackPanel>
</Grid>
The station panel is composed of a border with Opacity
set to 0.8 so the map underneath can be seen. Over this Border
, a StackPanel
is placed which has the DataContext
property bound CurrentStation
property of the MainPage
. There are two components on the StackPanel
: a Grid
with the information about the station and the list containing the routes from the station to the destination stations. All the textboxes in the Grid
are bound to the properties of the BikeStation
object in the CurrentStation
property.
The route list has its ItemsSource
bound to the Routes
property of the BikeStation
class. The list has ItemContainerStyle
set as well as ItemTemplateStyle
. The item container style sets the style of the container for each item. Here I have a simple style which just changes the color of the selected item.
<Style x:Key="ListItemStyle" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid x:Name="Container"
Background="{StaticResource LimeBrush}"
Margin="5,3,5,3">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SelectionStates">
<VisualState x:Name="Unselected"/>
<VisualState x:Name="Selected">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Background"
Storyboard.TargetName="Container">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource OrangeBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="SelectedUnfocused">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Background"
Storyboard.TargetName="Container">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource OrangeBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Unfocused"/>
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="Background"
Storyboard.TargetName="Container">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource OrangeBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
If you have read the part about styling the buttons, you can see that the concept is the same. For some of the states in which the item can be at a time, I am changing the background color of the container. There is only one difference in contradiction to styling the buttons. The buttons do not contain the ContentPresenter
tag, because there is no need to put anything inside of the button. However, here we have just styling for the container and the content of each list item will be different. It will be the ItemTemplate
which will be placed into the ContentPresenter
. The ItemTemplate
specifies the DataTemplate
for each list item.
<Grid Width="395" Background="Transparent" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="95"/>
<ColumnDefinition Width="25"/>
<ColumnDefinition Width="210"/>
<ColumnDefinition Width="65"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Image Source="/Icons/Others/ClockWhite.png/"
Height="30" Width="30"/>
<TextBlock Text="{Binding TotalTime}"/>
<TextBlock Text=" min"/>
</StackPanel>
<Image Source="/Icons/Others/NextWhite.png"
Height="25" Width="25" Grid.Column="1"/>
<TextBlock Text="{Binding Path=To.Address}" Grid.Column="2"/>
<StackPanel Grid.Column="3" Orientation="Horizontal">
<Image Source="/Icons/Others/HouseWhite.png"
Height="30" Width="30" />
<TextBlock Text="{Binding Path=To.FreePlaces}"/>
</StackPanel>
</Grid>
The application bar
In order to allow the user to perform some action, the easiest way is to use the Application Bar - the semi-transparent panel in the bottom of the phone's display. Its XAML declaration is actually already commented in the template provided by Visual Studio. You can put a maximum of four buttons directly to the panel, and you also have the possibility to put the menu items below these buttons. I have here just two buttons with the following functions:
- Button to get the directions (from one bike station to stations near the entered address).
- Button to get the nearest stations to the actual position provided by the phone's GPS.
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" Opacity="0.8">
<shell:ApplicationBarIconButton
IconUri="/Icons/ApplicationBar/Directions.png"
Text="Directions" Click="GetDirections_Click"/>
<shell:ApplicationBarIconButton
IconUri="/Icons/ApplicationBar/Location.png"
Text="Here" Click="ShowNearStations_Click"/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
Handling user's actions
This chapter generally describes the actions that are taken when the user presses one of the ApplicationBar buttons.
Getting the nearest stations
The ShowNearStations_Click
method which is the event handler for the first application bar button will just set the current position to the actual position of the GeoCoordinateWatcher
and call the ShowNearStations
method. This method takes as the parameter the current location.
this.DepartureStations = GetNearStations(location, BikeConst.ANGLE_DISTANCE);
this.map.SetView(location, BikeConst.ZOOM_DETAIL);
this.StationPanel.Visibility = System.Windows.Visibility.Visible;
if (this.CurrentStation != null)
{
this.CurrentStation.IsSelected = false;
}
if (this.DepartureStations.Count > 0)
{
this.CurrentStation = this.DepartureStations[0];
this.CurrentStation.IsSelected = true;
}
In this method, first we obtain the stations which are near the desired location, and then we zoom to the station by using the map's SetView
method. After that, we just assure that one of the stations will be selected to show the information about it. Let's observe now the GetNearStations
method which is in fact the most important one.
private ObservableCollection<bikestation>
GetNearStations(GeoCoordinate coordinate, double distance)
{
ObservableCollection<bikestation> collection =
new ObservableCollection<bikestation>();
if (this.Stations != null)
{
double lat = coordinate.Latitude;
double lng = coordinate.Longitude;
var stationList = from s in this.Stations
where (Math.Abs(s.Location.Latitude - lat) <
distance & Math.Abs(s.Location.Longitude - lng)
< distance)
select s;
foreach (BikeStation station in stationList)
{
station.WalkDistance =
GeoMath.ComputeDistance(station.Location, coordinate);
}
var result = stationList.Where(x => x.WalkDistance < 400);
foreach (BikeStation station in result)
{
collection.Add(station);
}
foreach (BikeStation station in collection)
{
_serviceCaller.GetStationInformation(station);
}
}
return collection;
}
Here we use LINQ to get the closest stations. We select stations of which latitude and longitude are not too "far away" from the actual location. This in fact will not give us stations in a circular distance but rather all stations in a square around the actual location.
To compute the exact distance of each station in this collection, we call the ComputeDistance
method which uses the spherical law of cosines to compute the distance.
When all the distances are computed, then the Where
method is used to reduce the collection only to those stations where the distance from the place is lower than 400 meters.
The reason to use LINQ here was to eliminate the number of stations for which I will have to compute the spherical distance, because it is a costly operation.
To summarize, the GetNearStations
method will fill the ObservableCollection
with the nearest stations. Later, we will bind the content of this collection to the map.
Getting the directions
Here we call the GeocodeAddress
of the ServiceCaller
classes which was described above in the chapter dedicated to "Getting the data". Because GeocodeAddress
is an asynchronous operation, we have to assign an event handler to perform the actions when the address has been geocoded.
void BikePlaceGeocoded(object sender, AddressGeocodedEventArgs e)
{
this.CurrentStation.Routes.Clear();
this.CurrentRoute = null;
this.Arrival = e.Location;
this.ArrivalStations =
GetNearStations(this.Arrival, BikeConst.ANGLE_DISTANCE);
foreach (BikeStation destination in this.ArrivalStations)
{
BikeStation[] list = { this.CurrentStation, destination };
BikeRoute route = new BikeRoute();
route.From = this.CurrentStation;
route.To = destination;
this.CurrentStation.Routes.Add(route);
_serviceCaller.CalculateRoute(route);
}
}
In the event handler, we will first clear the Routes
collection of the current stations, to erase any routes which might be there from previous searches, and also deselect the current route. In AddressGeocodedEventArgs
, we obtain the location of the station, which is assigned to the Arrival
property. We call the GetNearStations
method this time to obtain all the arrival stations.
Then a loop over all arrival stations creates a new BikeRoute
for each route from the actually selected station to the arrival station. In this loop, the CalculateRoute
method of ServiceCaller
is called which asks the Bing services to get the driving directions. When the directions are obtained, the BikeRoute
object will be altered.
Visualizing on map
Here we assume that we have all the data in the properties of the MainPage
class and we just have to show it on the map.
Visualizing the user location
Let's start by visualizing the current position and the destination position. To visualize a single point, the Pushpin
component is used.
<map:map>
<map:Pushpin Location="{Binding Departure}"
Style="{StaticResource PlaceMarkStyle}"/>
<map:Pushpin Location="{Binding Arrival}"
Style="{StaticResource PlaceMarkStyle}"/>
</map:map>
We have two Pushpin
s with Location
properties bounding the Departure
and Arrival
properties of the MainPage
class, both of which are of type GeoCoordinate
. Both of these components use the PlaceMarkStyle
.
<Style x:Key="PlaceMarkStyle" TargetType="map:Pushpin">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Canvas>
<Path Width="16" Height="15"
Canvas.Top="13" Stretch="Fill"
Stroke="#FF000000" Fill="#FF000000"
Data="F1 M 8,28 L 0,16L 16,16L 8,28 Z "/>
<Rectangle Width="6" Height="13"
Canvas.Left="5" Stretch="Fill"
Stroke="#FF000000" Fill="#FF000000"/>
<Canvas.RenderTransform>
<CompositeTransform TranslateX="-16"
TranslateY="-14"/>
</Canvas.RenderTransform>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The style overrides the control template from the standard to a little arrow pointing to the location. The arrow is composed of two objects: a rectangle and a triangle being the top of the arrow. What is important here is that the default relative point when zooming is the lower left corner of the Pushpin
. That is why the RenderTransform
is used which translates the Pushpin
to the lower left corner of the component.
Visualizing the stations
Because there can be several stations on the map at the same time, the MapItemsControl
object is used which serves for visualizing several objects of the same type on the map.
<map:MapItemsControl ItemTemplate="{StaticResource StationTemplate}"
ItemsSource="{Binding DepartureStations}"/>
<map:MapItemsControl ItemTemplate="{StaticResource StationTemplate}"
ItemsSource="{Binding ArrivalStations}"/>
The ItemSource
properties are bound respectively to the DepartureStations
and ArrivalStations
collections - basically saying that we want to visualize all the stations in these two collections. This time, we do not apply a style but rather create a new DataTemplate
which will be applied to all the items in the collection. We want to show each station as a customized Pushpin
. This time the Pushpin
customization is a little more complicated because the Pushpin
has to show the detailed information about the station when it is selected.
<DataTemplate x:Key="StationTemplate">
<map:Pushpin Location="{Binding Location}"
MouseLeftButtonDown="Pushpin_MouseLeftButtonDown">
<map:Pushpin.Template>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Canvas VerticalAlignment="Bottom">
<Ellipse x:Name="Ellipse" Width="35" Height="35"
Stretch="Fill" StrokeThickness="4" Stroke="Black"
Fill="{Binding IsSelected,Converter=
{StaticResource BoolToBrush}"/>
<Path x:Name="Path" Width="16"
Height="27" Canvas.Left="10"
Canvas.Top="30" Stretch="Fill"
StrokeThickness="3" StrokeLineJoin="Round"
Stroke="Black" Fill="Black"
Data="F1 M 35,41L 23,81L 11,41"/>
<Canvas.RenderTransform>
<CompositeTransform TranslateX="-17.5"
TranslateY="-30.5"/>
</Canvas.RenderTransform>
</Canvas>
<Border Background="Black" Opacity="0.8"
Grid.Column="1" Margin="20,0,0,0"
Visibility="{Binding IsSelected,
Converter={StaticResource BootToVisibility}}"
HorizontalAlignment="Center">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel HorizontalAlignment="Left">
<Image Source="/Icons/Others/BicycleWhite.png"
Height="30" Width="30"/>
<Image Source="/Icons/Others/HouseWhite.png"
Height="30" Width="30" />
</StackPanel>
<StackPanel Margin="3" Grid.Column="1">
<TextBlock Text="{Binding Path=Free}"/>
<TextBlock Text="{Binding Path=FreePlaces}"/>
</StackPanel>
</Grid>
</StackPanel>
</Border>
</Grid>
</ControlTemplate>
</map:Pushpin.Template>
</map:Pushpin>
</DataTemplate>
The pushpin is composed of a grid with two columns. The left column contains the actual marker build of on Ellipse
and a Path
object, and the right column contains the information about the object. Again, this time we have to use the render transform to translate the Pushpin
to the lower left corner. This DataTemplate
is defined directly in the MainPage
class because it has a MouseLeftButtonDown
event wired to a method which is part of this class. In this method, we are just changing the currently selected BikeStation
to the one on which the user clicks.
private void Pushpin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (this.CurrentStation != null)
{
this.CurrentStation.IsSelected = false;
}
BikeStation station = (BikeStation)((Pushpin)sender).DataContext;
station.IsSelected = true;
this.CurrentStation = station;
}
In the DataTemplate
, two dependency properties depend on the IsSelected
property of the BikeStation
: the Visibility
of the right column containing the detailed information and also the color of the pushpin. Thus the boolean value has to be converted to the appropriate type. In my application, I have used the Generic Boolean to Value Converter idea which I found on the blog of Anthony Jones, so all credit for this goes to him.
Visualizing the routes
The last items to be placed on the map are the routes. I have decided to always visualize only the selected route which is in the CurrentRoute
property. This time we use the MapPolyline
component. This component has its Location
property bound to the Locati
ons property of BikeRoute
which is of type LocationCollection
.
<map:MapPolyline Locations="{Binding Path=CurrentRoute.Locations}"
Stroke="Black" StrokeThickness="3"/>
Tombstoning
Tombstoning is the name for the process of saving an application's state each time the application is either being closed or only deactivated. You have to implement tombstoning because WP7 will only allow one application to be running at one time (with the exception of some "Choosers" and "Launchers"). So when the user, for example, receives a phone call, the application has to save its current state and after the user finishes, come back and pretend that nothing happened. Furthermore, even when the user closes the application correctly, the next time he will open it, maybe he should see it as before closing.
General explanation
In order to explain it correctly, I have drawn the following diagram. There are basically three states for your application and several events which allow transitions between these states.
In your App.xaml.cs file, there are already empty event handlers for all of these events - so basically, waiting for the code to be written and implement tombstoning.
When the user launches the application, it gets to its "Running" state. Then we have two possibilities. Either the user closes the application correctly by clicking the "back" button, or he will receive a call, start a search, and take a picture or perform any other interrupting activity. If that happens, we have to tombstone the application - save its current state. After the application is "tombstoned" in its deactivated state, there are again two possibilities. Either the user will press the "back" button and come back to the application directly, or he will go to the menu and launch the application again. We should implement the tombstoning so that the previous two scenarios will end up the same.
General approach description
We will wrap all the data which needs to be stored to a data model class. This class in my case is called BikeSituation
and contains all the stations and routes visualized on the map. After that, this class can be persisted. When the application is closed, this class will be serialized and stored in IsolatedStorage
. When the application is just deactivated, this class will be stored in the PhoneApplicationService.Current.State
dictionary. The PhoneApplicationService
class is dedicated to managing the application life cycle.
Implementation
The first important step is to wrap the stations and the selected route to a data class called BikeSituation
. We can also add the "Zoo
m" property if we want to persist the zoom.
public class BikeSituation implements INotifyPropertyChanged{
private ObservableCollection<bikestation> _arrivalStations;
private ObservableCollection<bikestation> _departureStations;
private BikeCoordinate _departure;
private BikeCoordinate _arrival;
private BikeRoute _currentRoute;
private BikeStation _currentStation;
private double _zoom;
public ObservableCollection<bikestation> ArrivalStations
{
get
{
if (_arrivalStations == null)
{
_arrivalStations = new ObservableCollection<bikestation>();
}
return _arrivalStations;
}
set
{
_arrivalStations = value;
OnPropertyChanged("ArrivalStations");
}
}
}
Now when this class is ready, the MainPage
will contain only this class - representing the current situation on the map. The BikeSituation
object can be stored directly in the DataContext
property.
public class MainPage{
...
public BikeSituation Situation
{
get
{
return (BikeSituation)this.DataContext;
}
set
{
this.DataContext = value;
}
}
}
So now let's start to implement the methods in App.xaml.cs. I will start with the description of the Application_Closing
method.
private void Application_Closing(object sender, ClosingEventArgs e)
{
MainPage page = RootFrame.Content as MainPage;
BikeSituation situation = page.Situation;
SaveLocalCopy(situation);
}
Here, we first get the current situation, and then a method to serialize the situation to the local IsolatedStorage
is called.
private void SaveLocalCopy(BikeSituation situation)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream fs = isf.CreateFile("Situation.dat"))
{
XmlSerializer ser = new XmlSerializer(typeof(BikeSituation));
ser.Serialize(fs, situation);
}
}
}
This method will be used again later while performing the deactivation. IsolatedStorage
classes are used here, which implement IDisposable
so we should use the using
directive to dispose the resources when we are finished.
Now let's take a look at Application_Deactivated
. This method is an event handler which gets fired when the user interrupts the application (phone call, search...).
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
MainPage page = RootFrame.Content as MainPage;
BikeSituation situation = page.Situation;
SaveLocalCopy(situation);
if (situation != null)
{
if (PhoneApplicationService.Current.State.ContainsKey("Situation"))
{
PhoneApplicationService.Current.State.Remove("Situation");
}
PhoneApplicationService.Current.State.Add("Situation", situation);
}
}
Here we obtain the situation and perform the Save as in the "Closing
" method. But in addition, we also store this situation object to the application state dictionary. If the application was interrupted and then the user comes back to the application, we can load the data object directly from PhoneApplicationService.Current.State
. If he will instead go to the menu and start the application again, we will still have the copy of the latest situation in the IsolatedStorage
.
Now my Application_Activated
event handler stays empty - and that is for a reason. If there is a data object in the dictionary, then I perform the necessary actions in the constructor.
private voeid Application_Activated(object sender, ActivatedEventArgs e)
{
}
The last piece of the puzzle from the App file is the Application_Launching
event handler which gets called when the user launches the application (from the menu or application tile).
private void Application_Launching(object sender, LaunchingEventArgs e)
{
BikeSituation situation = new BikeSituation();
try
{
using (IsolatedStorageFile isf =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (isf.FileExists("Situation.dat"))
{
XmlSerializer ser = new XmlSerializer(typeof(BikeSituation));
object obj = ser.Deserialize(isf.OpenFile("Situation.dat",
System.IO.FileMode.Open)) as BikeSituation;
if (obj != null && obj is BikeSituation)
{
situation = obj as BikeSituation;
PhoneApplicationService.Current.State.Add("Situation", situation);
}
}
}
}
catch (Exception ex)
{ }
This is somewhat interesting. If there is a data object in the IsolatedStorage
, than I will load the object and place it in the PhoneApplicationService.Current.State
dictionary. This way, the "situation" will be loaded in the constructor. To complete the explanation, here is the part of the constructor which loads the BikeSituation
object from the application state dictionary and places it in the this.Situation
property. Remember that this property is mapped directly to the DataContext
of the MainPage
class.
public void MainPage(){
...
if (PhoneApplicationService.Current.State.ContainsKey("Situation"))
{
this.Situation =
PhoneApplicationService.Current.State["Situation"] as BikeSituation;
PhoneApplicationService.Current.State.Remove("Situation");
}
else
{
this.Situation = new BikeSituation();
}
}
You can see that if there is no object in the dictionary (definitely the first time the application runs), than we create a new BikeSituation
and assign it to the DataContext
.
Some details
- Storing objects to the IsolatedStorage is slower than storing in the application state.
IsolatedStorage
is done on the phone's hard disk where the application state stays in memory. If you have some large data, you should think whether it is necessary to store it in the IsolatedStorage
.
- XMLSerialization can be tricky. Here are three issues which I have encountered:
- Circular references - In my example, I have a
BikeSituation
which contains BikeRoute
s starting from a station and later the BikeRoute
object itself is composed of two BikeStation
objects (starting and ending point stations of the route). This results in an error during the serialization. You can use [XMLIgnore]
to one of the properties to avoid this error.
- GeoCoordinate serialization - I kept getting a
FormatException
while serializing the GeoCoordinate
inside of BikeStation
and BikeSituation
classes. I am not sure what was the cause - I ended up writing my own class "BikeCoordinate
" which contains just the latitude and longitude and it serializes just fine. This means that later Converters are needed to bind the map objects (Pushpin
s) to these new coordinates types. All of this is presented in the source code.
- One object deserialized into two different objects: In the
BikeSituation
, I have the CurrentStation
property which contains a reference to just one of the Station
s in the DepartureStations
collection. However, serialization and later deserialization will result in two different objects: one in the collection and the other in the CurrentStation
property. There are several ways to avoid it, for example, ignore the CurrentStation
property during the serialization and then setting to it the right reference to the object from the collection.
- 5 second rule - Note that if you want to pass the certification process, your application should start within 5 seconds. So generally avoid storing large objects to the
IsolatedStorage
during the tombstoning process.
Technical tips
When playing with Bing Maps, I discovered some interesting things, let me share them here:
- Bing Maps for Silverlight and Bing Maps for WP7 are different. To be specific, the namespaces used are different so it is little bit harder to reuse some code. I thought that I will be able to reuse the Model from my previous Bing Maps Silverlight, but it is not possible. For example, the
Location
and LocationCollection
classes are present in both APIs but they are not the same. So if you would like to reuse the model of your phone and web application, you should probably store the locations as double
values and then use converters to convert them to the desired type.
- You cannot bind the Stroke property of the
MapPolyline
component. This property is not a Dependency Property - I discovered that when I wanted to show more routes on the map and assign different colors to each of them. Here the binding is not possible, however you can override the Loaded
event and assign the color when the MapPolyline
object is added to the map. This assumes that the color will not change.
Summary
I tried here to show how to build an application which visualizes data on the Bing Map component on WP7. This time, the data is coming from the bike sharing system of Rennes, specifically from its Web Services. However, the same way, you could visualize any other geo data from any other source.
In order to keep it coherent, I decided to describe all the applications, so maybe for some readers, there is a lot of general Silverlight knowledge here which they already possess - on the other hand, I hope it will be useful for Silverlight beginners (like me).
Also please note that right now, I don't have a device to try it on, so I am not sure about the performance on the actual device.
There are lots of further improvements that can be done: allow user to search nearby stations of any location, speed up the GUI by processing the asynchronous callback on separate threads. I will continue to work on it and update the article in future.
This is my first real article so any feedback will be highly appreciated.
History
- 12/4/2010 - First version.
- 12/23/2010 - Tombstoning implemented and description added.
- 1/5/2011 - Added the link to the online version.