ViewModel Locator
В MVVM Light Toolkit есть класс ViewModelLocator, который по задумке автора тулкита должен предоставлять доступ к экземлярам всех ViewModel'ей приложения. Как я уже писал, для каждой ViewModel'и во ViewModelLocator'е можно объявить свойство, которое будет возвращать корректно инициализированный экземпляр. Вот так, например, объявляется свойство Main для доступа к ViewModel'и главного окна приложения:
public class ViewModelLocator { private static MainViewModel _main; public ViewModelLocator() { CreateMain(); } public static MainViewModel MainStatic { get { if (_main == null) CreateMain(); return _main; } } public MainViewModel Main { get { return MainStatic; } } public static void CreateMain() { if (_main == null) _main = new MainViewModel(new DataProvider()); } }
Далее в XAML можно определить ViewModelLocator как статический ресурс и привязать его свойство Main к свойству DataContext соответствующего View.
<Application.Resources> <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" /> </Application.Resources> <Window DataContext="{Binding Main, Source={StaticResource Locator}}">
Такой способ организации ViewModel'ей вызывает много вопросов:
- Экземпляр ViewModel'и является статическим свойством т.е. он уникален в рамках домена приложения. Что делать если вам понадобятся два и более экзипляров одновременно?
- Допустим даже одного экземпляра вам достаточно, но использовать его нужно несколько раз. Как изменять состояние экземпляра ViewModel'и в зависимости от контекста его использования?
- Если ViewModelLocator содержит свойства для нескольких ViewModel'ей, зачем создавать экземпляры их всех при создании ViewModelLocator'а?
Я не использую ViewModelLocator из MVVM Light Toolkit т.к считаю его реализацию весьма неудачной. В этой статье я не буду рассуждать о возможных путях “исправления” ViewModelLocator'а. Вместо этого я хочу описать способ организации ViewModel'ей без использования Locator'а, который позволяет ответить на поставленные выше вопросы. Кроме того я покажу как можно обеспечить для ViewModel'ей поддержку отображения данных в design time т.е. Blendability.
Демонстрационное приложение
За основу я возьму приложение с тэгами StackOwerflow, которое уже не раз использовалось в предыдущих статьях посвященных MVVM. В приложении будет два окна. В главном будет список тэгов и кнопка, вызывающая диалог с параметрами выделенного тэга.
Второе окно будет модальным диалогом, позволяющим редактировать параметры тэга.
Два окна в нашем случае это два View и две соответствующие им ViewModel'и. Как же огранизовать это хозяйство? Давайте посмотрим как наши окна будут использоваться. Главное окно потому и главное, что единственный его экземпляр будет создаваться во время запуска и будет жить пока живет приложение. Модальный же диалог мы будем создавать каждый раз новый.
Самые нетерпеливые могут не читать дальше, а сразу скачать исходный код здесь.
Blendability
Для того чтобы обеспечить отображение тестовых данных в дизайнере Visual Studio придумана уже масса решений. Я здесь приведу то, которое лично мне нравится больше всего. Вместо одного класса для ViewModel'и я буду использовать два. Один с реальными данными, командами и т.д. Другой же только с теми тестовыми данными, которые я хочу увидеть в дизайнере. Для удобства при именовании этих классов я использую следующую нотацию: ViewModel.cs и ViewModelDesign.cs.
Вот так, например, выглядит класс EditTagViewModelDesign. Вы видите, что в нем ничего нет кроме свойства Tag, которое содержит тестовые данные.
internal sealed class EditTagViewModelDesign { public TagInfo Tag { get { return new TagInfo { Name = "DummyTag", Score = 99 }; } } }
Привязать тестовую ViewModel ко View можно следующим образом:
<UserControl x:Class="ViewModelLocatorSample.EditTagView" ... d:DataContext="{Binding Source={StaticResource editVm}}"> <UserControl.Resources> <vm:EditTagViewModelDesign x:Key="editVm" /> </UserControl.Resources> ... </UserControl>
В ресурсах View я создаю экземпляр тестовой ViewModel'и как статический ресурс и инициализирую им DataContext этого самого View. Т.к. этот DataContext относится только к design time, то никаких проблем для реальной ViewModel'и он не создаст.После таких нехитрых манипуляций в дизайнере Visual Studio можно увидеть макет диалога с тестовыми данными.
Application startup
Во время запуска создается и инициализируется главное окно приложения:
private void Application_Startup(...) { ... var mainView = container.Resolve<MainView>(); mainView.DataContext = container.Resolve<MainViewModel>(); var windowService = container.Resolve<IWindowService>(); windowService.Show("Tags App", mainView); }
Здесь сперва View и ViewModel вытягиваются из IOC контейнера. ViewModel инициализирует DataContext на View. И наконец, некий WindowService отображает View.
WindowService
Давайте разберемся что из себя представляет WindowService. Дня начала нужно сказать, что все View в приложении являются UserControl'ами и для того чобы показать их пользователю, сперва необходимо поместить их в какое-нибудь окно. Именно этим и занимается WindowService. Он содержит всего 2 метода: Show (для отображения немодальных окон) и ShowDialog (для отображения модальных диалогов соответственно).
public void Show(string title, FrameworkElement content) { var window = new Window { WindowStartupLocation = WindowStartupLocation.CenterOwner, Title = title, Content = content, SizeToContent = SizeToContent.WidthAndHeight }; if (window == Application.Current.MainWindow) window.WindowStartupLocation = WindowStartupLocation.CenterScreen; else window.Owner = Application.Current.MainWindow; window.Show(); } public bool? ShowDialog(string title, FrameworkElement content) { var window = new CommonWindow(_messenger) { Owner = Application.Current.MainWindow, WindowStartupLocation = WindowStartupLocation.CenterOwner, ShowInTaskbar = false, WindowStyle = WindowStyle.ToolWindow, Title = title, Content = content, SizeToContent = SizeToContent.WidthAndHeight }; return window.ShowDialog(); }
Отображение диалога
Давайте теперь посмотрим что нужно сделать чтобы показать диалог редактирования параметров тэга. Вся работа происходит во ViewModel'и главного окна.
private void EditExecute() { var tags = CollectionViewSource.GetDefaultView(Tags); var currentTag = tags.CurrentItem as TagInfo; var viewModel = _container.Resolve<EditTagViewModel>(); viewModel.Tag = new TagInfo(currentTag); var view = _container.Resolve<EditTagView>(); view.DataContext = viewModel; var windowService = _container.Resolve<IWindowService>(); var result = windowService.ShowDialog("Edit Tag", view); if (result.HasValue && result.Value == true) { currentTag.Name = viewModel.Tag.Name; currentTag.Score = viewModel.Tag.Score; } }
Из контейнера я вытягиваю экземпляры нужных мне View и ViewModel'и. Инициализирую ViewModel диалога параметрами выделенного в списке тэга. Отображаю диалог. Далее, если пользователь закрыл диалог, нажав на кнопку Оk, то я обновляю тэг в списке, иначе игнорирую результаты редактирования.
Закрытие диалога
Ранее я уже писал о Messenger'е из MVVM Light Toolkit'а. Именно собщения от ViewModel'и используются окном отображающим View для того чтобы закрыться.
В процессе создания окно регистрируется как получатель сообщений определенного типа.
_messenger.Register<CloseViewMessage>(this, Close);
ViewModel же в свою очередь при обработке нажаний на кнопки Ok и Cancel отправляет окну сообщение о том, что пора закрываться.
private ICommand _okCommand; public ICommand OkCommand { get { return _okCommand ?? (_okCommand = new RelayCommand( () => _messenger.Send( new CloseViewMessage("EditTagView", true)))); } } private ICommand _cancelCommand; public ICommand CancelCommand { get { return _cancelCommand ?? (_cancelCommand = new RelayCommand( () => _messenger.Send( new CloseViewMessage("EditTagView", false)))); } }
На этом у меня все. Исходный код можно скачать здесь.
К MVVMlight отношение интересно было узнать.
ReplyDeleteЧто-то в статье полезное есть - факт. Данные в дизайн тайме это да.
А вот операции с вьюхами из вью-модель это ни в какие ворота. В MVVM View и ViewModel слабо связаны. То есть, к вьюхам из модели обращаться нельзя. Даже чужим вьюхам. Это уже не MVVM паттерн. Тоже факт.
"Нельзя" - это слишком категорично. Можно, но, как правило, это не самая лучшая идея. Для рассмотренного примера это не смертельно. Но я согласен, что надо было выносить из вьюмодели ссылки на вьюхи для того чтобы показать корректную реализацию.
ReplyDelete