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