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

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

Thursday, October 14, 2010

Benefits of MEF

До MEFа...

На моем текущем проекте есть модуль, который умеет анализировать файлы, полученные от пользователя, и вытягивать из них интересную для бизнеса информацию. В общих чертах структура модуля выглядит вот так:

Parsers

ParseManager организует всю работу по анализу содержимого файлов. При этом он сам понятия не имеет, как именно извлекать данные. Зато по типу файла он может найти соответствующий парсер и делегировать ему эту задачу. Для каждого типа файлов реализован свой собственный парсер. Список доступных парсеров хранится в конфигурационном файле приложения. Это позволяет свободно изменять коллекцию парсеров.

Давайте посмотрим на пример, который я собрал по мотивам реального проекта. Интерфейс IParser описывыет контракт, который должны реализовывать все парсеры:

public interface IParser
{
IEnumerable<string> Parse();
}

Вот пример реализации парсера для текстовых файлов. Он не выполняет никакой полезной работы, а просто возвращает свое имя в качестве результата:

public class Parser : IParser
{
public IEnumerable<string> Parse()
{
return new List<string> { "TxtParser" };
}
}

Давайте теперь посмотрим, как организована инфраструктура подключения парсеров. В конфигурационном файле для этого отведена специальная секция:

<parseConfiguration>
<parsers>
<add extension="docx" type="DocxParser.Parser, DocxParser" />
<add extension="txt" type="TxtParser.Parser, TxtParser" />
</parsers>
</parseConfiguration>

Для того чтобы можно было работать с содержимым parseConfiguration секции, мы реализовали три класса:

  1. ParseSettings – описывающий саму секцию
  2. ParserDataCollection – описывающий коллекцию элементов нашей секции
  3. ParserData – описывающий отдельный элемент коллекции, т.е. отдельный парсер

Я не буду приводить здесь код этих классов, вы сможете скачать их вместе с остальными исходниками этого проекта отсюда.

И наконец, самый интересный момент. Давайте посмотрим, как же приложение находит нужный парсер и создает его экземпляр. Тут все достаточно просто:

  1. Загружаем из конфигурационного файла нужную нас секцию.
  2. Среди элементов этой секции ищем тот, у которого свойство Extension соответствует типу полученного от пользователя файла.

private static IEnumerable<string> Parse(string fileName)
{
IParser parser = null;
string fileExtension =
Path.GetExtension(fileName).Substring(1);

ParseSettings settings =
(ParseSettings)ConfigurationManager.GetSection(
ParseSettings.ParseSettingsSection);

ParserData parserSettings = (
from parserData in settings.Parsers.Cast<ParserData>()
where string.Equals(
parserData.Extension,
fileExtension,
StringComparison.OrdinalIgnoreCase)
select parserData
).FirstOrDefault();

Type parserType = Type.GetType(parserSettings.ParserType, true);
parser = Activator.CreateInstance(parserType) as IParser;

return parser.Parse();
}

Основным недостатком этого подхода, на мой взгляд, является необходимость написания большого объема вспомогательного кода. Если нам понадобится добавить в приложение какой-то другой вид плагинов, например модули для создания изображений (preview) для наших файлов, то нам придется добавлять в конфигурацию еще одну секцию и еще раз реализовывать три класса описывающие ее содержимое. Кроме того, если понадобится использовать наши парсеры в другом приложении, то мы не сможем отдать их просто так. В новом приложении тоже надо будет организовывать работу с конфигурацией.

Если вы дочитали до этого места, то наверняка уже не раз подумали: «Ну а где же собственно про MEF?» Не переживайте, сейчас все будет :)

Немного теории

Мне лениво придумывать свое определение для MEF, поэтому я просто переведу немного текста с официального сайта: «MEF (Managed Extensibility Framework) упрощает создание расширяемых приложений, предлагая функционал по обнаружению и подключению модулей.»

Основные понятия MEFа:

  • Composable part — модуль, который предоставляет сервисы другим модулям или использует сервисы других модулей. Модули в MEF могут находиться как в отдельных сборках, так и в одной с основным приложением сборке.
  • Export—Функциональность которую модуль предоставляет для использования другими модулями.
  • Import—Функциональность, которую модуль хочет получить от других модулей.
  • Contracts—Строковый идентификатор для экспорта и импорта. Экспортер указывает контракт для каждого экспорта, который он предоставляет. Импортер со своей стороны указывает контракт для каждого импорта, который он потребляет.
  • Composition—Используя коллекцию модулей, MEF строит граф зависимостей между ними, пытаясь каждому импорту найти соответствующий экспорт.
  • Catalog – содержит коллекцию доступных модулей
  • Container – используя каталог, строит композицию модулей.

MEF

Если не вдаваться в детали, то можно очень просто описать как работает MEF: Сперва Catalog формирует коллекцию доступных модулей, затем Container пытается найти для каждого импорта, соответствующий экспорт.

MEF в деле...

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

Интерфейс IParser оставляем без изменений:

public interface IParser
{
IEnumerable<string> Parse();
}

А вот парсеры получат новые атрибуты:

[Export(typeof(IParser))]
[PartMetadata("Extension", "txt")]
public class Parser : IParser
{
public IEnumerable<string> Parse()
{
return new List<string> { "TxtParser" };
}
}

Здесь атрибут Export означает, что мы хотим пометить класс Parser как доступный для инфраструктуры MEF при построении композиции. typeof(IParser) в экспорте означает, что контрактом (строковым идентификатором) для экспорта будет “PluginsViaMEF.Contracts.IParser”. Позже я покаже как мы сможем использовать этот экспорт.

Давайте теперь посмотрим на второй атрибут парсера – PartMetadata. Этот атрибут позволяет привязывать к модулям различную вспомогательную информацию. Он позволяет множественное использование и представляет собой словарь со строковым ключом. Позже вы увидите как можно из кода получить доступ к метаданным модуля.

Как вы видите, мы сохранили в метаданные парсера тип файлов, с которым этот парсер может работать. Т.е больше нет необходимости хранить эту информацию в файле конфигурации. Кроме того список доступных парсеров мы теперь будем получать с помощью MEF. Получается что нам боьше нечего хранить в конфигурации. Мы уберем из нее секцию связанную с парсерами. И заодно удалим из проекта классы ParseSettings, ParserDataCollection и ParserData. Они нам больше не нужны.

