Wednesday, November 20, 2013

MVVM Part 2

Эту статью я посвящу использованию паттерна MVVM. Для того, чтобы ознакомиться с классическим паттерном MVVM, рекомендую посмотреть мою статью Основы паттерна MVVM. Паттерн MVVM (Model-View-ViewModel) предназначен для создания приложений WPF и Silverlight. Поскольку эта статья не является вступительной, я рассмотрю разработанный компанией GalaSoft набор компонентов  MVVM Light Toolkit для того, чтобы помочь программистам использовать все возможности паттерна MVVM. Выбор шаблона для MvvmLight выглдит следующим образом:
Чтобы не изобретать что-то новое, возьмем за основу мою первую статью по MVVM и реализуем такой же вариант электронной библиотеки с книгами. Как видим со скриншота выше, я выбрал MvvmLight (WPF4), так как в тестовом примере не буду использовать новые возможности .Net Framework 4.5/WPF 4.5. Чтобы показать, как это работает, этого будет достаточно. Вот как будет выглядеть только что созданный проект:
Перед тем, как мы удалим половину ненужного кода, который создал для нас дизайнер форм, я хотел бы описать плюсы и минусы при работе с MvvmLight

Начнем с хороших новостей. 
1. Чтобы не реализовывать интерфейс INotifyPropertyChanged для классов ViewModel, мы можем использовать класс ViewModelBase, в котором этот интерфейс уже реализован.
2. Для использования команд (интерфейса ICommand) используются команды RelayCommand и RelayCommand<T>.
3. EventToCommand behavior уже описан в Основах паттерна MVVM. Его суть – в том, чтобы привязывать команды к событиям UI контролов.
4. Messenger позволяет организовывать обмен сообщениями внутри приложения.
5. DispatcherHelper – это, по сути, wrapper для класса Dispatcher для упрощения работы с потоками.

Перейдем к неприятным моментам при работе с MvvmLight.
1. Для модели данных придётся реализовывать либо интерфейс INotifyPropertyChanged, либо базовый класс, который будет реализовывать этот интерфейс в себе, а затем наследоваться от этого класса. Использование для таких целей класса ViewModelBase выглядит не очень уместно.
2. Структура проекта построена таким образом, что нет полного разделения на слои (Model-View-ViewModel). Разделение в проекте дизайнер добавляет только разделение на Model- ViewModel и полностью пропускает View. Представления лежат отдельно от остальной части проекта.
3. Сомнительна целесообразность реализации универсального, по сути, класса ViewModelLocator. Этот класс включает в себя статические ссылки на ViewModel  и также служит как отправная точка для привязки байндинга. Я отдаю предпочтение использованию ресурсов с привязкой модели к представлению (View). Выглядит это так:
<DataTemplate DataType="vm:MainViewModel">
    <view:MainWindow />
</DataTemplate>
4. Зачистка ресурсов проводится таким способом:
Closing += (s, e) =>ViewModelLocator.Cleanup();
Проделать столько титанических усилий, чтобы не писать реализацию в View и затем добавить такой код. Если действительно нужна такая зачистка, ее лучше сделать через Blend Behavior.
Я субъективно описал плюсы и минусы MVVM Light Toolkit. Если подходить с умом к выбору тех или иных возможностей, то MvvmLight – неплохой скелет для построения своего приложения. Приступим к реализации интерфейса, который предназначен для того, что реализовать основные параметры книги (Book).
public interface IBook
{
    string Author { get; set; }
    string Title { get; set;  }
    DateTime Year { get; set; }
    string SN { get; set; }  
    int Count { get; set; }
}

