Статьи

Перевод стартового комплекта Meetup APIMASH на Windows Phone

Не так давно, в рамках усилий моей команды (которую мы называем APIMASH ) по созданию набора начальных наборов для демонстрации использования различных общедоступных API в приложениях Магазина Windows, я создал приложение, использующее Meetup. com API вместе с JavaScript SDK Bing Maps для извлечения предстоящих Meetups для данного города, а также для отображения кафе рядом с местоположением Meetup . Вы можете увидеть скриншот ниже:

Цель моего начального набора (и других наборов на сайте) — продемонстрировать, как вызывать API из приложения Магазина Windows, как отображать результаты (в моем случае с использованием привязки данных), а также обеспечить отправную точку для люди, которые могут захотеть «смешать» один из стартовых наборов с дополнительными данными из других API.

Переосмысление для Windows Phone

Следующим шагом в APIMASH является вывод наших стартовых комплектов на Windows Phone. В моем случае мой первоначальный начальный комплект был написан с использованием HTML и JavaScript и построен на основе шаблона Grid App Visual Studio, который использует элемент управления WinJS ListView для отображения данных на домашней странице.

Поскольку WinJS не поддерживается в Windows Phone, мне пришлось принять решение о том, как перенести приложение на эту платформу. Я мог бы выбрать использование элемента управления WebBrowser и спроектировать приложение, используя стиль разработки одностраничного приложения (SPA), используя такие библиотеки, как KnockoutJS и т. Д. И если бы я сегодня запускал приложение с нуля, которое хотел создать с HTML и JavaScript и развертыванием как в Магазине Windows, так и в Windows Phone, это, вероятно, будет путь, который я исследую. Возможно, я сделаю это в следующем посте. В это время…

Учитывая различия в пользовательском интерфейсе между ПК и телефоном, я решил, что просто подойду к телефонной версии приложения так же, как и к версии Магазина Windows … найду шаблон Visual Studio, который наиболее точно соответствует моим потребностям, и изменим его так, чтобы мало, что нужно для достижения UX и функциональности, которую я желал.

Я остановился на шаблоне приложения Windows Phone Databound, как показано в диалоговом окне ниже:

WPDataboundAppTemplate

Этот шаблон предоставляет простой список привязки данных на главной странице, и когда вызывается любой данный элемент, приложение переходит на страницу сведений для этого конкретного элемента. Приложение использует упрощенную архитектуру MVVM, а также демонстрирует использование примеров данных.

Понимание шаблона

