Thursday, March 13, 2014

Работа с командами в MVVM

Здравствуйте, уважаемые читатели. Наконец-то у меня получилось собраться с мыслями и продолжить заниматься любимым делом, а именно – написанием статей и самообучением. В последнее время я заметил, что все больше и больше разработчики интересуются использованием такого паттерна, как 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 Light Toolkit. Рекомендую представления также спрятать в папку, например, View в вашем проекте. Еще для вспомогательных классов можно выделить уровень Helpers или Common, в который можно вынести общую логику по проекту. Для небольших проектов такая архитектура будет достаточной. Для более сложных проектов на основе WPF/Silverlight можно самостоятельно продумать базовый ViewModelBase. ViewModelBase – это базовый класс, который реализует базовою логику для всех ViewModel. Весь процесс построения программного обеспечения на паттерне 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