Книга: Microsoft Visual C#. Подробное руководство. 8-е издание
Назад: 19. Перечисляемые коллекции
Дальше: 21. Запрос данных, находящихся в памяти, с помощью выражений в виде запросов

20. Отделение логики приложения и обработка событий

Прочитав эту главу, вы научитесь:

объявлять тип делегата для создания абстракции сигнатуры метода;

создавать экземпляр делегата для ссылки на конкретный метод;

вызывать метод через делегата;

определять лямбда-выражение для конкретного указания кода, выполняемого делегатом;

объявлять поле события;

обрабатывать событие с помощью делегата;

инициировать событие.

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

События в C# придерживаются примерно такого же сценария. В коде, который используется в данной книге для упражнений, зачастую предполагается последовательное выполнение инструкций. Хотя в большинстве случаев все так и происходит, иногда требуется прервать текущий ход выполнения программы для решения другой, более важной задачи. Когда выполнение этой задачи завершится, программа может продолжить выполнение с того места, где оно было прервано. Классическими примерами этого стиля программирования являются формы универсальной платформы Windows (UWP), которые вы использовали в упражнениях по разработке графических приложений. Форма показывает на экране такие элементы управления, как кнопки и текстовые поля. При щелчке на кнопке или наборе текста в текстовом поле вы ожидаете от формы немедленной реакции. Приложению приходится приостанавливать свою работу и обрабатывать введенные вами данные.

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

Сначала давайте рассмотрим работу делегатов.

Основные сведения о делегатах

Делегат является ссылкой на метод. Это весьма простое, но чрезвычайно эффективное средство. Позвольте пояснить.

174178.png

ПРИМЕЧАНИЕ Свое название делегаты получили потому, что при вызове они делегируют обработку тому методу, на который ссылаются.

Обычно при написании инструкции, вызывающей метод, указывается имя метода (и, возможно, объект или структура, которой этот метод принадлежит). По коду можно абсолютно точно понять, какой метод запускается и где именно вы его запускаете. Рассмотрим следующий простой пример, в котором вызывается метод performCalculation объекта, относящегося к типу Processor (сейчас совершенно неважно, чем этот метод занимается или как определен класс Processor):

Processor p = new Processor();

p.performCalculation();

Делегат — это объект, ссылающийся на метод. Ссылку на метод можно присвоить делегату практически так же, как можно присвоить int-значение int-переменной. В следующем примере создается делегат по имени performCalculationDelegate, ссылающийся на метод performCalculation, принадлежащий объекту типа Processor. Я умышленно не стал показывать некоторые элементы инструкции, объявляющие делегат, поскольку сейчас важнее понять саму концепцию, а не разбираться в синтаксисе (вскоре будет показан и полный синтаксис):

Processor p = new Processor();

delegate ... performCalculationDelegate ...;

performCalculationDelegate = p.performCalculation;

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

После сохранения в делегате ссылки на метод performCalculation, принадлежащий Processor-объекту, приложение может в дальнейшем вызывать метод через делегат:

performCalculationDelegate();

Это похоже на обычный вызов метода, и не зная, в чем именно дело, можно подумать, что запускается метод performCalculationDelegate. Но общеязыковая среда выполнения (common language runtime (CLR)) знает, что это делегат, поэтому извлекает метод, на который он ссылается, и запускает этот метод. Позже вы можете вносить изменения в метод, на который ссылается делегат, следовательно, инструкция, вызывающая делегат, может фактически при каждом выполнении вызывать разные методы. Кроме того, делегат может одновременно ссылаться более чем на один метод (что можно представить в виде коллекции ссылок на методы), и при вызове делегата будут запущены все методы, на которые он ссылается.

174185.png

ПРИМЕЧАНИЕ Те, кто знаком с языком C++, могут считать делегат аналогом указателя на функцию. Но в отличие от указателей на функции, делегаты обладают полной безопасностью относительно используемых типов. Делегат можно заставить ссылаться только на тот метод, который соответствует сигнатуре делегата, и вы не можете вызвать делегат, который не ссылается на подходящий метод.

Примеры делегатов в библиотеке классов .NET Framework

Библиотека классов Microsoft .NET Framework широко использует делегаты для многих своих типов, примерами могут послужить рассмотренные в главе 18 «Использование коллекций» методы Find и Exists класса List<T>. Если помните, эти методы выполняют сквозной поиск в коллекции List<T>, либо возвращая соответствующий элемент, либо проверяя наличие этого элемента. При реализации класса List<T> его разработчики не имели ни малейшего представления о том, что в коде вашего приложения должно подтверждаться соответствие, поэтому они позволили вам определить это путем предоставления своего собственного кода в форме предиката. Фактически предикат является делегатом, возвращающим булево значение.

Напомнить порядок использования метода Find поможет следующий код:

struct Person

{

    public int ID { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }

}

...

List<Person> personnel = new List<Person>()

{

    new Person() { ID = 1, Name = "John", Age = 47 },

    new Person() { ID = 2, Name = "Sid", Age = 28 },

    new Person() { ID = 3, Name = "Fred", Age = 34 },

    new Person() { ID = 4, Name = "Paul", Age = 22 },

};

...

// Поиск элемента списка, у которого ID равен 3

Person match = personnel.Find(p => p.ID == 3);

Другими примерами методов, использующих делегаты для выполнения своих операций в классе List<T>, могут послужить Average, Max, Min, Count и Sum. Эти методы получают в качестве параметра делегат Func. Он ссылается на метод, возвращающий значение (то есть на функцию). В следующих примерах метод Average используется для вычисления среднего возраста элементов в коллекции personnel (делегат Func<T> просто возвращает значение, находящееся в поле Age каждого элемента коллекции), метод Max используется для определения элемента с самым высоким ID, а метод Count вычисляет, сколько элементов имеют значение Age между 30 и 39 включительно:

double averageAge = personnel.Average(p => p.Age);

Console.WriteLine($"Average age is {averageAge}");

...

int id = personnel.Max(p => p.ID);

Console.WriteLine($"Person with highest ID is {id}");

...

int thirties = personnel.Count(p => p.Age >= 30 && p.Age <= 39);

Console.WriteLine($"Number of personnel in their thirties is {thirties}");

Этот код выводит на экран следующую информацию:

Average age is 32.75

Person with highest ID is 4

Number of personnel in their thirties is 1

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

Типы делегатов Func<T, …> и Action<T, …>

