Как стать автором
Обновить

Руководство разработчика Prism — часть 6, продвинутые сценарии MVVM

Время на прочтение 36 мин
Количество просмотров 19K
Автор оригинала: microsoft patterns & practices
Оглавление
  1. Введение
  2. Инициализация приложений Prism
  3. Управление зависимостями между компонентами
  4. Разработка модульных приложений
  5. Реализация паттерна MVVM
  6. Продвинутые сценарии MVVM
  7. Создание пользовательского интерфейса
    1. Рекомендации по разработке пользовательского интерфейса
  8. Навигация
    1. Навигация на основе представлений (View-Based Navigation)
  9. Взаимодействие между слабо связанными компонентами

В предыдущей главе было описано, как создать основные элементы паттерна MVVM, разделив интерфейс пользователя, логику представления и бизнес-логику по отдельным классам (представление, модель представления и модель), реализовать между ними взаимодействие (посредством привязки данных, команд и интерфейсов валидации данных), организовать их создание и настройку.

Реализация паттерна MVVM, используя эти основные элементы, скорее всего, подойдёт под большинство сценариев в вашем приложении. Однако можно встретиться с более сложными сценариями, которые требуют расширения паттерна MVVM, или применения более продвинутых методов. Это, скорее всего, произойдёт, если ваше приложение будет большим или сложным, но с этим можно встретиться и во многих небольших приложениях. Библиотека Prism предоставляет компоненты, которые реализуют многие из этих методов, позволяя вам легко использовать их в ваших приложениях.

Эта глава описывает некоторые сложные сценарии и то, как их поддерживает паттерн MVVM. Следующий раздел показывает, как команды могут быть объединены в цепочки или связаны с дочерними представлениями, а также как они могут быть расширены для поддержки пользовательских требований. Следующие разделы описывают, как обрабатывать асинхронные запросы данных и последующее взаимодействие с пользовательским интерфейсом, а также как обработать запросы взаимодействия между представлением и моделью представления.

Раздел «Продвинутое создание и настройка», даёт представление о том, как создавать и настраивать компоненты при использовании контейнера внедрения зависимости, такого как Unity Application Block (Unity), или Managed Extensibility Framework (MEF). Заключительный раздел описывает, как можно протестировать приложения MVVM, и даёт представление о модульном тестировании классов модели и модели представления, а также о тестировании поведений.

Команды


Команды дают способ разделить логику реализации команды от её представления в UI. Привязка данных или поведения дают возможность декларативно связывать элементы в представлении с командами, предоставленными моделью представления. В разделе, «Команды» в главе 5, было описано, как команды могут быть реализованы в виде объектов команды, или как методов команды в модели представления, и как они могут быть вызваны элементами управления в представлении: используя поведения, или свойство Command, которое есть у некоторых элементов управления.
Заметка. Маршрутизируемые команды WPF.
Hужно отметить, что команды, реализованные как объекты команд, или методы команд в паттерне MVVM несколько отличаются от встроенной реализации WPF команд, названных маршрутизируемыми командами (у Silverlight нет никаких маршрутизируемых реализаций команд). Маршрутизируемые команды WPF передают сообщение о команде через элементы в дереве UI. Поэтому, сообщения направляются вверх или вниз по дереву UI от элемента с фокусом, или к явно указанному целевому элементу. По умолчанию, они не передаются к компонентам за пределами дерева UI, таким как модель представления, связанная с представлением. Однако маршрутизируемые команды WPF могут использовать обработчик, определенный в code-behind представления, чтобы направить вызов команды классу модели представления.

Составные команды


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

Например, если ваше приложение позволяет пользователю редактировать разнообразные элементы одновременно, можно позволить пользователю сохранять все элементы, используя единственную команду, представленную кнопкой на панели инструментов. В этом случае, команда Save All вызовет каждую из команд Save, реализованную экземпляром модели представления каждого элемента, как показано на следующей иллюстрации.

Реализация составной команды SaveAll.

Prism поддерживает такой сценарий через класс CompositeCommand.

Класс CompositeCommand представляет команду, которая складывается из разнообразных дочерних команд. Когда вызывается составная команда, каждая из её дочерних команд вызывается поочередно. Это полезно в ситуациях, когда вы хотите представить группу команд как единственную команду в UI, или где-то, где вы хотите вызвать несколько команд, как одну логическую команду.

Например, класс CompositeCommand используется в Stock Trader RI, чтобы реализовать команду SubmitAllOrders, представленную кнопкой Submit All в buy/sell представлении. Когда пользователь нажимает кнопку Submit All, выполняется каждая SubmitCommand, определенная buy/sell транзакцией.

Класс CompositeCommand обслуживает список дочерних команд (экземпляры DelegateCommand ). Метод Execute класса CompositeCommand просто вызывает метод Execute на каждой из дочерних команд поочередно. Метод CanExecute так же вызывает метод CanExecute каждой дочерней команды, но если какая-либо из дочерних команд не может быть выполнена, метод CanExecute возвратит false. Другими словами, по умолчанию, CompositeCommand может быть выполнен только тогда, когда могут быть выполнены все дочерние команды.

Регистрация и удаления дочерних команд


Дочерние команды регистрируются или удаляются методами RegisterCommand и UnregisterCommand. В Stock Trader RI, например, команды Submit и Cancel для каждого buy/sell заказа регистрируются в составных командах SubmitAllOrders и CancelAllOrders, как показано в следующем примере (см. класс OrdersController ).

commandProxy.SubmitAllOrdersCommand.RegisterCommand(
    orderCompositeViewModel.SubmitCommand);
commandProxy.CancelAllOrdersCommand.RegisterCommand(
    orderCompositeViewModel.CancelCommand);

Заметка
Предыдущий объект commandProxy обеспечивает доступ экземпляра к командам составного объекта Submit и Cancel, которые определяются статически. Для получения дополнительной информации, смотрите файл StockTraderRICommands.cs .

Выполнение команд в активных дочерних представлениях

Часто бывает, что ваше приложение должно показать коллекцию дочерних представлений в пределах UI, где у каждого дочернего представления будет соответствующая модель представления, которая, в свою очередь, может реализовать одну или более команд. Составные команды могут использоваться для представления команд, реализованных дочерними представлениями, и могут помочь скоординировать то, как они будут вызываться изнутри родительского представления. Чтобы поддерживать эти сценарии, классы DelegateCommand и CompositeCommand были разработаны с учётом работы с регионами Prism.

