Руководство для начинающих (часть 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.
Достаточно часто одно логическое событие представляется двумя фактическими: нисходящим и всплывающим. Чтобы определить, как эти события были созданы, используется соглашение об именовании. Нисходящие события обычно имеют вид 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), входящий в состав общего решения вверху этой статьи. В этом проекте рассматриваются два вопроса:
При работе это приложение выглядит следующим образом: В этом приложении в окне Window1 размещен один пользовательский элемент управления UserControl. Он порождает два события, на которые подписан элемент Window1. Мы уже обсуждали различные способы подписки на события RoutedEvent, так что я не буду останавливаться на этом, а расскажу лучше, как порождать собственное событие RoutedEvent. // Перенаправление фактического события 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. Впрочем, мы не всегда будем его использовать. Но об этом позже. // Перенаправление фактического события 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 На этом мы закончим краткое знакомство с перенаправленными событиями. Надеюсь, вы получили некоторое представление о том, как они работают. Перенаправленные команды
С помощью встроенных команд можно реализовать довольно сложную функциональность без процедурного кода. Например, посмотрим на один из проектов, входящих в состав общего решения вверху данной статьи. В этом небольшом проекте 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> Как видите, все, что нужно сделать с кнопкой, это задать команду, например команду «Вырезать». И этого достаточно, чтобы получить функциональность вырезания в элементе, в котором находится фокус. Это небольшое демонстрационное приложение выглядит следующим образом: Круто? Думаю, да. А если нужно создать собственные команды? Это можно сделать следующим образом:
Как уже говорилось, команды также строятся на основе стратегии маршрутизации, как мы уже видели на примере событий 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 содержит множество элементов автоматизации, например элемент 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. А на следующем рисунке показаны некоторые доступные элементы автоматизации. Я настоятельно рекомендую исследовать эту тему более подробно, поскольку все это довольно интересно. Демонстрационные приложения О них уже говорилось выше, поэтому повторим кратко. В прилагаемом решении содержатся четыре демонстрационных приложения. Я просто подумал, что стоит остановиться на каждом чуть подробнее, чтобы вы могли поработать с ними самостоятельно.
Ссылки
С уважением, Саша Барбер. |