Параметр, получаемый Average, Max, Count и другими методами класса List<T>, по сути является делегатом-обобщением Func<T, TResult>; параметры типа ссылаются на тип параметра, переданного делегату, и на тип возвращаемого значения. Для методов Average, Max и класса List<Person>, показанного в тексте, первым параметром T является тип данных в списке (структура Person), а параметр типа TResult определяется контекстом, в котором используется делегат. В следующем примере типом TResult является int, потому что значение, возвращаемое методом Count, должно быть целым числом:

int thirties = personnel.Count(p => p.Age >= 30 && p.Age <= 39);

Следовательно, в этом примере метод Count ожидает, что типом делегата будет Func<Person, int>.

Все это можно назвать теорией, потому что компилятор автоматически генерирует делегата на основе типа List<T>, но вас все же стоило ознакомить с этой особенностью, поскольку такое происходит в библиотеке классов .NET Framework практически повсеместно. Фактически в пространстве имен System определяется целое семейство типов делегатов Func, от Func<TResult> для функций, возвращающих результат, не получая каких-либо параметров, до Func<T1, T2, T3, T4, …, T16, TResult> для функций, получающих 16 параметров. Когда вам придется создавать собственный тип делегата, соответствующий этой схеме, нужно будет присмотреться к использованию соответствующего типа делегата Func. Очередная встреча с типами делегатов Func вам предстоит в главе 21 «Запрос данных, находящихся в памяти, с помощью выражений запросов».

В дополнение к Func в пространстве имен System определяется также серия типов делегатов Action. Делегаты типа Action используются для ссылок на метод, выполняющий действие, а не возвращающий значение (методы типа void). Семейство типов делегатов Action доступно также в диапазоне от Action<T> (определяющем делегата, получающего всего один параметр) до Action<T1, T2, T3, T4, …, T16>.

Сценарий автоматизированной фабрики

Предположим, что вы создаете систему управления для автоматизированной фабрики. Фабрика состоит из большого количества различных машин, каждая из которых выполняет отдельную задачу в производстве конечной продукции, выпускаемой фабрикой: обработка и укладка металлических листов, сварка листов, их покраска и т.д. Каждая машина была собрана и установлена специалистом поставщика. Всеми машинами управляет компьютер, и каждый поставщик предоставил набор функций, который можно использовать для управления его машиной. Ваша задача заключается в объединении различных систем, используемых машинами, в единую программу управления. Одним из аспектов, на котором вы собрались сосредоточить свои усилия, является разработка средств выключения всех машин, причем быстрого выключения в случае возникновения острой необходимости!

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

StopFolding();       // Машина для обработки и укладки

FinishWelding();     // Машина для сварки

PaintOff();          // Машина для покраски

Реализация системы управления фабрикой без использования делегатов

К реализации функции выключения машин в программе управления можно применить следующий простой подход:

class Controller

{

    // Поля, представляющие различные машины

    private FoldingMachine folder;

    private WeldingMachine welder;

    private PaintingMachine painter;

    ...

    public void ShutDown()

    {

        folder.StopFolding();

        welder.FinishWelding();

        painter.PaintOff();

    }

    ...

}

Этот подход позволяет создать вполне работоспособную, но недостаточно хорошо расширяемую и гибкую систему. Если на фабрике будет закуплена новая машина, код придется изменить, поскольку класс Controller и код для управления машинами имеют весьма тесную связь.

Реализация системы управления фабрикой с использованием делегата

Хотя имена у всех методов разные, у них один и тот же «профиль»: они не получают никаких параметров и не возвращают никаких значений. (Что произойдет при других условиях, вы увидите чуть позже, так что немного потерпите.) То есть для всех методов используется следующий общий формат:

void methodName();

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

delegate void stopMachineryDelegate();

Обратите внимание на следующие особенности:

• здесь используется ключевое слово delegate;

• здесь указываются возвращаемый тип (в данном случае void), имя делегата (stopMachineryDelegate) и любые параметры (в рассматриваемом случае их нет).

После объявления делегата можно создать экземпляр и заставить его ссылаться на соответствующий метод, воспользовавшись оператором составного присваивания +=. Сделать это можно в конструкторе класса контроллера:

class Controller

{

    delegate void stopMachineryDelegate();       // Тип делегата

    private stopMachineryDelegate stopMachinery; // Экземпляр делегата

    ...

    public Controller()

    {

        this.stopMachinery += folder.StopFolding;

    }

    ...

}

Освоить этот синтаксис нетрудно. К делегату добавляется новый метод, причем следует запомнить, что в данный момент этот метод не вызывается. Чтобы получить новое назначение при использовании с делегатами, оператор + перегружается. (Более подробно перегрузка операторов рассматривается в главе 22 «Перегрузка операторов».) Обратите внимание на то, что здесь просто указывается имя метода без круглых скобок и параметров.

В отношении неинициализированного делегата оператор += можно использовать без всяких опасений. Инициализация будет выполнена автоматически. В качестве альтернативного варианта для явной инициализации делегата с единственным указанным методом можно воспользоваться ключевым словом new:

this.stopMachinery = new stopMachineryDelegate(folder.StopFolding);

Метод можно вызвать, задействуя для этого делегата:

public void ShutDown()

{

    this.stopMachinery();

    ...

}

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

174192.png

ПРИМЕЧАНИЕ При попытке задействовать делегата, не прошедшего инициализацию и не ссылающегося ни на один метод, будет выдано исключение NullReferenceException.

Важным преимуществом от использования делегата является то, что он может одновременно ссылаться на более чем один метод. Чтобы добавить методы к делегату, нужно просто воспользоваться оператором +=:

public Controller()

{

    this.stopMachinery += folder.StopFolding;

    this.stopMachinery += welder.FinishWelding;

    this.stopMachinery += painter.PaintOff;

}

Когда в методе Shutdown класса Controller задействуется делегат this.stopMachinery(), каждый метод вызывается по очереди в автоматическом режиме. Метод Shutdown не нуждается в сведениях о том, сколько машин имеется и как называются их методы.

Метод можно удалить из делегата, воспользовавшись для этого оператором составного присваивания –=:

this.stopMachinery -= folder.StopFolding;

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

• Сделать переменную делегата stopMachinery открытой:

    public stopMachineryDelegate stopMachinery;

• Оставить переменную делегата stopMachinery закрытой, но предоставить к ней доступ, создав свойство для чтения и записи:

    private stopMachineryDelegate stopMachinery;

    ...

    public stopMachineryDelegate StopMachinery

    {

        get

        {

            return this.stopMachinery;

        }

        set

        {

            this.stopMachinery = value;

        }

    }

