Wednesday, December 22, 2010

MVVM: ViewModelLocator and multiple windows

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'ей вызывает много вопросов:

  1. Экземпляр ViewModel'и является статическим свойством т.е. он уникален в рамках домена приложения. Что делать если вам понадобятся два и более экзипляров одновременно?
  2. Допустим даже одного экземпляра вам достаточно, но использовать его нужно несколько раз. Как изменять состояние экземпляра ViewModel'и в зависимости от контекста его использования?
  3. Если ViewModelLocator содержит свойства для нескольких ViewModel'ей, зачем создавать экземпляры их всех при создании ViewModelLocator'а?

Я не использую ViewModelLocator из MVVM Light Toolkit т.к считаю его реализацию весьма неудачной. В этой статье я не буду рассуждать о возможных путях “исправления” ViewModelLocator'а. Вместо этого я хочу описать способ организации ViewModel'ей без использования Locator'а, который позволяет ответить на поставленные выше вопросы. Кроме того я покажу как можно обеспечить для ViewModel'ей поддержку отображения данных в design time т.е. Blendability.

Демонстрационное приложение

За основу я возьму приложение с тэгами StackOwerflow, которое уже не раз использовалось в предыдущих статьях посвященных MVVM. В приложении будет два окна. В главном будет список тэгов и кнопка, вызывающая диалог с параметрами выделенного тэга.

mainWindow

Второе окно будет модальным диалогом, позволяющим редактировать параметры тэга.

EditWindow

Два окна в нашем случае это два 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 можно увидеть макет диалога с тестовыми данными.

VSDesigner

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))));
    }
}

На этом у меня все. Исходный код можно скачать здесь.

2 comments:

  1. К MVVMlight отношение интересно было узнать.
    Что-то в статье полезное есть - факт. Данные в дизайн тайме это да.

    А вот операции с вьюхами из вью-модель это ни в какие ворота. В MVVM View и ViewModel слабо связаны. То есть, к вьюхам из модели обращаться нельзя. Даже чужим вьюхам. Это уже не MVVM паттерн. Тоже факт.

    ReplyDelete
  2. "Нельзя" - это слишком категорично. Можно, но, как правило, это не самая лучшая идея. Для рассмотренного примера это не смертельно. Но я согласен, что надо было выносить из вьюмодели ссылки на вьюхи для того чтобы показать корректную реализацию.

    ReplyDelete