Разработка для нескольких платформ

Портируемые библиотеки классов: введение

Билл Кратохвил

Продукты и технологии:

Portable Class Libraries, Windows Phone 7, Silverlight, Microsoft .NET Framework

В статье рассматриваются:

  • приложение, управляющее паролями и ориентированное на несколько платформ;
  • применение инфраструктур встраивания зависимостей (dependency injection frameworks);
  • сценарии использования.

Исходный код можно скачать по ссылке

Проект Portable Class Library (PCL) будет генерировать управляемую сборку, на которую можно ссылаться из Windows Phone 7, Silverlight, Microsoft .NET Framework и Xbox 360. Это позволяет максимально увеличить степень повторного использования вашего кода и уменьшить количество проектов, особенно в приложениях, ориентированных на несколько платформ, которые имеют общую кодовую базу, как в случае демонстрационного приложения в этой статье. Я потратил время на написание приложения Windows Phone 7 специально для данной статьи, а приложения WPF/Silverlight дались мне безо всяких усилий. Единственное ограничение — PCL не может ссылаться на проекты, специфичные для конкретной платформы; он может ссылаться только на другие PCL-проекты. На первый взгляд это может показаться серьезным препятствием, особенно в случае приложений, использующих Prism и встраивание зависимостей (dependency injection, DI). Но при тщательном планировании вы сможете обойти это ограничение и создать эффективные PCL-проекты.

До появления PCL проекты решений могли ссылаться только на сборки для той же платформы. Например, проекты Silverlight ссылались на другие сборки Silverlight, .NET-проекты — на .NET-сборки и т. д. Для эффективного кодирования мы могли накапливать неподдающееся управлению количество проектов; при создании общих кодовых баз (код, который можно использовать на всех платформах) нам приходилось создавать проект для каждой платформы.

Решение Password Manager, ориентированное на несколько платформ

Решение Password Manager (passwordmgr.codeplex.com) — это приложение, ориентированное на несколько платформ с единой кодовой базой (проекты Windows Phone 7 выступают в роли хоста, а проекты Silverlight и WPF связываются с файлами проекта для Phone). Как видите, у этого небольшого приложения устрашающее количество проектов.

Зачем так много проектов? Это популярный вопрос, который я часто слышу от разработчиков, когда занимаюсь архитектурой инфраструктур для клиентских решений. Типичный рефрен: «Они делают решение запутанным и усложняют его понимание» — что само по себе неплохой аргумент.

Я всегда отвечаю: «Для четкого разделения обязанностей и максимального увеличения степени повторного использования». Каждый проект должен быть предназначен для одной цели (и реализовать ее хорошо и независимо от других проектов). В качестве примера обратите внимание на явное разделение в решении PasswordMgr и возможность легко заменять уровень доступа к данным (data access layer, DAL) на другой, используя DI. Тем не менее при повторном использовании этого кода вы будете вынуждены задействовать сборки SQLite и связанные проекты, даже если вы не планировали применять SQLite (возможно, вы предпочли бы SQL Server для своего DAL).

Как только вы ссылаетесь на другой проект, ваш проект становится жестко связанным, заставляя вас перетягивать к себе не только его, но и любые его зависимости от других проектов. И чем больше у вас проектов, тем сложнее повторно использовать код в других решениях.

PCL помогает значительно сократить количество проектов, которыми вам приходится управлять, особенно если вам нужно иметь четкое разделение обязанностей, позволяющее легко повторно использовать ваши проекты в других модулях или решениях. Главное — поддерживать свободное связывание своих проектов, программируя с применением интерфейсов. Это позволит использовать инфраструктуры DI, такие как Managed Extensibility Framework (MEF) и Unity, которые дают возможность легко конфигурировать реализацию для интерфейсов. То есть реализацией DAL для интерфейсов могли бы быть SQL Server, облако или классы SQLite.

После установки необходимых компонентов (как описано в документации MSDN по ссылке bit.ly/fxatk0) вы получите новый шаблон в диалоге Add New Project для создания PCL.

Применение инфраструктур DI

Когда пытаешься задействовать мощные инфраструктуры DI, быстро возникает вопрос: «Как использовать PCL с этими инфраструктурами, если нельзя ссылаться на их компоненты, т. е. на атрибут [Dependency] или [Export]?». Например, SecurityViewModel в следующем коде имеет Unity-атрибут [Dependency], который будет разрешать реализацию ISecurityViewModel:

namespace MsdnDemo.MvpVmViewModels
{
  public class SecurityViewModel : PresentationViewModelBase
  {
    [Dependency]
    public ISecurityViewModel UserInfo { get; set; }

    public bool IsAuthenticated
    {
      get { return UserInfo.IsAuthenticated; }
      set
      {
        UserInfo.IsAuthenticated = value;
        OnPropertyChanged("IsAuthenticated");

        if(value)
        {
          IsAdmin = UserInfo.IsInRole("Admin");
          IsGuest = UserInfo.IsInRole("Guest");
        }
      }
    }
  }
}

Поскольку допускаются ссылки только на другие PCL-проекты, может показаться непрактичным применять PCL с DI или другими общими ресурсами, которые имеются в ваших повторно используемых библиотеках. На самом деле это ограничение помогает введению четкого разделения обязанностей, даже повышая степень повторного использования ваших проектов: в будущем у вас могут появиться проекты, не использующие DI, но способные получить выигрыш от вашего PCL-проекта.

Я продемонстрирую принципы использования PCL с DI, используя приложение-пример. Это простая программа с двумя модулями — гостевым (guest) и основным (main), — которые загружаются по запросу с помощью подсистемы защиты на основе ролей. Доступная функциональность для этих модулей и представлений будет определяться ролями, назначенными как модулю, так и пользователю, вошедшему в систему. Учетных записей три: Admin, Guest и Jane Doe (пользователь). Бизнес-правило состоит в том, что учетная запись Guest никогда не получает доступа к основному модулю.

До появления PCL проекты решений могли ссылаться только на сборки для той же платформы.

Магия этого приложения — в его простоте; вы не найдете никакого отделенного кода в представлениях или объектов предметной области, «оскверненных» требованиями UI. В ViewModels хранится состояние, специфичное для UI, а презентаторы (presenters) управляют View/ViewModel, выделенными под конкретные задачи, через прикладную логику и DAL-уровни. Внутренние операции обрабатываются инфраструктурой, например щелчки кнопок, благодаря чему разработчик может сосредоточиться на прикладной логике. Разработчики, впервые увидевшие такое приложение, быстро научатся, откуда следует начинать изучение кода — всегда с Presenter.

PCL помогает значительно сократить количество проектов, которыми вам приходится управлять.

Жестко связанные компоненты

Жесткое связывание имеет место быть только между компонентами, показанными на рис. 1. Такое связывание ожидаемо в шаблоне Model-View-Presenter ViewModel (MVPVM) при использовании Prism и DI. В этом шаблоне модуль отвечает за создание экземпляра Presenter, который в свою очередь создает экземпляры необходимых View и ViewModel, связывая их так, как это требуется. Модули и их компоненты ничего не знают друг о друге.

Рис. 1. Жестко связанные компоненты

В идеале, я создал бы отдельные проекты для каждого модуля (ApplicationController, Main и Guest), чтобы получить возможность повторного использования этих модулей (с данной инфраструктурой) в других решениях. (Заметьте: поскольку и GuestPresenter, и MainPresenter совместно используют MainViewModel, мне также пришлось бы переместить этот ViewModel в общий проект, доступный GuestPresenter и MainPresenter. Как видите, количество проектов может очень быстро наращиваться во имя разделения обязанностей и повторного использования, особенно если кодировать для нескольких платформ.) Вы также увидите, что из-за сохранения простоты приложения-примера единственная возможность повторного использования какой-либо его части заключается в копировании-вставке. Ключ — в поиске баланса, и PCL помогает в этом.

На рис. 2 показано, как все уровни (презентационный, прикладной логики и доступа к данным) делят PCL-ресурсы. Поскольку защита — одна из основных задач практически в любом приложении, я могу безопасно интегрировать ее со всеми ее сущностями в UC0100, который будет моим PCL, и это будет, по всей видимости, единственный повторно используемый компонент в этом приложении-примере.

Рис. 2. Все уровни и общие PCL-ресурсы

Рассмотрение всех аспектов PCL, которые я буду использовать для управления своей инфраструктурой, выходит за рамки этой статьи; однако я расскажу о некоторых сценариях использования — соответствующие блоки на рис. 3 выделены серым фоном.

Рис. 3. Сценарии использования PCL

Сценарий использования «UC0100-020 База»