Регионы Prism (описанные в разделе, «Регионы» в Главе 7) дают возможность дочерним представлениям быть связанными с регионами в UI. Они часто используются, чтобы отделить разметку дочерних представлений от их региона и его позиции в UI. Регионы основаны на именованных заполнителях, которые присоединены к определенным элементам управления разметкой. Следующая иллюстрация показывает пример, где каждое дочернее представление было добавлено к региону EditRegion, и разработчик UI захотел использовать элемент TabControl, чтобы разместить представления в этой области.

Определение EditRegion, используя элемент управления Tab control.

Составные команды на родительском уровне представления часто используются, чтобы скоординировать то, как вызываются команды на дочернем уровне представления. В некоторых случаях, вам захочется, чтобы команды для всех показанных представлений выполнялись, как в примере команды Save All, описанном ранее. В других случаях вы захотите, чтобы команда была выполнена только на активном представлении. В этом случае, составная команда выполнит дочерние команды только на представлениях, которые являются активными. Например, можно захотеть реализовать команду Zoom на панели инструментов, которая заставляет масштабироваться только активный в настоящий момент элемент, как показано в следующей схеме.

Определение EditRegion, используя элемент управления Tab control.

Для поддержки этого сценария, Prism предоставляет интерфейс IActiveAware. Интерфейс IActiveAware определяет свойство IsActive, которое возвращает true, когда элемент управления активен, и событие IsActiveChanged, которое генерируется всякий раз, когда активное состояние изменяется.

Можно реализовать интерфейс IActiveAware на дочерних представлениях или моделях представления. Это, прежде всего, используется для того, чтобы отследить активное состояние дочернего представления в области. Является ли представление активным, определяется адаптером области, который контролирует представления в пределах определённого элемента управления областью. К примеру, для элемента TabControl, показанного ранее, есть адаптер региона, который устанавливает представление на выбранной в настоящий момент вкладке как активное.

Класс DelegateCommand также реализует интерфейс IActiveAware. Определяя true для параметра monitorCommandActivity в конструкторе, CompositeCommand может быть сконфигурирован так, чтобы оценить активное состояние дочернего элемента DelegateCommand (в дополнение к состоянию CanExecute ). Когда эти параметры будут установлены в true, класс CompositeCommand рассмотрит активное состояние каждого дочернего элемента DelegateCommand, определяя возвращаемое значение для метода CanExecute и выполняя дочерние команды в пределах метода Execute.

Когда параметр monitorCommandActivity установлен в true, класс CompositeCommand показывает следующее поведение:
  • CanExecute. Возвращает true только тогда, когда все активные команды могут быть выполнены. Дочерние команды, являющиеся неактивными, не обрабатывается.
  • Execute. Выполняет все активные команды. Дочерние команды, которые не активны, не обрабатывается.

Можно использовать эту функциональность, чтобы реализовать пример, описанный ранее. Реализовывая интерфейс IActiveAware в ваших дочерних моделях представления, вы будете уведомлены, когда ваше дочернее представление в регионе станет активным или неактивным. Когда активное состояние дочернего представления изменяется, можно обновить активное состояние дочерних команд. Затем, когда пользователь вызывает составную команду Zoom, она будет вызвана только на активном дочернем представлении.

Привязка команд в пределах коллекций


Другой общий сценарий, с которым вы будете часто встречаться, отображая коллекцию элементов в представлении — когда вы нуждаетесь в UI для каждого элемента в коллекции, который будет связан с командой на родительском уровне представления (вместо уровня элемента).

Например, в приложении, показанном на следующей иллюстрации, представление показывает коллекцию элементов в ListBox. Шаблон данных, используемый, чтобы показать каждый элемент, определяет кнопку Delete, которая позволяет пользователю удалять отдельные элементы из коллекции.

Привязка команд в пределах коллекции.

Поскольку модель представления реализует команду Delete, проблема состоит в том, чтобы присоединить кнопку Delete в UI для каждого элемента, к команде Delete, реализованной моделью представления. Трудность возникает из-за того, что контекст данных для каждого из элементов в ListBox ссылается на элемент в коллекции вместо родительской модели представления, которая реализует команду Delete.

Один из подходов к решению этой проблемы – привязать кнопку в шаблоне данных к команде в родительском представлении, используя свойство ElementName для гарантии, что привязка осуществляется относительно родительского элемента управления, а не относительно шаблона данных. Следующий XAML иллюстрирует этот метод.

<Grid x:Name="root">
    <ListBox ItemsSource="{Binding Path=Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Button Content="{Binding Path=Name}"
                        Command="{Binding ElementName=root,
                        Path=DataContext.DeleteCommand}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Содержание кнопки в шаблоне данных связано со свойством Name элемента в коллекции. Однако команда для кнопки связывается через контекст данных корневого элемента с командой Delete. Это позволяет кнопке быть связанной с командой на родительском уровне представления вместо уровня элемента. Можно использовать свойство CommandParameter, чтобы задать элемент, к которому будет применена команда, или можно реализовать команду так, чтобы работать с выбранным в настоящий момент элементом (через CollectionView ).

Поведения с привязкой команд


В Silverlight 3 и более ранних версиях, Silverlight не предоставлял элементы управления, поддерживающие команды. Интерфейс ICommand был доступен, но никакие элементы управления не реализовывали свойство Command, чтобы позволить им быть привязанными к реализации ICommand. Чтобы преодолеть это ограничение и поддержать MVVM паттерн в Silverlight 3, библиотека Prism (версия 2.0) обеспечила механизм, позволяющий любому элементу управления Silverlight быть связанным с объектом команды, используя присоединенное поведение. Этот механизм также работал в WPF, что позволяло реализациям модели представления быть снова и в приложениях Silverlight, и в WPF.

Следующий пример показывает, как в Prism используется присоединённое поведение, чтобы привязать объект команды, определённый на модели представления к событию щелчка кнопки.

<Button Content="Submit All"
   prism:Click.Command="{Binding Path=SubmitAllCommand}"
   prism:Click.CommandParameter="{Binding Path=TickerSymbol}" />

В Silverlight 4 добавилась поддержка свойства Command во всех унаследованных от Hyperlink и ButtonBase элементах управления, позволяя им быть привязанными непосредственно к объекту команды таким же образом как в WPF. Использование свойства Command для этих элементов управления описывается в разделе "Commands" в главе 5, "Implementing the MVVM Pattern". Однако, присоединённое поведение команды остаётся в библиотеке Prism по причинам обратной совместимости и для поддержки разработки пользовательских поведений, как будет описано позже.