• Предоставить полную инкапсуляцию, реализовав отдельные методы Add и Remove. Метод Add получает метод в качестве параметра и добавляет его к делегату, а метод Remove удаляет указанный метод из делегата (обратите внимание на то, что метод указывается в качестве параметра путем использования типа делегата):

    public void Add(stopMachineryDelegate stopMethod)

    {

        this.stopMachinery += stopMethod;

    }

 

    public void Remove(stopMachineryDelegate stopMethod)

    {

        this.stopMachinery -= stopMethod;

    }

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

Какой бы из приемов вы ни выбрали, код, добавляющий методы управления машинами к делегату, нужно убирать из конструктора Controller. Тогда можно будет создать экземпляр класса Controller и объекты, представляющие другие подобные этим машины (в этом примере применяется подход с использованием Add и Remove):

Controller control = new Controller();

FoldingMachine folder = new FoldingMachine();

WeldingMachine welder = new WeldingMachine();

PaintingMachine painter = new PaintingMachine();

...

control.Add(folder.StopFolding);

control.Add(welder.FinishWelding);

control.Add(painter.PaintOff);

...

control.ShutDown();

...

Объявление и использование делегатов

В следующих упражнениях вы завершите разработку приложения, формирующего часть системы для компании Wide World Importers, которая занимается импортом и продажей строительных материалов и инструментов. Приложение, над которым вы будете работать, дает клиентам возможность просматривать товарные позиции, имеющиеся на складах компании Wide World Importers, и размещать заказы на них. Приложение содержит форму, показывающую доступные на данный момент товары, а также панель со списком уже выбранных клиентом товаров. Когда клиент желает разместить заказ, он может щелкнуть на кнопке формы Checkout (Разместить заказ). Затем заказ обрабатывается и панель очищается.

На момент размещения клиентом заказа производится несколько действий.

• Клиент получает требование об оплате.

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

• Для доставки товара формируется сопроводительная накладная, содержащая краткие сведения о заказе.

Логика процессов проверки и доставки не зависит от логики размещения заказа, и порядок, в котором эти процессы происходят, роли не играет. Более того, любой из этих элементов в будущем может быть откорректирован, и по мере того как в перспективе станут изменяться условия ведения бизнеса или нормативные требования, для операции размещения заказа может потребоваться дополнительная обработка. Поэтому, чтобы проще было сопровождать и корректировать программу, желательно отделить логику оплаты и оформления заказа от процессов проверки и доставки. Начнем с исследования приложения, чтобы увидеть, что на данный момент оно не в состоянии выполнить эту задачу. Затем вы измените структуру приложения, чтобы удалить зависимости между логикой размещения заказа и логикой проверки и доставки.

Исследование приложения Wide World Importers

Откройте в среде Microsoft Visual Studio 2015 проект Delegates, который находится в папке \Microsoft Press\VCSBS\Chapter 20\Delegates вашей папки документов.

Щелкните в меню Отладка на пункте Начать отладку. Проект будет собран и запущен. Появится форма, показывающая доступные товары, а также панель с данными о заказе (изначально она пуста). Приложение отображает товары в элементе управления GridView, который позволяет осуществлять горизонтальную прокрутку (рис. 20.1).

20_01.tif 

Рис. 20.1

Выберите один или несколько товаров, после чего щелкните на кнопке Add (Добавить), чтобы включить их в корзину покупателя. При этом выберите хотя бы один товар с возрастными ограничениями на приобретение (с пометкой Age Restricted Yes). Как только товар будет добавлен, он тут же появится на расположенной справа панели Order Details (Сведения о заказе). Обратите внимание на то, что при многократном добавлении одного и того же товара его количество с каждым щелчком будет увеличиваться. (В этой версии приложения возможность удаления товара из корзины не реализована.)

На панели Order Details щелкните на кнопке Checkout (Разместить заказ). Появится сообщение, показывающее, что заказ был размещен. Заказу присваивается уникальный идентификационный номер (ID), и этот ID отображается вместе со стоимостью заказа. Щелкните на кнопке Закрыть, чтобы убрать сообщение с экрана, а затем вернитесь в среду Visual Studio 2015, чтобы остановить отладку.

Раскройте в обозревателе решений узел проекта Delegates, после чего откройте файл Package.appxmanifest. Появится конструктор манифеста приложения. Щелкните в нем на вкладке Упаковка (Packaging). Обратите внимание на значение в поле Имя пакета (Package Name). Оно имеет форму глобально-уникального идентификатора (globally unique identifier (GUID)).

Используя Проводник, зайдите в папку %USERPROFILE%\AppData\Local\Packages\yyy\LocalState, где yyy — это значение идентификатора, начинающегося с GUID, на который вы только что обратили внимание. Это локальная папка для приложения Wide World Importers. В ней вы должны увидеть два файла, один с именем audit-nnnnnn.xml (где nnnnnn — идентификационный номер показанного ранее заказа), а другой с именем dispatch-nnnnnn.txt. Первый файл был создан проверочным компонентом приложения, а второй является сопроводительной накладной, созданной компонентом доставки товара.

174199.png

ПРИМЕЧАНИЕ Если файла audit-nnnnnn.xml не будет, значит вы при размещении заказа не выбрали ни одного товара с возрастными ограничениями на приобретение. В таком случае вернитесь в приложение и создайте новый заказ, включающий один или несколько таких товаров.

Откройте в среде Visual Studio файл audit-nnnnnn.xml. В нем содержатся список имеющихся в заказе товаров с возрастными ограничениями на приобретение, а также номер и дата заказа. Он должен иметь следующий вид (рис. 20.2).

20_02.tif 

Рис. 20.2

Когда закончите изучение списка, закройте файл в среде Visual Studio.

Откройте в Блокноте файл dispatch-nnnnnn.txt. Он содержит краткие сведения о заказе с перечислением ID заказа и его стоимости. Он должен иметь примерно следующий вид (рис. 20.3).

20_03.tif 

Рис. 20.3

Закройте Блокнот, вернитесь в среду Visual Studio 2015 и остановите отладку.

Обратите внимание на то, что в среде Visual Studio решение состоит из следующих проектов.

• Delegates. Этот проект содержит само приложение. Пользовательский интерфейс определен в файле MainPage.xaml, а логика приложения содержится в файле MainPage.xaml.cs.

• AuditService. Этот проект содержит компоненты, реализующие процесс проверки. Он упакован как библиотека классов и содержит всего один класс Auditor. Этот класс предоставляет единственный открытый метод AuditOrder, который проверяет заказ и создает файл audit-nnnnnn.xml, если в заказе содержится какой-либо товар с возрастными ограничениями на приобретение.

