Руководство для начинающих (часть 3)

Я программист .NET. Я много программирую на VB.NET и C#, ASP.NET/Winforms/WPF/WCF Flash Silverlight. Но когда начал писать данные статьи, я, естественно, выбрал мой любимый язык — C#. Через некоторое время я получил сообщение от одного человека с просьбой публиковать исходный код на VB.NET и C# в статьях этой серии. Я ответил, что у меня нет времени. И тогда этот человек, Роберт Рэнк (Robert Ranck), вызвался помочь с преобразованием моих исходных проектов на C# в VB.NET.

За этот и последующие проекты VB.NET следует благодарить Роберта Рэнка. Спасибо, Роберт! Ваше участие, несомненно, сделает эту серию более доступной для всех разработчиков .NET.

Введение

Это моя третья работа из серии статей о WPF для начинающих. В ней мы обсудим две важные составляющие разработки приложений WPF: перенаправленные события и перенаправленные команды. А вот и предполагаемое содержание этой серии:

В этой статье я планирую кратко остановиться на следующих вопросах:

  • перенаправленные события: что это такое, как они работают, как их использовать и создавать;
  • перенаправленные команды: что это такое, как они работают, как их использовать и создавать;
  • элементы автоматизации;
  • демонстрационные приложения.

Перенаправленные события

Перенаправленные события — понятие новое для большинства разработчиков. В доброй старой платформе .NET 1.x/2.0 все мы, вероятно, использовали бы какие-нибудь пользовательские события или подключили какие-нибудь делегаты к существующим событиям, например, так:

private System.Web.Forms.Button button1;

button1.click+=new EventHandler(button1_Click);

...

private void button1_Click(object sender, EventArge e)

{

    //Событие Click

}

Это все хорошо и правильно. Класс System.Web.Forms.Button предоставляет событие OnClick, подписавшись на которое с помощью стандартного делегата EventHandler, можно получать событие, когда объект System.Web.Forms.Button создает внутреннее событие OnClick. Далее такой тип подписки и уведомления о событиях будет называться событиями CLR.
В WPF все немного по-другому. Существует три способа уведомления о событиях:

  • Всплывающая маршрутизация. События «всплывают» по дереву VisualTree (дерево визуальных элементов интерфейса пользователя) до корневого элемента.
  • Нисходящая маршрутизация. События опускаются по дереву VisualTree.
  • Прямая маршрутизация. Аналог событий CLR в старой платформе .NET 1.x/2.0. Только подписчик видит событие.


Когда событие порождается при движении вверх или вниз по дереву VisualTree, вызываются обработчики, подписанные на событие RoutedEvent. При таком прохождении дерева VisualTreeвыполняется обход не всего дерева, а только той его части, которая непосредственно связана с элементом, породившим событие.
Если вы хотите ознакомиться с другими ресурсами, в блоге Джоша Смита (JoshSmith) есть отличная запись по этой теме.

Достаточно часто одно логическое событие представляется двумя фактическими: нисходящим и всплывающим. Чтобы определить, как эти события были созданы, используется соглашение об именовании. Нисходящие события обычно имеют вид PreviewXXX, а всплывающие просто XXX, например: PreviewKeyDown (нисходящее) и KeyDown (всплывающее).

Чтобы лучше понять перенаправление событий, в рамках общего демонстрационного решения (вверху этой статьи) я подготовил проект Part3_RoutedEventViewer — стандартное приложение WPF, которое можно использовать для исследования команд WPF. При запуске приложение должно выглядеть следующим образом:

Это небольшое демонстрационное приложение поможет вам лучше понять, как работают перенаправленные события. Но прежде чем переходить к другим снимкам экрана, давайте ознакомимся с кодом XAML приложения.

<Window x:Class="Part3_RoutedEventViewer.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="Examining Routed Events" Height="300" Width="300"

    WindowState="Maximized">

    <Grid x:Name="gridMain">

        <Grid.Resources>

         //Пропущено для простоты

        </Grid.Resources>

        <Grid.RowDefinitions>

            <RowDefinition Height="auto"/>

            <RowDefinition Height="auto"/>

            <RowDefinition Height="100*"/>

       </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">

            <Button x:Name="btnTop"  Margin="10" Padding="2"

                Content="Examining Routed Events" Height="auto"/>

            <Button x:Name="btnClearItems"  Margin="10" Padding="2"

                Content="Clear Items" Height="auto" Click="btnClearItems_Click"/>

        </StackPanel>

        <ListView x:Name="lvResults" Margin="0,0,0,0"

                IsSynchronizedWithCurrentItem="True" Grid.Row="2" >

                <ListView.View>

                    <GridView ColumnHeaderContainerStyle="{StaticResource headerContainerStyle}" >

                        <GridViewColumn  Header="RoutedEventName" Width="150"

                        CellTemplate="{StaticResource RoutedEventNameTemplate}"/>

                        <GridViewColumn  Header="SenderName" Width="100"

                        CellTemplate="{StaticResource SenderNameTemplate}"/>

                        <GridViewColumn  Header="ArgsSource" Width="100"

                        CellTemplate="{StaticResource ArgsSourceTemplate}"/>

                        <GridViewColumn  Header="OriginalSource" Width="100"

                        CellTemplate="{StaticResource OriginalSourceTemplate}"/>

                </GridView>

                </ListView.View>

        </ListView>

    </Grid>

