Паттерн MVVM (Model-View-ViewModel) предназначен для создания приложений для WPF/Silverlight. Вначале окунемся немного в историю создания паттерна
MVVM. MVVM паттерн был разработан Джоном Госсманом (John Gossman) в 2005
году как модификация шаблона Presentation Model (его блог) На данном этапе разработки
программного обеспечения с визуальным интерфейсом для проектирования чаще всего
используют такие паттерны:
- MVP - используют для Windows Forms;
- MVC - для ASP MVC;
- MVVM - для приложений написанных на WPF/Silverlight.
Цель данной статьи - ознакомить читателя с принципами паттерна MVVM, поэтому
паттерны MVP и MVC мы пропустим.
Основная особенность данного паттерна заключается в том, что весь код с View (представление)
выносится в ViewModel (модель
представления), а вся привязка осуществляется черед байндинг, прописанный в XAML разметке.
Для простоты работы с MVVM был
разработан MVVM Toolkit
, который включает шаблон для Visual Studio и позволяет использовать данный паттерн без особых
усилий. Ми же рассмотрим MVVM в
классическом его представлении. Для начала создадим в Visual Studio WPF приложение.
Создадим сразу папки для разделения логики по слоям. (Model - для модели данных, ViewModel – для модели
представления, которая будет, по сути, связывать Model + View, View - представление).
Перетащим сразу MainWindows.xaml в View и чтобы
проект запустился, изменим в App.xaml StartupUri
<Application x:Class="WpfApplicationMVVM.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="View\MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
- автор книги;
- название;
- год издания;
- серийный номер
- количество;
Для того чтобы уведомлять View об
изменениях внутри класса Book, данный
класс необходимо наследовать от INotifyPropertyChanged.
public class Book : 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 void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Затем создадим в папке ViewModel класс LibraryViewModel, который свяжет представление с моделью данных.
public class LibraryViewModel : INotifyPropertyChanged
{
#region Private Variables
private ObservableCollection<Book> _books;
#endregion
#region Constructor
public LibraryViewModel()
{
_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 { return _books; }
set
{
_books = value;
OnPropertyChanged("Books");
}
}
private Book _selectedBook;
public Book SelectedBook
{
get { return _selectedBook; }
set
{
_selectedBook = value;
OnPropertyChanged("SelectedBook");
}
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Command
#endregion
#region Private Methods
private void OnPropertyChanged(string propertyChanged)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyChanged));
}
#endregion
}
Осталось определить визуальное представление созданной информации и сделать
привязку MainWindow.xaml с LibraryViewModel. Для этого
в XAML разметку MainWindow.xaml добавим
следующий код:
<Window x:Class="WpfApplicationMVVM.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Library" Height="350" Width="525">
<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" />
<Button Content="Remove book" Margin="3" />
</StackPanel>
</Grid>
</Grid>
</Window>
Затем делаем привязку созданной модели представления с DataContext MainWindow. Для этого
изменяем класс MainWindow.cs на приведенный ниже.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region Private Variables
private readonly LibraryViewModel _libraryViewModel;
#endregion
#region Constructor
public MainWindow()
{
InitializeComponent();
_libraryViewModel = new LibraryViewModel();
DataContext = _libraryViewModel;
}
#endregion
}
После этого можно запустить форму и просмотреть полученный результат.
После того как нажмем на какой-то элемент в списке, получим более подробную
информацию.
По такому принципу работает MVVM модель. Но такая модель не полна без событий. Для того
чтобы передать управляющие действия от визуальных элементов в модель, используются команды, которые реализуют
интерфейс ICommand.
Одна из простых реализаций DelegateCommand. Добавим в проект папку Utils и просто
скопируем реализацию по приведенной выше ссылке.
public class DelegateCommand : ICommand
{
private readonly Predicate<object> _canExecute;
private readonly Action<object> _execute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<object> execute)
: this(execute, null)
{
}
public DelegateCommand(Action<object> execute,
Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
{
return true;
}
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, EventArgs.Empty);
}
}
}
Теперь добавим реализацию команд для кнопок добавления и удаления новой книги.
public class LibraryViewModel : INotifyPropertyChanged
{
#region Private Variables
private ObservableCollection<Book> _books;
#endregion
#region Constructor
public LibraryViewModel()
{
_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 { return _books; }
set
{
_books = value;
OnPropertyChanged("Books");
}
}
private Book _selectedBook;
public Book SelectedBook
{
get { return _selectedBook; }
set
{
_selectedBook = value;
OnPropertyChanged("SelectedBook");
RemoveBookCommand.RaiseCanExecuteChanged();
}
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Command
private DelegateCommand _addBookCommand;
public DelegateCommand AddBookCommand
{
get
{
return _addBookCommand ??
(_addBookCommand = new DelegateCommand(AddNewBook));
}
}
private void AddNewBook(object arg)
{
_books.Add(new Book() { Author = "Test1", Title = "Test1", Count = 5, SN = "ISBN-10:
0735667454", Year = DateTime.Now
});
}
private DelegateCommand _removeBookCommand;
public DelegateCommand RemoveBookCommand
{
get
{
return _removeBookCommand ??
(_removeBookCommand = new DelegateCommand(RemoveBook, CanRemoveBook));
}
}
private void RemoveBook(object args)
{
_books.Remove(SelectedBook);
}
private bool CanRemoveBook(object args)
{
if(SelectedBook == null)
return false;
var book = FindBook(SelectedBook);
if (book == null)
return false;
return true;
}
#endregion
#region Private Methods
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);
}
private void OnPropertyChanged(string propertyChanged)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyChanged));
}
#endregion
}
В представлении изменилось только две строчки.
<Button Content="Add new book" Margin="3" Command="{Binding AddBookCommand}" />
<Button Content="Remove
book" Margin="3" Command="{Binding
RemoveBookCommand}"/>
После всего проделанного можно запустить приложение и убедиться в том, что
все работает.
Примечание 1: Не все можно решить с помощью команд. В некоторых случаях
может потребоваться прямая привязка, к какому то событию во View. Это
нарушает принцип MVVM. Поэтому
для таких случаев в WPF есть
поддержка Behaviors. Иногда
это решает проблему.
Примечание 2: Пример можно было решить, не написав в классе MainWindow.cs ни одной
дополнительной строчки кода. Для этого нужно в ресурсы добавить DataTemplate, в котором указать привязку ViewModel к конкретной View. В приведенном
случае это MainWindow.xaml (View) к LibraryViewModel.cs (ViewModel). Второй вариант
байндинга можно сделать через IoC контейнер, но для этого нужно будет переписать класс MainWindow.cs, чтобы он
принимал нужную модель как параметр и устанавливал DataContext для этой модели. Если статья кого-то заинтересует, постараюсь написать об этих тонкостях более подробно.
Добрый день! Спасибо Вам за статью! Подскажите пожалуйста, а как быть при использовании нескольких связанных таблиц? Как я понимаю, их каким-то образом надо связывать в ViewModel?
ReplyDeleteЗдравствуйте. Если у вас есть таблицы вам нужно добавить уровень DAL (data access layer) в котором имплементировать всю логику с помощью ORM (EntityFramewrok) или MicroORM (Dapper). Поскольку ваши таблицы (database entity) != model вам нужно перегнать необходимые для роботы поля в модель. Можно это сделать с помощью automapper или emitmapper. Логику по выборке и перегонки с db entity в model спрячьте за слоем сервисов (business layer). И по сути всю это логику нужно будет заинджектить через конструктор. Если вы знакомы с тем как строить 3 layer architecture, тогда это то что вам нужно. Если же нет и вы хотите это попробовать в тестовых целях - посмотрите в сторону EF Core для примера, и чтобы не лепить разных слоев для работы с базой и бизнес логикой, просто прокидывайте DbContext у ваши ViewModel, тогда Model будут данные с ваших таблиц. Сгенерировать таблицы можно кстати тоже автоматом через Visual Studio ислользуя scaffolding.
Delete