• DeliveryService. Этот проект содержит компонент, выполняющий логику доставки, упакованный как библиотека классов. Функциональные средства, связанные с доставкой, содержат класс Shipper, предоставляющий открытый метод ShipOrder, который обрабатывает процесс доставки, а также создает сопроводительную накладную.

174206.png

ПРИМЕЧАНИЕ Вы, конечно, можете изучить код классов Auditor и Shipper classes, но глубоко вникать во внутреннюю работу этих компонентов в данном приложении нет никакой необходимости.

• DataTypes. Этот проект содержит типы данных, используемые другими проектами. В классе Product определяются особенности товаров, показываемых приложением, а данные товаров хранятся в классе ProductsDataSource. (В данный момент в приложении используется небольшой жестко заданный набор товаров. В производственной системе эта информация должна извлекаться из базы данных или из веб-сервиса.) В классах Order и OrderItem реализуется структура заказа, в каждом заказе (order) содержится одна или несколько товарных позиций (order items).

Выведите в окно редактора файл MainPage.xaml.cs, принадлежащий проекту Delegates, и изучите в нем закрытые поля и конструктор MainPage. Для нас важны следующие элементы:

...

private Auditor auditor = null;

private Shipper shipper = null;

public MainPage()

{

    ...

    this.auditor = new Auditor();

    this.shipper = new Shipper();

}

В полях auditor и shipper содержатся ссылки на экземпляры классов Auditor и Shipper, а конструктор создает экземпляры этих объектов.

Найдите метод CheckoutButtonClicked. Этот метод запускается после щелчка пользователя на кнопке Checkout, предназначенной для размещения заказа. Первые несколько строк имеют следующий вид:

private void CheckoutButtonClicked(object sender, RoutedEventArgs e)

{

    try

    {

        // Выполнение обработки, связанной с размещением заказа

        if (this.requestPayment())

        {

            this.auditor.AuditOrder(this.order);

            this.shipper.ShipOrder(this.order);

        }

        ...

    }

    ...

}

Это метод реализует обработку, связанную с размещением заказа. Он требует от пользователя оплаты заказа, после чего задействует метод AuditOrder объекта auditor, а затем метод ShipOrder объекта shipper. Сюда будет добавляться любая востребуемая в будущем дополнительная бизнес-логика. Весь остальной код этого метода (после инструкции if) занимается управлением пользовательским интерфейсом: отображением для пользователя окна сообщения и очисткой панели сведений о заказе Order Details.

174212.png

ПРИМЕЧАНИЕ В целях упрощения кода имеющийся в данном приложении метод requestPayment в настоящий момент просто возвращает true, чтобы показать, что платеж был получен. В реальном мире этот метод должен был бы выполнить полноценную обработку платежа.

Хотя приложение работает в точном соответствии с тем, как было анонсировано, компоненты Auditor и Shipper довольно сильно интегрированы в обработку, связанную с размещением заказа. Если эти компоненты будут изменены, приложение придется обновлять. К тому же, если придется вносить в процесс, связанный с размещением заказа, какую-либо дополнительную логику, возможно, выполняемую с использованием других компонентов, эту часть приложения придется переработать.

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

Создание компонента CheckoutController

В обозревателе решений щелкните правой кнопкой мыши на решении Delegates, укажите на пункт Добавить, а затем щелкните на пункте Создать проект. Раскройте в левой панели диалогового окна Добавить новый проект узел Windows, а затем щелкните на узле Универсальные. Выберите в средней панели шаблон Библиотека классов (Универсальное приложение Windows). Наберите в поле Имя строку CheckoutService, а затем щелкните на кнопке OK.

В обозревателе решений раскройте проект CheckoutService, щелкните правой кнопкой мыши на файле Class1.cs и на пункте Переименовать. Измените имя файла на CheckoutController.cs, а затем нажмите Ввод. Получив предложение, позвольте среде Visual Studio переименовать все ссылки на Class1 в ссылки на CheckoutController. В проекте CheckoutService щелкните правой кнопкой мыши на узле Ссылки, а затем щелкните на пункте Добавить ссылку. Щелкните в левой панели диалогового окна Менеджер ссылок — CheckoutService на пункте Решение. Выберите в средней панели проект DataTypes, а затем щелкните на кнопке OK. Класс CheckoutController будет использовать класс Order, определенный в проекте DataTypes.

Выведите в окно редактора файл CheckoutController.cs и добавьте к списку в самом начале файла следующую директиву using:

using DataTypes;

Добавьте к классу CheckoutController открытый тип делегата по имени CheckoutDelegate, выделенный в следующем примере кода жирным шрифтом:

public class CheckoutController

{

    public delegate void CheckoutDelegate(Order order);

}

Этот тип делегата можно использовать для ссылки на методы, получающие параметр типа Order и не возвращающие результат. Это соответствует профилю методов AuditOrder и ShipOrder классов Auditor и Shipper.

Добавьте основанный на этом типе делегата открытый делегат по имени CheckoutProcessing:

public class CheckoutController

{

    public delegate void CheckoutDelegate(Order order);

    public CheckoutDelegate CheckoutProcessing = null;

}

Выведите в окно редактора файл MainPage.xaml.cs проекта Delegates и найдите метод requestPayment (в конце файла). Вырежьте этот метод из класса MainPage. Вернитесь к файлу CheckoutController.cs и вставьте метод requestPayment в класс CheckoutController, добавив тем самым к нему код, выделенный в следующем примере жирным шрифтом:

public class CheckoutController

{

    public delegate void CheckoutDelegate(Order order);

    public CheckoutDelegate CheckoutProcessing = null;

 

    private bool requestPayment()

    {

        // Здесь производится обработка платежа

 

        // Логика обработки платежа в данном примере не реализована,

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

        return true;

    }

}

Добавьте к классу CheckoutController метод StartCheckoutProcessing, выделенный здесь жирным шрифтом:

public class CheckoutController

{

    public delegate void CheckoutDelegate(Order order);

    public CheckoutDelegate CheckoutProcessing = null;

 

    private bool requestPayment()

    {

        ...

    }

    public void StartCheckoutProcessing(Order order)

    {

        // Обработка данных для размещения заказа

        if (this.requestPayment())

        {

            if (this.CheckoutProcessing != null)

            {

                this.CheckoutProcessing(order);

            }

        }

    }

}

Этот метод предоставляет функциональные средства для размещения заказа, ранее реализованные в методе CheckoutButtonClicked класса MainPage. Он запрашивает проведение оплаты заказа, а затем исследует делегата CheckoutProcessing: если его значение не равно null (то есть он ссылается на один или несколько методов), метод задействует делегата. В этом случае запускаются все методы, на которые ссылается этот делегат.