Подход с использованием поведения является обычным методом для реализации и инкапсуляции интерактивного поведения и может быть легко применён к элементам управления в представлении. Использование поведений для поддержки команд, как было показано ранее, является только одним из многих сценариев, которые могут поддерживать поведения. Microsoft Expression Blend предоставляет множество поведений, включая InvokeCommandAction и CallMethodAction, описанные в разделе, «Invoking Command Methods from the View» в главе 5, "Implementing the MVVM Pattern", а Expression Blend Behaviors SDK предоставляет возможность разработки пользовательских поведений. С помощью Expression Blend можно легко создавать и редактировать поведения, что делает очень лёгкой задачу добавлений в приложение. Для получения дополнительной информации о разработке пользовательских поведений в Expression Blend, см. "Creating Custom Behaviors" на MSDN.

Хотя введение поддерживающих команды элементов управления в Silverlight 4 и Expression Blend Behaviors SDK устраняют большую часть потребности в поведениях Prism, можно найти полезным их компактный синтаксис и реализация, а также их возможность лёгкого расширения.

Применение поведений

Поведения команд основаны на паттерне присоединённого поведения. Этот паттерн соединяет события, вызываемые элементами управления, и объекты команд, предоставленных моделью представления. Поведение команды состоит из двух частей: присоединённое свойство и объект поведения. Присоединённое свойство устанавливает отношения между целевым элементом управления и объектом поведения. Объект поведения контролирует целевой элемент управления и возбуждает события, основанные на событиях или изменениях состояния в элементе управления или в модели представления.

Команды, инициируемые при событии Click в ButtonBase -элементах управления, предоставляет класс ButtonBaseClickCommandBehavior. Следующая иллюстрация показывает отношения между ButtonBase, ButtonBaseClickCommandBehavior, и объектом ICommand, предоставленным моделью представления.

Перенаправление события ButtonClick в IComman.

Ваше приложение, возможно, должно будет вызвать команды не только при событии Click из ButtonBase, или, возможно, нужно будет настроить то, как поведение взаимодействует с целевым элементом управления или моделью представления, с которой оно связывается. В этих случаях вы должны будете определить свою собственную присоединённую реализацию свойства и/или поведения.

Библиотека Prism предоставляет класс CommandBehaviorBase , чтобы облегчить создание поведения, взаимодействующего с объектами ICommand . Этот класс вызывает команду и наблюдает за изменениями в событии CanExecuteChanged команды, и может использоваться для расширения поддержки команд как в Silverlight, так и в WPF.

Чтобы создать пользовательское поведение, создайте класс, унаследованный от CommandBehaviorBase и укажите элемент управления, который вы хотите контролировать. Параметр типа для этого класса определяет тип элемента управления, к которому может быть присоединено поведение. В конструкторе класса можно подписаться на события элемента управления, которых вы хотите отслеживать. Следующий пример показывает реализацию класса ButtonBaseClickCommandBehavior .

public class ButtonBaseClickCommandBehavior : CommandBehaviorBase<ButtonBase> {
    public ButtonBaseClickCommandBehavior(ButtonBase clickableObject) : base(clickableObject) {
        clickableObject.Click += OnClick;
    }
    private void OnClick(object sender, System.Windows.RoutedEventArgs e) {
        ExecuteCommand();
    }
}

Используя класс CommandBehaviorBase , можно определить пользовательские классы поведений. Это позволит вам настраивать то, как поведение будет взаимодействовать с целевым элементом управления или командой, предоставленной моделью представления. Например, вы можете создать поведение, которое вызывает связанную команду, основанную на различных событиях элемента управления, или, основываясь на состоянии CanExecute связанной команды, изменяет визуальное состояние элемента управления.

Чтобы поддерживать декларативное присоединение поведения команды к целевому элементу управления, используется присоединённое свойство. Присоединённое свойство позволяет поведению быть присоединённым к элементу управления в XAML. Оно управляет созданием и ассоциацией реализации поведения с целевым элементом управления. Присоединённое свойство определяется в пределах статического класса. В Prism поведения команд основаны на том соглашении, что имя статического класса отсылает к событию, которое используется, чтобы вызвать команду. Имя присоединённого свойства указывает на тип объекта, к которому привязываются данные. Поэтому, поведение, приведённое раннее, использовал статический класс под названием Click , который определяет присоединённое свойство под названием Command . Это позволяет использование Click.Command синтаксис, показанный ранее.

Объект поведения команды фактически также связывается с целевым элементом управления через присоединённое свойство. Однако это присоединённое свойство является приватным в статическом классе и не видно разработчику.

public static readonly DependencyProperty CommandProperty =
                              DependencyProperty.RegisterAttached(
                                      "Command",
                                      typeof(ICommand),
                                      typeof(Click),
                                      new PropertyMetadata(OnSetCommandCallback));

private static readonly DependencyProperty ClickCommandBehaviorProperty =
                              DependencyProperty.RegisterAttached(
                                      "ClickCommandBehavior",
                                      typeof(ButtonBaseClickCommandBehavior),
                                      typeof(Click),
                                      null);

Реализация присоединённого свойства Command создаёт экземпляр класса ButtonBaseClickCommandBehavior , через метод обратного вызова OnSetCommandCallback , как показано в следующем примере.

private static void OnSetCommandCallback(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e) {
     ButtonBase buttonBase = dependencyObject as ButtonBase;
     if (buttonBase != null)  {
        ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase);
        behavior.Command = e.NewValue as ICommand;
     }
}
private static void OnSetCommandParameterCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) {
    ButtonBase buttonBase = dependencyObject as ButtonBase;
    if (buttonBase != null) {
        ButtonBaseClickCommandBehavior behavior = GetOrCreateBehavior(buttonBase);
        behavior.CommandParameter = e.NewValue;
    }
}
private static ButtonBaseClickCommandBehavior GetOrCreateBehavior(ButtonBase buttonBase ) {
    ButtonBaseClickCommandBehavior behavior =
        buttonBase.GetValue(ClickCommandBehaviorProperty) as ButtonBaseClickCommandBehavior;
    if ( behavior == null ) {
        behavior = new ButtonBaseClickCommandBehavior(buttonBase);
        buttonBase.SetValue(ClickCommandBehaviorProperty, behavior);
    }
    return behavior;
}

Для получения более подробной информации о присоединяемых свойствах, смотрите Attached Properties Overview на MSDN.

Обработка асинхронных взаимодействий


Ваша модель представления часто должна будет взаимодействовать со службами и компонентами внутри приложения асинхронно. Особенно, если вы создаёте приложение Silverlight, или взаимодействуете с веб-сервисами, или другими ресурсами по сети, или если приложение выполняет фоновые вычисления, или обращение к файловой системе. Выполнение этих операций асинхронно гарантирует, что ваше приложение останется отзывчивым для пользователя.

