Опубликован: 05.08.2010 | Доступ: свободный | Студентов: 2010 / 47 | Оценка: 4.50 / 4.40 | Длительность: 60:26:00
Самостоятельная работа 12:

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

Создание нового проекта из копии существующего

  • Зайдите в проводнике Windows Explorer в каталог решения EventsAndCommands и скопируйте папку Notepad1
  • Переименуйте копию проекта в Notepad2
  • Зайдите внутрь папки Notepad2 и переименуйте файл проекта тоже в Notepad2. Здесь же удалите каталоги obj и bin, которые оболочка вновь создаст при запуске приложения
  • Запустите оболочку, выделите узел решения EventsAndCommands и командой File/Add/Existing Project зарегистрируйте в решении оболочки проект Notepad2
  • Не запуская проект, откройте на редактирование любой файл проекта Notepad2, вызовите жестом Ctrl+H окно замены с указанными на рисунке настройками и сделайте замену по всему проекту


Должно получиться 20 замен.

  • В панели Solution Explorer вызовите контекстное меню для узла проекта Notepad2 и командой Set as StartUp Project назначьте его стартовым
  • Запустите приложение Notepad2 и убедитесь, что все работает как в проекте Notepad1

Краткий анализ задачи

Теперь будем модифицировать этот проект, подгоняя его под использование механизма команд. Но прежде проведем ревизию библиотечных команд, возможно не все из них закрывают наши источники и часть команд придется создавать самим. За основу возьмем источники меню, поскольку там наиболее полно представлены задачи нашего приложения. Интересующие нас библиотечные команды находятся в классе ApplicationCommands пространства имен System.Windows.Input

Сопоставление задач приложения и библиотечных команд
Раздел меню Задача Библиотечная команда Встроенные жесты Наши жесты Регулировать доступность ?
File New ApplicationCommands.New Ctrl+N Ctrl+N  
Open ApplicationCommands.Open Ctrl+O Ctrl+O  
Save ApplicationCommands.Save Ctrl+S Ctrl+S Да
Save As ApplicationCommands.SaveAs      
Page Setup        
Print Preview ApplicationCommands.PrintPreview Ctrl+F2 Ctrl+F2  
Print ApplicationCommands.Print Ctrl+P Ctrl+P  
Exit        
Edit Undo ApplicationCommands.Undo Ctrl+Z Ctrl+Z Да
Redo ApplicationCommands.Redo Ctrl+Y Ctrl+Y Да
Cut ApplicationCommands.Cut Ctrl+X Ctrl+X Да
Copy ApplicationCommands.Copy Ctrl+C Ctrl+C Да
Paste ApplicationCommands.Paste Ctrl+V Ctrl+V Да
Delete ApplicationCommands.Delete Del Del Да
Find ApplicationCommands.Find Ctrl+F Ctrl+F  
Find Next     F3  
Replace ApplicationCommands.Replace Ctrl+H Ctrl+H Да
Go To     Ctrl+G  
Select All ApplicationCommands.SelectAll Ctrl+A Ctrl+A Да
Format Font        
Word Wrap     Ctrl+W  
Help About        

Глядя на таблицу, можно сделать следующие выводы для нашего приложения:

  1. Не все задачи имеют библиотечные команды и часть команд придется создать вручную (такие команды называются пользовательскими или настраиваемыми).
  2. Не для всех задач нужно регулировать доступность источников, поэтому эти задачи можно оставить как есть, ничего не меняя, поскольку они у нас работают нормально.
  3. Задачи, которые не имеют библиотечных команд, не требуют регулирования доступности, поэтому их можно оставить как есть и не создавать для них команды. Но мы все-таки их создадим, для тренировки.
  4. Встроенные жесты библиотечных команд и наши жесты совпадают, мы их так подгадали, поэтому, для тренировки, в задачах, к которым мы применим команды, наши жесты можно удалить (а они заменятся на встроенные)

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

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

План задач, которые предстоит реализовать командами
Где присоединить к источнику? Где привязать к окну? Задача Библиотечная команда Alias (псевдоним) Жесты Рег?
Разметка Разметка Save Save SaveCommand Ctrl+S Да
Разметка Разметка Page Setup   PageSetupCommand   Нет
Разметка Разметка Undo Undo UndoCommand Ctrl+Z Да
Разметка Код Redo Redo RedoCommand Ctrl+Y Да
Разметка Код Cut Cut CutCommand Ctrl+X Да
Разметка Код Copy Copy CopyCommand Ctrl+C Да
Код Разметка Paste Paste PasteCommand Ctrl+V Да
Код Разметка Delete Delete DeleteCommand Del Да
Код Разметка Find Next   FindNextCommand F3 Да
Код Код Replace Replace ReplaceCommand Ctrl+H Да
Код Код Go To   GoToCommand Ctrl+G Нет
Код Код Select All SelectAll SelectAllCommand Ctrl+A Да

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

Создание и привязка команд

  • Откройте файл Window1.xaml.cs текущего проекта Notepad2, найдите объявление поля modified и переименуйте его вновь в IsModified так
Было        bool modified = false;    // Флаг изменений содержимого
   
   Стало       bool IsModified = false;  // Флаг изменений содержимого
  • Откройте файл EnabledControls.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 Notepad2
{
    partial class Window1
    {
        // Вызов размещен в конструкторе класса
        void AdditionalHandlers()
        {
        }
    }
}
  • Внесите в файл EnabledControls.cs код определения и инициализации команд вместе со встроенными жестами
namespace Notepad2
{
    partial class Window1
    {
        // Объявляем и инициализируем поля команд 
        // Их обязательно нужно объявлять статическими, чтобы
        // размещались в объекте-типе и уже были созданы перед
        // созданием элементов, к которым присоединяются
        // Последний параметр означает жесты
        public static RoutedCommand SaveCommand = ApplicationCommands.Save;
        public static RoutedCommand PageSetupCommand = 
            new RoutedCommand("PageSetup", typeof(Window1), null);// Без жеста
        public static RoutedCommand UndoCommand = ApplicationCommands.Undo;
        public static RoutedCommand RedoCommand = ApplicationCommands.Redo;
        public static RoutedCommand CutCommand = ApplicationCommands.Cut;
        public static RoutedCommand CopyCommand = ApplicationCommands.Copy;
        public static RoutedCommand PasteCommand = ApplicationCommands.Paste;
        public static RoutedCommand DeleteCommand = ApplicationCommands.Delete;
        public static RoutedCommand FindNextCommand;// Определим в ст. конструкторе
        public static RoutedCommand ReplaceCommand = ApplicationCommands.Replace;
        public static RoutedCommand GoToCommand;// Определим в ст. конструкторе
        public static RoutedCommand SelectAllCommand = ApplicationCommands.SelectAll;
    
        // Вызов размещен в конструкторе класса
        void AdditionalHandlers()
        {
        }
    }
}

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

  • Добавьте в класс Window1 файла EnabledControls.cs статический конструктор со следующим кодом
namespace Notepad2
{
    partial class Window1
    {
        ..............................................
    
        // Статический конструктор
        static Window1()
        {
            // Определяем с добавлением жестов
            InputGestureCollection coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.F3, ModifierKeys.None, "F3"));
            FindNextCommand = new RoutedCommand("FindNext", typeof(Window1), coll);
            coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.G, ModifierKeys.Control, "Ctrl+G"));
            GoToCommand = new RoutedCommand("GoTo", typeof(Window1), coll);
        }
    
        // Вызов размещен в конструкторе класса
        void AdditionalHandlers()
        {
        }
    }
}

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

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

В соответствии с планом, присоединим команды к источникам, вначале в коде.

  • Добавьте в функцию AdditionalHandlers() файла EnabledControls.cs следующий код
void AdditionalHandlers()
        {
            Clipboard.Clear();// Временно, чтобы испытать начальное состояние
            // Присоединяем команды к источникам, жесты уже встроены в команды
            //SaveCommand       - присоединим в разметке
            //PageSetupCommand  - присоединим в разметке
            //UndoCommand       - присоединим в разметке
            //RedoCommand       - присоединим в разметке
            //CutCommand        - присоединим в разметке
            //CopyCommand       - присоединим в разметке
            btnPaste.Command = itemPaste.Command = contextPaste.Command = PasteCommand;
            btnDelete.Command = itemDelete.Command = contextDelete.Command = DeleteCommand;
            itemFindNext.Command = FindNextCommand;
            itemReplace.Command = ReplaceCommand;
            itemGoTo.Command = GoToCommand;
            itemSelectAll.Command = SelectAllCommand;
        }
  • Добавьте в открывающем дескрипторе <Window> файла Window1.xaml параметр отображения пространства имен класса процедурного кода на разметку, чтобы компилятор видел вставляемые в разметку команды. Имя отображения можно принять произвольно - выберите myCmd

Когда вы начнете вручную набирать запись xmlns:myCmd=, то после ввода знака присваивания IntelliSense выдаст подсказку, в которой нужно выбрать выделенную на снимке опцию списка


В результате будет догенерирована следующая запись

<Window x:Class="Notepad2.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:myCmd="clr-namespace:Notepad2"
        
    Title="Window1: Управление состоянием источников команд"
    Width="500" Height="375"
    MinWidth="500" MinHeight="375"
    WindowStartupLocation="CenterScreen"
    ResizeMode="CanResizeWithGrip"
    Loaded="Window_Loaded"
    Icon="Notepad.ico"
    Closing="Window_Closing"
    Activated="Window_Activated"
        >
    ..............................................
    
</Window>

Теперь отредактируем разметку 12 запланированных источников задач в соответствии с таблицей

