Windows Phone 7: Dependency Injection на основе Funq или nuget-пакет PhoneTools, как полезный инструмент
Windows Mobile | создано: 10.02.2012 | опубликовано: 11.02.2012 | обновлено: 04.09.2025 | просмотров: 6568 | всего комментариев: 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)
Приложение называется "Проверка орфографии", а выполняет ф-ию перевода.
Разъяснение написано в постскриптуме