Давайте теперь посмотрим что изменилось в основном приложении и как оно теперь будет находить и подключать парсеры. Во-первых мы добавим в класс Programm поле _parser:

[Import]
public IParser _parser;

Обратите внимание на атрибут Import. Мы не указываем здесь явно контракт. По умолчанию MEF использует как контракт тип объекта помеченного атрибутом. Т.е. в нашем случае это будет “PluginsViaMEF.Contracts.IParser”. Вы помните, что именно с таким контрактом мы экспортировали наши парсеры. Теперь, при построении композиции, MEF проинициализирует поле _parser экземпляром одного из парсеров. Наша задача помочь ему выбрать конкретный парсер. Т.к. если MEF обнаружит несколько экспортов соответствующих одному импорту, то он выкинет исключение и перестанет строить композицию.

private void Compose(string fileExtension)
{
var container = new CompositionContainer();
var catalog = new DirectoryCatalog(@".\");

CompositionBatch batch = new CompositionBatch();
batch.AddPart(this);
batch.AddPart(
catalog
.Parts
.Where(
p => p.Metadata.ContainsKey("Extension") &&
string.Equals(
p.Metadata["Extension"].ToString(),
fileExtension,
StringComparison.OrdinalIgnoreCase)
)
.First().CreatePart());

container.Compose(batch);
}

Итак, для того чтобы получить список доступных парсеров, мы создаем DirectoryCatalog и скармливаем ему папку в которой должны лежать наши парсеры. Далее мы используем мета-данные парсеров, чтобы получить только один парсер, который соответствует типу полученного от пользователя файла. И наконец, контейнеру для построения композиции мы даем только основную сборку программы и найденный парсер. Контейнер создаст экземпляр парсера и проинициализирует им поле _pаrser в главном модуле.

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

Ссылки:

  1. MEF community site on Codeplex

Tuesday, September 21, 2010

MVVM: Command bindings, Messaging and Multithreading

В предыдущих статьях, посвященных MVVM Light toolkit, я уже приводил примеры использования RelayCommand и ViewModelBase классов. Сейчас я хочу подробнее остановиться на еще трех классах, которые предлагает этот toolkit, а именно:

  1. EventToCommand behavior позволяет привязывать команды к любым событиям любых UI контролов.
  2. Класс Messenger позволяет организовывать обмен сообщениями внутри приложения.
  3. Класс DispatcherHelper облегчает жизнь при работе с UI потоком.

EventToCommand

Несмотря на все свои достоинства, в WPF команды поддерживаются очень ограниченным числом UI элементов (только производными от типа ButtonBase). Более того команду можно привязать только к событию Click. Получается, что для того чтоб запустить комаду, определенную во ViewModel, по событию сгенерированному UI элементом, не поддерживающим команды, нам понадобится в codebehind создавать обработчик этого события и из него руками вызывать команду.

Однако всех этих сложностей можно избежать, если использовать тип EventToCommand предлагаемый MVVM Light toolkit. Давайте рассмотрим небольшой пример. Допустим у нас есть TextBox и мы хотим к его событию TextChanged привязать команду ChangeMode. Сделать это очень просто:

<TextBox Text="{Binding TagName, UpdateSourceTrigger=PropertyChanged}"
         Grid.Column="1" Margin="0 0 0 5" MinWidth="200">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TextChanged">
            <cmd:EventToCommand
                Command="{Binding ChangeMode, Mode=OneWay}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

Messenger

Messenger позволяет организовать обмен сообщениями внутри приложения. В отличие от событий, отправитель сообщений не хранит ссылки на получателей. Получатели в общем случае могут даже и не знать отправителя.

Messaging

Вместо того чтобы подписываться на сообщения от какого-то конкретного объекта получатели просто сообщают Messenger’у какие именно сообщения они хотят получать. Например, вот так можно зарегистрироваться для получения сообщений типа TagInfo:

Messenger.Default.Register<TagInfo>(this,
    x =>
    {
        Tags.Add(x);
    });

Теперь получателю будут приходить все сообщения, содержащие TagInfo, от любого отправителя. Лямбда, приведенная в примере, описывает реакцию получателя на сообщение.

С другой стороны отправители не занимаются рассылкой самостоятельно. Вместо этого они создают экземпляр сообщения нужного типа и просят Messenger’а его оправить:

Messenger.Default.Send<TagInfo, MainViewModel>(
    new TagInfo
    {
        Name = TagName,
        Score = TagScore
    });

После отправки это сообщение будет доставлено всем заинтересованным получателям.

Хочу отметить следующие три момента связанные с Messenger’ом:

  • Связи между отправителями и получателями можно настраивать. Совсем не обязательно рассылать сообщения всем подряд. Например, используя токены при регистрации и отправке, можно организовать общение только между конкретными экземплярами отправителя и получателя.
  • Messenger никак не работает с потоками. Т.е. если необходимо пробрасывать сообщения из одного потока в другой, то организовывать такую переброску нужно самостоятельно.
  • Messenger не хранит жестких ссылок на получателей. Т.е. после регистрации ничто не мешает сборщику мусора удалить экземпляр получателя.

DispatcherHelper

DispatcherHelper – это класс, который упрощает запуск операций в потоке UI из других потоков. При инициализации DispatcherHelper сохраняет в своем свойстве UIDispatcher ссылку на диспетчера потока UI. После этого любая операция, запущенная с помощью DispatcherHelper.CheckBeginInvokeOnUI() гарантированно будет выполнена в потоке UI. При этом если запуск происходит из потока UI, то операция будет выполнена стазу же синхронно. Если же из другого потока, то операция будет поставлена в очередь выполнения диспетчера потока UI и выполнена асинхронно.

Вот так, например, можно отправить сообщение из фонового потока в поток UI:

DispatcherHelper.CheckBeginInvokeOnUI(
    () =>
    {
        Messenger.Default.Send<TagInfo, MainViewModel>(
            new TagInfo
            {
                Name = DateTime.Now.ToLongTimeString(),
                Score = 1
            });
    }
);

На этом у меня все. Исходный код проекта, демонстрирующего описанный функционал, можно скачать здесь.

Wednesday, September 15, 2010

MVVM: How do I wire up my View and ViewModels?

В этой статье я хочу рассмотреть три способа связывания View c ViewModel.

Способ первый. В коде. Как мне кажется наиболее гибкий вариант. Позволяет использовать параметризированные конструкторы. Это полезно когда, например, вам нужно инжектировать View во ViewModel. Я уже показывал этот способ, когда мы рассматривали ViewModel-First approach.

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

Способ второй. Объявить ViewModel как статический ресурс и затем в XAML прописать привязку для View. Этот способ менее гибкий, т.к. для создания ресурса будет использован дефолтный конструктор.

В MVVM Light Toolkit проблему дефолтного конструктора решили. Вместо того чтоб объявлять сам ViewModel как статический ресурс, ресурсом там сделали класс ViewModelLocator, который внутри себя в коде создает нужные ViewModel и соответственно может использовать параметризованные конструкторы. Однако, я пока не нашел решения как используя этот способ инжектировать View во ViewModel.

Этот способ мы уже тоже видели в примерах, где использовался MVVM Light Toolkit.

<Application.Resources>
<vm:ViewModelLocator x:Key="Locator"
d:IsDataSource="True" />
</Application.Resources>

<Window
DataContext="{Binding Main, Source={StaticResource Locator}}">

Способ третий. Наверное не совсем очевидный но тем не менее. Внутри DataTemplate свойство DataContext всегда хранит ссылку на объект, к которому это DataTemplate применяется. Если использовать термины MVVM, то DataTemplate – это View, а сам объект – это ViewModel. Т.е. внутри DataTemplate можно привязывать UI контролы к любым свойствам и командам, которые светит объект.

Давайте рассмотрим небольшой пример демонстрирующий использование DataTemplate. За основу возьмем демо приложение из предыдущих статей. Список тегов для него я взял со StackOverflow. Давайте добавим к тегам еще их рейтинг. Описывать каждый тег теперь будет тип TagInfo. Это будет наш ViewModel.

public sealed class TagInfo
{
public string Name { get; set; }
public int Score { get; set; }
}

А вот так выглядит DataTemplate для TagInfo.

<DataTemplate DataType="{x:Type m:TagInfo}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" MinWidth="150" />
<TextBlock Text="{Binding Score}" />
</StackPanel>
</DataTemplate>

Если помните, ListBox в главном окне привязан к коллекции Tags из MainViewModel. Раньше эта коллекция хранила строки, а теперь объекты TagInfo. Когда ListBox захочет отобразить свои элементы, то он надет для них наш DataTemplate и подставит ему в DataContext соответствующий TagInfo.

Вот так будет выглядеть список тегов после применения DataTemplate.

DataTemplatesDemo

Исходный код этого примера можно скачать отсюда.

MVVM: multiple Views for a single ViewModel

В примерах, которые я показывал до сих пор, всегда был один ViewModel, который связывался только с одним View. Давайте теперь посмотрим, как можно связать ViewModel с несколькими View.

Давайте в демо приложение из предыдущей статьи добавим еще одно окошко DetailsWindow, в котором будем показывать текущее число тегов. XAML для нового окна будет очень простой.


<Window x:Class="MultipleViewsDemo.DetailsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="150" Height="75"
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
WindowStartupLocation="CenterScreen"
Title="DetailsWindow"
DataContext="{Binding Main, Source={StaticResource Locator}}">

<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="Number of Tags:" />
<TextBlock Text="{Binding Tags.Count}" Margin="5 0 0 0" />
</StackPanel>
</Window>

Ключевой момент, на который нужно обратить внимание – это привязка свойства DataContext нового окна. Она абсолютно идентична привязке DataContext’a главного окна. Т.е. оба окна привязаны к одному и тому же ViewModel.

DataContext="{Binding Main, Source={StaticResource Locator}}"

Теперь при добавлении новых тегов оба окна будут реагировать на изменения во ViewModel. Главное окно будет показывать новые теги в списке, а в DetailsWindow будет обновляться количество тегов.

Исходный код можно скачать отсюда.

Tuesday, September 14, 2010

MVVM Light Toolkit overview

В предыдущих двух статьях про MVVM для создания примеров я пользовался только стандартными средствами, которые out of box предоставляет WPF. Для очень простых случаев, возможно, такой подход и оправдан. Недостаток его в том, что приходится писать много plumbing кода. А это рутина, от которой всегда хочется избавиться.

Для имплементации команды AddNewTagCommand мы создавали приватный класс во ViewModel. Но у нас была всего одна команда. Если понадобится больше команд, то писать для каждой из них свой класс – не самая лучшая идея. Надо будет каким-то образом выносить общую функциональность.

Наш ViewModel реализовывал интерфейс INotifyPropertyChanged. Дублировать его реализацию в каждом ViewModel нехорошо. Поэтому ее тоже надо выносить, например, в базовый класс.

Не смотря на все удобство команд, они почему-то поддерживаются только контролами производными от ButtonBase и только для события Click. Что же делать, если вы хотите использовать команду для реакции на SelectionChanged у ListBox’a? Написать свой собственный behavior, который сможет привязать команду к произвольному событию.

Для того чтобы использовать эти наработки в других своих WPF приложениях, наверняка вы вынесете их в отдельную библиотеку. Поздравляю, вы написали свой собственный MVVM Toolkit! :)

На самом деле MVVM – это просто паттерн, и для его применения не нужны никакие тулкиты. Все тулкиты – это просто хэлперы, которые облегчают вам жизнь, избавляя от рутины. Список желающих помочь вам оказался настолько внушительным, что я отказался от первоначальной идеи делать обзор и сравнение их всех. Итак, согласно Википедии сейчас существует 14 различных MVVM toolkit’ов:

Для полноты картины следует добавить сюда еще PRISM (который, на самом деле, гораздо больше чем просто очередной MVVM toolkit) и nRote.

В комментариях в блоге MVVM Light Toolkit автора спросили: «Laurent, почему ты написал свой собственный тулкит? Ведь ты общаешься со столькими WPF гуру, каждый из которых, кстати, тоже написал свой тулкит :) Так почему же вы, парни, не пообщались и не написали что-то одно? Вместо того чтобы запутывать людей этой горой тулкитов, функционал которых пересекается на 90% и отличается нюансами имплементации.»

Я хочу привести здесь ответ, потому что он достаточно четко описывает идеи заложенные в MVVM Light Toolkit: «I think that the main reason is that we have different reasons to use (and make) such a framework. For me, the main reason is really Blendability (design time data, specifically), with testability of the UI a close second. For others, validation plays a very large role in their adoption of the MVVM pattern, etc. In addition, to be useful to me, a framework needs to work in Silverlight as well as in WPF, while others do not care about Silverlight (yet?) and find it too limited. I cannot rely on WPF-only mechanisms in my framework, but others do because their craft is in a different area than mine.

