История одного маппера
Однажды холодным зимним вечером я читал блог 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/
Типы, подтипы и вариантность.
Скоро (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: }
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.
Наиболее подробно, с примерами, применение шаблонов описано в этой статье.


 
        
        