Windows Phone 7: Dependency Injection на основе Funq или nuget-пакет PhoneTools, как полезный инструмент
Windows Mobile | создано: 10.02.2012 | опубликовано: 10.02.2012 | обновлено: 13.01.2024 | просмотров: 6273 | всего комментариев: 2
Наверное многие из вас уже если не писали приложения на Windows Phone, то как минимум предпринимали попытки это сделать. Как только создается проект в Visual Studio, сразу хочется писать, но не всегда всё так просто. Возникают вопросы: А как же MVVM на WP7? Как сделать Dependency Injection? Какой контейнер использовать? И много других вопросов.
Написав несколько приложений, получилось собрать всё самое полезное в один nuget-пакет под названием PhoneTools. О нем и пойдет речь в этой статье: как использовать и всё такое.
Постановка задачи
Хочется написать приложение с использованием MVVM, Dependency Injection, UnityContainer и чтобы всё это было для WP7.
MVVM для Windows Phone 7
Не могу не сказать, что я являюсь большим поклонником MVVM. И после некоторого количества приложений для WPF и Silverlight, не мог не попробовать это диво дивное и чудо чудное под Windows Phone. Для MVVM буду пользовать опять же Prism. Благо, что его установить для WP7-приложения тоже можно через nuget-пакет… Но обо всём по порядку.
Создаем приложение
Создаем новый проект Silverlight for Windows Phone.
Далее выбираем платформу Windows Phone 7.1. Подтверждаем выбор и смотрим что у нас получилось на данный момент.
Я сразу же добавлю папки в проект, которые обязательно мне пригодятся: ViewModels и Engine. Что касаемо второй папки, то название может быть и другим, например Core или Infrastructure, это на ваше усмотрение, впрочем, как и для первой папки. Установим nuget-пакет Prism:
PM> Install-Package Prism.Phone Successfully installed 'Prism.Phone 4.0.1.0'. Successfully added 'Prism.Phone 4.0.1.0' to PhoneExtensionDemo. PM>
Отлично. Идем дальше. Теперь я добавлю самый быстрый контейнер, который будет использоваться для инъекций зависимостей (Dependency Injection). Называется этот контейнер – Funq. Всё описание и принципы работы отлично описаны на сайте разработчика (есть видео… много!). Хорошо… Добавил.
Примечание:Очень жать что автор Funq не сделал nuget-пакет, хотя скорее всего это вопрос времени. Поэтому придется скачать его с официального сайта и добавить референс “вручную”.
Теперь установил тот самый пакет, который является основой для написания статьи – PhoneTools. Можно через визуальный менеджер:
А можно из командной строки менеджера:
PM> Install-Package PhoneTools Successfully installed 'PhoneTools 0.4.0'. Successfully added 'PhoneTools 0.4.0' to PhoneExtensionDemo. PM>
Готово. Теперь давай разберем, что к чему и почему. Посмотрите на вид проекта, который получился:
Нужные библиотеки на месте, появилась папка Extensions и кучка файлов, вот их-то мы и будем пользовать дальше при разработки приложения, которое будет проверить орфографию на основе сервиса Yandex.
Что же в папке?
BusyStates.cs – синглтон класс, который отвечает за состояние IsBusy. Дальше будет видно каким образом можно его использовать.
ContainerLocator.cs – реализация контейнера на основе Funq.
DataService.cs – в моих приложения очень часто (во всех предыдущих - всегда) используется какой-нибудь web-сервис. Так вот это как раз реализация доступа к сервису.
Result.cs – этот базовый класс для результатов обработки полученных данных.
ServiceError.cs – аргумент для передачи результата работы запроса. Если возникнет ошибка в обработке запроса web-сервиса, то этот класс получит ошибку, которую можно будет правильно отобразить на стороне клиента.
SettingsStore.cs – у меня ни разу не получилось написать приложение, которое не пользовало настройки, которые требовалось сохранять. Этот класс в помощь при работе с настройками.
ViewModelLocator.cs – один из самых главных, ну если не “главных”, то важных уж точно. Этот класс позволит “находить” требуемые ViewModel’и по мере необходимости.
Даёшь ViewModel!
С классами вроде как разобрались, подробности далее по ходу дела. Создадим новый класс MainViewModel.cs в папку ViewModels. Унаследуюсь от ViewModel и реализую абстрактные методы базового класса:
namespace PhoneExtensionDemo.ViewModels { using Calabonga.Phone.Extensions; public class MainViewModel: ViewModel { public override void OnPageResumeFromTombstoning() { } } }
Так как, базовый класс требует параметров, предоставим ему их:
namespace PhoneExtensionDemo.ViewModels { using System; using Calabonga.Phone.Extensions; public class MainViewModel : ViewModel { public MainViewModel(INavigationService service, IPhoneApplicationServiceFacade facade) : base(service, facade, new Uri("MainPage.xaml", UriKind.Relative)) { } public override void OnPageResumeFromTombstoning() { } } }
Попробую запустить проект… Уп-п-п-с-с! Не работает. Оказывается в классе ContainerLocator уже есть регистрация MainViewModel, а она требует ISettingsStore и IDataService. Доведем MainViewModel до ума предварительно раскомментировав регистрацию в ContainerLocator для ISettingsStore :
this.Container.Register<ISettingsStore>(c => new SettingsStore());
и:
this.Container.Register<IDataService>(new DataService());
Так как в ViewModelLocator уже содержит свойство MainViewModel:
public MainViewModel MainViewModel { get { return this.containerLocator.Container.Resolve<MainViewModel>(); } }
Теперь MainViewModel должен выглядеть так:
public class MainViewModel : ViewModel { private readonly ISettingsStore settingsStore; private readonly IDataService dataService; public MainViewModel(INavigationService service, IPhoneApplicationServiceFacade facade, ISettingsStore settings, IDataService data) : base(service, facade, new Uri("MainPage.xaml", UriKind.Relative)) { this.settingsStore = settings; this.dataService = data; } public override void OnPageResumeFromTombstoning() { } }
Теперь приложение компилируется, но по-прежнему, ничего ценного не замечено в библиотеке.
А что же в XAML?
Добавим несколько строк в App.xaml, сначала зарегистрируем namespace:
xmlns:viewmodels="clr-namespace:PhoneExtensionDemo"
и теперь новый ресурс:
<Application.Resources> <viewmodels:ViewModelLocator x:Key="ViewModelLocator" /> </Application.Resources>
После чего можно будет воспользоваться ViewModelLocator в файле разметки MainPage.xaml. Привяжем DataContext к MainViewModel (строки 10-11):
<phone:PhoneApplicationPage x:Class="PhoneExtensionDemo.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" d:DesignHeight="768" d:DesignWidth="480" DataContext="{Binding MainViewModel, Source={StaticResource ViewModelLocator}}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" Orientation="Portrait" shell:SystemTray.IsVisible="True" SupportedOrientations="Portrait" mc:Ignorable="d">
А если в Expression Blend?
Чтобы продемонстрировать всю мощь данного подхода при разработке под Windows Phone, давайте я создам во MainViewModel свойство Title, а привязку в xaml сделаю из Blend’а.
#region свойство Title /// <summary> /// заголовок для главной страницы. /// </summary> public string Title { get { return "Проверка орфографии"; } } #endregion свойство Title
Привязка в Blend выглядит:
Запущу-ка, приложение… Ба-а-а-а! She is alive!!!
Заголовок берется из MainViewModel.
А как же сервис?
Может это покажется банальным, но я не буду оригинален и воспользуюсь бесплатным сервисом для перевода от Bing, ибо задача этот статьи совсем в другом. Ключевые моменты на картинке:
Как видно на картинке, адрес сервиса http://api.microsofttranslator.com/V2/Soap.svc, но единственный нюанс в том, что для того чтобы вызывать методы сервиса, в каждых из них требуется подставлять так называемый AppID. Получить его можно зарегистрировав приложение по адресу http://www.bing.com/developers/appids.aspx. После регистрации, вы получите что-то на подобие:
354676E4BD3EEF01CCD570F183F157F04E7E76F4
Этот код и будет подставляться в методы для перевода в виде параметра appId.
Внимание:У вас должен быть свой код, приведенный код недействителен, потому что используется как пример.
Немного классов в помощь
Создаю в папке Engine так называемый, хелпер для инициализации сервиса с той целью, чтобы инициализация работа сама подставляя адрес сервиса и тип Binding’а:
namespace PhoneExtensionDemo.Engine { public class ServiceHelper { [System.Diagnostics.CodeAnalysis. SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] public LanguageServiceClient GetService { get { BasicHttpBinding binding = new BasicHttpBinding(BasicHttpSecurityMode.None); binding.MaxReceivedMessageSize = int.MaxValue; binding.MaxBufferSize = int.MaxValue; return new LanguageServiceClient( binding, new EndpointAddress( new Uri("http://api.microsofttranslator.com/V2/Soap.svc"))); } } } }
Думаю, не надо объяснять код. Итак всё понятно, в конце концов, статья не о сервисах. А вот конструктор класса DataService наверное, надо обязательно показать:
public class DataService : IDataService { public DataService() { if (!DesignerProperties.IsInDesignTool) { ServiceHelper helper = new ServiceHelper(); this.service = helper.GetService; this.service.TranslateCompleted += (service_TranslateCompleted); } } <... другой код ...> }
Теперь снова к классам.
Настало время немного покодировать, открываю DataService и начинаю инициализацию сервиса при помощи хелпера, потом еще немного покодировать чтобы настроить данные для отправки сервису, немного переменных, немного обобщенных (generic) типов для обработки результатов… Вот что получилось…
namespace PhoneExtensionDemo.Engine { public class TranslateResult: Result<string> { } }
Этот класс будет использоваться как результат запроса, возвращенный с сервера переводчика. А вот тут еще немного скрытых переменных:
private TranslateResult translateResult; private Action<TranslateResult> translateAction; private readonly LanguageServiceClient service; private string contentType = string.Empty; private string category = string.Empty; private const string appId = AppId.Code;
Строки с третьей по шестую можно даже не смотреть. Эти переменные используются в методе запроса на перевод. Обратите внимание на первую и вторую строки. В первой, я создал скрытый экземпляр класса описанного выше, а во второй строке обобщенный (generic) делегат для возврата результата на MainViewModel. Пришло время заняться интерфейсом IDataService, в который я только один метод:
public interface IDataService { void Translate(string text, string from, string to, Action<TranslateResult> action); }
Обратите внимание на третью строку, в которой как раз и используется TranslateResult. Так как DataService должен реализовывать мой интерфейс, то надо написать пару методов. Один открытый (для реализации IDataSource):
/// <summary> /// перевод текста /// </summary> /// <param name="text">текст для перевода</param> /// <param name="from">язык с которого</param> /// <param name="to">язык на котороый</param> /// <param name="action">делегат с результатом</param> public void Translate(string text, string from, string to, Action<TranslateResult> action) { this.translateAction = action; this.service.TranslateAsync( appId, text, from, to, contentType, category); }
А также потребуется скрытый внутренний метод, в котором-то и происходит обработка результатов (это наиболее правильное место, где можно понять для чего нужна библиотека PhoneTools):
private void service_TranslateCompleted(object sender, TranslateCompletedEventArgs e) { // создаем экземпляр результата this.translateResult = new TranslateResult(); // проверяем на ошибки и возвращаем результат if (e.Error == null) { // если не ошибок //получаем результат this.translateResult.Data = e.Result; } else { // если есть ошибки // получаем информацию об ошибке и сохраняем в результат this.translateResult.Error = new ServiceError(e.Error); } // "стреляем" делегатом с полученными данными this.translateAction.Invoke(this.translateResult); }
Назад к ViewModel
Давайте подготовим наш MainViewModel к тому, чтобы он мог работать с сервисом перевода. Для этого создам-ка я два новых свойства. Одно будет выводить результат перевода, а второе будет принимать от пользователя слово на русском, для перевода на английский.
Примечание:Следует расставить все точки на Ё. Сервис перевода имеет очень большое количество возможностей, начиная от выбора направления перевода (несколько десятков языков), и заканчивая возможность произношения слова голосом. Использование всех возможностей не есть задача данной статьи.
Свойство номер один:
#region свойство ResultText /// <summary> /// Calabonga: поле для хранения значений свойства <see cref="ResultText"/> /// </summary> private string _result; /// <summary> /// Calabonga: результат перевода. /// </summary> public string ResultText { get { return _result; } set { _result = value; RaisePropertyChanged(() => this.ResultText); } }
Тут всё банально, теперь второе свойство:
#region свойство RusText /// <summary> /// Calabonga: поле для хранения значений свойства <see cref="RusText"/> /// </summary> private string _rusText; /// <summary> /// Calabonga: свойство для ввода слова для перевода. /// </summary> public string RusText { get { return _rusText; } set { _rusText = value; RaisePropertyChanged(() => this.RusText); } } #endregion свойство RusText
И тут всё просто. Ой, едрён-батон! А как же пользователь будет запускать процесс перевода? Надо тогда еще и команду (ICommand) добавить в мой MainViewModel!
Сказано – сделано:
#region команда TranslateCommand /// <summary> /// Calabonga: Команда TranslateCommand /// </summary> public DelegateCommand TranslateCommand { get { return new DelegateCommand(() => this.TranslateCommandExecute()); } } /// <summary> /// Calabonga: Процедура выполняет команды TranslateCommand /// </summary> private void TranslateCommandExecute() { // выполнение команды Translate // подставим свойство RusText как параметр this.dataService.Translate(this.RusText, "ru", "en", (result) => onTranslateComplete(result)); } #endregion // end команда TranslateCommand
Обратите внимание на строку номер 17, в которой как параметр подставляется свойство RusText, и жёстко кодируется направление перевода “ru” и “en”. Если поменять местами, то при вводе английских слов, будет приходить перевод на русском. Но меня пока итак устроит. Ибо цель статьи не в переводе. А в 18 строке метод onTranslateComplete, который будет нашему свойству ResultText присваивать полученное с сервера значение:
private void onTranslateComplete(Engine.TranslateResult result) { if (result.Error == null) { // если не получено ошибок this.ResultText = result.Data; } else { // если получена ошибка this.ResultText = result.Error.Message; } }
Привязка данных
Я открыл MainPage.xaml в Blend’е и привел форму к правильному виду:
Теперь надо сделать привязку данных, благо что это теперь очень просто. Сначала привяжу свойство для ввода:
Потом кнопку:
И, наконец, TextBlock для отображения результат перевода:
Таким образом, если сейчас запустить приложение на исполнение, то должно всё заработать?! Проверю… Ура! She is alive again! What’s a miracle!
Так программа работает, когда нет ошибок подключения к сервису перевода:
А так, когда нет подключения:
А зачем тогда нужен BusyStates.cs файл?
Это регистрация в классе состояний сервиса информации о том, что запущен процесс получения данных. Такую регистрацию нужно будет делать для всех методов. И второй кусочек кода такой:
private void onTranslateComplete(Engine.TranslateResult result) { if (result.Error == null) { // если не получено ошибок this.ResultText = result.Data; } else { // если получена ошибка this.ResultText = result.Error.Message; } // удалим из состояний информацию об этот запросе BusyStates.Instance.Remove(BusyStates.IsLoadDataComplete); RaisePropertyChanged(() => this.IsBusy); }
А после выполнения запроса удалим этот флаг (см. строки 10-11). Для того чтобы информацию о занятости приложения можно было показать пользователю, добавлю в MainViewModel еще одно свойство:
#region свойство IsBusy /// <summary> /// Calabonga: описание. /// </summary> public bool IsBusy { get { return BusyStates.Instance.IsBusy; } } #endregion свойство IsBusy
А теперь осталось сделать привязку… Но к какому компоненту нужно привязываться? Есть несколько вариантов, например, можно сделать какой-нибудь конвертор, который будет Boolean значение превращать в Visibility и просто будет прятать кнопку от глаз пользователя. Еще один вариант – привязать свойство IsEnabled у кнопки отправки запроса:
<Button Margin="17,215,17,0" VerticalAlignment="Top" Command="{Binding TranslateCommand, Mode=OneWay}" Content="Перевод" IsEnabled="{Binding IsBusy, Mode=OneWay}" />
Еще один вариант – использовать индикатор занятости из shell:
<shell:SystemTray.ProgressIndicator> <shell:ProgressIndicator IsIndeterminate = "true" IsVisible="{Binding IsBusy}"/> </shell:SystemTray.ProgressIndicator>
Я же поступлю по-другому, я когда-то делал свой собственных индикатор (BusyIndicator), вот его-то я и буду пользовать:
К нему и буду привязываться:
В коде xaml это выглядит так:
<clb:BusyIndicator Grid.Row="1" IsWaiting="{Binding IsBusy, Mode=OneWay}" ForegroundAnimation="{StaticResource PhoneAccentBrush}"> <!-- здесь основной контент Grid --> </clb:BusyIndicator>
Запущу еще разок программ и попробую… Короче, всё работает так как и планировалось. На время запроса форма показывает индикатор занятости:
Вместо заключения
Поставленные задачи выполнены. Nuget-пакет PhoneTools будет постоянно обновляться пополняясь новыми фишечками и прибамбасами, которые будут полезны при разработке под Windows Phone. Пишите комментарии и всем легкой отладки!
Ссылки
Как создать свой собственный nuget-пакет
Funq - Самый быстрый DI контейнер
Bing Translator. Using the SOAP API
Регистрация приложения при использовании переводчика
Вместо заключения
P.S.: Изначально планировалось сделать приложение по проверки орфографии на основе сервисов Yandex, но с Bing получилось лучше. Поэтому название Title звучит как "проверка орфографии", забыл поменять и за это прошу прощения.
P.P.S.: Внимание, после обновление пакета PhoneTools до версии 0.4.2 больше не требуется отдельная установка пакета PrismPhone. Теперь достаточно просто установить PhoneTools и всё.
Комментарии к статье (2)
Приложение называется "Проверка орфографии", а выполняет ф-ию перевода.
Разъяснение написано в постскриптуме