For what it is worth, there is quite an intense collaboration between some of the disciples and me on this toolkit. The toolkit I provide is a "blend" (pun intended) of code and ideas coming from different sources. The credits section on the Get Started page lists some of the major contributors, most of them from the disciples group. I have fished many ideas and solved many problems on the group.

Lastly, I would be a bit reluctant to engage in a large scale operation to provide a "one and only" MVVM framework. To be blunt, it sounds like a lot less fun than what I am doing now. The good news is that many at Microsoft are aware of this effort, and I think we will see improvements in the framework (WPF and Silverlight) directly that should make some of this toolkit redundant. This is why I put a lot of effort in keeping it "light", because I am pretty sure it will be obsolete some day.»

Итак, MVVM Light Toolkit – это легковесная библиотека классов, которая упростит вам жизнь, избавив от рутины при имплементации MVVM паттерна для WPF, Silverlight и Windows Phone 7 приложений. Давайте посмотрим, что именно предлагает этот тулкит:

  1. Классы RelayCommand и RelayCommand<T>. Реализуют интерфейс ICommand для WPF и Silverlight. Упрощают создание команд.
  2. Класс ViewModelBase может быть использован как базовый для всех ViewModel’ов. Избавляет от необходимости реализовывать INotifyPropertyChanged.
  3. EventToCommand behavior позволяет привязывать команды к любым событиям любых UI контролов.
  4. Класс Messenger позволяет организовывать обмен сообщениями внутри приложения.
  5. Класс DispatcherHelper облегчает жизнь при работе с потоками.

Список небольшой, но минимально достаточный. Если для решения ваших задач вам чего-то не хватает, то смотрите на другие фреймворки из приведенного выше списка. Я же перейду от слов к коду и покажу какие изменения произойдут в демо приложении, которое мы создали в предыдущих статьях.

Давайте начнем с команд. Т.к. Теперь у нас есть RelayCommand, то мы можем избавиться от класса CustomCommand. Теперь команда AddNewTagCommand будет выглядеть вот так:

public ICommand _addNewTagCommand;
public ICommand AddNewTagCommand
{
get
{
if (_addNewTagCommand == null)
_addNewTagCommand = new RelayCommand(AddNewTagExecute);

return _addNewTagCommand;
}
}

public void AddNewTagExecute()
{
Tags.Add(NewTag);
NewTag = string.Empty;
}

Обратите внимание, что логика команды переехала во ViewModel.

Кроме реализации ICommand из нашего ViewModel ушла также и реализация INotifyPropertyChanged. Вместо этого теперь наш ViewModel наследуется от ViewModelBase, в котором INotifyPropertyChanged уже реализован.

public sealed class MainViewModel : ViewModelBase

На этом все. В следующей статье я рассмотрю примеры использования EventToCommand behavior’a, Messenger’a и DispatcherHelper’a.

Исходный код можно скачать отсюда

Sunday, September 12, 2010

MVVM: ViewModel-First approach

Я решил ненадолго отложить обзор MVVM тулкитов, а вместо этого продолжить разбираться с самим паттерном.

В предыдущей статье я описал основные блоки MVVM, их ответственности и на простом примере показал взаимодействие между ними. Тот вариант реализации MVVM называется View-First, т.к. именно View через свое свойство DataContext знает про ViewModel, а ViewModel наоборот абсолютно ничего не знает про View.

MVVM-ViewFirst

Сегодня я хочу описать другой подход к организации взаимодействия между блоками в паттерне MVVM. Называется он ViewModel-First и отличается от предыдущего тем, что здесь ViewModel знает про View c которым работает и может вызывать его методы, подписываться на его события и т.д.

MVVM-ViewModelFirst

Зачем ViewModel’у может понадобиться знать про View? Как правило для того чтобы управлять состоянием UI, но при этом не иметь жестких ссылок на UI контролы.

Для того чтобы применить ViewModel-First подход нужно сделать следующее:

  1. Определить максимально узкий и независимый от элементов UI интерфейс, необходимый для взаимодействия с View из ViewModel
  2. Имплементировать этот интерфейс во View
  3. Каким-либо образом пропихнуть экземпляр этого интерфейса во 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, IView
{
...

public void SetReadyState()
{
TagsListView.SelectedIndex =
GetFirstVisible(TagsListView) + 3;
}
...
}
ViewModel тоже нужно будет модифицировать так, чтобы он мог принимать интерфейс IView. Мы сделаем это через дополнительный параметр конструктора.

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 из нашего демо.

Saturday, September 11, 2010

Introduction to MVVM

Я собираюсь написать серию статей про MVVM, где подробно рассмотрю проблемы, которые решает этот паттерн, его отличия от аналогов, его структуру, модификации, существующие рекомендации по использованию, а также некоторые доступные сейчас фреймворки, избавляющие от рутины при использовании MVVM.

Это первая обзорная статья. Здесь я хочу представить сам паттерн и привести небольшой пример иллюстрирующий его идею.

Итак. При создании любого WPF приложения следует тщательно подумать над следующими вопросами:

  1. Как правильно спроектировать приложение?
  2. Как правильно обеспечить разделение ответственности между модулями?
  3. Как обеспечить тестирование приложения? Какие модули можно покрыть юнит тестами?

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

MVVM появился не на пустом месте. Существует целое семейство так называемых Separated Presentation паттернов, отделяющих визуальные компоненты UI от логики определяющей состояние и поведение этих компонентов, а также от бизнес логики. Первым из них в далеком 1979 году появился Model-View-Controller паттерн, затем Model-View-Presenter и его модификации.

В 2004 году Мартин Фаулер (Martin Fowler) представил Presentation Model паттерн, в котором блок модели представления (presentation model) играет роль фасада для модели и координирует все взаимодействие представления с моделью. Presentation Model Фаулера не привязан ни к какой конкретной платформе, и не описывает, как представление должно использовать данные из модели представления.

