Этюды в тональности C#.

(с) Герман Иванов.http://german2004.da.ru

TV паттерн программирования. (Статья 2)

В предыдущей статье мы с вами рассмотрели достоинства и недостатки MVC паттерна программирования, и начали создавать приложение, базирующееся на паттерне, использовавшемся в библиотеке Turbo Vision. Эту библиотеку фирма Borland выпустила для своего компилятора Turbo Pascal 6. 0, еще в добрые старые времена господства MSDOS.

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

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

В библиотеке Turbo Vision команды были объявлены как именованные константы, которые в дальнейшем упаковывались в структуру(record), именуемую событием. Сама идеология паттерна TV, подразумевает, что команда, возникшая в одном из блоков нашего кода, должна посетить, в идеале, все остальные блоки кода. Поэтому, мы с вами, изначально обречены на многочисленные вызовы разных методов, которым эта команда передается в качестве параметра. С помощью этих методов объекты будут спихивать возникающие события друг-другу.

В языке C# структура (struct) является типом данных, передаваемым по значению, в противовес объектам, передаваемым как ссылка (указатель на объект). Поэтому если мы с вами пойдем по пути, предложенному Borland, нам придется гонять взад вперед по нашему приложению довольно таки объемные блоки данных. В случае же использования объекта, мы с вами передаем всего лишь указатель на него, что намного быстрее. Поэтому использование объектов, в нашей ситуации, более предпочтительно.

Вместе с тем, реализуя события в виде объекта, мы сильно захламляем "кучу". Для каждого вновь генерируемого события, выделяется оперативная память, которая впоследствии, по уничтожению события, возвращается обратно сборщиком мусора (Garbage Collector). Сборка мусора процесс неторопливый, поэтому наша программа, теоретически, может занимать под себя довольно много оперативной памяти. Это не есть хорошо. Впрочем, мы можем с этим бороться, принудительно вызывая Garbage Collector, сразу после того, как мы обработали много команд подряд.

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

Прежде чем создавать сам объект события, давайте решим, как будет выглядеть идентификатор команды. Надо же нам как-то выяснять, какая именно команда вызвала то или иное событие? На мой взгляд, эту задачу лучше всего выполняет перечисление C# (enum). Я буду использовать следующее перечисление, в котором указаны все задействованные мной в этом примере команды.


public enum MyCmd { None, FileOpen, FileSave,TextChange, LogAdd }


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

Дальше давайте приступим к созданию объекта, инкапсулирующего в себе вновь возникшее событие. Он будет довольно несложен.


public class TMyEvent {

public MyCmd cmd = MyCmd.None;

public object sender=null;

public object Parameter=null;

public TMyEvent(object sender,MyCmd cmd) {

this.sender=sender;

this.cmd = cmd;

}

public TMyEvent(object sender,MyCmd cmd,object Parameter):this(sender,cmd) {

this.Parameter=Parameter;

}

}


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

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

Также поле sender может вам пригодиться для адресной маршрутизации событий вместо используемой мной тут широковещательной маршрутизации... Хм. Что-то я размечтался и забежал далеко вперед.

Следующее поле, названной мной Parameter, это опциональный параметр, который возможно потребуется для выполнения этой конкретной команды. Я выбрал для него тип object потому, что используя полиморфизм, мы сможем подставить на место этого параметра любой произвольный объект. Так если для выполнения команды потребуется параметр - строка, именно ее мы и передадим. Если команде потребуется более сложная структура параметра, мы всегда сможем создать вспомогательный объект в который он и будет упакован.

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

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

Ну вот, мы с вами разобрались с командами и их упаковкой в событие. Теперь давайте подумаем над тем, как именно мы собираемся научить произвольные объекты NET Framework их обрабатывать.

Программисты, создававшие Turbo Vision, просто создали свою иерархию наследуемых объектов, в которые обработка событий была вложена изначально, на уровне базового предка. Для нас такой подход неприемлем, так как мы хотим научить работать с нашими событиями любые произвольные объекты Net Framework. Ну и что же нам делать?

В этой ситуации, к нам на выручку придет механизм интерфейсов С# (inteface). Мы просто создадим нужный интерфейс, а затем, при необходимости научить некий произвольный объект понимать наши события, реализуем его в коде его потомка. Вот и все!