В обозревателе решений в проекте Delegates щелкните правой кнопкой мыши на узле Ссылки, а затем щелкните на пункте Добавить ссылку. Щелкните на левой панели диалогового окна Менеджер ссылок — Delegates на пункте Решение. Выберите в средней панели проект CheckoutService, а затем щелкните на кнопке OK.

Вернитесь к файлу MainPage.xaml.cs проекта Delegates и добавьте к списку в его начале следующую директиву using:

using CheckoutService;

Добавьте к классу MainPage показанную жирным шрифтом закрытую переменную по имени checkoutController, имеющую тип CheckoutController, и инициализируйте ее значением null:

public ... class MainPage : ...

{

    ...

    private Auditor auditor = null;

    private Shipper shipper = null;

    private CheckoutController checkoutController = null;

    ...

}

Найдите конструктор MainPage. Создайте после инструкций, создающих компоненты Auditor и Shipper, экземпляр компонента CheckoutController, выделенный здесь жирным шрифтом:

public MainPage()

{

    ...

    this.auditor = new Auditor();

    this.shipper = new Shipper();

    this.checkoutController = new CheckoutController();

}

Добавьте к конструктору после только что введенной инструкции следующие инструкции, выделенные жирным шрифтом:

public MainPage()

{

    ...

    this.checkoutController = new CheckoutController();

    this.checkoutController.CheckoutProcessing += this.auditor.AuditOrder;

    this.checkoutController.CheckoutProcessing += this.shipper.ShipOrder;

}

Этот код добавляет к делегату CheckoutProcessing объекта CheckoutController ссылки на методы AuditOrder и ShipOrder объектов Auditor и Shipper.

Найдите метод CheckoutButtonClicked. Замените в блоке try код, выполняющий обработку данных для размещения заказа (блок инструкции if), инструкцией, показанной здесь жирным шрифтом:

private void CheckoutButtonClicked(object sender, RoutedEventArgs e)

{

    try

    {

        // Обработка данных для размещения заказа

        this.checkoutController.StartCheckoutProcessing(this.order);

 

        // Вывод на экран кратких сведений о заказе

    ...

    }

    ...

}

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

Тестирование приложения

Щелкните в меню Отладка на пункте Начать отладку. Приложение будет собрано и запущено. Когда появится форма Wide World Importers, выберите несколько товаров (включая хотя бы один с возрастными ограничениями на его приобретение), а затем щелкните на кнопке Checkout.

Когда появится сообщение о размещении заказа — Order Placed, запишите номер заказа, после чего щелкните на кнопке Close или OK.

Перейдите в Проводник и зайдите в папку %USERPROFILE%\AppData\Local\Packages\yyy\LocalState, где yyy является значением идентификатора, начинающегося с ранее записанного GUID. Убедитесь в том, что были созданы новые файлы audit-nnnnnn.xml и dispatch-nnnnnn.txt, где nnnnnn является числом, идентифицирующим новый заказ. Изучите эти файлы и убедитесь в том, что в них содержатся данные заказа.

Вернитесь в среду Visual Studio 2015 и остановите отладку.

Лямбда-выражения и делегаты

Во всех показанных до сих пор примерах добавления метода к делегату использовалось имя метода. Например, в ранее рассмотренном сценарии автоматизированной фабрики метод StopFolding объекта folder добавлялся к делегату stopMachinery следующим образом:

this.stopMachinery += folder.StopFolding;

Этот подход хорош в том случае, когда имеется метод, соответствующий сигнатуре делегата. А что делать, если у метода StopFolding будет следующая сигнатура:

void StopFolding(int shutDownTime); // Завершение работы через указанное количество

                                    // секунд

Теперь эта сигнатура отличается от сигнатуры методов FinishWelding и PaintOff, и поэтому использовать одного и того же делегата для управления всеми тремя методами нельзя. Так что делать?

Создание метода-адаптера

Одним из способов обхода данной проблемы является создание еще одного метода по имени FinishFolding, но такого, который сам по себе никаких параметров не принимает:

void FinishFolding()

{

    folder.StopFolding(0); // Немедленное завершение работы

}

Затем метод FinishFolding можно добавить к делегату stopMachinery вместо метода StopFolding, используя для этого тот же синтаксис, что и раньше:

this.stopMachinery += folder.FinishFolding;

При задействовании делегат stopMachinery вызывает FinishFolding, который в свою очередь вызывает метод StopFolding, передавая ему параметр 0.

174219.png

ПРИМЕЧАНИЕ Метод FinishFolding является классическим примером адаптера — метода, который преобразует (или адаптирует) метод, чтобы придать ему другую сигнатуру. Этот шаблон используется довольно широко и относится к набору шаблонов, описание которых дано в книге Эриха Гамма (Erich Gamma), Ричарда Хелма (Richard Helm), Ральфа Джонсона (Ralph Johnson) и Джона Влиссидеса (John Vlissides) «Приемы объектно-ориентированного проектирования. Паттерны проектирования» (Addison-Wesley Professional, 1994).

Зачастую такие методы-адаптеры имеют небольшой размер и запросто могут затеряться среди целого моря методов, особенно в большом по объему классе. Более того, метод вряд ли будет вызываться, за исключением тех случаев, когда он используется для адаптации метода StopFolding при использовании его делегатом. Для подобных ситуаций в C# предоставляется лямбда-выражение. Такие выражения рассматривались в главе 18, и примеры с ними ранее уже не раз были показаны в данной главе. В сценарии фабрики можно использовать следующее лямбда-выражение:

this.stopMachinery += (() => folder.StopFolding(0));

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

Включение уведомлений путем использования событий

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

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

Объявление события

Событие объявляется в классе, предназначенном для действия в качестве источника события. Обычно источником события является класс, отслеживающий свою среду и инициирующий событие, когда происходит что-либо значимое. На автоматизированной фабрике источником события может быть класс, отслеживающий температуру каждой машины. Такой класс будет инициировать событие «перегрев машины», если обнаружит, что у машины превышен порог теплового излучения, то есть она перегрелась. При инициировании события у него есть список методов, подлежащих вызову. Иногда эти методы называют подписчиками. Эти методы должны быть подготовлены к обработке события «перегрев машины» и к принятию необходимого для исправления ситуации действия — остановке машин.

Событие объявляется точно так же, как и поле. Но поскольку события предназначены для использования с делегатами, типом события должен быть делегат, а перед объявлением должно ставиться ключевое слово event. Для объявления событий следует использовать такой синтаксис:

event delegateTypeName eventName

