Этюды в тональности C#. TV паттерн программирования 3

Этюды в тональности C#. TV паттерн программирования

Окончание. Начало в КГ №№ 1, 2 (2004)

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

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

Редактор
Следующим шагом мы с вами приступим к реализации класса текстового редактора. Откройте ваш проект в Visual Studio и создайте новый файл класса. Назовем мы этот класс MyEdit. Наследовать этот класс мы будем от готового компонента Net Framework, называющегося RichTextBox. Функциональность нашего редактора будет довольно скромной, тем не менее, на его примере я постараюсь вам продемонстрировать общий подход к созданию компонентов, управляемых командами.
Точно так же, как и тулбар, редактор будет реализовывать интерфейс IHandleEvent, то есть уметь принимать и отправлять команды. Для того чтобы реализовать этот интерфейс, нам необходимо иметь в коде будущего класса событие (event), называющееся OnMyCommand. Вторым необходимым условием реализации интерфейса IHandleEvent является существование в коде нашего класса метода, называющегося HandleEvent. Этот метод обязан принимать в качестве параметра объект типа TMyEvent. Напомню: обязательность наличия этих элементов кода задается в описании интерфейса. Это описание мы определили в глобальном модуле, подключенном к нашему приложению. Если вы хотите изменить интерфейс, то предварительно должны исправить это его объявление.
using System; using System.Windows.Forms; public class MyEdit: Rich TextBox,IHandleEvent { pub
lic event MyCommand Handler OnMyCommand; public void HandleEvent (TMyEvent MyEvent) { if(OnMyCommand
==null) return; if(MyEvent==null) return; OnMyCommand(new TMy Event(this, MyCmd.LogAdd, "MyEdit
 process "+MyEvent. cmd.ToString())); this.Text+="Append "+My Event.cmd.ToString()+&q
uot;\n"; } } public MyEdit() { this.Dock = DockStyle. Fill; } protected override void OnTextCha
nged(EventArgs e){ base.OnTextChanged (e); OnMyCommand(new TMy Event(this,MyCmd.LogAdd,"My Edit
 generate text change ")); OnMyCommand(new TMy Event(this,MyCmd.TextChange)); } }

Для того чтобы продемонстрировать вам процесс генерации новой команды компонентом, я перехватил обработчик события OnTextChanged. Это событие объявлено в классе RichTextBox, играющем роль предка для нашего редактора. Возникает это событие в тот момент, когда пользователь изменяет текст в редакторе. Мы в ответ на это событие от редактора сгенерируем две управляющие команды.
Первая команда заносит в лог сам факт генерации команды редактором. Нам эта команда необходима для того, чтобы факт изменения текста не остался нами незамеченным. Мы всегда сможем с помощью лога убедиться в том, что команда действительно была сгенерирована редактором.
Вторая команда, идущая следом, посылает остальным блокам нашего приложения сообщение о том, что пользователь изменил текст. Остальные компоненты могут отреагировать на наше сообщение так, как им заблагорассудится. Или могут вовсе его проигнорировать, если обработка этого события им не нужна. Нашему компоненту редактора до этого нет никакого дела. Он выпустил команду в свет, все остальное ему совершенно безразлично.
В реальном приложении вы могли бы использовать эту команду, например, для того, чтобы в строке статуса вывести общее количество символов в редактируемом тексте или разрешить пользователю нажатие кнопки сохранения файла.
В методе HandleEvent нашего редактора мы на этот раз оформим также механизм обработки внешних команд. Получив команду от других блоков приложения, наш редактор первым делом проверяет, установлен ли его собственный ответный канал выдачи команд OnMyCommand. Затем убеждается в том, что передаваемое событие не было уже кем-то выполнено ранее (Event не равно null). Если какое-либо из этих условий не выполняется, обработка события прерывается.
В том случае, если оба условия выполнились, метод генерирует новую команду, добавляющую в лог строчку о том, что команда прошла через код его обработчика. Затем добавляет в текст редактора фразу об успешном приеме им этого события. Я добавил последнее действие как пример модификации содержимого редактора в зависимости от внешних команд.

Главная форма приложения
Ну вот мы с вами и рассмотрели все функциональные элементы нашего будущего приложения. Строго говоря, мы могли бы оформить как отдельные классы, реализующие интерфейс IHandleEvent, и список ListBox, и даже саму главную форму приложения. Но, на мой взгляд, никогда не следует излишне злоупотреблять той или иной технологией. Множество программистов попадаются в известную ловушку ООП. Начиная писать в объектно ориентированном стиле, они уже не могут остановиться и сводят к этой идеологии все поставленные перед ними задачи.
С одной стороны, это хорошо — улучшается структурированность кода. Но с другой стороны программист из-за этого впадает в этакое научное самозаглубление, хорошо описанное Станиславом Лемом в его романе "Осмотр на месте". Программист больше не решает задачу, поставленную перед ним насущным алгоритмом. Сконцентрировавшись на каком-то частном ее аспекте, он уже сам придумывает себе проблемы и сам с ними успешно или нет борется. Избежать подобного самозаглубления довольно-таки нетрудно. Необходимо просто остановиться и задать самому себе вопрос: "А это вообще кому-нибудь, кроме меня самого, нужно?"
Я и сам неоднократно попадал в эту ловушку. После бессонной ночи, потраченной, скажем, на программирование горячих клавиш к кнопкам на тулбаре, приходишь к заказчику и видишь, что он принципиально не пользуется клавиатурой, предпочитая ей мышь. Я научился сам себя одергивать в такой ситуации и не ударяться в излишнее самокопание в коде — надеюсь, это получится и у вас.
Поэтому, потратив довольно много времени на создание компонентов вроде редактора и тулбара, я проскочил "галопом по Европам" через создание класса кнопки на тулбаре. Признаюсь, меня преследовал соблазн реализовать и в этом классе интерфейс IHandle-Event, но в нашем случае это только затруднит вам восприятие идеологии TV-паттерна. Таким образом, преследуя вроде бы благую цель чистого ООП-подхода, я упустил бы самую главную свою задачу, а именно — просто и доступно рассказать вам об этой программной модели. Поэтому я взял и остановился. На мой взгляд, вовремя.
По этой же самой причине я не стал реализовывать интерфейс IHandleEvent и для компонента ListBox, играющего в нашем приложении роль лога. Ничего нового в его коде я бы вам показать не смог, поэтому реализация интерфейса в этом случае была бы банальным повторением уже изученного материала. Если вы хотите потренироваться, попробуйте реализовать этот интерфейс самостоятельно. Я также не стал реализовывать механизмы загрузки и сохранения в файл текста редактора, ограничившись лишь существованием вызывающих их команд. Поэтому соответствующие кнопки у нас есть, а самого кода нет.
Сейчас же давайте лучше разберемся с кодом главной формы приложения. Именно главная форма объединяет в единое целое все созданные нами компоненты. Изучите прилагаемый код, а ниже я вам подробно поясню назначение каждой его строчки.
using System; using System.Windows. Forms; public class TVForm :Form { MyToolbar tbar = new My
 Toolbar(); MyEdit edit = new My Edit(); ListBox log = new List Box(); private void Handle Event(TMy
Event MyEvent) { log.Items.Add("Main Form Proccess "+MyEvent.cmd. ToString()); if(MyEvent.
cmd == My Cmd.LogAdd) { log.Items.Add(My Event.Parameter as string); MyEvent=null; } foreach(Control
 ctrl in this.Controls) { if(!(ctrl is IHandleEvent))continue; if(MyEvent.sender== ctrl)continue; if
(MyEvent==null) break; (ctrl as IHandle Event).HandleEvent(MyEvent); } MyEvent=null; } public TVForm
() { this.tbar.OnMyCommand+ =new MyCommandHandler(Handle Event); this.edit.OnMyCommand+ =new MyComma
ndHandler (HandleEvent); this.log.Height = 100; this.log.Dock = Dock Style.Bottom; this.Controls.Add
(edit); this.Controls.Add(tbar); this.Controls.Add(log); } static void Main() { Application.Run(new 
TVForm()); } }

Строчки using предназначены для подключения пространства имен. Дальше мы начинаем описывать класс главной формы приложения, названный мной TVForm.
В этом классе мы с вами заводим три переменных, называющихся tbar, edit и log. Тут же, не отходя далеко от кассы, заполним эти переменные тремя вновь созданными объектами. Первый из них — это экземпляр нашего класса MyToolbar, второй — наш редактор MyEdit. Последним объектом будет экземпляр стандартного для WinForms класса ListBox, играющий в нашем приложении роль лог-файла.
Следом за объявлением переменных в коде идет глобальный обработчик команд HandleEvent. Именно он обрабатывает, или, по меньшей мере, пропускает через себя все команды, возникающие в нашем приложении. Подключение этого обработчика к методам OnMyCommand разных объектов мы рассмотрим чуть позже, когда будем изучать конструктор формы. Сейчас же давайте посмотрим, какая реакция на внешние команды в нем содержится.
При попадании в этот обработчик любой внешней команды метод тут же заносит информацию о нем в ListBox, таким образом ведя учет входящей информации. Следующим шагом у нас следует проверка, а не является ли обрабатываемая команда как раз самой командой занесения строчки текста в лог. Если это так, то обработчик добавляет параметр команды MyEvent.Parameter в список строк лога. Распознав команду по ее коду, обработчик уже точно знает, что данный параметр является строкой. Даже если это будет не так, ничего страшного не произойдет. Все объекты NetFramework имеют в своих предках Object, а он в свою очередь имеет метод, приводящий его содержимое к текстовой строке. Поэтому даже если параметром будет null, ошибки не произойдет, а в лог просто будет добавлена пустая строка. Выведя строку в лог, обработчик считает, что эта команда обработана, и сбрасывает ее, приведя значение Event к null.

После обработки команды MyCmd.LogAdd в коде располагается цикл foreach. В этом цикле перебираются все элементы формы (контролы), которые код извлекает из коллекции Controls главной формы. О таком методе обработки визуальных элементов формы я вам уже рассказывал, когда мы с вами говорили о добавлении кнопок на панель тулбара. Собственно говоря, использовать этот механизм в данном приложении не обязательно. У нас и так имеются ссылки на все три наших объекта, реализующих интерфейс IHandleEvent. Тем не менее, данный механизм будет очень полезен, если вы вдруг решите, скажем, реализовать интерфейс и у элемента ListBox. В данном случае вам не придется что-либо переписывать в коде нашего обработчика команд, он "подхватит" ListBox автоматически, как только обнаружит, что этот класс реализует IHandleEvent. Ну как, ощутили еще одну пользу от использования ООП вообще и интерфейсов в частности?
Но вернемся к коду. С помощью конструкции is пропускаются те элементы формы, которые не поддерживают интерфейс IHandleEvent. Кроме них, пропускается также и сам элемент — инициатор события. Если бы мы так не сделали, то получили бы закольцовку. Команды генерировались бы вновь и вновь, пока это безобразие не завершила бы сама cреда Net Framework, выведя соответствующее сообщение о переполнении стека вызова.
Следующая команда в цикле довольно любопытна. В ней проверяется, а не стало ли равно null событие, которое мы обрабатываем. Если это произошло, то цикл передачи сообщения обрывается, и мы из него выходим. За счет этой нехитрой команды любой контрол внутри своего обработчика может прервать прохождение команды по приложению. Такая необходимость может возникнуть в том случае, если контрол знает, что данная команда направлена только ему. Пример такой адресной маршрутизации событий мы только что изучали, когда говорили об обработке главной формой команды MyCmd.LogAdd. Главная форма точно знает, что остальным контролам на форме нет никакого дела до вывода информации в лог, так зачем им вообще передавать эту команду? Отработав "свою" команду, контрол может запретить другим элементам после себя как-либо на нее реагировать — зачем зря тратить процессорное время?

Из неочевидных возможностей, предоставляемых этим алгоритмом, отмечу еще и то, что любой контрол теоретически может внутри своего обработчика и вовсе переопределить команду. Всего-то делов — исправить код команды и параметр! Тем не менее, я не рекомендую вам увлекаться этой возможностью, так как нам заранее неизвестно, каким по счету в цикле foreach окажется наш самонадеянный контрол. Многие контролы на форме могут успеть получить старый вариант команды и, следовательно, вообще никогда не получат новый. В оригинальной Turbo Vision этот нюанс обходился с помощью довольно сложного механизма флагов PreProcess и PostProcess. Если вы не реализуете нечто подобное в своем коде, я вам не рекомендую чрезмерно увлекаться модификацией команд в теле обработчика. Если у вас возникла подобная необходимость, лучше и вовсе сбросить команду, а взамен нее сгенерировать новую — такую, какая вам необходима. В этом случае вы можете быть уверены, что ваша новая команда достигнет своего адресата.
Ну и, в конце концов, после всех проверок в нашем коде вызывается метод HandleEvent найденного элемента формы — пускай он отработает событие, если умеет это делать. Так как все контролы, не реализующие интерфейс IHandleEvent, уже отфильтровались предыдущими фрагментами кода, мы точно уверены, что этот метод в нем есть. Для того, чтобы его вызвать, нам лишь необходимо привести найденный контрол к типу реализуемого им интерфейса IHandleEvent, что мы и делаем с помощью кодового слова as.
После того как событие было отработано, команда сбрасывается в null. Это необходимо для того, чтобы сборщик мусора мог очистить память, занимаемую объектом, инкапсулирующим команду. В принципе это не обязательно, так как он все равно это сделает тогда, когда переменная выйдет из поля видимости метода, но так будет надежнее.

Дальше в коде формы располагается конструктор класса. В нем мы первым делом подключаем к нашему глобальному обработчику событий Handle-Event события OnMyCommand компонентов, реализующих интерфейс IHandleEvent. Затем настраиваем окно лога, говоря, что оно должно прижиматься к низу формы и быть высотой в 100 пикселей.
Последующие три команды this.Controls.Add() поочередно добавляют на форму наши визуальные компоненты редактора, тулбара и лога. Обратите внимание на очередность добавления элементов на форму. Первым по счету всегда следует добавлять тот контрол, который впоследствии растянется по поверхности всей формы — то есть тот, чье свойство Dock равняется DockStyle.Fill. В нашем примере таким контролом является редактор. Затем уже добавляются все остальные элементы. Если вы нарушите порядок, результат будет совершенно непредсказуемым. Вероятнее всего, элемент с установленным DockStyle.Fill просто накроет собой, как одеялом, все остальные контролы на форме.
Последний метод, оставшийся у нас нерассмотренным, называется Main. Ничего примечательного в нем нет. Он просто создает экземпляр класса нашей формы TVForm и запускает приложение на выполнение. Как только форма по той или иной причине закрывается, приложение тут же и завершает свою работу.

Запускаем программу
Вот мы с вами и закончили создание своего первого приложения в стиле паттерна TV. Компилируете приложение и запускаете его на выполнение. На экране перед вами должно появиться окошко с тулбаром, на котором расположены две кнопки, текстовый редактор и список лога внизу. Если вы попытаетесь набить текст в поле редактора, на каждый вводимый вами символ в списке лога будет добавляться строчка, индицирующая это событие. Если вы нажмете какую-либо кнопку на тулбаре, в окне редактора появится соответствующее сообщение, и оно же будет продублировано, опять-таки, в окошке лога. При всем при этом ни редактор, ни тулбар, ни лог ничего не знают о существовании друг друга. И их незнание никак не мешает им успешно взаимодействовать, совместно выполняя одну задачу.
Ну вот и все. Что дальше?

А дальше можно придумать массу интересных вещей, расширяющих предлагаемую мной модель. Первое, что сразу приходит в голову — создать класс-менеджер команд. В его задачи будет входить маршрутизация команд между контролами, расположенными на произвольной форме. Некоторые зачатки этого менеджера я описал, когда перебирал все контролы на форме, отфильтровывая только те, что поддерживают нужный нам интерфейс. Хорошо было бы как-то унифицировать этот процесс, выделив его в отдельный класс.
Также неплохо бы организовать механизм, позволяющий передавать не только широковещательные команды, но и адресные. К примеру, контрол отправляет команду не всем элементам на форме, а только тем, которые реализуют вспомогательный интерфейс класса, способного записывать лог-файлы. При такой системе быстродействие этого механизма существенно бы выросло.
Дерзайте: информации по Turbo Vision в Сети довольно много. Фирмой "Борланд" в свое время был выпущен большой учебник для начинающих пользователей этой библиотеки. Электронную версию этого учебника вы также без труда найдете в Интернет. Предлагаемая Turbo Vision программная модель, на мой взгляд, забыта совершенно незаслуженно. Кто знает, может, кто-нибудь из вас, моих читателей, поднимет выпавшее из рук "Борланд" знамя и напишет ее реализацию для современных языков программирования, создав таким образом кроссплатформенную модель визуального интерфейса пользователя.

Ну, или если вам лень создавать всю библиотеку, так хоть напишете некоторую ее часть для себя самого. Как говорил один из героев фильма "Люди в черном", оно того стоит.

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



Компьютерная газета. Статья была опубликована в номере 04 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета