Я решил ненадолго отложить обзор MVVM тулкитов, а вместо этого продолжить разбираться с самим паттерном.
В предыдущей статье я описал основные блоки MVVM, их ответственности и на простом примере показал взаимодействие между ними. Тот вариант реализации MVVM называется View-First, т.к. именно View через свое свойство DataContext знает про ViewModel, а ViewModel наоборот абсолютно ничего не знает про View.
Сегодня я хочу описать другой подход к организации взаимодействия между блоками в паттерне MVVM. Называется он ViewModel-First и отличается от предыдущего тем, что здесь ViewModel знает про View c которым работает и может вызывать его методы, подписываться на его события и т.д.
Зачем ViewModel’у может понадобиться знать про View? Как правило для того чтобы управлять состоянием UI, но при этом не иметь жестких ссылок на UI контролы.
Для того чтобы применить ViewModel-First подход нужно сделать следующее:
- Определить максимально узкий и независимый от элементов UI интерфейс, необходимый для взаимодействия с View из ViewModel
- Имплементировать этот интерфейс во View
- Каким-либо образом пропихнуть экземпляр этого интерфейса во 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, IViewViewModel тоже нужно будет модифицировать так, чтобы он мог принимать интерфейс IView. Мы сделаем это через дополнительный параметр конструктора.
{
...
public void SetReadyState()
{
TagsListView.SelectedIndex =
GetFirstVisible(TagsListView) + 3;
}
...
}
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 из нашего демо.
В случае VM First, как мне кажется, лучше управлять выбором и фокусом опосредованно через свойства. А реальные действия выполнять во вью через баиндинг на attached свойства или behaviors. Тогда VM может вообще забыть о View, ну, разве что отслеживать попытки закрытия окна и т.п.
ReplyDelete