Наконец в 2005м году в своем блоге Джон Госсман (John Gossman) представил паттерн MVVM, который является реализацией Presentation Model Фаулера, учитывающей специфическую для WPF функциональность, такую, например, как привязки (Bindings) и команды (Commands).

MVVMStructure

В MVVM блок Model представляет собой один или несколько модулей реализующих бизнес-логику приложения. Блок View представляет собой XAML-файл содержащий UI представление. И наконец, блок ViewModel является как бы фасадом, который преобразовывает интерфейс модели в форму удобную для использования в UI представлении, а также содержит логику управления состоянием UI. При этом очень важно, что ViewModel ничего не знает о конкретных UI контролах находящихся во View.

Разделение UI (View) и логики управления состоянием UI (ViewModel) на отдельные компоненты является серьезным аргументом в пользу использования MVVM вместо CodeBehind, т.к. позволяет легко покрывать ViewModel unit-тестами.

Такое разделение не было бы возможно без data bindings – технологии связывания данных появившейся в WPF. Именно data bindings позволяет связывать свойства во View со свойствами во ViewModel при этом, не создавая жестких связей между ними.

Команды (commands) – еще одна технология WPF, которую активно использует MVVM. Именно команды позволяют пользователю, используя UI контролы на View, запускать функциональность, реализованную во ViewModel.

Давайте теперь рассмотрим небольшое демо, иллюстрирующее MVVM паттерн. Единственное окно нашего приложения будет содержать всего три UI элемента:

  • ListBox, со списком неких строковых элементов
  • TextBox, в который пользователь сможет ввести текст для нового элемента списка
  • Button, нажав на который пользователь сможет добавить новый элемент в список

MVVMDemo1App

Давайте посмотрим на XAML, описывающий это окно:

<Window x:Class="PureMVVMSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="250">
<DockPanel LastChildFill="True">
<TextBlock Text="Tags:" Margin="5 5 0 0" DockPanel.Dock="Top" />

<StackPanel DockPanel.Dock="Bottom">
<TextBox
Text="{Binding NewTag, UpdateSourceTrigger=PropertyChanged}"
Margin="5" />
<Button
Content="Add" MaxWidth="100" Margin="5"
Command="{Binding AddNewTagCommand}" />
</StackPanel>

<ListBox
Margin="5 0"
SelectionMode="Single"
ItemsSource="{Binding Tags}" />
</DockPanel>
</Window>

Обратите внимание на следующие два момента. Во-первых, ни одному из элементов не присвоено имя. Это потому что имена контролам просто не нужны. Мы не будем ниоткуда явно на них ссылаться. Во вторых, точно так же как и имена, отсутствуют и обработчики событий. Дело в том, что обработчики событий потребуют от нас написания какого-то кода в CodeBehind, а это как раз одна из вещей которых мы хотим избежать.

Вместо имен для явных ссылок и обработчиков событий, мы используем data bindings и commands. Давайте посмотрим на описание ListBox’a.

<ListBox
Margin="5 0"
SelectionMode="Single"
ItemsSource="{Binding Tags}" />

Обратите внимание на binding, который связывает свойство ListBox’a ItemsSource со свойством Tags, определенным во ViewModel. Что именно представляет из себя Tags мы рассмотрим позже. Сейчас же важно понять, что этот binding является единственным местом, где мы связываем UI контрол ListBox с источником данных для него. Причем, смотрите здесь нет никаких упоминаний о типе ViewModel’a. Т.е. View и ViewModel не привязаны друг к другу жестко. Наш ListBox может отображать данные из абсолютно любого ViewModel, в котором определено свойство Tags возвращающее коллекцию элементов.

Давайте теперь посмотрим на описание кнопки. Вы видите, что здесь нет обработчика события Click. Зато к свойству Command кнопки привязана команда AddNewTagCommand определенная во ViewModel. Точно также как и в случае привязки источника данных к ListView здесь нет жесткой привязки нашего View к какому-то конкретному ViewModel.

<Button
Content="Add" MaxWidth="100" Margin="5"
Command="{Binding AddNewTagCommand}" />

Ну и наконец, давайте убедимся в отсутствии какой-либо логики в CodeBehind нашего View. Вы видите, что здесь есть только конструктор.

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

Model в нашем случае очень простая. Она реализует некий интерфейс IDataProvider, позволяя во ViewModel получать необходимые данные.

internal interface IDataProvider
{
IEnumerable<string> Tags { get; }
}

internal sealed class DataProvider : IDataProvider
{
public IEnumerable<string> Tags
{
get
{
return new List<string>
{
"c#", "java", "php", ".net"
};
}
}
}

Разобравшись с Model и View, давайте теперь рассмотрим ViewModel – наиболее интересный блок в MVVM. Для начала давайте посмотрим на свойство Tags, из которого получает данные ListBox во View:

public ObservableCollection<string> Tags
{
get;
private set;
}

Итак, Tags – это обычное свойство, которое светит коллекцию. Тип коллекции ObservableCollection выбран неслучайно. ObservableCollection умеет нотифицировать клиентов в случае добавления \ удаления ее элементов. Это позволяет UI контролам связанным со свойством Tags обновлять свой контент самостоятельно без какого-либо дополнительного кода.

Инициализируется Tags в конструкторе класса ViewModel.

public MainViewModel(IDataProvider provider)
{
Tags = new ObservableCollection<string>(provider.Tags);
}

Теперь давайте разберемся, что представляет из себя команда и посмотрим, как она реализована в нашем демо. Все команды в WPF должны реализовывать интерфейс ICommand:

public interface ICommand
{
event EventHandler CanExecuteChanged;

bool CanExecute(object parameter);
void Execute(object parameter);
}

Тут все достаточно прозрачно. Метод Execute содержит логику самой команды. Метод CanExecute определяет можно или нет выполнить команду. Событие CanExecuteChanged оповещает клиентов об изменении возможности выполнения команды.

public void Execute(object parameter)
{
_viewModel.Tags.Add(_viewModel.NewTag);
_viewModel.NewTag = string.Empty;
}

В нашем случае метод Execute всего лишь добавляет новый элемент в коллекцию Tags. При этом текст для нового элемента он берет из свойства NewTag определенного во ViewModel. Обратите внимание, что свойство NewTag привязано к свойству Text UI контрола TextBox описанного на View.

<TextBox
Text="{Binding NewTag, UpdateSourceTrigger=PropertyChanged}"
Margin="5" />

Процесс добавления элемента в список можно описать следующим образом:

  1. Пользователь вводит текст в TextBox
  2. DataBinding пропихивает значение свойства Text TextBox’a в свойство NewTag во ViewModel.
  3. Команда AddNewTagCommand добавляет новый элемент в коллекцию Tags используя значение свойства NewTag.
  4. При добавлении элемента коллекция Tags уведомляет своих клиентов (в нашем случае ListBox) об изменениях. Клиенты обновляют свой контент.

Т.е. вы видите, что команда не использует явные ссылки на UI контролы. Вместо того чтобы явно взять текст из TextBox’a и затем добавить его в коллекцию элементов ListBox’a, команда работает только со свойствами определенными во ViewModel и не зависящими от View.

Метод CanExecute для определения возможности выполнения команды аналогичным образом использует только свойства определенные во ViewModel.

public bool CanExecute(object parameter)
{
return
!string.IsNullOrEmpty(_viewModel.NewTag) &&
!_viewModel.Tags.Contains(_viewModel.NewTag);
}

Последний момент, который я хочу показать – это как же все-таки устанавливается связь между View и ViewModel. При запуске приложения свойству DataContext определенному на View присваивается экземпляр ViewModel. И все. Этого достаточно для работы data binding’a.

private void Application_Startup(object sender, StartupEventArgs e)
{
var viewModel = new MainViewModel(new DataProvider());

var mainWindow = new MainWindow
{
DataContext = viewModel
};

Current.MainWindow = mainWindow;
mainWindow.Show();
}

На этом, пожалуй, все. Полный код демо можно скачать отсюда. В следующей части я рассмотрю существующие MVVM тулкиты и более детально остановлюсь на MVVM Light Toolkit.

Saturday, August 28, 2010

Static constructors vs Type initializers

Недавно в обсуждении с коллегами возник вопрос: есть ли разница между инициализацией статического поля при объявлении и инициализацией его в статическом конструкторе. Т.е. отличаюся ли типы Test в примере ниже:

public class Test
{
public static object o = new object();
}

public class Test
{
public static object o;

public static Test()
{
o = new object();
}
}

Оказывается отличаются. Более того второй пример абсолютно идентичен следующему:

public class Test
{
public static object o = new object();

public static Test()
{
}
}

Подробные объяснения почему так получилось читаем здесь.

Friday, August 27, 2010

Testable Singleton Clients

Вчера послушал 19й Подкаст Петербургской Группы Alt.Net. Ребята так сильно пинали несчастный Singleton, что я решил написать пару слов в его защиту. Одним из недостатков синглтона назвали то, что очень трудно, если вообще возможно, писать юнит тесты для классов использующих синглтон. Я попробую опровергнуть это утверждение.

Давайте рассмотрим простенький пример. У нас есть класс TextProvider, который имплементит интерфейс ITextProvider и при этом является синглтоном.

public interface ITextProvider
{
    string GetText();
}

public sealed class TextProvider : ITextProvider
{
    private static readonly ITextProvider _instance = new TextProvider();

    private TextProvider()
    { }

    public static ITextProvider Instance
    {
        get { return _instance; }
    }

    public string GetText()
    {
        return "Hello from Singleton!";
    }
}

Классу Client, который использует наш синглтон, вообще говоря совсем не обязательно знать что он работает именно с синглтоном. Client просто получает в конструктор ссылку на интерфейс и дальше работает именно с ней.

public class Client
{
    private ITextProvider _textProvider;

    public Client(ITextProvider provider)
    {
        _textProvider = provider;
    }

    public void ConsumeText()
    {
        _textProvider.GetText();
    }
}

Теперь создать новый экземпляр клиента в коде можно вот так:

Client client = new Client(TextProvider.Instance);

А в тестах клиенту можно подсунуть замоканого провайдера:

ITextProvider provider = _mock.StrictMock<ITextProvider>();
Client client = new Client(provider);

Thursday, August 12, 2010

C# iterators: Compiler generated code example

В ходе обсуждения предыдущего поста возник вот такой вопрос: «Не совсем представляю, как это по произвольному классу, который реализует IEnumerable (т.е. просто должен уметь вернуть instance, реализующий IEnumerator), оно само сгенерирует реализацию IEnumerator». “Оно” – это в данном случае компилятор C# :)

Начну издалека. Для перебора элементов коллекций в C# есть удобный оператор цикла – foreach, который в общем виде выглядит вот так:

foreach (Type varName in enumerableObject)
{
...
}

Например:

IEnumerable<string> _names = new List<string> { "a", "b" };
foreach (string name in _names)
{
Console.WriteLine(name);
}

На самом деле компилятор развернет этот код вот так:

IEnumerable<string> _names = new List<string> { "a", "b" };

IEnumerator<string> ne = _names.GetEnumerator();
while (ne.MoveNext())
{
string name = ne.Current;
Console.WriteLine(name);
}

Получается, что foreach выполняет два действия:

  • вызывает у переданной ему коллекции метод GetEnumerator()
  • используя полученный объект IEnumerator, пробегается по всем элементам коллекции

Давайте посмотрим, что представляет собой IEnumerator:

public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}

public interface IEnumerator<T> : IDisposable, IEnumerator
{
T Current { get; }
}

C# реализует специальную конструкцию – итератор, которая позволяет не создавать IEnumerator руками, каждый раз, когда вам это понадобится. Давайте посмотрим как это выглядит в коде. Класс OpenFileControl (неважно откуда) получает список имен файлов и реализует метод GetStreams(), который возвращает IEnumerable<Stream> для этого списка. Это почти боевой пример. Реальный OpenFileControl получает и имена, и стримы для файлов с WCF сервиса.

public class OpenFileControl
{
private IEnumerable<string> _files = new List<string>();

public IEnumerable<Stream> GetStreams()
{
foreach (string file in _files)
{
yield return File.Open(file, FileMode.Open);
}
}
}

Теперь воспользуемся Reflector’ом чтобы посмотреть, что сгенерил для этого примера компилятор:

public class OpenFileControl
{
private IEnumerable<string> _files;

public IEnumerable<Stream> GetStreams();

// Nested Types
[CompilerGenerated]
private sealed class <GetStreams>d__0 : IEnumerable<Stream>, IEnumerable, IEnumerator<Stream>, IEnumerator, IDisposable
{
// Fields
private int <>1__state;
private Stream <>2__current;
public OpenFileControl <>4__this;
public IEnumerator<string> <>7__wrap2;
private int <>l__initialThreadId;
public string <file>5__1;

// Methods
[DebuggerHidden]
public <GetStreams>d__0(int <>1__state);
private void <>m__Finally3();
private bool MoveNext();
[DebuggerHidden]
IEnumerator<Stream> IEnumerable<Stream>.GetEnumerator();
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator();
[DebuggerHidden]
void IEnumerator.Reset();
void IDisposable.Dispose();

// Properties
Stream IEnumerator<Stream>.Current { [DebuggerHidden] get; }
object IEnumerator.Current { [DebuggerHidden] get; }
}
}

Имена у сгенерированных объектов, конечно, не особо читабельные, но разобраться все-таки можно. Вы видите, что в OpenFileControl добавился приватный класс, реализующий одновременно и IEnumerable<Stream>, и IEnumerator<Stream>. При этом наш метод GetStreams() изменился и теперь он просто возвращает экземпляр сгенерированного класса:

public IEnumerable<Stream> GetStreams()
{
return new <GetStreams>d__0(-2) { <>4__this = this };
}

Логика по перебору элементов коллекции, как и ожидалось, переехала в метод MoveNext() сгенерированного энумератора.

private bool MoveNext()
{
try
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>7__wrap2 = this.<>4__this._files.GetEnumerator();
this.<>1__state = 1;
while (this.<>7__wrap2.MoveNext())
{
this.<file>5__1 = this.<>7__wrap2.Current;
this.<>2__current = File.Open(this.<file>5__1, FileMode.Open);
this.<>1__state = 2;
return true;
Label_0078:
this.<>1__state = 1;
}
this.<>m__Finally3();
break;

case 2:
goto Label_0078;
}
return false;
}
fault
{
this.System.IDisposable.Dispose();
}
}

Использовать OpenFileControl можно примерно так:

OpenFileControl ofc = new OpenFileControl();
foreach (Stream s in ofc.GetStreams())
{
using (s)
{
byte[] buf = new byte[10];

s.Read(buf, 0, 10);

// ...
}
}

Обратите внимание на то, что GetStreams() не открывает все файлы одновременно. На каждой итерации цикла открыт только текущий файл.

Tuesday, August 10, 2010

DIP: real world usage sample

Вчера, просматривая очередное DnrTV! видео, я наткнулся на интересный пример реализации принципа Dependency Inversion. Я уже подробно описывал этот принцип вот здесь, но для того чтобы вы поняли, что меня заинтересовало, еще раз кратенько повторю основные моменты.

Итак. Для того чтобы убрать из класса Manager жесткую ссылку на класс Worker, мы создали интерфейс IWorker, где определили весь функционал, который требуется Manager’у от Worker’а. Затем мы заменили в Manager’е ссылку на конкретный класс Worker ссылкой на интерфейс IWorker, а в классе Worker реализовали этот интерфейс. Полученные зависимости выглядели как на рисунке ниже.

DIP2

В этой реализации есть один нюанс. Любой класс, который мы захотим использовать вместо Worker должен реализовывать интерфейс IWorker.

Давайте рассмотрим другой пример. Допустим, у нас есть два класса: Radio и TV.

public class Radio
{
    public int Volume { get; private set; }
}
public class TV
{
    public int TvVolume { get; private set; }
}

И мы хотим создать некий класс Remote, который бы управлял громкостью радио и телевизоров. Вооружившись знанием DIP, не составляет никакой проблемы определить вот такой интерфейс IDevice:

public interface IDevice
{
    int Volume { get; set; }
}

Реализовать этот интерфейс в классах Radio и TV. И затем определить класс Remote следующим образом:

public class Remote
{
    private IDevice _device;

    public Remote(IDevice device)
    {
        _device = device;
    }

    public void IncreaseVolume()
    {
        _device.Volume +=1;
    }
}

А что делать, если мы не хотим чтобы все классы, которыми можно управлять из Remote, обязательно реализовывали IDevice? Причины могут быть разные. Например, мы не хотим расширять API наших девайсов, добавляя реализацию IDevice. Или, как в случае с классом TV, мы не хотим изменять его API. Там уже есть свойство TvVolume, и нет смысла его переименовывать в Volume или добавлять свойство Volume рядом.

Мы пойдем другим путем. Вместо выделения общего для всех устройств интерфейса IDevice мы создадим интерфейс IRemote, в который включим всю функциональность нужную для управления громкостью устройства.

public interface IRemote
{
    void IncreaseVolume();
}

Теперь класс Radio можно переписать так:

public class Radio
{
    public int Volume { get; private set; }

    public IRemote GetRemote
    {
        return new Remote(this);
    }

    private ctass Remote : IRemote
    {
        private Radio _radio;

        public Remote(Radio radio)
        {
            _radio = radio;
        }

        public void IncreaseVolume()
        {
            _radio.Volume +=1;
        }
    }
}

Переписав аналогичным образом класс TV, мы получим возможность написать нечто подобное:

Radio radio = new Radio();
Console.Write(radio.Volume);
IRemote radioRemote = radio.GetRemote();
radioRemote.IncreaseVolume();
Console.Write(radio.Volume);

TV tv = new TV();
Console.Write(tv.TvVolume);
IRemote tvRemote = tv.GetRemote();
tvRemote.IncreaseVolume();
Console.Write(tv.TvVolume);

Не будем останавливаться и определим еще один интерфейс:

public interface IRemotable
{
    IRemote GetRemote();
}

Реализовав его в классах Radio и TV, мы сможем переписать клиентский код вот так:

IRemotable radio = new Radio();
IRemote radioRemote = radio.GetRemote();
radioRemote.IncreaseVolume();

IRemotable tv = new TV();
IRemote tvRemote = tv.GetRemote();
tvRemote.IncreaseVolume();

Как видите, мы успешно развернули зависимости. Теперь клиентский код может управлять любым устройством реализующим IRemotable, вообще не зная конкретный тип этого устройства.

Ну как? Ничего не напоминает? Наши IRemote и IRemotable по смыслу – это один в один IEnumerator и IEnumerable из .Net BCL, про интенсивность использования которых, я думаю, не стоит даже говорить.

Вот такой вот нашелся интересный пример реализации DIP сразу же с отличным примером практического использования. Кому интересно, рекомендую посмотреть оригинал.

Sunday, August 1, 2010

Устойчивость. Принципы проектирования модулей (часть 2)

Давайте рассмотрим последние три принципа проектирования списка Роберта Мартина, касающиеся проектирования модулей. Эти принципы позволяют управлять связностью (coupling) ваших модулей. Как вы, наверно, помните из поста про метрики кода, чем меньше связность, тем лучше. Однако, полностью избавиться от зависимостей между модулями все равно нельзя. Если модули должны взаимодействовать, то между ними должна быть связь. Давайте разберемся, каких зависимостей следует избегать, а какие стоит использовать.

ADP. The Acyclic Dependencies Principle

Граф зависимостей между модулями приложения не должен содержать циклов.

Давайте рассмотрим пример. Мы разрабатываем приложение, которое состоит из нескольких модулей. В соответствии с принципом REP, каждый модуль и ничего не знает про исходники модулей, от которых он зависит.

DAG

Когда, например, разработчики модуля Task1 выпустят новую версию, то достаточно просто определить какие модули теперь нуждаются в обновлении: Task Window и Application. Разработчики этих модулей должны будут решить, когда они проинтегрируют свои модули с новой версией Task1.

Кроме того процесс сборки всего приложения тоже не вызывает затруднений. Достаточно пройтись по графу зависимостей снизу вверх и собрать последовательно каждый модуль.

Допустим, у нас появляются новые требования, и для их реализации мы добавляем в Task2 ссылку на Application.

DCG

В графе зависимостей у нас появился цикл. Теперь все модули, которые зависят от Task2, зависят еще и от Application. Теперь не получится просто пройтись по графу, чтобы собрать все приложение. Получается для того чтобы собрать Application нужно собрать все модули под ним, включая Task2. И одновременно, чтобы собрать Task2, нужно иметь уже собранный Application.

Одновременно – здесь ключевое слово. Получилось, что циклическая ссылка объединила все модули, попавшие в цикл, в один большой виртуальный модуль. Объединение их в один реальный модуль – это один из вариантов избавления от циклических ссылок. Но этот вариант не самый удачный.

Второй вариант избавления от циклов – вынесение функционала, от которого зависят Task2 и Application в отдельный новый модуль.

И наконец, третий вариант борьбы с циклами – применение принципа инверсии зависимостей (DIP). Для этого надо объявить в Task2 интерфейс, описывающий все, что Task2 хочет получать из Application. А в Application создать имплементацию этого интерфейса. Это позволит развернуть зависимость между Task2 и Application.

Стабильность

На картинке ниже изображена схема зависимостей между классами. Вы видите, что здесь применен принцип инверсии зависимостей. Вместо того чтобы явно ссылаться на KeyboardReader и PrinterWriter из класса Copy, мы объявили два интерфейса IReader и IWriter, поместили ссылки на них в класс Copy, и реализовали эти интерфейсы в KeyboardReader и PrinterWriter соответственно.

DIP

Как вы думаете, для каких из приведенных на рисунке типов вероятность изменения будет наименьшей? Наименьшей она будет для интерфейсов IReader и IWriter. По двум причинам.

Во-первых, интерфейсы приведенные на схеме ни от кого не зависят. Т.е. в нашей системе нет таких типов внесение изменений в которые, вызвало бы необходимость изменять IReader или IWriter. Типы, которые ни от кого не зависят, Мартин называет «независимыми» (independent).

Во-вторых, на интерфейсы ссылаются и те, кто их реализует, и те, кто их использует. Прежде чем вносить изменения в интерфейс стоит семь раз подумать хотите ли затем вносить изменения во все типы зависящие от этого интерфейса. Именно этот большой объем работы по внесению изменений во все зависимые типы, как раз и является тем фактором, что уменьшает вероятность изменения интерфейсов. Типы, от которых много кто зависит, Мартин называет «ответственными» (responsible).

Понятия независимости и ответственности для типов, как раз формируют определение стабильности. Наиболее стабильными являются независимые и ответственные типы, т.к. они не имеют причин для изменения и имеют много причин не изменяться. Соответственно чем меньше у типа ответственности и чем менее он независим, тем менее он стабилен, тем выше для него вероятность измениться.

SDP. The Stable Dependencies Principle

Зависимость должна быть в сторону большей стабильности. Модуль должен зависеть только от модулей более стабильных, чем он сам.

А как определить кто более стабильный, а кто нет? Как известно, для того чтобы что-то оценить, надо его изменить. Нестабильность любого пакета можно посчитать достаточно просто:

Instability

  • Сe (Efferent couplings) – число типов внутри пакета, которые зависят от типов из других пакетов.
  • Ca (Afferent couplings) – число типов в других пакетах, которые зависят от типов внутри пакета.
  • I (Instability) – Нестабильность пакета. Как видно из формулы эта метрика имеет диапазон [0, 1]. При 0 мы имеем идеально стабильный пакет, абсолютно независимый, и при этом ответственный. А при 1, наоборот, максимально нестабильный пакет, от которого никто не зависит, но который сам имеет много зависимостей.

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

SAP. Stable abstraction principle

Абстракция увеличивает стабильность. Уровень абстракции определяет уровень стабильности модуля.

Стабильные модули должны быть абстрактными. При этом их стабильность не будет препятствовать их расширению. И наоборот. Нестабильные модули должны быть конкретными. Это будет позволять изменять их.

Вспомните наш предыдущий пример. Интерфейсы IReader и IWriter – это абстракции. Они очень стабильны, т.к. ни от кого не зависят, и наоборот много кто зависит от них. Они могут быть легко расширены за счет новых реализаций и при этом останутся стабильными.

Стабильные пакеты более абстрактны, то есть имеют больше абстрактных классов и интерфейсов, редко изменяются. От них все зависят, а сами они мало зависят от остальных пакетов.

Принципы SDP и SAP вместе являются аналогом принципа Dependency Inversion. SDP рекомендует создавать связи только от менее стабильных пакетов к более стабильным, а SAP говорит, что стабильность подразумевает абстрактность.

В отличие от типов, которые однозначно либо абстрактные, либо нет, для модулей существует градация уровня абстрактности. Абстрактность модуля определяется отношением определенных в нем абстрактных типов к общему числу его типов.