Опубликован: 05.08.2010 | Уровень: специалист | Доступ: платный
Самостоятельная работа 12:

События и команды в WPF

Обработчики уровня класса

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

Для одного события можно создать несколько обработчиков уровня класса, по одному для каждого дочернего типа. Регистрация обработчиков должна выполняться только в коде, а именно в статическом конструкторе класса окна. Статический конструктор отличается тем, что он не должен иметь перегрузок, не принимает аргументы (конструктор по умолчанию) и вызывается сразу, как только объект-тип будет загружен в память (не дожидаясь создания хотя бы одного объекта-экземпляра).

Посмотрим это на простом примере, нам важен сам код регистрации обработчика уровня класса.

  • Добавьте в класс окна Window1 следующий код (теперь листинг файла Window1.xaml.cs приводится полностью)
using System;
using System.Collections.Generic;
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 UserEvents
{
    public partial class Window1 : Window
    {
        /*
        static Window1()
        {
            // Добавить владельца события ?????
            MyButton.TapEvent.AddOwner(typeof(FrameworkElement));
        }
        */
    
        // Статический конструктор (без аргументов и перегрузок)
        static Window1()
        {
            // Зарегистрировать обработчик уровня любого класса дерева, который 
            // сработает перед всеми обработчиками экземпляра этого класса
            EventManager.RegisterClassHandler(
                typeof(Window1), 
                MyButton.TapEvent,
                new RoutedEventHandler(SuperMethod1));
            EventManager.RegisterClassHandler(
                typeof(System.Windows.Controls.Primitives.UniformGrid), 
                MyButton.TapEvent,
                new RoutedEventHandler(SuperMethod2));
        }
    
        // Обработчик уровня класса
        static void SuperMethod1(object sender, RoutedEventArgs e)
        {
            String typeName = sender.GetType().Name;
            System.Diagnostics.Debug.WriteLine(
                String.Format("{0})  {1}: Суперобработчик события Tab",
                ++MyButton.count, typeName));
        }
    
        // Обработчик уровня класса
        static void SuperMethod2(object sender, RoutedEventArgs e)
        {
            String typeName = sender.GetType().Name;
            System.Diagnostics.Debug.WriteLine(
                String.Format("{0})  {1}: Суперобработчик события Tab",
                ++MyButton.count, typeName));
        }
    
        // Конструктор экземпляра
        public Window1()
        {
            InitializeComponent();
    
            // Динамический способ присоединения обработчиков
            nWindow1.AddHandler(MyButton.TapEvent,
                new RoutedEventHandler(this.nWindow1_Tap));
            nDockPanel.AddHandler(MyButton.TapEvent,
                new RoutedEventHandler(this.nDockPanel_Tap));
        }
    
        private void MyButton_Tap(object sender, RoutedEventArgs e)
        {
            this.ShowTap(sender, e);
        }
    
        private void UniformGrid_Tap(object sender, RoutedEventArgs e)
        {
            this.ShowTap(sender, e);
        }
    
        private void Grid_Tap(object sender, RoutedEventArgs e)
        {
            this.ShowTap(sender, e);
        }
    
        private void nDockPanel_Tap(object sender, RoutedEventArgs e)
        {
            this.ShowTap(sender, e);
        }
    
        private void nWindow1_Tap(object sender, RoutedEventArgs e)
        {
            this.ShowTap(sender, e);
        }
    
        void ShowTap(object obj, RoutedEventArgs args)
        {
            if (MyButton.count == 0)
            {
                System.Diagnostics.Debug.WriteLine(
                    String.Format("\n\t Стратегия маршрутизации: {0}",
                    args.RoutedEvent.RoutingStrategy));
            }
    
            String typeName = obj.GetType().Name;
            System.Diagnostics.Debug.WriteLine(
                String.Format("{0})  {1}: Наблюдаю событие Tap", 
                ++MyButton.count, typeName));
        }
    }
}

Закомментированный код оставлен для размышлений по поводу метода AddOwner(). Как и где его применять, я так и не смог разобраться по MSDN.

  • Запустите приложение, результат будет таким (для стратегии Bubble )
