Эту статью
я посвящу использованию паттерна 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.
Начнем с хороших новостей.
3. EventToCommand behavior уже описан в Основах паттерна MVVM. Его суть – в том, чтобы привязывать команды к событиям UI контролов.
4. Messenger позволяет организовывать обмен сообщениями внутри приложения.
5. DispatcherHelper – это, по сути, wrapper для класса Dispatcher для упрощения работы с потоками.
Начнем с хороших новостей.
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 : IBook, INotifyPropertyChanged
{
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(this, new 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