Возьмем, к примеру, делегата StopMachineryDelegate из автоматизированной фабрики. Он был перемещен в класс по имени TemperatureMonitor, который предоставляет интерфейс для различных датчиков контроля температуры оборудования (по логике это место больше подходит для события, чем класс Controller):

class TemperatureMonitor

{

    public delegate void StopMachineryDelegate();

    ...

}

Событие MachineOverheating, задействующее делегата stopMachineryDelegate, можно определить следующим образом:

class TemperatureMonitor

{

    public delegate void StopMachineryDelegate();

    public event StopMachineryDelegate MachineOverheating;

    ...

}

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

Подписка на событие

Как и делегаты, события поставляются в готовом виде с оператором +=. С помощью этого оператора осуществляется подписка на событие. В автоматизированной фабрике программные средства, управляющие каждой машиной, могут быть подготовлены под вызов методов, останавливающих их работу, при выдаче события MachineOverheating следующим образом:

class TemperatureMonitor

{

    public delegate void StopMachineryDelegate();

    public event StopMachineryDelegate MachineOverheating;

    ...

}

...

TemperatureMonitor tempMonitor = new TemperatureMonitor();

...

tempMonitor.MachineOverheating += (() => { folder.StopFolding(0); });

tempMonitor.MachineOverheating += welder.FinishWelding;

tempMonitor.MachineOverheating += painter.PaintOff;

Обратите внимание на то, что здесь используется точно такой же синтаксис, что и при добавлении метода к делегату. Можно даже подписаться на событие, используя лямбда-выражение. Когда будет инициировано событие tempMonitor.MachineOverheating, оно вызовет все подписавшиеся на него методы и остановит машины.

Отмена подписки на событие

Зная, что для прикрепления делегата к событию используется оператор +=, можно догадаться, что для открепления делегата от события используется оператор –=. Вызов оператора –= приводит к удалению метода из внутренней коллекции делегата, принадлежащего событию. Зачастую это действие называют отменой подписки на событие.

Инициирование события

Инициировать событие можно, вызвав его наподобие метода. При инициировании события вызываются по порядку все прикрепленные делегаты. Например, в следующем фрагменте кода показан класс TemperatureMonitor с закрытым методом Notify, инициирующим событие MachineOverheating:

class TemperatureMonitor

{

    public delegate void StopMachineryDelegate();

    public event StopMachineryDelegate MachineOverheating;

    ...

    private void Notify()

    {

        if (this.MachineOverheating != null)

        {

            this.MachineOverheating();

        }

    }

    ...

}

Это общая идиома. Проверка на null необходима, поскольку поле события изначально имеет значение null и становится непустым, только когда на событие с использованием оператора += подписывается какой-нибудь метод. Если попытаться инициировать событие, имеющее значение null, будет выдано исключение NullReferenceException. Если делегат, определяющий событие, ожидает получения каких-либо параметров, то при инициировании события должны быть предоставлены соответствующие аргументы. Примеры этого будут показаны чуть позже.

174928.png

ВНИМАНИЕ У событий есть очень полезное встроенное свойство обеспечения безопасности. Открытое событие, такое как MachineOverheating, может быть инициировано только методами в том классе, в котором оно определено (в классе TemperatureMonitor). Любые попытки инициировать событие за пределами класса приведут к тому, что компилятор выдаст ошибку.

Основные сведения о событиях пользовательского интерфейса

Как уже упоминалось, события широко применяются классами .NET Framework и элементами управления, используемыми для создания графических интерфейсов пользователя. Например, класс Button, являющийся производным от класса ButtonBase, наследует открытое событие по имени Click, имеющее тип RoutedEventHandler. Делегат RoutedEventHandler ожидает передачи двух параметров: ссылки на объект, ставший причиной инициирования события, и объекта RoutedEventArgs, который содержит дополнительную информацию о событии:

public delegate void RoutedEventHandler(Object sender, RoutedEventArgs e);

Класс Button имеет следующий вид:

public class ButtonBase: ...

{

    public event RoutedEventHandler Click;

    ...

}

 

public class Button: ButtonBase

{

    ...

}

Когда происходит щелчок на показанной на экране кнопке, класс Button автоматически инициирует событие Click. Это обстоятельство упрощает создание делегата для избранного метода и прикрепление этого делегата к требуемому событию. В следующем примере показан пример кода для UWP-формы, содержащей кнопку, названную okay, и код для подключения к событию Click кнопки okay к методу okayClick:

partial class MainPage :

    global::Windows.UI.Xaml.Controls.Page,

    global::Windows.UI.Xaml.Markup.IComponentConnector,

    global::Windows.UI.Xaml.Markup.IComponentConnector2

{

    ...

    public void Connect(int connectionId, object target)

    {

        switch(connectionId)

        {

            case 1:

            {

                this.okay = (global::Windows.UI.Xaml.Controls.Button)(target);

                ...

                ((global::Windows.UI.Xaml.Controls.Button)this.okay).Click +=

                                                          this.okayClick;

                ...

            }

            break;

        default:

            break;

        }

        this._contentLoaded = true;

    }

    ...

}

Обычно этот код скрыт от ваших глаз. При использовании в среде Visual Studio 2015 окна конструктора и установке в описании формы на языке XAML для свойства Click кнопки okay метода okayClick среда Visual Studio 2015 создает этот код самостоятельно. Вам останется только записать логику своего приложения в обрабатывающем событие методе okayClick, в той части кода, к которой у вас есть доступ и которая в данном случае находится в файле MainPage.xaml.cs:

public sealed partial class MainPage : Page

{

    ...

    private void okayClick(object sender, RoutedEventArgs e)

    {

        // Ваш код для обработки события Click

    }

}

События, инициируемые различными элементами управления пользовательского графического интерфейса, неизменно развиваются по этому шаблону. События являются типом делегата, чья сигнатура имеет тип возвращаемого значения void и два аргумента. Первым аргументом всегда является отправитель (источник) события — sender, вторым аргументом всегда является экземпляр класса EventArgs (или класса, производного от EventArgs).

Наличие аргумента sender позволяет многократно использовать один и тот же метод для нескольких событий. Метод делегата может исследовать аргумент sender и отреагировать на него соответствующим образом. Например, один и тот же метод можно использовать для подписки на событие Click для двух кнопок. (Один и тот же метод добавляется к двум разным событиям.) При инициировании события имеющийся в методе код может исследовать аргумент sender, чтобы выяснить, на какой из кнопок был сделан щелчок.

Использование событий