Чтобы улучшить расширяемость корпоративных приложений, полезно иметь единый стандарт подключения платформы. Это поможет новым разработчикам, а также опытным разработчикам, которые, возможно, не часто имели дело с каким-то модулем. Они смогут быстро войти в курс дела для выполнения поставленных задач, и на «выслеживание» кода будет потрачено минимальное время. С учетом этого я создал ModuleBase, предназначенный для работы с Prism-интерфейсом IModule — точнее, с методом Initialize. Проблема здесь в том, что IModule не был доступен, так как находится в сборке Prism (не PCL). Я хотел, чтобы база поддерживала средства протоколирования, поэтому интерфейсу ILogger должен быть предоставлен экземпляр до вызова метода Initialize. Этот ModuleBase теперь служит своего рода контрактом для всех модулей во всех решениях и проектах, так как реализует IModule-метод Initialize, показанный на рис. 4.

Рис. 4. Метод Initialize в классе ModuleBase

public class ModuleBase
{
  public ILogger Logger { get; set; }

  /// <summary>
  /// Вызывается диспетчером каталога Prism. Предоставляет
  /// точку подключения для регистрации типов/представлений
  /// и инициализирует модель представления.
  /// </summary>
  public virtual void Initialize()
  {
    try
    {
      // Предоставляет точки подключения для регистраций
      RegisterTypes();
      RegisterViews();

      InitializeModule();
    }
    catch (Exception ex)
    {
      Logger.Log("ERROR in [{0}] {1}{2}",
        GetType().Name, ex.Message, ex.StackTrace);
    }
  }

В отличие от PCL в моем проекте MsdnDemo.Phone есть специфичная для платформы ссылка на мою сборку Prism, поэтому код DI может находиться в классе PresentationModuleBase внутри этого проекта; это будет базовый класс для всех остальных классов. В реальном приложении этот класс, как и другие базовые классы DI, находился бы в отдельных повторно используемых проектах.

Чтобы улучшить расширяемость корпоративных приложений, полезно иметь единый стандарт подключения платформы.

Когда модуль разрешается (создается его экземпляр) контейнером DI, он присваивает Logger значение, сконфигурированное моим GwnBootstrapper. А когда задается значение свойства Logger, оно передает экземпляр базовому Logger, в конечном счете предоставляя ModuleBase ILogger ссылку для виртуальных методов Initialize (и других) (рис. 5).

Рис. 5. Задание свойства Logger

public class PresentationModuleBase : ModuleBase, IModule
{
  [Dependency]
  public override ILogger Logger {get;set;}

  [Dependency]
  public IUnityContainer Container { get; set; }

  [Dependency]
  public IRegionManager RegionManager { get; set; }

  [Dependency]
  public IEventAggregator EventAggregator { get; set; }

  [Dependency]
  public IRegionViewRegistry RegionViewRegistry { get; set; }

Примечание: так как встраивание конструктора происходит до встраивания аксессора set (в инфраструктуре Unity), в конструкторе ModuleBase не может быть выражений протоколирования.

Следующий код MainModule, производный от PresentationModuleBase, одинаков для всех трех платформ (Windows Phone 7, Silverlight и WPF):

public class MainModule : PresentationModuleBase
{
  protected override void RegisterViews()
  {
    base.RegisterViews();

    // Создаем экземпляр презентатора, который в свою очередь
    // создает экземпляры (разрешает) View и ViewModel
    var presenter = Container.Resolve<MainPresenter>();

    // Загружаем это представление в MainRegion
    RegionViewRegistry
      .RegisterViewWithRegion(MvpVm.MainRegion, () =>
      presenter.View);

    // Активизируем это представление после загрузки модуля
    RaiseViewEvent(MvpVm.MainModule,
      presenter.View.GetType().Name, ProcessType.ActivateView);
  }
}

Сценарий использования «UC0200-050 Сущности»

Применение Plain Old CLR Objects (POCO) обеспечивает максимальную степень повторного использования. Хотя PCL поддерживает INotifyPropertyChanged, я, возможно, не всегда буду использовать эти сущности с XAML. Может быть, мне понадобится задействовать их в проекте ASP.NET MVC 3 или в Entity Framework. Поэтому мои объекты UserEntity и SecurityEntity могут быть POCO-классами корпоративного уровня, которые можно легко повторно использовать в любом приложении на любой платформе, как показано на рис. 6.

Применение Plain Old CLR Objects (POCO) обеспечивает максимальную степень повторного использования.

рис. 6. Объекты UserEntity и SecurityEntity

public class UserEntity
{
  public int Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string PrimaryEmail { get; set; }
  public string Password { get; set; }
  public override string ToString()
  {
    return string.Format("{0} {1} ({2})", FirstName,
      LastName, Password);
  }
}

public class SecurityEntity : ISecurityViewModel
{
  private string _login;
  public int Id { get; set; }
  public bool IsAuthenticated { get; set; }
  public IEnumerable<string> Roles { get; set; }