Когда пользователь инициирует асинхронный запрос или фоновую задачу, трудно предсказать время (или успешность) их выполнения, и то, какой поток возвратит ответ первым. Поскольку UI может быть обновлён только в собственном потоке, вам будет необходимо частое его обновление используя диспетчеризацию запроса на потоке UI.

Получение данных и взаимодействие с веб-сервисами


Взаимодействуя с веб-сервисами или другими технологиями удалённого доступа, вы будете часто встречаться с паттерном IAsyncResult . В этом паттерне, вместо того, чтобы вызвать метод, например GetQuestionnaire , используется пара методов: BeginGetQuestionnaire и EndGetQuestionnaire . Чтобы инициировать асинхронный запрос, вы вызываете BeginGetQuestionnaire . Чтобы получить результаты или определить, было ли исключение, вы вызываете EndGetQuestionnaire при окончании выполнения задачи.
Заметка
В .NET Framework 4.5 были добавлены новые ключевые слова await и async и новый паттерн взаимодействия *Async . Подробнее можно почитать в статье "Asynchronous Programming with Async and Await (C# and Visual Basic)".

Чтобы определить, когда необходимо вызвать EndGetQuestionnaire , можно сделать запрос относительно завершения задачи, или (предпочтительно) задать обратный вызов при вызове BeginGetQuestionnaire . При подходе с обратным вызовом, он будет вызван при завершении задачи, позволяя вызвать EndGetQuestionnaire оттуда, как показано ниже.

IAsyncResult asyncResult = this.service.BeginGetQuestionnaire(
	GetQuestionnaireCompleted, 
	null // Объект состояния, не используется в данном примере
);
private void GetQuestionnaireCompleted(IAsyncResult result) {
   try {
     questionnaire = this.service.EndGetQuestionnaire(ar);
   }
   catch (Exception ex) {
     // Сделать что-то для обработки ошибки.
   }
}

Важно отметить, что при вызове метода End* (в данном случае, EndGetQuestionnaire ), будут вброшены любые исключения, которые произошли во время выполнения запроса. Ваше приложение должно обработать их и, возможно, сообщить о них ориентированным на многопоточное исполнение способом через UI. Если вы не обработаете их, то поток завершится, и вы будете не в состоянии обработать результаты.

Поскольку ответ обычно не находится на потоке UI, если вы планируете изменять что-то, что будет влиять на состояние UI, то вы должны будете диспетчеризировать ответ в поток UI, используя или Dispatcher потока или объекты SynchronizationContext . В WPF и Silverlight, обычно используются Dispatcher .

В следующем примере кода объект Questionnaire возвращается асинхронно, и затем устанавливается как контекст данных для QuestionnaireView . В Silverlight можно использовать метод CheckAccess диспетчера, чтобы определить, находитесь ли вы в потоке UI. Если нет, то необходимо использовать метод BeginInvoke , чтобы выполнить запрос в потоке UI.

var dispatcher = System.Windows.Deployment.Current.Dispatcher;
if (dispatcher.CheckAccess()) {
    QuestionnaireView.DataContext = questionnaire;
}
else {
    dispatcher.BeginInvoke(() => { Questionnaire.DataContext = questionnaire; });
}

MVVM RI показывает пример того, как использовать интерфейс службы, основанный на IAsyncResult , подобный предыдущим примерам. В нём также производится обертка службы, чтобы предоставить более простой механизм обратного вызова и для обработки диспетчеризации обратного вызова в поток вызывающей стороны. Например, следующий пример кода показывает извлечение анкетного опроса.

this.questionnaireRepository.GetQuestionnaireAsync(
    result => {
        this.Questionnaire = result.Result;
    });

Объект result возвращает обертку результата, полученную в дополнение к ошибкам, которые, возможно, произошли. Следующий пример кода показывает, как ошибки могут быть обработаны.

this.questionnaireRepository.GetQuestionnaireAsync(
    result => {
        if (result.Error == null) {
          this.Questionnaire = result.Result;
          ...
        }
        else {
          // Handle error.
        }
    })

Паттерны взаимодействия с пользователем


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

Есть разнообразные способы взаимодействовать с пользователем в этих случаях, но реализация их в стиле MVVM, которая сохраняет чистое разделение ответственности, может быть сложной. Например, в не-MVVM приложении, вы использовали бы класс MessageBox в файле code-behind UI, чтобы просто запросить у пользователя ответ. В приложении MVVM это будет не очень правильным, так как нарушает разделение ответственности между представлением и моделью представления.

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

Есть два общих подхода к реализации таких видов взаимодействия с пользователем в паттерне MVVM. Один подход заключается в реализации службы, которая может использоваться моделью представления, чтобы инициировать взаимодействие с пользователем, таким образом, сохраняя независимость от реализации представления. Другой подход использует события, генерируемые моделью представления для выражения намерения взаимодействовать с пользователем, наряду с компонентами в представлении, которые связываются с этими событиями и управляют визуальными аспектами взаимодействия. Оба этих подхода описываются в следующих разделах.

Использование службы взаимодействия


В этом подходе модель представления полагается на компонент службы взаимодействия для инициирования взаимодействия с пользователем через окно сообщения. Этот подход предоставляет чистое разделение ответственности и тестируемость, инкапсулируя визуальную реализацию взаимодействия в отдельном компоненте службы. Как правило, у модели представления есть зависимость от интерфейса службы взаимодействия. Это зачастую достигается через внедрение зависимости или локатор служб.

После того, как у модели представления есть ссылка на службу взаимодействия, она может программно запросить взаимодействие с пользователем всякий раз, когда необходимый. Служба взаимодействия реализует визуальные аспекты взаимодействия, как показано на следующей иллюстрации. Использование ссылки на интерфейс в модели представления позволяет использовать различные реализации, в зависимости от требований реализации пользовательского интерфейса. Например, могут быть предоставлены реализации службы взаимодействия для WPF и Silverlight, что позволит увеличить повторное использование логики представления приложения.

Использование службы, для взаимодействия с пользователем.

Модальные взаимодействия, такие как MessageBox или модальное всплывающее окно для получения определённого ответа, могут быть реализованы синхронным способом, используя вызов блокирующего метода, как показано в следующем примере.

var result =
    interactionService.ShowMessageBox(
        "Are you sure you want to cancel this operation?",
        "Confirm",
        MessageBoxButton.OK);
if (result == MessageBoxResult.Yes) {
    CancelRequest();
}

Однако, один из недостатков этого подхода - то, что он использует модель синхронного программирования, которая не совместима с другими механизмами взаимодействия в Silverlight. Это может привести к многочисленным трудностям реализации службы взаимодействия. Альтернативная асинхронная реализация позволяет модели представления обеспечить обратный вызов для обработки завершения взаимодействия. Следующий код иллюстрирует этот подход.

interactionService.ShowMessageBox(
    "Are you sure you want to cancel this operation?",
    "Confirm",
    MessageBoxButton.OK,
    result => {
        if (result == MessageBoxResult.Yes) {
            CancelRequest();
        }
    });

Асинхронный подход обеспечивает большую гибкость при реализации службы взаимодействия, позволяя создавать модальные и немодальные взаимодействия. Например, в WPF может использоваться класс MessageBox , чтобы реализовать действительно модальное взаимодействие с пользователем; тогда как в Silverlight всплывающее окно может использоваться, чтобы реализовать псевдомодальное взаимодействие.

Использование объектов запроса взаимодействия


Другой подход к реализации простого взаимодействия с пользователем – позволение модели представления обращаться с запросом взаимодействия непосредственно к представлению через объект запроса. Объект запроса взаимодействия инкапсулирует детали запроса и его ответ, и связывается с представлением через события. Представление подписывается на эти события, чтобы инициировать пользовательскую часть взаимодействия. Представление будет обычно инкапсулировать пользовательское взаимодействие в поведении, которое связывается с данными к объекту запроса взаимодействия, предоставленному моделью представления, как показано на следующей иллюстрации.

Использование объектов запроса взаимодействия.

Этот подход обеспечивает простой, но гибкий механизм, который сохраняет чистое разделение между моделью представления и представлением – это позволяет модели представления инкапсулировать логику представления, включая любое необходимое взаимодействие с пользователем, а представлению – полностью инкапсулировать визуальные аспекты взаимодействия. Реализация модели представления, включая ее ожидаемые взаимодействия с пользователем посредством представления, может быть легко протестирована, и у разработчика UI есть большой выбор реализации взаимодействия в пределах представления через использование различных поведений.

Этот подход является непротиворечивым с паттерном MVVM, позволяя представлению отражать изменения состояния, за которыми оно наблюдает в модели представления, и использовать двухстороннюю привязку для передачи данных между ними. Инкапсуляция не визуальных элементов взаимодействия в объекте запроса взаимодействия, и использование соответствующего поведения для управления визуальными элементами, очень подобны объектам команды, и объектам поведения.

Библиотека Prism непосредственно поддерживает этот паттерн через интерфейс IInteractionRequest и класс InteractionRequest . Интерфейс IInteractionRequest определяет событие, чтобы инициировать взаимодействие. Поведения в представлении связываются с этим интерфейсом и подписываются на событие, которое он представляет. Класс InteractionRequest реализует интерфейс IInteractionRequest и определяет два метода Raise , чтобы позволить модели представления инициировать взаимодействия и для определения контекста запроса, и, дополнительно, делегата обратного вызова.

Инициирование запросов взаимодействия от модели представления

Класс InteractionRequest координирует взаимодействие модели представления с представлением во время запроса взаимодействия. Метод Raise позволяет модели представления инициировать взаимодействие и определять объект контекста (типа T ) и метод обратного вызова, который вызывают после того, как взаимодействие завершается. Объект контекста позволяет модели представления передавать данные и состояние представлению, для использования во время взаимодействия с пользователем. Если метод обратного вызова был определен, объект контекста будет передаваться обратно в модель представления. Это позволяет любым изменениям, сделанными пользователем, быть переданными в модель представления.
public interface IInteractionRequest {
    event EventHandler<InteractionRequestedEventArgs> Raised;
}

public class InteractionRequest<T> : IInteractionRequest {
    public event EventHandler<InteractionRequestedEventArgs> Raised;

    public void Raise(T context, Action<T> callback) {
        var handler = this.Raised;
        if (handler != null) {
            handler(
                this,
                new InteractionRequestedEventArgs(context, () => callback(context)));
        }
    }
}

Prism предоставляет предопределённые классы контекста, которые поддерживают общие сценарии запроса взаимодействия. Класс Notification является базовым классом для всех объектов контекста. Он нужен в случае, когда запрос взаимодействия используется для уведомления пользователя о важном событии в приложении. Он имеет два свойства – Title и Content , которые будет показаны пользователю. Как правило, уведомления являются односторонними, таким образом, не ожидается, что пользователь изменит эти значения во время взаимодействия.

Класс Confirmation наследуется от класса Notification и добавляет третье свойство – Confirmed , которое используется, чтобы показать, что пользователь подтвердил или отменил запрос. Класс Confirmation используется, чтобы реализовать взаимодействия стиля MessageBox , где необходимо получить ответ да/нет от пользователя. Можно определить пользовательский класс контекста, который наследуется от класса Notification , для инкапсуляции данных и состояний, которые необходимы для взаимодействия с пользователем.

Чтобы использовать класс InteractionRequest , класс модели представления должен создать экземпляр InteractionRequest и определить свойство только для чтения, чтобы позволить представлению связаться с ним. Когда модель представления захочет инициировать запрос, она вызовет метод Raise , передавая объект контекста и, дополнительно, делегат обратного вызова.

public IInteractionRequest ConfirmCancelInteractionRequest {
    get {
        return this.confirmCancelInteractionRequest;
    }
}

this.confirmCancelInteractionRequest.Raise(
    new Confirmation("Are you sure you wish to cancel?"),
    confirmation => {
        if (confirmation.Confirmed) {
            this.NavigateToQuestionnaireList();
        }
    });
}

MVVM Reference Implementation (MVVM RI) иллюстрирует, как используются интерфейс IInteractionRequest и класс InteractionRequest , чтобы реализовать взаимодействие модели представления и представления в приложении (см. QuestionnaireViewModel.cs ).

Использование поведений для реализации взаимодействия с пользователем

Поскольку объект запроса взаимодействия представляет логическое взаимодействие, точный интерфейс для взаимодействия определяется в представлении. Поведения часто используются, чтобы инкапсулировать внешний вид взаимодействия с пользователем. Это позволяет разработчику UI выбирать соответствующее поведение и связывать его с объектом запроса взаимодействия в модели представления.

Представление должно быть настроено для обнаружения события запроса взаимодействия и предоставления соответствующего визуального представления для запроса. Microsoft Expression Blend Behaviors Framework поддерживает понятие триггеров и действий. Триггеры используются, чтобы инициировать действия всякий раз, когда определённое событие случается.