Если вы ничего не поняли, не расстраивайтесь. Чуть позже вы разберетесь, в чем тут соль. Раз уж мы с вами решили использовать интерфейс, давайте, для начала, подумаем, как он будет выглядеть. В Turbo Vision, у каждого объекта иерархии имелся единственный метод обработчик событий. Назывался он HandleEvent. В предлагаемой мной упрощенной модели паттерна мы используем два метода. Один для обработки входящих событий(HandleEvent), а второй для информирования вышестоящих элементов о том, что внутри него возникло новое событие(OnMyCommand).

Чем-то предлагаемый мной подход похож на шоссе с двусторонним движением. Оба метода, с точки зрения числа и типа параметров выглядят одинаково. Так и должно быть, так как выполняют одинаковую функцию, ну прямо как две полосы движения на шоссе. По одной полосе машины едут туда, а по другой обратно.

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

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


public delegate void MyCommandHandler(TMyEvent MyEvent);

public interface IHandleEvent {

event MyCommandHandler OnMyCommand;

void HandleEvent(TMyEvent MyEvent);

}


Проанализировав предлагаемый код, вы обнаружите в нем объявление делегата MyCommandHandler. В нем мы описываем шаблон метода, предназначенного для обработки событий. В этом делегате описывается метод, не возвращающий какое либо значение(void) и имеющий всего один параметр. Этот параметр, и есть класс события, который "едет" туда или обратно, по нашему двустороннему "шоссе".

Интерфейс IHandleEvent описывает событие и метод обработчик событий, о которых я подробно написал выше. Поэтому я не стану на них останавливаться.

Ну вот, мы с вами и описали все общие логические структуры, необходимые для реализации базового TV паттерна. Оформите их в отдельный файл, назовите его, скажем, GLOBALS. CS и подключите его к своему проекту. Как вы его еще не создали? Так создавайте побыстрей. Сделайте новый проект "C# Windows Application" и подключите к нему файл с этими глобальными структурами.

Как правило, интерфейс Windows приложение состоит из нескольких стандартных кирпичиков. К таким кирпичикам относятся строка статуса, главное меню, тулбар и тому подобные вещи. Вот давайте мы такое приложение и реализуем. Мы с вами сделаем, скажем, несложный текстовый редактор. Дабы проще отслеживать прохождение событий мы, вместо строки статуса, добавим целый ListBox. В нем будут накапливаться те строчки, которые обычно быстро пробегают по статусной строке. Сверху редактора, у нас будет находиться тулбар с кнопками. Именно с его помощью мы и будем посылать управляющие команды. Ни тулбар, ни редактор, ни ListBox ничего не знают о существовании друг друга. Они просто, в момент своего создания, подключаются к нашему единому каналу маршрутизации событий и, в дальнейшем, управляются только проходящими по нему командами.

Реализацию нашего приложения мы начнем с тулбара. Создаем новый класс и указываем его предком System. Windows. Panel. Также говорим, что этот класс реализует у нас интерфейс IHandleEvent. После того как вы сообщите Visual Studio о своем решении, она любезно оформит для вас заготовки для всех методов этого интерфейса. А именно событие OnMyCommand и обработчик HandleEvent.

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


using System.Windows.Forms;

public class MyToolbar:Panel,IHandleEvent {

// вспомогательный класс кнопки

public class MyButton:Button {

public MyCmd cmd=MyCmd.None;

public MyButton(string text,MyCmd cmd) {

this.Text = text;

this.cmd = cmd;

}

}

// реализация интерфейса IHandleEvent

public event MyCommandHandler OnMyCommand;

public void HandleEvent(TMyEvent MyEvent){

if(OnMyCommand!=null) {

OnMyCommand(new TMyEvent(

this,

MyCmd.LogAdd,"MyToolbar process "+MyEvent.cmd.ToString()

));

}

}

// конструктор объекта

public MyToolbar() {

this.Controls.AddRange(

new MyButton[]{

new MyButton("Open",MyCmd.FileOpen),

new MyButton("Save",MyCmd.FileSave)

});

foreach(Control ctrl in this.Controls) {

if (ctrl is MyButton) {

(ctrl as MyButton).Click += new System.EventHandler(btn_Click);

(ctrl as MyButton).Width = 60;

(ctrl as MyButton).Dock = DockStyle.Left;

}

}

this.Height=24;

this.Dock = DockStyle.Top;

}

// обработчик нажатия кнопок на тулбаре

private void btn_Click(object sender,System.EventArgs e){

if(OnMyCommand!=null) {

OnMyCommand(new TMyEvent(

this,

MyCmd.LogAdd,"MyButton generate "+(sender as MyButton).cmd)

);

OnMyCommand(new TMyEvent(this,(sender as MyButton).cmd,null));

}

}

}


