Friday, December 24, 2010

Blendability or performance?

Недавно я написал про то как можно обеспечить Blendability для View. Сама по себе идея очень даже хороша. Конечно, гораздо удобнее видеть в дизайнере как будут отображаться данные, чем строить View вслепую, а потом, запустив приложение, удивляться что что-нибудь выглядит не так как задумывалось.

Однако реальность как всегда сурова. На моем текущем проекте XAML дизайнер Visual Studio 2010 тратит около 10 секунд для отрисовки View. И это на новом железе с кучей оперативки. И это при том что к производительности самой студии претензий нет.

Решением проблемы стал отказ от дизайнера. Сильной проблемы в этом нет, т.к. все равно XAML я пишу всегда руками. Зато файлы c XAML открываются тепеpь практически также быстро как и cs-файлы. Отменить включение дизайнера при открытии XAML-файлов можно в опциях студии: Text Editor -> XAML -> Miscellaneous.

Blendability

Вот так хорошая идея с Blendability пала жертвой неудовлетворительной производительности дизайнера.

Window.SizeToContent

В предыдущей статье все View демонстрационного приложения представляли собой UserControl'ы.

<UserControl
    x:Class="ViewModelLocatorSample.EditTagView"
    ...
    Height="84" Width="243">
    ...
</UserControl>

Для того чтобы показать View пользователю нужно было создать окно, которому в Content поместить нужный View.

public void Show(string title, FrameworkElement content)
{
    var window = new Window
    {
        Title = title,
        Content = content
    };

    window.Show();
}

Не знаю как вы, но лично я не знал как подогнать размеры окна под размер UserControl'а. Если вообще не устанавливать окну никаких размеров, то получим нечто подобное.

SizeToContent0
Понятное дело, что указыать какие-то конкретные значения смысла нет, т.к. у каждой View свои размеры. Можно присваивать окну размеры UserControl'а.

var window = new Window
{
    Title = title,
    Content = content,
    Width = content.Width,
    Height = content.Height
};

Но как видите это тоже не самая лучшая идея, т.к часть контента оказалась обрезана окном.

SizeToContent1

Решение проблемы оказалось очень простым. У Window есть свойство SizeToContent, которое (ну кто бы мог подумать) позволяет подгонять под размер контента только высоту, только ширину или сразу оба размера.

var window = new Window
{
    Title = title,
    Content = content,
    SizeToContent = SizeToContent.WidthAndHeight
};

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

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