</Window>

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

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace Part3_RoutedEventViewer

{

    /// <summary>

    /// Демонстрационное приложение, отображающее некоторые сведения о событиях,

    /// полученных в результате действий пользователя. Оно позволяет пользователям

    /// увидеть разницу между нисходящими и всплывающими событиями

    /// </summary>

    public partial class Window1 : Window

    {

        #region Ctor

        /// <summary>

        /// Связывает некоторые стандартные всплывающие/нисходящие события <see cref="RoutedEvent">RoutedEvents</see>

        /// для элементов <see cref="FrameworkElement">FrameworkElement</see>.

        /// Это демонстрационное приложение отображает некоторые сведения о событиях,

        /// полученных в результате действий пользователя.

        /// </summary>

        public Window1()

        {

            InitializeComponent();

            UIElement[] els = { this, gridMain, btnTop, lvResults };

            foreach (UIElement el in els)

            {

                //Клавиатура

                el.PreviewKeyDown += GenericHandler;

                el.PreviewKeyUp += GenericHandler;

                el.PreviewTextInput += GenericHandler;

                el.KeyDown += GenericHandler;

                el.KeyUp += GenericHandler;

                el.TextInput += GenericHandler;

                //Мышь

                el.MouseDown += GenericHandler;

                el.MouseUp += GenericHandler;

                el.PreviewMouseDown += GenericHandler;

                el.PreviewMouseUp += GenericHandler;

                //Перо

                el.StylusDown += GenericHandler;

                el.StylusUp += GenericHandler;

                el.PreviewStylusDown += GenericHandler;

                el.PreviewStylusUp += GenericHandler;

                el.AddHandler(Button.ClickEvent, new RoutedEventHandler(GenericHandler));

            }

        }

        #endregion

        #region Private Methods

        /// <summary>

        /// Создает новый класс <see cref="EventDemoClass">EventDemoClass</see>

        /// для представления события <see cref="RoutedEvent">RoutedEvent</see>.

        /// Добавляет этот новый класс EventDemoClass в список.

        /// </summary>

        private void GenericHandler(object sender, RoutedEventArgs e)

        {

            lvResults.Items.Add(new EventDemoClass()

            {

                RoutedEventName = e.RoutedEvent.Name,

                SenderName = typeWithoutNamespace(sender),

                ArgsSource = typeWithoutNamespace(e.Source),

                OriginalSource = typeWithoutNamespace(e.OriginalSource)

            });

        }

        /// <summary>

        /// Возвращает имя типа без пространства имен

        /// </summary>

        private string typeWithoutNamespace(object obj)

        {

            string[] astr = obj.GetType().ToString().Split('.');

            return astr[astr.Length - 1];

        }

        /// <summary>

        /// Очищает список событий

        /// </summary>

        private void btnClearItems_Click(object sender, RoutedEventArgs e)

        {

            lvResults.Items.Clear();

        }

        #endregion

    }

    #region EventDemoClass CLASS

    /// <summary>

    /// Простой класс данных, используемый для отображения событий данных

    /// </summary>

    public class EventDemoClass

    {

        public string RoutedEventName { get; set; }

        public string SenderName { get; set; }

        public string ArgsSource { get; set; }

        public string OriginalSource { get; set; }

    }

    #endregion

}

А здесь приводится версия VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Input

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

Imports System.Windows.Navigation