Итак, что же мы тут имеем. Про вспомогательный класс MyButton я вам уже рассказывал. Следом за ним в листинге идет реализация интерфейса IHandleEvent, а именно обработчик события OnMyCommand и метод HandleEvent. Событие OnMyCommand это наша дверь в канал исходящих событий. Если нашему тулбару потребуется проинформировать внешний мир, о каком либо происшествии внутри себя, например нажатии кнопки, он будет запускать информацию о нем во внешний мир с помощью этого объявленного публичного события.

Метод HandleEvent напротив, принимает события из внешнего мира. Так как я, в нашем приложении, не придумал какой-либо функциональности, требующей обработки нашим тулбаром, в этом методе у нас стоит заглушка. Сначала она проверяет, подключен ли канал исходящих событий(OnMyCommand не равен null), и если канал доступен, отправляет в него свою собственную, вновь созданную команду. В этой команде (MyCmd. LogAdd) он просит добавить в лог строчку текста с описанием той команды, которую его попросили выполнить. В реальном приложении, вы можете разместить тут обработку команд, отвечающих за разрешение-запрет кнопок тулбара в зависимости от происходящих в мире событий.

Следующим в нашем объекте тулбара идет его конструктор. В нем мы создаем две кнопки и добавляем их на поверхность тулбара. Затем в цикле foreach мы перебираем все элементы, находящиеся на поверхности нашего тулбара и таким образом находим только что созданные нами кнопки. Каждой из них мы добавляем обработчик ее нажатия (один на всех), указываем ширину и говорим, что они должны прижиматься к левому краю тулбара. Почему я делаю это так извращенно? А затем, чтобы избежать занудного создания, по крупному счету не нужных мне временных переменных. Посмотрите код, который генерирует мастер Visual Studio при создании элементов, и обратите насколько предлагаемо мной решение короче и изящнее.

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

Последний метод нашего объекта тулбара, это обработчик нажатия сразу всех кнопок. Если вы раньше обращали внимание на код нажатия кнопок, генерируемый мастером Visual Studio то знаете, что по принятым в нем правилам, каждой кнопке назначается свой собственный метод-обработчик. На мой взгляд, такой подход, во-первых, неудобен с точки зрения кодирования ответной реакции, а во-вторых, захламляет нашу программу большим количеством "лишних" функций. Обилие функций затрудняет восприятие программы, как самому программисту, так и тем, кто будет читать код вслед за ним.

Итак, вернемся к нашему обработчику нажатия кнопки. Так как мы точно знаем, что метод вызван кнопкой типа MyButton, я не стал проверять "национальность" объекта sender, а просто привел его к MyButton. Считав из поля cmd кнопки команду, с которой она ассоциирована, я по описанному выше механизму отправляю во внешний мир событие, информирующее заинтересованные стороны о факте нажатия на кнопку. Перед отправкой этого события, я отправляю еще одну, вновь сгенерированную команду с просьбой добавить в лог строчку о том, что событие сгенерировано именно этой кнопкой.

Давайте теперь остановимся и посмотрим, что именно мы сделали. А сделали мы очень интересную штуку. Наш тулбар является полнофункциональным объектом, который хоть сейчас можно кидать на форму. Например вот таким образом:


class Form1:Form {

...

public Form1() {

...

this.Controls.Add(new MyToolbar());

...

}

...

}


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

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

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

Продолжение следует
 

Ссылки:


При перепечатке сохранение раздела "Ссылки" обязательно!!!