Pull to refresh

Commands in MVVM

Reading time 22 min
Views 82K
Original author: Dave Kerr
  • Пример 1 – Простое использование Command
  • Пример 2 — Простое использование Command в паре с лямда функциями
  • Пример 3 — Простое использование Command с параметрами
  • Пример 4 – Включение и отключение Command
  • Пример 5 – Command вызывающие события
  • Пример 6 – Асинхронные Command
  • Пример 7 — Асинхронные Command обновляющие интерфейс пользователя (UI)
  • Пример 8 — Асинхронные Command с возможность отмены
  • Пример 9 – Привязка событий к Command
  • Как это работает – Класс Command
  • Как это работает – Класс асинхронных Command
  • Как это работает – Класс привязки данных к Command

Вступление


На примере приложения, использующего паттерн MVVM (Model View View-Model) рассмотрим работу с командами (Commands).

Примеры используемые мною имеют одинаковую логику независимо от платформы: WPF, SilverLight, Windows Phone. В первую очередь рассмотрим несколько вариантов использования команд в любом MVVM приложении. Сначала примеры, потом разбор полетов.

Весь использованный код представляет собой часть моей MVVM библиотеки Apex, однако все части будут предоставлены в полном объеме, что позволит Вам легко интегрировать данные методы в свой проект или библиотеку, или можно просто подключить ссылку на Apex и сразу приступать к работе.

image
Скриншот 1: Команды в WPF

image
Скриншот 2: Команды в SilverLight

image
Скриншот3: Команды в Windows Phone

Что такое команды?

Команды являются привязанными объектами, что позволяет разделить логику и пользовательский интерфейс друг от друга.

Если рассматривать команды более подробно, то они представляют из себя следующее:
  • Команды представляют собой объекты, реализующие интерфейс ICommand
  • Обычно команды связанны с какой либо функцией
  • Элементы пользовательского интерфейса привязываются к командам — кода интерфейс активируется пользователем, то выполняется команда — вызывается соответствующая функция
  • Команды знают, включены ли они или нет
  • Функции могут отключать команды – автоматическое отключение всех пользовательских элементов ассоциированных с ней
  • На сомом деле существует множество различных применений команд. Например использование команд для создания асинхронных функций, обеспечивающих логику, которая может быть проверена с/без помощи использования пользовательского интерфейса и др.

Примеры

Для начала рассмотрим случаи, когда возникает необходимость использования команд. Каждый случай будет рассмотрен более подробно далее в соответствующем пункте.

Важно: каждый View Model в этой части примеров содержит наблюдаемые коллекции (observable collection) строк с именем «Messages» — каждый пример приложения показывает сообщение, отображающий информацию о том, что происходит.

Рассмотри базовую модель вида (View Model):

public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
    }

    private ObservableCollection<string> messages = new ObservableCollection<string>();

    public ObservableCollection<string> Messages
    {
      get { return messages; }
    }
}


Данная модель вида наследует от ViewModel', которая в свою очередь реализует интерфейс INotifyPropertyChanged, однако Вы можете использовать любой другой доступный тип реализации модели вида.

Пример 1 – Простое использование Command
Цель: Вызвать соответствующую функцию модели вида при нажатии пользователем на элемент.

Сейчас рассмотрим самый простой пример использования команд. В первую очередь добавим объект Command к нашей модели вида – ассоциированную функцию, которая вызывается при вызове команды:
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создание команды - вызов DoSimpleCommand.
      simpleCommand = new Command(DoSimpleCommand);
    }

    /// <summary>
    /// The SimpleCommand function.
    /// </summary>
    private void DoSimpleCommand()
    {
      //  Добавляем сообщение
      Messages.Add("Вызываем 'DoSimpleCommand'.");
    }

    /// <summary>
    /// Простой объект команды
    /// </summary>
    private Command simpleCommand;

    /// <summary>
    /// Получение простой команды
    /// </summary>
    public Command SimpleCommand
    {
      get { return simpleCommand; }
    }
}

Теперь добавим команду в свойство «Command'» элемента управления кнопка(button).
<Button Content="Simple Command" Command="{Binding SimpleCommand}" />

Вот и все. Самый простой пример сделан. Мы прикрепили объект команды к свойству Command элемента интерфейса. При активации элемента, то есть при нажатии на кнопку вызывается команда( вызывается соответствующая ей функция DoSimpleCommand).

Пример 2 - Простое использование Command в паре с лямда функциями
Цель: Вызвать функцию модели вида во время активации или нажатия на элемент интерфейса.