Imports System.Windows.Shapes

    ''' <summary>

    ''' Демонстрационное приложение, отображающее некоторые сведения о событиях,

    ''' полученных в результате действий пользователя. Оно позволяет пользователям

    ''' увидеть разницу между нисходящими и всплывающими событиями

    ''' </summary>

    Partial Public Class Window1

        Inherits Window

#Region "Ctor"

        ''' <summary>

        ''' Связывает некоторые стандартные всплывающие/нисходящие события <see cref="RoutedEvent">RoutedEvents</see>

        ''' для элементов <see cref="FrameworkElement">FrameworkElement</see>.

        ''' Это демонстрационное приложение отображает некоторые сведения о событиях,

        ''' полученных в результате действий пользователя.

        ''' </summary>

        Public Sub New()

            InitializeComponent()

            Dim els As UIElement() = {Me, gridMain, btnTop, lvResults}

            For Each el As UIElement In els

            'Клавиатура

            AddHandler el.PreviewKeyDown, AddressOf GenericHandler

            AddHandler el.PreviewKeyUp, AddressOf GenericHandler

            AddHandler el.PreviewTextInput, AddressOf GenericHandler

            AddHandler el.KeyDown, AddressOf GenericHandler

            AddHandler el.KeyUp, AddressOf GenericHandler

            AddHandler el.TextInput, AddressOf GenericHandler

            'Мышь

            AddHandler el.MouseDown, AddressOf GenericHandler

            AddHandler el.MouseUp, AddressOf GenericHandler

            AddHandler el.PreviewMouseDown, AddressOf GenericHandler

            AddHandler el.PreviewMouseUp, AddressOf GenericHandler

            'Перо

            AddHandler el.StylusDown, AddressOf GenericHandler

            AddHandler el.StylusUp, AddressOf GenericHandler

            AddHandler el.PreviewStylusDown, AddressOf GenericHandler

            AddHandler el.PreviewStylusUp, AddressOf GenericHandler

            el.AddHandler(Button.ClickEvent, New RoutedEventHandler(AddressOf GenericHandler))

        Next

        End Sub

#End Region

#Region "Private Methods"

        ''' <summary>

        'Создает новый класс <see cref="EventDemoClass">EventDemoClass</see>

        'для представления события <see cref="RoutedEvent">RoutedEvent</see>.

        'Добавляет этот новый класс EventDemoClass в список.

        ''' </summary>

        Private Sub GenericHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)

        Dim eventClass As New EventDemoClass()

        eventClass.RoutedEventName = e.RoutedEvent.Name

        eventClass.SenderName = typeWithoutNamespace(sender)

        eventClass.ArgsSource = typeWithoutNamespace(e.Source)

        eventClass.OriginalSource = typeWithoutNamespace(e.OriginalSource)

        lvResults.Items.Add(eventClass)

    End Sub

    ''' <summary>

    ''' Возвращает имя типа без пространства имен

    ''' </summary>

    Private Function typeWithoutNamespace(ByVal obj As Object) As String

        Dim astr As String() = obj.GetType().ToString().Split(".")

        Return astr(astr.Length - 1)

    End Function

    ''' <summary>

    ''' Очищает список событий

    ''' </summary>

    Private Sub btnClearItems_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)

        lvResults.Items.Clear()

    End Sub

#End Region

End Class

#Region "EventDemoClass CLASS"

''' <summary>

''' Простой класс данных, используемый для отображения событий данных

''' </summary>

Public Class EventDemoClass

#Region "Instance Fields"

    Private newRoutedEventName As String

    Private newSenderName As String

    Private newArgsSource As String

    Private newOriginalSource As String

#End Region

#Region "Propeties"

Public Property RoutedEventName() As String

    Get

        Return newRoutedEventName

    End Get

    Set(ByVal value As String)

        newRoutedEventName = value

    End Set

End Property

Public Property SenderName() As String

    Get

        Return newSenderName

    End Get

    Set(ByVal value As String)

        newSenderName = value

    End Set

End Property

Public Property ArgsSource() As String

    Get

        Return newArgsSource

    End Get

    Set(ByVal value As String)

        newArgsSource = value

    End Set

End Property

Public Property OriginalSource() As String

    Get

        Return newOriginalSource

    End Get

    Set(ByVal value As String)

        newOriginalSource = value

    End Set

End Property

#End Region

End Class

#End Region

Как видно из приведенного кода, в этом файле Window1.xaml есть следующее визуальное дерево.

С учетом этого рассмотрим несколько снимков экрана, сделанных с помощью демонстрационного проекта Part3_RoutedEventViewer.

Если щелкнуть элемент Window, можно увидеть следующие события. Отображаются только события уровня элемента Window, корневого в визуальном дереве.

Если нажать кнопку (левую), можно увидеть следующие события, поскольку элемент Button — дочерний по отношению к элементу Grid, который, в свою очередь, является дочерним по отношению к Window1.

Надеюсь, это поможет лучше понять, как перенаправленные события перемещаются по визуальному дереву.

Работать с перенаправленными событиями можно так же, как и с другими событиями. Например, в XAML это можно сделать следующим образом:

<Button x:Name="btnClearItems"  Content="Clear Items" Click="btnClearItems_Click"/>

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

Либо можно просто подписаться на перенаправленное событие в коде программной части.

Button btn = new Button();

btn.Click += new RoutedEventHandler(btn_Click);

....

....

....

void btn_Click(object sender, RoutedEventArgs e)

{

    //seen event do something

}

И версия VB.NET.

btn.AddHandler(Button.ClickEvent, New RoutedEventHandler(AddressOf btn_Click))

.....

.....

.....

Private Sub GenericHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)

    //seen event do something

End Sub

Или даже так (если вы любите использовать анонимные делегаты).

Button btn = new Button();

btn.Click += delegate(object sender, RoutedEventArgs e)

{

    //seen event do something

};

И даже можно добавить непосредственно в обработчики элементов интерфейса пользователя.

Button btn = new Button();

btn.AddHandler(Button.ClickEvent, new RoutedEventHandler(GenericHandler));

И на VB.NET.

btn.AddHandler(Button.ClickEvent, New RoutedEventHandler(AddressOf GenericHandler))

Я подготовил еще один демонстрационный проект (Part3_RoutedEventsExample), входящий в состав общего решения вверху этой статьи. В этом проекте рассматриваются два вопроса:

  1. Создание и использование события RoutedEvent, применяющего стандартные аргументы RoutedEventArgs.
  2. Создание и использование события RoutedEvent, применяющего пользовательские аргументы RoutedEventArgs.

При работе это приложение выглядит следующим образом:

В этом приложении в окне Window1 размещен один пользовательский элемент управления UserControl. Он порождает два события, на которые подписан элемент Window1. Мы уже обсуждали различные способы подписки на события RoutedEvent, так что я не буду останавливаться на этом, а расскажу лучше, как порождать собственное событие RoutedEvent.

Первый шаг — создание и регистрация события в объекте EventManager следующим образом. Обратите внимание, что это только один из возможных конструкторов класса RoutedEvent. Информацию о других конструкторах см. в документации MSDN.

// Перенаправление фактического события

public static readonly RoutedEvent CustomClickEvent =

    EventManager.RegisterRoutedEvent(

    "CustomClick", RoutingStrategy.Bubble,

    typeof(RoutedEventHandler),

    typeof(UserControlThatCreatesEvent));

А здесь приводится версия VB.NET.

Public Shared ReadOnly CustomClickEvent As RoutedEvent =

EventManager.RegisterRoutedEvent("CustomClick",

RoutingStrategy.Bubble, GetType(RoutedEventHandler),

GetType(UserControlThatCreatesEvent))

Важно то, что в объявлении RoutedEventмы информируем EventManager о стратегии событий, которая будет использоваться для всплывающих, нисходящих и прямых событий, обсуждавшихся выше. Кроме того, необходимо задать тип Type, создаваемый событием, и некоторые другие метаданные. Затем нужно создать раздел обработчиков (там должен быть только этот код, не добавляйте ничего лишнего) для фактического события, который будет использоваться, когда подписчики подключаются к событию RoutedEventили отключаются.

//добавление и удаление обработчиков

public event RoutedEventHandler CustomClick

{

    add { AddHandler(CustomClickEvent, value); }

    remove { RemoveHandler(CustomClickEvent, value); }

}

А здесь приводится версия VB.NET.

    'Использование стандартных аргументов события

    Public Custom Event CustomClick As RoutedEventHandler

        AddHandler(ByVal value As RoutedEventHandler)

            Me.AddHandler(CustomClickEvent, value)

        End AddHandler

        RemoveHandler(ByVal value As RoutedEventHandler)

            Me.RemoveHandler(CustomClickEvent, value)

        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)

            Me.RaiseEvent(e)

        End RaiseEvent

    End Event

И, наконец, необходимо создать событие — следующим образом (код VBбудет другим, см. прилагаемый проект).

//Создание пользовательского события CustomClickEvent

RoutedEventArgs args = new RoutedEventArgs(CustomClickEvent);

RaiseEvent(args);

А здесь приводится версия VB.NET.

Dim args As New RoutedEventArgs(CustomClickEvent)

MyBase.RaiseEvent(args)

Как видно из примера, для порождения фактического события RoutedEvent используется метод RaiseEvent(). Каждый элемент FrameworkElementпредоставляет этот метод, с помощью которого можно создать любое событие RoutedEvent. Впрочем, мы не всегда будем его использовать. Но об этом позже.

Как и в первом случае, необходимо зарегистрировать событие в EventManagerследующим образом (код VB будет другим, см. прилагаемый проект). Опять же, это только один из возможных конструкторов класса RoutedEvent. Информацию о других конструкторах см. в документации MSDN.

// Перенаправление фактического события