12 задач
Где присоединить к источнику? Задача Alias (псевдоним)
Разметка Save SaveCommand
Разметка Page Setup PageSetupCommand
Разметка Undo UndoCommand
Разметка Redo RedoCommand
Разметка Cut CutCommand
Разметка Copy CopyCommand
  Paste PasteCommand
  Delete DeleteCommand
  Find Next FindNextCommand
  Replace ReplaceCommand
  Go To GoToCommand
  Select All SelectAllCommand
  • В файле Window1.xaml текущего проекта удалите в источниках для первых 6 задач таблицы тексты жестов и замените записи с событием Click на записи с присоединением команд
  • В файле Window1.xaml текущего проекта удалите в источниках для последних 6 задач таблицы тексты жестов и записи события Click. Для этих источников присоединение команд мы уже выполнили в процедурном коде

Правленный код разметки станет таким (для удобства файл Window1.xaml с новым содержимым приводится полностью)

<Window x:Class="Notepad2.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:myCmd="clr-namespace:Notepad2"
        
    Title="Window1: Управление состоянием источников команд"
    Width="500" Height="375"
    MinWidth="500" MinHeight="375"
    WindowStartupLocation="CenterScreen"
    ResizeMode="CanResizeWithGrip"
    Loaded="Window_Loaded"
    Icon="Notepad.ico"
    Closing="Window_Closing"
    Activated="Window_Activated"
        >
    
    <Window.Resources>
        <!-- File -->
        <Image x:Shared="False" x:Key="iconNew"  Source="Images/NewDocumentHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconOpen" Source="Images/OpenHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconSave" Source="Images/SaveHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetupHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreviewHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconPrint" Source="Images/PrintHS.png" Width="16" Height="16" />
        <!-- Edit -->
        <Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_UndoHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_RedoHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconCut" Source="Images/CutHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconCopy" Source="Images/CopyHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconPaste" Source="Images/PasteHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconDelete" Source="Images/DeleteHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconFind" Source="Images/FindHS.png" Width="16" Height="16" />
        <Image x:Shared="False" x:Key="iconFont" Source="Images/FontHS.png" Width="16" Height="16" />
    </Window.Resources>
    
    <DockPanel LastChildFill="True">
        <!-- Меню -->
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="_File">
                <!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
                <MenuItem Name="itemNew" Click="NewOnExecute" Header="_New" InputGestureText="Ctrl+N" 
                  Icon="{StaticResource iconNew}" />
                <MenuItem Name="itemOpen" Click="OpenOnExecute" Header="_Open..." InputGestureText="Ctrl+O" 
                  Icon="{StaticResource iconOpen}" />
                <MenuItem Name="itemSave" Command="myCmd:Window1.SaveCommand" Header="_Save" 
                  Icon="{StaticResource iconSave}" /> 
                <MenuItem Name="itemSaveAs" Click="SaveAsOnExecute" Header="Save _As..." />
                <Separator />
                <MenuItem Name="itemPageSetup" Command="myCmd:Window1.PageSetupCommand" Header="Page Set_up..." 
                  Icon="{StaticResource iconPageSetup}" />
                <MenuItem Name="itemPrintPreview" Click="PrintPreviewOnExecute" Header="P_rint Preview" 
           InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
                <MenuItem Name="itemPrint" Click="PrintOnExecute" Header="_Print..." 
        InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
                <Separator />
                <MenuItem Name="itemExit" Click="ExitOnExecute" Header="E_xit" />
            </MenuItem>
            <MenuItem Header="_Edit">
                <MenuItem Name="itemUndo" Command="myCmd:Window1.UndoCommand" Header="_Undo" Icon="{StaticResource iconUndo}" />
                <MenuItem Name="itemRedo" Command="myCmd:Window1.RedoCommand" Header="_Redo" Icon="{StaticResource iconRedo}" />
                <Separator></Separator>
                <MenuItem Name="itemCut" Command="myCmd:Window1.CutCommand" Header="Cu_t" Icon="{StaticResource iconCut}" />
                <MenuItem Name="itemCopy" Command="myCmd:Window1.CopyCommand" Header="_Copy" Icon="{StaticResource iconCopy}" />
                <MenuItem Name="itemPaste"                           Header="_Paste" Icon="{StaticResource iconPaste}" />
                <MenuItem Name="itemDelete"                          Header="De_lete" Icon="{StaticResource iconDelete}" />
                <Separator></Separator>
                <MenuItem Name="itemFind" Click="FindOnExecute" Header="_Find..." InputGestureText="Ctrl+F" 
               Icon="{StaticResource iconFind}" />
                <MenuItem Name="itemFindNext"                        Header="Find _Next" />
                <MenuItem Name="itemReplace"                         Header="_Replace..." />
                <MenuItem Name="itemGoTo"                            Header="_Go To..." />
                <Separator></Separator>
                <MenuItem Name="itemSelectAll"                       Header="Select _All" />
            </MenuItem>
            <MenuItem Header="F_ormat">
                <MenuItem Name="itemFont" Click="FontOnExecute" Header="_Font..." Icon="{StaticResource iconFont}" />
                <Separator />
                <MenuItem Name="itemWordWrap" Click="WordWrapOnExecute" Header="_Word Wrap" IsCheckable="True" 
         IsChecked="True" InputGestureText="Ctrl+W" />
            </MenuItem>
            <MenuItem Header="_Help">
                <MenuItem Name="itemAbout" Click="AboutOnExecute" Header="_About" />
            </MenuItem>
        </Menu>
        
        <!-- Панель инструментов -->
        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button Name="btnNew" Click="NewOnExecute" Width="23" Content="{StaticResource iconNew}" />
                <Button Name="btnOpen" Click="OpenOnExecute" Width="23" Content="{StaticResource iconOpen}" />
                <Button Name="btnSave" Command="myCmd:Window1.SaveCommand" Width="23" Content="{StaticResource iconSave}" />
            </ToolBar>
            <ToolBar>
                <Button Name="btnUndo" Command="myCmd:Window1.UndoCommand" Width="23" Content="{StaticResource iconUndo}" />
                <Button Name="btnRedo" Command="myCmd:Window1.RedoCommand" Width="23" Content="{StaticResource iconRedo}" />
                <Separator />
                <Button Name="btnCut" Command="myCmd:Window1.CutCommand" Width="23" Content="{StaticResource iconCut}" />
                <Button Name="btnCopy" Command="myCmd:Window1.CopyCommand" Width="23" Content="{StaticResource iconCopy}" />
                <Button Name="btnPaste"                              Width="23" Content="{StaticResource iconPaste}" />
                <Button Name="btnDelete"                             Width="23" Content="{StaticResource iconDelete}" />
            </ToolBar>
            <ToolBar Header="Find:">
                <TextBox Width="100" />
                <Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
            </ToolBar>
        </ToolBarTray>
    
        <!-- Строка состояния -->
        <StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
            <Label>Simulator Application is Loading</Label>
            <Separator />
            <ProgressBar Height="20" Width="100" IsIndeterminate="True" />
        </StatusBar>
        
        <!-- Многострочное текстовое поле редактирования -->
        <TextBox TextWrapping="Wrap"
                 AcceptsReturn="True" 
                 AcceptsTab="True"
                 VerticalScrollBarVisibility="Auto"
                 Name="txtBox1"
                 TextChanged="txtBox1_TextChanged" 
                 HorizontalScrollBarVisibility="Auto"
                 >
            <TextBox.ContextMenu>
                <ContextMenu Width="100">
                  <MenuItem Name="contextCut" Command="myCmd:Window1.CutCommand" Header="Cu_t" Icon="{StaticResource iconCut}" />
                  <MenuItem Name="contextCopy" Command="myCmd:Window1.CopyCommand" Header="_Copy" Icon="{StaticResource iconCopy}"/>
                  <MenuItem Name="contextPaste"                    Header="_Paste" Icon="{StaticResource iconPaste}" />
                  <MenuItem Name="contextDelete"                   Header="De_lete" Icon="{StaticResource iconDelete}" />
                </ContextMenu>
            </TextBox.ContextMenu>
        </TextBox>
    </DockPanel>
</Window>

Эта разметка визуально получилась достаточно широкой, ее лучше прежде скопировать, а потом разбирать.

  • Удалите (я закомментировал) код создания жестов в функции CreateGestures() файла KeyGestures.cs для выбранных нами 12 задач (для задачи Page Setup жестов нет)
void CreateGestures()
        {
            // File
            gests.Add(new KeyGesture(Key.N, ModifierKeys.Control), NewOnExecute);//_New
            gests.Add(new KeyGesture(Key.O, ModifierKeys.Control), OpenOnExecute);//_Open...
            //gests.Add(new KeyGesture(Key.S, ModifierKeys.Control), SaveOnExecute);//_Save
            gests.Add(new KeyGesture(Key.F2, ModifierKeys.Control), PrintPreviewOnExecute);//P_rint Preview
            gests.Add(new KeyGesture(Key.P, ModifierKeys.Control), PrintOnExecute);//_Print...
    
            // Edit
            //gests.Add(new KeyGesture(Key.Z, ModifierKeys.Control), UndoOnExecute);//_Undo
            //gests.Add(new KeyGesture(Key.Y, ModifierKeys.Control), RedoOnExecute);//_Redo
            //gests.Add(new KeyGesture(Key.X, ModifierKeys.Control), CutOnExecute);//Cu_t
            //gests.Add(new KeyGesture(Key.C, ModifierKeys.Control), CopyOnExecute);//_Copy
            //gests.Add(new KeyGesture(Key.V, ModifierKeys.Control), PasteOnExecute);//_Paste
            //gests.Add(new KeyGesture(Key.Delete, ModifierKeys.None), DeleteOnExecute);//De_lete
            gests.Add(new KeyGesture(Key.F, ModifierKeys.Control), FindOnExecute);//_Find...
            //gests.Add(new KeyGesture(Key.F3, ModifierKeys.None), FindNextOnExecute);//Find _Next
            //gests.Add(new KeyGesture(Key.H, ModifierKeys.Control), ReplaceOnExecute);//_Replace...
            //gests.Add(new KeyGesture(Key.G, ModifierKeys.Control), GoToOnExecute);//_Go To...
            //gests.Add(new KeyGesture(Key.A, ModifierKeys.Control), SelectAllOnExecute);//Select _All
    
            // Format
            gests.Add(new KeyGesture(Key.W, ModifierKeys.Control), WordWrapOnExecute);//_Word Wrap
        }
  • Запустите приложение и убедитесь, что тексты жестов во всех источниках команд присутствуют несмотря на то, что мы их только что явно удалили. Теперь жесты в источники попадают из команд

В начальном состоянии источники команд являются недоступными, поскольку пока нет привязки к элементам визуального дерева и обработчикам. Исправим это и привяжем команды к коллекции CommandBindings окна Window1. Причем, в соответствии с намеченным планом выполним привязку одной части команд в коде, а остальной - в разметке.

В месте привязки для каждого объекта привязки нужно указать имя команды, имя обработчика события Executed и имя обработчика события CanExecute для управления доступностью источников. Оба события всплывающие, поэтому местом привязки можно выбрать любой элемент маршрута, например, окно Window1. Это корень визуального дерева и ни одно всплывающее событие мимо него не пройдет.

Обработчики события Executed для выполнения команд у нас есть, мы их уже создавали для обработки события Click. Например, для команды Save обработчик имеет имя SaveOnExecute и его сигнатура выглядит так

private void SaveOnExecute(object sender, RoutedEventArgs e)
        {
        }

Нам предстоит создать обработчики события CanExecute и мы разместим их в файле EnabledControls.cs. Они будут иметь несколько иную сигнатуру. Например, для команды Save такой обработчик должен иметь следующую заготовку

private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
  • Добавьте в класс Window1 файла EnabledControls.cs заготовки обработчиков события CanExecute для выбранных нами ранее 12 команд (число-то какое хорошее!)
// Обработчики события CanExecute команд
        private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void PageSetupCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void CutCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void PasteCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void DeleteCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void ReplaceCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void GoToCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }
        private void SelectAllCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
        }

Теперь пришла пора выполнить саму привязку в соответствии с намеченным планом

План 12 задач, которые предстоит реализовать командами
Где привязать к окну? Задача Alias (псевдоним) Жесты Регулировать доступность источников?
Разметка Save SaveCommand Ctrl+S Да
Разметка Page Setup PageSetupCommand   Нет
Разметка Undo UndoCommand Ctrl+Z Да
Код Redo RedoCommand Ctrl+Y Да
Код Cut CutCommand Ctrl+X Да
Код Copy CopyCommand Ctrl+C Да
Разметка Paste PasteCommand Ctrl+V Да
Разметка Delete DeleteCommand Del Да
Разметка Find Next FindNextCommand F3 Да
Код Replace ReplaceCommand Ctrl+H Да
Код Go To GoToCommand Ctrl+G Нет
Код Select All SelectAllCommand Ctrl+A Да

Обратите внимание, что источники команд PageSetupCommand и GoToCommand должны быть доступны всегда, поэтому привяжем для них только обработчики события Executed

  • Удалите в файле EnabledControls.cs заготовки обработчиков события CanExecute для команд PageSetupCommand и GoToCommand (или не удаляйте и пусть болтаются как незадействованные методы)
  • В файле Window1.xaml текущего проекта выполните привязку части команд к объекту окна, для этого после открывающего дескриптора окна вставьте следующую разметку
<Window x:Class="Notepad2.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:myCmd="clr-namespace:Notepad2"
        
    Title="Window1: Управление состоянием источников команд"
    Width="500" Height="375"
    MinWidth="500" MinHeight="375"
    WindowStartupLocation="CenterScreen"
    ResizeMode="CanResizeWithGrip"
    Loaded="Window_Loaded"
    Icon="Notepad.ico"
    Closing="Window_Closing"
    Activated="Window_Activated"
        >
        
    <!-- Привязка команд к объекту окна -->
    <Window.CommandBindings>
        <CommandBinding Command="myCmd:Window1.SaveCommand" Executed="SaveOnExecute" CanExecute="SaveCanExecute" />
        <CommandBinding Command="myCmd:Window1.PageSetupCommand" Executed="PageSetupOnExecute" />
        <CommandBinding Command="myCmd:Window1.UndoCommand" Executed="UndoOnExecute" CanExecute="UndoCanExecute" />
        <CommandBinding Command="myCmd:Window1.PasteCommand" Executed="PasteOnExecute" CanExecute="PasteCanExecute" />
        <CommandBinding Command="myCmd:Window1.DeleteCommand" Executed="DeleteOnExecute" CanExecute="DeleteCanExecute" />
        <CommandBinding Command="myCmd:Window1.FindNextCommand" Executed="FindNextOnExecute" CanExecute="FindNextCanExecute" />
    </Window.CommandBindings>
        
    ..........................................
</Window>
  • В файле EnabledControls.cs добавьте в функцию AdditionalHandlers() код привязки части команд к объекту окна, после чего функция должна стать такой
// Вызов размещен в конструкторе класса
        void AdditionalHandlers()
        {
            Clipboard.Clear();// Временно, чтобы испытать начальное состояние
            // Присоединяем команды к источникам, жесты уже встроены в команды
            //SaveCommand       - присоединим в разметке
            //PageSetupCommand  - присоединим в разметке
            //UndoCommand       - присоединим в разметке
            //RedoCommand       - присоединим в разметке
            //CutCommand        - присоединим в разметке
            //CopyCommand       - присоединим в разметке
            btnPaste.Command = itemPaste.Command = contextPaste.Command = PasteCommand;
            btnDelete.Command = itemDelete.Command = contextDelete.Command = DeleteCommand;
            itemFindNext.Command = FindNextCommand;
            itemReplace.Command = ReplaceCommand;
            itemGoTo.Command = GoToCommand;
            itemSelectAll.Command = SelectAllCommand;
    
            // Привязка части команд к объекту окна в коде
            // 0)
            this.CommandBindings.Add(new CommandBinding(RedoCommand, RedoOnExecute, RedoCanExecute));
            this.CommandBindings.Add(new CommandBinding(CutCommand, CutOnExecute, CutCanExecute));
            this.CommandBindings.Add(new CommandBinding(CopyCommand, CopyOnExecute, CopyCanExecute));
            // Теперь чуть подлиннее: создаем, настраиваем, привязываем!
            // 1)
            CommandBinding binding = new CommandBinding();
            binding.Command = ReplaceCommand;
            binding.Executed += ReplaceOnExecute;
            binding.CanExecute += ReplaceCanExecute;
            this.CommandBindings.Add(binding);
            // 2)
            binding = new CommandBinding(GoToCommand);
            binding.Executed += GoToOnExecute;
            this.CommandBindings.Add(binding);
            // 3)
            binding = new CommandBinding(SelectAllCommand, SelectAllOnExecute);
            binding.CanExecute += SelectAllCanExecute;
            this.CommandBindings.Add(binding);
        }

После того, как ссылка binding, которая пока единствественная адресовала объект, передаст адрес закрепленного за ней объекта в коллекцию CommandBindings элемента, этот объект станет адресоваться в двух местах: в коллекции и в ссылке. Поэтому такая ссылка станет уже излишней и ее можно будет использовать для присвоения адреса нового объекта. А коллекция так и будет продолжать адресовать брошенный ссылкой объект. В коде показано применение вариантов перегрузок конструктора класса CommandBinding и настройки свойств объекта.

  • Запустите приложение и убедитесь в следующем...

Источники, к которым мы присоединили команды, по прежнему остаются недоступными, как после того, как мы к ним команды только присоединили. Но заметьте, что два источника: File/Page Setup... и Edit/Go To... открылись и функционируют, а для Go To даже клавиатурный жест Ctrl+G действует нормально. Это происходит потому, что мы для них не зарегистрировали обработчики событий CanExecute и они свободно вызывают обработчики, зарегистрированные в событии Executed, а остальные источники ждут разрешение на доступность. Создание таких разрешений в обработчиках CanExecute и составляет суть реализации логики доступности источников команд.

Реализация логики доступности источников команд

  • В соответствии с логикой работы подобных приложений заполните заготовки обработчиков события CanExecute в файле EnabledControls.cs следующим образом (комментарии в коде)
// Обработчики события CanExecute команд
        private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если есть несохраненные изменения
            e.CanExecute = IsModified;
        }
        /*
        private void PageSetupCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Источник не регулируется
        }
        */
        private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если есть что откатывать
            e.CanExecute = IsModified;
        }
        private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если очередь отмены есть и стоим не самые первые
            e.CanExecute = txtBox1.CanRedo;
        }
        private void CutCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если есть выделенный текст
            e.CanExecute = txtBox1.SelectionLength > 0;
        }
        private void CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если есть выделенный текст
            e.CanExecute = txtBox1.SelectionLength > 0;
        }
        private void PasteCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если буфер обмена непустой и там содержится текстовый формат
            e.CanExecute = Clipboard.ContainsText();
        }
        private void DeleteCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Позволить, если есть выделенный текст
            e.CanExecute = txtBox1.SelectionLength > 0;
        }
        private void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            //!!!!! От фонаря - не хочется думать !!!!!
            e.CanExecute = txtBox1.CaretIndex < txtBox1.Text.Length;
        }
        private void ReplaceCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            //!!!!! От фонаря - не хочется думать !!!!!
            e.CanExecute = txtBox1.CaretIndex < txtBox1.Text.Length;
        }
        /*
        private void GoToCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Источник не регулируется
        }
        */
        private void SelectAllCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Заблокировать, если нечего выделять или уже все выделено
            if (txtBox1.Text == String.Empty || 
                txtBox1.SelectionLength == txtBox1.Text.Length)
                e.CanExecute = false;
            else
                e.CanExecute = true;
        }
  • Запустите приложение и убедитесь... - что все работает не так как надо!!!

Исправим это. Начнем с журнала откатов, встроенного в элемент TextBox. По логике, очередь изменений должна очищаться при создании нового документа или открытии существующего. А эта задача у нас выполняется в обработчиках NewOnExecute() и OpenOnExecute().

  • Откройте файл File.cs и добавьте в упомянутые обработчики код очистки журнала изменений объекта txtBox1 следующим образом
private void NewOnExecute(object sender, RoutedEventArgs e)
        {
            // Пользователь передумал или была ошибка записи изменений
            if (!CheckModifiedAndSaveIt())
                return;
    
            // Изменений нет или они успешно сохранены
            //txtBox1.Text = String.Empty;  // Вариант I
            //txtBox1.Text = "";            // Вариант II
            txtBox1.Clear();                // Вариант III
            strLoadedFile = null;
            IsModified = false;
            UpdateTitle();
            txtBox1.UndoLimit = 0;  // Очистка очереди отмены
            txtBox1.UndoLimit = -1; // Размер по умолчанию ограничен памятью
            txtBox1.Focus();
            // Надо отключить Cut, Copy и Delete
        }
    
        private void OpenOnExecute(object sender, RoutedEventArgs e)
        {
            if (DisplayOpenDialog())
            {
                txtBox1.CaretIndex = txtBox1.Text.Length;// Курсор в конец
                txtBox1.UndoLimit = 0;  // Очистка очереди отмены
                txtBox1.UndoLimit = -1; // Размер по умолчанию ограничен памятью
            }
            txtBox1.Focus();// Передача фокуса
        }
  • Откройте файл Edit.cs и добавьте в обработчик UndoOnExecute() следующий код
private void UndoOnExecute(object sender, RoutedEventArgs e)
        {
            txtBox1.Undo();
            if (!txtBox1.CanUndo)
                IsModified = false;
        }
  • Запустите приложение и убедитесь... - что опять все работает не так как надо!!!

Вроде бы все предусмотрели, в чем здесь дело?

  • Проставьте в файле Edit.cs вдоль левого поля в теле обработчиков UndoOnExecute(), RedoOnExecute(), CutOnExecute(), CopyOnExecute(), PasteOnExecute(), DeleteOnExecute(), SelectAllOnExecute() напротив исполнимого кода точки останова (Breakpoin), как показано на снимке

>

  • Запустите приложение и поэксперименируйте с источниками команд - ни один из отмеченных обработчиков (кроме DeleteOnExecute, да и тот на жест Del не реагирует) не срабатывает, хотя текстовый элемент свои функции выполняет исправно

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

  • Удалите все точки останова командой оболочки Debug/Delete All Breakpoints
  • Теперь в файле EnabledControls.cs проставляйте поодиночке точки останова и каждый раз запускайте приложение для обработчиков UndoCanExecute(), RedoCanExecute(), CutCanExecute(), CopyCanExecute(), PasteCanExecute(), DeleteCanExecute(), SelectAllCanExecute(). Для остановки процесса пользуйтесь командой Stop Debugging меню Debug оболочки или одноименной кнопкой панели инструментов!!!

Мы видим, что все эти обработчики события CanExecute срабатывают исправно для всех команд: и библиотечных, и пользовательских. Причем срабатывают перед тем, как элемент показывается при раскрытии меню, начальной отрисовке или наведении курсора. То есть срабатывает именно тогда, когда нужно принять решение, с какой доступностью этот элемент отобразить.

Итак, для правильной работы логики доступности источников команд нам нужно заставить выполняться именно наши обработчики для команд Undo, Redo и SelectAll.

  • Добавьте в статический конструктор файла EnabledControls.cs следующий код переопределения библиотечных команд на пользовательские
// Статический конструктор
        static Window1()
        {
            // Определяем с добавлением жестов
            InputGestureCollection coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.F3, ModifierKeys.None, "F3"));
            FindNextCommand = new RoutedCommand("FindNext", typeof(Window1), coll);
            coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.G, ModifierKeys.Control, "Ctrl+G"));
            GoToCommand = new RoutedCommand("GoTo", typeof(Window1), coll);
    
            // Заменяем библиотечные команды на свои для правильной работы логики
            coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.Z, ModifierKeys.Control, "Ctrl+Z"));
            UndoCommand = new RoutedCommand("Undo", typeof(Window1), coll);
            coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.Y, ModifierKeys.Control, "Ctrl+Y"));
            RedoCommand = new RoutedCommand("Redo", typeof(Window1), coll);
            coll = new InputGestureCollection();
            coll.Add(new KeyGesture(Key.A, ModifierKeys.Control, "Ctrl+A"));
            SelectAllCommand = new RoutedCommand("SelectAll", typeof(Window1), coll);
        }

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

Это плохая практика программирования, поскольку завтра могут выпустить другой транслятор или среду исполнения CLR, где этот порядок будет изменен, и наша программа станет работать неверно. Microsoft не дремлет (!!!) и регулярно посылает по сети обновления и исправления. А что они завтра пришлют, кто его знает. Поэтому лучше было бы в полях ссылки только объявить, а уже в конструкторе они бы наверняка получили адреса наших (пользовательских) команд. Можете так и сделать, но я у себя оставляю как есть.

  • Запустите приложение - теперь все работает как надо!!!

