Microsoft Windows Phone 7 поставляется с простым в использовании API геопозиционирования, который позволяет определять текущие координаты и перемещение пользователя, выражаемые в долготе и широте (а иногда и в высоте над уровнем моря). Располагая доступом к этим данным, вы можете добавить в приложение Windows Phone 7 средства распознавания местонахождения.
Если вы создаете приложение для кафе быстрого питания, было бы неплохо — помимо вывода меню и других предложений — находить ближайшее к пользователю кафе. Еще одна отличная возможность — поиск людей поблизости от вас. Достаточно вообразить, что вы торговый представитель, которому нужно посетить своих клиентов между деловыми встречами.
В этой статье основное внимание будет уделено тому, как получить доступ к таким данным из приложения Windows Phone 7 и какими способами можно визуализировать маршруты и местонахождения. Эти данные на самом деле поступают от Bing Maps API. Поэтому, прежде чем разбираться в некоторых «продвинутых» концепциях, важно рассмотреть основы Bing Maps API.
Введение в Bing Maps API
Первое, что вам понадобится, — рабочая учетная запись в Bing. Чтобы создать учетную запись для Bing Maps, зайдите на сайт Bing Maps Account Center (bingmapsportal.com) и используйте Windows Live ID. После этого у вас будет доступ к разделу дополнительной информации, связанной с учетной записью, где вы сможете создать ключ. Так как вы создаете приложение для Windows Phone 7, в качестве Application URL можно написать нечто вроде http://localhost.
Помимо создания учетной записи, на той же странице можно отслеживать использование Bing Maps API. Если вы решите задействовать Bing Maps в производственном приложении, то вам также потребуется вернуться на эту страницу, чтобы обеспечить корректное лицензирование.
Bing Maps API на самом деле предоставляет несколько сервисов, но приложения Windows Phone 7 будут использовать SOAP-сервисы. Я дам краткие обзоры следующих из таких сервисов:
- 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).
Сервис Geocode позволяет работать с координатами и адресами, сервис Imagery — с реальными изображениями (полученными с помощью аэрофотосъемки, с высоты птичьего полета и виды с дорог), сервис Route помогает вычислять маршрут между двумя и более точками, а сервис Search дает возможность искать конкретные места на основе ввода от пользователей (например, в виде «рестораны в Брюсселе»).
Чтобы задействовать эти сервисы, достаточно добавить ссылки на эти сервисы по указанным выше URL. Заметьте, что в результате этого создается или обновляется файл ServiceReferences.ClientConfig. На рис. 1 показан пример того, как можно вызвать метод Geocode одноименного сервиса.
Рис. 1. Запуск метода Geocode сервиса Geocode
// Создаем запрос
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";
// Выполняем запрос и отображаем результаты
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.";
};
Каждый метод сервиса работает по принципу «запрос-ответ». Вы создаете объект запроса, в котором формулируется вопрос к серверу, и настраиваете ключ API. В данном случае я создал GeocodeRequest и прошу сервер вернуть мне GeocodeLocation для «Brussels, Belgium». После создания запроса я просто запускаю клиент для асинхронной работы. Наконец, вы всегда будете получать некий ответ либо с нужной информацией, либо с ошибкой в случае любых проблем. Запустите приложение-пример из пакета исходного кода, который можно скачать для данной статьи, и посмотрите на GeocodeServiceClient в действии. Вы увидите, как на экране (через элементы управления, связанные с данными) отображается какое-либо место (или ошибка).
Это приложение будет использовать сервисы Geocode и Route для вычисления маршрута между двумя адресами и отображения результата пользователю.
Вычисление маршрута
Используя сервис Bing Route, можно вычислить маршрут от точки A до точки B. Как и в предыдущем примере, этот сервис работает по принципу «запрос-ответ». Сначала вам понадобится найти реальные географические координаты каждого адреса (с помощью GeocodeRequest), а затем вы сможете создать RouteRequest. Приложение-пример в пакете исходного кода для этой статьи содержит весь необходимый код, а на рис. 2 приведен короткий пример того, как это делается.
Существует много областей применения, в которых приложения приносят больше пользы, если им известно местонахождение пользователя. |
Рис. 2. Создание RouteRequest
// Создаем запрос
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;
// Выполняем запрос
var routeClient = new RouteServiceClient(
"BasicHttpBinding_IRouteService");
routeClient.CalculateRouteCompleted += new EventHandler<
CalculateRouteCompletedEventArgs>(OnRouteComplete);
routeClient.CalculateRouteAsync(routeRequest);
Заметьте, что свойство Waypoints запроса является набором, а это позволяет добавлять несколько точек маршрута (waypoints). Это может быть полезно, когда нужно знать маршрут для всего пути следования, а не просто от точки A до точки B.
Когда вы запускаете метод CalculateRouteAsync, сервис приступает к самой трудной части работы: вычислению маршрута, перечислению всех элементов на пути следования (таких действий, как повороты, съезды с магистрали и т. д.), расчету времени следования и расстояния, перечислению всех точек (геопозиционирование) и др. На рис. 3 показана общая схема некоторых важных данных, присутствующих в RouteResponse.
Используя сервис Bing Route, можно вычислить маршрут от точки A до точки B. |
Рис. 3. Содержимое RouteResponse
Увеличить
Отображение маршрута на карте
В первом примере я воспользуюсь RoutePath Points для отображения маршрута на карте. Поскольку вWindows Phone 7 Toolkit уже включен элемент управления Bing Maps, вам нужно лишь добавить ссылку на сборку Microsoft.Phone.Controls.Maps. После этого отображение карты на дисплее устройства не составит никакого труда. Вот пример того, как показать карту Брюсселя (CredentialsProvider необходим для задания ключа API):
<maps:Map Center="50.851041,4.361572" ZoomLevel="10"
CredentialsProvider="{StaticResource MapCredentials}" />
Если вы собираетесь использовать какие-либо элементы управления из сборки Maps, я советую добавлять ссылку на эту сборку до добавления любой ссылки на сервисы Bing. Тогда ссылка на сервис Bing позволит повторно использовать такие типы, как Microsoft.Phone.Controls.Maps.Platform.Location, вместо создания новых типов, и вам не придется писать методы преобразования или конвертеры значений для работы с некоторыми данными, возвращаемыми сервисом.
Теперь вы знаете, как вычислить маршрут между двумя точками и как показать карту на смартфоне. Давайте соединим эти знания и попробуем визуализировать маршрут на карте. Для рисования на карте будут использоваться Points вRoutePath. Элемент управления Map позволяет добавлять фигуры наподобие Pushpin (например, чтобы помечать начало и конец маршрута) и MapPolyline (чтобы рисовать маршрут на основе GeoCoordinates).
Поскольку тип точек, возвращаемых сервисом, не совпадает с типом точек, применяемых в элементе управления Maps, я создал два небольших метода расширения для приведения точек к корректному типу (рис. 4).
Рис. 4. Методы расширения для приведения точек к корректному типу
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;
}
Вы можете использовать эти методы расширения в RouteResponse по завершении метода CalculateRoute. После преобразования эти методы расширения будут возвращать типы, которые можно использовать, например, для связывания с элементом управления Bing Maps. Так как это приложение Silverlight, мы должны применять IValueConverter для выполнения реальной операции преобразования. На рис. 5 показан пример конвертера значений, который будет преобразовывать Locations в GeoCoordinates.
Рис. 5. Применение 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<Location>)
{
return (value as IEnumerable<Location>).ToCoordinates();
}
else
{
return null;
}
}
}
Теперь пора заняться конфигурированием связывания с данными. Связывание с данными использует конвертеры, поэтому важно объявлять их первыми. Это можно делать в ресурсах страницы или в ресурсах приложения (если вы планируете повторно использовать конвертеры), как показано ниже:
<phone:PhoneApplicationPage.Resources>
<converters:LocationConverter x:Key="locationConverter" />
<converters:ItineraryItemDisplayConverter
x:Key="itineraryConverter" />
</phone:PhoneApplicationPage.Resources>
После объявления конвертеров можно добавить элемент управления Maps и другие, оверлейные элементы управления (вроде MapPolyline и Pushpin), а потом связать их с необходимыми свойствами:
<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>
Поскольку в Windows Phone 7 Toolkit уже включен элемент управления Bing Maps, вам нужно лишь добавить ссылку на сборку Microsoft.Phone.Controls.Maps. |
Как видите, эти привязки используют уже объявленные конвертеры для преобразования данных в формат, понятный элементу управления Maps. Наконец, вам нужно задать следующие свойства по завершении CalculateMethod:
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;
}
}
На рис. 6 показано, как выглядит дисплей после запуска приложения и вычисления маршрута.
Рис. 6. Визуальное представление маршрута
Вывод инструкций
Как видите, отображение маршрута на карте — задача довольно стандартная. В следующем примере я покажу, как создать собственный элемент управления, который выводит инструкции по маршруту от начала до конца, используя текст и сводку из каждого ItineraryItem. Конечный результат приведен на рис. 7.
Рис. 7. Отображение инструкций по всему маршруту
Вы уже видели, что Legs является одним из свойств RouteResult. Свойство Legs содержит один или более объектов Leg (отрезков маршрута), каждый из которых включает набор ItineraryItems. Используя ItineraryItems, можно заполнить элемент управления, как показано на рис. 7. Для каждого отрезка на рис. 7 выводится ItineraryItem с полной длиной и временем в пути (в секундах), а также номер текущего отрезка. ItineraryItem не отслеживает номер текущего отрезка пути, поэтому я создал небольшой класс ItineraryItemDisplay:
public class ItineraryItemDisplay
{
public int Index { get; set; }
public long TotalSeconds { get; set; }
public string Text { get; set; }
public double Distance { get; set; }
}
Пример в пакете исходного кода для этой статьи также содержит метод расширения со следующей сигнатурой:
public static ObservableCollection <ItineraryItemDisplay>
ToDisplay(this ObservableCollection<ItineraryItem> items)
Код в этом методе перебирает все элементы, записывает важные значения в новый объект ItineraryItemDisplay и отслеживает текущий номер отрезка маршрута в свойстве Index. Наконец, ItineraryItemDisplayConverter выполняет преобразование при связывании с данными. Как вы, вероятно, заметили на рис. 7, каждый промежуточный отрезок (этап) форматируется (названия городов и улиц выделяются полужирным) с помощью собственного элемента управления ItineraryItemBlock. Его единственное предназначение — форматирование текста ItineraryItem для улучшения восприятия информации. На рис. 7 также видно, что кое-какая дополнительная информация помещается в синие блоки, но это осуществляется через обычное связывание с данными:
[TemplatePart(Name = "ItemTextBlock", Type =
typeof(TextBlock))] public class
ItineraryItemBlock : Control
Атрибут TemplatePart определяет элемент, который должен присутствовать в элементе управления Template, и его тип. В данном случае это должен быть TextBlock с именем 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>
Причина выбора TextBlock очевидна. С помощью TextBlock-свойства Inlines можно программно добавить контент в TextBlock. В собственном элементе управления можно переопределить OnApplyMethod, и именно там потребуется найти ItemTextBlock (рис. 8).
Рис. 8. Нахождение TextBlock
/// <summary>
/// При применении шаблона ищет блок текста
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Получаем текстовый блок
textBlock = GetTemplateChild("ItemTextBlock") as TextBlock;
if (textBlock == null)
throw new InvalidOperationException
("Unable to find 'ItemTextBlock' TextBlock
in the template.");
// Задаем текст, если он был указан до загрузки шаблона
if (!String.IsNullOrEmpty(Text))
SetItinerary(Text);
}
Далее анализируется ItineraryItem-свойство Text, и его содержимое используется для заполнения этого TextBlock с дополнительным форматированием. На практике это делается довольно легко, так как свойство Text содержит практически обычный текст и лишь некоторые части заключаются в XML-теги:
<VirtualEarth:Action>Turn</VirtualEarth:Action>
<VirtualEarth:TurnDir>left</VirtualEarth:TurnDir>onto
<VirtualEarth:RoadName>Guido Gezellestraat
</VirtualEarth:RoadName>
Поскольку мне нужно выделить лишь названия городов и улиц, я написал небольшой метод, который разбирает такие теги, как VirtualEarth:Action или VirtualEarth:TurnDir. После получения TextBlock вызывается метод SetItinerary, и именно в нем Inlines добавляется в TextBlock (рис. 9).
Рис. 9. Добавление Inlines в TextBlock методом SetItinerary
// Читаем ввод
string dummyXml = String.Format("<Itinerary
xmlns:VirtualEarth=\"http://dummy\">{0}</Itinerary>",
itinerary);
using (var stringReader = new StringReader(dummyXml))
{
// Отслеживаем предыдущий элемент
string previousElement = "";
// Разбираем dummyXml
using (var xmlReader = XmlReader.Create(stringReader))
{
// Читаем каждый элемент
while (xmlReader.Read())
{
// Добавляем тектсовый блок
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 });
}
}
// Сохраняем предыдущий элемент
if (xmlReader.NodeType == XmlNodeType.Element)
previousElement = xmlReader.Name;
else
previousElement = "";
}
}
}
Как видно в предыдущем примере XML-текста, не в каждой части текста содержится какой-либо XML-элемент. Чтобы этот текст можно было использовать в XmlReader, первым делом его нужно обернуть в бутафорский XML-элемент. Это позволяет создать новый XmlReader с этим текстом. Используя XmlReader-метод Read, вы можете перебрать каждую часть XML-строки.
На основе NodeType вы можете определить текущую позицию в XML-строке. Возьмем, к примеру, следующий элемент: <VirtualEarth:RoadName>Guido Gezellestraat</VirtualEarth:RoadName>. При использовании XmlReader-метода Read у вас будет три итерации. Первая — <VirtualEarth:RoadName>, и этоXmlNodeType.Element. Поскольку ставится цель отформатировать названия городов и улиц полужирным, именно здесь к TextBlock Inlines добавляется объект Run с FontWeight, установленным в «bold». Добавление объекта Run к Inlines просто добавляет кое-какой текст в конец строки TextBlock.
В любом другом случае форматирование не требуется, и тогда вы добавляете обычный объект Run, содержащий только текст, и не настраиваете никакие свойства форматирования.
ItineraryItem не отслеживает номер текущего отрезка пути. |
Вот и все, что нужно для собственного элемента управления. Вся запись ItineraryItemDisplay отображается с помощью собственного DataTemplate для ListBox. Этот DataTemplate также содержит ссылку на собственный элемент управления (рис. 10).
Рис. 10. Вся запись ItineraryItemDisplay в собственном DataTemplate для Listbox
<!-- Шаблон для полного элемента (включает длину и время) -->
<DataTemplate x:Key="ItineraryItemComplete">
<Grid Height="173" Margin="12,0,12,12">
<!-- Левая часть: 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>
<!-- Серый прямоугольник -->
<Rectangle Grid.ColumnSpan="4" Grid.RowSpan="3"
Fill="#FF0189B4" />
<!-- Поля метаданных -->
<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>
<!-- В правой части показываются инструкции -->
<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>
Теперь, когда собственный элемент управления и оформление готовы, остается лишь одна задача — реализовать это в элементе управления Pivot и в коде. Как я уже упоминал, собственные элемент управления и DataTemplate будут использоваться в ListBox:
<controls:PivotItem Header="Directions">
<ListBox ItemsSource=
"{Binding Itinerary, Converter=
{StaticResource itineraryConverter}}"
Grid.RowSpan="2" ItemTemplate=
"{StaticResource ItineraryItemComplete}" />
</controls:PivotItem>
Свойство ItemSource этого ListBox связано со свойством Itinerary, и именно так заполняется это свойство; остальное делает ItineraryItemDisplayConverter. Как видите, применив собственный элемент управления и добавив немного элементов оформления, вы можете получать промежуточные данные от сервиса Route и наглядно представлять их пользователю:
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;
}
}
Определение текущего местонахождения
Из предыдущих примеров вы узнали, как использовать сервисы Geocode и Route, чтобы получить инструкции по маршруту из точки A в точку B, и как визуализировать эти инструкции. Теперь пора рассмотреть API геопозиционирования.
GeoCoordinateWatcher — это класс, которым вы будете пользоваться для нахождения текущих GPS-координат:
coordinateWatcher = new GeoCoordinateWatcher
(GeoPositionAccuracy.High); coordinateWatcher.StatusChanged
+= new EventHandler<GeoPositionStatusChangedEventArgs>(
OnCoordinateUpdate);
coordinateWatcher.Start();
Если вы создаете приложение, которое показывает инструкции, то могли бы использовать изменения в текущем местонахождении пользователя для автоматической прокрутки каждого этапа и даже проигрывать звуковой сигнал. |
GeoCoordinateWatcher будет проходить через различные стадии после выполнения метода Start, но, когда состояние выставлено в Ready, вы получаете доступ к текущей позиции. По окончании работы с GeoCoordinateWatcher рекомендуется вызывать метод Stop:
private void OnCoordinateStatusChanged(object sender,
GeoPositionStatusChangedEventArgs e)
{
if (e.Status == GeoPositionStatus.Ready)
{
coordinateWatcher.Stop();
// Получаем позицию
fromLocation = coordinateWatcher.Position.Location;
LocationLoaded();
}
}
Теперь приложение-пример предоставляет и средства определения местонахождения. Кроме того, в классе GeoCoordinateWatcher есть событие PositionChanged, которое позволяет отслеживать изменения позиции. Если вы создаете приложение, которое показывает инструкции, то могли бы использовать изменения в текущем местонахождении пользователя для автоматической прокрутки каждого этапа и даже проигрывать звуковой сигнал на основе VirtualEarth:Action в ItineraryItem Text. В итоге вы получили бы приложение с настоящей GPS-навигацией.
Вы отлаживаете приложение, используя эмулятор Windows Phone 7? Если вы при этом тестируете функциональность геопозиционирования, то можете столкнуться с небольшой проблемой с GeoCoordinateWatcher Status: оно всегда будет содержать NoData и никогда не будет меняться на Ready. Вот почему важно писать код с применением интерфейса (IGeoPositionWatcher<GeoCoordinate>), а не самой реализации (GeoCoordinateWatcher). В статье Тима Хойера (Tim Heuer) в блоге bit.ly/cW4fM1 предлагается скачать исходный код класса EventListGeoLocationMock, который имитирует реальное GPS-устройство.
Класс EventListGeoLocationMock принимает набор GeoCoordinateEventMocks, который должен имитировать изменение координат пользователя с течением времени. Это позволяет вам протестировать определение местонахождения пользователя и его передвижения:
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();
По имени устройства вы могли бы определять, где выполняется ваше приложение — на реальном устройстве или в эмуляторе, и определять, какой IGeoPositionWatcher следует использовать. Проверяйте значение расширенного свойства DeviceName, которое всегда устанавливается в XDeviceEmulator при выполнении приложения в эмуляторе:
private static bool IsEmulator()
{
return (Microsoft.Phone.Info.DeviceExtendedProperties.
GetValue("DeviceName") as string) == "XDeviceEmulator";
}
В качестве альтернативы в блоге Драгоса Манолеску (Dragos Manolescu) (bit.ly/h72vXj) вы найдете другой способ имитировать потоки событийWindows Phone 7 с использованием Reactive Extensions (Rx).
Не забывайте, что ваше приложение выполняется на мобильном устройстве, и регулярное Wi-Fi-соединение доступно не всегда. |
Реальные приложения и производительность
Когда вы создаете приложение и хотите продать его, то очевидно, что оно должно быть привлекательным для пользователей. Пользователям нужно быстрое приложение с интересными им возможностями. Как демонстрируют предыдущие примеры, вам потребуется вызывать методы некоторых веб-сервисов и асинхронно обрабатывать несколько событий до того, как вы сможете показать пользователю некие результаты. Не забывайте, что ваше приложение выполняется на мобильном устройстве, и регулярное Wi-Fi-соединение доступно не всегда.
Уменьшение числа вызовов веб-сервисов и количества обменов данными по сети могло бы ускорить работу вашего приложения. Во введении я упоминал о приложении для кафе быстрого питания, которое предоставляет меню и поддерживает средства определения местонахождения. Если вы создаете такое приложение, то в облаке можно было разместить сервис, передающий меню и различные предложения на смартфоны. И почему бы не воспользоваться им для сложных расчетов вместо того, чтобы выполнять их на мобильном устройстве? Вот пример:
[ServiceContract]
public interface IRestaurantLocator
{
[OperationContract]
NearResult GetNear(Location location);
}
Вы могли бы создать сервис, принимающий текущее местонахождение пользователя. Этот сервис будет запускать несколько потоков (в примере применяется Parallel.ForEach) и параллельно вычислять расстояние между этой позицией и координатами других кафе (рис. 11).
Рис. 11. Вычисление расстояния между пользователем и тремя ближайшими кафе
public NearResult GetNear(BingRoute.Location location)
{
var near = new NearResult();
near.Restaurants = new List<RestaurantResult>();
...
Parallel.ForEach(restaurants, (resto) =>
{
try
{
// Формируем запрос геопозиционирования
var geoRequest = new BingGeo.GeocodeRequest();
...
// Получаем координаты кафе
var geoResponse = geoClient.Geocode(geoRequest);
// Местонахождение кафе
if (geoResponse.Results.Any())
{
var restoLocation =
geoResponse.Results.FirstOrDefault().
Locations.FirstOrDefault();
if (restoLocation != null)
{
// Формируем запрос маршрута
var fromWaypoint = new Waypoint();
fromWaypoint.Description = "Current Position";
...
var toWaypoint = new Waypoint();
...
// Создаем запрос
var routeRequest = new RouteRequest();
routeRequest.Waypoints = new Waypoint[2];
routeRequest.Waypoints[0] = fromWaypoint;
routeRequest.Waypoints[1] = toWaypoint;
...
// Выполняем запрос
var routeClient = new RouteServiceClient();
var routeResponse =
routeClient.CalculateRoute(routeRequest);
// Добавляем результат в список
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)
{
// Принимаем соответствующие меры для протоколирования
// ошибки и/или ее вывода конечному пользователю
}
});
// Получаем первые три кафе
int i = 1;
var topRestaurants = results.OrderBy(o => o.TotalSeconds)
.Take(3)
.Select(o => { o.Index = i++;
return o; });
near.Restaurants.AddRange(topRestaurants);
return near;
}
Параллельно перебирая все кафе из списка, вы преобразуете местонахождение каждого из них в географические координаты с помощью GeocodeServiceClient. На основе этого местонахождения и позиции пользователя вычисляется маршрут между ними; для этого применяется RouteServiceClient. Наконец, на основе свойства TotalSeconds в сводке маршрута отыскиваются три ближайших кафе, координаты которых передаются на устройство.
Здесь преимущество в том, что вычисления выполняются параллельно (используется Parallel.ForEach и то количество потоков, которое допускают машинные ресурсы), а как только они завершаются, в Windows Phone передаются лишь релевантные данные. С точки зрения производительности вы сразу же почувствуете разницу; мобильное приложение будет вызывать лишь один метод веб-сервиса и получать по сети только небольшой блок данных.
Кроме того, радикально уменьшится объем кода и число асинхронных вызовов в Windows Phone 7, как показано ниже:
var client = new RestaurantLocatorClient();
client.GetNearCompleted += new EventHandler<
GetNearCompletedEventArgs>(OnGetNearComplete);
client.GetNearAsync(location);
На рис. 12 демонстрируется отображение на дисплее смартфона ближайших кафе.
Рис. 12. Отображение на дисплее смартфона трех ближайших кафе
Уменьшение числа вызовов веб-сервисов и количества обменов данными по сети могло бы ускорить работу вашего приложения. |
Процесс передачи в Windows Phone 7 Marketplace
Последнее, о чем хотел бы упомянуть, — процесс передачи приложения в Windows Phone 7 Marketplace. Ваше приложение должно удовлетворять набору требований, чтобы оно было размещено на торговой площадке Marketplace. Одно из требований — определение возможностей вашего приложения в файле манифеста. Если вы решили использовать GeoCoordinateWatcher, вы также должны указать в этом файле возможность ID_CAP_LOCATION.
На странице MSDN Library «How to: Use the Capability Detection Tool for Windows Phone» (bit.ly/hp7fjG) поясняется, как с помощью утилиты Capability Detection Tool определить все возможности, используемые вашим приложением. Обязательно прочитайте эту статью, прежде чем передавать свое приложение!
Приложение-пример
В пакете исходного кода для этой статьи содержится решение с двумя проектами. Один из них — библиотека классов, которая включает элемент управления, методы расширения и стили; ее очень легко интегрировать в собственные проекты. Второй — пример Pivot-приложения Windows Phone 7, которое сочетает в себе все примеры в виде одной небольшой программы.