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

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.

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



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

MVVM – Model – View – ViewModel – паттерн организации PL (presentation layer – уровень представления).

Паттерн MVVM применяется при создании приложений с помощью WPF и Silverlight.   Этот паттерн был придуман архитектором этих самых WPF и Silverlight - John Gossman (его блог). Паттерн MVVM применяется в Expression Blend.

Идеологически MVVM похож на Presentation Model описанный небезызвестным Фаулером, но MVVM сильно опирается на возможности WPF.

Основная особенность MVVM заключается в том, что все поведение выносится из представления (view) в  модель представления (view model).  Связывание представления и модели представления осуществляется декларативными байндингами в XAML разметке. Это позволяет тестировать все детали интерфейса не используя сложных инструментальных средств.

Я сначала хотел кратко описать применение MVVM и Unity для построения PL, но понял что одного поста для описания возможностей MVVM очень мало.

В WPF для передачи данных между объектами и визуальными элементами используются байндинги (binding – привязка) в простонародии биндинги. Передача может быть как однонаправленная, так и двунаправленная. Работают байндинги с помощью зависимых свойств (DependencyProperty) или интерфейса INotifyPropertyChanged. Передача управляющих воздействий от визуальных элементов осуществляется с помощью команд, реализующих интерфейс ICommand.

Для начала надоевший уже пример SayHello.

Как всегда используется супер-сложный класс бизнес логики:

public interface ISayHelloService
{
    string SayHello(string name);
}
 
public class SayHelloSerivce : ISayHelloService
{
    public string SayHello(string name)
    {
        return "Привет, " + name;
    }
}

Теперь определение класса команды, которая состоит из пары делегатов

public class DelegateCommand : ICommand
{
    Func<object, bool> _canExecute;
    Action<object> _execute;

    //Конструктор
    public DelegateCommand(Func<object, bool> canExecute, Action<object> execute)
    {
        this._canExecute = canExecute;
        this._execute = execute;
    }

    //Проверка доступности команды
    public bool CanExecute(object parameter)
    {
        return this._canExecute(parameter);
    }

    //Выполнение команды
    public void Execute(object parameter)
    {
        this._execute(parameter);
    }

    //Служебное событие
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested += value; }
    }
}

Теперь напишем нашу модель представления.

public class ViewModel: INotifyPropertyChanged
{
    //Имя
    public string Name { get; set; }

    //Текст приветствия
    public string HelloText { get; set; }

    //Команда
    public ICommand SayHelloCommand
    {
        get
        {
            return _sayHelloCommand;
        }
    }


    ISayHelloService _service;

    ICommand _sayHelloCommand;

    
    //Конструктор
    public ViewModel(ISayHelloService service)
    {
        this._service = service;

        //Создаем команду
        this._sayHelloCommand = new DelegateCommand(
            o => CanExecuteHello(),
            o => ExecuteHello());
    }
        
    private void ExecuteHello()
    {
        this.HelloText = _service.SayHello(this.Name);
        OnPropertyChanged("HelloText");
    }

    private bool CanExecuteHello()
    {
        return !string.IsNullOrEmpty(this.Name);
    }


    //Для поддержка байндинга
    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, 
                    new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion
}

Получилось слегка многословно по причине того, что  пример искусственный.

Дело за разметкой:

<StackPanel>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="75" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock Text="Введите имя"/>
        <TextBox Text="{Binding Name}" Grid.Column="1"/>
    </Grid>
    <TextBox Text="{Binding HelloText}"/>
    <Button Content="Сказать привет" Command="{Binding SayHelloCommand}"/>
</StackPanel>

И немного изменим констрктор View:

public Window1(ViewModel model)
{
    InitializeComponent();
    DataContext = model;
}

В App.xaml уберем атрибут StartupUri, и добавим обработчик события Startup, в котором напишем следующий код:

var container = new UnityContainer();
container
    .RegisterType<ViewModel>()
    .RegisterType<ISayHelloService, SayHelloSerivce>();
var window = container.Resolve<Window1>();
window.Show();

Можно нажать F5 и смотреть что получилось.

Теперь воспользуемся фичами WPF.

Изменим код ViewModel.

public class ViewModel : INotifyPropertyChanged
{
    //Имя
    public string Name
    {
        get { return this._name; }
        set
        {
            this._name = value;
            OnPropertyChanged("Name");
            OnPropertyChanged("HelloText");
        }
    }


    //Текст приветствия
    public string HelloText
    {
        get
        {
            return _service.SayHello(this.Name);
        }
    }

    string _name;
    ISayHelloService _service;

    //Конструктор
    public ViewModel(ISayHelloService service)
    {
        this._service = service;
    }

    //Для поддержка байндинга
    #region INotifyPropertyChanged Members
    //Без изменений
    #endregion
}

В разметке View уберем кнопку и поставим Mode=OneWay для байндинга второго текстбокса.

Кроме этого слега изменим App.xml.cs

var container = new UnityContainer();
container
    .RegisterType<ViewModel>(new ContainerControlledLifetimeManager())
    .RegisterType<ISayHelloService, SayHelloSerivce>();
container.Resolve<Window1>().Show();
container.Resolve<Window1>().Show();

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