В предыдущем упражнении вы внесли в приложение Wide World Importers изменения, позволяющие отделить логику проверки и доставки заказа от процесса его размещения. Созданный вами класс CheckoutController вызывает компоненты проверки и доставки путем использования делегата и ничего не знает об этих компонентах или о запускаемых ими методах — эта обязанность возлагается на приложение, создающее объект типа CheckoutController и добавляющее соответствующие ссылки к делегату. Но, может быть, компоненту было бы полезно иметь возможность оповестить приложение о завершении возложенной на него обработки данных и позволить приложению навести необходимый порядок.

Поначалу это может показаться несколько странным, поскольку есть уверенность в том, что при задействовании приложением делегата в объекте типа CheckoutController все методы, на которые ссылается этот делегат, будут запущены и приложение продолжит работу, выполняя следующую инструкцию только после того, как эти методы завершат выполнение своей задачи. Но так бывает не всегда! В главе 24 «Сокращение времени отклика путем выполнения асинхронных операций» показано, что методы могут запускаться в асинхронном режиме и при вызове метода он может не завершить свою работу до того, как выполнение приложения продолжится с использованием следующей инструкции. Это особенно актуально для UWP-приложений, в которых затратные по времени операции выполняются в потоках, запускаемых в фоновом режиме, чтобы позволить пользовательскому интерфейсу восстановить свою способность к откликам. В методе CheckoutButtonClicked приложения Wide World Importers за кодом, который задействует делегата, следует инструкция, выводящая диалоговое окно с сообщением о том, что заказ был размещен:

private void CheckoutButtonClicked(object sender, RoutedEventArgs e)

{

    try

    {

        // Выполнение действий, связанных с размещением заказа

        this.checkoutController.StartCheckoutProcessing(this.order);

 

        // Вывод общих сведений о заказе

        MessageDialog dlg = new MessageDialog(...);

        dlg.ShowAsync();

        ...

    }

    ...

}

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

В следующем упражнении вы внесете изменения в классы Auditor и Shipper, позволяющие инициировать событие, возникающее только при условии завершения ими возложенной на них обработки данных. Приложение будет подписано на событие каждого из компонентов и при его возникновении выведет на экран соответствующее сообщение.

Добавление события к классу CheckoutController

Выведите в среде Visual Studio 2015 решение Delegates. Откройте в окне редактора файл Auditor.cs, принадлежащий проекту AuditService. Дополните класс Auditor открытым делегатом по имени AuditingCompleteDelegate. Этот делегат укажет на метод, получающий строковый параметр по имени message и возвращающий значение типа void. Определение этого делегата показано жирным шрифтом:

class Auditor

{

    public delegate void AuditingCompleteDelegate(string message);

    ...

}

Добавьте к классу Auditor после делегата AuditingCompleteDelegate открытое событие по имени AuditProcessingComplete. Это событие, показанное жирным шрифтом, должно быть основано на делегате AuditingCompleteDelegate:

class Auditor

{

    public delegate void AuditingCompleteDelegate(string message);

    public event AuditingCompleteDelegate AuditProcessingComplete;

    ...

    }

Найдите метод AuditOrder. Это метод, запускаемый при задействовании делегата в CheckoutController-объекте. Он вызывает еще один закрытый метод по имени doAuditing для реального выполнения операции проверки. Метод имеет следующий вид:

public void AuditOrder(Order order)

{

    this.doAuditing(order);

}

Прокрутите код до метода doAuditing. Код этого метода заключен в блок try-catch. Для создания XML-представления проверяемого заказа и сохранения его в файле им используются XML API-интерфейсы библиотеки классов среды .NET Framework. (Подробности внутренней работы этих интерфейсов в данной книге не рассматриваются.)

Добавьте в поле блока catch блок finally, инициирующий событие Audit­Pro­cessing­Complete и показанный далее жирным шрифтом:

private async void doAuditing(Order order)

{

    List<OrderItem> ageRestrictedItems = findAgeRestrictedItems(order);

    if (ageRestrictedItems.Count > 0)

    {

        try

        {

            ...

        }

        catch (Exception ex)

        {

            ...

        }

        finally

        {

            if (this.AuditProcessingComplete != null)

            {

                this.AuditProcessingComplete(

                    $"Audit record written for Order {order.OrderID}");

            }

        }

    }

}

Откройте в окне редактора файл Shipper.cs, принадлежащий проекту DeliveryService. Дополните класс Shipper открытым делегатом ShippingCompleteDelegate. Этот делегат укажет на метод, получающий строковый параметр по имени message и возвращающий значение типа void. Определение делегата показано далее жирным шрифтом:

class Shipper

{

    public delegate void ShippingCompleteDelegate(string message);

    ...

}

Добавьте к классу Shipper показанное жирным шрифтом открытое событие ShipProcessingComplete, основанное на делегате ShippingCompleteDelegate:

class Shipper

{

    public delegate void ShippingCompleteDelegate(string message);

    public event ShippingCompleteDelegate ShipProcessingComplete;

    ...

}

Найдите метод doShipping, выполняющий логику доставки. После имеющегося в этом методе блока catch добавьте показанный далее жирным шрифтом блок finally, инициирующий событие ShipProcessingComplete:

private async void doShipping(Order order)

{

    try

    {

        ...

    }

    catch (Exception ex)

    {

        ...

    }

    finally

    {

        if (this.ShipProcessingComplete != null)

        {

            this.ShipProcessingComplete(

                $"Dispatch note generated for Order {order.OrderID}");

        }

    }

}

Выведите в окно конструктора разметку для файла MainPage.xaml, принадлежащего проекту Delegates. В панели XAML прокрутите код до первой установки значений для элементов RowDefinition. Код в формате XAML должен иметь следующий вид:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

    <Grid Margin="12,0,12,0" Loaded="MainPageLoaded">

        <Grid.RowDefinitions>

            <RowDefinition Height="*"/>

            <RowDefinition Height="2*"/>

            <RowDefinition Height="*"/>

            <RowDefinition Height="10*"/>

            <RowDefinition Height="*"/>

    </Grid.RowDefinitions>

    ...

Измените значение свойства Height последнего элемента RowDefinition, показанного далее жирным шрифтом, на 2*:

<Grid.RowDefinitions>

    ...

    <RowDefinition Height="10*"/>

    <RowDefinition Height="2*"/>

</Grid.RowDefinitions>

Это изменение разметки делает доступным пространство в нижней части формы, которое будет использоваться в качестве области для отображения сообщений, получаемых от компонентов Auditor и Shipper после того, как они инициируют свои события. Более подробно разметка пользовательского интерфейса с помощью элемента управления Grid рассматривается в главе 25 «Реализация пользовательского интерфейса для приложений универсальной платформы Windows».

