Sunday, September 12, 2010

MVVM: ViewModel-First approach

Я решил ненадолго отложить обзор MVVM тулкитов, а вместо этого продолжить разбираться с самим паттерном.

В предыдущей статье я описал основные блоки MVVM, их ответственности и на простом примере показал взаимодействие между ними. Тот вариант реализации MVVM называется View-First, т.к. именно View через свое свойство DataContext знает про ViewModel, а ViewModel наоборот абсолютно ничего не знает про View.

MVVM-ViewFirst

Сегодня я хочу описать другой подход к организации взаимодействия между блоками в паттерне MVVM. Называется он ViewModel-First и отличается от предыдущего тем, что здесь ViewModel знает про View c которым работает и может вызывать его методы, подписываться на его события и т.д.

MVVM-ViewModelFirst

Зачем ViewModel’у может понадобиться знать про View? Как правило для того чтобы управлять состоянием UI, но при этом не иметь жестких ссылок на UI контролы.

Для того чтобы применить ViewModel-First подход нужно сделать следующее:

  1. Определить максимально узкий и независимый от элементов UI интерфейс, необходимый для взаимодействия с View из ViewModel
  2. Имплементировать этот интерфейс во View
  3. Каким-либо образом пропихнуть экземпляр этого интерфейса во ViewModel

Давайте на примере посмотрим как можно реализовать ViewModel-First подход. За основу возьмем приложение управляющее списком тегов из предыдущей статьи. Допустим у нас появилась новая задача: «Сделать так чтобы при добавлении нового тега в Listbox’e выделенным становился 4й видимый элемент списка».

Давайте немного подумаем :) Сейчас у нас добавляет новые элементы в список команда AddNewTagCommand, определенная на ViewModel. При этом она просто добавляет новый элемент в коллекцию Tags и абсолютно ничего не знает про то, что эта коллекция является источником для Listbox’а. И это очень хорошо, что не знает. В этом как раз одна из идей MVVM – ViewModel независящий от UI контролов.

С другой стороны именно при добавлении нового элемента нам нужно управлять выбранным элементом в Listbox’e. С точки зрения разделения ответственности между блоками в паттерне MVVM, управление состоянием UI контролов - это однозначно зона ответственности блока View.

Ладно, меньше слов – больше кода. Давайте посмотрим как решить поставленную задачу. Для начала определим интерфейс через который ViewModel будет взаимодействовать с View. Нам хватит одного метода. Назовем его SetReadyState. ViewModel будет знать, что после добавления элемента нужно дернуть у View этот метод, чтобы View мог привести себя в состояние готовности. При этом ViewModel’у незачем знать, что именно там будет делать View. Более того разные View могут делать там разные вещи.

internal interface IView
{
void SetReadyState();
}

Т.к. нам потребуется доступ к ListBox’у, то надо дать ему имя:

<ListBox
x:Name="TagsListView"
Margin="5 0"
SelectionMode="Single"
ItemsSource="{Binding Tags}" />

Теперь давайте посмотрим на имплементацию интерфейса во View.

public partial class MainWindow : Window, IView
{
...

public void SetReadyState()
{
TagsListView.SelectedIndex =
GetFirstVisible(TagsListView) + 3;
}
...
}
ViewModel тоже нужно будет модифицировать так, чтобы он мог принимать интерфейс IView. Мы сделаем это через дополнительный параметр конструктора.

internal sealed class MainViewModel : INotifyPropertyChanged
{
private IView _view;

public MainViewModel(IDataProvider provider, IView view)
{
Tags = new ObservableCollection<string>(provider.Tags);
_view = view;
}
...
}

После этого мы сможем в команде AddNewTagCommand после добавления нового элемента вызвать у View метод SetReadyState.

public void Execute(object parameter)
{
_viewModel.Tags.Add(_viewModel.NewTag);
_viewModel.NewTag = string.Empty;

_viewModel._view.SetReadyState();
}

И, наконец, последнее изменение. Надо при инициализации приложения пропихивать View в конструктор ViewModel.

private void Application_Startup(object sender, StartupEventArgs e)
{
var mainWindow = new MainWindow();
Current.MainWindow = mainWindow;

var viewModel =
new MainViewModel(new DataProvider(), mainWindow);

mainWindow.DataContext = viewModel;
mainWindow.Show();
}

Исходный код можно скачать отсюда

P.S. Небольшое пояснение.

Я перечитал еще разок эту и предыдущую статью про MVVM и решил прояснить вот такой момент, который может показаться противоречивым. В первой статье я написал что логика по управлению состоянием UI – это зло и ее надо переносить во ViewModel. А в этой статье наоборот написал, что управление состоянием UI контролов - это зона ответственности блока View.

На самом деле тут нет противоречия. Если использовать оригинальные термины, то View отвечает за UI Logic, а ViewModel за Presentation Logic. Под UI Logic я понимаю все операции, которым необходим непосредственный доступ к UI контролам такие как установка фокуса, смена выделенного элемента списка и т.д. А Presentation Logic – это операции, которые управляют состоянием UI при этом, не имея прямого доступа к UI. Например, команда AddNewTagCommand из нашего демо.

1 comment:

  1. В случае VM First, как мне кажется, лучше управлять выбором и фокусом опосредованно через свойства. А реальные действия выполнять во вью через баиндинг на attached свойства или behaviors. Тогда VM может вообще забыть о View, ну, разве что отслеживать попытки закрытия окна и т.п.

    ReplyDelete