Результат вывода с зарегистрированным обработчиком уровня класса
    Стратегия маршрутизации: Bubble
  1. MyButton: Наблюдаю событие Tap
  2. UniformGrid: Суперобработчик события Tab
  3. UniformGrid: Наблюдаю событие Tap
  4. Grid: Наблюдаю событие Tap
  5. DockPanel: Наблюдаю событие Tap
  6. Window1: Суперобработчик события Tab
  7. Window1: Наблюдаю событие Tap

Добавление информации в объект аргумента события

Объект RoutedEventArgs, который мы передали событию при возбуждении в перекрытом методе OnClick() несет в себе только ту информацию, которую предусмотрели разработчики библиотеки. Мы можем добавить свою информацию к нашему события, если расширим класс RoutedEventArgs.

Поскольку стандартный делегат

public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)

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

  • Добавьте в самый конец файла MyButton.cs после всего существующего кода следующее расширение библиотечного класса RoutedEventArgs объекта-аргумента события
namespace UserEvents
{
    // Расширяем класс аргументов
    class MyEventArgs : RoutedEventArgs
    {
        // Соблюдаем сигнатуру родителя
        public MyEventArgs(RoutedEvent routedEvent)
            : base(routedEvent)
        {
        }
    
        string message;// Базовое поле свойства
    
        public string Message // Свойство чтения/записи
        {
            get { return message; }
            set { message = value; }
        }
    }
}

Здесь в расширение мы скромно добавили только одно свойство вместе с базовым полем хранения данных. Но при необходимости можно добавить любое количество членов расширения.

  • Модифицируйте перекрытый метод OnClick() так, чтобы в возбуждаемое событие передавался объект-расширение с начальным значением нового свойства
// Перекроем метод щелчка базовой кнопки для возбуждения события
        protected override void OnClick()
        {
            count = 0;          // Сбрасываем счетчик
            //Console.Clear();    // Очищаем консоль
            //base.RaiseEvent(new RoutedEventArgs(MyButton.TapEvent));// Возбуждаем событие
    
            // Добавляем информацию и отсылаем
            MyEventArgs myEventArgs = new MyEventArgs(MyButton.TapEvent);
            myEventArgs.Message = "Привет студентам от Снеткова!";
            base.RaiseEvent(myEventArgs);// Возбуждаем событие
        }
  • Модифицируйте в файле Window1.xaml.cs вспомогательную функцию ShowTap() следующим образом
void ShowTap(object obj, RoutedEventArgs args)
        {
            if (MyButton.count == 0)
            {
                System.Diagnostics.Debug.WriteLine(
                    String.Format("\n\t Стратегия маршрутизации: {0}",
                    args.RoutedEvent.RoutingStrategy));
            }
    
            String typeName = obj.GetType().Name;
            /*
            System.Diagnostics.Debug.WriteLine(
                String.Format("{0}) {1}: Наблюдаю событие Tap", 
                ++MyButton.count, typeName));
            */
    
            // Повышаем полномочия ссылки и извлекаем сообщение
            MyEventArgs e = args as MyEventArgs;
            string message = e.Message;
    
            // Выводим информацию
            System.Diagnostics.Debug.WriteLine(
                String.Format("{0})  {1}: Наблюдаю событие Tap.\n"
                + "\tПолучил сообщение: {2}",
                ++MyButton.count, typeName, message));
    
            // Изменяем информацию в объекте-аргументе 
            e.Message = String.Format("Привет студентам от {0} и {1}!",
                typeName, args.RoutedEvent.OwnerType.Name);
        }

В каждом обработчике мы намеренно меняем значение свойства Message объекта-аргумента события на новое, чтобы подчеркнуть такую возможность. Один и тот же объект аргумента движется вместе с событием и последовательно попадает в подписанные на событие обработчики элементов, где и может быть изменен. Для получения доступа к свойству Message нашего объекта-аргумента предварительно нужно повысить полномочия ссылки базового типа RoutedEventArgs. Вспомним также, что в любом обработчике мы можем остановить дальнейшее продвижение события, присвоив свойству объекта-аргумента args. Handled значение true.

  • Запустите приложение, должен получиться такой результат