public static readonly RoutedEvent CustomClickWithCustomArgsEvent =

    EventManager.RegisterRoutedEvent(

    "CustomClickWithCustomArgs", RoutingStrategy.Bubble,

    typeof(CustomClickWithCustomArgsEventHandler),

    typeof(UserControlThatCreatesEvent));

А здесь приводится версия VB.NET.

Public Shared ReadOnly CustomClickWithCustomArgsEvent

As RoutedEvent = EventManager.RegisterRoutedEvent

("CustomClickWithCustomArgs", RoutingStrategy.Bubble,

GetType(CustomClickWithCustomArgsEventHandler),

GetType(UserControlThatCreatesEvent))

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

public delegate void CustomClickWithCustomArgsEventHandler(object sender, CustomEventArgs e);

А здесь приводится версия VB.NET.

Public Delegate Sub CustomClickWithCustomArgsEventHandler(ByVal sender As Object, ByVal e As CustomEventArgs)

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

//добавление и удаление обработчиков

public event CustomClickWithCustomArgsEventHandler CustomClickWithCustomArgs

{

    add { AddHandler(CustomClickWithCustomArgsEvent, value); }

    remove { RemoveHandler(CustomClickWithCustomArgsEvent, value); }

}

А здесь приводится версия VB.NET.

    'Использование пользовательских аргументов события

    Public Custom Event CustomClickWithCustomArgs As CustomClickWithCustomArgsEventHandler

        AddHandler(ByVal value As CustomClickWithCustomArgsEventHandler)

            Me.AddHandler(CustomClickWithCustomArgsEvent, value)

        End AddHandler

        RemoveHandler(ByVal value As CustomClickWithCustomArgsEventHandler)

            Me.RemoveHandler(CustomClickWithCustomArgsEvent, value)

        End RemoveHandler

        RaiseEvent(ByVal sender As Object, ByVal e As CustomEventArgs)

            Me.RaiseEvent(e)

        End RaiseEvent

    End Event

И, наконец, необходимо породить событие с помощью пользовательских аргументов RoutedEventArgs. Это делается следующим образом (код VBбудет другим, см. прилагаемый проект).

//Создание пользовательского события CustomClickWithCustomArgs

CustomEventArgs args = new CustomEventArgs(CustomClickWithCustomArgsEvent, ++clickedCount);

RaiseEvent(args);

Здесь используются пользовательские аргументы RoutedEventArgs для перенаправленного события CustomClickWithCustomArgsEvent.

/// <summary>

/// CustomEventArgs: класс пользовательских аргументов события,

/// содержащий значение int, которое представляет

/// число возникновений соответствующего события

/// </summary>

public class CustomEventArgs : RoutedEventArgs

{

    #region Instance fields

    public int SomeNumber { get; private set; }

    #endregion

    #region Ctor

    /// <summary>

    /// Создает новый объект CustomEventArgs

    /// с использованием предоставленных параметров

    /// </summary>

    /// <param name="someNumber">the value for the events args</param>

    public CustomEventArgs(RoutedEvent routedEvent,

        int someNumber)

        : base(routedEvent)

    {

        this.SomeNumber = someNumber;

    }

    #endregion

}

А здесь приводится версия VB.NET.

'Me.RaiseCustomClickWithCustomArgsEvent()

'Создание пользовательского события CustomClickWithCustomArgs

ClickedCount = ClickedCount + 1

Dim args As New CustomEventArgs(CustomClickWithCustomArgsEvent, ClickedCount)

MyBase.RaiseEvent(args)

Здесь используются пользовательские аргументы RoutedEventArgs для перенаправленного события CustomClickWithCustomArgsEvent.

Imports System

Imports System.Windows

''' <summary>

''' CustomEventArgs: класс пользовательских аргументов события,

''' содержащий значение int, которое представляет

''' число возникновений соответствующего события

''' </summary>

Public Class CustomEventArgs

    Inherits System.Windows.RoutedEventArgs

#Region "Instance Fields"

    Private newSomeNumber As Integer

#End Region

#Region "Properties"

    Public Property SomeNumber() As Integer

        Get

            Return newSomeNumber

        End Get

        Set(ByVal value As Integer)

            newSomeNumber = value

        End Set

    End Property

#End Region

#Region "Ctor"

    ''' <summary>

    ''' Создает новый объект CustomEventArgs

    ''' с использованием предоставленных параметров

    '''</summary>

    '''<param name="someNumber">the value for the events args</param>

    Public Sub New(ByVal routedEvent As System.Windows.RoutedEvent, ByVal someNumber As Integer)

        MyBase.New(routedEvent)

        Me.SomeNumber = someNumber

    End Sub

#End Region