Реализация класса, который реализует интерфейс IBook:
public class LibraryBook : IBookINotifyPropertyChanged
{
    private string _author;
    public string Author
    {
        get { return _author; }
        set
        {
            _author = value;
            OnPropertyChanged("Author");
        }
    }
    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            OnPropertyChanged("Title");
        }
    }
    private DateTime _year;
    public DateTime Year
    {
        get { return _year; }
        set
        {
            _year = value;
            OnPropertyChanged("Year");
        }
    }
    private string _sn;
    public string SN
    {
        get { return _sn; }
        set
        {
            _sn = value;
            OnPropertyChanged("SN");
        }
    }
    private int _count;
    public int Count
    {
        get { return _count; }
        set
        {
            _count = value;
            OnPropertyChanged("Count");
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(thisnew PropertyChangedEventArgs(propertyName));
    }
}
Для того, чтобы не разрешать прямой доступ к библиотеке, создадим интерфейс ILibraryBookService, благодаря которому мы спрячем нашу реализацию.
public interface ILibraryBookService
{
    void GetData(Action<ObservableCollection<IBook>, Exception> callback);
    IBook FindBook(IBook findBook);
    void CreateNewBook();
    void RemoveBook(IBook book);

}
Для реализации данного сервиса я создал два класса: DesignLibraryBookService и LibraryBookService. У меня эти два класса идентичные. Приведу реализацию одного из них, затем подробно объясню, почему их нужно создать именно два.
public class LibraryBookService : ILibraryBookService
{
    #region Variable
    private ObservableCollection<IBook> _books;
    #endregion

    #region Constructor
    public LibraryBookService()
    {
        _books = new ObservableCollection<IBook>();
        _books.Add(new LibraryBook { Author = "Jon Skeet", Title = "C# in Depth", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
        _books.Add(new LibraryBook { Author = "Martin Fowler", Title = "Refactoring: Improving the Design of Existing Code", Count = 2, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) });
        _books.Add(new LibraryBook { Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
    }
    #endregion

    #region Public Methods
    public void GetData(Action<ObservableCollection<IBook>, Exception> callback)
    {
        callback(_books, null);
    }

    public IBook FindBook(IBook findBook)
    {
        if (findBook == null)
            return null;

        return _books.FirstOrDefault(book => book.Author == findBook.Author
                                    && book.Title == findBook.Title
                                    && book.SN == findBook.SN
                                    && book.Year == findBook.Year);
    }

    public void CreateNewBook()
    {
        _books.Add(new LibraryBook { Author = "Test1", Title = "Test1", Count = 5, SN = "ISBN-10: 0735667454", Year = DateTime.Now });
    }

    public void RemoveBook(IBook book)
    {
        if(book == null)
            return;

        _books.Remove(book);
    }
    #endregion

}
Класс с приставкой Designer используется для возможности увидеть, нормально ли работает байндинг модель, сразу при построении приложения. Используется он для Blend и визуального редактора студии. В основном используется дизайнерами для того, чтобы добавить тестовые данные и видеть их на экране в процессе работы. Реальные данные для отображения могут загружаться с сервера, браться с базы данных и т.д. Поэтому и нужно создать аналогичный класс для дизайнера форм, чтобы увидеть привязку данных зразу, а не смотреть потом через тот же Snoop по xaml коду и искать ошибку. Это не очень увлекательный процесс поиска ошибок байндинга. Поэтому если Вы хотите посмотреть, как будут выглядеть Ваши данные на экране, создайте описанный выше класс.
Для того, чтобы это все работало, дизайнер MVVM Light создает класс ViewModelLocator,в котором с помощью IoC контейнера связываем интерфейсы с реализацией.
/// <summary>
/// This class contains static references to all the view models in the
/// application and provides an entry point for the bindings.
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class ViewModelLocator
{
    static ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        if (ViewModelBase.IsInDesignModeStatic)
        {
            SimpleIoc.Default.Register<ILibraryBookService, Design.DesignLibraryBookService>();
        }
        else
        {
            SimpleIoc.Default.Register<ILibraryBookService, LibraryBookService>();
        }

        SimpleIoc.Default.Register<MainViewModel>();
    }

    /// <summary>
    /// Gets the Main property.
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
        "CA1822:MarkMembersAsStatic",
        Justification = "This non-static member is needed for data binding purposes.")]
    public MainViewModel Main
    {
        get
        {
            return ServiceLocator.Current.GetInstance<MainViewModel>();
        }
    }