Стандартный EventTrigger , предоставленный Expression Blend, может использоваться, чтобы следить за событием запроса взаимодействия, связываясь с объектами запроса взаимодействия, представленными моделью представления. Однако, Prism определяет пользовательский EventTrigger , названный InteractionRequestTrigger , который автоматически соединяется с соответствующим Raised событием интерфейса IInteractionRequest . Это уменьшает количество XAML и уменьшает шанс непреднамеренного введения неправильного имени события.

После того, как событие генерируется, InteractionRequestTrigger вызовет указанное действие. Для Silverlight Prism предоставляет класс PopupChildWindowAction , который показывает всплывающее окно пользователю. Когда дочернее окно показывается, его контекст данных устанавливается в параметр контекста запроса взаимодействия. Используя свойство ContentTemplate класса PopupChildWindowAction , можно задать шаблон данных, чтобы определить разметку UI, которая будет использоваться для свойства Content объекта контекста. Заголовок всплывающего окна связывается со свойством Title объекта контекста.
Заметка
По умолчанию, определенный тип всплывающего окна, показанного классом PopupChildWindowAction , зависит от типа объекта контекста. Для объекта контекста Notification показывается NotificationChildWindow , в то время как для объекта контекста ConfirmationConfirmationChildWindow . NotificationChildWindow показывает простое всплывающее окно, чтобы показать уведомление, в то время как ConfirmationChildWindow также содержит кнопки OK и Cancel, для получения ответа пользователя. Можно переопределить это поведение, определяя всплывающее окно, используя свойство ChildWindow класса PopupChildWindowAction .

Следующий пример показывает, как InteractionRequestTrigger и PopupChildWindowAction используются, чтобы показать всплывающее окно подтверждения в RI MVVM.

<i:Interaction.Triggers>
    <prism:InteractionRequestTrigger
            SourceObject="{Binding ConfirmCancelInteractionRequest}">
        <prism:PopupChildWindowAction
                  ContentTemplate="{StaticResource ConfirmWindowTemplate}"/>
    </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

<UserControl.Resources>
    <DataTemplate x:Key="ConfirmWindowTemplate">
        <Grid MinWidth="250" MinHeight="100">
            <TextBlock TextWrapping="Wrap" Grid.Row="0" Text="{Binding}"/>
        </Grid>
    </DataTemplate>
</UserControl.Resources>

Заметка
Шаблон данных задаётся через свойство ContentTemplate , определяя разметку UI для свойства Content объекта контекста. В коде свойство Content является строкой, таким образом, TextBlock просто связывается со свойством Content непосредственно.

Поскольку пользователь взаимодействует со всплывающим окном, объект контекста обновляется согласно привязке, определённой во всплывающем окне, или в шаблоне данных. После того, как пользователь закрывает всплывающее окно, объект контекста передаётся назад к модели представления, наряду с любыми обновленными значениями, через метод обратного вызова. В примере подтверждения, используемом в RI MVVM, представление подтверждения по умолчанию ответственно за установку свойства Confirmed на предоставленном объекте Confirmation в true , когда щёлкают по кнопке OK.

Также могут быть определены различные триггеры и действия, для поддержки других механизмов взаимодействия. Реализация Prism InteractionRequestTrigger и класса PopupChildWindowAction может использоваться в качестве основания для разработки ваших собственных триггеров и действий.

Продвинутое создание и настройка


Чтобы успешно реализовать паттерн MVVM, вы должны будете полностью понять обязанности представления, модели, и модели представления так, чтобы можно было поместить код приложения в корректных классах. Реализация корректных паттернов позволит этим классам взаимодействовать (посредством привязки данных, команд, запросов взаимодействия, и так далее). Заключительным шагом будет рассмотрение, как представление, модель представления, и классы модели создаются и связываются друг с другом во время выполнения.

Выбор правильной стратегии на этом шаге особенно важен, если вы используете контейнер внедрения зависимости в своём приложении. Managed Extensibility Framework (MEF) и Unity Application Block (Unity) предоставляют возможность определить зависимости между представлением, моделью представления, и классами модели и разрешить их в контейнере во время выполнения.

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

Создание модели представления и представления, используя MEF


Используя MEF, можно определить зависимость представления от модели представления, используя атрибут Import , и определить конкретный тип модели представления, который создаётся, через атрибут Export . Можно также импортировать модель представления в представление через свойство, или как параметр конструктора.

Например, QuestionnaireView в представлении RI MVVM, объявлено свойство только для записи для модели представления, вместе с атрибутом Import . Когда представление создаётся, MEF создаёт экземпляр соответствующей экспортируемой модели представления и устанавливает значение этого свойства. Метод set свойства присваивает модель представления как контекст данных представления, как показано ниже.

[Import]
public QuestionnaireViewModel ViewModel {
    set { this.DataContext = value; }
}

Модель представления определяется и экспортируется, как показано ниже.

[Export]
public class QuestionnaireViewModel : NotificationObject {
    ...
}

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

public QuestionnaireView() {
     InitializeComponent();
}
[ImportingConstructor]
public QuestionnaireView(QuestionnaireViewModel viewModel) : this() {
    this.DataContext = viewModel;
}

Заметка
Можно использовать инжекцию свойства или инжекцию конструктора и в MEF, и в Unity. Однако можно предположить, что инжекция свойства более проста, потому что вы не должны создавать два конструктора. Инструменты времени проектирования, такие как Visual Studio и Expression Blend, требуют, чтобы у элементов управления был конструктор без параметров, чтобы показать их в дизайнере. Любые дополнительные конструкторы, которые вы определяете, должны гарантировать, что конструктор по умолчанию будет вызван, чтобы представление могло быть должным образом инициализировано через метод InitializeComponent .

Создание модели представления и представления, используя Unity


Использование Unity, как контейнера внедрения зависимости, подобно использованию MEF. Поддерживается как основанная на свойствах, так и основанная на конструкторе инжекция. Основная разница в том, что типы обычно не обнаруживаются неявно во время выполнения. Вместо этого, они должны быть зарегистрированы в контейнере.

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

public QuestionnaireView() {
    InitializeComponent();
}
public QuestionnaireView(QuestionnaireViewModel viewModel) : this() {
    this.DataContext = viewModel;
}

Заметка
Конструктор без параметров необходим, чтобы позволить представлению работать в инструментах времени проектирования, таких как Visual Studio и Expression Blend.

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

public QuestionnaireView() {
    InitializeComponent();
}

[Dependency]
public QuestionnaireViewModel ViewModel {
    set { this.DataContext = value; }
}