Однако это очень простая функция, которую я предпочту не прописывать в явном виде и взамен использую лямда выражение.

Во всех примерах данного мануала Вы можете как и явно создавать функцию, например, как в Пример 1, или использовать лямды, что несколько сократит код и сделает его более элегантным. Выбор за Вами.
public MainViewModel()
{
      //  Создание команды с помощью лямда выражения. Никаких дополнительных функций больше не надо.
      lambdaCommand = new Command(
        () =>
        {
          Messages.Add("Вызов команды с помощью лямда. Никаких дополнительных функций больше не надо.
");
        });
}

/// <summary>
/// The command object.
/// </summary>
private Command lambdaCommand;

/// <summary>
/// Gets the command.
/// </summary>
public Command LambdaCommand
{
  get { return lambdaCommand; }
}

Опять повторим процесс привязки команды к свойству Command нашей кнопки:
<Button Content="Lambda Command" Command="{Binding LambdaCommand}" />


Пример 3 - Простое использование Command с параметрами
Цель: Создание команды, которая использует параметр, передаваемый ей при привязке данных.

В этом примере используем объект Command (так же есть возможность использовать объект AsynchronousCommand, что мы увидим чуть позднее). Переданный параметр может быть использован в вызываемой функции.
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создаем параметризированную команду
      parameterizedCommand = new Command(DoParameterisedCommand);
    }

    /// <summary>
    /// Функция команды
    /// </summary>
    private void DoParameterisedCommand(object parameter)
    {
      Messages.Add("Вызов параметризированной команды – передаваемый параметр: '" + 
                   parameter.ToString() + "'.");
    }

    /// <summary>
    /// The command object.
    /// </summary>
    private Command parameterizedCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public Command ParameterisedCommand
    {
      get { return parameterizedCommand; }
    }
}

Повторим привязку команды к кнопке или любому другому элементу. Так же не забудем добавить параметр:
<Button Content="Parameterized Command" Command="{Binding ParameterizedCommand}" CommandParameter={Binding SomeObject} />

Везде, где используются команды возможно передавать в них параметры. При создании команды можно использовать Action (функция команды без параметров) или Action<object> (функция команды с параметром типа object ). Так же существует возможность использовать лямды:
//  Создание параметризованной команды используя лямда выражение
parameterizedCommand = new Command(
  (parameter) =>
  {
    Messages.Add("Вызов параметризованной команды – передаваемый параметр: '" + 
                 parameter.ToString() + "'.");
  });


Пример 4 – Включение и отключение Command
Цель: Добавить возможность включать/отключать команды из кода или XAML

Каждая команда содержит свойство CanExecute, которое при установке в него true включает команду, и при установке false — отключает команду, а так же связанный с ней элемент управления. В следующем примере при отключении команды исчезнет и кнопка с пользовательского интерфейса:
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создание включаемой/отключаемой команды
      enableDisableCommand = new Command(
          () =>
          {
            Messages.Add("Вкл/Откл команды.");
          }, false);
    }

    private void DisableCommand()
    {
      //  Отключение команды
      EnableDisableCommand.CanExecute = false;
    }

    private void EnableCommand()
    {
      //  Включение команды
      EnableDisableCommand.CanExecute = true;
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private Command enableDisableCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public Command EnableDisableCommand
    {
      get { return enableDisableCommand; }
    }
}

Делаем привязку к кнопке (или другому элементу):
<Button Content="Enable/Disable Command" Command="{Binding EnableDisableCommand}" />

Так же остается возможность привязать свойство команды CanExecute.
В данном примере, для управления состоянием команды в коде назначаем свойству CanExecute соответствующее значение. А для управления через интерфейс (XAML) используем дополнительный элемент CheckBox:
<CheckBox IsChecked="{Binding EnableDisableCommand.CanExecute, Mode=TwoWay}" Content="Enabled" />

Вне зависимости от того где и как мы создали объект команды, у нас всегда остается возможность передать как второй параметр булево значение, управляющее свойством CanExecute. По умолчанию – установлено в false. В примере передаем true для включения.

Пример 5 – Command вызывающие события
Цель: Узнать, когда выполнилась команда, или момент ее выполнения.

У каждой команду существует два события: Executed — вызывается, когда команда отработала, и Executing — в момент выполнения. Так же событие Executing позволяет отменить команду.

Важно: Бывает множество ситуаций, когда вышеописанные события становятся очень полезными. Например, Вы захотите показать всплывающее окно, которое будет спрашивать, хотите ли Вы продолжить в момент выполнения команды. И где разместить такой код? В коде команды? Плохая идея, так как придется создать код интерфейса в модели данных, что в первую очередь засоряет код, а во вторую, что намного важнее — не позволит его протестировать. Наилучшим будет разместить код в Виде (View). И с помощью событий Вы можете это сделать.
Вторым примером демонстрации полезности таких событий может быть ситуация, когда появляется необходимость переместить фокус на другой элемент после выполнения команды. Вы не сможете это сделать в модели вида, так как она не имеет доступ к контролам (элементам управления), однако подписавшись на события Вы сможете реализовать это без каких либо проблем.
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создание команды с событиями
      eventsCommand = new Command(
          () => 
                {
                    Messages.Add("Вызов команды с событиями.");
                });


Теперь привяжем к кнопке:
<Button Content="Events Command" Command="{Binding EventsCommand}" />

На данный момент нет никаких отличий от кода в предыдущих примерах. Подпишемся на события во View.
Важно: в моем представлении, DataContext с именем viewModel:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        viewModel.EventsCommand.Executing += 
          new Apex.MVVM.CancelCommandEventHandler(EventsCommand_Executing);
        viewModel.EventsCommand.Executed += 
          new Apex.MVVM.CommandEventHandler(EventsCommand_Executed);
    }

    void EventsCommand_Executed(object sender, Apex.MVVM.CommandEventArgs args)
    {
        viewModel.Messages.Add("Команда закончила свое выполнение. Говорит  View!");
    }

    void EventsCommand_Executing(object sender, Apex.MVVM.CancelCommandEventArgs args)
    {
        if (MessageBox.Show("Отменить команду?", 
                 "Cancel?", 
                 MessageBoxButton.YesNo) == MessageBoxResult.Yes)
            args.Cancel = true;
    }
}

Наконец то мы подобрались к пониманию всей мощи реализации команд. Есть возможность подписываться на события Executed и Executing в представлении (или даже в другом ViewModel, или объекте), а так же знаем когда оно возникает. Событие Executing передает объект CancelCommandEventArgs — это и есть свойство с именем «Cancel». Если его установить в true — то команда не выполнится. Оба объекта CommandEventArgs и CancelCommandEventArgs поддерживает еще одно свойство – параметр. Это тот параметр, который может быть передан в Command(если он вообще есть).

Пример 6 – Асинхронные Command
Цель: Если команды выполняются достаточно долго, то блокируется интерфейс. Должна быть возможность запускать их асинхронно.

Можно создать нечто вроде фонового процесса и там запустить команду. Однако сразу возникает несколько вопросов:
— Что будет, если мы захотим обновить ViewModel в функции, находящейся в потоке? Мы не можем это сделать, не прибегая к действиям в интерфейсе пользователя.
— Как убедиться, что одна команда не вызвала случайно несколько потоков при возникновении только одного воздействия на интерфейс?
— Как не замусорить View Model, если будет множество команд, которые должны будут выполняться в отдельных потоках?
— Как обеспечить совместимость с WP и Silverlight, если имеются различные опции для потоков на каждой системе?

Объект AsynchronousCommand был создан с учетом всех этих нюансов и не только. Знакомимся:
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создание асинхронной команды
      asyncCommand1 = new AsynchronousCommand(
          () =>
          {
            for (int i = 1; i <= 10; i++)
            {
              //  Доклад о ходе работы.
              asyncCommand1.ReportProgress(() => { Messages.Add(i.ToString()); });

              System.Threading.Thread.Sleep(200);
            }
          });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private AsynchronousCommand asyncCommand1;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public AsynchronousCommand AsyncCommand1
    {
      get { return asyncCommand1; }
    }
}

Привязка к элементу XAML:
<Button Content="Asynchronous Command" Command="{Binding AsyncCommand1}" />

После вызова команды вызывается соответствующая функция, которую мы указали с помощью лямда выражения, которая в свою очередь вызывается в отдельном потоке из пула потоков.

Если же возникает необходимость что-то сделать с объектами View Model (которые могут быть связаны с элементами пользовательского интерфейса), мы можем воспользоваться функцией ReportProgress:
asyncCommand1.ReportProgress(() => { Messages.Add(i.ToString()); });

ReportProgress гарантирует, что переданный код будет запущен в нужном потоке, что позволяет довольно просто изменять интерфейс пользователя при необходимости.

Пример 7 - Асинхронные Command обновляющие интерфейс пользователя (UI)
Цель: Команда выполняется дольно долго. Показать прогрессбар.

В AsynchronousCommand есть свойство с именем «IsExecuting». Если оно установлено в true — значит команда в процессе выполнения. Так как AsynchronousCommand реализует интерфейс INotifyPropertyChanged, который подразумевает, что у нас есть возможность привязаться к этому свойству и показывать процесс выполнения.
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создание асинхронной команды
      asyncCommand2 = new AsynchronousCommand(
          () =>
          {
            for (char c = 'A'; c <= 'Z'; c++)
            {
              //  Сообщать о прогрессе
              asyncCommand2.ReportProgress(() => { Messages.Add(c.ToString()); });

              System.Threading.Thread.Sleep(100);
            }
          });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private AsynchronousCommand asyncCommand2;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public AsynchronousCommand AsyncCommand2
    {
      get { return asyncCommand2; }
    }
}

Команду свяжем с кнопкой. А свойство IsExecuting привяжем к другому элементу интерфейса, например, StackPanel, в котором будут находится соответственно TextBlock и ProgressBar:
<Button Content="Asynchronous Command" Command="{Binding AsyncCommand2}" 
        Visibility="{Binding AsyncCommand2.IsExecuting, 
          Converter={StaticResource BooleanToVisibilityConverter}, 
          ConverterParameter=Invert}" />

<StackPanel Visibility="{Binding AsyncCommand2.IsExecuting, 
         Converter={StaticResource BooleanToVisibilityConverter}}">
  <TextBlock Text="The command is running!" />
  <ProgressBar Height="20" Width="120" IsIndeterminate="True" />
</StackPanel>

В этом примере, как только команда начинает выполняться кнопка исчезает, и появляются надпись и прогрессбар. Учтите, что мы осуществляем привязку к свойству IsExecuting команды.
asyncCommand1.ReportProgress(() => { Messages.Add(i.ToString()); });

На заметку: Параметр Invert может быть передан в BooleanToVisilityConverter, так как он представляет собой расширенную версию стандартного BooleanToVisibilityConverter, который определен в Apex.Converters. Инвертирует результат. Очень полезная штучка в определенных моментах.

Пример 8 - Асинхронные Command с возможностью отмены
Цель: Позволить пользователю отменять процесс выполнения асинхронной команды.

Воспользуемся некоторыми возможностями AsynchronousCommand. Каждый объект AsynchronousCommand так же содержит и команду с именем CancelCommand. И она может быть привязана к UI пользователя или вызвана внутри кода в нужном месте. При вызове этой команды, свойство IsCancellationRequested объекта AsynchronousCommand устанавливается в true (учтите, что свойство использует INotifyPropertyChanged и у Вас есть возможность привязаться к нему). Вы можете периодически вызывать функцию CancelIfRequested, и если вдруг она вернет true, то команда остановится.
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
       //  Создание команды с возможностью остановки
       cancellableAsyncCommand = new AsynchronousCommand(
         () => 
           {
             for(int i = 1; i <= 100; i++)
             {
               //  Остановить?
               if(cancellableAsyncCommand.CancelIfRequested())
                 return;

               //  Прогрессбар.
               cancellableAsyncCommand.ReportProgress( () => { Messages.Add(i.ToString()); } );

               System.Threading.Thread.Sleep(100);
             }
           });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private AsynchronousCommand cancellableAsyncCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public AsynchronousCommand CancellableAsyncCommand
    {
      get { return cancellableAsyncCommand; }
    }
}

Привяжем команду к кнопке, а свойство IsExecuting к StackPanel:
<Button Content="Cancellable Async Command" 
    Command="{Binding CancellableAsyncCommand}" 
    Visibility="{Binding CancellableAsyncCommand.IsExecuting, 
             Converter={StaticResource BooleanToVisibilityConverter},
    ConverterParameter=Invert}" />

<StackPanel Visibility="{Binding CancellableAsyncCommand.IsExecuting, 
      Converter={StaticResource BooleanToVisibilityConverter}}">
  <TextBlock Margin="4" Text="The command is running!" />
  <ProgressBar Margin="4" Height="20" 
     Width="120" IsIndeterminate="True" />
  <Button Margin="4" Content="Cancel" 
      Command="{Binding CancellableAsyncCommand.CancelCommand}" />
</StackPanel>

В этом примере показываем кнопку Cancel во время выполнения команды. Эта кнопка связана со свойством CancellableAsyncCommand.CancelCommand. Благодаря тому, что мы используем функцию CancelIfRequested, у нас есть красивая возможность остановить выполнение асинхронной команды.

На заметку: При остановке выполнения асинхронной команды событие Executed не вызывается. Однако вместо него вызывается событие Cancelled, которое может принимать те же самые параметры.

Пример 9 – Привязка событий к Command
Цель: Вызвать команду при активации пользовательского элемента, у которого не установлено свойство Command, но заданно событие.

В этом случае можно воспользоваться свойством прикрепления EventBindings. Оно располагается в классе Apex.Commands. EventBindings принимает EventBindingCollection, которое представляет собой простую коллекцию объектов EventBinding. Каждый EventBinding принимает два параметра: имя события и имя команды, которую нужно вызвать.
public class MainViewModel : ViewModel
{
    public MainViewModel()
    {
      //  Создаем прикрепленное свойство
      EventBindingCommand = new Command(
        () =>
        {
          Messages.Add("Команда вызвана событием.");
        });
    }
    
    /// <summary>
    /// The command object.
    /// </summary>
    private Command eventBindingCommand;

    /// <summary>
    /// Gets the command.
    /// </summary>
    public Command EventBindingCommand
    {
      get { return eventBindingCommand; }
    }
}

Связка команды с событием происходит следующим образом:
<Border Margin="20" Background="Red">

  <!—привязка команды  EventBindingCommand к событию  MouseLeftButtonDown. -->
  <apexCommands:EventBindings.EventBindings>
     <apexCommands:EventBindingCollection>
        <apexCommands:EventBinding EventName="MouseLeftButtonDown" 
            Command="{Binding EventBindingCommand}" />
     </apexCommands:EventBindingCollection>
  </apexCommands:EventBindings.EventBindings>
  
  <TextBlock VerticalAlignment="Center" 
     HorizontalAlignment="Center" Text="Left Click on Me" 
     FontSize="16" Foreground="White" />

</Border>

При использовании EventBindings допускается прикрепление любой команды к любому событию.

Как это работает – Класс Command
Рассмотрим класс Command, который использовался в примерах 1-5.
/// <summary>
/// Класс ViewModelCommand – реализующий интерфейс ICommand, вызывает нужную функцию.
/// </summary>
public class Command : ICommand
{
    /// <summary>
    /// Инициализация нового экземпляра класса без параметров <see cref="Command"/>.
    /// </summary>
    /// <param name="action">Действие.</param>
    /// <param name="canExecute">Если установлено в<c>true</c> [can execute] (выполнение разрешено).</param>
    public Command(Action action, bool canExecute = true)
    {
        //  Set the action.
        this.action = action;
        this.canExecute = canExecute;
    }