    /// <summary>
    /// Cleans up all the resources.
    /// </summary>
    public static void Cleanup()
    {
    }

}
Как видим, приведенный выше класс реализует простой SimpleIoc контейнер. Для примера я не стал ничего менять, но для промышленной эксплуатации нужно использовать, например, Unity или MEF. Если в двух словах, то функциональность MEF и функциональность типичной инфраструктуры IoC перекрываются, но не совпадают. Большинство инфраструктур IoC позволяет выполнять задачи, которые просто не поддерживаются MEF. Вероятно, Вы могли бы задействовать IoC-контейнер с богатой функциональностью и с некоторыми усилиями эмулировать некоторые специфичные для MEF возможности. На базовом уровне MEF — это инфраструктура IoC, встроенная прямо в .NET Framework. Она не столь мощная, как многие из нынешних популярных инфраструктур IoC, но позволяет весьма неплохо выполнять основные функции типичного IoC-контейнера. При разработке Enterprise Model лучше использовать какой-либо из тулкитов для MVVM паттера и объединить с PRISM. Prism содержит указания, которые помогут более легко спроектировать и построить богатый, гибкий и легкий в обслуживании интерфейс для Windows Presentation Foundation (WPF), настольных приложений, Silverlight многофункциональных интернет-приложений (RIA) и Windows Phone 7 приложений. Prism уже содержит класс Bootstraper, который позволяет задать выбранный IoC при проектировании User Interface. В последний раз, когда я работал с призмом, он поддерживал только UnityBootstrapper для того, чтобы резолвить интерфейсы с помощью IoC контейнера Unity, и MefBootstrapper для того же, только с помощью MEF. Вы можете более детально ознакомиться с этим по ссылке, приведенной в источниках литературы, так как ознакомление с этим инструментом отклонило нас от основной темы статьи.
Возвращаемся к классу ViewModelLocator, который был описан выше. Для того чтобы класс был доступен везде в приложении, MVVM Light Toolkit создает его в ресурсах App.xaml.
<Application x:Class="MvvmLightLibrary.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:MvvmLightLibrary.ViewModel"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             StartupUri="MainWindow.xaml"
             mc:Ignorable="d">
   
    <Application.Resources>
        <!--Global View Model Locator-->
        <vm:ViewModelLocator x:Key="Locator"
                             d:IsDataSource="True" />
    </Application.Resources>
   

</Application>
Теперь по ключу Locator мы можем обращаться к этому классу с любого места. Приведем код, который реализует всю необходимую логику по отображению в MainWindow.xaml:
<Window x:Class="MvvmLightLibrary.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:ignore="http://www.ignore.com"
        mc:Ignorable="d ignore"
        Height="300"
        Width="300"
        Title="MVVM Light Library Application"
        DataContext="{Binding Main, Source={StaticResource Locator}}">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Skins/MainSkin.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListView x:Name ="library" Grid.Column ="0" ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <WrapPanel>
                        <TextBlock Text="Author: " />
                        <TextBlock Text="{Binding Author}" FontWeight="Bold" />
                        <TextBlock Text=", " />
                        <TextBlock Text="Caption: " />
                        <TextBlock Text="{Binding Title}" FontWeight="Bold" />
                        <TextBlock Text="Count: " />
                        <TextBlock Text="{Binding Count}" FontWeight="Bold" />
                    </WrapPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid Grid.Row="0" DataContext="{Binding ElementName=library, Path=SelectedItem}">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Label Grid.Row="0" Grid.Column="0" Content="Author" />
                <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Author}" />
                <Label Grid.Row="1" Grid.Column="0" Content="Title" />
                <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Title}" />
                <Label Grid.Row="2" Grid.Column="0" Content="Year" />
                <DatePicker Grid.Row="2" Grid.Column="1" SelectedDate="{Binding Year}" />
                <Label Grid.Row="3" Grid.Column="0" Content="Serial Number" />
                <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding SN}" />
                <Label Grid.Row="4" Grid.Column="0" Content="Count" />
                <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Count}" />
            </Grid>
            <StackPanel Grid.Row="1" Orientation="Horizontal">
                <Button Content="Add new book" Margin="3"  Command="{Binding AddBookCommand}" />
                <Button Content="Remove book" Margin="3" Command="{Binding RemoveBookCommand}"/>
            </StackPanel>
        </Grid>
    </Grid>
</Window>
Байндинг сразу задается в xaml коде через строчку
DataContext="{Binding Main, Source={StaticResource Locator}}
Поэтому никакой дополнительной логики в классе MainWindow.csписать не нужно. Если Вы откроете этот класс, то увидите, что тулкит создал строчку кода:
Closing += (s, e) => ViewModelLocator.Cleanup();
Этот код нужен для того, чтобы почистить за собой ресурсы. Но поскольку в таком маленьком примере чистить ничего не нужно, Вы можете спокойно удалить эту строчку.

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

No comments:

Post a Comment