Тип модели представления регистрируется в контейнере Unity.

container.RegisterType<QuestionnaireViewModel>();

После этого, представление можно создать через контейнер.

var view = container.Resolve<QuestionnaireView>();

Создание модели представления и представления, используя внешний класс


Может быть полезным, определить контроллер или класс службы, чтобы скоординировать создание классов модели представления и представления. Этот подход может использоваться с контейнером внедрения зависимости, таким как MEF или Unity, или когда представление явно создаёт необходимую модель представления.

Этот подход особенно полезен при реализации навигации в вашем приложении. В этом случае, контроллер связывается с элементом заполнителя или регионом в UI, и координирует создание и размещение представлений в этом заполнителе или регионе.

Например, RI MVVM использует класс службы, чтобы создать представления, используя контейнер и показать их на главной странице. В этом примере, представления определяются именами представления. Навигация инициируется через вызов метода ShowView в службе UI.

private void NavigateToQuestionnaireList() {
    // Попросить сервис показать представление "questionnaire list".
    this.uiService.ShowView(ViewNames.QuestionnaireTemplatesList);
}

Служба UI ассоциируется с элементом заполнителем в UI приложения. Она инкапсулирует создание необходимого представления и координирует его появление в UI. Метод ShowView в UIService создаёт экземпляр представления через контейнер (так, чтобы его модель представления и другие зависимости могли быть разрешены), и затем показывает его в надлежащем месте, как показано ниже.

public void ShowView(string viewName) {
    var view = this.ViewFactory.GetView(viewName);
    this.MainWindow.CurrentView = view;
}

Заметка
Prism предоставляет обширную поддержку навигации с использованием регионов. Навигация использует механизм, подобный предыдущему подходу, за исключением того, что менеджер регионов ответственен за координирование создания и размещения представления в определенном регионе. Для получения дополнительной информации, смотрите раздел, "View-Based Navigation" в Главе 8, "Navigation".

Тестирование MVVM приложений


Тестирование моделей и моделей представления из приложений MVVM похоже на тестирование любых других классов. Могут быть использованы и те же самые инструменты и методы - такие как поблочное тестирование и mocking платформы. Однако, есть некоторые паттерны, применяемые при тестировании классов моделей и моделей представления.

Тестирование реализаций INotifyPropertyChanged


Реализация интерфейса INotifyPropertyChanged позволяет представлениям реагировать на изменения, порождённые в моделях представления и моделях. Эти изменения не ограничиваются доменными данными, показываемыми в элементах управления; они также используются, чтобы управлять представлением, как состояния модели представления определяют, какая анимация должна научатся, или какой элемент управления должен быть отключён.

Простые случаи

Свойства, которые могут быть обновлены непосредственно тестовым кодом, могут быть протестированы с помощью присоединения обработчика событий к событию PropertyChanged и проверки, генерируется ли событие после установки нового значения для свойства. Классы помощников, такие как класс ChangeTracker , используемый в демонстрационных проектах MVVM, могут использоваться, чтобы присоединить обработчик и собрать результаты. Это предотвращает дублирование при записи тестов. Следующий пример кода показывает тест, использующий этот класс помощника.

var changeTracker = new PropertyChangeTracker(viewModel);
viewModel.CurrentState = "newState";
CollectionAssert.Contains(changeTracker.ChangedProperties, "CurrentState");

Свойства, которые являются результатом генерации кода, который гарантирует реализацию интерфейса INotifyPropertyChanged , такие как в коде, сгенерированном дизайнером модели, обычно не нуждаются в тестировании.

Вычисленные и не устанавливаемые свойства

Когда свойства не могут быть установлены тестовым кодом, такие как свойства, с приватными методами set или расчётные свойства только для чтения, тестовый код должен стимулировать объект изменить это свойство и сгенерировать уведомление. Однако, структура теста является такой же, как в более простых случаев, что показано в следующем примере кода, где изменение в модели заставляют свойство в модели представления измениться.

var changeTracker = new PropertyChangeTracker(viewModel);

var question = viewModel.Questions.First() as OpenQuestionViewModel;
question.Question.Response = "some text";

CollectionAssert.Contains(changeTracker.ChangedProperties, "UnansweredQuestions");

Уведомления об изменении всех свойств

Когда вы реализуете интерфейс INotifyPropertyChanged , для объекта позволяется сгенерировать событие PropertyChanged с нулевой или пустой строкой названия изменённого свойства, чтобы указать, что все свойства в объекте, возможно, изменились. Эти случаи могут быть протестированы точно так же, как случаи уведомления об изменении одиночного свойства.

Тестирование реализаций INotifyDataErrorInfo


Есть несколько механизмов, позволяющих привязке выполнить валидацию ввода, такие как выдача исключений, при установки свойства, реализация интерфейса IDataErrorInfo , и (в Silverlight) реализация интерфейса INotifyDataErrorInfo . Реализация интерфейса INotifyDataErrorInfo предоставляет большую гибкость, потому что он поддерживает указание нескольких ошибок на свойство, асинхронное и перекрёстную валидацию, но и, с другой стороны, также требует большего тестирования.

Есть два аспекта в тестировании реализаций INotifyDataErrorInfo : тестирование, что правила валидации реализуются правильно, и тестирование, что требования для реализаций интерфейса, таких как генерирование события ErrorsChanged , когда изменяется результат вызова метода GetErrors , удовлетворяются.

Тестирование правил валидации


Логику валидации обычно просто протестировать, потому что обычно это автономный процесс, где вывод зависит от ввода. Для каждого свойства со связанными правилами валидации, должны быть проведены тесты на результат вызова метода GetErrors с именем проверяемого свойства для допустимых значений, недопустимых значений, граничных значений, и так далее. Если логика валидации разделена, как, к примеру, при использовании атрибутов валидации данных, тесты могут быть сконцентрированы на разделённой логике валидации. С другой стороны, пользовательские правила проверки допустимости должны быть полностью протестированы.

// Недопустимый случай.
var notifyErrorInfo = (INotifyDataErrorInfo)question;
question.Response = -15;
Assert.IsTrue(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any());
// Допустимый случай.
var notifyErrorInfo = (INotifyDataErrorInfo)question;
question.Response = 15;
Assert.IsFalse(notifyErrorInfo.GetErrors("Response").Cast<ValidationResult>().Any());

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

Тестирование требований реализаций INotifyDataErrorInfo


Помимо создания правильных значений для метода GetErrors , реализации интерфейса INotifyDataErrorInfo должны гарантировать, что событие ErrorsChanged генерируется при изменении результата вызова GetErrors . Дополнительно, свойство HasErrors должно отразить полное ошибочное состояние объекта, реализующего интерфейс.

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