Стратегия маршрутизации: Bubble
1)  MyButton: Наблюдаю событие Tap.
    Получил сообщение: Привет студентам от Снеткова!
2)  UniformGrid: Суперобработчик события Tab
3)  UniformGrid: Наблюдаю событие Tap.
    Получил сообщение: Привет студентам от MyButton и MyButton!
4)  Grid: Наблюдаю событие Tap.
    Получил сообщение: Привет студентам от UniformGrid и MyButton!
5)  DockPanel: Наблюдаю событие Tap.
    Получил сообщение: Привет студентам от Grid и MyButton!
6)  Window1: Суперобработчик события Tab
7)  Window1: Наблюдаю событие Tap.
    Получил сообщение: Привет студентам от DockPanel и MyButton!

Задание для Упражнения 3

  1. Перенаправьте вывод обработчиков в консольное окно приложения

Модель команд

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

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

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

На самом деле всплывают не сами команды, а генерируемые ими маршрутизованные события. Существует всего несколько специальных событий для поддержки команд. В классе CommandBinding определены события PreviewCanExecute и CanExecute типа CanExecuteRoutedEventHandler, а также события PreviewExecuted и Executed типа ExecutedRoutedEventHandler. В интерфейсе ICommand, наследуемом классом RoutedCommand, определено еще событие CanExecuteChanged. Эти события проходят по дереву элементов до тех пор, пока не будет найден прослушивающий элемент с привязкой CommandBinding для конкретной команды. Вот и все события команд.

Команда включает в себя несколько ингредиентов:

  1. Объект команды - представляет определенную задачу и следит за тем, когда она может быть выполнена. Самого кода, выполняющего задачу, команда не содержит.
  2. Источник команды - элемент управления, жест (комбинация клавиш, клавиш и мыши, функциональные клавиши) или специализированный код, при активизации которого запускается команда. Источников одной команды может быть несколько: меню, контекстное меню, кнопка панели инструментов, жесты пользователя
  3. Обработчик команды - метод, который выполняется при запуске команды
  4. Прослушивающий элемент привязки обработчиков - объект, занимающийся отслеживанием маршрутизованных событий команды и переадресующий ее выполнение соответствующему обработчику
  5. Целевой объект команды - необязательный элемент, который ждет обслуживания своей командой и на котором команда выполняется (например, выделенный текст, который нужно вырезать или скопировать в буфер обмена). Целевым объектом может служить элемент привязки обработчиков команды при прямом вызове, в котором WPF начинает искать привязку.

Объекты команд

Объекты команд порождаются классом RoutedCommand, который единственный в WPF реализует (и наследует, конечно) интерфейс ICommand, объявленный следующим образом

public interface ICommand
    {
        event EventHandler CanExecuteChanged;
        bool CanExecute(object parameter);
        void Execute(object parameter);
    }

Реализующий класс RoutedCommand расширяет сигнатуру объявленных в ICommand методов до второго аргумента target

public bool CanExecute(object parameter, System.Windows.IInputElement target)
public void Execute(object parameter, System.Windows.IInputElement target)

где - аргумент parameter содержит сведения о команде, а аргумент target - сведения о целевом элементе команды.

При возбуждении элемента-источника, к которому прикреплена команда (объект RoutedCommand ), вызывается метод CanExecute(). Этот метод возбуждает туннельное событие PreviewCanExecute, которое начинает спускаться от корня визуального дерева элементов к источнику команды и собирает информацию с целевых элементов о возможности выполнения команды. Получив результаты опроса от события PreviewCanExecute метод CanExecute() возбуждает парное восходящее событие CanExecute с заданием оповестить целевые элементы принять состояние в зависимости от полученной информации от события PreviewCanExecute, а сам возвращает флаг возможности или невозможности выполнения команды.