Мы спроектировали более-менее сносный текстовый редактор, для этого спукались на относительно низкий (подробный) уровень программирования. Конечно, в получившемся продукте еще много недоработок, да и такую задачу можно было бы решить в два счета на C++Builder. Но мы-то не блокнот проектировали, а знакомились с новым механизмом команд WPF на примере блокнота.

Вначале мы попытались проектировать без привлечения команд. Создали основу блокнота, но управление доступностью источников реализовали только для одной задачи Save, и то с большим трудом. Приходилось пускаться на всякие ухищрения. Затем часть задач переключили на команды. И сразу удалось гораздо легче и понятнее реализовать доступность всех источников пользовательского интерфейса. Выгода от применения механизма команд очевидна.

Отображение позиции курсора в строке состояния

Чтобы более-менее завершить наше блокнотоподобное приложение, немного подукрасим его. Добавим в строку состояния, которая у нас имеет имя statusBar, вывод информации о положении курсора. Опять же, постараемся минимально вмешиваться в уже созданный код. Для этого все решение разместим в отдельном файле.

  • Выделите узел текущего проекта и добавьте командой Project/Add New Item новый файл Code File с именем CaretPosition.cs

  • Заполните файл CaretPosition.cs следующим кодом
using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
    
namespace Notepad2
{
    // Часть класса главного окна
    partial class Window1
    {
        // Создаем экземпляр и регистрируем обработчики
        CaretPosition caretPosition;
        private void CreateCaretPosition()
        {
            // Отображение в StatusBar номера строки и столбца
            caretPosition = new CaretPosition();// Создаем объект
            caretPosition.TxtBox = txtBox1;     // Присоединяем TextBox
    
            // Дополняем StatusBar
            this.statusBar.Items.Add(
                new System.Windows.Controls.Separator());
            this.statusBar.Items.Add(caretPosition.StrLineCol);
            // Увековечиваем себя!
            this.statusBar.Items.Insert(0, new System.Windows.Controls.Separator());
            this.statusBar.Items.Insert(0, "Снетков В.М.");   
    
            // Регистрируем обработчик события перемещения каретки
            txtBox1.SelectionChanged +=
                new RoutedEventHandler(txtBox1_SelectionChanged);
        }
    
        // Обработчик инициирует вычисление и отрисовку нового положения
        void txtBox1_SelectionChanged(object sender, RoutedEventArgs e)
        {
            caretPosition.CaretChanged();
        }
    }
     
    class CaretPosition
    {
        // Закрытые поля
        StatusBarItem strLineCol = new StatusBarItem();
        TextBox txtBox = new TextBox();
    
        // Открытые свойства
        // Для добавления в строку состояния клиента
        public StatusBarItem StrLineCol // Папа '-->'
        {
            // Только для чтения
            get { return strLineCol; }
        }
        // Для присоединения к TextBox клиента
        public TextBox TxtBox // Мама '>--'
        {
            // Только для записи
            set { txtBox = value; }
        }
    
        // Вычисляет номер строки
        int GetLine()
        {
            int count = 0;
            int pos = 0;
            int caretPos = txtBox.SelectionStart + 1; //txtBox1.CaretIndex
            while (pos < caretPos)
            {
                count++;        // Счетчик строк
                pos = txtBox.Text.IndexOf("\r\n", pos);// \n - перевод строки
                if (pos != -1)  // Нашли очередную пару
                    pos += 2;   // Сдвигаемся правее найденных
                else
                    break;// Больше нет
            }
            return count;
        }
    
        public void CaretChanged()
        {
            if (!txtBox.IsFocused)
                return;
            int posChar = txtBox.CaretIndex;
            int line = GetLine();
            int column = posChar - txtBox.GetCharacterIndexFromLineIndex(line - 1) + 1;
    
            // Обновляем в строке состояния
            strLineCol.Content = String.Format(" Ln {0} \t Col {1}", line, column);
        }
    }
}
  • В файле Window1.xaml.cs добавьте в конструктор класса Window1 код вызова функции CreateCaretPosition()
public Window1()
        {
            InitializeComponent();
    
            // Создание жестов
            this.CreateGestures();
    
            // Дополнительные обработчики в файле EnabledControls.cs
            AdditionalHandlers();
    
            // Отображение в StatusBar номера строки и столбца
            // Функция находится в файле CaretPosition.cs
            CreateCaretPosition();
        }
  • Запустите приложение - номера строк и столбцов отображаются. Поэкспериментируйте с функциональностью, разберитесь с кодом

Обратите внимание, как мы из вновь созданного класса CaretPosition подключились (мама) через свойство TxtBox к существующему объект txtBox1 окна и извлекаем нужную информацию уже в новом классе. И как созданный в новом классе элемент строки состояния подключили (папа) обратно к окну. Получился как бы канал связи, по которому информация из объекта txtBox1 окна поступает в экземпляр нового класса, а затем в обработанном виде возвращается обратно в окно для отображения в объекте statusBar.

Снимок блокнота на данный момент будет таким

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


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

При выполнении в лабораторной работе упражнения №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" или один из зависимых от них компонентов. Не удается найти указанный файл.

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