Команды
9.2. Концепция команд
Система команд Mozilla - это система распространения команд. Реальные команды реализуются прикладным программистом. Создать и запустить команду гораздо проще, чем понять, как они вызываются, находятся и выполняются.
Система распространения очень не похожа на традиционную клиент- серверную архитектуру. В такой архитектуре серверы выполняют команды независимо и удаленно от клиентского GUI. Зачастую они выполняются без видимого эффекта. Появляется лишь сообщение об их выполнении. Например, HTTP запросы GET и POST игнорируют графическую оболочку клиентского браузера, они лишь возвращают код статуса, и, может быть, новый документ.
Команды Mozilla не похожи на клиент-серверные команды. Они тесно связаны с клиентской графической оболочкой. Примером может служить операция Bold в компоновщике Mozilla (да и в любом текстовом процессоре). Bold применяет операцию стиля на выделенном фрагменте, который обычно виден на дисплее. Система команд Mozilla должна позволить команде взаимодействовать с графической оболочкой, а не прятаться где-то на сервере. Конечно, если требуется, все клиент- серверные преимущества также могут быть реализованы.
Последний абзац означает, что команды не "удалены" от прикладного программиста. Они часть того кода, который он создает и подключает к XML-документу, как и любое иное содержание.
Mozilla использует специальный дизайн кода, называемый Command pattern, "образец команды", чтобы отделить имена команд от их реализации. Большинство современных пакетов разработки GUI включают технологии, подобные Command pattern. Ниже будет показано, как работают такие образцы.
9.2.1. Шаблон проектирования "функтор"
Функтор - хорошо известный пример шаблонов проектирования программ. Это самый нижний уровень системы команд. Функтор можно реализовать как объект. Этот объект отвечает за выполнение единственной функции. В листинге 9.3 сравниваются функтор и функция. Оба они реализуют команду "разделить пополам", вычисляющую половину от данного числа:
// Plain function function halve_function(num) { return num / 2.0; } // Functor var num = null; var halve_functor = { enabled : true, exec : function () { num /= 2.0; return true; } } // Examples of use num = halve_function(23); // sets num = 11.5 num = 23; halve_functor.exec(); // sets num = 11.5Листинг 9.3. Объект функтор и функция, выполняющие единственную операцию.
Объект functor не только кажется необоснованно сложным по сравнению с простой функцией, но он вдобавок требует лишней глобальной переменной для своей работы. Однако он гораздо более гибок, поскольку имеет стандартизованный интерфейс. Все functor -объекты имеют единственный метод exec(), выполняющий реализованную функтором команду и возвращающий лишь сообщение об ошибке или об успешном выполнении. Таким образом, все функторы для программиста выглядят одинаково.
Объект functor может хранить информацию о состоянии. В листинге 9.3 приведен пример очень распространенного параметра состояния enabled. На практике большинство систем, реализующих команды, включая Mozilla, в явном виде поддерживают состояние enabled. Прикладной код может опрашивать это состояние, чтобы узнать, доступна ли команда, и затем реагировать соответствующим образом. Объект functor может хранить любую информацию состояния о команде, какая представляется удобной, а не только состояние enabled. Другими состояниями могут быть имя команды, уникальный id, флаги, означающие, готова ли команда к выполнению, блокирована или оптимизирована; язык, на котором команда реализована, или ее версия и т.д.
В данном примере объект functor модифицирует глобальную переменную num при выполнении команды. Это позволяет методу exec() не иметь параметров, что делает все объекты functor в этом отношении одинаковыми. num может быть свойством объекта functor, но это неудачное решение. Это плохо, потому что состояние функтора не должно зависеть от его выполнения. Хотя он содержит информацию о состоянии (свойство enabled ), реализация команды не использует информацию о состоянии при выполнении. Информация о состоянии используется кодом, который вызывает функтор.
Функтор в приведенном примере может быть реализован и по-другому. Изменим метод exec() так:
exec: function () { return really_halve_it(); }
В этом варианте функция really_halve_it() выполняет всю реальную работу за команду и может быть реализована где-нибудь в другом месте, например, в библиотеке.
В последнем примере объекта functor подчеркивается, что он не содержит реализации команды. Это всего лишь точка доступа к команде и ее состоянию. Это proxy-объект, обработчик, фасад, или представительство реальной функциональности. Такой proxy-объект разделяет код и объект, что требуется системой команд Mozilla. Прикладной программист должен знать, где находится proxy-объект, но не обязан знать, где находится его реализация. Этот подход подобен конструкции дескрипторов файлов, символическим ссылкам, подмонтированным по сети файловым системам и алиасам.
В литературе о функторах, не связанной с Mozilla, метод exec() часто обозначают как execute(). В Mozilla proxy-объект (функтор) команды называют обработчиком команды. Обычно они не слишком часто используются, потому что удобнее применять концепцию более высокого уровня - контроллер команды.
9.2.2. Шаблон команды и контроллеры
"Команда" - хорошо известный шаблон проектирования, основывающийся на шаблоне "функтор". Он отвечает за разделение строения графической оболочки приложения и реализации команды. Благодаря функторам нам не нужно знать место конкретного расположения реализации команды. Благодаря шаблону команды не нужно даже знать, существует ли такая команда.
На практике в коде приложения мы просто предполагаем, что данная команда существует или будет реализована потом.
Чтобы реализовать шаблон команды, нужно создать объект, содержащий набор функторов. Когда пользователь передает этому объекту имя команды, следует вызвать соответствующий объект-функтор. Это и есть шаблон команды. В листинге 9.4 приведен пример такого объекта, называемого в Mozilla контроллером. Данный пример реализует простой светофор.
// functors var stop = { exec: function () { top.light = "Red"; }; var slow = { exec: function () { top.light = "Amber"; }; var go = { exec: function () { top.light = "Green"; }; // controller containing functors var controller = { _cmds: { stop:stop, slow:slow, go:go }, supportsCommand : function (cmd) { return (cmd in this._cmds); }, doCommand : function (cmd) { return _cmds[cmd].exec(); }, isCommandEnabled : function (cmd) { return true; }, onEvent : function (event_name) { return void; } }; // set the light to green controller.doCommand("go");Листинг 9.4. Объект Controller, реализующий светофор
Объект controller содержит объект _cmds, устанавливающий соответствие между именами свойств и функторами. В языках с полным сокрытием информации, наподобие C++, такой объект был бы назван внутренним. Три имеющиеся функции обрабатывают конкретное имя команды, полученное в виде строки. supportsCommand() сообщает, знает ли контроллер о команде; doCommand() выполняет функтор данной команды; onEvent() передает имя события, которое может быть для данной команды значимо.
Последний метод - isCommandEnabled() в данном случае является некоторым жульничеством. Он должен проверять свойство enabled у соответствующего функтора, но данный контроллер знает, что все три лампочки светофора всегда доступны, - поэтому просто возвращает true. Это и потому также так, что ни один из приведенных функторов в листинге 9.4 не реализует свойство enabled. Кажется, что они должны реализовать три полноценных функтора, но фактически они реализуют лишь то, что требуется контроллеру.
Этот последний пункт важен. Поскольку контроллер полностью скрывает реализацию команды (функторов) от пользователя, они могут быть реализованы любым способом. В Mozilla обычно в простых контроллерах вообще избегают использовать функторы, и команды и состояния реализуются явно. Это было бы невозможно, если бы контроллер имел метод getFunctor(cmd), но к счастью, в простейшей форме контроллера этого можно избежать. В листинге 9.5 показано, как можно упростить листинг 9.4.
var controller = { supportsCommand : function (cmd) { return (cmd=="red" || cmd=="amber" || cmd=="green"); }, doCommand : function (cmd) { if ( cmd == "red" ) top.light = "Red"; if ( cmd == "amber" ) top.light = "Amber"; if ( cmd == "green" ) top.light = "Green"; }, isCommandEnabled : function (cmd) { return true; }, onEvent : function (event_name) { return void; } }; // set the light to green controller.doCommand("go");Листинг 9.5. Объект controller, не имеющий отдельных функторов
В данном примере контроллер действует, как если бы все три функтора имелись в наличии, но ни один из них в действительности не реализуется.
Сложные контроллеры также могут многое выиграть от явной реализации команды. Иногда возникают проблемы с синхронизацией, которые лучше всего решить в контроллере. Пример - реализация команды Undo в редакторах, подобных Classic Composer. Контроллер отвечает за реализацию конкретной команды, так что его логично использовать и для сохранения истории команд. Если нужно выполнить Redo или Undo, контроллер может прочитать верхний элемент истории команд и вызвать соответствующую команду, реализующую этот запрос. Все подобные механизмы с историей состояний можно сначала реализовать вне контроллера, но затем спрятать в контроллере так, чтобы сохранить главную цель существования контроллера - управление ( controlling ). Контроллеры также удобны для размещения в них макро-языков, механизмов синхронизации, и всего, что связано с существованием команд.
9.2.3. Место расположения контроллера
Mozilla не ограничивается единственным статическим контроллером. Можно создать столько контроллеров, сколько вам требуется. Так сделано, например, в компоновщике Mozilla, где имеются три контроллера, каждый из которых реализует свой набор команд.
Все контроллеры должны быть помещены туда, где платформа сможет их найти. Их можно поместить в несколько разных мест.
Контроллеры можно поместить в объект window.
Несколько XUL-тегов являются подходящими местами. Можно использовать <button>, <checkbox>, <radio>, <toolbarbutton>, <menu>, <menuitem>, и <key>.
XUL-тег <command> подобен функтору, когда он явно реализует команду, используя обработчик события. Если такой тег создан, платформа добавит функтор к постоянному внутреннему контроллеру.
Использование каждого из этих мест будет рассматриваться позднее, при обсуждении синтаксиса команд.
Если в каком-то месте имеется более одного контроллера, то набор контроллеров называется цепочкой контроллеров. Такое множество контроллеров упорядоченно, и при необходимости они просматриваются по очереди. Это значит, что первый контроллер в цепочке, который может выполнить команду, и будет тем контроллером, который ее выполнит.
9.2.4. Диспетчеры и поиск и выполнение команды
Каждый контроллер содержит набор команд; в одном месте может быть ноль или более контроллеров; диспетчер контроллеров работает с набором таких мест. К счастью, это все. Задачей диспетчера контроллеров является поиск и выполнение конкретной команды.
Mozilla имеет один диспетчер для HTML-документов и один - для документов XUL. Диспетчер HTML невидим и недоступен скриптам ни при каких обстоятельствах. С диспетчером команд XUL можно работать. Каждое окно XUL-приложения имеет объект dispatcher в корне дерева документов, и этот объект всегда присутствует и доступен.
Диспетчер имеет метод getControllerForCommand(). Данный метод получает имя команды в виде строки и возвращает контроллер, способный выполнить эту команду. Диспетчером и возвращаемым контроллером нужно явным образом управлять из скрипта - они ничего не делают автоматически. В файлах chrome классической Mozilla вызов диспетчера спрятан в функции JavaScript, называемой goDoCommand(). Эта функция доступна в каждом окне классической Mozilla и применяется чаще всего.
Диспетчер использует информацию о состоянии текущего фокуса, чтобы решить, какой контроллер нужен для данной команды. Эта информация о состоянии включает состояние фокуса окна, все фокусы тегов <iframe>, и любой элемент DOM в фокусном кольце, если он имеет в настоящее время фокус. Другими словами, диспетчеру нужна информация об иерархии фокуса. Диспетчер начинает с наиболее глубокого, специфичного положения фокуса (обычно это элемент формы или меню), и переходит по дереву DOM вверх вплоть до верхнего окна. На каждом элементе DOM, способном получить фокус, он исследует цепочку контроллеров в поисках объектов-контроллеров. Далее он исследует поддержку нужной команды, запрашивая поддерживаемый контроллером метод Command(). Первый же объект controller, реализующий выполнение нужной команды, возвращается. Если на данном уровне результата не находится, диспетчер переходит дальше вверх по фокусной иерархии. Если он доходит до верха, не найдя результата, он возвращает значение null.
Окно XUL и его контент не полностью инициализируют фокус при создании окна. Даже если окно является текущим окном экрана, оно может не иметь фокуса. Это значит, что имеющиеся контроллеры не всегда автоматически доступны, даже если они находятся в вершине текущего окна. Программист должен явно дать окну фокус, если контроллер этого окна должен быть доступен. Простейший способ сделать это - одна строка скрипта:
window.focus();
Важно, чтобы фокус был установлен и до, и после выполнения команды. Если этого не сделать, кольцо фокуса может быть разрушено, и рано или поздно могут возникнуть проблемы. Это ограничение было исправлено в версии платформы 1.4, но явное указание фокуса все еще рекомендуется. Диспетчер не просматривает все подряд в поисках реализации команды.
Если виджет, имеющий фокус, имеет атрибут command, диспетчер не принимает его во внимание. Он исследует только контроллеры.
9.2.5. Извещение об изменении
Одно важное свойство системы команд - это система извещений команд об изменениях. Она называется также системой обновления команд.
Команды можно создавать и управлять ими, их можно искать и выполнять, но что будет, если команда изменится? Предположим, состояние команды enabled изменит значение с true на false. Как та часть приложения, которая использует команду, узнает, что команда более недоступна? Извещение об изменении решает эту проблему.
В "События" , "События", мы рассматривали шаблон проектирования "источник события - наблюдатель". Mozilla использует этот же шаблон для отслеживания изменения состояния команд. Вместо того чтобы один или более тегов XUL наблюдали за атрибутами другого тега XUL, они наблюдают за proxy-функцией конкретной команды. Если состояние команды меняется, диспетчер рассылает извещение об изменении его функторов, а наблюдающие теги XUL получают эти извещения. Даже если функторы помещены внутрь кода контроллера для простоты, извещения об изменении состояния все равно генерируются. Таким образом, удобно считать, что функторы всегда имеют место, поскольку это объясняет появление сообщений.
Диспетчер пользуется списком наблюдаемых тегов XUL в форме объектов DOM. Это обычные объекты DOM, но они называются объектами, обновляющими команды ( command updaters ), обновителями команд, когда используются в этом качестве. Диспетчер может добавлять и удалять эти объекты из списка.
Событие DOM генерируется и передается системе обработки событий с помощью метода dispatchEvent(). Извещение об изменении команды передается системе "источник события-наблюдатель" с помощью метода UpdateCommands() диспетчера или окна. Все наблюдатели, ожидающие некоторого события, получат его. В результате такое событие действует точно так же, как и любое событие, которое может получить XUL-тег.
События обновления команд работают в XUL, но не в HTML. Далее мы коротко рассмотрим синтаксис и способы использования таких событий.