Если метод CanExecute() вернул значение true, значит исполнение команды возможно и следом вызывается метод Execute(), который последовательно возбуждает парные события PreviewExecuted и Executed как указание выполнить команду во всех целевых элементах прикрепленными к ним обработчиками. Событие отмены выполнения CanExecuteChanged вызывается, если диспетчер команд, в котором централизованно выполняются командные операции, обнаруживает в источнике команды изменения, которые могут сделать недействительной команду, вызванную, но еще не выполненную в целевом элементе.

Библиотечные классы команд

Библиотека .NET Framework имеет множество готовых команд, наиболее часто встречающихся в приложениях. Команды представлены в пространствах имен System.Windows. Input и System.Windows. Documents следующими статическими классами:

  • ApplicationCommands
  • ComponentCommands
  • MediaCommands
  • NavigationCommands
  • EditingCommands

В каждом из приведенных классов команда объявлена как статическое свойство только для чтения типа RoutedUICommand (или RoutedCommand ), следовательно при использовании команды создавать экземпляр класса команды не нужно. Команды имеют имена, ассоциируемые с выполняемой задачей, например, Cut, Copy, New и т.д.

Присоединение команды к источнику

Чтобы команда работала, ее нужно присоединить к свойству Command источника команды. Источников, к которым присоединена одна и та же команда, может быть несколько. Элементы, к которым можно присоединить команду, должны наследовать интерфейс ICommandSource. Только такие элементы будут иметь свойство Command. Этих элементов в WPF немного и все они показаны на рисунке


Интерфейс ICommandSource реализует свои свойства в ближайших наследниках ButtonBase, Hyperlink, InputBinding, MenuItem по общему синтаксису:

public System.Windows.Input.ICommand Command { get; set; }
    public object CommandParameter { get; set; }
    public System.Windows.IInputElement CommandTarget { get; set; }

В источнике для присоединения команды достаточно присвоить значение только ссылке Command на объект команды (на экземпляр RoutedCommand или RoutedUICommand ). Остальные свойства являются необязательными и служат для уточнения команды:

  • CommandParameter - тип данных, определяемый пользователем, который используется для передачи информации обработчикам, реализующим команду
  • CommandTarget - идентифицирует элемент, в котором должна выполняться данная команда. Если значение свойства CommandTarget не задано, в качестве цели команды используется элемент, в котором установлен фокус ввода

Обратите внимание, что два наследуемые источниками команд свойства-ссылки Command и CommandTarget имеют типы интерфейсов, что позволяет адресовать любые команды и целевые элементы, наследующие эти интерфейсы.

Вот, например, как можно присоединить команду Paste к элементу меню в разметке

<StackPanel>
  <Menu Header="Edit">
    <MenuItem Command="ApplicationCommands.Paste" />
  </Menu>
  <TextBox />
</StackPanel>

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

<StackPanel>
  <Menu Header="Edit">
    <MenuItem Command="Paste" />
  </Menu>
  <TextBox />
</StackPanel>

Теперь, если объект TextBox имеет фокус ввода и буфер обмена содержит текст, то при выборе пользователем этого пункта меню текст из буфера будет вставлен в элемент TextBox. Обратите внимание, что объект MenuItem не устанавливает свойство Header для отображения в нем названия команды Paste. Если свойство для текста названия команды в источнике опущено, то источник способен сам извлекать эту информацию из объекта присоединенной команды.

А вот альтернативный вариант присоединения команды Paste к элементу меню в процедурном коде

// Создание объектов пользовательского интерфейса
StackPanel mainStackPanel = new StackPanel();
TextBox pasteTextBox = new TextBox();
Menu stackPanelMenu = new Menu();
MenuItem pasteMenuItem = new MenuItem();
	
// Добавление объектов в коллекции панели и меню
stackPanelMenu.Items.Add(pasteMenuItem);
mainStackPanel.Children.Add(stackPanelMenu);
mainStackPanel.Children.Add(pasteTextBox);
	
// Присоединение библиотечной команды Paste к источнику команд
pasteMenuItem.Command = ApplicationCommands.Paste;

Аналогичный пример можно привести для присоединения команды к контекстному меню.

<StackPanel>
  <StackPanel.ContextMenu>
    <ContextMenu>
      <MenuItem Command="ApplicationCommands.Paste" />
    </ContextMenu>
  </StackPanel.ContextMenu>
</StackPanel>
// Создание объектов пользовательского интерфейса
StackPanel cmdSourcePanel = new StackPanel();
ContextMenu cmdSourceContextMenu = new ContextMenu();
MenuItem cmdSourceMenuItem = new MenuItem();
	
// Добавление ContextMenu в StackPanel
cmdSourcePanel.ContextMenu = cmdSourceContextMenu;
cmdSourcePanel.ContextMenu.Items.Add(cmdSourceMenuItem);
	
// Присоединение библиотечной команды Paste к источнику команд
cmdSourceMenuItem.Command = ApplicationCommands.Paste;

Привязка команды к прослушивающему элементу

Как только мы присоединим команду к элементу-источнику, он сразу же становится недоступен, как будто бы его свойство IsEnabled приобрело значение false. Это происходит потому, что мы не привязали команду к прослушивающему ее элементу. Как же это сделать?

Любой элемент управления WPF наследует от класса System.Windows. UIElement, который имеет свойство-коллекцию CommandBindings типа CommandBindingCollection, предназначенную для того, чтобы можно было сделать этот элемент прослушивающим команды. В эту коллекцию помещаются специальные объекты привязки CommandBinding, у которых имеется свойство Command и событие Executed. Мы создаем объекты CommandBinding, присоединяем к их свойству Command нужные объекты команд, а к событию Executed - соответствующие обработчики. Затем эти настроенные объекты добавляем в коллекцию CommandBindings элемента. Так элемент становится прослушивающим.

Как уже говорилось, когда источник команды возбуждает присоединенный к нему объект команды, тот последовательно выполняет методы CanExecute() и Execute() интерфейса ICommand. Эти методы генерируют командные события, которые движутся по дереву элементов, проверяя их коллекции CommandBindings с целью обнаружить объект CommandBinding, соответствующий возбужденной команде. Как только такой объект привязки будет обнаружен в прослушивающем команду элементе, немедленно выполнится подписанный на событие Executed обработчик.

В качестве прослушивающего можно сделать любой элемент логического дерева. Но для получения наибольшей гибкости привязки команд рекомендуется добавлять в корневой элемент - окно (или контейнер элемента Page ).

Упражнение 4. Привязка команд в разметке

Построим простое приложение, в котором присоединим библиотечные команды ApplicationCommands. Open и ApplicationCommands. Save, каждую - к двум элементам-источникам: кнопке и главному меню окна. Дополнительным источником команд будет уже встроенная в них клавиатурная комбинация Ctrl+O и Ctrl+S. В качестве прослушивающего элемента выберем окно. В коллекцию CommandBindings окна добавим два объекта привязки CommandBinding, связывающие между собой команду и обработчик. Привязку выполним в разметке XAML.

  • Добавьте к решению командой оболочки File/Add/New Project новый проект с именем BindingCommandsXAML и назначьте его стартовым в панели Solution Explorer командой контекстного меню Set as StartUp Project

  • Заполните файл разметки Window1.xaml следующим кодом
<Window x:Class="BindingCommandsXAML.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1: Декларативная привязка" Height="300" Width="300"
    Background="#FFD4D0C8"
    WindowStartupLocation="CenterScreen"
    >
    <StackPanel Margin="5">
        <Menu>
            <MenuItem Header="_File">
                <MenuItem Command="ApplicationCommands.Open" />
                <MenuItem Command="ApplicationCommands.Save" />
            </MenuItem>
        </Menu>
        <Button Margin="5" Padding="5" Focusable="False"
                Command="ApplicationCommands.Open" 
                Content="Open" 
                />
        <Button Margin="5" Padding="5" Focusable="False"
                Command="ApplicationCommands.Save" 
                Content="Save" 
                />
    </StackPanel>
</Window>

Мы присоединили к интерфейсным элементам объекты команд и сделали их таким образом источниками этих команд.

  • Запустите проект и убедитесь, что источники команд не получают фокус ввода и являются недоступными

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

  • Добавьте в разметку объекты привязки следующим образом