  public string Login
  {
    get { return _login; }
    set
    {
      _login = value;
      Id = 0;
      IsAuthenticated = false;
      Roles = new List<string>();
    }
  }

  public bool IsInRole(string roleName)
  {
    if (Roles == null)
      return false;

    return Roles.FirstOrDefault(r => r == roleName) != null;
  }

  public bool IsInRole(string[] roles)
  {
    return roles.Any(role => IsInRole(role));
  }
}

Если вы предполагаете использовать UserEntity POCO в ViewModel, нужно создать оболочку. ViewModel будет генерировать уведомления, и «за кулисами» данные будут передаваться в POCO, как видно из фрагмента класса MainViewModel, показанном на рис. 7.

рис. 7. Передача данных в POCO

public UserEntity SelectedUser
{
  get { return _selectedUser; }
  set
  {
    _selectedUser = value;

    OnPropertyChanged("SelectedUser");
    OnPropertyChanged("FirstName");
    OnPropertyChanged("LastName");
    OnPropertyChanged("Password");
    OnPropertyChanged("PrimaryEmail");
  }
}

public string FirstName
{
  get { return _selectedUser.FirstName; }
  set
  {
    _selectedUser.FirstName = value;
    OnPropertyChanged("FirstName");
  }
}

Обратите внимание на то, что я показал только свойство FirstName, тем не менее и все остальные свойства UserEntity имеют сравнимую оболочку. Если я обновляю SelectedUser, он генерирует уведомление об изменении свойства для всех свойств UserEntity, так чтобы XAML уведомлялся об обновлении UI-полей, если это необходимо.

Сценарий использования «UC0100-060 События»

Агрегация событий, функция Prism, предоставляет средства для свободного связывания приложений, в то же время обеспечивая отличный способ взаимодействия между отделенными компонентами. Для этого каждому компоненту разрешается просто публиковать события, ничего не зная о подписчиках. Аналогично компоненты могут подписываться на события и обрабатывать ответы, ничего не зная о публикаторе.

Некоторые утверждают, что агрегация событий затрудняет отслеживание путей выполнения кода, но при должном протоколировании картина на деле прямо противоположная. Например, у меня есть раскрывающийся список в другом модуле, который позволяет заменять валюту, используемую в значениях денежных сумм. Компонент, над которым я работаю, зависит от этой настройки и применяется для расчета значения свойства ViewModel; кроме того, между моим компонентом и компонентом с раскрывающимся списком есть другие уровни логики (возможно, даже в отдельном модуле). Если это значение изменяется и мой компонент не уведомляется об этом, отладить такую ошибку будет проще при использовании агрегации событий, чем трассировкой потенциально всех уровней логики для выявления полного пути (или нескольких путей) с использованием других средств. Благодаря агрегации событий есть только две точки отладки: подписчик и публикатор. Если я протоколирую их обоих (как это делается в приложении-примере), область поиска проблема сужается до единственной точки сбоя.

Событие Prism должно находиться в проекте MsdnDemo.Phone из-за его зависимости от Prism CompositePresentationEvent; как таковое, оно не может содержаться в PCL:

public class MessageEvent :
  CompositePresentationEvent<MessageEventArgs> {
}

EventArgs, от которого зависят события, отлично обслуживается PCL, так как я могу иметь в нем общий набор аргументов события, который используется во множестве корпоративных приложений. На Рис. 8 показан MessageEventArgs, который обрабатывается предыдущим MessageEvent.

Рис. 8. MessageEventArgs обрабатывается MessageEvent

public class MessageEventArgs : EventArgs
{
  public object Sender { get; set; }
  public Enum Type { get; set; }
  public string Message { get; set; }
  public bool IsError { get; set; }
  public bool IsInvalid { get; set; }
  public int StatusCode { get; set; }
  public int ErrorCode { get; set; }
  private Exception _exception;
  public Exception Exception
  {
    get { return _exception; }
    set
    {
      _exception = value;
      IsError = true;
    }
  }
}

Сценарий использования «UC0100-100 MvpVmBase»

Наибольший выигрыш от повторного использования кода — надежность; в высшей степени вероятно, как в случае приложения MsdnDemo.Phone, что модульные тесты проверяют большую часть функциональности его кода. В сочетании с тем фактом, что это приложение уже эксплуатировалось какое-то время, дает какую-то гарантию того, что код стабилен и его можно использовать повторно. Когда членам вашей группы придется повторно использовать его под новые требования, это позволит ускорить разработку и повысить надежность нового продукта. Дополнительные выгоды: над кодом проще работать, новое приложение можно создать за несколько часов. Вы просто создаете нужное количество View, ViewModel и Presenter (применяя подходящие интерфейсы), и ваш новый модуль готов.

Классу PresentationPresenterBase (базовому классу Presenter) в приложении MsdnDemo.Phone нужно просто передать View и ViewModel, которые будут использоваться в Presenter. Заметьте, что на рис. 9 оба презентатора используют один и тот же ViewModel.

Презентаторы в MsdnDemo.Phone

Рис. 9. Презентаторы в MsdnDemo.Phone

PresentationPresenterBase наследует от MvpVmPresenter — класса в моем PCL, который выступает в роли контракта для всех общих Presenter и их ViewModel в рамках моих корпоративных приложений. ЕГо код показан на рис. 10.

рис. 10. Класс PresentationPresenterBase

public class MvpVmPresenter<TView,TViewModel>
  : IMvpVmPresenter
    where TView: IView
    where TViewModel : IMvpVmViewModel
{

  public IShell Shell { get; set; }
  public TView View { get; set; }
  public TViewModel ViewModel { get; set; }

  /// <summary>
  /// Вызывается при активизации представления
  /// (контроллером приложения)
  /// </summary>
  public virtual void ViewActivated(ViewEventArgs e){}
}

Как и в случае ILogger, на который ссылался класс PresentationModuleBase, мне пришлось аналогичным образом обернуть и этот ILogger (передавая экземпляр базовому классу), а также Shell, View и ViewModel, поскольку они встраиваются через DI.

PresentationPresenterBase подобно PresentationModuleBase отвечает за обработку всех сервисов, связанных с DI, так как в нем есть ссылки на сборки, специфичные для платформ Prism и Unity. Впоследствии обязанности их базовых классов в PCL можно будет расширить.

Обратите внимание на рис. 11: когда контейнер DI разрешает указанный ViewModel (TViewModel), он задает привязки для ButtonCommand в ViewModel (строка 99). Это позволяет Presenter обрабатывать все щелчки кнопок через ExecuteButtonCommandHandler в строке 56 на рис. 9.

ViewModel могут быть общими, как в случае MainViewModel, и каждый Presenter может использовать MainViewModel по-разному, в зависимости от его требований.

Базовый класс Presenter

Рис. 11. Базовый класс Presenter

То, что объекты Presenter обрабатывают щелчки кнопок (в отличие от ViewModel), — одно из многих преимуществ, которые в свое время заставили архитекторов перейти от шаблонов Presentation-Model-Pattern и Application-Model-Pattern к шаблону Model-View-Presenter.

ViewModel могут быть общими, как в случае MainViewModel, и каждый Presenter может использовать MainViewModel по-разному, в зависимости от его требований. Если бы я перенес обработку щелчков кнопок в ViewModel, мне пришлось бы использовать условные выражения, которые со временем привели бы к разбуханию кода и потребовали регрессивного тестирования, так как новый код мог бы влиять на логику модулей, не задействованных в цикле разработки. Исключив из View и ViewModel прикладную логику, я обеспечиваю максимальную степень их повторного использования.

Заключение

PCL помогает значительно уменьшить количество проектов, необходимых для ориентированных на несколько платформ приложений, в то же время предоставляя контракты для приложений корпоративного уровня. Ограничение, связанное с невозможностью ссылок на сборки, отличные от PCL, даже полезно для повышения дисциплины кодирования, позволяя четко разделять проекты по их обязанностям. Сочетая эти преимущества PCL с DI, вы сможете добиться максимальной степени повторного использования отделенных проектов, проверенных временем (и доступными модульными тестами), повысить масштабируемость, расширяемость и ускорить работу над новыми задачами.


Билл Кратохвил (Bill Kratochvil) — независимый подрядчик, ведущий технолог и архитектор элитной группы разработчиков, работающей на конфиденциальным проектом для ведущей компании в области медицинской промышленности. Его собственная компания, Global Webnet LLC, находится в Амарилло, штат Техас.

Выражаю благодарность за рецензирование статьи экспертам Кристине Хелтон (Christina Helton) и Дэвиду Кину (David Kean).