Прокрутите код в XAML-панели до самого конца. Добавьте перед предпоследним тегом </Grid> следующие выделенные жирным шрифтом элементы ScrollViewer и TextBlock:

            ...

            </Grid>

            <ScrollViewer Grid.Row="4" VerticalScrollBarVisibility="Visible">

                <TextBlock x:Name="messageBar" FontSize="18" />

            </ScrollViewer>

        </Grid>

        </Grid>

    </Page>

Эта разметка добавляет к области в нижней части экрана элемент управления TextBlock по имени messageBar. Этот элемент управления будет использоваться для вывода сообщений от объектов типа Auditor и Shipper.

Выведите в окно редактора файл MainPage.xaml.cs. Найдите метод Checkout­ButtonClicked и удалите код, выводящий на экран краткие сведения о заказе. После этого блок try должен приобрести следующий вид:

private void CheckoutButtonClicked(object sender, RoutedEventArgs e)

{

    try

    {

        // Выполнение действий, связанных с размещением заказа

        this.checkoutController.StartCheckoutProcessing(this.order);

 

        // Удаление сведений о заказе, чтобы пользователь мог

        // начать все сначала и сделать новый заказ

 

        this.order = new Order { Date = DateTime.Now, Items = new

        List<OrderItem>(),

        OrderID = Guid.NewGuid(), TotalValue = 0 };

        this.orderDetails.DataContext = null;

        this.orderValue.Text = $"{order.TotalValue:C}");

        this.listViewHeader.Visibility = Visibility.Collapsed;

        this.checkout.IsEnabled = false;

    }

    catch (Exception ex)

    {

    ...

    }

}

Добавьте к классу MainPage закрытый метод по имени displayMessage. Этот метод будет получать строковый параметр по имени message и возвращать значение типа void. Добавьте к телу этого метода инструкцию, которая присоединяет значение параметра message к свойству Text элемента управления messageBar TextBlock, за которым следует символ новой строки (добавляемый код выделен жирным шрифтом):

private void displayMessage(string message)

{

    this.messageBar.Text += message + "\n";

}

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

Найдите конструктор для класса MainPage и добавьте к нему код, выделенный жирным шрифтом:

public MainPage()

{

    ...

    this.auditor = new Auditor();

    this.shipper = new Shipper();

    this.checkoutController = new CheckoutController();

    this.checkoutController.CheckoutProcessing += this.auditor.AuditOrder;

    this.checkoutController.CheckoutProcessing += this.shipper.ShipOrder;

 

    this.auditor.AuditProcessingComplete += this.displayMessage;

    this.shipper.ShipProcessingComplete += this.displayMessage;

}

Эти инструкции осуществляют подписку на события, предоставляемые объектами типа Auditor и Shipper. При инициировании событий запускается метод displayMessage. Обратите внимание на то, что обработка обоих событий осуществляется одним и тем же методом.

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

Когда появится форма Wide World Importers, выберите несколько товаров, включая хотя бы один с возрастными ограничениями на его приобретение, а затем щелкните на кнопке Checkout.

Убедитесь в том, что в элементе управления TextBlock в нижней части формы сначала появляется сообщение о выполнении записи с результатом проверки «Audit record written», а затем сообщение о создании сопроводительной накладной «Dispatch note generated» (рис. 20.4).

20_04.tif 

Рис. 20.4

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

Завершив работу, вернитесь в среду Visual Studio 2015 и остановите отладку.

Выводы

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

Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 21.

Если сейчас вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Увидев диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.

Краткий справочник

Чтобы

Сделайте следующее

Объявить тип делегата

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

delegate void myDelegate();

Создать экземпляр делегата, инициализированный одним указанным методом

Воспользуйтесь таким же синтаксисом, который использовался для класса или структуры: напишите ключевое слово new, после него укажите имя типа (имя делегата), а дальше в круглых скобках укажите аргументы. Аргумент должен быть методом, чья сигнатура в точности совпадает с сигнатурой делегата, например:

delegate void myDelegate();

private void myMethod() { ... }

...

myDelegate del = new myDelegate(this.myMethod);

Вызвать делегата

Используйте такой же синтаксис, что и для вызова метода, например:

myDelegate del;

...

del();

Объявить событие

Напишите ключевое слово event, затем укажите имя типа (он должен относиться к делегатам), после чего укажите имя события, например:

class MyClass

{

    public delegate void MyDelegate();

 

    ...

    public event myDelegate MyEvent;

}

Подписаться на событие

Создайте экземпляр делегата (того же типа, что и событие) и присоедините его к событию, воспользовавшись оператором +=, например:

class MyEventHandlingClass

{

    private MyClass myClass = new MyClass();

    ...

    public void Start()

    {

    myClass.MyEvent += new myClass.MyDelegate

        (this.eventHandlingMethod);

    }

 

    private void eventHandlingMethod()

    {

        ...

    }

}

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

public void Start()

{

    myClass.MyEvent += this.eventHandlingMethod;

}

Отменить подписку на событие

Создайте экземпляр делегата (того же типа, что и событие) и отсоедините его от события, воспользовавшись оператором –=, например:

class MyEventHandlingClass

{

    private MyClass myClass = new MyClass();

    ...

    public void Stop()

    {

        myClass.MyEvent -= new myClass.MyDelegate

            (this.eventHandlingMethod);

    }

    ...

}

Или:

public void Stop()

{

    myClass.MyEvent -= this.eventHandlingMethod;

}

Инициировать событие

Воспользуйтесь таким же синтаксисом, что и при вызове метода. Предоставляемые аргументы должны совпадать по типу с параметрами, ожидаемыми делегатом, на которого ссылается событие. Не забудьте проверить событие на содержание null-значения, например:

class MyClass

{

    public event myDelegate MyEvent;

    ...

    private void RaiseEvent()

    {

        if (this.MyEvent != null)

        {

            this.MyEvent();

        }

    }

    ...

}

Назад: 19. Перечисляемые коллекции
Дальше: 21. Запрос данных, находящихся в памяти, с помощью выражений в виде запросов

Антон
Перезвоните мне пожалуйста 8(812)642-29-99 Антон.
Антон
Перезвоните мне пожалуйста по номеру 8(904) 332-62-08 Антон.
Антон
Перезвоните мне пожалуйста, 8 (904) 606-17-42 Антон.
Антон
Перезвоните мне пожалуйста по номеру. 8 (953) 367-35-45 Антон
Ксения
Текст от профессионального копирайтера. Готово через 1 день. Консультация бесплатно. Жми roholeva(точка)com