<Window x:Class="BindingCommandsXAML.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1: Декларативная привязка" Height="300" Width="300"
    Background="#FFD4D0C8"
    WindowStartupLocation="CenterScreen"
    >
    <Window.CommandBindings>
        <CommandBinding 
            Command="ApplicationCommands.Open" 
            Executed="OpenCommand_Executed">
        </CommandBinding>
        <CommandBinding 
            Command="ApplicationCommands.Save" 
            Executed="SaveCommand_Executed">
        </CommandBinding>
    </Window.CommandBindings>
    <StackPanel Margin="5">
        <Menu>
            <MenuItem Header="_File">
                <MenuItem Command="ApplicationCommands.Open" />
                <MenuItem Command="ApplicationCommands.Save" />
            </MenuItem>
        </Menu>
        <Button Margin="5" Padding="5" Focusable="False"
                Command="ApplicationCommands.Open" 
                Content="Open" 
                />
        <Button Margin="5" Padding="5" Focusable="False"
                Command="ApplicationCommands.Save" 
                Content="Save" 
                />
    </StackPanel>
</Window>
  • Вызовите контекстное меню для записей привязки обработчиков и командой Navigate to Event Handler создайте заготовки обработчиков в файле процедурного кода
  • Запустите приложение и убедитесь, что источники команд стали доступными, хотя обработчики еще пустые
  • В меню оболочки командой Project/Add Reference добавьте к текущему проекту ссылку на библиотечную сборку System.Windows.Forms.dll

Мы уже обсуждали ранее, что нельзя напрямую размещать в окне WPF интерфейсные элементы Windows Forms, для этого предназначен переходной контейнер <WindowsFormsHost>. Но в нашем случае мы будем открывать самостоятельные диалоговые окна Windows Forms и не собираемся смешивать в одном окне интерфейсные элементы из разных технологий.

  • Дополните в процедурном файле Window1.xaml.cs класс Window1 следующим кодом
public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    
        // Обработчик команды Open
        System.Windows.Forms.OpenFileDialog openFileDialog = null;
        private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            if (openFileDialog == null)
                openFileDialog = new System.Windows.Forms.OpenFileDialog();
            
            openFileDialog.ShowDialog();
            this.Focus();
        }
    
        // Обработчик команды Save
        System.Windows.Forms.SaveFileDialog saveFileDialog = null;
        private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            if (saveFileDialog == null)
                saveFileDialog = new System.Windows.Forms.SaveFileDialog();
    
            saveFileDialog.ShowDialog();
            this.Focus();
        }
    }
  • Запустите приложение и испытайте возможность запуска каждой команды через любой из трех источников: кнопку, меню или клавиатурную комбинацию

Рекомендуется при совместном использовании библиотек WPF и Windows Forms не подключать пространство имен System.Windows.Forms в инструкции using, а использовать полное имя требуемых классов Windows Forms, что мы и сделали. Это нужно для того, чтобы избежать конфликта имен, поскольку в библиотеках разных технологий встречаются типы с одинаковыми именами, например, Color, Pen, Brush.

Перекрытие функций диспетчеризации событий

  • Для тренировки, перекройте в классе Window1 функцию диспетчеризации OnKeyDown() клавиатурного события KeyDown. Код начните вводить с ключевого слова override, чтобы воспользоваться услугами подсказчика кода IntelliSense
  • Заполните созданную оболочкой заготовку кодом, обеспечивающим закрытие окна приложения по нажатию клавиши Esc
// Выход по клавише Esc с предупреждением 
        protected override void OnKeyDown(KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Escape:
                    MessageBoxResult result =
                        MessageBox.Show("Закрыть приложение ?", "",
                        MessageBoxButton.OKCancel, 
                        MessageBoxImage.Question);
                    if (result == MessageBoxResult.OK)
                        this.Close();
                    break;
            }
        }
  • Запустите приложение, раскройте мышью меню File и нажимайте на клавишу Esc

Мы видим, что клавиша Esc действует только на те элементы, которым на момент нажатия передан фокус ввода. Но если бы мы перекрыли функцию OnPreviewKeyDown(), то окно приложения закрывалось бы сразу. Для проверки этого можно в коде просто поменять имя перекрытой функции с OnKeyDown на OnPreviewKeyDown, так как обе функции имеют одинаковую сигнатуру.

Для сравнения можно привести код использования равнозначного по функциональности класса диалогового окна из библиотеки System.Windows.Forms.dll

protected override void OnKeyDown(KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Escape:
                    System.Windows.Forms.DialogResult result =
                        System.Windows.Forms.MessageBox.Show("Закрыть приложение ?", "",
                        System.Windows.Forms.MessageBoxButtons.OKCancel, 
                        System.Windows.Forms.MessageBoxIcon.Question);
                    if (result == System.Windows.Forms.DialogResult.OK)
                        this.Close();
                    break;
            }
        }

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

Названия кнопок в разметке мы жестко подписали, но существует другой способ, когда подпись можно извлечь из самой команды (если она есть). Если объект команды произведен от класса RoutedCommand, то там нет свойства Text, которое можно было бы использовать. Но если команда имеет тип RoutedUICommand, то этот класс добавляет такое свойство и оно может иметь значение.

  • Перепишите элементы кнопок в разметке файла Window1.xaml следующим образом
<Button Margin="5" Padding="5" Focusable="False"
                Command="ApplicationCommands.Open" 
                Content="{x:Static ApplicationCommands.Open}" 
                />
        <Button Margin="5" Padding="5" Focusable="False"
                Command="ApplicationCommands.Save" 
                Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}" 
                />
  • Запустите приложение - сейчас надписи на кнопках должны выглядеть так


Алексей Бабушкин
Алексей Бабушкин

При выполнении в лабораторной работе упражнения №1 , а именно при выполнении нижеследующего кода:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

using Microsoft.Xna.Framework.Graphics;

   

namespace Application1

{

    public partial class MainForm : Form

    {

        // Объявим поле графического устройства для видимости в методах

        GraphicsDevice device;

   

        public MainForm()

        {

            InitializeComponent();

   

            // Подпишемся на событие Load формы

            this.Load += new EventHandler(MainForm_Load);

   

            // Попишемся на событие FormClosed формы

            this.FormClosed += new FormClosedEventHandler(MainForm_FormClosed);

        }

   

        void MainForm_FormClosed(object sender, FormClosedEventArgs e)

        {

            //  Удаляем (освобождаем) устройство

            device.Dispose();

            // На всякий случай присваиваем ссылке на устройство значение null

            device = null;       

        }

   

        void MainForm_Load(object sender, EventArgs e)

        {

            // Создаем объект представления для настройки графического устройства

            PresentationParameters presentParams = new PresentationParameters();

            // Настраиваем объект представления через его свойства

            presentParams.IsFullScreen = false; // Включаем оконный режим

            presentParams.BackBufferCount = 1;  // Включаем задний буфер

                                                // для двойной буферизации

            // Переключение переднего и заднего буферов

            // должно осуществляться с максимальной эффективностью

            presentParams.SwapEffect = SwapEffect.Discard;

            // Устанавливаем размеры заднего буфера по клиентской области окна формы

            presentParams.BackBufferWidth = this.ClientSize.Width;

            presentParams.BackBufferHeight = this.ClientSize.Height;

   

            // Создадим графическое устройство с заданными настройками

            device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware,

                this.Handle, presentParams);

        }

   

        protected override void OnPaint(PaintEventArgs e)

        {

            device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);

   

            base.OnPaint(e);

        }

    }

}

Выбрасывается исключение:

Невозможно загрузить файл или сборку "Microsoft.Xna.Framework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d" или один из зависимых от них компонентов. Не удается найти указанный файл.

Делаю все пунктуально. В чем может быть проблема?

Dmitriy Ivanchenko
Dmitriy Ivanchenko
Украина, Кировоград, Виктория-П, 2011
Татьяна Ковалюк
Татьяна Ковалюк
Украина, Киев, Киевский политехнический институт, 1974