Тестирование требований интерфейса должно включить, по крайней мере, следующие проверки:
  • Свойство HasErrors отражает полное ошибочное состояние объекта. Установка допустимого значения для ранее недопустимого свойства не приводит к изменению для этого свойства, если у других свойств есть недопустимые значения.
  • Событие ErrorsChanged генерируется, когда ошибочное состояние для свойства изменяется, что должно быть отражено в результате вызова метода GetErrors . Изменение состояния ошибок может идти от допустимого состояния (то есть, никакие ошибки) к недопустимому состоянию и наоборот, или оно может пойти от недопустимого состояния до другого недопустимого состояния. Обновлённый результат вызова GetErrors должен быть доступен для обработчиков события ErrorsChanged .

Тестируя реализации интерфейса INotifyPropertyChanged , классы помощников, такие как класс NotifyDataErrorInfoTestHelper в демонстрационных проектах MVVM, обычно облегчают написание тестов для реализаций интерфейса INotifyDataErrorInfo , обрабатывая повторные служебные операции и стандартные проверки. Они особенно полезны, когда интерфейс реализуется, не полагаясь на некоторого менеджера ошибок, допускающего повторное использование. Следующий пример показывает этот класс помощника.
var helper =
    new NotifyDataErrorInfoTestHelper<NumericQuestion, int?>(
        question,
        q => q.Response);

helper.ValidatePropertyChange(
    6,
    NotifyDataErrorInfoBehavior.Nothing);
helper.ValidatePropertyChange(
    20,
    NotifyDataErrorInfoBehavior.FiresErrorsChanged
    | NotifyDataErrorInfoBehavior.HasErrors
    | NotifyDataErrorInfoBehavior.HasErrorsForProperty);
helper.ValidatePropertyChange(
    null,
    NotifyDataErrorInfoBehavior.FiresErrorsChanged
    | NotifyDataErrorInfoBehavior.HasErrors
    | NotifyDataErrorInfoBehavior.HasErrorsForProperty);
helper.ValidatePropertyChange(
    2,
    NotifyDataErrorInfoBehavior.FiresErrorsChanged);

Тестирование асинхронных вызовов служб


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

Стандартные паттерны, используемые для реализации асинхронных операций, предоставляют различные гарантии относительно потока, в котором произойдёт уведомление о состоянии работы. Хотя асинхронный шаблон, основанный на событиях, гарантирует, что обработчики для событий вызываются в потоке, который является подходящим для приложения, шаблон разработки IAsyncResult не предоставляет никаких гарантии, что какие-либо изменения, влияющие на представление, вызовутся в потоке UI.

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

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

Следующий пример кода показывает тест для соответствующего уведомления в потоке UI, при завершении асинхронной работы, используя заглушки для служб. В этом примере, тестовый код получает обратный вызов, предоставленный моделью представления, когда он выполняет асинхронный вызов службы. Тогда тест моделирует завершение вызова позже в тесте, вызывая обратный вызов. Этот подход позволяет тестировать компоненты, которые используют асинхронную службу, без сложности создания асинхронных тестов.

questionnaireRepositoryMock
    .Setup(
        r =>
            r.SubmitQuestionnaireAsync(
                It.IsAny<Questionnaire>(),
                It.IsAny<Action<IOperationResult>>()))
    .Callback<Questionnaire, Action<IOperationResult>>(
        (q, a) => callback = a);

uiServicemock
    .Setup(svc => svc.ShowView(ViewNames.QuestionnaireTemplatesList))
    .Callback<string>(viewName => requestedViewName = viewName);
submitResultMock
    .Setup(sr => sr.Error)
    .Returns<Exception>(null);

CompleteQuestionnaire(viewModel);
viewModel.Submit();
// Изображаем метод обратного вызова в UI потоке.
callback(submitResultMock.Object);
// Проверяем ожидаемое поведение – запрос на навигацию к списку.
Assert.AreEqual(ViewNames.QuestionnaireTemplatesList, requestedViewName);

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

Дополнительная информация


Для получения дополнительной информации о логическом дереве, см. "Trees in WPF" на MSDN: http://msdn.microsoft.com/en-us/library/ms753391.aspx

Для получения дополнительной информации о присоединенных свойствах, см. "Attached Properties Overview" на MSDN: http://msdn.microsoft.com/en-us/library/cc265152(VS.95).aspx

Для получения дополнительной информации о MEF, см."Managed Extensibility Framework Overview" на MSDN: http://msdn.microsoft.com/en-us/library/dd460648.aspx.

Для получения дополнительной информации о Unity, см."Unity Application Block" на MSDN: http://www.msdn.com/unity.

Для получения дополнительной информации о DelegateCommand , см.Chapter 5, "Implementing the MVVM Pattern."

Для получения дополнительной информации об использовании поведений Microsoft Expression Blend, см."Working with built-in behaviors" на MSDN: http://msdn.microsoft.com/en-us/library/ff724013(v=Expression.40).aspx.

Для получения дополнительной информации о создании пользовательских поведений с Microsoft Expression Blend, см."Creating Custom Behaviors" на MSDN: http://msdn.microsoft.com/en-us/library/ff724708(v=Expression.40).aspx.

Для получения дополнительной информации о создании пользовательских триггеров и действий с Microsoft Expression Blend, см."Creating Custom Triggers and Actions" на MSDN: http://msdn.microsoft.com/en-us/library/ff724707(v=Expression.40).aspx.

Для получения дополнительной информации об использовании диспетчера в WPF и Silverlight, см."Threading Model" и "The Dispatcher Class" на MSDN: http://msdn.microsoft.com/en-us/library/ms741870.aspx
http://msdn.microsoft.com/en-us/library/ms615907(v=VS.95).aspx.

Для получения дополнительной информации о unit-тестировании в Silverlight, см."Unit Testing with Silverlight 2": http://www.jeff.wilcox.name/2008/03/silverlight2-unit-testing/.

Для получения дополнительной информации о навигации с использованием регионов, см. раздел "View-Based Navigation" in Chapter 8, "Navigation."

Для получения дополнительной информации об асинхронном паттерне, основанном на событии, см."Event-based Asynchronous Pattern Overview" на MSDN: http://msdn.microsoft.com/en-us/library/wewwczdw.aspx

Для получения дополнительной информации о шаблоне разработки IAsyncResult , см."Asynchronous Programming Overview" на MSDN: http://msdn.microsoft.com/en-us/library/ms228963.aspx
Теги:
Хабы:
+2
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн