Sunday, April 27, 2014

Введение в Prism 5

Здравствуйте, уважаемые читатели моего блога. В этой статье мы рассмотрим использование классического паттерна MVVM при помощи мощного инструмента для создания Enterprise решений Prism. Я выбрал тему по нескольким причинам. Первая причина заключается в том, что я захотел вспомнить принцип работы с этим набором библиотек. Вторая причина заключается в том, что недавно вышел   Prism 5.0 for .NET 4.5.  В призме добавилось множество новых изменений, с которыми вы можете ознакомиться по ссылке What's New in Prism Library 5.0 for WPF. Мы же для примера пока поработаем только с некоторой частью из новшеств нового Prism. Для того чтобы продемонстрировать, как работает Prism, я создам WPF приложение PrismLibraryApplication, которое будет имитировать работу электронной библиотеки.
Для приложения я использовал последний фреймворк 4.5.1, так как наконец перешел на Visual Studio 2013, и это позволило мне поработать немного над приложениями  Windows Store Apps, изучить новый Type Script и добраться к Призму. Следующим этапом нужно добавить в наше приложение ссылку на последний Prism. Для этого воспользуемся Package Manager Console.
После этого необходимо указать команду установки нового Prism.
PM> Install-Package Prism
После этого у нас в проект добавится много библиотек, часть из которых, к сожалению, для нашего простого примера не пригодится. Ниже на рисунке можно посмотреть, сколько разных библиотек добавилось в проект.
Сразу для проекта зададим слои для работы с паттерном MVVM.
Для того чтобы у вас не возникало проблем с этими слоями, я сейчас вкратце их распишу. В папке Assets мы будем сохранять стили, ресурсы и т.д. Крупные проекты, в которых множество стилей, шаблонов, ресурсов и других графических элементов, которые играют важную роль в отображении визуальной части, рекомендуется вынести в отдельную библиотеку. Папка Helpers хранит в себе информацию о вспомогательных классах. Для крупных проектов можно вынести в отдельную библиотеку с приставкой Core и хранить там всю базовую логику. В папке Models мы будем хранить наши модели. В папке ViewModels соответственно наши модели представления, которые будут связующим звеном между нашими моделями и представлением. Views – это наше графическое представление. Теперь дело за малым: добавить необходимую реализацию. Добавим в наш проект модель Book, которая будет хранить информацию о книге из библиотеки (папка Models).
public class Book : BindableBase
{
    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");
        }
    }
}
Я не использовал для примера стандартный механизм INotifyPropertyChanged , а воспользовался готовым, предоставленным библиотекой Prism. Класс BindableBase с пространства имен Prism.Mvvm используется вместо NotificationObject, потому что данный класс помечен атрибутом Obsolete (что означает, что он устарел и будет исключен в ближайшее время из использования). Следующим делом создадим ViewModel, которая будет связывать созданную модель с представлением.
public class LibraryViewModel : BindableBase
{
    #region Constructor
    public LibraryViewModel()
    {
        AddBookCommand = new DelegateCommand(AddNewBook);
        RemoveBookCommand = new DelegateCommand(RemoveBook, CanRemoveBook);
        Books = new ObservableCollection<Book>();
        Books.Add(new Book() { Author = "Jon Skeet", Title = "C# in Depth", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
        Books.Add(new Book() { 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 Book() { Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
    }
    #endregion

    #region Public Properties
    public ObservableCollection<Book> Books { get; set; }

    private Book _selectedBook;
    public Book SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            _selectedBook = value;
            OnPropertyChanged("SelectedBook");
            RemoveBookCommand.RaiseCanExecuteChanged();
        }
    }
    #endregion

    #region Command
    public DelegateCommand AddBookCommand { get; private set; }

    public DelegateCommand RemoveBookCommand { get; private set; }
    #endregion

    private void AddNewBook()
    {
        Books.Add(new Book() { Author = "Test1", Title = "Test1", Count = 5, SN = "ISBN-10: 0735667454", Year = DateTime.Now });
    }

    private void RemoveBook()
    {
        Books.Remove(SelectedBook);
    }

    private bool CanRemoveBook()
    {
        if (SelectedBook == null)
            return false;
        var book = FindBook(SelectedBook);
        if (book == null)
            return false;

        return true;
    }

    private Book FindBook(Book 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);
    }
}
Этот код несложный, но для начинающих немного объясню его. С использованием класса BindableBase мы уже ознакомились, теперь перейдем к классу DelegateCommand. Этот класс предназначен для работы с интерфейсом ICommand. Возможно, вам уже приходилось сталкиваться с самописной реализацией этого паттерна. Если же вам не приходилось использования данный паттерн в каком-либо WPF Toolkit, вы можете посмотреть примеры его использовани: "Основы паттерна MVVM" и "Работа с командами в MVVM". Этих двух статей должно хватить для ознакомления. Для коллекции я использую ObservableCollection, для того чтобы уведомлять UI об изменении коллекции. Если вам по душе CollectionView, можете воспользоваться этой коллекцией. Это дело вкуса и предпочтения, так как для данного контекста эта коллекция также подойдет. Далее мы создадим главную модель представления, вокруг которой и будет построенная вся логика.
public class MainWindowViewModel
{
    public MainWindowViewModel()
    {
        LibraryViewModel = new LibraryViewModel();
    }

    public LibraryViewModel LibraryViewModel { get; set; }
}
У нас она очень простенькая. Поэтому не будем на ней долго останавливаться и перейдем к представлению. Для этого наше главное окно переместим в папку Views. Структура проекта будет примерно такой:
Но это еще не все. После этого нам необходимо будет изменить namespace в исходном файле MainWindow.xaml.
<Window x:Class="PrismLibraryApplication.Views.MainWindow"
После этого следует перейти в MainWindow.xaml.cs и тоже изменить пространство имен, как показано ни рисунке ниже, и добавить наследование от интерфейса IView.
namespace PrismLibraryApplication.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, IView
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}
Namespace мы изменили, чтобы весь проект был сделан в одной структуре. То есть пробуем хотя бы по минимуму делать сразу хороший дизайн. Теперь остановимся на интерфейсе IView. Этот интерфейс взят с пространства имен Prism.Mvvm и свойства которые он имеет посмотреть ниже.
namespace Microsoft.Practices.Prism.Mvvm
{
    public interface IView
    {
        object DataContext { get; set; }
    }
}
Как видим, такое свойство у нас одно: DataContext. Этот интерфейс необходим для автоматического связывания ViewModel и View. Далее мы рассмотрим это связывание более детально. После того как мы изменили наше главное окно, необходимо перейти в App.xaml и изменить StartupUri, иначе вы будете удивлены тем, что ваша форма не показывается на экране.
StartupUri="Views\MainWindow.xaml"
Все остальное остается без изменений. Теперь перейдем к самой сложной части нашей работы  это реализация MainWindow.xaml. Переходим в данный класс и добавляем следующий код:
<Window x:Class="PrismLibraryApplication.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:basicMvvmQuickstartDesktop="clr-namespace:PrismLibraryApplication"
        xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Mvvm;assembly=Microsoft.Practices.Prism.Mvvm.Desktop"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:views="clr-namespace:PrismLibraryApplication.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        prism:ViewModelLocator.AutoWireViewModel="True">
    <Grid DataContext="{Binding LibraryViewModel}">
        <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, UpdateSourceTrigger=PropertyChanged}" />
                <Label Grid.Row="1" Grid.Column="0" Content="Title" />
                <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
                <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, UpdateSourceTrigger=PropertyChanged}" />
            </Grid>

            <StackPanel Grid.Row="1" Orientation="Horizontal">
                <Button x:Name="NewBookButton" Content="Add new book" Margin="3" Command="{Binding AddBookCommand}" />
                <Button x:Name="RemoveBookButton" Content="Remove book" Margin="3" Command="{Binding RemoveBookCommand}"/>
            </StackPanel>
        </Grid>
    </Grid>