    /// <summary>
    /// Инициализация нового экземпляра класса с параметрами <see cref="Command"/> class.
    /// </summary>
    /// <param name="parameterizedAction">Параметризированное действие.</param>
    /// <param name="canExecute"> Если установлено в <c>true</c> [can execute](выполнение разрешено).</param>
    public Command(Action<object> parameterizedAction, bool canExecute = true)
    {
        //  Set the action.
        this.parameterizedAction = parameterizedAction;
        this.canExecute = canExecute;
    }


В первую очередь создаем два перегруженных конструктора, в который передается действие без параметров: Action, или с параметрами: Action<object>, где object — тип.

Далее задаем флаг canExecute, отвечающий за возможность выполнения команды. После изменения флага canExecute необходимо вызвать canExecuteChanged.
/// <summary>
/// Действие(или параметризованное действие) которое вызывается при активации команды.
/// </summary>
protected Action action = null;
protected Action<object> parameterizedAction = null;

/// <summary>
/// Будевое значение, отвечающие за возможность выполнения команды.
/// </summary>
private bool canExecute = false;

/// <summary>
/// Установка /  получение значения, отвечающего за возможность выполнения команды
/// </summary>
/// <value>
///     <c>true</c> если выполнение разрешено; если запрещено - <c>false</c>.
/// </value>
public bool CanExecute
{
    get { return canExecute; }
    set
    {
        if (canExecute != value)
        {
            canExecute = value;
            EventHandler canExecuteChanged = CanExecuteChanged;
            if (canExecuteChanged != null)
                canExecuteChanged(this, EventArgs.Empty);
        }
    }
}

Далее реализуем интерфейс ICommand
/// <summary>
/// Определяем метод, определющий, что выполнение команды допускается в текущем состоянии
/// </summary>
/// <param name="parameter">Этот параметр используется командой.
///  Если команда вызывается без использования параметра,
///  то этот объект может быть установлен в  null.</param>
/// <returns>
/// > если выполнение команды разрешено; если запрещено - false.
/// </returns>
bool ICommand.CanExecute(object parameter)
{
    return canExecute;
}

/// <summary>
/// Задание метода, который будет вызван при активации команды.
/// </summary>
/// <param name="parameter"> Этот параметр используется командой.
///  Если команда вызывается без использования параметра,
///  то этот объект может быть установлен в  null.</param>
void ICommand.Execute(object parameter)
{
    this.DoExecute(parameter);

}

Функцию DoExecute разберем чуть позже.
/// <summary>
///  Вызывается, когда меняется возможность выполнения команды
/// </summary>
public event EventHandler CanExecuteChanged;

/// <summary>
/// Вызывается во время выполнения команды
/// </summary>
public event CancelCommandEventHandler Executing;

/// <summary>
/// Вызывается, когда команды выполнена
/// </summary>
public event CommandEventHandler Executed;


А теперь реализуем функцию Invoke для каждого события. Таким образом у нас появится возможность вызывать их из производных классов.
protected void InvokeAction(object param)
{
    Action theAction = action;
    Action<object> theParameterizedAction = parameterizedAction;
     if (theAction != null)
        theAction();
    else if (theParameterizedAction != null)
        theParameterizedAction(param);
}

protected void InvokeExecuted(CommandEventArgs args)
{
    CommandEventHandler executed = Executed;

    //  Вызвать все события
    if (executed != null)
        executed(this, args);
}

protected void InvokeExecuting(CancelCommandEventArgs args)
{
    CancelCommandEventHandler executing = Executing;

    //  Call the executed event.
    if (executing != null)
        executing(this, args);
}


На заметку: InvokeAction вызывает либо действие без параметров, либо с параметрами, смотря какое из них установлено.
/// <summary>
/// Выполнение команды
/// </summary>
/// <param name="param">The param.</param>
public virtual void DoExecute(object param)
{
    //  Вызывает выполнении команды с возможностью отмены
    CancelCommandEventArgs args = 
       new CancelCommandEventArgs() { Parameter = param, Cancel = false };
    InvokeExecuting(args);

    //  Если событие было отменено -  останавливаем.
    if (args.Cancel)
        return;

    //  Вызываем действие с / без параметров, в зависимости от того. Какое было устанвленно.
    InvokeAction(param);

    //  Call the executed function.
    InvokeExecuted(new CommandEventArgs() { Parameter = param });
}

DoExecute достаточно проста. Просто вызывает соответствующее событие с возможностью отменить выполнение.

Выше описанный класс реализует интерфейс ICommand и предоставляет весь необходимый функционал, использующийся в примерах 1-5.

Как это работает – Класс асинхронных Command
В примерах 6-8 используется класс AsynchronousCommand, который в свою очередь наследует класс Command, описанный выше.

Сначала объявляем касс и конструктор:
/// <summary>
/// Асинхронные команды -  это команды, которые выполняются в отдельных потоках их пула потоков..
/// </summary>
public class AsynchronousCommand : Command, INotifyPropertyChanged
{
    /// <summary>
    /// Инициализация нового экземпляра класса без параметров <see cref="AsynchronousCommand"/>.
    /// </summary>
    /// <param name="action">Действие.</param>
    /// <param name="canExecute"> Если установлено в 
    ///  <c>true</c> команда может выполняться.</param>
    public AsynchronousCommand(Action action, bool canExecute = true) 
      : base(action, canExecute)
    { 
      //  Инициализация команды
      Initialise();
    }

