В этой серии статей будет рассказано о создании отлаженного, работающего приложения для Windows Phone от начала до конца. Приложение называется Realworld Stocks, и полный исходный код будет доступен на CodePlex по мере развития серии. Я буду использовать Mercurial, чтобы поощрять разветвление и, возможно, даже получать запросы от разработчиков, которые хотят внести свои собственные реальные решения.
Убедитесь, что вы загружаете Windows Phone 7.5 SDK
Посмотреть серию Введение и план
Выбор
В мире Microsoft у нас нет недостатка в клиентских и серверных HTTP-стеках на выбор. Вместо того, чтобы пытаться охватить каждую возможную комбинацию, и чтобы эта тема не стала слишком длинной, я оставлю эту часть краткой.
Наши первые важные решения основаны на следующих вариантах:
Мыло или отдых?
WCF или ASP.NET MVC?
Не вдаваясь в это слишком глубоко и, конечно же, не предполагая, что это единственно верный путь, я лично предпочитаю создавать конечные точки ASP.NET MVC на основе REST по следующим причинам:
- Очень простая, гибкая маршрутизация из коробки
- Возвращать JSON так же просто, как и возвращать Json ()
- Модель привязки / приведения параметров входящего запроса
- Простые атрибуты ActionFilter добавляют кеширование и сжатие GZip
- Нет конфигурации XML, нет генерации кода / прокси, нет ссылок на сервисы
Создание сервисов
Поскольку мы решили пойти по пути ASP.NET MVC для этого проекта, я собираюсь рассказать вам о создании сервисов с использованием MVC. Если вы следили за этим, то у вас уже есть настройки веб-проекта. Если нет, см. Часть 2: Файл -> Новый проект.
Должен ли я создать сервис среднего уровня?
Многие мобильные приложения взаимодействуют с какими-либо сторонними сервисами. Решение, которое вам нужно принять, заключается в том, должен ли телефон подключаться напрямую к этой услуге, или есть ли преимущества подключения мобильного приложения к веб-службе среднего уровня, которая впоследствии свяжется со сторонней службой. Решение здесь определенно зависит от ваших конкретных потребностей, но есть очень реальные преимущества для создания сервиса среднего уровня. Некоторые из преимуществ включают в себя:
- Ключи API — если сторонней службе требуется ключ API, его можно легко украсть на телефоне. Если ваш веб-сервер передает вам этот вызов, гораздо безопаснее хранить его там.
- Кэширование — многие сервисы будут ограничивать количество запросов, которые вы делаете (используя ваш ключ API). Если вы можете ограничить эти запросы, кэшируя ответы на своем сервере, это может значительно улучшить время отклика для ваших пользователей, а также снизить использование вашей сторонней службы.
- Формирование модели. Многие поставщики услуг возвращают XML. Они могут даже вернуть тонну XML, который вас даже не волнует в вашем приложении. Если ваше мобильное приложение напрямую связывается с вашим сервером, ваш сервер может запрашивать данные третьих сторон и формировать их наиболее эффективным способом, чтобы вернуться к мобильному приложению, включая: объединение нескольких вызовов сторонних служб в один ответ HTTP на телефон, удалив ненужные элементы, которые телефонное приложение не будет использовать, и превратив XML в сжатый JSON, делая его намного более компактным.
Достаточно фона, время реализации
Далее вы узнаете, как эффективно создавать и использовать сервисы, а также легко улучшать их с течением времени. Конечно, чтобы получить полное представление о том, как это работает на практике, убедитесь, что вы извлекли последний код и проверили его.
Шаг 1: Определите ваши сущности / модель в проекте WP7
В RealworldStocks.Client.Core у меня есть папка Models , в которую входят различные классы, наследуемые от NotifyObject, как показано ниже.
шаг 2: классы модели «Добавить как ссылку» в ваш веб-проект
Поскольку вы хотите, чтобы эти же классы были сериализованы из вашей службы, они понадобятся вам в веб-проекте, но вы хотите синхронизировать их при добавлении, удалении или переименовании свойств. Чтобы сделать это, в RealWorldStocks.Web , щелкните правой кнопкой мыши в вашей модели папку и выберите Add -> Существующий элемент …
Затем перейдите в папку RealWorldStocks.Client.Core \ Models и выберите сущности, которые вы планируете сериализовать и представить своему клиенту, но не нажимайте Add! Рядом с кнопкой «Добавить» находится стрелка раскрывающегося списка, откройте ее и убедитесь, что вы нажимаете « Добавить как ссылку».
Обратите внимание, что файлы имеют небольшие «ярлыки» на своих значках.
Шаг 3. Верните данные в формате JSON с контроллера MVC.
Ниже представлен полный StocksController для демонстрации снимков и получения последних новостных статей. Следующий код охватывает несколько понятий с начала этого поста. Я рекомендую вам ознакомиться с полным исходным кодом, чтобы понять, как они работают под прикрытием.
- [AllowJsonGet] — Этот атрибут работает вокруг функции безопасности MVC 2, которая позволяет явно возвращать JSON из запросов HTTP GET
- [NoCache] — WebClient в WP7 автоматически кеширует запросы. Это может застать вас врасплох и вызвать очень тонкие ошибки, если вы не будете осторожны. Этот атрибут добавит соответствующие заголовки HTTP-контроля кеша в наш ответ, говорящий приложению не кэшировать возвращенный JSON. Если бы этого не было, каждый раз, когда мы запрашивали обновление для символа MSFT, мы немедленно возвращали кешированный ранее телефонный ответ, даже не пытаясь связаться с сервером.
- [Compress] — Этот атрибут проверяет входящее Accept-Encoding запроса и, если он поддерживает GZip, автоматически GZip сжимает исходящий ответ. Наше приложение для Windows Phone использует SharpGIS . GZipWebClient для его распаковки — обсуждается ниже.
- [OutputCache] — В приведенном ниже методе GetSnapshot есть OutputCache с длительностью 60 . Этот атрибут инструктирует MVC возвращать кэшированную версию этого ответа, если параметры входящего маршрута одинаковы — в этом случае, если запрашивается один и тот же символ. Это означает, что если несколько пользователей Realworld Stocks запросят моментальный снимок MSFT, наш сервис будет кэшировать его в течение 60 секунд и давать им одинаковый ответ, вместо того, чтобы каждый раз попадать в Yahoo.
[AllowJsonGet] [NoCache] [Compress] public class StocksController : Controller { private readonly IStocksService _stocksService; private readonly INewsService _newsService; public StocksController() { _stocksService = new YahooStocksService(); _newsService = new FakeNewsService(); } public ActionResult GetSnapshots(string[] symbols) { var model = _stocksService.GetSnapshots(symbols); return Json(model); } public ActionResult GetNews(string[] symbols) { var model = _newsService.GetNews(symbols); return Json(model); } [OutputCache(Duration = 60)] public ActionResult GetSnapshot(string symbol) { return GetSnapshots(new[] { symbol }); } }
Попробуй это!
Если все пойдет по плану, вы сможете найти этот URL в браузере. На моей машине, размещенной под IIS, URL-адрес:
Http:? //localhost/RealWorldStocks.Web/Stocks/GetSnapshots символы = MSFT и символы = NOK
Шаг 4. Создание надежного HttpClient
Теперь, когда у нас есть наш сервис, возвращающий JSON, давайте начнем использовать его из приложения. Следующий HttpClient решает ряд проблем от нашего имени.
- Сжатие GZip с использованием SharpGIS.GZipWebClient (можно найти на NuGet!)
- Поддержка тайм-аута для автоматического уничтожения слишком длинных запросов (по умолчанию 30 секунд)
- Регистрация запросов и ответов в окне отладки
- Десериализация JSON в определенный тип модели автоматически
public static class HttpClient { public static TimeSpan Timeout = TimeSpan.FromSeconds(30); public static void BeginRequest<T>(HttpRequest<T> request, Action<HttpResponse<T>> callback) { BeginRequest(request.Url, callback); } public static void BeginRequest<T>(string url, Action<HttpResponse<T>> callback) { var client = new GZipWebClient(); var timer = new Timer(state => client.CancelAsync(), null, Timeout, TimeSpan.FromMilliseconds(-1)); Debug.WriteLine("HTTP Request: {0}", url); client.DownloadStringCompleted += (s, e) => ProcessResponse(callback, e); client.DownloadStringAsync(new Uri(url, UriKind.Absolute), timer); } private static void ProcessResponse<T>(Action<HttpResponse<T>> callback, DownloadStringCompletedEventArgs e) { var timer = (Timer) e.UserState; if (timer != null) timer.Dispose(); try { if (e.Error == null) { string json = e.Result.Replace("&", "&"); Debug.WriteLine("HTTP Response: {0}\r\n", json); var model = SerializationHelper.Deserialize<T>(json); Deployment.Current.Dispatcher.BeginInvoke(() => callback(new HttpResponse<T>(model))); } else { throw new WebException("Error getting the web service data", e.Error); } } catch (SerializationException ex) { var httpException = new HttpException("Unable to deserialize the model", ex); Debug.WriteLine(ex); Deployment.Current.Dispatcher.BeginInvoke(() => callback(new HttpResponse<T>(httpException))); } catch (WebException ex) { var httpException = new HttpException(ex); Debug.WriteLine(ex); Deployment.Current.Dispatcher.BeginInvoke(() => callback(new HttpResponse<T>(httpException))); } } }
логирование
Ведение журнала полезно, чтобы увидеть следующее в окне отладки во время тестирования приложения.
INFO: HTTP Request: http://legacy/RealWorldStocks.Web/Stocks/GetSnapshots?symbols=MSFT&symbols=NOK&symbols=AAPL&isTrial=False&clientVersion=1.0.0.0 INFO: HTTP Response: [{"Symbol":"MSFT","Company":"Microsoft Corpora","OpeningPrice":27.08,"LastPrice":27.19,"DaysChange":0.03,"DaysChangePercentFormatted":"+0.11%","DaysChangeFormatted":"+0.03","DaysRangeMin":0,"DaysRangeMax":0,"Volume":56897792,"PreviousClose":27.16}]
Шаг 5. Создайте клиентский API для связи с сервером.
Вызов конечной точки HTTP так же прост, как и объявление URL вместе с любыми параметрами строки запроса для настройки запроса. Поскольку мы определенно не хотим распространять эту деталь реализации по всей нашей кодовой базе, мы пишем простую абстракцию, которая обрабатывает следующее:
- Объявляет, какой тип модели возвращается с сервера, определяя метод как HttpRequest <T>, где T — это то, что возвращается с сервера
- Объявляет базовый путь URL для запроса, например, «Stocks / GetSnapshot»
- Создает любые параметры строки запроса, которые будут отправлены вместе с запросом.
- По сути, он строит строку, подобную следующей, и автоматически анализирует JSON в тип модели, которую мы ожидаем от сервера: http: //localhost/RealWorldStocks.Web/Stocks/GetSnapshots? Symbols = MSFT & symbols = NOK
public class StocksWebService : HttpService, IStocksWebService { public StocksWebService() { #if DEBUG BaseUrl = DynamicLocalhost.ReplaceLocalhost("http://localhost/RealWorldStocks.Web/"); #else BaseUrl = "http://services.mydomain.com/v1/"; #endif } public HttpRequest<StockSnapshot> GetSnapshot(string symbol) { var queryString = new QueryString { {"symbol", symbol} }; return CreateHttpRequest<StockSnapshot>("Stocks/GetSnapshot", queryString); } public HttpRequest<IEnumerable<StockSnapshot>> GetWatchListSnapshots() { var queryString = new QueryString(); queryString.AddMany("symbols", WatchList.Current.Select(m => m.Symbol)); return CreateHttpRequest<IEnumerable<StockSnapshot>>("Stocks/GetSnapshots", queryString); } public HttpRequest<IEnumerable<News>> GetNewsForWatchList() { var queryString = new QueryString(); queryString.AddMany("symbols", WatchList.Current.Select(m => m.Symbol)); return CreateHttpRequest<IEnumerable<News>>("Stocks/GetNews", queryString); } }
DynamicLocalhost
DynamicLocalhost — это пакет NuGet, который я написал для упрощения работы служб отладки на нескольких машинах с несколькими разработчиками.
Вы можете прочитать больше о пакете DynamicLocalhost здесь .
Шаг 6: Собираем все вместе — оркеструем запрос из ViewModel
Кажется, что для этого достаточно много кода, но большая часть этого материала — инфраструктура, которую я просто копирую / вставляю во все свои проекты. Без этих проблем инфраструктуры я могу просто начать писать методы контроллера и определять клиентский API для запросов. Оттуда я только начинаю использовать его из ViewModel, всего за 5 минут.
Сопрограммы — сделать асинхронным сексуальным, так как C # 2
Как быстро я хотел бы поговорить о сопрограмм. Одна из этих удивительных вещей была впервые представлена мне в докладе Роба Айзенберга «Создай свой собственный MVVM Framework из MIX ’10». Это был отличный разговор и вдохновение для проекта Caliburn.Micro.
Сопрограмма является UpdateWatchList метод ниже.
- Показать глобальный BusyIndictator с дружеским сообщением о загрузке
- Создание асинхронного HTTP-запроса
- Когда мы получим HTTP-ответ, проверьте, не произошла ли ошибка, если так, покажите дружественный MessageBox
- Если он вернулся успешно, мы привязываем данные к нашей коллекции ObservableCollection, чтобы пользовательский интерфейс обновлялся сам
- Скрыть глобальный BusyIndicator
Это на самом деле много чего происходит, и это асинхронно. Заметьте, как нет лямбд, колбэков, анонимных делегатов методов? Это действительно довольно элегантно и очень мне подходит, пока я терпеливо жду C # 5 и официальной поддержки асинхронного компилятора.
private IEnumerable<IResult> UpdateWatchList() { BusyIndictator.Show("Loading watch list..."); var request = _stocksWebService.GetWatchListSnapshots().Execute(); yield return request; if (!request.Response.HasError) { WatchList.RepopulateObservableCollection(request.Response.Model); } else { MessageBox.Show("We had troubles updating your watch list, please try again in a few moments", "Unable to contact server", MessageBoxButton.OK); } yield return BusyIndictator.HideResult(); }
Резюме
Это охватывало много вещей. Я надеюсь, что он не был слишком пугающим или подавляющим — большая часть этого кода на самом деле является инфраструктурным материалом многократного использования, который можно просто вставить в любой проект и сразу же использовать. Я хотел бы получить отзывы от людей, особенно тех, кто выбирает другой путь, например, WCF и Service References.