End Class

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

Перенаправленные команды
Система команд WPF построена на основе классов RoutedCommand и RoutedEvent. От простого обработчика событий, присоединенного к кнопке или таймеру, команды отличаются тем, что они отделяют семантику и инициатор действия от логики. Это позволяет вызывать для нескольких разнородных источников одну и ту же логику команды, а также настраивать ее для различных целевых объектов. Примерами команд служат операции редактирования «Копировать», «Вырезать» и «Вставить», доступные во многих приложениях. Семантика команды унифицирована для различных приложений и классов, однако логика действия специфична для конкретного объекта. Сочетание клавиш CTRL+Xвызывает команду «Вырезать» в классах текста, классах изображений и веб-браузерах. Однако фактическая логика выполнения этой операции определяется объектом или приложением, в котором она выполняется, а не источником, в котором она вызывается. В текстовом объекте можно вырезать и поместить выделенный текст в буфер обмена. В графическом объекте можно вырезать выделенное изображение. Однако для вызова команды в обоих классах может использоваться один и тот же источник команды, например объект KeyGestureили кнопка на панели инструментов. В платформах .NET3.0/3.5 предусмотрено множество готовых команд для выполнения типичных задач. Ниже приведено несколько примеров таких команд.

  • Класс ApplicationCommands содержит такие команды, как вырезание, копирование и вставка. Полный список команд, доступных в классе ApplicationCommands, см. здесь.
  • Класс MediaCommands содержит такие команды, как «Усиление баса», «Следующий канал», «Предыдущий канал» и «Отключение звука». Полный список команд, доступных в классе MediaCommands, см. здесь.
  • Класс NavigationCommands содержит такие команды, как «Переход назад», «Переход вперед» и «Избранное». Полный список команд, доступных в классе NavigationCommands, см. здесь.
  • Класс ComponentCommands содержит такие команды, как «Вниз», «Фокус на страницу вверх» и «В конец». Полный список команд, доступных в классе ComponentCommands, см. здесь.
  • Класс EditingCommands содержит такие команды, как «Выровнять по центру», «Возврат» и «Удалить». Полный список команд, доступных в классе EditingCommands, см. здесь.

С помощью встроенных команд можно реализовать довольно сложную функциональность без процедурного кода. Например, посмотрим на один из проектов, входящих в состав общего решения вверху данной статьи. В этом небольшом проекте Part3_Using_Built_In_Commands используются встроенные команды класса EditingCommands для создания простого текстового редактора с функциями вырезания, вставки, копирования, отмены и повторения действий. Ниже приведен код, весь написанный только на XAML.

<Window x:Class="Part3_Using_Built_In_Commands.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="Simple use of Built In ApplicationCommands" Height="500" Width="500"

    ResizeMode="NoResize"

    WindowStartupLocation="CenterScreen">

    <StackPanel Orientation="Vertical" Width="auto">

        <StackPanel Orientation="Horizontal" Background="Gainsboro" Margin="10" Height="40">

            <Button Command="ApplicationCommands.Cut" CommandTarget="{Binding ElementName=textBox}"

        Margin="5,5,5,5" Content ="ApplicationCommands.Cut"/>

            <Button Command="Copy" CommandTarget="{Binding ElementName=textBox}"

        Margin="5,5,5,5" Content="ApplicationCommands.Copy"/>

            <Button Command="Paste" CommandTarget="{Binding ElementName=textBox}"

        Margin="5,5,5,5" Content="ApplicationCommands.Paste"/>

            <Button Command="Undo" CommandTarget="{Binding ElementName=textBox}"

        Margin="5,5,5,5" Content="ApplicationCommands.Undo"/>

            <Button Command="Redo" CommandTarget="{Binding ElementName=textBox}"

        Margin="5,5,5,5" Content="ApplicationCommands.Redo"/>

        </StackPanel>

        <TextBlock HorizontalAlignment="Left" Margin="5,5,5,5"

    Text="This window demonstrates built in commands (standard ones),

    with no procedual code at all......that's pretty neat I think.

    Type into the text box and use the buttons provided to see

    what it does" TextWrapping="Wrap" Height="auto"/>

        <Label Content="Type in the textbox, maybe try selecting some text...

    Watch the buttons become enabled"/>

        <TextBox x:Name="textBox" HorizontalAlignment="Left" Margin="5,5,5,5" MaxLines="60" Height="300" Width="470"

    Background="#FFF1FFB2"/>

    </StackPanel>

</Window>

Как видите, все, что нужно сделать с кнопкой, это задать команду, например команду «Вырезать». И этого достаточно, чтобы получить функциональность вырезания в элементе, в котором находится фокус. Это небольшое демонстрационное приложение выглядит следующим образом:

Круто? Думаю, да. А если нужно создать собственные команды? Это можно сделать следующим образом:

  • объявить команду RoutedCommand;
  • создать привязку CommandBinding, для которой используются приемники RoutedCommand;
  • создать команду RoutedCommand для элемента управления для ввода.

Как уже говорилось, команды также строятся на основе стратегии маршрутизации, как мы уже видели на примере событий RoutedEvent. Это означает, что объявления команды и привязки команды и приемника команды можно отделить от элемента интерфейса пользователя, использующего такую команду. Это очень здорово, если вдуматься. Такая стратегия позволяет создавать привлекательные приложения с поддержкой обложек. Если интерфейс пользователя содержит ссылку на правильную команду и находится в окне с привязкой команды и приемниками команды, вся фоновая логика будет работать корректно. Я не буду подробно останавливаться на этом, чтобы не повторить статью Джоша Смита (Josh Smith) из отличной серии статей о приложении Podder. Не хочется красть его идеи.

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

Как видно из этого рисунка, используются три различных класса. Посмотрим на код для каждого из них.

Шаг 1. Объявление команды RoutedCommand. Первый шаг — это определение команды RoutedCommand. Это делается следующим образом:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Input; //for the

namespace Part3_Using_Our_Own_Commands

{

    /// <summary>

    /// Объявление новой команды <see cref="RoutedCommand">RoutedCommand</see>, которая

    /// используется в классе <see cref="Window1">Window1</see>, в котором

    /// объявляются привязки команды и приемник команды. Фактическая

    /// команда используется в объекте Button в пользовательском элементе управления <see cref="UserControlThatUsesCustomCommand">

    /// UserControlThatUsesCustomCommand</see>

    /// </summary>

    public class CustomCommands

    {

        #region Instance Fields

        public static readonly RoutedCommand simpleCommand =

        new RoutedCommand("simpleCommand",typeof(CustomCommands));

        #endregion

    }

}

И на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows.Input

''' <summary>

''' Объявление новой команды <see cref="RoutedCommand">RoutedCommand</see>, которая

''' используется в классе <seecref="Window1">Window1</see>, в котором

''' объявляются привязки команды и приемник команды. Фактическая

''' команда используется в объекте Buttonв пользовательском элементе управления <see cref="UserControlThatUsesCustomCommand">

''' UserControlThatUsesCustomCommand</see>