    /// <summary>
    /// Инициализация нового экземпляра класса с параметрами<see cref="AsynchronousCommand"/>.
    /// </summary>
    /// <param name="parameterizedAction">Параметризированное действие.</param>
    /// <param name="canExecute"> Если установлено в <c>true</c> [can execute] (может выполняться).</param>
    public AsynchronousCommand(Action<object> parameterizedAction, bool canExecute = true)
      : base(parameterizedAction, canExecute) 
    {

      //  Инициализация команды
      Initialise(); 
    }


Благодаря реализации интерфейса INotifyPropertyChanged появляется возможность уведомления при изменении переменной IsExecuting. Так как оба конструктора вызывают метод Initialise, рассмотрим его более подробно:
/// <summary>
/// Команда отмены
/// </summary>
private Command cancelCommand;

/// <summary>
/// Получение команды отмены.
/// </summary>
public Command CancelCommand
{
  get { return cancelCommand; }
}

/// <summary>
/// Получить/Установить значение, указывающее, поступила ли команда отмены
/// </summary>
/// <value>
///     <c>true</c> если есть запрос на отмену; запроса нет -  <c>false</c>.
/// </value>
public bool IsCancellationRequested
{
  get
  {
    return isCancellationRequested;
  }
  set
  {
    if (isCancellationRequested != value)
    {
      isCancellationRequested = value;
      NotifyPropertyChanged("IsCancellationRequested");
    }
  }
}

/// <summary>
/// Инициализация экземпляра
/// </summary>
private void Initialise()
{
  //  Конструктор команды отмены
  cancelCommand = new Command(
    () =>
    {
      //  Set the Is Cancellation Requested flag.
      IsCancellationRequested = true;
    }, true);
}


Все, что тут выполняется, несмотря на обилие кода — просто установка флага IsCancellationRequested в значение true. Инициализация создает объект и позволяет получить к нему доступ. Так же имеется свойство IsCancellationRequested, которое информирует, когда оно меняет свое состояние.

Так же необходимо знать, когда команда в процессе выполнения. Добавим следующий код:
/// <summary>
/// Флаг, отображающий, что команда в процессе выполнения.
/// </summary>
private bool isExecuting = false;

/// <summary>
/// Получение/Установка флага, который показывает, что команда в процессе выполнения..
/// </summary>
/// <value>
///     <c>true</c> если в процессе выполнения; иначе <c>false</c>.
/// </value>
public bool IsExecuting
{
  get
  {
    return isExecuting;
  }
  set
  {
    if (isExecuting != value)
    {
      isExecuting = value;
      NotifyPropertyChanged("IsExecuting");
    }
  }
}


Продолжим. Так как у нас есть возможность отмены добавим события Cancelled и PropertyChanged (реализующее интерфейс INotifyPropertyChanged):
/// <summary>
/// The property changed event.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;

/// <summary>
/// Возникает, когда команда отменена.
/// </summary>
public event CommandEventHandler Cancelled;

Так же изменился и метод DoExecute.

/// <summary>
/// Выполнение команды.
/// </summary>
/// <param name="param">Параметр.</param>
public override void DoExecute(object param)
{
  //  Если уже в процессе выполнения, тоне продолжаем.
  if (IsExecuting)
    return;

  //  Вызов выподняющейся команды, что позволяет отменить ее выполнение.
  CancelCommandEventArgs args = 
     new CancelCommandEventArgs() { Parameter = param, Cancel = false };
  InvokeExecuting(args);

  //  Если отмена -  прерываем.
  if (args.Cancel)
    return;

  //  В процессе выполнения.
  IsExecuting = true;


Мы не запускаем команду, если она уже выполняется, однако есть возможность ее отменить и установить флаг выполнения.
//  Сохранение вызванного диспатчера.
#if !SILVERLIGHT
      callingDispatcher = Dispatcher.CurrentDispatcher;
#else
      callingDispatcher = System.Windows.Application.Current.RootVisual.Dispatcher;
#endif


Мы должны сохранять диспатчер выполняемой команды, так как при выведении данных о процессе выполнения, ссылаемся на соответствующий диспатчер.

Нужно помнить, что процесс сохранения отличается на Silverlight и WPF.
// Run the action on a new thread from the thread pool
// (this will therefore work in SL and WP7 as well).
ThreadPool.QueueUserWorkItem(
    (state) =>
    {
      //   Вызов действия.
      InvokeAction(param);

      //  Fire the executed event and set the executing state.
      ReportProgress(
        () =>
        {
          //  Больше не в процессе выполнения.
          IsExecuting = false;

          //  если отменили,
          //  вызвать событие отмены - , если нет – продолжить выполнение.
          if(IsCancellationRequested)
            InvokeCancelled(new CommandEventArgs() { Parameter = param });
          else
            InvokeExecuted(new CommandEventArgs() { Parameter = param });

          //  Юольше не запрашиваем отмену.
          IsCancellationRequested = false;
        }
      );
    }
  );
}

А теперь про потоки. Используя пул потоков, отправляем функцию InvokeAction в очередь, которая вызовет функцию команды в отдельном потоке. Так же не следует забывать, что ReportProgress в зависимости от диспатчера, и именно тут нужно изменять свойства и вызывать Executed. При вызове диспатчера (после успешного завершения действия), необходимо очистить флаг IsExecuting, а так же вызывать одно из событий: Cancelled или Executed. И так, осталось рассмотреть только ReportProgress:
/// <summary>
/// Reports progress on the thread which invoked the command.
/// </summary>
/// <param name="action">The action.</param>
public void ReportProgress(Action action)
{
  if (IsExecuting)
  {
    if (callingDispatcher.CheckAccess())
      action();
    else
      callingDispatcher.BeginInvoke(((Action)(() => { action(); })));
  }
}



Как это работает – Класс привязки дынных Command
Код EventBindings несколько сложнее из-за различий в WPF и Silverlight. В WPF EventBindingsCollection это FreezableCollection, то есть наследники наследуют также и контекст данных. В Silverlight нет FreezableCollection, поэтому необходимо передавать контекст данных вручную.
public static class EventBindings
{
  /// <summary>
  /// Свойство Event Bindings.
  /// </summary>
    private static readonly DependencyProperty EventBindingsProperty =
      DependencyProperty.RegisterAttached("EventBindings", 
      typeof(EventBindingCollection), typeof(EventBindings),
      new PropertyMetadata(null, new PropertyChangedCallback(OnEventBindingsChanged)));

    /// <summary>
    /// Gets the event bindings.
    /// </summary>
    /// <param name="o">The o.</param>
    /// <returns></returns>
    public static EventBindingCollection GetEventBindings(DependencyObject o)
    {
        return (EventBindingCollection)o.GetValue(EventBindingsProperty);
    }

    /// <summary>
    /// Sets the event bindings.
    /// </summary>
    /// <param name="o">The o.</param>
    /// <param name="value">The value.</param>
    public static void SetEventBindings(DependencyObject o, 
                       EventBindingCollection value)
    {
        o.SetValue(EventBindingsProperty, value);
    }

    /// <summary>
    /// Called when event bindings changed.
    /// </summary>
    /// <param name="o">The o.</param>
    /// <param name="args">The <see
    /// cref="System.Windows.DependencyPropertyChangedEventArgs"/>
    /// instance containing the event data.</param>
    public static void OnEventBindingsChanged(DependencyObject o, 
           DependencyPropertyChangedEventArgs args)
    {
        //  Cast the data.
        EventBindingCollection oldEventBindings = 
          args.OldValue as EventBindingCollection;
        EventBindingCollection newEventBindings = 
          args.NewValue as EventBindingCollection;

        //  If we have new set of event bindings, bind each one.
        if (newEventBindings != null)
        {
            foreach (EventBinding binding in newEventBindings)
            {
                binding.Bind(o);
#if SILVERLIGHT
                //  If we're in Silverlight we don't inherit the
                //  data context so we must set this helper variable.
                binding.ParentElement = o as FrameworkElement;
#endif
            }
        }
    }
}


Рассмотрим EventBinding.Bind:
public void Bind(object o)
{
    try
    {
        //  Получаем информацию о событии по его имени
        EventInfo eventInfo = o.GetType().GetEvent(EventName);

        //  Get the method info for the event proxy.
        MethodInfo methodInfo = GetType().GetMethod("EventProxy", 
                   BindingFlags.NonPublic | BindingFlags.Instance);

        //  Create a delegate for the event to the event proxy.
        Delegate del = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, methodInfo);

        //  Add the event handler. (Removing it first if it already exists!)
        eventInfo.RemoveEventHandler(o, del);
        eventInfo.AddEventHandler(o, del);
    }
    catch (Exception e)
    {
        string s = e.ToString();
    }
}
/// <summary>
/// Proxy to actually fire the event.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="e">The <see
///    cref="System.EventArgs"/> instance
///    containing the event data.</param>
private void EventProxy(object o, EventArgs e)
{   
#if SILVERLIGHT

    //  If we're in Silverlight, we have NOT inherited the data context
    //  because the EventBindingCollection is not a framework element and
    //  therefore out of the logical tree. However, we can set it here 
    //  and update the bindings - and it will all work.
    DataContext = ParentElement != null ? ParentElement.DataContext : null;
    var bindingExpression = GetBindingExpression(EventBinding.CommandProperty);
    if(bindingExpression != null)
        bindingExpression.UpdateSource();
    bindingExpression = GetBindingExpression(EventBinding.CommandParameterProperty);
    if (bindingExpression != null)
        bindingExpression.UpdateSource();

#endif

    if (Command != null)
        Command.Execute(CommandParameter);
}



Tags:
Hubs:
+14
Comments 5
Comments Comments 5

Articles