Шаблон приложения для Windows Phone Databound довольно прост. Это состоит из:

  • 2 страницы XAML, MainPage.xaml и DetailsPage.xaml (обе с сопровождающими файлами «кода позади» C #).
  • Файл App.xaml, основной целью которого в контексте шаблона приложения является создание и инициализация основной модели представления приложения (подробнее об этом чуть позже)
  • Набор образцов данных времени разработки, реализованных в файле XAML (этот файл находится в папке SampleData с подходящим именем)
  • Пара классов ViewModel, реализованная в C #, MainViewModel, который реализует коллекцию элементов, которая будет связана со списком в MainPage.xaml, и ItemViewModel, который содержит свойства, которые представляют элемент.

Давайте начнем с ViewModels. Их задача — абстрагироваться от необходимости извлечения и анализа данных со страниц. ItemViewModel имеет три свойства, LineOne, LineTwo и LineThree, которые реализованы с закрытыми членами для внутреннего представления (чьи имена начинаются с подчеркивания и строчной буквы) и общедоступных свойств (чьи имена начинаются с заглавной буквы). Каждый вызывает NotifyPropertyChanged, если значение свойства изменяется, так что любые элементы управления, связанные с данными, могут обновлять значение.

Мы могли бы просто вставить значения, которые мы хотим для данной встречи, в эти свойства, что позволило бы избежать некоторых изменений в другом месте кода, но есть несколько проблем с этим… во-первых, было бы потенциально сложно придерживаться таких общих имен свойств , поскольку они ничего не говорят нам о том, что представляет собой свойство, и два, нам, вероятно, на самом деле нужно более трех свойств. Мы вернемся к ItemViewModel на мгновение.

MainViewModel, который, как отмечалось выше, создается и инициализируется при запуске приложения в App.xaml.cs, отвечает за создание ObservableCollection элементов ItemViewModel и (через свою функцию LoadData) заполнение коллекции данными. В коде шаблона данные времени выполнения заполняются простым добавлением группы экземпляров класса ItemViewModel, инициализированных примерами данных.

Модификации шаблона

Чтобы изменить код шаблона для наших целей, мы начнем с класса ItemViewModel.cs. Во-первых, мы переименуем класс и имя файла в MeetupViewModel, чтобы лучше описать, что представляет собой модель. Одна приятная особенность C #, если вы не использовали ее раньше, — это поддержка рефакторинга, в частности, для переименования участников. Поэтому, когда мы меняем имя класса с ItemViewModel на MeetupViewModel, Visual Studio дает нам подсказку, что доступны некоторые контекстно-зависимые команды, которые мы можем расширить с помощью Ctrl +. комбинация клавиш, которая показывает следующее:

Rename

Эта команда выполнит поиск проекта, найдет все ссылки на класс ItemViewModel и обновит их до нового имени. Если вы предпочитаете просматривать каждое изменение, вы можете использовать Переименовать с предварительным просмотром … Это очень удобный способ быстро обновить все (ну, почти все …, если на элемент ссылается строка, например, при вызове NotifyPropertyChanged в объявления свойств, переименование не будет обновлять его).

Затем мы обновим свойства и заменим общие свойства LineOne, LineTwo и т. Д. На более описательные свойства, включая MeetupID, Name, Url, Description, CityState (объединяет поля City и State из свойства места встречи Meetup для более удобного отображения ), и больше. Одним из преимуществ использования ViewModel является то, что мы можем описать, как мы хотим, чтобы данные выглядели для нашего приложения, независимо от того, как на самом деле выглядят исходные данные. Когда мы загружаем данные, мы можем массажировать их в соответствии с форматом, который мы хотим использовать.

Это все, что нужно для класса MeetupViewModel.

Для MainViewModel (посмотрите полный код здесь ), мы начнем с конструктора. Для поддержки извлечения оперативных данных мы добавим пару строк кода для создания экземпляра класса System.Net.WebClient и обработки его события DownloadStringCompleted:

 public MainViewModel()

 {

     _client = new WebClient();

     _client.DownloadStringCompleted += 

         _client_DownloadStringCompleted;

     this.Items = 

         new ObservableCollection<MeetupViewModel>();

 }

В функции LoadData нам нужно удалить код, который создает образцы данных, и заменить его кодом, который создает URL-адрес запроса для API Meetup… в приведенном ниже коде я имею в виду статические члены класса AppConstants, который используется для предоставления единого местоположения для настройки наиболее распространенных параметров приложения (какой город искать, какие ключевые слова искать, ключ API Meetup и т. д.). Кроме того, учтите, что для простоты я не добавил никакой обработки исключений или кода, чтобы иметь дело с доступностью сети (или ее отсутствием), поэтому для производственного приложения вы захотите добавить этот код самостоятельно. Вот обновленные данные LoadData:

В функции LoadData нам нужно удалить код, который создает примеры данных, и заменить его кодом, который создает URL-адрес запроса для API Meetup… в приведенном ниже коде я имею в виду статические члены класса AppConstants, который используется для предоставления единого местоположения для настройки наиболее распространенных параметров приложения (какой город искать, какие ключевые слова искать, ключ API Meetup и т. д.). Кроме того, учтите, что для простоты я не добавил никакой обработки исключений или кода, чтобы иметь дело с доступностью сети (или ее отсутствием), поэтому для производственного приложения вы захотите добавить этот код самостоятельно. Вот обновленные данные LoadData:

   1: public void LoadData()

   2: {

   3:     AppConstants.meetupUri += "&city=" 

   4:         + AppConstants.meetupCity

   5:         + "&state=" + AppConstants.meetupState

   6:         + "&page=" + AppConstants.maxMeetupsToFind

   7:         + "&key=" + AppConstants.meetupKey

   8:         + "&radius=" + AppConstants.meetupDistance;

   9:     if (AppConstants.meetupKeywords != "")

  10:     {

  11:         AppConstants.meetupUri += 

  12:             "&text=" + AppConstants.meetupKeywords;

  13:     }

  14:  

  15:     _client.DownloadStringAsync(new 

  16:         Uri(AppConstants.meetupUri));

  17: }

Довольно просто … мы просто создаем URI с требуемыми параметрами и вызываем DownloadStringAsync с URI.

Затем мы добавляем функцию-обработчик для обработки ответа от запроса WebClient, вот так (извиняюсь за любые дополнительные разрывы строк в коде… вы можете просмотреть код на gihub для более читаемой версии ):

   1: void _client_DownloadStringCompleted(object sender, 

   2:     DownloadStringCompletedEventArgs e)

   3: {

   4:     XElement meetupElements = 

   5:         XElement.Parse(e.Result);

   6:  

   7:  

   8:     var meetups =

   9:         from meetup in 

  10:             meetupElements.Descendants("item")

  11:         where meetup.Element("venue") != null

  12:         select new MeetupViewModel

  13:         {

  14:             MeetupID = 

  15:                 meetup.Element("id").Value,

  16:             Name = 

  17:                 meetup.Element("name").Value,

  18:             Url = 

  19:                 meetup.Element("event_url").Value,

  20:             Description = 

  21:                 meetup.Element("description").Value,

  22:             CityState = 

  23:             meetup.Element("venue").Element("city").Value 

  24:             + ", " +

  25:             meetup.Element("venue").Element("state").Value,

  26:             Latitude = 

  27:               meetup.Element("venue").Element("lat").Value,

  28:             Longitude = 

  29:               meetup.Element("venue").Element("lon").Value,

  30:             LatLong = 

  31:               meetup.Element("venue").Element("lat").Value 

  32:               + ", " +

  33:               meetup.Element("venue").Element("lon").Value

  34:         };

  35:  

  36:     var index = 0;

  37:     foreach (MeetupViewModel 

  38:         meetupItem in meetups)

  39:     {

  40:         meetupItem.ID = index.ToString();

  41:         this.Items.Add(meetupItem);

  42:         index++;

  43:     }

  44:  

  45:     this.IsDataLoaded = true;

  46:}

В приведенном выше коде мы начинаем с использования Linq to XML (требуется ссылка на System.Xml.Linq) для анализа XML, возвращенного из запроса в API Meetup. Мы могли бы запросить JSON в качестве формата, как в версии HTML / JS, но Linq to XML довольно крутой, поэтому XML имеет больше смысла здесь.

Разобравшись в серию элементов, мы запускаем запрос Linq, который ищет все встречи лично (поскольку мы сосредоточены на поиске местных кафе рядом с каждой встречей, в том числе онлайн / виртуальные встречи не имеют особого смысла) и для каждого из возвращаемых элементов создает новый экземпляр класса MeetupViewModel, сопоставляя требуемые значения элементов в данных XML со свойствами класса MeetupViewModel.

Как только это будет сделано, мы зациклим результаты и добавим к каждому простой числовой идентификатор (это свойство используется в навигации приложения, чтобы определить, какой элемент был вызван…, чтобы узнать, как его использовать, см. DetailsPage.xaml.cs ), а затем добавьте элемент ObservableCollection, созданный при запуске приложения, и увеличьте значение идентификатора.

Предполагая, что ошибок нет, мы получили данные! Но так как мы изменили имена свойств, код привязки данных, найденный в MainPage.xaml и DetailsPage.xaml, больше не будет работать. Давайте это исправим.

В MainPage.xaml изменения довольно просты … нам просто нужно обновить привязки к новым именам свойств. В шаблоне MainPage.xaml содержит ItemTemplate для элемента управления LongListSelector, который имеет DataTemplate, содержащий два TextBlocks внутри StackPanel. Они связаны с LineOne и LineTwo соответственно, и я хочу, чтобы они отображали Имя встречи и составное свойство CityState, поэтому мне просто нужно обновить их следующим образом (некоторые атрибуты опущены для удобства чтения):

   1: <TextBlock Text="{Binding Name}" 

   2:     TextWrapping="Wrap"/>

   3: <TextBlock Text="{Binding CityState}" 

   4:     TextWrapping="Wrap" 

   5:     Margin="12,-6,12,0"/>

Я также обновил имя приложения и имя страницы, чтобы оно соответствовало моему приложению, и добавил значок в стиле Meetup для каждой строки в списке, и результат выглядит следующим образом:

Главная страница

Обратите внимание, что не было никаких изменений в коде для MainPage.xaml, так как страница полагается исключительно на декларативное связывание данных для отображения элементов. Нажатие любого из элементов приведет к переходу к DetailsPage.xaml, но если мы сделаем это сейчас, приложение не будет работать, потому что мы еще не обновили эту страницу.

Здесь изменения немного более существенны, но не так резко. Как и в случае с MainPage.xaml, мы должны изменить имя приложения и страницы, а затем также обновить имена свойств-заполнителей на имена в нашем классе MeetupViewModel. Мы также добавим элемент управления картой на страницу, а также пару кнопок, чтобы показать нам близлежащие кафе и перейти на веб-сайт для конкретной встречи. Вот как выглядит обновленный XAML (некоторые атрибуты опущены для удобства чтения):

   1: <StackPanel x:Name="TitlePanel" 

   2:     Grid.Row="0" Margin="12,17,0,28">

   3:     <TextBlock 

   4:         Text="APIMASH - Meetup and Maps"/>

   5:     <TextBlock 

   6:         Text="{Binding Name}" 

   7:         Margin="9,-7,0,0"/>

   8: </StackPanel>

   9:  

  10: <Grid x:Name="ContentPanel" 

  11:     Grid.Row="1" Margin="12,0,12,0">

  12:     <ScrollViewer Margin="0,0,0,365">

  13:         <TextBlock 

  14:             Text="{Binding Description}" 

  15:             TextWrapping="Wrap"/>

  16:     </ScrollViewer>

  17:     <maps:Map x:Name="MyMap" 

  18:         Loaded="MyMap_Loaded" 

  19:         LandmarksEnabled="True" 

  20:         PedestrianFeaturesEnabled="True" 

  21:         VerticalAlignment="Bottom" 

  22:         Height="280" 

  23:         Width="430" Margin="13,0,13,80"/>

  24:     <Button Content="Need COFFEE!" 

  25:         HorizontalAlignment="Left" 

  26:         Margin="0,527,0,0" 

  27:         VerticalAlignment="Top" 

  28:         Height="80" 

  29:         Width="230" 

  30:         Click="Button_Click"/>

  31:     <Button 

  32:         Content="Meetup Site" 

  33:         HorizontalAlignment="Left" 

  34:         Margin="216,527,0,0" 

  35:         VerticalAlignment="Top" 

  36:         Height="80" Width="230" 

  37:         Click="Button_Click_1"/>

  38: </Grid>

В выделенном фрагменте кода (DetailsPage.xaml.cs) нам сначала понадобятся объявления об использовании пространств имен различных функций, которые мы добавим:

   1: using System.Device.Location;

   2: using System.Windows.Shapes;

   3: using System.Windows.Media;

   4: using Microsoft.Phone.Maps.Controls;

   5: using Microsoft.Phone.Tasks;

   6: using APIMASH_MeetupMaps_StarterKit.Customization;

Все, кроме двух последних, связаны с функциональностью карты. Microsoft.Phone.Tasks позволяет нам получить доступ к двум задачам (MapsTask и WebBrowserTask), которые мы будем использовать для обработки нажатий кнопок. И последний обеспечивает более легкий доступ к классу AppConstants, содержащему наши статические переменные, который включает в себя поисковый термин, используемый ниже.

Далее, поскольку мы будем использовать его значение в некотором другом коде, нам нужно переместить объявление переменной индекса в начало класса codebehind:

   1: public partial class DetailsPage : PhoneApplicationPage

   2: {

   3:     int index;

   4:  

   5:     // Constructor

   6:     public DetailsPage()

   7:     {

   8:         // etc.

   9:     }

Событие OnNavigatedTo не изменяется, за исключением изменения, в котором была объявлена ​​переменная индекса.

Когда элемент управления карты загружен, он запускает событие Loaded, которое отображается на обработчик MyMap_Loaded:

   1: private void MyMap_Loaded(object sender, 

   2:     RoutedEventArgs e)

   3: {

   4:     double lat = 

   5:         double.Parse(App.ViewModel.Items[index].Latitude);

   6:     double lon = 

   7:         double.Parse(App.ViewModel.Items[index].Longitude);

   8:  

   9:     MyMap.Center = new GeoCoordinate(lat, lon);

  10:     MyMap.ZoomLevel = 15;

  11:  

  12:     // Create a small circle to 

  13:     // mark the current meetup location.

  14:     Ellipse myCircle = new Ellipse();

  15:     myCircle.Fill = 

  16:         new SolidColorBrush(Colors.Red);

  17:     myCircle.Height = 20;

  18:     myCircle.Width = 20;

  19:     myCircle.Opacity = 50;

  20:  

  21:     MapOverlay myOverlay = new MapOverlay();

  22:     myOverlay.Content = myCircle;

  23:     myOverlay.PositionOrigin = new Point(0.5, 0.5);

  24:     myOverlay.GeoCoordinate = MyMap.Center;

  25:  

  26:     MapLayer myLayer = new MapLayer();

  27:     myLayer.Add(myOverlay);

  28:     MyMap.Layers.Add(myLayer);

  29: }

Приведенный выше код извлекает значения широты и долготы из текущего элемента встречи, основываясь на индексе выбранного элемента, центрирует карту по этим координатам, а затем добавляет объект эллипса, чтобы отметить местоположение встречи.

Последний бит дополнительного кода — обработчики событий нажатия кнопки:

   1: private void Button_Click(object sender, 

   2:     RoutedEventArgs e)

   3: {

   4:     double lat = 

   5:         double.Parse(App.ViewModel.Items[index].Latitude);

   6:     double lon = 

   7:         double.Parse(App.ViewModel.Items[index].Longitude);

   8:  

   9:     MapsTask getCoffeeTask = new MapsTask();

  10:     getCoffeeTask.Center = new GeoCoordinate(lat, lon);

  11:     getCoffeeTask.SearchTerm = AppConstants.searchTerm;

  12:     getCoffeeTask.ZoomLevel = 16;

  13:     getCoffeeTask.Show();

  14: }

  15:  

  16: private void Button_Click_1(object sender, 

  17:     RoutedEventArgs e)

  18: {

  19:     WebBrowserTask meetupTask = 

  20:         new WebBrowserTask();

  21:  

  22:     meetupTask.Uri = 

  23:         new Uri(App.ViewModel.Items[index].Url);

  24:     meetupTask.Show();

  25: }

Во-первых, мы снова получаем широту и долготу из элемента встречи и используем его для запуска нового MapsTask, устанавливая его SearchTerm в соответствии с настроенным searchTerm в нашем классе AppConstants (который по умолчанию равен «coffee»).

Обработчик второго клика использует WebBrowserTask для запуска нового окна браузера для веб-сайта для просматриваемой встречи.

Вот как выглядит DetailsPage.xaml, когда мы закончим:

DetailsPage

Нажав «Нужен КОФЕ!» (а кто нет?) запускает приложение Карты и показывает кафе в этом районе:

CoffeeMap

Поскольку мы используем встроенное приложение «Карты» на телефоне, теперь мы можем легко выбрать нужную кофейню и узнать, как дойти пешком или проехать от нашего текущего местоположения, без необходимости писать какой-либо код самостоятельно.

Наконец, что не менее важно, нажатие кнопки «Сайт встречи» открывает новое окно браузера с URL-адресом, указывающим на просматриваемый сайт встречи:

MeetupSite

Еще одна часть кода шаблона, которую необходимо обновить, — это данные примера. Это не критическая часть приложения, поэтому я оставлю это в качестве упражнения для читателя. Или, если вы не хотите пытаться обновить это самостоятельно, вы всегда можете посмотреть обновленную версию в нашем репозитории Github .

Резюме

Хотя у меня были некоторые опасения по поводу попытки перевести мое исходное приложение Windows 8 на HTML и JavaScript в приложение для телефона на XAML и C #, на самом деле все оказалось проще, чем я ожидал. От начала и до конца мне потребовалось всего около 2 дней для завершения проекта

Весь код этого проекта , а также все другие наши стартовые наборы APIMASH доступны на сайте APIMASH Github .

Здание для обоих

Вероятно, существуют сценарии, особенно когда вы заранее знаете, что хотите нацеливаться как на Windows 8, так и на Windows Phone, где может иметь смысл либо использовать XAML и C # для обоих, либо использовать сторонние фреймворки, такие как KnockoutJS и т. Д. создать приложение HTML и JavaScript, которое может работать на любой платформе. Но, несмотря на некоторые различия в доступе к удаленным данным, концепции создания этого приложения для Windows Phone на самом деле не сильно отличаются от первоначальной версии Windows 8.

Если вы заинтересованы в разработке приложений для Windows 8 и Windows Phone 8, в Центре разработки Windows Phone есть хорошая документация . Есть также серия видеороликов на 9 канале .

AppBuilder

Если вы еще не зарегистрировались, зайдите на сайт AppBuilder . Это бесплатно, и вы можете найти много информативных видео и многое другое, чтобы помочь вам начать. А AppBuilder недавно добавил вознаграждения, позволяющие вам зарабатывать очки, которые вы можете использовать для игр XBOX, бесплатной учетной записи Windows Store или Windows Phone для разработчиков и многого другого!