Windows Phone 7: Dependency Injection на основе Funq или nuget-пакет PhoneTools, как полезный инструмент

ru-RU | создано: 10.02.2012 | опубликовано: 10.02.2012 | обновлено: 02.01.2018 | просмотров за всё время: 5120

Наверное многие из вас уже если не писали приложения на 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.

1

Далее выбираем платформу Windows Phone 7.1. Подтверждаем выбор и смотрим что у нас получилось на данный момент.

2

Я сразу же добавлю папки в проект, которые обязательно мне пригодятся: 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. Можно через визуальный менеджер:

 

image

А можно из командной строки менеджера:

PM> Install-Package PhoneTools
Successfully installed 'PhoneTools 0.4.0'.
Successfully added 'PhoneTools 0.4.0' to PhoneExtensionDemo.
 
PM> 

Готово. Теперь давай разберем, что к чему и почему. Посмотрите на вид проекта, который получился:

3

Нужные библиотеки на месте, появилась папка 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 выглядит:

4

Запущу-ка, приложение… Ба-а-а-а! She is alive!!!

5

Заголовок берется из MainViewModel.

А как же сервис?

Может это покажется банальным, но я не буду оригинален и воспользуюсь бесплатным сервисом для перевода от Bing, ибо задача этот статьи совсем в другом. Ключевые моменты на картинке:

6

Как видно на картинке, адрес сервиса 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’е и привел форму к правильному виду:

7

Теперь надо сделать привязку данных, благо что это теперь очень просто. Сначала привяжу свойство для ввода:

8

Потом кнопку:

9

И, наконец, TextBlock для отображения результат перевода:

10

Таким образом, если сейчас запустить приложение на исполнение, то должно всё заработать?! Проверю… Ура! She is alive again! What’s a miracle!

Так программа работает, когда нет ошибок подключения к сервису перевода:

11

А так, когда нет подключения:

12

А зачем тогда нужен 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), вот его-то я и буду пользовать:

14

К нему и буду привязываться:

13

В коде xaml это выглядит так:

<clb:BusyIndicator Grid.Row="1" 
   IsWaiting="{Binding IsBusy, Mode=OneWay}" 
   ForegroundAnimation="{StaticResource PhoneAccentBrush}">
   <!--  здесь основной контент Grid  -->
</clb:BusyIndicator>

Запущу еще разок программ и попробую… Короче, всё работает так как и планировалось. На время запроса форма показывает индикатор занятости:

15

Вместо заключения

Поставленные задачи выполнены. 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)

13.02.2012 8:30:00 Евгений

Приложение называется "Проверка орфографии", а выполняет ф-ию перевода.

14.02.2012 1:17:00 Calabonga

Разъяснение написано в постскриптуме