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))));
}
}
На этом у меня все. Исходный код можно скачать здесь.