''' </summary>

Public Class CustomCommands

    Public Sub New()

    End Sub

#Region "Instance Fields"

    Public Shared ReadOnly simpleCommand

    As New RoutedCommand("simpleCommand",

    GetType(CustomCommands))

#End Region

End Class

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

Шаг 2. Создание привязки команды, для которой используются приемники перенаправленной команды. Класс CommandBinding обеспечивает обработку конкретной команды для данного элемента и определяет связь между командой, ее событиями и обработчиками, присоединенными к этому элементу. Как правило, это делается на XAML, хотя можно сделать и в коде. В этом примере я буду использовать только XAML. Посмотрим пример.

<Window.CommandBindings>

    <CommandBinding Command="{x:Static local:CustomCommands.simpleCommand}"

        CanExecute="simpleCommand_CanExecute"

        Executed="simpleCommand_Executed"

    />

</Window.CommandBindings>

Эта привязка CommandBinding определяет приемники команды (события), которые перенаправленная команда будет использовать для того, чтобы определить, разрешается ли выполнять команду и что делать при фактическом ее выполнении. Для этого используются два перенаправленных события CanExecute и Executed, которые в данном случае связаны с двумя методами в коде программной части. Посмотрим пример.

private void simpleCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

{

    e.CanExecute = !(string.IsNullOrEmpty(txtCantBeEmpty.Text));

}

private void simpleCommand_Executed(object sender, ExecutedRoutedEventArgs e)

{

    MessageBox.Show(txtCantBeEmpty.Text);

}

И на VB.NET.

        Private Sub simpleCommand_CanExecute(ByVal sender As Object, ByVal e As CanExecuteRoutedEventArgs)

            e.CanExecute = Not (String.IsNullOrEmpty(txtCantBeEmpty.Text))

        End Sub

        Private Sub simpleCommand_Executed(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs)

            MessageBox.Show(txtCantBeEmpty.Text)

        End Sub

Этих двух событий достаточно, чтобы включить и отключить элемент интерфейса пользователя, который использует перенаправленную команду. Перейдем к последнему шагу — использованию перенаправленной команды в некотором элементе интерфейса пользователя. Как показано на предыдущем рисунке, я использую пользовательский элемент управления UserControlThatUsesCustomCommand, размещенный в окне Window1. Посмотрим на код.

<UserControl x:Class="Part3_Using_Our_Own_Commands.UserControlThatUsesCustomCommand"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:Part3_Using_Our_Own_Commands"

    Height="auto" Width="auto">

    <Button Margin="5,5,5,5" Command="{x:Static local:CustomCommands.simpleCommand}"

            Content="Click me if you can"/>

</UserControl>

Эта кнопка связывает перенаправленную команду simpleCommand с событием, но не с событием Click типа RoutedEvent. Итак, как это работает. Используется та же перенаправленная команда, для которой в окне Window1 настроена привязка CommandBindings. При нажатии кнопки вызывается метод simpleCommand_Executed(..) в элементе Window1. Аналогично, если текстовое поле txtCantBeEmpty пустое, кнопка будет отключена. Для этого используется метод simpleCommand_CanExecute(..) в элементе Window1.

Элементы автоматизации

Рассказывая о перенаправленных событиях, я говорил, что каждый элемент FrameworkElement предоставляет метод RaiseEvent(). Представим себе, что иногда возникает потребность порождать событие нажатия кнопки программным путем. Сейчас, как мы уже знаем, можно просто породить перенаправленное событие, например Button.Click. Раньше в WinFormsмы бы просто вызвали метод PerformClick() объекта Button.

Но теперь с кнопкой может быть связана перенаправленная команда и событие Click типа RoutedEvent может остаться без кода.

В таком случае, если вызвать метод RaiseEvent() объекта Button, он фактически не будет ничего делать с присоединенной перенаправленной командой. Так что нужно искать альтернативный подход. К счастью, в платформе .NET (хотя и не очень внятно) предусмотрен метод имитации метода Button.Click (аналогично методу PerformClick() объекта Buttonв WinForms). Для этого используются два пространства имен и одна ссылка на сборку.

Пространства имен

  • System.Windows.Automation.Peers
  • System.Windows.Automation.Provider

Сборка

  • UIAutomationProvider

Пространство имен System.Windows.Automation.Peers содержит множество элементов автоматизации, например элемент ButtonAutomationPeer, который можно использовать для имитации нажатия реальной кнопки. В блоге Джоша Смита есть отличная запись об этом. Не знаю, где он нашел достаточно информации для этого поста, ее не так много.

Я слегка изменил код Джоша, чтобы метод UIElementAutomationPeer.CreatePeerForElement возвращал общий объект AutomationPeer, тогда как Джош использовал ButtonAutomationPeer. В любом случае главное для вас — понять, что благодаря этому элементу автоматизации мы можем правильно моделировать нажатие кнопки. Так что независимо от того, используется для нажатия в кнопке перенаправленная команда или перенаправленное событие, мы получим нужный результат. Использование этого кода будет эквивалентно нажатию реальной кнопки.

Ниже приведен фрагмент кода, иллюстрирующий программное нажатие кнопки с помощью элементов автоматизации.

AutomationPeer peer = UIElementAutomationPeer.CreatePeerForElement(start);

IInvokeProvider invokeProv = peer.GetPattern(PatternInterface.Invoke)

              as IInvokeProvider;

invokeProv.Invoke();

Я уже говорил, что все это немного невразумительно. Например, для чего нужен этот метод GetPattern. Повторюсь: каждый из множества элементов автоматизации связан с различными элементами интерфейса пользователя, поэтому перечисление PatternInterface содержит значения, используемые для таких элементов. Например, значение PatternInterface.Scroll, скорее всего, будет использоваться не для элемента Button, а для некоторого прокручиваемого элемента управления. На рисунке ниже представлены все возможные значения перечисления PatternInterface.

А на следующем рисунке показаны некоторые доступные элементы автоматизации. Я настоятельно рекомендую исследовать эту тему более подробно, поскольку все это довольно интересно.

Демонстрационные приложения

О них уже говорилось выше, поэтому повторим кратко. В прилагаемом решении содержатся четыре демонстрационных приложения. Я просто подумал, что стоит остановиться на каждом чуть подробнее, чтобы вы могли поработать с ними самостоятельно.

  • Part3_RoutedEventViewer: простое приложение для просмотра всплывающих и нисходящих событий.
  • Part3_RoutedEventsExample: простое приложение, использующее пользовательские события RoutedEvent со стандартными и пользовательскими аргументами RoutedEventArgs.
  • Part3_Using_Built_In_Commands: простой текстовый редактор для демонстрации использования встроенных команд ApplicationCommands.
  • Part3_Using_Our_Own_Commands: очень простое приложение, демонстрирующее использование пользовательских перенаправленных команд.

Ссылки

С уважением,

Саша Барбер.