</Window>
Внешний вид окна представлен на рисунке ниже.
Интерфейс не очень удачный, потому что я не специализируюсь на UI дизайне, и потому что я поленился для этой цели открыть Expression Bland. Первым делом мы указываем проект, который запускаем.
xmlns:basicMvvmQuickstartDesktop="clr-namespace:PrismLibraryApplication"
Затем добавляем для использования ссылку на Prism.Mvvm.Desktop.
xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Mvvm;assembly=Microsoft.Practices.Prism.Mvvm.Desktop"
Затем проделаем аналогичное действие для указания представлений.
xmlns:views="clr-namespace:PrismLibraryApplication.Views"
И последним важным этапом является указание призму автоматически искать ViewModel для связывания.
prism:ViewModelLocator.AutoWireViewModel="True"
Примечание. Параметр выше, который используется для связывания ViewModelLocator.AutoWireViewModel, будет связывать вашу модель представления с самим представлением, только если они будут удовлетворять такие требования. Первое: ваше представление наследуется от интерфейса IView. Второе: если имя класса главного окна называется MainWindow, то модель представления должна называться MainWindowViewModel, иначе у Призма не получится связать ваше представление с моделью представления. Я с этой проблемой повозился полчаса, пока не понял, в чем ошибка. Не наступайте на те же грабли, что и я.
Поскольку у нас очень простенький пример, то нас автоматическое связывание спасло. Но обычно такой подход не используется для бизнес-приложений. В бизнес-приложениях нужно связывать данные через IoC контейнер (Призм поддерживает Unity) или MEF. Все это можно сделать через класс UnityBootstrapper для IoC контейнера Unity или через MefBootstrapper для MEF. Можно также написать кастомную реализацию, наследуясь от класса Bootstrapper, если вы хотите использовать какой-то другой IoC контейнер, например, контейнер Autofac. Но как показывает практика, сделать это не так-то просто и нужно обладать достаточно сильными знаниями, чтобы попытаться удержать кучу данных в голове, затем правильно их связав. Мне приходилось с таким сталкиваться в реальном проекте, где мы использовали MefBootstrapper и который правил только один человек, потому что в той завязке, которая может выйти, среднестатистический разработчик может сломать себе мозг. 
На этой позитивной ноте завершу свою первую статью по новому Призму. Надеюсь, материал получился несложным и для вас не будет особых проблем попробовать строить свою бизнес-модель с использованием богатых возможностей Prism 5.  


No comments:

Post a Comment