Здравствуйте, уважаемые читатели. Наконец-то у меня получилось собраться с мыслями и
продолжить заниматься любимым делом, а именно – написанием статей и самообучением. В последнее время я заметил, что все больше и
больше разработчики интересуются использованием такого паттерна, как MVVM, для написания
программ для WPF и
Silverlight. В
этой статье постараюсь раскрыть свое виденье использования паттерна Command, который является основной отправной точкой
соприкосновения View и ViewModel, так как позволяет
обрабатывать пользовательские сообщения (нажатие клавиш, обработка текста,
работа с жестами и т.д.) – то есть обеспечивает полное взаимодействие с пользователем. У
многих начинающих разработчиков возникает резонный вопрос: почему использование именно ICommand, а не старых добрых .Net событий (events). Причину этого мы сейчас рассмотрим.
Для того чтобы нормально работать с паттерном MVVM, нужно сразу разбить приложение на слои. Обычно это основные слои Models, в котором будет храниться информация о модели и данных, Views – для хранения представления (контролы, диалоговые окна и т.д.), и напоследок ViewModels – для хранения информации о модели представления (промежуточный слой для связывания модели (Model) и представления (View).
Эта статья не для начинающих. Здесь не будет сведений о том, что такое MVVM и как его использовать, для этого есть другие статьи, как, например, "Основы паттерна MVVM" и "MVVM Part 2" – использование паттерна MVVM на примере набора компонентов MVVM Light Toolkit, разработанных компанией GalaSoft. Примерный вид структуры созданного вами приложения можно посмотреть на рисунке ниже.
Для того чтобы нормально работать с паттерном MVVM, нужно сразу разбить приложение на слои. Обычно это основные слои Models, в котором будет храниться информация о модели и данных, Views – для хранения представления (контролы, диалоговые окна и т.д.), и напоследок ViewModels – для хранения информации о модели представления (промежуточный слой для связывания модели (Model) и представления (View).
Эта статья не для начинающих. Здесь не будет сведений о том, что такое MVVM и как его использовать, для этого есть другие статьи, как, например, "Основы паттерна MVVM" и "MVVM Part 2" – использование паттерна MVVM на примере набора компонентов MVVM Light Toolkit, разработанных компанией GalaSoft. Примерный вид структуры созданного вами приложения можно посмотреть на рисунке ниже.
Этот
шаблон создан на основе набора компонентов MVVM
Light Toolkit. Рекомендую
представления также спрятать в папку, например, View в вашем проекте.
Еще для вспомогательных классов можно выделить уровень Helpers или Common, в который можно вынести
общую логику по проекту. Для небольших проектов такая архитектура будет достаточной.
Для более сложных проектов на основе WPF/Silverlight можно самостоятельно продумать базовый ViewModelBase. ViewModelBase – это базовый класс, который реализует базовою логику для
всех ViewModel. Весь процесс построения программного обеспечения на паттерне MVVM основан на
наследовании. Композиция в данном случае реализована слабо.
Вернемся непосредственно к тому, почему именно паттерн Команда (Command) является, по сути,основополагающим во VM (view model). Попробуем не перепрыгивать с места на место, а постепенно дойдем к использованию интерфейса ICommand в приложениях, которые используют за основу паттерн MVVM. Создадим простое приложение OnlineLibrary без использования MVVM на WPF, а затем перепишем код под паттерн MVVM. Добавим кнопку в дизайнере, на которую повесим обработчик события.
Вернемся непосредственно к тому, почему именно паттерн Команда (Command) является, по сути,основополагающим во VM (view model). Попробуем не перепрыгивать с места на место, а постепенно дойдем к использованию интерфейса ICommand в приложениях, которые используют за основу паттерн MVVM. Создадим простое приложение OnlineLibrary без использования MVVM на WPF, а затем перепишем код под паттерн MVVM. Добавим кнопку в дизайнере, на которую повесим обработчик события.
<Window x:Class="OnlineLibrary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Content="Click
Me" HorizontalAlignment="Left" Margin="131,135,0,0" VerticalAlignment="Top"
Width="141"
Click="ButtonBase_OnClick"/>
</Grid>
</Window>
На
обработчик события не будем ничего мудрить и просто выведем сообщение "Hello World". Как говорится, классика
жанра.
/// <summary>
///
Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
}
private void
ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello
World");
}
}
Что мы в итоге имеем – код, реализованный в нашем представлении View,
которым в нашем случае выступает MainWindow.xaml. Теперь немного усложним задачу. В окно добавим текстовое поле, а также логику, в которой кнопка "Click Me" будет
доступна только в случае, если в текстовом поле есть какой-то текст.
Xaml-код представлен
ниже.
<Window x:Class="OnlineLibrary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button" Content="Click
Me" HorizontalAlignment="Left" Margin="131,135,0,0" VerticalAlignment="Top"
Width="141"
Click="ButtonBase_OnClick"/>
<TextBox x:Name="textBox" HorizontalAlignment="Left"
Height="23" Margin="131,100,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="141"
TextChanged="TextBoxBase_OnTextChanged"/>
</Grid>
</Window>
Если
внимательно посмотреть на код, то я добавил имя для кнопки, чтобы не искать
эту кнопку по дереву, а просто делать ей доступность по конкретным условиям.
/// <summary>
///
Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
button.IsEnabled
= false;
}
private void
ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello
World");
}
private void
TextBoxBase_OnTextChanged(object sender, TextChangedEventArgs e)
{
button.IsEnabled
= !string.IsNullOrEmpty(textBox.Text);
}
}
Код несложный,
но мы все также портим представление кодом. Чем больше пишем код в
представлении, используя подход, который часто используют разработчики Windows Forms,
тем хуже это отражается на нашем дизайне. Мы не можем отдать наш код дизайнеру,
который может работать параллельно с вами, а пишем весь код сами. При этом все
больше и больше приводим себя к тому, что код становится сложнее сопровождать и
разрабатывать. Это путь в никуда. Чем больше у вас контролов в UI и чем больше событий у вас реализовано в
коде, тем хуже. Вы пришли к тому, что просто взяли методологию разработки Windows Forms,
которую клепают все и вся, и перенесли в WPF.
Так как испортить интерфейс на WPF намного проще,
чем на том же Windows Forms,
можно вас поздравить с тем, что чем сложнее у вас будет становиться интерфейс,
тем больше вы себя будете загонять в тупик, пока сами не запутаетесь в своих
событиях, а также в том, как они связаны.
Следующий способ
обработки нажатия на клавишу можно смоделировать с помощью маршрутизируемых
команд RoutedCommand или RoutedUICommand.
Для нашего случая подходит любой из двух вариантов, нопредпочтительно для
работы с UI элементами использовать RoutedUICommand, во-первых, так как он имеет больше
свойств, а во-вторых, RoutedUICommand наследуется от RoutedCommand. Для этого я создал свою RoutedUICommand в отдельном
классе. Реализация этого класса приведена ниже.
public static class Commands
{
public static readonly RoutedUICommand MyCommand = new RoutedUICommand();
}
После этого необходимо изменить реализацию MainWindow.xaml на приведенную ниже.
<Window x:Class="OnlineLibrary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:OnlineLibrary"
Title="MainWindow" Height="350" Width="525">
<Window.CommandBindings>
<CommandBinding Command="{x:Static local:Commands.MyCommand}" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"
/>
</Window.CommandBindings>
<Grid>
<Button x:Name="button" Content="Click
Me" HorizontalAlignment="Left" Margin="131,135,0,0" VerticalAlignment="Top"
Width="141"
Command="{x:Static local:Commands.MyCommand}"/>
<TextBox x:Name="textBox" HorizontalAlignment="Left"
Height="23" Margin="131,100,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="141"
/>
</Grid>
</Window>
Посмотрите внимательно на код. Мы избавились от метода TextChanged, который
использовали в предыдущем примере, для изменения видимости кнопки. Вместо него
добавилось использование класса CommandBinding. Этот класс привязывает RoutedCommand к обработчикам событий, реализующим данную
команду. Более детально с работой CommandBinding вы можете ознакомиться по ссылке. Основная суть – в том, что метод CanExecute позволяет указать, будет доступен метод Executed или нет. Реализация этого всего приведена
ниже.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
}
private void
CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute
= textBox != null && textBox.IsLoaded && !string.IsNullOrEmpty(textBox.Text);
e.Handled
= true;
}
private void
CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Hello
World");
}
}
Давайте сейчас вынесем это код во ViewModel и постараемся избавиться от кода в нашем
представлении. Для этого создадим уровень модели представления ViewModel и добавим новую модель MainViewModel, в
которую перенесем нужную нам реализацию.
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string
_mainText;
public string
MainText
{
get { return
_mainText; }
set
{
_mainText
= value;
OnPropertyChanged("MainText");
}
}
[NotifyPropertyChangedInvocator]
protected virtual void
OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if
(handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#region Command
public void
CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute
= !string.IsNullOrEmpty(_mainText);
e.Handled
= true;
}
public void
CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Hello
World");
}
#endregion
}
Структура
проекта выглядит следующим образом:
В
приведенном выше коде есть одна большая ошибка: настроить байндинг на события CanExecute и Execute с представления очень сложно. Учитывая следующий факт:
<Window.CommandBindings>
<CommandBinding Command="{x:Static local:Commands.MyCommand}" CanExecute="{Binding CommandBinding_CanExecute}"
Executed="{Binding CommandBinding_Executed}" />
</Window.CommandBindings>
В
вызове конструктора MainWindow сначала происходит
инициализация компонентов InitializeComponent(), а только после этого – создание модели.
/// <summary>
///
Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
var
viewModel = new MainViewModel();
DataContext
= viewModel;
}
}
Поэтому код, который инициализирует CommandBinding, выполнится раньше, чем будет создана модель, и
привязан к модели. Для того чтобы такой подход заработал, необходимо реализовать
логику немного иначе.
<Window x:Class="OnlineLibrary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:OnlineLibrary"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding}">
<Grid>
<Button x:Name="button" Content="Click
Me" HorizontalAlignment="Left" Margin="131,135,0,0" VerticalAlignment="Top"
Width="141"
Command="{x:Static local:Commands.MyCommand}"/>
<TextBox x:Name="textBox" HorizontalAlignment="Left"
Height="23" Margin="131,100,0,0"
TextWrapping="Wrap" VerticalAlignment="Top"
Width="141"
Text="{Binding MainText, UpdateSourceTrigger=PropertyChanged}"
/>
</Grid>
</Window>
Осталось немного изменить логику в классе MainWindow.cs.
/// <summary>
///
Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
var
viewModel = new MainViewModel();
var
myCommandBinding = new CommandBinding(
Commands.MyCommand,
viewModel.CommandBinding_Executed,
viewModel.CommandBinding_CanExecute);
// Add the CommandBinding to the root Window.
DataContext
= viewModel;
CommandBindings.Add(myCommandBinding);
}
}
Получаем все равно много кода. Но плюс – в том, что логика уже почти отвязана
от представления, что не может не радовать. То, как реализованы RoutedCommand и RoutedUICommand (через события), привело к тому, что их редко используют. Вместо них обычно
пишется своя реализация, которая базируется на использовании интерфейса ICommand. Если
вы посмотрите MVVM Light Toolkit, то там уже есть готовая
реализация класса RelayCommand, в котором реализована вся необходимая логика. Вы можете либо скопировать себе этот класс, либо использовать простенький класс, который я использую для
таких целей.
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 MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string
_mainText;
public string
MainText
{
get { return
_mainText; }
set
{
_mainText
= value;
OnPropertyChanged("MainText");
MyCommand.RaiseCanExecuteChanged();
}
}
[NotifyPropertyChangedInvocator]
protected virtual void
OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if
(handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#region Command
private DelegateCommand _myCommand;
public DelegateCommand MyCommand
{
get { return
_myCommand ?? (_myCommand = new DelegateCommand(MyExecute, CanMyExecute)); }
}
private bool
CanMyExecute(object obj)
{
return !string.IsNullOrEmpty(_mainText);
}
private void
MyExecute(object obj)
{
MessageBox.Show("Hello
World");
}
#endregion
}
Теперь у нас доступна команда DelegateCommand MyCommand, которая реализует интерфейс ICommand.
Реализация класса MainWindow сократилась до трех строк.
/// <summary>
///
Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
var
viewModel = new MainViewModel();
DataContext
= viewModel;
}
}
И немножко изменилась реализация в xaml-коде.
<Window x:Class="OnlineLibrary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding}">
<Grid>
<Button x:Name="button" Content="Click
Me" HorizontalAlignment="Left" Margin="131,135,0,0" VerticalAlignment="Top"
Width="141"
Command="{Binding MyCommand}"/>
<TextBox x:Name="textBox" HorizontalAlignment="Left"
Height="23" Margin="131,100,0,0"
TextWrapping="Wrap" VerticalAlignment="Top"
Width="141"
Text="{Binding MainText, UpdateSourceTrigger=PropertyChanged}"
/>
</Grid>
</Window>
Теперь
байндинг на команду осуществляется так:
Command="{Binding MyCommand}"
Осталась
самая малость – убрать код из конструктора класса MainWindow,
оставить в нем только строку инициализации компонентов. Для этого необходимо перейти в файл App.xaml и добавить создание нашей MainViewModel.
<Application x:Class="OnlineLibrary.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:OnlineLibrary.ViewModel"
StartupUri="MainWindow.xaml">
<Application.Resources>
<local:MainViewModel x:Key="MyMaiViewMOdel"
/>
</Application.Resources>
</Application>
В классе MainWindow.xaml изменится только байндинг главного окна.
<Window x:Class="OnlineLibrary.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" DataContext="{StaticResource MyMaiViewMOdel}">
Остальной
код останется без изменений. Поменялась только строчка
DataContext="{StaticResource MyMaiViewMOdel}"
После
этого мы можем убрать создание нашей ViewModel с конструктора
класса MainWindow.
/// <summary>
///
Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public
MainWindow()
{
InitializeComponent();
}
}
Мы вернули наш класс MainWindow.cs к первоначальному виду. После проделанной работы мы можем разделить
написание логики и дизайна, поскольку никакого кода во View непосредственно не осталось. Весь код
перенесся в xaml и во ViewModel. Внимательные читатели, конечно же, обратили
внимание, что паттерн MVVM состоит из трех частей. Мы нигде не упоминали часть Model, которая выступает за данные, которые будут
связаны с View с помощью нашей модели представления MainViewModel.
Но поскольку у нас к данным относится только свойство MainText, я
решил по пустякам ради одного свойства не создавать отдельный класс и
интерфейс. Поэтому реализация MVVM без явного выделения модели (Model) имеет место быть. Надеюсь, этот краткий
экскурс в использование команд даст вам представление о том, зачем был придуман
паттерн MVVM и какая роль в нем отводится командам.
No comments:
Post a Comment