История одного маппера

Однажды холодным зимним вечером я читал блог Brad Wilson, а именно вот эту статью и понял что нужно писать View-specific models для ASP.NET приложения. Основная проблема с написанием таких моделей заключается в том что приходится писать много “водопроводного кода” для маппинга сущностей базы, возвращаемых ORM на эти самые модели и наоборот. Причем Linq (если поддерживает ORM) позволяет описывать прямое преобразование, но не обратное.

Я начал искать object-to-object мапперы. Сразу нашел AutoMapper, он меня отпугнул монструозностью конфигурации и жутко неудобным (читай статическим) API использования. Кроме того отзывы о скорости работы этого маппера крайне негативные.

Следующим мне на глаза попался EmitMapper. Гораздо более приятный API для использования и довольно высокая скорость работы за счет кодогенерации. Но настройка и кастомизация выполняется очень многословно и непонятно.

В обоих проектах меня не устроил тяжелый API для маппинга. По сути маппинг из типа A в тип B - не более чем функция  A → B, или в нотации типов C# - Func<A,B>.

Я сел писать свой маппер. Естественно для скорости надо заниматься кодогенерацией, но писать свой кодогенератор в несколько килострок кода времени не хватит и проект будет заброшен. Но, к счастью, в .NET начиная с версии 3.5 есть кодогенератор и AST для него. Это классы наследники Expression из пространства имен System.Linq.Expressions, а компилятор тихо сидит в методе Expression<T>.Compile.

Таким образом задача упрощается до безобразия. Необходимо собрать expression tree и скомпилировать его. За два вечера я написал маппер, который поддерживает маппинг массивов, списков, сложных типов, конфигурацию с помощью expression tree и flattering.

Результаты сего труда я залил на Codeplex. Проект назвал Expression Mapper.

Скорость работы мапппера.

Пока писал маппер нашел бенчмарк на хабре. Решил прогнать свой маппер на таком бенчмарке. Результаты немного поразили:

Handwritten Mapper: 88 milliseconds
Emit Mapper: 157 milliseconds
Auto Mapper: 31969 milliseconds
Expression Mapper: 119 milliseconds

Недостатки.

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

Ссылка на страницу проекта - http://expressionmapper.codeplex.com/



Вот как-то так

MCPD

MCTS

Вчера сдал последний экзамен Microsoft по .NET разработке.



Типы, подтипы и вариантность.

Скоро (22 марта 2010)  выходит Visual Studio 2010, которая будет поддерживать C# 4.0 Больше всего вопросов возникает из-за новой фичи языка – ко- и контр- вариантности. Попытаюсь дать объяснение на человеческом языке.

В любой системе типов существуют отношения между типами. Нас интересует отношение типа-подтип. Для типов A и B будем обозначать A :> B, если A является подтипом B (B является супертипом A). Если A является подтипом B, то везде где в программе требуется значение типа B можно подставлять значение типа A без каких-либо дополнительных конструкций.

Например во многих языках тип целых чисел является подтипом вещественных. В ОО-языках такое отношение реализуется за счет наследования. Если A является наследником B, то A является подтипом B.

Тут стоит вспомнить принцип LSP (принцип подстановки Барбары Лисков). Он ошибочно приписывается к ООП, хотя имеет к нему весьма отдаленное отношение. Принцип гласит что если A :> B, то любое утверждение для B должно быть верно для A. Выполнение этого принципа означает что поведение программы при подстановке значения типа A там где требуется B не изменится.

Но это я ушел в сторону.  Когда у нас чистый ООП язык (как smalltalk) тогда отношения типов-подтипов исчерпываются наследованием, которое создает довольно простые отношения. Все становится сложно когда появляются типы, параметризуемые другими типами (обобщенные типы).

Будем обозначать обобщенный типа как T<`a>, где `a – параметр типа. Конкретный тип при подстановке параметра будем обозначать T<A>, где A – какой-то тип. Для иллюстраций нам понадобится обобщенный тип с одним параметром, хотя типов-параметром может быть много.

Тут возникает интересный вопрос. Если A :> B, то как связаны T<A> и  T<B> ?

Тип T<`a> называется ковариантными, если для A :> B выполняется T<A> :> T<B>, и контрвариантым, если A :> B выполняется T<B> :> T<A>,
если же T<A> и  T<B> не связаны никакими отношениями, то такой тип называет инвариантым.

Примеры.

1)IEnumerable<T>. Например если Apple унаследован от Fruit (то есть Apple :> Fruit), то вполне резонно было бы иметь IEnumerable<Apple> :> IEnumerable<Fruit>. Действительно, в .NET 4 IEnumerable<T> является ковариантым и имеет сигнатуру IEnumerable<out T>.

2)Action<T>. Например есть метод void Eat(Fruit f), он имеет тип Action<Fruit>, и у нас Apple :> Fruit. Тогда было бы хорошо иметь Action<Fuit> :> Action<Apple>, то есть если нам куда-то понадобится передавать Action<Apple> мы могли бы туда передать Action<Fruit>. В .NET 4 Action<T> является конртвариантным и имеет сигнатуру Action<in T>.

Магические слова in и out.

Такие модификаторы были выбраны неслучайно. Ко- и контр- вариантность может приводить к ошибкам при неумелом использовании. Например массивы в .NET 2 и выше являются ковариантными. То есть там где требуется Fruit[] можно передать Apple[]. Но программист может внутри метода, обрабатывающего Fruit[] присвоить элементу массива значение типа Banana. Что приведет к runtime error.

Чтобы ковариантность была безопасной необходимо чтобы ковариантные типы-аргументы были только в выходных значениях методов. То есть для T<out `a> можно писать методы возвращающие `a или имеющие out-параметры типа `a. также могут быть get-only свойства, возвращающие `a.

Аналогично для контрвариантного T<in `a> параметры типа `a могут быть только во входных параметрах методов.

Темная сторона силы.

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

PS. В C# отношение тип-подтип проверяется оператором is.



MVVM и TreeView

У многих программистов существует неудержимое желание организовывать любые объекты в иерархии, даже когда иерархия фактически не существует. При разработке интерфейса это проявляется в использовании контролов типа TreeView.

Основной недостаток TreeView в WPF заключается в том что не существует тривиальных путей подружить этот контрол с паттерном MVVM.

Привязать данные к элементам дерева достаточно просто. Для этого есть HierarchicalDataTemplate. В первом приближении ViewModel для элементов дерева будет выглядеть так:

   1: public class TreeViewItemModel: ViewModelBase
   2: {
   3:     string _name;
   4:  
   5:     public TreeViewItemModel()
   6:     {
   7:         Children = new ObservableCollection<TreeViewItemModel>();
   8:     }
   9:  
  10:     public string Name
  11:     {
  12:         get { return _name; }
  13:         set { _name = value; OnPropertyChanged("Name"); }
  14:     }
  15:  
  16:     public ObservableCollection<TreeViewItemModel> Children { get; private set; }
  17: }

ViewModelBase – класс из MVVM Toolkit.

В XAML пишем простой темплейт

   1: <TreeView.ItemTemplate>
   2:     <HierarchicalDataTemplate ItemsSource="{Binding Children}">
   3:         <TextBlock Text="{Binding Name}" />
   4:     </HierarchicalDataTemplate>
   5: </TreeView.ItemTemplate>

Теперь можно в качестве ItemsSource для TreeView указать любую коллекцию TreeViewItemModel.

Обеспечить обратную связь – от TreeView к модели представления – гораздо сложнее. Свойство TreeView.SelectedValue не имеет сеттера, поэтому привязать его в XAML к модели представления не получится, надо искать обходные пути.

Можно привязать свойство TreeViewItem.IsSelected к модели вида, но делать это надо в стиле дерева, а не в DataTemplate.

   1: <TreeView.ItemContainerStyle>
   2:     <Style TargetType="{x:Type TreeViewItem}">
   3:         <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
   4:     </Style>
   5: </TreeView.ItemContainerStyle>

В класс TreeViewItemModel надо добавить следующее свойство:

   1: bool _isSelected;
   2:  
   3: public bool IsSelected
   4: {
   5:     get { return _isSelected; }
   6:     set { _isSelected = value; OnPropertyChanged("IsSelected"); }
   7: }

Теперь можно создать модель представления всего дерева:

   1: public class TreeViewModel : ViewModelBase
   2: {
   3:    public TreeViewModel()
   4:    {
   5:        TopLevelItems = new ObservableCollection<TreeViewItemModel>();
   6:    }
   7:  
   8:    public ObservableCollection<TreeViewItemModel> TopLevelItems { get; private set; }
   9:  
  10:    public TreeViewItemModel SelectedItem 
  11:    {
  12:        get
  13:        {
  14:            return TopLevelItems
  15:                       .Traverse(item => item.Children)
  16:                       .FirstOrDefault(m => m.IsSelected);
  17:        }
  18:    }
  19: }
 
Изобретение метода Traverse для обхода древообразных структур данных оставлю как домашнее задание.
 
Теперь в любой момент времени у TreeViewModel можно получить выбранный элемент. Но свойство SelectedItem не поддерживает событие PropertyChanged, поэтому в интерфейсе вы не увидите изменений.
 
Надо исправить эту досадную ситуацию. Для этого надо подписаться на событие PropertyChanged для всех узлов дерева (в том числе для всех добавляемых) и желательно отписываться при удалении узлов из дерева, при изменении свойства IsSelected узла кидать событие PropertyChanged для свойства SelectedItem.
 
Полный код TreeViewModel:
   1: public class TreeViewModel : ViewModelBase
   2: {
   3:     PropertyChangedEventHandler _propertyChangedHandler;
   4:     NotifyCollectionChangedEventHandler _collectionChangedhandler;
   5:  
   6:     public TreeViewModel()
   7:     {
   8:         TopLevelItems = new ObservableCollection<TreeViewItemModel>();
   9:         _propertyChangedHandler = new PropertyChangedEventHandler(item_PropertyChanged);
  10:         _collectionChangedhandler = new NotifyCollectionChangedEventHandler(items_CollectionChanged);
  11:         TopLevelItems.CollectionChanged += _collectionChangedhandler;
  12:     }
  13:  
  14:     public ObservableCollection<TreeViewItemModel> TopLevelItems { get; private set; }
  15:  
  16:     public TreeViewItemModel SelectedItem 
  17:     {
  18:         get
  19:         {
  20:             return TopLevelItems
  21:                        .Traverse(item => item.Children)
  22:                        .FirstOrDefault(m => m.IsSelected);
  23:         }
  24:     }
  25:  
  26:     void subscribePropertyChanged(TreeViewItemModel item)
  27:     {
  28:         item.PropertyChanged += _propertyChangedHandler;
  29:         item.Children.CollectionChanged += _collectionChangedhandler;
  30:         foreach (var subitem in item.Children)
  31:         {
  32:             subscribePropertyChanged(subitem);
  33:         }
  34:     }
  35:  
  36:     void unsubscribePropertyChanged(TreeViewItemModel item)
  37:     {
  38:         foreach (var subitem in item.Children)
  39:         {
  40:             unsubscribePropertyChanged(subitem);
  41:         }
  42:         item.Children.CollectionChanged -= _collectionChangedhandler;
  43:         item.PropertyChanged -= _propertyChangedHandler;
  44:     }
  45:  
  46:  
  47:     void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  48:     {
  49:         if (e.OldItems != null)
  50:         {
  51:             foreach (TreeViewItemModel item in e.OldItems)
  52:             {
  53:                 unsubscribePropertyChanged(item);
  54:             }
  55:         }
  56:         
  57:         if (e.NewItems != null)
  58:         {
  59:             foreach (TreeViewItemModel item in e.NewItems)
  60:             {
  61:                 subscribePropertyChanged(item);
  62:             }
  63:         }
  64:     }
  65:  
  66:     void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
  67:     {
  68:         if (e.PropertyName == "IsSelected")
  69:         {
  70:             OnPropertyChanged("SelectedItem");
  71:         }
  72:     }
  73: }

XAML код элемента TreeView:

   1: <TreeView ItemsSource="{Binding TopLevelItems}">
   2:     <TreeView.ItemContainerStyle>
   3:         <Style TargetType="{x:Type TreeViewItem}">
   4:             <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
   5:         </Style>
   6:     </TreeView.ItemContainerStyle>
   7:  
   8:     <TreeView.ItemTemplate>
   9:         <HierarchicalDataTemplate ItemsSource="{Binding Children}">
  10:             <!--Здесь можно задать любой шаблон для элемента-->
  11:             <TextBlock Text="{Binding Name}" />
  12:         </HierarchicalDataTemplate>
  13:     </TreeView.ItemTemplate>            
  14: </TreeView>

PS. TreeViewItem также имеет свойство IsExpanded, которое очень помогает если надо сделать отложенную загрузку элементов дерева.



Паттерн MVVM. Часть 2.

В первой части я рассказал про байндинги и команды, которые позволяют вынести из формы всю логику во viewmodel.

На просторах интернета можно найти MVVM Toolkit, в котором есть необходимый код, упрощающий разработку приложений с использованием MVVM.

Кроме байндингов и команд немаловажную роль в MVVM играют шаблоны данных (DataTemplate). Они позволяют задавать шаблоны отображения определенных типов, что заметно упрощает композицию элементов UI.

Наиболее подробно, с примерами, применение шаблонов описано в этой статье.